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/
/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.
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
for the development of applications that are boilerplate free.
- 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 parseFloat*(s: cstring): BiggestFloat {.importc, nodecl.}

View file

@ -7,10 +7,11 @@ import karax
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring)) =
cont: proc (httpStatus: int; response: cstring);
kxi: KaraxInstance = kxi) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
redraw()
redraw(kxi)
type
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)];
data: cstring;
cont: proc (httpStatus: int, response: cstring)) =
ajax("POST", url, headers, data, cont)
cont: proc (httpStatus: int, response: cstring);
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, kxi)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring)) =
ajax("GET", url, headers, nil, cont)
cont: proc (httpStatus: int, response: cstring);
kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

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

View file

@ -47,12 +47,18 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkCaseStmt,
nnkTryStmt, nnkFinally:
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
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:
result = n
of nnkCallKinds:
@ -76,7 +82,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
let key = getName x[0]
if key.startsWith("on"):
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:
result.add newDotAsgn(tmp, key, x[1])
else:
@ -84,7 +90,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
elif ck != ComponentKind.Tag:
call.add x
elif eqIdent(x, "setFocus"):
result.add newCall(x, tmp)
result.add newCall(x, tmp, ident"kxi")
else:
result.add tcall2(x, tmp)
if tmpContext == nil:

View file

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