stateful components now in a workable state; renamed .component macro to .compact which much better captures its meaning

This commit is contained in:
Andreas Rumpf 2017-06-20 00:14:54 +02:00
commit c86916e37d
10 changed files with 185 additions and 291 deletions

View file

@ -1,5 +1,5 @@
import karax, karaxdsl, vdom, kdom, components import karax, karaxdsl, vdom, kdom, compact
const const
Play = 0 Play = 0
@ -12,7 +12,7 @@ proc play(n: Node) {.importcpp.}
proc pause(n: Node) {.importcpp.} proc pause(n: Node) {.importcpp.}
proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} 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) = proc handler(ev: Event; n: VNode) =
let myVideo = document.getElementById(id) let myVideo = document.getElementById(id)
case n.key case n.key

View file

@ -1,5 +1,5 @@
import vdom, karax, karaxdsl, jstrutils, components, localstorage import vdom, karax, karaxdsl, jstrutils, compact, localstorage
type type
Filter = enum Filter = enum
@ -77,7 +77,7 @@ proc toChecked(checked: bool): cstring =
proc selected(v: Filter): cstring = proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil)) (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): result = buildHtml(tr):
li(class=toClass(completed)): li(class=toClass(completed)):
if not selected: if not selected:
@ -92,7 +92,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compon
onblur = focusLost, onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus) onkeyupenter = editEntry, value = d, setFocus)
proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} =
result = buildHtml(footer(class = "footer")): result = buildHtml(footer(class = "footer")):
span(class = "todo-count"): span(class = "todo-count"):
strong: strong:
@ -111,7 +111,7 @@ proc makeFooter(entriesCount, completedCount: int): VNode {.component.} =
button(class = "clear-completed", onclick = clearCompleted): button(class = "clear-completed", onclick = clearCompleted):
text "Clear completed (" & &completedCount & ")" text "Clear completed (" & &completedCount & ")"
proc makeHeader(): VNode {.component.} = proc makeHeader(): VNode {.compact.} =
result = buildHtml(header(class = "header")): result = buildHtml(header(class = "header")):
h1: h1:
text "todos" text "todos"

View file

@ -51,13 +51,13 @@ proc render(x: VComponent): VNode =
var gid: int var gid: int
proc carousel(): Carousel = proc changed(c: VComponent): bool =
result = Carousel(kind: VNodeKind.component, key: -1)
result.render = render
result.list = images
result.changed = proc (c: VComponent): bool =
let x = Carousel(c) let x = Carousel(c)
result = x.change result = x.change
proc carousel(): Carousel =
result = newComponent(Carousel, render, changed)
result.list = images
result.cntdown = ticksUntilChange result.cntdown = ticksUntilChange
result.myid = gid result.myid = gid
inc gid inc gid

View file

@ -1,5 +1,5 @@
import vdom, karax, karaxdsl, jdict, jstrutils, components import vdom, karax, karaxdsl, jdict, jstrutils, compact
type type
Filter = enum Filter = enum

121
src/compact.nim Normal file
View file

@ -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.. <params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template dwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): Node =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, cstring(key), val)
template dregister(key, val) =
bind jdict.`[]=`
`[]=`(dcomponents, cstring(key), val)
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
else:
result.add getAst(dwrapper(newname name, unpackCall))
result.add getAst(dregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,225 +0,0 @@
## Components in Karax are built by the ``.component`` macro annotation.
import macros, jdict, kdom, vdom, tables, strutils
type
StateDict*[V] = ref object
defaultValue*: V
proc get[V](d: StateDict[V], k: VKey): V {.importcpp: "#[#]".}
proc put[V](d: StateDict[V], k: VKey, v: V) {.importcpp: "#[#] = #".}
proc contains*[V](d: StateDict[V], k: VKey): bool {.importcpp: "#.hasOwnProperty(#)".}
proc del*[V](d: StateDict[V], k: VKey) {.importcpp: "delete #[#]".}
proc newStateDict*[V](): StateDict[V] {.importcpp: "{@}".}
var
dirty = newStateDict[bool]()
someDirty*: bool
proc markDirty*(key: VKey) =
dirty.put(key, true)
someDirty = true
proc unmarkDirty*(key: VKey) = dirty.del key
proc isDirty*(key: VKey): bool = dirty.contains(key)
proc `[]`*[V](d: StateDict[V], k: VKey): V =
if d.contains(k): result = d.get(k)
else: result = d.defaultValue
proc `[]=`*[V](d: StateDict[V], k: VKey, v: V) =
d.put(k, v)
markDirty(k)
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()
template toState(x): untyped = newIdentNode("state" & x)
proc accessState(sv: NimNode): NimNode {.compileTime.} =
newTree(nnkBracketExpr, sv, newIdentNode("key"))
proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl: 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: newCall("type", val)
if usedType.kind == nnkEmpty:
error("cannot determine the variable's type", c)
for i in 0 .. c.len-3:
let v = $c[i]
let sv = toState v
decl.add(quote do:
var `sv` = newStateDict[`usedType`]())
if val.kind != nnkEmpty:
decl.add newTree(nnkAsgn, newDotExpr(sv, newIdentNode"defaultValue"),
val)
else:
error("component state must have a primitive initializer")
names[v] = true
of nnkStmtList, nnkStmtListExpr:
for x in n: stateDecl(x, names, decl)
of nnkDo:
stateDecl(n.body, names, decl)
of nnkCommentStmt: discard
else:
error("invalid 'state' declaration", n)
proc doState(n: NimNode; names: TableRef[string, bool];
decl: NimNode): NimNode =
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)
return newTree(nnkNone)
of nnkSym, nnkIdent:
let v = $n
if v in names:
let sv = toState v
return accessState(sv)
else: discard
result = copyNimNode(n)
for i in 0..<n.len:
let x = doState(n[i], names, decl)
if x.kind != nnkNone: result.add x
proc compBody(body, decl: NimNode): NimNode =
var names = newTable[string, bool]()
result = doState(body, names, decl)
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 component*(prc: untyped): untyped =
## A component takes an proc body and registers it as a component to the
## virtual dom.
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.. <params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
let decl = newTree(nnkStmtList)
let newBody = compBody(n, decl)
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template dwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): Node =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, cstring(key), val)
template dregister(key, val) =
bind jdict.`[]=`
`[]=`(dcomponents, cstring(key), val)
result = newTree(nnkStmtList, decl, newBody)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
else:
result.add getAst(dwrapper(newname name, unpackCall))
result.add getAst(dregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result
when isMainModule:
proc public*(key: VKey; x, y: int, b: bool; s: cstring): VNode {.component.} =
state:
var foo = 89
proc callback() =
foo = 78
let cc = callback
when false:
proc private(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard

View file

@ -1,6 +1,6 @@
## Karax -- Single page applications for Nim. ## Karax -- Single page applications for Nim.
import kdom, vdom, jstrutils, components, jdict, vstyles import kdom, vdom, jstrutils, compact, jdict, vstyles
export kdom.Event export kdom.Event
@ -82,7 +82,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) =
template detach(n: VNode) = template detach(n: VNode) =
if n.kind == VNodeKind.component: if n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
if x.onDetach != nil: x.onDetach(x) if x.onDetachImpl != nil: x.onDetachImpl(x)
n.dom = nil n.dom = nil
template attach(n: VNode) = template attach(n: VNode) =
n.dom = result n.dom = result
@ -104,9 +104,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node =
return result return result
elif n.kind == VNodeKind.component: elif n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
if x.onAttach != nil: x.onAttach(x) if x.onAttachImpl != nil: x.onAttachImpl(x)
assert x.render != nil assert x.renderImpl != nil
result = vnodeToDom(x.render(x), kxi) result = vnodeToDom(x.renderImpl(x), kxi)
attach n attach n
return result return result
else: else:
@ -147,7 +147,7 @@ proc replaceById(id: cstring; newTree: Node) =
type type
EqResult = enum EqResult = enum
different, similar, identical changed, different, similar, identical
proc eq(a, b: VNode; deep: bool): EqResult = proc eq(a, b: VNode; deep: bool): EqResult =
if a.kind != b.kind: return different if a.kind != b.kind: return different
@ -162,15 +162,16 @@ proc eq(a, b: VNode; deep: bool): EqResult =
for i in 0..<a.len: for i in 0..<a.len:
if eq(a[i], b[i], deep) == different: return different if eq(a[i], b[i], deep) == different: return different
elif b.kind == VNodeKind.component: elif b.kind == VNodeKind.component:
# let x = VComponent(b) # different component names mean different components:
# assert x.changed != nil if a.text != b.text: return different
# return if x.changed(x): different else: identical let x = VComponent(b)
return different assert x.changedImpl != nil
return if x.changedImpl(x): changed else: identical
elif deep: elif deep:
if a.len != b.len: return different if a.len != b.len: return different
for i in 0..<a.len: for i in 0..<a.len:
let res = eq(a[i], b[i], deep) let res = eq(a[i], b[i], deep)
if res == different: return different if res <= different: return different
elif res == similar: elif res == similar:
# but continue, maybe something makes it 'different'! # but continue, maybe something makes it 'different'!
result = similar result = similar
@ -181,7 +182,8 @@ proc eq(a, b: VNode; deep: bool): EqResult =
# Do not test event listeners here! # Do not test event listeners here!
return result return result
proc updateDirtyElements(parent, current: Node, newNode: VNode, when false:
proc updateDirtyElements(parent, current: Node, newNode: VNode,
kxi: KaraxInstance) = kxi: KaraxInstance) =
if newNode.key >= 0 and isDirty(newNode.key): if newNode.key >= 0 and isDirty(newNode.key):
unmarkDirty(newNode.key) unmarkDirty(newNode.key)
@ -216,21 +218,17 @@ proc updateDom(newNode, oldNode: VNode) =
proc updateElement(parent, current: Node, newNode, oldNode: VNode; proc updateElement(parent, current: Node, newNode, oldNode: VNode;
kxi: KaraxInstance): bool = 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) let res = eq(newNode, oldNode, deep=false)
if res == different: 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) detach(oldNode)
let n = vnodeToDom(newNode, kxi) n = vnodeToDom(newNode, kxi)
if parent == nil: if parent == nil:
replaceById(kxi.rootId, n) replaceById(kxi.rootId, n)
else: else:
@ -264,7 +262,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode;
a[i] = b a[i] = b
#updateDom(a, b) #updateDom(a, b)
action action
of different: break of different, changed: break
of similar: of similar:
#updateDom(a, b) #updateDom(a, b)
a[i] = b a[i] = b
@ -318,9 +316,6 @@ proc dodraw(kxi: KaraxInstance) =
let olddom = document.getElementById(kxi.rootId) let olddom = document.getElementById(kxi.rootId)
discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi)
#assert same(newtree, document.getElementById("ROOT")) #assert same(newtree, document.getElementById("ROOT"))
if someDirty:
updateDirtyElements(nil, olddom, newtree, kxi)
someDirty = false
kxi.currentTree = newtree kxi.currentTree = newtree
if not kxi.postRenderCallback.isNil: if not kxi.postRenderCallback.isNil:

View file

@ -1,5 +1,5 @@
import macros, karax, vdom, components import macros, karax, vdom, compact
from strutils import startsWith, toLowerAscii from strutils import startsWith, toLowerAscii
const const

View file

@ -1,3 +1,3 @@
## Include file that contains the common imports for the Karax framework. ## Include file that contains the common imports for the Karax framework.
import karax, karaxdsl, vdom, components import karax, karaxdsl, vdom, compact

View file

@ -120,10 +120,10 @@ type
## is not part of the virtual DOM anymore. ## is not part of the virtual DOM anymore.
VComponent* = ref object of VNode VComponent* = ref object of VNode
render*: proc(self: VComponent): VNode renderImpl*: proc(self: VComponent): VNode
changed*: proc(self: VComponent): bool changedImpl*: proc(self: VComponent): bool
onAttach*: proc(self: VComponent) onAttachImpl*: proc(self: VComponent)
onDetach*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent)
proc value*(n: VNode): cstring = n.text proc value*(n: VNode): cstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v 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 = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) 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, changed: (proc(self: VComponent): bool) not nil,
onAttach: proc(self: VComponent) = nil, onAttach: proc(self: VComponent) = nil,
onDetach: proc(self: VComponent) = nil): VNode = onDetach: proc(self: VComponent) = nil): T =
result = VComponent(kind: VNodeKind.component, key: -1, render: render, ## Use this template to create new components.
changed: changed, onAttach: onAttach, onDetach: onDetach) 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 = "") = proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
if n.attrs.isNil: if n.attrs.isNil: