import { render } from 'js-widgets'; import { effect, state } from 'js-widgets/ext'; import { classes, createRef } from 'js-widgets/util'; import { makeComponentsMobxAware } from 'js-widgets/mobx-tools'; import { autorun, makeAutoObservable } from 'mobx'; // === types ========================================================= type Todo = { id: number; title: string; completed: boolean; }; // === constants ===================================================== const ENTER_KEY = 13; const ESC_KEY = 27; const STORAGE_KEY = 'todomvc::data'; // === Filter ======================================================== class Filter { static none = new Filter(() => true); static active = new Filter((todo) => !todo.completed); static completed = new Filter((todo) => todo.completed); private constructor(public apply: (todo: Todo) => boolean) {} } // === mobx store ==================================================== const store = makeAutoObservable({ filter: Filter.none, todos: [] as Todo[], init(todos: Todo[] = [], filter = Filter.none) { this.todos = todos; this.filter = filter; }, setFilter(filter: Filter) { this.filter = filter; }, setTodoTitle(id: number, title: string) { const idx = this.todos.findIndex((todo) => todo.id === id); if (idx >= 0) { this.todos[idx].title = title; } }, addTodo(title: string) { const id = store.todos.reduce((max, todo) => Math.max(max, todo.id + 1), 0); this.todos.push({ id, title, completed: false }); }, deleteTodo(id: number) { this.todos = this.todos.filter((todo) => todo.id !== id); }, deleteCompleted() { this.todos = this.todos.filter((todo) => !todo.completed); }, setCompleted(id: number, completed = true) { const idx = this.todos.findIndex((todo) => todo.id === id); if (idx >= 0) { this.todos[idx].completed = completed; } }, setAllCompleted(completed = true) { this.todos.forEach((todo) => (todo.completed = completed)); } }); // === routing ======================================================= function establishRouting(): void { const route = () => { switch (window.location.hash) { case '#/active': { store.setFilter(Filter.active); break; } case '#/completed': { store.setFilter(Filter.completed); break; } case '#/': { store.setFilter(Filter.none); break; } default: { store.setFilter(Filter.none); window.location.hash = '#/'; } } }; route(); window.addEventListener('hashchange', route); } // === storage functions ============================================= function loadTodos(): Todo[] { try { try { const storedTodos = JSON.parse(localStorage.getItem(STORAGE_KEY) as any); if (Array.isArray(storedTodos) && storedTodos.length > 0) { return storedTodos; } } catch {} localStorage.removeItem(STORAGE_KEY); } catch {} return []; } function saveTodos(todos: Todo[]) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); } catch {} } // === components ==================================================== // --- Header -------------------------------------------------------- function Header() { const [s, set] = state({ title: '' }); const onInput = (ev: any) => set.title(ev.target.value); const onKeyDown = (ev: any) => { if (ev.keyCode === ENTER_KEY) { const newTitle = ev.target.value.trim(); set.title(''); if (newTitle) { ev.preventDefault(); store.addTodo(newTitle); } } }; return () => (

todos

); } // --- TodoItem ------------------------------------------------------ function TodoItem(p: { todo: Todo; // }) { const inputFieldRef = createRef(); const [s, set] = state({ active: false, title: p.todo.title }); const onDeleteClick = () => store.deleteTodo(p.todo.id); const onToggleClick = (ev: any) => store.setCompleted(p.todo.id, ev.target.checked); const onDoubleClick = () => set.active(true); const onInput = (ev: any) => set.title(ev.target.value); const onBlur = (ev: any) => { const title = ev.target.value.trim(); set.active(false); set.title(title); if (title) { store.setTodoTitle(p.todo.id, title); } else { store.deleteTodo(p.todo.id); } }; const onKeyDown = (ev: any) => { if (ev.keyCode === ENTER_KEY) { ev.target.blur(); } else if (ev.keyCode === ESC_KEY) { set.active(false); } }; effect(() => { if (inputFieldRef.current) { inputFieldRef.current.focus(); } }); return () => { const className = classes({ editing: s.active, completed: p.todo.completed }); return (
  • ); }; } // --- Main ---------------------------------------------------------- function Main() { const onChange = () => store.setAllCompleted(!store.todos.every((todo) => todo.completed)); return () => { const completed = !store.todos.every((todo) => todo.completed); const filteredTodos = store.todos.filter(store.filter.apply); return (
    ); }; } // --- Filters ------------------------------------------------------- function Filters() { return ( ); } // --- Footer -------------------------------------------------------- function Footer() { const onClearCompletedClick = () => store.deleteCompleted(); return () => { const numCompleted = store.todos.filter((todo) => todo.completed).length; const numRemaining = store.todos.length - numCompleted; return ( ); }; } // --- App ----------------------------------------------------------- function App() { return (
    {!!store.todos.length &&
    } {!!store.todos.length &&
    }
    ); } // === main ========================================================== makeComponentsMobxAware(); store.init(loadTodos()); autorun(() => saveTodos(store.todos)); establishRouting(); render(, '.todoapp');