Compare commits

..

No commits in common. "master" and "1.0.0" have entirely different histories.

37 changed files with 1827 additions and 2306 deletions

View file

@ -1,31 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: CI
# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
container: nimlang/nim
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
# Runs a single command using the runners shell
- name: Tests
run: |
nimble develop -y
nim c -r tests/tester.nim

2
.gitignore vendored
View file

@ -3,5 +3,3 @@ nimcache/
karax/tools/karun karax/tools/karun
*.code-workspace *.code-workspace
*.exe *.exe
app.js
app.html

20
.travis.yml Normal file
View file

@ -0,0 +1,20 @@
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
- ./koch tools
- cd ..
before_script:
- set -e
- export PATH=$(pwd)/nim/bin:$(pwd):$PATH
script:
- nimble develop
- nim c -r tests/tester.nim

View file

@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system.
These extensions are: These extensions are:
``VNodeKind.int`` ``VNodeKind.int``
The node has a single integer field. The node havs a single integer field.
``VNodeKind.bool`` ``VNodeKind.bool``
The node has a single boolean field. The node has a single boolean field.
``VNodeKind.vthunk`` ``VNodeKint.vthunk``
The node is a `virtual thunk`:idx:. This means there is a The node is a `virtual thunk`:idx:. This means there is a
function attached to it that produces the ``VNode`` structure function attached to it that produces the ``VNode`` structure
on demand. on demand.
``VNodeKind.dthunk`` ``VNodeKint.dthunk``
The node is a `DOM thunk`:idx:. This means there is a The node is a `DOM thunk`:idx:. This means there is a
function attached to it that produces the ``Node`` DOM structure function attached to it that produces the ``Node`` DOM structure
on demand. on demand.
@ -53,6 +53,6 @@ Event system
Karax does not abstract over the event system the DOM offers much: The same Karax does not abstract over the event system the DOM offers much: The same
``Event`` structure is used. Every callback has the ``Event`` structure is used. Every callback has the
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``. signature ``proc (ev: Event; n: VNode)``.

View file

@ -1,3 +1,4 @@
include karax / prelude include karax / prelude
var lines: seq[kstring] = @[] var lines: seq[kstring] = @[]

View file

@ -1,15 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Carousel app</title> <title>Carousel app</title>
</head> </head>
<body id="body"> <body id="body">
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="carousel.js"></script> <script type="text/javascript" src="nimcache/carousel.js"></script>
</body> </body>
</html> </html>

View file

@ -1,17 +0,0 @@
include karax / prelude
import karax / vstyles
proc createDom(): VNode =
result = buildHtml(tdiv):
tdiv(style = style(StyleAttr.color, "red".cstring)):
text "red"
tdiv(style = style(StyleAttr.color, "blue")):
text "blue"
# explicit `kstring` required for varargs overload
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))):
text "italic orange"
# can use a string directly
tdiv(style = "font-style: oblique; color: pink".toCss):
text "oblique pink"
setRenderer createDom

View file

@ -1,9 +1,10 @@
include karax / prelude include karax / prelude
import random import random
proc createDom(): VNode = proc createDom(): VNode =
result = buildHtml(tdiv): result = buildHtml(tdiv):
if rand(100) <= 50: if random(100) <= 50:
text "Hello World!" text "Hello World!"
else: else:
text "Hello Universe" text "Hello Universe"

View file

@ -1,5 +1,6 @@
include karax / prelude include karax / prelude
from sugar import `=>` from future import `=>`
import karax / errors import karax / errors
proc loginField(desc, field, class: kstring; proc loginField(desc, field, class: kstring;
@ -17,7 +18,7 @@ const
proc validateNotEmpty(field: kstring): proc () = proc validateNotEmpty(field: kstring): proc () =
result = proc () = result = proc () =
let x = getVNodeById(field) let x = getVNodeById(field)
if x.text == "": if x.text.isNil or x.text == "":
errors.setError(field, field & " must not be empty") errors.setError(field, field & " must not be empty")
else: else:
errors.setError(field, "") errors.setError(field, "")
@ -27,7 +28,7 @@ var loggedIn: bool
proc loginDialog(): VNode = proc loginDialog(): VNode =
result = buildHtml(tdiv): result = buildHtml(tdiv):
if not loggedIn: if not loggedIn:
loginField("Name: ", username, "input", validateNotEmpty) loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty) loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()): button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login" text "Login"

View file

@ -1,15 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Player app</title> <title>Player app</title>
</head> </head>
<body id="body"> <body id="body">
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="playerapp.js"></script> <script type="text/javascript" src="nimcache/playerapp.js"></script>
</body> </body>
</html> </html>

View file

@ -1,15 +1,15 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Todo app</title> <title>Todo app</title>
</head> </head>
<body id="body"> <body id="body">
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="scrollapp.js"></script> <script type="text/javascript" src="nimcache/scrollapp.js"></script>
</body> </body>
</html> </html>

View file

@ -1,16 +1,16 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>Todo app</title> <title>Todo app</title>
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
</head> </head>
<body id="body"> <body id="body">
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="todoapp.js"></script> <script type="text/javascript" src="nimcache/todoapp.js"></script>
</body> </body>
</html> </html>

View file

@ -9,7 +9,7 @@ var
selectedEntry = -1 selectedEntry = -1
filter: Filter filter: Filter
entriesLen: int entriesLen: int
doneswitch = true
const const
contentSuffix = cstring"content" contentSuffix = cstring"content"
completedSuffix = cstring"completed" completedSuffix = cstring"completed"
@ -62,9 +62,9 @@ proc toggleEntry(ev: Event; n: VNode) =
markAsCompleted(id, not isCompleted(id)) markAsCompleted(id, not isCompleted(id))
proc onAllDone(ev: Event; n: VNode) = proc onAllDone(ev: Event; n: VNode) =
for i in 0..<entriesLen: clear()
markAsCompleted(i, doneswitch) selectedEntry = -1
doneswitch = not doneswitch
proc clearCompleted(ev: Event, n: VNode) = proc clearCompleted(ev: Event, n: VNode) =
for i in 0..<entriesLen: for i in 0..<entriesLen:
if isCompleted(i): setEntryContent(i, nil) if isCompleted(i): setEntryContent(i, nil)
@ -124,8 +124,8 @@ proc createDom(data: RouterData): VNode =
section(class = "todoapp"): section(class = "todoapp"):
makeHeader() makeHeader()
section(class = "main"): section(class = "main"):
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone) input(class = "toggle-all", `type` = "checkbox", name = "toggle")
label(`for` = "toggle"): label(`for` = "toggle-all", onclick = onAllDone):
text "Mark all as complete" text "Mark all as complete"
var entriesCount = 0 var entriesCount = 0
var completedCount = 0 var completedCount = 0

View file

@ -1,3 +1,4 @@
include login include login
const const
@ -11,7 +12,7 @@ var allMessages: seq[TextMessage] = @[]
proc doSendMessage() = proc doSendMessage() =
let inputField = getVNodeById(message) let inputField = getVNodeById(message)
allMessages.add(TextMessage(name: "you", content: inputField.getInputText)) allMessages.add(TextMessage(name: "you", content: inputField.text))
inputField.setInputText "" inputField.setInputText ""
proc main(): VNode = proc main(): VNode =

View file

@ -9,5 +9,5 @@
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="ajaxtest.js"></script> <script type="text/javascript" src="nimcache/ajaxtest.js"></script>
</html> </html>

View file

@ -10,5 +10,5 @@
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.2/echarts-en.js"></script> <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/echarts/4.0.2/echarts-en.js"></script>
<script type="text/javascript" src="echartstest.js"></script> <script type="text/javascript" src="nimcache/echartstest.js"></script>
</html> </html>

View file

@ -8,6 +8,6 @@
<body id="body"> <body id="body">
<div id="ROOT"></div> <div id="ROOT"></div>
<script type="text/javascript" src="demo.js"></script> <script type="text/javascript" src="nimcache/demo.js"></script>
</body> </body>
</html> </html>

View file

@ -10,7 +10,7 @@
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="scrollapp.js"></script> <script type="text/javascript" src="nimcache/scrollapp.js"></script>
<script type="text/javascript"> <script type="text/javascript">
onload(window.location.pathname.split('/')[1]) onload(window.location.pathname.split('/')[1])
</script> </script>

View file

@ -1,15 +1,14 @@
# Package # Package
version = "1.2.1" version = "1.0.0"
author = "Andreas Rumpf" author = "Andreas Rumpf"
description = "Karax is a framework for developing single page applications in Nim." description = "Karax is a framework for developing single page applications in Nim."
license = "MIT" license = "MIT"
# Dependencies # Dependencies
requires "nim >= 0.18.0" requires "nim >= 0.16.1"
requires "ws"
requires "dotenv"
skipDirs = @["examples", "experiments", "tests"] skipDirs = @["examples", "experiments", "tests"]
bin = @["karax/tools/karun"] bin = @["karax/tools/karun"]

View file

@ -61,7 +61,7 @@ proc newname*(n: NimNode): NimNode =
n[1] = newname(n[1]) n[1] = newname(n[1])
result = n result = n
elif n.kind == nnkSym: elif n.kind == nnkSym:
result = ident(n.strVal) result = ident($n.symbol)
else: else:
result = n result = n

View file

@ -11,7 +11,4 @@ type
proc newWebSocket*(url, key: cstring): WebSocket proc newWebSocket*(url, key: cstring): WebSocket
{.importcpp: "new WebSocket(@)".} {.importcpp: "new WebSocket(@)".}
proc newWebSocket*(url: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc send*(w: WebSocket; data: cstring) {.importcpp.} proc send*(w: WebSocket; data: cstring) {.importcpp.}

View file

@ -4,66 +4,6 @@
## handling. ## handling.
import karax import karax
import jsffi except `&`
import jscore
import dom
type
ProgressEvent* {.importc.}= object
loaded*: float
total*: float
FormData* {.importc.} = JsObject
HttpRequest* {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
XMLHttpRequestUpload* {.importc.} = JsObject
proc newFormData*(): FormData {.importcpp: "new FormData()".}
proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
when not declared(dom.File):
type
DomFile = ref FileObj
FileObj {.importc.} = object of Blob
lastModified: int
name: cstring
else:
type
DomFile = dom.File
proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
cont: proc (httpStatus: int; response: cstring);
headers: openarray[(cstring, cstring)] = []) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
var this {.importc: "this".}: ThisObj
var formData = newFormData()
formData.append("upload_file",file)
formData.append("filename", DomFile(file).name)
let ajax = newRequest()
ajax.open("POST", url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
contWrapper(this.status, this.responseText)
ajax.upload.onprogress = onprogress
ajax.send(formData.to(cstring))
proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)]; proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring; data: cstring;
@ -76,8 +16,20 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
cont(httpStatus, response) cont(httpStatus, response)
if doRedraw: redraw(kxi) if doRedraw: redraw(kxi)
var this {.importc: "this".}: ThisObj type
HttpRequest {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
proc setRequestHeader(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc send(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
proc open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
var this {.importc: "this".}: ThisObj
let ajax = newRequest() let ajax = newRequest()
ajax.open(meth, url, true) ajax.open(meth, url, true)
for a, b in items(headers): for a, b in items(headers):
@ -129,4 +81,3 @@ proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
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

@ -7,16 +7,17 @@ export kdom.Event, kdom.Blob
when defined(nimNoNil): when defined(nimNoNil):
{.experimental: "notnil".} {.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs.} proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
## The preferred way of debugging karax applications. ## the preferred way of debugging karax applications. Now deprecated,
## you can now use ``system.echo`` instead.
type type
PatchKind = enum PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach
Patch = object Patch = object
k: PatchKind k: PatchKind
parent, current: Node parent, current: Node
newNode, oldNode: VNode n: VNode
PatchV = object PatchV = object
parent, newChild: VNode parent, newChild: VNode
pos: int pos: int
@ -30,14 +31,13 @@ type
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this. ## know about this.
rootId: cstring rootId: cstring not nil
renderer: proc (data: RouterData): VNode {.closure.} renderer: proc (data: RouterData): VNode {.closure.}
currentTree: VNode currentTree: VNode
postRenderCallback: proc (data: RouterData) postRenderCallback: proc (data: RouterData)
toFocus: Node toFocus: Node
toFocusV: VNode toFocusV: VNode
renderId: int renderId: int
rendering: bool
patches: seq[Patch] # we reuse this to save allocations patches: seq[Patch] # we reuse this to save allocations
patchLen: int patchLen: int
patchesV: seq[PatchV] patchesV: seq[PatchV]
@ -78,24 +78,6 @@ template keyeventBody() =
# Do not call redraw() here! That is already done # Do not call redraw() here! That is already done
# by ``karax.addEventHandler``. # by ``karax.addEventHandler``.
proc karaxEvents(d: Node): JSeq[(cstring, NativeEventHandler)] {.importcpp: "#.karaxEvents".}
proc `karaxEvents=`(d: Node; value: JSeq[(cstring, NativeEventHandler)]) {.importcpp: "#.karaxEvents = #".}
proc addEventShell(d: Node; name: cstring; h: NativeEventHandler) =
# The DOM is such a pathetic piece of junk that it doesn't
# offer 'removeAllEventHandlers()'. Hence we store the event
# handler twice in 'd' so that we can emulate this properly.
# This is required to fix bug #139.
d.addEventListener(name, h)
if d.karaxEvents == nil:
d.karaxEvents = newJSeq[(cstring, NativeEventHandler)]()
d.karaxEvents.add((name, h))
proc removeAllEventHandlers(d: Node) =
if d.karaxEvents != nil:
for i in 0..<d.karaxEvents.len:
d.removeEventListener(d.karaxEvents[i][0], d.karaxEvents[i][1])
proc wrapEvent(d: Node; n: VNode; k: EventKind; proc wrapEvent(d: Node; n: VNode; k: EventKind;
action: EventHandler): NativeEventHandler = action: EventHandler): NativeEventHandler =
proc stdWrapper(): NativeEventHandler = proc stdWrapper(): NativeEventHandler =
@ -124,18 +106,18 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
case k case k
of EventKind.onkeyuplater: of EventKind.onkeyuplater:
result = laterWrapper() result = laterWrapper()
d.addEventShell("keyup", result) d.addEventListener("keyup", result)
of EventKind.onkeyupenter: of EventKind.onkeyupenter:
result = enterWrapper() result = enterWrapper()
d.addEventShell("keyup", result) d.addEventListener("keyup", result)
else: else:
result = stdWrapper() result = stdWrapper()
d.addEventShell(toEventName[k], result) d.addEventListener(toEventName[k], result)
# --------------------- DOM diff ----------------------------------------- # --------------------- DOM diff -----------------------------------------
template detach(n: VNode) = template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, nil, n) addPatch(kxi, pkDetach, nil, nil, n)
template attach(n: VNode) = template attach(n: VNode) =
n.dom = result n.dom = result
@ -152,11 +134,7 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
if kxi.byId.contains(id): if kxi.byId.contains(id):
result = kxi.byId[id] result = kxi.byId[id]
proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node = proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
if useAttachedNode:
if n.dom != nil:
if n.id != nil: kxi.byId[n.id] = n
return n.dom
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result = document.createTextNode(n.text) result = document.createTextNode(n.text)
attach n attach n
@ -167,7 +145,7 @@ proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
return result return result
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 = toDom(x, useAttachedNode, kxi) result = vnodeToDom(x, kxi)
#n.key = result.key #n.key = result.key
attach n attach n
return result return result
@ -185,14 +163,14 @@ proc toDom*(n: VNode; useAttachedNode: bool; kxi: KaraxInstance = nil): Node =
x.expanded = x.renderImpl(x) x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil) # x.updatedImpl(x, nil)
assert x.expanded != nil assert x.expanded != nil
result = toDom(x.expanded, useAttachedNode, kxi) result = vnodeToDom(x.expanded, kxi)
attach n attach n
return result return result
else: else:
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, toDom(k, useAttachedNode, kxi)) 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
@ -253,7 +231,7 @@ when defined(profileKarax):
for i in low(DifferEnum)..high(DifferEnum): for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i] echo i, " value: ", a[i]
proc eq(a, b: VNode; recursive: bool): EqResult = proc eq(a, b: VNode): EqResult =
if a.kind != b.kind: if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind] when defined(profileKarax): inc reasons[deKind]
return different return different
@ -272,12 +250,9 @@ proc eq(a, b: VNode; recursive: bool): EqResult =
if a.text != b.text: return different if a.text != b.text: return different
if a.len != b.len: return different if a.len != b.len: return different
for i in 0..<a.len: for i in 0..<a.len:
if eq(a[i], b[i], recursive) == different: return different if eq(a[i], b[i]) == different: return different
elif a.kind == VNodeKind.dthunk: elif a.kind == VNodeKind.dthunk:
if a.dom == b.dom:
return identical return identical
else: # fix #119
return different
elif a.kind == VNodeKind.verbatim: elif a.kind == VNodeKind.verbatim:
if a.text != b.text: if a.text != b.text:
return different return different
@ -299,14 +274,6 @@ proc eq(a, b: VNode; recursive: bool): EqResult =
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b): if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
when defined(profileKarax): inc reasons[deSimilar] when defined(profileKarax): inc reasons[deSimilar]
return similar return similar
if recursive:
if a.len != b.len:
return different
for i in 0..<a.len:
if eq(a[i], b[i], true) != identical:
return different
# Do not test event listeners here! # Do not test event listeners here!
return result return result
@ -331,9 +298,6 @@ proc updateAttributes(newNode, oldNode: VNode) =
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) = proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom let d = oldNode.dom
if d != nil:
removeAllEventHandlers(d)
when false:
for i in 0..<oldNode.events.len: for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0] let k = oldNode.events[i][0]
let name = case k let name = case k
@ -357,18 +321,16 @@ when false:
printV(n[i], depth & " ") printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node; proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na, oldNode: VNode) = na: VNode) =
let L = kxi.patchLen let L = kxi.patchLen
if L >= kxi.patches.len: if L >= kxi.patches.len:
# allocate more space: # allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na))
newNode: na, oldNode: oldNode))
else: else:
kxi.patches[L].k = ka kxi.patches[L].k = ka
kxi.patches[L].parent = parenta kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta kxi.patches[L].current = currenta
kxi.patches[L].newNode = na kxi.patches[L].n = na
kxi.patches[L].oldNode = oldNode
inc kxi.patchLen inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) = proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
@ -382,40 +344,26 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
kxi.patchesV[L].pos = pos kxi.patchesV[L].pos = pos
inc kxi.patchLenV inc kxi.patchLenV
proc moveDom(dest, src: VNode) =
dest.dom = src.dom
src.dom = nil
if dest.id != nil:
kxi.byId[dest.id] = dest
assert dest.len == src.len
for i in 0..<dest.len:
moveDom(dest[i], src[i])
proc applyPatch(kxi: KaraxInstance) = proc applyPatch(kxi: KaraxInstance) =
for i in 0..<kxi.patchLen: for i in 0..<kxi.patchLen:
let p = kxi.patches[i] let p = kxi.patches[i]
case p.k case p.k
of pkReplace: of pkReplace:
let nn = toDom(p.newNode, useAttachedNode = true, kxi) let nn = vnodeToDom(p.n, kxi)
if p.parent == nil: if p.parent == nil:
replaceById(kxi.rootId, nn) replaceById(kxi.rootId, nn)
else: else:
if p.current.parentNode == p.parent:
p.parent.replaceChild(nn, p.current) p.parent.replaceChild(nn, p.current)
else: # fix #121
p.parent.appendChild(nn)
of pkSame:
moveDom(p.newNode, p.oldNode)
of pkRemove: of pkRemove:
p.parent.removeChild(p.current) p.parent.removeChild(p.current)
of pkAppend: of pkAppend:
let nn = toDom(p.newNode, useAttachedNode = true, kxi) let nn = vnodeToDom(p.n, kxi)
p.parent.appendChild(nn) p.parent.appendChild(nn)
of pkInsertBefore: of pkInsertBefore:
let nn = toDom(p.newNode, useAttachedNode = true, kxi) let nn = vnodeToDom(p.n, kxi)
p.parent.insertBefore(nn, p.current) p.parent.insertBefore(nn, p.current)
of pkDetach: of pkDetach:
let n = p.oldNode let n = p.n
if n.id != nil: kxi.byId.del(n.id) if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component: if n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
@ -429,38 +377,16 @@ proc applyPatch(kxi: KaraxInstance) =
assert p.newChild.dom != nil assert p.newChild.dom != nil
kxi.patchLenV = 0 kxi.patchLenV = 0
# ASSUME: We patch both the virtual DOM and the real DOM and throw away proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
# the newly produced DOM. Thus on updates like 'newNode.dom = oldNode.dom'
# are required. The only exception is when the top level node is replaced.
# Then we have to take the new virtual DOM. In fact, we trigger a full DOM
# rebuild then. However, we don't have to consider old event handlers then
# so everything stays simple.
# We also do not produce "Patch sets" anymore, everything is done as simply
# as possible. Ok, let's assume that we seek to update event handler lists:
# The new node has captures to itself or to other new nodes, never to old
# nodes! --> We cannot ever use the old VDOM, we have to use the new virtual
# DOM. For identical nodes we need to take over the .dom field from the old
# node since we don't recompute them. This must be done recursively. In
# vnodeToDom we have to check whether the 'dom' field was already set. If so,
# There is nothing to do.
# "Similar" nodes can have the opposite effect; consider
#
# AAAABAAAA
# AAAACDAAAA
#
# In this example B did change to C and 'D' is new. However, replacing B by
# CD is fine.
#
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
when defined(stats): when defined(stats):
if kxi.recursion > 100: if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false) echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode)
if oldNode.kind == VNodeKind.text: if oldNode.kind == VNodeKind.text:
echo oldNode.text echo oldNode.text
#return
#doAssert false, "overflow!"
inc kxi.recursion inc kxi.recursion
let result = eq(newNode, oldNode, false) result = eq(newNode, oldNode)
case result case result
of componentsIdentical: of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode), kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
@ -478,54 +404,90 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance) =
if newNode.events.len != 0 or oldNode.events.len != 0: if newNode.events.len != 0 or oldNode.events.len != 0:
mergeEvents(newNode, oldNode, kxi) mergeEvents(newNode, oldNode, kxi)
when false:
if oldNode.kind == VNodeKind.input or oldNode.kind == VNodeKind.textarea:
if oldNode.text != newNode.text:
oldNode.text = newNode.text
oldNode.dom.value = newNode.text
let newLength = newNode.len let newLength = newNode.len
let oldLength = oldNode.len let oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return if newLength == 0 and oldLength == 0: return result
let minLength = min(newLength, oldLength) let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind assert oldNode.kind == newNode.kind
var commonPrefix = 0 var commonPrefix = 0
let isSpecial = oldNode.kind == VNodeKind.component or
oldNode.kind == VNodeKind.vthunk or
oldNode.kind == VNodeKind.dthunk
template eqAndUpdate(a: VNode; i: int; b: VNode; j: int; info, action: untyped) =
let oldLen = kxi.patchLen
let oldLenV = kxi.patchLenV
assert i < a.len
assert j < b.len
let r = if isSpecial:
diff(a[i], b[j], parent, current, kxi)
else:
diff(a[i], b[j], current, current.childNodes[j], kxi)
case r
of identical, componentsIdentical, similar:
a[i] = b[j]
action
of usenewNode:
kxi.addPatchV(b, j, a[i])
action
# unfortunately, we need to propagate the changes upwards:
result = useNewNode
of different:
# undo what 'diff' would have done:
kxi.patchLen = oldLen
kxi.patchLenV = oldLenV
if result != different: result = r
break
# compute common prefix: # compute common prefix:
while commonPrefix < minLength: while commonPrefix < minLength:
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical: eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"):
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
inc commonPrefix inc commonPrefix
else:
break
# compute common suffix: # compute common suffix:
var oldPos = oldLength - 1 var oldPos = oldLength - 1
var newPos = newLength - 1 var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix: while oldPos >= commonPrefix and newPos >= commonPrefix:
if eq(newNode[newPos], oldNode[oldPos], true) == identical: eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"):
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
dec oldPos dec oldPos
dec newPos dec newPos
else:
break
let pos = min(oldPos, newPos) + 1 let pos = min(oldPos, newPos) + 1
# now the different children are in commonPrefix .. pos - 1: # now the different children are in commonPrefix .. pos - 1:
for i in commonPrefix..pos-1: for i in commonPrefix..pos-1:
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi) let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if r == usenewNode:
#oldNode[i] = newNode[i]
kxi.addPatchV(oldNode, i, newNode[i])
elif r != different:
newNode[i] = oldNode[i]
#else:
# result = usenewNode
if oldPos + 1 == oldLength: if oldPos + 1 == oldLength:
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i], nil) kxi.addPatch(pkAppend, current, nil, newNode[i])
result = usenewNode
else: else:
let before = current.childNodes[oldPos + 1] let before = current.childNodes[oldPos + 1]
for i in pos..newPos: for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil) kxi.addPatch(pkInsertBefore, current, before, newNode[i])
result = usenewNode
# XXX call 'attach' here? # XXX call 'attach' here?
for i in pos..oldPos: for i in pos..oldPos:
detach(oldNode[i]) detach(oldNode[i])
#doAssert i < current.childNodes.len #doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil) kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
result = usenewNode
of different: of different:
detach(oldNode) detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode, nil) kxi.addPatch(pkReplace, parent, current, newNode)
of usenewNode: doAssert(false, "eq returned usenewNode") of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats): when defined(stats):
dec kxi.recursion dec kxi.recursion
@ -555,12 +517,11 @@ proc applyComponents(kxi: KaraxInstance) =
x.renderedVersion = x.version x.renderedVersion = x.version
if oldExpanded == nil: if oldExpanded == nil:
detach(x) detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded, nil) kxi.addPatch(pkReplace, parent, current, x.expanded)
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist" echo "Component ", newNode.text, ": old expansion didn't exist"
else: else:
diff(x.expanded, oldExpanded, parent, current, kxi) let res = diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
if res == usenewNode: if res == usenewNode:
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)" echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
@ -588,7 +549,7 @@ when defined(stats):
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) = proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position]) detach(parent[position])
let current = parent.dom let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil) kxi.addPatch(pkRemove, current, current.childNodes[position], nil)
parent.delete(position) parent.delete(position)
applyPatch(kxi) applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -596,11 +557,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) = proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom let current = parent.dom
if position >= parent.len: if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid, nil) kxi.addPatch(pkAppend, current, nil, kid)
parent.add(kid) parent.add(kid)
else: else:
let before = current.childNodes[position] let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid, nil) kxi.addPatch(pkInsertBefore, current, before, kid)
parent.insert(kid, position) parent.insert(kid, position)
applyPatch(kxi) applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -608,7 +569,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) = proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom let olddom = oldNode.dom
doAssert olddom != nil doAssert olddom != nil
diff(newNode, oldNode, nil, olddom, kxi) discard diff(newNode, oldNode, nil, olddom, kxi)
# this is a bit nasty: Since we cannot patch the 'parent' of # this is a bit nasty: Since we cannot patch the 'parent' of
# the current VNode (because we don't store it at all!), we # the current VNode (because we don't store it at all!), we
# need to override the fields individually: # need to override the fields individually:
@ -622,41 +583,21 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
var onhashChange {.importc: "window.onhashchange".}: proc() var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring var hashPart {.importc: "window.location.hash".}: cstring
proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
## enforce a full redraw for the next redraw operation.
## This can be used as a temporary way to workaround DOM diffing
## problems or to avoid the DOM diffing when you already know
## it should use a completely new DOM.
## This is an experimental API.
kxi.currentTree = nil
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc dodraw(kxi: KaraxInstance) = proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return if kxi.renderer.isNil: return
kxi.renderId = 0
if kxi.rendering:
# there is a render already running, delay 1 frame
kxi.renderId = reqFrame(proc () = kxi.dodraw)
return
kxi.rendering = true
let rdata = RouterData(hashPart: hashPart) let rdata = RouterData(hashPart: hashPart)
let newtree = kxi.renderer(rdata) let newtree = kxi.renderer(rdata)
inc kxi.runCount inc kxi.runCount
newtree.id = kxi.rootId newtree.id = kxi.rootId
kxi.toFocus = nil kxi.toFocus = nil
if kxi.currentTree == nil: if kxi.currentTree == nil:
let asdom = toDom(newtree, useAttachedNode = true, kxi) let asdom = vnodeToDom(newtree, kxi)
replaceById(kxi.rootId, asdom) replaceById(kxi.rootId, asdom)
else: else:
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
let olddom = document.getElementById(kxi.rootId) let olddom = document.getElementById(kxi.rootId)
diff(newtree, kxi.currentTree, nil, olddom, kxi) discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
#kout cstring"patch len ", patches.len
when defined(profileKarax): when defined(profileKarax):
echo "<<<<<<<<<<<<<<" echo "<<<<<<<<<<<<<<"
echa reasons echa reasons
@ -675,12 +616,16 @@ proc dodraw(kxi: KaraxInstance) =
# 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 kxi.toFocus != nil: if kxi.toFocus != nil:
kxi.toFocus.focus() kxi.toFocus.focus()
kxi.rendering = false kxi.renderId = 0
when defined(stats): when defined(stats):
kxi.recursion = 0 kxi.recursion = 0
var total = 0 var total = 0
echo "depth ", depth(kxi.currentTree, total), " total ", total echo "depth ", depth(kxi.currentTree, total), " total ", total
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc redraw*(kxi: KaraxInstance = kxi) = proc redraw*(kxi: KaraxInstance = kxi) =
# we buffer redraw requests: # we buffer redraw requests:
when false: when false:
@ -768,7 +713,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc();
if not kxi.surpressRedraws: redraw(kxi) if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper) addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} = proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated.} =
## Now deprecated, instead pass a callback to ``setRenderer`` that receives ## Now deprecated, instead pass a callback to ``setRenderer`` that receives
## a ``data: RouterData`` parameter. ## a ``data: RouterData`` parameter.
proc wrapper() = proc wrapper() =
@ -800,10 +745,11 @@ proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild) parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) = proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let body = document.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
document.body.prepend(s) body.prepend(s)
redraw(kxi) redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} = proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
@ -817,18 +763,8 @@ proc setInputText*(n: VNode; s: cstring) =
n.text = s n.text = s
if n.dom != nil: n.dom.value = s if n.dom != nil: n.dom.value = s
proc getInputText*(n: VNode): cstring =
if n.dom != nil:
result = n.dom.value
proc toChecked*(checked: bool): cstring = proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil)) (if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring = proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil)) (if disabled: cstring"disabled" else: cstring(nil))
proc toSelected*(selected: bool): cstring =
(if selected: cstring"selected" else: cstring(nil))
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi)

View file

@ -11,8 +11,8 @@ const
proc getName(n: NimNode): string = proc getName(n: NimNode): string =
case n.kind case n.kind
of nnkIdent, nnkSym: of nnkIdent:
result = $n result = $n.ident
of nnkAccQuoted: of nnkAccQuoted:
result = "" result = ""
for i in 0..<n.len: for i in 0..<n.len:
@ -27,8 +27,6 @@ proc getName(n: NimNode): string =
expectKind(n, nnkIdent) expectKind(n, nnkIdent)
of nnkDotExpr: of nnkDotExpr:
result = getName(n[0]) & "." & getName(n[1]) result = getName(n[0]) & "." & getName(n[1])
of nnkOpenSymChoice, nnkClosedSymChoice:
result = getName(n[0])
else: else:
#echo repr n #echo repr n
expectKind(n, nnkIdent) expectKind(n, nnkIdent)
@ -126,8 +124,6 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1])) result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
elif key in SpecialAttrs: elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1]) result.add newDotAsgn(tmp, key, x[1])
if key == "value":
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(key, "setFocus"): elif eqIdent(key, "setFocus"):
result.add newCall(key, tmp, x[1], ident"kxi") result.add newCall(key, tmp, x[1], ident"kxi")
elif eqIdent(key, "events"): elif eqIdent(key, "events"):

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,98 +1,55 @@
## Simple tool to quickly run Karax applications. Generates the HTML
## required to run a Karax app and opens it in a browser.
import os,
strutils,
parseopt,
browsers,
times,
tables
import os, strutils, browsers,times, tables
import parseopt
import threadpool
import static_server
const const
css = """ css = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
""" """
html = """
const html = """
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>$1</title> <title>$1</title>
$2 $2
</head> </head>
<body id="body" class="site"> <body id="body">
<div id="ROOT"></div> <div id="ROOT" />
<script type="text/javascript" src="/app.js"></script> <script type="text/javascript" src="$1.js"></script>
$3
</body> </body>
</html> </html>
""" """
const websocket = """
<script type="text/javascript">
var ws = new WebSocket("ws://localhost:8080/ws");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
if(evt.data == "refresh"){
window.location.href = window.location.href
}
};
ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>
"""
proc exec(cmd: string) = proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0: if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd quit "External command failed: " & cmd
proc build(rest: string, selectedCss: string, run: bool, watch: bool) = proc build(name: string, rest: string, selectedCss: string, run: bool) =
echo("Building...") echo("Building...")
let cmd = "nim js --out:" & "app" & ".js " & rest createDir("nimcache")
if watch: exec("nim js --out:nimcache/" & name & ".js " & rest)
discard os.execShellCmd(cmd) let dest = "nimcache" / name & ".html"
else: writeFile(dest, html % [name, selectedCss])
exec cmd if run: openDefaultBrowser(dest)
let dest = "app" & ".html"
let script = if watch: websocket else: ""
writeFile(dest, html % ["app", selectedCss, script])
if run: openDefaultBrowser("http://localhost:8080")
proc watchBuild(filePath: string, selectedCss: string, rest: string) {.thread.} =
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true:
sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
var (_, _, ext) = splitFile(path)
if ext in [".scss",".sass",".less",".styl",".pcss",".postcss"]:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(rest,selectedCss, false, true)
files[path] = getLastModificationTime(path)
else:
if absolutePath(path) in [absolutePath("app" & ".js"),absolutePath("app" & ".html")]:
continue
files[path] = getLastModificationTime(path)
proc serve(){.thread.} =
serveStatic()
proc main = proc main =
var op = initOptParser() var op = initOptParser()
var rest = op.cmdLineRest var rest = op.cmdLineRest
var file = "" var file = ""
var run = false var run = false
var watch = false
var selectedCss = "" var selectedCss = ""
var watch = false
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
while true: while true:
op.next() op.next()
case op.kind case op.kind
@ -102,11 +59,8 @@ proc main =
run = true run = true
rest = rest.replace("--run ") rest = rest.replace("--run ")
of "css": of "css":
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css selectedCss = css
rest = rest.substr(rest.find(" ")) rest = rest.replace("--css ")
else: discard else: discard
of cmdShortOption: of cmdShortOption:
if op.key == "r": if op.key == "r":
@ -119,11 +73,26 @@ proc main =
of cmdEnd: break of cmdEnd: break
if file.len == 0: quit "filename expected" if file.len == 0: quit "filename expected"
if run: let name = file.splitFile.name
spawn serve() build(name, rest, selectedCss, run)
echo("after build")
if watch: if watch:
spawn watchBuild(file, selectedCss, rest) # TODO: launch http server
build(rest,selectedCss, run, watch) while true:
sync() sleep(300)
for path in walkDirRec("."):
if ".git" in path:
continue
if files.hasKey(path):
if files[path] != getLastModificationTime(path):
echo("File changed: " & path)
build(name, rest, selectedCss, run)
files[path] = getLastModificationTime(path)
else:
files[path] = getLastModificationTime(path)
main() main()

View file

@ -1 +0,0 @@
switch("threads", "on")

View file

@ -1,141 +0,0 @@
import
std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
logging, httpcore, asyncfile, asynchttpserver, tables, times]
from cgi import decodeUrl
import ws, dotenv
var logger = newConsoleLogger()
addHandler(logger)
when defined(release):
setLogFilter(lvlError)
type
RawHeaders* = seq[tuple[key, val: string]]
proc toStr(headers: RawHeaders): string =
$newHttpHeaders(headers)
proc send(request: Request, code: HttpCode, headers: RawHeaders,
body: string): Future[void] =
return request.respond(code, body, newHttpHeaders(headers))
proc statusContent(request: Request, status: HttpCode, content: string,
headers: RawHeaders): Future[void] =
try:
result = send(request, status, headers, content)
debug(" ", status, " ", toStr(headers))
except:
error("Could not send response: ", osErrorMsg(osLastError()))
proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
result = Http200
let mimes = newMimetypes()
for p in paths:
if fileExists(p):
if fpOthersRead notin getFilePermissions(p):
return Http403
let fileSize = getFileSize(p)
let extPos = searchExtPos(p)
let mimetype = mimes.getMimetype(
if extPos >= 0: p.substr(extPos + 1)
else: "")
if fileSize < 10_000_000: # 10 mb
var file = readFile(p)
var hashed = getMD5(file)
# If the user has a cached version of this file and it matches our
# version, let them use it
if req.headers.getOrDefault("If-None-Match") == hashed:
await req.statusContent(Http304, "", default(RawHeaders))
else:
await req.statusContent(Http200, file, @{
"Content-Type": mimetype,
"ETag": hashed
})
else:
let headers = @{
"Content-Type": mimetype,
"Content-Length": $fileSize
}
await req.statusContent(Http200, "", headers)
var fileStream = newFutureStream[string]("sendStaticIfExists")
var file = openAsync(p, fmRead)
# Let `readToStream` write file data into fileStream in the
# background.
asyncCheck file.readToStream(fileStream)
# The `writeFromStream` proc will complete once all the data in the
# `bodyStream` has been written to the file.
while true:
let (hasValue, value) = await fileStream.read()
if hasValue:
await req.client.send(value)
else:
break
file.close()
return
# If we get to here then no match could be found.
return Http404
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
# Find static file.
var reqPath = cgi.decodeUrl(req.url.path)
var staticDir = getEnv("staticDir") # it's assumed a relative dir
var status = Http400
var path = staticDir / reqPath
normalizePathEnd(path, false)
if dirExists(path):
status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
else:
status = await sendStaticIfExists(req, @[path])
return status
proc handleWs(req: Request) {.async.} =
var ws = await newWebSocket(req)
await ws.send("Welcome to simple echo server")
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
for path in watchedFiles:
files[path] = getLastModificationTime(path)
while ws.readyState == Open:
await sleepAsync(500)
var changed = false
for path in watchedFiles:
if files[path] != getLastModificationTime(path):
changed = true
files[path] = getLastModificationTime(path)
if changed:
await ws.send("refresh")
changed = false
proc serveStatic*() =
if fileExists("static.env"):
var env: DotEnv
env = initDotEnv(getCurrentDir(), "static.env")
env.overload()
else:
putEnv("staticDir", "assets/")
var server = newAsyncHttpServer()
proc cb(req: Request) {.gcsafe, async.} =
if req.url.path == "/ws":
await handleWs(req)
if req.url.path == "/":
await req.respond(Http200, readFile "app.html")
elif req.url.path == "/app.js":
let file = absolutePath("app" & ".js")
if not file.fileExists:
error(file, " does not exist!")
if fpUserRead notin os.getFilePermissions(file):
error("Could not read ", file, "!")
await req.respond(Http200, readFile(file))
else:
let status = await handleFileRequest(req)
if status != Http200:
await req.respond(status, "")
waitFor server.serve(Port(8080), cb)
when isMainModule:
serveStatic()

View file

@ -8,7 +8,7 @@ else:
Node* = ref object Node* = ref object
import macros, vstyles, kbase import macros, vstyles, kbase
from strutils import toUpperAscii, toLowerAscii, tokenize from strutils import toUpperAscii
type type
VNodeKind* {.pure.} = enum VNodeKind* {.pure.} = enum
@ -35,19 +35,7 @@ type
mark, ruby, rt, rp, bdi, dbo, span, br, wbr, mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object", ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map, param, video, audio, source, track, canvas, map,
area, math, area, svg, math,
# SVG elements, see: https://www.w3.org/TR/SVG2/eltindex.html
animate, animateMotion, animateTransform, circle, clipPath, defs, desc,
`discard` = "discard", ellipse, feBlend, feColorMatrix, feComponentTransfer,
feComposite, feConvolveMatrix, feDiffuseLighting, feDisplacementMap,
feDistantLight, feDropShadow, feFlood, feFuncA, feFuncB, feFuncG, feFuncR,
feGaussianBlur, feImage, feMerge, feMergeNode, feMorphology, feOffset,
fePointLight, feSpecularLighting, feSpotLight, feTile, feTurbulence,
filter, foreignObject, g, image, line, linearGradient, marker, mask,
metadata, mpath, path, pattern, polygon, polyline, radialGradient, rect,
`set` = "set", stop, svg, switch, symbol, txt = "text", textPath, tspan,
unknown, use, view,
table, caption, colgroup, col, tbody, thead, table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th, tfoot, tr, td, th,
@ -57,10 +45,6 @@ type
keygen, output, progress, meter, keygen, output, progress, meter,
details, summary, command, menu details, summary, command, menu
const selfClosing = {area, base, br, col, embed, hr, img, input,
link, meta, param, source, track, wbr}
type type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM. EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked. onclick, ## An element is clicked.
@ -105,16 +89,9 @@ type
onanimationiteration, onanimationiteration,
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)
onload, # img
ontransitioncancel,
ontransitionend,
ontransitionrun,
ontransitionstart,
onwheel # fires when the user rotates a wheel button on a pointing device.
macro buildLookupTables(): untyped = macro buildLookupTables(): untyped =
var a = newTree(nnkBracket) var a = newTree(nnkBracket)
@ -317,12 +294,6 @@ proc sameAttrs*(a, b: VNode): bool =
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) = proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
n.events.add((event, handler, nil)) n.events.add((event, handler, nil))
when kstring is cstring:
proc len(a: kstring): int =
# xxx: maybe move where kstring is defined
# without this, `n.field.len` fails on js (non web) platform
if a == nil: 0 else: a.len
template toStringAttr(field) = template toStringAttr(field) =
if n.field.len > 0: if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field result.add " " & astToStr(field) & " = " & $n.field
@ -408,8 +379,6 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result.addEscaped(n.text) result.addEscaped(n.text)
elif n.kind == VNodeKind.verbatim:
result.add(n.text)
else: else:
let kind = $n.kind let kind = $n.kind
result.add('<') result.add('<')
@ -428,17 +397,7 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
result.add("=\"") result.add("=\"")
result.addEscapedAttr(v) result.addEscapedAttr(v)
result.add('"') result.add('"')
if n.style != nil: # XXX add style to string
result.add " style=\""
for k, v in pairs(n.style):
if v.len == 0: continue
for t in tokenize($k, seps={'A' .. 'Z'}):
if t.isSep: result.add '-'
result.add toLowerAscii(t.token)
result.add ": "
result.add v
result.add "; "
result.add('"')
if n.len > 0: if n.len > 0:
result.add('>') result.add('>')
if n.len > 1: if n.len > 1:
@ -463,13 +422,8 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
result.add("</") result.add("</")
result.add(kind) result.add(kind)
result.add(">") result.add(">")
elif n.kind in selfClosing:
result.add(" />")
else: else:
result.add(">") result.add(" />")
result.add("</")
result.add(kind)
result.add(">")
proc `$`*(n: VNode): kstring = proc `$`*(n: VNode): kstring =

View file

@ -1,9 +1,5 @@
##[
see examples/hellostyle.nim
]##
import std/[macros, strutils] import macros, kbase
import kbase
when defined(js): when defined(js):
import kdom, jdict import kdom, jdict
@ -238,10 +234,7 @@ proc eq*(a, b: VStyle): bool =
if a[i] != b[i]: return false if a[i] != b[i]: return false
return true return true
proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} = proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
## inserts (a, value) in sorted order of key `a`
# worst case quadratic complexity (if given styles in reverse order), hopefully
# not a concern assuming small cardinal
var i = 0 var i = 0
while i < s.len: while i < s.len:
if s[i] == a: if s[i] == a:
@ -250,8 +243,8 @@ proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
elif s[i] > a: elif s[i] > a:
s.add "" s.add ""
s.add "" s.add ""
# insertion point here, shift all remaining pairs by 2 indexes # insertion point here:
for j in countdown(s.len-1, i+3, 2): for j in countdown(s.len-1, i, 2):
s[j] = s[j-2] s[j] = s[j-2]
s[j-1] = s[j-3] s[j-1] = s[j-3]
s[i] = a s[i] = a
@ -296,24 +289,6 @@ proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
result[] = @[] result[] = @[]
result.setAttr a, val result.setAttr a, val
proc toCss*(a: string): VStyle =
##[
See example in hellostyle.nim
Allows passing a css string directly, eg:
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
tdiv(style = "font-style: oblique; color: pink".toCss): discard
]##
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for ai in a.split(";"):
var ai = ai.strip
if ai.len == 0: continue
let aj = ai.strip.split(":", maxsplit=1)
result.setAttr(aj[0], aj[1])
when defined(js): when defined(js):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}

394
readme.md
View file

@ -1,394 +0,0 @@
![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png)
![Github Actions](https://img.shields.io/github/workflow/status/karaxnim/karax/CI?style=for-the-badge) ![GitHub issues](https://img.shields.io/github/issues-raw/karaxnim/karax?style=for-the-badge) ![GitHub](https://img.shields.io/github/license/karaxnim/karax?style=for-the-badge) ![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/karaxnim/karax?sort=semver&style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge)
# Karax
Karax is a framework for developing single page applications in Nim.
## Install
To use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).
Then you can install karax through nimble:
``nimble install karax``
## Try Karax
To try it out, run:
``cd ~/projects # Insert your favourite directory for projects``
``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``
``cd karax``
``cd examples/todoapp``
``nim js todoapp.nim``
``open todoapp.html``
``cd ../..``
``cd examples/mediaplayer``
``nim js playerapp.nim``
``open playerapp.html``
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up in the generated JavaScript code.
## 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.
## Hello World
The simplest Karax program looks like this:
```nim
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom
```
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
embeds and invokes the generated JavaScript code::
``nim c karax/tools/karun``
``karax/tools/karun -r helloworld.nim``
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
``buildHtml``:
```nim
let tmp1 = tree(VNodeKind.tdiv)
add(tmp1, text "Hello World!")
tmp1
```
(I shortened the IDs for better readability.)
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
construction so that it composes with all of Nim's control flow constructs:
```nim
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if rand(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom
```
Produces:
```nim
let tmp1 = tree(VNodeKind.tdiv)
if rand(100) <= 50:
add(tmp1, text "Hello World!")
else:
add(tmp1, text "Hello Universe")
tmp1
```
## Event model
Karax does not change the DOM's event model much, here is a program
that writes "Hello simulated universe" on a button click:
```nim
include karax / prelude
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom
```
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
string"; for the JS target that is an immutable JavaScript string) which
is preferred for efficiency on the JS target. However, on the native targets
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
construction is also avaible for the native targets (!) and the ``kstring``
abstraction helps to deal with these conflicting requirements.
Karax's DSL is quite flexible when it comes to event handlers, so the
following syntax is also supported:
```nim
include karax / prelude
from sugar import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom
```
The ``buildHtml`` macro produces this code for us:
```nim
let tmp2 = tree(VNodeKind.tdiv)
let tmp3 = tree(VNodeKind.button)
addEventHandler(tmp3, EventKind.onclick,
() => lines.add "Hello simulated universe", kxi)
add(tmp3, text "Say hello!")
add(tmp2, tmp3)
for x in lines:
let tmp4 = tree(VNodeKind.tdiv)
add(tmp4, text x)
add(tmp2, tmp4)
tmp2
```
As the examples grow larger it becomes more and more visible of what
a DSL that composes with the builtin Nim control flow constructs buys us.
Once you have tasted this power there is no going back and languages
without AST based macro system simply don't cut it anymore.
## Attaching data to an event handler
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
additional data that should be passed to the event handler needs to be
done via Nim's closures. In general this means a pattern like this:
```nim
proc menuAction(menuEntry: kstring): proc() =
result = proc() =
echo "clicked ", menuEntry
proc buildMenu(menu: seq[kstring]): VNode =
result = buildHtml(tdiv):
for m in menu:
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(m)):
```
## DOM diffing
Ok, so now we have seen DOM creation and event handlers. But how does
Karax actually keep the DOM up to date? The trick is that every event
handler is wrapped in a helper proc that triggers a *redraw* operation
that calls the *renderer* that you initially passed to ``setRenderer``.
So a new virtual DOM is created and compared against the previous
virtual DOM. This comparison produces a patch set that is then applied
to the real DOM the browser uses internally. This process is called
"virtual DOM diffing" and other frameworks, most notably Facebook's
*React*, do quite similar things. The virtual DOM is faster to create
and manipulate than the real DOM so this approach is quite efficient.
## Form validation
Most applications these days have some "login"
mechanism consisting of ``username`` and ``password`` and
a ``login`` button. The login button should only be clickable
if ``username`` and ``password`` are not empty. An error
message should be shown as long as one input field is empty.
To create new UI elements we write a ``loginField`` proc that
returns a ``VNode``:
```nim
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
```
We use the ``karax / errors`` module to help with this error
logic. The ``errors`` module is mostly a mapping from strings to
strings but it turned out that the logic is tricky enough to warrant
a library solution. ``validateNotEmpty`` returns a closure that
captures the ``field`` parameter:
```nim
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field).getInputText
if x.isNil or x == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
```
This indirection is required because
event handlers in Karax need to have the type ``proc ()``
or ``proc (ev: Event; n: VNode)``. The errors module also
gives us a handy ``disableOnError`` helper. It returns
``"disabled"`` if there are errors. Now we have all the
pieces together to write our login dialog:
```nim
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setRenderer loginDialog
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)
This code still has a bug though, when you run it, the ``login`` button is not
disabled until some input fields are validated! This is easily fixed,
at initialization we have to do:
```nim
setError username, username & " must not be empty"
setError password, password & " must not be empty"
```
There are likely more elegant solutions to this problem.
## Routing
For routing ``setRenderer`` can be called with a callback that takes a parameter of
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
```nim
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
...
setRenderer createDom
```
(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)
## Server Side HTML Rendering
Karax can also be used to render HTML on the server. Only a subset of
modules can be used since there is no JS interpreter.
```nim
import karax / [karaxdsl, vdom]
const places = @["boston", "cleveland", "los angeles", "new orleans"]
proc render*(): string =
let vnode = buildHtml(tdiv(class = "mt-3")):
h1: text "My Web Page"
p: text "Hello world"
ul:
for place in places:
li: text place
dl:
dt: text "Can I use Karax for client side single page apps?"
dd: text "Yes"
dt: text "Can I use Karax for server side HTML rendering?"
dd: text "Yes"
result = $vnode
echo render()
```
## Generate HTML with event handlers
If you are writing a static site generator or do server-side HTML rendering
via ``nim c``, you may want to override ``addEventHandler`` when using event
handlers to avoid compiler complaints.
Here's an example of auto submit a dropdown when a value is selected:
```nim
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let
names = @["nim", "c", "python"]
selected_name = request.params.getOrDefault("name")
hello = buildHtml(html):
form(`method` = "get"):
select(name="name", onchange="this.form.submit()"):
for name in names:
if name == selected_name:
option(selected = ""): text name
else:
option: text name
```
## License
MIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).

327
readme.rst Normal file
View file

@ -0,0 +1,327 @@
Karax – Single page applications in Nim |travis|
================================================
Karax is a framework for developing single page applications in Nim.
The API is reasonably stable and version 1 should arrive anytime soon
now.
To try it out, run::
cd ~/projects # Insert your favourite directory for projects
nimble develop karax # This will clone Karax and create a link to it in ~/.nimble
cd karax
cd examples/todoapp
nim js todoapp.nim
open todoapp.html
cd ../..
cd examples/mediaplayer
nim js playerapp.nim
open playerapp.html
It uses a virtual DOM like React, but is much smaller than the existing
frameworks plus of course it's written in Nim for Nim. No external
dependencies! And thanks to Nim's whole program optimization only what
is used ends up in the generated JavaScript code.
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
Hello World
===========
The simplest Karax program looks like this:
.. code-block:: nim
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom
Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
here. ``tdiv`` produces a ``<div>`` virtual DOM node.
As you can see, karax comes with its own ``buildHtml`` DSL for convenient
construction of (virtual) DOM trees (of type ``VNode``). Karax provides
a tiny build tool called ``karun`` that generates the HTML boilerplate code that
embeds and invokes the generated JavaScript code::
nim c karax/tools/karun
karax/tools/karun -r helloworld.nim
Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
``buildHtml``:
.. code-block:: nim
let tmp1 = tree(VNodeKind.tdiv)
add(tmp1, text "Hello World!")
tmp1
(I shortened the IDs for better readability.)
Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
construction so that it composes with all of Nim's control flow constructs:
.. code-block:: nim
include karax / prelude
import random
proc createDom(): VNode =
result = buildHtml(tdiv):
if random(100) <= 50:
text "Hello World!"
else:
text "Hello Universe"
randomize()
setRenderer createDom
Produces:
.. code-block:: nim
let tmp1 = tree(VNodeKind.tdiv)
if random(100) <= 50:
add(tmp1, text "Hello World!")
else:
add(tmp1, text "Hello Universe")
tmp1
Event model
===========
Karax does not change the DOM's event model much, here is a program
that writes "Hello simulated universe" on a button click:
.. code-block:: nim
include karax / prelude
# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button:
text "Say hello!"
proc onclick(ev: Event; n: VNode) =
lines.add "Hello simulated universe"
for x in lines:
tdiv:
text x
setRenderer createDom
``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
string"; for the JS target that is an immutable JavaScript string) which
is preferred for efficiency on the JS target. However, on the native targets
``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
construction is also avaible for the native targets (!) and the ``kstring``
abstraction helps to deal with these conflicting requirements.
Karax's DSL is quite flexible when it comes to event handlers, so the
following syntax is also supported:
.. code-block:: nim
include karax / prelude
from sugar import `=>`
var lines: seq[kstring] = @[]
proc createDom(): VNode =
result = buildHtml(tdiv):
button(onclick = () => lines.add "Hello simulated universe"):
text "Say hello!"
for x in lines:
tdiv:
text x
setRenderer createDom
The ``buildHtml`` macro produces this code for us:
.. code-block:: nim
let tmp2 = tree(VNodeKind.tdiv)
let tmp3 = tree(VNodeKind.button)
addEventHandler(tmp3, EventKind.onclick,
() => lines.add "Hello simulated universe", kxi)
add(tmp3, text "Say hello!")
add(tmp2, tmp3)
for x in lines:
let tmp4 = tree(VNodeKind.tdiv)
add(tmp4, text x)
add(tmp2, tmp4)
tmp2
As the examples grow larger it becomes more and more visible of what
a DSL that composes with the builtin Nim control flow constructs buys us.
Once you have tasted this power there is no going back and languages
without AST based macro system simply don't cut it anymore.
Attaching data to an event handler
==================================
Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
additional data that should be passed to the event handler needs to be
done via Nim's closures. In general this means a pattern like this:
.. code-block:: nim
proc menuAction(menuEntry: kstring): proc() =
result = proc() =
echo "clicked ", menuEntry
proc buildMenu(menu: seq[kstring]): VNode =
result = buildHtml(tdiv):
for m in menu:
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(m)):
DOM diffing
===========
Ok, so now we have seen DOM creation and event handlers. But how does
Karax actually keep the DOM up to date? The trick is that every event
handler is wrapped in a helper proc that triggers a *redraw* operation
that calls the *renderer* that you initially passed to ``setRenderer``.
So a new virtual DOM is created and compared against the previous
virtual DOM. This comparison produces a patch set that is then applied
to the real DOM the browser uses internally. This process is called
"virtual DOM diffing" and other frameworks, most notably Facebook's
*React*, do quite similar things. The virtual DOM is faster to create
and manipulate than the real DOM so this approach is quite efficient.
Form validation
===============
Most applications these days have some "login"
mechanism consisting of ``username`` and ``password`` and
a ``login`` button. The login button should only be clickable
if ``username`` and ``password`` are not empty. An error
message should be shown as long as one input field is empty.
To create new UI elements we write a ``loginField`` proc that
returns a ``VNode``:
.. code-block:: nim
proc loginField(desc, field, class: kstring;
validator: proc (field: kstring): proc ()): VNode =
result = buildHtml(tdiv):
label(`for` = field):
text desc
input(class = class, id = field, onchange = validator(field))
We use the ``karax / errors`` module to help with this error
logic. The ``errors`` module is mostly a mapping from strings to
strings but it turned out that the logic is tricky enough to warrant
a library solution. ``validateNotEmpty`` returns a closure that
captures the ``field`` parameter:
.. code-block:: nim
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text.isNil or x.text == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
This indirection is required because
event handlers in Karax need to have the type ``proc ()``
or ``proc (ev: Event; n: VNode)``. The errors module also
gives us a handy ``disableOnError`` helper. It returns
``"disabled"`` if there are errors. Now we have all the
pieces together to write our login dialog:
.. code-block:: nim
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
var loggedIn: bool
proc loginDialog(): VNode =
result = buildHtml(tdiv):
if not loggedIn:
loginField("Name :", username, "input", validateNotEmpty)
loginField("Password: ", password, "password", validateNotEmpty)
button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
text "Login"
p:
text errors.getError(username)
p:
text errors.getError(password)
else:
p:
text "You are now logged in."
setRenderer loginDialog
(Full example `here <https://github.com/pragmagic/karax/blob/master/examples/login.nim>`_.)
This code still has a bug though, when you run it, the ``login`` button is not
disabled until some input fields are validated! This is easily fixed,
at initialization we have to do:
.. code-block:: nim
setError username, username & " must not be empty"
setError password, password & " must not be empty"
There are likely more elegant solutions to this problem.
Routing
=======
For routing ``setRenderer`` can be called with a callback that takes a parameter of
type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
.. code-block:: nim
proc createDom(data: RouterData): VNode =
if data.hashPart == "#/": filter = all
elif data.hashPart == "#/completed": filter = completed
elif data.hashPart == "#/active": filter = active
result = buildHtml(tdiv(class="todomvc-wrapper")):
section(class = "todoapp"):
...
setRenderer createDom
(Full example `here <https://github.com/pragmagic/karax/blob/master/examples/todoapp/todoapp.nim>`_.)

View file

@ -9,7 +9,7 @@
<div id="ROOT" /> <div id="ROOT" />
<script type="text/javascript" src="diffDomTests.js"></script> <script type="text/javascript" src="nimcache/diffDomTests.js"></script>
<script type="text/javascript"> <script type="text/javascript">
onload(window.location.pathname.split('/')[1]) onload(window.location.pathname.split('/')[1])
</script> </script>

View file

@ -22,19 +22,15 @@ proc shortRepr(n: VNode): string =
var err = 0 var err = 0
proc doDiff(a, b: VNode; expected: varargs[string]) = proc doDiff(a, b: VNode; expected: varargs[string]) =
diff(b, a, nil, vnodeToDom(a, kxi), kxi) discard diff(b, a, nil, vnodeToDom(a, kxi), kxi)
var j = 0
for i in 0..<kxi.patchLen: for i in 0..<kxi.patchLen:
if kxi.patches[i].k != pkSame: let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n)
let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode if i >= expected.len:
let p = $kxi.patches[i].k & " " & shortRepr(n)
if j >= expected.len:
echo "patches differ; expected nothing but got: ", p echo "patches differ; expected nothing but got: ", p
inc err inc err
elif p != expected[j]: elif p != expected[i]:
echo "patches differ; expected ", expected[i], " but got: ", p echo "patches differ; expected ", expected[i], " but got: ", p
inc err inc err
inc j
#hasDom(kxi.currentTree) #hasDom(kxi.currentTree)
kxi.patchLen = 0 kxi.patchLen = 0

View file

@ -8,6 +8,6 @@
</head> </head>
<body id="body"> <body id="body">
<div id="ROOT"></div> <div id="ROOT"></div>
<script src="domEventTests.js" type="text/javascript"></script> <script src="nimcache/domEventTests.js" type="text/javascript"></script>
</body> </body>
</html> </html>

View file

@ -5,10 +5,6 @@ import "../karax" / [karaxdsl, vdom]
when defined(js): when defined(js):
{.error: "Use 'nim c' to compile this example".} {.error: "Use 'nim c' to compile this example".}
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let tab = buildHtml(table): let tab = buildHtml(table):
tr: tr:
td: td:
@ -20,8 +16,5 @@ let tab = buildHtml(table):
text "Cell C" text "Cell C"
td: td:
text "Cell D" text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab echo tab