Compare commits

...

69 commits

Author SHA1 Message Date
geotre
85c81d8c0e
Fix issue #195 (#196) 2021-05-15 21:22:56 +02:00
geotre
6d51d7d08c
Fix issue #192 (#197) 2021-05-15 21:22:33 +02:00
Arham Jain
93d9b73867
Moved CI from Travis to Github Actions (#198)
* Create ci.yml

* removed travis build file

* changed docker image

* Update readme to include CI results
2021-05-06 22:56:55 +02:00
Andreas Rumpf
ac210fb634
Update readme.md 2021-05-06 17:57:48 +02:00
Willyboar
df828e145d
beautify README (#194)
* beautify README

* Fix small issues

* Revert "Fix small issues"

This reverts commit 2a480addd7e2a55faaa95dd2145a6dc0aa4b95f7.

* Small Fixes vol.2

* Fix licence link

Co-authored-by: codewski <devon@codewski.com>
Co-authored-by: ΝΙΚΟΣ <nikos@Macintosh.local>
2021-05-06 17:57:08 +02:00
Arham Jain
cedd39a65f
Remove kout deprecation (#193) 2021-05-03 10:26:15 +02:00
Matt Haggard
12b98b2338
Make 'karun -w -r ...' do watching for more than one iteration (#190) 2021-03-31 11:43:43 +02:00
flywind
8a7f05ac2a
Update readme.rst (#184) 2021-02-09 11:55:46 +01:00
Antonis Geralis
0fca6e961d
add all svg elements acording to the standard (#178) 2021-02-04 19:24:19 +01:00
Double_oxygeN
32de202845
add picture tag as a kind of virtual DOM (#174) 2021-01-25 07:51:24 +01:00
Antonis Geralis
6cff2d8b05
Refactor static_server (#169) 2021-01-08 17:07:19 +01:00
Araq
4c06cb3204 1.2.1 2020-12-08 18:03:14 +01:00
Araq
9a627985d0 added a getInputText API, cleaner and more robust than the .text attribute which is often updated too late in event handlers after a redraw 2020-12-08 18:02:59 +01:00
Constantine Molchanov
24a037ad19
Get rid of annoying deprecation warnings (#164)
* Get rid of annoying deprecation warnings

Changed existsFile to fileExists and existsDir to dirExists.

* Install packages during nimble develop
2020-11-25 11:28:12 +01:00
Xie Yanbo
40daae64e3
fix typo (#163) 2020-11-02 08:17:27 +01:00
Xie Yanbo
d3fc084c42
added documentation on rendering to string when using event handlers (#162) 2020-10-30 10:44:38 +01:00
Demian Florentin
bcab551f05
onwheel event added (#161) 2020-10-22 11:38:22 +02:00
Timothee Cour
04779f8502
add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss) (#158)
* add toCss: allows tdiv(style = "font-style: oblique; color: pink".toCss)

* fixup to work with nim c
2020-10-07 12:23:16 +02:00
Timothee Cour
bd18fc980c
fix #153 (#159) 2020-10-01 22:13:54 +02:00
Araq
5a2ea1eef9 1.1.3 2020-10-01 19:56:30 +02:00
Timothee Cour
d2f88b4648
close #6 (#157) 2020-10-01 19:35:25 +02:00
Timothee Cour
d8fe79fa40
make vstyles.setAttr kstring overload public (#156) 2020-10-01 09:28:42 +02:00
OctoCat
f0dd16ebfe
Fixed the error with password (#155)
* Fixed the error with password

* Changed to old style
2020-09-28 08:46:23 +02:00
Bung
0813a7adf9
add transition events (#151) 2020-08-26 18:03:52 +02:00
Bung
7a36b19729
watch ignore precompile style lang (#150) 2020-07-31 17:54:05 +02:00
Bung
98a2d1ce50
add static server,websocket refresh watching mode (#149)
* add static server,websocket refresh watching mode

* watch optional

* we dont quit when building error in watch mode

* staticDir,publicUrl configure through dot env

* remove unused import

* serve optional
2020-07-23 10:40:04 +02:00
tarunama
22e2e226cb
Refactor examples html, nim files. (#147)
* refactor: remove no need first new line

* arrange: add indent in HTML files

Co-authored-by: Takuya Yamamoto <takuya.yamamoto@tabian.co>
2020-07-02 10:36:20 +02:00
dawkot
f1cd9e606d
Added toSelected (#145)
Co-authored-by: dawkot <dawkot@github.com>
2020-05-02 13:42:53 +02:00
Miran
80f500874b
fix deprecated stuff (#144) 2020-04-27 10:59:54 +02:00
Araq
5f21dcdc56 1.1.2 2020-04-12 23:41:55 +02:00
Araq
02038d40c1 fixes #141 2020-04-12 23:41:24 +02:00
Araq
6b753009ac 1.1.1 2020-04-12 00:25:05 +02:00
Araq
d7da2a5bd5 fixes #139 2020-04-12 00:22:46 +02:00
Araq
c00d7dc340 Merge branch 'master' of github.com:pragmagic/karax 2020-04-01 17:07:00 +02:00
Araq
7f061de317 prepare for Nim version 1.2 2020-04-01 17:06:53 +02:00
hitoshihrm
0a7136b8fc
3 tiny changes for "All Done" button. (#136)
1."onAllDone" callback fixation.
Because the callback deletes all tasks instead of making them done, fixed.

2.Retouching checkbox and label property.
Because a checkbox didn't work and label did'nt affect the checkbox, I set
the callback on checkbox, and rename label's "for" property.

3.Adding Global var "doneswitch" for convinience.
I guess the button should be able to make "All Done" and "All Undone"
because the checkbox is not a button. So make it done, undone button
interfaced with the checkbox's display.
2020-03-12 09:23:07 +01:00
Bùi Thành Nhân
7440393d3a
Self-closing tags (#133) (#134) 2020-03-04 10:02:03 +01:00
Jairo
f6bda9ae89 Reactive value (#129)
Change proposed in https://github.com/pragmagic/karax/issues/61#issuecomment-522233192
2019-11-12 10:18:08 +01:00
Jairo
1911a1cfe0 Update karun.nim (#124)
Add common meta tag for Mobile first development
2019-10-10 17:23:45 +02:00
Timothee Cour
db4acbf7d6 fixes #121 (#122) 2019-09-30 09:01:07 +02:00
Andreas Rumpf
6a0c2871ad added an experimental avoidDomDiffing API; refs #121 2019-09-28 23:25:53 +02:00
Andreas Rumpf
9cf08c4f9c fixes a critical event handling regression 2019-09-27 23:04:17 +02:00
Araq
983cc8882b fixes #101 2019-09-26 21:19:15 +02:00
Araq
8dfb8727cd fixes #113 2019-09-26 21:16:11 +02:00
Araq
9396b3e69d Merge branch 'master' of github.com:pragmagic/karax 2019-09-26 21:01:54 +02:00
Timothee Cour
e6877a5198 fixes #119 (#120) 2019-09-21 23:18:05 +02:00
Timothee Cour
7a7cdd8e76 add onload (#116) 2019-09-21 23:17:41 +02:00
Dmitry Matveyev
023f5d2463 Fix typos and add small clarification to docs/docs.rst (#115) 2019-09-13 11:05:28 +02:00
Dmitry Matveyev
65e305e6e9 Fix script src path for html files to be up to date with readme (#114) 2019-09-09 11:15:19 +02:00
Araq
05766db4b6 Merge branch 'master' of github.com:pragmagic/karax 2019-09-02 12:33:23 +02:00
Araq
e422087217 avoid exponential DOM diffing 2019-08-28 19:38:20 +02:00
Araq
d08a0356d2 readme.rst: fixed a typo and made the snippet runnable 2019-08-28 19:38:20 +02:00
Araq
9c8a34e567 readme.rst: fixed a typo and made the snippet runnable 2019-08-15 11:07:03 +02:00
Jairo
1bbd9c694e Update karun.nim (#109)
Remove ugly debug echo (sorry)
2019-07-19 18:38:43 +02:00
Zed
b99a5430f0 Add support for rendering styles to string (#102) 2019-07-10 14:46:08 +02:00
Zed
fc062c2d48 Fix tag termination for elements with no children (#103) 2019-07-10 14:44:51 +02:00
Araq
5e9709bc5b fixes #91 2019-06-03 14:59:51 +02:00
Jairo
c091af1622 Update kdom_impl.nim (#94)
* Update kdom_impl.nim

Support full old syntax (maybe support modern syntax too? object options)
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

* Update kdom_impl.nim

use MDN naming

* Update kdom_impl.nim
2019-05-15 19:06:43 +02:00
Hitesh Jasani
33af725e7d Add docs explaining server side HTML rendering (#97)
* Add docs explaining server side HTML rendering

* Fix indentation
2019-05-15 19:06:17 +02:00
Araq
cd3e7cc1c8 fixed indentation 2019-05-15 18:48:38 +02:00
liuxiaodong
ad3c0a63ee update verbatim for static HTML (#96) 2019-05-15 18:36:22 +02:00
Andreas Rumpf
20fe35355c server side Karax rendering: show how events can be handled 2019-03-14 11:21:49 +01:00
Andreas Rumpf
50bed1f6e9 used sugar.nim instead of future.nim in the login.nim example 2019-03-14 11:09:06 +01:00
liuxiaodong
1ea7208b4d add uploadFile and a path tag (#88)
* add uploadFile and a path tag
* update uploadFile and circle tag
2019-03-10 20:15:13 +01:00
Jairo
4186a4b37b Update Bulma (#89) 2019-03-08 13:32:35 +01:00
Araq
0118aea506 use stdlib's dom module if possible 2019-01-31 21:28:31 +01:00
Araq
2936245d39 add newWebsocket variant that lacks the key 2019-01-31 16:16:37 +01:00
Araq
fc9f9312ef improve deprecation message 2019-01-31 16:15:03 +01:00
Araq
c8c7b13d2f update readme.rst, v1 has arrived 2018-12-12 15:18:36 +01:00
37 changed files with 2306 additions and 1827 deletions

31
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,31 @@
# 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,3 +3,5 @@ nimcache/
karax/tools/karun
*.code-workspace
*.exe
app.js
app.html

View file

@ -1,20 +0,0 @@
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:
``VNodeKind.int``
The node havs a single integer field.
The node has a single integer field.
``VNodeKind.bool``
The node has a single boolean field.
``VNodeKint.vthunk``
``VNodeKind.vthunk``
The node is a `virtual thunk`:idx:. This means there is a
function attached to it that produces the ``VNode`` structure
on demand.
``VNodeKint.dthunk``
``VNodeKind.dthunk``
The node is a `DOM thunk`:idx:. This means there is a
function attached to it that produces the ``Node`` DOM structure
on demand.
@ -53,6 +53,6 @@ Event system
Karax does not abstract over the event system the DOM offers much: The same
``Event`` structure is used. Every callback has the
signature ``proc (ev: Event; n: VNode)``.
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.

View file

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

View file

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

17
examples/hellostyle.nim Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,5 +10,5 @@
<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="nimcache/echartstest.js"></script>
<script type="text/javascript" src="echartstest.js"></script>
</html>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,66 @@
## handling.
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)];
data: cstring;
@ -16,20 +76,8 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
cont(httpStatus, response)
if doRedraw: redraw(kxi)
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()
ajax.open(meth, url, true)
for a, b in items(headers):
@ -81,3 +129,4 @@ proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -7,17 +7,16 @@ export kdom.Event, kdom.Blob
when defined(nimNoNil):
{.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
## the preferred way of debugging karax applications. Now deprecated,
## you can now use ``system.echo`` instead.
proc kout*[T](x: T) {.importc: "console.log", varargs.}
## The preferred way of debugging karax applications.
type
PatchKind = enum
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
Patch = object
k: PatchKind
parent, current: Node
n: VNode
newNode, oldNode: VNode
PatchV = object
parent, newChild: VNode
pos: int
@ -31,13 +30,14 @@ type
KaraxInstance* = ref object ## underlying karax instance. Usually you don't have
## know about this.
rootId: cstring not nil
rootId: cstring
renderer: proc (data: RouterData): VNode {.closure.}
currentTree: VNode
postRenderCallback: proc (data: RouterData)
toFocus: Node
toFocusV: VNode
renderId: int
rendering: bool
patches: seq[Patch] # we reuse this to save allocations
patchLen: int
patchesV: seq[PatchV]
@ -78,6 +78,24 @@ template keyeventBody() =
# Do not call redraw() here! That is already done
# 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;
action: EventHandler): NativeEventHandler =
proc stdWrapper(): NativeEventHandler =
@ -106,18 +124,18 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
case k
of EventKind.onkeyuplater:
result = laterWrapper()
d.addEventListener("keyup", result)
d.addEventShell("keyup", result)
of EventKind.onkeyupenter:
result = enterWrapper()
d.addEventListener("keyup", result)
d.addEventShell("keyup", result)
else:
result = stdWrapper()
d.addEventListener(toEventName[k], result)
d.addEventShell(toEventName[k], result)
# --------------------- DOM diff -----------------------------------------
template detach(n: VNode) =
addPatch(kxi, pkDetach, nil, nil, n)
addPatch(kxi, pkDetach, nil, nil, nil, n)
template attach(n: VNode) =
n.dom = result
@ -134,7 +152,11 @@ proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
if kxi.byId.contains(id):
result = kxi.byId[id]
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
proc toDom*(n: VNode; useAttachedNode: bool; 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:
result = document.createTextNode(n.text)
attach n
@ -145,7 +167,7 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
return result
elif n.kind == VNodeKind.vthunk:
let x = callThunk(vcomponents[n.text], n)
result = vnodeToDom(x, kxi)
result = toDom(x, useAttachedNode, kxi)
#n.key = result.key
attach n
return result
@ -163,14 +185,14 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
x.expanded = x.renderImpl(x)
# x.updatedImpl(x, nil)
assert x.expanded != nil
result = vnodeToDom(x.expanded, kxi)
result = toDom(x.expanded, useAttachedNode, kxi)
attach n
return result
else:
result = document.createElement(toTag[n.kind])
attach n
for k in n:
appendChild(result, vnodeToDom(k, kxi))
appendChild(result, toDom(k, useAttachedNode, kxi))
# text is mapped to 'value':
if n.text != nil:
result.value = n.text
@ -231,7 +253,7 @@ when defined(profileKarax):
for i in low(DifferEnum)..high(DifferEnum):
echo i, " value: ", a[i]
proc eq(a, b: VNode): EqResult =
proc eq(a, b: VNode; recursive: bool): EqResult =
if a.kind != b.kind:
when defined(profileKarax): inc reasons[deKind]
return different
@ -250,9 +272,12 @@ proc eq(a, b: VNode): EqResult =
if a.text != b.text: return different
if a.len != b.len: return different
for i in 0..<a.len:
if eq(a[i], b[i]) == different: return different
if eq(a[i], b[i], recursive) == different: return different
elif a.kind == VNodeKind.dthunk:
return identical
if a.dom == b.dom:
return identical
else: # fix #119
return different
elif a.kind == VNodeKind.verbatim:
if a.text != b.text:
return different
@ -274,6 +299,14 @@ proc eq(a, b: VNode): EqResult =
if a.class != b.class or not eq(a.style, b.style) or not sameAttrs(a, b):
when defined(profileKarax): inc reasons[deSimilar]
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!
return result
@ -298,12 +331,15 @@ proc updateAttributes(newNode, oldNode: VNode) =
proc mergeEvents(newNode, oldNode: VNode; kxi: KaraxInstance) =
let d = oldNode.dom
for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0]
let name = case k
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
else: toEventName[k]
d.removeEventListener(name, oldNode.events[i][2])
if d != nil:
removeAllEventHandlers(d)
when false:
for i in 0..<oldNode.events.len:
let k = oldNode.events[i][0]
let name = case k
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
else: toEventName[k]
d.removeEventListener(name, oldNode.events[i][2])
shallowCopy(oldNode.events, newNode.events)
applyEvents(oldNode)
@ -321,16 +357,18 @@ when false:
printV(n[i], depth & " ")
proc addPatch(kxi: KaraxInstance; ka: PatchKind; parenta, currenta: Node;
na: VNode) =
na, oldNode: VNode) =
let L = kxi.patchLen
if L >= kxi.patches.len:
# allocate more space:
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta, n: na))
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
newNode: na, oldNode: oldNode))
else:
kxi.patches[L].k = ka
kxi.patches[L].parent = parenta
kxi.patches[L].current = currenta
kxi.patches[L].n = na
kxi.patches[L].newNode = na
kxi.patches[L].oldNode = oldNode
inc kxi.patchLen
proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
@ -344,26 +382,40 @@ proc addPatchV(kxi: KaraxInstance; parent: VNode; pos: int; newChild: VNode) =
kxi.patchesV[L].pos = pos
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) =
for i in 0..<kxi.patchLen:
let p = kxi.patches[i]
case p.k
of pkReplace:
let nn = vnodeToDom(p.n, kxi)
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
if p.parent == nil:
replaceById(kxi.rootId, nn)
else:
p.parent.replaceChild(nn, p.current)
if p.current.parentNode == p.parent:
p.parent.replaceChild(nn, p.current)
else: # fix #121
p.parent.appendChild(nn)
of pkSame:
moveDom(p.newNode, p.oldNode)
of pkRemove:
p.parent.removeChild(p.current)
of pkAppend:
let nn = vnodeToDom(p.n, kxi)
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.appendChild(nn)
of pkInsertBefore:
let nn = vnodeToDom(p.n, kxi)
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
p.parent.insertBefore(nn, p.current)
of pkDetach:
let n = p.n
let n = p.oldNode
if n.id != nil: kxi.byId.del(n.id)
if n.kind == VNodeKind.component:
let x = VComponent(n)
@ -377,16 +429,38 @@ proc applyPatch(kxi: KaraxInstance) =
assert p.newChild.dom != nil
kxi.patchLenV = 0
proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): EqResult =
# ASSUME: We patch both the virtual DOM and the real DOM and throw away
# 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):
if kxi.recursion > 100:
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode)
echo "newNode ", newNode.kind, " oldNode ", oldNode.kind, " eq ", eq(newNode, oldNode, false)
if oldNode.kind == VNodeKind.text:
echo oldNode.text
#return
#doAssert false, "overflow!"
inc kxi.recursion
result = eq(newNode, oldNode)
let result = eq(newNode, oldNode, false)
case result
of componentsIdentical:
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
@ -404,90 +478,54 @@ proc diff(newNode, oldNode: VNode; parent, current: Node; kxi: KaraxInstance): E
if newNode.events.len != 0 or oldNode.events.len != 0:
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 oldLength = oldNode.len
if newLength == 0 and oldLength == 0: return result
if newLength == 0 and oldLength == 0: return
let minLength = min(newLength, oldLength)
assert oldNode.kind == newNode.kind
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:
while commonPrefix < minLength:
eqAndUpdate(newNode, commonPrefix, oldNode, commonPrefix, cstring"prefix"):
if eq(newNode[commonPrefix], oldNode[commonPrefix], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[commonPrefix], oldNode[commonPrefix])
inc commonPrefix
else:
break
# compute common suffix:
var oldPos = oldLength - 1
var newPos = newLength - 1
while oldPos >= commonPrefix and newPos >= commonPrefix:
eqAndUpdate(newNode, newPos, oldNode, oldPos, cstring"suffix"):
if eq(newNode[newPos], oldNode[oldPos], true) == identical:
kxi.addPatch(pkSame, nil, nil, newNode[newPos], oldNode[oldPos])
dec oldPos
dec newPos
else:
break
let pos = min(oldPos, newPos) + 1
# now the different children are in commonPrefix .. pos - 1:
for i in commonPrefix..pos-1:
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
diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
if oldPos + 1 == oldLength:
for i in pos..newPos:
kxi.addPatch(pkAppend, current, nil, newNode[i])
result = usenewNode
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
else:
let before = current.childNodes[oldPos + 1]
for i in pos..newPos:
kxi.addPatch(pkInsertBefore, current, before, newNode[i])
result = usenewNode
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
# XXX call 'attach' here?
for i in pos..oldPos:
detach(oldNode[i])
#doAssert i < current.childNodes.len
kxi.addPatch(pkRemove, current, current.childNodes[i], nil)
result = usenewNode
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
of different:
detach(oldNode)
kxi.addPatch(pkReplace, parent, current, newNode)
kxi.addPatch(pkReplace, parent, current, newNode, nil)
of usenewNode: doAssert(false, "eq returned usenewNode")
when defined(stats):
dec kxi.recursion
@ -517,24 +555,25 @@ proc applyComponents(kxi: KaraxInstance) =
x.renderedVersion = x.version
if oldExpanded == nil:
detach(x)
kxi.addPatch(pkReplace, parent, current, x.expanded)
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
when defined(karaxDebug):
echo "Component ", newNode.text, ": old expansion didn't exist"
else:
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
if res == usenewNode:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
discard "diff created a patchset for us, so this is fine"
elif res != different:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil"
else:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
assert x.expanded.dom != nil, "expanded.dom is nil"
diff(x.expanded, oldExpanded, parent, current, kxi)
when false:
if res == usenewNode:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
discard "diff created a patchset for us, so this is fine"
elif res != different:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
x.expanded = oldExpanded
assert oldExpanded.dom != nil, "old expanded.dom is nil"
else:
when defined(karaxDebug):
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
assert x.expanded.dom != nil, "expanded.dom is nil"
inc i
setLen(kxi.components, 0)
@ -549,7 +588,7 @@ when defined(stats):
proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
detach(parent[position])
let current = parent.dom
kxi.addPatch(pkRemove, current, current.childNodes[position], nil)
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, nil)
parent.delete(position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -557,11 +596,11 @@ proc runDel*(kxi: KaraxInstance; parent: VNode; position: int) =
proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
let current = parent.dom
if position >= parent.len:
kxi.addPatch(pkAppend, current, nil, kid)
kxi.addPatch(pkAppend, current, nil, kid, nil)
parent.add(kid)
else:
let before = current.childNodes[position]
kxi.addPatch(pkInsertBefore, current, before, kid)
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
parent.insert(kid, position)
applyPatch(kxi)
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
@ -569,7 +608,7 @@ proc runIns*(kxi: KaraxInstance; parent, kid: VNode; position: int) =
proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
let olddom = oldNode.dom
doAssert olddom != nil
discard diff(newNode, oldNode, nil, olddom, kxi)
diff(newNode, oldNode, nil, olddom, kxi)
# this is a bit nasty: Since we cannot patch the 'parent' of
# the current VNode (because we don't store it at all!), we
# need to override the fields individually:
@ -583,21 +622,41 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
var onhashChange {.importc: "window.onhashchange".}: proc()
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) =
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 newtree = kxi.renderer(rdata)
inc kxi.runCount
newtree.id = kxi.rootId
kxi.toFocus = nil
if kxi.currentTree == nil:
let asdom = vnodeToDom(newtree, kxi)
let asdom = toDom(newtree, useAttachedNode = true, kxi)
replaceById(kxi.rootId, asdom)
else:
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
let olddom = document.getElementById(kxi.rootId)
discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
#kout cstring"patch len ", patches.len
diff(newtree, kxi.currentTree, nil, olddom, kxi)
when defined(profileKarax):
echo "<<<<<<<<<<<<<<"
echa reasons
@ -616,16 +675,12 @@ proc dodraw(kxi: KaraxInstance) =
# now that it's part of the DOM, give it the focus:
if kxi.toFocus != nil:
kxi.toFocus.focus()
kxi.renderId = 0
kxi.rendering = false
when defined(stats):
kxi.recursion = 0
var total = 0
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) =
# we buffer redraw requests:
when false:
@ -713,7 +768,7 @@ proc addEventHandler*(n: VNode; k: EventKind; action: proc();
if not kxi.surpressRedraws: redraw(kxi)
addEventListener(n, k, wrapper)
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated.} =
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
## Now deprecated, instead pass a callback to ``setRenderer`` that receives
## a ``data: RouterData`` parameter.
proc wrapper() =
@ -745,11 +800,10 @@ proc prepend(parent, kid: Element) =
parent.insertBefore(kid, parent.firstChild)
proc loadScript*(jsfilename: cstring; kxi: KaraxInstance = kxi) =
let body = document.getElementById("body")
let s = document.createElement("script")
s.setAttr "type", "text/javascript"
s.setAttr "src", jsfilename
body.prepend(s)
document.body.prepend(s)
redraw(kxi)
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
@ -763,8 +817,18 @@ proc setInputText*(n: VNode; s: cstring) =
n.text = 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 =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(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 =
case n.kind
of nnkIdent:
result = $n.ident
of nnkIdent, nnkSym:
result = $n
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
@ -27,6 +27,8 @@ proc getName(n: NimNode): string =
expectKind(n, nnkIdent)
of nnkDotExpr:
result = getName(n[0]) & "." & getName(n[1])
of nnkOpenSymChoice, nnkClosedSymChoice:
result = getName(n[0])
else:
#echo repr n
expectKind(n, nnkIdent)
@ -124,6 +126,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result.add newDotAsgn(tmp, key, newCall("style", toKstring x[1]))
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
if key == "value":
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(key, tmp, x[1], ident"kxi")
elif eqIdent(key, "events"):

File diff suppressed because it is too large Load diff

1232
karax/kdom_impl.nim Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,55 +1,98 @@
## 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
css = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.3/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.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>
<html>
<head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>$1</title>
$2
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="$1.js"></script>
<body id="body" class="site">
<div id="ROOT"></div>
<script type="text/javascript" src="/app.js"></script>
$3
</body>
</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) =
if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd
proc build(name: string, rest: string, selectedCss: string, run: bool) =
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
echo("Building...")
createDir("nimcache")
exec("nim js --out:nimcache/" & name & ".js " & rest)
let dest = "nimcache" / name & ".html"
writeFile(dest, html % [name, selectedCss])
if run: openDefaultBrowser(dest)
let cmd = "nim js --out:" & "app" & ".js " & rest
if watch:
discard os.execShellCmd(cmd)
else:
exec cmd
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 =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
var selectedCss = ""
var watch = false
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
var selectedCss = ""
while true:
op.next()
case op.kind
@ -59,8 +102,11 @@ proc main =
run = true
rest = rest.replace("--run ")
of "css":
selectedCss = css
rest = rest.replace("--css ")
if op.val != "":
selectedCss = readFile(op.val)
else:
selectedCss = css
rest = rest.substr(rest.find(" "))
else: discard
of cmdShortOption:
if op.key == "r":
@ -73,26 +119,11 @@ proc main =
of cmdEnd: break
if file.len == 0: quit "filename expected"
let name = file.splitFile.name
build(name, rest, selectedCss, run)
echo("after build")
if run:
spawn serve()
if watch:
# TODO: launch http server
while true:
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)
spawn watchBuild(file, selectedCss, rest)
build(rest,selectedCss, run, watch)
sync()
main()

1
karax/tools/karun.nims Normal file
View file

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

View file

@ -0,0 +1,141 @@
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
import macros, vstyles, kbase
from strutils import toUpperAscii
from strutils import toUpperAscii, toLowerAscii, tokenize
type
VNodeKind* {.pure.} = enum
@ -35,7 +35,19 @@ type
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, svg, math,
area, 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,
tfoot, tr, td, th,
@ -45,6 +57,10 @@ type
keygen, output, progress, meter,
details, summary, command, menu
const selfClosing = {area, base, br, col, embed, hr, img, input,
link, meta, param, source, track, wbr}
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
@ -89,9 +105,16 @@ type
onanimationiteration,
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)
onload, # img
ontransitioncancel,
ontransitionend,
ontransitionrun,
ontransitionstart,
onwheel # fires when the user rotates a wheel button on a pointing device.
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
@ -294,6 +317,12 @@ proc sameAttrs*(a, b: VNode): bool =
proc addEventListener*(n: VNode; event: EventKind; handler: EventHandler) =
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) =
if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field
@ -379,6 +408,8 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
if n.kind == VNodeKind.text:
result.addEscaped(n.text)
elif n.kind == VNodeKind.verbatim:
result.add(n.text)
else:
let kind = $n.kind
result.add('<')
@ -397,7 +428,17 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
result.add("=\"")
result.addEscapedAttr(v)
result.add('"')
# XXX add style to string
if n.style != nil:
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:
result.add('>')
if n.len > 1:
@ -422,8 +463,13 @@ proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
result.add("</")
result.add(kind)
result.add(">")
else:
elif n.kind in selfClosing:
result.add(" />")
else:
result.add(">")
result.add("</")
result.add(kind)
result.add(">")
proc `$`*(n: VNode): kstring =

View file

@ -1,5 +1,9 @@
##[
see examples/hellostyle.nim
]##
import macros, kbase
import std/[macros, strutils]
import kbase
when defined(js):
import kdom, jdict
@ -234,7 +238,10 @@ proc eq*(a, b: VStyle): bool =
if a[i] != b[i]: return false
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
while i < s.len:
if s[i] == a:
@ -243,8 +250,8 @@ proc setAttr(s: VStyle; a, value: kstring) {.noSideEffect.} =
elif s[i] > a:
s.add ""
s.add ""
# insertion point here:
for j in countdown(s.len-1, i, 2):
# insertion point here, shift all remaining pairs by 2 indexes
for j in countdown(s.len-1, i+3, 2):
s[j] = s[j-2]
s[j-1] = s[j-3]
s[i] = a
@ -289,6 +296,24 @@ proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
result[] = @[]
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):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}

394
readme.md Normal file
View file

@ -0,0 +1,394 @@
![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).

View file

@ -1,327 +0,0 @@
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" />
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
<script type="text/javascript" src="diffDomTests.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>

View file

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

View file

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

View file

@ -5,6 +5,10 @@ import "../karax" / [karaxdsl, vdom]
when defined(js):
{.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):
tr:
td:
@ -16,5 +20,8 @@ let tab = buildHtml(table):
text "Cell C"
td:
text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab