example/assets/js/app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
import h3 from "./h3.js"; import Paginator from "./paginator.js"; import Todo from "./todo.js"; let todos = []; let filteredTodos = []; let view = null; let emptyTodoError = false; let filter = ""; let pagesize = 10; let page = 1; // State management via localStorage const save = () => { localStorage.setItem("h3_todo_list", JSON.stringify(todos)); }; const load = () => { const lsTodos = localStorage.getItem("h3_todo_list"); if (lsTodos) { todos = JSON.parse(lsTodos); } }; // Actual DOM creation/updateing const update = () => { save(); view.update({ vnode: build() }); }; const toggle = (todo) => { todo.done = !todo.done; update(); } const remove = (todo) => { todos = todos.filter(({ key }) => key !== todo.key); update(); } // Todo component const todo = Todo({ toggle, remove }); // Paginator Component const paginator = Paginator(update); // UI Methods // Add a todo item const addTodo = () => { const newTodo = document.getElementById("new-todo"); if (!newTodo.value) { emptyTodoError = true; update(); document.getElementById("new-todo").focus(); return; } emptyTodoError = false; todos.unshift({ key: `todo_${Date.now()}__${newTodo.value}`, // Make todos "unique-enough" to ensure they are processed correctly text: newTodo.value, }); newTodo.value = ""; update(); document.getElementById("new-todo").focus(); }; // Add a todo item when pressing enter in the input field. const addTodoOnEnter = (event) => { if (event.keyCode == 13) { addTodo(); event.preventDefault(); } }; // Set the todo filter. const setFilter = (event) => { let f = document.getElementById("filter-text"); filter = f.value; update(); f = document.getElementById("filter-text"); f.focus(); }; // Display todo items const displayTodos = () => { const start = (page - 1) * pagesize; const end = Math.min(start + pagesize, filteredTodos.length); return filteredTodos.slice(start, end).map(todo); }; // Clear error message const clearError = () => { emptyTodoError = false; update(); document.getElementById("new-todo").focus(); }; // Filtering function for todo items const filterTodos = ({ text }) => text.match(filter); // Main rendering function (creates virtual dom) const build = () => { const hash = window.location.hash; filteredTodos = todos.filter(filterTodos); if (hash.match(/page=(\d+)/)) { page = parseInt(hash.match(/page=(\d+)/)[1]); } // Recalculate page in case data is filtered. page = Math.min(Math.ceil(filteredTodos.length / pagesize), page) || 1; const emptyTodoErrorClass = emptyTodoError ? "" : ".hidden"; const paginatorData = { size: pagesize, page: page, total: filteredTodos.length, }; return h3("div#todolist.todo-list-container", [ h3("h1", ["To Do List"]), h3("form.add-todo-form", [ h3("input", { id: "new-todo", placeholder: "What do you want to do?", autofocus: true, onkeydown: addTodoOnEnter, }), h3( "span.submit-todo", { onclick: addTodo, }, ["+"] ), ]), h3(`div.error${emptyTodoErrorClass}`, [ h3("span.error-message", ["Please enter a non-empty todo item."]), h3( "span.dismiss-error", { onclick: clearError, }, ["✘"] ), ]), h3("div.navigation-bar", [ h3("input", { id: "filter-text", placeholder: "Type to filter todo items...", onkeyup: setFilter, value: filter, }), paginator(paginatorData), ]), h3("div.todo-list", displayTodos()), ]); }; load(); view = build(); document.getElementById("app").appendChild(view.render()); |