Native rendering and state

Coordinate components with custom events

Dispatch a small CustomEvent when task state changes so counters and filters can react without direct coupling.

The task list should not need to know every counter, filter, or status widget that cares about its changes. A custom DOM event can publish a small fact at the board boundary and let interested ancestors respond.

Create a CustomEvent with a stable name and minimal detail. Set bubbles: true so a listener on the board or document can receive an event dispatched from a card. The producer reports what happened. It does not reach into the counter’s private markup.

card.dispatchEvent(new CustomEvent('taskchange', {
  bubbles: true,
  detail: { id: card.dataset.taskId, complete }
}))

board.addEventListener('taskchange', event => {
  remainingOutput.value = board.querySelectorAll(
    '.task:not([data-complete])'
  ).length
})

Complete a task and the remaining count updates without the card knowing which output element exists.

Event names are strings shared by producers and consumers. Pick a project-specific name, document its detail shape, and avoid changing it casually. A typo does not fail loudly. The listener simply never runs.

detail is passed by reference. Do not expose a mutable internal object a listener can modify unexpectedly. A small new object with an ID and primitive state makes the boundary obvious.

Custom events are synchronous. Every listener runs during dispatchEvent(). A thrown error or heavy calculation can complicate the originating action. Keep listeners small and schedule genuinely async work through the appropriate API.

Try this on your board: add one listener that updates a remaining-task output and another that logs changes. Remove the logger without touching task-card code. Dispatch a test event with a nonexistent ID and make the consumer ignore it safely. An event is a notification, not proof that its data is valid. If an ancestor receives nothing, confirm the event uses bubbles: true, the names match exactly, and dispatch happens from an element inside the listener’s subtree.

Lesson completed