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 send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
|
||||
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
|
||||
let ajax = newRequest()
|
||||
|
|
|
|||
103
src/karax.nim
103
src/karax.nim
|
|
@ -37,6 +37,7 @@ type
|
|||
patchLenV: int
|
||||
runCount: int
|
||||
components: seq[ComponentPair]
|
||||
surpressRedraws*: bool
|
||||
when defined(stats):
|
||||
recursion: int
|
||||
|
||||
|
|
@ -74,7 +75,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
|||
let action = action
|
||||
let n = n
|
||||
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()
|
||||
else: action(ev, n)
|
||||
|
||||
|
|
@ -117,7 +118,7 @@ proc applyEvents(n: VNode; kxi: KaraxInstance) =
|
|||
for i in 0..<len(n.events):
|
||||
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:
|
||||
result = document.createTextNode(n.text)
|
||||
attach n
|
||||
|
|
@ -134,6 +135,7 @@ proc vnodeToDom(n: VNode; kxi: KaraxInstance): Node =
|
|||
return result
|
||||
elif n.kind == VNodeKind.component:
|
||||
let x = VComponent(n)
|
||||
if x.realDomImpl != nil: return x.realDomImpl(x)
|
||||
if x.onAttachImpl != nil: x.onAttachImpl(x)
|
||||
assert x.renderImpl != nil
|
||||
if x.expanded == nil:
|
||||
|
|
@ -175,16 +177,17 @@ proc same(n: VNode, e: Node; nesting = 0): bool =
|
|||
result = true
|
||||
if n.kind != VNodeKind.text:
|
||||
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
|
||||
for i in 0 ..< n.len:
|
||||
if not same(n[i], e[i], nesting+1): return false
|
||||
else:
|
||||
kout toTag[n.kind], e.nodename
|
||||
echo "VDOM: ", toTag[n.kind], " DOM: ", e.nodename
|
||||
|
||||
proc replaceById(id: cstring; newTree: Node) =
|
||||
let x = document.getElementById(id)
|
||||
x.parentNode.replaceChild(newTree, x)
|
||||
newTree.id = id
|
||||
|
||||
type
|
||||
EqResult = enum
|
||||
|
|
@ -331,7 +334,8 @@ proc applyPatch(kxi: KaraxInstance) =
|
|||
if n.kind == VNodeKind.component:
|
||||
let x = VComponent(n)
|
||||
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
|
||||
for i in 0..<kxi.patchLenV:
|
||||
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:
|
||||
mergeEvents(newNode, oldNode, kxi)
|
||||
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
|
||||
if oldNode.text != newNode.text:
|
||||
oldNode.text = newNode.text
|
||||
oldNode.dom.value = newNode.text
|
||||
when false:
|
||||
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
|
||||
if oldNode.text != newNode.text:
|
||||
oldNode.text = newNode.text
|
||||
oldNode.dom.value = newNode.text
|
||||
|
||||
let newLength = newNode.len
|
||||
let oldLength = oldNode.len
|
||||
|
|
@ -463,24 +468,42 @@ proc applyComponents(kxi: KaraxInstance) =
|
|||
while i < kxi.components.len:
|
||||
let x = kxi.components[i].oldNode
|
||||
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 parent = kxi.components[i].parent
|
||||
x.updatedImpl(x, newNode)
|
||||
let oldExpanded = x.expanded
|
||||
x.expanded = x.renderImpl(x)
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, " re-rendered"
|
||||
x.renderedVersion = x.version
|
||||
if oldExpanded == nil:
|
||||
detach(x)
|
||||
kxi.addPatch(pkReplace, parent, current, x.expanded)
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": old expansion didn't exist"
|
||||
else:
|
||||
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
|
||||
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"
|
||||
elif res != different:
|
||||
when defined(karaxDebug):
|
||||
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
|
||||
x.expanded = oldExpanded
|
||||
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
||||
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"
|
||||
inc i
|
||||
setLen(kxi.components, 0)
|
||||
|
|
@ -493,6 +516,40 @@ when defined(stats):
|
|||
result = m + 1
|
||||
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) =
|
||||
if kxi.renderer.isNil: return
|
||||
let newtree = kxi.renderer()
|
||||
|
|
@ -562,6 +619,18 @@ proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
|
|||
kxi = result
|
||||
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;
|
||||
kxi: KaraxInstance = kxi) =
|
||||
## Implements the foundation of Karax's event management.
|
||||
|
|
@ -571,7 +640,19 @@ proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
|
|||
## a ``redraw``.
|
||||
proc wrapper(ev: Event; n: VNode) =
|
||||
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)
|
||||
|
||||
proc setOnHashChange*(action: proc (hashPart: cstring)) =
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
|
|||
result = n
|
||||
of nnkVarSection, nnkLetSection, nnkConstSection:
|
||||
result = n
|
||||
of nnkCallKinds:
|
||||
of nnkCallKinds - {nnkInfix}:
|
||||
let op = getName(n[0])
|
||||
let ck = isComponent(op)
|
||||
if ck != ComponentKind.None:
|
||||
|
|
|
|||
|
|
@ -583,7 +583,7 @@ proc javaEnabled*(h: Navigator): bool
|
|||
# ClassList "methods"
|
||||
proc add*(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)
|
||||
|
||||
# Style "methods"
|
||||
|
|
@ -644,9 +644,9 @@ type
|
|||
proc getBoundingClientRect*(e: Node): BoundingRect {.
|
||||
importcpp: "getBoundingClientRect", nodecl.}
|
||||
proc clientHeight*(): int {.
|
||||
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl}
|
||||
importcpp: "(window.innerHeight || document.documentElement.clientHeight)@", nodecl.}
|
||||
proc clientWidth*(): int {.
|
||||
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl}
|
||||
importcpp: "(window.innerWidth || document.documentElement.clientWidth)@", nodecl.}
|
||||
|
||||
proc inViewport*(el: Node): bool =
|
||||
let rect = el.getBoundingClientRect()
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## This module contains wrappers for the HTML 5 local storage.
|
||||
|
||||
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem"}
|
||||
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem"}
|
||||
proc getItem*(key: cstring): cstring {.importc: "localStorage.getItem".}
|
||||
proc setItem*(key, value: cstring) {.importc: "localStorage.setItem".}
|
||||
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".}
|
||||
|
|
|
|||
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)
|
||||
onAttachImpl*: 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
|
||||
## should call 'markDirty' instead which is an alias for
|
||||
## 'inc version'.
|
||||
|
|
@ -185,7 +186,7 @@ proc getDebugId(): int =
|
|||
gid
|
||||
|
||||
template newComponent*[T](t: typeDesc[T];
|
||||
render: (proc(self: VComponent): VNode) not nil,
|
||||
render: (proc(self: VComponent): VNode) = nil,
|
||||
onAttach: proc(self: VComponent) = nil,
|
||||
onDetach: proc(self: VComponent) = nil,
|
||||
changed: (proc(self, newInstance: VComponent): bool) = defaultChangedImpl,
|
||||
|
|
@ -219,10 +220,28 @@ proc getAttr*(n: VNode; key: cstring): cstring =
|
|||
proc takeOverAttr*(newNode, oldNode: VNode) =
|
||||
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 `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
|
||||
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
|
||||
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 tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue