Code Monkey home page Code Monkey logo

karax's Introduction

karax

Github Actions GitHub issues GitHub GitHub tag (latest SemVer) https://nim-lang.org

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.

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:

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:

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:

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:

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:

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 available 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:

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:

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.

Reactivity

Karax's reactivity model is different to mainstream frameworks, who usually implement it by creating reactive state. Karax instead reacts to events.

This 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).

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:

include karax/prelude
import karax/kdom except setInterval

proc setInterval(cb: proc(), interval: int): Interval {.discardable.} =
  kdom.setInterval(proc =
    cb()
    if not kxi.surpressRedraws: redraw(kxi)
  , interval)

var v = 10

proc update =
  v += 10

setInterval(update, 200)

proc main: VNode =
  buildHtml(tdiv):
    text $v

setRenderer main

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:

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:

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:

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:

# 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:

setError username, username & " must not be empty"
setError password, password & " must not be empty"

There are likely more elegant solutions to this problem.

Boolean attributes

Some HTML attributes don't have meaningful values; instead, they are treated like a boolean whose value is false when the attribute is not set, and true when the attribute is set to any value. Some examples of these attributes are disabled and contenteditable.

In Karax, these attributes can be set/cleared with a boolean value:

proc submitButton(dataIsValid: bool): VNode =
  buildHtml(tdiv):
    button(disabled = not dataIsValid):
      if dataIsValid:
        text "Submit"
      else:
        text "Cannot submit, data is invalid!"

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:

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.

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()

You can embed raw html using the verbatim proc:

let vg = """
<svg height="100" width="100">
<circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
Sorry, your browser does not support inline SVG.
</svg>
"""
let wrap = buildHtml(tdiv(class="wrapper")):
    verbatim(vg)

echo wrap

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:

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

Debugging

Karax will accept various compile time flags to add additional checks and debug info.

e.g. nim js -d:debugKaraxDsl myapp.nim

flag name description
debugKaraxDsl prints the Nim code produced by the buildHtml macro to the terminal at compile time
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
karaxDebug* prints debug info when checking the dom output and applying component state
stats* track statistics about recursion depth when rendering
profileKarax* track statistics about why nodes differ

* = used when debugging karax itself, not karax apps

License

MIT License. See here.

karax's People

Contributors

ajusa avatar alehander92 avatar araq avatar bluenote10 avatar bung87 avatar chancyk avatar choltreppe avatar darkmusic avatar dawkot avatar daylinmorgan avatar dom96 avatar geotre avatar gogolxdong avatar hitoshi44 avatar kidandcat avatar metagn avatar moigagoo avatar monyarm avatar not-lum avatar oskca avatar planetis-m avatar pnuzhdin avatar ringabout avatar ryukoposting avatar scattergory avatar timotheecour avatar untoreh avatar willyboar avatar xyb avatar zedeus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

karax's Issues

Calling proc with module path raises Error: Expected a node of kind nnkIdent, got nnkDotExpr

Consider the following component code (likebutton.nim):

import future

include karax/prelude


var liked = false


proc render*(): VNode =
  buildHtml:
    if liked:
      text "You liked this"
    else:
      button(onclick = () => (liked = true)):
        text "Like"

and the root component (index.nim):

include karax/prelude

from likebutton import nil


proc render(): VNode =
  buildHtml(tdiv):
    h1:
      text "Hello Karax!"
    likebutton.render()


when isMainModule:
  setRenderer render

karun -r index.nim fails with index.nim(10, 15) Error: Expected a node of kind nnkIdent, got nnkDotExpr

From https://forum.nim-lang.org/t/4038

Uncaught TypeError: Converting circular structure to JSON while using toJson

It happens when you are using a copy of an object with a recursive type in the deep even if the real value is nil.

For example:

import karax, kajax, vdom, components, karaxdsl

type
  Column = object
    t: ref Column

  Test = object
    columns: seq[Column]

var test1 = Test(columns: @[Column(t: nil), Column(t: nil)])
let test2 = test1

kout toJSON(test1)
kout toJSON(test2)
{"columns":[{"t":null},{"t":null}]}
test.js:402 Uncaught TypeError: Converting circular structure to JSON
    at JSON.stringify (<anonymous>)
    at test.js:402

Of course, it will not work anyway in a case t will have real value. But user might have an ability to prepare data before marshaling e.g. set all known refs to nil.

One possible solution is to delete m_type field in toJson e.g.:

proc toJson*[T](data: T): cstring
  {.importc: """
    (function some(obj) {
      function clear(obj) {
          delete obj["m_type"];
          for (var i in obj) {
              if (obj[i] !== null && typeof(obj[i])=="object") {
                  clear(obj[i]);
              }
          }
      }
      clear(obj);
      return JSON.stringify(obj);
    })
  """.}

Documentation or Tutorials?

This really is a neat little project. After looking at some of the examples, the TODO one seems to be the best one, but I'm having a little difficulty of trying to make sense of how this framework... well... works. I understand that this is still in early development and in an experimental phase, but something would be nice.

Support for elements not managed by Karax, within Karax app

I'm running into some issues using Karax in a project. I have embedded the Monaco editor within my Karax app, and redraw operations fail. The same proc returns false when it detects the outside changes in the DOM. Is there an existing workaround to this issue? If not, I'm happy to add the functionality myself and submit a PR. All I need is some guidance on the best way to solve this problem. I can also provide more details if needed!

Karax license?

Nimble says Karax is MIT licensed, but actually the repo does not contain a license, which probably means that both private and commercial use are not permitted ;).

JS Error when ascii escaped characters are used with text macro

Hi:

import karax / [vdom, karax, karaxdsl, jstrutils, compact, localstorage]

type
  Filter = enum
    all, active, completed

var
  selectedEntry = -1
  filter: Filter
  entriesLen: int

const
  contentSuffix = cstring"content"
  completedSuffix = cstring"completed"
  lenSuffix = cstring"entriesLen"

proc getEntryContent(pos: int): cstring =
  result = getItem(&pos & contentSuffix)
  if result == cstring"null":
    result = nil

proc isCompleted(pos: int): bool =
  var value = getItem(&pos & completedSuffix)
  result = value == cstring"true"

proc setEntryContent(pos: int, content: cstring) =
  setItem(&pos & contentSuffix, content)

proc markAsCompleted(pos: int, completed: bool) =
  setItem(&pos & completedSuffix, &completed)

proc addEntry(content: cstring, completed: bool) =
  setEntryContent(entriesLen, content)
  markAsCompleted(entriesLen, completed)
  inc entriesLen
  setItem(lenSuffix, &entriesLen)

proc updateEntry(pos: int, content: cstring, completed: bool) =
  setEntryContent(pos, content)
  markAsCompleted(pos, completed)

proc onTodoEnter(ev: Event; n: VNode) =
  addEntry(n.value, false)
  n.value = ""

proc removeHandler(ev: Event; n: VNode) =
  updateEntry(n.index, cstring(nil), false)

proc editHandler(ev: Event; n: VNode) =
  selectedEntry = n.index

proc focusLost(ev: Event; n: VNode) = selectedEntry = -1

proc editEntry(ev: Event; n: VNode) =
  setEntryContent(n.index, n.value)
  selectedEntry = -1

proc toggleEntry(ev: Event; n: VNode) =
  let id = n.index
  markAsCompleted(id, not isCompleted(id))

proc onAllDone(ev: Event; n: VNode) =
  clear()
  selectedEntry = -1

proc clearCompleted(ev: Event, n: VNode) =
  for i in 0..<entriesLen:
    if isCompleted(i): setEntryContent(i, nil)

proc toClass(completed: bool): cstring =
  (if completed: cstring"completed" else: cstring(nil))

proc selected(v: Filter): cstring =
  (if filter == v: cstring"selected" else: cstring(nil))

proc createEntry(id: int; d: cstring; completed, selected: bool): VNode {.compact.} =
  result = buildHtml(tr):
    li(class=toClass(completed)):
      if not selected:
        tdiv(class = "view"):
          input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
                onclick=toggleEntry, index=id)
          label(onDblClick=editHandler, index=id):
            text d
          button(class = "destroy", index=id, onclick=removeHandler)
      else:
        input(class = "edit", name = "title", index=id,
          onblur = focusLost,
          onkeyupenter = editEntry, value = d, setFocus=true)

proc makeFooter(entriesCount, completedCount: int): VNode {.compact.} =
  result = buildHtml(footer(class = "footer")):
    span(class = "todo-count"):
      strong:
        text(&entriesCount)
      text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
    ul(class = "filters"):
      li:
        a(class = selected(all), href = "#/"):
          text "All"
      li:
        a(class = selected(active), href = "#/active"):
          text "Active"
      li:
        a(class = selected(completed), href = "#/completed"):
          text "Completed"
    button(class = "clear-completed", onclick = clearCompleted):
      text "Clear completed (" & &completedCount & ")"

proc makeHeader(): VNode {.compact.} =
  result = buildHtml(header(class = "header")):
    h1:
      text "todos"
    input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
          onkeyupenter = onTodoEnter, setFocus)

proc createDom(): VNode =
  result = buildHtml(tdiv(class="todomvc-wrapper")):
    section(class = "todoapp"):
      makeHeader()
      section(class = "main"):
        input(class = "toggle-all", `type` = "checkbox", name = "toggle")
        label(`for` = "toggle-all", onclick = onAllDone):
          text "Mark all as complete"
          text "Testing latin character \243"
        var entriesCount = 0
        var completedCount = 0
        ul(class = "todo-list"):
          #for i, d in pairs(entries):
          for i in 0..entriesLen-1:
            var d0 = getEntryContent(i)
            var d1 = isCompleted(i)
            if d0 != nil:
              let b = case filter
                      of all: true
                      of active: not d1
                      of completed: d1
              if b:
                createEntry(i, d0, d1, i == selectedEntry)
              inc completedCount, ord(d1)
              inc entriesCount
      makeFooter(entriesCount, completedCount)

setOnHashChange(proc(hash: cstring) =
  if hash == "#/": filter = all
  elif hash == "#/completed": filter = completed
  elif hash == "#/active": filter = active
)

if hasItem(lenSuffix):
  entriesLen = parseInt getItem(lenSuffix)
else:
  entriesLen = 0
setRenderer createDom

This returns an error on this statement:

text "Testing latin character \243"

URIError: malformed URI sequence
toJSStr

Add docs and tutorials

Karax looks like a cool project and I would like to use it, but there are barely any docs and no getting started guide.

Component DOM siblings been re-rendered even if only one of them been changed

There is a placeholder which changes each time input gets focus. I expect input to be not re-rendered or even not to lose focus because placeholder and input are siblings but it actually has been re-rendered and lost focus.

import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils

type TextInput* = ref object of VComponent
  value: cstring
  placeholder: cstring

proc render(x: VComponent): VNode =
  let self = TextInput(x)

  let style = style(
    (StyleAttr.position, cstring"relative"),
    (StyleAttr.paddingLeft, cstring"10px"),
    (StyleAttr.paddingRight, cstring"5px"),
    (StyleAttr.height, cstring"30px"),
    (StyleAttr.lineHeight, cstring"30px"),
    (StyleAttr.border, cstring"solid 1px black"),
    (StyleAttr.fontSize, cstring"12px"),
    (StyleAttr.fontWeight, cstring"600")
  ).merge(self.style)

  let inputStyle = style.merge(style(
    (StyleAttr.color, cstring"inherit"),
    (StyleAttr.fontSize, cstring"inherit"),
    (StyleAttr.fontWeight, cstring"inherit"),
    (StyleAttr.fontFamily, cstring"inherit"),
    (StyleAttr.position, cstring"absolute"),
    (StyleAttr.top, cstring"0"),
    (StyleAttr.left, cstring"0"),
    (StyleAttr.height, cstring"100%"),
    (StyleAttr.width, cstring"100%"),
    (StyleAttr.border, cstring"none"),
    (StyleAttr.backgroundColor, cstring"transparent"),
  ))

  let placeholderStyle = style(
    (StyleAttr.opacity, cstring"0.35"),
    (StyleAttr.fontStyle, cstring"italic"),
    (StyleAttr.overflow, cstring"hidden")
  )

  proc onfocus(ev: Event; n: VNode) =
    self.placeholder = self.placeholder & cstring"1"
    kout cstring"focus"
    markDirty(self)

  proc onblur(ev: Event; n: VNode) =
    kout cstring"blur"

  result = buildHtml(tdiv(style=style)):
    if self.value == nil or len(self.value) == 0:
      tdiv(style=placeholderStyle):
        text self.placeholder
    input(style=inputStyle, value=self.value, onblur=onblur, onfocus=onfocus)

proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""; placeholder: cstring = cstring""): TextInput =
  result = newComponent(TextInput, render)
  result.style = style
  result.value = value
  result.placeholder = placeholder

proc createDom(): VNode =
  result = buildHtml(tdiv):
    newTextInput(placeholder=cstring"test")

setRenderer createDom

SetConstr is not defined when using json

I assume the json mod is nim js friendly.

Reproduce using the following as source after compiling to js in the default html template:

include karaxprelude
import json
var jString = "{}"
var jNode = parseJson(jString)

Strange behaviour of focus/blur and VComponent.markDirty

Try to click in the input. Observe that border of the input has blinked to red but return back to the black because input has been replaced by markDirty which caused blur event right after the focus.

import vdom, kdom, vstyles, karax, karaxdsl, jdict, jstrutils

type TextInput* = ref object of VComponent
  value: cstring
  isActive: bool

proc render(x: VComponent): VNode =
  let self = TextInput(x)

  let style = style(
    (StyleAttr.position, cstring"relative"),
    (StyleAttr.paddingLeft, cstring"10px"),
    (StyleAttr.paddingRight, cstring"5px"),
    (StyleAttr.height, cstring"30px"),
    (StyleAttr.lineHeight, cstring"30px"),
    (StyleAttr.border, cstring"solid 1px " & (if self.isActive: cstring"red" else: cstring"black")),
    (StyleAttr.fontSize, cstring"12px"),
    (StyleAttr.fontWeight, cstring"600")
  ).merge(self.style)

  let inputStyle = style.merge(style(
    (StyleAttr.color, cstring"inherit"),
    (StyleAttr.fontSize, cstring"inherit"),
    (StyleAttr.fontWeight, cstring"inherit"),
    (StyleAttr.fontFamily, cstring"inherit"),
    (StyleAttr.position, cstring"absolute"),
    (StyleAttr.top, cstring"0"),
    (StyleAttr.left, cstring"0"),
    (StyleAttr.height, cstring"100%"),
    (StyleAttr.width, cstring"100%"),
    (StyleAttr.border, cstring"none"),
    (StyleAttr.backgroundColor, cstring"transparent"),
  ))

  proc onfocus(ev: Event; n: VNode) =
    if not self.isActive:
      kout cstring"focus"
      self.isActive = true
      markDirty(self)

  proc onblur(ev: Event; n: VNode) =
    if self.isActive:
      kout cstring"blur"
      self.isActive = false
      markDirty(self)

  result = buildHtml(tdiv(style=style)):
    input(style=inputStyle, value=self.value, onblur=onblur, onfocus=onfocus)

proc newTextInput*(style: VStyle = VStyle(); value: cstring = cstring""): TextInput =
  result = newComponent(TextInput, render)
  result.style = style
  result.value = value

proc createDom(): VNode =
  result = buildHtml(tdiv):
    newTextInput(value=cstring"test")

setRenderer createDom

EOL (\n) not being passed to textarea

The textarea character "\n" is being ignored when passed through the text() procedure.
It can be inserted by another procedure by using javascript calls but not by setting the variable and having it loaded during the renderDom process.
In short, everything comes out as a single continuous line.

This is very easily tested, but I can generate examples if needed.

No support for Shadow DOMs

This would be a useful feature for my current project.

Seems like a lot of changes would be required to support it though? For a start setRenderer would need to take an element rather than a string for the root dom and the KaraxInstance would also need to contain the rootElement.

I'm not sure what other changes would also need to be made but hopefully it can be done without breaking backwards compatibility.

nimble install: system module needs string

Hi there,
I wanted again to try Karax with Nim 0.17.0 and a "nimble install" at the project root gives me

$ nimble install                                                           *[master] 
     Info Hint: used config file '/etc/nim.cfg' [Conf]
     Error: Could not read package info file in /home/vince/bacasable/karax/karax.nimble;
        ...   Reading as ini file failed with: 
        ...     Invalid section: .
        ...   Evaluating as NimScript file failed with: 
        ...     Error: system module needs 'string'.

If it is not necessary to do that (as discussed in #2), then I have the same errors as discussed here #6 (comment)

meaning with nim js todoapp.nim:

/home/vince/bacasable/karax/src/components.nim(82, 13) Error: type mismatch: got (NimNode, proc (bl: typed, op: string): NimNode{.noSideEffect.}, void)
but expected one of: 
proc add(x: var cstring; y: cstring)
proc add[T](x: var seq[T]; y: openArray[T])
proc add(x: var string; y: cstring)
proc add[T](x: var seq[T]; y: T)
proc add(x: var string; y: string)
proc add(x: var string; y: char)
proc add(parent, kid: VNode)
proc add(c: ClassList; class: cstring)
proc add[A, B](t: var OrderedTable[A, B]; key: A; val: B)
proc add(father, child: NimNode): NimNode
proc add[A, B](t: TableRef[A, B]; key: A; val: B)
proc add(father: NimNode; children: varargs[NimNode]): NimNode
proc add[A, B](t: var Table[A, B]; key: A; val: B)
proc add[A, B](t: OrderedTableRef[A, B]; key: A; val: B)

buildHtml to take multiple elements

Not sure if this is by design, but having multiple top level elements in buildHtml does not seem to work:

proc createDom(): VNode =
  result = buildHtml():
    tdiv():
      text "test"
    tdiv():
      text "test"

Leads to: Error: expression 'tmp94011' is of type 'VNode' and has to be discarded

Wrapping them into one top level element solve the problem.

Nested VComponent Redraw is not working easily

I made a VComponent which has a child VComponent

when child vcomponent calls
markDirty(self)
redraw()

it doesn't redraw the child component.
I can see the change when the parent component gets update.

Thank you.

How to set and get cookie?

I used

        function setCookie(cname, cvalue, exdays) {
            var exdate = new Date();
            exdate.setDate(exdate.getDate() + exdays);
            document.cookie = cname + "=" + escape(cvalue) + ((exdays == null) ? "" : ";expires=" + exdate.toGMTString());
        }

        function getCookie(cname) {
            var name = cname + "=";
            var ca = document.cookie.split(';');
            for (var i = 0; i < ca.length; i++) {
                var c = ca[i];
                while (c.charAt(0) == ' ') {
                    c = c.substring(1);
                }
                if (c.indexOf(name) == 0) {
                    return c.substring(name.length, c.length);
                }
            }
            return "";
        }

in the html and import as

proc setCookie(cname:kstring, cvalue:kstring, exdays:int) {.importc.}
proc getCookie(cname:kstring):kstring {.importc.}

or

proc setCookie(cname:kstring, cvalue:kstring, exdays:int) = 
  if cname == kstring"":
    document.cookie = kstring""
  else:
    document.cookie = cname & "=" & cvalue & ";expires={exdays}"

proc getCookie(cname:kstring):cstring = document.cookie

Diffing performance issue

I can confirm that the event handler update problem (#35) is solved now for me. However it seems that this can trigger problems in the diffing. The problem is not visible in the small example attached to the other issue, but with a slightly more complex VDOM I'm now seeing update times >10 sec and "unresponsive scripts" warnings. The old diffing which did not update the event handlers was very quick.

I had some difficulties to reproduce the problem in smaller examples, so I'm attaching a more complete version. The problem should be reproducible via karun and clicking on one of the "x" elements.

include karaxprelude
import sequtils, future

type
  Model = ref object
    entries: seq[cstring]

proc postRenderCallback() =
  discard

proc onClick(ev: Event, n: VNode) =
  discard

proc onDragStart(ev: Event, n: VNode) =
  discard

proc dummy(): VNode =
  result = buildHtml():
    tdiv()

proc panel(title: string, body: VNode): VNode =
  result = buildHtml():
    tdiv(class="panel panel-default"):
      tdiv(class="panel-heading"):
        text title
      tdiv(class="panel-collapse"):
        tdiv(class="panel-body"):
          body

proc threePanels(viewL, viewC, viewR: VNode): VNode =
  result = buildHtml():
    tdiv(class="row"):
      tdiv(class="col-xs-2 sidebar sidebar-left"):
        viewL
      tdiv(class="col-xs-8 col-xs-offset-2"):
        viewC
      tdiv(class="col-xs-2 sidebar sidebar-right"):
        viewR

proc renderEntry(model: Model, entries: seq[cstring], elementIndex: int): VNode =
  proc onRemove(ev: Event, n: VNode) =
    model.entries.delete(elementIndex)
  result = buildHtml():
    span(id=entries[elementIndex], class="some-classes", draggable="true", ondragstart=onDragStart):
      span(class="label-float-left"):
        text entries[elementIndex]
      a(type="button", class="close label-float-right", onclick=onRemove):
        text "×"

proc dropzone(model: Model, entries: seq[cstring]): VNode =
  result = buildHtml():
    tdiv(class="dropzone"):
      for elementIndex, entry in entries.pairs():
        renderEntry(model, entries, elementIndex)

proc rowElement(model: Model, title: string, entries: seq[cstring], dropzoneId: cstring): VNode =
  result = buildHtml():
    tdiv(class="row vertical-align"):
      tdiv(class="col-xs-2"):
        text title
      tdiv(class="col-xs-10"):
        dropzone(model, entries)

proc viewCenter(model: Model): VNode =
  panel("Panel Header"):
    flatHtml():
      tdiv():
        rowElement(model, "Entries", model.entries, "rows".cstring)
        tdiv(class="form-group text-right"):
          tdiv(class="checkbox"):
            label():
              input(type="checkbox")
              text "Automatic update"
          button(type="button", class="btn btn-primary btn-sm", onclick=onClick):
            text "Update"

proc view(model: Model): VNode =
  result = buildHtml():
    tdiv():
      tdiv():
        text "Navbar here"
      tdiv(class="container-fluid below-navbar"):
        threePanels(
          dummy(),
          viewCenter(model),
          dummy(),
        )

proc runMain() =
  var model = Model(
    entries: toSeq(1..20).map(x => cstring($x))
  )
  proc renderer(): VNode =
    view(model)
  setRenderer(renderer, root="ROOT", clientPostRenderCallback=postRenderCallback)

runMain()

Empty page rendered if the VNode argument in render proc is not called `n`

Consider the following code:

include karax/prelude


const maxMessageLength = 10
var buttonEnabled = true


proc renderTweetBox*(): VNode =
  buildHtml(tdiv):
    textarea():
      proc onkeyup(event: Event, n: VNode) =
        buttonEnabled =
          if n.value.len < maxMessageLength: true
          else: false

    if buttonEnabled:
      button():
        text "Tweet"
    else:
      button(disabled = "disabled"):
        text "Tweet"


setRenderer renderTweetBox

It works fine (although nimsuggest raises a warning).

If I rename n to this, it stops working.

Compilation should not depend on variable names.

Event handler with arguments raises error

This code works:

include karax/prelude


const maxMessageLength = 10
var buttonEnabled = true


proc renderTweetBox*(): VNode =
  buildHtml(tdiv):
    textarea():
      proc onkeyup(event: Event, n: VNode) =
        buttonEnabled =
          if n.value.len < maxMessageLength: true
          else: false

    if buttonEnabled:
      button():
        text "Tweet"
    else:
      button(disabled = "disabled"):
        text "Tweet"


setRenderer renderTweetBox

but line 11 gets underlined by nimsuggest in VSCode with the following error:

type mismatch: got <VNode, EventKind, proc (event: Event, n: VNode){.gcsafe, locks: 0.}, KaraxInstance>

but expected one of:
proc addEventHandler(n: VNode; k: EventKind; action: EventHandler;
kxi: KaraxInstance = kxi)
first type mismatch at position: 3
required type: EventHandler
but expression 'proc (event: Event; n: VNode) = buttonEnabled = if len(value(n)) < 10: true else: false' is of type: proc (event: Event, n: VNode){.gcsafe, locks: 0.}
proc addEventHandler(n: VNode; k: EventKind; action: proc (); kxi: KaraxInstance = kxi)
first type mismatch at position: 3
required type: proc (){.closure.}
but expression 'proc (event: Event; n: VNode) = buttonEnabled = if len(value(n)) < 10: true else: false' is of type: proc (event: Event, n: VNode){.gcsafe, locks: 0.}

expression: addEventHandler(tmp179021, onkeyup, proc (event: Event; n: VNode) = buttonEnabled = if len(
value(n)) < 10: true else: false, kxi)

There is no such problem with handlers without arguments.

No easy way to specify attributes without repetition

    # TODO: Surely there is a better way to handle this.
    let rankSelect = buildHtml():
      if isAdmin:
        select(class="form-select", value = $profile.user.rank):
          for r in Rank:
            option(text $r)
      else:
        select(class="form-select", value = $profile.user.rank, disabled=""):
          for r in Rank:
            option(text $r)

There should be an easy way to avoid this repetition.

Publishing helper modules seperately?

Have you considered publishing the jdict and jstrutils modules as separate nimble packages? I think they are very universal and I would like to import them in other projects.

kdom enhancements + refactoring

I would like to propose some enhancements/refactoring to the kdom (mostly event-centric). Ideally these should also be considered for addition to the Nim JS dom as well (in fact, some of these changes are already in the Nim JS dom). The reason I am suggesting considering these enhancements for karax is because karax provides a convenient framework for testing and proving these changes, but I am fine with moving this over to Nim instead if requested.

The need for these changes initially arose from the lack of drag event functionality in the kdom, and noticing the divergence from the standard JavaScript event model, though I understand this was a deliberate design decision and can respect the desire to keep it that way.

NOTE: Some of these changes are breaking changes, in particular removing certain fields from Event that belong to child events (x, y, keyCode, etc.). As far as karax is concerned, this would require one small change: the enterWrapper() proc in karax.nim will need to cast the Event to KeyboardEvent in order to retrieve the keyCode property.

The changes I am proposing are as follows:

  • Refactor Event model to include more specific events in addition to the base Event:
    • UIEvent, which derives from Event
    • KeyboardEvent, which derives from UIEvent
    • MouseEvent, which derives from UIEvent
    • DragEvent, which derives from MouseEvent
    • Modify existing TouchEvent to derive from UIEvent
  • Add EventPhase enum to assist with determining phase of Event.
  • Expose a DomEvent enum to assist with addEventListener calls.
  • Add KeyboardEventKey enum to assist with getModifierState() calls.
  • Add MouseButtons enum to assist with mouse events.
  • Add helper enums to assist with drag events:
    • DataTransferItemKind
    • DataTransferDropEffect
    • DataTransferEffectAllowed
    • DragEventTypes
  • Add overload to addEventListener that accepts AddEventListenerOptions parameter, as well as AddEventListenerOptions type.
  • Add JFile object to assist with retrieving properties of files dropped onto the browser window.
  • Add Element.clientWidth, Element.clientHeight properties.
  • Add Window.screen property.
  • Add Window.getComputedStyle().
  • Change clearTimeout to accept a ref TimeOut instead of TimeOut.
  • Change setTimeout to return ref Timeout instead of Timeout.
  • Add Element.focus().
  • Add Document.querySelector(), Document.querySelectorAll().
  • Add Event.stopImmediatePropagation().
  • Add KeyboardEvent.getModifierState(), MouseEvent.getModifierState().
  • Add DataTransfer methods:
    • clearData()
    • getData()
    • setData()
    • setDragImage()
  • Add DataTransferItem method:
    • getAsFile()

Intervening elements are re-rendered if an element is added both before and after them

This example might explain better

include karax / prelude
import future
var page = 1

proc createDom(): VNode = buildHtml(tdiv):
  h1:
    text "Testing"
  if page == 3 or page == 4:
    tdiv: text "Some text"
  input(`type`="text")
  button(onclick=() => (page = 1)): text "1"
  button(onclick=() => (page = 2)): text "2"
  button(onclick=() => (page = 3)): text "3"
  button(onclick=() => (page = 4)): text "4"
  if page == 2 or page == 4:
    tdiv: text "Some text"
setRenderer createDom

Going from page 1 to page 2 or page 1 to page 3 works as expected. But page 1 to page 4 causes the input and buttons 1, 2 and 3 to be re-rendered.

Unicode enum string representation issue

There seems to be an issue with using the $ macro on enum strings containing unicode values in karax.
You can use karun --run to run the following code, and there should be a javascript error thrown, which can be seen in the browser developer tools.

include karax / prelude

type 
    NavType = enum 
        Home = "Home", 
        Review = "[漢字 Review]"
         
var 
    currentNav = Review

doAssert(currentNav == Review, "Check 1 failed") # OK
doAssert($Review == "[漢字 Review]", "Check 2 failed") # OK
doAssert($currentNav == $Review, "Check 3 failed")  # Fails here

proc createDom(): VNode =
    result = buildHtml(tdiv()):
        text "Current page: " & $currentNav
                    
setRenderer createDom

The assertion failure message is the following, which indicates the unicode characters in the string are in their escaped form, which is why the final assertion fails.

Uncaught Error: Error: unhandled exception: $currentNav == "[\xE6\xBC\xA2\xE5\xAD\x97 Review]" Check 3 failed [AssertionError]

This same test (without the karax content) works fine as a nim c -r test.

Diff recursion explosion with nested tags

I have a problem in a larger karax application, which was hard to reproduce, but I think this artificial example should be sufficient:

import vdom, karax, karaxdsl

type
  User = object
    name: cstring

var users = @[@[User(name: cstring"a")], @[User(name: cstring"b")]]

const MAX = 250

for z in 0..<MAX:
  users.add(@[])
  for y in 0..<20:
    users[^1].add(User(name: cstring("c" & $z)))

var specialLine = -1

proc render: VNode =
  result = buildHtml(tdiv):
    for i, line in users:
      var special = ""
      if specialLine == i:
        special = "focus"

      tdiv(class="line " & special):
        for m in line:
          span:
            text(m.name)

proc ok: VNode =
  # render()
  result = buildHtml(tdiv):
    tdiv:
      tdiv:
        render()

proc top: VNode =
  result = buildHtml(tdiv):
    section(id="main"):
      ok()

setRenderer(top)

I start this with karun --run example.nim and stats enabled and the only modifications I have are that instead of if kxi.recursion > 100:stuff I have if kxi.recursion mod 1000 == 0:kout kxi.recursion,to more easily monitor the value itself

When I call redraw, normally it logs ~5000 kxi.recursion which is almost instant.

When I change specialLine in the console, (e.g. to 2), the first redraw after that logs ~1200000 kxi.recursion.

If we test with changing ok: if you just have render() there, you log ~40000 kxi.recursion, but for each nested tdiv in which render VNode goes, it logs 3 * more recursion.

As a whole, even the 5000 value is weird, because there is if kxi.recursion > 100: debug log, so I assume that may be karax is not supposed to go even to that number: I guess it has also something to do with the huge number of tags (but 250 * ~20 shoudn't be that much)

Event handler doesn't update when DOM elements are deleted

I'm currently dealing with issues which come down to the following behavior. Small example with dynamic deletes from a closure event handler:

include karaxprelude

var elements = @[cstring"A", cstring"B", cstring"C", cstring"D", cstring"E"]

proc renderElement(text: cstring, index: int): VNode =

  kout(cstring"re-building closure for", text, index)
  proc remove(ev: Event, n: VNode) =
    kout(cstring"Deleting element from index: ", index)
    elements.delete(index)

  result = buildHtml():
    tdiv(onClick=remove):
      text text

proc buildDom(): VNode =
  result = buildHtml():
    tdiv:
      for index, text in elements.pairs:
        renderElement(text, index)

setRenderer buildDom

The initial render shows elements A B C D E. Clicking on C correctly says Deleting element from index: 2 and the sequence becomes A B D E. From the log output I can see that the VNodes + closures should have been updated correctly. However, clicking e.g. again on the item on index 2 (now the D) produces the log message Deleting element from index: 3, resulting in the wrong element to be deleted. Since the index 3 is D's old index, it looks like the event handler hasn't been updated.

Example from karax readme: Error: cannot open 'vdom'

I wanted to give karax a try on my MacBook Pro with Nim 0.16.0 installed, starting with the very first example on the readme, but in iTerm I got:

$ cd examples/todoapp
$ nim js todoapp.nim
Hint: used config file '/usr/local/Cellar/nim/0.16.0/nim/config/nim.cfg' [Conf]
Hint: system [Processing]
Hint: todoapp [Processing]
todoapp.nim(2, 8) Error: cannot open 'vdom'

Allow to customize the ID of the root tag

Currently the ID is hard-coded to "ROOT", which can clash with existing IDs. Ideally the ID should be configurable.

Also, if the ID is not found, the error message could be more helpful (currently fails with a cryptic "x_84928 is null" error).

echartstest foreign node issue with click routing

type
  clickHandler* = proc():VNode

var view* :clickHandler 

proc click*(x : clickHandler): proc() = 
  result = proc() = view = x

proc echartSection():VNode = 
    buildHtml tdiv(id = "echartSection", style = style((width, kstring"600px"), (height, kstring"400px")))

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:
          if v == Products:
              a(href = "#/" & $v, onclick = click echartSection):
                text kstring($v)
          else:
            a(href = "#/" & $v):
              text kstring($v)
    tdiv:
      text "other section"

setRenderer createDom, "ROOT", postRender
setForeignNodeId "echartSection"

got

Uncaught Error: Initialize failed: invalid dom.
    at Object.init (echarts.js:1778)
    at Object.post_render_256045 [as postRenderCallback] (echartstest.js:3454)
    at dodraw_249631 (echartstest.js:2870)
    at HEX3Aanonymous_251001 (echartstest.js:2913)

regression with lastest Nim

Uncaught TypeError: Cannot set property 'renderId' of null
at init_215046
Uncaught TypeError: Cannot read property 'renderer' of null
at dodraw_213631 (todoapp.js:2780)
at HEX3Aanonymous_215049 (todoapp.js:3252)

Issue with case statement

There is a small issue with case statements in karaxdsl. For example

include karaxprelude

type
  TestEnum {.pure.} = enum
    ValA, ValB

var e = TestEnum.ValA

proc renderDom(): VNode =
  result = buildHtml():
    tdiv():
      case e
      of TestEnum.ValA:
        tdiv: text "A"
      of TestEnum.ValB:
        tdiv: text "B"

does not compile, because the macro generates an add call for the case identifier:

let tmp123076 = tree(VNodeKind.tdiv)
case add(tmp123076, e)
of TestEnum.ValA:
  ...

Will try to push a fix in a moment...

Color helpers

Hi I want to propose following color helpers:

type
  RGB = tuple
    R: int
    G: int
    B: int

proc `&`*(x: RGB): cstring =
  &x.R & cstring"," & &x.G & cstring"," & &x.B

proc rgb*(rgb: RGB): cstring =
  cstring"rgb(" & &rgb & cstring")"

proc rgba*(rgb: RGB, alfa: float): cstring =
  cstring"rgba(" & &rgb & cstring"," & &alfa & cstring")"

Usage:

const WhiteColor* = (R: 255, G: 255, B: 255)

tdiv(style=style(StyleAttr.color, rgba(WhiteColor, 0.5)):
  text "Test"

is there a way to pass argument to dom event callback?

hello

i want to pass fixed arguments to dom event callback.

i hope it would like this

for option in self.product.options:

button(onclick=onDelClick("A3333", ev, node)):

"A3333" is fixed.

do you have any suggestion?

Add support for "refs"

I'm considering to add a "refs" system to Karax at some point, similar to solutions in React/Vue. It's probably best to write down the idea for now, to see if this fits the general design.

The problem arises when an event handler needs to access arbitrary DOM elements, e.g., you click a button and the onclick needs to fetch the position of a bunch of divs. Currently I see two ways to implement this:

  1. Use DOM id tags in combination with getElementById(id) to fetch the elements. Here is an example of this in my transition groups demo. This has a few drawbacks:

    • Unnecessary pollution of DOM id namespace.
    • For a huge DOM, calling getElementById repeatedly is significantly slower than just storing a reference once.
    • List-like elements need to encode their index in the id.
  2. I have been experimenting with adding a "refs" system on user side:

    var vnodeMap = newJDict[cstring, (VNode, VNode)]()
    
    proc registerAs(n: VNode, name: cstring): VNode =
      if name in vnodeMap:
        # store new candidate node
        vnodeMap[name] = (vnodeMap[name][0], n)
      else:
        vnodeMap[name] = (n, n)
      result = n
    
    # Usage in the `buildHtml` vdom generation:
    buildsImportantDiv().registerAs("importantDiv")

    Storing a tuple of VNode is necessary, because not every generated VNode ends up being used. Thus, the tuple stores (old-vnode, candidate-vnode) and on retrieval there is additional logic to move the candidate into the old-vnode position if it has ended up with a non-nil node.dom. Overall, solving the refs problem on client side is a bit complicated and it would be nice to have some solution in Karax.


I haven't really thought it through yet how to implement it in Karax. Some ideas:

A syntactically naive approach would be:

var
  refDivA: Ref = nil # maybe just use VNode?
  refDivB: Ref = nil

proc view(): VNode =
  result = buildHtml():
    tdiv(ref=refDivA): text "A"
    tdiv(ref=refDivB): text "B"

Karax would have update the refs on rendering accordingly. But on first glance this approach becomes weird when dealing with a list of elements/references: The view would have to adjust the length of a seq[Ref] so that for i in 0..<N: tdiv(ref=refs[i]) would not lead to out-of-bound errors when Karax wants to update a reference. So probably that's too error prone and not the kind of responsibility for a view.

An alternative would be to use a callback for ref with signature Ref -> void (or VNode -> void if there is no need for an explicit Ref type). This would be similar to how it is solved in React. The usage with lists could look like:

var
  refs: newJDict[int, Ref]()

proc view(): VNode =
  result = buildHtml():
    tdiv:
      for i in 0..<N:
        tdiv(ref = (r: Ref) => refs[i] = r)

This on the other hand raises the question if the callback should rather have both a register and an unregister call, either by having a second argument in the callback or by having two optional callbacks refAdd and refRemove. This would enable other things that would fall into the "component lifecycle" of React/Vue, but I'm not sure if Karax needs them.

What do you guys think is best?

Is div still a keyword?

The readme refers to div as a keyword in nim. Is this still the case. Wouldn't using div be better?

Graph without explanation

The graph showing performance comparison should really have some kind of explanation with it. Specifically the colours aren't documented anywhere and makes the whole thing rather confusing..

echartstest.nim doesn't work

/cc @Araq as reported on gitter:
nim js echartstest.nim
open echartstest.html

shows

Customers
Products

hyperlinks but nothing happens when I click them (and no errors are shown).

If I edit echartstest.nim with
currentView = Customers
=>
currentView = Products

then a chart is shown (but clicking on the links doesn't work either)

note: there's also another warning, but not error:

There is a chart instance already initialized on the dom
see https://translate.google.fr/translate?hl=en&sl=zh-CN&u=https://zhidao.baidu.com/question/813945019601723452.html&prev=search for details on that

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.