Simulation 03
Search & Filter

Live search and filter over a dataset. Demonstrates how model wires input to a signal, and computed derives the filtered list — with zero manual DOM manipulation.

Live Telemetry
Apollo Mission Archive missions found
— No missions match —

Source Code

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

  <!-- Live search result count -->
  <span text="resultCount"></span> missions found

  <!-- Inputs bound to signals -->
  <input  model="query"        placeholder="Search missions...">
  <select model="statusFilter">
    <option value="">All Status</option>
    <option value="success">Success</option>
    <option value="partial">Partial</option>
    <option value="failure">Failure</option>
  </select>

  <!-- Filtered list -->
  <ul each="results" key="id">
    <template>
      <li>
        <span text="name"></span>
        <span text="year"></span>
        <span text="statusLabel" cls="statusCls"></span>
      </li>
    </template>
  </ul>

  <!-- Empty state -->
  <p show="isEmpty">No missions match</p>

</div>
search.js
import { bind, signal, computed } from 'rdbl'

// ── Signals (user inputs) ─────────────────────────────────────
const query        = signal('')
const statusFilter = signal('')

// ── Computed (derived filtered list) ──────────────────────────
const results = computed(() => {
  const q  = query().toLowerCase().trim()
  const sf = statusFilter()

  return MISSIONS.filter(m => {
    const matchesQ  = !q  || m.name.toLowerCase().includes(q)
    const matchesSF = !sf || m.status === sf
    return matchesQ && matchesSF
  })
})

const resultCount = computed(() => results().length)
const isEmpty     = computed(() => results().length === 0)

// ── Bind ──────────────────────────────────────────────────────
// model= wires inputs to signals (two-way)
// each= renders results (re-renders on every filter change)
bind(document.querySelector('#search-app'), {
  query, statusFilter,
  results, resultCount, isEmpty,
})

Key Patterns

model= captures input changes

model="query" binds the input value to the query signal two-ways. Type anything and query() updates instantly. No event listener boilerplate needed.

<input model="query" placeholder="Search...">

// In state:
const query = signal('')
// query() always reflects the current input value

computed= reacts to multiple signals

The results computed reads both query and statusFilter. When either changes, the entire filtered list re-evaluates automatically. rdbl tracks dependencies by observing which signals are called during execution.

const results = computed(() => {
  const q  = query()         // ← tracked dependency
  const sf = statusFilter()  // ← tracked dependency

  return data.filter(item =>
    (!q  || item.name.includes(q)) &&
    (!sf || item.status === sf)
  )
})

Keyed each= efficiently diffs the list

As you type, the filtered list changes. rdbl uses the key attribute to match existing DOM nodes to list items — adding, removing, or reordering them without recreating unchanged rows.

<ul each="results" key="id">
  <template>
    <li><span text="name"></span></li>
  </template>
</ul>
Notice what's missing No event listener on the input. No document.querySelector calls. No manual list re-rendering. The entire filtering pipeline is declarative: wire inputs to signals, derive output with computed, render with each.
Flight Manual ▶ ← Todo Mission