Skip to content

Commit 808d714

Browse files
committed
feat: add persistent todo example
1 parent e4d52e9 commit 808d714

6 files changed

Lines changed: 338 additions & 0 deletions

File tree

examples/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ <h1>MoonBit</h1>
143143
<li><span class="name">Streams</span><span class="targets"><a class="t-js" href="streams/streams.js.html">js</a><a class="t-wasm" href="streams/streams.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/streams" target="_blank">src</a></span></li>
144144
<li><span class="name">SVG</span><span class="targets"><a class="t-js" href="svg/svg.js.html">js</a><a class="t-wasm" href="svg/svg.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/svg" target="_blank">src</a></span></li>
145145
<li><span class="name">Timers</span><span class="targets"><a class="t-js" href="timers/timers.js.html">js</a><a class="t-wasm" href="timers/timers.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/timers" target="_blank">src</a></span></li>
146+
<li><span class="name">Todo</span><span class="targets"><a class="t-js" href="todo/todo.js.html">js</a><a class="t-wasm" href="todo/todo.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/todo" target="_blank">src</a></span></li>
146147
<li><span class="name">Touch Events</span><span class="targets"><a class="t-js" href="touch-events/touch-events.js.html">js</a><a class="t-wasm" href="touch-events/touch-events.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/touch-events" target="_blank">src</a></span></li>
147148
<li><span class="name">Trusted Types</span><span class="targets"><a class="t-js" href="trusted-types/trusted-types.js.html">js</a><a class="t-wasm" href="trusted-types/trusted-types.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/trusted-types" target="_blank">src</a></span></li>
148149
<li><span class="name">URL</span><span class="targets"><a class="t-js" href="url/url.js.html">js</a><a class="t-wasm" href="url/url.wasm.html">wasm</a><a class="t-src" href="https://github.com/bikallem/webapi/tree/main/examples/url" target="_blank">src</a></span></li>

examples/todo/main.mbt

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
///|
2+
/// Todo app example built with generated WebAPI bindings.
3+
using @webapi {
4+
document,
5+
type HTMLDivElement,
6+
type HTMLInputElement,
7+
type HTMLButtonElement,
8+
type HTMLUListElement,
9+
type HTMLLIElement,
10+
type EventListener,
11+
}
12+
13+
///|
14+
fn main {
15+
let app = document.get_element_by_id("app").unwrap()
16+
let storage = @webapi.window.local_storage()
17+
let storage_key = "webapi.todo.example.items.v1"
18+
let entry_sep = "\u{1e}"
19+
let field_sep = "\u{1f}"
20+
21+
let mut next_id = 0
22+
let todos : Array[(Int, Bool, String)] = []
23+
24+
let container : HTMLDivElement = document.create_element("div").into()
25+
container.set_attribute(
26+
"style", "max-width: 640px; margin: 2em auto; padding: 1em; font-family: system-ui;",
27+
)
28+
29+
let heading = document.create_element("h1")
30+
heading.set_text_content("Todo App")
31+
32+
let description = document.create_element("p")
33+
description
34+
..set_attribute("style", "margin-top: 0.25em; color: #475467;")
35+
.set_text_content("Add tasks, mark them done, and remove them.")
36+
37+
let controls : HTMLDivElement = document.create_element("div").into()
38+
controls.set_attribute("style", "display: flex; gap: 0.5em; margin: 1em 0;")
39+
40+
let todo_input : HTMLInputElement = document.create_element("input").into()
41+
todo_input
42+
..set_type_("text")
43+
..set_attribute("placeholder", "What needs to be done?")
44+
.set_attribute("style", "flex: 1; padding: 0.6em 0.75em; font-size: 1em;")
45+
46+
let add_btn : HTMLButtonElement = document.create_element("button").into()
47+
add_btn
48+
..set_type_("button")
49+
..set_text_content("Add")
50+
.set_attribute("style", "padding: 0.6em 1em; font-size: 1em;")
51+
52+
let clear_btn : HTMLButtonElement = document.create_element("button").into()
53+
clear_btn
54+
..set_type_("button")
55+
..set_text_content("Clear All")
56+
.set_attribute("style", "padding: 0.6em 1em; font-size: 1em;")
57+
58+
controls.append_child(todo_input) |> ignore
59+
controls.append_child(add_btn) |> ignore
60+
controls.append_child(clear_btn) |> ignore
61+
62+
let status : HTMLDivElement = document.create_element("div").into()
63+
status
64+
..set_attribute("id", "todo-status")
65+
.set_attribute(
66+
"style", "margin: 0.5em 0; color: #475467; font-size: 0.95em; font-weight: 600;",
67+
)
68+
69+
let empty_state : HTMLDivElement = document.create_element("div").into()
70+
empty_state
71+
..set_attribute("id", "todo-empty")
72+
.set_attribute(
73+
"style", "margin: 0.5em 0 0.25em 0; color: #667085; font-size: 0.95em;",
74+
)
75+
empty_state.set_text_content("No tasks yet. Add one above.")
76+
77+
let list : HTMLUListElement = document.create_element("ul").into()
78+
list
79+
..set_attribute("id", "todo-list")
80+
.set_attribute(
81+
"style", "list-style: none; padding: 0; margin: 0.75em 0 0 0; border-top: 1px solid #e4e7ec;",
82+
)
83+
84+
let save_todos = fn() {
85+
let encoded = todos.map(fn(todo) {
86+
let (_, done, text) = todo
87+
let done_text = if done { "1" } else { "0" }
88+
done_text + field_sep + text
89+
})
90+
storage.set_item(storage_key, encoded.join(entry_sep))
91+
}
92+
93+
let update_status = fn() {
94+
let total = todos.length()
95+
let done = todos.fold(init=0, fn(acc, todo) {
96+
let (_, is_done, _) = todo
97+
if is_done {
98+
acc + 1
99+
} else {
100+
acc
101+
}
102+
})
103+
status.set_text_content(
104+
"Total: " + total.to_string() + " | Completed: " + done.to_string(),
105+
)
106+
if total == 0 {
107+
empty_state.set_attribute(
108+
"style", "margin: 0.5em 0 0.25em 0; color: #667085; font-size: 0.95em;",
109+
)
110+
} else {
111+
empty_state.set_attribute(
112+
"style", "display: none; margin: 0.5em 0 0.25em 0; color: #667085; font-size: 0.95em;",
113+
)
114+
}
115+
}
116+
117+
let append_todo_to_dom = fn(todo_id : Int, done : Bool, text : String) {
118+
let item : HTMLLIElement = document.create_element("li").into()
119+
item.set_attribute(
120+
"style", "display: flex; align-items: center; justify-content: space-between; gap: 0.5em; padding: 0.5em 0.25em; border-bottom: 1px solid #e4e7ec;",
121+
)
122+
123+
let left : HTMLDivElement = document.create_element("div").into()
124+
left.set_attribute(
125+
"style", "display: flex; align-items: center; gap: 0.6em; flex: 1; min-width: 0;",
126+
)
127+
128+
let checkbox : HTMLInputElement = document.create_element("input").into()
129+
checkbox.set_type_("checkbox")
130+
131+
let label = document.create_element("span")
132+
label
133+
..set_text_content(text)
134+
.set_attribute("style", "word-break: break-word;")
135+
136+
let apply_done_style = fn(is_done : Bool) {
137+
if is_done {
138+
item.class_list().add(["done"])
139+
label.set_attribute(
140+
"style", "word-break: break-word; color: #667085; text-decoration: line-through;",
141+
)
142+
} else {
143+
item.class_list().remove(["done"])
144+
label.set_attribute("style", "word-break: break-word;")
145+
}
146+
}
147+
148+
checkbox.set_checked(done)
149+
apply_done_style(done)
150+
151+
let delete_btn : HTMLButtonElement = document
152+
.create_element("button")
153+
.into()
154+
delete_btn
155+
..set_type_("button")
156+
..set_text_content("Delete")
157+
.set_attribute("style", "padding: 0.35em 0.6em; font-size: 0.9em;")
158+
159+
checkbox.add_event_listener(
160+
"click",
161+
EventListener::new(fn(_event) {
162+
let checked = checkbox.checked()
163+
match
164+
todos.search_by(fn(todo) {
165+
let (id, _, _) = todo
166+
id == todo_id
167+
}) {
168+
Some(pos) => {
169+
let (_, _, current_text) = todos[pos]
170+
todos[pos] = (todo_id, checked, current_text)
171+
apply_done_style(checked)
172+
save_todos()
173+
update_status()
174+
}
175+
None => ()
176+
}
177+
}),
178+
)
179+
180+
delete_btn.add_event_listener(
181+
"click",
182+
EventListener::new(fn(_event) {
183+
match
184+
todos.search_by(fn(todo) {
185+
let (id, _, _) = todo
186+
id == todo_id
187+
}) {
188+
Some(pos) => {
189+
todos.remove(pos) |> ignore
190+
list.remove_child(item) |> ignore
191+
save_todos()
192+
update_status()
193+
}
194+
None => ()
195+
}
196+
}),
197+
)
198+
199+
left.append_child(checkbox) |> ignore
200+
left.append_child(label) |> ignore
201+
item.append_child(left) |> ignore
202+
item.append_child(delete_btn) |> ignore
203+
list.append_child(item) |> ignore
204+
}
205+
206+
let load_todos = fn() {
207+
todos.clear()
208+
list.set_text_content("")
209+
next_id = 0
210+
211+
match storage.get_item(storage_key) {
212+
Some(raw) =>
213+
if raw != "" {
214+
for encoded in raw.split(entry_sep) {
215+
if !encoded.is_empty() {
216+
match encoded.find(field_sep) {
217+
Some(split_idx) => {
218+
let done_flag = encoded.view(end_offset=split_idx).to_string()
219+
let text = encoded
220+
.view(start_offset=split_idx + 1)
221+
.to_string()
222+
if text != "" {
223+
let done = done_flag == "1"
224+
let todo_id = next_id
225+
next_id = next_id + 1
226+
todos.push((todo_id, done, text))
227+
append_todo_to_dom(todo_id, done, text)
228+
}
229+
}
230+
None => ()
231+
}
232+
}
233+
}
234+
}
235+
None => ()
236+
}
237+
238+
update_status()
239+
}
240+
241+
add_btn.add_event_listener(
242+
"click",
243+
EventListener::new(fn(_event) {
244+
let text = todo_input.value().trim().to_string()
245+
if text != "" {
246+
let todo_id = next_id
247+
next_id = next_id + 1
248+
todos.push((todo_id, false, text))
249+
append_todo_to_dom(todo_id, false, text)
250+
todo_input.set_value("")
251+
save_todos()
252+
update_status()
253+
}
254+
}),
255+
)
256+
257+
clear_btn.add_event_listener(
258+
"click",
259+
EventListener::new(fn(_event) {
260+
todos.clear()
261+
list.set_text_content("")
262+
save_todos()
263+
update_status()
264+
}),
265+
)
266+
267+
let children : Array[&@webapi.TNode] = [
268+
heading, description, controls, status, empty_state, list,
269+
]
270+
for child in children {
271+
container.append_child(child) |> ignore
272+
}
273+
app.append_child(container) |> ignore
274+
275+
load_todos()
276+
}

examples/todo/moon.pkg

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {
2+
"bikallem/webapi",
3+
}
4+
5+
options(
6+
"is-main": true,
7+
link: {
8+
"wasm-gc": {
9+
"imported-string-constants": "_",
10+
"use-js-builtin-string": true,
11+
},
12+
},
13+
"supported-targets": [ "js", "wasm-gc" ],
14+
)

examples/todo/pkg.generated.mbti

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "bikallem/webapi-examples/todo"
3+
4+
// Values
5+
6+
// Errors
7+
8+
// Types and methods
9+
10+
// Type aliases
11+
12+
// Traits
13+

examples/todo/todo.js.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<link rel="shortcut icon" href="data:," />
6+
<link rel="stylesheet" href="../styles.css">
7+
<title>Todo App</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module" src="../_build/js/release/build/todo/todo.js"></script>
12+
</body>
13+
</html>

examples/todo/todo.wasm.html

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<link rel="shortcut icon" href="data:," />
6+
<link rel="stylesheet" href="../styles.css">
7+
<title>Todo App (wasm-gc)</title>
8+
</head>
9+
<body>
10+
<div id="app"></div>
11+
<script type="module">
12+
import { wasmImportObject } from "../../src/webapi.mjs";
13+
const { instance } = await WebAssembly.instantiateStreaming(
14+
fetch("../_build/wasm-gc/release/build/todo/todo.wasm"),
15+
wasmImportObject,
16+
{ builtins: ["js-string"], importedStringConstants: "_" }
17+
);
18+
instance.exports._start();
19+
</script>
20+
</body>
21+
</html>

0 commit comments

Comments
 (0)