Events and updates
A packing list you cannot check off is a poster. This step adds the two things you do with the list: mark an item packed, and add a new one.
Changing the state
Add two functions to src/store.ts:
// The packing list's state and the two ways it changes.
import { signal } from "zerolith";
export interface Item
{
id: number;
name: string;
packed: boolean;
}
export const items = signal<Item[]>([
{ id: 1, name: "Passport", packed: true },
{ id: 2, name: "Phone charger", packed: false },
{ id: 3, name: "Rain jacket", packed: false }
], { name: "items" });
export function toggle(id: number): void
{
items.set(items.get().map((item) =>
(item.id === id ? { ...item, packed: !item.packed } : item)));
}
export function add(name: string): void
{
const id = Math.max(0, ...items.get().map((item) => item.id)) + 1;
items.set([...items.get(), { id, name, packed: false }]);
}Both functions build a new array and set it, rather than changing the old array in place. A signal notices set(), and it compares the new value with the old one: the same array, even with a changed item inside, looks like no change at all. Setting a new array is what tells the page something happened, and each then works out which rows actually differ by their keys.
toggle copies only the item that changes; every other item keeps its object, so its row sees no change. add picks the next free id, one more than the largest so far.
Listening to events
Update src/components/packing.ts:
// The packing list page: check items off, and add new ones.
import { type View, computed, each, html } from "zerolith";
import { add, items, toggle } from "../store.ts";
export function Packing(): View
{
const count = computed(() => items.get().length);
const submitted = (event: SubmitEvent): void =>
{
event.preventDefault();
const form = event.currentTarget as HTMLFormElement;
const name = String(new FormData(form).get("name") ?? "").trim();
if (name !== "")
{
add(name);
form.reset();
}
};
return html`
<h1>Packing list</h1>
<p>${count} items</p>
<ul>
${each(items, (item) => item.id, (item) => html`
<li>
<label>
<input type="checkbox"
.checked=${computed(() => item.get().packed)}
@change=${() => toggle(item.get().id)}>
${computed(() => item.get().name)}
</label>
</li>`)}
</ul>
<form @submit=${submitted}>
<label>New item
<input name="name" autocomplete="off" required>
</label>
<button>Add</button>
</form>`;
}An attribute that starts with @ listens to an event: @change on the checkbox, @submit on the form. The value is the function to call. Two more binding forms appear here:
.checked=${...}with a leading dot sets a property on the element instead of an attribute, which is what a checkbox's checked state needs.- A plain attribute, like
name="name", is static markup.
The form is a real form: it has a label, a required field, and a submit button, so it works with the keyboard and with assistive technology, and pressing Enter submits it. The handler stops the browser's own submission, reads the field through FormData, and clears the form after adding.
What you learned
@event=${handler}listens to an event;.property=${value}sets a property.- State changes by setting a new value; a signal ignores a value that is the same as the old one.
- Native forms and labels give you keyboard and screen reader support for free.