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.