first example with virtual dom; virtual dom still buggy
This commit is contained in:
commit
4d72d5a999
8 changed files with 638 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
nimcache/
|
||||||
13
dyn.nim
Normal file
13
dyn.nim
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
|
||||||
|
type
|
||||||
|
EntryPoint = proc()
|
||||||
|
|
||||||
|
|
||||||
|
proc dynmain =
|
||||||
|
echo "dynamically loaded"
|
||||||
|
|
||||||
|
|
||||||
|
var plugins {.importc.}: seq[(string, EntryPoint)]
|
||||||
|
|
||||||
|
plugins.add(("dyn", EntryPoint dynmain))
|
||||||
18
example.html
Normal file
18
example.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Todo app</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body id="body">
|
||||||
|
|
||||||
|
<script type="text/javascript" src="nimcache/example.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
onload(window.location.pathname.split('/')[1])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="ROOT" />
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
52
example.nim
Normal file
52
example.nim
Normal file
|
|
@ -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()
|
||||||
12
jstrutils.nim
Normal file
12
jstrutils.nim
Normal file
|
|
@ -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.}
|
||||||
352
karax.nim
Normal file
352
karax.nim
Normal file
|
|
@ -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.}
|
||||||
108
karaxdsl.nim
Normal file
108
karaxdsl.nim
Normal file
|
|
@ -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..<n.len:
|
||||||
|
result.add getName(n[i])
|
||||||
|
of nnkStrLit..nnkTripleStrLit:
|
||||||
|
result = n.strVal
|
||||||
|
else:
|
||||||
|
#echo repr n
|
||||||
|
expectKind(n, nnkIdent)
|
||||||
|
|
||||||
|
var gid {.compileTime.}: int
|
||||||
|
|
||||||
|
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
|
||||||
|
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
|
||||||
|
|
||||||
|
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'.
|
||||||
|
# If expressions need to be distinguished from if statements. Since
|
||||||
|
# we know we start in a statement context, it's pretty simple to
|
||||||
|
# figure out expression contexts: In calls everything is an expression
|
||||||
|
# (except for the last child of the macros we consider here),
|
||||||
|
# lets, consts, types can be considered as expressions
|
||||||
|
# case is complex, calls are assumed to produce a value.
|
||||||
|
case n.kind
|
||||||
|
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
|
||||||
|
if tmpContext != nil:
|
||||||
|
result = newCall(bindSym"add", tmpContext, n)
|
||||||
|
else:
|
||||||
|
result = n
|
||||||
|
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
|
||||||
|
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
|
||||||
|
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
|
||||||
|
# recurse for the last son:
|
||||||
|
result = copyNimTree(n)
|
||||||
|
let L = n.len
|
||||||
|
assert n.len == result.len
|
||||||
|
if L > 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
|
||||||
82
vdom.nim
Normal file
82
vdom.nim
Normal file
|
|
@ -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..<n.kids.len: yield n.kids[i]
|
||||||
|
|
||||||
|
iterator attrs*(n: VNode): (cstring, cstring) =
|
||||||
|
for i in countup(0, n.attrs.len-2, 2):
|
||||||
|
yield (n.attrs[i], n.attrs[i+1])
|
||||||
|
|
||||||
|
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
|
||||||
|
n.events.add((event, handler))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue