commit 4d72d5a9992a13c69640b7f5e452d310113f310b Author: Andreas Rumpf Date: Thu Mar 30 23:52:47 2017 +0200 first example with virtual dom; virtual dom still buggy 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..