From c86916e37df24647cee1f4b24f565cd539e4a02a Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 20 Jun 2017 00:14:54 +0200 Subject: [PATCH] stateful components now in a workable state; renamed .component macro to .compact which much better captures its meaning --- examples/mediaplayer/mediaplayer.nim | 4 +- examples/todoapp/todoapp.nim | 8 +- experiments/example.nim | 10 +- experiments/scrollapp.nim | 2 +- src/compact.nim | 121 ++++++++++++++ src/components.nim | 225 --------------------------- src/karax.nim | 83 +++++----- src/karaxdsl.nim | 2 +- src/karaxprelude.nim | 2 +- src/vdom.nim | 19 ++- 10 files changed, 185 insertions(+), 291 deletions(-) create mode 100644 src/compact.nim delete mode 100644 src/components.nim diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 890c078..3f970a8 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, kdom, components +import karax, karaxdsl, vdom, kdom, compact const Play = 0 @@ -12,7 +12,7 @@ proc play(n: Node) {.importcpp.} proc pause(n: Node) {.importcpp.} proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} -proc mplayer*(id, resource: cstring): VNode {.component.} = +proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) case n.key diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 802bfc9..f74abc3 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, components, localstorage +import vdom, karax, karaxdsl, jstrutils, compact, localstorage type Filter = enum @@ -77,7 +77,7 @@ proc toChecked(checked: bool): cstring = proc selected(v: Filter): cstring = (if filter == v: cstring"selected" else: cstring(nil)) -proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.component.} = +proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} = result = buildHtml(tr): li(class=toClass(completed)): if not selected: @@ -92,7 +92,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compon onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus) -proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = +proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} = result = buildHtml(footer(class = "footer")): span(class = "todo-count"): strong: @@ -111,7 +111,7 @@ proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = button(class = "clear-completed", onclick = clearCompleted): text "Clear completed (" & &completedCount & ")" -proc makeHeader(): VNode {.component.} = +proc makeHeader(): VNode {.compact.} = result = buildHtml(header(class = "header")): h1: text "todos" diff --git a/experiments/example.nim b/experiments/example.nim index 5653e22..fa1e739 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -51,13 +51,13 @@ proc render(x: VComponent): VNode = var gid: int +proc changed(c: VComponent): bool = + let x = Carousel(c) + result = x.change + proc carousel(): Carousel = - result = Carousel(kind: VNodeKind.component, key: -1) - result.render = render + result = newComponent(Carousel, render, changed) result.list = images - result.changed = proc (c: VComponent): bool = - let x = Carousel(c) - result = x.change result.cntdown = ticksUntilChange result.myid = gid inc gid diff --git a/experiments/scrollapp.nim b/experiments/scrollapp.nim index f9c8ccb..86a802f 100644 --- a/experiments/scrollapp.nim +++ b/experiments/scrollapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jdict, jstrutils, components +import vdom, karax, karaxdsl, jdict, jstrutils, compact type Filter = enum diff --git a/src/compact.nim b/src/compact.nim new file mode 100644 index 0000000..51de228 --- /dev/null +++ b/src/compact.nim @@ -0,0 +1,121 @@ +## Components in Karax are built by the ``.component`` macro annotation. + +import macros, jdict, kdom, vdom, tables, strutils + +var + vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]() + dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]() + +type + ComponentKind* {.pure.} = enum + None, + Tag, + VNode, + Node + +var + allcomponents {.compileTime.} = initTable[string, ComponentKind]() + +proc isComponent*(x: string): ComponentKind {.compileTime.} = + allcomponents.getOrDefault(x) + +proc addTags() {.compileTime.} = + let x = (bindSym"VNodeKind").getTypeImpl + expectKind(x, nnkEnumTy) + for i in ord(VNodeKind.html)..ord(VNodeKind.high): + # +1 because of empty node at the start of the enum AST: + let tag = $x[i+1] + allcomponents[tag] = ComponentKind.Tag + +static: + addTags() + +proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} = + #let t = symbolicType.getTypeImpl + let t = repr(symbolicType) + case t + of "cstring": + result = quote do: + args[`index`].text + of "int", "VKey": + result = quote do: + args[`index`].intValue + of "bool": + result = quote do: + args[`index`].intValue != 0 + elif t.endsWith"Kind": + result = quote do: + `symbolicType`(args[`index`].intValue) + else: + # just pass it along, maybe there is some conversion for it: + result = quote do: + args[`index`] + +proc newname*(n: NimNode): NimNode = + if n.kind == nnkPostfix: + n[1] = newname(n[1]) + result = n + elif n.kind == nnkSym: + result = ident($n.symbol) + else: + result = n + +macro compact*(prc: untyped): untyped = + ## A 'compact' tree generation proc is one that only depends on its + ## inputs and should be stored as a compact virtual DOM tree and + ## only expanded on demand (when its inputs changed). + var n = prc.copyNimNode + 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", n[0]) + let name = n[0] + let params = params(n) + let rettype = repr params[0] + var isvirtual = ComponentKind.None + if rettype == "VNode": + isvirtual = ComponentKind.VNode + elif rettype == "Node": + isvirtual = ComponentKind.Node + else: + 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) + var unpackCall = newCall(n[0]) + var counter = 0 + for i in 1.. = 0 and isDirty(newNode.key): - unmarkDirty(newNode.key) - let n = vnodeToDom(newNode, kxi) - if parent == nil: - replaceById(kxi.rootId, 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], kxi) - # leave early if we know there cannot be anything left to do: - #if dirtyCount <= 0: return +when false: + proc updateDirtyElements(parent, current: Node, newNode: VNode, + kxi: KaraxInstance) = + if newNode.key >= 0 and isDirty(newNode.key): + unmarkDirty(newNode.key) + let n = vnodeToDom(newNode, kxi) + if parent == nil: + replaceById(kxi.rootId, 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], kxi) + # leave early if we know there cannot be anything left to do: + #if dirtyCount <= 0: return proc updateStyles(newNode, oldNode: VNode; deep: bool) = # we keep the oldNode, but take over the style from the new node: @@ -216,21 +218,17 @@ proc updateDom(newNode, oldNode: VNode) = proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): bool = - if oldNode.kind == newNode.kind and oldNode.kind == VNodeKind.component: - let x = VComponent(oldNode) - assert x.changed != nil - if x.changed(x): - let n = vnodeToDom(x.render(x), kxi) - if parent == nil: - replaceById(kxi.rootId, n) - else: - parent.replaceChild(n, current) - return true - let res = eq(newNode, oldNode, deep=false) - if res == different: - detach(oldNode) - let n = vnodeToDom(newNode, kxi) + if res <= different: + var n: Node + if res == changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + n = vnodeToDom(x.renderImpl(x), kxi) + result = true + else: + detach(oldNode) + n = vnodeToDom(newNode, kxi) if parent == nil: replaceById(kxi.rootId, n) else: @@ -264,7 +262,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; a[i] = b #updateDom(a, b) action - of different: break + of different, changed: break of similar: #updateDom(a, b) a[i] = b @@ -318,9 +316,6 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) - if someDirty: - updateDirtyElements(nil, olddom, newtree, kxi) - someDirty = false kxi.currentTree = newtree if not kxi.postRenderCallback.isNil: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index a3558c8..e46ee62 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -1,5 +1,5 @@ -import macros, karax, vdom, components +import macros, karax, vdom, compact from strutils import startsWith, toLowerAscii const diff --git a/src/karaxprelude.nim b/src/karaxprelude.nim index 285730a..ed9349c 100644 --- a/src/karaxprelude.nim +++ b/src/karaxprelude.nim @@ -1,3 +1,3 @@ ## Include file that contains the common imports for the Karax framework. -import karax, karaxdsl, vdom, components +import karax, karaxdsl, vdom, compact diff --git a/src/vdom.nim b/src/vdom.nim index f25b1a7..8c19ea2 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -120,10 +120,10 @@ type ## is not part of the virtual DOM anymore. VComponent* = ref object of VNode - render*: proc(self: VComponent): VNode - changed*: proc(self: VComponent): bool - onAttach*: proc(self: VComponent) - onDetach*: proc(self: VComponent) + renderImpl*: proc(self: VComponent): VNode + changedImpl*: proc(self: VComponent): bool + onAttachImpl*: proc(self: VComponent) + onDetachImpl*: proc(self: VComponent) proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,12 +143,15 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) -proc vcomponent*(render: (proc(self: VComponent): VNode) not nil, +template newComponent*[T](t: typeDesc[T]; + render: (proc(self: VComponent): VNode) not nil, changed: (proc(self: VComponent): bool) not nil, onAttach: proc(self: VComponent) = nil, - onDetach: proc(self: VComponent) = nil): VNode = - result = VComponent(kind: VNodeKind.component, key: -1, render: render, - changed: changed, onAttach: onAttach, onDetach: onDetach) + onDetach: proc(self: VComponent) = nil): T = + ## Use this template to create new components. + T(kind: VNodeKind.component, key: -1, + text: cstring(astToStr(t)), renderImpl: render, + changedImpl: changed, onAttachImpl: onAttach, onDetachImpl: onDetach) proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: