wip: stateful components

This commit is contained in:
Araq 2017-04-16 09:51:10 +02:00
commit 0255f58964
5 changed files with 99 additions and 130 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,33 @@
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 = 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
markDirty key
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

@ -30,6 +30,53 @@ proc addTags() {.compileTime.} =
static:
addTags()
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(c, "cannot determine the variable's type")
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(n, "invalid 'state' declaration")
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)
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)

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:
@ -114,6 +116,8 @@ proc same(n: VNode, e: Node): bool =
var
dorender: proc (): VNode {.closure.}
currentTree: VNode
dirty = newJDict[cstring, bool]()
dirtyCount: int
proc replaceById(id: cstring; newTree: Node) =
let x = document.getElementById(id)
@ -145,7 +149,27 @@ proc equalsTree(a, b: VNode): bool =
b.calcHash()
return a.hash == b.hash
else:
return eq(a, b)
result = eq(a, b)
proc markDirty*(key: VKey) =
dirty[&key] = true
inc dirtyCount
proc updateDirtyElements(parent, current: Node, newNode: VNode) =
if newNode.key >= 0 and dirty.contains(&newNode.key):
dirty.del(&newNode.key)
dec dirtyCount
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 +241,9 @@ proc dodraw() =
let olddom = document.getElementById("ROOT")
updateElement(nil, olddom, newtree, currentTree)
#assert same(newtree, document.getElementById("ROOT"))
if dirtyCount > 0:
updateDirtyElements(nil, olddom, newtree)
dirtyCount = 0
currentTree = newtree
# now that it's part of the DOM, give it the focus:
if toFocus != nil: