diff --git a/example.html b/example.html
index 9b2fb48..35b9837 100644
--- a/example.html
+++ b/example.html
@@ -1,8 +1,9 @@
-
+
Todo app
+
diff --git a/example.nim b/example.nim
index c7d922b..0ea805e 100644
--- a/example.nim
+++ b/example.nim
@@ -1,30 +1,22 @@
import vdom, karax, karaxdsl, jdict, jstrutils
-when false:
- var plugins {.exportc.}: seq[(string, proc())] = @[]
-
- proc onInput(val: cstring) =
- kout val
- if val == "dyn":
- let body = getElementById("body")
- body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
- redraw()
- kout(plugins.len)
- if plugins.len > 0:
- plugins[0][1]()
+type
+ Filter = enum
+ all, active, completed
var
- entries: seq[cstring]
+ entries: seq[(cstring, bool)]
selectedEntry = -1
+ filter: Filter
-proc onTodoEnter(val: cstring) =
- entries.add val
+proc onTodoEnter(ev: Event; n: VNode) =
+ entries.add((n.value, false))
+ n.value = ""
proc removeHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "remove:")
- #entries.delete(id)
- entries[id] = nil
+ entries[id] = (cstring(nil), false)
proc editHandler(ev: Event; n: VNode) =
let id = suffixAsInt(n.id, "edit:")
@@ -33,57 +25,111 @@ proc editHandler(ev: Event; n: VNode) =
when defined(usecache):
var entryCache = newJDict[int, VNode]()
-proc editInput(i: int; d: cstring): VNode =
- proc onTodoChange(val: cstring) =
- entries[i] = val
- selectedEntry = -1
- result = enterInput("todo-edit", d, onTodoChange)
- result.setOnfocuslost(proc (ev: Event; n: VNode) = selectedEntry = -1)
+proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
-proc createEntry(i: int; d: cstring; selected: bool): VNode =
+proc editEntry(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "todo-edit:")
+ entries[id][0] = n.value
+ selectedEntry = -1
+
+proc toggleEntry(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "toggle:")
+ entries[id][1] = not entries[id][1]
+
+proc onAllDone(ev: Event; n: VNode) =
+ entries = @[]
+ selectedEntry = -1
+
+proc clearCompleted(ev: Event, n: VNode) =
+ for i in 0..= 0
+ result = buildHtml(tdiv(class="todomvc-wrapper")):
+ section(class = "todoapp"):
+ header(class = "header"):
+ h1:
+ text "todos"
+ input(class = "new-todo", placeholder="What needs to be done?", name = "newTodo",
+ onenter = onTodoEnter)
+ section(class = "main"):
+ input(class = "toggle-all", `type` = "checkbox", name = "toggle")
+ label(`for` = "toggle-all", onclick = onAllDone):
+ text "Mark all as complete"
+ var entriesCount = 0
+ var completedCount = 0
+ ul(class = "todo-list"):
+ for i, d in pairs(entries):
+ if d[0] != nil:
+ let b = case filter
+ of all: true
+ of active: not d[1]
+ of completed: d[1]
+ if b:
+ createEntry(i, d[0], d[1], i == selectedEntry)
+ inc completedCount, ord(d[1])
+ inc entriesCount
+ footer(class = "footer"):
+ span(class = "todo-count"):
+ strong:
+ text(&entriesCount)
+ text cstring" item" & &(if entriesCount != 1: "s left" else: " left")
+ ul(class = "filters"):
+ li:
+ anchor(class = selected(all), href = "#/"):
+ text "All"
+ li:
+ anchor(class = selected(active), href = "#/active"):
+ text "Active"
+ li:
+ anchor(class = selected(completed), href = "#/completed"):
+ text "Completed"
+ button(class = "clear-completed", onclick = clearCompleted):
+ text "Clear completed (" & &completedCount & ")"
+setOnHashChange(proc(hash: cstring) =
+ if hash == cstring"#/": filter = all
+ elif hash == cstring"#/completed": filter = completed
+ elif hash == cstring"#/active": filter = active
+)
setRenderer createDom
proc onload(session: cstring) {.exportc.} =
- for i in 0..10_000:
- entries.add("Entry " & $i)
+ for i in 0..1: # 0_000:
+ entries.add((cstring"Entry " & &i, false))
init()
diff --git a/jdict.nim b/jdict.nim
index 7d27183..0eecb58 100644
--- a/jdict.nim
+++ b/jdict.nim
@@ -9,3 +9,5 @@ proc newJDict*[K, V](): JDict[K, V] {.importcpp: "{@}".}
proc contains*[K, V](d: JDict[K, V], k: K): bool {.importcpp: "#.hasOwnProperty(#)".}
+proc del*[K, V](d: JDict[K, V], k: K) {.importcpp: "delete #[#]".}
+
diff --git a/jstrutils.nim b/jstrutils.nim
index 99dbcbb..b398537 100644
--- a/jstrutils.nim
+++ b/jstrutils.nim
@@ -2,6 +2,8 @@
proc split*(s, sep: cstring): seq[cstring] {.importcpp, nodecl.}
+proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.}
+
proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.}
proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.}
@@ -11,3 +13,6 @@ proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nod
proc `&`*(a, b: cstring): cstring {.importcpp: "(# + #)", nodecl.}
proc toCstr*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
proc `&`*(s: int): cstring {.importcpp: "((#)+'')", nodecl.}
+proc `&`*(s: bool): cstring {.importcpp: "((#)+'')", nodecl.}
+
+proc `&`*(s: cstring): cstring {.importcpp: "(#)", nodecl.}
diff --git a/karax.nim b/karax.nim
index efb9f43..587f2a7 100644
--- a/karax.nim
+++ b/karax.nim
@@ -42,7 +42,8 @@ proc vnodeToDom(n: VNode): Element =
if n.class != nil:
result.class = n.class
for k, v in attrs(n):
- result.setAttribute(k, v)
+ if v != nil:
+ result.setAttribute(k, v)
let myn = n
for e, h in items(n.events):
proc wrapper(): proc (ev: Event) =
@@ -90,9 +91,10 @@ proc replaceById(id: cstring; newTree: Node) =
proc equals(a, b: VNode): bool =
if a.kind != b.kind: return false
if a.id != b.id: return false
- if a.kind == VNodeKind.text:
- if a.text != b.text: return false
+ #if a.kind == VNodeKind.text:
+ if a.text != b.text: return false
if not sameAttrs(a, b): return false
+ if a.class != b.class: return false
# XXX test event listeners here?
# --> maybe give nodes a hash?
when false:
@@ -196,6 +198,9 @@ template onImpl(s) {.dirty.} =
proc setOnclick*(e: VNode; action: EventHandler) =
onImpl EventKind.onclick
+proc setOnDblclick*(e: VNode; action: EventHandler) =
+ onImpl EventKind.onclick
+
proc setOnfocuslost*(e: VNode; action: EventHandler) =
onImpl EventKind.onblur
@@ -205,6 +210,14 @@ proc setOnchanged*(e: VNode; action: EventHandler) =
proc setOnscroll*(e: VNode; action: EventHandler) =
onImpl EventKind.onscroll
+proc setOnHashChange*(action: proc (hashPart: cstring)) =
+ var onhashChange {.importc: "window.onhashchange".}: proc()
+ var hashPart {.importc: "window.location.hash".}: cstring
+ proc wrapper() =
+ action(hashPart)
+ redraw()
+ onhashchange = wrapper
+
var
linkCounter: int
@@ -219,13 +232,14 @@ proc link*(action: EventHandler): VNode =
result.setAttr("href", "#")
result.setOnclick action
-proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
- result = newVNode(VNodeKind.button)
- result.add text(caption)
- if action != nil:
- result.setOnClick action
- if disabled:
- result.setAttr("disabled", "true")
+when false:
+ proc button*(caption: cstring; action: EventHandler; disabled=false): VNode =
+ result = newVNode(VNodeKind.button)
+ result.add text(caption)
+ if action != nil:
+ result.setOnClick action
+ if disabled:
+ result.setAttr("disabled", "true")
proc select*(choices: openarray[cstring]): VNode =
result = newVNode(VNodeKind.select)
@@ -299,29 +313,36 @@ proc tr*(kids: varargs[VNode]): VNode =
proc getAttr(e: Element; key: cstring): cstring {.
importcpp: "#.getAttribute(#)", nodecl.}
-proc realtimeInput*(id, val: cstring; changed: proc(value: cstring)): VNode =
+template nativeValue(ev): cstring = cast[Element](ev.target).value
+template setNativeValue(ev, val) = cast[Element](ev.target).value = val
+
+proc realtimeInput*(val: cstring; changed: EventHandler): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
var timer: Timeout
- proc wrapper() =
- changed(document.getElementById(id).value)
- redraw()
proc onkeyup(ev: Event; n: VNode) =
+ proc wrapper() =
+ n.value = nativeValue(ev)
+ changed(ev, n)
+ setNativeValue(ev, n.value)
+ redraw()
+
if timer != nil: clearTimeout(timer)
timer = setTimeout(wrapper, 400)
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
- result.id = id
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
-proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): VNode =
+proc enterInput*(id, val: cstring; onenter: EventHandler): VNode =
#let oldElem = getElementById(id)
#if oldElem != nil: return oldElem
#let newVal = if oldElem.isNil: val else: $oldElem.value
proc onkeyup(ev: Event; n: VNode) =
if ev.keyCode == 13:
- onenter(document.getElementById(id).value)
+ n.value = nativeValue(ev)
+ onenter(ev, n)
+ setNativeValue(ev, n.value)
redraw()
result = tree(VNodeKind.input, [(cstring"type", cstring"text")])
@@ -329,6 +350,14 @@ proc enterInput*(id, val: cstring; onenter: proc(value: cstring)): VNode =
result.value = val
result.addEventListener(EventKind.onkeyup, onkeyup)
+proc setOnEnter*(n: VNode; onenter: EventHandler) =
+ proc onkeyup(ev: Event; n: VNode) =
+ if ev.keyCode == 13:
+ n.value = nativeValue(ev)
+ onenter(ev, n)
+ setNativeValue(ev, n.value)
+ redraw()
+ n.addEventListener(EventKind.onkeyup, onkeyup)
proc ajax(meth, url: cstring; headers: openarray[(cstring, cstring)];
data: cstring;
@@ -366,7 +395,7 @@ proc ajaxGet*(url: string; headers: openarray[(cstring, cstring)];
proc setupErrorHandler*(useAlert=false) =
## Installs an error handler that transforms native JS unhandled
## exceptions into Nim based stack traces. If `useAlert` is false,
- ## the error message it put into the console, otherwise `alert`
+ ## the error message is put into the console, otherwise `alert`
## is called.
proc stackTraceAsCstring(): cstring = cstring(getStackTrace())
{.emit: """
@@ -381,3 +410,17 @@ proc setupErrorHandler*(useAlert=false) =
};""".}
{.pop.}
+
+when false:
+ var plugins {.exportc.}: seq[(string, proc())] = @[]
+
+ proc onInput(val: cstring) =
+ kout val
+ if val == "dyn":
+ let body = getElementById("body")
+ body.prepend(tree("script", [("type", "text/javascript"), ("src", "nimcache/dyn.js")]))
+ redraw()
+ kout(plugins.len)
+ if plugins.len > 0:
+ plugins[0][1]()
+
diff --git a/karaxdsl.nim b/karaxdsl.nim
index 5afe3a2..e90a9b3 100644
--- a/karaxdsl.nim
+++ b/karaxdsl.nim
@@ -1,12 +1,8 @@
import macros, karax, vdom
-from strutils import startsWith
+from strutils import startsWith, toLowerAscii
const
- SupportedTags* = ["tdiv", "div",
- "table", "tr", "td", "th", "thead", "tbody",
- "link", "span", "label", "br"
- ]
StmtContext = ["kout", "inc", "echo", "dec"]
proc getName(n: NimNode): string =
@@ -23,11 +19,13 @@ proc getName(n: NimNode): string =
#echo repr n
expectKind(n, nnkIdent)
-var gid {.compileTime.}: int
-
proc newDotAsgn(tmp: NimNode, key: string, x: NimNode): NimNode =
result = newTree(nnkAsgn, newDotExpr(tmp, newIdentNode key), x)
+proc isTag(s: string): bool =
+ for i in VNodeKind.low.succ..VNodeKind.high:
+ if $i == s: return true
+
proc tcall2(n, tmpContext: NimNode): NimNode =
# we need to distinguish statement and expression contexts:
# every call statement 's' needs to be transformed to 'dest.add s'.
@@ -62,11 +60,8 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
result = n
of nnkCallKinds:
let op = getName(n[0])
- let idx = find(SupportedTags, op)
- if idx >= 0:
- let tag = SupportedTags[idx]
- inc gid
- let tmp = genSym(nskLet, "tmp" & $gid)
+ if isTag(op):
+ let tmp = genSym(nskLet, "tmp")
result = newTree(
if tmpContext == nil: nnkStmtListExpr else: nnkStmtList,
newLetStmt(tmp, newCall(bindSym"tree", newDotExpr(bindSym"VNodeKind", n[0]))))
@@ -77,14 +72,14 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
let key = getName x[0]
if key.startsWith("on"):
result.add newCall(!("set" & key), tmp, x[1])
- elif key == "id" or key == "class":
+ elif key == "id" or key == "class" or key == "value":
result.add newDotAsgn(tmp, key, x[1])
else:
result.add newCall(bindSym"setAttr", tmp, newLit(key), x[1])
else:
result.add tcall2(x, tmp)
- if tag == "link":
- result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#")
+ #if tag == "link":
+ # result.add newCall(bindSym"setAttr", tmp, newLit"href", newLit"#")
if tmpContext == nil:
result.add tmp
else:
@@ -98,7 +93,12 @@ proc tcall2(n, tmpContext: NimNode): NimNode =
macro buildHtml*(tag, children: untyped): VNode =
expectKind children, nnkDo
- let call = newCall(tag, body(children))
+ var call: NimNode
+ if tag.kind in nnkCallKinds:
+ call = tag
+ else:
+ call = newCall(tag)
+ call.add body(children)
result = tcall2(call, nil)
when defined(debugKaraxDsl):
echo repr result
diff --git a/readme.rst b/readme.rst
index a126f09..a5fd38a 100644
--- a/readme.rst
+++ b/readme.rst
@@ -5,5 +5,5 @@ Karax is a prototype for developing single page applications in Nim.
To try it out, run::
- nim js example.nim
- open example.html
+ nim js todoapp.nim
+ open todoapp.html
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..ceaf090
--- /dev/null
+++ b/style.css
@@ -0,0 +1,378 @@
+html,
+body {
+ margin: 0;
+ padding: 0;
+}
+
+.todomvc-wrapper {
+ visibility: visible !important;
+}
+
+button {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ background: none;
+ font-size: 100%;
+ vertical-align: baseline;
+ font-family: inherit;
+ font-weight: inherit;
+ color: inherit;
+ -webkit-appearance: none;
+ appearance: none;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+body {
+ font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ line-height: 1.4em;
+ background: #f5f5f5;
+ color: #4d4d4d;
+ min-width: 230px;
+ max-width: 550px;
+ margin: 0 auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ font-weight: 300;
+}
+
+button,
+input[type="checkbox"] {
+ outline: none;
+}
+
+.hidden {
+ display: none;
+}
+
+.todoapp {
+ background: #fff;
+ margin: 130px 0 40px 0;
+ position: relative;
+ box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
+ 0 25px 50px 0 rgba(0, 0, 0, 0.1);
+}
+
+.todoapp input::-webkit-input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::-moz-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp input::input-placeholder {
+ font-style: italic;
+ font-weight: 300;
+ color: #e6e6e6;
+}
+
+.todoapp h1 {
+ position: absolute;
+ top: -155px;
+ width: 100%;
+ font-size: 100px;
+ font-weight: 100;
+ text-align: center;
+ color: rgba(175, 47, 47, 0.15);
+ -webkit-text-rendering: optimizeLegibility;
+ -moz-text-rendering: optimizeLegibility;
+ text-rendering: optimizeLegibility;
+}
+
+.new-todo,
+.edit {
+ position: relative;
+ margin: 0;
+ width: 100%;
+ font-size: 24px;
+ font-family: inherit;
+ font-weight: inherit;
+ line-height: 1.4em;
+ border: 0;
+ outline: none;
+ color: inherit;
+ padding: 6px;
+ border: 1px solid #999;
+ box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
+ box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+}
+
+.new-todo {
+ padding: 16px 16px 16px 60px;
+ border: none;
+ background: rgba(0, 0, 0, 0.003);
+ box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
+}
+
+.main {
+ position: relative;
+ z-index: 2;
+ border-top: 1px solid #e6e6e6;
+}
+
+label[for='toggle-all'] {
+ display: none;
+}
+
+.toggle-all {
+ position: absolute;
+ top: -55px;
+ left: -12px;
+ width: 60px;
+ height: 34px;
+ text-align: center;
+ border: none; /* Mobile Safari */
+}
+
+.toggle-all:before {
+ content: '>';
+ font-size: 22px;
+ color: #e6e6e6;
+ padding: 10px 27px 10px 27px;
+}
+
+.toggle-all:checked:before {
+ color: #737373;
+}
+
+.todo-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.todo-list li {
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #ededed;
+}
+
+.todo-list li:last-child {
+ border-bottom: none;
+}
+
+.todo-list li.editing {
+ border-bottom: none;
+ padding: 0;
+}
+
+.todo-list li.editing .edit {
+ display: block;
+ width: 506px;
+ padding: 13px 17px 12px 17px;
+ margin: 0 0 0 43px;
+}
+
+.todo-list li.editing .view {
+ display: none;
+}
+
+.todo-list li .toggle {
+ text-align: center;
+ width: 40px;
+ /* auto, since non-WebKit browsers doesn't support input styling */
+ height: auto;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ margin: auto 0;
+ border: none; /* Mobile Safari */
+ -webkit-appearance: none;
+ appearance: none;
+}
+
+.todo-list li .toggle:after {
+ content: url('data:image/svg+xml,');
+}
+
+.todo-list li .toggle:checked:after {
+ content: url('data:image/svg+xml,');
+}
+
+.todo-list li label {
+ white-space: pre-line;
+ word-break: break-all;
+ padding: 15px 60px 15px 15px;
+ margin-left: 45px;
+ display: block;
+ line-height: 1.2;
+ transition: color 0.4s;
+}
+
+.todo-list li.completed label {
+ color: #d9d9d9;
+ text-decoration: line-through;
+}
+
+.todo-list li .destroy {
+ display: none;
+ position: absolute;
+ top: 0;
+ right: 10px;
+ bottom: 0;
+ width: 40px;
+ height: 40px;
+ margin: auto 0;
+ font-size: 30px;
+ color: #cc9a9a;
+ margin-bottom: 11px;
+ transition: color 0.2s ease-out;
+}
+
+.todo-list li .destroy:hover {
+ color: #af5b5e;
+}
+
+.todo-list li .destroy:after {
+ content: 'x';
+}
+
+.todo-list li:hover .destroy {
+ display: block;
+}
+
+.todo-list li.editing:last-child {
+ margin-bottom: -1px;
+}
+
+.footer {
+ color: #777;
+ padding: 10px 15px;
+ height: 20px;
+ text-align: center;
+ border-top: 1px solid #e6e6e6;
+}
+
+.footer:before {
+ content: '';
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 50px;
+ overflow: hidden;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
+ 0 8px 0 -3px #f6f6f6,
+ 0 9px 1px -3px rgba(0, 0, 0, 0.2),
+ 0 16px 0 -6px #f6f6f6,
+ 0 17px 2px -6px rgba(0, 0, 0, 0.2);
+}
+
+.todo-count {
+ float: left;
+ text-align: left;
+}
+
+.todo-count strong {
+ font-weight: 300;
+}
+
+.filters {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+ position: absolute;
+ right: 0;
+ left: 0;
+}
+
+.filters li {
+ display: inline;
+}
+
+.filters li a {
+ color: inherit;
+ margin: 3px;
+ padding: 3px 7px;
+ text-decoration: none;
+ border: 1px solid transparent;
+ border-radius: 3px;
+}
+
+.filters li a.selected,
+.filters li a:hover {
+ border-color: rgba(175, 47, 47, 0.1);
+}
+
+.filters li a.selected {
+ border-color: rgba(175, 47, 47, 0.2);
+}
+
+.clear-completed,
+html .clear-completed:active {
+ float: right;
+ position: relative;
+ line-height: 20px;
+ text-decoration: none;
+ cursor: pointer;
+ position: relative;
+}
+
+.clear-completed:hover {
+ text-decoration: underline;
+}
+
+.info {
+ margin: 65px auto 0;
+ color: #bfbfbf;
+ /* font-size: 10px; */
+ text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+ text-align: center;
+}
+
+.info p {
+ line-height: 1;
+}
+
+.info a {
+ color: inherit;
+ text-decoration: none;
+ font-weight: 400;
+}
+
+.info a:hover {
+ text-decoration: underline;
+}
+
+/*
+ Hack to remove background from Mobile Safari.
+ Can't use it globally since it destroys checkboxes in Firefox
+*/
+@media screen and (-webkit-min-device-pixel-ratio:0) {
+ .toggle-all,
+ .todo-list li .toggle {
+ background: none;
+ }
+
+ .todo-list li .toggle {
+ height: 40px;
+ }
+
+ .toggle-all {
+ -webkit-transform: rotate(90deg);
+ transform: rotate(90deg);
+ -webkit-appearance: none;
+ appearance: none;
+ }
+}
+
+@media (max-width: 430px) {
+ .footer {
+ height: 50px;
+ }
+
+ .filters {
+ bottom: 10px;
+ }
+}
\ No newline at end of file
diff --git a/todoapp.html b/todoapp.html
new file mode 100644
index 0000000..8470a8e
--- /dev/null
+++ b/todoapp.html
@@ -0,0 +1,19 @@
+
+
+
+
+ Todo app
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/todoapp.nim b/todoapp.nim
new file mode 100644
index 0000000..40bc4bb
--- /dev/null
+++ b/todoapp.nim
@@ -0,0 +1,121 @@
+
+import vdom, karax, karaxdsl, jdict, jstrutils
+
+type
+ Filter = enum
+ all, active, completed
+
+var
+ entries: seq[(cstring, bool)]
+ selectedEntry = -1
+ filter: Filter
+
+proc onTodoEnter(ev: Event; n: VNode) =
+ entries.add((n.value, false))
+ n.value = ""
+
+proc removeHandler(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "remove:")
+ entries[id] = (cstring(nil), false)
+
+proc editHandler(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "edit:")
+ selectedEntry = id
+
+proc focusLost(ev: Event; n: VNode) = selectedEntry = -1
+
+proc editEntry(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "todo-edit:")
+ entries[id][0] = n.value
+ selectedEntry = -1
+
+proc toggleEntry(ev: Event; n: VNode) =
+ let id = suffixAsInt(n.id, "toggle:")
+ entries[id][1] = not entries[id][1]
+
+proc onAllDone(ev: Event; n: VNode) =
+ entries = @[]
+ selectedEntry = -1
+
+proc clearCompleted(ev: Event, n: VNode) =
+ for i in 0..