Merge branch 'master' of github.com:pragmagic/karax

This commit is contained in:
Andreas Rumpf 2017-06-21 08:21:19 +02:00
commit ad0a3ffda9
12 changed files with 156 additions and 67 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
nimcache/ nimcache/
/tests/tester

18
.travis.yml Normal file
View file

@ -0,0 +1,18 @@
sudo: false
language: c
os: linux
install:
- git clone https://github.com/nim-lang/nim
- cd nim
- git clone --depth 1 https://github.com/nim-lang/csources.git
- cd csources
- sh build.sh
- cd ../
- bin/nim c koch
- ./koch boot -d:release
- cd ..
before_script:
- set -e
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
script:
- nim c -r tests/tester.nim

View file

@ -1,5 +1,5 @@
Karax -- Single page applications in Nim Karax – Single page applications in Nim |travis|
======================================== ================================================
Karax is a framework for developing single page applications in Nim. Karax is a framework for developing single page applications in Nim.
It's still in heavy development, so keep in mind that the API is subject It's still in heavy development, so keep in mind that the API is subject
@ -33,3 +33,6 @@ Goals
- Leverage Nim's macro system to produce a framework that allows - Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free. for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible. - Keep it small, keep it fast, keep it flexible.
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
:target: https://travis-ci.org/pragmagic/karax

View file

@ -23,3 +23,4 @@ proc isInt*(s: cstring): bool {.asmNoStackFrame.} =
""" """
proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.} proc parseInt*(s: cstring): int {.importcpp: "parseInt(#, 10)", nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}

View file

@ -7,10 +7,11 @@ import karax
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring; data: cstring;
cont: proc (httpStatus: int; response: cstring)) = cont: proc (httpStatus: int; response: cstring);
kxi: KaraxInstance = kxi) =
proc contWrapper(httpStatus: int; response: cstring) = proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response) cont(httpStatus, response)
redraw() redraw(kxi)
type type
HttpRequest {.importc.} = ref object HttpRequest {.importc.} = ref object
@ -39,12 +40,14 @@ proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)]; proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring; data: cstring;
cont: proc (httpStatus: int, response: cstring)) = cont: proc (httpStatus: int, response: cstring);
ajax("POST", url, headers, data, cont) kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, kxi)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)]; proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring)) = cont: proc (httpStatus: int, response: cstring);
ajax("GET", url, headers, nil, cont) kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".} proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".} proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -14,12 +14,26 @@ proc key*(e: Node): VKey =
else: result = -1 else: result = -1
proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.} proc `key=`*(e: Node; x: VKey) {.importcpp: "#.karaxKey = #", nodecl.}
var type
toFocus: Node KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
toFocusV: VNode ## know about this.
rootId: cstring not nil
renderer: proc (): VNode {.closure.}
currentTree: VNode
postRenderCallback: proc ()
toFocus: Node
toFocusV: VNode
proc setFocus*(n: VNode) = var
toFocusV = n kxi*: KaraxInstance ## The current Karax instance. This is always used
## as the default. **Note**: Within the karax DSL
## always a symbol of the name *kxi* is assumed, so
## if you have a local karax instance to use instead
## in your 'buildHtml' statement, it needs to be named
## 'kxi'.
proc setFocus*(n: VNode; kxi: KaraxInstance = kxi) =
kxi.toFocusV = n
# ----------------- event wrapping --------------------------------------- # ----------------- event wrapping ---------------------------------------
@ -68,13 +82,13 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind; action: EventHandler) =
template detach(n: VNode) = n.dom = nil template detach(n: VNode) = n.dom = nil
template attach(n: Vnode) = n.dom = result template attach(n: Vnode) = n.dom = result
proc vnodeToDom(n: VNode): 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
elif n.kind == VNodeKind.vthunk: elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n) let x = callThunk(vcomponents[n.text], n)
result = vnodeToDom(x) result = vnodeToDom(x, kxi)
#n.key = result.key #n.key = result.key
attach n attach n
return result return result
@ -87,7 +101,7 @@ proc vnodeToDom(n: VNode): Node =
result = document.createElement(toTag[n.kind]) result = document.createElement(toTag[n.kind])
attach n attach n
for k in n: for k in n:
appendChild(result, vnodeToDom(k)) appendChild(result, vnodeToDom(k, kxi))
# text is mapped to 'value': # text is mapped to 'value':
if n.text != nil: if n.text != nil:
result.value = n.text result.value = n.text
@ -102,8 +116,8 @@ proc vnodeToDom(n: VNode): Node =
result.setAttr(k, v) result.setAttr(k, v)
for e, h in items(n.events): for e, h in items(n.events):
wrapEvent(result, n, e, h) wrapEvent(result, n, e, h)
if n == toFocusV and toFocus.isNil: if n == kxi.toFocusV and kxi.toFocus.isNil:
toFocus = result kxi.toFocus = result
if not n.style.isNil: applyStyle(result, n.style) if not n.style.isNil: applyStyle(result, n.style)
proc same(n: VNode, e: Node): bool = proc same(n: VNode, e: Node): bool =
@ -114,10 +128,6 @@ proc same(n: VNode, e: Node): bool =
for i in 0 ..< n.len: for i in 0 ..< n.len:
if not same(n[i], e[i]): return false if not same(n[i], e[i]): return false
var
dorender: proc (): VNode {.closure.}
currentTree: VNode
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)
@ -154,18 +164,19 @@ proc eq(a, b: VNode; deep: bool): EqResult =
# Do not test event listeners here! # Do not test event listeners here!
return result return result
proc updateDirtyElements(parent, current: Node, newNode: VNode) = proc updateDirtyElements(parent, current: Node, newNode: VNode,
kxi: KaraxInstance) =
if newNode.key >= 0 and isDirty(newNode.key): if newNode.key >= 0 and isDirty(newNode.key):
unmarkDirty(newNode.key) unmarkDirty(newNode.key)
let n = vnodeToDom(newNode) let n = vnodeToDom(newNode, kxi)
if parent == nil: if parent == nil:
replaceById("ROOT", n) replaceById(kxi.rootId, n)
else: else:
parent.replaceChild(n, current) parent.replaceChild(n, current)
elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and elif newNode.kind != VNodeKind.text and newNode.kind != VNodeKind.vthunk and
newNode.kind != VNodeKind.dthunk: newNode.kind != VNodeKind.dthunk:
for i in 0..newNode.len-1: for i in 0..newNode.len-1:
updateDirtyElements(current, current[i], newNode[i]) updateDirtyElements(current, current[i], newNode[i], kxi)
# leave early if we know there cannot be anything left to do: # leave early if we know there cannot be anything left to do:
#if dirtyCount <= 0: return #if dirtyCount <= 0: return
@ -186,13 +197,14 @@ proc updateDom(newNode, oldNode: VNode) =
for i in 0 ..< newNode.len: for i in 0 ..< newNode.len:
updateDom(newNode[i], oldNode[i]) updateDom(newNode[i], oldNode[i])
proc updateElement(parent, current: Node, newNode, oldNode: VNode) = proc updateElement(parent, current: Node, newNode, oldNode: VNode;
kxi: KaraxInstance) =
let res = eq(newNode, oldNode, deep=false) let res = eq(newNode, oldNode, deep=false)
if res == different: if res == different:
detach(oldNode) detach(oldNode)
let n = vnodeToDom(newNode) let n = vnodeToDom(newNode, kxi)
if parent == nil: if parent == nil:
replaceById("ROOT", n) replaceById(kxi.rootId, n)
else: else:
parent.replaceChild(n, current) parent.replaceChild(n, current)
else: else:
@ -206,7 +218,7 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
assert oldNode.kind == newNode.kind assert oldNode.kind == newNode.kind
when defined(simpleDiff): when defined(simpleDiff):
for i in 0..min(newLength, oldLength)-1: for i in 0..min(newLength, oldLength)-1:
updateElement(current, current[i], newNode[i], oldNode[i]) updateElement(current, current[i], newNode[i], oldNode[i], kxi)
if newLength > oldLength: if newLength > oldLength:
for i in oldLength..newLength-1: for i in oldLength..newLength-1:
current.appendChild(vnodeToDom(newNode[i])) current.appendChild(vnodeToDom(newNode[i]))
@ -242,14 +254,14 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
var pos = min(oldPos, newPos) + 1 var pos = min(oldPos, newPos) + 1
for i in commonPrefix..pos-1: for i in commonPrefix..pos-1:
updateElement(current, current.childNodes[i], newNode[i], oldNode[i]) updateElement(current, current.childNodes[i], newNode[i], oldNode[i], kxi)
var nextChildPos = oldPos + 1 var nextChildPos = oldPos + 1
while pos <= newPos: while pos <= newPos:
if nextChildPos == oldLength: if nextChildPos == oldLength:
current.appendChild(vnodeToDom(newNode[pos])) current.appendChild(vnodeToDom(newNode[pos], kxi))
else: else:
current.insertBefore(vnodeToDom(newNode[pos]), current.childNodes[nextChildPos]) current.insertBefore(vnodeToDom(newNode[pos], kxi), current.childNodes[nextChildPos])
# added new Node, so old state of VDOM have one more Node # added new Node, so old state of VDOM have one more Node
inc oldLength inc oldLength
inc pos inc pos
@ -262,48 +274,57 @@ proc updateElement(parent, current: Node, newNode, oldNode: VNode) =
when false: when false:
var drawTimeout: Timeout var drawTimeout: Timeout
proc dodraw() = proc dodraw(kxi: KaraxInstance) =
if dorender.isNil: return if kxi.renderer.isNil: return
let newtree = dorender() let newtree = kxi.renderer()
newtree.id = "ROOT" newtree.id = kxi.rootId
toFocus = nil kxi.toFocus = nil
if currentTree == nil: if kxi.currentTree == nil:
currentTree = newtree kxi.currentTree = newtree
let asdom = vnodeToDom currentTree let asdom = vnodeToDom(kxi.currentTree, kxi)
replaceById("ROOT", asdom) replaceById(kxi.rootId, asdom)
else: else:
let olddom = document.getElementById("ROOT") let olddom = document.getElementById(kxi.rootId)
updateElement(nil, olddom, newtree, currentTree) updateElement(nil, olddom, newtree, kxi.currentTree, kxi)
#assert same(newtree, document.getElementById("ROOT")) #assert same(newtree, document.getElementById("ROOT"))
if someDirty: if someDirty:
updateDirtyElements(nil, olddom, newtree) updateDirtyElements(nil, olddom, newtree, kxi)
someDirty = false someDirty = false
currentTree = newtree kxi.currentTree = newtree
if not kxi.postRenderCallback.isNil:
kxi.postRenderCallback()
# now that it's part of the DOM, give it the focus: # now that it's part of the DOM, give it the focus:
if toFocus != nil: if kxi.toFocus != nil:
toFocus.focus() kxi.toFocus.focus()
proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".} proc reqFrame(callback: proc()) {.importc: "window.requestAnimationFrame".}
proc redraw*() = proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests: # we buffer redraw requests:
when false: when false:
if drawTimeout != nil: if drawTimeout != nil:
clearTimeout(drawTimeout) clearTimeout(drawTimeout)
drawTimeout = setTimeout(dodraw, 30) drawTimeout = setTimeout(dodraw, 30)
elif true: elif true:
reqFrame(dodraw) reqFrame(proc () = kxi.dodraw)
else: else:
dodraw() dodraw()
proc init(ev: Event) = proc init(ev: Event) =
reqFrame(dodraw) reqFrame(proc () = kxi.dodraw)
proc setRenderer*(renderer: proc (): VNode) = proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
dorender = renderer clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback)
kxi = result
window.onload = init window.onload = init
proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler) = proc addEventHandler*(n: VNode; k: EventKind; action: EventHandler;
kxi: KaraxInstance = kxi) =
## Implements the foundation of Karax's event management. ## Implements the foundation of Karax's event management.
## Karax DSL transforms ``tag(onEvent = handler)`` to ## Karax DSL transforms ``tag(onEvent = handler)`` to
## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)`` ## ``tempNode.addEventHandler(tagNode, EventKind.onEvent, wrapper)``
@ -311,7 +332,7 @@ 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() redraw(kxi)
addEventListener(n, k, wrapper) addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) = proc setOnHashChange*(action: proc (hashPart: cstring)) =
@ -340,10 +361,10 @@ proc setupErrorHandler*() =
proc prepend(parent, kid: Element) = proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild) parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring) = proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let body = getElementById("body") let body = getElementById("body")
let s = document.createElement("script") let s = document.createElement("script")
s.setAttr "type", "text/javascript" s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename s.setAttr "src", jsfilename
body.prepend(s) body.prepend(s)
redraw() redraw(kxi)

View file

@ -47,12 +47,18 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
assert n.len == result.len assert n.len == result.len
if L > 0: if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext) result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt, of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkTryStmt, nnkFinally: nnkFinally:
# recurse for every child: # recurse for every child:
result = copyNimNode(n) result = copyNimNode(n)
for x in n: for x in n:
result.add tcall2(x, tmpContext) result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkVarSection, nnkLetSection, nnkConstSection: of nnkVarSection, nnkLetSection, nnkConstSection:
result = n result = n
of nnkCallKinds: of nnkCallKinds:
@ -76,7 +82,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
let key = getName x[0] let key = getName x[0]
if key.startsWith("on"): if key.startsWith("on"):
result.add newCall(bindSym"addEventHandler", result.add newCall(bindSym"addEventHandler",
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1]) tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif key in SpecialAttrs: elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])
else: else:
@ -84,7 +90,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
elif ck != ComponentKind.Tag: elif ck != ComponentKind.Tag:
call.add x call.add x
elif eqIdent(x, "setFocus"): elif eqIdent(x, "setFocus"):
result.add newCall(x, tmp) result.add newCall(x, tmp, ident"kxi")
else: else:
result.add tcall2(x, tmp) result.add tcall2(x, tmp)
if tmpContext == nil: if tmpContext == nil:

View file

@ -273,6 +273,7 @@ type
textDecoration*: cstring textDecoration*: cstring
textIndent*: cstring textIndent*: cstring
textTransform*: cstring textTransform*: cstring
transform*: cstring
top*: cstring top*: cstring
verticalAlign*: cstring verticalAlign*: cstring
visibility*: cstring visibility*: cstring
@ -520,6 +521,7 @@ proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
# Event "methods" # Event "methods"
proc preventDefault*(ev: Event) proc preventDefault*(ev: Event)
proc stopPropagation*(ev: Event)
# TouchEvent "methods" # TouchEvent "methods"
proc identifiedTouch*(list: TouchList): Touch proc identifiedTouch*(list: TouchList): Touch
@ -545,9 +547,6 @@ proc decodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.} proc encodeURIComponent*(uri: cstring): cstring {.importc, nodecl.}
proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.} proc isFinite*(x: BiggestFloat): bool {.importc, nodecl.}
proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.} proc isNaN*(x: BiggestFloat): bool {.importc, nodecl.}
proc parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}
proc parseInt*(s: cstring): int {.importc, nodecl.}
proc parseInt*(s: cstring, radix: int):int {.importc, nodecl.}
proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.} proc id*(n: Node): cstring {.importcpp: "#.id", nodecl.}
@ -566,7 +565,7 @@ proc getElementsByClass*(n: Node; name: cstring): seq[Node] {.
type type
BoundingRect* {.importc.} = object BoundingRect* {.importc.} = object
top*, bottom*, left*, right*: int top*, bottom*, left*, right*, x*, y*, width*, height*: float
proc getBoundingClientRect*(e: Node): BoundingRect {. proc getBoundingClientRect*(e: Node): BoundingRect {.
importcpp: "getBoundingClientRect", nodecl.} importcpp: "getBoundingClientRect", nodecl.}
@ -578,8 +577,8 @@ proc clientWidth*(): int {.
proc inViewport*(el: Node): bool = proc inViewport*(el: Node): bool =
let rect = el.getBoundingClientRect() let rect = el.getBoundingClientRect()
result = rect.top >= 0 and rect.left >= 0 and result = rect.top >= 0 and rect.left >= 0 and
rect.bottom <= clientHeight() and rect.bottom <= clientHeight().float and
rect.right <= clientWidth() rect.right <= clientWidth().float
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}

View file

@ -73,6 +73,9 @@ type
ondragstart, ## The user starts dragging an element or text selection. ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target. ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onkeyupenter, ## vdom extension: an input field received the ENTER key press onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions) ## passed (useful for on-the-fly text completions)

18
tests/compiler_tests.nim Normal file
View file

@ -0,0 +1,18 @@
include karaxprelude
block:
type
TestEnum {.pure.} = enum
ValA, ValB
var e = TestEnum.ValA
proc renderDom(): VNode {.used.} =
result = buildHtml():
tdiv():
case e
of TestEnum.ValA:
tdiv: text "A"
of TestEnum.ValB:
tdiv: text "B"

16
tests/tester.nim Normal file
View file

@ -0,0 +1,16 @@
# For now we only test that the things still compile.
import os
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "command failed " & cmd
proc main =
exec("nim js tests/diffDomTests.nim")
exec("nim js tests/compiler_tests.nim")
exec("nim js examples/todoapp/todoapp.nim")
exec("nim js examples/scrollapp/scrollapp.nim")
exec("nim js examples/mediaplayer/playerapp.nim")
main()