Your First Component
Let’s build a real component: a todo list with add, toggle, and delete.
Step 1: The Data Model
Section titled “Step 1: The Data Model”Start with the state. A todo has an id, text, and done flag:
import { signal } from "@whisq/core";
interface Todo { id: number; text: string; done: boolean;}
const todos = signal<Todo[]>([ { id: 1, text: "Learn Whisq", done: false }, { id: 2, text: "Build something", done: false },]);
let nextId = 3;Step 2: The Todo Item
Section titled “Step 2: The Todo Item”Each todo is a li with a checkbox, text, and delete button:
import { li, input, span, button } from "@whisq/core";
function TodoItem(todo: Todo) { return li({ class: () => todo.done ? "done" : "" }, input({ type: "checkbox", checked: todo.done, onchange: () => toggleTodo(todo.id), }), span(todo.text), button({ onclick: () => removeTodo(todo.id) }, "✕"), );}Step 3: Actions
Section titled “Step 3: Actions”Functions that update the signal:
function addTodo(text: string) { todos.value = [...todos.value, { id: nextId++, text, done: false }];}
function toggleTodo(id: number) { todos.value = todos.value.map(t => t.id === id ? { ...t, done: !t.done } : t );}
function removeTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id);}Step 4: The Add Form
Section titled “Step 4: The Add Form”An input with an “Add” button:
import { signal, div, form, input, button } from "@whisq/core";
function AddTodo() { const text = signal("");
return form({ onsubmit: (e) => { e.preventDefault(); if (text.value.trim()) { addTodo(text.value.trim()); text.value = ""; } }, }, input({ type: "text", placeholder: "What needs doing?", value: () => text.value, oninput: (e) => text.value = (e.target as HTMLInputElement).value, }), button({ type: "submit" }, "Add"), );}Step 5: The Full App
Section titled “Step 5: The Full App”Put it all together:
import { signal, computed, component, div, h1, p, ul, form, input, button, li, span, mount,} from "@whisq/core";
interface Todo { id: number; text: string; done: boolean;}
const todos = signal<Todo[]>([ { id: 1, text: "Learn Whisq", done: false }, { id: 2, text: "Build something", done: false },]);let nextId = 3;
const remaining = computed(() => todos.value.filter(t => !t.done).length);
function addTodo(text: string) { todos.value = [...todos.value, { id: nextId++, text, done: false }];}
function toggleTodo(id: number) { todos.value = todos.value.map(t => t.id === id ? { ...t, done: !t.done } : t );}
function removeTodo(id: number) { todos.value = todos.value.filter(t => t.id !== id);}
const App = component(() => { const text = signal("");
return div({ class: "app" }, h1("Whisq Todos"), form({ onsubmit: (e) => { e.preventDefault(); if (text.value.trim()) { addTodo(text.value.trim()); text.value = ""; } }, }, input({ type: "text", placeholder: "What needs doing?", value: () => text.value, oninput: (e) => text.value = (e.target as HTMLInputElement).value, }), button({ type: "submit" }, "Add"), ), ul( () => todos.value.map(todo => li({ class: () => todo.done ? "done" : "" }, input({ type: "checkbox", checked: todo.done, onchange: () => toggleTodo(todo.id), }), span(todo.text), button({ onclick: () => removeTodo(todo.id) }, "✕"), ) ), ), p(() => `${remaining.value} item${remaining.value !== 1 ? "s" : ""} remaining`), );});
mount(App({}), document.getElementById("app")!);What You Learned
Section titled “What You Learned”In this tutorial you used:
signal()— reactive state for the todo list and input textcomputed()— derived value (remaining count)component()— the root app component- Element functions —
div,h1,ul,li,form,input,button,span,p - Reactive children —
() => todos.value.map(...)for dynamic lists - Reactive props —
class: () => ...for conditional styling - Events —
onclick,onsubmit,oninput,onchange
Next Steps
Section titled “Next Steps”- Signals — Deep dive into
signal(),computed(),effect() - Elements — All the element functions and props
- Components — Lifecycle, context, error boundaries