Skip to content

Instantly share code, notes, and snippets.

@geophree
Created April 27, 2020 19:48
Show Gist options
  • Save geophree/a317c658fd6d7bc51e1e80aa5243be7d to your computer and use it in GitHub Desktop.
Save geophree/a317c658fd6d7bc51e1e80aa5243be7d to your computer and use it in GitHub Desktop.
Crank.js TodoMVC alternative events/handlers implementation
// 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