Simulation 02
Todo Mission

A full todo app with a Yesterday/Today view. Demonstrates each, model, getItemContext, computed filtering, and effect-based localStorage persistence — all without a build step.

Live Telemetry
Mission Log
— No objectives logged —

Source Code

todo.html
<div id="todo-app">

  <!-- View selector -->
  <button cls="todayTabCls"     onclick="showToday">
    Today <span text="activeCount"></span>
  </button>
  <button cls="yesterdayTabCls" onclick="showCompleted">
    Yesterday <span text="completedCount"></span>
  </button>

  <!-- Add form, hidden in yesterday view -->
  <div show="isToday">
    <input model="newTodo" placeholder="Mission objective...">
    <button onclick="addTodo">Launch</button>
  </div>

  <!-- Keyed list -->
  <ul each="visibleTodos" key="id">
    <template>
      <li>
        <span text="text" cls="textCls"></span>
        <button onclick="toggleTodo" text="actionLabel"></button>
        <button onclick="removeTodo">Abort</button>
      </li>
    </template>
  </ul>

  <p text="summary"></p>
</div>
todo.js
import { bind, signal, computed, effect, getItemContext } from 'rdbl'

// Item factory — bakes display properties into each item
function makeTodo(text, done = false) {
  return {
    id:          Date.now() + Math.random(),
    text,
    done,
    textCls:     done ? 'todo-text done'  : 'todo-text',
    actionLabel: done ? 'RESTORE'         : 'COMPLETE',
  }
}

// ── Signals ──────────────────────────────────────────────────
const todos   = signal([makeTodo('Review mission parameters')])
const view    = signal('today')   // 'today' | 'yesterday'
const newTodo = signal('')

// ── Computed ─────────────────────────────────────────────────
const activeTodos    = computed(() => todos().filter(t => !t.done))
const completedTodos = computed(() => todos().filter(t =>  t.done))
const visibleTodos   = computed(() =>
  view() === 'today' ? activeTodos() : completedTodos()
)
const summary = computed(() => {
  const done  = completedTodos().length
  const total = todos().length
  return `${done} of ${total} missions complete`
})
const isToday         = computed(() => view() === 'today')
const todayTabCls     = computed(() => view() === 'today'     ? 'tab tab-active' : 'tab')
const yesterdayTabCls = computed(() => view() === 'yesterday' ? 'tab tab-active' : 'tab')

// ── Effect: persist on every change ──────────────────────────
effect(() => {
  localStorage.setItem('todos', JSON.stringify(todos()))
})

// ── Handlers ─────────────────────────────────────────────────
function addTodo() {
  const text = newTodo().trim()
  if (!text) return
  todos.set([...todos(), makeTodo(text)])
  newTodo.set('')
}

function toggleTodo(event, el) {
  const { item } = getItemContext(el)
  todos.set(todos().map(t =>
    t.id !== item.id ? t : {
      ...t,
      done:        !t.done,
      textCls:     !t.done ? 'todo-text done' : 'todo-text',
      actionLabel: !t.done ? 'RESTORE'        : 'COMPLETE',
    }
  ))
}

function removeTodo(event, el) {
  const { item } = getItemContext(el)
  todos.set(todos().filter(t => t.id !== item.id))
}

// ── Bind ─────────────────────────────────────────────────────
bind(document.querySelector('#todo-app'), {
  todos, newTodo, visibleTodos,
  activeCount: computed(() => activeTodos().length),
  completedCount: computed(() => completedTodos().length),
  isEmpty: computed(() => visibleTodos().length === 0),
  isToday, todayTabCls, yesterdayTabCls, summary,
  addTodo, toggleTodo, removeTodo,
  showToday:     () => view.set('today'),
  showCompleted: () => view.set('completed'),
})
State shape
// Signals (reactive sources)
todos    : Signal<Todo[]>   // array of all todos
view     : Signal<string>   // 'today' | 'yesterday'
newTodo  : Signal<string>   // input value

// Computeds (derived from signals)
activeTodos     // todos where done === false
completedTodos  // todos where done === true
visibleTodos    // whichever list is active
activeCount     // number
completedCount  // number
isEmpty         // boolean
isToday         // boolean — controls show= on add form
todayTabCls     // CSS class string for today tab
yesterdayTabCls // CSS class string for yesterday tab
summary         // "N of M missions complete"

// Per-item display properties (baked into the array items)
item.textCls     // 'todo-text' | 'todo-text done'
item.actionLabel // 'COMPLETE' | 'RESTORE'

// Handlers
addTodo, toggleTodo, removeTodo
showToday, showCompleted

Key Patterns

Yesterday/Today — filter with computed

Two views, one array. visibleTodos is a computed that filters based on the view signal. Switching tabs just calls view.set('yesterday') — the list re-renders automatically.

const visibleTodos = computed(() =>
  view() === 'today' ? activeTodos() : completedTodos()
)

Per-item display properties

Inside an each template, the scope is the item itself. For conditional styling, bake the derived properties into the item when creating or updating it — instead of reaching back to the parent scope.

// When toggling, update the display properties too
todos.set(todos().map(t =>
  t.id !== item.id ? t : {
    ...t,
    done:        !t.done,
    textCls:     !t.done ? 'done' : '',
    actionLabel: !t.done ? 'RESTORE' : 'COMPLETE',
  }
))

getItemContext — which item was clicked?

Inside a list event handler, getItemContext(el) walks up the DOM to find the bound item for that row. Works regardless of how deeply nested the clicked element is.

function removeTodo(event, el) {
  const { item, index } = getItemContext(el)
  todos.set(todos().filter(t => t.id !== item.id))
}

localStorage persistence with effect

One effect handles all persistence. It runs whenever todos changes — no need to call it manually.

effect(() => {
  localStorage.setItem('todos', JSON.stringify(todos()))
})
Try it Add some tasks above, refresh the page — they persist. That's effect + localStorage in four lines.
Next: Search & Filter ▶ ← Counter