From a799413f7f394e725958972315fe7d54cfcd7f2f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 13 Apr 2017 21:07:04 +0200 Subject: [PATCH 001/264] added .component macro to karax --- experiments/scrollapp.nim | 39 ++------- src/components.nim | 103 +++++++++++++++++++++++ src/dom.nim | 2 + src/karax.nim | 170 +++++++------------------------------- src/karaxdsl.nim | 18 +++- src/vdom.nim | 23 +++--- 6 files changed, 169 insertions(+), 186 deletions(-) create mode 100644 src/components.nim 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.. Date: Thu, 13 Apr 2017 21:22:57 +0200 Subject: [PATCH 002/264] readme.rst: better english --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index c08ec99..64582eb 100644 --- a/readme.rst +++ b/readme.rst @@ -15,7 +15,7 @@ To try it out, run:: It uses a virtual DOM like React, but is much smaller than the existing frameworks plus of course it's written in Nim for Nim. No external dependencies! And thanks to Nim's whole program optimization only what -is used ends up producing JavaScript code. +is used ends up in the generated JavaScript code. Goals From be0f9c9e08b85591b38bd784bb3f6013e6856a18 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 19:36:16 +0200 Subject: [PATCH 003/264] added support for all HTML5 tags and drag&drop events --- src/components.nim | 12 ++++++ src/karax.nim | 11 ++---- src/vdom.nim | 94 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 85 insertions(+), 32 deletions(-) diff --git a/src/components.nim b/src/components.nim index 6d6a8f2..30d5922 100644 --- a/src/components.nim +++ b/src/components.nim @@ -9,6 +9,7 @@ var type ComponentKind* {.pure.} = enum None, + Tag, VNode, Node @@ -18,6 +19,17 @@ var 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) diff --git a/src/karax.nim b/src/karax.nim index 80d67c8..0007ce5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -111,7 +111,6 @@ proc same(n: VNode, e: Node): bool = var dorender: proc (): VNode {.closure.} - drawTimeout: Timeout currentTree: VNode proc setRenderer*(renderer: proc (): VNode) = @@ -125,9 +124,8 @@ proc getElementById*(id: cstring): Element {.importc: "document.getElementById", #proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: # "document.getElementsByClassName", nodecl.} - -proc textContent(e: Node): cstring {. - importcpp: "#.textContent", nodecl.} +#proc textContent(e: Node): cstring {. +# importcpp: "#.textContent", nodecl.} proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -215,6 +213,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = for i in 0..oldPos-pos: current.removeChild(current.childNodes[pos]) +when false: + var drawTimeout: Timeout proc dodraw() = let newtree = dorender() @@ -316,9 +316,6 @@ proc setOnHashChange*(action: proc (hashPart: cstring)) = redraw() onhashchange = wrapper -proc getAttr(e: Node; key: cstring): cstring {. - importcpp: "#.getAttribute(#)", nodecl.} - template nativeValue(ev): cstring = cast[Element](ev.target).value template setNativeValue(ev, val) = cast[Element](ev.target).value = val diff --git a/src/vdom.nim b/src/vdom.nim index 2670be5..1f55e61 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -1,37 +1,81 @@ -# Virtual DOM implementation +## Virtual DOM implementation for Karax. from dom import Event -import shash +import shash, macros +from strutils import toUpperAscii type VNodeKind* {.pure.} = enum - text, int, bool, vthunk, dthunk, - anchor, - tdiv, - table, tr, td, th, thead, tbody, - link, span, label, br, select, option, - fieldset, input, button, ul, li, section, header, footer, - h1, h2, h3, h4, h5, strong + text = "#text", int = "#int", bool = "#bool", + vthunk = "#vthunk", dthunk = "#dthunk", -const - toTag*: array[VNodeKind, cstring] = [ - cstring"#text", "#int", "#bool", "#vthunk", "#dthunk", - "A", "DIV", "TABLE", "TR", "TD", - "TH", "THEAD", - "TBODY", "LINK", - "SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL", - "LI", "SECTION", "HEADER", "FOOTER", "H1", "H2", "H3", "H4", "H5", "STRONG" - ] + html, head, title, base, link, meta, style, + script, noscript, + body, section, nav, article, aside, + h1, h2, h3, h4, h5, h6, + header, footer, address, main + + p, hr, pre, blockquote, ol, ul, li, + dl, dt, dd, + figure, figcaption, + + tdiv = "div", + + anchor, em, strong, small, + strikethrough = "s", cite, quote, + dfn, abbr, data, time, code, `var` = "var", samp, + kdb, sub, sup, italic = "i", bold = "b", underlined = "u", + mark, ruby, rt, rp, bdi, dbo, span, br, wbr, + ins, del, img, iframe, embed, `object` = "object", + param, video, audio, source, track, canvas, map, + area, svg, math, + + table, caption, colgroup, col, tbody, thead, + tfoot, tr, td, th, + + form, fieldset, legend, label, input, button, + select, datalist, optgroup, option, textarea, + keygen, output, progress, meter, + details, summary, command, menu type - EventKind* {.pure.} = enum - onclick, ondblclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll + EventKind* {.pure.} = enum ## The events supported by the virtual DOM. + onclick, ## An element is clicked. + ondblclick, ## An element is double clicked. + onkeyup, ## A key was released. + onkeydown, ## A key is pressed. + onkeypressed, # A key was pressed. + onblur, ## An element lost the focus. + onchange, ## The selected value of an element was changed. + onscroll, ## The user scrolled within an element. + ondrag, ## An element or text selection is being dragged (every 350ms). + ondragend, ## A drag operation is being ended (by releasing a mouse button + ## or hitting the escape key). + ondragenter, ## A dragged element or text selection enters a valid drop target. + ondragleave, ## A dragged element or text selection leaves a valid drop target. + ondragover, ## An element or text selection is being dragged over a valid + ## drop target (every 350ms). + ondragstart, ## The user starts dragging an element or text selection. + ondrop ## An element is dropped on a valid drop target. -const - toEventName*: array[EventKind, cstring] = [ - cstring"click", "dblclick", "keyup", "keydown", "keypressed", "blur", - "change", "scroll" - ] +macro buildLookupTables(): untyped = + var a = newTree(nnkBracket) + for i in low(VNodeKind)..high(VNodeKind): + let x = $i + let y = if x[0] == '#': x else: toUpperAscii(x) + a.add(newCall("cstring", newLit(y))) + var e = newTree(nnkBracket) + for i in low(EventKind)..high(EventKind): + e.add(newCall("cstring", newLit(substr($i, 2)))) + + template tmpl(a, e) {.dirty.} = + const + toTag*: array[VNodeKind, cstring] = a + toEventName*: array[EventKind, cstring] = e + + result = getAst tmpl(a, e) + +buildLookupTables() type EventHandler* = proc (ev: Event; target: VNode) {.closure.} From 74c5fb48ff20b470d23aabfb036009778ebd6853 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 19:39:00 +0200 Subject: [PATCH 004/264] added support for all HTML5 tags and drag&drop events --- src/karaxdsl.nim | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index dce3034..d4bc1fb 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -23,10 +23,6 @@ proc getName(n: NimNode): string = proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode = result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x) -proc isTag(s: string): bool = - for i in VNodeKind.low.succ..VNodeKind.high: - if $i == s: return true - proc tcall2(n, tmpContext: NimNode): NimNode = # we need to distinguish statement and expression contexts: # every call statement 's' needs to be transformed to 'dest.add s'. @@ -62,10 +58,9 @@ proc tcall2(n, tmpContext: NimNode): NimNode = of nnkCallKinds: let op = getName(n[0]) let ck = isComponent(op) - let isTag = isTag(op) - if isTag or ck != ComponentKind.None: + if ck != ComponentKind.None: let tmp = genSym(nskLet, "tmp") - let call = if isTag: + let call = if ck == ComponentKind.Tag: newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])) elif ck == ComponentKind.VNode: newCall(bindSym"vthunk", newLit(op)) @@ -85,7 +80,7 @@ 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 not isTag: + elif ck != ComponentKind.Tag: call.add x elif eqIdent(x, "setFocus"): result.add newCall(x, tmp) From 199408f160d298a265dbe5750fdf8bfb31aa07bb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 19:39:18 +0200 Subject: [PATCH 005/264] wrote more documentation --- docs.rst | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 docs.rst diff --git a/docs.rst b/docs.rst new file mode 100644 index 0000000..8298ba9 --- /dev/null +++ b/docs.rst @@ -0,0 +1,58 @@ + +Virtual DOM +=========== + +The virtual dom is in the ``vdom`` module. + +The ``VNodeKind`` enum describes every tag that the HTML 5 spec includes +and some extensions that allow for an efficient component system. + +These extensions are: + +``VNodeKind.int`` + The node havs a single integer field. + +``VNodeKind.bool`` + The node has a single boolean field. + +``VNodeKint.vthunk`` + The node is a `virtual thunk`:idx:. This means there is a + function attached to it that produces the ``VNode`` structure + on demand. + +``VNodeKint.dthunk`` + The node is a `DOM thunk`:idx:. This means there is a + function attached to it that produces the ``Node`` DOM structure + on demand. + +For convenience of the resulting Nim DSL these tags have enum names +that differ from their HTML equivalent: + +================= ======================================================= +Enum value HTML +================= ======================================================= +``tdiv`` ``div`` (changed because ``div`` is a keyword in Nim) +``italic`` ``i`` +``bold`` ``b`` +``strikethrough`` ``s`` +``underlined`` ``u`` +================= ======================================================= + + +A virtual dom node also has a special field set called ``key``, an integer +that can be used as a data model specific key/id. It can be accessed in event +handlers to change the data model. See the todoapp for an example of how to +use it. + +**Note**: This is not a hint for the DOM diff algorithm, multipe nodes can +all have the same key value. + + +Event system +============ + +Karax does not abstract over the event system the DOM offers much: The same +``Event`` structure is used. Every callback has the +signature ``proc (ev: Event; n: VNode)``. + + From 5d7166261712c0c590b519dddc413a599b598e07 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 20:33:03 +0200 Subject: [PATCH 006/264] fixed .component macro --- docs.rst => docs/docs.rst | 0 src/components.nim | 27 +++++++++++++++------------ src/karax.nim | 1 - src/karaxprelude.nim | 3 +++ 4 files changed, 18 insertions(+), 13 deletions(-) rename docs.rst => docs/docs.rst (100%) create mode 100644 src/karaxprelude.nim diff --git a/docs.rst b/docs/docs.rst similarity index 100% rename from docs.rst rename to docs/docs.rst diff --git a/src/components.nim b/src/components.nim index 30d5922..52775df 100644 --- a/src/components.nim +++ b/src/components.nim @@ -78,7 +78,9 @@ macro component*(prc: untyped): untyped = isvirtual = ComponentKind.Node else: error "component must return VNode or Node" - n[0] = ident("inner" & $name) + 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.. Date: Fri, 14 Apr 2017 20:49:42 +0200 Subject: [PATCH 007/264] fixed components; added playerapp example --- examples/mediaplayer/mediaplayer.nim | 42 ++++++++++++++++++++++++++++ examples/mediaplayer/playerapp.html | 18 ++++++++++++ examples/mediaplayer/playerapp.nim | 24 ++++++++++++++++ examples/mediaplayer/playerapp.nims | 1 + src/components.nim | 20 +++++++++---- 5 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 examples/mediaplayer/mediaplayer.nim create mode 100644 examples/mediaplayer/playerapp.html create mode 100644 examples/mediaplayer/playerapp.nim create mode 100644 examples/mediaplayer/playerapp.nims diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim new file mode 100644 index 0000000..d22e57d --- /dev/null +++ b/examples/mediaplayer/mediaplayer.nim @@ -0,0 +1,42 @@ + +import karax, karaxdsl, vdom, components + +const + Play = 0 + Big = 1 + Small = 2 + Normal = 3 + +proc paused(n: Node): bool {.importcpp: "#.paused".} +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 handler(ev: Event; n: VNode) = + let myVideo = document.getElementById(id) + case n.key + of Play: + if myVideo.paused: + myVideo.play() + else: + myVideo.pause() + of Big: myVideo.width = 560 + of Small: myVideo.width = 320 + of Normal: myVideo.width = 420 + else: discard + + result = buildHtml(tdiv): + button(onclick=handler, key=Play): + text "Play/Pause" + button(onclick=handler, key=Big): + text "Big" + button(onclick=handler, key=Small): + text "Small" + button(onclick=handler, key=Normal): + text "Normal" + br() + br() + video(id=id, width="420"): + source(src=resource, `type`="video/mp4"): + text "Your browser does not support HTML5 video." diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html new file mode 100644 index 0000000..7b617f6 --- /dev/null +++ b/examples/mediaplayer/playerapp.html @@ -0,0 +1,18 @@ + + + + + Player app + + + + +
+ + + + + + diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim new file mode 100644 index 0000000..7a1ddb4 --- /dev/null +++ b/examples/mediaplayer/playerapp.nim @@ -0,0 +1,24 @@ +## Example app that shows how to write and embed a custom component. + +include karaxprelude +import mediaplayer + +const url = "https://www.w3schools.com/html/mov_bbb.mp4" + +proc createDom(): VNode = + result = buildHtml(table): + tr: + td: + mplayer("vid1", url) + td: + mplayer("vid2", url) + tr: + td: + mplayer("vid3", url) + td: + mplayer("vid4", url) + +setRenderer createDom + +proc onload() {.exportc.} = + init() diff --git a/examples/mediaplayer/playerapp.nims b/examples/mediaplayer/playerapp.nims new file mode 100644 index 0000000..9028ff1 --- /dev/null +++ b/examples/mediaplayer/playerapp.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/src/components.nim b/src/components.nim index 52775df..fbbc00d 100644 --- a/src/components.nim +++ b/src/components.nim @@ -91,21 +91,29 @@ macro component*(prc: untyped): untyped = unpackCall.add unpack(typ, counter) inc counter - template vwrapper(pname, cname, nameStrLit, unpackCall) {.dirty.} = + template vwrapper(pname, unpackCall) {.dirty.} = proc pname(args: seq[VNode]): VNode = unpackCall - vcomponents[cstring(nameStrLit)] = cname - template dwrapper(pname, cname, nameStrLit, unpackCall) {.dirty.} = + template dwrapper(pname, unpackCall) {.dirty.} = proc pname(args: seq[VNode]): Node = unpackCall - dcomponents[cstring(nameStrLit)] = cname + + 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, realName, newLit(nn), unpackCall)) + result.add getAst(vwrapper(newname name, unpackCall)) + result.add getAst(vregister(newLit(nn), realName)) else: - result.add getAst(dwrapper(newname name, realName, newLit(nn), unpackCall)) + result.add getAst(dwrapper(newname name, unpackCall)) + result.add getAst(dregister(newLit(nn), realName)) allcomponents[nn] = isvirtual when defined(debugKaraxDsl): echo repr result From 3f14cdcf0d061aa80d7b15b42eec6b9bd9af4c56 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 20:51:37 +0200 Subject: [PATCH 008/264] added missing files --- experiments/oldwidgets.nim | 113 +++++++++++ experiments/scrollapp.nims | 1 + experiments/style.css | 378 +++++++++++++++++++++++++++++++++++++ 3 files changed, 492 insertions(+) create mode 100644 experiments/oldwidgets.nim create mode 100644 experiments/scrollapp.nims create mode 100644 experiments/style.css diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim new file mode 100644 index 0000000..46d87c2 --- /dev/null +++ b/experiments/oldwidgets.nim @@ -0,0 +1,113 @@ +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 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 ceil(f: float): int {.importc: "Math.ceil", nodecl.} + + +when false: + var plugins {.exportc.}: seq[(string, proc())] = @[] + + proc onInput(val: cstring) = + kout val + if val == "dyn": + kout(plugins.len) + if plugins.len > 0: + plugins[0][1]() + diff --git a/experiments/scrollapp.nims b/experiments/scrollapp.nims new file mode 100644 index 0000000..532b206 --- /dev/null +++ b/experiments/scrollapp.nims @@ -0,0 +1 @@ +--path: "../src" diff --git a/experiments/style.css b/experiments/style.css new file mode 100644 index 0000000..ceaf090 --- /dev/null +++ b/experiments/style.css @@ -0,0 +1,378 @@ +html, +body { + margin: 0; + padding: 0; +} + +.todomvc-wrapper { + visibility: visible !important; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #4d4d4d; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; + font-weight: 300; +} + +button, +input[type="checkbox"] { + outline: none; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 300; + color: #e6e6e6; +} + +.todoapp h1 { + position: absolute; + top: -155px; + width: 100%; + font-size: 100px; + font-weight: 100; + text-align: center; + color: rgba(175, 47, 47, 0.15); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + border: 0; + outline: none; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +.new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +label[for='toggle-all'] { + display: none; +} + +.toggle-all { + position: absolute; + top: -55px; + left: -12px; + width: 60px; + height: 34px; + text-align: center; + border: none; /* Mobile Safari */ +} + +.toggle-all:before { + content: '>'; + font-size: 22px; + color: #e6e6e6; + padding: 10px 27px 10px 27px; +} + +.toggle-all:checked:before { + color: #737373; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle:after { + content: url('data:image/svg+xml,'); +} + +.todo-list li .toggle:checked:after { + content: url('data:image/svg+xml,'); +} + +.todo-list li label { + white-space: pre-line; + word-break: break-all; + padding: 15px 60px 15px 15px; + margin-left: 45px; + display: block; + line-height: 1.2; + transition: color 0.4s; +} + +.todo-list li.completed label { + color: #d9d9d9; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #cc9a9a; + margin-bottom: 11px; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover { + color: #af5b5e; +} + +.todo-list li .destroy:after { + content: 'x'; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + color: #777; + padding: 10px 15px; + height: 20px; + text-align: center; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a.selected, +.filters li a:hover { + border-color: rgba(175, 47, 47, 0.1); +} + +.filters li a.selected { + border-color: rgba(175, 47, 47, 0.2); +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 20px; + text-decoration: none; + cursor: pointer; + position: relative; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #bfbfbf; + /* font-size: 10px; */ + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } + + .toggle-all { + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} \ No newline at end of file From 546162c3882e604956f494da1f6a4f8689fe4a4d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 14 Apr 2017 20:59:25 +0200 Subject: [PATCH 009/264] added what's left to do --- readme.rst | 11 +++++++++++ todo.rst | 6 ------ 2 files changed, 11 insertions(+), 6 deletions(-) delete mode 100644 todo.rst diff --git a/readme.rst b/readme.rst index 64582eb..daa9fef 100644 --- a/readme.rst +++ b/readme.rst @@ -11,6 +11,9 @@ To try it out, run:: nim js todoapp.nim open todoapp.html + cd ../mediaplayer + nim js playerapp.nim + open playerapp.html It uses a virtual DOM like React, but is much smaller than the existing frameworks plus of course it's written in Nim for Nim. No external @@ -24,3 +27,11 @@ Goals - Leverage Nim's macro system to produce a framework that allows for the development of applications that are boilerplate free. - Keep it small, keep it fast, keep it flexible. + + +Todo +==== + +- Expose a way to access the attached real DOM element so that scroll + positions etc can be used. +- Expose a nice API to do Ajax requests. diff --git a/todo.rst b/todo.rst deleted file mode 100644 index 7ddd5be..0000000 --- a/todo.rst +++ /dev/null @@ -1,6 +0,0 @@ -- Better virtual dom diff'ing algorithm: find longest common subsequence -- add CSS changes to todo example.nim -- handle onscroll events -- introduce a way to test karax applications -- introduce a way to do AJAX - From cfed5e05ec544e3cd4403836507b30d8c666eb74 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 11:17:12 +0200 Subject: [PATCH 010/264] added more event kinds --- src/vdom.nim | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/vdom.nim b/src/vdom.nim index 1f55e61..5e66a23 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -48,6 +48,20 @@ type onblur, ## An element lost the focus. onchange, ## The selected value of an element was changed. onscroll, ## The user scrolled within an element. + + onmousedown, ## A pointing device button (usually a mouse) is pressed + ## on an element. + onmouseenter, ## A pointing device is moved onto the element that + ## has the listener attached. + onmouseleave, ## A pointing device is moved off the element that + ## has the listener attached. + onmousemove, ## A pointing device is moved over an element. + onmouseout, ## A pointing device is moved off the element that + ## has the listener attached or off one of its children. + onmouseover, ## A pointing device is moved onto the element that has + ## the listener attached or onto one of its children. + onmouseup, ## A pointing device button is released over an element. + ondrag, ## An element or text selection is being dragged (every 350ms). ondragend, ## A drag operation is being ended (by releasing a mouse button ## or hitting the escape key). From 5bd43f13f94ec6c41e5d4013b284bddc364b87a7 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 11:18:15 +0200 Subject: [PATCH 011/264] added code snippet --- experiments/oldwidgets.nim | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim index 46d87c2..fce9908 100644 --- a/experiments/oldwidgets.nim +++ b/experiments/oldwidgets.nim @@ -111,3 +111,22 @@ when false: if plugins.len > 0: plugins[0][1]() +var + images: seq[cstring] = @[cstring"a", "b", "c", "d"] + +proc carousel*(): VNode = + var currentIndex = 0 + + proc next(ev: Event; n: VNode) = + currentIndex = (currentIndex + 1) mod images.len + + proc prev(ev: Event; n: VNode) = + currentIndex = (currentIndex - 1) mod images.len + + result = buildHtml(tdiv): + text images[currentIndex] + button(onclick = next): + text "Next" + button(onclick = prev): + text "Previous" + From d9bb8d99e8837371507b3ab39b0700e0f7a824ab Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 11:18:43 +0200 Subject: [PATCH 012/264] made readme.rst less confusing --- readme.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index daa9fef..8f45568 100644 --- a/readme.rst +++ b/readme.rst @@ -10,8 +10,9 @@ To try it out, run:: cd examples/todoapp nim js todoapp.nim open todoapp.html + cd ../.. - cd ../mediaplayer + cd examples/mediaplayer nim js playerapp.nim open playerapp.html From 5bc7d4e3369b093bfc66f68c3d8391ffcf0777af Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 11:50:05 +0200 Subject: [PATCH 013/264] removed the need to call karax.init explicitly --- examples/mediaplayer/playerapp.html | 3 --- examples/mediaplayer/playerapp.nim | 3 --- examples/todoapp/todoapp.html | 3 --- examples/todoapp/todoapp.nim | 9 +++------ src/karax.nim | 11 ++++++----- 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/examples/mediaplayer/playerapp.html b/examples/mediaplayer/playerapp.html index 7b617f6..9e076e6 100644 --- a/examples/mediaplayer/playerapp.html +++ b/examples/mediaplayer/playerapp.html @@ -10,9 +10,6 @@
- diff --git a/examples/mediaplayer/playerapp.nim b/examples/mediaplayer/playerapp.nim index 7a1ddb4..e611eec 100644 --- a/examples/mediaplayer/playerapp.nim +++ b/examples/mediaplayer/playerapp.nim @@ -19,6 +19,3 @@ proc createDom(): VNode = mplayer("vid4", url) setRenderer createDom - -proc onload() {.exportc.} = - init() diff --git a/examples/todoapp/todoapp.html b/examples/todoapp/todoapp.html index 726070a..488a3bf 100644 --- a/examples/todoapp/todoapp.html +++ b/examples/todoapp/todoapp.html @@ -11,9 +11,6 @@
- diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 23840f7..f2f0083 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -106,11 +106,8 @@ proc createDom(): VNode = text "Clear completed (" & &completedCount & ")" setOnHashChange(proc(hash: cstring) = - if hash == cstring"#/": filter = all - elif hash == cstring"#/completed": filter = completed - elif hash == cstring"#/active": filter = active + if hash == "#/": filter = all + elif hash == "#/completed": filter = completed + elif hash == "#/active": filter = active ) setRenderer createDom - -proc onload() {.exportc.} = - init() diff --git a/src/karax.nim b/src/karax.nim index 99e103b..00efc5e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -112,14 +112,11 @@ var dorender: proc (): VNode {.closure.} currentTree: VNode -proc setRenderer*(renderer: proc (): VNode) = - dorender = renderer - proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.} proc clearTimeout*(t: Timeout) {.importc, nodecl.} #proc targetElem*(e: Event): Element = cast[Element](e.target) -proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.} +proc getElementById(id: cstring): Element {.importc: "document.getElementById", nodecl.} #proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: # "document.getElementsByClassName", nodecl.} @@ -259,9 +256,13 @@ proc redraw*() = else: dodraw() -proc init*() = +proc init(ev: Event) = reqFrame(dodraw) +proc setRenderer*(renderer: proc (): VNode) = + dorender = renderer + window.onload = init + proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} From 686ad23f2f0f7311c2e4220339eb6f10afe25f6f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 15 Apr 2017 15:50:07 +0200 Subject: [PATCH 014/264] reworked event handling again; added scrollapp example --- examples/scrollapp/scrollapp.html | 15 ++ examples/scrollapp/scrollapp.nim | 24 +++ examples/scrollapp/scrollapp.nims | 1 + examples/todoapp/todoapp.nim | 12 +- experiments/oldwidgets.nim | 71 +++++++++ readme.rst | 2 - src/dom.nim | 110 ++++++++------ src/karax.nim | 235 ++++++++++-------------------- src/karaxdsl.nim | 3 +- src/vdom.nim | 12 +- tests/diffDomTests.nim | 12 +- tests/diffDomTests.nims | 1 + 12 files changed, 273 insertions(+), 225 deletions(-) create mode 100644 examples/scrollapp/scrollapp.html create mode 100644 examples/scrollapp/scrollapp.nim create mode 100644 examples/scrollapp/scrollapp.nims create mode 100644 tests/diffDomTests.nims diff --git a/examples/scrollapp/scrollapp.html b/examples/scrollapp/scrollapp.html new file mode 100644 index 0000000..1ff8b1b --- /dev/null +++ b/examples/scrollapp/scrollapp.html @@ -0,0 +1,15 @@ + + + + + Todo app + + + + +
+ + + + + diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim new file mode 100644 index 0000000..e5ef9c1 --- /dev/null +++ b/examples/scrollapp/scrollapp.nim @@ -0,0 +1,24 @@ +## Example that shows how to accomplish an "infinitely scrolling" app. + +include karaxprelude +import jstrutils, dom + +var entries: seq[cstring] = @[] +for i in 1..500: + entries.add(cstring("Entry ") & &i) + +proc scrollEvent(ev: Event; n: VNode) = + let d = n.dom + if d != nil and inViewport(d.lastChild): + # "load" more data: + for i in 1..50: + entries.add(cstring("Loaded Entry ") & &i) + +proc createDom(): VNode = + result = buildHtml(): + tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"): + for x in entries: + tdiv: + text x + +setRenderer createDom diff --git a/examples/scrollapp/scrollapp.nims b/examples/scrollapp/scrollapp.nims new file mode 100644 index 0000000..9028ff1 --- /dev/null +++ b/examples/scrollapp/scrollapp.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index f2f0083..9027603 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -59,8 +59,8 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode = button(class = "destroy", key=id, onclick=removeHandler) else: input(class = "edit", name = "title", key=id, - onfocusLost = focusLost, - onenter = editEntry, value = d, setFocus) + onblur = focusLost, + onkeyupenter = editEntry, value = d, setFocus) proc createDom(): VNode = result = buildHtml(tdiv(class="todomvc-wrapper")): @@ -69,7 +69,7 @@ proc createDom(): VNode = h1: text "todos" input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", - onenter = onTodoEnter, setFocus) + onkeyupenter = onTodoEnter, setFocus) section(class = "main"): input(class = "toggle-all", `type` = "checkbox", name = "toggle") label(`for` = "toggle-all", onclick = onAllDone): @@ -94,13 +94,13 @@ proc createDom(): VNode = text cstring" item" & &(if entriesCount != 1: "s left" else: " left") ul(class = "filters"): li: - anchor(class = selected(all), href = "#/"): + a(class = selected(all), href = "#/"): text "All" li: - anchor(class = selected(active), href = "#/active"): + a(class = selected(active), href = "#/active"): text "Active" li: - anchor(class = selected(completed), href = "#/completed"): + a(class = selected(completed), href = "#/completed"): text "Completed" button(class = "clear-completed", onclick = clearCompleted): text "Clear completed (" & &completedCount & ")" diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim index fce9908..023dc98 100644 --- a/experiments/oldwidgets.nim +++ b/experiments/oldwidgets.nim @@ -100,6 +100,50 @@ proc suffixAsInt*(s, prefix: cstring): int = parseInt(suffix(s, prefix)) #proc ceil(f: float): int {.importc: "Math.ceil", nodecl.} +proc realtimeInput*(val: cstring; action: EventHandler): VNode = + var timer: Timeout + proc onkeyup(ev: Event; n: VNode) = + proc wrapper() = keyeventBody() + + if timer != nil: clearTimeout(timer) + timer = setTimeout(wrapper, 400) + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + +proc enterInput*(id, val: cstring; action: EventHandler): VNode = + proc onkeyup(ev: Event; n: VNode) = + if ev.keyCode == 13: keyeventBody() + + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.id = id + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + +proc setOnEnter*(n: VNode; action: EventHandler) = + proc onkeyup(ev: Event; n: VNode) = + if ev.keyCode == 13: keyeventBody() + n.addEventListener(EventKind.onkeyup, onkeyup) + +proc setOnscroll*(action: proc(min, max: VKey; diff: int)) = + var oldY = window.pageYOffset + + proc wrapper(ev: Event) = + let dir = window.pageYOffset - oldY + if dir == 0: return + + var a = VKey high(int) + var b = VKey 0 + var h, count: int + document.visibleKeys(a, b, h, count) + let avgh = h / count + let diff = toInt(dir.float / avgh) + if diff != 0: + oldY = window.pageYOffset + action(a, b, diff) + redraw() + + document.addEventListener("scroll", wrapper) when false: var plugins {.exportc.}: seq[(string, proc())] = @[] @@ -130,3 +174,30 @@ proc carousel*(): VNode = button(onclick = prev): text "Previous" +#proc targetElem*(e: Event): Element = cast[Element](e.target) + +#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: +# "document.getElementsByClassName", nodecl.} +#proc textContent(e: Node): cstring {. +# importcpp: "#.textContent", nodecl.} + +proc isElementInViewport(el: Node; h: var int): bool = + let rect = el.getBoundingClientRect() + h = rect.bottom - rect.top + result = rect.top >= 0 and rect.left >= 0 and + rect.bottom <= clientHeight() and + rect.right <= clientWidth() + +proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = + # we only care about nodes that have a key: + var hh = 0 + # do not recurse if there is a 'key' field already: + if e.key >= 0: + if isElementInViewport(e, hh): + inc count + inc h, hh + a = min(a, e.key) + b = max(b, e.key) + else: + for i in 0..= 0 and rect.left >= 0 and + rect.bottom <= clientHeight() and + rect.right <= clientWidth() + +proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} +proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} +proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} diff --git a/src/karax.nim b/src/karax.nim index 00efc5e..a535ae7 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,28 +1,12 @@ -# Simple lib to write JS UIs +## Karax -- Single page applications for Nim. import dom, vdom, jstrutils, components, jdict -export dom.Event, dom.cloneNode, dom - -proc len(x: Node): int {.importcpp: "#.childNodes.length".} -proc `[]`(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".} +export dom.Event #, dom.cloneNode, dom proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. -proc id*(e: Node): cstring {.importcpp: "#.id", nodecl.} -proc `id=`*(e: Node; x: cstring) {.importcpp: "#.id = #", nodecl.} -proc class*(e: Node): cstring {.importcpp: "#.className", nodecl.} -proc `class=`*(e: Node; v: cstring) {.importcpp: "#.className = #", nodecl.} - -proc value*(e: Node): cstring {.importcpp: "#.value", nodecl.} -proc `value=`*(e: Node; v: cstring) {.importcpp: "#.value = #", nodecl.} - -proc `disabled=`*(e: Node; v: bool) {.importcpp: "#.disabled = #", nodecl.} - -proc getElementsByClass*(e: Node; name: cstring): seq[Node] {. - importcpp: "#.getElementsByClassName(#)", nodecl.} - proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".} proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.} proc key*(e: Node): VKey = @@ -30,26 +14,6 @@ proc key*(e: Node): VKey = else: result = -1 proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} -type - BoundingRect {.importc.} = object - top, bottom, left, right: int - -proc getBoundingClientRect(e: Node): BoundingRect {. - importcpp: "getBoundingClientRect", nodecl.} -proc clientHeight(): int {. - importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl} -proc clientWidth(): int {. - importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl} - -when false: - proc pageYOffset(): int {. - importcpp: "(window.pageYOffset)@", nodecl} - proc pageXOffset(): int {. - importcpp: "(window.pageXOffset)@", nodecl} - -type - Timeout* = ref object - var toFocus: Node toFocusV: VNode @@ -57,24 +21,69 @@ var proc setFocus*(n: VNode) = toFocusV = n -proc isElementInViewport(el: Node; h: var int): bool = - let rect = el.getBoundingClientRect() - h = rect.bottom - rect.top - result = rect.top >= 0 and rect.left >= 0 and - rect.bottom <= clientHeight() and - rect.right <= clientWidth() +# ----------------- event wrapping --------------------------------------- + +template nativeValue(ev): cstring = cast[Element](ev.target).value +template setNativeValue(ev, val) = cast[Element](ev.target).value = val + +template keyeventBody() = + n.value = nativeValue(ev) + action(ev, n) + setNativeValue(ev, n.value) + # Do not call redraw() here! That is already done + # by ``karax.addEventHandler``. + +proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = + proc stdWrapper(): (proc (ev: Event)) = + let action = action + let n = n + result = proc (ev: Event) = + action(ev, n) + + proc enterWrapper(): (proc (ev: Event)) = + let action = action + let n = n + result = proc (ev: Event) = + if ev.keyCode == 13: keyeventBody() + + proc laterWrapper(): (proc (ev: Event)) = + let action = action + let n = n + var timer: Timeout + result = proc (ev: Event) = + proc wrapper() = keyeventBody() + if timer != nil: clearTimeout(timer) + timer = setTimeout(wrapper, 400) + + case k + of EventKind.onkeyuplater: + d.addEventListener("keyup", laterWrapper()) + of EventKind.onkeyupenter: + d.addEventListener("keyup", enterWrapper()) + else: + d.addEventListener(toEventName[k], stdWrapper()) + +# --------------------- DOM diff ----------------------------------------- + +template detach(n: VNode) = n.dom = nil +template attach(n: Vnode) = n.dom = result proc vnodeToDom(n: VNode): Node = if n.kind == VNodeKind.text: result = document.createTextNode(n.text) + attach n elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) - return vnodeToDom(x) + result = vnodeToDom(x) + attach n + return result elif n.kind == VNodeKind.dthunk: - let x = callThunk(dcomponents[n.text], n) - return x + result = callThunk(dcomponents[n.text], n) + attach n + return result else: result = document.createElement(toTag[n.kind]) + attach n for k in n: appendChild(result, vnodeToDom(k)) # text is mapped to 'value': @@ -89,14 +98,8 @@ proc vnodeToDom(n: VNode): Node = for k, v in attrs(n): if v != nil: result.setAttr(k, v) - let myn = n for e, h in items(n.events): - proc wrapper(): proc (ev: Event) = - let hh = h - result = proc (ev: Event) = - assert myn != nil - hh(ev, myn) - result.addEventListener(toEventName[e], wrapper()) + wrapEvent(result, n, e, h) if n == toFocusV and toFocus.isNil: toFocus = result @@ -112,23 +115,12 @@ var dorender: proc (): VNode {.closure.} currentTree: VNode -proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.} -proc clearTimeout*(t: Timeout) {.importc, nodecl.} -#proc targetElem*(e: Event): Element = cast[Element](e.target) - -proc getElementById(id: cstring): Element {.importc: "document.getElementById", nodecl.} - -#proc getElementsByClassName*(cls: cstring): seq[Element] {.importc: -# "document.getElementsByClassName", nodecl.} -#proc textContent(e: Node): cstring {. -# importcpp: "#.textContent", nodecl.} - proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) #newTree.id = id -proc equals(a, b: VNode): bool = +proc equalsShallow(a, b: VNode): bool = if a.kind != b.kind: return false if a.id != b.id: return false if a.key != b.key: return false @@ -138,7 +130,7 @@ proc equals(a, b: VNode): bool = if a.text != b.text: return false if a.len != b.len: return false for i in 0.. newLength: for i in countdown(oldLength-1, newLength): + detach(oldNode[i]) current.removeChild(current.lastChild) else: var commonPrefix = 0 @@ -191,9 +185,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], - newNode[i], - oldNode[i]) + updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) var nextChildPos = oldPos + 1 while pos <= newPos: @@ -206,7 +198,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = inc pos inc nextChildPos - for i in 0..oldPos-pos: + for i in pos..oldPos: + detach(oldNode[i]) current.removeChild(current.childNodes[pos]) when false: @@ -229,20 +222,6 @@ proc dodraw() = if toFocus != nil: toFocus.focus() -proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = - # we only care about nodes that have a key: - var hh = 0 - # do not recurse if there is a 'key' field already: - if e.key >= 0: - if isElementInViewport(e, hh): - inc count - inc h, hh - a = min(a, e.key) - b = max(b, e.key) - else: - for i in 0.. Date: Sun, 16 Apr 2017 01:36:42 +0200 Subject: [PATCH 015/264] bugfix: virtual thunks need to have a key of -1 --- src/vdom.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vdom.nim b/src/vdom.nim index d138c7a..25f6c12 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -124,10 +124,10 @@ template callThunk*(fn: typed; n: VNode): untyped = fn(n.kids) proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.vthunk, text: name, kids: @args) + VNode(kind: VNodeKind.vthunk, text: name, key: -1, kids: @args) proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.dthunk, text: name, kids: @args) + VNode(kind: VNodeKind.dthunk, text: name, key: -1, kids: @args) proc eq*(a, b: VNode): bool = if a.kind != b.kind: return false From 0255f58964a7c2ccd8761991d1f938481d0b7e22 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 16 Apr 2017 09:51:10 +0200 Subject: [PATCH 016/264] wip: stateful components --- experiments/example.html | 4 -- experiments/example.nim | 148 ++++++--------------------------------- experiments/example.nims | 1 + src/components.nim | 47 +++++++++++++ src/karax.nim | 29 +++++++- 5 files changed, 99 insertions(+), 130 deletions(-) create mode 100644 experiments/example.nims diff --git a/experiments/example.html b/experiments/example.html index 35b9837..2303d95 100644 --- a/experiments/example.html +++ b/experiments/example.html @@ -3,7 +3,6 @@ Todo app - @@ -11,9 +10,6 @@
- diff --git a/experiments/example.nim b/experiments/example.nim index 0ea805e..78dcaae 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,135 +1,33 @@ -import vdom, karax, karaxdsl, jdict, jstrutils - -type - Filter = enum - all, active, completed +import vdom, components, karax, karaxdsl, jdict, jstrutils var - entries: seq[(cstring, bool)] - selectedEntry = -1 - filter: Filter + images: seq[cstring] = @[cstring"a", "b", "c", "d"] -proc onTodoEnter(ev: Event; n: VNode) = - entries.add((n.value, false)) - n.value = "" +proc carousel*(key: VKey): VNode {.component.} = + state: + var counter = 0 -proc removeHandler(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "remove:") - entries[id] = (cstring(nil), false) + proc next(ev: Event; n: VNode) = + counter = (counter + 1) mod images.len + markDirty key -proc editHandler(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "edit:") - selectedEntry = id - -when defined(usecache): - var entryCache = newJDict[int, VNode]() - -proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 - -proc editEntry(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "todo-edit:") - entries[id][0] = n.value - selectedEntry = -1 - -proc toggleEntry(ev: Event; n: VNode) = - let id = suffixAsInt(n.id, "toggle:") - entries[id][1] = not entries[id][1] - -proc onAllDone(ev: Event; n: VNode) = - entries = @[] - selectedEntry = -1 - -proc clearCompleted(ev: Event, n: VNode) = - for i in 0..= 0 and dirty.contains(&newNode.key): + dirty.del(&newNode.key) + dec dirtyCount + let n = vnodeToDom(newNode) + if parent == nil: + replaceById("ROOT", 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]) + # leave early if we know there cannot be anything left to do: + #if dirtyCount <= 0: return proc updateElement(parent, current: Node, newNode, oldNode: VNode) = if not equalsShallow(newNode, oldNode): @@ -217,6 +241,9 @@ proc dodraw() = let olddom = document.getElementById("ROOT") updateElement(nil, olddom, newtree, currentTree) #assert same(newtree, document.getElementById("ROOT")) + if dirtyCount > 0: + updateDirtyElements(nil, olddom, newtree) + dirtyCount = 0 currentTree = newtree # now that it's part of the DOM, give it the focus: if toFocus != nil: From f0e2d41fdee293a14b37b92fe1995ece556e6c32 Mon Sep 17 00:00:00 2001 From: Araq Date: Mon, 17 Apr 2017 09:39:13 +0200 Subject: [PATCH 017/264] next step to get stateful components --- experiments/example.nim | 1 - experiments/oldwidgets.nim | 48 ++++++++++++++++ src/components.nim | 111 ++++++++++++++++++++++++------------- src/karax.nim | 15 ++--- 4 files changed, 126 insertions(+), 49 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index 78dcaae..61235f8 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -10,7 +10,6 @@ proc carousel*(key: VKey): VNode {.component.} = proc next(ev: Event; n: VNode) = counter = (counter + 1) mod images.len - markDirty key result = buildHtml(tdiv(key=key)): text images[counter] diff --git a/experiments/oldwidgets.nim b/experiments/oldwidgets.nim index 023dc98..c947d7f 100644 --- a/experiments/oldwidgets.nim +++ b/experiments/oldwidgets.nim @@ -201,3 +201,51 @@ proc visibleKeys(e: Node; a, b: var VKey; h, count: var int) = else: for i in 0..= 0 and dirty.contains(&newNode.key): - dirty.del(&newNode.key) - dec dirtyCount + if newNode.key >= 0 and isDirty(newNode.key): + unmarkDirty(newNode.key) let n = vnodeToDom(newNode) if parent == nil: replaceById("ROOT", n) @@ -241,9 +234,9 @@ proc dodraw() = let olddom = document.getElementById("ROOT") updateElement(nil, olddom, newtree, currentTree) #assert same(newtree, document.getElementById("ROOT")) - if dirtyCount > 0: + if someDirty: updateDirtyElements(nil, olddom, newtree) - dirtyCount = 0 + someDirty = false currentTree = newtree # now that it's part of the DOM, give it the focus: if toFocus != nil: From c74b90876d10904ca95fdf738e38e510094c64d2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 18 Apr 2017 10:46:15 +0200 Subject: [PATCH 018/264] make playerapp compile again --- examples/mediaplayer/mediaplayer.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index d22e57d..18179c1 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, components +import karax, karaxdsl, vdom, dom, components const Play = 0 From 549a643fb9b1f40dda35991517f788776cd8d17a Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Wed, 19 Apr 2017 09:48:28 +0700 Subject: [PATCH 019/264] Update readme.rst --- readme.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.rst b/readme.rst index 791d68b..907f016 100644 --- a/readme.rst +++ b/readme.rst @@ -7,6 +7,8 @@ to change. To try it out, run:: + nimble install # For first time + cd examples/todoapp nim js todoapp.nim open todoapp.html From be4159fc117c0ac79d26570162665b3d9953bb96 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 19 Apr 2017 11:17:00 +0200 Subject: [PATCH 020/264] stateful components now work --- experiments/example.nim | 2 +- src/components.nim | 93 ++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 40 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index 61235f8..3fe4000 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -6,7 +6,7 @@ var proc carousel*(key: VKey): VNode {.component.} = state: - var counter = 0 + var counter: int = 0 proc next(ev: Event; n: VNode) = counter = (counter + 1) mod images.len diff --git a/src/components.nim b/src/components.nim index f86cd14..b93742d 100644 --- a/src/components.nim +++ b/src/components.nim @@ -4,8 +4,9 @@ import macros, jdict, dom, vdom, tables, strutils type StateDict*[V] = ref object + defaultValue*: V -proc `[]`*[V](d: StateDict[V], k: VKey): V {.importcpp: "#[#]".} +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(#)".} @@ -24,6 +25,10 @@ proc markDirty*(key: VKey) = 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) @@ -56,50 +61,62 @@ proc addTags() {.compileTime.} = static: addTags() -proc stateDecl(n: NimNode; names: TableRef[string, bool]) = + +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) + for x in n: stateDecl(x, names, decl) of nnkDo: - stateDecl(n.body, names) - else: discard - -proc accessesState(n: NimNode; names: TableRef[string, bool]): bool = - case n.kind - of nnkSym, nnkIdent: - result = $n in names - of nnkBracketExpr, nnkDotExpr: - result = accessesState(n[0], names) + stateDecl(n.body, names, decl) + of nnkCommentStmt: discard else: - for i in 0.. Date: Fri, 21 Apr 2017 12:49:37 +0200 Subject: [PATCH 021/264] attempt to optimize todoapp wiht .component annotation --- examples/todoapp/todoapp.nim | 54 ++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 9027603..7fd6104 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils +import vdom, karax, karaxdsl, jstrutils, components type Filter = enum @@ -47,7 +47,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 = +proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.component.} = result = buildHtml(tr): li(class=toClass(completed)): if not selected: @@ -62,14 +62,36 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode = onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus) +proc makeFooter(entriesCount, completedCount: int): VNode {.component.} = + result = buildHtml(footer(class = "footer")): + span(class = "todo-count"): + strong: + text(&entriesCount) + text cstring" item" & &(if entriesCount != 1: "s left" else: " left") + ul(class = "filters"): + li: + a(class = selected(all), href = "#/"): + text "All" + li: + a(class = selected(active), href = "#/active"): + text "Active" + li: + a(class = selected(completed), href = "#/completed"): + text "Completed" + button(class = "clear-completed", onclick = clearCompleted): + text "Clear completed (" & &completedCount & ")" + +proc makeHeader(): VNode {.component.} = + result = buildHtml(header(class = "header")): + h1: + text "todos" + input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", + onkeyupenter = onTodoEnter, setFocus) + proc createDom(): VNode = result = buildHtml(tdiv(class="todomvc-wrapper")): section(class = "todoapp"): - header(class = "header"): - h1: - text "todos" - input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", - onkeyupenter = onTodoEnter, setFocus) + makeHeader() section(class = "main"): input(class = "toggle-all", `type` = "checkbox", name = "toggle") label(`for` = "toggle-all", onclick = onAllDone): @@ -87,23 +109,7 @@ proc createDom(): VNode = createEntry(i, d[0], d[1], i == selectedEntry) inc completedCount, ord(d[1]) inc entriesCount - footer(class = "footer"): - span(class = "todo-count"): - strong: - text(&entriesCount) - text cstring" item" & &(if entriesCount != 1: "s left" else: " left") - ul(class = "filters"): - li: - a(class = selected(all), href = "#/"): - text "All" - li: - a(class = selected(active), href = "#/active"): - text "Active" - li: - a(class = selected(completed), href = "#/completed"): - text "Completed" - button(class = "clear-completed", onclick = clearCompleted): - text "Clear completed (" & &completedCount & ")" + makeFooter(entriesCount, completedCount) setOnHashChange(proc(hash: cstring) = if hash == "#/": filter = all From 371ae67c6480e1a694e6fd82e657f4028a53dea0 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 21 Apr 2017 18:15:12 +0200 Subject: [PATCH 022/264] fixes a bug in the components DSL tranformation --- src/components.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components.nim b/src/components.nim index b93742d..c3cda9c 100644 --- a/src/components.nim +++ b/src/components.nim @@ -102,7 +102,7 @@ proc doState(n: NimNode; names: TableRef[string, bool]; # handle 'state' declaration and remove it from the AST: if n.len == 2 and repr(n[0]) == "state": stateDecl(n[1], names, decl) - return nil #newTree(nnkEmpty) + return newTree(nnkNone) of nnkSym, nnkIdent: let v = $n if v in names: @@ -112,7 +112,7 @@ proc doState(n: NimNode; names: TableRef[string, bool]; result = copyNimNode(n) for i in 0.. Date: Fri, 21 Apr 2017 18:15:50 +0200 Subject: [PATCH 023/264] experiments: added an animation to the example --- experiments/example.nim | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index 3fe4000..229cce6 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,5 +1,5 @@ -import vdom, components, karax, karaxdsl, jdict, jstrutils +import vdom, components, karax, karaxdsl, jdict, jstrutils, dom var images: seq[cstring] = @[cstring"a", "b", "c", "d"] @@ -7,14 +7,29 @@ var proc carousel*(key: VKey): VNode {.component.} = state: var counter: int = 0 + var cntdown: int = 5 + var timer: Timeout = nil + + proc docount() = + cntdown = cntdown - 1 + if cntdown == 0: + counter = (counter + 1) mod images.len + cntdown = 5 + else: + timer = setTimeout(docount, 30) + redraw() proc next(ev: Event; n: VNode) = - counter = (counter + 1) mod images.len + if timer != nil: + clearTimeout(timer) + timer = setTimeout(docount, 30) result = buildHtml(tdiv(key=key)): text images[counter] button(onclick = next): text "Next" + if cntdown != 5: + text &cntdown proc createDom(): VNode = result = buildHtml(table): From 27cacbd2715b1d1f7287f89e789793da546a9623 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 21 Apr 2017 18:30:02 +0200 Subject: [PATCH 024/264] moved ajax support into its own module and make the callback call 'redraw' automatically --- src/ajax.nim | 41 +++++++++++++++++++++++++++++++++++++++++ src/karax.nim | 33 +-------------------------------- 2 files changed, 42 insertions(+), 32 deletions(-) create mode 100644 src/ajax.nim diff --git a/src/ajax.nim b/src/ajax.nim new file mode 100644 index 0000000..05e9740 --- /dev/null +++ b/src/ajax.nim @@ -0,0 +1,41 @@ +## Karax -- Single page applications for Nim. + +## This module implements support for `ajax`:idx: socket +## handling. + +import karax + +proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int; response: cstring)) = + proc contWrapper(httpStatus: int; response: cstring) = + cont(httpStatus, response) + redraw() + + proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".} + {.emit: """ + var ajax = new XMLHttpRequest(); + ajax.open(`meth`,`url`,true);""".} + for a, b in items(headers): + setRequestHeader(a, b) + {.emit: """ + ajax.onreadystatechange = function(){ + if(this.readyState == 4){ + if(this.status == 200){ + `contWrapper`(this.status, this.responseText); + } else { + `contWrapper`(this.status, this.statusText); + } + } + } + ajax.send(`data`); + """.} + +proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)]; + data: cstring; + cont: proc (httpStatus: int, response: cstring)) = + ajax("PUT", url, headers, data, cont) + +proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)]; + cont: proc (httpStatus: int, response: cstring)) = + ajax("GET", url, headers, nil, cont) diff --git a/src/karax.nim b/src/karax.nim index 1908d6f..7d7267c 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -2,7 +2,7 @@ import dom, vdom, jstrutils, components, jdict -export dom.Event #, dom.cloneNode, dom +export dom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. @@ -281,37 +281,6 @@ proc setOnHashChange*(action: proc (hashPart: cstring)) = redraw() onhashchange = wrapper -proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; - data: cstring; - cont: proc (httpStatus: int; response: cstring)) = - proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".} - {.emit: """ - var ajax = new XMLHttpRequest(); - ajax.open(`meth`,`url`,true);""".} - for a, b in items(headers): - setRequestHeader(a, b) - {.emit: """ - ajax.onreadystatechange = function(){ - if(this.readyState == 4){ - if(this.status == 200){ - `cont`(this.status, this.responseText); - } else { - `cont`(this.status, this.statusText); - } - } - } - ajax.send(`data`); - """.} - -proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)]; - data: cstring; - cont: proc (httpStatus: int, response: cstring)) = - ajax("PUT", url, headers, data, cont) - -proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)]; - cont: proc (httpStatus: int, response: cstring)) = - ajax("GET", url, headers, nil, cont) - {.push stackTrace:off.} proc setupErrorHandler*(useAlert=false) = From 7d3224183a5df50a2e1dd7eb91b68435341e5fca Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 21 Apr 2017 18:58:03 +0200 Subject: [PATCH 025/264] completed first version of the Ajax API --- readme.rst | 6 ------ src/ajax.nim | 7 +++++-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/readme.rst b/readme.rst index 907f016..abecc8b 100644 --- a/readme.rst +++ b/readme.rst @@ -30,9 +30,3 @@ Goals - Leverage Nim's macro system to produce a framework that allows for the development of applications that are boilerplate free. - Keep it small, keep it fast, keep it flexible. - - -Todo -==== - -- Expose a nice API to do Ajax requests. diff --git a/src/ajax.nim b/src/ajax.nim index 05e9740..d002380 100644 --- a/src/ajax.nim +++ b/src/ajax.nim @@ -31,11 +31,14 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; ajax.send(`data`); """.} -proc ajaxPut*(url: string; headers: openarray[(cstring, cstring)]; +proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; cont: proc (httpStatus: int, response: cstring)) = ajax("PUT", url, headers, data, cont) -proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)]; +proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; cont: proc (httpStatus: int, response: cstring)) = ajax("GET", url, headers, nil, cont) + +proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} +proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} From 32384224125606671cdb14b2b8faf5d53451dbaf Mon Sep 17 00:00:00 2001 From: Roman Vishnevskii Date: Sat, 22 Apr 2017 13:01:05 +0700 Subject: [PATCH 026/264] Add local storage for todoapp --- examples/todoapp/todoapp.nim | 118 ++++++++++++++++++++++++----------- 1 file changed, 80 insertions(+), 38 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 9027603..051b6ed 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,21 +1,52 @@ -import vdom, karax, karaxdsl, jstrutils +import vdom, karax, karaxdsl, jstrutils, components type Filter = enum all, active, completed var - entries: seq[(cstring, bool)] selectedEntry = -1 filter: Filter + entriesSize: int + +proc getItem(key : cstring ): cstring {.importc: "localStorage.getItem"} +proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"} +proc clearEntries() {.importc: "localStorage.clear"} + +proc getEntryCstring(pos : int): cstring = + result = getItem(cstring($pos & "cstring")) + if result == cstring"null": + result = nil + +proc getEntryBool(pos : int): bool = + var value = getItem(cstring($pos & "bool")) + result = value == cstring"true" + +proc setEntryCstring(pos : int, value : cstring) = + setItem(cstring($pos & "cstring"), value) + +proc setEntryBool(pos : int, value : bool) = + var val = cstring"true" + if not value: + val = cstring"false" + setItem(cstring($pos & "bool"), val) + +proc addEntry(str : cstring, bval : bool) = + setEntryCstring(entriesSize, str) + setEntryBool(entriesSize, bval) + inc entriesSize + +proc setEntry(pos : int, str : cstring, bval : bool) = + setEntryCstring(pos, str) + setEntryBool(pos, bval) proc onTodoEnter(ev: Event; n: VNode) = - entries.add((n.value, false)) + addEntry(n.value, false) n.value = "" proc removeHandler(ev: Event; n: VNode) = - entries[n.key] = (cstring(nil), false) + setEntry(n.key, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = selectedEntry = n.key @@ -23,20 +54,20 @@ proc editHandler(ev: Event; n: VNode) = proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - entries[n.key][0] = n.value + setEntryCstring(n.key, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = let id = n.key - entries[id][1] = not entries[id][1] + setEntryBool(id, not getEntryBool(id)) proc onAllDone(ev: Event; n: VNode) = - entries = @[] + clearEntries() selectedEntry = -1 proc clearCompleted(ev: Event, n: VNode) = - for i in 0.. Date: Sat, 22 Apr 2017 13:29:24 +0700 Subject: [PATCH 027/264] Codestyle fixes --- examples/todoapp/todoapp.nim | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 051b6ed..81d03f9 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -8,36 +8,36 @@ type var selectedEntry = -1 filter: Filter - entriesSize: int + entriesLen: int proc getItem(key : cstring ): cstring {.importc: "localStorage.getItem"} proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"} proc clearEntries() {.importc: "localStorage.clear"} -proc getEntryCstring(pos : int): cstring = - result = getItem(cstring($pos & "cstring")) - if result == cstring"null": - result = nil +proc getEntryCstring(pos: int): cstring = + result = getItem(cstring($pos & "cstring")) + if result == cstring"null": + result = nil -proc getEntryBool(pos : int): bool = - var value = getItem(cstring($pos & "bool")) - result = value == cstring"true" +proc getEntryBool(pos: int): bool = + var value = getItem(cstring($pos & "bool")) + result = value == cstring"true" -proc setEntryCstring(pos : int, value : cstring) = - setItem(cstring($pos & "cstring"), value) +proc setEntryCstring(pos: int, value: cstring) = + setItem(cstring($pos & "cstring"), value) -proc setEntryBool(pos : int, value : bool) = - var val = cstring"true" - if not value: - val = cstring"false" - setItem(cstring($pos & "bool"), val) +proc setEntryBool(pos: int, value : bool) = + var val = cstring"true" + if not value: + val = cstring"false" + setItem(cstring($pos & "bool"), val) -proc addEntry(str : cstring, bval : bool) = - setEntryCstring(entriesSize, str) - setEntryBool(entriesSize, bval) - inc entriesSize +proc addEntry(str: cstring, bval : bool) = + setEntryCstring(entriesLen, str) + setEntryBool(entriesLen, bval) + inc entriesLen -proc setEntry(pos : int, str : cstring, bval : bool) = +proc setEntry(pos: int, str: cstring, bval: bool) = setEntryCstring(pos, str) setEntryBool(pos, bval) @@ -66,7 +66,7 @@ proc onAllDone(ev: Event; n: VNode) = selectedEntry = -1 proc clearCompleted(ev: Event, n: VNode) = - for i in 0.. Date: Sat, 22 Apr 2017 13:33:20 +0700 Subject: [PATCH 028/264] Codestyle fixes --- examples/todoapp/todoapp.nim | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 81d03f9..1ba06a6 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -15,18 +15,18 @@ proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"} proc clearEntries() {.importc: "localStorage.clear"} proc getEntryCstring(pos: int): cstring = - result = getItem(cstring($pos & "cstring")) + result = getItem(&pos & cstring"cstring") if result == cstring"null": result = nil -proc getEntryBool(pos: int): bool = +proc isCompleted(pos: int): bool = var value = getItem(cstring($pos & "bool")) result = value == cstring"true" proc setEntryCstring(pos: int, value: cstring) = setItem(cstring($pos & "cstring"), value) -proc setEntryBool(pos: int, value : bool) = +proc markAsCompleted(pos: int, value : bool) = var val = cstring"true" if not value: val = cstring"false" @@ -34,19 +34,19 @@ proc setEntryBool(pos: int, value : bool) = proc addEntry(str: cstring, bval : bool) = setEntryCstring(entriesLen, str) - setEntryBool(entriesLen, bval) + markAsCompleted(entriesLen, bval) inc entriesLen -proc setEntry(pos: int, str: cstring, bval: bool) = +proc updateEntry(pos: int, str: cstring, bval: bool) = setEntryCstring(pos, str) - setEntryBool(pos, bval) + markAsCompleted(pos, bval) proc onTodoEnter(ev: Event; n: VNode) = addEntry(n.value, false) n.value = "" proc removeHandler(ev: Event; n: VNode) = - setEntry(n.key, cstring(nil), false) + updateEntry(n.key, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = selectedEntry = n.key @@ -59,7 +59,7 @@ proc editEntry(ev: Event; n: VNode) = proc toggleEntry(ev: Event; n: VNode) = let id = n.key - setEntryBool(id, not getEntryBool(id)) + markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = clearEntries() @@ -67,7 +67,7 @@ proc onAllDone(ev: Event; n: VNode) = proc clearCompleted(ev: Event, n: VNode) = for i in 0.. Date: Sat, 22 Apr 2017 10:45:42 +0200 Subject: [PATCH 029/264] todoapp: use local storage properly --- examples/todoapp/todoapp.nim | 54 +++++++++++++++++++----------------- src/jstrutils.nim | 2 ++ src/localstorage.nim | 10 +++++++ 3 files changed, 40 insertions(+), 26 deletions(-) create mode 100644 src/localstorage.nim diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 1ba06a6..802bfc9 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -1,5 +1,5 @@ -import vdom, karax, karaxdsl, jstrutils, components +import vdom, karax, karaxdsl, jstrutils, components, localstorage type Filter = enum @@ -10,36 +10,35 @@ var filter: Filter entriesLen: int -proc getItem(key : cstring ): cstring {.importc: "localStorage.getItem"} -proc setItem(key, value : cstring ) {.importc: "localStorage.setItem"} -proc clearEntries() {.importc: "localStorage.clear"} +const + contentSuffix = cstring"content" + completedSuffix = cstring"completed" + lenSuffix = cstring"entriesLen" -proc getEntryCstring(pos: int): cstring = - result = getItem(&pos & cstring"cstring") +proc getEntryContent(pos: int): cstring = + result = getItem(&pos & contentSuffix) if result == cstring"null": result = nil proc isCompleted(pos: int): bool = - var value = getItem(cstring($pos & "bool")) + var value = getItem(&pos & completedSuffix) result = value == cstring"true" -proc setEntryCstring(pos: int, value: cstring) = - setItem(cstring($pos & "cstring"), value) +proc setEntryContent(pos: int, content: cstring) = + setItem(&pos & contentSuffix, content) -proc markAsCompleted(pos: int, value : bool) = - var val = cstring"true" - if not value: - val = cstring"false" - setItem(cstring($pos & "bool"), val) +proc markAsCompleted(pos: int, completed: bool) = + setItem(&pos & completedSuffix, &completed) -proc addEntry(str: cstring, bval : bool) = - setEntryCstring(entriesLen, str) - markAsCompleted(entriesLen, bval) +proc addEntry(content: cstring, completed: bool) = + setEntryContent(entriesLen, content) + markAsCompleted(entriesLen, completed) inc entriesLen + setItem(lenSuffix, &entriesLen) -proc updateEntry(pos: int, str: cstring, bval: bool) = - setEntryCstring(pos, str) - markAsCompleted(pos, bval) +proc updateEntry(pos: int, content: cstring, completed: bool) = + setEntryContent(pos, content) + markAsCompleted(pos, completed) proc onTodoEnter(ev: Event; n: VNode) = addEntry(n.value, false) @@ -54,7 +53,7 @@ proc editHandler(ev: Event; n: VNode) = proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - setEntryCstring(n.key, n.value) + setEntryContent(n.key, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = @@ -62,12 +61,12 @@ proc toggleEntry(ev: Event; n: VNode) = markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = - clearEntries() + clear() selectedEntry = -1 proc clearCompleted(ev: Event, n: VNode) = for i in 0.. Date: Sat, 22 Apr 2017 10:55:28 +0200 Subject: [PATCH 030/264] add benchmark result --- docs/benchmark.png | Bin 0 -> 30568 bytes readme.rst | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 docs/benchmark.png diff --git a/docs/benchmark.png b/docs/benchmark.png new file mode 100644 index 0000000000000000000000000000000000000000..3949ad5830ea2f655fdecceec5d8d273cc10769f GIT binary patch literal 30568 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 BUxWYv literal 0 HcmV?d00001 diff --git a/readme.rst b/readme.rst index abecc8b..0371c7c 100644 --- a/readme.rst +++ b/readme.rst @@ -21,7 +21,10 @@ To try it out, run:: It uses a virtual DOM like React, but is much smaller than the existing frameworks plus of course it's written in Nim for Nim. No external dependencies! And thanks to Nim's whole program optimization only what -is used ends up in the generated JavaScript code. +is used ends up in the generated JavaScript code. But don't take my +word for it, look at this benchmark result: + +.. image:: docs/benchmark.png Goals From b1e60fbd21f5480b7e7bcfe1a915e214836a2b95 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 22 Apr 2017 10:56:14 +0200 Subject: [PATCH 031/264] readme.rst: changed the wording slightly --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 0371c7c..4aafac8 100644 --- a/readme.rst +++ b/readme.rst @@ -22,7 +22,7 @@ It uses a virtual DOM like React, but is much smaller than the existing frameworks plus of course it's written in Nim for Nim. No external dependencies! And thanks to Nim's whole program optimization only what is used ends up in the generated JavaScript code. But don't take my -word for it, look at this benchmark result: +word for it, look at this: .. image:: docs/benchmark.png From 30491d401a711bbe253c84890cdf35e6f4df8723 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 24 Apr 2017 16:27:20 +0200 Subject: [PATCH 032/264] update karax.nimble file --- karax.nimble | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/karax.nimble b/karax.nimble index be4879f..103da81 100644 --- a/karax.nimble +++ b/karax.nimble @@ -1,9 +1,10 @@ # Package version = "0.1.0" -author = "Araq" +author = "Xored Software, Inc." description = "Karax is a framework for developing single page applications in Nim." license = "MIT" +srcDir = "src" # Dependencies From cf9b22ae62f8c99f2922a3aa4841398b37189eae Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 24 Apr 2017 17:45:40 +0200 Subject: [PATCH 033/264] bugfix: make ajax module work --- src/ajax.nim | 40 +++++++++++++++++++++++----------------- src/karax.nim | 1 + 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/ajax.nim b/src/ajax.nim index d002380..9a345e6 100644 --- a/src/ajax.nim +++ b/src/ajax.nim @@ -12,24 +12,30 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; cont(httpStatus, response) redraw() - proc setRequestHeader(a, b: cstring) {.importc: "ajax.setRequestHeader".} - {.emit: """ - var ajax = new XMLHttpRequest(); - ajax.open(`meth`,`url`,true);""".} + type + HttpRequest {.importc.} = ref object + ThisObj {.importc.} = ref object + readyState, status: int + responseText, statusText: cstring + + proc setRequestHeader(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".} + proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".} + proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".} + proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".} + proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)",} + + var this {.importc: "this".}: ThisObj + let ajax = newRequest() + ajax.open(meth, url, true) for a, b in items(headers): - setRequestHeader(a, b) - {.emit: """ - ajax.onreadystatechange = function(){ - if(this.readyState == 4){ - if(this.status == 200){ - `contWrapper`(this.status, this.responseText); - } else { - `contWrapper`(this.status, this.statusText); - } - } - } - ajax.send(`data`); - """.} + ajax.setRequestHeader(a, b) + ajax.statechange proc() = + if this.readyState == 4: + if this.status == 200: + contWrapper(this.status, this.responseText) + else: + contWrapper(this.status, this.statusText) + ajax.send(data) proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; diff --git a/src/karax.nim b/src/karax.nim index 7d7267c..892f6d2 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -223,6 +223,7 @@ when false: var drawTimeout: Timeout proc dodraw() = + if dorender.isNil: return let newtree = dorender() newtree.id = "ROOT" toFocus = nil From 23f00055a97a4a4fd3e381eb556bd2f4ce3bda28 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 24 Apr 2017 17:46:37 +0200 Subject: [PATCH 034/264] renamed ajax.nim to kajax.nim --- experiments/ajaxtest.html | 13 +++++++++++++ experiments/ajaxtest.nim | 6 ++++++ src/{ajax.nim => kajax.nim} | 0 3 files changed, 19 insertions(+) create mode 100644 experiments/ajaxtest.html create mode 100644 experiments/ajaxtest.nim rename src/{ajax.nim => kajax.nim} (100%) diff --git a/experiments/ajaxtest.html b/experiments/ajaxtest.html new file mode 100644 index 0000000..39062ad --- /dev/null +++ b/experiments/ajaxtest.html @@ -0,0 +1,13 @@ + + + + + Karax AJAX Test + + + + +
+ + + diff --git a/experiments/ajaxtest.nim b/experiments/ajaxtest.nim new file mode 100644 index 0000000..7af60cd --- /dev/null +++ b/experiments/ajaxtest.nim @@ -0,0 +1,6 @@ +import dom, kajax + +proc cb(httpStatus: int, response: cstring) = + echo "Worked!" + +ajaxGet("https://httpbin.org/get", @[], cb) diff --git a/src/ajax.nim b/src/kajax.nim similarity index 100% rename from src/ajax.nim rename to src/kajax.nim From 8666d8d6d6e67e5e82ed61d1e84c95ab17e29cc5 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 24 Apr 2017 17:52:29 +0200 Subject: [PATCH 035/264] kajax: PUT should have been POST --- src/kajax.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/kajax.nim b/src/kajax.nim index 9a345e6..be230f0 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -37,10 +37,10 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; contWrapper(this.status, this.statusText) ajax.send(data) -proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)]; +proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; cont: proc (httpStatus: int, response: cstring)) = - ajax("PUT", url, headers, data, cont) + ajax("POST", url, headers, data, cont) proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; cont: proc (httpStatus: int, response: cstring)) = From 2d1e6560616d0d23ac9e8addcb396d6b4b3c2614 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 26 Apr 2017 14:46:01 +0200 Subject: [PATCH 036/264] better setupErrorHandler --- src/karax.nim | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 892f6d2..ed956d8 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -284,23 +284,17 @@ proc setOnHashChange*(action: proc (hashPart: cstring)) = {.push stackTrace:off.} -proc setupErrorHandler*(useAlert=false) = +proc setupErrorHandler*() = ## Installs an error handler that transforms native JS unhandled ## exceptions into Nim based stack traces. If `useAlert` is false, ## the error message is put into the console, otherwise `alert` ## is called. proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) - {.emit: """ - window.onerror = function(msg, url, line, col, error) { - var x = "Error: " + msg + "\n" + `stackTraceAsCstring`() - if (`useAlert`) - alert(x); - else - console.log(x); - var suppressErrorAlert = true; - return suppressErrorAlert; - };""".} - + var onerror {.importc: "window.onerror".} = + proc (msg, url: cstring, line, col: int, error: cstring): bool = + var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() + kout(x) + return true # suppressErrorAlert {.pop.} proc prepend(parent, kid: Element) = From 21c911ffd363cb01604902064dd265dabc9b821f Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 26 Apr 2017 14:51:09 +0200 Subject: [PATCH 037/264] added karaxdsl.flatHtml macro --- src/karaxdsl.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index ade1c24..5ba525a 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -117,3 +117,8 @@ macro buildHtml*(children: untyped): VNode = result = tcall2(body(children), nil) when defined(debugKaraxDsl): echo repr result + +macro flatHtml*(tag: untyped): VNode = + result = tcall2(tag, nil) + when defined(debugKaraxDsl): + echo repr result From 63c7cbc43bd35bea406478f0f38f63a16c6df59b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 12:57:53 +0200 Subject: [PATCH 038/264] test_diagram.xml --- experiments/Untitled Diagram.xml | 1 + 1 file changed, 1 insertion(+) create mode 100644 experiments/Untitled Diagram.xml diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml new file mode 100644 index 0000000..fd1e674 --- /dev/null +++ b/experiments/Untitled Diagram.xml @@ -0,0 +1 @@ +UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== \ No newline at end of file From 521ef9baecb61d1dce3b54f0e3d2d37bf664624d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:01:50 +0200 Subject: [PATCH 039/264] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index fd1e674..6f1b80e 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -UzV2zq1wL0osyPDNT0nNUTV2VTV2LsrPL4GwciucU3NyVI0MMlNUjV1UjYwMgFjVyA2HrCFY1qAgsSg1rwSLBiADYTaQg2Y1AA== \ No newline at end of file +1VZNj5swEP01HLsCO4Hk2jRtD61UKYd2j16YgLuGQY4JpL++Bg8Bl+6qatVIySHyvBnPx3vWJAHfld0HLeriM2agAhZmXcDfBYwl68h+98DFAavtxgG5lpmDogk4yB9AYEhoIzM4eYEGURlZ+2CKVQWp8TChNbZ+2BGVX7UWOSyAQyrUEv0qM1M4dMPiCf8IMi/GylG8dZ4nkT7nGpuK6gWMH4ePc5dizEWDngqRYTuD+D7gO41o3KnsdqB6akfa3L33L3ivfWuozJ9cSKgNcxlHh8wyQSZqU2COlVD7CX07jAd9gtBahSmVPUb2CJ0033r4ga3JfBxdldEX51uP5iNl+A7GXOgNiMaghabCnxBrynGUSu1QoR4atbxCnKYWPxmNzzDzZMn2Kewzu+H6iV6kh6ATNjqlKEYPTugcKIpfpbEvHrAE274N0aCEkWc/u6C3l1/jJv7tgST4vRyb/yNH8oocK+YJ8iZ8CKNkRL6AlnYI0Pcj1epGUlHps1DNL8trpl5bSAOHWgz9tnZV+gr9LU1n0Aa614laUjBe4NQn7eYtme206KJxPRWzJReH/04aX5DG74Kz2Kfsur5vwdlq+dDYXZDG1rdjzZrTj+bgm/0x4fuf \ No newline at end of file From b44f699dd017d36c7bf2b24317e1611c9e2f6e6e Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:09:27 +0200 Subject: [PATCH 040/264] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index 6f1b80e..ad93172 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -1VZNj5swEP01HLsCO4Hk2jRtD61UKYd2j16YgLuGQY4JpL++Bg8Bl+6qatVIySHyvBnPx3vWJAHfld0HLeriM2agAhZmXcDfBYwl68h+98DFAavtxgG5lpmDogk4yB9AYEhoIzM4eYEGURlZ+2CKVQWp8TChNbZ+2BGVX7UWOSyAQyrUEv0qM1M4dMPiCf8IMi/GylG8dZ4nkT7nGpuK6gWMH4ePc5dizEWDngqRYTuD+D7gO41o3KnsdqB6akfa3L33L3ivfWuozJ9cSKgNcxlHh8wyQSZqU2COlVD7CX07jAd9gtBahSmVPUb2CJ0033r4ga3JfBxdldEX51uP5iNl+A7GXOgNiMaghabCnxBrynGUSu1QoR4atbxCnKYWPxmNzzDzZMn2Kewzu+H6iV6kh6ATNjqlKEYPTugcKIpfpbEvHrAE274N0aCEkWc/u6C3l1/jJv7tgST4vRyb/yNH8oocK+YJ8iZ8CKNkRL6AlnYI0Pcj1epGUlHps1DNL8trpl5bSAOHWgz9tnZV+gr9LU1n0Aa614laUjBe4NQn7eYtme206KJxPRWzJReH/04aX5DG74Kz2Kfsur5vwdlq+dDYXZDG1rdjzZrTj+bgm/0x4fuf \ No newline at end of file +1VdNc5swEP01HJtBEgZzresmh3amMz40OSqwBjUy65Hlr/76ChAGFdvjOLE95sBIb1cf+/ZpER4bzTaPis/zn5iC9Kifbjz2zaM0GhDzLoFtDQTxsAYyJdIaIi0wEX/Bgr5FlyKFheOoEaUWcxdMsCgg0Q7GlcK16zZF6a465xn0gEnCZR/9LVKd1+iQhi3+BCLLm5VJGNeWV568ZQqXhV3Po2xaPbV5xpu5bKCLnKe47kBs7LGRQtR1a7YZgSypbWirx30/YN3tW0GhTxkQ2W3obRM6pIYJ20Wlc8yw4HLcol+r8KCcwDe9XM+kaRLThI3QzyX8QAe2+9KYCq22tW3QdF/sDH9A663VAF9qNFC78A/EuZ1jKqQcoURVbdTwCmGSGHyhFb5Bx5JG8atfzlwHV0Z0kB4LLXCpEutFreC4ysB6sV1qjOIBZ2C2b1wUSK7Fyp2dW+1lO7+Wf9OwKdifjuFl0hEdSUdAnYR88R98EjXIL1DCBAHqflIVXClVdukVl8v/ilcne+tcaJjMebXftSmVbobOpWkFSsPmOFF9CpoBzO7T1ubYdtdtoSNNeco7RS70P07aHoq6Ai+wOEnRpCPnTkF5tiPa6lLZThXteRJkt5Mg60vQo2aUz+5CiKGrw9038SpCJD2KzhGi7wiROkokrhLp5aUY3O7DFeythqUWCb0LMdLBLdUY9ej7kDwvK7O4LzMSXkln5PgN6eRze/SGGgfUPbs+Y+9gde/N6dOovtaJjg+e6ODd95xPOJ+Bf8NbS3i0ut2Ok+H1apbptn+lla3z58/G/wA= \ No newline at end of file From 1f0c5d98dd38a5eabc06e81a904cff887805d890 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 13 May 2017 13:16:19 +0200 Subject: [PATCH 041/264] Update Untitled Diagram.xml --- experiments/Untitled Diagram.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml index ad93172..a3019f8 100644 --- a/experiments/Untitled Diagram.xml +++ b/experiments/Untitled Diagram.xml @@ -1 +1 @@ -1VdNc5swEP01HJtBEgZzresmh3amMz40OSqwBjUy65Hlr/76ChAGFdvjOLE95sBIb1cf+/ZpER4bzTaPis/zn5iC9Kifbjz2zaM0GhDzLoFtDQTxsAYyJdIaIi0wEX/Bgr5FlyKFheOoEaUWcxdMsCgg0Q7GlcK16zZF6a465xn0gEnCZR/9LVKd1+iQhi3+BCLLm5VJGNeWV568ZQqXhV3Po2xaPbV5xpu5bKCLnKe47kBs7LGRQtR1a7YZgSypbWirx30/YN3tW0GhTxkQ2W3obRM6pIYJ20Wlc8yw4HLcol+r8KCcwDe9XM+kaRLThI3QzyX8QAe2+9KYCq22tW3QdF/sDH9A663VAF9qNFC78A/EuZ1jKqQcoURVbdTwCmGSGHyhFb5Bx5JG8atfzlwHV0Z0kB4LLXCpEutFreC4ysB6sV1qjOIBZ2C2b1wUSK7Fyp2dW+1lO7+Wf9OwKdifjuFl0hEdSUdAnYR88R98EjXIL1DCBAHqflIVXClVdukVl8v/ilcne+tcaJjMebXftSmVbobOpWkFSsPmOFF9CpoBzO7T1ubYdtdtoSNNeco7RS70P07aHoq6Ai+wOEnRpCPnTkF5tiPa6lLZThXteRJkt5Mg60vQo2aUz+5CiKGrw9038SpCJD2KzhGi7wiROkokrhLp5aUY3O7DFeythqUWCb0LMdLBLdUY9ej7kDwvK7O4LzMSXkln5PgN6eRze/SGGgfUPbs+Y+9gde/N6dOovtaJjg+e6ODd95xPOJ+Bf8NbS3i0ut2Ok+H1apbptn+lla3z58/G/wA= \ No newline at end of file +5VhNc9owEP01PjYjS8bGx4bQ9tDOZIZDk6OwN7YaYTFCfPXXV7ZkbGHDEEJgMuXASE+rlfR230rgkdFs813Sef5LpMA9jNKNRx48jKOBr79LYGuAIB4aIJMsNZDfABP2FyyILLpkKSwcQyUEV2zugokoCkiUg1Epxdo1exHcXXVOM+gAk4TyLvqbpSo36BCHDf4DWJbXK/thbEamNHnNpFgWdj0Pk5fqY4ZntPZlD7rIaSrWLYiMPTKSQijTmm1GwEtqa9rMvG8HRnf7llCoUyZEdhtqWx8dUs2E7QqpcpGJgvJxg95Xx4PSAdK9XM24bvq6CRumnkr4Dg9s97keKpTcmrFB3X22Hv6AUlubA3SphIaahX8KMbc+XhjnI8GFrDaqeYUwSTS+UFK8QmskjeIpKj2bw5UnOkiPhRZiKRNrhW3CUZmBtSK70OiMBzEDvX1tIoFTxVaud2pzL9vZNfzrhg1BfziGHxOO6Eg4AuwE5Au6Q35UI48gmT4EyM8TquBKobJLryhf7hWvVvTWOVMwmdNqv2tdKt0InUvTCqSCzXGiuhTUE4jdp63Nse2um0Ln1+UpbxW5EL2ftJqiFmseDrle4T5lK93MVHVIA03lPqL999j9D1Nny4XSxBX6Yug6mcLJ6+kLR+picfmTHalbhShOKlR1+9m9J57sjObSqMbOrUXDBPpFdj9EEYrOrkXk8rWomvpVSrptGcwFK9Si5fmxBFr6jl1948HeE2DPfifKfnvdMDtoBL47ykmaJ91C6WE9C5FPUS7DPXbwNcul36HoHF0hR1fYEZbvCgu/RVnn6SS43fMq6L2zy1z08adIRkJumY1Rz+X9jvT82DSLu2nmh1fKM//4O/5k3R79HRUH2NUuIuQNrPa+7y9G9bUUHR9UdPDm1/gF9BmgG76tw6PV7XacDK9Xs3S3+e/EvFWa/6fI+B8= \ No newline at end of file From 28f3e61b2e7680646ca81858196139140e8fe8bb Mon Sep 17 00:00:00 2001 From: Andre Smit Date: Mon, 15 May 2017 09:46:38 -0500 Subject: [PATCH 042/264] Add oncontextmenu event --- src/vdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vdom.nim b/src/vdom.nim index 25f6c12..1dc389e 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -41,6 +41,7 @@ type type EventKind* {.pure.} = enum ## The events supported by the virtual DOM. onclick, ## An element is clicked. + oncontextmenu, ## An element is right-clicked. ondblclick, ## An element is double clicked. onkeyup, ## A key was released. onkeydown, ## A key is pressed. From 9160776999e5b93d81b5c788694a2ee626efe99b Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 10:00:39 +0300 Subject: [PATCH 043/264] Resolve a quote bug --- src/components.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components.nim b/src/components.nim index c3cda9c..7fcc707 100644 --- a/src/components.nim +++ b/src/components.nim @@ -79,8 +79,8 @@ proc stateDecl(n: NimNode; names: TableRef[string, bool]; decl: NimNode) = for i in 0 .. c.len-3: let v = $c[i] let sv = toState v - decl.add quote do: - var `sv` = newStateDict[`usedType`]() + decl.add(quote do: + var `sv` = newStateDict[`usedType`]()) if val.kind != nnkEmpty: decl.add newTree(nnkAsgn, newDotExpr(sv, newIdentNode"defaultValue"), val) From 084a1988c809d1aec7d2d587a011bb8d22e389ae Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 10:01:02 +0300 Subject: [PATCH 044/264] Resolve a macro bug --- src/karaxdsl.nim | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 5ba525a..faa43c3 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -101,20 +101,22 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result = n macro buildHtml*(tag, children: untyped): VNode = - expectKind children, nnkDo + let kids = newProc(procType=nnkDo, body=children) + expectKind kids, nnkDo var call: NimNode if tag.kind in nnkCallKinds: call = tag else: call = newCall(tag) - call.add body(children) + call.add body(kids) result = tcall2(call, nil) when defined(debugKaraxDsl): echo repr result macro buildHtml*(children: untyped): VNode = - expectKind children, nnkDo - result = tcall2(body(children), nil) + let kids = newProc(procType=nnkDo, body=children) + expectKind kids, nnkDo + result = tcall2(body(kids), nil) when defined(debugKaraxDsl): echo repr result From 76a2031c37021a4dbf2ae43dadad689f57f91c28 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Thu, 18 May 2017 12:58:43 +0300 Subject: [PATCH 045/264] Support submit and input event in the vdom --- src/vdom.nim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/vdom.nim b/src/vdom.nim index 1dc389e..af0c98a 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -73,6 +73,9 @@ type ondragstart, ## The user starts dragging an element or text selection. ondrop, ## An element is dropped on a valid drop target. + onsubmit, ## A form is submitted + oninput, ## An input value changes + onkeyupenter, ## vdom extension: an input field received the ENTER key press onkeyuplater ## vdom extension: a key was pressed and some time ## passed (useful for on-the-fly text completions) From d6c7be710673b2393ef849a055cc9a1cb01bb122 Mon Sep 17 00:00:00 2001 From: Araq Date: Thu, 18 May 2017 15:53:58 +0200 Subject: [PATCH 046/264] added vstyles karax module for inline CSS support --- experiments/example.nim | 12 +++- src/dom.nim | 2 +- src/karax.nim | 4 +- src/karaxdsl.nim | 2 +- src/vdom.nim | 6 +- src/vstyles.nim | 154 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/vstyles.nim diff --git a/experiments/example.nim b/experiments/example.nim index 229cce6..caa7285 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,5 +1,5 @@ -import vdom, components, karax, karaxdsl, jdict, jstrutils, dom +import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, dom var images: seq[cstring] = @[cstring"a", "b", "c", "d"] @@ -24,10 +24,20 @@ proc carousel*(key: VKey): VNode {.component.} = clearTimeout(timer) timer = setTimeout(docount, 30) + let col = + case counter + of 0: cstring"#4d4d4d" + of 1: cstring"#ff00ff" + of 2: cstring"#00ffff" + of 3: cstring"#ffff00" + else: cstring"red" + result = buildHtml(tdiv(key=key)): text images[counter] button(onclick = next): text "Next" + tdiv(style = style(StyleAttr.color, col)): + text "This changes its color." if cntdown != 5: text &cntdown diff --git a/src/dom.nim b/src/dom.nim index f3d3f73..f2dabcf 100644 --- a/src/dom.nim +++ b/src/dom.nim @@ -188,7 +188,7 @@ type vspace*: int width*: int - Style = ref StyleObj + Style* = ref StyleObj StyleObj {.importc.} = object of RootObj background*: cstring backgroundAttachment*: cstring diff --git a/src/karax.nim b/src/karax.nim index ed956d8..2ac4e91 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,6 +1,6 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict +import dom, vdom, jstrutils, components, jdict, vstyles export dom.Event @@ -104,6 +104,7 @@ proc vnodeToDom(n: VNode): Node = wrapEvent(result, n, e, h) if n == toFocusV and toFocus.isNil: toFocus = result + if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = if toTag[n.kind] == e.nodename: @@ -135,6 +136,7 @@ proc equalsShallow(a, b: VNode): bool = if not equalsShallow(a[i], b[i]): return false if not sameAttrs(a, b): return false if a.class != b.class: return false + if a.style != b.style: return false # XXX test event listeners here? return true diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index faa43c3..190545c 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key"] + SpecialAttrs = ["id", "class", "value", "key", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index 1dc389e..9d2b90d 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -1,7 +1,7 @@ ## Virtual DOM implementation for Karax. from dom import Event, Node -import shash, macros +import shash, macros, vstyles from strutils import toUpperAscii type @@ -77,6 +77,7 @@ type onkeyuplater ## vdom extension: a key was pressed and some time ## passed (useful for on-the-fly text completions) + macro buildLookupTables(): untyped = var a = newTree(nnkBracket) for i in low(VNodeKind)..high(VNodeKind): @@ -99,6 +100,7 @@ buildLookupTables() type EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int + VNode* = ref object kind*: VNodeKind key*: VKey @@ -109,6 +111,7 @@ type events*: seq[(EventKind, EventHandler)] hash*: Hash validHash*: bool + style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. @@ -143,6 +146,7 @@ proc eq*(a, b: VNode): bool = if a.attrs.len != b.attrs.len: return false for i in 0.. Date: Mon, 29 May 2017 10:59:59 +0200 Subject: [PATCH 047/264] renamed dom module to 'kdom' to avoid conflicts with Nim's stdlib dom module; it will further diverge from it anyway --- examples/mediaplayer/mediaplayer.nim | 2 +- examples/scrollapp/scrollapp.nim | 5 +++-- experiments/ajaxtest.nim | 2 +- experiments/example.nim | 2 +- src/components.nim | 2 +- src/karax.nim | 4 ++-- src/{dom.nim => kdom.nim} | 0 src/vdom.nim | 2 +- src/vstyles.nim | 2 +- tests/diffDomTests.nim | 2 +- 10 files changed, 12 insertions(+), 11 deletions(-) rename src/{dom.nim => kdom.nim} (100%) diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 18179c1..890c078 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -1,5 +1,5 @@ -import karax, karaxdsl, vdom, dom, components +import karax, karaxdsl, vdom, kdom, components const Play = 0 diff --git a/examples/scrollapp/scrollapp.nim b/examples/scrollapp/scrollapp.nim index e5ef9c1..8ead611 100644 --- a/examples/scrollapp/scrollapp.nim +++ b/examples/scrollapp/scrollapp.nim @@ -1,7 +1,7 @@ ## Example that shows how to accomplish an "infinitely scrolling" app. include karaxprelude -import jstrutils, dom +import jstrutils, kdom, vstyles var entries: seq[cstring] = @[] for i in 1..500: @@ -16,7 +16,8 @@ proc scrollEvent(ev: Event; n: VNode) = proc createDom(): VNode = result = buildHtml(): - tdiv(onscroll=scrollEvent, style="height: 400px; overflow: scroll"): + tdiv(onscroll=scrollEvent, style=style( + (StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))): for x in entries: tdiv: text x diff --git a/experiments/ajaxtest.nim b/experiments/ajaxtest.nim index 7af60cd..720b6b2 100644 --- a/experiments/ajaxtest.nim +++ b/experiments/ajaxtest.nim @@ -1,4 +1,4 @@ -import dom, kajax +import kdom, kajax proc cb(httpStatus: int, response: cstring) = echo "Worked!" diff --git a/experiments/example.nim b/experiments/example.nim index caa7285..fd1e208 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,5 +1,5 @@ -import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, dom +import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, kdom var images: seq[cstring] = @[cstring"a", "b", "c", "d"] diff --git a/src/components.nim b/src/components.nim index 7fcc707..555de77 100644 --- a/src/components.nim +++ b/src/components.nim @@ -1,6 +1,6 @@ ## Components in Karax are built by the ``.component`` macro annotation. -import macros, jdict, dom, vdom, tables, strutils +import macros, jdict, kdom, vdom, tables, strutils type StateDict*[V] = ref object diff --git a/src/karax.nim b/src/karax.nim index 2ac4e91..3915b7f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -1,8 +1,8 @@ ## Karax -- Single page applications for Nim. -import dom, vdom, jstrutils, components, jdict, vstyles +import kdom, vdom, jstrutils, components, jdict, vstyles -export dom.Event +export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. diff --git a/src/dom.nim b/src/kdom.nim similarity index 100% rename from src/dom.nim rename to src/kdom.nim diff --git a/src/vdom.nim b/src/vdom.nim index 9d2b90d..a10dce6 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -1,6 +1,6 @@ ## Virtual DOM implementation for Karax. -from dom import Event, Node +from kdom import Event, Node import shash, macros, vstyles from strutils import toUpperAscii diff --git a/src/vstyles.nim b/src/vstyles.nim index 022476e..f3d8dbc 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -1,5 +1,5 @@ -import dom, macros +import kdom, macros type StyleAttr* {.pure.} = enum ## the style attributes supported by the virtual DOM. diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index 7fdcf85..d2c09ab 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -1,5 +1,5 @@ -import dom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils +import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils var entries: seq[cstring] From a9b5216a423c0e19c67d6e303d6441cb2a0c04d0 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 5 Jun 2017 10:21:38 +0200 Subject: [PATCH 048/264] diff algorithm special cases diffing of virtual styles --- src/karax.nim | 131 +++++++++++++++++++++++++++----------------------- 1 file changed, 72 insertions(+), 59 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3915b7f..198a244 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -123,22 +123,26 @@ proc replaceById(id: cstring; newTree: Node) = x.parentNode.replaceChild(newTree, x) #newTree.id = id -proc equalsShallow(a, b: VNode): bool = - if a.kind != b.kind: return false - if a.id != b.id: return false - if a.key != b.key: return false +type + EqResult = enum + different, similar, identical + +proc equalsShallow(a, b: VNode): EqResult = + if a.kind != b.kind: return different + if a.id != b.id: return different + if a.key != b.key: return different if a.kind == VNodeKind.text: - if a.text != b.text: return false + if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: - if a.text != b.text: return false - if a.len != b.len: return false + if a.text != b.text: return different + if a.len != b.len: return different for i in 0.. oldLength: - for i in oldLength..newLength-1: - current.appendChild(vnodeToDom(newNode[i])) - elif oldLength > newLength: - for i in countdown(oldLength-1, newLength): + else: + if res == similar: + # we keep the oldNode, but take over the style from the new node: + if oldNode.dom != nil: + if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) + else: oldNode.dom.style = Style() + oldNode.style = newNode.style + + if newNode.kind != VNodeKind.text: + let newLength = newNode.len + var oldLength = oldNode.len + let minLength = min(newLength, oldLength) + assert oldNode.kind == newNode.kind + when defined(simpleDiff): + for i in 0..min(newLength, oldLength)-1: + updateElement(current, current[i], newNode[i], oldNode[i]) + if newLength > oldLength: + for i in oldLength..newLength-1: + current.appendChild(vnodeToDom(newNode[i])) + elif oldLength > newLength: + for i in countdown(oldLength-1, newLength): + detach(oldNode[i]) + current.removeChild(current.lastChild) + else: + var commonPrefix = 0 + 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]): + dec oldPos + dec newPos + + var pos = min(oldPos, newPos) + 1 + for i in commonPrefix..pos-1: + updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + + var nextChildPos = oldPos + 1 + while pos <= newPos: + if nextChildPos == oldLength: + current.appendChild(vnodeToDom(newNode[pos])) + else: + current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) + # added new Node, so old state of VDOM have one more Node + inc oldLength + inc pos + inc nextChildPos + + for i in pos..oldPos: detach(oldNode[i]) - current.removeChild(current.lastChild) - else: - var commonPrefix = 0 - 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]): - dec oldPos - dec newPos - - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) - - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) - else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos - - for i in pos..oldPos: - detach(oldNode[i]) - current.removeChild(current.childNodes[pos]) + current.removeChild(current.childNodes[pos]) when false: var drawTimeout: Timeout From 06f528370979d9eaa94a765fbf9c4ad3951f654c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 5 Jun 2017 10:25:46 +0200 Subject: [PATCH 049/264] vstyle diff: performance improvement --- src/vdom.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vdom.nim b/src/vdom.nim index a10dce6..7cab49a 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -146,7 +146,9 @@ proc eq*(a, b: VNode): bool = if a.attrs.len != b.attrs.len: return false for i in 0.. Date: Mon, 5 Jun 2017 17:06:58 +0200 Subject: [PATCH 050/264] stuff --- src/karax.nim | 80 ++++++++++++++++++++++++++++++++------------------- src/vdom.nim | 18 ------------ 2 files changed, 51 insertions(+), 47 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 198a244..555b3bb 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -127,33 +127,35 @@ type EqResult = enum different, similar, identical -proc equalsShallow(a, b: VNode): EqResult = +proc eq(a, b: VNode; shallow: bool): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different - if a.key != b.key: return different + result = identical + if a.key != b.key: + kout cstring"different keys", a.key, b.key + if b.key == 0: + kout cstring"key for ", cstring($b.kind) + return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different for i in 0..= 0 and isDirty(newNode.key): @@ -170,8 +172,21 @@ proc updateDirtyElements(parent, current: Node, newNode: VNode) = # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return +proc updateStyles(newNode, oldNode: VNode; shallow: bool) = + # we keep the oldNode, but take over the style from the new node: + if oldNode.dom != nil: + kout cstring"updateStyle deep updated dom" + if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) + else: oldNode.dom.style = Style() + oldNode.style = newNode.style + if not shallow: + assert newNode.len == oldNode.len + for i in 0 ..< newNode.len: + updateStyles(newNode[i], oldNode[i], shallow) + proc updateElement(parent, current: Node, newNode, oldNode: VNode) = - let res = equalsShallow(newNode, oldNode) + let res = eq(newNode, oldNode, shallow=true) + kout cstring($res), cstring"shallow" if res == different: detach(oldNode) let n = vnodeToDom(newNode) @@ -180,12 +195,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = else: parent.replaceChild(n, current) else: - if res == similar: - # we keep the oldNode, but take over the style from the new node: - if oldNode.dom != nil: - if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) - else: oldNode.dom.style = Style() - oldNode.style = newNode.style + if res == similar: updateStyles(newNode, oldNode, true) if newNode.kind != VNodeKind.text: let newLength = newNode.len @@ -204,16 +214,27 @@ 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]): - inc commonPrefix + + template eqAndUpdate(a, b: VNode; action: untyped) = + let r = eq(a, b, false) + kout cstring($r), cstring"eqAndUpdate" + case r + of identical: action + of different: break + of similar: + updateStyles(a, b, false) + action + + while commonPrefix < minLength: + eqAndUpdate(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]): - dec oldPos - dec newPos + while oldPos >= commonPrefix and newPos >= commonPrefix: + eqAndUpdate(newNode[newPos], oldNode[oldPos]): + dec oldPos + dec newPos var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: @@ -241,6 +262,7 @@ proc dodraw() = if dorender.isNil: return let newtree = dorender() newtree.id = "ROOT" + kout cstring"do draw!", cstring getStackTrace() toFocus = nil if currentTree == nil: currentTree = newtree diff --git a/src/vdom.nim b/src/vdom.nim index 7cab49a..4275c7b 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -133,24 +133,6 @@ 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 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.. Date: Mon, 5 Jun 2017 21:31:26 +0200 Subject: [PATCH 051/264] new diff algorithm works --- src/karax.nim | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 555b3bb..3fa2d7e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -127,26 +127,22 @@ type EqResult = enum different, similar, identical -proc eq(a, b: VNode; shallow: bool): EqResult = +proc eq(a, b: VNode; deep: bool): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical - if a.key != b.key: - kout cstring"different keys", a.key, b.key - if b.key == 0: - kout cstring"key for ", cstring($b.kind) - return different + if a.key != b.key: return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different for i in 0.. Date: Mon, 5 Jun 2017 23:20:22 +0200 Subject: [PATCH 052/264] diff algorithm bugfix: don't be too smart in key handling --- src/karax.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3fa2d7e..1445b1b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -75,12 +75,12 @@ proc vnodeToDom(n: VNode): Node = elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) result = vnodeToDom(x) - n.key = result.key + #n.key = result.key attach n return result elif n.kind == VNodeKind.dthunk: result = callThunk(dcomponents[n.text], n) - n.key = result.key + #n.key = result.key attach n return result else: @@ -95,8 +95,8 @@ proc vnodeToDom(n: VNode): Node = result.id = n.id if n.class != nil: result.class = n.class - if n.key >= 0: - result.key = n.key + #if n.key >= 0: + # result.key = n.key for k, v in attrs(n): if v != nil: result.setAttr(k, v) From fca6b2a6ca1ef5b4ebad9d431742a5247ccca9ae Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Tue, 6 Jun 2017 20:02:46 +0200 Subject: [PATCH 053/264] added all css styles to close #20 --- src/vstyles.nim | 121 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 112 insertions(+), 9 deletions(-) diff --git a/src/vstyles.nim b/src/vstyles.nim index f3d8dbc..649c322 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -2,51 +2,112 @@ import kdom, macros type - StyleAttr* {.pure.} = enum ## the style attributes supported by the virtual DOM. + StyleAttr* {.pure.} = enum + ## The style attributes supported by the virtual DOM. + ## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp + alignContent + alignItems + alignSelf + animation + animationDelay + animationDirection + animationDuration + animationFillMode + animationIterationCount + animationName + animationTimingFunction + animationPlayState background backgroundAttachment backgroundColor backgroundImage backgroundPosition backgroundRepeat + backgroundClip + backgroundOrigin + backgroundSize + backfaceVisibility border borderBottom borderBottomColor + borderBottomLeftRadius + borderBottomRightRadius borderBottomStyle borderBottomWidth + borderCollapse borderColor + borderImage + borderImageOutset + borderImageRepeat + borderImageSlice + borderImageSource + borderImageWidth borderLeft borderLeftColor borderLeftStyle borderLeftWidth + borderRadius borderRight borderRightColor borderRightStyle borderRightWidth + borderSpacing borderStyle borderTop borderTopColor + borderTopLeftRadius + borderTopRightRadius borderTopStyle borderTopWidth borderWidth bottom + boxDecorationBreak + boxShadow + boxSizing captionSide clear clip color + columnCount + columnFill + columnGap + columnRule + columnRuleColor + columnRuleStyle + columnRuleWidth + columns + columnSpan + columnWidth + content + counterIncrement + counterReset cursor direction display emptyCells + filter + flex + flexBasis + flexDirection + flexFlow + flexGrow + flexShrink + flexWrap cssFloat font fontFamily fontSize + fontSizeAdjust fontStretch fontStyle fontVariant fontWeight + hangingPunctuation height + hyphens + icon + imageOrientation + justifyContent left letterSpacing lineHeight @@ -63,7 +124,22 @@ type maxWidth minHeight minWidth + navDown + navIndex + navLeft + navRight + navUp + opacity + order + orphans + outline + outlineColor + outlineOffset + outlineStyle + outlineWidth overflow + overflowX + overflowY padding paddingBottom paddingLeft @@ -71,26 +147,53 @@ type paddingTop pageBreakAfter pageBreakBefore + pageBreakInside + perspective + perspectiveOrigin position + quotes + resize right - scrollbar3dLightColor - scrollbarArrowColor - scrollbarBaseColor - scrollbarDarkshadowColor - scrollbarFaceColor - scrollbarHighlightColor - scrollbarShadowColor - scrollbarTrackColor + scrollbar3dLightColor # note: missing in w3schools reference + scrollbarArrowColor # note: missing in w3schools reference + scrollbarBaseColor # note: missing in w3schools reference + scrollbarDarkshadowColor # note: missing in w3schools reference + scrollbarFaceColor # note: missing in w3schools reference + scrollbarHighlightColor # note: missing in w3schools reference + scrollbarShadowColor # note: missing in w3schools reference + scrollbarTrackColor # note: missing in w3schools reference tableLayout + tabSize textAlign + textAlignLast textDecoration + textDecorationColor + textDecorationLine + textDecorationStyle textIndent + textJustify + textOverflow + textShadow textTransform top + transform + transformOrigin + transformStyle + transition + transitionDelay + transitionDuration + transitionProperty + transitionTimingFunction + unicodeBidi + userSelect verticalAlign visibility + whiteSpace width + wordBreak wordSpacing + wordWrap + widows zIndex macro buildLookupTables(): untyped = From e49fa7edf7ff285b55ef3e37999fc39914a66548 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 7 Jun 2017 10:07:37 +0200 Subject: [PATCH 054/264] disable smart vstyle diffing for now --- src/karax.nim | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/karax.nim b/src/karax.nim index 1445b1b..f8929e1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -149,7 +149,8 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if a.style != b.style: return similar + # XXX Fixme: smart diffing still is broken for complex apps: + if a.style != b.style: return different # similar # Do not test event listeners here! return result From dc5ba1f6461d90439eafb19b4fb5d173690f5482 Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Fri, 9 Jun 2017 15:44:16 +0700 Subject: [PATCH 055/264] Added stopPropagation event method --- src/kdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kdom.nim b/src/kdom.nim index f2dabcf..349f02d 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -520,6 +520,7 @@ proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false) # Event "methods" proc preventDefault*(ev: Event) +proc stopPropagation*(ev: Event) # TouchEvent "methods" proc identifiedTouch*(list: TouchList): Touch From d5596c8ff50af6002ac24b26a63b0b451627ca6a Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Fri, 19 May 2017 20:15:07 +0200 Subject: [PATCH 056/264] added post render callback --- src/karax.nim | 8 +++++++- src/kdom.nim | 7 ++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f8929e1..cc52e2f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -117,6 +117,7 @@ proc same(n: VNode, e: Node): bool = var dorender: proc (): VNode {.closure.} currentTree: VNode + postRenderCallback: proc () proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -279,6 +280,10 @@ proc dodraw() = updateDirtyElements(nil, olddom, newtree) someDirty = false currentTree = newtree + + if not postRenderCallback.isNil: + postRenderCallback() + # now that it's part of the DOM, give it the focus: if toFocus != nil: toFocus.focus() @@ -299,9 +304,10 @@ proc redraw*() = proc init(ev: Event) = reqFrame(dodraw) -proc setRenderer*(renderer: proc (): VNode) = +proc setRenderer*(renderer: proc (): VNode, clientPostRenderCallback: proc () = nil) = dorender = renderer window.onload = init + postRenderCallback = clientPostRenderCallback proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## Implements the foundation of Karax's event management. diff --git a/src/kdom.nim b/src/kdom.nim index 349f02d..0adb1dd 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -273,6 +273,7 @@ type textDecoration*: cstring textIndent*: cstring textTransform*: cstring + transform*: cstring top*: cstring verticalAlign*: cstring visibility*: cstring @@ -567,7 +568,7 @@ proc getElementsByClass*(n: Node; name: cstring): seq[Node] {. type BoundingRect* {.importc.} = object - top*, bottom*, left*, right*: int + top*, bottom*, left*, right*, x*, y*, width*, height*: float proc getBoundingClientRect*(e: Node): BoundingRect {. importcpp: "getBoundingClientRect", nodecl.} @@ -579,8 +580,8 @@ proc clientWidth*(): int {. proc inViewport*(el: Node): bool = let rect = el.getBoundingClientRect() result = rect.top >= 0 and rect.left >= 0 and - rect.bottom <= clientHeight() and - rect.right <= clientWidth() + rect.bottom <= clientHeight().float and + rect.right <= clientWidth().float proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} From 14660f9d685d1b14cdead9cb09421f0f8ff7512e Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 13:53:53 +0200 Subject: [PATCH 057/264] implemented refs draft --- experiments/refs/demo.html | 13 +++++++++++++ experiments/refs/demo.nim | 39 ++++++++++++++++++++++++++++++++++++++ experiments/refs/demo.nims | 1 + src/karax.nim | 10 ++++++++-- src/karaxdsl.nim | 2 +- src/vdom.nim | 4 ++++ 6 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 experiments/refs/demo.html create mode 100644 experiments/refs/demo.nim create mode 100644 experiments/refs/demo.nims diff --git a/experiments/refs/demo.html b/experiments/refs/demo.html new file mode 100644 index 0000000..e1fe419 --- /dev/null +++ b/experiments/refs/demo.html @@ -0,0 +1,13 @@ + + + + + + Refs + + + +
+ + + diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim new file mode 100644 index 0000000..dffb1e9 --- /dev/null +++ b/experiments/refs/demo.nim @@ -0,0 +1,39 @@ +include karaxprelude +import random + +var + modelData = @[5, 2, 4] + + refA = VNodeRef() + refB = VNodeRef() + refSeq = newSeq[VNodeRef]() + +proc onClick(ev: Event, n: VNode) = + kout(refA) + kout(refB) + kout(refSeq) + +proc secureRefSlot(i: int): VNodeRef = + while refSeq.len <= i: + refSeq.add(VNodeRef()) + result = refSeq[i] + +proc view(): VNode = + result = buildHtml(): + tdiv: + button(onclick=onClick): + text "click me" + # storing refs to single elements is straightforward now + tdiv(nref=refA): + text "A" + tdiv(nref=refB): + text "B" + # It's a bit more tricky when containers are involved: + for i, x in modelData.pairs: + tdiv(nref=secureRefSlot(i)): + text "List item: " & $x + +proc renderer(): VNode = + view() + +setRenderer renderer diff --git a/experiments/refs/demo.nims b/experiments/refs/demo.nims new file mode 100644 index 0000000..9028ff1 --- /dev/null +++ b/experiments/refs/demo.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/src/karax.nim b/src/karax.nim index cc52e2f..d8a4cb1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -65,8 +65,14 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = # --------------------- DOM diff ----------------------------------------- -template detach(n: VNode) = n.dom = nil -template attach(n: Vnode) = n.dom = result +template detach(n: VNode) = + if not n.nref.isNil(): + n.nref.vnode = nil + n.dom = nil +template attach(n: VNode) = + if not n.nref.isNil(): + n.nref.vnode = n + n.dom = result proc vnodeToDom(n: VNode): Node = if n.kind == VNodeKind.text: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 190545c..8693f5b 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style"] + SpecialAttrs = ["id", "class", "value", "key", "style", "nref"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index d55e7b4..f4ccff8 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,6 +101,9 @@ macro buildLookupTables(): untyped = buildLookupTables() type + VNodeRef* = ref object + vnode*: VNode + EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int @@ -108,6 +111,7 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring + nref*: VNodeRef kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] From 472f1deb58cb1ae718e395034e1e4099cf53c7c9 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 14:38:39 +0200 Subject: [PATCH 058/264] cleaned up example --- experiments/refs/demo.nim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index dffb1e9..cfbf7a3 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -1,5 +1,5 @@ include karaxprelude -import random +import future, sequtils var modelData = @[5, 2, 4] @@ -9,9 +9,9 @@ var refSeq = newSeq[VNodeRef]() proc onClick(ev: Event, n: VNode) = - kout(refA) - kout(refB) - kout(refSeq) + kout(refA.vnode) + kout(refB.vnode) + kout(refSeq.map(nref => nref.vnode)) proc secureRefSlot(i: int): VNodeRef = while refSeq.len <= i: From e33b9dc09a2242b6b85c027ebc76d0ba982018b2 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Thu, 15 Jun 2017 19:01:15 +0200 Subject: [PATCH 059/264] added base methods for VNodeRef --- experiments/refs/demo.nim | 31 ++++++++++++++++++++++++------- src/karax.nim | 2 ++ src/vdom.nim | 6 +++++- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index cfbf7a3..6ecc7db 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -1,16 +1,30 @@ include karaxprelude import future, sequtils +type + CustomRef = ref object of VNodeRef + +method onAttach(r: CustomRef) = + kout(cstring"custom ref attached") + +method onDetach(r: CustomRef) = + kout(cstring"custom ref detached") + var modelData = @[5, 2, 4] refA = VNodeRef() refB = VNodeRef() + refC = CustomRef() refSeq = newSeq[VNodeRef]() proc onClick(ev: Event, n: VNode) = - kout(refA.vnode) - kout(refB.vnode) + modelData.add(0) + +proc showRefs() = + kout(refA) + kout(refB) + kout(refC) kout(refSeq.map(nref => nref.vnode)) proc secureRefSlot(i: int): VNodeRef = @@ -24,10 +38,13 @@ proc view(): VNode = button(onclick=onClick): text "click me" # storing refs to single elements is straightforward now - tdiv(nref=refA): - text "A" - tdiv(nref=refB): - text "B" + if modelData.len mod 2 == 0: + tdiv(nref=refA): + text "A" + tdiv(nref=refB): + text "B" + tdiv(nref=refC): + text "C" # It's a bit more tricky when containers are involved: for i, x in modelData.pairs: tdiv(nref=secureRefSlot(i)): @@ -36,4 +53,4 @@ proc view(): VNode = proc renderer(): VNode = view() -setRenderer renderer +setRenderer(renderer, showRefs) diff --git a/src/karax.nim b/src/karax.nim index d8a4cb1..b454050 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -68,10 +68,12 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = template detach(n: VNode) = if not n.nref.isNil(): n.nref.vnode = nil + n.nref.onDetach() n.dom = nil template attach(n: VNode) = if not n.nref.isNil(): n.nref.vnode = n + n.nref.onAttach() n.dom = result proc vnodeToDom(n: VNode): Node = diff --git a/src/vdom.nim b/src/vdom.nim index f4ccff8..9f9c642 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,7 +101,7 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VNodeRef* = ref object + VNodeRef* = ref object of RootObj vnode*: VNode EventHandler* = proc (ev: Event; target: VNode) {.closure.} @@ -122,6 +122,10 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. +method onAttach*(v: VNodeRef) {.base.} = discard +method onDetach*(v: VNodeRef) {.base.} = discard +method changed*(v: VNodeRef): bool {.base.} = false + proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 08538f7f2675814436c60de3316323c7991692ef Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 16:45:06 +0200 Subject: [PATCH 060/264] move parseInt to jstrutils.nim to avoid conflicts; introduce travis testing --- .travis.yml | 18 ++++++++++++++++++ src/jstrutils.nim | 1 + src/kdom.nim | 3 --- tests/tester.nim | 15 +++++++++++++++ 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/tester.nim diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..0c60bd2 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: c +os: linux +install: + - git clone https://github.com/nim-lang/nim + - cd nim + - git clone --depth 1 https://github.com/nim-lang/csources.git + - cd csources + - sh build.sh + - cd ../ + - bin/nim c koch + - ./koch boot -d:release + - cd .. +before_script: + - set -e + - export PATH=$(pwd)/nim/bin:$(pwd):$PATH +script: + - nim c -r tests/tester.nim diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 0be4076..a16473d 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -23,3 +23,4 @@ proc isInt*(s: cstring): bool {.asmNoStackFrame.} = """ proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} +proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} diff --git a/src/kdom.nim b/src/kdom.nim index 0adb1dd..f933b87 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -547,9 +547,6 @@ proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.} proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.} proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.} proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.} -proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.} -proc parseInt*(s: cstring): int {.importc, nodecl.} -proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.} proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.} diff --git a/tests/tester.nim b/tests/tester.nim new file mode 100644 index 0000000..5dc7426 --- /dev/null +++ b/tests/tester.nim @@ -0,0 +1,15 @@ +# For now we only test that the things still compile. + +import os + +proc exec(cmd: string) = + if os.execShellCmd(cmd) != 0: + quit "command failed " & cmd + +proc main = + exec("nim js tests/diffDomTests.nim") + exec("nim js examples/todoapp/todoapp.nim") + exec("nim js examples/scrollapp/scrollapp.nim") + exec("nim js examples/mediaplayer/playerapp.nim") + +main() From b3fba429ad89ee45419e9c3555bdbd53a9c53ba7 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 18:02:35 +0200 Subject: [PATCH 061/264] introduce KaraxInstance; 100% backwards compatible --- src/karax.nim | 121 ++++++++++++++++++++++++++--------------------- src/karaxdsl.nim | 4 +- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index cc52e2f..d9f5773 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -14,12 +14,26 @@ proc key*(e: Node): VKey = else: result = -1 proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} -var - toFocus: Node - toFocusV: VNode +type + KaraxInstance* = ref object ## underlying karax instance. Usually you don't have + ## know about this. + rootId: cstring not nil + renderer: proc (): VNode {.closure.} + currentTree: VNode + postRenderCallback: proc () + toFocus: Node + toFocusV: VNode -proc setFocus*(n: VNode) = - toFocusV = n +var + kxi*: KaraxInstance ## The current Karax instance. This is always used + ## as the default. **Note**: Within the karax DSL + ## always a symbol of the name *kxi* is assumed, so + ## if you have a local karax instance to use instead + ## in your 'buildHtml' statement, it needs to be named + ## 'kxi'. + +proc setFocus*(n: VNode; kxi: KaraxInstance = kxi) = + kxi.toFocusV = n # ----------------- event wrapping --------------------------------------- @@ -68,13 +82,13 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = template detach(n: VNode) = n.dom = nil template attach(n: Vnode) = n.dom = result -proc vnodeToDom(n: VNode): Node = +proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = if n.kind == VNodeKind.text: result = document.createTextNode(n.text) attach n elif n.kind == VNodeKind.vthunk: let x = callThunk(vcomponents[n.text], n) - result = vnodeToDom(x) + result = vnodeToDom(x, kxi) #n.key = result.key attach n return result @@ -87,7 +101,7 @@ proc vnodeToDom(n: VNode): Node = result = document.createElement(toTag[n.kind]) attach n for k in n: - appendChild(result, vnodeToDom(k)) + appendChild(result, vnodeToDom(k, kxi)) # text is mapped to 'value': if n.text != nil: result.value = n.text @@ -102,8 +116,8 @@ proc vnodeToDom(n: VNode): Node = result.setAttr(k, v) for e, h in items(n.events): wrapEvent(result, n, e, h) - if n == toFocusV and toFocus.isNil: - toFocus = result + if n == kxi.toFocusV and kxi.toFocus.isNil: + kxi.toFocus = result if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = @@ -114,11 +128,6 @@ proc same(n: VNode, e: Node): bool = for i in 0 ..< n.len: if not same(n[i], e[i]): return false -var - dorender: proc (): VNode {.closure.} - currentTree: VNode - postRenderCallback: proc () - proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) @@ -155,18 +164,19 @@ proc eq(a, b: VNode; deep: bool): EqResult = # Do not test event listeners here! return result -proc updateDirtyElements(parent, current: Node, newNode: VNode) = +proc updateDirtyElements(parent, current: Node, newNode: VNode, + kxi: KaraxInstance) = if newNode.key >= 0 and isDirty(newNode.key): unmarkDirty(newNode.key) - let n = vnodeToDom(newNode) + let n = vnodeToDom(newNode, kxi) if parent == nil: - replaceById("ROOT", n) + 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]) + updateDirtyElements(current, current[i], newNode[i], kxi) # leave early if we know there cannot be anything left to do: #if dirtyCount <= 0: return @@ -187,13 +197,14 @@ proc updateDom(newNode, oldNode: VNode) = for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) -proc updateElement(parent, current: Node, newNode, oldNode: VNode) = +proc updateElement(parent, current: Node, newNode, oldNode: VNode; + kxi: KaraxInstance) = let res = eq(newNode, oldNode, deep=false) if res == different: detach(oldNode) - let n = vnodeToDom(newNode) + let n = vnodeToDom(newNode, kxi) if parent == nil: - replaceById("ROOT", n) + replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) else: @@ -207,7 +218,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = assert oldNode.kind == newNode.kind when defined(simpleDiff): for i in 0..min(newLength, oldLength)-1: - updateElement(current, current[i], newNode[i], oldNode[i]) + updateElement(current, current[i], newNode[i], oldNode[i], kxi) if newLength > oldLength: for i in oldLength..newLength-1: current.appendChild(vnodeToDom(newNode[i])) @@ -243,14 +254,14 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) + updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi) var nextChildPos = oldPos + 1 while pos <= newPos: if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos])) + current.appendChild(vnodeToDom(newNode[pos], kxi)) else: - current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) + current.insertBefore(vnodeToDom(newNode[pos], kxi), current.childNodes[nextChildPos]) # added new Node, so old state of VDOM have one more Node inc oldLength inc pos @@ -263,53 +274,57 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) = when false: var drawTimeout: Timeout -proc dodraw() = - if dorender.isNil: return - let newtree = dorender() - newtree.id = "ROOT" - toFocus = nil - if currentTree == nil: - currentTree = newtree - let asdom = vnodeToDom currentTree - replaceById("ROOT", asdom) +proc dodraw(kxi: KaraxInstance) = + if kxi.renderer.isNil: return + let newtree = kxi.renderer() + newtree.id = kxi.rootId + kxi.toFocus = nil + if kxi.currentTree == nil: + kxi.currentTree = newtree + let asdom = vnodeToDom(kxi.currentTree, kxi) + replaceById(kxi.rootId, asdom) else: - let olddom = document.getElementById("ROOT") - updateElement(nil, olddom, newtree, currentTree) + let olddom = document.getElementById(kxi.rootId) + updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) if someDirty: - updateDirtyElements(nil, olddom, newtree) + updateDirtyElements(nil, olddom, newtree, kxi) someDirty = false - currentTree = newtree + kxi.currentTree = newtree - if not postRenderCallback.isNil: - postRenderCallback() + if not kxi.postRenderCallback.isNil: + kxi.postRenderCallback() # now that it's part of the DOM, give it the focus: - if toFocus != nil: - toFocus.focus() + if kxi.toFocus != nil: + kxi.toFocus.focus() proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} -proc redraw*() = +proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: when false: if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) elif true: - reqFrame(dodraw) + reqFrame(proc () = kxi.dodraw) else: dodraw() proc init(ev: Event) = - reqFrame(dodraw) + reqFrame(proc () = kxi.dodraw) -proc setRenderer*(renderer: proc (): VNode, clientPostRenderCallback: proc () = nil) = - dorender = renderer +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) + kxi = result window.onload = init - postRenderCallback = clientPostRenderCallback -proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = +proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler; + kxi: KaraxInstance = kxi) = ## Implements the foundation of Karax's event management. ## Karax DSL transforms ``tag(onEvent = handler)`` to ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)`` @@ -317,7 +332,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = ## a ``redraw``. proc wrapper(ev: Event; n: VNode) = action(ev, n) - redraw() + redraw(kxi) addEventListener(n, k, wrapper) proc setOnHashChange*(action: proc (hashPart: cstring)) = @@ -346,10 +361,10 @@ proc setupErrorHandler*() = proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) -proc loadScript*(jsfilename: cstring) = +proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = let body = getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename body.prepend(s) - redraw() + redraw(kxi) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 190545c..a3558c8 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -76,7 +76,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = let key = getName x[0] if key.startsWith("on"): result.add newCall(bindSym"addEventHandler", - tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) + tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) elif key in SpecialAttrs: result.add newDotAsgn(tmp, key, x[1]) else: @@ -84,7 +84,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = elif ck != ComponentKind.Tag: call.add x elif eqIdent(x, "setFocus"): - result.add newCall(x, tmp) + result.add newCall(x, tmp, ident"kxi") else: result.add tcall2(x, tmp) if tmpContext == nil: From 28c4f4b76a0cf66618a70a9a4e9f6d9f220040a6 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 16 Jun 2017 22:57:32 +0200 Subject: [PATCH 062/264] update kajax.nim to use the 'kxi' context too --- src/kajax.nim | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/kajax.nim b/src/kajax.nim index be230f0..414b9d2 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -7,10 +7,11 @@ import karax proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; - cont: proc (httpStatus: int; response: cstring)) = + cont: proc (httpStatus: int; response: cstring); + kxi: KaraxInstance = kxi) = proc contWrapper(httpStatus: int; response: cstring) = cont(httpStatus, response) - redraw() + redraw(kxi) type HttpRequest {.importc.} = ref object @@ -39,12 +40,14 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; data: cstring; - cont: proc (httpStatus: int, response: cstring)) = - ajax("POST", url, headers, data, cont) + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("POST", url, headers, data, cont, kxi) proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; - cont: proc (httpStatus: int, response: cstring)) = - ajax("GET", url, headers, nil, cont) + cont: proc (httpStatus: int, response: cstring); + kxi: KaraxInstance = kxi) = + ajax("GET", url, headers, nil, cont, kxi) proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} From 7b246d0d40612e230cd53bbbb535428711aff585 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sat, 17 Jun 2017 16:57:36 +0200 Subject: [PATCH 063/264] added travis link --- readme.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.rst b/readme.rst index 4aafac8..13e670a 100644 --- a/readme.rst +++ b/readme.rst @@ -1,4 +1,4 @@ -Karax -- Single page applications in Nim +Karax – Single page applications in Nim [![Build Status](https://travis-ci.org/pragmagic/karax.svg?branch=master)](https://travis-ci.org/pragmagic/karax) ======================================== Karax is a framework for developing single page applications in Nim. From 972fcb0950a275e06f3fab0b054d264cc386fdbd Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sat, 17 Jun 2017 17:05:37 +0200 Subject: [PATCH 064/264] proper rst solution --- readme.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/readme.rst b/readme.rst index 13e670a..3fac7ad 100644 --- a/readme.rst +++ b/readme.rst @@ -1,5 +1,5 @@ -Karax – Single page applications in Nim [![Build Status](https://travis-ci.org/pragmagic/karax.svg?branch=master)](https://travis-ci.org/pragmagic/karax) -======================================== +Karax – Single page applications in Nim |travis| +================================================ Karax is a framework for developing single page applications in Nim. It's still in heavy development, so keep in mind that the API is subject @@ -33,3 +33,6 @@ Goals - Leverage Nim's macro system to produce a framework that allows for the development of applications that are boilerplate free. - Keep it small, keep it fast, keep it flexible. + +.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master + :target: https://travis-ci.org/pragmagic/karax From b189b24a4f6f3afb5856db9634a55cbf6ca86375 Mon Sep 17 00:00:00 2001 From: Fabian Keller Date: Sun, 18 Jun 2017 00:01:27 +0200 Subject: [PATCH 065/264] fixes #25 (#26) --- .gitignore | 1 + src/karaxdsl.nim | 10 ++++++++-- tests/compiler_tests.nim | 18 ++++++++++++++++++ tests/{diffDomTests.nims => nim.cfg} | 0 tests/tester.nim | 1 + 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/compiler_tests.nim rename tests/{diffDomTests.nims => nim.cfg} (100%) diff --git a/.gitignore b/.gitignore index 67d9b34..42b07a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ nimcache/ +/tests/tester \ No newline at end of file diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index a3558c8..e3e8290 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -47,12 +47,18 @@ proc tcall2(n, tmpContext: NimNode): NimNode = assert n.len == result.len if L > 0: result[L-1] = tcall2(result[L-1], tmpContext) - of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt, - nnkTryStmt, nnkFinally: + of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt, + nnkFinally: # recurse for every child: result = copyNimNode(n) for x in n: result.add tcall2(x, tmpContext) + of nnkCaseStmt: + # recurse for children, but don't add call for case ident + result = copyNimNode(n) + result.add n[0] + for i in 1 ..< n.len: + result.add tcall2(n[i], tmpContext) of nnkVarSection, nnkLetSection, nnkConstSection: result = n of nnkCallKinds: diff --git a/tests/compiler_tests.nim b/tests/compiler_tests.nim new file mode 100644 index 0000000..4352cab --- /dev/null +++ b/tests/compiler_tests.nim @@ -0,0 +1,18 @@ +include karaxprelude + +block: + type + TestEnum {.pure.} = enum + ValA, ValB + + var e = TestEnum.ValA + + proc renderDom(): VNode {.used.} = + result = buildHtml(): + tdiv(): + case e + of TestEnum.ValA: + tdiv: text "A" + of TestEnum.ValB: + tdiv: text "B" + diff --git a/tests/diffDomTests.nims b/tests/nim.cfg similarity index 100% rename from tests/diffDomTests.nims rename to tests/nim.cfg diff --git a/tests/tester.nim b/tests/tester.nim index 5dc7426..482cad3 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -8,6 +8,7 @@ proc exec(cmd: string) = proc main = exec("nim js tests/diffDomTests.nim") + exec("nim js tests/compiler_tests.nim") exec("nim js examples/todoapp/todoapp.nim") exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") From d1e0e1367b417114ddd5ac44b149c069c43eb389 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 18 Jun 2017 18:01:48 +0200 Subject: [PATCH 066/264] wip --- experiments/example.nim | 79 +++++++++++++++++++++++---------------- experiments/refs/demo.nim | 18 +++++---- src/karax.nim | 4 ++ src/vdom.nim | 11 +++--- 4 files changed, 66 insertions(+), 46 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index fd1e208..d48143d 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -1,57 +1,72 @@ -import vdom, vstyles, components, karax, karaxdsl, jdict, jstrutils, kdom +import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom + +type + Car = ref object of VComponent + counter: int + cntdown: int + timer: TimeOut + list: seq[cstring] + change: bool + onclick: proc (ev: Event; n: VNode) + +const ticksUntilChange = 5 + +proc car*(images: seq[cstring]): Car = + new(result) + result.list = images + result.cntdown = ticksUntilChange + + proc docount() = + dec result.cntdown + if result.cntdown == 0: + result.counter = (result.counter + 1) mod result.list.len + result.cntdown = ticksUntilChange + else: + result.timer = setTimeout(docount, 30) + result.change = true + redraw() + + result.onclick = proc (ev: Event; n: VNode) = + result.change = false + if result.timer != nil: + clearTimeout(result.timer) + result.timer = setTimeout(docount, 30) + +method changed(c: Car): bool = true var images: seq[cstring] = @[cstring"a", "b", "c", "d"] -proc carousel*(key: VKey): VNode {.component.} = - state: - var counter: int = 0 - var cntdown: int = 5 - var timer: Timeout = nil - - proc docount() = - cntdown = cntdown - 1 - if cntdown == 0: - counter = (counter + 1) mod images.len - cntdown = 5 - else: - timer = setTimeout(docount, 30) - redraw() - - proc next(ev: Event; n: VNode) = - if timer != nil: - clearTimeout(timer) - timer = setTimeout(docount, 30) - +proc carousel*(): VNode = + let c = car(images) let col = - case counter + case c.counter of 0: cstring"#4d4d4d" of 1: cstring"#ff00ff" of 2: cstring"#00ffff" of 3: cstring"#ffff00" else: cstring"red" - - result = buildHtml(tdiv(key=key)): - text images[counter] - button(onclick = next): + result = buildHtml(tdiv(nref = c)): + text images[c.counter] + button(onclick = c.onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): text "This changes its color." - if cntdown != 5: - text &cntdown + if c.cntdown != ticksUntilChange: + text &c.cntdown proc createDom(): VNode = result = buildHtml(table): tr: td: - carousel(0) + carousel() td: - carousel(1) + carousel() tr: td: - carousel(2) + carousel() td: - carousel(3) + carousel() setRenderer createDom diff --git a/experiments/refs/demo.nim b/experiments/refs/demo.nim index 6ecc7db..a0db59f 100644 --- a/experiments/refs/demo.nim +++ b/experiments/refs/demo.nim @@ -2,7 +2,7 @@ include karaxprelude import future, sequtils type - CustomRef = ref object of VNodeRef + CustomRef = ref object of VComponent method onAttach(r: CustomRef) = kout(cstring"custom ref attached") @@ -13,10 +13,10 @@ method onDetach(r: CustomRef) = var modelData = @[5, 2, 4] - refA = VNodeRef() - refB = VNodeRef() + refA = VComponent() + refB = VComponent() refC = CustomRef() - refSeq = newSeq[VNodeRef]() + refSeq = newSeq[VComponent]() proc onClick(ev: Event, n: VNode) = modelData.add(0) @@ -25,11 +25,13 @@ proc showRefs() = kout(refA) kout(refB) kout(refC) - kout(refSeq.map(nref => nref.vnode)) + for i in 0 ..< refSeq.len: + kout(refSeq[i].vnode) + #kout(refSeq.map(nref => nref.vnode)) -proc secureRefSlot(i: int): VNodeRef = +proc secureRefSlot(i: int): VComponent = while refSeq.len <= i: - refSeq.add(VNodeRef()) + refSeq.add(VComponent()) result = refSeq[i] proc view(): VNode = @@ -53,4 +55,4 @@ proc view(): VNode = proc renderer(): VNode = view() -setRenderer(renderer, showRefs) +setRenderer(renderer, "ROOT", showRefs) diff --git a/src/karax.nim b/src/karax.nim index 77cebdf..56357ef 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -150,6 +150,9 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.id != b.id: return different result = identical if a.key != b.key: return different + if a.nref != nil and changed(a.nref): + kout cstring"yes, change!" + return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -201,6 +204,7 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = proc updateDom(newNode, oldNode: VNode) = newNode.dom = oldNode.dom + newNode.nref = oldNode.nref assert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) diff --git a/src/vdom.nim b/src/vdom.nim index 9f9c642..e478da1 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -101,8 +101,7 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VNodeRef* = ref object of RootObj - vnode*: VNode + VComponent* = ref object of RootObj EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int @@ -111,7 +110,7 @@ type kind*: VNodeKind key*: VKey id*, class*, text*: cstring - nref*: VNodeRef + nref*: VComponent kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] @@ -122,9 +121,9 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. -method onAttach*(v: VNodeRef) {.base.} = discard -method onDetach*(v: VNodeRef) {.base.} = discard -method changed*(v: VNodeRef): bool {.base.} = false +method onAttach*(v: VComponent) {.base.} = discard +method onDetach*(v: VComponent) {.base.} = discard +method changed*(v: VComponent): bool {.base.} = false proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 4801b8d8c115fec6301a4307cd18a791290fedcc Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 19 Jun 2017 21:25:34 +0200 Subject: [PATCH 067/264] new component example works --- experiments/example.nim | 79 +++++++++++++++++++++++------------------ src/karax.nim | 61 ++++++++++++++++++++----------- src/karaxdsl.nim | 2 +- src/vdom.nim | 22 ++++++++---- 4 files changed, 100 insertions(+), 64 deletions(-) diff --git a/experiments/example.nim b/experiments/example.nim index d48143d..5653e22 100644 --- a/experiments/example.nim +++ b/experiments/example.nim @@ -2,59 +2,68 @@ import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom type - Car = ref object of VComponent + Carousel = ref object of VComponent counter: int - cntdown: int + cntdown, myid: int timer: TimeOut list: seq[cstring] change: bool - onclick: proc (ev: Event; n: VNode) const ticksUntilChange = 5 -proc car*(images: seq[cstring]): Car = - new(result) - result.list = images - result.cntdown = ticksUntilChange - - proc docount() = - dec result.cntdown - if result.cntdown == 0: - result.counter = (result.counter + 1) mod result.list.len - result.cntdown = ticksUntilChange - else: - result.timer = setTimeout(docount, 30) - result.change = true - redraw() - - result.onclick = proc (ev: Event; n: VNode) = - result.change = false - if result.timer != nil: - clearTimeout(result.timer) - result.timer = setTimeout(docount, 30) - -method changed(c: Car): bool = true - var images: seq[cstring] = @[cstring"a", "b", "c", "d"] -proc carousel*(): VNode = - let c = car(images) +proc render(x: VComponent): VNode = + let self = Carousel(x) + + proc docount() = + dec self.cntdown + if self.cntdown == 0: + self.counter = (self.counter + 1) mod self.list.len + self.cntdown = ticksUntilChange + else: + self.timer = setTimeout(docount, 30) + self.change = true + redraw() + + proc onclick(ev: Event; n: VNode) = + self.change = false + if self.timer != nil: + clearTimeout(self.timer) + self.timer = setTimeout(docount, 30) + let col = - case c.counter + case self.counter of 0: cstring"#4d4d4d" of 1: cstring"#ff00ff" of 2: cstring"#00ffff" of 3: cstring"#ffff00" else: cstring"red" - result = buildHtml(tdiv(nref = c)): - text images[c.counter] - button(onclick = c.onclick): + result = buildHtml(tdiv()): + text self.list[self.counter] + button(onclick = onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): - text "This changes its color." - if c.cntdown != ticksUntilChange: - text &c.cntdown + text "This changes its color." & &self.myid + if self.cntdown != ticksUntilChange: + text &self.cntdown + +var gid: int + +proc carousel(): Carousel = + result = Carousel(kind: VNodeKind.component, key: -1) + result.render = render + result.list = images + result.changed = proc (c: VComponent): bool = + let x = Carousel(c) + result = x.change + result.cntdown = ticksUntilChange + result.myid = gid + inc gid + #result.onAttach = proc (_: VComponent) = + # result.cntdown = ticksUntilChange + # kout cstring"attached!" proc createDom(): VNode = result = buildHtml(table): diff --git a/src/karax.nim b/src/karax.nim index 56357ef..f15326d 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -80,14 +80,11 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = # --------------------- DOM diff ----------------------------------------- template detach(n: VNode) = - if not n.nref.isNil(): - n.nref.vnode = nil - n.nref.onDetach() + if n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onDetach != nil: x.onDetach(x) n.dom = nil template attach(n: VNode) = - if not n.nref.isNil(): - n.nref.vnode = n - n.nref.onAttach() n.dom = result proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = @@ -105,6 +102,13 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = #n.key = result.key attach n return result + elif n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onAttach != nil: x.onAttach(x) + assert x.render != nil + result = vnodeToDom(x.render(x), kxi) + attach n + return result else: result = document.createElement(toTag[n.kind]) attach n @@ -150,9 +154,6 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.id != b.id: return different result = identical if a.key != b.key: return different - if a.nref != nil and changed(a.nref): - kout cstring"yes, change!" - return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -160,6 +161,11 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.len != b.len: return different for i in 0..= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode[newPos], oldNode[oldPos]): + eqAndUpdate(newNode, newPos, oldNode[oldPos]): dec oldPos dec newPos var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi) + if updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi): + newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 while pos <= newPos: @@ -297,7 +316,7 @@ proc dodraw(kxi: KaraxInstance) = replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) #assert same(newtree, document.getElementById("ROOT")) if someDirty: updateDirtyElements(nil, olddom, newtree, kxi) @@ -363,7 +382,7 @@ proc setupErrorHandler*() = ## the error message is put into the console, otherwise `alert` ## is called. proc stackTraceAsCstring(): cstring = cstring(getStackTrace()) - var onerror {.importc: "window.onerror".} = + var onerror {.importc: "window.onerror", used.} = proc (msg, url: cstring, line, col: int, error: cstring): bool = var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() kout(x) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 07a336f..a3558c8 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style", "nref"] + SpecialAttrs = ["id", "class", "value", "key", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index e478da1..f25b1a7 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -8,6 +8,7 @@ type VNodeKind* {.pure.} = enum text = "#text", int = "#int", bool = "#bool", vthunk = "#vthunk", dthunk = "#dthunk", + component = "#component", html, head, title, base, link, meta, style, script, noscript, @@ -101,16 +102,13 @@ macro buildLookupTables(): untyped = buildLookupTables() type - VComponent* = ref object of RootObj - EventHandler* = proc (ev: Event; target: VNode) {.closure.} VKey* = int - VNode* = ref object + VNode* = ref object of RootObj kind*: VNodeKind key*: VKey id*, class*, text*: cstring - nref*: VComponent kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: attrs: seq[cstring] @@ -121,9 +119,11 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. -method onAttach*(v: VComponent) {.base.} = discard -method onDetach*(v: VComponent) {.base.} = discard -method changed*(v: VComponent): bool {.base.} = false + VComponent* = ref object of VNode + render*: proc(self: VComponent): VNode + changed*: proc(self: VComponent): bool + onAttach*: proc(self: VComponent) + onDetach*: proc(self: VComponent) proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,6 +143,13 @@ 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, + 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) + proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: n.attrs = @[key, val] @@ -160,6 +167,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] +proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) From c86916e37df24647cee1f4b24f565cd539e4a02a Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 20 Jun 2017 00:14:54 +0200 Subject: [PATCH 068/264] 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: From e58cbb78d20c94490a86337b5316a69ce0831884 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 08:21:05 +0200 Subject: [PATCH 069/264] fixes jstrutils.isInt --- src/jstrutils.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 0be4076..f0f22c0 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -19,7 +19,7 @@ proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} proc isInt*(s: cstring): bool {.asmNoStackFrame.} = asm """ - return s.match(/^[0-9]+$/); + return `s`.match(/^[0-9]+$/); """ proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} From d9648be60c21da7536359935f4daa802e8a3393d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 17:20:31 +0200 Subject: [PATCH 070/264] sane markDirty() mechanism for components --- experiments/carousel.html | 15 ++++++++++++ experiments/{example.nim => carousel.nim} | 11 +++------ experiments/{example.nims => carousel.nims} | 0 experiments/example.html | 15 ------------ src/karax.nim | 1 + src/vdom.nim | 26 +++++++++++++++++---- 6 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 experiments/carousel.html rename experiments/{example.nim => carousel.nim} (89%) rename experiments/{example.nims => carousel.nims} (100%) delete mode 100644 experiments/example.html diff --git a/experiments/carousel.html b/experiments/carousel.html new file mode 100644 index 0000000..979dd38 --- /dev/null +++ b/experiments/carousel.html @@ -0,0 +1,15 @@ + + + + + Carousel app + + + + +
+ + + + + diff --git a/experiments/example.nim b/experiments/carousel.nim similarity index 89% rename from experiments/example.nim rename to experiments/carousel.nim index fa1e739..a820be7 100644 --- a/experiments/example.nim +++ b/experiments/carousel.nim @@ -1,3 +1,4 @@ +## This demo shows how you can develop your own stateful components with Karax. import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom @@ -7,7 +8,6 @@ type cntdown, myid: int timer: TimeOut list: seq[cstring] - change: bool const ticksUntilChange = 5 @@ -24,11 +24,10 @@ proc render(x: VComponent): VNode = self.cntdown = ticksUntilChange else: self.timer = setTimeout(docount, 30) - self.change = true + markDirty(self) redraw() proc onclick(ev: Event; n: VNode) = - self.change = false if self.timer != nil: clearTimeout(self.timer) self.timer = setTimeout(docount, 30) @@ -51,12 +50,8 @@ proc render(x: VComponent): VNode = var gid: int -proc changed(c: VComponent): bool = - let x = Carousel(c) - result = x.change - proc carousel(): Carousel = - result = newComponent(Carousel, render, changed) + result = newComponent(Carousel, render) result.list = images result.cntdown = ticksUntilChange result.myid = gid diff --git a/experiments/example.nims b/experiments/carousel.nims similarity index 100% rename from experiments/example.nims rename to experiments/carousel.nims diff --git a/experiments/example.html b/experiments/example.html deleted file mode 100644 index 2303d95..0000000 --- a/experiments/example.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Todo app - - - - -
- - - - - diff --git a/src/karax.nim b/src/karax.nim index 42be93f..57c5162 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -225,6 +225,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) n = vnodeToDom(x.renderImpl(x), kxi) + x.updatedImpl(x) result = true else: detach(oldNode) diff --git a/src/vdom.nim b/src/vdom.nim index 8c19ea2..4edcd4e 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -119,11 +119,16 @@ type dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. - VComponent* = ref object of VNode + VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode changedImpl*: proc(self: VComponent): bool + updatedImpl*: proc(self: VComponent) onAttachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent) + version*: int ## Update this to trigger a redraw by karax. Usually you + ## should call 'markDirty' instead which is an alias for + ## 'inc version'. + renderedVersion*: int ## Do not touch. Used by karax. proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -143,15 +148,28 @@ 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 defaultChangedImpl*(v: VComponent): bool = + ## The default implementation of 'changed'. + result = v.version != v.renderedVersion + +proc defaultUpdatedImpl*(v: VComponent) = + v.renderedVersion = v.version + 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): T = + onDetach: proc(self: VComponent) = nil, + changed: (proc(self: VComponent): bool) = defaultChangedImpl, + updated: proc(self: VComponent) = defaultUpdatedImpl): 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) + changedImpl: changed, updatedImpl: updated, + onAttachImpl: onAttach, onDetachImpl: onDetach) + +template markDirty*(c: VComponent) = + ## mark the component as dirty so that it is re-rendered. + inc c.version proc setAttr*(n: VNode; key: cstring; val: cstring = "") = if n.attrs.isNil: From a535ad709ba566767287b5af2a92613c30b14517 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 17:57:02 +0200 Subject: [PATCH 071/264] carousel is now a real example --- {experiments => examples/carousel}/carousel.html | 0 {experiments => examples/carousel}/carousel.nim | 11 ++--------- examples/carousel/carousel.nims | 1 + experiments/carousel.nims | 1 - tests/tester.nim | 1 + 5 files changed, 4 insertions(+), 10 deletions(-) rename {experiments => examples/carousel}/carousel.html (100%) rename {experiments => examples/carousel}/carousel.nim (86%) create mode 100644 examples/carousel/carousel.nims delete mode 100644 experiments/carousel.nims diff --git a/experiments/carousel.html b/examples/carousel/carousel.html similarity index 100% rename from experiments/carousel.html rename to examples/carousel/carousel.html diff --git a/experiments/carousel.nim b/examples/carousel/carousel.nim similarity index 86% rename from experiments/carousel.nim rename to examples/carousel/carousel.nim index a820be7..c2db56c 100644 --- a/experiments/carousel.nim +++ b/examples/carousel/carousel.nim @@ -5,7 +5,7 @@ import vdom, vstyles, karax, karaxdsl, jdict, jstrutils, kdom type Carousel = ref object of VComponent counter: int - cntdown, myid: int + cntdown: int timer: TimeOut list: seq[cstring] @@ -44,21 +44,14 @@ proc render(x: VComponent): VNode = button(onclick = onclick): text "Next" tdiv(style = style(StyleAttr.color, col)): - text "This changes its color." & &self.myid + text "This changes its color." if self.cntdown != ticksUntilChange: text &self.cntdown -var gid: int - proc carousel(): Carousel = result = newComponent(Carousel, render) result.list = images result.cntdown = ticksUntilChange - result.myid = gid - inc gid - #result.onAttach = proc (_: VComponent) = - # result.cntdown = ticksUntilChange - # kout cstring"attached!" proc createDom(): VNode = result = buildHtml(table): diff --git a/examples/carousel/carousel.nims b/examples/carousel/carousel.nims new file mode 100644 index 0000000..1451fc0 --- /dev/null +++ b/examples/carousel/carousel.nims @@ -0,0 +1 @@ +--path: "../../src" diff --git a/experiments/carousel.nims b/experiments/carousel.nims deleted file mode 100644 index 6b5771c..0000000 --- a/experiments/carousel.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" diff --git a/tests/tester.nim b/tests/tester.nim index 5dc7426..e2d7cfd 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -11,5 +11,6 @@ proc main = exec("nim js examples/todoapp/todoapp.nim") exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") + exec("nim js examples/carousel/carousel.nim") main() From 8dbc1da708a7e48433f335e4b76e02a30e47e638 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 18:20:57 +0200 Subject: [PATCH 072/264] fixes #28 --- src/jstrutils.nim | 1 + src/vstyles.nim | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 63a9ae8..507e7a6 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -14,6 +14,7 @@ proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.} proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.} +proc `&`*(s: float): cstring {.importcpp: "((#)+'')", nodecl.} proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.} diff --git a/src/vstyles.nim b/src/vstyles.nim index 649c322..161c207 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -255,3 +255,11 @@ proc applyStyle*(n: Node; s: VStyle) = n.style = Style() for x in s.mask: n.style.setStyle(toStyleAttrName[x], s.attrs[x]) + +import jstrutils + +proc rgb*(r, g, b: range[0..255]): cstring = + cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" + +proc rgba*(r, g, b: range[0..255], alpha: float): cstring = + cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" From 67ee246f40b6c32f96d6bd208addd6ca5a4aa663 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 20:11:41 +0200 Subject: [PATCH 073/264] fixes new rgba proc --- experiments/Untitled Diagram.xml | 1 - src/vstyles.nim | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 experiments/Untitled Diagram.xml diff --git a/experiments/Untitled Diagram.xml b/experiments/Untitled Diagram.xml deleted file mode 100644 index a3019f8..0000000 --- a/experiments/Untitled Diagram.xml +++ /dev/null @@ -1 +0,0 @@ -5VhNc9owEP01PjYjS8bGx4bQ9tDOZIZDk6OwN7YaYTFCfPXXV7ZkbGHDEEJgMuXASE+rlfR230rgkdFs813Sef5LpMA9jNKNRx48jKOBr79LYGuAIB4aIJMsNZDfABP2FyyILLpkKSwcQyUEV2zugokoCkiUg1Epxdo1exHcXXVOM+gAk4TyLvqbpSo36BCHDf4DWJbXK/thbEamNHnNpFgWdj0Pk5fqY4ZntPZlD7rIaSrWLYiMPTKSQijTmm1GwEtqa9rMvG8HRnf7llCoUyZEdhtqWx8dUs2E7QqpcpGJgvJxg95Xx4PSAdK9XM24bvq6CRumnkr4Dg9s97keKpTcmrFB3X22Hv6AUlubA3SphIaahX8KMbc+XhjnI8GFrDaqeYUwSTS+UFK8QmskjeIpKj2bw5UnOkiPhRZiKRNrhW3CUZmBtSK70OiMBzEDvX1tIoFTxVaud2pzL9vZNfzrhg1BfziGHxOO6Eg4AuwE5Au6Q35UI48gmT4EyM8TquBKobJLryhf7hWvVvTWOVMwmdNqv2tdKt0InUvTCqSCzXGiuhTUE4jdp63Nse2um0Ln1+UpbxW5EL2ftJqiFmseDrle4T5lK93MVHVIA03lPqL999j9D1Nny4XSxBX6Yug6mcLJ6+kLR+picfmTHalbhShOKlR1+9m9J57sjObSqMbOrUXDBPpFdj9EEYrOrkXk8rWomvpVSrptGcwFK9Si5fmxBFr6jl1948HeE2DPfifKfnvdMDtoBL47ykmaJ91C6WE9C5FPUS7DPXbwNcul36HoHF0hR1fYEZbvCgu/RVnn6SS43fMq6L2zy1z08adIRkJumY1Rz+X9jvT82DSLu2nmh1fKM//4O/5k3R79HRUH2NUuIuQNrPa+7y9G9bUUHR9UdPDm1/gF9BmgG76tw6PV7XacDK9Xs3S3+e/EvFWa/6fI+B8= \ No newline at end of file diff --git a/src/vstyles.nim b/src/vstyles.nim index 161c207..c5a0931 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -262,4 +262,4 @@ proc rgb*(r, g, b: range[0..255]): cstring = cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" proc rgba*(r, g, b: range[0..255], alpha: float): cstring = - cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" + cstring"rgba(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" From 8a2b893d5ed31bf01faba73de9fbd877e99f120d Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Thu, 22 Jun 2017 01:09:52 +0700 Subject: [PATCH 074/264] Added onfocus (#29) --- src/vdom.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vdom.nim b/src/vdom.nim index 4edcd4e..be4fb5d 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -47,6 +47,7 @@ type onkeyup, ## A key was released. onkeydown, ## A key is pressed. onkeypressed, # A key was pressed. + onfocus, ## An element got the focus. onblur, ## An element lost the focus. onchange, ## The selected value of an element was changed. onscroll, ## The user scrolled within an element. From bd2d8e7d3f248306ab890859f4c08fc03ba1b632 Mon Sep 17 00:00:00 2001 From: Pavel Nuzhdin Date: Thu, 22 Jun 2017 01:11:54 +0700 Subject: [PATCH 075/264] Added pointer-events style (#30) --- src/kdom.nim | 1 + src/vstyles.nim | 1 + 2 files changed, 2 insertions(+) diff --git a/src/kdom.nim b/src/kdom.nim index f933b87..607d882 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -258,6 +258,7 @@ type paddingTop*: cstring pageBreakAfter*: cstring pageBreakBefore*: cstring + pointerEvents*: cstring position*: cstring right*: cstring scrollbar3dLightColor*: cstring diff --git a/src/vstyles.nim b/src/vstyles.nim index c5a0931..252dda4 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -150,6 +150,7 @@ type pageBreakInside perspective perspectiveOrigin + pointerEvents position quotes resize From 051e9985b14496e8951edd59402e01049956daa4 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 21 Jun 2017 21:08:20 +0200 Subject: [PATCH 076/264] added 'karun' tool for convenient testing of karax apps --- tools/karun.nim | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tools/karun.nim diff --git a/tools/karun.nim b/tools/karun.nim new file mode 100644 index 0000000..dd498ea --- /dev/null +++ b/tools/karun.nim @@ -0,0 +1,41 @@ +## Simple tool to quickly run Karax applications. Generates the HTML +## required to run a Karax app and opens it in a browser. + +import os, strutils, parseopt, browsers + +const + html = """ + + +$1 + +
+ + + +""" + +proc exec(cmd: string) = + if os.execShellCmd(cmd) != 0: + quit "External command failed: " & cmd + +proc main = + var op = initOptParser() + let rest = op.cmdLineRest + var file = "" + while true: + op.next() + case op.kind + of cmdLongOption, cmdShortOption: discard + of cmdArgument: file = op.key + of cmdEnd: break + + if file.len == 0: quit "filename expected" + let name = file.splitFile.name + createDir("nimcache") + exec("nim js --out:nimcache/" & name & ".js " & rest) + let dest = "nimcache" / name & ".html" + writeFile(dest, html % name) + openDefaultBrowser(dest) + +main() From c8030bc1853ba38e1d1a152f4a696bc78fba0953 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 09:17:19 +0200 Subject: [PATCH 077/264] diff algorithm also diffs components' expansions --- src/karax.nim | 27 ++++++++++++++++++++------- src/vdom.nim | 2 ++ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 57c5162..788cd61 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -177,8 +177,7 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - # XXX Fixme: smart diffing still is broken for complex apps: - if a.style != b.style: return different # similar + if a.style != b.style: return similar # Do not test event listeners here! return result @@ -206,7 +205,10 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = else: oldNode.dom.style = Style() oldNode.style = newNode.style if deep: - assert newNode.len == oldNode.len + if newNode.len != oldNode.len: + kout cstring"argh ", newNode.len, " ", oldNode.len + kout newNode, oldNode + doAssert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateStyles(newNode[i], oldNode[i], deep) @@ -224,8 +226,18 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if res == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) - n = vnodeToDom(x.renderImpl(x), kxi) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) x.updatedImpl(x) + if oldExpanded == nil: + n = vnodeToDom(x.expanded, kxi) + else: + if updateElement(parent, current, x.expanded, oldExpanded, kxi): + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" + else: + n = oldExpanded.dom + doAssert n != nil, "old expanded.dom is nil" result = true else: detach(oldNode) @@ -234,10 +246,10 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) + elif res == similar: + updateStyles(newNode, oldNode, false) else: - if res == similar: updateStyles(newNode, oldNode, false) newNode.dom = oldNode.dom - if newNode.kind != VNodeKind.text: let newLength = newNode.len var oldLength = oldNode.len @@ -266,8 +278,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; of different, changed: break of similar: #updateDom(a, b) + updateStyles(a[i], b, true) a[i] = b - #updateStyles(a, b, true) action while commonPrefix < minLength: @@ -299,6 +311,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; for i in pos..oldPos: detach(oldNode[i]) + doAssert pos < current.childNodes.len current.removeChild(current.childNodes[pos]) when false: diff --git a/src/vdom.nim b/src/vdom.nim index 4edcd4e..c11d532 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -129,6 +129,8 @@ type ## should call 'markDirty' instead which is an alias for ## 'inc version'. renderedVersion*: int ## Do not touch. Used by karax. + expanded*: VNode ## Do not touch. Used by karax. The VDOM the component + ## expanded to. proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v From 79270c3423a4e690f74c92f55a37c49310bc9c1b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 09:34:44 +0200 Subject: [PATCH 078/264] fixes #31 --- src/karax.nim | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 788cd61..c03ddbe 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -219,11 +219,11 @@ proc updateDom(newNode, oldNode: VNode) = updateDom(newNode[i], oldNode[i]) proc updateElement(parent, current: Node, newNode, oldNode: VNode; - kxi: KaraxInstance): bool = - let res = eq(newNode, oldNode, deep=false) - if res <= different: + kxi: KaraxInstance): EqResult = + result = eq(newNode, oldNode, deep=false) + if result <= different: var n: Node - if res == changed: + if result == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) let oldExpanded = x.expanded @@ -232,13 +232,13 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if oldExpanded == nil: n = vnodeToDom(x.expanded, kxi) else: - if updateElement(parent, current, x.expanded, oldExpanded, kxi): - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - else: + if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: + x.expanded = oldExpanded n = oldExpanded.dom doAssert n != nil, "old expanded.dom is nil" - result = true + else: + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" else: detach(oldNode) n = vnodeToDom(newNode, kxi) @@ -246,7 +246,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; replaceById(kxi.rootId, n) else: parent.replaceChild(n, current) - elif res == similar: + elif result == similar: updateStyles(newNode, oldNode, false) else: newNode.dom = oldNode.dom @@ -295,7 +295,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - if updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi): + if updateElement(current, current.childNodes[i], + newNode[i], oldNode[i], kxi) >= similar: newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 From 9aa3ba545ded60775e473529731444a6d3b69acb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 22 Jun 2017 21:21:54 +0200 Subject: [PATCH 079/264] wip --- src/karax.nim | 46 ++++++++++++++++++++++++++++++++------- src/vdom.nim | 1 + src/vstyles.nim | 12 ++++++++-- tests/blur.nim | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ tools/karun.nim | 14 +++++++++--- 5 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 tests/blur.nim diff --git a/src/karax.nim b/src/karax.nim index c03ddbe..01e5c0f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -106,7 +106,10 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = let x = VComponent(n) if x.onAttachImpl != nil: x.onAttachImpl(x) assert x.renderImpl != nil - result = vnodeToDom(x.renderImpl(x), kxi) + if x.expanded == nil: + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + result = vnodeToDom(x.expanded, kxi) attach n return result else: @@ -133,7 +136,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = if not n.style.isNil: applyStyle(result, n.style) proc same(n: VNode, e: Node): bool = - if toTag[n.kind] == e.nodename: + if n.kind == VNodeKind.component: + result = same(VComponent(n).expanded, e) + elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: if e.len != n.len: return false @@ -177,7 +182,9 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if a.style != b.style: return similar + if not eq(a.style, b.style): + kout cstring"yes, styles differ" + return similar # Do not test event listeners here! return result @@ -218,11 +225,22 @@ proc updateDom(newNode, oldNode: VNode) = for i in 0 ..< newNode.len: updateDom(newNode[i], oldNode[i]) +proc printV(n: VNode; depth: cstring = "") = + kout depth, cstring($n.kind), n.myid, cstring"key ", n.key + #for k, v in pairs(n.style): + # kout depth, "style: ", k, v + if n.kind == VNodeKind.component: + let nn = VComponent(n) + if nn.expanded != nil: printV(nn.expanded, ">>" & depth) + for i in 0 ..< n.len: + printV(n[i], depth & " ") + proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): EqResult = result = eq(newNode, oldNode, deep=false) if result <= different: var n: Node + var state = 0 if result == changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) @@ -231,7 +249,11 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; x.updatedImpl(x) if oldExpanded == nil: n = vnodeToDom(x.expanded, kxi) + state = 1 else: + kout cstring"now comparing components" + printV(oldExpanded) + printV(x.expanded) if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: x.expanded = oldExpanded n = oldExpanded.dom @@ -239,12 +261,16 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; else: n = x.expanded.dom doAssert n != nil, "expanded.dom is nil" + state = 2 + return else: detach(oldNode) n = vnodeToDom(newNode, kxi) + state = 3 if parent == nil: replaceById(kxi.rootId, n) else: + kout cstring"state ", state, parent, current parent.replaceChild(n, current) elif result == similar: updateStyles(newNode, oldNode, false) @@ -295,8 +321,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - if updateElement(current, current.childNodes[i], - newNode[i], oldNode[i], kxi) >= similar: + let res = updateElement(current, current.childNodes[i], + newNode[i], oldNode[i], kxi) + if res != different: newNode[i] = oldNode[i] var nextChildPos = oldPos + 1 @@ -323,6 +350,9 @@ proc dodraw(kxi: KaraxInstance) = let newtree = kxi.renderer() newtree.id = kxi.rootId kxi.toFocus = nil + #if kxi.currentTree != nil: + # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) + kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) @@ -330,8 +360,8 @@ proc dodraw(kxi: KaraxInstance) = else: let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) - #assert same(newtree, document.getElementById("ROOT")) kxi.currentTree = newtree + #kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -348,10 +378,10 @@ proc redraw*(kxi: KaraxInstance = kxi) = if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) - elif true: + elif false: reqFrame(proc () = kxi.dodraw) else: - dodraw() + dodraw(kxi) proc init(ev: Event) = reqFrame(proc () = kxi.dodraw) diff --git a/src/vdom.nim b/src/vdom.nim index f022b7f..16b0f36 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -119,6 +119,7 @@ type style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. + myid*: int VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode diff --git a/src/vstyles.nim b/src/vstyles.nim index 252dda4..a6f1996 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -213,7 +213,9 @@ type attrs: array[StyleAttr, cstring] mask: set[StyleAttr] -proc `==`*(a, b: VStyle): bool = +proc kout[T](x: T) {.importc: "console.log", varargs.} + +proc eq*(a, b: VStyle): bool = if a.isNil: if b.isNil: return true else: return false @@ -247,9 +249,10 @@ proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #".} proc merge*(a, b: VStyle): VStyle = ## merges two styles. ``b`` takes precedence over ``a``. - result = VStyle(mask: a.mask + b.mask) + result = VStyle(mask: {}) for i in low(StyleAttr)..high(StyleAttr): result.attrs[i] = if b.attrs[i].isNil: a.attrs[i] else: b.attrs[i] + if result.attrs[i] != nil: incl(result.mask, i) proc applyStyle*(n: Node; s: VStyle) = ## apply the style to the real DOM node ``n``. @@ -257,6 +260,11 @@ proc applyStyle*(n: Node; s: VStyle) = for x in s.mask: n.style.setStyle(toStyleAttrName[x], s.attrs[x]) +iterator pairs*(v: VStyle): (cstring, cstring) = + if v != nil: + for x in v.mask: + yield (toStyleAttrName[x], v.attrs[x]) + import jstrutils proc rgb*(r, g, b: range[0..255]): cstring = diff --git a/tests/blur.nim b/tests/blur.nim new file mode 100644 index 0000000..062c47f --- /dev/null +++ b/tests/blur.nim @@ -0,0 +1,58 @@ +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils + +type TextInput* = ref object of VComponent + value: cstring + isActive: bool + +var renderId: int +proc render(x: VComponent): VNode = + let self = TextInput(x) + + let style = style( + (StyleAttr.position, cstring"relative"), + (StyleAttr.paddingLeft, cstring"10px"), + (StyleAttr.paddingRight, cstring"5px"), + (StyleAttr.height, cstring"30px"), + (StyleAttr.lineHeight, cstring"30px"), + (StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ).merge(self.style) + + let inputStyle = style.merge(style( + (StyleAttr.color, cstring"inherit"), + (StyleAttr.fontSize, cstring"inherit"), + (StyleAttr.fontWeight, cstring"inherit"), + (StyleAttr.fontFamily, cstring"inherit"), + (StyleAttr.position, cstring"absolute"), + (StyleAttr.top, cstring"0"), + (StyleAttr.left, cstring"0"), + (StyleAttr.height, cstring"100%"), + (StyleAttr.width, cstring"100%"), + (StyleAttr.border, cstring"none"), + (StyleAttr.backgroundColor, cstring"transparent"), + )) + + proc flip(ev: Event; n: VNode) = + self.isActive = not self.isActive + markDirty(self) + + kout cstring"rendering ", self.myid + inc renderId + result = buildHtml(tdiv(style=style, key=renderId)): + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, + key=renderId) + +var gid = 0 +proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = + result = newComponent(TextInput, render) + result.style = style + result.value = value + inc gid + result.myid = gid + +proc createDom(): VNode = + result = buildHtml(tdiv): + newTextInput(value=cstring"test") + +setRenderer createDom diff --git a/tools/karun.nim b/tools/karun.nim index dd498ea..a67b9f5 100644 --- a/tools/karun.nim +++ b/tools/karun.nim @@ -21,12 +21,20 @@ proc exec(cmd: string) = proc main = var op = initOptParser() - let rest = op.cmdLineRest + var rest = op.cmdLineRest var file = "" + var run = false while true: op.next() case op.kind - of cmdLongOption, cmdShortOption: discard + of cmdLongOption: + if op.key == "run": + run = true + rest = rest.replace("--run ") + of cmdShortOption: + if op.key == "r": + run = true + rest = rest.replace("-r ") of cmdArgument: file = op.key of cmdEnd: break @@ -36,6 +44,6 @@ proc main = exec("nim js --out:nimcache/" & name & ".js " & rest) let dest = "nimcache" / name & ".html" writeFile(dest, html % name) - openDefaultBrowser(dest) + if run: openDefaultBrowser(dest) main() From cd74cd9d4e31589c21b7012da9894ce2598d93d3 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 11:09:22 +0200 Subject: [PATCH 080/264] another attempt to fix bug #31 --- src/karax.nim | 39 ++++++++++++++++++++++++--------------- tests/blur.nim | 5 ++--- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 01e5c0f..07d4fd5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -182,9 +182,7 @@ proc eq(a, b: VNode; deep: bool): EqResult = result = similar if not sameAttrs(a, b): return different if a.class != b.class: return different - if not eq(a.style, b.style): - kout cstring"yes, styles differ" - return similar + if not eq(a.style, b.style): return similar # Do not test event listeners here! return result @@ -212,9 +210,6 @@ proc updateStyles(newNode, oldNode: VNode; deep: bool) = else: oldNode.dom.style = Style() oldNode.style = newNode.style if deep: - if newNode.len != oldNode.len: - kout cstring"argh ", newNode.len, " ", oldNode.len - kout newNode, oldNode doAssert newNode.len == oldNode.len for i in 0 ..< newNode.len: updateStyles(newNode[i], oldNode[i], deep) @@ -232,6 +227,8 @@ proc printV(n: VNode; depth: cstring = "") = if n.kind == VNodeKind.component: let nn = VComponent(n) if nn.expanded != nil: printV(nn.expanded, ">>" & depth) + elif n.kind == VNodeKind.text: + kout depth, n.text for i in 0 ..< n.len: printV(n[i], depth & " ") @@ -251,17 +248,22 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; n = vnodeToDom(x.expanded, kxi) state = 1 else: - kout cstring"now comparing components" - printV(oldExpanded) - printV(x.expanded) - if updateElement(parent, current, x.expanded, oldExpanded, kxi) >= similar: + let res = updateElement(parent, current, x.expanded, oldExpanded, kxi) + if res != different: x.expanded = oldExpanded n = oldExpanded.dom doAssert n != nil, "old expanded.dom is nil" + #kout cstring"produced old DOM: ", cstring($res) + #printV(oldExpanded) + #printV(x.expanded) + return else: n = x.expanded.dom doAssert n != nil, "expanded.dom is nil" state = 2 + #kout cstring"produced new DOM:" + #printV(oldExpanded) + #printV(x.expanded) return else: detach(oldNode) @@ -270,8 +272,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; if parent == nil: replaceById(kxi.rootId, n) else: - kout cstring"state ", state, parent, current - parent.replaceChild(n, current) + #kout cstring"state ", state, parent, current + if n != current: + parent.replaceChild(n, current) elif result == similar: updateStyles(newNode, oldNode, false) else: @@ -301,7 +304,9 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; a[i] = b #updateDom(a, b) action - of different, changed: break + of different, changed: + if result != different: result = r + break of similar: #updateDom(a, b) updateStyles(a[i], b, true) @@ -325,6 +330,8 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; newNode[i], oldNode[i], kxi) if res != different: newNode[i] = oldNode[i] + else: + result = different var nextChildPos = oldPos + 1 while pos <= newPos: @@ -336,11 +343,13 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode; inc oldLength inc pos inc nextChildPos + result = different for i in pos..oldPos: detach(oldNode[i]) doAssert pos < current.childNodes.len current.removeChild(current.childNodes[pos]) + result = different when false: var drawTimeout: Timeout @@ -352,7 +361,7 @@ proc dodraw(kxi: KaraxInstance) = kxi.toFocus = nil #if kxi.currentTree != nil: # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) - kout cstring"dodraw -----------------------------" + #kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) @@ -361,7 +370,7 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) kxi.currentTree = newtree - #kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() diff --git a/tests/blur.nim b/tests/blur.nim index 062c47f..74b8ba1 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -39,9 +39,8 @@ proc render(x: VComponent): VNode = kout cstring"rendering ", self.myid inc renderId - result = buildHtml(tdiv(style=style, key=renderId)): - input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, - key=renderId) + result = buildHtml(tdiv(style=style)): + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip) var gid = 0 proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = From 64bc900456d292a282bd0f70fa1f693a149bd8e6 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 21:03:31 +0200 Subject: [PATCH 081/264] rewritten diff algorithm --- src/karax.nim | 147 +++++++++++++++++++++++++++++++++++++++++++-- src/kdom.nim | 94 +++++++++++++++++++++++++---- src/vdom.nim | 58 +++++++++--------- tests/difftest.nim | 37 ++++++++++++ tests/lists.nim | 18 ++++++ 5 files changed, 310 insertions(+), 44 deletions(-) create mode 100644 tests/difftest.nim create mode 100644 tests/lists.nim diff --git a/src/karax.nim b/src/karax.nim index 07d4fd5..cdb68f7 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -141,9 +141,13 @@ proc same(n: VNode, e: Node): bool = elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: - if e.len != n.len: return false + if e.len != n.len: + kout e.len, n.len + return false for i in 0 ..< n.len: if not same(n[i], e[i]): return false + else: + kout toTag[n.kind], e.nodename proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -232,6 +236,134 @@ 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, n: Node + +proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current, n: Node) = + patches.add(Patch(k: k, parent: parent, current: current, n: n)) + +proc apply(patches: seq[Patch]; kxi: KaraxInstance) = + for p in patches: + case p.k + of pkReplace: + if p.parent == nil: + replaceById(kxi.rootId, p.n) + else: + if p.n != p.current: + p.parent.replaceChild(p.n, p.current) + of pkRemove: + p.parent.removeChild(p.current) + of pkAppend: + p.parent.appendChild(p.n) + of pkInsertBefore: + p.parent.insertBefore(p.n, p.current) + +proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; + kxi: KaraxInstance): EqResult = + result = eq(newNode, oldNode, deep=false) + if result <= different: + var n: Node + if result == changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + if oldExpanded == nil: + n = vnodeToDom(x.expanded, kxi) + else: + let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) + if res != different: + x.expanded = oldExpanded + n = oldExpanded.dom + doAssert n != nil, "old expanded.dom is nil" + else: + n = x.expanded.dom + doAssert n != nil, "expanded.dom is nil" + return + else: + detach(oldNode) + n = vnodeToDom(newNode, kxi) + patches.addPatch(pkReplace, parent, current, n) + elif result == similar: + updateStyles(newNode, oldNode, false) + else: + newNode.dom = oldNode.dom + let newLength = newNode.len + var oldLength = oldNode.len + let minLength = min(newLength, oldLength) + + assert oldNode.kind == newNode.kind + var commonPrefix = 0 + + template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = + let oldLen = patches.len + if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + assert current != nil + assert current.childNodes[j] != nil, $info + assert oldNode.len == current.len + + let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + diff(parent, current, a[i], b[j], patches, kxi) + else: + diff(current, current.childNodes[j], a[i], b[j], patches, kxi) + case r + of identical, changed: + a[i] = b[j] + action + of different: + # undo what 'diff' would have done: + setLen(patches, oldLen) + if result != different: result = r + break + of similar: + updateStyles(a[i], b[j], true) + a[i] = b[j] + action + + while commonPrefix < minLength: + eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): + inc commonPrefix + + var oldPos = oldLength - 1 + var newPos = newLength - 1 + while oldPos >= commonPrefix and newPos >= commonPrefix: + eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): + dec oldPos + dec newPos + + var pos = min(oldPos, newPos) + 1 + for i in commonPrefix..pos-1: + let res = diff(current, current.childNodes[i], + newNode[i], oldNode[i], patches, kxi) + if res != different: + newNode[i] = oldNode[i] + else: + result = different + + var nextChildPos = oldPos + 1 + if nextChildPos == oldLength: + for i in pos..newPos: + patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) + result = different + else: + let before = current.childNodes[nextChildPos] + for i in pos..newPos: + patches.addPatch(pkInsertBefore, current, before, + vnodeToDom(newNode[i], kxi)) + 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) + result = different + + proc updateElement(parent, current: Node, newNode, oldNode: VNode; kxi: KaraxInstance): EqResult = result = eq(newNode, oldNode, deep=false) @@ -368,9 +500,14 @@ proc dodraw(kxi: KaraxInstance) = replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + when false: + discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) + else: + var patches: seq[Patch] = @[] + discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) + patches.apply(kxi) kxi.currentTree = newtree - doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -387,7 +524,7 @@ proc redraw*(kxi: KaraxInstance = kxi) = if drawTimeout != nil: clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) - elif false: + elif true: reqFrame(proc () = kxi.dodraw) else: dodraw(kxi) @@ -442,7 +579,7 @@ proc prepend(parent, kid: Element) = parent.insertBefore(kid, parent.firstChild) proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = - let body = getElementById("body") + let body = document.getElementById("body") let s = document.createElement("script") s.setAttr "type", "text/javascript" s.setAttr "src", jsfilename diff --git a/src/kdom.nim b/src/kdom.nim index 607d882..6799976 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -94,6 +94,8 @@ type previousSibling*: Node innerHTML*: cstring style*: Style + when defined(nodejs): + id*: cstring Document* = ref DocumentObj DocumentObj {.importc.} = object of NodeObj @@ -395,12 +397,87 @@ type TimeOut* {.importc.} = ref object of RootObj Interval* {.importc.} = object of RootObj -proc len*(x: Node): int {.importcpp: "#.childNodes.length".} -proc `[]`*(x: Node; idx: int): Element {.importcpp: "#.childNodes[#]".} +when defined(nodejs): + # we provide a dummy DOM for nodejs for testing purposes + proc len*(x: Node): int = x.childNodes.len + proc `[]`*(x: Node; idx: int): Element = + assert idx >= 0 and idx < x.childNodes.len + result = cast[Element](x.childNodes[idx]) + + var document* = Document(nodeType: DocumentNode) + + proc getElem(x: Element; id: cstring): Element = + if x.id == id: return x + for i in 0.." -proc calcHash*(n: VNode) = - if n.validHash: return - n.validHash = true - var h: Hash = ord n.kind - if n.id != nil: - h &= "id" - h &= n.id - if n.class != nil: - h &= "class" - h &= n.class - if n.key >= 0: - h &= "k" - h &= n.key - for k, v in attrs(n): - h &= " " - h &= k - h &= "=" - h &= v - if n.kind == VNodeKind.text or n.text != nil: - h &= "t" - h &= n.text - else: - for child in items(n): - calcHash(child) - h &= child.hash - n.hash = h +when false: + proc calcHash*(n: VNode) = + if n.validHash: return + n.validHash = true + var h: Hash = ord n.kind + if n.id != nil: + h &= "id" + h &= n.id + if n.class != nil: + h &= "class" + h &= n.class + if n.key >= 0: + h &= "k" + h &= n.key + for k, v in attrs(n): + h &= " " + h &= k + h &= "=" + h &= v + if n.kind == VNodeKind.text or n.text != nil: + h &= "t" + h &= n.text + else: + for child in items(n): + calcHash(child) + h &= child.hash + n.hash = h proc `$`*(n: VNode): cstring = var res = "" diff --git a/tests/difftest.nim b/tests/difftest.nim new file mode 100644 index 0000000..9f80037 --- /dev/null +++ b/tests/difftest.nim @@ -0,0 +1,37 @@ + +include "../src/karax" +import "../src/karaxdsl" + +proc doDiff(a, b: VNode) = + var patches: seq[Patch] = @[] + echo diff(nil, vnodeToDom(a, kxi), b, a, patches, kxi) + echo patches + +proc testAppend() = + let a = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + let b = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + li: text "C" + doDiff(a, b) + +proc testInsert() = + let a = buildHtml(tdiv): + ul: + li: text "A" + li: text "C" + let b = buildHtml(tdiv): + ul: + li: text "A" + li: text "B" + li: text "C" + doDiff(a, b) + +kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) + +#testAppend() +testInsert() diff --git a/tests/lists.nim b/tests/lists.nim new file mode 100644 index 0000000..cce9abf --- /dev/null +++ b/tests/lists.nim @@ -0,0 +1,18 @@ +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils + +var contents: seq[cstring] = @[] + +proc onTodoEnter(e: Event; n: VNode) = + contents.insert(n.value & "BBBB") + contents.insert(n.value) + n.value = "" + +proc createDom(): VNode = + result = buildHtml(tdiv): + input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", + onkeyupenter = onTodoEnter, setFocus) + for c in contents: + tdiv: + text c + +setRenderer createDom From f89da765fab9a45873374e1f59015dd1065ba08b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 21:33:48 +0200 Subject: [PATCH 082/264] another bugfix for smart-diffing --- src/karax.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/karax.nim b/src/karax.nim index cdb68f7..36a0506 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -321,7 +321,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch if result != different: result = r break of similar: - updateStyles(a[i], b[j], true) + updateStyles(a[i], b[j], false) a[i] = b[j] action From 74c07f063444ebfeebd20345da4923c20734c32d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 23 Jun 2017 22:21:06 +0200 Subject: [PATCH 083/264] code cleanups --- src/karax.nim | 256 +++++++++----------------------------------------- 1 file changed, 45 insertions(+), 211 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 36a0506..00863c6 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -152,13 +152,12 @@ proc same(n: VNode, e: Node): bool = proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) - #newTree.id = id type EqResult = enum changed, different, similar, identical -proc eq(a, b: VNode; deep: bool): EqResult = +proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical @@ -169,60 +168,25 @@ proc eq(a, b: VNode; deep: bool): EqResult = if a.text != b.text: return different if a.len != b.len: return different for i in 0..= 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) = +proc updateStyles(newNode, oldNode: VNode) = # we keep the oldNode, but take over the style from the new node: if oldNode.dom != nil: if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) else: oldNode.dom.style = Style() oldNode.style = newNode.style - if deep: - doAssert newNode.len == oldNode.len - for i in 0 ..< newNode.len: - updateStyles(newNode[i], oldNode[i], deep) - -proc updateDom(newNode, oldNode: VNode) = - newNode.dom = oldNode.dom - assert newNode.len == oldNode.len - for i in 0 ..< newNode.len: - updateDom(newNode[i], oldNode[i]) proc printV(n: VNode; depth: cstring = "") = kout depth, cstring($n.kind), n.myid, cstring"key ", n.key @@ -264,55 +228,32 @@ proc apply(patches: seq[Patch]; kxi: KaraxInstance) = proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; kxi: KaraxInstance): EqResult = - result = eq(newNode, oldNode, deep=false) - if result <= different: - var n: Node - if result == changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.updatedImpl(x) - if oldExpanded == nil: - n = vnodeToDom(x.expanded, kxi) - else: - let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) - if res != different: - x.expanded = oldExpanded - n = oldExpanded.dom - doAssert n != nil, "old expanded.dom is nil" - else: - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - return - else: - detach(oldNode) - n = vnodeToDom(newNode, kxi) - patches.addPatch(pkReplace, parent, current, n) - elif result == similar: - updateStyles(newNode, oldNode, false) - else: + result = eq(newNode, oldNode) + case result + of identical: newNode.dom = oldNode.dom let newLength = newNode.len var oldLength = oldNode.len let minLength = min(newLength, oldLength) + if minLength == 0: return result assert oldNode.kind == newNode.kind var commonPrefix = 0 template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = let oldLen = patches.len - if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: - assert current != nil - assert current.childNodes[j] != nil, $info - assert oldNode.len == current.len + when false: + if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + assert current != nil + assert current.childNodes[j] != nil, $info + assert oldNode.len == current.len let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: diff(parent, current, a[i], b[j], patches, kxi) else: diff(current, current.childNodes[j], a[i], b[j], patches, kxi) case r - of identical, changed: + of identical, changed, similar: a[i] = b[j] action of different: @@ -320,10 +261,10 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch setLen(patches, oldLen) if result != different: result = r break - of similar: - updateStyles(a[i], b[j], false) - a[i] = b[j] - action + #of similar: + # updateStyles(a[i], b[j]) + # a[i] = b[j] + # action while commonPrefix < minLength: eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): @@ -338,20 +279,18 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch var pos = min(oldPos, newPos) + 1 for i in commonPrefix..pos-1: - let res = diff(current, current.childNodes[i], - newNode[i], oldNode[i], patches, kxi) - if res != different: + if diff(current, current.childNodes[i], + newNode[i], oldNode[i], patches, kxi) != different: newNode[i] = oldNode[i] else: result = different - var nextChildPos = oldPos + 1 - if nextChildPos == oldLength: + if oldPos + 1 == oldLength: for i in pos..newPos: patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) result = different else: - let before = current.childNodes[nextChildPos] + let before = current.childNodes[oldPos + 1] for i in pos..newPos: patches.addPatch(pkInsertBefore, current, before, vnodeToDom(newNode[i], kxi)) @@ -359,153 +298,48 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) - doAssert i < current.childNodes.len + #doAssert i < current.childNodes.len patches.addPatch(pkRemove, current, current.childNodes[i], nil) result = different - -proc updateElement(parent, current: Node, newNode, oldNode: VNode; - kxi: KaraxInstance): EqResult = - result = eq(newNode, oldNode, deep=false) - if result <= different: - var n: Node - var state = 0 - if result == changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.updatedImpl(x) - if oldExpanded == nil: - n = vnodeToDom(x.expanded, kxi) - state = 1 - else: - let res = updateElement(parent, current, x.expanded, oldExpanded, kxi) - if res != different: - x.expanded = oldExpanded - n = oldExpanded.dom - doAssert n != nil, "old expanded.dom is nil" - #kout cstring"produced old DOM: ", cstring($res) - #printV(oldExpanded) - #printV(x.expanded) - return - else: - n = x.expanded.dom - doAssert n != nil, "expanded.dom is nil" - state = 2 - #kout cstring"produced new DOM:" - #printV(oldExpanded) - #printV(x.expanded) - return - else: + of similar: + updateStyles(newNode, oldNode) + of changed: + assert oldNode.kind == VNodeKind.component + let x = VComponent(oldNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.updatedImpl(x) + if oldExpanded == nil: detach(oldNode) - n = vnodeToDom(newNode, kxi) - state = 3 - if parent == nil: - replaceById(kxi.rootId, n) + let n = vnodeToDom(x.expanded, kxi) + patches.addPatch(pkReplace, parent, current, n) else: - #kout cstring"state ", state, parent, current - if n != current: - parent.replaceChild(n, current) - elif result == similar: - updateStyles(newNode, oldNode, false) - else: - newNode.dom = oldNode.dom - if newNode.kind != VNodeKind.text: - let newLength = newNode.len - var oldLength = oldNode.len - let minLength = min(newLength, oldLength) - assert oldNode.kind == newNode.kind - when defined(simpleDiff): - for i in 0..min(newLength, oldLength)-1: - updateElement(current, current[i], newNode[i], oldNode[i], kxi) - if newLength > oldLength: - for i in oldLength..newLength-1: - current.appendChild(vnodeToDom(newNode[i])) - elif oldLength > newLength: - for i in countdown(oldLength-1, newLength): - detach(oldNode[i]) - current.removeChild(current.lastChild) + let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) + if res != different: + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" else: - var commonPrefix = 0 - - template eqAndUpdate(a: VNode; i: int; b: VNode; action: untyped) = - let r = eq(a[i], b, true) - case r - of identical: - a[i] = b - #updateDom(a, b) - action - of different, changed: - if result != different: result = r - break - of similar: - #updateDom(a, b) - updateStyles(a[i], b, true) - a[i] = b - action - - while commonPrefix < minLength: - eqAndUpdate(newNode, commonPrefix, oldNode[commonPrefix]): - inc commonPrefix - - var oldPos = oldLength - 1 - var newPos = newLength - 1 - while oldPos >= commonPrefix and newPos >= commonPrefix: - eqAndUpdate(newNode, newPos, oldNode[oldPos]): - dec oldPos - dec newPos - - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - let res = updateElement(current, current.childNodes[i], - newNode[i], oldNode[i], kxi) - if res != different: - newNode[i] = oldNode[i] - else: - result = different - - var nextChildPos = oldPos + 1 - while pos <= newPos: - if nextChildPos == oldLength: - current.appendChild(vnodeToDom(newNode[pos], kxi)) - else: - current.insertBefore(vnodeToDom(newNode[pos], kxi), current.childNodes[nextChildPos]) - # added new Node, so old state of VDOM have one more Node - inc oldLength - inc pos - inc nextChildPos - result = different - - for i in pos..oldPos: - detach(oldNode[i]) - doAssert pos < current.childNodes.len - current.removeChild(current.childNodes[pos]) - result = different - -when false: - var drawTimeout: Timeout + assert x.expanded.dom != nil, "expanded.dom is nil" + of different: + detach(oldNode) + let n = vnodeToDom(newNode, kxi) + patches.addPatch(pkReplace, parent, current, n) proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return let newtree = kxi.renderer() newtree.id = kxi.rootId kxi.toFocus = nil - #if kxi.currentTree != nil: - # kout cstring"same? ", same(kxi.currentTree, document.getElementById(kxi.rootId)) - #kout cstring"dodraw -----------------------------" if kxi.currentTree == nil: kxi.currentTree = newtree let asdom = vnodeToDom(kxi.currentTree, kxi) replaceById(kxi.rootId, asdom) else: let olddom = document.getElementById(kxi.rootId) - when false: - discard updateElement(nil, olddom, newtree, kxi.currentTree, kxi) - else: - var patches: seq[Patch] = @[] - discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) - patches.apply(kxi) + var patches: seq[Patch] = @[] + discard diff(nil, olddom, newtree, kxi.currentTree, patches, kxi) + patches.apply(kxi) kxi.currentTree = newtree #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) From 4bfe66edd6de36657efe6e3e6de12bb1d026b8b2 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Sat, 24 Jun 2017 08:22:31 +0200 Subject: [PATCH 084/264] optimized VStyle representation --- src/jdict.nim | 9 +++++ src/vstyles.nim | 95 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index 0eecb58..f541ac7 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -11,3 +11,12 @@ proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty( proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} +type + JSeq*[T] = ref object + +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 len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} +proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} diff --git a/src/vstyles.nim b/src/vstyles.nim index a6f1996..2f854e1 100644 --- a/src/vstyles.nim +++ b/src/vstyles.nim @@ -1,5 +1,5 @@ -import kdom, macros +import kdom, macros, jdict type StyleAttr* {.pure.} = enum @@ -208,62 +208,93 @@ macro buildLookupTables(): untyped = buildLookupTables() -type - VStyle* = ref object - attrs: array[StyleAttr, cstring] - mask: set[StyleAttr] +# I optimized the heck out of this representation since profiling showed +# it's relevant. +# even index: key, odd index: value; done this way for memory efficiency: -proc kout[T](x: T) {.importc: "console.log", varargs.} +type + VStyle* = JSeq[cstring] proc eq*(a, b: VStyle): bool = if a.isNil: if b.isNil: return true else: return false elif b.isNil: return false - if a.mask != b.mask: return false - for x in a.mask: - if a.attrs[x] != b.attrs[x]: return false + if a.len != b.len: return false + for i in 0.. a: + s.add nil + s.add nil + # insertion point here: + for j in countdown(s.len-1, i, 2): + s[j] = s[j-2] + s[j-1] = s[j-3] + s[i] = a + s[i+1] = value + return + inc i, 2 + s.add a + s.add value + +proc setAttr*(s: VStyle; attr: StyleAttr, value: cstring) {.noSideEffect.} = assert value != nil, "value must not be nil" - s.attrs[attr] = value - incl(s.mask, attr) + setAttr(s, toStyleAttrName[attr], value) -proc getAttr*(s: VStyle; attr: StyleAttr): cstring = +proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} = ## returns 'nil' if the attribute has not been set. - result = s.attrs[attr] + var i = 0 + let a = toStyleAttrName[attr] + while i < s.len: + if s[i] == a: + return s[i+1] + elif s[i] > a: + return nil + inc i, 2 -proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle = +proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle {.noSideEffect.} = ## constructs a VStyle object from a list of (attribute, value)-pairs. - result = VStyle(mask: {}) + result = newJSeq[cstring]() for x in pairs: result.setAttr x[0], x[1] -proc style*(a: StyleAttr; val: cstring): VStyle = +proc style*(a: StyleAttr; val: cstring): VStyle {.noSideEffect.} = ## constructs a VStyle object from a single (attribute, value)-pair. - result = VStyle(mask: {}) + result = newJSeq[cstring]() result.setAttr a, val -proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #".} +proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} -proc merge*(a, b: VStyle): VStyle = +proc merge*(a, b: VStyle): VStyle {.noSideEffect.} = ## merges two styles. ``b`` takes precedence over ``a``. - result = VStyle(mask: {}) - for i in low(StyleAttr)..high(StyleAttr): - result.attrs[i] = if b.attrs[i].isNil: a.attrs[i] else: b.attrs[i] - if result.attrs[i] != nil: incl(result.mask, i) + result = newJSeq[cstring]() + for i in 0.. Date: Sat, 24 Jun 2017 08:23:22 +0200 Subject: [PATCH 085/264] optimized the new diff algorihtm --- src/karax.nim | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 00863c6..3f3ebed 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -205,26 +205,30 @@ type pkReplace, pkRemove, pkAppend, pkInsertBefore Patch = object k: PatchKind - parent, current, n: Node + parent, current: Node + n: VNode -proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current, n: Node) = +proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current: Node; + n: VNode) = patches.add(Patch(k: k, parent: parent, current: current, n: n)) proc apply(patches: seq[Patch]; kxi: KaraxInstance) = for p in patches: case p.k of pkReplace: + let nn = vnodeToDom(p.n, kxi) if p.parent == nil: - replaceById(kxi.rootId, p.n) + replaceById(kxi.rootId, nn) else: - if p.n != p.current: - p.parent.replaceChild(p.n, p.current) + p.parent.replaceChild(nn, p.current) of pkRemove: p.parent.removeChild(p.current) of pkAppend: - p.parent.appendChild(p.n) + let nn = vnodeToDom(p.n, kxi) + p.parent.appendChild(nn) of pkInsertBefore: - p.parent.insertBefore(p.n, p.current) + let nn = vnodeToDom(p.n, kxi) + p.parent.insertBefore(nn, p.current) proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch]; kxi: KaraxInstance): EqResult = @@ -248,7 +252,8 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch assert current.childNodes[j] != nil, $info assert oldNode.len == current.len - let r = if oldNode.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or + oldNode.kind == VNodeKind.dthunk: diff(parent, current, a[i], b[j], patches, kxi) else: diff(current, current.childNodes[j], a[i], b[j], patches, kxi) @@ -287,13 +292,12 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch if oldPos + 1 == oldLength: for i in pos..newPos: - patches.addPatch(pkAppend, current, nil, vnodeToDom(newNode[i], kxi)) + patches.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, - vnodeToDom(newNode[i], kxi)) + patches.addPatch(pkInsertBefore, current, before, newNode[i]) result = different # XXX call 'attach' here? for i in pos..oldPos: @@ -312,8 +316,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch x.updatedImpl(x) if oldExpanded == nil: detach(oldNode) - let n = vnodeToDom(x.expanded, kxi) - patches.addPatch(pkReplace, parent, current, n) + patches.addPatch(pkReplace, parent, current, x.expanded) else: let res = diff(parent, current, x.expanded, oldExpanded, patches, kxi) if res != different: @@ -323,8 +326,7 @@ proc diff(parent, current: Node; newNode, oldNode: VNode; patches: var seq[Patch assert x.expanded.dom != nil, "expanded.dom is nil" of different: detach(oldNode) - let n = vnodeToDom(newNode, kxi) - patches.addPatch(pkReplace, parent, current, n) + patches.addPatch(pkReplace, parent, current, newNode) proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return From 8a1424669f6269db0b1ba94d3ab43b75270272bd Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 26 Jun 2017 21:03:39 +0200 Subject: [PATCH 086/264] optimized diff algorithm further; fixed a stupid bug; cleaned up the tests --- src/jdict.nim | 2 + src/karax.nim | 30 +++--- tests/diffDomTests.html | 1 - tests/diffDomTests.nim | 223 +++++++++++++--------------------------- tests/difftest.nim | 29 +++++- 5 files changed, 117 insertions(+), 168 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index f541ac7..b239af2 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -20,3 +20,5 @@ proc `[]=`*[T](s: JSeq[T], i: int, v: T) {.importcpp: "#[#] = #", noSideEffect.} proc newJSeq*[T](): JSeq[T] {.importcpp: "[@]".} proc len*[T](s: JSeq[T]): int {.importcpp: "#.length", noSideEffect.} proc add*[T](s: JSeq[T]; x: T) {.importcpp: "#.push(#)", noSideEffect.} + +proc shrink*[T](s: JSeq[T]; shorterLen: int) {.importcpp: "#.length = #", noSideEffect.} diff --git a/src/karax.nim b/src/karax.nim index 3f3ebed..f07bf71 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -208,12 +208,13 @@ type parent, current: Node n: VNode -proc addPatch(patches: var seq[Patch]; k: PatchKind; parent, current: Node; +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: seq[Patch]; kxi: KaraxInstance) = - for p in patches: +proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) = + for i in 0.. Diff dom tests - diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index d2c09ab..e28c6f6 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -2,182 +2,107 @@ import kdom, vdom, times, karax, karaxdsl, jdict, jstrutils, parseutils, sequtils var - entries: seq[cstring] - results: seq[cstring] - timeout: Timeout + entries: seq[cstring] + results: seq[cstring] proc reset() = - results.add cstring"reset started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - redraw() - results.add cstring"reset finished" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + redrawSync() -proc checkOrder(order : seq[int]): bool = - var ul = getElementById("ul") - if ul == nil or len(ul.children) != len(order): - return false - var pos = 0 - for child in ul.children: - if child.id != $order[pos]: - return false - inc pos - return true +proc checkOrder(order: seq[int]): bool = + var ul = getElementById("ul") + if ul == nil or len(ul.children) != len(order): + kout ul, len(order) + return false + var pos = 0 + for child in ul.children: + if child.id != $order[pos]: + kout pos + return false + inc pos + return true -proc check1() = - let result = checkOrder(@[0, 1, 2, 3, 4, 7, 5]) - if result: - results.add cstring"test1 - OK" - else: - results.add cstring"test1 - FAIL" +proc check(name: cstring; order: seq[int]) = + let result = checkOrder(order) + results.add name & (if result: cstring" - OK" else: cstring" -FAIL") # result: 0 1 2 3 4 7 5 proc test1() = - results.add cstring"test1 started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - entries.insert(cstring("7"), 5) - redraw() - timeout = setTimeout(check1, 20) + results.add cstring"test1 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + entries.insert(cstring("7"), 5) + redrawSync() + check("test1", @[0, 1, 2, 3, 4, 7, 5]) -proc check2() = - let result = checkOrder(@[8, 0, 1, 2, 3, 4, 7, 5]) - if result: - results.add cstring"test1 - OK" - else: - results.add cstring"test2 - FAIL" - -# result: 8 0 1 2 3 4 7 5 proc test2() = - results.add cstring"test2 started" - entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5") ] - entries.insert(cstring("7"), 5) - entries.insert(cstring("8"), 0) - redraw() - timeout = setTimeout(check2, 20) + results.add cstring"test2 started" + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + entries.insert(cstring("7"), 5) + entries.insert(cstring("8"), 0) + redrawSync() + check("test2", @[8, 0, 1, 2, 3, 4, 7, 5]) -proc check3() = - let result = checkOrder(@[2, 3, 4, 1]) - if result: - results.add cstring"test3 - OK" - else: - results.add cstring"test3 - FAIL" - -# result: 2 3 4 1 proc test3() = - results.add cstring"test3 started" - entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1") ] - redraw() - timeout = setTimeout(check3, 20) + results.add cstring"test3 started" + entries = @[cstring("2"), cstring("3"), cstring("4"), cstring("1")] + redrawSync() + check("test3", @[2, 3, 4, 1]) -proc check4() = - let result = checkOrder(@[5, 6, 7, 8]) - if result: - results.add cstring"test4 - OK" - else: - results.add cstring"test4 - FAIL" - -# result: 5 6 7 8 proc test4() = - results.add cstring"test4 started" - entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] - redraw() - timeout = setTimeout(check4, 20) + results.add cstring"test4 started" + entries = @[cstring("5"), cstring("6"), cstring("7"), cstring("8") ] + redrawSync() + check("test4", @[5, 6, 7, 8]) -proc check5() = - let result = checkOrder(@[0, 1, 3, 5, 4, 5]) - if result: - results.add cstring"test5 - OK" - else: - results.add cstring"test5 - FAIL" - -# result: 0 1 3 5 4 5 proc test5() = - results.add cstring"test5 started" - entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5") ] - redraw() - timeout = setTimeout(check5, 20) + results.add cstring"test5 started" + entries = @[cstring("0"), cstring("1"), cstring("3"), cstring("5"), cstring("4"), cstring("5")] + redrawSync() + check("test 5", @[0, 1, 3, 5, 4, 5]) -proc check6() = - let result = checkOrder(@[]) - if result: - results.add cstring"test6 - OK" - else: - results.add cstring"test6 - FAIL" - -# result: empty proc test6() = - results.add cstring"test6 started" - entries = @[] - redraw() - timeout = setTimeout(check6, 20) - -proc check7() = - let result = checkOrder(@[2]) - if result: - results.add cstring"test7 - OK" - else: - results.add cstring"test7 - FAIL" - redraw() + results.add cstring"test6 started" + entries = @[] + redrawSync() + check("test 6", @[]) # result: 2 proc test7() = - results.add cstring"test7 started" - entries = @[cstring("2")] - redraw() - timeout = setTimeout(check7, 20) + results.add cstring"test7 started" + entries = @[cstring("2")] + redrawSync() + check("test 7", @[2]) proc createEntry(id: int): VNode = result = buildHtml(): button(id="" & $id): - text $id + text $id proc createDom(): VNode = - result = buildHtml(tdiv()): - ul(id="ul"): - for e in entries: - createEntry(parseInt(e)) - for r in results: - tdiv: - text r + result = buildHtml(tdiv()): + ul(id="ul"): + for e in entries: + createEntry(parseInt(e)) + for r in results: + tdiv: + text r proc onload() = - for i in 0..5: # 0_000: - entries.add(cstring($i)) + for i in 0..5: # 0_000: + entries.add(cstring($i)) + test1() + reset() + test2() + reset() + test3() + reset() + test4() + reset() + test5() + reset() + test6() + reset() + test7() - var dtReset = 100 - var dtTest = 500 - - var t = dtReset - timeout = setTimeout(test1, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test2, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test3, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test4, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test5, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test6, t) - t += dtTest - timeout = setTimeout(reset, t) - t += dtReset - - timeout = setTimeout(test7, t) - -onload() setRenderer createDom +onload() diff --git a/tests/difftest.nim b/tests/difftest.nim index 9f80037..bd90b6a 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -2,10 +2,19 @@ include "../src/karax" import "../src/karaxdsl" +proc hasDom(n: Vnode) = + if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: + discard + else: + doAssert n.dom != nil + for i in 0.. Date: Tue, 27 Jun 2017 23:54:58 +0200 Subject: [PATCH 087/264] smart-diff algorithm works now --- src/karax.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f07bf71..f8230c7 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -234,8 +234,10 @@ proc apply(patches: JSeq[Patch]; kxi: KaraxInstance) = proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): EqResult = result = eq(newNode, oldNode) case result - of identical: + of identical, similar: newNode.dom = oldNode.dom + if result == similar: updateStyles(newNode, oldNode) + let newLength = newNode.len var oldLength = oldNode.len let minLength = min(newLength, oldLength) @@ -306,8 +308,6 @@ proc diff(newNode, oldNode: VNode;parent, current: Node; patches: JSeq[Patch]): patches.addPatch(pkRemove, current, current.childNodes[i], nil) result = different - of similar: - updateStyles(newNode, oldNode) of changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) From fe7d72fa4e6d8dfd1051b4e6bc159aacfa35f6ff Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 08:46:48 +0200 Subject: [PATCH 088/264] special treatment for input and textarea fields; minor code cleanups --- src/karax.nim | 6 ++++-- src/vdom.nim | 1 - tests/blur.nim | 13 +++++-------- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f8230c7..09a3e1b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -52,7 +52,9 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = let action = action let n = n result = proc (ev: Event) = - action(ev, n) + if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input: + keyeventBody() + else: action(ev, n) proc enterWrapper(): (proc (ev: Event)) = let action = action @@ -189,7 +191,7 @@ proc updateStyles(newNode, oldNode: VNode) = oldNode.style = newNode.style proc printV(n: VNode; depth: cstring = "") = - kout depth, cstring($n.kind), n.myid, cstring"key ", n.key + kout depth, cstring($n.kind), cstring"key ", n.key #for k, v in pairs(n.style): # kout depth, "style: ", k, v if n.kind == VNodeKind.component: diff --git a/src/vdom.nim b/src/vdom.nim index 9a88d0a..e3f74b5 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -120,7 +120,6 @@ type style*: VStyle ## the style that should be applied to the virtual node. dom*: Node ## the attached real DOM node. Can be 'nil' if the virtual node ## is not part of the virtual DOM anymore. - myid*: int VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode diff --git a/tests/blur.nim b/tests/blur.nim index 74b8ba1..4ad3eb1 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -4,7 +4,6 @@ type TextInput* = ref object of VComponent value: cstring isActive: bool -var renderId: int proc render(x: VComponent): VNode = let self = TextInput(x) @@ -37,18 +36,16 @@ proc render(x: VComponent): VNode = self.isActive = not self.isActive markDirty(self) - kout cstring"rendering ", self.myid - inc renderId - result = buildHtml(tdiv(style=style)): - input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip) + proc onchanged(ev: Event; n: VNode) = + kout cstring"onchanged", n.value + + result = buildHtml(tdiv(style=style)): + textarea(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -var gid = 0 proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value - inc gid - result.myid = gid proc createDom(): VNode = result = buildHtml(tdiv): From c987323dbaac4770e1c733046372ecbf38a1e7df Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 09:44:50 +0200 Subject: [PATCH 089/264] test nested components (still buggy) --- tests/blur.nim | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/tests/blur.nim b/tests/blur.nim index 4ad3eb1..02a0de2 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -34,21 +34,37 @@ proc render(x: VComponent): VNode = proc flip(ev: Event; n: VNode) = self.isActive = not self.isActive + kout cstring"onflip", n.value markDirty(self) proc onchanged(ev: Event; n: VNode) = kout cstring"onchanged", n.value result = buildHtml(tdiv(style=style)): - textarea(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) + input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value +type + Combined = ref object of VComponent + a, b: TextInput + +proc renderComb(self: VComponent): VNode = + let self = Combined(self) + result = buildHtml(tdiv(style=self.style)): + self.a + self.b + +proc newCombined*(style: VStyle = VStyle()): Combined = + result = newComponent(Combined, renderComb) + result.a = newTextInput(style, "AAA") + result.b = newTextInput(style, "BBB") + proc createDom(): VNode = result = buildHtml(tdiv): - newTextInput(value=cstring"test") + newCombined() setRenderer createDom From ba88fb9a0b2e15c7eebf49498706b6a9c4ffdc49 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 09:46:26 +0200 Subject: [PATCH 090/264] cancel redraw requests as an attempt to improve performance --- src/karax.nim | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 09a3e1b..0b1d8db 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -23,6 +23,7 @@ type postRenderCallback: proc () toFocus: Node toFocusV: VNode + renderId: int var kxi*: KaraxInstance ## The current Karax instance. This is always used @@ -354,7 +355,8 @@ proc dodraw(kxi: KaraxInstance) = if kxi.toFocus != nil: kxi.toFocus.focus() -proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} +proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".} +proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".} proc redraw*(kxi: KaraxInstance = kxi) = # we buffer redraw requests: @@ -363,14 +365,15 @@ proc redraw*(kxi: KaraxInstance = kxi) = clearTimeout(drawTimeout) drawTimeout = setTimeout(dodraw, 30) elif true: - reqFrame(proc () = kxi.dodraw) + cancelFrame kxi.renderId + kxi.renderId = reqFrame(proc () = kxi.dodraw) else: dodraw(kxi) proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi) proc init(ev: Event) = - reqFrame(proc () = kxi.dodraw) + kxi.renderId = reqFrame(proc () = kxi.dodraw) proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = From 0ad9d68c8102f336b4e721e30e06c40186e1880c Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 11:26:09 +0200 Subject: [PATCH 091/264] fixes #35 --- src/karax.nim | 39 ++++++++++++++++++++++++++++++--------- src/kdom.nim | 1 + src/vdom.nim | 5 +++-- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 0b1d8db..5ea7d62 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -48,8 +48,9 @@ template keyeventBody() = # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. -proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = - proc stdWrapper(): (proc (ev: Event)) = +proc wrapEvent(d: Node; n: VNode; k: EventKind; + action: EventHandler): NativeEventHandler = + proc stdWrapper(): NativeEventHandler = let action = action let n = n result = proc (ev: Event) = @@ -57,13 +58,13 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = keyeventBody() else: action(ev, n) - proc enterWrapper(): (proc (ev: Event)) = + proc enterWrapper(): NativeEventHandler = let action = action let n = n result = proc (ev: Event) = if ev.keyCode == 13: keyeventBody() - proc laterWrapper(): (proc (ev: Event)) = + proc laterWrapper(): NativeEventHandler = let action = action let n = n var timer: Timeout @@ -74,11 +75,14 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) = case k of EventKind.onkeyuplater: - d.addEventListener("keyup", laterWrapper()) + result = laterWrapper() + d.addEventListener("keyup", result) of EventKind.onkeyupenter: - d.addEventListener("keyup", enterWrapper()) + result = enterWrapper() + d.addEventListener("keyup", result) else: - d.addEventListener(toEventName[k], stdWrapper()) + result = stdWrapper() + d.addEventListener(toEventName[k], result) # --------------------- DOM diff ----------------------------------------- @@ -90,6 +94,11 @@ template detach(n: VNode) = template attach(n: VNode) = n.dom = result +proc applyEvents(n: VNode; kxi: KaraxInstance) = + let dest = n.dom + for i in 0.. Date: Wed, 28 Jun 2017 12:56:51 +0200 Subject: [PATCH 092/264] combined components work --- tests/blur.nim | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/blur.nim b/tests/blur.nim index 02a0de2..f40cb97 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -58,8 +58,12 @@ proc renderComb(self: VComponent): VNode = self.a self.b +proc changed(self: VComponent): bool = + let self = Combined(self) + result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) + proc newCombined*(style: VStyle = VStyle()): Combined = - result = newComponent(Combined, renderComb) + result = newComponent(Combined, renderComb, changed=changed) result.a = newTextInput(style, "AAA") result.b = newTextInput(style, "BBB") From c681f0055f2d63c7cee91788e65e8cb2acf7db9d Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 28 Jun 2017 15:23:19 +0200 Subject: [PATCH 093/264] update value property of real DOM nodes; minor DOM diff speedups; allow setFocus=false in the DSL --- src/jdict.nim | 2 +- src/karax.nim | 90 +++++++++++++++++++++++++++++------------------- src/karaxdsl.nim | 4 ++- tests/blur.nim | 9 +++++ 4 files changed, 67 insertions(+), 38 deletions(-) diff --git a/src/jdict.nim b/src/jdict.nim index b239af2..6e2aa9f 100644 --- a/src/jdict.nim +++ b/src/jdict.nim @@ -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.} diff --git a/src/karax.nim b/src/karax.nim index 5ea7d62..ad8ddfc 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -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..= 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.. Date: Wed, 28 Jun 2017 22:54:24 +0200 Subject: [PATCH 094/264] make setFocus: bool work --- examples/todoapp/todoapp.nim | 2 +- src/karaxdsl.nim | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index f74abc3..0caa70c 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -90,7 +90,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac else: input(class = "edit", name = "title", key=id, onblur = focusLost, - onkeyupenter = editEntry, value = d, setFocus) + onkeyupenter = editEntry, value = d, setFocus=true) proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} = result = buildHtml(footer(class = "footer")): diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 47cd761..15c6fc6 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -86,7 +86,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode = elif key in SpecialAttrs: result.add newDotAsgn(tmp, key, x[1]) elif eqIdent(key, "setFocus"): - result.add newCall(x, tmp, x[1], ident"kxi") + result.add newCall(key, tmp, x[1], ident"kxi") else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) elif ck != ComponentKind.Tag: From e7399094667f3fb6d31477feefce7457050d67b4 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 29 Jun 2017 11:04:43 +0200 Subject: [PATCH 095/264] rollback dom.value behaviour; fixes removeEventListener problem --- src/karax.nim | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ad8ddfc..595ff60 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -213,12 +213,13 @@ proc updateStyles(newNode, oldNode: VNode) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom - for i in 0.. Date: Thu, 29 Jun 2017 12:25:08 +0200 Subject: [PATCH 096/264] different detach handling to make event handlers work better --- src/karax.nim | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 595ff60..b8c2aa1 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -16,7 +16,7 @@ proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} type PatchKind = enum - pkReplace, pkRemove, pkAppend, pkInsertBefore + pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach Patch = object k: PatchKind parent, current: Node @@ -99,10 +99,8 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; # --------------------- DOM diff ----------------------------------------- template detach(n: VNode) = - if n.kind == VNodeKind.component: - let x = VComponent(n) - if x.onDetachImpl != nil: x.onDetachImpl(x) - n.dom = nil + addPatch(kxi, pkDetach, nil, nil, n) + template attach(n: VNode) = n.dom = result @@ -213,13 +211,12 @@ proc updateStyles(newNode, oldNode: VNode) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom - if d != nil: - for i in 0..= kxi.patches.len: @@ -266,6 +263,12 @@ proc apply(kxi: KaraxInstance) = of pkInsertBefore: let nn = vnodeToDom(p.n, kxi) p.parent.insertBefore(nn, p.current) + of pkDetach: + let n = p.n + if n.kind == VNodeKind.component: + let x = VComponent(n) + if x.onDetachImpl != nil: x.onDetachImpl(x) + n.dom = nil kxi.patchLen = 0 proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = @@ -273,6 +276,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom + assert oldNode.dom != nil if result == similar: updateStyles(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) @@ -280,9 +284,9 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E # oldNode.dom.value = newNode.text let newLength = newNode.len - var oldLength = oldNode.len - let minLength = min(newLength, oldLength) + let oldLength = oldNode.len if newLength == 0 and oldLength == 0: return result + let minLength = min(newLength, oldLength) assert oldNode.kind == newNode.kind var commonPrefix = 0 From ea7eee756498452224bc822aeeb3d3a27829e4e8 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 29 Jun 2017 17:10:29 +0200 Subject: [PATCH 097/264] better input element handling --- src/karax.nim | 13 ++++++++----- tests/blur.nim | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index b8c2aa1..3c41d7e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -54,9 +54,11 @@ template nativeValue(ev): cstring = cast[Element](ev.target).value template setNativeValue(ev, val) = cast[Element](ev.target).value = val template keyeventBody() = - n.value = nativeValue(ev) + let v = nativeValue(ev) + n.value = v action(ev, n) - setNativeValue(ev, n.value) + if n.value != v: + setNativeValue(ev, n.value) # Do not call redraw() here! That is already done # by ``karax.addEventHandler``. @@ -276,12 +278,13 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom - assert oldNode.dom != nil if result == similar: updateStyles(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) - #if newNode.kind == VNodeKind.input or newNode.kind == VNodeKind.textarea: - # oldNode.dom.value = newNode.text + if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: + if oldNode.text != newNode.text: + oldNode.text = newNode.text + oldNode.dom.value = newNode.text let newLength = newNode.len let oldLength = oldNode.len diff --git a/tests/blur.nim b/tests/blur.nim index 84d69dd..844ab8b 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -3,6 +3,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type TextInput* = ref object of VComponent value: cstring isActive: bool + onchange: proc (value: cstring) proc render(x: VComponent): VNode = let self = TextInput(x) @@ -38,15 +39,23 @@ proc render(x: VComponent): VNode = markDirty(self) proc onchanged(ev: Event; n: VNode) = - kout cstring"onchanged", n.value + if self.onchange != nil and self.value != n.value: + self.onchange n.value + self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput = +proc setValue(x: TextInput; value: cstring) = + x.value = value + markDirty(x) + +proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", + onchange: proc(v: cstring) = nil): TextInput = result = newComponent(TextInput, render) result.style = style result.value = value + result.onchange = onchange type Combined = ref object of VComponent @@ -76,8 +85,36 @@ proc newCombined*(style: VStyle = VStyle()): Combined = result.a = newTextInput(style, "AAA") result.b = newTextInput(style, "BBB") + +var + persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"] + selected = -1 + errmsg = cstring"" + ti = newTextInput(VStyle(), "", proc (v: cstring) = + if v.len > 0: + if selected >= 0: persons[selected] = v + errmsg = "" + else: + errmsg = "name must not be empty" + ) + +proc renderPerson(text: cstring, index: int): VNode = + proc select(ev: Event, n: VNode) = + selected = index + ti.setValue(persons[selected]) + + result = buildHtml(): + tdiv(onClick=select): + text text + proc createDom(): VNode = result = buildHtml(tdiv): - newCombined() + tdiv: + for index, text in persons.pairs: + renderPerson(text, index) + tdiv: + ti + tdiv: + text errmsg setRenderer createDom From d538b7bc938f8c70cf72980b9e5a7ae8905192e3 Mon Sep 17 00:00:00 2001 From: Araq Date: Sun, 2 Jul 2017 09:34:58 +0200 Subject: [PATCH 098/264] try to make components aware of the previous state --- src/karax.nim | 10 ++++++--- src/vdom.nim | 17 ++++++++------- tests/blur.nim | 56 +++++++++++++++++++++++++++++++------------------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3c41d7e..5edda89 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -132,7 +132,8 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = assert x.renderImpl != nil if x.expanded == nil: x.expanded = x.renderImpl(x) - x.updatedImpl(x) + # x.updatedImpl(x, nil) + assert x.expanded != nil result = vnodeToDom(x.expanded, kxi) attach n return result @@ -197,7 +198,7 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: return different let x = VComponent(b) assert x.changedImpl != nil - return if x.changedImpl(x): changed else: identical + return if x.changedImpl(x, VComponent(a)): changed else: identical if not sameAttrs(a, b): return different if a.class != b.class: return different if not eq(a.style, b.style): return similar @@ -282,6 +283,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: + echo "comparing ", oldNode.text != newNode.text if oldNode.text != newNode.text: oldNode.text = newNode.text oldNode.dom.value = newNode.text @@ -359,9 +361,11 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of changed: assert oldNode.kind == VNodeKind.component let x = VComponent(oldNode) + x.updatedImpl(x, VComponent newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) - x.updatedImpl(x) + x.version = VComponent(newNode).version + #x.updatedImpl(x) if oldExpanded == nil: detach(oldNode) kxi.addPatch(pkReplace, parent, current, x.expanded) diff --git a/src/vdom.nim b/src/vdom.nim index b0f7293..bbb4eac 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -124,14 +124,13 @@ type VComponent* = ref object of VNode ## The abstract class for every karax component. renderImpl*: proc(self: VComponent): VNode - changedImpl*: proc(self: VComponent): bool - updatedImpl*: proc(self: VComponent) + changedImpl*: proc(self, newInstance: VComponent): bool + updatedImpl*: proc(self, newInstance: VComponent) onAttachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent) version*: int ## Update this to trigger a redraw by karax. Usually you ## should call 'markDirty' instead which is an alias for ## 'inc version'. - renderedVersion*: int ## Do not touch. Used by karax. expanded*: VNode ## Do not touch. Used by karax. The VDOM the component ## expanded to. @@ -153,19 +152,19 @@ 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 defaultChangedImpl*(v: VComponent): bool = +proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. - result = v.version != v.renderedVersion + result = v.version != newInstance.version -proc defaultUpdatedImpl*(v: VComponent) = - v.renderedVersion = v.version +proc defaultUpdatedImpl*(v, newInstance: VComponent) = + discard template newComponent*[T](t: typeDesc[T]; render: (proc(self: VComponent): VNode) not nil, onAttach: proc(self: VComponent) = nil, onDetach: proc(self: VComponent) = nil, - changed: (proc(self: VComponent): bool) = defaultChangedImpl, - updated: proc(self: VComponent) = defaultUpdatedImpl): T = + changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl, + updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = ## Use this template to create new components. T(kind: VNodeKind.component, key: -1, text: cstring(astToStr(t)), renderImpl: render, diff --git a/tests/blur.nim b/tests/blur.nim index 844ab8b..3823f93 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -41,18 +41,33 @@ proc render(x: VComponent): VNode = proc onchanged(ev: Event; n: VNode) = if self.onchange != nil and self.value != n.value: self.onchange n.value - self.value = n.value + #self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -proc setValue(x: TextInput; value: cstring) = - x.value = value - markDirty(x) +#proc setValue(x: TextInput; value: cstring) = +# x.value = value +# markDirty(x) + +proc update(current, next: VComponent) = + let current = TextInput(current) + let next = TextInput(next) + if not current.isActive: + current.value = next.value + echo "updated! ", current.value + else: + echo "not updated! ", current.value + #markDirty(current) + +proc changed(current, next: VComponent): bool = + let current = TextInput(current) + let next = TextInput(next) + result = next.value != current.value or true proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = - result = newComponent(TextInput, render) + result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value result.onchange = onchange @@ -76,32 +91,25 @@ proc renderComb(self: VComponent): VNode = button(onclick=bu): text "reset" -proc changed(self: VComponent): bool = - let self = Combined(self) - result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) +when false: + proc changed(self: VComponent): bool = + let self = Combined(self) + result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) -proc newCombined*(style: VStyle = VStyle()): Combined = - result = newComponent(Combined, renderComb, changed=changed) - result.a = newTextInput(style, "AAA") - result.b = newTextInput(style, "BBB") + proc newCombined*(style: VStyle = VStyle()): Combined = + result = newComponent(Combined, renderComb, changed=changed) + result.a = newTextInput(style, "AAA") + result.b = newTextInput(style, "BBB") var persons: seq[cstring] = @[cstring"Karax", "Abathur", "Fenix"] selected = -1 errmsg = cstring"" - ti = newTextInput(VStyle(), "", proc (v: cstring) = - if v.len > 0: - if selected >= 0: persons[selected] = v - errmsg = "" - else: - errmsg = "name must not be empty" - ) proc renderPerson(text: cstring, index: int): VNode = proc select(ev: Event, n: VNode) = selected = index - ti.setValue(persons[selected]) result = buildHtml(): tdiv(onClick=select): @@ -113,7 +121,13 @@ proc createDom(): VNode = for index, text in persons.pairs: renderPerson(text, index) tdiv: - ti + newTextInput(VStyle(), if selected >= 0: persons[selected] else: "", proc (v: cstring) = + if v.len > 0: + if selected >= 0: persons[selected] = v + errmsg = "" + else: + errmsg = "name must not be empty" + ) tdiv: text errmsg From 43ee8903561b0d06adb26c1d88f6d8ecb4cd7929 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 3 Jul 2017 23:03:30 +0200 Subject: [PATCH 099/264] fix exponential blowup in diff algorith; wip --- src/karax.nim | 96 +++++++++++++++++++++++++++++++++------------- tests/difftest.nim | 37 +++++++++++++----- 2 files changed, 96 insertions(+), 37 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 3c41d7e..0be777a 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -34,6 +34,7 @@ type renderId: int patches: seq[Patch] # we reuse this to save allocations patchLen: int + recursion: int var @@ -161,6 +162,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = proc same(n: VNode, e: Node): bool = if n.kind == VNodeKind.component: result = same(VComponent(n).expanded, e) + elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: + # we don't check these for now: + result = true elif toTag[n.kind] == e.nodename: result = true if n.kind != VNodeKind.text: @@ -178,7 +182,7 @@ proc replaceById(id: cstring; newTree: Node) = type EqResult = enum - changed, different, similar, identical + changed, different, similar, identical, usenewNode proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different @@ -274,6 +278,13 @@ proc apply(kxi: KaraxInstance) = kxi.patchLen = 0 proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult = + if kxi.recursion > 100: + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) + if oldNode.kind == VNodeKind.text: + echo oldNode.text + return + #doAssert false, "overflow!" + inc kxi.recursion result = eq(newNode, oldNode) case result of identical, similar: @@ -293,68 +304,85 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E assert oldNode.kind == newNode.kind var commonPrefix = 0 + let isSpecial = oldNode.kind == VNodeKind.component or + oldNode.kind == VNodeKind.vthunk or + oldNode.kind == VNodeKind.dthunk template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = let oldLen = kxi.patchLen - when false: - if oldNode.kind notin {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: - assert current != nil - assert current.childNodes[j] != nil, $info - assert oldNode.len == current.len - - let r = if oldNode.kind == VNodeKind.component or oldNode.kind == VNodeKind.vthunk or - oldNode.kind == VNodeKind.dthunk: + assert i < a.len + assert j < b.len + let r = if isSpecial: diff(a[i], b[j], parent, current, kxi) else: - diff(a[i], b[j], current, current.childNodes[j], kxi) + (assert j < current.len; + diff(a[i], b[j], current, current.childNodes[j], kxi)) case r of identical, changed, similar: a[i] = b[j] action + of usenewNode: + b[j] = a[i] + action of different: # undo what 'diff' would have done: kxi.patchLen = oldLen - if result != different: result = r + #if result != different: result = r break - #of similar: - # updateStyles(a[i], b[j]) - # a[i] = b[j] - # action - + # compute common prefix: while commonPrefix < minLength: eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"): inc commonPrefix + # compute common suffix: var oldPos = oldLength - 1 var newPos = newLength - 1 while oldPos >= commonPrefix and newPos >= commonPrefix: eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): dec oldPos dec newPos + echo "came here" - var pos = min(oldPos, newPos) + 1 - for i in commonPrefix..pos-1: - if diff(newNode[i], oldNode[i], current, current.childNodes[i], - kxi) != different: - newNode[i] = oldNode[i] - else: - result = different + let pos = min(oldPos, newPos) + 1 + # now the different children are in commonPrefix .. pos - 1: + when false: + for i in commonPrefix..pos-1: + detach(oldNode[i]) + kxi.addPatch(pkReplace, current, current.childNodes[i], newNode[i]) + when true: + for i in commonPrefix..pos-1: + let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], + kxi) + if r == usenewNode: + oldNode[i] = newNode[i] + elif r != different: + newNode[i] = oldNode[i] + #else: + # result = different if oldPos + 1 == oldLength: for i in pos..newPos: kxi.addPatch(pkAppend, current, nil, newNode[i]) - result = different + result = usenewNode + #result = different else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: kxi.addPatch(pkInsertBefore, current, before, newNode[i]) - result = different + result = usenewNode + #result = different # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) #doAssert i < current.childNodes.len kxi.addPatch(pkRemove, current, current.childNodes[i], nil) - result = different + result = usenewNode #different + # after the applied patch, conceptually the nodes are identical, so + # no further search is required. 'changed' needs to be propagated + # for the component system to work. 'similar' was transformed into + # identical too: + #if result == different or result == similar: + # result = identical of changed: assert oldNode.kind == VNodeKind.component @@ -375,6 +403,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of different: detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) + of usenewNode: doAssert(false, "eq returned usenewNode") + dec kxi.recursion + +when defined(stats): + proc depth(n: VNode; total: var int): int = + var m = 0 + for i in 0..= expected.len: + echo "patches differ; expected nothing but got: ", p + elif p != expected[i]: + echo "patches differ; expected ", expected[i], " but got: ", p #hasDom(kxi.currentTree) + kxi.patchLen = 0 proc testAppend() = let a = buildHtml(tdiv): @@ -26,7 +41,7 @@ proc testAppend() = li: text "A" li: text "B" li: text "C" - doDiff(a, b) + doDiff(a, b, "pkAppend li C") proc testInsert() = let a = buildHtml(tdiv): @@ -38,7 +53,7 @@ proc testInsert() = li: text "A" li: text "B" li: text "C" - doDiff(a, b) + doDiff(a, b, "pkInsert li B") proc testDelete() = let a = buildHtml(tdiv): @@ -49,10 +64,12 @@ proc testDelete() = let b = buildHtml(tdiv): ul: discard - doDiff(a, b) + doDiff(a, b, "pkDetach li A", "pkRemove nil", + "pkDetach li B", "pkRemove nil", + "pkDetach li C", "pkRemove nil") kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) -testAppend() +#testAppend() testInsert() -testDelete() +#testDelete() From 5f7f33bb0232810cd9f9edd9454caa7230814e81 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 4 Jul 2017 08:04:04 +0200 Subject: [PATCH 100/264] wip --- src/karax.nim | 7 +++---- tests/difftest.nim | 33 +++++++++++++++++++++++++++++---- tests/tester.nim | 1 + 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 0be777a..147965c 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -282,7 +282,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) if oldNode.kind == VNodeKind.text: echo oldNode.text - return + #return #doAssert false, "overflow!" inc kxi.recursion result = eq(newNode, oldNode) @@ -315,7 +315,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let r = if isSpecial: diff(a[i], b[j], parent, current, kxi) else: - (assert j < current.len; + (assert same(b, current); #(j < current.len, "j: " & $j & " len " & $current.len); diff(a[i], b[j], current, current.childNodes[j], kxi)) case r of identical, changed, similar: @@ -327,7 +327,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of different: # undo what 'diff' would have done: kxi.patchLen = oldLen - #if result != different: result = r + if result != different: result = r break # compute common prefix: while commonPrefix < minLength: @@ -341,7 +341,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"): dec oldPos dec newPos - echo "came here" let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: diff --git a/tests/difftest.nim b/tests/difftest.nim index 52b354f..8bd4d0f 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -19,15 +19,18 @@ proc shortRepr(n: VNode): string = for i in 0..= expected.len: echo "patches differ; expected nothing but got: ", p + inc err elif p != expected[i]: echo "patches differ; expected ", expected[i], " but got: ", p + inc err #hasDom(kxi.currentTree) kxi.patchLen = 0 @@ -53,7 +56,24 @@ proc testInsert() = li: text "A" li: text "B" li: text "C" - doDiff(a, b, "pkInsert li B") + doDiff(a, b, "pkInsertBefore li B") + +proc testInsert2() = + let a = buildHtml(tdiv): + tdiv: + tdiv: + ul: + li: text "A" + li: text "D" + let b = buildHtml(tdiv): + tdiv: + tdiv: + ul: + li: text "A" + li: text "B" + li: text "C" + li: text "D" + doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C") proc testDelete() = let a = buildHtml(tdiv): @@ -70,6 +90,11 @@ proc testDelete() = kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) -#testAppend() +testAppend() testInsert() -#testDelete() +testInsert2() +testDelete() +if err == 0: + echo "Success" +else: + quit 1 diff --git a/tests/tester.nim b/tests/tester.nim index 4f2ae66..60a787b 100644 --- a/tests/tester.nim +++ b/tests/tester.nim @@ -13,5 +13,6 @@ proc main = exec("nim js examples/scrollapp/scrollapp.nim") exec("nim js examples/mediaplayer/playerapp.nim") exec("nim js examples/carousel/carousel.nim") + exec("nim js -d:nodejs -r tests/difftest.nim") main() From a93e739081fbb3572777665165343854c394e5b5 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 5 Jul 2017 16:07:52 +0200 Subject: [PATCH 101/264] new component system: blur.nim example now works --- src/karax.nim | 5 ++-- src/vdom.nim | 13 +++++++-- tests/blur.nim | 75 ++++++++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 5edda89..3fb0fc4 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -283,7 +283,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: - echo "comparing ", oldNode.text != newNode.text if oldNode.text != newNode.text: oldNode.text = newNode.text oldNode.dom.value = newNode.text @@ -364,8 +363,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E x.updatedImpl(x, VComponent newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) - x.version = VComponent(newNode).version - #x.updatedImpl(x) + x.renderedVersion = x.version + #echo "expanding ", x.debugId if oldExpanded == nil: detach(oldNode) kxi.addPatch(pkReplace, parent, current, x.expanded) diff --git a/src/vdom.nim b/src/vdom.nim index bbb4eac..997b059 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -131,8 +131,11 @@ type version*: int ## Update this to trigger a redraw by karax. Usually you ## should call 'markDirty' instead which is an alias for ## 'inc version'. + renderedVersion*: int ## Do not touch. Used by karax. The last version of the + ## component we rendered. expanded*: VNode ## Do not touch. Used by karax. The VDOM the component ## expanded to. + debugId*: int proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v @@ -154,11 +157,16 @@ proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. - result = v.version != newInstance.version + result = v.version != v.renderedVersion proc defaultUpdatedImpl*(v, newInstance: VComponent) = discard +var gid = 0 +proc getDebugId(): int = + inc(gid) + gid + template newComponent*[T](t: typeDesc[T]; render: (proc(self: VComponent): VNode) not nil, onAttach: proc(self: VComponent) = nil, @@ -169,7 +177,8 @@ template newComponent*[T](t: typeDesc[T]; T(kind: VNodeKind.component, key: -1, text: cstring(astToStr(t)), renderImpl: render, changedImpl: changed, updatedImpl: updated, - onAttachImpl: onAttach, onDetachImpl: onDetach) + onAttachImpl: onAttach, onDetachImpl: onDetach, + debugId: getDebugId()) template markDirty*(c: VComponent) = ## mark the component as dirty so that it is re-rendered. diff --git a/tests/blur.nim b/tests/blur.nim index 3823f93..25c0b24 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -1,7 +1,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type TextInput* = ref object of VComponent - value: cstring + value, guid: cstring isActive: bool onchange: proc (value: cstring) @@ -35,63 +35,60 @@ proc render(x: VComponent): VNode = proc flip(ev: Event; n: VNode) = self.isActive = not self.isActive - kout cstring"onflip", n.value + echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version markDirty(self) proc onchanged(ev: Event; n: VNode) = - if self.onchange != nil and self.value != n.value: + if self.onchange != nil: self.onchange n.value - #self.value = n.value + self.value = n.value result = buildHtml(tdiv(style=style)): input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip, onkeyup=onchanged) -#proc setValue(x: TextInput; value: cstring) = -# x.value = value -# markDirty(x) +proc changed(current, next: VComponent): bool = + let current = TextInput(current) + let next = TextInput(next) + if current.guid != next.guid: + result = true + else: + result = defaultChangedImpl(current, next) proc update(current, next: VComponent) = let current = TextInput(current) let next = TextInput(next) - if not current.isActive: - current.value = next.value - echo "updated! ", current.value - else: - echo "not updated! ", current.value - #markDirty(current) + current.value = next.value + current.guid = next.guid + next.isActive = current.isActive -proc changed(current, next: VComponent): bool = - let current = TextInput(current) - let next = TextInput(next) - result = next.value != current.value or true - -proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring"", +proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value result.onchange = onchange - -type - Combined = ref object of VComponent - a, b: TextInput - -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" + result.guid = guid when false: + type + Combined = ref object of VComponent + a, b: TextInput + + 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) result = self.a.changedImpl(self.a) or self.b.changedImpl(self.b) @@ -121,7 +118,7 @@ proc createDom(): VNode = for index, text in persons.pairs: renderPerson(text, index) tdiv: - newTextInput(VStyle(), if selected >= 0: persons[selected] else: "", proc (v: cstring) = + newTextInput(VStyle(), &selected, if selected >= 0: persons[selected] else: "", proc (v: cstring) = if v.len > 0: if selected >= 0: persons[selected] = v errmsg = "" From 7779560b6b0002bae211dfe78d0310a683773fcb Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 Jul 2017 18:34:32 +0200 Subject: [PATCH 102/264] more tests and diff fixes; wip --- src/karax.nim | 55 +++++++++++++++++++++++-------- tests/diffDomTests.nim | 1 - tests/difftest.nim | 74 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 114 insertions(+), 16 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 147965c..adbd2f2 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -21,6 +21,9 @@ type k: PatchKind parent, current: Node n: VNode + PatchV = object + parent, newChild: VNode + pos: int type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have @@ -34,7 +37,11 @@ type renderId: int patches: seq[Patch] # we reuse this to save allocations patchLen: int - recursion: int + patchesV: seq[PatchV] + patchLenV: int + runCount: int + when defined(stats): + recursion: int var @@ -251,6 +258,17 @@ proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; kxi.patches[L].n = na inc kxi.patchLen +proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = + let L = kxi.patchLenV + if L >= kxi.patchesV.len: + # allocate more space: + kxi.patchesV.add(PatchV(parent: parent, newChild: newChild, pos: pos)) + else: + kxi.patchesV[L].parent = parent + kxi.patchesV[L].newChild = newChild + kxi.patchesV[L].pos = pos + inc kxi.patchLenV + proc apply(kxi: KaraxInstance) = for i in 0.. 100: - echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) - if oldNode.kind == VNodeKind.text: - echo oldNode.text - #return - #doAssert false, "overflow!" - inc kxi.recursion + when defined(stats): + if kxi.recursion > 100: + echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode) + if oldNode.kind == VNodeKind.text: + echo oldNode.text + #return + #doAssert false, "overflow!" + inc kxi.recursion result = eq(newNode, oldNode) case result of identical, similar: @@ -310,6 +333,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) = let oldLen = kxi.patchLen + let oldLenV = kxi.patchLenV assert i < a.len assert j < b.len let r = if isSpecial: @@ -322,11 +346,13 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E a[i] = b[j] action of usenewNode: - b[j] = a[i] + #b[j] = a[i] + kxi.addPatchV(b, j, a[i]) action of different: # undo what 'diff' would have done: kxi.patchLen = oldLen + kxi.patchLenV = oldLenV if result != different: result = r break # compute common prefix: @@ -403,7 +429,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) of usenewNode: doAssert(false, "eq returned usenewNode") - dec kxi.recursion + when defined(stats): + dec kxi.recursion when defined(stats): proc depth(n: VNode; total: var int): int = @@ -416,6 +443,7 @@ when defined(stats): proc dodraw(kxi: KaraxInstance) = if kxi.renderer.isNil: return let newtree = kxi.renderer() + inc kxi.runCount newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: @@ -428,7 +456,7 @@ proc dodraw(kxi: KaraxInstance) = #kout cstring"patch len ", patches.len apply(kxi) kxi.currentTree = newtree - doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() @@ -437,8 +465,8 @@ proc dodraw(kxi: KaraxInstance) = if kxi.toFocus != nil: kxi.toFocus.focus() kxi.renderId = 0 - kxi.recursion = 0 when defined(stats): + kxi.recursion = 0 var total = 0 echo "depth ", depth(kxi.currentTree, total), " total ", total @@ -467,7 +495,8 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", ## Setup Karax. Usually the return value can be ignored. result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, - patches: newSeq[Patch](60)) + patches: newSeq[Patch](60), + patchesV: newSeq[PatchV](30)) kxi = result window.onload = init diff --git a/tests/diffDomTests.nim b/tests/diffDomTests.nim index e28c6f6..56f8b9f 100644 --- a/tests/diffDomTests.nim +++ b/tests/diffDomTests.nim @@ -26,7 +26,6 @@ proc check(name: cstring; order: seq[int]) = let result = checkOrder(order) results.add name & (if result: cstring" - OK" else: cstring" -FAIL") -# result: 0 1 2 3 4 7 5 proc test1() = results.add cstring"test1 started" entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] diff --git a/tests/difftest.nim b/tests/difftest.nim index 8bd4d0f..3fbb800 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -43,8 +43,10 @@ proc testAppend() = ul: li: text "A" li: text "B" - li: text "C" - doDiff(a, b, "pkAppend li C") + li: + tdiv: + text "C" + doDiff(a, b, "pkAppend li div C") proc testInsert() = let a = buildHtml(tdiv): @@ -88,12 +90,80 @@ proc testDelete() = "pkDetach li B", "pkRemove nil", "pkDetach li C", "pkRemove nil") +proc testDeleteMiddle() = + let a = buildHtml(tdiv): + ul: + li: + tdiv: text "A" + li: + tdiv: text "B" + li: + tdiv: text "C" + li: + tdiv: text "D" + li: + tdiv: text "E" + li: + tdiv: text "F" + li: + tdiv: text "G" + li: + tdiv: text "H" + let b = buildHtml(tdiv): + ul: + li: + tdiv: text "A" + li: + tdiv: text "B" + li: + tdiv: text "C" + li: + tdiv: text "D" + li: + tdiv: text "E" + li: + tdiv: text "F" + li: + tdiv: text "H" + doDiff(a, b, "pkDetach li div G", "pkRemove nil") + +proc createEntry(id: cstring): VNode = + result = buildHtml(): + button(id="" & id): + text id + +proc createEntries(entries: seq[cstring]): VNode = + result = buildHtml(tdiv()): + ul(id="ul"): + for e in entries: + createEntry(e) + for r in entries: + tdiv: + text r + +proc testWild() = + var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] + let a = createEntries(entries) + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + let b = createEntries(entries) + doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil") + +proc testWildInsert() = + var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] + let a = createEntries(entries) + entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] + let b = createEntries(entries) + doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7") + kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) testAppend() testInsert() testInsert2() testDelete() +testWild() +testWildInsert() +testDeleteMiddle() if err == 0: echo "Success" else: From 907c372c27b019b0f3327cb084d680754d79cc96 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 6 Jul 2017 18:53:07 +0200 Subject: [PATCH 103/264] code cleanups --- src/karax.nim | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index adbd2f2..b83f8c0 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -339,14 +339,12 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let r = if isSpecial: diff(a[i], b[j], parent, current, kxi) else: - (assert same(b, current); #(j < current.len, "j: " & $j & " len " & $current.len); - diff(a[i], b[j], current, current.childNodes[j], kxi)) + diff(a[i], b[j], current, current.childNodes[j], kxi) case r of identical, changed, similar: a[i] = b[j] action of usenewNode: - #b[j] = a[i] kxi.addPatchV(b, j, a[i]) action of different: @@ -370,44 +368,32 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: - when false: - for i in commonPrefix..pos-1: - detach(oldNode[i]) - kxi.addPatch(pkReplace, current, current.childNodes[i], newNode[i]) - when true: - for i in commonPrefix..pos-1: - let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], - kxi) - if r == usenewNode: - oldNode[i] = newNode[i] - elif r != different: - newNode[i] = oldNode[i] - #else: - # result = different + for i in commonPrefix..pos-1: + let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], + kxi) + if r == usenewNode: + #oldNode[i] = newNode[i] + kxi.addPatchV(oldNode, i, newNode[i]) + elif r != different: + newNode[i] = oldNode[i] + #else: + # result = different if oldPos + 1 == oldLength: for i in pos..newPos: kxi.addPatch(pkAppend, current, nil, newNode[i]) result = usenewNode - #result = different else: let before = current.childNodes[oldPos + 1] for i in pos..newPos: kxi.addPatch(pkInsertBefore, current, before, newNode[i]) result = usenewNode - #result = different # XXX call 'attach' here? for i in pos..oldPos: detach(oldNode[i]) #doAssert i < current.childNodes.len kxi.addPatch(pkRemove, current, current.childNodes[i], nil) - result = usenewNode #different - # after the applied patch, conceptually the nodes are identical, so - # no further search is required. 'changed' needs to be propagated - # for the component system to work. 'similar' was transformed into - # identical too: - #if result == different or result == similar: - # result = identical + result = usenewNode of changed: assert oldNode.kind == VNodeKind.component @@ -456,7 +442,7 @@ proc dodraw(kxi: KaraxInstance) = #kout cstring"patch len ", patches.len apply(kxi) kxi.currentTree = newtree - #doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) + doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: kxi.postRenderCallback() From 920bd2db9d6e3432bcfe2dd4ae020ac40fa59d90 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Fri, 7 Jul 2017 16:40:48 +0200 Subject: [PATCH 104/264] bugfix: component handling --- src/karax.nim | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index a2ef499..ba992d9 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -167,9 +167,9 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node = kxi.toFocus = result if not n.style.isNil: applyStyle(result, n.style) -proc same(n: VNode, e: Node): bool = +proc same(n: VNode, e: Node; nesting = 0): bool = if n.kind == VNodeKind.component: - result = same(VComponent(n).expanded, e) + result = same(VComponent(n).expanded, e, nesting+1) elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: # we don't check these for now: result = true @@ -177,10 +177,10 @@ proc same(n: VNode, e: Node): bool = result = true if n.kind != VNodeKind.text: if e.len != n.len: - kout e.len, n.len + kout e.len, n.len, toTag[n.kind], nesting return false for i in 0 ..< n.len: - if not same(n[i], e[i]): return false + if not same(n[i], e[i], nesting+1): return false else: kout toTag[n.kind], e.nodename @@ -298,6 +298,7 @@ proc apply(kxi: KaraxInstance) = for i in 0.. Date: Fri, 7 Jul 2017 17:37:29 +0200 Subject: [PATCH 105/264] make todoapp work again --- src/karax.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index ba992d9..c35e72a 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -311,7 +311,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E #doAssert false, "overflow!" inc kxi.recursion result = eq(newNode, oldNode) - assert(same(oldNode, current)) case result of identical, similar: newNode.dom = oldNode.dom @@ -350,6 +349,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E of usenewNode: kxi.addPatchV(b, j, a[i]) action + # unfortunately, we need to propagate the changes upwards: + result = useNewNode of different: # undo what 'diff' would have done: kxi.patchLen = oldLen @@ -372,8 +373,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E let pos = min(oldPos, newPos) + 1 # now the different children are in commonPrefix .. pos - 1: for i in commonPrefix..pos-1: - let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], - kxi) + let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) if r == usenewNode: #oldNode[i] = newNode[i] kxi.addPatchV(oldNode, i, newNode[i]) From ec4a413a7bcbcc28c22f8faa26cbce9cb0200e07 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 10 Jul 2017 18:00:35 +0200 Subject: [PATCH 106/264] bugfix: better diff'ing for complex components --- src/karax.nim | 5 ++--- tests/blur.nim | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index c35e72a..13c52a2 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -411,9 +411,8 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E else: let res = diff(x.expanded, oldExpanded, parent, current, kxi) if res == usenewNode: - #oldNode[i] = newNode[i] - #kxi.addPatchV(oldNode, i, newNode[i]) - kxi.addPatch(pkReplace, parent, current, x.expanded) + #kxi.addPatch(pkReplace, parent, current, x.expanded) + discard "diff created a patchset for us, so this is fine" elif res != different: x.expanded = oldExpanded assert oldExpanded.dom != nil, "old expanded.dom is nil" diff --git a/tests/blur.nim b/tests/blur.nim index 25c0b24..fbb0a46 100644 --- a/tests/blur.nim +++ b/tests/blur.nim @@ -59,7 +59,7 @@ proc update(current, next: VComponent) = let next = TextInput(next) current.value = next.value current.guid = next.guid - next.isActive = current.isActive + #next.isActive = current.isActive proc newTextInput*(style: VStyle = VStyle(); guid: cstring; value: cstring = cstring"", onchange: proc(v: cstring) = nil): TextInput = @@ -107,10 +107,12 @@ var proc renderPerson(text: cstring, index: int): VNode = proc select(ev: Event, n: VNode) = selected = index + errmsg = "" result = buildHtml(): - tdiv(onClick=select): + tdiv: text text + button(onClick = select) proc createDom(): VNode = result = buildHtml(tdiv): From 6c152ed516b2154fc16b26248676be9aa5601533 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Mon, 10 Jul 2017 21:42:22 +0200 Subject: [PATCH 107/264] smart attribute comparisons --- src/karax.nim | 19 ++++++++++++++++--- src/vdom.nim | 3 +++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 13c52a2..2035f19 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -210,9 +210,8 @@ proc eq(a, b: VNode): EqResult = let x = VComponent(b) assert x.changedImpl != nil return if x.changedImpl(x, VComponent(a)): changed else: identical - if not sameAttrs(a, b): return different if a.class != b.class: return different - if not eq(a.style, b.style): return similar + if not eq(a.style, b.style) or not sameAttrs(a, b): return similar # Do not test event listeners here! return result @@ -223,6 +222,16 @@ proc updateStyles(newNode, oldNode: VNode) = else: oldNode.dom.style = Style() oldNode.style = newNode.style +proc updateAttributes(newNode, oldNode: VNode) = + # we keep the oldNode, but take over the attributes from the new node: + if oldNode.dom != nil: + for k, _ in attrs(oldNode): + oldNode.dom.removeAttribute(k) + for k, v in attrs(newNode): + if v != nil: + oldNode.dom.setAttr(k, v) + takeOverAttr(newNode, oldNode) + proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = let d = oldNode.dom for i in 0.. 100: @@ -314,7 +325,9 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E case result of identical, similar: newNode.dom = oldNode.dom - if result == similar: updateStyles(newNode, oldNode) + if result == similar: + updateStyles(newNode, oldNode) + updateAttributes(newNode, oldNode) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: diff --git a/src/vdom.nim b/src/vdom.nim index 997b059..f751676 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -199,6 +199,9 @@ proc getAttr*(n: VNode; key: cstring): cstring = for i in countup(0, n.attrs.len-2, 2): if n.attrs[i] == key: return n.attrs[i+1] +proc takeOverAttr*(newNode, oldNode: VNode) = + oldNode.attrs = newNode.attrs + proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y From 1e378b2ac238754a0fab34f8e351f4df80608fdd Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 00:20:43 +0200 Subject: [PATCH 108/264] separate component updating from diffing algorithm --- src/karax.nim | 87 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 2035f19..b8b1d60 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -24,6 +24,9 @@ type PatchV = object parent, newChild: VNode pos: int + ComponentPair = object + oldNode, newNode: VComponent + parent, current: Node type KaraxInstance* = ref object ## underlying karax instance. Usually you don't have @@ -40,6 +43,7 @@ type patchesV: seq[PatchV] patchLenV: int runCount: int + components: seq[ComponentPair] when defined(stats): recursion: int @@ -190,7 +194,7 @@ proc replaceById(id: cstring; newTree: Node) = type EqResult = enum - changed, different, similar, identical, usenewNode + componentsIdentical, different, similar, identical, usenewNode proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different @@ -207,9 +211,7 @@ proc eq(a, b: VNode): EqResult = elif b.kind == VNodeKind.component: # different component names mean different components: if a.text != b.text: return different - let x = VComponent(b) - assert x.changedImpl != nil - return if x.changedImpl(x, VComponent(a)): changed else: identical + return componentsIdentical if a.class != b.class: return different if not eq(a.style, b.style) or not sameAttrs(a, b): return similar # Do not test event listeners here! @@ -279,7 +281,7 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = kxi.patchesV[L].pos = pos inc kxi.patchLenV -proc apply(kxi: KaraxInstance) = +proc applyPatch(kxi: KaraxInstance) = for i in 0.. 100: @@ -323,11 +323,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E inc kxi.recursion result = eq(newNode, oldNode) case result - of identical, similar: + of identical, componentsIdentical, similar: newNode.dom = oldNode.dom if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) + elif result == componentsIdentical: + kxi.components.add ComponentPair(oldNode: VComponent(oldNode), + newNode: VComponent(newNode), + parent: parent, + current: current) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: @@ -356,7 +361,7 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E else: diff(a[i], b[j], current, current.childNodes[j], kxi) case r - of identical, changed, similar: + of identical, componentsIdentical, similar: a[i] = b[j] action of usenewNode: @@ -410,27 +415,6 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E #doAssert i < current.childNodes.len kxi.addPatch(pkRemove, current, current.childNodes[i], nil) result = usenewNode - - of changed: - assert oldNode.kind == VNodeKind.component - let x = VComponent(oldNode) - x.updatedImpl(x, VComponent newNode) - let oldExpanded = x.expanded - x.expanded = x.renderImpl(x) - x.renderedVersion = x.version - if oldExpanded == nil: - detach(oldNode) - kxi.addPatch(pkReplace, parent, current, x.expanded) - else: - let res = diff(x.expanded, oldExpanded, parent, current, kxi) - if res == usenewNode: - #kxi.addPatch(pkReplace, parent, current, x.expanded) - discard "diff created a patchset for us, so this is fine" - elif res != different: - x.expanded = oldExpanded - assert oldExpanded.dom != nil, "old expanded.dom is nil" - else: - assert x.expanded.dom != nil, "expanded.dom is nil" of different: detach(oldNode) kxi.addPatch(pkReplace, parent, current, newNode) @@ -438,6 +422,38 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E when defined(stats): dec kxi.recursion +proc applyComponents(kxi: KaraxInstance) = + # the first 'diff' pass detects components in the VDOM. The + # 'applyComponents' expands components and so on until no + # components are left to check. + var i = 0 + # beware: 'diff' appends to kxi.components! + # So this is actually a fixpoint iteration: + while i < kxi.components.len: + let x = kxi.components[i].oldNode + let newNode = kxi.components[i].newNode + if x.changedImpl != nil and x.changedImpl(x, newNode): + let current = kxi.components[i].current + let parent = kxi.components[i].parent + x.updatedImpl(x, newNode) + let oldExpanded = x.expanded + x.expanded = x.renderImpl(x) + x.renderedVersion = x.version + if oldExpanded == nil: + detach(x) + kxi.addPatch(pkReplace, parent, current, x.expanded) + else: + let res = diff(x.expanded, oldExpanded, parent, current, kxi) + if res == usenewNode: + discard "diff created a patchset for us, so this is fine" + elif res != different: + x.expanded = oldExpanded + assert oldExpanded.dom != nil, "old expanded.dom is nil" + else: + assert x.expanded.dom != nil, "expanded.dom is nil" + inc i + setLen(kxi.components, 0) + when defined(stats): proc depth(n: VNode; total: var int): int = var m = 0 @@ -453,16 +469,16 @@ proc dodraw(kxi: KaraxInstance) = newtree.id = kxi.rootId kxi.toFocus = nil if kxi.currentTree == nil: - kxi.currentTree = newtree - let asdom = vnodeToDom(kxi.currentTree, kxi) + let asdom = vnodeToDom(newtree, kxi) replaceById(kxi.rootId, asdom) else: doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) let olddom = document.getElementById(kxi.rootId) discard diff(newtree, kxi.currentTree, nil, olddom, kxi) #kout cstring"patch len ", patches.len - apply(kxi) - kxi.currentTree = newtree + applyComponents(kxi) + applyPatch(kxi) + kxi.currentTree = newtree doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) if not kxi.postRenderCallback.isNil: @@ -503,7 +519,8 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", result = KaraxInstance(rootId: root, renderer: renderer, postRenderCallback: clientPostRenderCallback, patches: newSeq[Patch](60), - patchesV: newSeq[PatchV](30)) + patchesV: newSeq[PatchV](30), + components: @[]) kxi = result window.onload = init From 5838271f8b6e2d9c1b7188db3f79fa5e807d13a7 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 12:43:32 +0200 Subject: [PATCH 109/264] nicer indentation --- src/karax.nim | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index b8b1d60..22ceceb 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -239,8 +239,8 @@ proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = for i in 0.. Date: Tue, 11 Jul 2017 12:54:53 +0200 Subject: [PATCH 110/264] renamed key to 'index'; all virtual components have a 'key' attribute --- examples/mediaplayer/mediaplayer.nim | 10 +++++----- examples/todoapp/todoapp.nim | 16 ++++++++-------- src/karax.nim | 24 +++++++++--------------- src/karaxdsl.nim | 2 +- src/vdom.nim | 26 ++++++++++++++------------ 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/examples/mediaplayer/mediaplayer.nim b/examples/mediaplayer/mediaplayer.nim index 3f970a8..7bc9c65 100644 --- a/examples/mediaplayer/mediaplayer.nim +++ b/examples/mediaplayer/mediaplayer.nim @@ -15,7 +15,7 @@ proc `width=`(n: Node, w: int) {.importcpp: "#.width = #".} proc mplayer*(id, resource: cstring): VNode {.compact.} = proc handler(ev: Event; n: VNode) = let myVideo = document.getElementById(id) - case n.key + case n.index of Play: if myVideo.paused: myVideo.play() @@ -27,13 +27,13 @@ proc mplayer*(id, resource: cstring): VNode {.compact.} = else: discard result = buildHtml(tdiv): - button(onclick=handler, key=Play): + button(onclick=handler, index=Play): text "Play/Pause" - button(onclick=handler, key=Big): + button(onclick=handler, index=Big): text "Big" - button(onclick=handler, key=Small): + button(onclick=handler, index=Small): text "Small" - button(onclick=handler, key=Normal): + button(onclick=handler, index=Normal): text "Normal" br() br() diff --git a/examples/todoapp/todoapp.nim b/examples/todoapp/todoapp.nim index 0caa70c..47ddeae 100644 --- a/examples/todoapp/todoapp.nim +++ b/examples/todoapp/todoapp.nim @@ -45,19 +45,19 @@ proc onTodoEnter(ev: Event; n: VNode) = n.value = "" proc removeHandler(ev: Event; n: VNode) = - updateEntry(n.key, cstring(nil), false) + updateEntry(n.index, cstring(nil), false) proc editHandler(ev: Event; n: VNode) = - selectedEntry = n.key + selectedEntry = n.index proc focusLost(ev: Event; n: VNode) = selectedEntry = -1 proc editEntry(ev: Event; n: VNode) = - setEntryContent(n.key, n.value) + setEntryContent(n.index, n.value) selectedEntry = -1 proc toggleEntry(ev: Event; n: VNode) = - let id = n.key + let id = n.index markAsCompleted(id, not isCompleted(id)) proc onAllDone(ev: Event; n: VNode) = @@ -83,12 +83,12 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac if not selected: tdiv(class = "view"): input(class = "toggle", `type` = "checkbox", checked = toChecked(completed), - onclick=toggleEntry, key=id) - label(onDblClick=editHandler, key=id): + onclick=toggleEntry, index=id) + label(onDblClick=editHandler, index=id): text d - button(class = "destroy", key=id, onclick=removeHandler) + button(class = "destroy", index=id, onclick=removeHandler) else: - input(class = "edit", name = "title", key=id, + input(class = "edit", name = "title", index=id, onblur = focusLost, onkeyupenter = editEntry, value = d, setFocus=true) diff --git a/src/karax.nim b/src/karax.nim index 22ceceb..c96901b 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -7,13 +7,6 @@ export kdom.Event proc kout*[T](x: T) {.importc: "console.log", varargs.} ## the preferred way of debugging karax applications. -proc hasProp(e: Node; prop: cstring): bool {.importcpp: "(#.hasOwnProperty(#))".} -proc rawkey(e: Node): VKey {.importcpp: "#.karaxKey", nodecl.} -proc key*(e: Node): VKey = - if e.hasProp"karaxKey": result = e.rawkey - else: result = -1 -proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} - type PatchKind = enum pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach @@ -200,7 +193,7 @@ proc eq(a, b: VNode): EqResult = if a.kind != b.kind: return different if a.id != b.id: return different result = identical - if a.key != b.key: return different + if a.index != b.index: return different if a.kind == VNodeKind.text: if a.text != b.text: return different elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: @@ -211,6 +204,7 @@ proc eq(a, b: VNode): EqResult = elif b.kind == VNodeKind.component: # different component names mean different components: if a.text != b.text: return different + if VComponent(a).key != VComponent(b).key: return different return componentsIdentical if a.class != b.class: return different if not eq(a.style, b.style) or not sameAttrs(a, b): return similar @@ -246,7 +240,7 @@ proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = applyEvents(oldNode, kxi) proc printV(n: VNode; depth: cstring = "") = - kout depth, cstring($n.kind), cstring"key ", n.key + kout depth, cstring($n.kind), cstring"key ", n.index #for k, v in pairs(n.style): # kout depth, "style: ", k, v if n.kind == VNodeKind.component: @@ -323,16 +317,16 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E inc kxi.recursion result = eq(newNode, oldNode) case result - of identical, componentsIdentical, similar: + of componentsIdentical: + kxi.components.add ComponentPair(oldNode: VComponent(oldNode), + newNode: VComponent(newNode), + parent: parent, + current: current) + of identical, similar: newNode.dom = oldNode.dom if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) - elif result == componentsIdentical: - kxi.components.add ComponentPair(oldNode: VComponent(oldNode), - newNode: VComponent(newNode), - parent: parent, - current: current) if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index 15c6fc6..c17d725 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -4,7 +4,7 @@ from strutils import startsWith, toLowerAscii const StmtContext = ["kout", "inc", "echo", "dec", "!"] - SpecialAttrs = ["id", "class", "value", "key", "style"] + SpecialAttrs = ["id", "class", "value", "index", "style"] proc getName(n: NimNode): string = case n.kind diff --git a/src/vdom.nim b/src/vdom.nim index f751676..8e4f983 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -105,11 +105,11 @@ buildLookupTables() type EventHandler* = proc (ev: Event; target: VNode) {.closure.} NativeEventHandler* = proc (ev: Event) {.closure.} - VKey* = int + VKey* = cstring VNode* = ref object of RootObj kind*: VNodeKind - key*: VKey + index*: int ## a generally useful 'index' id*, class*, text*: cstring kids: seq[VNode] # even index: key, odd index: value; done this way for memory efficiency: @@ -123,6 +123,8 @@ type ## is not part of the virtual DOM anymore. VComponent* = ref object of VNode ## The abstract class for every karax component. + key*: VKey ## key that determines if two components are + ## identical. renderImpl*: proc(self: VComponent): VNode changedImpl*: proc(self, newInstance: VComponent): bool updatedImpl*: proc(self, newInstance: VComponent) @@ -140,20 +142,20 @@ type proc value*(n: VNode): cstring = n.text proc `value=`*(n: VNode; v: cstring) = n.text = v -proc intValue*(n: VNode): int = n.key -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 intValue*(n: VNode): int = n.index +proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i) +proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b)) +proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x) template callThunk*(fn: typed; n: VNode): untyped = ## for internal usage only. fn(n.kids) proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = - VNode(kind: VNodeKind.vthunk, text: name, key: -1, kids: @args) + VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args) 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, index: -1, kids: @args) proc defaultChangedImpl*(v, newInstance: VComponent): bool = ## The default implementation of 'changed'. @@ -174,7 +176,7 @@ template newComponent*[T](t: typeDesc[T]; changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl, updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = ## Use this template to create new components. - T(kind: VNodeKind.component, key: -1, + T(kind: VNodeKind.component, index: -1, text: cstring(astToStr(t)), renderImpl: render, changedImpl: changed, updatedImpl: updated, onAttachImpl: onAttach, onDetachImpl: onDetach, @@ -206,7 +208,7 @@ proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc add*(parent, kid: VNode) = parent.kids.add kid -proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, key: -1) +proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1) proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode = result = newVNode(kind) @@ -217,8 +219,8 @@ proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; result = tree(kind, kids) for a in attrs: result.setAttr(a[0], a[1]) -proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), key: -1) -proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, key: -1) +proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) +proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) iterator items*(n: VNode): VNode = for i in 0.. Date: Tue, 11 Jul 2017 18:59:38 +0200 Subject: [PATCH 111/264] better event handling for components and in the DSL --- src/karax.nim | 1 + src/karaxdsl.nim | 38 ++++++++++++- src/vdom.nim | 19 ++++++- tests/components.nim | 128 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 tests/components.nim diff --git a/src/karax.nim b/src/karax.nim index c96901b..f284d31 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -61,6 +61,7 @@ template setNativeValue(ev, val) = cast[Element](ev.target).value = val template keyeventBody() = let v = nativeValue(ev) n.value = v + assert action != nil action(ev, n) if n.value != v: setNativeValue(ev, n.value) diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index c17d725..cfbe391 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -59,6 +59,19 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add n[0] for i in 1 ..< n.len: result.add tcall2(n[i], tmpContext) + of nnkProcDef: + let name = getName n[0] + if name.startsWith"on": + # turn it into an anon proc: + let anon = copyNimTree(n) + anon[0] = newEmptyNode() + if tmpContext == nil: + error "no VNode to attach the event handler to" + else: + result = newCall(bindSym"addEventHandler", tmpContext, + newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) + else: + result = n of nnkVarSection, nnkLetSection, nnkConstSection: result = n of nnkCallKinds: @@ -87,6 +100,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode = result.add newDotAsgn(tmp, key, x[1]) elif eqIdent(key, "setFocus"): result.add newCall(key, tmp, x[1], ident"kxi") + elif eqIdent(key, "events"): + result.add newCall(bindSym"mergeEvents", tmp, x[1]) else: result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) elif ck != ComponentKind.Tag: @@ -100,7 +115,28 @@ proc tcall2(n, tmpContext: NimNode): NimNode = else: result.add newCall(bindSym"add", tmpContext, tmp) elif tmpContext != nil and op notin StmtContext: - result = newCall(bindSym"add", tmpContext, n) + var hasEventHandlers = false + for i in 1.. 0: + persons[index] = v + errmsg = "" + else: + errmsg = "name must not be empty" + button: + proc onclick(ev: Event; n: VNode) = + persons.delete(index) + errmsg = "" + echo persons + text "(x)" + + +proc createDom(): VNode = + result = buildHtml(tdiv): + tdiv: + for index, text in persons.pairs: + renderPerson(text, index) + tdiv: + newTextInput(VStyle(), &persons.len, ""): + proc onkeyuplater(ev: Event; n: VNode) = + let v = n.value + if v.len > 0: + persons.add v + errmsg = "" + else: + errmsg = "name must not be empty" + tdiv: + text errmsg + +setRenderer createDom From b3c83ed11998efb03b2a578ca0832430e7457343 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 20:41:01 +0200 Subject: [PATCH 112/264] optimized the diff algorithm further --- src/karax.nim | 55 +++++++++++++++++++++++++++++++++++++++++++-------- src/vdom.nim | 2 +- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f284d31..6c048cc 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -190,13 +190,33 @@ type EqResult = enum componentsIdentical, different, similar, identical, usenewNode + DifferEnum = enum + deKind, deId, deIndex, deText, deComponent, deClass, + deSimilar + +when defined(profileKarax): + var + reasons: array[DifferEnum, int] + + proc echa(a: array[DifferEnum, int]) = + for i in low(DifferEnum)..high(DifferEnum): + echo i, " value: ", a[i] + proc eq(a, b: VNode): EqResult = - if a.kind != b.kind: return different - if a.id != b.id: return different + if a.kind != b.kind: + when defined(profileKarax): inc reasons[deKind] + return different + if a.id != b.id: + when defined(profileKarax): inc reasons[deId] + return different result = identical - if a.index != b.index: return different + if a.index != b.index: + when defined(profileKarax): inc reasons[deIndex] + return different if a.kind == VNodeKind.text: - if a.text != b.text: return different + if a.text != b.text: + when defined(profileKarax): inc reasons[deText] + return similar elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: if a.text != b.text: return different if a.len != b.len: return different @@ -204,11 +224,19 @@ proc eq(a, b: VNode): EqResult = if eq(a[i], b[i]) == different: return different elif b.kind == VNodeKind.component: # different component names mean different components: - if a.text != b.text: return different - if VComponent(a).key != VComponent(b).key: return different + if a.text != b.text: + when defined(profileKarax): inc reasons[deComponent] + return different + if VComponent(a).key != VComponent(b).key: + when defined(profileKarax): inc reasons[deComponent] + return different return componentsIdentical - if a.class != b.class: return different - if not eq(a.style, b.style) or not sameAttrs(a, b): return similar + if a.class != b.class: + when defined(profileKarax): inc reasons[deClass] + return different + if not eq(a.style, b.style) or not sameAttrs(a, b): + when defined(profileKarax): inc reasons[deSimilar] + return similar # Do not test event listeners here! return result @@ -328,6 +356,10 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E if result == similar: updateStyles(newNode, oldNode) updateAttributes(newNode, oldNode) + if oldNode.kind == VNodeKind.text: + oldNode.text = newNode.text + oldNode.dom.nodeValue = newNode.text + if newNode.events.len != 0 or oldNode.events.len != 0: mergeEvents(newNode, oldNode, kxi) if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea: @@ -471,7 +503,14 @@ proc dodraw(kxi: KaraxInstance) = let olddom = document.getElementById(kxi.rootId) discard diff(newtree, kxi.currentTree, nil, olddom, kxi) #kout cstring"patch len ", patches.len + when defined(profileKarax): + echo "<<<<<<<<<<<<<<" + echa reasons applyComponents(kxi) + when defined(profileKarax): + echo "--------------" + echa reasons + echo ">>>>>>>>>>>>>>" applyPatch(kxi) kxi.currentTree = newtree doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) diff --git a/src/vdom.nim b/src/vdom.nim index 4ffb7f4..2e3b7dd 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -217,7 +217,7 @@ proc getAttr*(n: VNode; key: cstring): cstring = if n.attrs[i] == key: return n.attrs[i+1] proc takeOverAttr*(newNode, oldNode: VNode) = - oldNode.attrs = newNode.attrs + shallowCopy oldNode.attrs, newNode.attrs proc len*(x: VNode): int = x.kids.len proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] From e4d8cb4c206571751c12f59d2ae72e8f0a30f75a Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 11 Jul 2017 22:26:46 +0200 Subject: [PATCH 113/264] make tests green again --- tests/difftest.nim | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tests/difftest.nim b/tests/difftest.nim index 3fbb800..cfdd9b4 100644 --- a/tests/difftest.nim +++ b/tests/difftest.nim @@ -52,12 +52,16 @@ proc testInsert() = let a = buildHtml(tdiv): ul: li: text "A" - li: text "C" + li: + button: + text "C" let b = buildHtml(tdiv): ul: li: text "A" li: text "B" - li: text "C" + li: + button: + text "C" doDiff(a, b, "pkInsertBefore li B") proc testInsert2() = @@ -66,7 +70,9 @@ proc testInsert2() = tdiv: ul: li: text "A" - li: text "D" + li: + button: + text "D" let b = buildHtml(tdiv): tdiv: tdiv: @@ -74,7 +80,9 @@ proc testInsert2() = li: text "A" li: text "B" li: text "C" - li: text "D" + li: + button: + text "D" doDiff(a, b, "pkInsertBefore li B", "pkInsertBefore li C") proc testDelete() = @@ -108,7 +116,8 @@ proc testDeleteMiddle() = li: tdiv: text "G" li: - tdiv: text "H" + button: + tdiv: text "H" let b = buildHtml(tdiv): ul: li: @@ -124,7 +133,8 @@ proc testDeleteMiddle() = li: tdiv: text "F" li: - tdiv: text "H" + button: + tdiv: text "H" doDiff(a, b, "pkDetach li div G", "pkRemove nil") proc createEntry(id: cstring): VNode = @@ -146,14 +156,14 @@ proc testWild() = let a = createEntries(entries) entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] let b = createEntries(entries) - doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil") + doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 5", "pkRemove nil") proc testWildInsert() = var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] let a = createEntries(entries) entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] let b = createEntries(entries) - doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7") + doDiff(a, b, "pkInsertBefore button 7", "pkAppend div 5") kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) From 2ced69038786eb8456825ef46fb362360248c461 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 12 Jul 2017 12:23:24 +0200 Subject: [PATCH 114/264] better event merging for components --- src/karax.nim | 12 ++++++++---- src/vdom.nim | 6 +++--- tests/components.nim | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index 6c048cc..d5de6f5 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -190,11 +190,12 @@ type EqResult = enum componentsIdentical, different, similar, identical, usenewNode - DifferEnum = enum - deKind, deId, deIndex, deText, deComponent, deClass, - deSimilar - when defined(profileKarax): + type + DifferEnum = enum + deKind, deId, deIndex, deText, deComponent, deClass, + deSimilar + var reasons: array[DifferEnum, int] @@ -227,6 +228,9 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: when defined(profileKarax): inc reasons[deComponent] return different + if VComponent(a).key.isNil and VComponent(b).key.isNil: + when defined(profileKarax): inc reasons[deComponent] + return different if VComponent(a).key != VComponent(b).key: when defined(profileKarax): inc reasons[deComponent] return different diff --git a/src/vdom.nim b/src/vdom.nim index 2e3b7dd..f31d6d6 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -160,17 +160,17 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args) -proc setEvent(v: VNode; kind: EventKind; handler: EventHandler) = +proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) = assert handler != nil for i in 0.. 0: persons.add v From e94071b077d125534d8e95006c1b7c4e74171b27 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 12 Jul 2017 13:23:58 +0200 Subject: [PATCH 115/264] components work without 'key' attribute set --- src/karax.nim | 6 +++--- tests/components.nim | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index d5de6f5..9d74a9e 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -228,9 +228,9 @@ proc eq(a, b: VNode): EqResult = if a.text != b.text: when defined(profileKarax): inc reasons[deComponent] return different - if VComponent(a).key.isNil and VComponent(b).key.isNil: - when defined(profileKarax): inc reasons[deComponent] - return different + #if VComponent(a).key.isNil and VComponent(b).key.isNil: + # when defined(profileKarax): inc reasons[deComponent] + # return different if VComponent(a).key != VComponent(b).key: when defined(profileKarax): inc reasons[deComponent] return different diff --git a/tests/components.nim b/tests/components.nim index 99e1bed..6024689 100644 --- a/tests/components.nim +++ b/tests/components.nim @@ -54,7 +54,7 @@ proc newTextInput*(style: VStyle = VStyle(); key: cstring; result = newComponent(TextInput, render, changed=changed, updated=update) result.style = style result.value = value - result.key = key + #result.key = key when false: type From f9525dc646224afe8988303e87dc8ffafc5fae94 Mon Sep 17 00:00:00 2001 From: Araq Date: Fri, 14 Jul 2017 16:17:06 +0200 Subject: [PATCH 116/264] added support for i18n (candidate for the stdlib?) --- src/i18n.nim | 152 ++++++++++++ src/languages.nim | 616 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 768 insertions(+) create mode 100644 src/i18n.nim create mode 100644 src/languages.nim diff --git a/src/i18n.nim b/src/i18n.nim new file mode 100644 index 0000000..42274ff --- /dev/null +++ b/src/i18n.nim @@ -0,0 +1,152 @@ +## Karax -- Single page applications for Nim. + +## i18n support for Karax applications. We distinguish between: +## +## - translate: Lookup text translations +## - localize: Localize Date and Time objects to local formats +## +## Localization has not yet been implemented. + +import languages, jdict, jstrutils + +var currentLanguage = Language.enUS + +proc getLanguage(): cstring = + {.emit: "`result` = (navigator.languages && navigator.languages.length) ? navigator.languages[0] : navigator.language;".} + +proc detectLanguage*(): Language = + let x = getLanguage() + for i in low(Language)..high(Language): + if languageToCode[i] == x: return i + return Language.enUS + +proc setCurrentLanguage*(newLanguage = detectLanguage()) = + currentLanguage = newLanguage + +proc getCurrentLanguage*(): Language = currentLanguage + +type Translation* = JDict[cstring, cstring] + +var + translations: array[Language, Translation] + +proc registerTranslation*(lang: Language; t: Translation) = translations[lang] = t + +proc addTranslation*(lang: Language; key, val: cstring) = + if translations[lang].isNil: translations[lang] = newJDict[cstring, cstring]() + translations[lang][key] = val + +proc translate(x: cstring): cstring = + let y = translations[currentLanguage] + if y != nil and y.contains(x): + result = y[x] + else: + result = x + +type TranslatedString* = distinct cstring + +template i18n*(x: string): TranslatedString = TranslatedString(translate(cstring x)) + +proc raiseInvalidFormat(errmsg: string) = + raise newException(ValueError, errmsg) + +discard """ + $[1:item|items]1$ selected. +""" + +proc `{}`(x: cstring; i: int): char {.importcpp: "#.charCodeAt(#)".} +proc sadd(s: JSeq[char]; c: char) {.importcpp: "#.push(String.fromCharCode(#))".} + +proc parseChoice(f: cstring; i, choice: int, + r: JSeq[char]) = + var i = i + while i < f.len: + var n = 0 + let oldI = i + var toAdd = false + while i < f.len and f{i} >= '0' and f{i} <= '9': + n = n * 10 + ord(f{i}) - ord('0') + inc i + if oldI != i: + if f{i} == ':': + inc i + else: + raiseInvalidFormat"':' after number expected" + toAdd = choice == n + else: + # an else section does not start with a number: + toAdd = true + while i < f.len and f{i} != ']' and f{i} != '|': + if toAdd: r.sadd f{i} + inc i + if toAdd: break + inc i + +proc join(x: JSeq[char]): cstring {.importcpp: "#.join(\"\")".} +proc add(x: JSeq[char]; y: cstring) = + for i in 0..= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + r.add args[idx] + of '$': + inc(i) + r.add '$' + of '[': + let start = i+1 + while i < f.len and f{i} != ']': inc i + inc i + if i >= f.len: raiseInvalidFormat"']' expected" + case f{i} + of '#': + parseChoice(f, start, parseInt args[num], r) + inc i + inc num + of '1'..'9', '-': + var j = 0 + var negative = f{i} == '-' + if negative: inc i + while f{i} >= '0' and f{i} <= '9': + j = j * 10 + ord(f{i}) - ord('0') + inc i + let idx = if not negative: j-1 else: args.len-j + parseChoice(f, start, parseInt args[idx], r) + else: raiseInvalidFormat"argument index expected after ']'" + else: + raiseInvalidFormat("'#', '$', or number expected") + if i < f.len and f{i} == '$': inc i + else: + r.sadd f{i} + inc i + result = join(r) + + +when isMainModule: + addTranslation(Language.deDE, "$#$ -> $#$", "$2 macht $1") + setCurrentLanguage(Language.deDE) + + echo(i18n"$#$ -> $#$" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"1", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"0", "2"]) + echo(i18n"$[1:item |items ]1 -> $1" % [cstring"2", "2"]) + echo(i18n"$[1:item |items ]1$ -> $1" % [cstring"3", "2"]) + echo(i18n"$1 $[1:item |4:huha |items ]1$ -> $1" % [cstring"4", "2"]) diff --git a/src/languages.nim b/src/languages.nim new file mode 100644 index 0000000..6b6b027 --- /dev/null +++ b/src/languages.nim @@ -0,0 +1,616 @@ +## Karax -- Single page applications for Nim. + +type + Language* {.pure.} = enum ## A pure enum that contains most (all?) + ## known human languages in current use. + afZA ## "South Africa - Afrikaans" + amET ## "Ethiopia - Amharic" + arAE ## "U.A.E. - Arabic" + arBH ## "Bahrain - Arabic" + arDZ ## "Algeria - Arabic" + arEG ## "Egypt - Arabic" + arIQ ## "Iraq - Arabic" + arJO ## "Jordan - Arabic" + arKW ## "Kuwait - Arabic" + arLB ## "Lebanon - Arabic" + arLY ## "Libya - Arabic" + arMA ## "Morocco - Arabic" + arOM ## "Oman - Arabic" + arQA ## "Qatar - Arabic" + arSA ## "Saudi Arabia - Arabic" + arSY ## "Syria - Arabic" + arTN ## "Tunisia - Arabic" + arYE ## "Yemen - Arabic" + arnCL ## "Chile - Mapudungun" + asIN ## "India - Assamese" + azAZ ## "Azerbaijan - Azeri (Cyrillic)" + baRU ## "Russia - Bashkir" + beBY ## "Belarus - Belarusian" + bgBG ## "Bulgaria - Bulgarian" + bnBD ## "Bangladesh - Bengali" + bnIN ## "India - Bengali" + boCN ## "People's Republic of China - Tibetan" + brFR ## "France - Breton" + bsBA ## "Bosnia and Herzegovina - Bosnian (Cyrillic)" + caES ## "Spain - Catalan" + coFR ## "France - Corsican" + csCZ ## "Czech Republic - Czech" + cyGB ## "United Kingdom - Welsh" + daDK ## "Denmark - Danish" + deAT ## "Austria - German" + deCH ## "Switzerland - German" + deDE ## "Germany - German" + deLI ## "Liechtenstein - German" + deLU ## "Luxembourg - German" + dsbDE ## "Germany - Lower Sorbian" + dvMV ## "Maldives - Divehi" + elGR ## "Greece - Greek" + en029 ## "Caribbean - English" + enAU ## "Australia - English" + enBZ ## "Belize - English" + enCA ## "Canada - English" + enGB ## "United Kingdom - English" + enIE ## "Ireland - English" + enIN ## "India - English" + enJM ## "Jamaica - English" + enMY ## "Malaysia - English" + enNZ ## "New Zealand - English" + enPH ## "Republic of the Philippines - English" + enSG ## "Singapore - English" + enTT ## "Trinidad and Tobago - English" + enUS ## "United States - English" + enZA ## "South Africa - English" + enZW ## "Zimbabwe - English" + esAR ## "Argentina - Spanish" + esBO ## "Bolivia - Spanish" + esCL ## "Chile - Spanish" + esCO ## "Colombia - Spanish" + esCR ## "Costa Rica - Spanish" + esDO ## "Dominican Republic - Spanish" + esEC ## "Ecuador - Spanish" + esES ## "Spain - Spanish" + esGT ## "Guatemala - Spanish" + esHN ## "Honduras - Spanish" + esMX ## "Mexico - Spanish" + esNI ## "Nicaragua - Spanish" + esPA ## "Panama - Spanish" + esPE ## "Peru - Spanish" + esPR ## "Puerto Rico - Spanish" + esPY ## "Paraguay - Spanish" + esSV ## "El Salvador - Spanish" + esUS ## "United States - Spanish" + esUY ## "Uruguay - Spanish" + esVE ## "Bolivarian Republic of Venezuela - Spanish" + etEE ## "Estonia - Estonian" + euES ## "Spain - Basque" + faIR ## "Iran - Persian" + fiFI ## "Finland - Finnish" + filPH ## "Philippines - Filipino" + foFO ## "Faroe Islands - Faroese" + frBE ## "Belgium - French" + frCA ## "Canada - French" + frCH ## "Switzerland - French" + frFR ## "France - French" + frLU ## "Luxembourg - French" + frMC ## "Principality of Monaco - French" + fyNL ## "Netherlands - Frisian" + gaIE ## "Ireland - Irish" + gdGB ## "United Kingdom - Scottish Gaelic" + glES ## "Spain - Galician" + gswFR ## "France - Alsatian" + guIN ## "India - Gujarati" + haNG ## "Nigeria - Hausa (Latin)" + heIL ## "Israel - Hebrew" + hiIN ## "India - Hindi" + hrBA ## "Bosnia and Herzegovina - Croatian" + hrHR ## "Croatia - Croatian" + hsbDE ## "Germany - Upper Sorbian" + huHU ## "Hungary - Hungarian" + hyAM ## "Armenia - Armenian" + idID ## "Indonesia - Indonesian" + igNG ## "Nigeria - Igbo" + iiCN ## "People's Republic of China - Yi" + isIS ## "Iceland - Icelandic" + itCH ## "Switzerland - Italian" + itIT ## "Italy - Italian" + iuCA ## "Canada - Inuktitut (Latin)" + jaJP ## "Japan - Japanese" + kaGE ## "Georgia - Georgian" + kkKZ ## "Kazakhstan - Kazakh" + klGL ## "Greenland - Greenlandic" + kmKH ## "Cambodia - Khmer" + knIN ## "India - Kannada" + koKR ## "Korea - Korean" + kokIN ## "India - Konkani" + kyKG ## "Kyrgyzstan - Kyrgyz" + lbLU ## "Luxembourg - Luxembourgish" + loLA ## "Lao P.D.R. - Lao" + ltLT ## "Lithuania - Lithuanian" + lvLV ## "Latvia - Latvian" + miNZ ## "New Zealand - Maori" + mkMK ## "Macedonia (FYROM) - Macedonian (FYROM)" + mlIN ## "India - Malayalam" + mnCN ## "People's Republic of China - Mongolian (Traditional Mongolian)" + mnMN ## "Mongolia - Mongolian (Cyrillic)" + mohCA ## "Canada - Mohawk" + mrIN ## "India - Marathi" + msBN ## "Brunei Darussalam - Malay" + msMY ## "Malaysia - Malay" + mtMT ## "Malta - Maltese" + nbNO ## "Norway - Norwegian (Bokmal)" + neNP ## "Nepal - Nepali" + nlBE ## "Belgium - Dutch" + nlNL ## "Netherlands - Dutch" + nnNO ## "Norway - Norwegian (Nynorsk)" + nsoZA ## "South Africa - Sesotho sa Leboa" + ocFR ## "France - Occitan" + orIN ## "India - Oriya" + paIN ## "India - Punjabi" + plPL ## "Poland - Polish" + prsAF ## "Afghanistan - Dari" + psAF ## "Afghanistan - Pashto" + ptBR ## "Brazil - Portuguese" + ptPT ## "Portugal - Portuguese" + qutGT ## "Guatemala - K'iche" + quzBO ## "Bolivia - Quechua" + quzEC ## "Ecuador - Quechua" + quzPE ## "Peru - Quechua" + rmCH ## "Switzerland - Romansh" + roRO ## "Romania - Romanian" + ruRU ## "Russia - Russian" + rwRW ## "Rwanda - Kinyarwanda" + saIN ## "India - Sanskrit" + sahRU ## "Russia - Yakut" + seFI ## "Finland - Sami (Northern)" + seNO ## "Norway - Sami (Northern)" + seSE ## "Sweden - Sami (Northern)" + siLK ## "Sri Lanka - Sinhala" + skSK ## "Slovakia - Slovak" + slSI ## "Slovenia - Slovenian" + smaNO ## "Norway - Sami (Southern)" + smaSE ## "Sweden - Sami (Southern)" + smjNO ## "Norway - Sami (Lule)" + smjSE ## "Sweden - Sami (Lule)" + smnFI ## "Finland - Sami (Inari)" + smsFI ## "Finland - Sami (Skolt)" + sqAL ## "Albania - Albanian" + srBA ## "Bosnia and Herzegovina - Serbian (Cyrillic)" + srCS ## "Serbia and Montenegro (Former) - Serbian (Cyrillic)" + srME ## "Montenegro - Serbian (Cyrillic)" + srRS ## "Serbia - Serbian (Cyrillic)" + svFI ## "Finland - Swedish" + svSE ## "Sweden - Swedish" + swKE ## "Kenya - Kiswahili" + syrSY ## "Syria - Syriac" + taIN ## "India - Tamil" + teIN ## "India - Telugu" + tgTJ ## "Tajikistan - Tajik (Cyrillic)" + thTH ## "Thailand - Thai" + tkTM ## "Turkmenistan - Turkmen" + tnZA ## "South Africa - Setswana" + trTR ## "Turkey - Turkish" + ttRU ## "Russia - Tatar" + tzmDZ ## "Algeria - Tamazight (Latin)" + ugCN ## "People's Republic of China - Uyghur" + ukUA ## "Ukraine - Ukrainian" + urPK ## "Islamic Republic of Pakistan - Urdu" + uzUZ ## "Uzbekistan - Uzbek (Cyrillic)" + viVN ## "Vietnam - Vietnamese" + woSN ## "Senegal - Wolof" + xhZA ## "South Africa - isiXhosa" + yoNG ## "Nigeria - Yoruba" + zhCN ## "People's Republic of China - Chinese (Simplified) Legacy" + zhHK ## "Hong Kong S.A.R. - Chinese (Traditional) Legacy" + zhMO ## "Macao S.A.R. - Chinese (Traditional) Legacy" + zhSG ## "Singapore - Chinese (Simplified) Legacy" + zhTW ## "Taiwan - Chinese (Traditional) Legacy" + zuZA ## "South Africa - isiZulu" + +const + languageToName*: array[Language, cstring] = [ + Language.afZA: cstring"South Africa - Afrikaans", + Language.amET: cstring"Ethiopia - Amharic", + Language.arAE: cstring"U.A.E. - Arabic", + Language.arBH: cstring"Bahrain - Arabic", + Language.arDZ: cstring"Algeria - Arabic", + Language.arEG: cstring"Egypt - Arabic", + Language.arIQ: cstring"Iraq - Arabic", + Language.arJO: cstring"Jordan - Arabic", + Language.arKW: cstring"Kuwait - Arabic", + Language.arLB: cstring"Lebanon - Arabic", + Language.arLY: cstring"Libya - Arabic", + Language.arMA: cstring"Morocco - Arabic", + Language.arOM: cstring"Oman - Arabic", + Language.arQA: cstring"Qatar - Arabic", + Language.arSA: cstring"Saudi Arabia - Arabic", + Language.arSY: cstring"Syria - Arabic", + Language.arTN: cstring"Tunisia - Arabic", + Language.arYE: cstring"Yemen - Arabic", + Language.arnCL: cstring"Chile - Mapudungun", + Language.asIN: cstring"India - Assamese", + Language.azAZ: cstring"Azerbaijan - Azeri (Cyrillic)", + Language.baRU: cstring"Russia - Bashkir", + Language.beBY: cstring"Belarus - Belarusian", + Language.bgBG: cstring"Bulgaria - Bulgarian", + Language.bnBD: cstring"Bangladesh - Bengali", + Language.bnIN: cstring"India - Bengali", + Language.boCN: cstring"People's Republic of China - Tibetan", + Language.brFR: cstring"France - Breton", + Language.bsBA: cstring"Bosnia and Herzegovina - Bosnian (Cyrillic)", + Language.caES: cstring"Spain - Catalan", + Language.coFR: cstring"France - Corsican", + Language.csCZ: cstring"Czech Republic - Czech", + Language.cyGB: cstring"United Kingdom - Welsh", + Language.daDK: cstring"Denmark - Danish", + Language.deAT: cstring"Austria - German", + Language.deCH: cstring"Switzerland - German", + Language.deDE: cstring"Germany - German", + Language.deLI: cstring"Liechtenstein - German", + Language.deLU: cstring"Luxembourg - German", + Language.dsbDE: cstring"Germany - Lower Sorbian", + Language.dvMV: cstring"Maldives - Divehi", + Language.elGR: cstring"Greece - Greek", + Language.en029: cstring"Caribbean - English", + Language.enAU: cstring"Australia - English", + Language.enBZ: cstring"Belize - English", + Language.enCA: cstring"Canada - English", + Language.enGB: cstring"United Kingdom - English", + Language.enIE: cstring"Ireland - English", + Language.enIN: cstring"India - English", + Language.enJM: cstring"Jamaica - English", + Language.enMY: cstring"Malaysia - English", + Language.enNZ: cstring"New Zealand - English", + Language.enPH: cstring"Republic of the Philippines - English", + Language.enSG: cstring"Singapore - English", + Language.enTT: cstring"Trinidad and Tobago - English", + Language.enUS: cstring"United States - English", + Language.enZA: cstring"South Africa - English", + Language.enZW: cstring"Zimbabwe - English", + Language.esAR: cstring"Argentina - Spanish", + Language.esBO: cstring"Bolivia - Spanish", + Language.esCL: cstring"Chile - Spanish", + Language.esCO: cstring"Colombia - Spanish", + Language.esCR: cstring"Costa Rica - Spanish", + Language.esDO: cstring"Dominican Republic - Spanish", + Language.esEC: cstring"Ecuador - Spanish", + Language.esES: cstring"Spain - Spanish", + Language.esGT: cstring"Guatemala - Spanish", + Language.esHN: cstring"Honduras - Spanish", + Language.esMX: cstring"Mexico - Spanish", + Language.esNI: cstring"Nicaragua - Spanish", + Language.esPA: cstring"Panama - Spanish", + Language.esPE: cstring"Peru - Spanish", + Language.esPR: cstring"Puerto Rico - Spanish", + Language.esPY: cstring"Paraguay - Spanish", + Language.esSV: cstring"El Salvador - Spanish", + Language.esUS: cstring"United States - Spanish", + Language.esUY: cstring"Uruguay - Spanish", + Language.esVE: cstring"Bolivarian Republic of Venezuela - Spanish", + Language.etEE: cstring"Estonia - Estonian", + Language.euES: cstring"Spain - Basque", + Language.faIR: cstring"Iran - Persian", + Language.fiFI: cstring"Finland - Finnish", + Language.filPH: cstring"Philippines - Filipino", + Language.foFO: cstring"Faroe Islands - Faroese", + Language.frBE: cstring"Belgium - French", + Language.frCA: cstring"Canada - French", + Language.frCH: cstring"Switzerland - French", + Language.frFR: cstring"France - French", + Language.frLU: cstring"Luxembourg - French", + Language.frMC: cstring"Principality of Monaco - French", + Language.fyNL: cstring"Netherlands - Frisian", + Language.gaIE: cstring"Ireland - Irish", + Language.gdGB: cstring"United Kingdom - Scottish Gaelic", + Language.glES: cstring"Spain - Galician", + Language.gswFR: cstring"France - Alsatian", + Language.guIN: cstring"India - Gujarati", + Language.haNG: cstring"Nigeria - Hausa (Latin)", + Language.heIL: cstring"Israel - Hebrew", + Language.hiIN: cstring"India - Hindi", + Language.hrBA: cstring"Bosnia and Herzegovina - Croatian", + Language.hrHR: cstring"Croatia - Croatian", + Language.hsbDE: cstring"Germany - Upper Sorbian", + Language.huHU: cstring"Hungary - Hungarian", + Language.hyAM: cstring"Armenia - Armenian", + Language.idID: cstring"Indonesia - Indonesian", + Language.igNG: cstring"Nigeria - Igbo", + Language.iiCN: cstring"People's Republic of China - Yi", + Language.isIS: cstring"Iceland - Icelandic", + Language.itCH: cstring"Switzerland - Italian", + Language.itIT: cstring"Italy - Italian", + Language.iuCA: cstring"Canada - Inuktitut (Latin)", + Language.jaJP: cstring"Japan - Japanese", + Language.kaGE: cstring"Georgia - Georgian", + Language.kkKZ: cstring"Kazakhstan - Kazakh", + Language.klGL: cstring"Greenland - Greenlandic", + Language.kmKH: cstring"Cambodia - Khmer", + Language.knIN: cstring"India - Kannada", + Language.koKR: cstring"Korea - Korean", + Language.kokIN: cstring"India - Konkani", + Language.kyKG: cstring"Kyrgyzstan - Kyrgyz", + Language.lbLU: cstring"Luxembourg - Luxembourgish", + Language.loLA: cstring"Lao P.D.R. - Lao", + Language.ltLT: cstring"Lithuania - Lithuanian", + Language.lvLV: cstring"Latvia - Latvian", + Language.miNZ: cstring"New Zealand - Maori", + Language.mkMK: cstring"Macedonia (FYROM) - Macedonian (FYROM)", + Language.mlIN: cstring"India - Malayalam", + Language.mnCN: cstring"People's Republic of China - Mongolian (Traditional Mongolian)", + Language.mnMN: cstring"Mongolia - Mongolian (Cyrillic)", + Language.mohCA: cstring"Canada - Mohawk", + Language.mrIN: cstring"India - Marathi", + Language.msBN: cstring"Brunei Darussalam - Malay", + Language.msMY: cstring"Malaysia - Malay", + Language.mtMT: cstring"Malta - Maltese", + Language.nbNO: cstring"Norway - Norwegian (Bokmal)", + Language.neNP: cstring"Nepal - Nepali", + Language.nlBE: cstring"Belgium - Dutch", + Language.nlNL: cstring"Netherlands - Dutch", + Language.nnNO: cstring"Norway - Norwegian (Nynorsk)", + Language.nsoZA: cstring"South Africa - Sesotho sa Leboa", + Language.ocFR: cstring"France - Occitan", + Language.orIN: cstring"India - Oriya", + Language.paIN: cstring"India - Punjabi", + Language.plPL: cstring"Poland - Polish", + Language.prsAF: cstring"Afghanistan - Dari", + Language.psAF: cstring"Afghanistan - Pashto", + Language.ptBR: cstring"Brazil - Portuguese", + Language.ptPT: cstring"Portugal - Portuguese", + Language.qutGT: cstring"Guatemala - K'iche", + Language.quzBO: cstring"Bolivia - Quechua", + Language.quzEC: cstring"Ecuador - Quechua", + Language.quzPE: cstring"Peru - Quechua", + Language.rmCH: cstring"Switzerland - Romansh", + Language.roRO: cstring"Romania - Romanian", + Language.ruRU: cstring"Russia - Russian", + Language.rwRW: cstring"Rwanda - Kinyarwanda", + Language.saIN: cstring"India - Sanskrit", + Language.sahRU: cstring"Russia - Yakut", + Language.seFI: cstring"Finland - Sami (Northern)", + Language.seNO: cstring"Norway - Sami (Northern)", + Language.seSE: cstring"Sweden - Sami (Northern)", + Language.siLK: cstring"Sri Lanka - Sinhala", + Language.skSK: cstring"Slovakia - Slovak", + Language.slSI: cstring"Slovenia - Slovenian", + Language.smaNO: cstring"Norway - Sami (Southern)", + Language.smaSE: cstring"Sweden - Sami (Southern)", + Language.smjNO: cstring"Norway - Sami (Lule)", + Language.smjSE: cstring"Sweden - Sami (Lule)", + Language.smnFI: cstring"Finland - Sami (Inari)", + Language.smsFI: cstring"Finland - Sami (Skolt)", + Language.sqAL: cstring"Albania - Albanian", + Language.srBA: cstring"Bosnia and Herzegovina - Serbian (Cyrillic)", + Language.srCS: cstring"Serbia and Montenegro (Former) - Serbian (Cyrillic)", + Language.srME: cstring"Montenegro - Serbian (Cyrillic)", + Language.srRS: cstring"Serbia - Serbian (Cyrillic)", + Language.svFI: cstring"Finland - Swedish", + Language.svSE: cstring"Sweden - Swedish", + Language.swKE: cstring"Kenya - Kiswahili", + Language.syrSY: cstring"Syria - Syriac", + Language.taIN: cstring"India - Tamil", + Language.teIN: cstring"India - Telugu", + Language.tgTJ: cstring"Tajikistan - Tajik (Cyrillic)", + Language.thTH: cstring"Thailand - Thai", + Language.tkTM: cstring"Turkmenistan - Turkmen", + Language.tnZA: cstring"South Africa - Setswana", + Language.trTR: cstring"Turkey - Turkish", + Language.ttRU: cstring"Russia - Tatar", + Language.tzmDZ: cstring"Algeria - Tamazight (Latin)", + Language.ugCN: cstring"People's Republic of China - Uyghur", + Language.ukUA: cstring"Ukraine - Ukrainian", + Language.urPK: cstring"Islamic Republic of Pakistan - Urdu", + Language.uzUZ: cstring"Uzbekistan - Uzbek (Cyrillic)", + Language.viVN: cstring"Vietnam - Vietnamese", + Language.woSN: cstring"Senegal - Wolof", + Language.xhZA: cstring"South Africa - isiXhosa", + Language.yoNG: cstring"Nigeria - Yoruba", + Language.zhCN: cstring"People's Republic of China - Chinese (Simplified) Legacy", + Language.zhHK: cstring"Hong Kong S.A.R. - Chinese (Traditional) Legacy", + Language.zhMO: cstring"Macao S.A.R. - Chinese (Traditional) Legacy", + Language.zhSG: cstring"Singapore - Chinese (Simplified) Legacy", + Language.zhTW: cstring"Taiwan - Chinese (Traditional) Legacy", + Language.zuZA: cstring"South Africa - isiZulu"] + + languageToCode*: array[Language, cstring] = [ + Language.afZA: cstring"af-ZA", + Language.amET: cstring"am-ET", + Language.arAE: cstring"ar-AE", + Language.arBH: cstring"ar-BH", + Language.arDZ: cstring"ar-DZ", + Language.arEG: cstring"ar-EG", + Language.arIQ: cstring"ar-IQ", + Language.arJO: cstring"ar-JO", + Language.arKW: cstring"ar-KW", + Language.arLB: cstring"ar-LB", + Language.arLY: cstring"ar-LY", + Language.arMA: cstring"ar-MA", + Language.arOM: cstring"ar-OM", + Language.arQA: cstring"ar-QA", + Language.arSA: cstring"ar-SA", + Language.arSY: cstring"ar-SY", + Language.arTN: cstring"ar-TN", + Language.arYE: cstring"ar-YE", + Language.arnCL: cstring"arn-CL", + Language.asIN: cstring"as-IN", + Language.azAZ: cstring"az-AZ", + Language.baRU: cstring"ba-RU", + Language.beBY: cstring"be-BY", + Language.bgBG: cstring"bg-BG", + Language.bnBD: cstring"bn-BD", + Language.bnIN: cstring"bn-IN", + Language.boCN: cstring"bo-CN", + Language.brFR: cstring"br-FR", + Language.bsBA: cstring"bs-BA", + Language.caES: cstring"ca-ES", + Language.coFR: cstring"co-FR", + Language.csCZ: cstring"cs-CZ", + Language.cyGB: cstring"cy-GB", + Language.daDK: cstring"da-DK", + Language.deAT: cstring"de-AT", + Language.deCH: cstring"de-CH", + Language.deDE: cstring"de-DE", + Language.deLI: cstring"de-LI", + Language.deLU: cstring"de-LU", + Language.dsbDE: cstring"dsb-DE", + Language.dvMV: cstring"dv-MV", + Language.elGR: cstring"el-GR", + Language.en029: cstring"en-029", + Language.enAU: cstring"en-AU", + Language.enBZ: cstring"en-BZ", + Language.enCA: cstring"en-CA", + Language.enGB: cstring"en-GB", + Language.enIE: cstring"en-IE", + Language.enIN: cstring"en-IN", + Language.enJM: cstring"en-JM", + Language.enMY: cstring"en-MY", + Language.enNZ: cstring"en-NZ", + Language.enPH: cstring"en-PH", + Language.enSG: cstring"en-SG", + Language.enTT: cstring"en-TT", + Language.enUS: cstring"en-US", + Language.enZA: cstring"en-ZA", + Language.enZW: cstring"en-ZW", + Language.esAR: cstring"es-AR", + Language.esBO: cstring"es-BO", + Language.esCL: cstring"es-CL", + Language.esCO: cstring"es-CO", + Language.esCR: cstring"es-CR", + Language.esDO: cstring"es-DO", + Language.esEC: cstring"es-EC", + Language.esES: cstring"es-ES", + Language.esGT: cstring"es-GT", + Language.esHN: cstring"es-HN", + Language.esMX: cstring"es-MX", + Language.esNI: cstring"es-NI", + Language.esPA: cstring"es-PA", + Language.esPE: cstring"es-PE", + Language.esPR: cstring"es-PR", + Language.esPY: cstring"es-PY", + Language.esSV: cstring"es-SV", + Language.esUS: cstring"es-US", + Language.esUY: cstring"es-UY", + Language.esVE: cstring"es-VE", + Language.etEE: cstring"et-EE", + Language.euES: cstring"eu-ES", + Language.faIR: cstring"fa-IR", + Language.fiFI: cstring"fi-FI", + Language.filPH: cstring"fil-PH", + Language.foFO: cstring"fo-FO", + Language.frBE: cstring"fr-BE", + Language.frCA: cstring"fr-CA", + Language.frCH: cstring"fr-CH", + Language.frFR: cstring"fr-FR", + Language.frLU: cstring"fr-LU", + Language.frMC: cstring"fr-MC", + Language.fyNL: cstring"fy-NL", + Language.gaIE: cstring"ga-IE", + Language.gdGB: cstring"gd-GB", + Language.glES: cstring"gl-ES", + Language.gswFR: cstring"gsw-FR", + Language.guIN: cstring"gu-IN", + Language.haNG: cstring"ha-NG", + Language.heIL: cstring"he-IL", + Language.hiIN: cstring"hi-IN", + Language.hrBA: cstring"hr-BA", + Language.hrHR: cstring"hr-HR", + Language.hsbDE: cstring"hsb-DE", + Language.huHU: cstring"hu-HU", + Language.hyAM: cstring"hy-AM", + Language.idID: cstring"id-ID", + Language.igNG: cstring"ig-NG", + Language.iiCN: cstring"ii-CN", + Language.isIS: cstring"is-IS", + Language.itCH: cstring"it-CH", + Language.itIT: cstring"it-IT", + Language.iuCA: cstring"iu-CA", + Language.jaJP: cstring"ja-JP", + Language.kaGE: cstring"ka-GE", + Language.kkKZ: cstring"kk-KZ", + Language.klGL: cstring"kl-GL", + Language.kmKH: cstring"km-KH", + Language.knIN: cstring"kn-IN", + Language.koKR: cstring"ko-KR", + Language.kokIN: cstring"kok-IN", + Language.kyKG: cstring"ky-KG", + Language.lbLU: cstring"lb-LU", + Language.loLA: cstring"lo-LA", + Language.ltLT: cstring"lt-LT", + Language.lvLV: cstring"lv-LV", + Language.miNZ: cstring"mi-NZ", + Language.mkMK: cstring"mk-MK", + Language.mlIN: cstring"ml-IN", + Language.mnCN: cstring"mn-CN", + Language.mnMN: cstring"mn-MN", + Language.mohCA: cstring"moh-CA", + Language.mrIN: cstring"mr-IN", + Language.msBN: cstring"ms-BN", + Language.msMY: cstring"ms-MY", + Language.mtMT: cstring"mt-MT", + Language.nbNO: cstring"nb-NO", + Language.neNP: cstring"ne-NP", + Language.nlBE: cstring"nl-BE", + Language.nlNL: cstring"nl-NL", + Language.nnNO: cstring"nn-NO", + Language.nsoZA: cstring"nso-ZA", + Language.ocFR: cstring"oc-FR", + Language.orIN: cstring"or-IN", + Language.paIN: cstring"pa-IN", + Language.plPL: cstring"pl-PL", + Language.prsAF: cstring"prs-AF", + Language.psAF: cstring"ps-AF", + Language.ptBR: cstring"pt-BR", + Language.ptPT: cstring"pt-PT", + Language.qutGT: cstring"qut-GT", + Language.quzBO: cstring"quz-BO", + Language.quzEC: cstring"quz-EC", + Language.quzPE: cstring"quz-PE", + Language.rmCH: cstring"rm-CH", + Language.roRO: cstring"ro-RO", + Language.ruRU: cstring"ru-RU", + Language.rwRW: cstring"rw-RW", + Language.saIN: cstring"sa-IN", + Language.sahRU: cstring"sah-RU", + Language.seFI: cstring"se-FI", + Language.seNO: cstring"se-NO", + Language.seSE: cstring"se-SE", + Language.siLK: cstring"si-LK", + Language.skSK: cstring"sk-SK", + Language.slSI: cstring"sl-SI", + Language.smaNO: cstring"sma-NO", + Language.smaSE: cstring"sma-SE", + Language.smjNO: cstring"smj-NO", + Language.smjSE: cstring"smj-SE", + Language.smnFI: cstring"smn-FI", + Language.smsFI: cstring"sms-FI", + Language.sqAL: cstring"sq-AL", + Language.srBA: cstring"sr-BA", + Language.srCS: cstring"sr-CS", + Language.srME: cstring"sr-ME", + Language.srRS: cstring"sr-RS", + Language.svFI: cstring"sv-FI", + Language.svSE: cstring"sv-SE", + Language.swKE: cstring"sw-KE", + Language.syrSY: cstring"syr-SY", + Language.taIN: cstring"ta-IN", + Language.teIN: cstring"te-IN", + Language.tgTJ: cstring"tg-TJ", + Language.thTH: cstring"th-TH", + Language.tkTM: cstring"tk-TM", + Language.tnZA: cstring"tn-ZA", + Language.trTR: cstring"tr-TR", + Language.ttRU: cstring"tt-RU", + Language.tzmDZ: cstring"tzm-DZ", + Language.ugCN: cstring"ug-CN", + Language.ukUA: cstring"uk-UA", + Language.urPK: cstring"ur-PK", + Language.uzUZ: cstring"uz-UZ", + Language.viVN: cstring"vi-VN", + Language.woSN: cstring"wo-SN", + Language.xhZA: cstring"xh-ZA", + Language.yoNG: cstring"yo-NG", + Language.zhCN: cstring"zh-CN", + Language.zhHK: cstring"zh-HK", + Language.zhMO: cstring"zh-MO", + Language.zhSG: cstring"zh-SG", + Language.zhTW: cstring"zh-TW", + Language.zuZA: cstring"zu-ZA"] From bb846644b5a6b6f7148712fc67c5c33cbc062f44 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 8 Aug 2017 13:29:29 +0200 Subject: [PATCH 117/264] added support for a --define:karaxDebug switch --- src/karax.nim | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/karax.nim b/src/karax.nim index 9d74a9e..5cee5cf 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -463,24 +463,38 @@ proc applyComponents(kxi: KaraxInstance) = while i < kxi.components.len: let x = kxi.components[i].oldNode let newNode = kxi.components[i].newNode + when defined(karaxDebug): + echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil if x.changedImpl != nil and x.changedImpl(x, newNode): + when defined(karaxDebug): + echo "Component ", newNode.text, " did change" let current = kxi.components[i].current let parent = kxi.components[i].parent x.updatedImpl(x, newNode) let oldExpanded = x.expanded x.expanded = x.renderImpl(x) + when defined(karaxDebug): + echo "Component ", newNode.text, " re-rendered" x.renderedVersion = x.version if oldExpanded == nil: detach(x) kxi.addPatch(pkReplace, parent, current, x.expanded) + when defined(karaxDebug): + echo "Component ", newNode.text, ": old expansion didn't exist" else: let res = diff(x.expanded, oldExpanded, parent, current, kxi) if res == usenewNode: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" discard "diff created a patchset for us, so this is fine" elif res != different: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever" x.expanded = oldExpanded assert oldExpanded.dom != nil, "old expanded.dom is nil" else: + when defined(karaxDebug): + echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)" assert x.expanded.dom != nil, "expanded.dom is nil" inc i setLen(kxi.components, 0) From 7c08c6bb76498f05b512ef09b4129a75496ec620 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 16 Aug 2017 19:42:16 +0200 Subject: [PATCH 118/264] first version; wip --- experiments/nextgen.nim | 181 +++++++++++++++++++++++++++++++++++++ experiments/scrollapp.nims | 1 - src/karax.nim | 32 ++++++- src/vdom.nim | 3 +- 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 experiments/nextgen.nim delete mode 100644 experiments/scrollapp.nims diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..eb94658 --- /dev/null +++ b/experiments/nextgen.nim @@ -0,0 +1,181 @@ + +import kdom, jdict, jstrutils + +type + Message = enum + Unchanged + Changed + Mark + Inserted + Deleted + + State = object + stale: int + outdated: bool + phantom: bool + +type + ReactiveBase* = ref object of RootObj ## everything that is a "reactive" + ## value derives from that + sinks*: seq[proc(msg: Message, pos: int)] + Reactive*[T] = ref object of ReactiveBase + value*: T + + RString* = Reactive[cstring] +# RTime* = Reactive[Time] + RInt* = Reactive[int] + + RBool* = Reactive[bool] + + RSeq*[T] = ref object of ReactiveBase + s: seq[T] + L: RInt + +proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = + x.sinks.add sink + +proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = + for s in x.sinks: s(msg, pos) + +var toTrack: proc (msg: Message; pos: int) = nil + +proc now[T](x: Reactive[T]): T = + if toTrack != nil: + x.addSink toTrack + result = x.value + +proc `:=`[T](x: Reactive[T], f: proc(): T) = + var state: State + + toTrack = proc(msg: Message; pos: int) = + if not state.phantom: + case msg.kind: + of Changed, Unchanged: + state.outdated = state.outdated or (msg.kind == Changed) + dec state.stale + if state.stale == 0: + if state.outdated: + var t = f() + let thisChanged = if x.value != t: Changed else: Unchanged + x.value = t + x.broadcast(thisChanged, pos) + state.outdated = false + else: + x.broadcast(Unchanged, pos) + of Mark: + if state.stale == 0: + x.broadcast(msg, pos) + inc state.stale + of Inserted, Deleted: + dec state.stale + if state.stale == 0: + x.broadcast(msg, pos) + #state.phantom = true + + x.value = f() + toTrack = nil + +proc `<-`[T](x: Reactive[T], val: T) = + if x.value != val: + x.value = val + x.broadcast(Mark) + x.broadcast(Changed) + +proc changed*(x: ReactiveBase) = + x.broadcast(Mark) + x.broadcast(Changed) + +proc subscribe[T](x: Reactive[T], f: proc(x: T)) = + let reactor = proc (msg: Message, pos: int) = + case msg: + of Mark: discard + of Changed, Unchanged: f(x.value) + of Inserted, Deleted: discard + x.addSink reactor + +template lift1(op: untyped) = + proc op[T](a: Reactive[T]): (proc(): T) = + result = proc(): T = op(a.now) + +template lift2(op: untyped) = + proc op[T](a, b: Reactive[T]): (proc(): T) = + result = proc(): T = op(a.now, b.now) + +lift1 `not` +lift2 `&` + +proc newReactive[T](x: T): Reactive[T] = + result = Reactive[T](value: x) + +proc rstr(x: cstring): RString = + result = RString(value: x) + +proc newRSeq*[T](len: int): RSeq[T] = + result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0)) + +proc newRSeq*[T](data: seq[T]): RSeq[T] = + result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0)) + for i in 0..high(data): + result.s[i] = data[i] + +proc `[]=`[T](x: RSeq[T]; index: int; v: T) = + x.s[index] = v + +proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] + +proc insert*[T](x: RSeq[T]; y: T; position = 0) = + x.s.insert(y, position) + x.broadcast(Mark) + x.broadcast(Inserted, position) + +proc delete*[T](x: RSeq[T]; position = 0) = + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + +proc map[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = + let xl = x.L.value + let res = newRSeq[U](xl) + for i in 0.. Date: Thu, 17 Aug 2017 08:36:17 +0200 Subject: [PATCH 119/264] nextgen uses the karax DSL and DOM diffing --- experiments/nextgen.nim | 28 ++++++++++------------------ src/karax.nim | 1 + 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index eb94658..8df39a2 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,5 +1,5 @@ -import kdom, jdict, jstrutils +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type Message = enum @@ -155,27 +155,19 @@ type var gu = newReactive(User(firstname: "Some", lastname: "Body")) -proc renderUser(u: Reactive[User]): Node = - result = document.createElement("button") - result.appendChild document.createTextNode(u.now.firstname & " " & u.now.lastname) - result.addEventListener "click", proc (ev: Event) = - gu <- User(firstname: "Another", lastname: "Guy") +proc renderUser(u: Reactive[User]): VNode = + result = buildHtml(button): + text u.now.firstname & " " & u.now.lastname + proc onclick(ev: Event; n: VNode) = + gu <- User(firstname: "Another", lastname: "Guy") -proc replaceById(id: cstring; newTree: Node) = - let x = document.getElementById(id) - x.parentNode.replaceChild(newTree, x) - newTree.id = id - -template track(r: ReactiveBase; a, b: Node) = +template track(r: ReactiveBase; a, b: VNode) = r.addSink proc(m: Message; pos: int) = if m == Changed: - a.parentNode.replaceChild(b, a) + runDiff(kxi, a, b) -proc main(): Node = +proc main(): VNode = result = renderUser(gu) track gu, result, renderUser(gu) -proc init(ev: Event) = - replaceById("ROOT", main()) - -window.onload = init +setInitializer(main) diff --git a/src/karax.nim b/src/karax.nim index b0b7761..3f53405 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -187,6 +187,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) + newTree.id = id type EqResult = enum From 46b12594984d571a0a19e2e8c9f98fc75f238c62 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 17 Aug 2017 11:28:11 +0200 Subject: [PATCH 120/264] added .track macro --- experiments/nextgen.nim | 170 +++---------------------------------- src/reactive.nim | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 159 deletions(-) create mode 100644 src/reactive.nim diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 8df39a2..fb95a6b 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,173 +1,25 @@ -import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils - -type - Message = enum - Unchanged - Changed - Mark - Inserted - Deleted - - State = object - stale: int - outdated: bool - phantom: bool - -type - ReactiveBase* = ref object of RootObj ## everything that is a "reactive" - ## value derives from that - sinks*: seq[proc(msg: Message, pos: int)] - Reactive*[T] = ref object of ReactiveBase - value*: T - - RString* = Reactive[cstring] -# RTime* = Reactive[Time] - RInt* = Reactive[int] - - RBool* = Reactive[bool] - - RSeq*[T] = ref object of ReactiveBase - s: seq[T] - L: RInt - -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = - x.sinks.add sink - -proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = - for s in x.sinks: s(msg, pos) - -var toTrack: proc (msg: Message; pos: int) = nil - -proc now[T](x: Reactive[T]): T = - if toTrack != nil: - x.addSink toTrack - result = x.value - -proc `:=`[T](x: Reactive[T], f: proc(): T) = - var state: State - - toTrack = proc(msg: Message; pos: int) = - if not state.phantom: - case msg.kind: - of Changed, Unchanged: - state.outdated = state.outdated or (msg.kind == Changed) - dec state.stale - if state.stale == 0: - if state.outdated: - var t = f() - let thisChanged = if x.value != t: Changed else: Unchanged - x.value = t - x.broadcast(thisChanged, pos) - state.outdated = false - else: - x.broadcast(Unchanged, pos) - of Mark: - if state.stale == 0: - x.broadcast(msg, pos) - inc state.stale - of Inserted, Deleted: - dec state.stale - if state.stale == 0: - x.broadcast(msg, pos) - #state.phantom = true - - x.value = f() - toTrack = nil - -proc `<-`[T](x: Reactive[T], val: T) = - if x.value != val: - x.value = val - x.broadcast(Mark) - x.broadcast(Changed) - -proc changed*(x: ReactiveBase) = - x.broadcast(Mark) - x.broadcast(Changed) - -proc subscribe[T](x: Reactive[T], f: proc(x: T)) = - let reactor = proc (msg: Message, pos: int) = - case msg: - of Mark: discard - of Changed, Unchanged: f(x.value) - of Inserted, Deleted: discard - x.addSink reactor - -template lift1(op: untyped) = - proc op[T](a: Reactive[T]): (proc(): T) = - result = proc(): T = op(a.now) - -template lift2(op: untyped) = - proc op[T](a, b: Reactive[T]): (proc(): T) = - result = proc(): T = op(a.now, b.now) - -lift1 `not` -lift2 `&` - -proc newReactive[T](x: T): Reactive[T] = - result = Reactive[T](value: x) - -proc rstr(x: cstring): RString = - result = RString(value: x) - -proc newRSeq*[T](len: int): RSeq[T] = - result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0)) - -proc newRSeq*[T](data: seq[T]): RSeq[T] = - result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0)) - for i in 0..high(data): - result.s[i] = data[i] - -proc `[]=`[T](x: RSeq[T]; index: int; v: T) = - x.s[index] = v - -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] - -proc insert*[T](x: RSeq[T]; y: T; position = 0) = - x.s.insert(y, position) - x.broadcast(Mark) - x.broadcast(Inserted, position) - -proc delete*[T](x: RSeq[T]; position = 0) = - x.s.delete(position) - x.broadcast(Mark) - x.broadcast(Deleted, position) - -proc map[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = - let xl = x.L.value - let res = newRSeq[U](xl) - for i in 0.. Date: Sat, 19 Aug 2017 14:54:10 +0200 Subject: [PATCH 121/264] reactive sequence works now --- experiments/nextgen.nim | 67 ++++++++++++++++++++++++++++--------- src/karax.nim | 18 ++++++++++ src/reactive.nim | 73 +++++++++++++++++++++++++++++++++++------ src/vdom.nim | 4 +++ 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index fb95a6b..4f955aa 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -2,24 +2,61 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive type - User = ref object + User = ref object of ReactiveBase firstname, lastname: cstring -var gu = @[newReactive(User(firstname: "Some", lastname: "Body")), - newReactive(User(firstname: "Some", lastname: "One")), - newReactive(User(firstname: "Some", lastname: "Two"))] +var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), + (User(firstname: "Some", lastname: "One")), + (User(firstname: "Some", lastname: "Two"))]) var clicks = 0 -proc renderUser(u: Reactive[User]): VNode {.track.} = - result = buildHtml(button): - text u.now.firstname & " " & u.now.lastname - proc onclick(ev: Event; n: VNode) = - inc clicks - u <- User(firstname: "Another", lastname: &clicks) +discard """ + # Text gets a *reactive* string in the first place! + # Text can register and knows how to update itself! + proc toReact(): RString = + observe(u): + u.firstname & u.lastname -proc main(): VNode = - result = buildHtml(tdiv): - for i in 0..high(gu): - renderUser(gu[i]) + let t = text(u.firstname & " " & u.lastname) + observe(u, t.update(u.firstname & " " & u.lastname)) + t +""" -setInitializer(main) +proc renderUser(u: User): VNode = #{.track.} = + proc inner(u: User): VNode = + result = buildHtml(tdiv): + text u.firstname & " " & u.lastname + button: + text "(X)" + proc onclick(ev: Event; n: VNode) = + #u.firstname = "kfdj" + #u.lastname = &clicks + #notifyObservers(u) + gu.deleteElem(u) + + result = inner(u) + doTrack(u, result, inner(u)) + +template vmap(x: RSeq; elem, f: untyped): VNode = + let tmp = buildHtml(elem): + for i in 0..= parent.len: + kxi.addPatch(pkAppend, current, nil, kid) + parent.add(kid) + else: + let before = current.childNodes[position] + kxi.addPatch(pkInsertBefore, current, before, kid) + parent.insert(kid, position) + applyPatch(kxi) + proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom discard diff(newNode, oldNode, nil, olddom, kxi) diff --git a/src/reactive.nim b/src/reactive.nim index a1782f0..4fcc851 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -32,7 +32,7 @@ type s: seq[T] L: RInt -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = +proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = x.sinks.add sink proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = @@ -88,7 +88,7 @@ proc `<-`*[T](x: Reactive[T], val: T) = x.broadcast(Mark) x.broadcast(Changed) -proc changed*(x: ReactiveBase) = +proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) @@ -125,10 +125,17 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = for i in 0..high(data): result.s[i] = data[i] -proc `[]=`[T](x: RSeq[T]; index: int; v: T) = +proc `[]=`*[T](x: RSeq[T]; index: int; v: T) = x.s[index] = v -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] +proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index] +proc len*[T](x: RSeq[T]): int = x.s.len + +proc add*[T](x: RSeq[T]; y: T) = + let position = x.s.len + x.s.add(y) + x.broadcast(Mark) + x.broadcast(Inserted, position) proc insert*[T](x: RSeq[T]; y: T; position = 0) = x.s.insert(y, position) @@ -140,6 +147,16 @@ proc delete*[T](x: RSeq[T]; position = 0) = x.broadcast(Mark) x.broadcast(Deleted, position) +proc deleteElem*[T](x: RSeq[T]; y: T) = + var position = -1 + for i in 0..= 0: + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = let xl = x.L.value let res = newRSeq[U](xl) @@ -159,24 +176,60 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = - when r is Reactive: + when r is ReactiveBase: addSink r, key, proc(m: Message; pos: int) = if m == Changed: runDiff(kxi, a, b) +template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = + bind addSink, Message, RSeq, Changed, Deleted, Inserted + addSink r, proc(m: Message; pos: int) = + #when r is RSeq: + #echo "Message: ", m, " ", pos + if m == Changed: karax.runDiff(kxi, a, b) + +template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = + bind addSink, Message, RSeq, Changed, Deleted, Inserted + addSink r, proc(m: Message; pos: int) = + #when r is RSeq: + #echo "Message: ", m, " ", pos + case m + of Deleted: karax.runDel(kxi, a, pos) + of Inserted: + karax.runIns(kxi, a, b, pos) + else: discard + +template notifyImpl(r: untyped) = + when r is ReactiveBase: + notifyObservers(r) + +proc root(n: NimNode): NimNode = + result = n + while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] + +proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = + result = n + #if n.kind in {nnkAsgn, nnkFastAsgn}: + # + # n[0] + #else: + # recurse() + macro track*(procDef: untyped): untyped = let params = params(procDef) let key = lineInfo(procDef) var call = newCall(procDef.name) var trackings = newStmtList() - for b in procDef.body: trackings.add b + var paramList: seq[NimNode] = @[] for j in 1.. Date: Tue, 22 Aug 2017 18:04:49 +0200 Subject: [PATCH 122/264] wip; nothing works --- experiments/nextgen.nim | 109 +++++++++++++++++++++++++++++++--------- src/reactive.nim | 17 +++++-- src/vdom.nim | 11 +++- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 4f955aa..d311eb9 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,6 +1,25 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive +proc newTextInput*(text: RString; focus: RBool): VNode = + proc onFlip(ev: Event; target: VNode) = + focus <- not focus.value + + proc onKeyupEnter(ev: Event; target: VNode) = + text <- target.value + #text.notifyObservers() + + proc onkeyup(ev: Event; n: VNode) = + # keep displayValue up to date, but do not tell the client yet! + text.value = n.value + + result = buildHtml(input(`type`="text", + value=text.value, onblur=onFlip, onfocus=onFlip, + onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value)) + +var + errmsg = rstr("") + type User = ref object of ReactiveBase firstname, lastname: cstring @@ -8,6 +27,43 @@ type var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) +var selected = newReactive[User](nil) + +proc toUI*(): RString = + result = RString() + result.subscribe proc (v: cstring) = + if v.len > 0: + if selected >= 0: + # XXX what's happening here? + persons[selected].firstname = v + notifyObservers(persons[selected]) + errmsg <- "" + else: + errmsg <- "name must not be empty" + +var inp = toUI() + +proc adaptFocus(def = false): RBool = + result = RBool() + result.value = def + result.subscribe proc (hasFocus: bool) = + if not hasFocus: + inp.notifyObservers() + +var focus = adaptFocus() + +proc styler(): VStyle = + result = style( + (StyleAttr.position, cstring"relative"), + (StyleAttr.paddingLeft, cstring"10px"), + (StyleAttr.paddingRight, cstring"5px"), + (StyleAttr.height, cstring"30px"), + (StyleAttr.lineHeight, cstring"30px"), + (StyleAttr.border, cstring"solid 8px " & (if focus.value: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ) + var clicks = 0 discard """ @@ -22,20 +78,23 @@ discard """ t """ -proc renderUser(u: User): VNode = #{.track.} = - proc inner(u: User): VNode = - result = buildHtml(tdiv): - text u.firstname & " " & u.lastname - button: - text "(X)" - proc onclick(ev: Event; n: VNode) = - #u.firstname = "kfdj" - #u.lastname = &clicks - #notifyObservers(u) - gu.deleteElem(u) +template observe(s: cstring): RString = + let tmp = rstr(s) + u.subscribeSelf proc () = + tmp <- s + temp - result = inner(u) - doTrack(u, result, inner(u)) +proc renderUser(u: User; selected: RBool): VNode = + result = buildHtml(tdiv): + let displayName = observe(u.firstname & " " & u.lastname) + if selected == u: + newTextInput displayName, focus + else: + text displayName + button: + text "(x)" + proc onclick(ev: Event; n: VNode) = + gu.deleteElem(u) template vmap(x: RSeq; elem, f: untyped): VNode = let tmp = buildHtml(elem): @@ -44,18 +103,18 @@ template vmap(x: RSeq; elem, f: untyped): VNode = doTrackResize(x, tmp, f(x[pos])) tmp -proc main(gu: RSeq[User]): VNode = #{.track.} = - proc inner(gu: RSeq[User]): VNode = - result = buildHtml(tdiv): - vmap(gu, tdiv, renderUser) - tdiv: - button: - text "Add User" - proc onclick(ev: Event; n: VNode) = - inc clicks - gu.add User(firstname: "Added", lastname: &clicks) - result = inner(gu) - doTrack(gu, result, inner(gu)) +proc main(gu: RSeq[User]): VNode = + result = buildHtml(tdiv): + tdiv: + button: + text "Add User" + proc onclick(ev: Event; n: VNode) = + inc clicks + gu.add User(firstname: "Added", lastname: &clicks) + tdiv: + text errmsg + vmap(gu, tdiv, renderUser) + proc init(): VNode = main(gu) diff --git a/src/reactive.nim b/src/reactive.nim index 4fcc851..a0944a9 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -8,6 +8,7 @@ type Mark Inserted Deleted + Replaced State = object stale: int @@ -70,7 +71,7 @@ template wrapObserver(f: untyped) = of Mark: if state.stale == 0: x.broadcast(msg, pos) inc state.stale - of Inserted, Deleted: + of Inserted, Deleted, Replaced: dec state.stale if state.stale == 0: x.broadcast(msg, pos) @@ -92,12 +93,20 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribe[T](x: Reactive[T], f: proc(x: T)) = +proc subscribeVal*[T](x: Reactive[T], f: proc(x: T)) = let reactor = proc (msg: Message, pos: int) = case msg: of Mark: discard of Changed, Unchanged: f(x.value) - of Inserted, Deleted: discard + of Inserted, Deleted, Replaced: discard + x.addSink reactor + +proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) = + let reactor = proc (msg: Message, pos: int) = + case msg: + of Mark: discard + of Changed, Unchanged: f() + of Inserted, Deleted, Replaced: discard x.addSink reactor template lift1(op: untyped) = @@ -127,6 +136,8 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = proc `[]=`*[T](x: RSeq[T]; index: int; v: T) = x.s[index] = v + x.broadcast(Mark) + x.broadcast(Replaced, index) proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index] proc len*[T](x: RSeq[T]): int = x.s.len diff --git a/src/vdom.nim b/src/vdom.nim index bf84cb8..63f6669 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -239,8 +239,15 @@ proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; result = tree(kind, kids) for a in attrs: result.setAttr(a[0], a[1]) -proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) -proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) +proc rawtext*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) +proc rawtext*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) + +import reactive + +proc text*(s: RString): VNode = + result = VNode(kind: VNodeKind.text, text: s, index: -1) + s.subscribe proc(v: cstring) = + result.dom.value = v iterator items*(n: VNode): VNode = for i in 0.. Date: Wed, 23 Aug 2017 19:35:27 +0200 Subject: [PATCH 123/264] more reactive stuff works now --- experiments/nextgen.nim | 55 +++++++++++++++++++++++++-------- src/karax.nim | 9 ++++-- src/reactive.nim | 67 +++++++++++++++++------------------------ src/vdom.nim | 25 +++++++++------ 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index d311eb9..40acf04 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,7 +1,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive -proc newTextInput*(text: RString; focus: RBool): VNode = +proc newTextInput*(text: RString; focus: RBool): VNode {.track.} = proc onFlip(ev: Event; target: VNode) = focus <- not focus.value @@ -23,20 +23,22 @@ var type User = ref object of ReactiveBase firstname, lastname: cstring + selected: bool var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) -var selected = newReactive[User](nil) +var prevSelected: User = nil #newReactive[User](nil) proc toUI*(): RString = result = RString() result.subscribe proc (v: cstring) = if v.len > 0: - if selected >= 0: + let p = prevSelected #selected.value + if p != nil: # XXX what's happening here? - persons[selected].firstname = v - notifyObservers(persons[selected]) + p.firstname = v + notifyObservers(p) errmsg <- "" else: errmsg <- "name must not be empty" @@ -76,20 +78,31 @@ discard """ let t = text(u.firstname & " " & u.lastname) observe(u, t.update(u.firstname & " " & u.lastname)) t -""" template observe(s: cstring): RString = let tmp = rstr(s) u.subscribeSelf proc () = tmp <- s temp +""" -proc renderUser(u: User; selected: RBool): VNode = +proc renderUser(u: User): VNode {.track.} = result = buildHtml(tdiv): - let displayName = observe(u.firstname & " " & u.lastname) - if selected == u: - newTextInput displayName, focus + let displayName = u.firstname & " " & u.lastname + if u.selected: + # == selected.value: + !(inp <- displayName) + newTextInput inp, focus else: + button: + text "..." + proc onclick(ev: Event; n: VNode) = + if prevSelected != nil: + prevSelected.selected = false + notifyObservers(prevSelected) + u.selected = true + notifyObservers(u) + prevSelected = u text displayName button: text "(x)" @@ -103,6 +116,25 @@ template vmap(x: RSeq; elem, f: untyped): VNode = doTrackResize(x, tmp, f(x[pos])) tmp +template vmapIt(x: RSeq; elem, call: untyped): VNode = + var it {.inject}: type(x[0]) + let tmp = buildHtml(elem): + for i in 0.. 0: + # x.sinks[0] = sink + #else: + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = for s in x.sinks: s(msg, pos) @@ -93,7 +93,7 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribeVal*[T](x: Reactive[T], f: proc(x: T)) = +proc subscribe*[T](x: Reactive[T], f: proc(x: T)) = let reactor = proc (msg: Message, pos: int) = case msg: of Mark: discard @@ -186,11 +186,10 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = +template trackImpl(r: ReactiveBase; a, b: untyped) = when r is ReactiveBase: - addSink r, key, proc(m: Message; pos: int) = - if m == Changed: - runDiff(kxi, a, b) + addSink r, proc(m: Message; pos: int) = + if m == Changed: karax.runDiff(kxi, a, b) template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -207,40 +206,30 @@ template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = case m of Deleted: karax.runDel(kxi, a, pos) of Inserted: + let it {.used.} = r[pos] karax.runIns(kxi, a, b, pos) else: discard -template notifyImpl(r: untyped) = - when r is ReactiveBase: - notifyObservers(r) - -proc root(n: NimNode): NimNode = - result = n - while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] - -proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = - result = n - #if n.kind in {nnkAsgn, nnkFastAsgn}: - # - # n[0] - #else: - # recurse() - macro track*(procDef: untyped): untyped = let params = params(procDef) - let key = lineInfo(procDef) - var call = newCall(procDef.name) var trackings = newStmtList() - var paramList: seq[NimNode] = @[] + var inner = copyNimTree(procDef) + inner[0] = ident($procDef.name & "Inner") + var call = newCall(inner[0]) + trackings.add inner + trackings.add newAssignment(ident"result", call) + for j in 1.. Date: Mon, 28 Aug 2017 11:29:12 +0200 Subject: [PATCH 124/264] improvments to the new local diff algorithm --- src/karax.nim | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f68e3ff..2df1d9f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -182,7 +182,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = for i in 0 ..< n.len: if not same(n[i], e[i], nesting+1): return false else: - kout toTag[n.kind], e.nodename + echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) @@ -334,7 +334,8 @@ proc applyPatch(kxi: KaraxInstance) = if n.kind == VNodeKind.component: let x = VComponent(n) if x.onDetachImpl != nil: x.onDetachImpl(x) - n.dom = nil + # XXX for some reason this causes assertion errors otherwise: + if not kxi.surpressRedraws: n.dom = nil kxi.patchLen = 0 for i in 0.. Date: Mon, 28 Aug 2017 11:29:43 +0200 Subject: [PATCH 125/264] created a nice 'makeReactive' macro --- src/reactive.nim | 128 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/src/reactive.nim b/src/reactive.nim index 307d317..34c630e 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -1,4 +1,6 @@ +import jdict + type Message = enum Unchanged @@ -14,9 +16,11 @@ type phantom: bool type + SinkSeq = seq[proc(msg: Message, pos: int)] ReactiveBase* = ref object of RootObj ## everything that is a "reactive" ## value derives from that - sinks*: seq[proc(msg: Message, pos: int)] + sinks*: SinkSeq + dups: JDict[cstring, bool] id: int Reactive*[T] = ref object of ReactiveBase value*: T @@ -33,10 +37,16 @@ type var rid: int +proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = + if x.dups == nil: x.dups = newJDict[cstring, bool]() + if not x.dups.contains(key): + x.dups[key] = true + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid + proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = - #if x.sinks.len > 0: - # x.sinks[0] = sink - #else: x.sinks.add sink if x.id == 0: inc rid @@ -78,6 +88,29 @@ template wrapObserver(f: untyped) = #state.phantom = true helper +template glitchFree(f: untyped) = + var state: State + proc helper(msg: Message; pos: int) = + case msg.kind: + of Changed, Unchanged: + state.outdated = state.outdated or (msg.kind == Changed) + dec state.stale + if state.stale == 0: + if state.outdated: + x.value = f + x.broadcast(msg.kind, pos) + state.outdated = false + else: + x.broadcast(Unchanged, pos) + of Mark: + if state.stale == 0: x.broadcast(msg, pos) + inc state.stale + of Inserted, Deleted, Replaced: + dec state.stale + if state.stale == 0: + x.broadcast(msg, pos) + helper + proc `:=`[T](x: Reactive[T], f: proc(): T) = toTrack = wrapObserver(f()) x.value = f() @@ -186,17 +219,29 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; a, b: untyped) = +template protect(r: ReactiveBase; body: untyped) = + var tmp: seq[proc(msg: Message, pos: int)] + swap(r.sinks, tmp) + body + swap(r.sinks, tmp) + +template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = when r is ReactiveBase: - addSink r, proc(m: Message; pos: int) = - if m == Changed: karax.runDiff(kxi, a, b) + addSink r, key, proc(m: Message; pos: int) = + if m == Changed: + protect r: + echo "runDiff A" + karax.runDiff(kxi, a, b) template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted addSink r, proc(m: Message; pos: int) = #when r is RSeq: #echo "Message: ", m, " ", pos - if m == Changed: karax.runDiff(kxi, a, b) + if m == Changed: + protect r: + echo "runDiff B" + karax.runDiff(kxi, a, b) template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -226,10 +271,75 @@ macro track*(procDef: untyped): untyped = let param = x[i] call.add(param) + let key = lineInfo(procDef) for j in 1..