diff --git a/experiments/menus.nim b/experiments/menus.nim
index 3f79d83..6ea0529 100644
--- a/experiments/menus.nim
+++ b/experiments/menus.nim
@@ -1,6 +1,6 @@
## Example program that shows how to create menus with Karax.
-include karaxprelude
+include prelude
import jstrutils, kdom
proc contentA(): VNode =
diff --git a/karax/kajax.nim b/karax/kajax.nim
index 80184d1..114b29d 100644
--- a/karax/kajax.nim
+++ b/karax/kajax.nim
@@ -8,12 +8,13 @@ import karax
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)
- redraw(kxi)
+ if doRedraw: redraw(kxi)
type
HttpRequest {.importc.} = ref object
@@ -47,30 +48,35 @@ proc ajax*(meth, url: cstring; headers: openarray[(cstring, cstring)];
proc ajaxPost*(url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
cont: proc (httpStatus: int, response: cstring);
+ doRedraw: bool = true,
kxi: KaraxInstance = kxi) =
- ajax("POST", url, headers, data, cont, 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, kxi, true, data)
+ 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, 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, 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, kxi)
+ ajax("DELETE", url, headers, nil, cont, doRedraw, kxi)
proc toJson*[T](data: T): cstring {.importc: "JSON.stringify".}
diff --git a/karax/karaxdsl.nim b/karax/karaxdsl.nim
index 5d0cde4..307153a 100644
--- a/karax/karaxdsl.nim
+++ b/karax/karaxdsl.nim
@@ -19,6 +19,12 @@ proc getName(n: NimNode): string =
result.add getName(n[i])
of nnkStrLit..nnkTripleStrLit:
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)
else:
#echo repr n
expectKind(n, nnkIdent)
diff --git a/karax/vdom.nim b/karax/vdom.nim
index f9df2bf..b5556eb 100644
--- a/karax/vdom.nim
+++ b/karax/vdom.nim
@@ -84,6 +84,10 @@ type
onsubmit, ## A form is submitted
oninput, ## An input value changes
+ onanimationstart,
+ onanimationend,
+ 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)
diff --git a/readme.rst b/readme.rst
index 2a34365..38ae2b5 100644
--- a/readme.rst
+++ b/readme.rst
@@ -2,8 +2,8 @@ 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.
+The API is reasonably stable and version 1 should arrive anytime soon
+now.
To try it out, run::
@@ -25,10 +25,7 @@ To try it out, run::
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
+is used ends up in the generated JavaScript code.
Goals
@@ -40,3 +37,289 @@ Goals
.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master
:target: https://travis-ci.org/pragmagic/karax
+
+
+Hello World
+===========
+
+The simplest Karax program looks like this:
+
+.. code-block:: nim
+
+ include karax / prelude
+
+ proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ text "Hello World!"
+
+ setRenderer createDom
+
+
+Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead
+here. ``tdiv`` produces a ``
`` virtual DOM node.
+
+As you can see, karax comes with its own ``buildHtml`` DSL for convenient
+construction of (virtual) DOM trees (of type ``VNode``). Karax provides
+a tiny build tool called ``karun`` that generates the HTML boilerplate code that
+embeds and invokes the generated JavaScript code::
+
+ nim c karax/tools/karun
+ karax/tools/karun -r helloworld.nim
+
+Via ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by
+``buildHtml``:
+
+.. code-block:: nim
+
+ let tmp1 = tree(VNodeKind.tdiv)
+ add(tmp1, text "Hello World!")
+ tmp1
+
+(I shortened the IDs for better readability.)
+
+Ok, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree
+construction so that it composes with all of Nim's control flow constructs:
+
+
+.. code-block:: nim
+
+ include karax / prelude
+ import random
+
+ proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ if random(100) <= 50:
+ text "Hello World!"
+ else:
+ text "Hello Universe"
+
+ randomize()
+ setRenderer createDom
+
+
+Produces:
+
+.. code-block:: nim
+
+ let tmp1 = tree(VNodeKind.tdiv)
+ if random(100) <= 50:
+ add(tmp1, text "Hello World!")
+ else:
+ add(tmp1, text "Hello Universe")
+ tmp1
+
+
+Event model
+===========
+
+Karax does not change the DOM's event model much, here is a program
+that writes "Hello simulated universe" on a button click:
+
+.. code-block:: nim
+
+ include karax / prelude
+
+ var lines: seq[kstring] = @[]
+
+ proc createDom(): VNode =
+ result = buildHtml(tdiv):
+ button:
+ text "Say hello!"
+ proc onclick(ev: Event; n: VNode) =
+ lines.add "Hello simulated universe"
+ for x in lines:
+ tdiv:
+ text x
+
+ setRenderer createDom
+
+
+``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible
+string"; for the JS target that is an immutable JavaScript string) which
+is preferred for efficiency on the JS target. However, on the native targets
+``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML
+construction is also avaible for the native targets (!) and the ``kstring``
+abstraction helps to deal with these conflicting requirements.
+
+Karax's DSL is quite flexible when it comes to event handlers, so the
+following syntax is also supported:
+
+.. code-block:: nim
+
+ include karax / prelude
+ from 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
+
+
+The ``buildHtml`` macro produces this code for us:
+
+.. code-block:: nim
+
+ let tmp2 = tree(VNodeKind.tdiv)
+ let tmp3 = tree(VNodeKind.button)
+ addEventHandler(tmp3, EventKind.onclick,
+ () => lines.add "Hello simulated universe", kxi)
+ add(tmp3, text "Say hello!")
+ add(tmp2, tmp3)
+ for x in lines:
+ let tmp4 = tree(VNodeKind.tdiv)
+ add(tmp4, text x)
+ add(tmp2, tmp4)
+ tmp2
+
+As the examples grow larger it becomes more and more visible of what
+a DSL that composes with the builtin Nim control flow constructs buys us.
+Once you have tasted this power there is no going back and languages
+without AST based macro system simply don't cut it anymore.
+
+
+Attaching data to an event handler
+==================================
+
+Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any
+additional data that should be passed to the event handler needs to be
+done via Nim's closures. In general this means a pattern like this:
+
+.. code-block:: nim
+
+ proc menuAction(menuEntry: kstring): proc() =
+ result = proc() =
+ echo "clicked ", menuEntry
+
+ proc buildMenu(menu: seq[kstring]): VNode =
+ result = buildHtml(tdiv):
+ for m in menu:
+ nav(class="navbar is-primary"):
+ tdiv(class="navbar-brand"):
+ a(class="navbar-item", onclick = menuAction(m)):
+
+
+DOM diffing
+===========
+
+Ok, so now we have seen DOM creation and event handlers. But how does
+Karax actually keep the DOM up to date? The trick is that every event
+handler is wrapped in a helper proc that triggers a *redraw* operation
+that calls the *renderer* that you initially passed to ``setRenderer``.
+So a new virtual DOM is created and compared against the previous
+virtual DOM. This comparison produces a patch set that is then applied
+to the real DOM the browser uses internally. This process is called
+"virtual DOM diffing" and other frameworks, most notably Facebook's
+*React*, do quite similar things. The virtual DOM is faster to create
+and manipulate than the real DOM so this approach is quite efficient.
+
+
+Form validation
+===============
+
+Most applications these days have some "login"
+mechanism consisting of ``username`` and ``password`` and
+a ``login`` button. The login button should only be clickable
+if ``username`` and ``password`` are not empty. An error
+message should be shown as long as one input field is empty.
+
+To create new UI elements we write a ``loginField`` proc that
+returns a ``VNode``:
+
+.. code-block:: nim
+
+ proc loginField(desc, field, class: kstring;
+ validator: proc (field: kstring): proc ()): VNode =
+ result = buildHtml(tdiv):
+ label(`for` = field):
+ text desc
+ input(class = class, id = field, onchange = validator(field))
+
+We use the ``karax / errors`` module to help with this error
+logic. The ``errors`` module is mostly a mapping from strings to
+strings but it turned out that the logic is tricky enough to warrant
+a library solution. ``validateNotEmpty`` returns a closure that
+captures the ``field`` parameter:
+
+.. code-block:: nim
+
+ proc validateNotEmpty(field: kstring): proc () =
+ result = proc () =
+ let x = getVNodeById(field)
+ if x.text.isNil or x.text == "":
+ errors.setError(field, field & " must not be empty")
+ else:
+ errors.setError(field, "")
+
+This indirection is required because
+event handlers in Karax need to have the type ``proc ()``
+or ``proc (ev: Event; n: VNode)``. The errors module also
+gives us a handy ``disableOnError`` helper. It returns
+``"disabled"`` if there are errors. Now we have all the
+pieces together to write our login dialog:
+
+
+.. code-block:: nim
+
+ # some consts in order to prevent typos:
+ const
+ username = kstring"username"
+ password = kstring"password"
+
+ var loggedIn: bool
+
+ proc loginDialog(): VNode =
+ result = buildHtml(tdiv):
+ if not loggedIn:
+ loginField("Name :", username, "input", validateNotEmpty)
+ loginField("Password: ", password, "password", validateNotEmpty)
+ button(onclick = () => (loggedIn = true), disabled = errors.disableOnError()):
+ text "Login"
+ p:
+ text errors.getError(username)
+ p:
+ text errors.getError(password)
+ else:
+ p:
+ text "You are now logged in."
+
+ setRenderer loginDialog
+
+(Full example `here `_.)
+
+This code still has a bug though, when you run it, the ``login`` button is not
+disabled until some input fields are validated! This is easily fixed,
+at initialization we have to do:
+
+.. code-block:: nim
+ setError username, username & " must not be empty"
+ setError password, password & " must not be empty"
+
+There are likely more elegant solutions to this problem.
+
+
+Routing
+=======
+
+For routing ``setRenderer`` can be called with a callback that takes a parameter of
+type ``RouterData``. Here is the relevant excerpt from the famous "Todo App" example:
+
+.. code-block:: nim
+
+ proc createDom(data: RouterData): VNode =
+ if data.hashPart == "#/": filter = all
+ elif data.hashPart == "#/completed": filter = completed
+ elif data.hashPart == "#/active": filter = active
+ result = buildHtml(tdiv(class="todomvc-wrapper")):
+ section(class = "todoapp"):
+ ...
+
+ setRenderer createDom
+
+(Full example `here `_.)