added .component macro to karax

This commit is contained in:
Andreas Rumpf 2017-04-13 21:07:04 +02:00
commit a799413f7f
6 changed files with 169 additions and 186 deletions

View file

@ -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

103
src/components.nim Normal file
View file

@ -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.. <params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(wname, nameStrLit, unpackCall) {.dirty.} =
proc wname*(args: seq[VNode]): VNode =
unpackCall
vcomponents[cstring(nameStrLit)] = wname
template dwrapper(wname, nameStrLit, unpackCall) {.dirty.} =
proc wname(args: seq[VNode]): Node =
unpackCall
dcomponents[cstring(nameStrLit)] = wname
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, newLit($name), unpackCall))
else:
result.add getAst(dwrapper(newname name, newLit($name), unpackCall))
allcomponents[$name] = isvirtual
when defined(debugKaraxDsl):
echo repr result
when isMainModule:
proc foo(x, y: int, b: bool; s: cstring): VNode {.component.} =
discard

View file

@ -520,6 +520,8 @@ proc item*(list: TouchList, i: int): Touch
{.pop.}
proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
var
window* {.importc, nodecl.}: Window
document* {.importc, nodecl.}: Document

View file

@ -1,6 +1,6 @@
# Simple lib to write JS UIs
import dom, vdom, jstrutils
import dom, vdom, jstrutils, components, jdict
export dom.Event, dom.cloneNode, dom
@ -68,8 +68,12 @@ proc isElementInViewport(el: Node; h: var int): bool =
proc vnodeToDom(n: VNode): Node =
if n.kind == VNodeKind.text:
result = document.createTextNode(n.text)
elif n.kind == VNodeKind.thunk:
return vnodeToDom(n.callThunk)
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
return vnodeToDom(x)
elif n.kind == VNodeKind.dthunk:
let x = callThunk(dcomponents[n.text], n)
return x
else:
result = document.createElement(toTag[n.kind])
for k in n:
@ -85,7 +89,7 @@ proc vnodeToDom(n: VNode): Node =
result.key = n.key
for k, v in attrs(n):
if v != nil:
result.setAttribute(k, v)
result.setAttr(k, v)
let myn = n
for e, h in items(n.events):
proc wrapper(): proc (ev: Event) =
@ -136,23 +140,26 @@ proc equals(a, b: VNode): bool =
if a.key != b.key: return false
if a.kind == VNodeKind.text:
if a.text != b.text: return false
elif a.kind == VNodeKind.thunk:
if a.thunk != b.thunk: return false
elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk:
if a.text != b.text: return false
if a.len != b.len: return false
for i in 0..<a.len:
if not equals(a[i], b[i]): return false
if not sameAttrs(a, b): return false
if a.class != b.class: return false
# XXX test event listeners here?
# --> 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()

View file

@ -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)

View file

@ -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..<a.kids.len:
if not eq(a.kids[i], b.kids[i]): return false
else:
if a.text != b.text: return false
if a.text != b.text: return false
if a.attrs.len != b.attrs.len: return false
for i in 0..<a.attrs.len:
if a.attrs[i] != b.attrs[i]: return false