Skip to content

Instantly share code, notes, and snippets.

@kt3k
Last active July 19, 2022 07:35
Show Gist options
  • Save kt3k/1644876b0881e145b5d62da123723bdf to your computer and use it in GitHub Desktop.
Save kt3k/1644876b0881e145b5d62da123723bdf to your computer and use it in GitHub Desktop.
/** @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");
});
/** 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