From bb846644b5a6b6f7148712fc67c5c33cbc062f44 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Tue, 8 Aug 2017 13:29:29 +0200 Subject: [PATCH 01/18] added support for a --define:karaxDebug switch --- src/karax.nim | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/karax.nim b/src/karax.nim index 9d74a9e..5cee5cf 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -463,24 +463,38 @@ proc applyComponents(kxi: KaraxInstance) = while i < kxi.components.len: let x = kxi.components[i].oldNode let newNode = kxi.components[i].newNode + when defined(karaxDebug): + echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil if 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) From 7c08c6bb76498f05b512ef09b4129a75496ec620 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 16 Aug 2017 19:42:16 +0200 Subject: [PATCH 02/18] first version; wip --- experiments/nextgen.nim | 181 +++++++++++++++++++++++++++++++++++++ experiments/scrollapp.nims | 1 - src/karax.nim | 32 ++++++- src/vdom.nim | 3 +- 4 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 experiments/nextgen.nim delete mode 100644 experiments/scrollapp.nims diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..eb94658 --- /dev/null +++ b/experiments/nextgen.nim @@ -0,0 +1,181 @@ + +import kdom, jdict, jstrutils + +type + Message = enum + Unchanged + Changed + Mark + Inserted + Deleted + + State = object + stale: int + outdated: bool + phantom: bool + +type + ReactiveBase* = ref object of RootObj ## everything that is a "reactive" + ## value derives from that + sinks*: seq[proc(msg: Message, pos: 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 + +proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = + x.sinks.add sink + +proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = + for s in x.sinks: s(msg, pos) + +var toTrack: proc (msg: Message; pos: int) = nil + +proc now[T](x: Reactive[T]): T = + if toTrack != nil: + x.addSink toTrack + result = x.value + +proc `:=`[T](x: Reactive[T], f: proc(): T) = + var state: State + + toTrack = proc(msg: Message; pos: int) = + if not state.phantom: + 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: + dec state.stale + if state.stale == 0: + x.broadcast(msg, pos) + #state.phantom = true + + x.value = f() + toTrack = nil + +proc `<-`[T](x: Reactive[T], val: T) = + if x.value != val: + x.value = val + x.broadcast(Mark) + x.broadcast(Changed) + +proc changed*(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: 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): 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 + +proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] + +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 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.. Date: Thu, 17 Aug 2017 08:36:17 +0200 Subject: [PATCH 03/18] nextgen uses the karax DSL and DOM diffing --- experiments/nextgen.nim | 28 ++++++++++------------------ src/karax.nim | 1 + 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index eb94658..8df39a2 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,5 +1,5 @@ -import kdom, jdict, jstrutils +import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils type Message = enum @@ -155,27 +155,19 @@ type var gu = newReactive(User(firstname: "Some", lastname: "Body")) -proc renderUser(u: Reactive[User]): Node = - result = document.createElement("button") - result.appendChild document.createTextNode(u.now.firstname & " " & u.now.lastname) - result.addEventListener "click", proc (ev: Event) = - gu <- User(firstname: "Another", lastname: "Guy") +proc renderUser(u: Reactive[User]): VNode = + result = buildHtml(button): + text u.now.firstname & " " & u.now.lastname + proc onclick(ev: Event; n: VNode) = + gu <- User(firstname: "Another", lastname: "Guy") -proc replaceById(id: cstring; newTree: Node) = - let x = document.getElementById(id) - x.parentNode.replaceChild(newTree, x) - newTree.id = id - -template track(r: ReactiveBase; a, b: Node) = +template track(r: ReactiveBase; a, b: VNode) = r.addSink proc(m: Message; pos: int) = if m == Changed: - a.parentNode.replaceChild(b, a) + runDiff(kxi, a, b) -proc main(): Node = +proc main(): VNode = result = renderUser(gu) track gu, result, renderUser(gu) -proc init(ev: Event) = - replaceById("ROOT", main()) - -window.onload = init +setInitializer(main) diff --git a/src/karax.nim b/src/karax.nim index b0b7761..3f53405 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -187,6 +187,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = proc replaceById(id: cstring; newTree: Node) = let x = document.getElementById(id) x.parentNode.replaceChild(newTree, x) + newTree.id = id type EqResult = enum From 46b12594984d571a0a19e2e8c9f98fc75f238c62 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Thu, 17 Aug 2017 11:28:11 +0200 Subject: [PATCH 04/18] added .track macro --- experiments/nextgen.nim | 170 +++---------------------------------- src/reactive.nim | 182 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 159 deletions(-) create mode 100644 src/reactive.nim diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 8df39a2..fb95a6b 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,173 +1,25 @@ -import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils - -type - Message = enum - Unchanged - Changed - Mark - Inserted - Deleted - - State = object - stale: int - outdated: bool - phantom: bool - -type - ReactiveBase* = ref object of RootObj ## everything that is a "reactive" - ## value derives from that - sinks*: seq[proc(msg: Message, pos: 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 - -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = - x.sinks.add sink - -proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = - for s in x.sinks: s(msg, pos) - -var toTrack: proc (msg: Message; pos: int) = nil - -proc now[T](x: Reactive[T]): T = - if toTrack != nil: - x.addSink toTrack - result = x.value - -proc `:=`[T](x: Reactive[T], f: proc(): T) = - var state: State - - toTrack = proc(msg: Message; pos: int) = - if not state.phantom: - 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: - dec state.stale - if state.stale == 0: - x.broadcast(msg, pos) - #state.phantom = true - - x.value = f() - toTrack = nil - -proc `<-`[T](x: Reactive[T], val: T) = - if x.value != val: - x.value = val - x.broadcast(Mark) - x.broadcast(Changed) - -proc changed*(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: 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): 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 - -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[index] - -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 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.. Date: Sat, 19 Aug 2017 14:54:10 +0200 Subject: [PATCH 05/18] reactive sequence works now --- experiments/nextgen.nim | 67 ++++++++++++++++++++++++++++--------- src/karax.nim | 18 ++++++++++ src/reactive.nim | 73 +++++++++++++++++++++++++++++++++++------ src/vdom.nim | 4 +++ 4 files changed, 137 insertions(+), 25 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index fb95a6b..4f955aa 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -2,24 +2,61 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive type - User = ref object + User = ref object of ReactiveBase firstname, lastname: cstring -var gu = @[newReactive(User(firstname: "Some", lastname: "Body")), - newReactive(User(firstname: "Some", lastname: "One")), - newReactive(User(firstname: "Some", lastname: "Two"))] +var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), + (User(firstname: "Some", lastname: "One")), + (User(firstname: "Some", lastname: "Two"))]) var clicks = 0 -proc renderUser(u: Reactive[User]): VNode {.track.} = - result = buildHtml(button): - text u.now.firstname & " " & u.now.lastname - proc onclick(ev: Event; n: VNode) = - inc clicks - u <- User(firstname: "Another", lastname: &clicks) +discard """ + # Text gets a *reactive* string in the first place! + # Text can register and knows how to update itself! + proc toReact(): RString = + observe(u): + u.firstname & u.lastname -proc main(): VNode = - result = buildHtml(tdiv): - for i in 0..high(gu): - renderUser(gu[i]) + let t = text(u.firstname & " " & u.lastname) + observe(u, t.update(u.firstname & " " & u.lastname)) + t +""" -setInitializer(main) +proc renderUser(u: User): VNode = #{.track.} = + proc inner(u: User): VNode = + result = buildHtml(tdiv): + text u.firstname & " " & u.lastname + button: + text "(X)" + proc onclick(ev: Event; n: VNode) = + #u.firstname = "kfdj" + #u.lastname = &clicks + #notifyObservers(u) + gu.deleteElem(u) + + result = inner(u) + doTrack(u, result, inner(u)) + +template vmap(x: RSeq; elem, f: untyped): VNode = + let tmp = buildHtml(elem): + for i in 0..= 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) + proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = let olddom = oldNode.dom discard diff(newNode, oldNode, nil, olddom, kxi) diff --git a/src/reactive.nim b/src/reactive.nim index a1782f0..4fcc851 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -32,7 +32,7 @@ type s: seq[T] L: RInt -proc addSink[T](x: Reactive[T]; sink: proc(msg: Message; pos: int)) = +proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = x.sinks.add sink proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = @@ -88,7 +88,7 @@ proc `<-`*[T](x: Reactive[T], val: T) = x.broadcast(Mark) x.broadcast(Changed) -proc changed*(x: ReactiveBase) = +proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) @@ -125,10 +125,17 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = for i in 0..high(data): result.s[i] = data[i] -proc `[]=`[T](x: RSeq[T]; index: int; v: T) = +proc `[]=`*[T](x: RSeq[T]; index: int; v: T) = x.s[index] = v -proc `[]`[T](x: RSeq[T]; index: int): T = x.s[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) @@ -140,6 +147,16 @@ proc delete*[T](x: RSeq[T]; position = 0) = x.broadcast(Mark) x.broadcast(Deleted, position) +proc deleteElem*[T](x: RSeq[T]; y: T) = + var position = -1 + for i in 0..= 0: + x.s.delete(position) + x.broadcast(Mark) + x.broadcast(Deleted, position) + proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = let xl = x.L.value let res = newRSeq[U](xl) @@ -159,24 +176,60 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = - when r is Reactive: + when r is ReactiveBase: addSink r, key, proc(m: Message; pos: int) = if m == Changed: runDiff(kxi, a, b) +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: 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: + karax.runIns(kxi, a, b, pos) + else: discard + +template notifyImpl(r: untyped) = + when r is ReactiveBase: + notifyObservers(r) + +proc root(n: NimNode): NimNode = + result = n + while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] + +proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = + result = n + #if n.kind in {nnkAsgn, nnkFastAsgn}: + # + # n[0] + #else: + # recurse() + macro track*(procDef: untyped): untyped = let params = params(procDef) let key = lineInfo(procDef) var call = newCall(procDef.name) var trackings = newStmtList() - for b in procDef.body: trackings.add b + var paramList: seq[NimNode] = @[] for j in 1.. Date: Tue, 22 Aug 2017 18:04:49 +0200 Subject: [PATCH 06/18] wip; nothing works --- experiments/nextgen.nim | 109 +++++++++++++++++++++++++++++++--------- src/reactive.nim | 17 +++++-- src/vdom.nim | 11 +++- 3 files changed, 107 insertions(+), 30 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index 4f955aa..d311eb9 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,6 +1,25 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive +proc newTextInput*(text: RString; focus: RBool): VNode = + proc onFlip(ev: Event; target: VNode) = + focus <- not focus.value + + proc onKeyupEnter(ev: Event; target: VNode) = + text <- target.value + #text.notifyObservers() + + 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("") + type User = ref object of ReactiveBase firstname, lastname: cstring @@ -8,6 +27,43 @@ type var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) +var selected = newReactive[User](nil) + +proc toUI*(): RString = + result = RString() + result.subscribe proc (v: cstring) = + if v.len > 0: + if selected >= 0: + # XXX what's happening here? + persons[selected].firstname = v + notifyObservers(persons[selected]) + errmsg <- "" + else: + errmsg <- "name must not be empty" + +var inp = toUI() + +proc adaptFocus(def = false): RBool = + result = RBool() + result.value = def + result.subscribe proc (hasFocus: bool) = + if not hasFocus: + inp.notifyObservers() + +var focus = 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 focus.value: cstring"red" else: cstring"black")), + (StyleAttr.fontSize, cstring"12px"), + (StyleAttr.fontWeight, cstring"600") + ) + var clicks = 0 discard """ @@ -22,20 +78,23 @@ discard """ t """ -proc renderUser(u: User): VNode = #{.track.} = - proc inner(u: User): VNode = - result = buildHtml(tdiv): - text u.firstname & " " & u.lastname - button: - text "(X)" - proc onclick(ev: Event; n: VNode) = - #u.firstname = "kfdj" - #u.lastname = &clicks - #notifyObservers(u) - gu.deleteElem(u) +template observe(s: cstring): RString = + let tmp = rstr(s) + u.subscribeSelf proc () = + tmp <- s + temp - result = inner(u) - doTrack(u, result, inner(u)) +proc renderUser(u: User; selected: RBool): VNode = + result = buildHtml(tdiv): + let displayName = observe(u.firstname & " " & u.lastname) + if selected == u: + newTextInput displayName, focus + else: + text displayName + button: + text "(x)" + proc onclick(ev: Event; n: VNode) = + gu.deleteElem(u) template vmap(x: RSeq; elem, f: untyped): VNode = let tmp = buildHtml(elem): @@ -44,18 +103,18 @@ template vmap(x: RSeq; elem, f: untyped): VNode = doTrackResize(x, tmp, f(x[pos])) tmp -proc main(gu: RSeq[User]): VNode = #{.track.} = - proc inner(gu: RSeq[User]): VNode = - result = buildHtml(tdiv): - vmap(gu, tdiv, renderUser) - tdiv: - button: - text "Add User" - proc onclick(ev: Event; n: VNode) = - inc clicks - gu.add User(firstname: "Added", lastname: &clicks) - result = inner(gu) - doTrack(gu, result, inner(gu)) +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(firstname: "Added", lastname: &clicks) + tdiv: + text errmsg + vmap(gu, tdiv, renderUser) + proc init(): VNode = main(gu) diff --git a/src/reactive.nim b/src/reactive.nim index 4fcc851..a0944a9 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -8,6 +8,7 @@ type Mark Inserted Deleted + Replaced State = object stale: int @@ -70,7 +71,7 @@ template wrapObserver(f: untyped) = of Mark: if state.stale == 0: x.broadcast(msg, pos) inc state.stale - of Inserted, Deleted: + of Inserted, Deleted, Replaced: dec state.stale if state.stale == 0: x.broadcast(msg, pos) @@ -92,12 +93,20 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribe[T](x: Reactive[T], f: proc(x: T)) = +proc subscribeVal*[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: discard + 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) = @@ -127,6 +136,8 @@ proc newRSeq*[T](data: seq[T]): RSeq[T] = 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 diff --git a/src/vdom.nim b/src/vdom.nim index bf84cb8..63f6669 100644 --- a/src/vdom.nim +++ b/src/vdom.nim @@ -239,8 +239,15 @@ proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; 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), index: -1) -proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) +proc rawtext*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) +proc rawtext*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) + +import reactive + +proc text*(s: RString): VNode = + result = VNode(kind: VNodeKind.text, text: s, index: -1) + s.subscribe proc(v: cstring) = + result.dom.value = v iterator items*(n: VNode): VNode = for i in 0.. Date: Wed, 23 Aug 2017 19:35:27 +0200 Subject: [PATCH 07/18] more reactive stuff works now --- experiments/nextgen.nim | 55 +++++++++++++++++++++++++-------- src/karax.nim | 9 ++++-- src/reactive.nim | 67 +++++++++++++++++------------------------ src/vdom.nim | 25 +++++++++------ 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim index d311eb9..40acf04 100644 --- a/experiments/nextgen.nim +++ b/experiments/nextgen.nim @@ -1,7 +1,7 @@ import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, reactive -proc newTextInput*(text: RString; focus: RBool): VNode = +proc newTextInput*(text: RString; focus: RBool): VNode {.track.} = proc onFlip(ev: Event; target: VNode) = focus <- not focus.value @@ -23,20 +23,22 @@ var type User = ref object of ReactiveBase firstname, lastname: cstring + selected: bool var gu = newRSeq(@[ (User(firstname: "Some", lastname: "Body")), (User(firstname: "Some", lastname: "One")), (User(firstname: "Some", lastname: "Two"))]) -var selected = newReactive[User](nil) +var prevSelected: User = nil #newReactive[User](nil) proc toUI*(): RString = result = RString() result.subscribe proc (v: cstring) = if v.len > 0: - if selected >= 0: + let p = prevSelected #selected.value + if p != nil: # XXX what's happening here? - persons[selected].firstname = v - notifyObservers(persons[selected]) + p.firstname = v + notifyObservers(p) errmsg <- "" else: errmsg <- "name must not be empty" @@ -76,20 +78,31 @@ discard """ let t = text(u.firstname & " " & u.lastname) observe(u, t.update(u.firstname & " " & u.lastname)) t -""" template observe(s: cstring): RString = let tmp = rstr(s) u.subscribeSelf proc () = tmp <- s temp +""" -proc renderUser(u: User; selected: RBool): VNode = +proc renderUser(u: User): VNode {.track.} = result = buildHtml(tdiv): - let displayName = observe(u.firstname & " " & u.lastname) - if selected == u: - newTextInput displayName, focus + let displayName = u.firstname & " " & u.lastname + if u.selected: + # == selected.value: + !(inp <- displayName) + newTextInput inp, focus else: + button: + text "..." + proc onclick(ev: Event; n: VNode) = + if prevSelected != nil: + prevSelected.selected = false + notifyObservers(prevSelected) + u.selected = true + notifyObservers(u) + prevSelected = u text displayName button: text "(x)" @@ -103,6 +116,25 @@ template vmap(x: RSeq; elem, f: untyped): VNode = 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.. 0: + # x.sinks[0] = sink + #else: + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid proc broadcast(x: ReactiveBase, msg: Message; pos = 0) = for s in x.sinks: s(msg, pos) @@ -93,7 +93,7 @@ proc notifyObservers*(x: ReactiveBase) = x.broadcast(Mark) x.broadcast(Changed) -proc subscribeVal*[T](x: Reactive[T], f: proc(x: T)) = +proc subscribe*[T](x: Reactive[T], f: proc(x: T)) = let reactor = proc (msg: Message, pos: int) = case msg: of Mark: discard @@ -186,11 +186,10 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = +template trackImpl(r: ReactiveBase; a, b: untyped) = when r is ReactiveBase: - addSink r, key, proc(m: Message; pos: int) = - if m == Changed: - runDiff(kxi, a, b) + addSink r, proc(m: Message; pos: int) = + if m == Changed: karax.runDiff(kxi, a, b) template doTrack*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -207,40 +206,30 @@ template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = case m of Deleted: karax.runDel(kxi, a, pos) of Inserted: + let it {.used.} = r[pos] karax.runIns(kxi, a, b, pos) else: discard -template notifyImpl(r: untyped) = - when r is ReactiveBase: - notifyObservers(r) - -proc root(n: NimNode): NimNode = - result = n - while result.kind in {nnkDotExpr, nnkBracketExpr}: result = result[0] - -proc analyse(n: NimNode; paramList: seq[NimNode]): NimNode = - result = n - #if n.kind in {nnkAsgn, nnkFastAsgn}: - # - # n[0] - #else: - # recurse() - macro track*(procDef: untyped): untyped = let params = params(procDef) - let key = lineInfo(procDef) - var call = newCall(procDef.name) var trackings = newStmtList() - var paramList: seq[NimNode] = @[] + var inner = copyNimTree(procDef) + inner[0] = ident($procDef.name & "Inner") + var call = newCall(inner[0]) + trackings.add inner + trackings.add newAssignment(ident"result", call) + for j in 1.. Date: Mon, 28 Aug 2017 11:29:12 +0200 Subject: [PATCH 08/18] improvments to the new local diff algorithm --- src/karax.nim | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/karax.nim b/src/karax.nim index f68e3ff..2df1d9f 100644 --- a/src/karax.nim +++ b/src/karax.nim @@ -182,7 +182,7 @@ proc same(n: VNode, e: Node; nesting = 0): bool = 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) @@ -334,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.. Date: Mon, 28 Aug 2017 11:29:43 +0200 Subject: [PATCH 09/18] created a nice 'makeReactive' macro --- src/reactive.nim | 128 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 119 insertions(+), 9 deletions(-) diff --git a/src/reactive.nim b/src/reactive.nim index 307d317..34c630e 100644 --- a/src/reactive.nim +++ b/src/reactive.nim @@ -1,4 +1,6 @@ +import jdict + type Message = enum Unchanged @@ -14,9 +16,11 @@ type 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*: seq[proc(msg: Message, pos: int)] + sinks*: SinkSeq + dups: JDict[cstring, bool] id: int Reactive*[T] = ref object of ReactiveBase value*: T @@ -33,10 +37,16 @@ type var rid: int +proc addSink(x: ReactiveBase; key: cstring; sink: proc(msg: Message; pos: int)) = + if x.dups == nil: x.dups = newJDict[cstring, bool]() + if not x.dups.contains(key): + x.dups[key] = true + x.sinks.add sink + if x.id == 0: + inc rid + x.id = rid + proc addSink(x: ReactiveBase; sink: proc(msg: Message; pos: int)) = - #if x.sinks.len > 0: - # x.sinks[0] = sink - #else: x.sinks.add sink if x.id == 0: inc rid @@ -78,6 +88,29 @@ template wrapObserver(f: untyped) = #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], f: proc(): T) = toTrack = wrapObserver(f()) x.value = f() @@ -186,17 +219,29 @@ proc map*[T, U](x: RSeq[T], f: proc(x: T): U): RSeq[U] = import macros -template trackImpl(r: ReactiveBase; a, b: untyped) = +template protect(r: ReactiveBase; body: untyped) = + var tmp: seq[proc(msg: Message, pos: int)] + swap(r.sinks, tmp) + body + swap(r.sinks, tmp) + +template trackImpl(r: ReactiveBase; key: cstring; a, b: untyped) = when r is ReactiveBase: - addSink r, proc(m: Message; pos: int) = - if m == Changed: karax.runDiff(kxi, a, b) + addSink r, key, proc(m: Message; pos: int) = + if m == Changed: + protect r: + echo "runDiff A" + karax.runDiff(kxi, a, b) 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: karax.runDiff(kxi, a, b) + if m == Changed: + protect r: + echo "runDiff B" + karax.runDiff(kxi, a, b) template doTrackResize*(r: ReactiveBase; a, b: untyped) {.dirty.} = bind addSink, Message, RSeq, Changed, Deleted, Inserted @@ -226,10 +271,75 @@ macro track*(procDef: untyped): untyped = let param = x[i] call.add(param) + let key = lineInfo(procDef) for j in 1..