Merge branch 'stateful-components'

This commit is contained in:
Andreas Rumpf 2017-04-19 11:26:08 +02:00
commit 2c0dad5094
7 changed files with 200 additions and 139 deletions

View file

@ -3,7 +3,6 @@
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
@ -11,9 +10,6 @@
<div id="ROOT" />
<script type="text/javascript" src="nimcache/example.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>
</body>
</html>

View file

@ -1,135 +1,32 @@
import vdom, karax, karaxdsl, jdict, jstrutils
type
Filter = enum
all, active, completed
import vdom, components, karax, karaxdsl, jdict, jstrutils
var
entries: seq[(cstring, bool)]
selectedEntry = -1
filter: Filter
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
proc onTodoEnter(ev: Event; n: VNode) =
entries.add((n.value, false))
n.value = ""
proc carousel*(key: VKey): VNode {.component.} =
state:
var counter: int = 0
proc removeHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "remove:")
entries[id] = (cstring(nil), false)
proc next(ev: Event; n: VNode) =
counter = (counter + 1) mod images.len
proc editHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "edit:")
selectedEntry = id
when defined(usecache):
var entryCache = newJDict[int, VNode]()
proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
proc editEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "todo-edit:")
entries[id][0] = n.value
selectedEntry = -1
proc toggleEntry(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "toggle:")
entries[id][1] = not entries[id][1]
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring"")
proc toVis(visible: bool): cstring =
(if visible: cstring"visible" else: "hidden")
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil))
proc createEntry(id: int; d: cstring; completed, selected: bool): VNode =
# implement caching:
when defined(usecache):
if entryCache.contains(i):
let old = entryCache[i]
return old
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
else:
input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
when defined(usecache):
entryCache[i] = result
result = buildHtml(tdiv(key=key)):
text images[counter]
button(onclick = next):
text "Next"
proc createDom(): VNode =
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
header(class = "header"):
h1:
text "todos"
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onenter = onTodoEnter)
section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete"
var entriesCount = 0
var completedCount = 0
ul(class = "todo-list"):
for i, d in pairs(entries):
if d[0] != nil:
let b = case filter
of all: true
of active: not d[1]
of completed: d[1]
if b:
createEntry(i, d[0], d[1], i == selectedEntry)
inc completedCount, ord(d[1])
inc entriesCount
footer(class = "footer"):
span(class = "todo-count"):
strong:
text(&entriesCount)
text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
ul(class = "filters"):
li:
anchor(class = selected(all), href = "#/"):
text "All"
li:
anchor(class = selected(active), href = "#/active"):
text "Active"
li:
anchor(class = selected(completed), href = "#/completed"):
text "Completed"
button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")"
result = buildHtml(table):
tr:
td:
carousel(0)
td:
carousel(1)
tr:
td:
carousel(2)
td:
carousel(3)
setOnHashChange(proc(hash: cstring) =
if hash == cstring"#/": filter = all
elif hash == cstring"#/completed": filter = completed
elif hash == cstring"#/active": filter = active
)
setRenderer createDom
proc onload(session: cstring) {.exportc.} =
for i in 0..1: # 0_000:
entries.add((cstring"Entry " & &i, false))
init()

1
experiments/example.nims Normal file
View file

@ -0,0 +1 @@
--path: "../src"

View file

@ -201,3 +201,51 @@ proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) =
else:
for i in 0..<e.len:
visibleKeys(e[i], a, b, h, count)
template toState(x): untyped = "state" & x
proc accessState(sv: string): NimNode {.compileTime.} =
newTree(nnkBracketExpr, newIdentNode(sv), newIdentNode("key"))
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl, init: NimNode) =
case n.kind
of nnkVarSection, nnkLetSection:
for c in n:
expectKind c, nnkIdentDefs
let typ = c[^2]
let val = c[^1]
let usedType = if typ.kind != nnkEmpty: typ else: val
if usedType.kind == nnkEmpty:
error("cannot determine the variable's type", c)
for i in 0 .. c.len-3:
let v = $c[i]
let sv = toState v
decl.add quote do:
var `sv` = newJDict[VKey, `usedType`]()
if val.kind != nnkEmpty:
init.add newTree(nnkAsgn, accessState(sv), val)
names[v] = true
of nnkStmtList, nnkStmtListExpr:
for x in n: stateDecl(x, names, decl, init)
of nnkDo:
stateDecl(n.body, names, decl, init)
of nnkCommentStmt: discard
else:
error("invalid 'state' declaration", n)
proc doState(n: NimNode; names: TableRef[string, bool];
decl, init: NimNode): NimNode =
result = n
case n.kind
of nnkCallKinds:
# handle 'state' declaration and remove it from the AST:
if n.len == 2 and repr(n[0]) == "state":
stateDecl(n[1], names, decl, init)
result = newTree(nnkEmpty)
of nnkSym, nnkIdent:
let v = $n
if v in names:
let sv = toState v
result = accessState(sv)
else:
for i in 0..<n.len:
result[i] = doState(n[i], names, decl, init)

View file

@ -2,6 +2,37 @@
import macros, jdict, dom, vdom, tables, strutils
type
StateDict*[V] = ref object
defaultValue*: V
proc get[V](d: StateDict[V], k: VKey): V {.importcpp: "#[#]".}
proc put[V](d: StateDict[V], k: VKey, v: V) {.importcpp: "#[#] = #".}
proc contains*[V](d: StateDict[V], k: VKey): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[V](d: StateDict[V], k: VKey) {.importcpp: "delete #[#]".}
proc newStateDict*[V](): StateDict[V] {.importcpp: "{@}".}
var
dirty = newStateDict[bool]()
someDirty*: bool
proc markDirty*(key: VKey) =
dirty.put(key, true)
someDirty = true
proc unmarkDirty*(key: VKey) = dirty.del key
proc isDirty*(key: VKey): bool = dirty.contains(key)
proc `[]`*[V](d: StateDict[V], k: VKey): V =
if d.contains(k): result = d.get(k)
else: result = d.defaultValue
proc `[]=`*[V](d: StateDict[V], k: VKey, v: V) =
d.put(k, v)
markDirty(k)
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]()
@ -30,6 +61,63 @@ proc addTags() {.compileTime.} =
static:
addTags()
template toState(x): untyped = newIdentNode("state" & x)
proc accessState(sv: NimNode): NimNode {.compileTime.} =
newTree(nnkBracketExpr, sv, newIdentNode("key"))
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl: NimNode) =
case n.kind
of nnkVarSection, nnkLetSection:
for c in n:
expectKind c, nnkIdentDefs
let typ = c[^2]
let val = c[^1]
let usedType = if typ.kind != nnkEmpty: typ else: newCall("type", val)
if usedType.kind == nnkEmpty:
error("cannot determine the variable's type", c)
for i in 0 .. c.len-3:
let v = $c[i]
let sv = toState v
decl.add quote do:
var `sv` = newStateDict[`usedType`]()
if val.kind != nnkEmpty:
decl.add newTree(nnkAsgn, newDotExpr(sv, newIdentNode"defaultValue"),
val)
else:
error("component state must have a primitive initializer")
names[v] = true
of nnkStmtList, nnkStmtListExpr:
for x in n: stateDecl(x, names, decl)
of nnkDo:
stateDecl(n.body, names, decl)
of nnkCommentStmt: discard
else:
error("invalid 'state' declaration", n)
proc doState(n: NimNode; names: TableRef[string, bool];
decl: NimNode): NimNode =
case n.kind
of nnkCallKinds:
# handle 'state' declaration and remove it from the AST:
if n.len == 2 and repr(n[0]) == "state":
stateDecl(n[1], names, decl)
return nil #newTree(nnkEmpty)
of nnkSym, nnkIdent:
let v = $n
if v in names:
let sv = toState v
return accessState(sv)
else: discard
result = copyNimNode(n)
for i in 0..<n.len:
let x = doState(n[i], names, decl)
if x != nil: result.add x
proc compBody(body, decl: NimNode): NimNode =
var names = newTable[string, bool]()
result = doState(body, names, decl)
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
@ -67,7 +155,7 @@ macro component*(prc: untyped): untyped =
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc")
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
@ -77,7 +165,7 @@ macro component*(prc: untyped): untyped =
elif rettype == "Node":
isvirtual = ComponentKind.Node
else:
error "component must return VNode or Node"
error "component must return VNode or Node", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
@ -91,6 +179,9 @@ macro component*(prc: untyped): untyped =
unpackCall.add unpack(typ, counter)
inc counter
let decl = newTree(nnkStmtList)
let newBody = compBody(n, decl)
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
@ -107,7 +198,8 @@ macro component*(prc: untyped): untyped =
bind jdict.`[]=`
`[]=`(dcomponents, cstring(key), val)
result = newTree(nnkStmtList, n)
result = newTree(nnkStmtList, decl, newBody)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
@ -119,8 +211,15 @@ macro component*(prc: untyped): untyped =
echo repr result
when isMainModule:
proc public*(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard
proc private(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard
proc public*(key: VKey; x, y: int, b: bool; s: cstring): VNode {.component.} =
state:
var foo = 89
proc callback() =
foo = 78
let cc = callback
when false:
proc private(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard

View file

@ -75,10 +75,12 @@ proc vnodeToDom(n: VNode): Node =
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = vnodeToDom(x)
n.key = result.key
attach n
return result
elif n.kind == VNodeKind.dthunk:
result = callThunk(dcomponents[n.text], n)
n.key = result.key
attach n
return result
else:
@ -145,7 +147,22 @@ proc equalsTree(a, b: VNode): bool =
b.calcHash()
return a.hash == b.hash
else:
return eq(a, b)
result = eq(a, b)
proc updateDirtyElements(parent, current: Node, newNode: VNode) =
if newNode.key >= 0 and isDirty(newNode.key):
unmarkDirty(newNode.key)
let n = vnodeToDom(newNode)
if parent == nil:
replaceById("ROOT", n)
else:
parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and
newNode.kind != VNodeKind.dthunk:
for i in 0..newNode.len-1:
updateDirtyElements(current, current[i], newNode[i])
# leave early if we know there cannot be anything left to do:
#if dirtyCount <= 0: return
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
if not equalsShallow(newNode, oldNode):
@ -217,6 +234,9 @@ proc dodraw() =
let olddom = document.getElementById("ROOT")
updateElement(nil, olddom, newtree, currentTree)
#assert same(newtree, document.getElementById("ROOT"))
if someDirty:
updateDirtyElements(nil, olddom, newtree)
someDirty = false
currentTree = newtree
# now that it's part of the DOM, give it the focus:
if toFocus != nil:

View file

@ -124,10 +124,10 @@ template callThunk*(fn: typed; n: VNode): untyped =
fn(n.kids)
proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, kids: @args)
VNode(kind: VNodeKind.vthunk, text: name, key: -1, kids: @args)
proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.dthunk, text: name, kids: @args)
VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args)
proc eq*(a, b: VNode): bool =
if a.kind != b.kind: return false