diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..affdd05 --- /dev/null +++ b/readme.md @@ -0,0 +1,394 @@ +![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png) + +![Travis (.com) branch](https://img.shields.io/travis/com/karaxnim/karax/master?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 ``
`` 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 +``` + +## Licence +MIT Licence. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt). \ No newline at end of file diff --git a/readme.rst b/readme.rst deleted file mode 100644 index 8e1651b..0000000 --- a/readme.rst +++ /dev/null @@ -1,382 +0,0 @@ -Karax – Single page applications in Nim |travis| -================================================ - -Karax is a framework for developing single page applications in Nim. - -To try it out, run:: - - cd ~/projects # Insert your favourite directory for projects - - nimble develop karax # This will clone Karax and create a link to it in ~/.nimble - - cd karax - - cd examples/todoapp - nim js todoapp.nim - open todoapp.html - cd ../.. - - cd examples/mediaplayer - nim js playerapp.nim - open playerapp.html - -It uses a virtual DOM like React, but is much smaller than the existing -frameworks plus of course it's written in Nim for Nim. No external -dependencies! And thanks to Nim's whole program optimization only what -is used ends up in the generated JavaScript code. - - -Goals -===== - -- Leverage Nim's macro system to produce a framework that allows - for the development of applications that are boilerplate free. -- Keep it small, keep it fast, keep it flexible. - -.. |travis| image:: https://travis-ci.org/pragmagic/karax.svg?branch=master - :target: https://travis-ci.org/pragmagic/karax - - -Hello World -=========== - -The simplest Karax program looks like this: - -.. code-block:: nim - - include karax / prelude - - proc createDom(): VNode = - result = buildHtml(tdiv): - text "Hello World!" - - setRenderer createDom - - -Since ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead -here. ``tdiv`` produces a ``
`` 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 rand(100) <= 50: - text "Hello World!" - else: - text "Hello Universe" - - randomize() - setRenderer createDom - - -Produces: - -.. code-block:: 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: - -.. code-block:: nim - - include karax / prelude - # alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson] - - var lines: seq[kstring] = @[] - - proc createDom(): VNode = - result = buildHtml(tdiv): - button: - text "Say hello!" - proc onclick(ev: Event; n: VNode) = - lines.add "Hello simulated universe" - for x in lines: - tdiv: - text x - - setRenderer createDom - - -``kstring`` is Karax's alias for ``cstring`` (which stands for "compatible -string"; for the JS target that is an immutable JavaScript string) which -is preferred for efficiency on the JS target. However, on the native targets -``kstring`` is mapped to ``string`` for efficiency. The DSL for HTML -construction is also avaible for the native targets (!) and the ``kstring`` -abstraction helps to deal with these conflicting requirements. - -Karax's DSL is quite flexible when it comes to event handlers, so the -following syntax is also supported: - -.. code-block:: nim - - include karax / prelude - from sugar import `=>` - - var lines: seq[kstring] = @[] - - proc createDom(): VNode = - result = buildHtml(tdiv): - button(onclick = () => lines.add "Hello simulated universe"): - text "Say hello!" - for x in lines: - tdiv: - text x - - setRenderer createDom - - -The ``buildHtml`` macro produces this code for us: - -.. code-block:: nim - - let tmp2 = tree(VNodeKind.tdiv) - let tmp3 = tree(VNodeKind.button) - addEventHandler(tmp3, EventKind.onclick, - () => lines.add "Hello simulated universe", kxi) - add(tmp3, text "Say hello!") - add(tmp2, tmp3) - for x in lines: - let tmp4 = tree(VNodeKind.tdiv) - add(tmp4, text x) - add(tmp2, tmp4) - tmp2 - -As the examples grow larger it becomes more and more visible of what -a DSL that composes with the builtin Nim control flow constructs buys us. -Once you have tasted this power there is no going back and languages -without AST based macro system simply don't cut it anymore. - - -Attaching data to an event handler -================================== - -Since the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any -additional data that should be passed to the event handler needs to be -done via Nim's closures. In general this means a pattern like this: - -.. code-block:: nim - - proc menuAction(menuEntry: kstring): proc() = - result = proc() = - echo "clicked ", menuEntry - - proc buildMenu(menu: seq[kstring]): VNode = - result = buildHtml(tdiv): - for m in menu: - nav(class="navbar is-primary"): - tdiv(class="navbar-brand"): - a(class="navbar-item", onclick = menuAction(m)): - - -DOM diffing -=========== - -Ok, so now we have seen DOM creation and event handlers. But how does -Karax actually keep the DOM up to date? The trick is that every event -handler is wrapped in a helper proc that triggers a *redraw* operation -that calls the *renderer* that you initially passed to ``setRenderer``. -So a new virtual DOM is created and compared against the previous -virtual DOM. This comparison produces a patch set that is then applied -to the real DOM the browser uses internally. This process is called -"virtual DOM diffing" and other frameworks, most notably Facebook's -*React*, do quite similar things. The virtual DOM is faster to create -and manipulate than the real DOM so this approach is quite efficient. - - -Form validation -=============== - -Most applications these days have some "login" -mechanism consisting of ``username`` and ``password`` and -a ``login`` button. The login button should only be clickable -if ``username`` and ``password`` are not empty. An error -message should be shown as long as one input field is empty. - -To create new UI elements we write a ``loginField`` proc that -returns a ``VNode``: - -.. code-block:: nim - - proc loginField(desc, field, class: kstring; - validator: proc (field: kstring): proc ()): VNode = - result = buildHtml(tdiv): - label(`for` = field): - text desc - input(class = class, id = field, onchange = validator(field)) - -We use the ``karax / errors`` module to help with this error -logic. The ``errors`` module is mostly a mapping from strings to -strings but it turned out that the logic is tricky enough to warrant -a library solution. ``validateNotEmpty`` returns a closure that -captures the ``field`` parameter: - -.. code-block:: nim - - proc validateNotEmpty(field: kstring): proc () = - result = proc () = - let x = getVNodeById(field).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: - - -.. 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 `_.) - - -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. - -.. code-block:: 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: - -.. code-block:: 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