diff --git a/experiments/nextgen.nim b/experiments/nextgen.nim new file mode 100644 index 0000000..56330d0 --- /dev/null +++ b/experiments/nextgen.nim @@ -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) diff --git a/experiments/scrollapp.nims b/experiments/scrollapp.nims deleted file mode 100644 index 532b206..0000000 --- a/experiments/scrollapp.nims +++ /dev/null @@ -1 +0,0 @@ ---path: "../src" diff --git a/src/kajax.nim b/src/kajax.nim index 414b9d2..c226c50 100644 --- a/src/kajax.nim +++ b/src/kajax.nim @@ -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() diff --git a/src/karax.nim b/src/karax.nim index 9d74a9e..aef2730 100644 --- a/src/karax.nim +++ b/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..= 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)) = diff --git a/src/karaxdsl.nim b/src/karaxdsl.nim index cfbe391..dd480e1 100644 --- a/src/karaxdsl.nim +++ b/src/karaxdsl.nim @@ -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: diff --git a/src/kdom.nim b/src/kdom.nim index 106666c..2c015a9 100644 --- a/src/kdom.nim +++ b/src/kdom.nim @@ -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() diff --git a/src/localstorage.nim b/src/localstorage.nim index 5744124..e2fb62e 100644 --- a/src/localstorage.nim +++ b/src/localstorage.nim @@ -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".} diff --git a/src/reactive.nim b/src/reactive.nim new file mode 100644 index 0000000..4d83a92 --- /dev/null +++ b/src/reactive.nim @@ -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..= 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..