update value property of real DOM nodes; minor DOM diff speedups; allow setFocus=false in the DSL

This commit is contained in:
Andreas Rumpf 2017-06-28 15:23:19 +02:00
commit c681f0055f
4 changed files with 67 additions and 38 deletions

View file

@ -17,7 +17,7 @@ type
proc `[]`*[T](s: JSeq[T], i: int): T {.importcpp: "#[#]", noSideEffect.}
proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.}
proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".}
proc newJSeq*[T](len: int = 0): JSeq[T] {.importcpp: "new Array(#)".}
proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.}
proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.}

View file

@ -14,6 +14,14 @@ proc key*(e: Node): VKey =
else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore
Patch = object
k: PatchKind
parent, current: Node
n: VNode
type
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this.
@ -24,6 +32,9 @@ type
toFocus: Node
toFocusV: VNode
renderId: int
patches: seq[Patch] # we reuse this to save allocations
patchLen: int
var
kxi*: KaraxInstance ## The current Karax instance. This is always used
@ -33,8 +44,9 @@ var
## in your 'buildHtml' statement, it needs to be named
## 'kxi'.
proc setFocus*(n: VNode; kxi: KaraxInstance = kxi) =
kxi.toFocusV = n
proc setFocus*(n: VNode; enabled = true; kxi: KaraxInstance = kxi) =
if enabled:
kxi.toFocusV = n
# ----------------- event wrapping ---------------------------------------
@ -199,7 +211,7 @@ proc updateStyles(newNode, oldNode: VNode) =
else: oldNode.dom.style = Style()
oldNode.style = newNode.style
proc mergeEvents(newNode, oldNode: VNode) =
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom
for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0]
@ -222,21 +234,22 @@ proc printV(n: VNode; depth: cstring = "") =
for i in 0 ..< n.len:
printV(n[i], depth & " ")
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore
Patch = object
k: PatchKind
parent, current: Node
n: VNode
template addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na: VNode) =
let L = kxi.patchLen
if L >= kxi.patches.len:
# allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na))
else:
kxi.patches[L].k = ka
kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta
kxi.patches[L].n = na
inc kxi.patchLen
proc addPatch(patches: JSeq[Patch]; k: PatchKind; parent, current: Node;
n: VNode) =
patches.add(Patch(k: k, parent: parent, current: current, n: n))
proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) =
for i in 0..<patches.len:
let p = patches[i]
proc apply(kxi: KaraxInstance) =
for i in 0..<kxi.patchLen:
let p = kxi.patches[i]
case p.k
of pkReplace:
let nn = vnodeToDom(p.n, kxi)
@ -252,15 +265,18 @@ proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) =
of pkInsertBefore:
let nn = vnodeToDom(p.n, kxi)
p.parent.insertBefore(nn, p.current)
kxi.patchLen = 0
proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): EqResult =
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
result = eq(newNode, oldNode)
case result
of identical, similar:
newNode.dom = oldNode.dom
if result == similar: updateStyles(newNode, oldNode)
if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode)
mergeEvents(newNode, oldNode, kxi)
if newNode.kind == VNodeKind.input or newNode.kind == VNodeKind.textarea:
oldNode.dom.value = newNode.text
let newLength = newNode.len
var oldLength = oldNode.len
@ -271,7 +287,7 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]):
var commonPrefix = 0
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
let oldLen = patches.len
let oldLen = kxi.patchLen
when false:
if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
assert current != nil
@ -280,16 +296,16 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]):
let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk:
diff(a[i], b[j], parent, current, patches)
diff(a[i], b[j], parent, current, kxi)
else:
diff(a[i], b[j], current, current.childNodes[j], patches)
diff(a[i], b[j], current, current.childNodes[j], kxi)
case r
of identical, changed, similar:
a[i] = b[j]
action
of different:
# undo what 'diff' would have done:
shrink(patches, oldLen)
kxi.patchLen = oldLen
if result != different: result = r
break
#of similar:
@ -311,25 +327,25 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]):
var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1:
if diff(newNode[i], oldNode[i], current, current.childNodes[i],
patches) != different:
kxi) != different:
newNode[i] = oldNode[i]
else:
result = different
if oldPos + 1 == oldLength:
for i in pos..newPos:
patches.addPatch(pkAppend, current, nil, newNode[i])
kxi.addPatch(pkAppend, current, nil, newNode[i])
result = different
else:
let before = current.childNodes[oldPos + 1]
for i in pos..newPos:
patches.addPatch(pkInsertBefore, current, before, newNode[i])
kxi.addPatch(pkInsertBefore, current, before, newNode[i])
result = different
# XXX call 'attach' here?
for i in pos..oldPos:
detach(oldNode[i])
#doAssert i < current.childNodes.len
patches.addPatch(pkRemove, current, current.childNodes[i], nil)
kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
result = different
of changed:
@ -340,9 +356,9 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]):
x.updatedImpl(x)
if oldExpanded == nil:
detach(oldNode)
patches.addPatch(pkReplace, parent, current, x.expanded)
kxi.addPatch(pkReplace, parent, current, x.expanded)
else:
let res = diff(x.expanded, oldExpanded, parent, current, patches)
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
if res != different:
x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil"
@ -350,7 +366,7 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]):
assert x.expanded.dom != nil, "expanded.dom is nil"
of different:
detach(oldNode)
patches.addPatch(pkReplace, parent, current, newNode)
kxi.addPatch(pkReplace, parent, current, newNode)
proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return
@ -363,9 +379,9 @@ proc dodraw(kxi: KaraxInstance) =
replaceById(kxi.rootId, asdom)
else:
let olddom = document.getElementById(kxi.rootId)
var patches = newJSeq[Patch]()
discard diff(newtree, kxi.currentTree, nil, olddom, patches)
patches.apply(kxi)
discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
#kout cstring"patch len ", patches.len
apply(kxi)
kxi.currentTree = newtree
#doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -375,6 +391,7 @@ proc dodraw(kxi: KaraxInstance) =
# now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil:
kxi.toFocus.focus()
kxi.renderId = 0
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
@ -386,8 +403,8 @@ proc redraw*(kxi: KaraxInstance = kxi) =
clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30)
elif true:
cancelFrame kxi.renderId
kxi.renderId = reqFrame(proc () = kxi.dodraw)
if kxi.renderId == 0:
kxi.renderId = reqFrame(proc () = kxi.dodraw)
else:
dodraw(kxi)
@ -400,7 +417,8 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback)
postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60))
kxi = result
window.onload = init

View file

@ -85,12 +85,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(x, tmp, x[1], ident"kxi")
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif ck != ComponentKind.Tag:
call.add x
elif eqIdent(x, "setFocus"):
result.add newCall(x, tmp, ident"kxi")
result.add newCall(x, tmp, bindSym"true", ident"kxi")
else:
result.add tcall2(x, tmp)
if tmpContext == nil:

View file

@ -54,9 +54,18 @@ type
proc renderComb(self: VComponent): VNode =
let self = Combined(self)
proc bu(ev: Event; n: VNode) =
self.a.value = ""
self.b.value = ""
markDirty(self.a)
markDirty(self.b)
result = buildHtml(tdiv(style=self.style)):
self.a
self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool =
let self = Combined(self)