Last active
July 19, 2022 07:35
-
-
Save kt3k/1644876b0881e145b5d62da123723bdf to your computer and use it in GitHub Desktop.
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
/** @jsx h */ | |
import { bind, cl, h } from "https://deno.land/x/[email protected]/paul.ts"; | |
import { Todo, TodoCollection } from "./todo-models"; | |
type Filter = "all" | "completed" | "uncompleted"; | |
type Query = <T = HTMLElement>(q: string) => T | null; | |
const hashToFilter = { | |
"#/all": "all", | |
"#/active": "uncompleted", | |
"#/completed": "completed", | |
} as const; | |
function TodoApp({ todos, filter }: { todos: TodoCollection; filter: Filter }) { | |
const uncompleted = todos.uncompleted(); | |
const completed = todos.completed(); | |
const visibleItems = filter === "completed" | |
? completed | |
: filter === "uncompleted" | |
? uncompleted | |
: todos; | |
return ( | |
<section class="todoapp" data-framework="paul"> | |
<header class="header"> | |
<h1>todos</h1> | |
<input | |
class="new-todo" | |
autofocus | |
autocomplete="off" | |
placeholder="What needs to be done?" | |
/> | |
</header> | |
<section class={cl("main", { hidden: todos.length === 0 })}> | |
<input | |
id="toggle-all" | |
class={cl("toggle-all", { hidden: todos.length === 0 })} | |
type="checkbox" | |
checked={uncompleted.length === 0} | |
/> | |
<label for="toggle-all" class={cl({ hidden: todos.length === 0 })}> | |
Mark all as complete | |
</label> | |
<ul class="todo-list"> | |
{visibleItems.map((todo) => ( | |
<li | |
class={cl("todo", { completed: todo.completed })} | |
key={todo.id} | |
> | |
<div class="view"> | |
<input | |
class="toggle" | |
type="checkbox" | |
checked={todo.completed} | |
/> | |
<label>{todo.title}</label> | |
<button class="destroy"></button> | |
</div> | |
<input class="edit" type="text" /> | |
</li> | |
))} | |
</ul> | |
</section> | |
<footer class={cl("footer", { hidden: todos.length === 0 })}> | |
<span class="todo-count"> | |
<strong>{uncompleted.length}</strong> | |
item{uncompleted.length !== 1 && <span class="plural">s</span>} | |
left | |
</span> | |
<ul class="filters"> | |
<li> | |
<a class={cl({ selected: filter === "all" })} href="#/all"> | |
All | |
</a> | |
</li> | |
<li> | |
<a | |
class={cl({ selected: filter === "uncompleted" })} | |
href="#/active" | |
> | |
Active | |
</a> | |
</li> | |
<li> | |
<a | |
class={cl({ selected: filter === "completed" })} | |
href="#/completed" | |
> | |
Completed | |
</a> | |
</li> | |
</ul> | |
<button | |
class={cl("clear-completed", { hidden: completed.length === 0 })} | |
> | |
Clear completed | |
</button> | |
</footer> | |
</section> | |
); | |
} | |
bind("todoapp", ( | |
el: HTMLElement, | |
{ on, query, emit, morph }: { | |
on: (a: any, e: any, b?: any, c?: any) => void; | |
query: Query; | |
emit: (e: any, b?: any) => void; | |
morph: (el: HTMLElement, html: any) => void; | |
}, | |
) => { | |
on("keypress", ".new-todo", (e) => { | |
if ((e as any).code !== "Enter") { | |
// If not a Enter, ignore the event | |
return; | |
} | |
const newInput = query<HTMLInputElement>(".new-todo")!; | |
const title = query<HTMLInputElement>(".new-todo")?.value?.trim(); | |
if (!title) { | |
return; | |
} | |
newInput.value = ""; | |
todos.add(new Todo(`${todos.maxId() + 1}`, title, false)); | |
todos.save(); | |
render(); | |
}); | |
on("click", (e) => { | |
todos.getById((e.target as Element).parentElement!.parentElement!.id) | |
?.toggle(); | |
todos.save(); | |
render(); | |
}); | |
on("click", ".toggle-all", (e) => { | |
if ((e.target as any).checked) { | |
todos.completeAll(); | |
} else { | |
todos.uncompleteAll(); | |
} | |
todos.save(); | |
render(); | |
}); | |
on("click", ".destroy", (e) => { | |
const toRemove = todos.getById( | |
(e.target as Element).parentElement!.parentElement!.id, | |
); | |
todos.remove(toRemove); | |
todos.save(); | |
render(); | |
}); | |
on("click", ".clear-completed", () => { | |
todos.completed().forEach((todo) => { | |
todos.remove(todo); | |
}); | |
todos.save(); | |
render(); | |
}); | |
on("dblclick", ".todo > .view > label", (e) => { | |
const todoItem = (e.target as Element).parentElement!.parentElement!; | |
const todo = todos.getById(todoItem.id); | |
todoItem.classList.add("editing"); | |
const editInput = todoItem.querySelector<HTMLInputElement>(".edit")!; | |
editInput.value = todo.title; | |
editInput.focus(); | |
}); | |
on("keypress", ".edit", (e) => { | |
const input: HTMLInputElement = e.target as any; | |
if ((e as any).code === "Enter") { | |
input.blur(); | |
} else if ((e as any).code === "Escape") { | |
input.value = todos.getById(input.parentElement!.id).title; | |
input.blur(); | |
} | |
}); | |
on("focusout", ".edit", (e) => { | |
const input = e.target as HTMLInputElement; | |
const value = input.value.trim(); | |
const todoItem = input.parentElement!; | |
if (value) { | |
todos.getById(todoItem.id).title = value; | |
todoItem.classList.remove("editing"); | |
} else { | |
todos.remove(todos.getById(todoItem.id)); | |
todoItem.classList.remove("editing"); | |
} | |
render(); | |
}); | |
on(window, "onhashchange", () => { | |
filter = hashToFilter[location.hash]; | |
render(); | |
}); | |
const render = () => { | |
morph(el, <TodoApp todos={todos} filter={filter} />); | |
}; | |
// Prepare data in closure | |
let filter: Filter; | |
const todos = TodoCollection.restore(); | |
// Set up UI | |
query<HTMLInputElement>(".new-todo")!.focus(); | |
emit(window, "onhashchange"); | |
}); |
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
/** Todo represents a single Todo item. */ | |
export class Todo { | |
constructor( | |
public id: string, | |
public title: string, | |
public completed: boolean, | |
) {} | |
toggle() { | |
this.completed = !this.completed; | |
} | |
} | |
const KEY = "capsule-todomvc"; | |
/** TodoCollection represents a collection of Todos. */ | |
export class TodoCollection { | |
constructor(public todos: Todo[] = []) {} | |
getById(id: string): Todo | null { | |
return this.todos.find((todo) => todo.id === id); | |
} | |
remove(toRemove: Todo): void { | |
this.todos = this.todos.filter((todo) => todo.id !== toRemove.id); | |
} | |
add(todo: Todo): void { | |
this.todos.push(todo); | |
} | |
get length(): number { | |
return this.todos.length; | |
} | |
has(test: Todo): boolean { | |
return this.todos.some((todo) => todo.id === test.id); | |
} | |
completed(): TodoCollection { | |
return new TodoCollection(this.todos.filter((todo) => todo.completed)); | |
} | |
uncompleted(): TodoCollection { | |
return new TodoCollection(this.todos.filter((todo) => !todo.completed)); | |
} | |
completeAll(): void { | |
this.todos.forEach((todo) => { | |
todo.completed = true; | |
}); | |
} | |
uncompleteAll(): void { | |
this.todos.forEach((todo) => { | |
todo.completed = false; | |
}); | |
} | |
forEach(f: (todo: Todo) => void): void { | |
this.todos.forEach(f); | |
} | |
toJSON(): string { | |
return JSON.stringify(this.todos); | |
} | |
static fromJson(json: string) { | |
return new TodoCollection( | |
JSON.parse(json).map( | |
({ id, title, completed }) => new Todo(id, title, completed), | |
), | |
); | |
} | |
save() { | |
localStorage.setItem(KEY, this.toJSON()); | |
} | |
static restore(): TodoCollection { | |
return TodoCollection.fromJson(localStorage.getItem(KEY) || "[]"); | |
} | |
maxId() { | |
return Math.max(0, ...this.todos.map((todo) => +todo.id)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment