Merge branch 'master' of github.com:pragmagic/karax
This commit is contained in:
commit
ad0a3ffda9
12 changed files with 156 additions and 67 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
nimcache/
|
||||
/tests/tester
|
||||
18
.travis.yml
Normal file
18
.travis.yml
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.}
|
||||
|
|
|
|||
|
|
@ -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".}
|
||||
|
|
|
|||
119
src/karax.nim
119
src/karax.nim
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
11
src/kdom.nim
11
src/kdom.nim
|
|
@ -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.}
|
||||
|
|
|
|||
|
|
@ -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
18
tests/compiler_tests.nim
Normal 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
16
tests/tester.nim
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue