|
| 1 | +import { html, reactive, component, watch } from "@arrow-js/core"; |
| 2 | +import type { Props } from "@arrow-js/core"; |
| 3 | + |
| 4 | +type Todo = { id: number; text: string; done: boolean }; |
| 5 | +type Filter = "all" | "active" | "done"; |
| 6 | + |
| 7 | +const TodoItem = component( |
| 8 | + ( |
| 9 | + props: Props<{ |
| 10 | + todo: Todo; |
| 11 | + onToggle: (id: number) => void; |
| 12 | + onRemove: (id: number) => void; |
| 13 | + }> |
| 14 | + ) => { |
| 15 | + return html` |
| 16 | + <li class="${() => (props.todo.done ? "todo done" : "todo")}"> |
| 17 | + <button |
| 18 | + class="toggle" |
| 19 | + @click="${() => props.onToggle(props.todo.id)}" |
| 20 | + > |
| 21 | + ${() => (props.todo.done ? "\u2713" : "")} |
| 22 | + </button> |
| 23 | + <span>${() => props.todo.text}</span> |
| 24 | + <button |
| 25 | + class="remove" |
| 26 | + @click="${() => props.onRemove(props.todo.id)}" |
| 27 | + > |
| 28 | + \u00d7 |
| 29 | + </button> |
| 30 | + </li> |
| 31 | + `; |
| 32 | + } |
| 33 | +); |
| 34 | + |
| 35 | +const Filters = component((props: Props<{ filter: Filter }>) => { |
| 36 | + const filters: Filter[] = ["all", "active", "done"]; |
| 37 | + return html` |
| 38 | + <nav class="filters"> |
| 39 | + ${filters.map( |
| 40 | + (f) => html` |
| 41 | + <button |
| 42 | + class="${() => (props.filter === f ? "active" : "")}" |
| 43 | + @click="${() => { |
| 44 | + props.filter = f; |
| 45 | + }}" |
| 46 | + > |
| 47 | + ${f} |
| 48 | + </button> |
| 49 | + ` |
| 50 | + )} |
| 51 | + </nav> |
| 52 | + `; |
| 53 | +}); |
| 54 | + |
| 55 | +export function App() { |
| 56 | + const state = reactive({ |
| 57 | + todos: [ |
| 58 | + { id: 1, text: "Learn reactive state", done: true }, |
| 59 | + { id: 2, text: "Build a component", done: false }, |
| 60 | + { id: 3, text: "Render a keyed list", done: false }, |
| 61 | + ] as Todo[], |
| 62 | + input: "", |
| 63 | + filter: "all" as Filter, |
| 64 | + nextId: 4, |
| 65 | + }); |
| 66 | + |
| 67 | + const filtered = () => { |
| 68 | + if (state.filter === "active") return state.todos.filter((t) => !t.done); |
| 69 | + if (state.filter === "done") return state.todos.filter((t) => t.done); |
| 70 | + return state.todos; |
| 71 | + }; |
| 72 | + |
| 73 | + const remaining = () => state.todos.filter((t) => !t.done).length; |
| 74 | + |
| 75 | + const addTodo = () => { |
| 76 | + const text = state.input.trim(); |
| 77 | + if (!text) return; |
| 78 | + state.todos.push({ id: state.nextId, text, done: false }); |
| 79 | + state.nextId++; |
| 80 | + state.input = ""; |
| 81 | + }; |
| 82 | + |
| 83 | + const onToggle = (id: number) => { |
| 84 | + const todo = state.todos.find((t) => t.id === id); |
| 85 | + if (todo) todo.done = !todo.done; |
| 86 | + }; |
| 87 | + |
| 88 | + const onRemove = (id: number) => { |
| 89 | + const i = state.todos.findIndex((t) => t.id === id); |
| 90 | + if (i >= 0) state.todos.splice(i, 1); |
| 91 | + }; |
| 92 | + |
| 93 | + watch(() => { |
| 94 | + console.log( |
| 95 | + `[Arrow.js] ${remaining()} of ${state.todos.length} todos remaining` |
| 96 | + ); |
| 97 | + }); |
| 98 | + |
| 99 | + return html` |
| 100 | + <div class="app"> |
| 101 | + <h1>Nitro + Arrow.js</h1> |
| 102 | + <p class="subtitle">A ~3KB reactive UI with SSR</p> |
| 103 | +
|
| 104 | + <div class="input-row"> |
| 105 | + <input |
| 106 | + type="text" |
| 107 | + placeholder="What needs to be done?" |
| 108 | + .value="${() => state.input}" |
| 109 | + @input="${(e: Event) => { |
| 110 | + state.input = (e.target as HTMLInputElement).value; |
| 111 | + }}" |
| 112 | + @keydown="${(e: Event) => { |
| 113 | + if ((e as KeyboardEvent).key === "Enter") addTodo(); |
| 114 | + }}" |
| 115 | + /> |
| 116 | + <button class="add" @click="${addTodo}">Add</button> |
| 117 | + </div> |
| 118 | +
|
| 119 | + ${Filters(state)} |
| 120 | +
|
| 121 | + <ul class="todo-list"> |
| 122 | + ${() => |
| 123 | + filtered().map((todo) => |
| 124 | + TodoItem({ todo, onToggle, onRemove }).key(todo.id) |
| 125 | + )} |
| 126 | + </ul> |
| 127 | +
|
| 128 | + <footer class="footer"> |
| 129 | + <span>${() => remaining()} items left</span> |
| 130 | + <button |
| 131 | + class="clear" |
| 132 | + @click="${() => { |
| 133 | + state.todos = state.todos.filter((t) => !t.done); |
| 134 | + }}" |
| 135 | + > |
| 136 | + Clear done |
| 137 | + </button> |
| 138 | + </footer> |
| 139 | + </div> |
| 140 | + `; |
| 141 | +} |
0 commit comments