diff --git a/experiments/scrollapp.nim b/experiments/scrollapp.nim index 7b7ce8e..f9c8ccb 100644 --- a/experiments/scrollapp.nim +++ b/experiments/scrollapp.nim @@ -1,17 +1,14 @@ -import vdom, karax, karaxdsl, jdict, jstrutils +import vdom, karax, karaxdsl, jdict, jstrutils, components type Filter = enum all, active, completed -const - WindowSize = 50 # overestimating it does not harm - var entries: seq[(cstring, bool)] visibleA = 0 - visibleB = 10_000 + visibleB = 10 selectedEntry = -1 filter: Filter @@ -52,34 +49,15 @@ proc toChecked(checked: bool): cstring = proc selected(v: Filter): cstring = (if filter == v: cstring"selected" else: cstring(nil)) -when false: - proc entry(id: int, d: cstring; completed, selected: bool): proc (): VNode = - var lastid: type(id) - var lastd: type(d) - var lastCompleted: type(completed) - var lastSelected: type(selected) - proc inner(): VNode = - if lastid != id or lastd != d or lastCompleted != completed or lastSelected != selected: - result = logic(id, d, completed, selected) - lastid = id - lastd = d - lastCompleted = completed - lastSelected = selected - - result = inner - -proc lazyEntry(args: seq[VNode]): VNode = - let id = args[0].intValue - let d = args[1].text - let completed = args[2].intValue != 0 - let selected = args[3].intValue != 0 +proc createEntry(id: int; d: cstring; + completed: bool, selected: bool): VNode {.component.} = result = buildHtml(tr): li(class=toClass(completed), key = id): if not selected: tdiv(class = "view"): input(class = "toggle", `type` = "checkbox", checked = toChecked(completed), onclick=toggleEntry, key = id) - label(onDblClick=editHandler, key = id): + label(onclick=editHandler, key = id): text d button(class = "destroy", key = id, onclick = removeHandler) else: @@ -87,9 +65,6 @@ proc lazyEntry(args: seq[VNode]): VNode = onfocusLost = focusLost, onenter = editEntry, value = d, setFocus) -proc createEntry(id: int; d: cstring; completed, selected: bool): VNode = - result = thunk(lazyEntry, id, d, completed, selected) - proc myscroll(ev: Event; n: VNode) = kout cstring"scrolling event here" @@ -105,10 +80,6 @@ proc createDom(): VNode = input(class = "toggle-all", `type` = "checkbox", name = "toggle") label(`for` = "toggle-all", onclick = onAllDone): text "Mark all as complete" - tdiv(onscroll = myscroll, style = "overflow: visible; width: 0px; display: block; max-height: 1em"): - for i in 0..4: - tdiv: - text entries[i][0] var entriesCount = 0 var completedCount = 0 diff --git a/src/components.nim b/src/components.nim new file mode 100644 index 0000000..6d6a8f2 --- /dev/null +++ b/src/components.nim @@ -0,0 +1,103 @@ +## Components in Karax are built by the ``.component`` macro annotation. + +import macros, jdict, dom, 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, + VNode, + Node + +var + allcomponents {.compileTime.} = initTable[string, ComponentKind]() + +proc isComponent*(x: string): ComponentKind {.compileTime.} = + allcomponents.getOrDefault(x) + +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") + 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" + n[0] = ident("inner" & $name) + var unpackCall = newCall(n[0]) + var counter = 0 + for i in 1.. maybe give nodes a hash? return true proc equalsTree(a, b: VNode): bool = - if not a.validHash: - a.calcHash() - if not b.validHash: - b.calcHash() - return a.hash == b.hash + when false: + # hashing is too fragile now with component support: + if not a.validHash: + a.calcHash() + if not b.validHash: + b.calcHash() + return a.hash == b.hash + else: + return eq(a, b) proc updateElement(parent, current: Node, newNode, oldNode: VNode) = if not equals(newNode, oldNode): @@ -177,12 +184,14 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = current.removeChild(current.lastChild) else: var commonPrefix = 0 - while commonPrefix < minLength and equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): + while commonPrefix < minLength and + equalsTree(newNode[commonPrefix], oldNode[commonPrefix]): inc commonPrefix var oldPos = oldLength - 1 var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix and equalsTree(newNode[newPos], oldNode[oldPos]): + while oldPos >= commonPrefix and newPos >= commonPrefix and + equalsTree(newNode[newPos], oldNode[oldPos]): dec oldPos dec newPos @@ -254,18 +263,6 @@ proc redraw*() = proc init*() = reqFrame(dodraw) -#proc prepend*(parent, kid: Element) = -# parent.insertBefore(kid, parent.firstChild) -# prependChild(parent, kid) - -proc suffix*(s, prefix: cstring): cstring = - if s.startsWith(prefix): - result = s.substr(prefix.len) - else: - kout(cstring"bug! " & s & cstring" does not start with " & prefix) - -proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix)) - proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} @@ -291,8 +288,6 @@ proc setOnchanged*(e: VNode; action: EventHandler) = proc setOnscroll*(e: VNode; action: EventHandler) = onImpl EventKind.onscroll -#proc ceil(f: float): int {.importc: "Math.ceil", nodecl.} - proc setOnscroll*(action: proc(min, max: VKey; diff: int)) = var oldY = window.pageYOffset @@ -321,98 +316,6 @@ proc setOnHashChange*(action: proc (hashPart: cstring)) = redraw() onhashchange = wrapper -var - linkCounter: int - -proc link*(id: int): VNode = - result = newVNode(VNodeKind.anchor) - result.setAttr("href", "#") - inc linkCounter - result.setAttr("id", $linkCounter & ":" & $id) - -proc link*(action: EventHandler): VNode = - result = newVNode(VNodeKind.anchor) - result.setAttr("href", "#") - result.setOnclick action - -when false: - proc button*(caption: cstring; action: EventHandler; disabled=false): VNode = - result = newVNode(VNodeKind.button) - result.add text(caption) - if action != nil: - result.setOnClick action - if disabled: - result.setAttr("disabled", "true") - -proc select*(choices: openarray[cstring]): VNode = - result = newVNode(VNodeKind.select) - var i = 0 - for c in choices: - result.add tree(VNodeKind.option, [(cstring"value", toCstr(i))], text(c)) - inc i - -proc select*(choices: openarray[(int, cstring)]): VNode = - result = newVNode(VNodeKind.select) - for c in choices: - result.add tree(VNodeKind.option, [(cstring"value", toCstr(c[0]))], text(c[1])) - -var radioCounter: int - -proc radio*(choices: openarray[(int, cstring)]): VNode = - result = newVNode(VNodeKind.fieldset) - var i = 0 - inc radioCounter - for c in choices: - let id = cstring"radio_" & c[1] & toCstr(i) - var kid = tree(VNodeKind.input, [(cstring"type", cstring"radio"), - (cstring"id", id), (cstring"name", cstring"radio" & toCStr(radioCounter)), - (cstring"value", toCStr(c[0]))]) - if i == 0: - kid.setAttr(cstring"checked", cstring"checked") - var lab = tree(VNodeKind.label, [(cstring"for", id)], text(c[1])) - kid.add lab - result.add kid - inc i - -proc tag*(kind: VNodeKind; id=cstring(nil), class=cstring(nil)): VNode = - result = newVNode(kind) - result.id = id - result.class = class - -proc tdiv*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.tdiv, id, class) -proc span*(id=cstring(nil), class=cstring(nil)): VNode = tag(VNodeKind.span, id, class) - -proc valueAsInt*(e: Node): int = parseInt(e.value) - -proc th*(s: cstring): VNode = - result = newVNode(VNodeKind.th) - result.add text(s) - -proc td*(s: string): VNode = - result = newVNode(VNodeKind.td) - result.add text(s) - -proc td*(s: VNode): VNode = - result = newVNode(VNodeKind.td) - result.add s - -proc td*(class: cstring; s: VNode): VNode = - result = newVNode(VNodeKind.td) - result.add s - result.class = class - -proc table*(class=cstring(nil), kids: varargs[VNode]): VNode = - result = tag(VNodeKind.table, nil, class) - for k in kids: result.add k - -proc tr*(kids: varargs[VNode]): VNode = - result = newVNode(VNodeKind.tr) - for k in kids: - if k.kind in {VNodeKind.td, VNodeKind.th}: - result.add k - else: - result.add td(k) - proc getAttr(e: Node; key: cstring): cstring {. importcpp: "#.getAttribute(#)", nodecl.} @@ -426,9 +329,6 @@ template keyeventBody() = redraw() proc realtimeInput*(val: cstring; action: EventHandler): VNode = - #let oldElem = getElementById(id) - #if oldElem != nil: return oldElem - #let newVal = if oldElem.isNil: val else: $oldElem.value var timer: Timeout proc onkeyup(ev: Event; n: VNode) = proc wrapper() = keyeventBody() @@ -440,9 +340,6 @@ proc realtimeInput*(val: cstring; action: EventHandler): VNode = result.addEventListener(EventKind.onkeyup, onkeyup) proc enterInput*(id, val: cstring; action: EventHandler): VNode = - #let oldElem = getElementById(id) - #if oldElem != nil: return oldElem - #let newVal = if oldElem.isNil: val else: $oldElem.value proc onkeyup(ev: Event; n: VNode) = if ev.keyCode == 13: keyeventBody() @@ -508,16 +405,13 @@ proc setupErrorHandler*(useAlert=false) = {.pop.} -when false: - var plugins {.exportc.}: seq[(string, proc())] = @[] - - proc onInput(val: cstring) = - kout val - if val == "dyn": - let body = getElementById("body") - body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")])) - redraw() - kout(plugins.len) - if plugins.len > 0: - plugins[0][1]() +proc prepend*(parent, kid: Element) = + parent.insertBefore(kid, parent.firstChild) +proc loadScript*(jsfilename: cstring) = + let body = getElementById("body") + let s = document.createElement("script") + s.setAttr "type", "text/javascript" + s.setAttr "src", jsfilename + body.prepend(s) + redraw() diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 6b8e540..dce3034 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -1,5 +1,5 @@ -import macros, karax, vdom +import macros, karax, vdom, components from strutils import startsWith, toLowerAscii const @@ -61,11 +61,19 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result = n of nnkCallKinds: let op = getName(n[0]) - if isTag(op): + let ck = isComponent(op) + let isTag = isTag(op) + if isTag or ck != ComponentKind.None: let tmp = genSym(nskLet, "tmp") + let call = if isTag: + newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])) + elif ck == ComponentKind.VNode: + newCall(bindSym"vthunk", newLit(op)) + else: + newCall(bindSym"dthunk", newLit(op)) result = newTree( if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, - newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])))) + newLetStmt(tmp, call)) for i in 1 ..< n.len: # named parameters are transformed into attributes or events: let x = n[i] @@ -77,7 +85,9 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add newDotAsgn(tmp, key, x[1]) else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) - elif x.kind == nnkIdent: + elif not isTag: + call.add x + elif eqIdent(x, "setFocus"): result.add newCall(x, tmp) else: result.add tcall2(x, tmp) diff --git a/src/vdom.nim b/src/vdom.nim index 0ddfc3a..2670be5 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -5,7 +5,7 @@ import shash type VNodeKind* {.pure.} = enum - text, int, bool, thunk, + text, int, bool, vthunk, dthunk, anchor, tdiv, table, tr, td, th, thead, tbody, @@ -15,7 +15,8 @@ type const toTag*: array[VNodeKind, cstring] = [ - cstring"#text", "#int", "#bool", "#thunk", "A", "DIV", "TABLE", "TR", "TD", + cstring"#text", "#int", "#bool", "#vthunk", "#dthunk", + "A", "DIV", "TABLE", "TR", "TD", "TH", "THEAD", "TBODY", "LINK", "SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL", @@ -43,7 +44,6 @@ type # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] events*: seq[(EventKind, EventHandler)] - thunk*: proc (args: seq[VNode]): VNode hash*: Hash validHash*: bool @@ -55,23 +55,26 @@ proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, key: i) proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, key: ord(b)) proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, key: -1, text: x) -proc callThunk*(n: VNode): VNode = - assert n.kind == VNodeKind.thunk - result = n.thunk(n.kids) +template callThunk*(fn: typed; n: VNode): untyped = + ## for internal usage only. + fn(n.kids) -proc thunk*(cb: proc(args: seq[VNode]): VNode; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.thunk, thunk: cb, kids: @args) +proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = + VNode(kind: VNodeKind.vthunk, text: name, kids: @args) + +proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = + VNode(kind: VNodeKind.dthunk, text: name, kids: @args) proc eq*(a, b: VNode): bool = if a.kind != b.kind: return false if a.id != b.id: return false if a.class != b.class: return false + if a.key != b.key: return false if a.kind != VNodeKind.text: if a.kids.len != b.kids.len: return false for i in 0..