Skip to content

Commit aaa3ff3

Browse files
committed
feat: add crank-todomvc demo & supports sub-hash in mount-node
1 parent de76255 commit aaa3ff3

File tree

4 files changed

+357
-0
lines changed

4 files changed

+357
-0
lines changed

public/code/crank-todomvc.jsx

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
/** @jsx createElement */
2+
// import {createElement, Fragment} from "@bikeshaving/crank";
3+
// import {renderer} from "@bikeshaving/crank/dom";
4+
5+
await loadCss([
6+
'https://unpkg.com/[email protected]/base.css',
7+
'https://unpkg.com/[email protected]/index.css', // too aggressive
8+
])
9+
10+
// await loadJs('https://unpkg.com/@bikeshaving/crank/umd/index.js');
11+
await loadJs('https://unpkg.com/[email protected]/umd/index.js');
12+
const {Fragment, createElement, renderer} = Crank;
13+
14+
appendCss(`
15+
#mountNode { padding: 20px }
16+
body { max-width: none !important; } /* against todomvc-app-css */
17+
`);
18+
19+
/* temp hack for coldemo logic */
20+
// const prefix = '#/playground/crank-todomvc.jsx';
21+
const prefix = window.location.hash.replace(/\/+$/, '');
22+
23+
// https://github.com/bikeshaving/crank/tree/master/examples/todomvc
24+
const ENTER_KEY = 13;
25+
const ESC_KEY = 27;
26+
27+
function* Header() {
28+
let title = "";
29+
this.addEventListener("input", (ev) => {
30+
title = ev.target.value;
31+
});
32+
33+
this.addEventListener("keydown", (ev) => {
34+
if (ev.target.tagName === "INPUT" && ev.keyCode === ENTER_KEY) {
35+
if (title.trim()) {
36+
ev.preventDefault();
37+
const title1 = title.trim();
38+
title = "";
39+
this.dispatchEvent(
40+
new CustomEvent("todo.create", {
41+
bubbles: true,
42+
detail: {title: title1},
43+
}),
44+
);
45+
}
46+
}
47+
});
48+
49+
while (true) {
50+
yield (
51+
<header class="header">
52+
<h1>todos</h1>
53+
<input
54+
class="new-todo"
55+
placeholder="What needs to be done?"
56+
autofocus
57+
value={title}
58+
/>
59+
</header>
60+
);
61+
}
62+
}
63+
64+
function* TodoItem({todo}) {
65+
let active = false;
66+
let title = todo.title;
67+
this.addEventListener("click", (ev) => {
68+
if (ev.target.className === "toggle") {
69+
this.dispatchEvent(
70+
new CustomEvent("todo.toggle", {
71+
bubbles: true,
72+
detail: {id: todo.id, completed: !todo.completed},
73+
}),
74+
);
75+
} else if (ev.target.className === "destroy") {
76+
this.dispatchEvent(
77+
new CustomEvent("todo.destroy", {
78+
bubbles: true,
79+
detail: {id: todo.id},
80+
}),
81+
);
82+
}
83+
});
84+
85+
this.addEventListener("dblclick", (ev) => {
86+
if (ev.target.tagName === "LABEL") {
87+
active = true;
88+
this.refresh();
89+
ev.target.parentElement.nextSibling.focus();
90+
}
91+
});
92+
93+
this.addEventListener("input", (ev) => {
94+
if (ev.target.className === "edit") {
95+
title = ev.target.value;
96+
}
97+
});
98+
99+
this.addEventListener("keydown", (ev) => {
100+
if (
101+
ev.target.className === "edit" &&
102+
(ev.keyCode === ENTER_KEY || ev.keyCode === ESC_KEY)
103+
) {
104+
active = false;
105+
title = title.trim();
106+
if (title) {
107+
this.dispatchEvent(
108+
new CustomEvent("todo.edit", {
109+
bubbles: true,
110+
detail: {id: todo.id, title},
111+
}),
112+
);
113+
} else {
114+
this.dispatchEvent(
115+
new CustomEvent("todo.destroy", {
116+
bubbles: true,
117+
detail: {id: todo.id},
118+
}),
119+
);
120+
}
121+
}
122+
});
123+
124+
this.addEventListener(
125+
"blur",
126+
(ev) => {
127+
if (ev.target.className === "edit") {
128+
active = false;
129+
if (title) {
130+
this.dispatchEvent(
131+
new CustomEvent("todo.edit", {
132+
bubbles: true,
133+
detail: {id: todo.id, title},
134+
}),
135+
);
136+
} else {
137+
this.dispatchEvent(
138+
new CustomEvent("todo.destroy", {
139+
bubbles: true,
140+
detail: {id: todo.id},
141+
}),
142+
);
143+
}
144+
}
145+
},
146+
{capture: true},
147+
);
148+
149+
for ({todo} of this) {
150+
const classes = [];
151+
if (active) {
152+
classes.push("editing");
153+
}
154+
if (todo.completed) {
155+
classes.push("completed");
156+
}
157+
158+
yield (
159+
<li class={classes.join(" ")}>
160+
<div class="view">
161+
<input class="toggle" type="checkbox" checked={todo.completed} />
162+
<label>{todo.title}</label>
163+
<button class="destroy" />
164+
</div>
165+
<input class="edit" value={title} />
166+
</li>
167+
);
168+
}
169+
}
170+
171+
function Main({todos, filter}) {
172+
const completed = todos.every((todo) => todo.completed);
173+
this.addEventListener("click", (ev) => {
174+
if (ev.target.className === "toggle-all") {
175+
this.dispatchEvent(
176+
new CustomEvent("todo.toggleAll", {
177+
bubbles: true,
178+
detail: {completed: !completed},
179+
}),
180+
);
181+
}
182+
});
183+
184+
if (filter === "active") {
185+
todos = todos.filter((todo) => !todo.completed);
186+
} else if (filter === "completed") {
187+
todos = todos.filter((todo) => todo.completed);
188+
}
189+
190+
return (
191+
<section class="main">
192+
<input
193+
id="toggle-all"
194+
class="toggle-all"
195+
type="checkbox"
196+
checked={completed}
197+
/>
198+
<label for="toggle-all">Mark all as complete</label>
199+
<ul class="todo-list">
200+
{todos.map((todo) => (
201+
<TodoItem todo={todo} crank-key={todo.id} />
202+
))}
203+
</ul>
204+
</section>
205+
);
206+
}
207+
208+
function Filters({filter}) {
209+
return (
210+
<ul class="filters">
211+
<li>
212+
<a class={filter === "" ? "selected" : ""} href={`${prefix}/`}>
213+
All
214+
</a>
215+
</li>
216+
<li>
217+
<a class={filter === "active" ? "selected" : ""} href={`${prefix}/active`}>
218+
Active
219+
</a>
220+
</li>
221+
<li>
222+
<a class={filter === "completed" ? "selected" : ""} href={`${prefix}/completed`}>
223+
Completed
224+
</a>
225+
</li>
226+
</ul>
227+
);
228+
}
229+
230+
function Footer({todos, filter}) {
231+
const completed = todos.filter((todo) => todo.completed).length;
232+
const remaining = todos.length - completed;
233+
this.addEventListener("click", (ev) => {
234+
if (ev.target.className === "clear-completed") {
235+
this.dispatchEvent(new Event("todo.clearCompleted", {bubbles: true}));
236+
}
237+
});
238+
239+
return (
240+
<footer class="footer">
241+
<span class="todo-count">
242+
<strong>{remaining}</strong> {remaining === 1 ? "item" : "items"} left
243+
</span>
244+
<Filters filter={filter} />
245+
{!!completed && <button class="clear-completed">Clear completed</button>}
246+
</footer>
247+
);
248+
}
249+
250+
const STORAGE_KEY = "todos-crank";
251+
function save(todos) {
252+
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
253+
}
254+
255+
function* App() {
256+
let todos = [];
257+
let nextTodoId = 0;
258+
try {
259+
const storedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY));
260+
if (Array.isArray(storedTodos) && storedTodos.length) {
261+
todos = storedTodos;
262+
nextTodoId = Math.max(...storedTodos.map((todo) => todo.id)) + 1;
263+
} else {
264+
localStorage.removeItem(STORAGE_KEY);
265+
}
266+
} catch (err) {
267+
localStorage.removeItem(STORAGE_KEY);
268+
}
269+
270+
let filter = "";
271+
this.addEventListener("todo.create", (ev) => {
272+
todos.push({id: nextTodoId++, title: ev.detail.title, completed: false});
273+
this.refresh();
274+
save(todos);
275+
});
276+
277+
this.addEventListener("todo.edit", (ev) => {
278+
const i = todos.findIndex((todo) => todo.id === ev.detail.id);
279+
todos[i].title = ev.detail.title;
280+
this.refresh();
281+
save(todos);
282+
});
283+
284+
this.addEventListener("todo.toggle", (ev) => {
285+
const i = todos.findIndex((todo) => todo.id === ev.detail.id);
286+
todos[i].completed = ev.detail.completed;
287+
this.refresh();
288+
save(todos);
289+
});
290+
291+
this.addEventListener("todo.toggleAll", (ev) => {
292+
todos = todos.map((todo) => ({...todo, completed: ev.detail.completed}));
293+
this.refresh();
294+
save(todos);
295+
});
296+
297+
this.addEventListener("todo.clearCompleted", () => {
298+
todos = todos.filter((todo) => !todo.completed);
299+
this.refresh();
300+
save(todos);
301+
});
302+
303+
this.addEventListener("todo.destroy", (ev) => {
304+
todos = todos.filter((todo) => todo.id !== ev.detail.id);
305+
this.refresh();
306+
save(todos);
307+
});
308+
309+
const route = (ev) => {
310+
switch (window.location.hash) {
311+
case `${prefix}/active`: {
312+
filter = "active";
313+
break;
314+
}
315+
case `${prefix}/completed`: {
316+
filter = "completed";
317+
break;
318+
}
319+
case `${prefix}/`: {
320+
filter = "";
321+
break;
322+
}
323+
default: {
324+
filter = "";
325+
window.location.hash = `${prefix}/`;
326+
}
327+
}
328+
329+
if (ev != null) {
330+
this.refresh();
331+
}
332+
};
333+
334+
route();
335+
window.addEventListener("hashchange", route);
336+
try {
337+
while (true) {
338+
yield (
339+
<Fragment>
340+
<Header />
341+
{!!todos.length && <Main todos={todos} filter={filter} />}
342+
{!!todos.length && <Footer todos={todos} filter={filter} />}
343+
</Fragment>
344+
);
345+
}
346+
} finally {
347+
window.removeEventListener("hashchange", route);
348+
}
349+
}
350+
351+
// renderer.render(<App />, document.getElementsByClassName("todoapp")[0]);
352+
renderer.render(<App />, document.querySelector("#mountNode"));
353+
354+
/* temp hack for coldemo logic */
355+
App = undefined;

public/code/crank-todomvc.jsx.png

65.5 KB
Loading

public/code/index.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
- crank-todomvc.jsx
12
- crank-async-generator.jsx
23
- vue-threejs-movement.js
34
- vue-threejs-orbit-controls.js

src/AppRouter.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function AppRouter() {
1414
<Switch>
1515
<Route path="/" exact component={Gallery} />
1616
<Route path="/playground/:file?" component={Playground} />
17+
<Route path="/playground/:file/**" component={Playground} />
1718
<Route path="*">
1819
<Redirect to="/" />
1920
</Route>

0 commit comments

Comments
 (0)