Dynamic Views
You’ve got el() for creating elements. Now let’s make views dynamic: lists that update, content that switches, and elements that render elsewhere.
Rendering Lists with map()
Section titled “Rendering Lists with map()”map() renders a reactive list efficiently. When items change, it updates only what’s necessary.
First, add the map module to your service:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { createMapModule } from '@rimitive/view/map';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();
const svc = compose( SignalModule, ComputedModule, EffectModule, createElModule(adapter), createMapModule(adapter), MountModule);
const { el, map, signal, mount } = svc;Basic Usage
Section titled “Basic Usage”For primitive arrays (strings, numbers), just pass the array and a render function:
const items = signal(['Apple', 'Banana', 'Cherry']);
const list = el('ul')( map(items, (item) => el('li')(item)));The render function receives a reactive signal wrapping each item. You can pass it directly as a child:
map(items, (item) => el('li')(item) // item is a signal, passed directly)When items updates, map() reconciles the DOM efficiently—adding, removing, and reordering elements as needed.
Keyed Lists
Section titled “Keyed Lists”For object arrays, provide a key function so map() can track identity:
type Todo = { id: number; text: string; done: boolean };
const todos = signal<Todo[]>([ { id: 1, text: 'Learn Rimitive', done: false }, { id: 2, text: 'Build something', done: false },]);
const list = el('ul')( map( todos, (todo) => todo.id, // key function (todo) => el('li')( el('span')(computed(() => todo().text)), el('input').props({ type: 'checkbox', checked: computed(() => todo().done), })() ) ));The key function receives the plain item value and returns a unique identifier. This lets map() efficiently update items when the array changes.
Reactive Item Updates
Section titled “Reactive Item Updates”Each item is wrapped in a signal. When you update an item in the source array, the item signal updates too—no element recreation needed:
const toggleTodo = (id: number) => { todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t ));};
// The checkbox updates reactively without recreating the <li>Conditional Rendering with match()
Section titled “Conditional Rendering with match()”match() swaps elements based on a reactive value. When the value changes, the old element is disposed and a new one takes its place.
Add the match module:
import { createMatchModule } from '@rimitive/view/match';
const svc = compose( // ... other modules createMatchModule(adapter));
const { match } = svc;Show/Hide
Section titled “Show/Hide”Toggle visibility by returning null:
const showMessage = signal(true);
const message = match(showMessage, (show) => show ? el('div')('Hello!') : null);
const app = el('div')( el('button').props({ onclick: () => showMessage(!showMessage()) })('Toggle'), message);Switching Views
Section titled “Switching Views”Switch between different element types:
const isEditMode = signal(false);const text = signal('Click edit to change');
const content = match(isEditMode, (editing) => editing ? el('input').props({ value: text })() : el('span')(text));
const app = el('div')( content, el('button').props({ onclick: () => isEditMode(!isEditMode()) })( computed(() => isEditMode() ? 'Save' : 'Edit') ));Multi-Way Switch
Section titled “Multi-Way Switch”Use a discriminated value for multiple branches:
type Tab = 'home' | 'settings' | 'profile';const currentTab = signal<Tab>('home');
const tabContent = match(currentTab, (tab) => { switch (tab) { case 'home': return el('div')('Welcome home'); case 'settings': return el('div')('Settings panel'); case 'profile': return el('div')('Your profile'); }});Portals
Section titled “Portals”portal() renders content into a different DOM location—useful for modals, tooltips, and overlays that need to escape their parent’s overflow or z-index context.
Add the portal module:
import { createPortalModule } from '@rimitive/view/portal';
const svc = compose( // ... other modules createPortalModule(adapter));
const { portal } = svc;Basic Portal
Section titled “Basic Portal”Render to document.body (the default):
const showModal = signal(false);
const modal = match(showModal, (show) => show ? portal()( el('div').props({ className: 'modal-backdrop' })( el('div').props({ className: 'modal' })( el('h2')('Modal Title'), el('p')('Modal content here...'), el('button').props({ onclick: () => showModal(false) })('Close') ) ) ) : null);
const app = el('div')( el('button').props({ onclick: () => showModal(true) })('Open Modal'), modal // Rendered to body, not inside this div);Custom Target
Section titled “Custom Target”Portal to a specific element:
// Portal to a getterportal(() => document.getElementById('tooltip-root'))( el('div').props({ className: 'tooltip' })('Tooltip content'))
// Portal to a signal refconst targetRef = signal<HTMLElement | null>(null);
el('div').ref((el) => { targetRef(el); return () => targetRef(null);})();
portal(targetRef)(tooltipContent)Putting It Together
Section titled “Putting It Together”Here’s a todo list combining map() and match():
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule } from '@rimitive/signals/extend';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { createMapModule } from '@rimitive/view/map';import { createMatchModule } from '@rimitive/view/match';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();const svc = compose( SignalModule, ComputedModule, EffectModule, createElModule(adapter), createMapModule(adapter), createMatchModule(adapter), MountModule);
const { el, map, match, signal, computed, mount } = svc;
// Typestype Todo = { id: number; text: string; done: boolean };type Filter = 'all' | 'active' | 'done';
// Stateconst todos = signal<Todo[]>([]);const filter = signal<Filter>('all');const inputValue = signal('');let nextId = 0;
// Derived stateconst filteredTodos = computed(() => { const f = filter(); const items = todos(); if (f === 'all') return items; return items.filter(t => f === 'done' ? t.done : !t.done);});
const activeCount = computed(() => todos().filter(t => !t.done).length);
// Actionsconst addTodo = () => { const text = inputValue().trim(); if (!text) return; todos([...todos(), { id: nextId++, text, done: false }]); inputValue('');};
const toggleTodo = (id: number) => { todos(todos().map(t => t.id === id ? { ...t, done: !t.done } : t ));};
const removeTodo = (id: number) => { todos(todos().filter(t => t.id !== id));};
// Filter button helperconst FilterButton = (value: Filter, label: string) => el('button').props({ className: computed(() => filter() === value ? 'active' : ''), onclick: () => filter(value), })(label);
// Appconst App = () => el('div').props({ className: 'todo-app' })( el('h1')('Todos'),
// Input el('div').props({ className: 'input-row' })( el('input').props({ type: 'text', placeholder: 'What needs to be done?', value: inputValue, oninput: (e: Event) => inputValue((e.target as HTMLInputElement).value), onkeydown: (e: KeyboardEvent) => { if (e.key === 'Enter') addTodo(); }, })(), el('button').props({ onclick: addTodo })('Add') ),
// Filters el('div').props({ className: 'filters' })( FilterButton('all', 'All'), FilterButton('active', 'Active'), FilterButton('done', 'Done') ),
// List el('ul')( map( filteredTodos, (t) => t.id, (todo) => el('li').props({ className: computed(() => todo().done ? 'done' : '') })( el('input').props({ type: 'checkbox', checked: computed(() => todo().done), onclick: () => toggleTodo(todo().id), })(), el('span')(computed(() => todo().text)), el('button').props({ onclick: () => removeTodo(todo().id) })('×') ) ) ),
// Empty state match(filteredTodos, (items) => items.length === 0 ? el('p').props({ className: 'empty' })('No todos yet') : null ),
// Stats el('div').props({ className: 'stats' })( computed(() => `${activeCount()} items left`) ));
const app = mount(App());document.body.appendChild(app.element!);Lists, conditionals, portals—all reactive, all composable. Next up: Event Handling for more complex event patterns, then Adding Routing for navigation.