{"id":13458118,"url":"https://github.com/karaxnim/karax","last_synced_at":"2025-04-11T09:34:27.510Z","repository":{"id":37031295,"uuid":"86560897","full_name":"karaxnim/karax","owner":"karaxnim","description":"Karax. Single page applications for Nim.","archived":false,"fork":false,"pushed_at":"2025-04-10T23:20:55.000Z","size":968,"stargazers_count":1100,"open_issues_count":14,"forks_count":92,"subscribers_count":29,"default_branch":"master","last_synced_at":"2025-04-11T00:20:38.686Z","etag":null,"topics":["nim-language","spa"],"latest_commit_sha":null,"homepage":"","language":"Nim","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karaxnim.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-03-29T09:04:48.000Z","updated_at":"2025-04-10T23:20:27.000Z","dependencies_parsed_at":"2023-11-06T11:24:20.482Z","dependency_job_id":"ed00b766-882d-48f0-ab6e-76294988523f","html_url":"https://github.com/karaxnim/karax","commit_stats":null,"previous_names":["pragmagic/karax"],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karaxnim%2Fkarax","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karaxnim%2Fkarax/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karaxnim%2Fkarax/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karaxnim%2Fkarax/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karaxnim","download_url":"https://codeload.github.com/karaxnim/karax/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248368428,"owners_count":21092356,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["nim-language","spa"],"created_at":"2024-07-31T09:00:45.028Z","updated_at":"2025-04-11T09:34:27.496Z","avatar_url":"https://github.com/karaxnim.png","language":"Nim","funding_links":[],"categories":["Template engine","Uncategorized","Web"],"sub_categories":["Uncategorized","Frameworks"],"readme":"![karax](https://user-images.githubusercontent.com/22755228/117183486-482b2a00-ade0-11eb-88e6-d8eeb28951ca.png)\n\n![Github Actions](https://img.shields.io/github/actions/workflow/status/karaxnim/karax/ci.yml?branch=master\u0026style=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\u0026style=for-the-badge) ![https://nim-lang.org](https://img.shields.io/badge/nim-powered-ffc200?style=for-the-badge)\n \n# Karax\nKarax is a framework for developing single page applications in Nim.\n\n## Install\n\nTo use Karax you must have nim installed. You can follow the instructions [here](https://nim-lang.org/install.html).\n\nThen you can install karax through nimble:\n``nimble install karax``\n\n## Try Karax\nTo try it out, run:\n\n``cd ~/projects # Insert your favourite directory for projects``\n\n``nimble develop karax # This will clone Karax and create a link to it in ~/.nimble``\n\n``cd karax``\n\n``cd examples/todoapp``\n\n``nim js todoapp.nim``\n\n``open todoapp.html``\n\n``cd ../..``\n\n``cd examples/mediaplayer``\n\n``nim js playerapp.nim``\n\n``open playerapp.html``\n\nIt uses a virtual DOM like React, but is much smaller than the existing\nframeworks plus of course it's written in Nim for Nim. No external\ndependencies! And thanks to Nim's whole program optimization only what\nis used ends up in the generated JavaScript code.\n\n\n## Goals\n\n\n- Leverage Nim's macro system to produce a framework that allows\n  for the development of applications that are boilerplate free.\n- Keep it small, keep it fast, keep it flexible.\n\n\n\n## Hello World\n\n\nThe simplest Karax program looks like this:\n\n```nim\n\ninclude karax / prelude\n\nproc createDom(): VNode =\n  result = buildHtml(tdiv):\n    text \"Hello World!\"\n\nsetRenderer createDom\n```\n\nSince ``div`` is a keyword in Nim, karax choose to use ``tdiv`` instead\nhere. ``tdiv`` produces a ``\u003cdiv\u003e`` virtual DOM node.\n\nAs you can see, karax comes with its own ``buildHtml`` DSL for convenient\nconstruction of (virtual) DOM trees (of type ``VNode``). Karax provides\na tiny build tool called ``karun`` that generates the HTML boilerplate code that\nembeds and invokes the generated JavaScript code:\n\n```shell\nnim c karax/tools/karun\nkarax/tools/karun -r helloworld.nim\n```\n\nVia ``-d:debugKaraxDsl`` we can have a look at the produced Nim code by\n``buildHtml``:\n\n```nim\n\nlet tmp1 = tree(VNodeKind.tdiv)\nadd(tmp1, text \"Hello World!\")\ntmp1\n```\n(I shortened the IDs for better readability.)\n\nOk, so ``buildHtml`` introduces temporaries and calls ``add`` for the tree\nconstruction so that it composes with all of Nim's control flow constructs:\n\n\n```nim\n\ninclude karax / prelude\nimport random\n\nproc createDom(): VNode =\n  result = buildHtml(tdiv):\n    if rand(100) \u003c= 50:\n      text \"Hello World!\"\n    else:\n      text \"Hello Universe\"\n\nrandomize()\nsetRenderer createDom\n\n```\nProduces:\n\n```nim\n\nlet tmp1 = tree(VNodeKind.tdiv)\nif rand(100) \u003c= 50:\n  add(tmp1, text \"Hello World!\")\nelse:\n  add(tmp1, text \"Hello Universe\")\ntmp1\n```\n\n## Event model\n\nKarax does not change the DOM's event model much, here is a program\nthat writes \"Hello simulated universe\" on a button click:\n\n```nim\n\ninclude karax / prelude\n# alternatively: import karax / [kbase, vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils, jjson]\n\nvar lines: seq[kstring] = @[]\n\nproc createDom(): VNode =\n  result = buildHtml(tdiv):\n    button:\n      text \"Say hello!\"\n      proc onclick(ev: Event; n: VNode) =\n        lines.add \"Hello simulated universe\"\n    for x in lines:\n      tdiv:\n        text x\n\nsetRenderer createDom\n```\n\n``kstring`` is Karax's alias for ``cstring`` (which stands for \"compatible\nstring\"; for the JS target that is an immutable JavaScript string) which\nis preferred for efficiency on the JS target. However, on the native targets\n``kstring`` is mapped  to ``string`` for efficiency. The DSL for HTML\nconstruction is also available for the native targets (!) and the ``kstring``\nabstraction helps to deal with these conflicting requirements.\n\nKarax's DSL is quite flexible when it comes to event handlers, so the\nfollowing syntax is also supported:\n\n```nim\n\ninclude karax / prelude\nfrom sugar import `=\u003e`\n\nvar lines: seq[kstring] = @[]\n\nproc createDom(): VNode =\n  result = buildHtml(tdiv):\n    button(onclick = () =\u003e lines.add \"Hello simulated universe\"):\n      text \"Say hello!\"\n    for x in lines:\n      tdiv:\n        text x\n\nsetRenderer createDom\n```\n\nThe ``buildHtml`` macro produces this code for us:\n\n```nim\n\nlet tmp2 = tree(VNodeKind.tdiv)\nlet tmp3 = tree(VNodeKind.button)\naddEventHandler(tmp3, EventKind.onclick,\n                () =\u003e lines.add \"Hello simulated universe\", kxi)\nadd(tmp3, text \"Say hello!\")\nadd(tmp2, tmp3)\nfor x in lines:\n  let tmp4 = tree(VNodeKind.tdiv)\n  add(tmp4, text x)\n  add(tmp2, tmp4)\ntmp2\n```\nAs the examples grow larger it becomes more and more visible of what\na DSL that composes with the builtin Nim control flow constructs buys us.\nOnce you have tasted this power there is no going back and languages\nwithout AST based macro system simply don't cut it anymore.\n\n\n## Reactivity\n\nKarax's reactivity model is different to mainstream frameworks, who usually implement it by creating reactive state. Karax instead reacts to events.\n\nThis approach is simpler and easier to reason about, with the tradeoff being that events need to be wrapped to trigger a redraw. Karax does this for you with dom event handlers (`onclick`, `keyup`, etc) and ajax network calls (when using `karax/kajax`), but you will need to add it for things outside of that (websocket messages, document timing functions, etc).\n\n`karax/kdom` includes a definition for `setInterval`, the browser api that repeatedly calls a given function. By default it is not reactive, so this is how we might add reactivity with a call to `redraw`:\n\n```nim\ninclude karax/prelude\nimport karax/kdom except setInterval\n\nproc setInterval(cb: proc(), interval: int): Interval {.discardable.} =\n  kdom.setInterval(proc =\n    cb()\n    if not kxi.surpressRedraws: redraw(kxi)\n  , interval)\n\nvar v = 10\n\nproc update =\n  v += 10\n\nsetInterval(update, 200)\n\nproc main: VNode =\n  buildHtml(tdiv):\n    text $v\n\nsetRenderer main\n```\n\n\n## Attaching data to an event handler\n\n\nSince the type of an event handler is ``(ev: Event; n: VNode)`` or ``()`` any\nadditional data that should be passed to the event handler needs to be\ndone via Nim's closures. In general this means a pattern like this:\n\n```nim\n\nproc menuAction(menuEntry: kstring): proc() =\n  result = proc() =\n    echo \"clicked \", menuEntry\n\nproc buildMenu(menu: seq[kstring]): VNode =\n  result = buildHtml(tdiv):\n    for m in menu:\n      nav(class=\"navbar is-primary\"):\n        tdiv(class=\"navbar-brand\"):\n          a(class=\"navbar-item\", onclick = menuAction(m)):\n```\n\n## DOM diffing\n\nOk, so now we have seen DOM creation and event handlers. But how does\nKarax actually keep the DOM up to date? The trick is that every event\nhandler is wrapped in a helper proc that triggers a *redraw* operation\nthat calls the *renderer* that you initially passed to ``setRenderer``.\nSo a new virtual DOM is created and compared against the previous\nvirtual DOM. This comparison produces a patch set that is then applied\nto the real DOM the browser uses internally. This process is called\n\"virtual DOM diffing\" and other frameworks, most notably Facebook's\n*React*, do quite similar things. The virtual DOM is faster to create\nand manipulate than the real DOM so this approach is quite efficient.\n\n\n## Form validation\nMost applications these days have some \"login\"\nmechanism consisting of ``username`` and ``password`` and\na ``login`` button. The login button should only be clickable\nif ``username`` and ``password`` are not empty. An error\nmessage should be shown as long as one input field is empty.\n\nTo create new UI elements we write a ``loginField`` proc that\nreturns a ``VNode``:\n\n```nim\n\nproc loginField(desc, field, class: kstring;\n                validator: proc (field: kstring): proc ()): VNode =\n  result = buildHtml(tdiv):\n    label(`for` = field):\n      text desc\n    input(class = class, id = field, onchange = validator(field))\n```\n\nWe use the ``karax / errors`` module to help with this error\nlogic. The ``errors`` module is mostly a mapping from strings to\nstrings but it turned out that the logic is tricky enough to warrant\na library solution. ``validateNotEmpty`` returns a closure that\ncaptures the ``field`` parameter:\n\n```nim\n\nproc validateNotEmpty(field: kstring): proc () =\n  result = proc () =\n    let x = getVNodeById(field).getInputText\n    if x.isNil or x == \"\":\n      errors.setError(field, field \u0026 \" must not be empty\")\n    else:\n      errors.setError(field, \"\")\n```\n\nThis indirection is required because\nevent handlers in Karax need to have the type ``proc ()``\nor ``proc (ev: Event; n: VNode)``. The errors module also\ngives us a handy ``disableOnError`` helper. It returns\n``\"disabled\"`` if there are errors. Now we have all the\npieces together to write our login dialog:\n\n\n```nim\n\n# some consts in order to prevent typos:\nconst\n  username = kstring\"username\"\n  password = kstring\"password\"\n\nvar loggedIn: bool\n\nproc loginDialog(): VNode =\n  result = buildHtml(tdiv):\n    if not loggedIn:\n      loginField(\"Name :\", username, \"input\", validateNotEmpty)\n      loginField(\"Password: \", password, \"password\", validateNotEmpty)\n      button(onclick = () =\u003e (loggedIn = true), disabled = errors.disableOnError()):\n        text \"Login\"\n      p:\n        text errors.getError(username)\n      p:\n        text errors.getError(password)\n    else:\n      p:\n        text \"You are now logged in.\"\n\nsetRenderer loginDialog\n```\n\n(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/login.nim).)\n\nThis code still has a bug though, when you run it, the ``login`` button is not\ndisabled until some input fields are validated! This is easily fixed,\nat initialization we have to do:\n\n```nim\n\nsetError username, username \u0026 \" must not be empty\"\nsetError password, password \u0026 \" must not be empty\"\n```\nThere are likely more elegant solutions to this problem.\n\n## Boolean attributes\n\nSome HTML attributes don't have meaningful values; instead, they are treated like\na boolean whose value is `false` when the attribute is not set, and `true` when\nthe attribute is set to any value. Some examples of these attributes are `disabled`\nand `contenteditable`.\n\nIn Karax, these attributes can be set/cleared with a boolean value:\n\n```nim\nproc submitButton(dataIsValid: bool): VNode =\n  buildHtml(tdiv):\n    button(disabled = not dataIsValid):\n      if dataIsValid:\n        text \"Submit\"\n      else:\n        text \"Cannot submit, data is invalid!\"\n```\n\n## Routing\n\n\nFor routing ``setRenderer`` can be called with a callback that takes a parameter of\ntype ``RouterData``. Here is the relevant excerpt from the famous \"Todo App\" example:\n\n```nim\n\nproc createDom(data: RouterData): VNode =\n  if data.hashPart == \"#/\": filter = all\n  elif data.hashPart == \"#/completed\": filter = completed\n  elif data.hashPart == \"#/active\": filter = active\n  result = buildHtml(tdiv(class=\"todomvc-wrapper\")):\n    section(class = \"todoapp\"):\n        ...\n\nsetRenderer createDom\n```\n(Full example [here](https://github.com/karaxnim/karax/blob/master/examples/todoapp/todoapp.nim).)\n\n## Server Side HTML Rendering\n\nKarax can also be used to render HTML on the server.  Only a subset of\nmodules can be used since there is no JS interpreter.\n\n```nim\n\nimport karax / [karaxdsl, vdom]\n\nconst places = @[\"boston\", \"cleveland\", \"los angeles\", \"new orleans\"]\n\nproc render*(): string =\n  let vnode = buildHtml(tdiv(class = \"mt-3\")):\n    h1: text \"My Web Page\"\n    p: text \"Hello world\"\n    ul:\n      for place in places:\n        li: text place\n    dl:\n      dt: text \"Can I use Karax for client side single page apps?\"\n      dd: text \"Yes\"\n\n      dt: text \"Can I use Karax for server side HTML rendering?\"\n      dd: text \"Yes\"\n  result = $vnode\n\necho render()\n```\n\nYou can embed raw html using the `verbatim` proc:\n\n``` nim\nlet vg = \"\"\"\n\u003csvg height=\"100\" width=\"100\"\u003e\n\u003ccircle cx=\"50\" cy=\"50\" r=\"40\" stroke=\"black\" stroke-width=\"3\" fill=\"red\" /\u003e\nSorry, your browser does not support inline SVG.\n\u003c/svg\u003e\n\"\"\"\nlet wrap = buildHtml(tdiv(class=\"wrapper\")):\n    verbatim(vg)\n\necho wrap\n```\n\n## Generate HTML with event handlers\n\nIf you are writing a static site generator or do server-side HTML rendering\nvia ``nim c``, you may want to override ``addEventHandler`` when using event\nhandlers to avoid compiler complaints.\n\nHere's an example of auto submit a dropdown when a value is selected:\n\n```nim\n\ntemplate kxi(): int = 0\ntemplate addEventHandler(n: VNode; k: EventKind; action: string; kxi: int) =\n  n.setAttr($k, action)\n\nlet\n  names = @[\"nim\", \"c\", \"python\"]\n  selected_name = request.params.getOrDefault(\"name\")\n  hello = buildHtml(html):\n    form(`method` = \"get\"):\n      select(name=\"name\", onchange=\"this.form.submit()\"):\n        for name in names:\n          if name == selected_name:\n            option(selected = \"\"): text name\n          else:\n            option: text name\n```\n\n## Debugging\n\nKarax will accept various compile time flags to add additional checks and debug info.\n\ne.g. `nim js -d:debugKaraxDsl myapp.nim`\n\n| flag name       | description |\n| --------------- | ----------- |\n| debugKaraxDsl   | prints the Nim code produced by the `buildHtml` macro to the terminal at compile time |\n| debugKaraxSame  | Ensures that the rendered html dom matches the expected output from the vdom. Note that some browser extensions will modify the page and cause false positives |\n| karaxDebug*     | prints debug info when checking the dom output and applying component state |\n| stats*          | track statistics about recursion depth when rendering |\n| profileKarax*   | track statistics about why nodes differ |\n\n_* = used when debugging karax itself, not karax apps_\n\n## License\nMIT License. See [here](https://github.com/karaxnim/karax/blob/master/LICENSE.txt).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaraxnim%2Fkarax","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkaraxnim%2Fkarax","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaraxnim%2Fkarax/lists"}