Compare commits
69 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85c81d8c0e |
||
|
|
6d51d7d08c |
||
|
|
93d9b73867 |
||
|
|
ac210fb634 |
||
|
|
df828e145d |
||
|
|
cedd39a65f |
||
|
|
12b98b2338 |
||
|
|
8a7f05ac2a |
||
|
|
0fca6e961d |
||
|
|
32de202845 |
||
|
|
6cff2d8b05 |
||
|
|
4c06cb3204 | ||
|
|
9a627985d0 | ||
|
|
24a037ad19 |
||
|
|
40daae64e3 |
||
|
|
d3fc084c42 |
||
|
|
bcab551f05 |
||
|
|
04779f8502 |
||
|
|
bd18fc980c |
||
|
|
5a2ea1eef9 | ||
|
|
d2f88b4648 |
||
|
|
d8fe79fa40 |
||
|
|
f0dd16ebfe |
||
|
|
0813a7adf9 |
||
|
|
7a36b19729 |
||
|
|
98a2d1ce50 |
||
|
|
22e2e226cb |
||
|
|
f1cd9e606d |
||
|
|
80f500874b |
||
|
|
5f21dcdc56 | ||
|
|
02038d40c1 | ||
|
|
6b753009ac | ||
|
|
d7da2a5bd5 | ||
|
|
c00d7dc340 | ||
|
|
7f061de317 | ||
|
|
0a7136b8fc |
||
|
|
7440393d3a |
||
|
|
f6bda9ae89 | ||
|
|
1911a1cfe0 | ||
|
|
db4acbf7d6 | ||
|
|
6a0c2871ad | ||
|
|
9cf08c4f9c | ||
|
|
983cc8882b | ||
|
|
8dfb8727cd | ||
|
|
9396b3e69d | ||
|
|
e6877a5198 | ||
|
|
7a7cdd8e76 | ||
|
|
023f5d2463 | ||
|
|
65e305e6e9 | ||
|
|
05766db4b6 | ||
|
|
e422087217 | ||
|
|
d08a0356d2 | ||
|
|
9c8a34e567 | ||
|
|
1bbd9c694e | ||
|
|
b99a5430f0 | ||
|
|
fc062c2d48 | ||
|
|
5e9709bc5b | ||
|
|
c091af1622 | ||
|
|
33af725e7d | ||
|
|
cd3e7cc1c8 | ||
|
|
ad3c0a63ee | ||
|
|
20fe35355c | ||
|
|
50bed1f6e9 | ||
|
|
1ea7208b4d | ||
|
|
4186a4b37b | ||
|
|
0118aea506 | ||
|
|
2936245d39 | ||
|
|
fc9f9312ef | ||
|
|
c8c7b13d2f |
37 changed files with 2306 additions and 1827 deletions
31
.github/workflows/ci.yml
vendored
Normal file
31
.github/workflows/ci.yml
vendored
Normal 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
2
.gitignore
vendored
|
|
@ -3,3 +3,5 @@ nimcache/
|
||||||
karax/tools/karun
|
karax/tools/karun
|
||||||
*.code-workspace
|
*.code-workspace
|
||||||
*.exe
|
*.exe
|
||||||
|
app.js
|
||||||
|
app.html
|
||||||
|
|
|
||||||
20
.travis.yml
20
.travis.yml
|
|
@ -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
|
|
||||||
|
|
@ -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 havs a single integer field.
|
The node has a single integer field.
|
||||||
|
|
||||||
``VNodeKind.bool``
|
``VNodeKind.bool``
|
||||||
The node has a single boolean field.
|
The node has a single boolean field.
|
||||||
|
|
||||||
``VNodeKint.vthunk``
|
``VNodeKind.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.
|
||||||
|
|
||||||
``VNodeKint.dthunk``
|
``VNodeKind.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)``.
|
signature ``proc (ev: Event; n: VNode)`` or empty ``proc ()``.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
include karax / prelude
|
include karax / prelude
|
||||||
|
|
||||||
var lines: seq[kstring] = @[]
|
var lines: seq[kstring] = @[]
|
||||||
|
|
|
||||||
|
|
@ -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="nimcache/carousel.js"></script>
|
<script type="text/javascript" src="carousel.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
17
examples/hellostyle.nim
Normal file
17
examples/hellostyle.nim
Normal 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
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
|
|
||||||
include karax / prelude
|
include karax / prelude
|
||||||
import random
|
import random
|
||||||
|
|
||||||
proc createDom(): VNode =
|
proc createDom(): VNode =
|
||||||
result = buildHtml(tdiv):
|
result = buildHtml(tdiv):
|
||||||
if random(100) <= 50:
|
if rand(100) <= 50:
|
||||||
text "Hello World!"
|
text "Hello World!"
|
||||||
else:
|
else:
|
||||||
text "Hello Universe"
|
text "Hello Universe"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
|
|
||||||
include karax / prelude
|
include karax / prelude
|
||||||
from future import `=>`
|
from sugar import `=>`
|
||||||
import karax / errors
|
import karax / errors
|
||||||
|
|
||||||
proc loginField(desc, field, class: kstring;
|
proc loginField(desc, field, class: kstring;
|
||||||
|
|
@ -18,7 +17,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.isNil or x.text == "":
|
if 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, "")
|
||||||
|
|
@ -28,7 +27,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"
|
||||||
|
|
|
||||||
|
|
@ -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="nimcache/playerapp.js"></script>
|
<script type="text/javascript" src="playerapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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="nimcache/scrollapp.js"></script>
|
<script type="text/javascript" src="scrollapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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="nimcache/todoapp.js"></script>
|
<script type="text/javascript" src="todoapp.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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) =
|
||||||
clear()
|
for i in 0..<entriesLen:
|
||||||
selectedEntry = -1
|
markAsCompleted(i, doneswitch)
|
||||||
|
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", name = "toggle")
|
input(class = "toggle-all", `type` = "checkbox", id = "toggle", onclick = onAllDone)
|
||||||
label(`for` = "toggle-all", onclick = onAllDone):
|
label(`for` = "toggle"):
|
||||||
text "Mark all as complete"
|
text "Mark all as complete"
|
||||||
var entriesCount = 0
|
var entriesCount = 0
|
||||||
var completedCount = 0
|
var completedCount = 0
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
include login
|
include login
|
||||||
|
|
||||||
const
|
const
|
||||||
|
|
@ -12,7 +11,7 @@ var allMessages: seq[TextMessage] = @[]
|
||||||
|
|
||||||
proc doSendMessage() =
|
proc doSendMessage() =
|
||||||
let inputField = getVNodeById(message)
|
let inputField = getVNodeById(message)
|
||||||
allMessages.add(TextMessage(name: "you", content: inputField.text))
|
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
|
||||||
inputField.setInputText ""
|
inputField.setInputText ""
|
||||||
|
|
||||||
proc main(): VNode =
|
proc main(): VNode =
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,5 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/ajaxtest.js"></script>
|
<script type="text/javascript" src="ajaxtest.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -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="nimcache/echartstest.js"></script>
|
<script type="text/javascript" src="echartstest.js"></script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
|
|
||||||
<body id="body">
|
<body id="body">
|
||||||
<div id="ROOT"></div>
|
<div id="ROOT"></div>
|
||||||
<script type="text/javascript" src="nimcache/demo.js"></script>
|
<script type="text/javascript" src="demo.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/scrollapp.js"></script>
|
<script type="text/javascript" src="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>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
# Package
|
# Package
|
||||||
|
|
||||||
version = "1.0.0"
|
version = "1.2.1"
|
||||||
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.16.1"
|
requires "nim >= 0.18.0"
|
||||||
|
requires "ws"
|
||||||
|
requires "dotenv"
|
||||||
skipDirs = @["examples", "experiments", "tests"]
|
skipDirs = @["examples", "experiments", "tests"]
|
||||||
|
|
||||||
bin = @["karax/tools/karun"]
|
bin = @["karax/tools/karun"]
|
||||||
|
|
|
||||||
|
|
@ -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.symbol)
|
result = ident(n.strVal)
|
||||||
else:
|
else:
|
||||||
result = n
|
result = n
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,7 @@ 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.}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,66 @@
|
||||||
## 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;
|
||||||
|
|
@ -16,20 +76,8 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
|
||||||
cont(httpStatus, response)
|
cont(httpStatus, response)
|
||||||
if doRedraw: redraw(kxi)
|
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
|
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):
|
||||||
|
|
@ -81,3 +129,4 @@ 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".}
|
||||||
|
|
||||||
|
|
|
||||||
296
karax/karax.nim
296
karax/karax.nim
|
|
@ -7,17 +7,16 @@ export kdom.Event, kdom.Blob
|
||||||
when defined(nimNoNil):
|
when defined(nimNoNil):
|
||||||
{.experimental: "notnil".}
|
{.experimental: "notnil".}
|
||||||
|
|
||||||
proc kout*[T](x: T) {.importc: "console.log", varargs, deprecated.}
|
proc kout*[T](x: T) {.importc: "console.log", varargs.}
|
||||||
## the preferred way of debugging karax applications. Now deprecated,
|
## The preferred way of debugging karax applications.
|
||||||
## you can now use ``system.echo`` instead.
|
|
||||||
|
|
||||||
type
|
type
|
||||||
PatchKind = enum
|
PatchKind = enum
|
||||||
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach
|
pkReplace, pkRemove, pkAppend, pkInsertBefore, pkDetach, pkSame
|
||||||
Patch = object
|
Patch = object
|
||||||
k: PatchKind
|
k: PatchKind
|
||||||
parent, current: Node
|
parent, current: Node
|
||||||
n: VNode
|
newNode, oldNode: VNode
|
||||||
PatchV = object
|
PatchV = object
|
||||||
parent, newChild: VNode
|
parent, newChild: VNode
|
||||||
pos: int
|
pos: int
|
||||||
|
|
@ -31,13 +30,14 @@ 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 not nil
|
rootId: cstring
|
||||||
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,6 +78,24 @@ 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 =
|
||||||
|
|
@ -106,18 +124,18 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
|
||||||
case k
|
case k
|
||||||
of EventKind.onkeyuplater:
|
of EventKind.onkeyuplater:
|
||||||
result = laterWrapper()
|
result = laterWrapper()
|
||||||
d.addEventListener("keyup", result)
|
d.addEventShell("keyup", result)
|
||||||
of EventKind.onkeyupenter:
|
of EventKind.onkeyupenter:
|
||||||
result = enterWrapper()
|
result = enterWrapper()
|
||||||
d.addEventListener("keyup", result)
|
d.addEventShell("keyup", result)
|
||||||
else:
|
else:
|
||||||
result = stdWrapper()
|
result = stdWrapper()
|
||||||
d.addEventListener(toEventName[k], result)
|
d.addEventShell(toEventName[k], result)
|
||||||
|
|
||||||
# --------------------- DOM diff -----------------------------------------
|
# --------------------- DOM diff -----------------------------------------
|
||||||
|
|
||||||
template detach(n: VNode) =
|
template detach(n: VNode) =
|
||||||
addPatch(kxi, pkDetach, nil, nil, n)
|
addPatch(kxi, pkDetach, nil, nil, nil, n)
|
||||||
|
|
||||||
template attach(n: VNode) =
|
template attach(n: VNode) =
|
||||||
n.dom = result
|
n.dom = result
|
||||||
|
|
@ -134,7 +152,11 @@ 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 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:
|
if n.kind == VNodeKind.text:
|
||||||
result = document.createTextNode(n.text)
|
result = document.createTextNode(n.text)
|
||||||
attach n
|
attach n
|
||||||
|
|
@ -145,7 +167,7 @@ proc vnodeToDom*(n: VNode; 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 = vnodeToDom(x, kxi)
|
result = toDom(x, useAttachedNode, kxi)
|
||||||
#n.key = result.key
|
#n.key = result.key
|
||||||
attach n
|
attach n
|
||||||
return result
|
return result
|
||||||
|
|
@ -163,14 +185,14 @@ proc vnodeToDom*(n: VNode; 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 = vnodeToDom(x.expanded, kxi)
|
result = toDom(x.expanded, useAttachedNode, 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, vnodeToDom(k, kxi))
|
appendChild(result, toDom(k, useAttachedNode, 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
|
||||||
|
|
@ -231,7 +253,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): EqResult =
|
proc eq(a, b: VNode; recursive: bool): 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
|
||||||
|
|
@ -250,9 +272,12 @@ proc eq(a, b: VNode): 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]) == different: return different
|
if eq(a[i], b[i], recursive) == different: return different
|
||||||
elif a.kind == VNodeKind.dthunk:
|
elif a.kind == VNodeKind.dthunk:
|
||||||
return identical
|
if a.dom == b.dom:
|
||||||
|
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
|
||||||
|
|
@ -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):
|
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
|
||||||
|
|
||||||
|
|
@ -298,12 +331,15 @@ 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
|
||||||
for i in 0..<oldNode.events.len:
|
if d != nil:
|
||||||
let k = oldNode.events[i][0]
|
removeAllEventHandlers(d)
|
||||||
let name = case k
|
when false:
|
||||||
of EventKind.onkeyuplater, EventKind.onkeyupenter: cstring"keyup"
|
for i in 0..<oldNode.events.len:
|
||||||
else: toEventName[k]
|
let k = oldNode.events[i][0]
|
||||||
d.removeEventListener(name, oldNode.events[i][2])
|
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)
|
shallowCopy(oldNode.events, newNode.events)
|
||||||
applyEvents(oldNode)
|
applyEvents(oldNode)
|
||||||
|
|
||||||
|
|
@ -321,16 +357,18 @@ 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: VNode) =
|
na, oldNode: 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, n: na))
|
kxi.patches.add(Patch(k: ka, parent: parenta, current: currenta,
|
||||||
|
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].n = na
|
kxi.patches[L].newNode = 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) =
|
||||||
|
|
@ -344,26 +382,40 @@ 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 = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
if p.parent == nil:
|
if p.parent == nil:
|
||||||
replaceById(kxi.rootId, nn)
|
replaceById(kxi.rootId, nn)
|
||||||
else:
|
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:
|
of pkRemove:
|
||||||
p.parent.removeChild(p.current)
|
p.parent.removeChild(p.current)
|
||||||
of pkAppend:
|
of pkAppend:
|
||||||
let nn = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
p.parent.appendChild(nn)
|
p.parent.appendChild(nn)
|
||||||
of pkInsertBefore:
|
of pkInsertBefore:
|
||||||
let nn = vnodeToDom(p.n, kxi)
|
let nn = toDom(p.newNode, useAttachedNode = true, kxi)
|
||||||
p.parent.insertBefore(nn, p.current)
|
p.parent.insertBefore(nn, p.current)
|
||||||
of pkDetach:
|
of pkDetach:
|
||||||
let n = p.n
|
let n = p.oldNode
|
||||||
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)
|
||||||
|
|
@ -377,16 +429,38 @@ proc applyPatch(kxi: KaraxInstance) =
|
||||||
assert p.newChild.dom != nil
|
assert p.newChild.dom != nil
|
||||||
kxi.patchLenV = 0
|
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):
|
when defined(stats):
|
||||||
if kxi.recursion > 100:
|
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:
|
if oldNode.kind == VNodeKind.text:
|
||||||
echo oldNode.text
|
echo oldNode.text
|
||||||
#return
|
|
||||||
#doAssert false, "overflow!"
|
|
||||||
inc kxi.recursion
|
inc kxi.recursion
|
||||||
result = eq(newNode, oldNode)
|
let result = eq(newNode, oldNode, false)
|
||||||
case result
|
case result
|
||||||
of componentsIdentical:
|
of componentsIdentical:
|
||||||
kxi.components.add ComponentPair(oldNode: VComponent(oldNode),
|
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:
|
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 result
|
if newLength == 0 and oldLength == 0: return
|
||||||
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:
|
||||||
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
|
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:
|
||||||
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 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:
|
||||||
let r = diff(newNode[i], oldNode[i], current, current.childNodes[i], kxi)
|
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])
|
kxi.addPatch(pkAppend, current, nil, newNode[i], nil)
|
||||||
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])
|
kxi.addPatch(pkInsertBefore, current, before, newNode[i], nil)
|
||||||
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)
|
kxi.addPatch(pkRemove, current, current.childNodes[i], nil, nil)
|
||||||
result = usenewNode
|
|
||||||
of different:
|
of different:
|
||||||
detach(oldNode)
|
detach(oldNode)
|
||||||
kxi.addPatch(pkReplace, parent, current, newNode)
|
kxi.addPatch(pkReplace, parent, current, newNode, nil)
|
||||||
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
|
||||||
|
|
@ -517,24 +555,25 @@ 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)
|
kxi.addPatch(pkReplace, parent, current, x.expanded, nil)
|
||||||
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:
|
||||||
let res = diff(x.expanded, oldExpanded, parent, current, kxi)
|
diff(x.expanded, oldExpanded, parent, current, kxi)
|
||||||
if res == usenewNode:
|
when false:
|
||||||
when defined(karaxDebug):
|
if res == usenewNode:
|
||||||
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
when defined(karaxDebug):
|
||||||
discard "diff created a patchset for us, so this is fine"
|
echo "Component ", newNode.text, ": re-render triggered a DOM change (case A)"
|
||||||
elif res != different:
|
discard "diff created a patchset for us, so this is fine"
|
||||||
when defined(karaxDebug):
|
elif res != different:
|
||||||
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
|
when defined(karaxDebug):
|
||||||
x.expanded = oldExpanded
|
echo "Component ", newNode.text, ": re-render triggered no DOM change whatsoever"
|
||||||
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
x.expanded = oldExpanded
|
||||||
else:
|
assert oldExpanded.dom != nil, "old expanded.dom is nil"
|
||||||
when defined(karaxDebug):
|
else:
|
||||||
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
|
when defined(karaxDebug):
|
||||||
assert x.expanded.dom != nil, "expanded.dom is nil"
|
echo "Component ", newNode.text, ": re-render triggered a DOM change (case B)"
|
||||||
|
assert x.expanded.dom != nil, "expanded.dom is nil"
|
||||||
inc i
|
inc i
|
||||||
setLen(kxi.components, 0)
|
setLen(kxi.components, 0)
|
||||||
|
|
||||||
|
|
@ -549,7 +588,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)
|
kxi.addPatch(pkRemove, current, current.childNodes[position], nil, 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))
|
||||||
|
|
@ -557,11 +596,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)
|
kxi.addPatch(pkAppend, current, nil, kid, nil)
|
||||||
parent.add(kid)
|
parent.add(kid)
|
||||||
else:
|
else:
|
||||||
let before = current.childNodes[position]
|
let before = current.childNodes[position]
|
||||||
kxi.addPatch(pkInsertBefore, current, before, kid)
|
kxi.addPatch(pkInsertBefore, current, before, kid, nil)
|
||||||
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))
|
||||||
|
|
@ -569,7 +608,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
|
||||||
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
|
# 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:
|
||||||
|
|
@ -583,21 +622,41 @@ 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 = vnodeToDom(newtree, kxi)
|
let asdom = toDom(newtree, useAttachedNode = true, 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)
|
||||||
discard diff(newtree, kxi.currentTree, nil, olddom, kxi)
|
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
|
||||||
|
|
@ -616,16 +675,12 @@ 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.renderId = 0
|
kxi.rendering = false
|
||||||
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:
|
||||||
|
|
@ -713,7 +768,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.} =
|
proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
|
||||||
## 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() =
|
||||||
|
|
@ -745,11 +800,10 @@ 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
|
||||||
body.prepend(s)
|
document.body.prepend(s)
|
||||||
redraw(kxi)
|
redraw(kxi)
|
||||||
|
|
||||||
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
|
proc runLater*(action: proc(); later = 400): Timeout {.discardable.} =
|
||||||
|
|
@ -763,8 +817,18 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ const
|
||||||
|
|
||||||
proc getName(n: NimNode): string =
|
proc getName(n: NimNode): string =
|
||||||
case n.kind
|
case n.kind
|
||||||
of nnkIdent:
|
of nnkIdent, nnkSym:
|
||||||
result = $n.ident
|
result = $n
|
||||||
of nnkAccQuoted:
|
of nnkAccQuoted:
|
||||||
result = ""
|
result = ""
|
||||||
for i in 0..<n.len:
|
for i in 0..<n.len:
|
||||||
|
|
@ -27,6 +27,8 @@ 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)
|
||||||
|
|
@ -124,6 +126,8 @@ 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"):
|
||||||
|
|
|
||||||
1234
karax/kdom.nim
1234
karax/kdom.nim
File diff suppressed because it is too large
Load diff
1232
karax/kdom_impl.nim
Normal file
1232
karax/kdom_impl.nim
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
const
|
||||||
css = """
|
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">
|
<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">
|
<body id="body" class="site">
|
||||||
<div id="ROOT" />
|
<div id="ROOT"></div>
|
||||||
<script type="text/javascript" src="$1.js"></script>
|
<script type="text/javascript" src="/app.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(name: string, rest: string, selectedCss: string, run: bool) =
|
proc build(rest: string, selectedCss: string, run: bool, watch: bool) =
|
||||||
echo("Building...")
|
echo("Building...")
|
||||||
createDir("nimcache")
|
let cmd = "nim js --out:" & "app" & ".js " & rest
|
||||||
exec("nim js --out:nimcache/" & name & ".js " & rest)
|
if watch:
|
||||||
let dest = "nimcache" / name & ".html"
|
discard os.execShellCmd(cmd)
|
||||||
writeFile(dest, html % [name, selectedCss])
|
else:
|
||||||
if run: openDefaultBrowser(dest)
|
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 =
|
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 selectedCss = ""
|
|
||||||
var watch = false
|
var watch = false
|
||||||
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
|
var selectedCss = ""
|
||||||
|
|
||||||
while true:
|
while true:
|
||||||
op.next()
|
op.next()
|
||||||
case op.kind
|
case op.kind
|
||||||
|
|
@ -59,8 +102,11 @@ proc main =
|
||||||
run = true
|
run = true
|
||||||
rest = rest.replace("--run ")
|
rest = rest.replace("--run ")
|
||||||
of "css":
|
of "css":
|
||||||
selectedCss = css
|
if op.val != "":
|
||||||
rest = rest.replace("--css ")
|
selectedCss = readFile(op.val)
|
||||||
|
else:
|
||||||
|
selectedCss = css
|
||||||
|
rest = rest.substr(rest.find(" "))
|
||||||
else: discard
|
else: discard
|
||||||
of cmdShortOption:
|
of cmdShortOption:
|
||||||
if op.key == "r":
|
if op.key == "r":
|
||||||
|
|
@ -73,26 +119,11 @@ proc main =
|
||||||
of cmdEnd: break
|
of cmdEnd: break
|
||||||
|
|
||||||
if file.len == 0: quit "filename expected"
|
if file.len == 0: quit "filename expected"
|
||||||
let name = file.splitFile.name
|
if run:
|
||||||
build(name, rest, selectedCss, run)
|
spawn serve()
|
||||||
echo("after build")
|
|
||||||
if watch:
|
if watch:
|
||||||
# TODO: launch http server
|
spawn watchBuild(file, selectedCss, rest)
|
||||||
while true:
|
build(rest,selectedCss, run, watch)
|
||||||
sleep(300)
|
sync()
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
1
karax/tools/karun.nims
Normal file
1
karax/tools/karun.nims
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
switch("threads", "on")
|
||||||
141
karax/tools/static_server.nim
Normal file
141
karax/tools/static_server.nim
Normal 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()
|
||||||
|
|
@ -8,7 +8,7 @@ else:
|
||||||
Node* = ref object
|
Node* = ref object
|
||||||
|
|
||||||
import macros, vstyles, kbase
|
import macros, vstyles, kbase
|
||||||
from strutils import toUpperAscii
|
from strutils import toUpperAscii, toLowerAscii, tokenize
|
||||||
|
|
||||||
type
|
type
|
||||||
VNodeKind* {.pure.} = enum
|
VNodeKind* {.pure.} = enum
|
||||||
|
|
@ -35,7 +35,19 @@ 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, 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,
|
table, caption, colgroup, col, tbody, thead,
|
||||||
tfoot, tr, td, th,
|
tfoot, tr, td, th,
|
||||||
|
|
@ -45,6 +57,10 @@ 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.
|
||||||
|
|
@ -89,9 +105,16 @@ 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)
|
||||||
|
|
@ -294,6 +317,12 @@ 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
|
||||||
|
|
@ -379,6 +408,8 @@ 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('<')
|
||||||
|
|
@ -397,7 +428,17 @@ 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('"')
|
||||||
# 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:
|
if n.len > 0:
|
||||||
result.add('>')
|
result.add('>')
|
||||||
if n.len > 1:
|
if n.len > 1:
|
||||||
|
|
@ -422,8 +463,13 @@ 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(">")
|
||||||
else:
|
elif n.kind in selfClosing:
|
||||||
result.add(" />")
|
result.add(" />")
|
||||||
|
else:
|
||||||
|
result.add(">")
|
||||||
|
result.add("</")
|
||||||
|
result.add(kind)
|
||||||
|
result.add(">")
|
||||||
|
|
||||||
|
|
||||||
proc `$`*(n: VNode): kstring =
|
proc `$`*(n: VNode): kstring =
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
##[
|
||||||
|
see examples/hellostyle.nim
|
||||||
|
]##
|
||||||
|
|
||||||
import macros, kbase
|
import std/[macros, strutils]
|
||||||
|
import kbase
|
||||||
|
|
||||||
when defined(js):
|
when defined(js):
|
||||||
import kdom, jdict
|
import kdom, jdict
|
||||||
|
|
@ -234,7 +238,10 @@ 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:
|
||||||
|
|
@ -243,8 +250,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:
|
# insertion point here, shift all remaining pairs by 2 indexes
|
||||||
for j in countdown(s.len-1, i, 2):
|
for j in countdown(s.len-1, i+3, 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
|
||||||
|
|
@ -289,6 +296,24 @@ 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
Normal file
394
readme.md
Normal file
|
|
@ -0,0 +1,394 @@
|
||||||
|

|
||||||
|
|
||||||
|
    
|
||||||
|
|
||||||
|
# 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
327
readme.rst
|
|
@ -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>`_.)
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
<div id="ROOT" />
|
<div id="ROOT" />
|
||||||
|
|
||||||
<script type="text/javascript" src="nimcache/diffDomTests.js"></script>
|
<script type="text/javascript" src="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>
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,19 @@ 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]) =
|
||||||
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:
|
for i in 0..<kxi.patchLen:
|
||||||
let p = $kxi.patches[i].k & " " & shortRepr(kxi.patches[i].n)
|
if kxi.patches[i].k != pkSame:
|
||||||
if i >= expected.len:
|
let n = if kxi.patches[i].k == pkDetach: kxi.patches[i].oldNode else: kxi.patches[i].newNode
|
||||||
echo "patches differ; expected nothing but got: ", p
|
let p = $kxi.patches[i].k & " " & shortRepr(n)
|
||||||
inc err
|
if j >= expected.len:
|
||||||
elif p != expected[i]:
|
echo "patches differ; expected nothing but got: ", p
|
||||||
echo "patches differ; expected ", expected[i], " but got: ", p
|
inc err
|
||||||
inc err
|
elif p != expected[j]:
|
||||||
|
echo "patches differ; expected ", expected[i], " but got: ", p
|
||||||
|
inc err
|
||||||
|
inc j
|
||||||
#hasDom(kxi.currentTree)
|
#hasDom(kxi.currentTree)
|
||||||
kxi.patchLen = 0
|
kxi.patchLen = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
</head>
|
</head>
|
||||||
<body id="body">
|
<body id="body">
|
||||||
<div id="ROOT"></div>
|
<div id="ROOT"></div>
|
||||||
<script src="nimcache/domEventTests.js" type="text/javascript"></script>
|
<script src="domEventTests.js" type="text/javascript"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ 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:
|
||||||
|
|
@ -16,5 +20,8 @@ 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue