Taskboard 2. Interaction
Add delegated click events, conditional content and a keyed list. Move shared Taskboard state behind named PulseStore operations and observe its debounced delivery to the mounted view.
Table of contents
Stage 2. Add interaction with PulseStore
- Starting state
- Complete Stage 1 so TaskboardApp is mounted with serializable TaskboardState data.
- Applied change
- Handle clicks through onclick, render details with v-if and tasks with v-for plus key, then replace the plain state instance with createTaskboardStore and apply changes through named pulses.
- Observable result
- Toggle details changes the v-if content, and Add task appends the next keyed v-for item when PulseStore delivers its debounced subscriber update.
- Next step
- Continue with Stage 3 and keep this store instance while adding routes.
Build interaction before shared state
- Delegated events
- onclick participates in the Valyrian.js delegated event flow. Here each handler calls a PulseStore operation, so PulseStore takes control of update timing for that event.
- v-if
- v-if includes the details paragraph only while showDetails is true.
- v-for and key
- v-for renders each task through its callback child, and key preserves each task's stable identity.
- Why PulseStore
- Taskboard now needs one shared state object and named operations that routing, data loading and forms can reuse in later stages.
- PulseStore update flow
- store.state is read-only outside the store. Each named operation receives a mutable working copy as its first argument. During a delegated event, PulseStore uses preventUpdate() to suppress the event runtime update and delivers the committed state to readers through a debounced subscriber pass.
src/app.tsx
tsximport type { Task, TaskboardStore } from "./state";
export function TaskboardApp({ store }: { store: TaskboardStore }) {
const state = store.state;
return (
<main>
<h1>Taskboard</h1>
<button onclick={() => store.toggleDetails()}>Toggle details</button>
<p v-if={state.showDetails}>Tasks stay in one shared PulseStore.</p>
<button
onclick={() =>
store.addTask({
id: state.nextId,
title: "Task " + state.nextId,
})
}
>
Add task
</button>
<ul v-for={state.tasks}>
{(task: Task) => <li key={task.id}>{task.title}</li>}
</ul>
</main>
);
}
src/client-entry.tsx
tsximport { mount } from "valyrian.js";
import { TaskboardApp } from "./app";
import { createTaskboardStore } from "./state";
const store = createTaskboardStore();
mount("body", <TaskboardApp store={store} />);
src/state.ts
tsimport { createPulseStore } from "valyrian.js/pulses";
export type Task = { id: number; title: string };
export type TaskboardState = {
tasks: Task[];
showDetails: boolean;
nextId: number;
loaded: boolean;
};
export function createTaskboardStore(
initial: Partial<TaskboardState> = {},
) {
return createPulseStore(
{
tasks: (initial.tasks ?? [{ id: 1, title: "Learn Valyrian.js" }]).map(
(task) => ({ ...task }),
),
showDetails: initial.showDetails ?? false,
nextId: initial.nextId ?? 2,
loaded: initial.loaded ?? false,
},
{
toggleDetails(state) {
state.showDetails = !state.showDetails;
},
addTask(state, task: Task) {
state.tasks.push(task);
state.nextId = Math.max(state.nextId, task.id + 1);
},
replaceTasks(state, tasks: Task[]) {
state.tasks = tasks;
state.loaded = true;
state.nextId = tasks.reduce(
(nextId, task) => Math.max(nextId, task.id + 1),
1,
);
},
},
);
}
export type TaskboardStore = ReturnType<typeof createTaskboardStore>;