better event handling for components and in the DSL

This commit is contained in:
Andreas Rumpf 2017-07-11 18:59:38 +02:00
commit 507094bdfd
4 changed files with 183 additions and 3 deletions

View file

@ -61,6 +61,7 @@ template setNativeValue(ev, val) = cast[Element](ev.target).value = val
template keyeventBody() = template keyeventBody() =
let v = nativeValue(ev) let v = nativeValue(ev)
n.value = v n.value = v
assert action != nil
action(ev, n) action(ev, n)
if n.value != v: if n.value != v:
setNativeValue(ev, n.value) setNativeValue(ev, n.value)

View file

@ -59,6 +59,19 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result.add n[0] result.add n[0]
for i in 1 ..< n.len: for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext) 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: of nnkVarSection, nnkLetSection, nnkConstSection:
result = n result = n
of nnkCallKinds: of nnkCallKinds:
@ -87,6 +100,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])
elif eqIdent(key, "setFocus"): elif eqIdent(key, "setFocus"):
result.add newCall(key, tmp, x[1], ident"kxi") result.add newCall(key, tmp, x[1], ident"kxi")
elif eqIdent(key, "events"):
result.add newCall(bindSym"mergeEvents", tmp, x[1])
else: else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1]) result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif ck != ComponentKind.Tag: elif ck != ComponentKind.Tag:
@ -100,7 +115,28 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
else: else:
result.add newCall(bindSym"add", tmpContext, tmp) result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext: elif tmpContext != nil and op notin StmtContext:
result = newCall(bindSym"add", tmpContext, n) var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2: elif op == "!" and n.len == 2:
result = n[1] result = n[1]
else: else:

View file

@ -105,6 +105,9 @@ buildLookupTables()
type type
EventHandler* = proc (ev: Event; target: VNode) {.closure.} EventHandler* = proc (ev: Event; target: VNode) {.closure.}
NativeEventHandler* = proc (ev: Event) {.closure.} NativeEventHandler* = proc (ev: Event) {.closure.}
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
VKey* = cstring VKey* = cstring
VNode* = ref object of RootObj VNode* = ref object of RootObj
@ -114,7 +117,7 @@ type
kids: seq[VNode] kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency: # even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[cstring] attrs: seq[cstring]
events*: seq[(EventKind, EventHandler, NativeEventHandler)] events*: EventHandlers
when false: when false:
hash*: Hash hash*: Hash
validHash*: bool validHash*: bool
@ -157,9 +160,21 @@ proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode =
proc dthunk*(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) VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args)
proc setEvent(v: VNode; kind: EventKind; handler: EventHandler) =
assert handler != nil
for i in 0..<v.events.len:
if v.events[i][0] == kind:
v.events[i][1] = handler
return
v.events.add((kind, handler, nil))
proc mergeEvents*(v: VNode; handlers: EventHandlers) =
## Overrides or adds the event handlers to `v`'s internal event handler list.
for h in handlers: v.setEvent(h[0], h[1])
proc defaultChangedImpl*(v, newInstance: VComponent): bool = proc defaultChangedImpl*(v, newInstance: VComponent): bool =
## The default implementation of 'changed'. ## The default implementation of 'changed'.
result = v.version != v.renderedVersion result = v.key != newInstance.key or v.version != v.renderedVersion
proc defaultUpdatedImpl*(v, newInstance: VComponent) = proc defaultUpdatedImpl*(v, newInstance: VComponent) =
discard discard

128
tests/components.nim Normal file
View file

@ -0,0 +1,128 @@
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils
type TextInput* = ref object of VComponent
value: cstring
isActive: bool
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
echo "flip! ", self.isActive, " id: ", self.debugId, " version ", self.version
markDirty(self)
result = buildHtml(tdiv(style=style)):
input(style=inputStyle, value=self.value, onblur=flip, onfocus=flip,
events=self.events)
proc update(current, next: VComponent) =
let current = TextInput(current)
let next = TextInput(next)
current.value = next.value
current.key = next.key
proc changed(current, next: VComponent): bool = true
proc newTextInput*(style: VStyle = VStyle(); key: cstring;
value = cstring""): TextInput =
result = newComponent(TextInput, render, changed=changed, updated=update)
result.style = style
result.value = value
result.key = key
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)
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"]
errmsg = cstring""
proc renderPerson(text: cstring, index: int): VNode =
result = buildHtml(tdiv):
newTextInput(VStyle(), &index, text):
proc onkeyuplater(ev: Event; n: VNode) =
let v = n.value
if v.len > 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