Compare commits

...
Sign in to create a new pull request.

142 commits

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

* removed travis build file

* changed docker image

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

* Fix small issues

* Revert "Fix small issues"

This reverts commit 2a480addd7e2a55faaa95dd2145a6dc0aa4b95f7.

* Small Fixes vol.2

* Fix licence link

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

Changed existsFile to fileExists and existsDir to dirExists.

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

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

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

* watch optional

* we dont quit when building error in watch mode

* staticDir,publicUrl configure through dot env

* remove unused import

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

* arrange: add indent in HTML files

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

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

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

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

* Update kdom_impl.nim

use MDN naming

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

* Fix indentation
2019-05-15 19:06:17 +02:00
Araq
cd3e7cc1c8 fixed indentation 2019-05-15 18:48:38 +02:00
liuxiaodong
ad3c0a63ee update verbatim for static HTML (#96) 2019-05-15 18:36:22 +02:00
Andreas Rumpf
20fe35355c server side Karax rendering: show how events can be handled 2019-03-14 11:21:49 +01:00
Andreas Rumpf
50bed1f6e9 used sugar.nim instead of future.nim in the login.nim example 2019-03-14 11:09:06 +01:00
liuxiaodong
1ea7208b4d add uploadFile and a path tag (#88)
* add uploadFile and a path tag
* update uploadFile and circle tag
2019-03-10 20:15:13 +01:00
Jairo
4186a4b37b Update Bulma (#89) 2019-03-08 13:32:35 +01:00
Araq
0118aea506 use stdlib's dom module if possible 2019-01-31 21:28:31 +01:00
Araq
2936245d39 add newWebsocket variant that lacks the key 2019-01-31 16:16:37 +01:00
Araq
fc9f9312ef improve deprecation message 2019-01-31 16:15:03 +01:00
Araq
c8c7b13d2f update readme.rst, v1 has arrived 2018-12-12 15:18:36 +01:00
Araq
a2c2aac639 Karax v1 has arrived 2018-12-12 15:04:19 +01:00
Araq
18b5a12d1e added a new way for native DOM <-> virtual DOM interop, see nativenodes.nim example 2018-12-12 15:02:58 +01:00
Araq
a90efe752d merged #53 manually; todoapp must ignore empty input lines 2018-12-12 12:34:35 +01:00
Araq
068c30c97d make examples compile again with a config.nims file 2018-12-12 12:31:23 +01:00
Jairo
16008dbe52 Watch option (#79)
* Update karun.nim
* Watch option

Add a -w flag to watch for changes in folder and recompile.
2018-12-12 12:29:41 +01:00
Timothee Cour
b16112613a Update readme.rst (#77) 2018-10-10 00:43:08 +02:00
Timothee Cour
1ccd8b1c16 fix #73: echartstest.nim doesnt work (#75) 2018-09-28 09:24:39 +02:00
Andreas Rumpf
3a2effdf99 fixes readme.rst 2018-09-23 09:55:43 +02:00
Andreas Rumpf
fc0ca22785 update karax.nimble for latest Nimble 2018-09-23 09:52:22 +02:00
Araq
a07f1c7b82 fixes #72 2018-09-22 20:40:00 +02:00
LemonBoy
bd69de9f14 Don't choke on nnkDotExpr nodes (#71)
Fixes #69
2018-09-03 00:25:51 +02:00
Andreas Rumpf
1084849500 Merge branch 'master' of github.com:pragmagic/karax 2018-08-15 16:25:33 +02:00
Andreas Rumpf
b05c037bb0 Karax: prepare for the Nim release where 'nil' is not a thing for strings anymore 2018-08-15 16:25:26 +02:00
Dominik Picheta
b3a5ed9c54 Adds animation events. (#70) 2018-07-30 08:57:59 +02:00
Araq
015590e806 karax DSL: allow 'foo-bar' syntax for custom HTML attributes 2018-07-03 12:56:32 +02:00
Araq
6ab7c9455d make menus.nim example compile again 2018-07-03 12:56:05 +02:00
Araq
3b3aa6347d improve karax documentation 2018-07-03 12:19:19 +02:00
Araq
20fe1bcbee extended the readme.rst with plenty of examples from my blog post 2018-06-14 15:07:54 +02:00
Araq
fbfeced5b1 kajax.nim: add a switch to disable automatic redrawing when data was received 2018-06-14 15:07:34 +02:00
Andreas Rumpf
52026d1687 Karax DSL: allow 'style = {}' syntax for inline CSS 2018-06-10 13:27:58 +02:00
Andreas Rumpf
451ee92dd0 Merge branch 'master' of github.com:pragmagic/karax 2018-06-10 12:09:36 +02:00
Andreas Rumpf
330a7351f9 knete.nim: change order of parameters in replaceById 2018-06-10 12:09:30 +02:00
Araq
d8df257dd2 Merge branch 'master' of github.com:pragmagic/karax 2018-05-25 22:41:09 +02:00
Araq
092395ed5c bugfix: textareas are allowed to be weird 2018-05-25 22:41:01 +02:00
oskca
78cad87452 make --css works in karun (#64)
* make --css works in karun

* ignore karun executable in source tree
2018-05-25 09:12:22 +02:00
Araq
9882d1aec2 cleanup dom's PR 2018-05-24 00:01:01 +02:00
Dominik Picheta
9068686614 General improvements and fixes (#58)
* Fixes Nimble package structure.
* Improves readme.
* Fixes compatibility with devel.
* Add nice error message when ROOT is missing.
* Pass responseText instead of statusText in ajax callbacks.
* Implements verbatim node.
* Apparently it should just be true all the time.
* Small fix to readme.
* Fixes verbatim node updates.
* Move karun into the `karax` directory so that it's installable.
2018-05-23 23:30:06 +02:00
shinriyo
1e3a1e4745 add 'git clone' instructions to the readme (#51) 2018-03-14 12:23:39 +01:00
Andreas Rumpf
87edc4cdb9 fixes overly aggressive text node reuse DOM diff'ing bug 2018-03-07 11:52:25 +01:00
Andreas Rumpf
75739f8170 add experiment what Karax would look like without DOM diff'ing but with barebones reactive programming 2018-03-05 10:00:11 +01:00
Andreas Rumpf
dd0ea311a3 fixes an annoying karun bug 2018-03-05 09:58:39 +01:00
Araq
5576c4809d Karax: better support for routing; easier to use; todoapp bugfix: misapplied .compact macro 2018-02-17 14:13:26 +01:00
Andreas Rumpf
b36380a48e Merge branch 'master' of github.com:pragmagic/karax 2018-01-23 07:13:31 +01:00
Andreas Rumpf
db9325b3bb added karax.setForeignNodeId to make interfacing with ECharts easier 2018-01-23 07:12:47 +01:00
Andreas Rumpf
b5fc4de010 jjson: added the %* macro inspired from json.nim 2018-01-23 06:38:43 +01:00
Rayner De Los Santos F
7aace27355 Adding hasError method (#45) 2018-01-03 14:04:10 +01:00
Araq
271fdd9bde Merge branch 'darkmusic-master' 2017-12-12 15:13:11 +01:00
Araq
4428c4ed95 Merge branch 'master' of https://github.com/darkmusic/karax into darkmusic-master 2017-12-12 15:05:19 +01:00
Araq
135044d8d7 added vdom.getVNodeById for easier component creation 2017-12-05 16:32:11 +01:00
Andreas Rumpf
73c94c969c
Merge branch 'master' into master 2017-12-01 09:52:27 +01:00
Thomas Johnson
387c8b79fb
Merge branch 'master' into master 2017-11-30 11:52:18 -08:00
Araq
be759d9b42 bugfix: apply class changes consistently 2017-11-30 18:16:31 +01:00
Thomas Johnson
c9c30eaf45 Fixing spacing issue in kdom. (#43) 2017-11-26 15:20:01 -08:00
Thomas Johnson
22f24a1a8a Added blob support to ajaxPost. Cleanup in kdom and added InputElement and Blob support. Test updates. (#43) 2017-11-26 15:11:19 -08:00
Araq
1ad7567b28 added new experiments 2017-11-22 14:37:12 +01:00
Thomas Johnson
28bccf9223 Tab size fixes in domEventTests.css, .html for #43. 2017-11-21 17:14:59 -08:00
Thomas Johnson
b3100d5684 Tab size fixes in domEventTests for #43. 2017-11-21 17:01:23 -08:00
Thomas Johnson
d07fc9a8af Switched to non-minimized JQuery library for tests; code style updates to domEventTests for #43. 2017-11-21 16:52:38 -08:00
Araq
4571ad0dd5 merged PR #38 manually; refs #38 2017-11-21 23:44:45 +01:00
Andreas Rumpf
b8c9ff45fe added websockets' onopen event handler 2017-11-21 17:24:39 +01:00
Thomas Johnson
9ad41a6066 kdom changes and tests for #42 2017-11-20 17:12:33 -08:00
Araq
7425568122 attempt to make travis green again 2017-11-20 23:55:13 +01:00
Araq
84720812ea attempt to make travis green again 2017-11-20 23:47:06 +01:00
Araq
6c2513a48c attempt to make travis green again 2017-11-20 23:40:35 +01:00
Araq
22a20b3246 attempt to make travis green again 2017-11-20 23:33:56 +01:00
Araq
bf28a44439 fixes issues in the new examples 2017-11-20 14:04:34 +01:00
Araq
fb604321f3 added many more examples 2017-11-20 12:26:30 +01:00
Araq
bcc45feea3 added 'karun' tool to karax.nimble 2017-11-20 12:26:10 +01:00
Andreas Rumpf
e112f15092 tools/karun: produce CSS only with --css switch 2017-11-16 15:34:08 +01:00
Andreas Rumpf
a0665bc544 added errors.nim module for UI error handling 2017-11-13 15:27:58 +01:00
Andreas Rumpf
0ddf6098f7 improve performance for class-only changes; improves #40 but doesn't fix the real underlying combinatorial explosion 2017-10-31 09:40:52 +01:00
Andreas Rumpf
4be45499f7 improve karax.nimble 2017-10-10 12:59:38 +02:00
Andreas Rumpf
6ea55856b5 added JDict.keys iterator 2017-10-05 14:55:14 +02:00
Andreas Rumpf
6bd32a0e88 added toChecked and toDisabled helpers 2017-10-05 12:32:14 +02:00
Andreas Rumpf
9a8b020269 added 'kstrLits' macro 2017-10-05 12:29:52 +02:00
Andreas Rumpf
5bc983e0bf added lookuptables.nim helper module 2017-10-03 09:35:28 +02:00
Andreas Rumpf
8e8ada7cd2 move karax source code from 'src' to 'karax'; make karaxdsl work with native Nim; added jjson and jwebsockets modules 2017-10-02 22:54:25 +02:00
Andreas Rumpf
bb1732c834 make tests green again 2017-10-02 16:00:30 +02:00
Andreas Rumpf
57a6fca9d1 implement a autocomplete widget (work in progress) 2017-10-02 13:53:53 +02:00
Andreas Rumpf
312eb5180a added getVNodeById() for easier form handling 2017-10-02 13:52:49 +02:00
Andreas Rumpf
45951c96a4 added an example of how to do menus and auto-completion with Karax 2017-09-22 18:10:30 +02:00
Andreas Rumpf
55b551678a Merge branch 'master' of github.com:pragmagic/karax 2017-09-19 16:20:56 +02:00
Andreas Rumpf
34e2b8b7b4 added karax.runLater 2017-09-19 16:20:33 +02:00
78 changed files with 14873 additions and 652 deletions

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

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

7
.gitignore vendored
View file

@ -1,2 +1,7 @@
nimcache/ nimcache/
/tests/tester /tests/tester
karax/tools/karun
*.code-workspace
*.exe
app.js
app.html

View file

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

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Xored Software, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -10,17 +10,17 @@ and some extensions that allow for an efficient component system.
These extensions are: These extensions are:
``VNodeKind.int`` ``VNodeKind.int``
The node 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 ()``.

15
examples/button.nim Normal file
View file

@ -0,0 +1,15 @@
include karax / prelude
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

14
examples/buttonlambda.nim Normal file
View file

@ -0,0 +1,14 @@
include karax / prelude
from future 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

View file

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

View file

@ -1 +1 @@
--path: "../../src" --path: "../../karax"

1
examples/config.nims Normal file
View file

@ -0,0 +1 @@
--path: ".."

17
examples/hellostyle.nim Normal file
View file

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

View file

@ -0,0 +1,12 @@
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

7
examples/helloworld.nim Normal file
View file

@ -0,0 +1,7 @@
include karax / prelude
proc createDom(): VNode =
result = buildHtml(tdiv):
text "Hello World!"
setRenderer createDom

46
examples/login.nim Normal file
View file

@ -0,0 +1,46 @@
include karax / prelude
from sugar import `=>`
import karax / errors
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))
# some consts in order to prevent typos:
const
username = kstring"username"
password = kstring"password"
proc validateNotEmpty(field: kstring): proc () =
result = proc () =
let x = getVNodeById(field)
if x.text == "":
errors.setError(field, field & " must not be empty")
else:
errors.setError(field, "")
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."
setError username, username & " must not be empty"
setError password, password & " must not be empty"
when not declared(toychat):
setRenderer loginDialog

View file

@ -1,5 +1,5 @@
import karax, karaxdsl, vdom, kdom, compact import karax / [karax, karaxdsl, vdom, kdom, compact]
const const
Play = 0 Play = 0

View file

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

View file

@ -1,6 +1,6 @@
## Example app that shows how to write and embed a custom component. ## Example app that shows how to write and embed a custom component.
include karaxprelude include karax/prelude
import mediaplayer import mediaplayer
const url = "https://www.w3schools.com/html/mov_bbb.mp4" const url = "https://www.w3schools.com/html/mov_bbb.mp4"

View file

@ -1 +1 @@
--path: "../../src" --path: "../.."

View file

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

View file

@ -1,7 +1,7 @@
## Example that shows how to accomplish an "infinitely scrolling" app. ## Example that shows how to accomplish an "infinitely scrolling" app.
include karaxprelude include karax/prelude
import jstrutils, kdom, vstyles import karax / [jstrutils, kdom, vstyles]
var entries: seq[cstring] = @[] var entries: seq[cstring] = @[]
for i in 1..500: for i in 1..500:

View file

@ -1 +1 @@
--path: "../../src" --path: "../.."

View file

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

View file

@ -1,5 +1,5 @@
import vdom, karax, karaxdsl, jstrutils, compact, localstorage import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]
type type
Filter = enum Filter = enum
@ -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"
@ -41,8 +41,9 @@ proc updateEntry(pos: int, content: cstring, completed: bool) =
markAsCompleted(pos, completed) markAsCompleted(pos, completed)
proc onTodoEnter(ev: Event; n: VNode) = proc onTodoEnter(ev: Event; n: VNode) =
addEntry(n.value, false) if n.value.strip() != "":
n.value = "" addEntry(n.value, false)
n.value = ""
proc removeHandler(ev: Event; n: VNode) = proc removeHandler(ev: Event; n: VNode) =
updateEntry(n.index, cstring(nil), false) updateEntry(n.index, cstring(nil), false)
@ -61,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)
@ -71,9 +72,6 @@ proc clearCompleted(ev: Event, n: VNode) =
proc toClass(completed: bool): cstring = proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil)) (if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc selected(v: Filter): cstring = proc selected(v: Filter): cstring =
(if filter == v: cstring"selected" else: cstring(nil)) (if filter == v: cstring"selected" else: cstring(nil))
@ -92,7 +90,7 @@ proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compac
onblur = focusLost, onblur = focusLost,
onkeyupenter = editEntry, value = d, setFocus=true) onkeyupenter = editEntry, value = d, setFocus=true)
proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} = proc makeFooter(entriesCount, completedCount: int): VNode =
result = buildHtml(footer(class = "footer")): result = buildHtml(footer(class = "footer")):
span(class = "todo-count"): span(class = "todo-count"):
strong: strong:
@ -118,13 +116,16 @@ proc makeHeader(): VNode {.compact.} =
input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo", input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
onkeyupenter = onTodoEnter, setFocus) onkeyupenter = onTodoEnter, setFocus)
proc createDom(): VNode = 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")): result = buildHtml(tdiv(class="todomvc-wrapper")):
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
@ -144,12 +145,6 @@ proc createDom(): VNode =
inc entriesCount inc entriesCount
makeFooter(entriesCount, completedCount) makeFooter(entriesCount, completedCount)
setOnHashChange(proc(hash: cstring) =
if hash == "#/": filter = all
elif hash == "#/completed": filter = completed
elif hash == "#/active": filter = active
)
if hasItem(lenSuffix): if hasItem(lenSuffix):
entriesLen = parseInt getItem(lenSuffix) entriesLen = parseInt getItem(lenSuffix)
else: else:

View file

@ -1 +1 @@
--path: "../../src" --path: "../.."

35
examples/toychat.nim Normal file
View file

@ -0,0 +1,35 @@
include login
const
message = "message"
type
TextMessage = ref object
name, content: kstring
var allMessages: seq[TextMessage] = @[]
proc doSendMessage() =
let inputField = getVNodeById(message)
allMessages.add(TextMessage(name: "you", content: inputField.getInputText))
inputField.setInputText ""
proc main(): VNode =
result = buildHtml(tdiv):
loginDialog()
tdiv:
table:
for m in allMessages:
tr:
td:
bold:
text m.name
td:
text m.content
tdiv:
if loggedIn:
label(`for` = message):
text "Message: "
input(class = "input", id = message, onkeyupenter = doSendMessage)
setRenderer main

View file

@ -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>

2
experiments/config.nims Normal file
View file

@ -0,0 +1,2 @@
--path: "../karax"
--path: ".."

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>Karax ECharts Test</title>
</head>
<body id="body">
<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="echartstest.js"></script>
</html>

View file

@ -0,0 +1,58 @@
import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]
type
Views = enum
Customers, Products
var
currentView = Customers
type
EChart* = ref object
proc echartsInit(n: Element): EChart {.importc: "echarts.init".}
proc setOption(x: EChart; option: JsonNode) {.importcpp.}
proc postRender(data: RouterData) =
if currentView == Products:
let myChart = echartsInit(kdom.getElementById("echartSection"))
# specify chart configuration item and data
let option = %*{
"title": {
"text": "ECharts entry example"
},
"tooltip": {},
"legend": {
"data": ["Sales"]
},
"xAxis": {
"data": ["shirt","cardign","chiffon shirt","pants","heels","socks"]
},
"yAxis": {},
"series": [{
"name": "Sales",
"type": "bar",
"data": [5, 20, 36, 10, 10, 20]
}]
}
myChart.setOption(option)
proc createDom(data: RouterData): VNode =
let hash = data.hashPart
if hash == cstring"#/Products": currentView = Products
else: currentView = Customers
result = buildHtml(tdiv):
ul(class = "tabs"):
for v in low(Views)..high(Views):
li:
a(href = "#/" & $v):
text cstring($v)
if currentView == Products:
tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))
tdiv:
text "other section"
setRenderer createDom, "ROOT", postRender
setForeignNodeId "echartSection"

65
experiments/menus.nim Normal file
View file

@ -0,0 +1,65 @@
## Example program that shows how to create menus with Karax.
include prelude
import jstrutils, kdom
proc contentA(): VNode =
result = buildHtml(tdiv):
text "content A"
proc contentB(): VNode =
result = buildHtml(tdiv):
text "content B"
proc contentC(): VNode =
result = buildHtml(tdiv):
text "content C"
type
MenuItemHandler = proc(): VNode
var content: MenuItemHandler = contentA
proc menuAction(x: MenuItemHandler): proc() =
result = proc() = content = x
proc buildMenu(): VNode =
result = buildHtml(tdiv):
nav(class="navbar is-primary"):
tdiv(class="navbar-brand"):
a(class="navbar-item", onclick = menuAction(contentA)):
strong:
text "My awesome menu"
tdiv(id="navMenuTransparentExample", class="navbar-menu"):
tdiv(class = "navbar-start"):
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentB)):
text "Masters"
tdiv(class="navbar-dropdown is-boxed"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Inventory"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Product"
tdiv(class="navbar-item has-dropdown is-hoverable"):
a(class="navbar-link", onclick = menuAction(contentC)):
text "Transactions"
tdiv(class = "navbar-dropdown is-boxed", id="blogDropdown"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Bill"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Purchase Return"
a(class="navbar-item", onclick = menuAction(contentC)):
text "Sale Bill"
hr(class="navbar-divider"):
a(class="navbar-item", onclick = menuAction(contentC)):
text "Stock Adjustment"
content()
setRenderer buildMenu

View file

@ -0,0 +1,18 @@
include karax / prelude
import karax / kdom
proc myInput: VNode =
result = buildHtml:
input()
var inp = vnodeToDom(myInput())
proc myAwesomeComponent(x: Node): VNode =
result = dthunk(x)
proc main: VNode =
result = myAwesomeComponent(inp)
setRenderer main

View file

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

View file

@ -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>

View file

@ -0,0 +1,25 @@
## Example that shows how to accomplish an "infinitely scrolling" app.
include karax / prelude
import karax / [kdom, vstyles]
var entries: seq[cstring] = @[]
for i in 1..500:
entries.add(cstring("Entry ") & &i)
proc scrollEvent(ev: Event; n: VNode) =
let d = n.dom
if d != nil and inViewport(d.lastChild):
# "load" more data:
for i in 1..50:
entries.add(cstring("Loaded Entry ") & &i)
proc createDom(): VNode =
result = buildHtml():
tdiv(onscroll=scrollEvent, style=style(
(StyleAttr.height, cstring"400px"), (StyleAttr.overflow, cstring"scroll"))):
for x in entries:
tdiv:
text x
setRenderer createDom

View file

@ -0,0 +1,752 @@
## Raw DOM manipulation. No DOM diff'ing, no cry.
import macros, tables
from strutils import startsWith, toLowerAscii, toUpperAscii
when defined(js):
type kstring* = cstring
else:
type kstring* = string
type
Tag* {.pure.} = enum
text = "#text",
html, head, title, base, link, meta, style,
script, noscript,
body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6,
header, footer, address, main
p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd,
figure, figcaption,
tdiv = "div",
a, em, strong, small,
strikethrough = "s", cite, quote,
dfn, abbr, data, time, code, `var` = "var", samp,
kdb, sub, sup, italic = "i", bold = "b", underlined = "u",
mark, ruby, rt, rp, bdi, dbo, span, br, wbr,
ins, del, img, iframe, embed, `object` = "object",
param, video, audio, source, track, canvas, map,
area, svg, math,
table, caption, colgroup, col, tbody, thead,
tfoot, tr, td, th,
form, fieldset, legend, label, input, button,
select, datalist, optgroup, option, textarea,
keygen, output, progress, meter,
details, summary, command, menu
type
EventKind* {.pure.} = enum ## The events supported by the virtual DOM.
onclick, ## An element is clicked.
oncontextmenu, ## An element is right-clicked.
ondblclick, ## An element is double clicked.
onkeyup, ## A key was released.
onkeydown, ## A key is pressed.
onkeypressed, # A key was pressed.
onfocus, ## An element got the focus.
onblur, ## An element lost the focus.
onchange, ## The selected value of an element was changed.
onscroll, ## The user scrolled within an element.
onmousedown, ## A pointing device button (usually a mouse) is pressed
## on an element.
onmouseenter, ## A pointing device is moved onto the element that
## has the listener attached.
onmouseleave, ## A pointing device is moved off the element that
## has the listener attached.
onmousemove, ## A pointing device is moved over an element.
onmouseout, ## A pointing device is moved off the element that
## has the listener attached or off one of its children.
onmouseover, ## A pointing device is moved onto the element that has
## the listener attached or onto one of its children.
onmouseup, ## A pointing device button is released over an element.
ondrag, ## An element or text selection is being dragged (every 350ms).
ondragend, ## A drag operation is being ended (by releasing a mouse button
## or hitting the escape key).
ondragenter, ## A dragged element or text selection enters a valid drop target.
ondragleave, ## A dragged element or text selection leaves a valid drop target.
ondragover, ## An element or text selection is being dragged over a valid
## drop target (every 350ms).
ondragstart, ## The user starts dragging an element or text selection.
ondrop, ## An element is dropped on a valid drop target.
onsubmit, ## A form is submitted
oninput, ## An input value changes
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
macro buildLookupTables(): untyped =
var a = newTree(nnkBracket)
for i in low(Tag)..high(Tag):
let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind):
e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} =
const
toTagName*: array[Tag, kstring] = a
toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e)
buildLookupTables()
# ---------------- CSS ----------------------------------------------
type
StyleAttr* {.pure.} = enum
## The style attributes supported by the virtual DOM.
## Reference: https://www.w3schools.com/jsref/dom_obj_style.asp
alignContent
alignItems
alignSelf
animation
animationDelay
animationDirection
animationDuration
animationFillMode
animationIterationCount
animationName
animationTimingFunction
animationPlayState
background
backgroundAttachment
backgroundColor
backgroundImage
backgroundPosition
backgroundRepeat
backgroundClip
backgroundOrigin
backgroundSize
backfaceVisibility
border
borderBottom
borderBottomColor
borderBottomLeftRadius
borderBottomRightRadius
borderBottomStyle
borderBottomWidth
borderCollapse
borderColor
borderImage
borderImageOutset
borderImageRepeat
borderImageSlice
borderImageSource
borderImageWidth
borderLeft
borderLeftColor
borderLeftStyle
borderLeftWidth
borderRadius
borderRight
borderRightColor
borderRightStyle
borderRightWidth
borderSpacing
borderStyle
borderTop
borderTopColor
borderTopLeftRadius
borderTopRightRadius
borderTopStyle
borderTopWidth
borderWidth
bottom
boxDecorationBreak
boxShadow
boxSizing
captionSide
clear
clip
color
columnCount
columnFill
columnGap
columnRule
columnRuleColor
columnRuleStyle
columnRuleWidth
columns
columnSpan
columnWidth
content
counterIncrement
counterReset
cursor
direction
display
emptyCells
filter
flex
flexBasis
flexDirection
flexFlow
flexGrow
flexShrink
flexWrap
cssFloat
font
fontFamily
fontSize
fontSizeAdjust
fontStretch
fontStyle
fontVariant
fontWeight
hangingPunctuation
height
hyphens
icon
imageOrientation
justifyContent
left
letterSpacing
lineHeight
listStyle
listStyleImage
listStylePosition
listStyleType
margin
marginBottom
marginLeft
marginRight
marginTop
maxHeight
maxWidth
minHeight
minWidth
navDown
navIndex
navLeft
navRight
navUp
opacity
order
orphans
outline
outlineColor
outlineOffset
outlineStyle
outlineWidth
overflow
overflowX
overflowY
padding
paddingBottom
paddingLeft
paddingRight
paddingTop
pageBreakAfter
pageBreakBefore
pageBreakInside
perspective
perspectiveOrigin
pointerEvents
position
quotes
resize
right
scrollbar3dLightColor # note: missing in w3schools reference
scrollbarArrowColor # note: missing in w3schools reference
scrollbarBaseColor # note: missing in w3schools reference
scrollbarDarkshadowColor # note: missing in w3schools reference
scrollbarFaceColor # note: missing in w3schools reference
scrollbarHighlightColor # note: missing in w3schools reference
scrollbarShadowColor # note: missing in w3schools reference
scrollbarTrackColor # note: missing in w3schools reference
tableLayout
tabSize
textAlign
textAlignLast
textDecoration
textDecorationColor
textDecorationLine
textDecorationStyle
textIndent
textJustify
textOverflow
textShadow
textTransform
top
transform
transformOrigin
transformStyle
transition
transitionDelay
transitionDuration
transitionProperty
transitionTimingFunction
unicodeBidi
userSelect
verticalAlign
visibility
whiteSpace
width
wordBreak
wordSpacing
wordWrap
widows
zIndex
macro buildStyleLookupTable(): untyped =
var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} =
const
toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e)
buildStyleLookupTable()
type
StyleObj {.importc.} = object
background*: cstring
backgroundAttachment*: cstring
backgroundColor*: cstring
backgroundImage*: cstring
backgroundPosition*: cstring
backgroundRepeat*: cstring
border*: cstring
borderBottom*: cstring
borderBottomColor*: cstring
borderBottomStyle*: cstring
borderBottomWidth*: cstring
borderColor*: cstring
borderLeft*: cstring
borderLeftColor*: cstring
borderLeftStyle*: cstring
borderLeftWidth*: cstring
borderRight*: cstring
borderRightColor*: cstring
borderRightStyle*: cstring
borderRightWidth*: cstring
borderStyle*: cstring
borderTop*: cstring
borderTopColor*: cstring
borderTopStyle*: cstring
borderTopWidth*: cstring
borderWidth*: cstring
bottom*: cstring
captionSide*: cstring
clear*: cstring
clip*: cstring
color*: cstring
cursor*: cstring
direction*: cstring
display*: cstring
emptyCells*: cstring
cssFloat*: cstring
font*: cstring
fontFamily*: cstring
fontSize*: cstring
fontStretch*: cstring
fontStyle*: cstring
fontVariant*: cstring
fontWeight*: cstring
height*: cstring
left*: cstring
letterSpacing*: cstring
lineHeight*: cstring
listStyle*: cstring
listStyleImage*: cstring
listStylePosition*: cstring
listStyleType*: cstring
margin*: cstring
marginBottom*: cstring
marginLeft*: cstring
marginRight*: cstring
marginTop*: cstring
maxHeight*: cstring
maxWidth*: cstring
minHeight*: cstring
minWidth*: cstring
overflow*: cstring
padding*: cstring
paddingBottom*: cstring
paddingLeft*: cstring
paddingRight*: cstring
paddingTop*: cstring
pageBreakAfter*: cstring
pageBreakBefore*: cstring
pointerEvents*: cstring
position*: cstring
right*: cstring
scrollbar3dLightColor*: cstring
scrollbarArrowColor*: cstring
scrollbarBaseColor*: cstring
scrollbarDarkshadowColor*: cstring
scrollbarFaceColor*: cstring
scrollbarHighlightColor*: cstring
scrollbarShadowColor*: cstring
scrollbarTrackColor*: cstring
tableLayout*: cstring
textAlign*: cstring
textDecoration*: cstring
textIndent*: cstring
textTransform*: cstring
transform*: cstring
top*: cstring
verticalAlign*: cstring
visibility*: cstring
width*: cstring
wordSpacing*: cstring
zIndex*: int
Style* = ref StyleObj
ElementObj {.importc.} = object
id*, class*, value*: cstring
disabled*: bool
style*: Style
Element* = ref ElementObj
DocumentObj {.importc.} = object
Document = ref DocumentObj
proc setStyle(s: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyles*(e: Element; pairs: openArray[(StyleAttr, kstring)]) =
for x in pairs:
e.style.setStyle(toStyleAttrName[x[0]], x[1])
# ------- Tree manipulation ---------------------------
var document {.importc.}: Document
proc parent*(x: Element): Element {.importcpp: "#.parentNode".}
proc up*(x: Element; className: cstring): Element =
result = x
while result != nil and result.class != className:
result = result.parent
proc len*(x: Element): int {.importcpp: "#.childNodes.length".}
proc `[]`*(x: Element; idx: int): Element {.importcpp: "#.childNodes[#]".}
proc getElementById*(id: cstring): Element {.importc: "document.getElementById", nodecl.}
proc add*(n, child: Element) {.importcpp: "appendChild".}
proc removeChild(n, child: Element) {.importcpp.}
proc replaceChild(n, newNode, oldNode: Element) {.importcpp.}
proc replace*(self, by: Element) = replaceChild(self.parent, by, self)
proc delete*(self: Element) = removeChild(self.parent, self)
proc insertBefore(n, newNode, before: Element) {.importcpp.}
proc insert*(before, newNode: Element) = insertBefore(before.parent, newNode, before)
proc createTextNode(d: Document, text: cstring): Element {.importcpp.}
proc text*(s: kstring): Element = createTextNode(document, s)
iterator items*(n: Element): Element =
for i in 0..<n.len: yield n[i]
proc createElement(d: Document, identifier: cstring): Element {.importcpp.}
proc setAttr*(n: Element; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
proc tree*(kind: Tag; kids: varargs[Element]): Element =
result = createElement(document, toTagName[kind])
for k in kids: result.add k
proc tree*(kind: Tag; attrs: openarray[(kstring, kstring)];
kids: varargs[Element]): Element =
result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1])
# other arbitrary stuff belonging to Element
proc focus*(e: Element) {.importcpp.}
proc scrollIntoView*(e: Element) {.importcpp.}
proc blur*(e: Element) {.importcpp.}
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
# -------------- Timer handling --------------------
type
Timeout* {.importc.} = ref object of RootObj
proc setTimeout*(action: proc(); ms: int): Timeout {.importc, nodecl.}
proc clearTimeout*(t: Timeout) {.importc, nodecl.}
# -------------- Event handling --------------------
type
EventObj {.importc.} = object
target*: Element
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
code*: cstring
isComposing*: bool
key*: cstring
keyCode*: int
location*: int
Event* = ref EventObj
EventHandler* = proc (ev: Event) {.closure.}
proc addEventListener(e: Element, ev: cstring, cb: proc(ev: Event),
useCapture = false) {.importcpp.}
proc addEventHandler*(e: Element; k: EventKind; action: EventHandler) =
case k
of EventKind.onkeyuplater:
proc laterWrapper(): EventHandler =
let action = action
var timer: Timeout
result = proc (ev: Event) =
proc wrapper() = action(ev)
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
e.addEventListener("keyup", laterWrapper())
of EventKind.onkeyupenter:
proc enterWrapper(): EventHandler =
let action = action
result = proc (ev: Event) =
if ev.keyCode == 13: action(ev)
e.addEventListener("keyup", enterWrapper())
else:
e.addEventListener(toEventName[k], action)
proc addEventHandler*(e: Element; k: EventKind; action: proc()) =
addEventHandler e, k, (proc (ev: Event) = action())
proc prepareDragData*(ev: Event; datatype, data: cstring)
{.importcpp: "#.dataTransfer.setData(@)".}
proc recvDragData*(ev: Event; datatype: cstring): cstring
{.importcpp: "#.dataTransfer.getData(@)".}
proc preventDefault*(ev: Event) {.importcpp.}
# ------------------ Init handling -----------------------------------
proc replaceById*(newTree: Element; id: cstring = "ROOT") =
let x = getElementById(id)
x.parent.replaceChild(newTree, x)
newTree.id = id
proc setWindowOnload(h: EventHandler) {.importcpp: "window.onload = #".}
proc setInitializer*(initializer: proc (hashPart: kstring): Element;
root: cstring = "ROOT") =
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
setWindowOnload proc (ev: Event) =
replaceById root, initializer(hashPart)
onhashchange = proc () =
replaceById root, initializer(hashPart)
# ------------------ DSL section -------------------------------------
const
StmtContext = ["inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value"]
type
ComponentKind {.pure.} = enum
None,
Tag
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"Tag").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(Tag.html)..ord(Tag.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc getName(n: NimNode): string =
case n.kind
of nnkIdent:
result = $n.ident
of nnkAccQuoted:
result = ""
for i in 0..<n.len:
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
result = n.strVal
else:
#echo repr n
expectKind(n, nnkIdent)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
# If expressions need to be distinguished from if statements. Since
# we know we start in a statement context, it's pretty simple to
# figure out expression contexts: In calls everything is an expression
# (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil:
result = newCall(bindSym"add", tmpContext, n)
else:
result = n
of nnkForStmt, nnkIfExpr, nnkElifExpr, nnkElseExpr,
nnkOfBranch, nnkElifBranch, nnkExceptBranch, nnkElse,
nnkConstDef, nnkWhileStmt, nnkIdentDefs, nnkVarTuple:
# recurse for the last son:
result = copyNimTree(n)
let L = n.len
assert n.len == result.len
if L > 0:
result[L-1] = tcall2(result[L-1], tmpContext)
of nnkStmtList, nnkStmtListExpr, nnkWhenStmt, nnkIfStmt, nnkTryStmt,
nnkFinally:
# recurse for every child:
result = copyNimNode(n)
for x in n:
result.add tcall2(x, tmpContext)
of nnkCaseStmt:
# recurse for children, but don't add call for case ident
result = copyNimNode(n)
result.add n[0]
for i in 1 ..< n.len:
result.add tcall2(n[i], tmpContext)
of nnkProcDef:
let name = getName n[0]
if name.startsWith"on":
# turn it into an anon proc:
let anon = copyNimTree(n)
anon[0] = newEmptyNode()
if tmpContext == nil:
error "no Element to attach the event handler to"
else:
result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else:
result = n
of nnkVarSection, nnkLetSection, nnkConstSection:
result = n
of nnkCallKinds - {nnkInfix}:
let op = getName(n[0])
if isComponent(op) == ComponentKind.Tag:
let tmp = genSym(nskLet, "tmp")
let call = newCall(bindSym"tree", newDotExpr(bindSym"Tag", n[0]))
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, call))
for i in 1 ..< n.len:
# named parameters are transformed into attributes or events:
let x = n[i]
if x.kind == nnkExprEqExpr:
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1])
elif key in SpecialAttrs:
result.add newDotAsgn(tmp, key, x[1])
elif eqIdent(key, "style"):
result.add newCall(bindSym"applyStyles", tmp, toKstring x[1])
elif eqIdent(key, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
elif eqIdent(x, "setFocus"):
result.add newCall(bindSym"focus", tmp)
else:
result.add tcall2(x, tmp)
if tmpContext == nil:
result.add tmp
else:
result.add newCall(bindSym"add", tmpContext, tmp)
elif tmpContext != nil and op notin StmtContext:
var hasEventHandlers = false
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
hasEventHandlers = true
break
if not hasEventHandlers:
result = newCall(bindSym"add", tmpContext, n)
else:
let tmp = genSym(nskLet, "tmp")
var slicedCall = newCall(n[0])
let ex = newTree(nnkStmtListExpr)
ex.add newEmptyNode() # will become the let statement
for i in 1..<n.len:
let it = n[i]
if it.kind in {nnkProcDef, nnkStmtList}:
ex.add tcall2(it, tmp)
else:
slicedCall.add it
ex[0] = newLetStmt(tmp, slicedCall)
ex.add tmp
result = newCall(bindSym"add", tmpContext, ex)
elif op == "!" and n.len == 2:
result = n[1]
else:
result = n
else:
result = n
macro buildHtml*(tag, children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
var call: NimNode
if tag.kind in nnkCallKinds:
call = tag
else:
call = newCall(tag)
call.add body(kids)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
macro buildHtml*(children: untyped): Element =
let kids = newProc(procType=nnkDo, body=children)
expectKind kids, nnkDo
result = tcall2(body(kids), nil)
when defined(debugKaraxDsl):
echo repr result
macro flatHtml*(tag: untyped): Element =
result = tcall2(tag, nil)
when defined(debugKaraxDsl):
echo repr result

View file

@ -0,0 +1,151 @@
import knete, widgets
import karax / jstrutils
type
Attachable* = ref object of RootObj ## an element that is attachable to
## DOM elements.
attachedTo*: seq[Element]
TaskId = int
Task = ref object
id: TaskId
name, desc: kstring
ColumnId = distinct int
Column = ref object of Attachable
id: ColumnId
header: kstring
tasks: seq[Task]
Board = ref object
columns: seq[Column]
var b = Board(columns: @[Column(id: ColumnId 0, header: "To Do"),
Column(id: ColumnId 1, header: "Doing"),
Column(id: ColumnId 2, header: "Done")])
proc removeTask(t: TaskId) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
if c.tasks[i].id == t:
delete(c.tasks, i)
break
proc moveTask(t: TaskId; dest: Column) =
for c in b.columns:
for i in 0 ..< c.tasks.len:
let task = c.tasks[i]
if task.id == t:
delete(c.tasks, i)
dest.tasks.add task
return
var nextTaskId = 0 ## negative because they are not yet backed
## up by the database
proc createTask(c: Column; name, desc: kstring): Task =
dec nextTaskId
result = Task(id: TaskId nextTaskId, name: name, desc: desc)
c.tasks.add result
# --------------------- UI -----------------------------------------------
proc renderTask(t: Task): Element =
proc dodelete =
removeTask t.id
delete result
proc dragstart(ev: Event) =
ev.prepareDragData("taskid", ev.target.id)
result = buildHtml():
tdiv(draggable="true", ondragstart=dragstart, id = &t.id):
bindField t.name
br()
bindField t.desc
button(onclick = dodelete):
text cross
type
NewTaskPanel = object
c: Column
e: Element
proc open(p: var NewTaskPanel; c: Column) =
p.e.style.display = "block"
p.c = c
proc newTaskDialog(): NewTaskPanel =
var nameInp = buildHtml():
input(setFocus = true)
var descInp = buildHtml():
input()
proc close =
result.e.style.display = "none"
proc submit =
let t = createTask(result.c, nameInp.value, descInp.value)
close()
result.c.attachedTo[0].add renderTask(t)
result.e = buildHtml():
tdiv(style={display: "none", position: "fixed",
left: "0", top: "0", width: "100%", height: "100%",
overflow: "auto",
backgroundColor: "rgb(0,0,0)",
backgroundColor: "rgba(0,0,0,0.4)", zIndex: "1"}):
tdiv(style={backgroundColor: "#fefefe",
margin: "15% auto", padding: "20px", border: "1px solid #888",
width: "80%"}):
span(onclick = close, style={color: "#aaa",
cssFloat: "right",
fontSize: "28px",
fontWeight: "bold"}):
text cross
p:
text "Task name"
nameInp
p:
text "Task description"
descInp
span(onclick = submit, style={color: "#0f0",
cssFloat: "left",
fontWeight: "bold"}):
text "Submit"
var dialog = newTaskDialog()
proc renderColumn(c: Column): Element =
proc doadd(c: Column): proc () =
result = proc() = dialog.open(c)
proc allowDrop(ev: Event) = ev.preventDefault()
proc drop(ev: Event) =
ev.preventDefault()
let data = ev.recvDragData("taskid")
moveTask(parseInt data, c)
ev.target.up("mycolumn").add(getElementById(data))
result = buildHtml():
tdiv(class = "mycolumn", style = {cssFloat: "left", width: "20%"},
ondrop = drop, ondragover = allowDrop):
tdiv(class = "myheader"):
bindField c.header
span(onclick = doadd(c)):
text plus
for t in c.tasks:
renderTask(t)
c.attachedTo.add result
proc renderBoard(b: Board): Element =
result = buildHtml(tdiv):
dialog.e
for c in b.columns:
renderColumn(c)
proc main(hashPart: kstring): Element =
result = renderBoard(b)
setInitializer main

View file

@ -0,0 +1,43 @@
## Common widget implemenations
import knete
const
cross* = "x"
plus* = kstring"+"
const
LEFT* = 37
UP* = 38
RIGHT* = 39
DOWN* = 40
TAB* = 9
ESC* = 27
ENTER* = 13
proc editable*(x: kstring; onchanged: proc (value: kstring);
isEdited = false): Element =
proc onenter(ev: Event) = ev.target.blur()
proc submit(ev: Event) =
onchanged(ev.target.value)
# close the loop: This is a common pattern in Knete.
replace(result, editable(ev.target.value, onchanged, false))
proc makeEditable() =
# close the loop: This is a common pattern in Knete.
replace(result, editable(x, onchanged, true))
result = buildHtml():
if isEdited:
input(class = "edit",
onblur = submit,
onkeyupenter = onenter,
value = x,
setFocus = true)
else:
span(onclick = makeEditable):
text x
template bindField*(field): Element =
editable field, proc (value: kstring) =
field = value

View file

@ -1,13 +1,16 @@
# Package # Package
version = "0.1.0" version = "1.2.1"
author = "Xored Software, Inc." 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"
srcDir = "src"
# Dependencies # Dependencies
requires "nim >= 0.16.1" requires "nim >= 0.18.0"
requires "ws"
requires "dotenv"
skipDirs = @["examples", "experiments", "tests"]
skipDirs = @["examples/todoapp"] bin = @["karax/tools/karun"]
installExt = @["nim"]

160
karax/autocomplete.nim Normal file
View file

@ -0,0 +1,160 @@
import karax, karaxdsl, vdom, kdom, jstrutils
type
AutocompleteState* = ref object
choices, candidates: seq[cstring]
selected, maxMatches: int
showCandidates, controlPressed: bool
proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState =
## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum
## number of elements to show.
AutocompleteState(choices: choices, candidates: @[],
selected: -1, maxMatches: maxMatches, showCandidates: false,
controlPressed: false)
proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode =
var inp: VNode
proc commit(ev: Event) =
s.showCandidates = false
onselection(inp.dom.value)
when false:
if inp.dom != nil:
echo "setting to A ", inp.dom.value.isNil
result.text = inp.dom.value
else:
echo "setting to B ", inp.text.isNil
result.text = inp.text
for e in result.events:
if e[0] == EventKind.onchange:
e[1](ev, result)
proc onkeyuplater(ev: Event; n: VNode) =
if not s.controlPressed:
let v = n.value
s.candidates.setLen 0
for c in s.choices:
if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c)
proc onkeydown(ev: Event; n: VNode) =
const
LEFT = 37
UP = 38
RIGHT = 39
DOWN = 40
TAB = 9
ESC = 27
ENTER = 13
# UP: Move focus to the previous item. If on first item, move focus to the input.
# If on the input, move focus to last item.
# DOWN: Move focus to the next item. If on last item, move focus to the input.
# If on the input, move focus to the first item.
# ESCAPE: Close the menu.
# ENTER: Select the currently focused item and close the menu.
# TAB: Select the currently focused item, close the menu, and
# move focus to the next focusable element
s.controlPressed = false
case ev.keyCode
of UP:
s.controlPressed = true
s.showCandidates = true
if s.selected > 0:
dec s.selected
n.setInputText s.candidates[s.selected]
of DOWN:
s.controlPressed = true
s.showCandidates = true
if s.selected < s.candidates.len - 1:
inc s.selected
n.setInputText s.candidates[s.selected]
of ESC:
s.showCandidates = false
s.controlPressed = true
of ENTER:
s.controlPressed = true
# inp.setInputText s.choices[i]
commit(ev)
else:
discard
proc window(s: AutocompleteState): (int, int) =
var first, last: int
if s.selected >= 0:
first = s.selected - (s.maxMatches div 2)
last = s.selected + (s.maxMatches div 2)
if first < 0: first = 0
# too few because we had to trim first?
if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1
else:
first = 0
last = s.maxMatches - 1
if last > high(s.candidates): last = high(s.candidates)
# still too few because we're at the end?
if (last - first + 1) < s.maxMatches:
first = last - s.maxMatches + 1
if first < 0: first = 0
result = (first, last)
inp = buildHtml:
input(onkeyuplater = onkeyuplater,
onkeydown = onkeydown,
# onblur = proc (ev: Event; n: VNode) = commit(ev),
onfocus = proc (ev: Event; n: VNode) =
onkeyuplater(ev, n)
s.showCandidates = true)
proc select(i: int): proc(ev: Event; n: VNode) =
result = proc(ev: Event; n: VNode) =
s.selected = i
s.showCandidates = false
inp.setInputText s.choices[i]
commit(ev)
result = buildHtml(table):
tr:
td:
inp
if s.showCandidates:
let (first, last) = window(s)
for i in first..last:
tr:
td(onclick = select(i), class = cstring"button " &
(if i == s.selected: cstring"is-primary" else: "")):
text s.candidates[i]
when isMainModule:
const suggestions = @[cstring"ActionScript",
"AppleScript",
"Asp",
"BASIC",
"C",
"C++",
"Clojure",
"COBOL",
"Erlang",
"Fortran",
"Groovy",
"Haskell",
"Java",
"JavaScript",
"Lisp",
"Nim",
"Perl",
"PHP",
"Python",
"Ruby",
"Scala",
"Scheme"]
var s = newAutocomplete(suggestions)
proc main(): VNode =
result = buildHtml(tdiv):
autocomplete(s, proc (s: cstring) = echo "now ", s)#:
# proc onchange(ev: Event; n: VNode) =
# echo "now selected ", n.kind
setRenderer(main)

114
karax/compact.nim Normal file
View file

@ -0,0 +1,114 @@
## Components in Karax are built by the ``.component`` macro annotation.
when defined(js):
import jdict, kdom
import macros, vdom, tables, strutils, kbase
when defined(js):
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
else:
var
vcomponents* = newTable[kstring, proc(args: seq[VNode]): VNode]()
type
ComponentKind* {.pure.} = enum
None,
Tag,
VNode
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent*(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"VNodeKind").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
case t
of "cstring":
result = quote do:
args[`index`].text
of "int", "VKey":
result = quote do:
args[`index`].intValue
of "bool":
result = quote do:
args[`index`].intValue != 0
elif t.endsWith"Kind":
result = quote do:
`symbolicType`(args[`index`].intValue)
else:
# just pass it along, maybe there is some conversion for it:
result = quote do:
args[`index`]
proc newname*(n: NimNode): NimNode =
if n.kind == nnkPostfix:
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident(n.strVal)
else:
result = n
when defined(js):
macro compact*(prc: untyped): untyped =
## A 'compact' tree generation proc is one that only depends on its
## inputs and should be stored as a compact virtual DOM tree and
## only expanded on demand (when its inputs changed).
var n = prc.copyNimNode
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
var isvirtual = ComponentKind.None
if rettype == "VNode":
isvirtual = ComponentKind.VNode
else:
error "component must return VNode", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
var unpackCall = newCall(n[0])
var counter = 0
for i in 1 ..< params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, kstring(key), val)
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result

32
karax/errors.nim Normal file
View file

@ -0,0 +1,32 @@
## Error handling logic for form input validation.
import karax, jdict
var
gerrorMsgs = newJDict[cstring, cstring]()
gerrorCounter = 0
proc hasErrors*(): bool = gerrorCounter != 0
proc hasError*(field: cstring): bool = gerrorMsgs.contains(field) and len(gerrorMsgs[field]) > 0
proc disableOnError*(): cstring = toDisabled(hasErrors())
proc getError*(field: cstring): cstring =
if not gerrorMsgs.contains(field):
result = ""
else:
result = gerrorMsgs[field]
proc setError*(field, msg: cstring) =
let previous = getError(field)
if len(msg) == 0:
if len(previous) != 0: dec(gerrorCounter)
else:
if len(previous) == 0: inc(gerrorCounter)
gerrorMsgs[field] = msg
proc clearErrors*() =
let m = gerrorMsgs
for k in m.keys:
setError(k, "")

View file

@ -11,6 +11,12 @@ proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(
proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".} proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
iterator keys*[K, V](d: JDict[K, V]): K =
var kkk: K
{.emit: ["for (", kkk, " in ", d, ") {"].}
yield kkk
{.emit: ["}"].}
type type
JSeq*[T] = ref object JSeq*[T] = ref object

67
karax/jjson.nim Normal file
View file

@ -0,0 +1,67 @@
## This module implements some small zero-overhead 'JsonNode' type
## and helper that maps directly to JavaScript objects.
type
JsonNode* {.importc.} = ref object
proc `[]`*(obj: JsonNode; fieldname: cstring): JsonNode {.importcpp: "#[#]".}
proc `[]`*(obj: JsonNode; index: int): JsonNode {.importcpp: "#[#]".}
proc `[]=`*[T](obj: JsonNode; fieldname: cstring; value: T)
{.importcpp: "#[#] = #".}
proc length(x: JsonNode): int {.importcpp: "#.length".}
proc len*(x: JsonNode): int = (if x.isNil: 0 else: x.length)
proc newJsonNode*(fields: varargs[(cstring, JsonNode)]): JsonNode =
result = JsonNode()
for f in fields:
result[f[0]] = f[1]
proc newJObject*(): JsonNode =
result = JsonNode()
proc newJArray*(elements: varargs[JsonNode]): JsonNode {.importcpp: "#".}
proc newJNull*(): JsonNode = nil
template `%`*(x: typed): JsonNode = cast[JsonNode](x)
template `%`*(x: string): JsonNode = cast[JsonNode](cstring x)
proc getNum*(x: JsonNode): int {.importcpp: "#".}
proc getInt*(x: JsonNode): int {.importcpp: "#".}
proc getStr*(x: JsonNode): cstring {.importcpp: "#".}
proc getFNum*(x: JsonNode): cstring {.importcpp: "#".}
iterator items*(x: JsonNode): JsonNode =
for i in 0..<len(x): yield x[i]
import macros
proc toJson(x: NimNode): NimNode {.compiletime.} =
case x.kind
of nnkBracket:
result = newCall(bindSym"newJArray")
for i in 0 ..< x.len:
result.add(toJson(x[i]))
of nnkTableConstr:
result = newCall(bindSym"newJsonNode")
for i in 0 ..< x.len:
x[i].expectKind nnkExprColonExpr
let key = x[i][0]
let a = if x[i].kind in {nnkIdent, nnkSym, nnkAccQuoted}:
newLit($key)
else:
key
result.add newTree(nnkPar, newCall(bindSym"cstring", a), toJson(x[i][1]))
of nnkCurly:
x.expectLen(0)
result = newCall(bindSym"newJObject")
of nnkNilLit:
result = newCall(bindSym"newJNull")
else:
result = newCall(bindSym"%", x)
macro `%*`*(x: untyped): untyped =
## Convert an expression to a JsonNode directly, without having to specify
## `%` for every element.
result = toJson(x)

View file

@ -4,8 +4,14 @@ proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.} proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.}
proc strip*(s: cstring): cstring {.importcpp: "#.trim()", nodecl.}
proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.} proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.} proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
proc containsIgnoreCase*(a, b: cstring): bool {.
importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.}
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}

17
karax/jwebsockets.nim Normal file
View file

@ -0,0 +1,17 @@
## Websockets support for the JSON backend.
type
MessageEvent* {.importc.} = ref object
data*: cstring
WebSocket* {.importc.} = ref object
onmessage*: proc (e: MessageEvent)
onopen*: proc (e: MessageEvent)
proc newWebSocket*(url, key: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc newWebSocket*(url: cstring): WebSocket
{.importcpp: "new WebSocket(@)".}
proc send*(w: WebSocket; data: cstring) {.importcpp.}

132
karax/kajax.nim Normal file
View file

@ -0,0 +1,132 @@
## Karax -- Single page applications for Nim.
## This module implements support for `ajax`:idx: socket
## handling.
import karax
import jsffi except `&`
import jscore
import dom
type
ProgressEvent* {.importc.}= object
loaded*: float
total*: float
FormData* {.importc.} = JsObject
HttpRequest* {.importc.} = ref object
ThisObj {.importc.} = ref object
readyState, status: int
responseText, statusText: cstring
XMLHttpRequestUpload* {.importc.} = JsObject
proc newFormData*(): FormData {.importcpp: "new FormData()".}
proc append*(f: FormData, key: cstring, value: Blob) {.importcpp:"#.append(@)".}
proc append*(f: FormData, key: cstring, value: cstring) {.importcpp:"#.append(@)".}
proc setRequestHeader*(r: HttpRequest; a, b: cstring) {.importcpp: "#.setRequestHeader(@)".}
proc statechange*(r: HttpRequest; cb: proc()) {.importcpp: "#.onreadystatechange = #".}
proc send*(r: HttpRequest; data: cstring) {.importcpp: "#.send(#)".}
proc send*(r: HttpRequest, data: Blob) {.importcpp: "#.send(#)".}
proc open*(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest*(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
when not declared(dom.File):
type
DomFile = ref FileObj
FileObj {.importc.} = object of Blob
lastModified: int
name: cstring
else:
type
DomFile = dom.File
proc uploadFile*(url: cstring, file: Blob, onprogress :proc(data: ProgressEvent),
cont: proc (httpStatus: int; response: cstring);
headers: openarray[(cstring, cstring)] = []) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
proc upload(r: HttpRequest):XMLHttpRequestUpload {.importcpp: "#.upload".}
var this {.importc: "this".}: ThisObj
var formData = newFormData()
formData.append("upload_file",file)
formData.append("filename", DomFile(file).name)
let ajax = newRequest()
ajax.open("POST", url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
contWrapper(this.status, this.responseText)
ajax.upload.onprogress = onprogress
ajax.send(formData.to(cstring))
proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi,
useBinary: bool = false,
blob: Blob = nil) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
if doRedraw: redraw(kxi)
var this {.importc: "this".}: ThisObj
let ajax = newRequest()
ajax.open(meth, url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
if this.status == 200:
contWrapper(this.status, this.responseText)
else:
contWrapper(this.status, this.responseText)
if useBinary:
ajax.send(blob)
else:
ajax.send(data)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, doRedraw, kxi)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: Blob;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, "", cont, doRedraw, kxi, true, data)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, doRedraw, kxi)
proc ajaxPut*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("PUT", url, headers, data, cont, doRedraw, kxi)
proc ajaxDelete*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -2,18 +2,21 @@
import kdom, vdom, jstrutils, compact, jdict, vstyles import kdom, vdom, jstrutils, compact, jdict, vstyles
export kdom.Event export kdom.Event, kdom.Blob
when defined(nimNoNil):
{.experimental: "notnil".}
proc kout*[T](x: T) {.importc: "console.log", varargs.} proc kout*[T](x: T) {.importc: "console.log", varargs.}
## the preferred way of debugging karax applications. ## The preferred way of debugging karax applications.
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
@ -22,15 +25,19 @@ type
parent, current: Node parent, current: Node
type type
RouterData* = ref object ## information that is passed to the 'renderer' callback
hashPart*: cstring ## the hash part of the URL for routing.
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 (): VNode {.closure.} renderer: proc (data: RouterData): VNode {.closure.}
currentTree: VNode currentTree: VNode
postRenderCallback: proc () 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]
@ -38,8 +45,10 @@ type
runCount: int runCount: int
components: seq[ComponentPair] components: seq[ComponentPair]
surpressRedraws*: bool surpressRedraws*: bool
byId: JDict[cstring, VNode]
when defined(stats): when defined(stats):
recursion: int recursion: int
orphans: JDict[cstring, bool]
var var
@ -69,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 =
@ -83,7 +110,7 @@ proc wrapEvent(d: Node; n: VNode; k: EventKind;
let action = action let action = action
let n = n let n = n
result = proc (ev: Event) = result = proc (ev: Event) =
if ev.keyCode == 13: keyeventBody() if cast[KeyboardEvent](ev).keyCode == 13: keyeventBody()
proc laterWrapper(): NativeEventHandler = proc laterWrapper(): NativeEventHandler =
let action = action let action = action
@ -97,59 +124,75 @@ 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
if n.id != nil: kxi.byId[n.id] = n
proc applyEvents(n: VNode; kxi: KaraxInstance) = proc applyEvents(n: VNode) =
let dest = n.dom let dest = n.dom
for i in 0..<len(n.events): for i in 0..<len(n.events):
n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1]) n.events[i][2] = wrapEvent(dest, n, n.events[i][0], n.events[i][1])
proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node = proc getVNodeById*(id: cstring; kxi: KaraxInstance = kxi): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if kxi.byId.contains(id):
result = kxi.byId[id]
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
elif n.kind == VNodeKind.verbatim:
result = document.createElement("div")
result.innerHTML = n.text
attach n
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
elif n.kind == VNodeKind.dthunk: elif n.kind == VNodeKind.dthunk:
result = callThunk(dcomponents[n.text], n) result = n.dom #callThunk(dcomponents[n.text], n)
assert result != nil
#n.key = result.key #n.key = result.key
attach n attach n
return result return result
elif n.kind == VNodeKind.component: elif n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
if x.realDomImpl != nil: return x.realDomImpl(x)
if x.onAttachImpl != nil: x.onAttachImpl(x) if x.onAttachImpl != nil: x.onAttachImpl(x)
assert x.renderImpl != nil assert x.renderImpl != nil
if x.expanded == nil: if x.expanded == nil:
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
@ -162,22 +205,26 @@ proc vnodeToDom*(n: VNode; kxi: KaraxInstance): Node =
for k, v in attrs(n): for k, v in attrs(n):
if v != nil: if v != nil:
result.setAttr(k, v) result.setAttr(k, v)
applyEvents(n, kxi) applyEvents(n)
if n == kxi.toFocusV and kxi.toFocus.isNil: if kxi != nil and n == kxi.toFocusV and kxi.toFocus.isNil:
kxi.toFocus = result kxi.toFocus = result
if not n.style.isNil: applyStyle(result, n.style) if not n.style.isNil: applyStyle(result, n.style)
proc same(n: VNode, e: Node; nesting = 0): bool = proc same(n: VNode, e: Node; nesting = 0): bool =
if kxi.orphans.contains(n.id): return true
if n.kind == VNodeKind.component: if n.kind == VNodeKind.component:
result = same(VComponent(n).expanded, e, nesting+1) result = same(VComponent(n).expanded, e, nesting+1)
elif n.kind == VNodeKind.verbatim:
result = true
elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk: elif n.kind == VNodeKind.vthunk or n.kind == VNodeKind.dthunk:
# we don't check these for now: # we don't check these:
result = true result = true
elif toTag[n.kind] == e.nodename: elif toTag[n.kind] == e.nodename:
result = true result = true
if n.kind != VNodeKind.text: if n.kind != VNodeKind.text:
if e.len != n.len: # BUGFIX: Microsoft's Edge gives the textarea a child containing the text node!
echo "expected ", e.len, " real ", n.len, toTag[n.kind], " nesting ", nesting if e.len != n.len and n.kind != VNodeKind.textarea:
echo "expected ", n.len, " real ", e.len, " ", toTag[n.kind], " nesting ", nesting
return false return false
for i in 0 ..< n.len: for i in 0 ..< n.len:
if not same(n[i], e[i], nesting+1): return false if not same(n[i], e[i], nesting+1): return false
@ -206,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
@ -220,12 +267,20 @@ proc eq(a, b: VNode): EqResult =
if a.kind == VNodeKind.text: if a.kind == VNodeKind.text:
if a.text != b.text: if a.text != b.text:
when defined(profileKarax): inc reasons[deText] when defined(profileKarax): inc reasons[deText]
return similar return different # similar
elif a.kind == VNodeKind.vthunk or a.kind == VNodeKind.dthunk: elif a.kind == VNodeKind.vthunk:
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:
if a.dom == b.dom:
return identical
else: # fix #119
return different
elif a.kind == VNodeKind.verbatim:
if a.text != b.text:
return different
elif b.kind == VNodeKind.component: elif b.kind == VNodeKind.component:
# different component names mean different components: # different component names mean different components:
if a.text != b.text: if a.text != b.text:
@ -238,12 +293,20 @@ proc eq(a, b: VNode): EqResult =
when defined(profileKarax): inc reasons[deComponent] when defined(profileKarax): inc reasons[deComponent]
return different return different
return componentsIdentical return componentsIdentical
if a.class != b.class: #if:
when defined(profileKarax): inc reasons[deClass] # when defined(profileKarax): inc reasons[deClass]
return different # return different
if 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
@ -252,7 +315,9 @@ proc updateStyles(newNode, oldNode: VNode) =
if oldNode.dom != nil: if oldNode.dom != nil:
if newNode.style != nil: applyStyle(oldNode.dom, newNode.style) if newNode.style != nil: applyStyle(oldNode.dom, newNode.style)
else: oldNode.dom.style = Style() else: oldNode.dom.style = Style()
oldNode.dom.class = newNode.class
oldNode.style = newNode.style oldNode.style = newNode.style
oldNode.class = newNode.class
proc updateAttributes(newNode, oldNode: VNode) = proc updateAttributes(newNode, oldNode: VNode) =
# we keep the oldNode, but take over the attributes from the new node: # we keep the oldNode, but take over the attributes from the new node:
@ -266,38 +331,44 @@ 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, kxi) applyEvents(oldNode)
proc printV(n: VNode; depth: cstring = "") = when false:
kout depth, cstring($n.kind), cstring"key ", n.index proc printV(n: VNode; depth: cstring = "") =
#for k, v in pairs(n.style): kout depth, cstring($n.kind), cstring"key ", n.index
# kout depth, "style: ", k, v #for k, v in pairs(n.style):
if n.kind == VNodeKind.component: # kout depth, "style: ", k, v
let nn = VComponent(n) if n.kind == VNodeKind.component:
if nn.expanded != nil: printV(nn.expanded, ">>" & depth) let nn = VComponent(n)
elif n.kind == VNodeKind.text: if nn.expanded != nil: printV(nn.expanded, ">>" & depth)
kout depth, n.text elif n.kind == VNodeKind.text:
for i in 0 ..< n.len: kout depth, n.text
printV(n[i], depth & " ") for i in 0 ..< n.len:
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) =
@ -311,26 +382,41 @@ 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.kind == VNodeKind.component: if n.kind == VNodeKind.component:
let x = VComponent(n) let x = VComponent(n)
if x.onDetachImpl != nil: x.onDetachImpl(x) if x.onDetachImpl != nil: x.onDetachImpl(x)
@ -343,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),
@ -370,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
@ -470,11 +542,7 @@ proc applyComponents(kxi: KaraxInstance) =
let newNode = kxi.components[i].newNode let newNode = kxi.components[i].newNode
when defined(karaxDebug): when defined(karaxDebug):
echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil echo "Processing component ", newNode.text, " changed impl set ", x.changedImpl != nil
if x.realDomImpl != nil: if x.changedImpl != nil and x.changedImpl(x, newNode):
let current = kxi.components[i].current
let parent = kxi.components[i].parent
kxi.addPatch(pkReplace, parent, current, x)
elif x.changedImpl != nil and x.changedImpl(x, newNode):
when defined(karaxDebug): when defined(karaxDebug):
echo "Component ", newNode.text, " did change" echo "Component ", newNode.text, " did change"
let current = kxi.components[i].current let current = kxi.components[i].current
@ -487,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)
@ -519,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))
@ -527,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))
@ -539,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:
@ -550,20 +619,44 @@ proc runDiff*(kxi: KaraxInstance; oldNode, newNode: VNode) =
kxi.currentTree = newNode kxi.currentTree = newNode
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
var onhashChange {.importc: "window.onhashchange".}: proc()
var hashPart {.importc: "window.location.hash".}: cstring
proc avoidDomDiffing*(kxi: KaraxInstance = kxi) =
## enforce a full redraw for the next redraw operation.
## This can be used as a temporary way to workaround DOM diffing
## problems or to avoid the DOM diffing when you already know
## it should use a completely new DOM.
## This is an experimental API.
kxi.currentTree = nil
proc reqFrame(callback: proc()): int {.importc: "window.requestAnimationFrame".}
when false:
proc cancelFrame(id: int) {.importc: "window.cancelAnimationFrame".}
proc dodraw(kxi: KaraxInstance) = proc dodraw(kxi: KaraxInstance) =
if kxi.renderer.isNil: return if kxi.renderer.isNil: return
let newtree = kxi.renderer() kxi.renderId = 0
if kxi.rendering:
# there is a render already running, delay 1 frame
kxi.renderId = reqFrame(proc () = kxi.dodraw)
return
kxi.rendering = true
let rdata = RouterData(hashPart: hashPart)
let newtree = kxi.renderer(rdata)
inc kxi.runCount 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
@ -577,20 +670,17 @@ proc dodraw(kxi: KaraxInstance) =
doAssert same(kxi.currentTree, document.getElementById(kxi.rootId)) doAssert same(kxi.currentTree, document.getElementById(kxi.rootId))
if not kxi.postRenderCallback.isNil: if not kxi.postRenderCallback.isNil:
kxi.postRenderCallback() kxi.postRenderCallback(rdata)
# 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".}
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:
@ -608,26 +698,49 @@ proc redrawSync*(kxi: KaraxInstance = kxi) = dodraw(kxi)
proc init(ev: Event) = proc init(ev: Event) =
kxi.renderId = reqFrame(proc () = kxi.dodraw) kxi.renderId = reqFrame(proc () = kxi.dodraw)
proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT", proc setRenderer*(renderer: proc (data: RouterData): VNode,
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.
discardable.} =
## Setup Karax. Usually the return value can be ignored. ## Setup Karax. Usually the return value can be ignored.
if document.getElementById(root).isNil:
let msg = "Could not find a <div> with id=" & root &
". Karax needs it as its rendering target."
raise newException(Exception, $msg)
result = KaraxInstance(rootId: root, renderer: renderer, result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback, postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60), patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30), patchesV: newSeq[PatchV](30),
components: @[]) components: @[],
surpressRedraws: false,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result kxi = result
window.onload = init window.onload = init
onhashChange = proc() = redraw()
proc setInitializer*(renderer: proc (): VNode, root: cstring = "ROOT", proc setRenderer*(renderer: proc (): VNode, root: cstring = "ROOT",
clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} = clientPostRenderCallback: proc () = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored.
proc wrapRenderer(data: RouterData): VNode = result = renderer()
proc wrapPostRender(data: RouterData) =
if clientPostRenderCallback != nil: clientPostRenderCallback()
setRenderer(wrapRenderer, root, wrapPostRender)
proc setInitializer*(renderer: proc (data: RouterData): VNode, root: cstring = "ROOT",
clientPostRenderCallback:
proc (data: RouterData) = nil): KaraxInstance {.discardable.} =
## Setup Karax. Usually the return value can be ignored. ## Setup Karax. Usually the return value can be ignored.
result = KaraxInstance(rootId: root, renderer: renderer, result = KaraxInstance(rootId: root, renderer: renderer,
postRenderCallback: clientPostRenderCallback, postRenderCallback: clientPostRenderCallback,
patches: newSeq[Patch](60), patches: newSeq[Patch](60),
patchesV: newSeq[PatchV](30), patchesV: newSeq[PatchV](30),
components: @[], components: @[],
surpressRedraws: true) surpressRedraws: true,
byId: newJDict[cstring, VNode](),
orphans: newJDict[cstring, bool]())
kxi = result kxi = result
window.onload = init window.onload = init
@ -655,16 +768,21 @@ 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)) = proc setOnHashChange*(action: proc (hashPart: cstring)) {.deprecated: "use setRenderer instead".} =
var onhashChange {.importc: "window.onhashchange".}: proc() ## Now deprecated, instead pass a callback to ``setRenderer`` that receives
var hashPart {.importc: "window.location.hash".}: cstring ## a ``data: RouterData`` parameter.
proc wrapper() = proc wrapper() =
action(hashPart) action(hashPart)
redraw() redraw()
onhashchange = wrapper onhashchange = wrapper
{.push stackTrace:off.} proc setForeignNodeId*(id: cstring; kxi: KaraxInstance = kxi) =
## Declares a node ID as "foreign". Foreign nodes are not
## under Karax's control in the sense that Karax does not attempt
## to perform structural checks on them.
kxi.orphans[id] = true
{.push stackTrace:off.}
proc setupErrorHandler*() = proc setupErrorHandler*() =
## Installs an error handler that transforms native JS unhandled ## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false, ## exceptions into Nim based stack traces. If `useAlert` is false,
@ -674,7 +792,7 @@ proc setupErrorHandler*() =
var onerror {.importc: "window.onerror", used.} = var onerror {.importc: "window.onerror", used.} =
proc (msg, url: cstring, line, col: int, error: cstring): bool = proc (msg, url: cstring, line, col: int, error: cstring): bool =
var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring() var x = cstring"Error: " & msg & "\n" & stackTraceAsCstring()
kout(x) echo(x)
return true # suppressErrorAlert return true # suppressErrorAlert
{.pop.} {.pop.}
@ -682,9 +800,35 @@ 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 wrapper() =
action()
redraw()
result = setTimeout(wrapper, later)
proc setInputText*(n: VNode; s: cstring) =
## Sets the text of input elements.
n.text = s
if n.dom != nil: n.dom.value = s
proc getInputText*(n: VNode): cstring =
if n.dom != nil:
result = n.dom.value
proc toChecked*(checked: bool): cstring =
(if checked: cstring"checked" else: cstring(nil))
proc toDisabled*(disabled: bool): cstring =
(if disabled: cstring"disabled" else: cstring(nil))
proc toSelected*(selected: bool): cstring =
(if selected: cstring"selected" else: cstring(nil))
proc vnodeToDom*(n: VNode; kxi: KaraxInstance = nil): Node =
result = toDom(n, useAttachedNode = false, kxi)

View file

@ -1,25 +1,46 @@
import macros, karax, vdom, compact import macros, vdom, compact, kbase
from strutils import startsWith, toLowerAscii from strutils import startsWith, toLowerAscii
when defined(js):
import karax
const const
StmtContext = ["kout", "inc", "echo", "dec", "!"] StmtContext = ["kout", "inc", "echo", "dec", "!"]
SpecialAttrs = ["id", "class", "value", "index", "style"] SpecialAttrs = ["id", "class", "value", "index", "style"]
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:
result.add getName(n[i]) result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit: of nnkStrLit..nnkTripleStrLit:
result = n.strVal result = n.strVal
of nnkInfix:
# allow 'foo-bar' syntax:
if n.len == 3 and $n[0] == "-":
result = getName(n[1]) & "-" & getName(n[2])
else:
expectKind(n, nnkIdent)
of nnkDotExpr:
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)
proc toKstring(n: NimNode): NimNode =
if n.kind == nnkStrLit:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for child in n:
result.add toKstring(child)
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode = proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x) result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
@ -32,6 +53,11 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
# (except for the last child of the macros we consider here), # (except for the last child of the macros we consider here),
# lets, consts, types can be considered as expressions # lets, consts, types can be considered as expressions
# case is complex, calls are assumed to produce a value. # case is complex, calls are assumed to produce a value.
when defined(js):
template evHandler(): untyped = bindSym"addEventHandler"
else:
template evHandler(): untyped = ident"addEventHandler"
case n.kind case n.kind
of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr: of nnkLiterals, nnkIdent, nnkSym, nnkDotExpr, nnkBracketExpr:
if tmpContext != nil: if tmpContext != nil:
@ -68,7 +94,7 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if tmpContext == nil: if tmpContext == nil:
error "no VNode to attach the event handler to" error "no VNode to attach the event handler to"
else: else:
result = newCall(bindSym"addEventHandler", tmpContext, result = newCall(evHandler(), tmpContext,
newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi")) newDotExpr(bindSym"EventKind", n[0]), anon, ident("kxi"))
else: else:
result = n result = n
@ -94,10 +120,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
if x.kind == nnkExprEqExpr: if x.kind == nnkExprEqExpr:
let key = getName x[0] let key = getName x[0]
if key.startsWith("on"): if key.startsWith("on"):
result.add newCall(bindSym"addEventHandler", result.add newCall(evHandler(),
tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi")) tmp, newDotExpr(bindSym"EventKind", x[0]), x[1], ident("kxi"))
elif eqIdent(key, "style") and x[1].kind == nnkTableConstr:
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"):

8
karax/kbase.nim Normal file
View file

@ -0,0 +1,8 @@
## Karax -- Single page applications for Nim.
## Basic type definitions for Karax.
when defined(js):
type kstring* = cstring
else:
type kstring* = string

10
karax/kdom.nim Normal file
View file

@ -0,0 +1,10 @@
## Karax -- Single page applications for Nim.
from dom import nil
when not declared(dom.DomApiVersion):
include kdom_impl
else:
import dom
export dom

View file

@ -37,6 +37,38 @@ type
onsubmit*: proc (event: Event) {.nimcall.} onsubmit*: proc (event: Event) {.nimcall.}
onunload*: proc (event: Event) {.nimcall.} onunload*: proc (event: Event) {.nimcall.}
# https://developer.mozilla.org/en-US/docs/Web/Events
DomEvent* {.pure.} = enum
Abort = "abort",
BeforeInput = "beforeinput",
Blur = "blur",
Click = "click",
CompositionEnd = "compositionend",
CompositionStart = "compositionstart",
CompositionUpdate = "compositionupdate",
DblClick = "dblclick",
Error = "error",
Focus = "focus",
FocusIn = "focusin",
FocusOut = "focusout",
Input = "input",
KeyDown = "keydown",
KeyPress = "keypress",
KeyUp = "keyup",
Load = "load",
MouseDown = "mousedown",
MouseEnter = "mouseenter",
MouseLeave = "mouseleave",
MouseMove = "mousemove",
MouseOut = "mouseout",
MouseOver = "mouseover",
MouseUp = "mouseup",
Resize = "resize",
Scroll = "scroll",
Select = "select",
Unload = "unload",
Wheel = "wheel"
Window* = ref WindowObj Window* = ref WindowObj
WindowObj {.importc.} = object of EventTargetObj WindowObj {.importc.} = object of EventTargetObj
document*: Document document*: Document
@ -57,6 +89,7 @@ type
status*: cstring status*: cstring
toolbar*: ref ToolBar toolbar*: ref ToolBar
frames*: seq[Frame] frames*: seq[Frame]
screen*: Screen
Frame* = ref FrameObj Frame* = ref FrameObj
FrameObj {.importc.} = object of WindowObj FrameObj {.importc.} = object of WindowObj
@ -131,9 +164,8 @@ type
name*: cstring name*: cstring
readOnly*: bool readOnly*: bool
options*: seq[OptionElement] options*: seq[OptionElement]
selectedOptions*: seq[OptionElement]
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement clientWidth*, clientHeight*: int
HtmlElement* = ref object of Element
contentEditable*: string contentEditable*: string
isContentEditable*: bool isContentEditable*: bool
dir*: string dir*: string
@ -142,6 +174,87 @@ type
offsetLeft*: int offsetLeft*: int
offsetTop*: int offsetTop*: int
# https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
ValidityState* = ref ValidityStateObj
ValidityStateObj {.importc.} = object
badInput*: bool
customError*: bool
patternMismatch*: bool
rangeOverflow*: bool
rangeUnderflow*: bool
stepMismatch*: bool
tooLong*: bool
tooShort*: bool
typeMismatch*: bool
valid*: bool
valueMissing*: bool
# https://developer.mozilla.org/en-US/docs/Web/API/Blob
Blob* = ref BlobObj
BlobObj {.importc.} = object of RootObj
size*: int
`type`*: cstring
# https://developer.mozilla.org/en-US/docs/Web/API/File
File* = ref FileObj
FileObj {.importc.} = object of Blob
lastModified*: int
name*: cstring
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement
InputElement* = ref InputElementObj
InputElementObj {.importc.} = object of Element
# Properties related to the parent form
formAction*: cstring
formEncType*: cstring
formMethod*: cstring
formNoValidate*: bool
formTarget*: cstring
# Properties that apply to any type of input element that is not hidden
`type`*: cstring
autofocus*: bool
required*: bool
value*: cstring
validity*: ValidityState
validationMessage*: cstring
willValidate*: bool
# Properties that apply only to elements of type "checkbox" or "radio"
indeterminate*: bool
# Properties that apply only to elements of type "image"
alt*: cstring
height*: cstring
src*: cstring
width*: cstring
# Properties that apply only to elements of type "file"
accept*: cstring
files*: seq[Blob]
# Properties that apply only to text/number-containing or elements
autocomplete*: cstring
maxLength*: int
size*: int
pattern*: cstring
placeholder*: cstring
min*: cstring
max*: cstring
selectionStart*: int
selectionEnd*: int
selectionDirection*: cstring
# Properties not yet categorized
dirName*: cstring
accessKey*: cstring
list*: Element
multiple*: bool
labels*: seq[Element]
step*: cstring
valueAsDate*: cstring
valueAsNumber*: float
LinkElement* = ref LinkObj LinkElement* = ref LinkObj
LinkObj {.importc.} = object of ElementObj LinkObj {.importc.} = object of ElementObj
target*: cstring target*: cstring
@ -171,13 +284,19 @@ type
text*: cstring text*: cstring
value*: cstring value*: cstring
# https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement
FormElement* = ref FormObj FormElement* = ref FormObj
FormObj {.importc.} = object of ElementObj FormObj {.importc.} = object of ElementObj
acceptCharset*: cstring
action*: cstring action*: cstring
encoding*: cstring autocomplete*: cstring
`method`*: cstring
target*: cstring
elements*: seq[Element] elements*: seq[Element]
encoding*: cstring
enctype*: cstring
length*: int
`method`*: cstring
noValidate*: bool
target*: cstring
ImageElement* = ref ImageObj ImageElement* = ref ImageObj
ImageObj {.importc.} = object of ElementObj ImageObj {.importc.} = object of ElementObj
@ -284,55 +403,467 @@ type
wordSpacing*: cstring wordSpacing*: cstring
zIndex*: int zIndex*: int
# TODO: A lot of the fields in Event belong to a more specific type of event. EventPhase* = enum
# TODO: Should we clean this up? None = 0,
CapturingPhase,
AtTarget,
BubblingPhase
# https://developer.mozilla.org/en-US/docs/Web/API/Event
Event* = ref EventObj Event* = ref EventObj
EventObj {.importc.} = object of RootObj EventObj {.importc.} = object of RootObj
bubbles*: bool
cancelBubble*: bool
cancelable*: bool
composed*: bool
currentTarget*: Node
defaultPrevented*: bool
eventPhase*: int
target*: Node target*: Node
altKey*, ctrlKey*, shiftKey*: bool `type`*: cstring
button*: int isTrusted*: bool
clientX*, clientY*: int
# https://developer.mozilla.org/en-US/docs/Web/API/UIEvent
UIEvent* = ref UIEventObj
UIEventObj {.importc.} = object of Event
detail*: int64
view*: Window
# https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
KeyboardEvent* = ref KeyboardEventObj
KeyboardEventObj {.importc.} = object of UIEvent
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
code*: cstring
isComposing*: bool
key*: cstring
keyCode*: int keyCode*: int
layerX*, layerY*: int location*: int
modifiers*: int
ALT_MASK*, CONTROL_MASK*, SHIFT_MASK*, META_MASK*: int # https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
KeyboardEventKey* {.pure.} = enum
# Modifier keys
Alt,
AltGraph,
CapsLock,
Control,
Fn,
FnLock,
Hyper,
Meta,
NumLock,
ScrollLock,
Shift,
Super,
Symbol,
SymbolLock,
# Whitespace keys
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
End,
Home,
PageDown,
PageUp,
# Editing keys
Backspace,
Clear,
Copy,
CrSel,
Cut,
Delete,
EraseEof,
ExSel,
Insert,
Paste,
Redo,
Undo,
# UI keys
Accept,
Again,
Attn,
Cancel,
ContextMenu,
Escape,
Execute,
Find,
Finish,
Help,
Pause,
Play,
Props,
Select,
ZoomIn,
ZoomOut,
# Device keys
BrigtnessDown,
BrigtnessUp,
Eject,
LogOff,
Power,
PowerOff,
PrintScreen,
Hibernate,
Standby,
WakeUp,
# Common IME keys
AllCandidates,
Alphanumeric,
CodeInput,
Compose,
Convert,
Dead,
FinalMode,
GroupFirst,
GroupLast,
GroupNext,
GroupPrevious,
ModeChange,
NextCandidate,
NonConvert,
PreviousCandidate,
Process,
SingleCandidate,
# Korean keyboards only
HangulMode,
HanjaMode,
JunjaMode,
# Japanese keyboards only
Eisu,
Hankaku,
Hiragana,
HiraganaKatakana,
KanaMode,
KanjiMode,
Katakana,
Romaji,
Zenkaku,
ZenkakuHanaku,
# Function keys
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
Soft1,
Soft2,
Soft3,
Soft4,
# Phone keys
AppSwitch,
Call,
Camera,
CameraFocus,
EndCall,
GoBack,
GoHome,
HeadsetHook,
LastNumberRedial,
Notification,
MannerMode,
VoiceDial,
# Multimedia keys
ChannelDown,
ChannelUp,
MediaFastForward,
MediaPause,
MediaPlay,
MediaPlayPause,
MediaRecord,
MediaRewind,
MediaStop,
MediaTrackNext,
MediaTrackPrevious,
# Audio control keys
AudioBalanceLeft,
AudioBalanceRight,
AudioBassDown,
AudioBassBoostDown,
AudioBassBoostToggle,
AudioBassBoostUp,
AudioBassUp,
AudioFaderFront,
AudioFaderRear,
AudioSurroundModeNext,
AudioTrebleDown,
AudioTrebleUp,
AudioVolumeDown,
AUdioVolumeMute,
AudioVolumeUp,
MicrophoneToggle,
MicrophoneVolumeDown,
MicrophoneVolumeMute,
MicrophoneVolumeUp,
# TV control keys
TV,
TV3DMode,
TVAntennaCable,
TVAudioDescription,
TVAudioDescriptionMixDown,
TVAudioDescriptionMixUp,
TVContentsMenu,
TVDataService,
TVInput,
TVInputComponent1,
TVInputComponent2,
TVInputComposite1,
TVInputComposite2,
TVInputHDMI1,
TVInputHDMI2,
TVInputHDMI3,
TVInputHDMI4,
TVInputVGA1,
TVMediaContext,
TVNetwork,
TVNumberEntry,
TVPower,
TVRadioService,
TVSatellite,
TVSatelliteBS,
TVSatelliteCS,
TVSatelliteToggle,
TVTerrestrialAnalog,
TVTerrestrialDigital,
TVTimer,
# Media controller keys
AVRInput,
AVRPower,
ColorF0Red,
ColorF1Green,
ColorF2Yellow,
ColorF3Blue,
ColorF4Grey,
ColorF5Brown,
ClosedCaptionToggle,
Dimmer,
DisplaySwap,
DVR,
Exit,
FavoriteClear0,
FavoriteClear1,
FavoriteClear2,
FavoriteClear3,
FavoriteRecall0,
FavoriteRecall1,
FavoriteRecall2,
FavoriteRecall3,
FavoriteStore0,
FavoriteStore1,
FavoriteStore2,
FavoriteStore3,
Guide,
GuideNextDay,
GuidePreviousDay,
Info,
InstantReplay,
Link,
ListProgram,
LiveContent,
Lock,
MediaApps,
MediaAudioTrack,
MediaLast,
MediaSkipBackward,
MediaSkipForward,
MediaStepBackward,
MediaStepForward,
MediaTopMenu,
NavigateIn,
NavigateNext,
NavigateOut,
NavigatePrevious,
NextFavoriteChannel,
NextUserProfile,
OnDemand,
Pairing,
PinPDown,
PinPMove,
PinPUp,
PlaySpeedDown,
PlaySpeedReset,
PlaySpeedUp,
RandomToggle,
RcLowBattery,
RecordSpeedNext,
RfBypass,
ScanChannelsToggle,
ScreenModeNext,
Settings,
SplitScreenToggle,
STBInput,
STBPower,
Subtitle,
Teletext,
VideoModeNext,
Wink,
ZoomToggle,
# Speech recognition keys
SpeechCorrectionList,
SpeechInputToggle,
# Document keys
Close,
New,
Open,
Print,
Save,
SpellCheck,
MailForward,
MailReply,
MailSend,
# Application selector keys
LaunchCalculator,
LaunchCalendar,
LaunchContacts,
LaunchMail,
LaunchMediaPlayer,
LaunchMusicPlayer,
LaunchMyComputer,
LaunchPhone,
LaunchScreenSaver,
LaunchSpreadsheet,
LaunchWebBrowser,
LaunchWebCam,
LaunchWordProcessor,
LaunchApplication1,
LaunchApplication2,
LaunchApplication3,
LaunchApplication4,
LaunchApplication5,
LaunchApplication6,
LaunchApplication7,
LaunchApplication8,
LaunchApplication9,
LaunchApplication10,
LaunchApplication11,
LaunchApplication12,
LaunchApplication13,
LaunchApplication14,
LaunchApplication15,
LaunchApplication16,
# Browser control keys
BrowserBack,
BrowserFavorites,
BrowserForward,
BrowserHome,
BrowserRefresh,
BrowserSearch,
BrowserStop,
# Numeric keypad keys
Key11,
Key12,
Separator
MouseButtons* = enum
NoButton = 0,
PrimaryButton = 1,
SecondaryButton = 2,
AuxilaryButton = 4,
FourthButton = 8,
FifthButton = 16
# https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
MouseEvent* = ref MouseEventObj
MouseEventObj {.importc.} = object of UIEvent
altKey*, ctrlKey*, metaKey*, shiftKey*: bool
button*: int
buttons*: int
clientX*, clientY*: int
movementX*, movementY*: int
offsetX*, offsetY*: int offsetX*, offsetY*: int
pageX*, pageY*: int pageX*, pageY*: int
relatedTarget*: EventTarget
#region*: cstring
screenX*, screenY*: int screenX*, screenY*: int
which*: int
`type`*: cstring
x*, y*: int x*, y*: int
ABORT*: int
BLUR*: int DataTransferItemKind* {.pure.} = enum
CHANGE*: int File = "file",
CLICK*: int String = "string"
DBLCLICK*: int
DRAGDROP*: int # https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem
ERROR*: int DataTransferItem* = ref DataTransferItemObj
FOCUS*: int DataTransferItemObj {.importc.} = object of RootObj
KEYDOWN*: int kind*: cstring
KEYPRESS*: int `type`*: cstring
KEYUP*: int
LOAD*: int # https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer
MOUSEDOWN*: int DataTransfer* = ref DataTransferObj
MOUSEMOVE*: int DataTransferObj {.importc.} = object of RootObj
MOUSEOUT*: int dropEffect*: cstring
MOUSEOVER*: int effectAllowed*: cstring
MOUSEUP*: int files*: seq[Element]
MOVE*: int items*: seq[DataTransferItem]
RESET*: int types*: seq[cstring]
RESIZE*: int
SELECT*: int DataTransferDropEffect* {.pure.} = enum
SUBMIT*: int None = "none",
UNLOAD*: int Copy = "copy",
Link = "link",
Move = "move"
DataTransferEffectAllowed* {.pure.} = enum
None = "none",
Copy = "copy",
CopyLink = "copyLink",
CopyMove = "copyMove",
Link = "link",
LinkMove = "linkMove",
Move = "move",
All = "all",
Uninitialized = "uninitialized"
DragEventTypes* = enum
Drag = "drag",
DragEnd = "dragend",
DragEnter = "dragenter",
DragExit = "dragexit",
DragLeave = "dragleave",
DragOver = "dragover",
DragStart = "dragstart",
Drop = "drop"
# https://developer.mozilla.org/en-US/docs/Web/API/DragEvent
DragEvent* {.importc.} = object of MouseEvent
dataTransfer*: DataTransfer
TouchList* {.importc.} = ref object of RootObj TouchList* {.importc.} = ref object of RootObj
length*: int length*: int
TouchEvent* {.importc.} = ref object of Event Touch* = ref TouchObj
changedTouches*, targetTouches*, touches*: TouchList TouchObj {.importc.} = object of RootObj
Touch* {.importc.} = ref object of RootObj
identifier*: int identifier*: int
screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int screenX*, screenY*, clientX*, clientY*, pageX*, pageY*: int
target*: Element target*: Element
@ -340,6 +871,10 @@ type
rotationAngle*: int rotationAngle*: int
force*: float force*: float
TouchEvent* = ref TouchEventObj
TouchEventObj {.importc.} = object of UIEvent
changedTouches*, targetTouches*, touches*: seq[Touch]
Location* = ref LocationObj Location* = ref LocationObj
LocationObj {.importc.} = object of RootObj LocationObj {.importc.} = object of RootObj
hash*: cstring hash*: cstring
@ -397,6 +932,11 @@ type
TimeOut* {.importc.} = ref object of RootObj TimeOut* {.importc.} = ref object of RootObj
Interval* {.importc.} = object of RootObj Interval* {.importc.} = object of RootObj
AddEventListenerOptions* = object
capture*: bool
once*: bool
passive*: bool
when defined(nodejs): when defined(nodejs):
# we provide a dummy DOM for nodejs for testing purposes # we provide a dummy DOM for nodejs for testing purposes
proc len*(x: Node): int = x.childNodes.len proc len*(x: Node): int = x.childNodes.len
@ -483,6 +1023,7 @@ proc clearTimeout*(t: Timeout) {.importc, nodecl.}
# EventTarget "methods" # EventTarget "methods"
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false) proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), useCapture: bool = false)
proc addEventListener*(et: EventTarget, ev: cstring, cb: proc(ev: Event), options: AddEventListenerOptions)
proc removeEventListener*(et: EventTarget; ev: cstring; cb: proc(ev: Event)) proc removeEventListener*(et: EventTarget; ev: cstring; cb: proc(ev: Event))
# Window "methods" # Window "methods"
@ -491,7 +1032,7 @@ proc back*(w: Window)
proc blur*(w: Window) proc blur*(w: Window)
proc captureEvents*(w: Window, eventMask: int) {.deprecated.} proc captureEvents*(w: Window, eventMask: int) {.deprecated.}
proc clearInterval*(w: Window, interval: ref Interval) proc clearInterval*(w: Window, interval: ref Interval)
proc clearTimeout*(w: Window, timeout: TimeOut) proc clearTimeout*(w: Window, timeout: ref TimeOut)
proc close*(w: Window) proc close*(w: Window)
proc confirm*(w: Window, msg: cstring): bool proc confirm*(w: Window, msg: cstring): bool
proc disableExternalCapture*(w: Window) proc disableExternalCapture*(w: Window)
@ -500,6 +1041,7 @@ proc find*(w: Window, text: cstring, caseSensitive = false,
backwards = false) backwards = false)
proc focus*(w: Window) proc focus*(w: Window)
proc forward*(w: Window) proc forward*(w: Window)
proc getComputedStyle*(w: Window, e: Node, pe:Node = nil): Style
proc handleEvent*(w: Window, e: Event) proc handleEvent*(w: Window, e: Event)
proc home*(w: Window) proc home*(w: Window)
proc moveBy*(w: Window, x, y: int) proc moveBy*(w: Window, x, y: int)
@ -516,7 +1058,7 @@ proc scrollBy*(w: Window, x, y: int)
proc scrollTo*(w: Window, x, y: int) proc scrollTo*(w: Window, x, y: int)
proc setInterval*(w: Window, code: cstring, pause: int): ref Interval proc setInterval*(w: Window, code: cstring, pause: int): ref Interval
proc setInterval*(w: Window, function: proc (), pause: int): ref Interval proc setInterval*(w: Window, function: proc (), pause: int): ref Interval
proc setTimeout*(w: Window, code: cstring, pause: int): TimeOut proc setTimeout*(w: Window, code: cstring, pause: int): ref TimeOut
proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval proc setTimeout*(w: Window, function: proc (), pause: int): ref Interval
proc stop*(w: Window) proc stop*(w: Window)
proc requestAnimationFrame*(w: Window, function: proc (time: float)): int proc requestAnimationFrame*(w: Window, function: proc (time: float)): int
@ -526,6 +1068,7 @@ proc cancelAnimationFrame*(w: Window, id: int)
proc appendData*(n: Node, data: cstring) proc appendData*(n: Node, data: cstring)
proc cloneNode*(n: Node, copyContent: bool): Node proc cloneNode*(n: Node, copyContent: bool): Node
proc deleteData*(n: Node, start, len: int) proc deleteData*(n: Node, start, len: int)
proc focus*(e: Node)
proc getAttribute*(n: Node, attr: cstring): cstring proc getAttribute*(n: Node, attr: cstring): cstring
proc getAttributeNode*(n: Node, attr: cstring): Node proc getAttributeNode*(n: Node, attr: cstring): Node
proc hasChildNodes*(n: Node): bool proc hasChildNodes*(n: Node): bool
@ -533,7 +1076,7 @@ proc insertData*(n: Node, position: int, data: cstring)
proc removeAttribute*(n: Node, attr: cstring) proc removeAttribute*(n: Node, attr: cstring)
proc removeAttributeNode*(n, attr: Node) proc removeAttributeNode*(n, attr: Node)
proc replaceData*(n: Node, start, len: int, text: cstring) proc replaceData*(n: Node, start, len: int, text: cstring)
proc scrollIntoView*(n: Node) proc scrollIntoView*(n: Node, alignToTop = true)
proc setAttribute*(n: Node, name, value: cstring) proc setAttribute*(n: Node, name, value: cstring)
proc setAttributeNode*(n: Node, attr: Node) proc setAttributeNode*(n: Node, attr: Node)
@ -550,11 +1093,13 @@ proc releaseEvents*(d: Document, eventMask: int) {.deprecated.}
proc routeEvent*(d: Document, event: Event) proc routeEvent*(d: Document, event: Event)
proc write*(d: Document, text: cstring) proc write*(d: Document, text: cstring)
proc writeln*(d: Document, text: cstring) proc writeln*(d: Document, text: cstring)
proc querySelector*(d: Document, selectors: cstring): Element
proc querySelectorAll*(d: Document, selectors: cstring): seq[Element]
# Element "methods" # Element "methods"
proc blur*(e: Element) proc blur*(e: Element)
proc click*(e: Element) proc click*(e: Element)
proc focus*(e: Node) proc focus*(e: Element)
proc handleEvent*(e: Element, event: Event) proc handleEvent*(e: Element, event: Event)
proc select*(e: Element) proc select*(e: Element)
proc getElementsByTagName*(e: Element, name: cstring): seq[Element] proc getElementsByTagName*(e: Element, name: cstring): seq[Element]
@ -563,6 +1108,8 @@ proc getElementsByClassName*(e: Element, name: cstring): seq[Element]
# FormElement "methods" # FormElement "methods"
proc reset*(f: FormElement) proc reset*(f: FormElement)
proc submit*(f: FormElement) proc submit*(f: FormElement)
proc checkValidity*(e: FormElement): bool
proc reportValidity*(e: FormElement): bool
# EmbedElement "methods" # EmbedElement "methods"
proc play*(e: EmbedElement) proc play*(e: EmbedElement)
@ -593,12 +1140,37 @@ proc setAttribute*(s: Style, attr, value: cstring, caseSensitive=false)
# Event "methods" # Event "methods"
proc preventDefault*(ev: Event) proc preventDefault*(ev: Event)
proc stopImmediatePropagation*(ev: Event)
proc stopPropagation*(ev: Event) proc stopPropagation*(ev: Event)
# KeyboardEvent "methods"
proc getModifierState*(ev: KeyboardEvent, keyArg: cstring): bool
# MouseEvent "methods"
proc getModifierState*(ev: MouseEvent, keyArg: cstring): bool
# TouchEvent "methods" # TouchEvent "methods"
proc identifiedTouch*(list: TouchList): Touch proc identifiedTouch*(list: TouchList): Touch
proc item*(list: TouchList, i: int): Touch proc item*(list: TouchList, i: int): Touch
# DataTransfer "methods"
proc clearData*(dt: DataTransfer, format: cstring)
proc getData*(dt: DataTransfer, format: cstring): cstring
proc setData*(dt: DataTransfer, format: cstring, data: cstring)
proc setDragImage*(dt: DataTransfer, img: Element, xOffset: int64, yOffset: int64)
# DataTransferItem "methods"
proc getAsFile*(dti: DataTransferItem): File
# InputElement "methods"
proc setSelectionRange*(e: InputElement, selectionStart: int, selectionEnd: int, selectionDirection: cstring = "none")
proc setRangeText*(e: InputElement, replacement: cstring, startindex: int = 0, endindex: int = 0, selectionMode: cstring = "preserve")
proc setCustomValidity*(e: InputElement, error: cstring)
proc checkValidity*(e: InputElement): bool
# Blob "methods"
proc slice*(e: Blob, startindex: int = 0, endindex: int = e.size, contentType: cstring = "")
{.pop.} {.pop.}
proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".} proc setAttr*(n: Node; key, val: cstring) {.importcpp: "#.setAttribute(@)".}
@ -657,3 +1229,4 @@ proc inViewport*(el: Node): bool =
proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.} proc scrollTop*(e: Node): int {.importcpp: "#.scrollTop", nodecl.}
proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.} proc offsetHeight*(e: Node): int {.importcpp: "#.offsetHeight", nodecl.}
proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.} proc offsetTop*(e: Node): int {.importcpp: "#.offsetTop", nodecl.}
proc offsetLeft*(e: Node): int {.importcpp: "#.offsetLeft", nodecl.}

38
karax/lookuptables.nim Normal file
View file

@ -0,0 +1,38 @@
import macros, kbase
from strutils import `%`
macro fieldNamesAsArray*(t: typed; pattern = "$1"): untyped =
var impl = getTypeImpl(getTypeImpl(t)[1])
if impl.kind == nnkRefTy: impl = getTypeImpl(impl[0])
expectKind impl, nnkObjectTy
impl = impl[2]
expectKind impl, nnkRecList
result = newTree(nnkBracket)
for x in impl:
expectKind x, nnkIdentDefs
result.add(newCall(bindSym"kstring", newLit(pattern.strVal % $x[0])))
proc str(n: NimNode): NimNode =
if n.kind in {nnkStrLit, nnkTripleStrLit}:
result = newCall(bindSym"kstring", n)
else:
result = copyNimNode(n)
for i in 0..<n.len:
result.add str(n[i])
macro kstrLits*(x: untyped): untyped =
## Transforms every string literal "abc" to ``kstring"abc"``. This makes
## things much easier to write. String literals of the form ``r"abc"`` are
## not affected.
result = str(x)
when isMainModule:
type
MyObject = ref object
x*: string
next*: int
more, here*: float
const myObjectFields = fieldNamesAsArray(MyObject, "MyObject_$1")
for f in myObjectFields: echo f

View file

@ -1,3 +1,3 @@
## Include file that contains the common imports for the Karax framework. ## Include file that contains the common imports for the Karax framework.
import karax, karaxdsl, vdom, compact import kbase, karax, karaxdsl, vdom, compact, jstrutils

129
karax/tools/karun.nim Normal file
View file

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

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

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

View file

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

View file

@ -1,20 +1,26 @@
## Virtual DOM implementation for Karax. ## Virtual DOM implementation for Karax.
from kdom import Event, Node when defined(js):
import shash, macros, vstyles from kdom import Event, Node
from strutils import toUpperAscii else:
type
Event* = ref object
Node* = ref object
import macros, vstyles, kbase
from strutils import toUpperAscii, toLowerAscii, tokenize
type type
VNodeKind* {.pure.} = enum VNodeKind* {.pure.} = enum
text = "#text", int = "#int", bool = "#bool", text = "#text", int = "#int", bool = "#bool",
vthunk = "#vthunk", dthunk = "#dthunk", vthunk = "#vthunk", dthunk = "#dthunk",
component = "#component", component = "#component", verbatim = "#verbatim",
html, head, title, base, link, meta, style, html, head, title, base, link, meta, style,
script, noscript, script, noscript,
body, section, nav, article, aside, body, section, nav, article, aside,
h1, h2, h3, h4, h5, h6, h1, h2, h3, h4, h5, h6,
header, footer, address, main header, footer, address, main,
p, hr, pre, blockquote, ol, ul, li, p, hr, pre, blockquote, ol, ul, li,
dl, dt, dd, dl, dt, dd,
@ -29,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,
@ -39,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.
@ -78,25 +100,36 @@ type
onsubmit, ## A form is submitted onsubmit, ## A form is submitted
oninput, ## An input value changes oninput, ## An input value changes
onkeyupenter, ## vdom extension: an input field received the ENTER key press onanimationstart,
onkeyuplater ## vdom extension: a key was pressed and some time onanimationend,
## passed (useful for on-the-fly text completions) onanimationiteration,
onkeyupenter, ## vdom extension: an input field received the ENTER key press
onkeyuplater, ## vdom extension: a key was pressed and some time
## passed (useful for on-the-fly text completions)
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)
for i in low(VNodeKind)..high(VNodeKind): for i in low(VNodeKind)..high(VNodeKind):
let x = $i let x = $i
let y = if x[0] == '#': x else: toUpperAscii(x) let y = if x[0] == '#': x else: toUpperAscii(x)
a.add(newCall("cstring", newLit(y))) a.add(newCall("kstring", newLit(y)))
var e = newTree(nnkBracket) var e = newTree(nnkBracket)
for i in low(EventKind)..high(EventKind): for i in low(EventKind)..high(EventKind):
e.add(newCall("cstring", newLit(substr($i, 2)))) e.add(newCall("kstring", newLit(substr($i, 2))))
template tmpl(a, e) {.dirty.} = template tmpl(a, e) {.dirty.} =
const const
toTag*: array[VNodeKind, cstring] = a toTag*: array[VNodeKind, kstring] = a
toEventName*: array[EventKind, cstring] = e toEventName*: array[EventKind, kstring] = e
result = getAst tmpl(a, e) result = getAst tmpl(a, e)
@ -108,15 +141,15 @@ type
EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)] EventHandlers* = seq[(EventKind, EventHandler, NativeEventHandler)]
VKey* = cstring VKey* = kstring
VNode* = ref object of RootObj VNode* = ref object of RootObj
kind*: VNodeKind kind*: VNodeKind
index*: int ## a generally useful 'index' index*: int ## a generally useful 'index'
id*, class*, text*: cstring id*, class*, text*: kstring
kids: seq[VNode] kids: seq[VNode]
# even index: key, odd index: value; done this way for memory efficiency: # even index: key, odd index: value; done this way for memory efficiency:
attrs: seq[cstring] attrs: seq[kstring]
events*: EventHandlers events*: EventHandlers
when false: when false:
hash*: Hash hash*: Hash
@ -133,7 +166,6 @@ type
updatedImpl*: proc(self, newInstance: VComponent) updatedImpl*: proc(self, newInstance: VComponent)
onAttachImpl*: proc(self: VComponent) onAttachImpl*: proc(self: VComponent)
onDetachImpl*: proc(self: VComponent) onDetachImpl*: proc(self: VComponent)
realDomImpl*: proc(self: VComponent): kdom.Node
version*: int ## Update this to trigger a redraw by karax. Usually you version*: int ## Update this to trigger a redraw by karax. Usually you
## should call 'markDirty' instead which is an alias for ## should call 'markDirty' instead which is an alias for
## 'inc version'. ## 'inc version'.
@ -143,23 +175,23 @@ type
## expanded to. ## expanded to.
debugId*: int debugId*: int
proc value*(n: VNode): cstring = n.text proc value*(n: VNode): kstring = n.text
proc `value=`*(n: VNode; v: cstring) = n.text = v proc `value=`*(n: VNode; v: kstring) = n.text = v
proc intValue*(n: VNode): int = n.index proc intValue*(n: VNode): int = n.index
proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i) proc vn*(i: int): VNode = VNode(kind: VNodeKind.int, index: i)
proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b)) proc vn*(b: bool): VNode = VNode(kind: VNodeKind.int, index: ord(b))
proc vn*(x: cstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x) proc vn*(x: kstring): VNode = VNode(kind: VNodeKind.text, index: -1, text: x)
template callThunk*(fn: typed; n: VNode): untyped = template callThunk*(fn: typed; n: VNode): untyped =
## for internal usage only. ## for internal usage only.
fn(n.kids) fn(n.kids)
proc vthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc vthunk*(name: kstring; args: varargs[VNode, vn]): VNode =
VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args) VNode(kind: VNodeKind.vthunk, text: name, index: -1, kids: @args)
proc dthunk*(name: cstring; args: varargs[VNode, vn]): VNode = proc dthunk*(dom: Node): VNode =
VNode(kind: VNodeKind.dthunk, text: name, index: -1, kids: @args) VNode(kind: VNodeKind.dthunk, dom: dom)
proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) = proc setEventIfNoConflict(v: VNode; kind: EventKind; handler: EventHandler) =
assert handler != nil assert handler != nil
@ -193,7 +225,7 @@ template newComponent*[T](t: typeDesc[T];
updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T = updated: proc(self, newInstance: VComponent) = defaultUpdatedImpl): T =
## Use this template to create new components. ## Use this template to create new components.
T(kind: VNodeKind.component, index: -1, T(kind: VNodeKind.component, index: -1,
text: cstring(astToStr(t)), renderImpl: render, text: kstring(astToStr(t)), renderImpl: render,
changedImpl: changed, updatedImpl: updated, changedImpl: changed, updatedImpl: updated,
onAttachImpl: onAttach, onDetachImpl: onDetach, onAttachImpl: onAttach, onDetachImpl: onDetach,
debugId: getDebugId()) debugId: getDebugId())
@ -202,8 +234,8 @@ template markDirty*(c: VComponent) =
## mark the component as dirty so that it is re-rendered. ## mark the component as dirty so that it is re-rendered.
inc c.version inc c.version
proc setAttr*(n: VNode; key: cstring; val: cstring = "") = proc setAttr*(n: VNode; key: kstring; val: kstring = "") =
if n.attrs.isNil: if n.attrs.len == 0:
n.attrs = @[key, val] n.attrs = @[key, val]
else: else:
for i in countup(0, n.attrs.len-2, 2): for i in countup(0, n.attrs.len-2, 2):
@ -213,7 +245,7 @@ proc setAttr*(n: VNode; key: cstring; val: cstring = "") =
n.attrs.add key n.attrs.add key
n.attrs.add val n.attrs.add val
proc getAttr*(n: VNode; key: cstring): cstring = proc getAttr*(n: VNode; key: kstring): kstring =
for i in countup(0, n.attrs.len-2, 2): for i in countup(0, n.attrs.len-2, 2):
if n.attrs[i] == key: return n.attrs[i+1] if n.attrs[i] == key: return n.attrs[i+1]
@ -237,7 +269,12 @@ proc takeOverFields*(newNode, oldNode: VNode) =
proc len*(x: VNode): int = x.kids.len proc len*(x: VNode): int = x.kids.len
proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx] proc `[]`*(x: VNode; idx: int): VNode = x.kids[idx]
proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y proc `[]=`*(x: VNode; idx: int; y: VNode) = x.kids[idx] = y
proc add*(parent, kid: VNode) = parent.kids.add kid
proc add*(parent, kid: VNode) =
when not defined(js) and not defined(nimNoNil):
if parent.kids.isNil: parent.kids = @[]
parent.kids.add kid
proc delete*(parent: VNode; position: int) = proc delete*(parent: VNode; position: int) =
parent.kids.delete(position) parent.kids.delete(position)
proc insert*(parent, kid: VNode; position: int) = proc insert*(parent, kid: VNode; position: int) =
@ -248,18 +285,26 @@ proc tree*(kind: VNodeKind; kids: varargs[VNode]): VNode =
result = newVNode(kind) result = newVNode(kind)
for k in kids: result.add k for k in kids: result.add k
proc tree*(kind: VNodeKind; attrs: openarray[(cstring, cstring)]; proc tree*(kind: VNodeKind; attrs: openarray[(kstring, kstring)];
kids: varargs[VNode]): VNode = kids: varargs[VNode]): VNode =
result = tree(kind, kids) result = tree(kind, kids)
for a in attrs: result.setAttr(a[0], a[1]) for a in attrs: result.setAttr(a[0], a[1])
proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: cstring(s), index: -1) when defined(js):
proc text*(s: cstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1) proc text*(s: string): VNode = VNode(kind: VNodeKind.text, text: kstring(s), index: -1)
proc text*(s: kstring): VNode = VNode(kind: VNodeKind.text, text: s, index: -1)
when defined(js):
proc verbatim*(s: string): VNode =
VNode(kind: VNodeKind.verbatim, text: kstring(s), index: -1)
proc verbatim*(s: kstring): VNode =
VNode(kind: VNodeKind.verbatim, text: s, index: -1)
iterator items*(n: VNode): VNode = iterator items*(n: VNode): VNode =
for i in 0..<n.kids.len: yield n.kids[i] for i in 0..<n.kids.len: yield n.kids[i]
iterator attrs*(n: VNode): (cstring, cstring) = iterator attrs*(n: VNode): (kstring, kstring) =
for i in countup(0, n.attrs.len-2, 2): for i in countup(0, n.attrs.len-2, 2):
yield (n.attrs[i], n.attrs[i+1]) yield (n.attrs[i], n.attrs[i+1])
@ -272,8 +317,14 @@ 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 != nil: if n.field.len > 0:
result.add " " & astToStr(field) & " = " & $n.field result.add " " & astToStr(field) & " = " & $n.field
proc toString*(n: VNode; result: var string; indent: int) = proc toString*(n: VNode; result: var string; indent: int) =
@ -288,7 +339,7 @@ proc toString*(n: VNode; result: var string; indent: int) =
if n.kind == VNodeKind.text: if n.kind == VNodeKind.text:
result.add n.text result.add n.text
else: else:
if n.text != nil: if n.text.len > 0:
result.add " value = " result.add " value = "
result.add n.text result.add n.text
for child in items(n): for child in items(n):
@ -324,7 +375,116 @@ when false:
h &= child.hash h &= child.hash
n.hash = h n.hash = h
proc `$`*(n: VNode): cstring =
var res = "" proc add*(result: var string, n: VNode, indent = 0, indWidth = 2) =
toString(n, res, 0) ## adds the textual representation of `n` to `result`.
result = cstring(res)
proc addEscapedAttr(result: var string, s: kstring) =
# `addEscaped` alternative with less escaped characters.
# Only to be used for escaping attribute values enclosed in double quotes!
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
else: result.add(c)
proc addEscaped(result: var string, s: kstring) =
## same as ``result.add(escape(s))``, but more efficient.
for c in items(s):
case c
of '<': result.add("&lt;")
of '>': result.add("&gt;")
of '&': result.add("&amp;")
of '"': result.add("&quot;")
of '\'': result.add("&#x27;")
of '/': result.add("&#x2F;")
else: result.add(c)
proc addIndent(result: var string, indent: int) =
result.add("\n")
for i in 1..indent: result.add(' ')
if n.kind == VNodeKind.text:
result.addEscaped(n.text)
elif n.kind == VNodeKind.verbatim:
result.add(n.text)
else:
let kind = $n.kind
result.add('<')
result.add(kind)
if n.id.len > 0:
result.add " id=\""
result.addEscapedAttr(n.id)
result.add('"')
if n.class.len > 0:
result.add " class=\""
result.addEscapedAttr(n.class)
result.add('"')
for k, v in attrs(n):
result.add(' ')
result.add(k)
result.add("=\"")
result.addEscapedAttr(v)
result.add('"')
if n.style != nil:
result.add " style=\""
for k, v in pairs(n.style):
if v.len == 0: continue
for t in tokenize($k, seps={'A' .. 'Z'}):
if t.isSep: result.add '-'
result.add toLowerAscii(t.token)
result.add ": "
result.add v
result.add "; "
result.add('"')
if n.len > 0:
result.add('>')
if n.len > 1:
var noWhitespace = false
for i in 0..<n.len:
if n[i].kind == VNodeKind.text:
noWhitespace = true
break
if noWhitespace:
# for mixed leaves, we cannot output whitespace for readability,
# because this would be wrong. For example: ``a<b>b</b>`` is
# different from ``a <b>b</b>``.
for i in 0..<n.len: result.add(n[i], indent+indWidth, indWidth)
else:
for i in 0..<n.len:
result.addIndent(indent+indWidth)
result.add(n[i], indent+indWidth, indWidth)
result.addIndent(indent)
else:
result.add(n[0], indent+indWidth, indWidth)
result.add("</")
result.add(kind)
result.add(">")
elif n.kind in selfClosing:
result.add(" />")
else:
result.add(">")
result.add("</")
result.add(kind)
result.add(">")
proc `$`*(n: VNode): kstring =
when defined(js):
var res = ""
toString(n, res, 0)
result = kstring(res)
else:
result = ""
add(result, n)
proc getVNodeById*(n: VNode; id: cstring): VNode =
## Get the VNode that was marked with ``id``. Returns ``nil``
## if no node exists.
if n.id == id: return n
for i in 0..<n.len:
result = getVNodeById(n[i], id)
if result != nil: return result

View file

@ -1,5 +1,12 @@
##[
see examples/hellostyle.nim
]##
import kdom, macros, jdict import std/[macros, strutils]
import kbase
when defined(js):
import kdom, jdict
type type
StyleAttr* {.pure.} = enum StyleAttr* {.pure.} = enum
@ -200,10 +207,10 @@ type
macro buildLookupTables(): untyped = macro buildLookupTables(): untyped =
var e = newTree(nnkBracket) var e = newTree(nnkBracket)
for i in low(StyleAttr)..high(StyleAttr): for i in low(StyleAttr)..high(StyleAttr):
e.add(newCall("cstring", newLit($i))) e.add(newCall("kstring", newLit($i)))
template tmpl(e) {.dirty.} = template tmpl(e) {.dirty.} =
const const
toStyleAttrName: array[StyleAttr, cstring] = e toStyleAttrName: array[StyleAttr, kstring] = e
result = getAst tmpl(e) result = getAst tmpl(e)
buildLookupTables() buildLookupTables()
@ -212,8 +219,14 @@ buildLookupTables()
# it's relevant. # it's relevant.
# even index: key, odd index: value; done this way for memory efficiency: # even index: key, odd index: value; done this way for memory efficiency:
type when defined(js):
VStyle* = JSeq[cstring] type
VStyle* = JSeq[cstring]
else:
type
VStyle* = ref seq[string]
proc len*(a: VStyle): int = len(a[])
proc add*(a: VStyle; x: string) = add(a[], x)
proc eq*(a, b: VStyle): bool = proc eq*(a, b: VStyle): bool =
if a.isNil: if a.isNil:
@ -225,19 +238,20 @@ 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 kout[T](x: T) {.importc: "console.log", varargs.} proc setAttr*(s: VStyle; a, value: kstring) {.noSideEffect.} =
## inserts (a, value) in sorted order of key `a`
proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} = # 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:
s[i+1] = value s[i+1] = value
return return
elif s[i] > a: elif s[i] > a:
s.add nil s.add ""
s.add nil 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
@ -247,59 +261,94 @@ proc setAttr(s: VStyle; a, value: cstring) {.noSideEffect.} =
s.add a s.add a
s.add value s.add value
proc setAttr*(s: VStyle; attr: StyleAttr, value: cstring) {.noSideEffect.} = proc setAttr*(s: VStyle; attr: StyleAttr, value: kstring) {.noSideEffect.} =
assert value != nil, "value must not be nil" when kstring is cstring:
assert value != nil, "value must not be nil"
setAttr(s, toStyleAttrName[attr], value) setAttr(s, toStyleAttrName[attr], value)
proc getAttr*(s: VStyle; attr: StyleAttr): cstring {.noSideEffect.} = proc getAttr*(s: VStyle; attr: StyleAttr): kstring {.noSideEffect.} =
## returns 'nil' if the attribute has not been set. ## returns "" if the attribute has not been set.
var i = 0 var i = 0
let a = toStyleAttrName[attr] let a = toStyleAttrName[attr]
while i < s.len: while i < s.len:
if s[i] == a: if s[i] == a:
return s[i+1] return s[i+1]
elif s[i] > a: elif s[i] > a:
return nil return ""
inc i, 2 inc i, 2
proc style*(pairs: varargs[(StyleAttr, cstring)]): VStyle {.noSideEffect.} = proc style*(pairs: varargs[(StyleAttr, kstring)]): VStyle {.noSideEffect.} =
## constructs a VStyle object from a list of (attribute, value)-pairs. ## constructs a VStyle object from a list of (attribute, value)-pairs.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for x in pairs: for x in pairs:
result.setAttr x[0], x[1] result.setAttr x[0], x[1]
proc style*(a: StyleAttr; val: cstring): VStyle {.noSideEffect.} = proc style*(a: StyleAttr; val: kstring): VStyle {.noSideEffect.} =
## constructs a VStyle object from a single (attribute, value)-pair. ## constructs a VStyle object from a single (attribute, value)-pair.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
result.setAttr a, val result.setAttr a, val
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.} proc toCss*(a: string): VStyle =
##[
See example in hellostyle.nim
Allows passing a css string directly, eg:
tdiv(style = style((fontStyle, "italic".kstring), (color, "orange".kstring))): discard
tdiv(style = "font-style: oblique; color: pink".toCss): discard
]##
when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for ai in a.split(";"):
var ai = ai.strip
if ai.len == 0: continue
let aj = ai.strip.split(":", maxsplit=1)
result.setAttr(aj[0], aj[1])
when defined(js):
proc setStyle(d: Style; key, val: cstring) {.importcpp: "#[#] = #", noSideEffect.}
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
proc merge*(a, b: VStyle): VStyle {.noSideEffect.} = proc merge*(a, b: VStyle): VStyle {.noSideEffect.} =
## merges two styles. ``b`` takes precedence over ``a``. ## merges two styles. ``b`` takes precedence over ``a``.
result = newJSeq[cstring]() when defined(js):
result = newJSeq[cstring]()
else:
new(result)
result[] = @[]
for i in 0..<a.len: for i in 0..<a.len:
result.add a[i] result.add a[i]
for i in countup(0, b.len-1, 2): for i in countup(0, b.len-1, 2):
setAttr(result, b[i], b[i+1]) setAttr(result, b[i], b[i+1])
proc applyStyle*(n: Node; s: VStyle) {.noSideEffect.} = iterator pairs*(s: VStyle): (kstring, kstring) {.noSideEffect.} =
## apply the style to the real DOM node ``n``.
#n.style = Style() # optimized, this is a hotspot:
{.emit: "`n`.style = {};".}
for i in countup(0, s.len-1, 2):
n.style.setStyle(s[i], s[i+1])
iterator pairs*(s: VStyle): (cstring, cstring) {.noSideEffect.} =
if s != nil: if s != nil:
for i in countup(0, s.len-1, 2): for i in countup(0, s.len-1, 2):
yield (s[i], s[i+1]) yield (s[i], s[i+1])
import jstrutils when defined(js):
import jstrutils
else:
template `&`(x: untyped): untyped = $x
proc rgb*(r, g, b: range[0..255]): cstring = proc rgb*(r, g, b: range[0..255]): kstring =
cstring"rgb(" & &r & cstring", " & &g & cstring", " & &b & cstring")" kstring"rgb(" & &r & kstring", " & &g & kstring", " & &b & kstring")"
proc rgba*(r, g, b: range[0..255], alpha: float): cstring = proc rgba*(r, g, b: range[0..255], alpha: float): kstring =
cstring"rgba(" & &r & cstring", " & &g & cstring", " & &b & cstring", " & &alpha & cstring")" kstring"rgba(" & &r & kstring", " & &g & kstring", " & &b & kstring", " & &alpha & kstring")"

394
readme.md Normal file
View file

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

View file

@ -1,38 +0,0 @@
Karax – Single page applications in Nim |travis|
================================================
Karax is a framework for developing single page applications in Nim.
It's still in heavy development, so keep in mind that the API is subject
to change.
To try it out, run::
nimble install # For first time
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. But don't take my
word for it, look at this:
.. image:: docs/benchmark.png
Goals
=====
- Leverage Nim's macro system to produce a framework that allows
for the development of applications that are boilerplate free.
- Keep it small, keep it fast, keep it flexible.
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
:target: https://travis-ci.org/pragmagic/karax

View file

@ -1,121 +0,0 @@
## Components in Karax are built by the ``.component`` macro annotation.
import macros, jdict, kdom, vdom, tables, strutils
var
vcomponents* = newJDict[cstring, proc(args: seq[VNode]): VNode]()
dcomponents* = newJDict[cstring, proc(args: seq[VNode]): Node]()
type
ComponentKind* {.pure.} = enum
None,
Tag,
VNode,
Node
var
allcomponents {.compileTime.} = initTable[string, ComponentKind]()
proc isComponent*(x: string): ComponentKind {.compileTime.} =
allcomponents.getOrDefault(x)
proc addTags() {.compileTime.} =
let x = (bindSym"VNodeKind").getTypeImpl
expectKind(x, nnkEnumTy)
for i in ord(VNodeKind.html)..ord(VNodeKind.high):
# +1 because of empty node at the start of the enum AST:
let tag = $x[i+1]
allcomponents[tag] = ComponentKind.Tag
static:
addTags()
proc unpack(symbolicType: NimNode; index: int): NimNode {.compileTime.} =
#let t = symbolicType.getTypeImpl
let t = repr(symbolicType)
case t
of "cstring":
result = quote do:
args[`index`].text
of "int", "VKey":
result = quote do:
args[`index`].intValue
of "bool":
result = quote do:
args[`index`].intValue != 0
elif t.endsWith"Kind":
result = quote do:
`symbolicType`(args[`index`].intValue)
else:
# just pass it along, maybe there is some conversion for it:
result = quote do:
args[`index`]
proc newname*(n: NimNode): NimNode =
if n.kind == nnkPostfix:
n[1] = newname(n[1])
result = n
elif n.kind == nnkSym:
result = ident($n.symbol)
else:
result = n
macro compact*(prc: untyped): untyped =
## A 'compact' tree generation proc is one that only depends on its
## inputs and should be stored as a compact virtual DOM tree and
## only expanded on demand (when its inputs changed).
var n = prc.copyNimNode
for i in 0..6: n.add prc[i].copyNimTree
expectKind(n, nnkProcDef)
if n[0].kind == nnkEmpty:
error("please pass a non anonymous proc", n[0])
let name = n[0]
let params = params(n)
let rettype = repr params[0]
var isvirtual = ComponentKind.None
if rettype == "VNode":
isvirtual = ComponentKind.VNode
elif rettype == "Node":
isvirtual = ComponentKind.Node
else:
error "component must return VNode or Node", params[0]
let realName = if name.kind == nnkPostfix: name[1] else: name
let nn = $realName
n[0] = ident("inner" & nn)
var unpackCall = newCall(n[0])
var counter = 0
for i in 1.. <params.len:
let param = params[i]
let L = param.len
let typ = param[L-2]
for j in 0 .. L-3:
unpackCall.add unpack(typ, counter)
inc counter
template vwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): VNode =
unpackCall
template dwrapper(pname, unpackCall) {.dirty.} =
proc pname(args: seq[VNode]): Node =
unpackCall
template vregister(key, val) =
bind jdict.`[]=`
`[]=`(vcomponents, cstring(key), val)
template dregister(key, val) =
bind jdict.`[]=`
`[]=`(dcomponents, cstring(key), val)
result = newTree(nnkStmtList, n)
if isvirtual == ComponentKind.VNode:
result.add getAst(vwrapper(newname name, unpackCall))
result.add getAst(vregister(newLit(nn), realName))
else:
result.add getAst(dwrapper(newname name, unpackCall))
result.add getAst(dregister(newLit(nn), realName))
allcomponents[nn] = isvirtual
when defined(debugKaraxDsl):
echo repr result

View file

@ -1,53 +0,0 @@
## Karax -- Single page applications for Nim.
## This module implements support for `ajax`:idx: socket
## handling.
import karax
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int; response: cstring);
kxi: KaraxInstance = kxi) =
proc contWrapper(httpStatus: int; response: cstring) =
cont(httpStatus, response)
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 open(r: HttpRequest; meth, url: cstring; async: bool) {.importcpp: "#.open(@)".}
proc newRequest(): HttpRequest {.importcpp: "new XMLHttpRequest(@)".}
var this {.importc: "this".}: ThisObj
let ajax = newRequest()
ajax.open(meth, url, true)
for a, b in items(headers):
ajax.setRequestHeader(a, b)
ajax.statechange proc() =
if this.readyState == 4:
if this.status == 200:
contWrapper(this.status, this.responseText)
else:
contWrapper(this.status, this.statusText)
ajax.send(data)
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
kxi: KaraxInstance = kxi) =
ajax("POST", url, headers, data, cont, kxi)
proc ajaxGet*(url: cstring; headers: openarray[(cstring, cstring)];
cont: proc (httpStatus: int, response: cstring);
kxi: KaraxInstance = kxi) =
ajax("GET", url, headers, nil, cont, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
proc fromJson*[T](blob: cstring): T {.importc: "JSON.parse".}

View file

@ -1,4 +1,4 @@
include karaxprelude include ../karax/prelude
block: block:
type type

View file

@ -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>

View file

@ -1,6 +1,6 @@
include "../src/karax" include "../karax/karax"
import "../src/karaxdsl" import "../karax/karaxdsl"
proc hasDom(n: Vnode) = proc hasDom(n: Vnode) =
if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}: if n.kind in {VNodeKind.component, VNodeKind.vthunk, VNodeKind.dthunk}:
@ -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
@ -156,16 +160,17 @@ proc testWild() =
let a = createEntries(entries) let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let b = createEntries(entries) let b = createEntries(entries)
doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 5", "pkRemove nil") doDiff(a, b, "pkDetach button 7", "pkRemove nil", "pkDetach div 7", "pkRemove nil")
proc testWildInsert() = proc testWildInsert() =
var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")] var entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring("5")]
let a = createEntries(entries) let a = createEntries(entries)
entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")] entries = @[cstring("0"), cstring("1"), cstring("2"), cstring("3"), cstring("4"), cstring"7", cstring("5")]
let b = createEntries(entries) let b = createEntries(entries)
doDiff(a, b, "pkInsertBefore button 7", "pkAppend div 5") doDiff(a, b, "pkInsertBefore button 7", "pkInsertBefore div 7")
kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (): VNode = discard) kxi = KaraxInstance(rootId: cstring"ROOT", renderer: proc (data: RouterData): VNode = discard,
byId: newJDict[cstring, VNode]())
testAppend() testAppend()
testInsert() testInsert()

33
tests/domEventTests.css Normal file
View file

@ -0,0 +1,33 @@
#dragDiv {
background-color: #0d1224;
width: 500px;
height: 100px;
position: absolute;
top: 500px;
left: 200px;
text-align: center;
cursor: move;
color: rgb(60, 201, 163);
}
#results {
font-family: monospace;
font-size: 10pt;
width: 100%;
height: 400px;
overflow: auto;
background-color: #000000;
color: forestgreen;
}
#test10input1, #test10input2, #test11h3, #test12input {
visibility: hidden;
}
#test11h3 {
color: gold;
}
body {
background-color: dimgrey;
}

13
tests/domEventTests.html Normal file
View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>DOM Event Tests</title>
<link href="domEventTests.css" rel="stylesheet" />
<meta charset="UTF-8" />
<script src="jquery-3.2.1.js" type="text/javascript"></script>
</head>
<body id="body">
<div id="ROOT"></div>
<script src="domEventTests.js" type="text/javascript"></script>
</body>
</html>

624
tests/domEventTests.nim Normal file
View file

@ -0,0 +1,624 @@
import times, jstrutils, strutils, kdom, karax, karaxdsl, vdom, tables
var
results: seq[cstring]
dragEventsCompleted = initTable[DragEventTypes, bool]()
testRunning: array[13, bool]
testCompleted: array[13, bool]
test2ImageDrop = false
dblClickCount = 0
test6ClickCount = 0
test8Failure = false
test10inputFocusCount = 0
test12Failure = false
proc getLogText(text: string): cstring =
result = "[$#]: $#" % [format(getLocalTime(getTime()), "HH:mm:ss"), $text]
proc log(text: string, clear: bool = false) =
if clear:
results = @[]
results.add getLogText(text)
echo text
redrawSync()
# Scroll to top after adding text using JQuery
discard window.setTimeout("$('#results').animate({ scrollTop: 0 }, 100);", 100)
proc check(cond: bool, msg: string) =
if not cond:
log "FAIL: " & msg
doAssert(cond, msg)
proc check(cond: bool, msg: string, warn: bool): bool =
if not cond:
if warn:
log "WARNING: " & msg
else:
log "FAIL: " & msg
doAssert(cond, msg)
result = cond
proc dragDivDragStart(ev: kdom.Event) =
dragEventsCompleted[DragStart] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
if dragDiv != nil:
var style = window.getComputedStyle(dgev.target)
var dataX = $(parseInt(style.left) - dgev.clientX)
var dataY = $(parseInt(style.top) - dgev.clientY)
dgev.dataTransfer.setData("text/plain", dataX & "," & dataY)
var img = (ImageElement)document.createElement("img")
img.src ="https://nim-lang.org/assets/img/logo.svg"
dgev.dataTransfer.setDragImage(img, 0, 0)
proc dragDivDragOver(ev: kdom.Event) =
dragEventsCompleted[DragOver] = true
ev.preventDefault()
proc checkAllTests() =
var allTestsPassed = true
for a in testCompleted:
if not a:
allTestsPassed = false
break
if allTestsPassed:
log "All tests completed successfully!"
else:
log "Some tests failed. Check the log."
proc completeTest13() =
testRunning[12] = false
testCompleted[12] = true
log "Test 13 completed successfully."
checkAllTests()
proc test13() =
testRunning[12] = true
log "Test 13 started."
log "This test verifies KeyboardEvent getModifierState functionality."
log "To continue, press the left CTRL key."
proc test12focus1(ev: kdom.Event) =
ev.stopImmediatePropagation()
proc test12focus2(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc test12focus3(ev: kdom.Event) =
test12Failure = true
check false, "Unexpected listener invocation."
proc completeTest12() =
if not test12Failure:
var elmt = document.getElementById("test12input")
elmt.style.visibility = "hidden"
testRunning[11] = false
testCompleted[11] = true
log "Test 12 completed successfully."
test13()
proc test12() =
testRunning[11] = true
log "Test 12 started."
log "This test verifies stopImmediatePropagation functionality."
var elmt = document.getElementById("test12input")
elmt.style.visibility = "visible"
elmt.addEventListener($Focus, test12focus1)
elmt.addEventListener($Focus, test12focus2)
elmt.addEventListener($Focus, test12focus3)
elmt.focus()
discard setTimeout(completeTest12, 500)
proc completeTest11() =
document.querySelector("h3").style.visibility = "hidden"
testRunning[10] = false
testCompleted[10] = true
log "Test 11 completed."
test12()
proc test11() =
testRunning[10] = true
log "Test 11 started."
log "This test checks querySelector and querySelectorAll."
document.getElementById("test11h3").style.visibility = "visible"
var elmt = document.querySelector("h3")
check elmt != nil, "querySelector returned nil."
log "text: " & $elmt.innerHTML
check $elmt.innerHTML == "test 11", "Unexpected h3 value."
var elmts = document.querySelectorAll("h3")
log "elmts len: " & $elmts.len
check elmts.len == 1, "Unexpected elmts len."
log "text: " & $elmts[0].innerHTML
check $elmts[0].innerHTML == "test 11", "Unexpected h3 value."
discard setTimeout(completeTest11, 500)
proc completeTest10() =
testRunning[9] = false
testCompleted[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "hidden"
test10input2.style.visibility = "hidden"
log "Test 10 completed."
test11()
proc test10inputOnfocus(ev: kdom.Event) =
if testRunning[9]:
inc test10inputFocusCount
if test10inputFocusCount > 3:
discard setTimeout(completeTest10, 500)
proc test10() =
log "Test 10 started."
log "This test checks element.focus()."
testRunning[9] = true
var test10input1 = document.getElementById("test10input1")
var test10input2 = document.getElementById("test10input2")
test10input1.style.visibility = "visible"
test10input2.style.visibility = "visible"
test10input1.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input2.addEventListener($DomEvent.Focus, test10inputOnfocus)
test10input1.focus()
test10input2.focus()
test10input1.focus()
test10input2.focus()
proc completeTest9() =
var test9Value = $document.getElementById("test9value").value
doAssert(test9Value == "[test9]", "Unexpected test9 value.")
testRunning[8] = false
testCompleted[8] = true
test10()
proc test9() =
log "Test 9 started."
log "This tests the setTimeout(w: Window, code: cstring, pause: int) proc."
testRunning[8] = true
discard window.setTimeout("document.getElementById('test9value').value = '[test9]';", 500)
discard window.setTimeout(completeTest9, 1000)
proc completeTest8() =
if not test8Failure:
log "Test 8 completed successfully."
testRunning[7] = false
testCompleted[7] = true
test9()
else:
log "Test 8 failed."
proc failTest8() =
test8Failure = true
check(false, "ClearTimeout failed.")
proc test8() =
log "Test 8 started."
log "This test checks that clearTimeout() works correctly."
testRunning[7] = true
var timeout = setTimeout(failTest8, 1000)
clearTimeout(timeout)
discard setTimeout(completeTest8, 1200)
proc test7() =
log "Test 7 started."
log "This test ensures that mouse events correctly pass button and key modifier information."
log "To continue, right-click while holding SHIFT."
testRunning[6] = true
# A mouseMove handler is needed (even if empty) so mouseDown will have a value for movementX and movementY
proc mouseMove(ev: kdom.Event) =
discard
proc mouseDown(ev: kdom.Event) =
var me = (MouseEvent)ev
if testRunning[5]:
log "[ME] button: " & $me.button
log "[ME] buttons: " & $me.buttons
log "[ME] altKey: " & $me.altKey
log "[ME] ctrlKey: " & $me.ctrlKey
log "[ME] metaKey: " & $me.metaKey
log "[ME] shiftKey: " & $me.shiftKey
log "[ME] clientX: " & $me.clientX
check me.clientX > 0, "Unexpected clientX value."
log "[ME] clientY: " & $me.clientY
check me.clientY > 0, "Unexpected clientY value."
log "[ME] movementX: " & $me.movementX
check me.movementX > 0, "Unexpected movementX value."
log "[ME] movementY: " & $me.movementY
check me.movementY > 0, "Unexpected movementY value."
log "[ME] offsetX: " & $me.offsetX
check me.offsetX > 0, "Unexpected offsetX value."
log "[ME] offsetY: " & $me.offsetY
check me.offsetY > 0, "Unexpected offsetY value."
log "[ME] pageX: " & $me.pageX
check me.pageX > 0, "Unexpected pageX value."
log "[ME] pageY: " & $me.pageY
check me.pageY > 0, "Unexpected pageY value."
log "[ME] relatedTarget == nil: " & $(me.relatedTarget == nil)
log "[ME] screenX: " & $me.screenX
check me.screenX > 0, "Unexpected screenX value."
log "[ME] screenY: " & $me.screenY
check me.screenY > 0, "Unexpected screenY value."
log "[ME] x: " & $me.x
check me.x > 0, "Unexpected x value."
log "[ME] y: " & $me.y
check me.y > 0, "Unexpected y value."
if dblClickCount == 1:
inc test6ClickCount
if test6ClickCount > 1:
testRunning[5] = false
testCompleted[5] = true
log "Test 6 completed successfully."
test7()
elif testRunning[6]:
log "[ME] buttons: " & $me.buttons
var passed = check(me.buttons == (int)SecondaryButton, "Unexpected buttons value.", true)
log "[ME] shiftKey: " & $me.shiftKey
passed = passed and check(me.shiftKey, "Unexpected shiftKey value.", true)
var isShiftPressed = me.getModifierState($KeyboardEventKey.Shift)
log "[ME] getModifierState shift result: " & $isShiftPressed
passed = passed and check(isShiftPressed, "Unexpected getModifierState shift result.", true)
if passed:
log "Test 7 completed successfully."
testRunning[6] = false
testCompleted[6] = true
ev.preventDefault()
if testCompleted[6]:
test8()
proc dblClick(ev: kdom.Event) =
inc dblClickCount
check dblClickCount == 1, "Unexpected double-click event invocation."
log "Try double-clicking again."
proc test6() =
log "Test 6 started."
log "This test validates that addEventListener can be called by passing AddEventListenerOptions, and the options work correctly."
log "To continue, try double-clicking the mouse."
var options = AddEventListenerOptions(once:true)
document.addEventListener($DomEvent.DblClick, dblClick, options)
testRunning[5] = true
proc test5() =
log "Test 5 started."
log "This test validates the newly added properties: window.screen.width, window.scren.height, element.clientWidth, and element.clientHeight."
log "window.screen.width: " & $window.screen.width
check(window.screen.width > 0, "Unexpected value for window.screen.width.")
log "window.screen.height: " & $window.screen.height
check(window.screen.height > 0, "Unexpected value for window.screen.height.")
var elem = document.getElementById("results")
log "results div clientWidth: " & $elem.clientWidth
check(elem.clientWidth > 0, "Unexpected value for clientWidth.")
log "results div clientHeight: " & $elem.clientHeight
check(elem.clientHeight > 0, "Unexpected value for clientHeight.")
testRunning[4] = false
testCompleted[4] = true
log "Test 5 completed successfully."
test6()
proc keyDown(ev: kdom.Event) =
var kbev = ((KeyboardEvent)ev)
if testRunning[2]:
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
log "Test 3 completed successfully."
testRunning[2] = false
testCompleted[2] = true
testRunning[3] = true
log "Test 4 started."
log "This test ensures the child events have access to base event settings."
log "[KB] event.bubbles: " & $kbev.bubbles
check kbev.bubbles, "Unexpected event.bubbles value."
log "[KB] event.cancelBubble: " & $kbev.cancelBubble
check kbev.cancelBubble == false, "Unexpected event.cancelBubble value."
log "[KB] event.cancelable: " & $kbev.cancelable
check kbev.cancelable, "Unexpected event.cancelable value."
log "[KB] event.composed: " & $kbev.composed
check kbev.composed, "Unexpected event.composed value."
log "[KB] event.currentTarget.nodeName: " & $kbev.currentTarget.nodeName
check kbev.currentTarget.nodeName == "#document", "Unexpected event.currentTarget.nodeName value."
log "[KB] event.defaultPrevented: " & $kbev.defaultPrevented
check kbev.defaultPrevented == false, "Unexpected event.defaultPrevented value."
log "[KB] event.eventPhase: " & $kbev.eventPhase
check EventPhase(kbev.eventPhase) == BubblingPhase, "Unexpected event.eventPhase value."
log "[KB] event.isTrusted: " & $kbev.isTrusted
check kbev.isTrusted, "Unexpected event.isTrusted value."
log "[UI] kbev.detail: " & $kbev.detail
log "[UI] kbev.view.outerHeight: " & $kbev.view.outerHeight
check kbev.view.outerHeight > 0, "Unexpected view.outerHeight value."
log "[UI] kbev.view.outerWidth: " & $kbev.view.outerWidth
check kbev.view.outerWidth > 0, "Unexpected view.outerWidth value."
testRunning[3] = false
testCompleted[3] = true
log "Test 4 completed."
test5()
elif testRunning[12]:
if kbev.code == "ControlLeft":
log "[KB] altKey: " & $kbev.altKey
log "[KB] ctrlKey: " & $kbev.ctrlKey
log "[KB] metaKey: " & $kbev.metaKey
log "[KB] shiftKey: " & $kbev.shiftKey
log "[KB] code: " & $kbev.code
log "[KB] isComposing: " & $kbev.isComposing
log "[KB] key: " & $kbev.key
log "[KB] keyCode: " & $kbev.keyCode
log "[KB] location: " & $kbev.location
var isControl = kbev.getModifierState($KeyboardEventKey.Control)
log "[KB] getModifierState result: " & $isControl
check isControl, "Unexpected getModifierState return value."
completeTest13()
proc test3() =
log "Test 3 started."
log "This test validates KeyboardEvent handling."
log "Click on the browser to focus, and then press any key."
testRunning[2] = true
proc dragDivDrop(ev: kdom.Event) =
dragEventsCompleted[Drop] = true
var dgev = cast[DragEvent](ev)
var dragDiv = document.getElementById("dragDiv")
var data = $dgev.dataTransfer.getData("text/plain")
var dt = dgev.dataTransfer
if not test2ImageDrop:
var offset = data.split(",")
var newLeft = dgev.clientX + parseInt(offset[0])
var newTop = dgev.clientY + parseInt(offset[1])
if newLeft < 0: newLeft = 0
if newTop < 0: newTop = 0
if newLeft + dragDiv.clientWidth > window.outerWidth: newLeft = window.outerWidth - dragDiv.clientWidth
if newTop + dragDiv.clientHeight > window.outerHeight: newTop = window.outerHeight - dragDiv.clientHeight
dragDiv.style.left = $newLeft & "px"
dragDiv.style.top = $newTop & "px"
if testRunning[0]:
log "[drop] Drop coords: $#, $#" % [$newLeft, $newTop]
if testCompleted[0] and not testCompleted[1] and not testRunning[1]:
testRunning[1] = true
log "Test 2 started."
log "This test validates the DataTransfer and DataTransferItem objects passed as part of the drag events."
var isChrome = ($navigator.userAgent).contains("Chrome")
log "[DT] effectAllowed: " & $dt.effectAllowed
var expectedEffectAllowed = $Uninitialized
if isChrome:
expectedEffectAllowed = if test2ImageDrop: $DataTransferEffectAllowed.Copy else: $DataTransferEffectAllowed.All
check(dt.effectAllowed == $expectedEffectAllowed, "Unexpected effectAllowed value.")
if not test2ImageDrop:
log "[DT] dropEffect: " & $dt.dropEffect
var expectedDropEffect = if isChrome: $DataTransferDropEffect.None else: $DataTransferDropEffect.Move
check(dt.dropEffect == $expectedDropEffect, "Unexpected dropEffect value.")
var expectedFilesLen = if test2ImageDrop: 1 else: 0
log "[DT] files len: " & $dt.files.len
check(dt.files.len == expectedFilesLen, "Unexpected files length.")
var expectedItemsLen = 1
log "[DT] items len: " & $dt.items.len
check(dt.items.len == expectedItemsLen, "Unexpected items length.")
var expectedItemKind = if test2ImageDrop: $DataTransferItemKind.File else: $DataTransferItemKind.String
log "[DT] items[0].kind: " & $dt.items[0].kind
check(dt.items[0].kind == expectedItemKind, "Unexpected item kind.")
var expectedItemType = if test2ImageDrop: "image/png" else: "text/plain"
log "[DT] items[0].type: " & $dt.items[0].`type`
check(dt.items[0].`type` == expectedItemType, "Unexpected item type.")
var expectedTypesLen = if not isChrome and test2ImageDrop: 2 else: 1
log "[DT] types len: " & $dt.types.len
check(dt.types.len == expectedTypesLen, "Unexpected types length.")
if test2ImageDrop:
log "[DT] types[0]: " & $dt.types[0]
if isChrome:
check(dt.types[0] == "Files", "Unexpected type value.")
else:
log "[DT] types[1]: " & $dt.types[1]
check(dt.types[0] == "application/x-moz-file", "Unexpected type value.")
check(dt.types[1] == "Files", "Unexpected type value.")
if test2ImageDrop:
test2ImageDrop = false
# TODO: Determine why getAsString() does invoke callback
# proc getAsString*(dti: DataTransferItem, callback: proc(data: cstring))
var jfile = dt.items[0].getAsFile()
log "[DT] file name: " & $jfile.name
check(jfile.name.len > 0, "Unexpected file name length.")
testCompleted[1] = true
testRunning[1] = false
log "Test 2 completed successfully."
test3()
ev.preventDefault()
return
document.getElementById("dragDiv").style.visibility = "hidden"
log "To continue, drag a .PNG file (file type matters) anywhere on this page."
log "NOTE: If the browser redirects to the image, then consider this test as failed."
test2ImageDrop = true
testRunning[1] = false
ev.preventDefault()
proc dragDivDrag(ev: kdom.Event) =
dragEventsCompleted[Drag] = true
ev.preventDefault()
proc dragDivDragLeave(ev: kdom.Event) =
dragEventsCompleted[DragLeave] = true
if testRunning[0]:
log "[dragLeave]"
ev.preventDefault()
proc dragDivDragExit(ev: kdom.Event) =
dragEventsCompleted[DragExit] = true
if testRunning[0]:
log "[dragExit]"
ev.preventDefault()
proc dragDivDragEnd(ev: kdom.Event) =
dragEventsCompleted[DragEnd] = true
if testRunning[0]:
log "[dragEnd]"
ev.preventDefault()
proc dragDivDragEnter(ev: kdom.Event) =
dragEventsCompleted[DragEnter] = true
if testRunning[0]:
log "[dragEnter]"
ev.preventDefault()
proc initDragEvents() =
log "Drag event initialization started."
var dragDiv = document.getElementById("dragDiv")
document.addEventListener($DragEventTypes.DragOver, dragDivDragOver, false)
document.addEventListener($DragEventTypes.Drop, dragDivDrop, false)
document.addEventListener($DragEventTypes.Drag, dragDivDrag, false)
dragDiv.addEventListener($DragEventTypes.DragStart, dragDivDragStart, false)
dragDiv.addEventListener($DragEventTypes.DragExit, dragDivDragExit, false)
dragDiv.addEventListener($DragEventTypes.DragLeave, dragDivDragLeave, false)
dragDiv.addEventListener($DragEventTypes.DragEnd, dragDivDragEnd, false)
dragDiv.addEventListener($DragEventTypes.DragEnter, dragDivDragEnter, false)
log "Drag event initialization complete."
log "Drag the div to continue the test. Make sure you drag the div over another element."
log "Then click the \"Check test 1 status\" button."
proc initMouseEvents() =
log "Mouse event initialization started."
document.addEventListener($DomEvent.MouseDown, mouseDown, false)
document.addEventListener($DomEvent.MouseMove, mouseMove, false)
log "Mouse event initialization complete."
proc initKbEvents() =
log "KB event initialization started."
document.addEventListener($DomEvent.KeyDown, keyDown, false)
log "KB event initialization complete."
proc startTests() =
log "[Disclaimer: these tests have been tested successfully on Linux with Firefox and Chromium. Your results may vary.]"
log "Note: these tests require some manual steps, which will be shown here."
log "Note: new entries appear in this text area at the top, so read in this direction."
log " | | | | | | | | | | | | | | | | | | | | | |"
log " / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\ / \\"
log "Test 1 started."
log "This test checks that drag events are fired."
testRunning[0] = true
for evt in [Drag, DragEnd, DragEnter, DragExit, DragLeave, DragOver, DragStart, Drop]:
dragEventsCompleted.add(evt, false)
initKbEvents()
initMouseEvents()
initDragEvents()
proc checkTest1Status() =
var evtsCompleted = true
# Chrome doesn't seem to send DragExit, so ignore this
if (($navigator.userAgent).contains("Chrome")):
dragEventsCompleted[DragExit] = true
for evt, completed in dragEventsCompleted:
if not completed:
evtsCompleted = false
break
if evtsCompleted:
testRunning[0] = false
log "Test 1 completed successfully. Drag the div anywhere to start the next test."
testCompleted[0] = true
document.getElementById("checkTest1").style.visibility = "hidden"
else:
log "Test 1 has not completed yet. Make sure you drag the div over another element."
log "Status:"
for evt, completed in dragEventsCompleted:
log $evt & ": " & $completed
proc createDom(): VNode =
result = buildHtml(tdiv()):
tdiv(id="dragDiv", draggable="true"):
tdiv(class="divcontent"):
text "draggable div"
tdiv(id="results"):
for i in countdown(results.len - 1, 0):
tdiv:
text $results[i]
tdiv(id="checkTest1"):
button(onclick=checkTest1Status):
text "Check test 1 status"
input(`type`="hidden", id="test9value")
input(`type`="text", id="test10input1")
input(`type`="text", id="test10input2")
h3(id="test11h3"):
text "test 11"
input(`type`="text", id="test12input")
proc onload() =
startTests()
setRenderer createDom
onload()

10253
tests/jquery-3.2.1.js vendored Normal file

File diff suppressed because it is too large Load diff

27
tests/nativehtmlgen.nim Normal file
View file

@ -0,0 +1,27 @@
## Simple test that shows Karax can also do client-side HTML rendering.
import "../karax" / [karaxdsl, vdom]
when defined(js):
{.error: "Use 'nim c' to compile this example".}
template kxi(): int = 0
template addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =
n.setAttr($k, action)
let tab = buildHtml(table):
tr:
td:
text "Cell A"
td:
text "Cell B"
tr:
td:
text "Cell C"
td:
text "Cell D"
td:
a(href = "#/", onclick = "javascript:myFunc()"):
text"haha"
echo tab

View file

@ -1 +1 @@
--path: "../src" --path: "../karax"

View file

@ -14,5 +14,9 @@ proc main =
exec("nim js examples/mediaplayer/playerapp.nim") exec("nim js examples/mediaplayer/playerapp.nim")
exec("nim js examples/carousel/carousel.nim") exec("nim js examples/carousel/carousel.nim")
exec("nim js -d:nodejs -r tests/difftest.nim") exec("nim js -d:nodejs -r tests/difftest.nim")
exec("nim c tests/nativehtmlgen.nim")
for test in os.walkFiles("examples/*.nim"):
exec("nim js " & test)
main() main()

View file

@ -1,49 +0,0 @@
## 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
const
html = """
<!DOCTYPE html>
<html>
<head><title>$1</title></head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="$1.js"></script>
</body>
</html>
"""
proc exec(cmd: string) =
if os.execShellCmd(cmd) != 0:
quit "External command failed: " & cmd
proc main =
var op = initOptParser()
var rest = op.cmdLineRest
var file = ""
var run = false
while true:
op.next()
case op.kind
of cmdLongOption:
if op.key == "run":
run = true
rest = rest.replace("--run ")
of cmdShortOption:
if op.key == "r":
run = true
rest = rest.replace("-r ")
of cmdArgument: file = op.key
of cmdEnd: break
if file.len == 0: quit "filename expected"
let name = file.splitFile.name
createDir("nimcache")
exec("nim js --out:nimcache/" & name & ".js " & rest)
let dest = "nimcache" / name & ".html"
writeFile(dest, html % name)
if run: openDefaultBrowser(dest)
main()