Merge branch 'master' into devel
This commit is contained in:
commit
a6a1d7e150
9 changed files with 620 additions and 21 deletions
120
experiments/nextgen.nim
Normal file
120
experiments/nextgen.nim
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
|
||||||
|
import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive
|
||||||
|
|
||||||
|
proc textInput*(text: RString; focus: RBool): VNode {.track.} =
|
||||||
|
proc onFlip(ev: Event; target: VNode) =
|
||||||
|
focus <- not focus.value
|
||||||
|
|
||||||
|
proc onKeyupEnter(ev: Event; target: VNode) =
|
||||||
|
text <- target.value
|
||||||
|
|
||||||
|
proc onkeyup(ev: Event; n: VNode) =
|
||||||
|
# keep displayValue up to date, but do not tell the client yet!
|
||||||
|
text.value = n.value
|
||||||
|
|
||||||
|
result = buildHtml(input(`type`="text",
|
||||||
|
value=text.value, onblur=onFlip, onfocus=onFlip,
|
||||||
|
onkeyupenter=onkeyupenter, onkeyup=onkeyup, setFocus=focus.value))
|
||||||
|
|
||||||
|
var
|
||||||
|
errmsg = rstr("")
|
||||||
|
|
||||||
|
makeReactive:
|
||||||
|
type
|
||||||
|
User = ref object
|
||||||
|
firstname, lastname: cstring
|
||||||
|
selected: bool
|
||||||
|
|
||||||
|
var gu = newRSeq(@[ (User(rawFirstname: "Some", rawLastname: "Body")),
|
||||||
|
(User(rawFirstname: "Some", rawLastname: "One")),
|
||||||
|
(User(rawFirstname: "Some", rawLastname: "Two"))])
|
||||||
|
var prevSelected: User = nil #newReactive[User](nil)
|
||||||
|
|
||||||
|
proc unselect() =
|
||||||
|
if prevSelected != nil:
|
||||||
|
prevSelected.selected = false
|
||||||
|
prevSelected = nil
|
||||||
|
|
||||||
|
proc select(u: User) =
|
||||||
|
unselect()
|
||||||
|
u.selected = true
|
||||||
|
prevSelected = u
|
||||||
|
|
||||||
|
proc toUI*(isFirstname: bool): RString =
|
||||||
|
result = RString()
|
||||||
|
result.subscribe proc (v: cstring) =
|
||||||
|
if v.len > 0:
|
||||||
|
let p = prevSelected #selected.value
|
||||||
|
if p != nil:
|
||||||
|
if isFirstName:
|
||||||
|
p.firstname = v
|
||||||
|
else:
|
||||||
|
p.lastname = v
|
||||||
|
unselect()
|
||||||
|
errmsg <- ""
|
||||||
|
else:
|
||||||
|
errmsg <- "name must not be empty"
|
||||||
|
|
||||||
|
var inpFirstname = toUI(true)
|
||||||
|
var inpLastname = toUI(false)
|
||||||
|
|
||||||
|
proc adaptFocus(def = false): RBool =
|
||||||
|
result = RBool()
|
||||||
|
result.value = def
|
||||||
|
when false:
|
||||||
|
result.subscribe proc (hasFocus: bool) =
|
||||||
|
if not hasFocus:
|
||||||
|
unselect()
|
||||||
|
|
||||||
|
var focusA = adaptFocus()
|
||||||
|
var focusB = adaptFocus()
|
||||||
|
|
||||||
|
proc styler(): VStyle =
|
||||||
|
result = 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 8px " & (if focusA.value: cstring"red" else: cstring"black")),
|
||||||
|
(StyleAttr.fontSize, cstring"12px"),
|
||||||
|
(StyleAttr.fontWeight, cstring"600")
|
||||||
|
)
|
||||||
|
|
||||||
|
var clicks = 0
|
||||||
|
|
||||||
|
proc renderUser(u: User): VNode {.track.} =
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
if u.selected:
|
||||||
|
inpFirstname <- u.firstname
|
||||||
|
tdiv:
|
||||||
|
textInput inpFirstname, focusA
|
||||||
|
inpLastname <- u.lastname
|
||||||
|
tdiv:
|
||||||
|
textInput inpLastname, focusB
|
||||||
|
else:
|
||||||
|
button:
|
||||||
|
text "..."
|
||||||
|
proc onclick(ev: Event; n: VNode) =
|
||||||
|
select(u)
|
||||||
|
text u.firstname & " " & u.lastname
|
||||||
|
button:
|
||||||
|
text "(x)"
|
||||||
|
proc onclick(ev: Event; n: VNode) =
|
||||||
|
gu.deleteElem(u)
|
||||||
|
|
||||||
|
proc main(gu: RSeq[User]): VNode =
|
||||||
|
result = buildHtml(tdiv):
|
||||||
|
tdiv:
|
||||||
|
button:
|
||||||
|
text "Add User"
|
||||||
|
proc onclick(ev: Event; n: VNode) =
|
||||||
|
inc clicks
|
||||||
|
gu.add User(rawFirstname: "Added", rawLastname: &clicks)
|
||||||
|
tdiv:
|
||||||
|
text errmsg
|
||||||
|
vmapIt(gu, tdiv, renderUser(it))
|
||||||
|
|
||||||
|
proc init(): VNode = main(gu)
|
||||||
|
|
||||||
|
setInitializer(init)
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
--path: "../src"
|
|
||||||
|
|
@ -23,7 +23,7 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
|
||||||
proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
|
proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
|
||||||
proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
|
proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
|
||||||
proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
|
proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
|
||||||
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)",}
|
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
|
||||||
|
|
||||||
var this {.importc: "this".}: ThisObj
|
var this {.importc: "this".}: ThisObj
|
||||||
let ajax = newRequest()
|
let ajax = newRequest()
|
||||||
|
|
|
||||||
103
src/karax.nim
103
src/karax.nim
|
|
@ -37,6 +37,7 @@ type
|
||||||
patchLenV: int
|
patchLenV: int
|
||||||
runCount: int
|
runCount: int
|
||||||
components: seq[ComponentPair]
|
components: seq[ComponentPair]
|
||||||
|
surpressRedraws*: bool
|
||||||
when defined(stats):
|
when defined(stats):
|
||||||
recursion: int
|
recursion: int
|
||||||
|
|
||||||
|
|
@ -74,7 +75,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||||
let action = action
|
let action = action
|
||||||
let n = n
|
let n = n
|
||||||
result = proc (ev: Event) =
|
result = proc (ev: Event) =
|
||||||
if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input:
|
if n.kind == VNodeKind.textarea or n.kind == VNodeKind.input or n.kind == VNodeKind.select:
|
||||||
keyeventBody()
|
keyeventBody()
|
||||||
else: action(ev, n)
|
else: action(ev, n)
|
||||||
|
|
||||||
|
|
@ -117,7 +118,7 @@ proc applyEvents(n: VNode; kxi: KaraxInstance) =
|
||||||
for i in 0..<len(n.events):
|
for i in 0..<len(n.events):
|
||||||
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
|
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
|
||||||
|
|
||||||
proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node =
|
proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node =
|
||||||
if n.kind == VNodeKind.text:
|
if n.kind == VNodeKind.text:
|
||||||
result = document.createTextNode(n.text)
|
result = document.createTextNode(n.text)
|
||||||
attach n
|
attach n
|
||||||
|
|
@ -134,6 +135,7 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node =
|
||||||
return result
|
return result
|
||||||
elif n.kind == VNodeKind.component:
|
elif n.kind == VNodeKind.component:
|
||||||
let x = VComponent(n)
|
let x = VComponent(n)
|
||||||
|
if x.realDomImpl != nil: return x.realDomImpl(x)
|
||||||
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
||||||
assert x.renderImpl != nil
|
assert x.renderImpl != nil
|
||||||
if x.expanded == nil:
|
if x.expanded == nil:
|
||||||
|
|
@ -175,16 +177,17 @@ proc same(n: VNode, e: Node; nesting = 0): bool =
|
||||||
result = true
|
result = true
|
||||||
if n.kind != VNodeKind.text:
|
if n.kind != VNodeKind.text:
|
||||||
if e.len != n.len:
|
if e.len != n.len:
|
||||||
kout e.len, n.len, toTag[n.kind], nesting
|
echo "expected ", e.len, " real ", n.len, toTag[n.kind], " nesting ", nesting
|
||||||
return false
|
return false
|
||||||
for i in 0 ..< n.len:
|
for i in 0 ..< n.len:
|
||||||
if not same(n[i], e[i], nesting+1): return false
|
if not same(n[i], e[i], nesting+1): return false
|
||||||
else:
|
else:
|
||||||
kout toTag[n.kind], e.nodename
|
echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
|
||||||
|
|
||||||
proc replaceById(id: cstring; newTree: Node) =
|
proc replaceById(id: cstring; newTree: Node) =
|
||||||
let x = document.getElementById(id)
|
let x = document.getElementById(id)
|
||||||
x.parentNode.replaceChild(newTree, x)
|
x.parentNode.replaceChild(newTree, x)
|
||||||
|
newTree.id = id
|
||||||
|
|
||||||
type
|
type
|
||||||
EqResult = enum
|
EqResult = enum
|
||||||
|
|
@ -331,7 +334,8 @@ proc applyPatch(kxi: KaraxInstance) =
|
||||||
if n.kind == VNodeKind.component:
|
if n.kind == VNodeKind.component:
|
||||||
let x = VComponent(n)
|
let x = VComponent(n)
|
||||||
if x.onDetachImpl != nil: x.onDetachImpl(x)
|
if x.onDetachImpl != nil: x.onDetachImpl(x)
|
||||||
n.dom = nil
|
# XXX for some reason this causes assertion errors otherwise:
|
||||||
|
if not kxi.surpressRedraws: n.dom = nil
|
||||||
kxi.patchLen = 0
|
kxi.patchLen = 0
|
||||||
for i in 0..<kxi.patchLenV:
|
for i in 0..<kxi.patchLenV:
|
||||||
let p = kxi.patchesV[i]
|
let p = kxi.patchesV[i]
|
||||||
|
|
@ -366,10 +370,11 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
|
||||||
|
|
||||||
if newNode.events.len != 0 or oldNode.events.len != 0:
|
if newNode.events.len != 0 or oldNode.events.len != 0:
|
||||||
mergeEvents(newNode, oldNode, kxi)
|
mergeEvents(newNode, oldNode, kxi)
|
||||||
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
|
when false:
|
||||||
if oldNode.text != newNode.text:
|
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
|
||||||
oldNode.text = newNode.text
|
if oldNode.text != newNode.text:
|
||||||
oldNode.dom.value = newNode.text
|
oldNode.text = newNode.text
|
||||||
|
oldNode.dom.value = newNode.text
|
||||||
|
|
||||||
let newLength = newNode.len
|
let newLength = newNode.len
|
||||||
let oldLength = oldNode.len
|
let oldLength = oldNode.len
|
||||||
|
|
@ -463,24 +468,42 @@ proc applyComponents(kxi: KaraxInstance) =
|
||||||
while i < kxi.components.len:
|
while i < kxi.components.len:
|
||||||
let x = kxi.components[i].oldNode
|
let x = kxi.components[i].oldNode
|
||||||
let newNode = kxi.components[i].newNode
|
let newNode = kxi.components[i].newNode
|
||||||
if x.changedImpl != nil and x.changedImpl(x, newNode):
|
when defined(karaxDebug):
|
||||||
|
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
|
||||||
|
if x.realDomImpl != nil:
|
||||||
|
let current = kxi.components[i].current
|
||||||
|
let parent = kxi.components[i].parent
|
||||||
|
kxi.addPatch(pkReplace, parent, current, x)
|
||||||
|
elif x.changedImpl != nil and x.changedImpl(x, newNode):
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, " did change"
|
||||||
let current = kxi.components[i].current
|
let current = kxi.components[i].current
|
||||||
let parent = kxi.components[i].parent
|
let parent = kxi.components[i].parent
|
||||||
x.updatedImpl(x, newNode)
|
x.updatedImpl(x, newNode)
|
||||||
let oldExpanded = x.expanded
|
let oldExpanded = x.expanded
|
||||||
x.expanded = x.renderImpl(x)
|
x.expanded = x.renderImpl(x)
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, " re-rendered"
|
||||||
x.renderedVersion = x.version
|
x.renderedVersion = x.version
|
||||||
if oldExpanded == nil:
|
if oldExpanded == nil:
|
||||||
detach(x)
|
detach(x)
|
||||||
kxi.addPatch(pkReplace, parent, current, x.expanded)
|
kxi.addPatch(pkReplace, parent, current, x.expanded)
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, ": old expansion didn't exist"
|
||||||
else:
|
else:
|
||||||
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
|
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
|
||||||
if res == usenewNode:
|
if res == usenewNode:
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
||||||
discard "diff created a patchset for us, so this is fine"
|
discard "diff created a patchset for us, so this is fine"
|
||||||
elif res != different:
|
elif res != different:
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
|
||||||
x.expanded = oldExpanded
|
x.expanded = oldExpanded
|
||||||
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
||||||
else:
|
else:
|
||||||
|
when defined(karaxDebug):
|
||||||
|
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
|
||||||
assert x.expanded.dom != nil, "expanded.dom is nil"
|
assert x.expanded.dom != nil, "expanded.dom is nil"
|
||||||
inc i
|
inc i
|
||||||
setLen(kxi.components, 0)
|
setLen(kxi.components, 0)
|
||||||
|
|
@ -493,6 +516,40 @@ when defined(stats):
|
||||||
result = m + 1
|
result = m + 1
|
||||||
inc total
|
inc total
|
||||||
|
|
||||||
|
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
|
||||||
|
detach(parent[position])
|
||||||
|
let current = parent.dom
|
||||||
|
kxi.addPatch(pkRemove, current, current.childNodes[position], nil)
|
||||||
|
parent.delete(position)
|
||||||
|
applyPatch(kxi)
|
||||||
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
||||||
|
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
|
||||||
|
let current = parent.dom
|
||||||
|
if position >= parent.len:
|
||||||
|
kxi.addPatch(pkAppend, current, nil, kid)
|
||||||
|
parent.add(kid)
|
||||||
|
else:
|
||||||
|
let before = current.childNodes[position]
|
||||||
|
kxi.addPatch(pkInsertBefore, current, before, kid)
|
||||||
|
parent.insert(kid, position)
|
||||||
|
applyPatch(kxi)
|
||||||
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
||||||
|
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
|
||||||
|
let olddom = oldNode.dom
|
||||||
|
doAssert olddom != nil
|
||||||
|
discard diff(newNode, oldNode, nil, olddom, kxi)
|
||||||
|
# this is a bit nasty: Since we cannot patch the 'parent' of
|
||||||
|
# the current VNode (because we don't store it at all!), we
|
||||||
|
# need to override the fields individually:
|
||||||
|
takeOverFields(newNode, oldNode)
|
||||||
|
applyComponents(kxi)
|
||||||
|
applyPatch(kxi)
|
||||||
|
if kxi.currentTree == oldNode:
|
||||||
|
kxi.currentTree = newNode
|
||||||
|
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
|
||||||
|
|
||||||
proc dodraw(kxi: KaraxInstance) =
|
proc dodraw(kxi: KaraxInstance) =
|
||||||
if kxi.renderer.isNil: return
|
if kxi.renderer.isNil: return
|
||||||
let newtree = kxi.renderer()
|
let newtree = kxi.renderer()
|
||||||
|
|
@ -562,6 +619,18 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
||||||
kxi = result
|
kxi = result
|
||||||
window.onload = init
|
window.onload = init
|
||||||
|
|
||||||
|
proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
||||||
|
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
|
||||||
|
## Setup Karax. Usually the return value can be ignored.
|
||||||
|
result = KaraxInstance(rootId: root, renderer: renderer,
|
||||||
|
postRenderCallback: clientPostRenderCallback,
|
||||||
|
patches: newSeq[Patch](60),
|
||||||
|
patchesV: newSeq[PatchV](30),
|
||||||
|
components: @[],
|
||||||
|
surpressRedraws: true)
|
||||||
|
kxi = result
|
||||||
|
window.onload = init
|
||||||
|
|
||||||
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
|
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
|
||||||
kxi: KaraxInstance = kxi) =
|
kxi: KaraxInstance = kxi) =
|
||||||
## Implements the foundation of Karax's event management.
|
## Implements the foundation of Karax's event management.
|
||||||
|
|
@ -571,7 +640,19 @@ proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
|
||||||
## a ``redraw``.
|
## a ``redraw``.
|
||||||
proc wrapper(ev: Event; n: VNode) =
|
proc wrapper(ev: Event; n: VNode) =
|
||||||
action(ev, n)
|
action(ev, n)
|
||||||
redraw(kxi)
|
if not kxi.surpressRedraws: redraw(kxi)
|
||||||
|
addEventListener(n, k, wrapper)
|
||||||
|
|
||||||
|
proc addEventHandler*(n: VNode; k: EventKind; action: proc();
|
||||||
|
kxi: KaraxInstance = kxi) =
|
||||||
|
## Implements the foundation of Karax's event management.
|
||||||
|
## Karax DSL transforms ``tag(onEvent = handler)`` to
|
||||||
|
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
|
||||||
|
## where ``wrapper`` calls the passed ``action`` and then triggers
|
||||||
|
## a ``redraw``.
|
||||||
|
proc wrapper(ev: Event; n: VNode) =
|
||||||
|
action()
|
||||||
|
if not kxi.surpressRedraws: redraw(kxi)
|
||||||
addEventListener(n, k, wrapper)
|
addEventListener(n, k, wrapper)
|
||||||
|
|
||||||
proc setOnHashChange*(action: proc (hashPart: cstring)) =
|
proc setOnHashChange*(action: proc (hashPart: cstring)) =
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
|
||||||
result = n
|
result = n
|
||||||
of nnkVarSection, nnkLetSection, nnkConstSection:
|
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||||
result = n
|
result = n
|
||||||
of nnkCallKinds:
|
of nnkCallKinds - {nnkInfix}:
|
||||||
let op = getName(n[0])
|
let op = getName(n[0])
|
||||||
let ck = isComponent(op)
|
let ck = isComponent(op)
|
||||||
if ck != ComponentKind.None:
|
if ck != ComponentKind.None:
|
||||||
|
|
|
||||||
|
|
@ -583,7 +583,7 @@ proc javaEnabled*(h: Navigator): bool
|
||||||
# ClassList "methods"
|
# ClassList "methods"
|
||||||
proc add*(c: ClassList, class: cstring)
|
proc add*(c: ClassList, class: cstring)
|
||||||
proc remove*(c: ClassList, class: cstring)
|
proc remove*(c: ClassList, class: cstring)
|
||||||
proc contains*(c: ClassList, class: cstring):bool
|
proc contains*(c: ClassList, class: cstring): bool
|
||||||
proc toggle*(c: ClassList, class: cstring)
|
proc toggle*(c: ClassList, class: cstring)
|
||||||
|
|
||||||
# Style "methods"
|
# Style "methods"
|
||||||
|
|
@ -644,9 +644,9 @@ type
|
||||||
proc getBoundingClientRect*(e: Node): BoundingRect {.
|
proc getBoundingClientRect*(e: Node): BoundingRect {.
|
||||||
importcpp: "getBoundingClientRect", nodecl.}
|
importcpp: "getBoundingClientRect", nodecl.}
|
||||||
proc clientHeight*(): int {.
|
proc clientHeight*(): int {.
|
||||||
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
|
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl.}
|
||||||
proc clientWidth*(): int {.
|
proc clientWidth*(): int {.
|
||||||
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
|
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl.}
|
||||||
|
|
||||||
proc inViewport*(el: Node): bool =
|
proc inViewport*(el: Node): bool =
|
||||||
let rect = el.getBoundingClientRect()
|
let rect = el.getBoundingClientRect()
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## This module contains wrappers for the HTML 5 local storage.
|
## This module contains wrappers for the HTML 5 local storage.
|
||||||
|
|
||||||
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem"}
|
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
|
||||||
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem"}
|
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
|
||||||
proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
|
proc hasItem*(key: cstring): bool {.importcpp: "(localStorage.getItem(#) !== null)".}
|
||||||
proc clear*() {.importc: "localStorage.clear"}
|
proc clear*() {.importc: "localStorage.clear".}
|
||||||
|
|
||||||
proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}
|
proc removeItem*(key: cstring) {.importc: "localStorage.removeItem".}
|
||||||
|
|
|
||||||
380
src/reactive.nim
Normal file
380
src/reactive.nim
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
|
||||||
|
import jdict
|
||||||
|
|
||||||
|
type
|
||||||
|
Message = enum
|
||||||
|
Unchanged
|
||||||
|
Changed
|
||||||
|
Mark
|
||||||
|
Inserted
|
||||||
|
Deleted
|
||||||
|
Replaced
|
||||||
|
|
||||||
|
State = object
|
||||||
|
stale: int
|
||||||
|
outdated: bool
|
||||||
|
phantom: bool
|
||||||
|
|
||||||
|
type
|
||||||
|
SinkSeq = seq[proc(msg: Message, pos: int)]
|
||||||
|
ReactiveBase* = ref object of RootObj ## everything that is a "reactive"
|
||||||
|
## value derives from that
|
||||||
|
sinks*: SinkSeq
|
||||||
|
dups: JDict[cstring, int]
|
||||||
|
id: int
|
||||||
|
Reactive*[T] = ref object of ReactiveBase
|
||||||
|
value*: T
|
||||||
|
|
||||||
|
RString* = Reactive[cstring]
|
||||||
|
# RTime* = Reactive[Time]
|
||||||
|
RInt* = Reactive[int]
|
||||||
|
|
||||||
|
RBool* = Reactive[bool]
|
||||||
|
|
||||||
|
RSeq*[T] = ref object of ReactiveBase
|
||||||
|
s: seq[T]
|
||||||
|
L: RInt
|
||||||
|
|
||||||
|
var rid: int
|
||||||
|
|
||||||
|
proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) =
|
||||||
|
if x.dups == nil: x.dups = newJDict[cstring, int]()
|
||||||
|
if not x.dups.contains(key):
|
||||||
|
x.dups[key] = x.sinks.len
|
||||||
|
x.sinks.add sink
|
||||||
|
if x.id == 0:
|
||||||
|
inc rid
|
||||||
|
x.id = rid
|
||||||
|
else:
|
||||||
|
# update existing entry:
|
||||||
|
x.sinks[x.dups[key]] = sink
|
||||||
|
|
||||||
|
proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) =
|
||||||
|
x.sinks.add sink
|
||||||
|
if x.id == 0:
|
||||||
|
inc rid
|
||||||
|
x.id = rid
|
||||||
|
|
||||||
|
var inhibited: int
|
||||||
|
|
||||||
|
proc broadcast(x: ReactiveBase, msg: Message; pos = 0) =
|
||||||
|
if inhibited == 0:
|
||||||
|
for s in x.sinks: s(msg, pos)
|
||||||
|
|
||||||
|
var toTrack: seq[(cstring, proc (msg: Message; pos: int))] = @[]
|
||||||
|
|
||||||
|
template withTrack(key, t, body) =
|
||||||
|
toTrack.add((key, t))
|
||||||
|
body
|
||||||
|
discard toTrack.pop()
|
||||||
|
|
||||||
|
proc trackDependency*(r: ReactiveBase) =
|
||||||
|
for t in toTrack:
|
||||||
|
r.addSink t[0], t[1]
|
||||||
|
|
||||||
|
template wrapObserver(f: untyped) =
|
||||||
|
var state: State
|
||||||
|
proc helper(msg: Message; pos: int) =
|
||||||
|
case msg.kind:
|
||||||
|
of Changed, Unchanged:
|
||||||
|
state.outdated = state.outdated or (msg.kind == Changed)
|
||||||
|
dec state.stale
|
||||||
|
if state.stale == 0:
|
||||||
|
if state.outdated:
|
||||||
|
var t = f()
|
||||||
|
let thisChanged = if x.value != t: Changed else: Unchanged
|
||||||
|
x.value = t
|
||||||
|
x.broadcast(thisChanged, pos)
|
||||||
|
state.outdated = false
|
||||||
|
else:
|
||||||
|
x.broadcast(Unchanged, pos)
|
||||||
|
of Mark:
|
||||||
|
if state.stale == 0: x.broadcast(msg, pos)
|
||||||
|
inc state.stale
|
||||||
|
of Inserted, Deleted, Replaced:
|
||||||
|
dec state.stale
|
||||||
|
if state.stale == 0:
|
||||||
|
x.broadcast(msg, pos)
|
||||||
|
#state.phantom = true
|
||||||
|
helper
|
||||||
|
|
||||||
|
template glitchFree(f: untyped) =
|
||||||
|
var state: State
|
||||||
|
proc helper(msg: Message; pos: int) =
|
||||||
|
case msg.kind:
|
||||||
|
of Changed, Unchanged:
|
||||||
|
state.outdated = state.outdated or (msg.kind == Changed)
|
||||||
|
dec state.stale
|
||||||
|
if state.stale == 0:
|
||||||
|
if state.outdated:
|
||||||
|
x.value = f
|
||||||
|
x.broadcast(msg.kind, pos)
|
||||||
|
state.outdated = false
|
||||||
|
else:
|
||||||
|
x.broadcast(Unchanged, pos)
|
||||||
|
of Mark:
|
||||||
|
if state.stale == 0: x.broadcast(msg, pos)
|
||||||
|
inc state.stale
|
||||||
|
of Inserted, Deleted, Replaced:
|
||||||
|
dec state.stale
|
||||||
|
if state.stale == 0:
|
||||||
|
x.broadcast(msg, pos)
|
||||||
|
helper
|
||||||
|
|
||||||
|
proc `<-`*[T](x: Reactive[T], val: T) =
|
||||||
|
#if x.value != val:
|
||||||
|
x.value = val
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Changed)
|
||||||
|
|
||||||
|
proc notifyObservers*(x: ReactiveBase) =
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Changed)
|
||||||
|
|
||||||
|
proc subscribe*[T](x: Reactive[T], f: proc(x: T)) =
|
||||||
|
let reactor = proc (msg: Message, pos: int) =
|
||||||
|
case msg:
|
||||||
|
of Mark: discard
|
||||||
|
of Changed, Unchanged: f(x.value)
|
||||||
|
of Inserted, Deleted, Replaced: discard
|
||||||
|
x.addSink reactor
|
||||||
|
|
||||||
|
proc subscribeSelf*[T: ReactiveBase](x: T, f: proc()) =
|
||||||
|
let reactor = proc (msg: Message, pos: int) =
|
||||||
|
case msg:
|
||||||
|
of Mark: discard
|
||||||
|
of Changed, Unchanged: f()
|
||||||
|
of Inserted, Deleted, Replaced: discard
|
||||||
|
x.addSink reactor
|
||||||
|
|
||||||
|
template lift1(op: untyped) =
|
||||||
|
proc op[T](a: Reactive[T]): (proc(): T) =
|
||||||
|
result = proc(): T = op(a.now)
|
||||||
|
|
||||||
|
template lift2(op: untyped) =
|
||||||
|
proc op[T](a, b: Reactive[T]): (proc(): T) =
|
||||||
|
result = proc(): T = op(a.now, b.now)
|
||||||
|
|
||||||
|
lift1 `not`
|
||||||
|
lift2 `&`
|
||||||
|
|
||||||
|
proc newReactive*[T](x: T): Reactive[T] =
|
||||||
|
result = Reactive[T](value: x)
|
||||||
|
|
||||||
|
proc rstr*(x: cstring): RString =
|
||||||
|
result = RString(value: x)
|
||||||
|
|
||||||
|
proc newRSeq*[T](len: int = 0): RSeq[T] =
|
||||||
|
result = RSeq[T](s: newSeq[T](len), L: newReactive[int](0))
|
||||||
|
|
||||||
|
proc newRSeq*[T](data: seq[T]): RSeq[T] =
|
||||||
|
result = RSeq[T](s: newSeq[T](data.len), L: newReactive[int](0))
|
||||||
|
for i in 0..high(data):
|
||||||
|
result.s[i] = data[i]
|
||||||
|
|
||||||
|
proc `[]=`*[T](x: RSeq[T]; index: int; v: T) =
|
||||||
|
x.s[index] = v
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Replaced, index)
|
||||||
|
|
||||||
|
proc `[]`*[T](x: RSeq[T]; index: int): T = x.s[index]
|
||||||
|
proc len*[T](x: RSeq[T]): int = x.s.len
|
||||||
|
|
||||||
|
proc add*[T](x: RSeq[T]; y: T) =
|
||||||
|
let position = x.s.len
|
||||||
|
x.s.add(y)
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Inserted, position)
|
||||||
|
|
||||||
|
proc insert*[T](x: RSeq[T]; y: T; position = 0) =
|
||||||
|
x.s.insert(y, position)
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Inserted, position)
|
||||||
|
|
||||||
|
proc delete*[T](x: RSeq[T]; position = 0) =
|
||||||
|
x.s.delete(position)
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Deleted, position)
|
||||||
|
|
||||||
|
proc deleteElem*[T](x: RSeq[T]; y: T) =
|
||||||
|
var position = -1
|
||||||
|
for i in 0..<x.len:
|
||||||
|
if x[i] == y:
|
||||||
|
position = i; break
|
||||||
|
if position >= 0:
|
||||||
|
x.s.delete(position)
|
||||||
|
x.broadcast(Mark)
|
||||||
|
x.broadcast(Deleted, position)
|
||||||
|
|
||||||
|
when false:
|
||||||
|
proc `:=`[T](x: Reactive[T], f: proc(): T) =
|
||||||
|
toTrack = wrapObserver(f())
|
||||||
|
x.value = f()
|
||||||
|
toTrack = nil
|
||||||
|
|
||||||
|
proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] =
|
||||||
|
let xl = x.L.value
|
||||||
|
let res = newRSeq[U](xl)
|
||||||
|
for i in 0..<xl:
|
||||||
|
res.s[i] := proc(): U = f(x[i].now)
|
||||||
|
|
||||||
|
let reactor = proc (msg: Message, pos: int) =
|
||||||
|
case msg:
|
||||||
|
of Mark, Changed, Unchanged: discard "nothing to do"
|
||||||
|
of Inserted:
|
||||||
|
res.insert(f(x[pos]), pos)
|
||||||
|
of Deleted:
|
||||||
|
res.delete(pos)
|
||||||
|
x.addSink reactor
|
||||||
|
result = res
|
||||||
|
|
||||||
|
import macros
|
||||||
|
|
||||||
|
template protect(body: untyped) =
|
||||||
|
#var tmp: seq[proc(msg: Message, pos: int)]
|
||||||
|
#swap(r.sinks, tmp)
|
||||||
|
inc inhibited
|
||||||
|
body
|
||||||
|
dec inhibited
|
||||||
|
#swap(r.sinks, tmp)
|
||||||
|
|
||||||
|
template trackStart(key: cstring; a, b: untyped) =
|
||||||
|
proc differ(m: Message; pos: int) =
|
||||||
|
if m == Changed:
|
||||||
|
protect:
|
||||||
|
karax.runDiff(kxi, a, b)
|
||||||
|
toTrack.add((key, differ))
|
||||||
|
|
||||||
|
template trackEnd() =
|
||||||
|
discard toTrack.pop()
|
||||||
|
|
||||||
|
template doTrack(r: ReactiveBase; a, b: untyped) {.dirty.} =
|
||||||
|
bind addSink, Message, RSeq, Changed, Deleted, Inserted
|
||||||
|
addSink r, proc(m: Message; pos: int) =
|
||||||
|
#when r is RSeq:
|
||||||
|
#echo "Message: ", m, " ", pos
|
||||||
|
if m == Changed:
|
||||||
|
protect r:
|
||||||
|
karax.runDiff(kxi, a, b)
|
||||||
|
|
||||||
|
template doTrackResize(r: ReactiveBase; a, b: untyped) {.dirty.} =
|
||||||
|
bind addSink, Message, RSeq, Changed, Deleted, Inserted
|
||||||
|
addSink r, proc(m: Message; pos: int) =
|
||||||
|
#when r is RSeq:
|
||||||
|
#echo "Message: ", m, " ", pos
|
||||||
|
case m
|
||||||
|
of Deleted: karax.runDel(kxi, a, pos)
|
||||||
|
of Inserted:
|
||||||
|
let it {.used.} = r[pos]
|
||||||
|
karax.runIns(kxi, a, b, pos)
|
||||||
|
else: discard
|
||||||
|
|
||||||
|
macro track*(procDef: untyped): untyped =
|
||||||
|
let params = params(procDef)
|
||||||
|
var trackings = newStmtList()
|
||||||
|
var inner = copyNimTree(procDef)
|
||||||
|
inner[0] = ident($procDef.name & "Inner")
|
||||||
|
var call = newCall(inner[0])
|
||||||
|
|
||||||
|
for j in 1..<params.len:
|
||||||
|
let x = params[j]
|
||||||
|
expectKind x, nnkIdentDefs
|
||||||
|
for i in 0..x.len-3:
|
||||||
|
let param = x[i]
|
||||||
|
call.add(param)
|
||||||
|
|
||||||
|
trackings.add inner
|
||||||
|
let key = newCall("cstring", newLit lineInfo(procDef))
|
||||||
|
trackings.add getAst(trackStart(key, ident"result", call))
|
||||||
|
trackings.add newAssignment(ident"result", call)
|
||||||
|
trackings.add getAst(trackEnd())
|
||||||
|
|
||||||
|
result = copyNimTree(procDef)
|
||||||
|
result.body = trackings
|
||||||
|
when defined(debugKaraxDsl):
|
||||||
|
echo repr result
|
||||||
|
|
||||||
|
proc generatePrivateAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
|
||||||
|
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
|
||||||
|
proc name(self: typ): fieldTyp =
|
||||||
|
trackDependency(self)
|
||||||
|
result = self.hidden
|
||||||
|
proc `name=`(self: typ; val: fieldTyp) =
|
||||||
|
self.hidden = val
|
||||||
|
notifyObservers(self)
|
||||||
|
result = getAst(helper(name, hidden, typ, fieldTyp))
|
||||||
|
|
||||||
|
proc generatePublicAccessors(name, hidden, typ, fieldTyp: NimNode): NimNode =
|
||||||
|
template helper(name, hidden, typ, fieldTyp) {.dirty.} =
|
||||||
|
proc name*(self: typ): fieldTyp =
|
||||||
|
trackDependency(self)
|
||||||
|
result = self.hidden
|
||||||
|
proc `name=`*(self: typ; val: fieldTyp) =
|
||||||
|
self.hidden = val
|
||||||
|
notifyObservers(self)
|
||||||
|
result = getAst(helper(name, hidden, typ, fieldTyp))
|
||||||
|
|
||||||
|
proc transform(n: NimNode; stmts, obj: NimNode): NimNode =
|
||||||
|
if n.kind == nnkIdentDefs and obj.kind == nnkIdent:
|
||||||
|
for i in 0..n.len-3:
|
||||||
|
let it = n[i]
|
||||||
|
let itB = if it.kind == nnkPostFix: it[1] else: it
|
||||||
|
let hidden = newIdentNode("raw" & $itB)
|
||||||
|
if it.kind == nnkPostFix:
|
||||||
|
stmts.add generatePublicAccessors(itB, hidden, obj, n[n.len-2])
|
||||||
|
else:
|
||||||
|
stmts.add generatePrivateAccessors(itB, hidden, obj, n[n.len-2])
|
||||||
|
n[i] = hidden
|
||||||
|
result = n
|
||||||
|
else:
|
||||||
|
var objB = if n.kind == nnkTypeDef: n[0] else: obj
|
||||||
|
result = copyNimNode(n)
|
||||||
|
for i in 0..<n.len:
|
||||||
|
if n.kind == nnkObjectTy and i == 1 and n[1].kind == nnkEmpty:
|
||||||
|
result.add newTree(nnkOfInherit, ident("ReactiveBase"))
|
||||||
|
else:
|
||||||
|
result.add transform(n[i], stmts, objB)
|
||||||
|
|
||||||
|
macro makeReactive*(n: untyped): untyped =
|
||||||
|
var a = newStmtList()
|
||||||
|
a.add newEmptyNode()
|
||||||
|
let t = transform(n, a, newEmptyNode())
|
||||||
|
a[0] = t
|
||||||
|
result = a
|
||||||
|
when defined(debugKaraxDsl):
|
||||||
|
echo repr result
|
||||||
|
|
||||||
|
import vdom
|
||||||
|
|
||||||
|
template vmap*(x: RSeq; elem, f: untyped): VNode =
|
||||||
|
let tmp = buildHtml(elem):
|
||||||
|
for i in 0..<len(x):
|
||||||
|
f(x[i])
|
||||||
|
doTrackResize(x, tmp, f(x[pos]))
|
||||||
|
tmp
|
||||||
|
|
||||||
|
template vmapIt*(x: RSeq; elem, call: untyped): VNode =
|
||||||
|
var it {.inject.}: type(x[0])
|
||||||
|
let tmp = buildHtml(elem):
|
||||||
|
for i in 0..<len(x):
|
||||||
|
it = x[i]
|
||||||
|
call
|
||||||
|
doTrackResize(x, tmp, call)
|
||||||
|
tmp
|
||||||
|
|
||||||
|
proc text*(s: RString): VNode =
|
||||||
|
result = text(s.value)
|
||||||
|
s.subscribe proc(v: cstring) =
|
||||||
|
if result.dom != nil: result.dom.nodeValue = v
|
||||||
|
|
||||||
|
when isMainModule:
|
||||||
|
makeReactive:
|
||||||
|
type
|
||||||
|
Foo = ref object of ReactiveBase
|
||||||
|
x, y: int
|
||||||
|
Bar = ref object of ReactiveBase
|
||||||
|
z: string
|
||||||
|
exported*: int
|
||||||
|
Another = ref object
|
||||||
|
a, b: cstring
|
||||||
|
c: bool
|
||||||
21
src/vdom.nim
21
src/vdom.nim
|
|
@ -133,6 +133,7 @@ type
|
||||||
updatedImpl*: proc(self, newInstance: VComponent)
|
updatedImpl*: proc(self, newInstance: VComponent)
|
||||||
onAttachImpl*: proc(self: VComponent)
|
onAttachImpl*: proc(self: VComponent)
|
||||||
onDetachImpl*: proc(self: VComponent)
|
onDetachImpl*: proc(self: VComponent)
|
||||||
|
realDomImpl*: proc(self: VComponent): kdom.Node
|
||||||
version*: int ## Update this to trigger a redraw by karax. Usually you
|
version*: int ## Update this to trigger a redraw by karax. Usually you
|
||||||
## should call 'markDirty' instead which is an alias for
|
## should call 'markDirty' instead which is an alias for
|
||||||
## 'inc version'.
|
## 'inc version'.
|
||||||
|
|
@ -185,7 +186,7 @@ proc getDebugId(): int =
|
||||||
gid
|
gid
|
||||||
|
|
||||||
template newComponent*[T](t: typeDesc[T];
|
template newComponent*[T](t: typeDesc[T];
|
||||||
render: (proc(self: VComponent): VNode) not nil,
|
render: (proc(self: VComponent): VNode) = nil,
|
||||||
onAttach: proc(self: VComponent) = nil,
|
onAttach: proc(self: VComponent) = nil,
|
||||||
onDetach: proc(self: VComponent) = nil,
|
onDetach: proc(self: VComponent) = nil,
|
||||||
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
|
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
|
||||||
|
|
@ -219,10 +220,28 @@ proc getAttr*(n: VNode; key: cstring): cstring =
|
||||||
proc takeOverAttr*(newNode, oldNode: VNode) =
|
proc takeOverAttr*(newNode, oldNode: VNode) =
|
||||||
shallowCopy oldNode.attrs, newNode.attrs
|
shallowCopy oldNode.attrs, newNode.attrs
|
||||||
|
|
||||||
|
proc takeOverFields*(newNode, oldNode: VNode) =
|
||||||
|
template take(field) =
|
||||||
|
shallowCopy oldNode.field, newNode.field
|
||||||
|
take kind
|
||||||
|
take index
|
||||||
|
take id
|
||||||
|
take class
|
||||||
|
take text
|
||||||
|
take kids
|
||||||
|
take attrs
|
||||||
|
take events
|
||||||
|
take style
|
||||||
|
take dom
|
||||||
|
|
||||||
proc len*(x: VNode): int = x.kids.len
|
proc len*(x: VNode): int = x.kids.len
|
||||||
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
||||||
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
||||||
proc add*(parent, kid: VNode) = parent.kids.add kid
|
proc add*(parent, kid: VNode) = parent.kids.add kid
|
||||||
|
proc delete*(parent: VNode; position: int) =
|
||||||
|
parent.kids.delete(position)
|
||||||
|
proc insert*(parent, kid: VNode; position: int) =
|
||||||
|
parent.kids.insert(kid, position)
|
||||||
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1)
|
proc newVNode*(kind: VNodeKind): VNode = VNode(kind: kind, index: -1)
|
||||||
|
|
||||||
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue