wip: stateful components
This commit is contained in:
parent
5f083a7d01
commit
0255f58964
5 changed files with 99 additions and 130 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Todo app</title>
|
<title>Todo app</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body id="body">
|
<body id="body">
|
||||||
|
|
@ -11,9 +10,6 @@
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/example.js"></script>
|
<script type="text/javascript" src="nimcache/example.js"></script>
|
||||||
<script type="text/javascript">
|
|
||||||
onload(window.location.pathname.split('/')[1])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,135 +1,33 @@
|
||||||
|
|
||||||
import vdom, karax, karaxdsl, jdict, jstrutils
|
import vdom, components, karax, karaxdsl, jdict, jstrutils
|
||||||
|
|
||||||
type
|
|
||||||
Filter = enum
|
|
||||||
all, active, completed
|
|
||||||
|
|
||||||
var
|
var
|
||||||
entries: seq[(cstring, bool)]
|
images: seq[cstring] = @[cstring"a", "b", "c", "d"]
|
||||||
selectedEntry = -1
|
|
||||||
filter: Filter
|
|
||||||
|
|
||||||
proc onTodoEnter(ev: Event; n: VNode) =
|
proc carousel*(key: VKey): VNode {.component.} =
|
||||||
entries.add((n.value, false))
|
state:
|
||||||
n.value = ""
|
var counter = 0
|
||||||
|
|
||||||
proc removeHandler(ev: Event; n: VNode) =
|
proc next(ev: Event; n: VNode) =
|
||||||
let id = suffixAsInt(n.id, "remove:")
|
counter = (counter + 1) mod images.len
|
||||||
entries[id] = (cstring(nil), false)
|
markDirty key
|
||||||
|
|
||||||
proc editHandler(ev: Event; n: VNode) =
|
result = buildHtml(tdiv(key=key)):
|
||||||
let id = suffixAsInt(n.id, "edit:")
|
text images[counter]
|
||||||
selectedEntry = id
|
button(onclick = next):
|
||||||
|
text "Next"
|
||||||
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
|
|
||||||
|
|
||||||
proc createDom(): VNode =
|
proc createDom(): VNode =
|
||||||
result = buildHtml(tdiv(class="todomvc-wrapper")):
|
result = buildHtml(table):
|
||||||
section(class = "todoapp"):
|
tr:
|
||||||
header(class = "header"):
|
td:
|
||||||
h1:
|
carousel(0)
|
||||||
text "todos"
|
td:
|
||||||
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
|
carousel(1)
|
||||||
onenter = onTodoEnter)
|
tr:
|
||||||
section(class = "main"):
|
td:
|
||||||
input(class = "toggle-all", `type` = "checkbox", name = "toggle")
|
carousel(2)
|
||||||
label(`for` = "toggle-all", onclick = onAllDone):
|
td:
|
||||||
text "Mark all as complete"
|
carousel(3)
|
||||||
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 & ")"
|
|
||||||
|
|
||||||
setOnHashChange(proc(hash: cstring) =
|
|
||||||
if hash == cstring"#/": filter = all
|
|
||||||
elif hash == cstring"#/completed": filter = completed
|
|
||||||
elif hash == cstring"#/active": filter = active
|
|
||||||
)
|
|
||||||
setRenderer createDom
|
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
1
experiments/example.nims
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
--path: "../src"
|
||||||
|
|
@ -30,6 +30,53 @@ proc addTags() {.compileTime.} =
|
||||||
static:
|
static:
|
||||||
addTags()
|
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.} =
|
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
|
||||||
#let t = symbolicType.getTypeImpl
|
#let t = symbolicType.getTypeImpl
|
||||||
let t = repr(symbolicType)
|
let t = repr(symbolicType)
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,12 @@ proc vnodeToDom(n: VNode): Node =
|
||||||
elif n.kind == VNodeKind.vthunk:
|
elif n.kind == VNodeKind.vthunk:
|
||||||
let x = callThunk(vcomponents[n.text], n)
|
let x = callThunk(vcomponents[n.text], n)
|
||||||
result = vnodeToDom(x)
|
result = vnodeToDom(x)
|
||||||
|
n.key = result.key
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
elif n.kind == VNodeKind.dthunk:
|
elif n.kind == VNodeKind.dthunk:
|
||||||
result = callThunk(dcomponents[n.text], n)
|
result = callThunk(dcomponents[n.text], n)
|
||||||
|
n.key = result.key
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
|
|
@ -114,6 +116,8 @@ proc same(n: VNode, e: Node): bool =
|
||||||
var
|
var
|
||||||
dorender: proc (): VNode {.closure.}
|
dorender: proc (): VNode {.closure.}
|
||||||
currentTree: VNode
|
currentTree: VNode
|
||||||
|
dirty = newJDict[cstring, bool]()
|
||||||
|
dirtyCount: int
|
||||||
|
|
||||||
proc replaceById(id: cstring; newTree: Node) =
|
proc replaceById(id: cstring; newTree: Node) =
|
||||||
let x = document.getElementById(id)
|
let x = document.getElementById(id)
|
||||||
|
|
@ -145,7 +149,27 @@ proc equalsTree(a, b: VNode): bool =
|
||||||
b.calcHash()
|
b.calcHash()
|
||||||
return a.hash == b.hash
|
return a.hash == b.hash
|
||||||
else:
|
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) =
|
proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
|
||||||
if not equalsShallow(newNode, oldNode):
|
if not equalsShallow(newNode, oldNode):
|
||||||
|
|
@ -217,6 +241,9 @@ proc dodraw() =
|
||||||
let olddom = document.getElementById("ROOT")
|
let olddom = document.getElementById("ROOT")
|
||||||
updateElement(nil, olddom, newtree, currentTree)
|
updateElement(nil, olddom, newtree, currentTree)
|
||||||
#assert same(newtree, document.getElementById("ROOT"))
|
#assert same(newtree, document.getElementById("ROOT"))
|
||||||
|
if dirtyCount > 0:
|
||||||
|
updateDirtyElements(nil, olddom, newtree)
|
||||||
|
dirtyCount = 0
|
||||||
currentTree = newtree
|
currentTree = newtree
|
||||||
# now that it's part of the DOM, give it the focus:
|
# now that it's part of the DOM, give it the focus:
|
||||||
if toFocus != nil:
|
if toFocus != nil:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue