From 4d72d5a9992a13c69640b7f5e452d310113f310b Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 30 Mar 2017 23:52:47 +0200 Subject: [PATCH] first example with virtual dom; virtual dom still buggy --- .gitignore | 1 + dyn.nim | 13 ++ example.html | 18 +++ example.nim | 52 ++++++++ jstrutils.nim | 12 ++ karax.nim | 352 ++++++++++++++++++++++++++++++++++++++++++++++++++ karaxdsl.nim | 108 ++++++++++++++++ vdom.nim | 82 ++++++++++++ 8 files changed, 638 insertions(+) create mode 100644 .gitignore create mode 100644 dyn.nim create mode 100644 example.html create mode 100644 example.nim create mode 100644 jstrutils.nim create mode 100644 karax.nim create mode 100644 karaxdsl.nim create mode 100644 vdom.nim diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67d9b34 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nimcache/ diff --git a/dyn.nim b/dyn.nim new file mode 100644 index 0000000..9953d15 --- /dev/null +++ b/dyn.nim @@ -0,0 +1,13 @@ + + +type + EntryPoint = proc() + + +proc dynmain = + echo "dynamically loaded" + + +var plugins {.importc.}: seq[(string, EntryPoint)] + +plugins.add(("dyn", EntryPoint dynmain)) diff --git a/example.html b/example.html new file mode 100644 index 0000000..2e90b4e --- /dev/null +++ b/example.html @@ -0,0 +1,18 @@ + + + + + Todo app + + + + + + + +
+ + + diff --git a/example.nim b/example.nim new file mode 100644 index 0000000..d281cec --- /dev/null +++ b/example.nim @@ -0,0 +1,52 @@ + +import vdom, karax, karaxdsl + +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]() + +var entries: seq[cstring] + +proc onTodoEnter(val: cstring) = + entries.add val + +proc onclickHandler(ev: Event; n: VNode) = + let id = suffixAsInt(n.id, "remove:") + entries.delete(id) + +proc createEntry(i: int; d: cstring): VNode = + result = buildHtml(tr) do: + td: + text d + td: + span(id="remove:" & $i, onclick=onclickHandler): + text "[remove]" + +proc createDom(): VNode = + result = buildHtml(tdiv) do: + tdiv(id = "sheader"): + #text "plugin" + #realtimeInput("by-name", "", onInput) + #br() + + text "todo" + enterInput("todo-input", "", onTodoEnter) + table(class = "wl"): + for i, d in entries: + createEntry(i, d) + +setRenderer createDom + +proc onload(session: cstring) {.exportc.} = + for i in 0..10_000: + entries.add("Entry " & $i) + redraw() diff --git a/jstrutils.nim b/jstrutils.nim new file mode 100644 index 0000000..1c0b7a0 --- /dev/null +++ b/jstrutils.nim @@ -0,0 +1,12 @@ +# cstring handling utilities for performance + +proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.} + +proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.} +proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.} +proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} +proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} + +#proc len*(s: cstring): int {.importcpp: "#.length", nodecl.} +proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.} +proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.} diff --git a/karax.nim b/karax.nim new file mode 100644 index 0000000..03acd92 --- /dev/null +++ b/karax.nim @@ -0,0 +1,352 @@ +# Simple lib to write JS UIs + +import dom, vdom, jstrutils + +export dom.Element, 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: Element): cstring {.importcpp: "#.value", nodecl.} +proc `value=`*(e: Element; v: cstring) {.importcpp: "#.value = #", nodecl.} + +proc getElementsByClass*(e: Element; name: cstring): seq[Element] {. + importcpp: "#.getElementsByClassName(#)", nodecl.} + +type + Timeout* = ref object + +var document* {.importc.}: Document + +proc vnodeToDom(n: VNode): Element = + if n.kind == VNodeKind.text: + result = cast[Element](document.createTextNode(n.text)) + else: + result = document.createElement(toTag[n.kind]) + for k in n: + appendChild(result, vnodeToDom(k)) + # text is mapped to 'value': + if n.text != nil: + result.value = n.text + if n.id != nil: + result.id = n.id + if n.class != nil: + result.class = n.class + for k, v in attrs(n): result.setAttribute(k, v) + let myn = n + for e, h in items(n.events): + proc wrapper(): proc (ev: Event) = + result = proc (ev: Event) = + assert myn != nil + h(ev, myn) + result.addEventListener(toEventName[e], wrapper()) + +proc same(n: VNode, e: Element): bool = + if toTag[n.kind] == e.nodename: + result = true + if n.kind != VNodeKind.text: + if e.childNodes.len != n.len: return false + for i in 0 ..< n.len: + if not same(n[i], cast[Element](e.childNodes[i])): return false + +var + dorender: proc (): VNode {.closure.} + drawTimeout: Timeout + 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 getElementsByClassName*(cls: cstring): seq[Element] {.importc: +# "document.getElementsByClassName", nodecl.} + +proc textContent(e: Element): 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 = + if a.kind != b.kind: return false + if a.id != b.id: return false + if a.kind == VNodeKind.text: + if a.text != b.text: return false + #elif a.childNodes.len != b.childNodes.len: + # return false + # XXX test event listeners here? + # --> maybe give nodes a hash? + when false: + # this needs to be done differently in a virtual DOM: + if a.class != b.class: + # style differences are updated in place and we pretend + # it's still the same node + a.class = b.class + return true + +proc updateElement(parent, current: Node, newNode, oldNode: VNode) = + if not equals(newNode, oldNode): + let n = vnodeToDom(newNode) + if parent == nil: + replaceById("ROOT", n) + else: + parent.replaceChild(n, current) + elif newNode.kind != VNodeKind.text: + let newLength = newNode.len + let oldLength = oldNode.len + assert oldNode.kind == newNode.kind + when false: + if current.nodeName != toTag[oldNode.kind]: + kout current.nodeName + kout toTag[oldNode.kind] + assert false + for i in 0..min(newLength, oldLength)-1: + updateElement(current, current.childNodes[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): + current.removeChild(current.lastChild) + +proc dodraw() = + let newtree = dorender() + newtree.id = "ROOT" + if currentTree == nil: + currentTree = newtree + let asdom = vnodeToDom currentTree + replaceById("ROOT", asdom) + else: + let olddom = document.getElementById("ROOT") + updateElement(nil, olddom, newtree, currentTree) + +proc redraw*() = + # we buffer redraw requests: + if drawTimeout != nil: + clearTimeout(drawTimeout) + drawTimeout = setTimeout(dodraw, 30) + +#proc prepend*(parent, kid: Element) = +# parent.insertBefore(kid, parent.firstChild) +# prependChild(parent, kid) + +proc len(x: Element): int {.importcpp: "#.childNodes.length".} +proc `[]`(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".} + +proc isInt*(s: cstring): bool {.asmNoStackFrame.} = + asm """ + return s.match(/^[0-9]+$/); + """ + +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", "#") + addEventListener(result, EventKind.onclick, action) + +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: Element): int {.importcpp: "#.scrollTop", nodecl.} +proc offsetHeight*(e: Element): int {.importcpp: "#.offsetHeight", nodecl.} +proc offsetTop*(e: Element): int {.importcpp: "#.offsetTop", nodecl.} + +template onImpl(s) {.dirty.} = + proc wrapper(ev: Event; n: VNode) = + action(ev, n) + redraw() + addEventListener(e, s, wrapper) + +proc setOnclick*(e: VNode; action: EventHandler) = + onImpl EventKind.onclick + +proc setOnfocuslost*(e: VNode; action: EventHandler) = + onImpl EventKind.onblur + +proc setOnchanged*(e: VNode; action: EventHandler) = + onImpl EventKind.onchange + +proc setOnscroll*(e: VNode; action: EventHandler) = + onImpl EventKind.onscroll + +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: Element): 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: Element; key: cstring): cstring {. + importcpp: "#.getAttribute(#)", nodecl.} + +proc realtimeInput*(id, val: cstring; changed: proc(value: cstring)): VNode = + #let oldElem = getElementById(id) + #if oldElem != nil: return oldElem + #let newVal = if oldElem.isNil: val else: $oldElem.value + var timer: Timeout + proc wrapper() = + changed(document.getElementById(id).value) + redraw() + proc onkeyup(ev: Event; n: VNode) = + if timer != nil: clearTimeout(timer) + timer = setTimeout(wrapper, 400) + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.id = id + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + +proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): 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: + onenter(document.getElementById(id).value) + redraw() + + result = tree(VNodeKind.input, [(cstring"type", cstring"text")]) + result.id = id + result.value = val + result.addEventListener(EventKind.onkeyup, onkeyup) + + +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) = + ## Installs an error handler that transforms native JS unhandled + ## exceptions into Nim based stack traces. If `useAlert` is false, + ## the error message it 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; + };""".} + +{.pop.} diff --git a/karaxdsl.nim b/karaxdsl.nim new file mode 100644 index 0000000..4318fe1 --- /dev/null +++ b/karaxdsl.nim @@ -0,0 +1,108 @@ + +import macros, karax, vdom +from strutils import startsWith + +const + SupportedTags* = ["tdiv", "div", + "table", "tr", "td", "th", "thead", "tbody", + "link", "span", "label", "br" + ] + +proc getName(n: NimNode): string = + case n.kind + of nnkIdent: + result = $n.ident + of nnkAccQuoted: + result = "" + for i in 0.. 0: + result[L-1] = tcall2(result[L-1], tmpContext) + of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt, + nnkVarSection, nnkLetSection, nnkConstSection, + nnkTryStmt, nnkFinally: + # recurse for every child: + result = copyNimNode(n) + for x in n: + result.add tcall2(x, tmpContext) + of nnkCallKinds: + let op = getName(n[0]) + let idx = find(SupportedTags, op) + if idx >= 0: + let tag = SupportedTags[idx] + inc gid + let tmp = genSym(nskLet, "tmp" & $gid) + result = newTree( + if tmpContext == nil: nnkStmtListExpr else: nnkStmtList, + newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0])))) + for i in 1 ..< n.len: + # named parameters are transformed into attributes or events: + let x = n[i] + if x.kind == nnkExprEqExpr: + let key = getName x[0] + if key.startsWith("on"): + result.add newCall(!("set" & key), tmp, x[1]) + elif key == "id" or key == "class": + result.add newDotAsgn(tmp, key, x[1]) + else: + result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) + else: + result.add tcall2(x, tmp) + if tag == "link": + result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#") + if tmpContext == nil: + result.add tmp + else: + result.add newCall(bindSym"add", tmpContext, tmp) + elif tmpContext != nil and op != "kout": + result = newCall(bindSym"add", tmpContext, n) + else: + result = n + else: + result = n + +macro buildHtml*(tag, children: untyped): VNode = + expectKind children, nnkDo + let call = newCall(tag, body(children)) + result = tcall2(call, nil) + when defined(debugKaraxDsl): + echo repr result + +macro buildHtml*(children: untyped): VNode = + expectKind children, nnkDo + result = tcall2(body(children), nil) + when defined(debugKaraxDsl): + echo repr result diff --git a/vdom.nim b/vdom.nim new file mode 100644 index 0000000..6ca43e2 --- /dev/null +++ b/vdom.nim @@ -0,0 +1,82 @@ +# Virtual DOM implementation + +from dom import Event + +type + VNodeKind* {.pure.} = enum + text, + anchor, + tdiv, + table, tr, td, th, thead, tbody, + link, span, label, br, select, option, + fieldset, input + +const + toTag*: array[VNodeKind, cstring] = [ + cstring"#text", "A", "DIV", "TABLE", "TR", "TD", "TH", "THEAD", "TBODY", "LINK", + "SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT" + ] + +type + EventKind* {.pure.} = enum + onclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll + +const + toEventName*: array[EventKind, cstring] = [ + cstring"click", "keyup", "keydown", "keypressed", "blur", "change", "scroll" + ] + +type + EventHandler* = proc (ev: Event; target: VNode) {.closure.} + VNode* = ref object + kind*: VNodeKind + id*, class*, text*: cstring + kids: seq[VNode] + # even index: key, odd index: value; done this way for memory efficiency: + attrs: seq[cstring] + events*: seq[(EventKind, EventHandler)] + +proc value*(n: VNode): cstring = n.text +proc `value=`*(n: VNode; v: cstring) = n.text = v + +proc setAttr*(n: VNode; key: cstring; val: cstring = "") = + if n.attrs.isNil: + n.attrs = @[key, val] + else: + for i in countup(0, n.attrs.len-2, 2): + if n.attrs[i] == key: + n.attrs[i+1] = val + return + n.attrs.add key + n.attrs.add val + +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 len*(x: VNode): int = x.kids.len +proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] +proc add*(parent, kid: VNode) = parent.kids.add kid +proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind) + +proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode = + result = newVNode(kind) + for k in kids: result.add k + +proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; + kids: varargs[VNode]): VNode = + 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)) +proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s) + +iterator items*(n: VNode): VNode = + for i in 0..