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

View file

@ -54,9 +54,18 @@ type
proc renderComb(self: VComponent): VNode = proc renderComb(self: VComponent): VNode =
let self = Combined(self) 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)): result = buildHtml(tdiv(style=self.style)):
self.a self.a
self.b self.b
button(onclick=bu):
text "reset"
proc changed(self: VComponent): bool = proc changed(self: VComponent): bool =
let self = Combined(self) let self = Combined(self)