Created
April 27, 2020 19:48
-
-
Save geophree/a317c658fd6d7bc51e1e80aa5243be7d to your computer and use it in GitHub Desktop.
Crank.js TodoMVC alternative events/handlers implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// based on https://codesandbox.io/s/crank-todomvc-k6s0x | |
// try it at https://codesandbox.io/s/crank-todomvc-alternative-jluse | |
/** @jsx createElement */ | |
// @bikeshaving/crank 0.1.1 | |
// todomvc-app-css 2.2.0 | |
// todomvc-common 1.0.5 | |
import { createElement, Fragment } from "@bikeshaving/crank"; | |
import { renderer } from "@bikeshaving/crank/dom"; | |
const ENTER_KEY = 13; | |
const ESC_KEY = 27; | |
const createAction = (type, dataFunc = () => {}) => { | |
const creatorFunction = (...args) => | |
new CustomEvent(type, { | |
bubbles: true, | |
detail: { data: dataFunc(...args), creatorFunction } | |
}); | |
creatorFunction.type = type; | |
return creatorFunction; | |
}; | |
const Todo = { | |
create: createAction("todo.create", title => ({ title })), | |
edit: createAction("todo.edit", (id, title) => ({ id, title })), | |
destroy: createAction("todo.destroy", id => ({ id })), | |
toggle: createAction("todo.toggle", (id, completed) => ({ id, completed })), | |
toggleAll: createAction("todo.toggleAll", completed => ({ completed })), | |
clearCompleted: createAction("todo.clearCompleted") | |
}; | |
function* Header() { | |
let title = ""; | |
const setTitle = ev => (title = ev.target.value); | |
const createOnEnter = ev => { | |
if (ev.keyCode !== ENTER_KEY) { | |
return; | |
} | |
const todoTitle = title.trim(); | |
if (!todoTitle) { | |
return; | |
} | |
ev.preventDefault(); | |
title = ""; | |
this.dispatchEvent(Todo.create(todoTitle)); | |
}; | |
while (true) { | |
yield ( | |
<header class="header"> | |
<h1>todos</h1> | |
<input | |
class="new-todo" | |
placeholder="What needs to be done?" | |
autofocus | |
oninput={setTitle} | |
onkeydown={createOnEnter} | |
value={title} | |
/> | |
</header> | |
); | |
} | |
} | |
function* TodoItem({ todo }) { | |
let active = false; | |
let title = todo.title; | |
const destroy = () => this.dispatchEvent(Todo.destroy(todo.id)); | |
const toggle = () => | |
this.dispatchEvent(Todo.toggle(todo.id, !todo.completed)); | |
const editMode = ev => { | |
active = true; | |
this.refresh(); | |
ev.target.parentElement.nextSibling.focus(); | |
}; | |
const setTitle = ev => (title = ev.target.value); | |
const editOrDestroy = () => { | |
active = false; | |
title = title.trim(); | |
if (title) { | |
this.dispatchEvent(Todo.edit(todo.id, title)); | |
} else { | |
this.dispatchEvent(Todo.destroy(todo.id)); | |
} | |
}; | |
const keydownMaybeBlur = ev => { | |
if (ev.keyCode === ENTER_KEY || ev.keyCode === ESC_KEY) { | |
ev.target.blur(); | |
} | |
}; | |
for ({ todo } of this) { | |
const classes = []; | |
if (active) { | |
classes.push("editing"); | |
} | |
if (todo.completed) { | |
classes.push("completed"); | |
} | |
yield ( | |
<li class={classes.join(" ")}> | |
<div class="view"> | |
<input | |
class="toggle" | |
type="checkbox" | |
checked={todo.completed} | |
onclick={toggle} | |
/> | |
<label ondblclick={editMode}>{todo.title}</label> | |
<button class="destroy" onclick={destroy} /> | |
</div> | |
<input | |
class="edit" | |
value={title} | |
oninput={setTitle} | |
onblur={editOrDestroy} | |
onkeydown={keydownMaybeBlur} | |
/> | |
</li> | |
); | |
} | |
} | |
function Main({ todos, filter }) { | |
const completed = todos.every(todo => todo.completed); | |
const toggleAll = () => this.dispatchEvent(Todo.toggleAll(!completed)); | |
if (filter === "active") { | |
todos = todos.filter(todo => !todo.completed); | |
} else if (filter === "completed") { | |
todos = todos.filter(todo => todo.completed); | |
} | |
return ( | |
<section class="main"> | |
<input | |
id="toggle-all" | |
class="toggle-all" | |
type="checkbox" | |
checked={completed} | |
onclick={toggleAll} | |
/> | |
<label for="toggle-all">Mark all as complete</label> | |
<ul class="todo-list"> | |
{todos.map(todo => ( | |
<TodoItem todo={todo} crank-key={todo.id} /> | |
))} | |
</ul> | |
</section> | |
); | |
} | |
function Filters({ filter }) { | |
return ( | |
<ul class="filters"> | |
<li> | |
<a class={filter === "" ? "selected" : ""} href="#/"> | |
All | |
</a> | |
</li> | |
<li> | |
<a class={filter === "active" ? "selected" : ""} href="#/active"> | |
Active | |
</a> | |
</li> | |
<li> | |
<a class={filter === "completed" ? "selected" : ""} href="#/completed"> | |
Completed | |
</a> | |
</li> | |
</ul> | |
); | |
} | |
function Footer({ todos, filter }) { | |
const completed = todos.filter(todo => todo.completed).length; | |
const remaining = todos.length - completed; | |
const clearCompleted = () => this.dispatchEvent(Todo.clearCompleted()); | |
return ( | |
<footer class="footer"> | |
<span class="todo-count"> | |
<strong>{remaining}</strong> {remaining === 1 ? "item" : "items"} left | |
</span> | |
<Filters filter={filter} /> | |
{!!completed && ( | |
<button class="clear-completed" onclick={clearCompleted}> | |
Clear completed | |
</button> | |
)} | |
</footer> | |
); | |
} | |
const STORAGE_KEY = "todos-crank"; | |
function save(todos) { | |
try { | |
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); | |
} catch { | |
// ignore | |
} | |
} | |
function load() { | |
try { | |
try { | |
const storedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY)); | |
if (Array.isArray(storedTodos) && storedTodos.length) { | |
return storedTodos; | |
} | |
} catch { | |
// fall through | |
} | |
localStorage.removeItem(STORAGE_KEY); | |
} catch { | |
// ignore | |
} | |
return []; | |
} | |
function* App() { | |
let todos = load(); | |
let nextTodoId = todos.reduce((max, todo) => Math.max(max, todo.id + 1), 0); | |
let filter = ""; | |
const findTodo = id => todos.find(todo => todo.id === id); | |
const handleEvent = ev => { | |
if (!ev.detail) { | |
return; | |
} | |
const data = ev.detail.data; | |
switch (ev.detail.creatorFunction) { | |
case Todo.create: { | |
todos.push({ id: nextTodoId++, title: data.title, completed: false }); | |
break; | |
} | |
case Todo.edit: { | |
const todo = findTodo(data.id); | |
if (!todo) { | |
return; | |
} | |
todo.title = data.title; | |
break; | |
} | |
case Todo.destroy: { | |
todos = todos.filter(todo => todo.id !== data.id); | |
break; | |
} | |
case Todo.toggle: { | |
const todo = findTodo(data.id); | |
if (!todo) { | |
return; | |
} | |
todo.completed = data.completed; | |
break; | |
} | |
case Todo.toggleAll: { | |
todos = todos.map(todo => ({ ...todo, completed: data.completed })); | |
break; | |
} | |
case Todo.clearCompleted: { | |
todos = todos.filter(todo => !todo.completed); | |
break; | |
} | |
default: | |
return; | |
} | |
this.refresh(); | |
save(todos); | |
}; | |
for (const ac of Object.values(Todo)) { | |
this.addEventListener(ac.type, handleEvent); | |
} | |
const route = ev => { | |
switch (window.location.hash) { | |
case "#/active": { | |
filter = "active"; | |
break; | |
} | |
case "#/completed": { | |
filter = "completed"; | |
break; | |
} | |
case "#/": { | |
filter = ""; | |
break; | |
} | |
default: { | |
filter = ""; | |
window.location.hash = "#/"; | |
} | |
} | |
if (ev != null) { | |
this.refresh(); | |
} | |
}; | |
route(); | |
window.addEventListener("hashchange", route); | |
try { | |
while (true) { | |
yield ( | |
<Fragment> | |
<Header /> | |
{!!todos.length && <Main todos={todos} filter={filter} />} | |
{!!todos.length && <Footer todos={todos} filter={filter} />} | |
</Fragment> | |
); | |
} | |
} finally { | |
window.removeEventListener("hashchange", route); | |
} | |
} | |
renderer.render(<App />, document.getElementsByClassName("todoapp")[0]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment