clean todoapp example

This commit is contained in:
Andreas Rumpf 2017-04-05 00:44:58 +02:00
commit d22a1833cc
11 changed files with 713 additions and 95 deletions

View file

@ -1,8 +1,9 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">

View file

@ -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..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring"")
proc toVis(visible: bool): cstring =
(if visible: cstring"visible" else: "hidden")
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" 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 =
# implement caching:
when defined(usecache):
if entryCache.contains(i):
let old = entryCache[i]
return old
result = buildHtml(tr) do:
td(id="edit:" & $i, onclick=editHandler):
if selected:
editInput(i, d)
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
else:
text d
td:
span(id="remove:" & $i, onclick=removeHandler):
text "[remove]"
input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
when defined(usecache):
entryCache[i] = result
proc createDom(): VNode =
result = buildHtml(tdiv) do:
tdiv(id = "sheader"):
#text "plugin"
#realtimeInput("by-name", "", onInput)
#br()
text cstring"todo"
enterInput("todo-input", "", onTodoEnter)
var entriesCount = 0
table(class = "wl"):
for i, d in entries:
if d != nil:
createEntry(i, d, i == selectedEntry)
inc entriesCount
tdiv(id = "footer"):
text cstring"Entries: " & &entriesCount
proc onAllDone(ev: Event; n: VNode) =
entries = @[]
selectedEntry = -1
button "All done!", onAllDone, entriesCount == 0 or selectedEntry >= 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()

View file

@ -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 #[#]".}

View file

@ -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.}

View file

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

View file

@ -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

View file

@ -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

378
style.css Normal file
View file

@ -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,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
.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;
}
}

19
todoapp.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<title>Todo app</title>
<link rel="stylesheet" href="style.css">
</head>
<body id="body">
<div id="ROOT" />
<script type="text/javascript" src="nimcache/todoapp.js"></script>
<script type="text/javascript">
onload(window.location.pathname.split('/')[1])
</script>
</body>
</html>

121
todoapp.nim Normal file
View file

@ -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..<entries.len:
if entries[i][1]: entries[i][0] = nil
proc toClass(completed: bool): cstring =
(if completed: cstring"completed" else: cstring(nil))
proc toChecked(checked: bool): cstring =
(if checked: cstring"checked" 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 =
result = buildHtml(tr):
li(class=toClass(completed)):
if not selected:
tdiv(class = "view"):
input(class = "toggle", `type` = "checkbox", checked = toChecked(completed),
onclick=toggleEntry, id="toggle:" & &id)
label(onDblClick=editHandler, id="edit:" & &id):
text d
button(class = "destroy", id="remove:" & $id, onclick=removeHandler)
else:
input(class = "edit", name = "title", id = "todo-edit:" & &id,
onfocusLost = focusLost,
onenter = editEntry, value = d)
proc createDom(): VNode =
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..1: # 0_000:
entries.add((cstring"Entry " & &i, false))
init()

View file

@ -9,21 +9,24 @@ type
tdiv,
table, tr, td, th, thead, tbody,
link, span, label, br, select, option,
fieldset, input, button
fieldset, input, button, ul, li, section, header, footer,
h1, h2, h3, h4, h5, strong
const
toTag*: array[VNodeKind, cstring] = [
cstring"#text", "A", "DIV", "TABLE", "TR", "TD", "TH", "THEAD", "TBODY", "LINK",
"SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON"
"SPAN", "LABEL", "BR", "SELECT", "OPTION", "FIELDSET", "INPUT", "BUTTON", "UL",
"LI", "SECTION", "HEADER", "FOOTER", "H1", "H2", "H3", "H4", "H5", "STRONG"
]
type
EventKind* {.pure.} = enum
onclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll
onclick, ondblclick, onkeyup, onkeydown, onkeypressed, onblur, onchange, onscroll
const
toEventName*: array[EventKind, cstring] = [
cstring"click", "keyup", "keydown", "keypressed", "blur", "change", "scroll"
cstring"click", "dblclick", "keyup", "keydown", "keypressed", "blur",
"change", "scroll"
]
type