Event Handling
You’ve seen onclick in props. That works for simple cases. For more complex event handling—multiple listeners, automatic cleanup, batched updates—use on().
Basic Props Events
Section titled “Basic Props Events”For simple click handlers, props work fine:
const count = signal(0);
el('button').props({ onclick: () => count(count() + 1)})('Click me')This attaches the handler directly to the element. Simple and direct.
The on() Helper
Section titled “The on() Helper”on() provides automatic cleanup and batching. When multiple signals update in a handler, batching ensures only one re-render.
Add the on module:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule, BatchModule } from '@rimitive/signals/extend';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { OnModule } from '@rimitive/view/deps/addEventListener';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();
const svc = compose( SignalModule, ComputedModule, EffectModule, BatchModule, createElModule(adapter), OnModule, MountModule);
const { el, on, signal, mount } = svc;Using on() with .ref()
Section titled “Using on() with .ref()”on() returns a lifecycle callback for use with .ref():
const count = signal(0);
const button = el('button').ref( on('click', () => count(count() + 1)))('Click me');When the element is removed from the DOM, the listener is automatically cleaned up.
Multiple Listeners
Section titled “Multiple Listeners”Stack multiple on() calls:
const input = el('input') .ref( on('focus', () => console.log('focused')), on('blur', () => console.log('blurred')), on('input', (e) => handleInput(e)) )();Each listener is independently attached and cleaned up.
Input Handling
Section titled “Input Handling”A common pattern—controlled inputs with on():
const text = signal('');
const input = el('input') .props({ type: 'text', value: text, }) .ref( on('input', (e) => text((e.target as HTMLInputElement).value)) )();The value prop binds the input’s value to the signal. The on('input') handler updates the signal when the user types.
Form Submission
Section titled “Form Submission”Handle Enter key and form submission:
const searchQuery = signal('');
const handleSubmit = () => { console.log('Searching for:', searchQuery()); // perform search...};
const searchInput = el('input') .props({ type: 'text', placeholder: 'Search...', value: searchQuery, }) .ref( on('input', (e) => searchQuery((e.target as HTMLInputElement).value)), on('keydown', (e) => { if (e.key === 'Enter') handleSubmit(); }) )();
const searchButton = el('button').ref( on('click', handleSubmit))('Search');Automatic Batching
Section titled “Automatic Batching”When a handler updates multiple signals, on() batches them into a single update:
const firstName = signal('');const lastName = signal('');const loading = signal(false);
const handleSubmit = () => { // All three updates batched into one render firstName(''); lastName(''); loading(true);};
el('button').ref( on('click', handleSubmit))('Submit')Without batching, this would trigger three separate re-renders. With on(), it’s one.
Why Batching Matters
Section titled “Why Batching Matters”Consider a form with derived state:
const firstName = signal('John');const lastName = signal('Doe');const fullName = computed(() => `${firstName()} ${lastName()}`);
// Display updates once, not twiceel('button').ref( on('click', () => { firstName('Jane'); lastName('Smith'); }))('Change Name')The fullName computed only recalculates once after both signals update.
Event Options
Section titled “Event Options”Pass standard addEventListener options as the third argument:
// Capture phaseon('click', handler, { capture: true })
// Once onlyon('click', handler, { once: true })
// Passive (for scroll performance)on('scroll', handler, { passive: true })When to Use What
Section titled “When to Use What”Use props (onclick, oninput, etc.) when:
- Simple, single handler
- No need for cleanup management
- Handler updates one signal
Use on() when:
- Multiple listeners on one element
- Handler updates multiple signals (batching)
- Need explicit cleanup
- Using event options (capture, passive, once)
Both are valid. Props are simpler; on() is more powerful.
A Complete Example
Section titled “A Complete Example”A search form with debounced input:
import { compose } from '@rimitive/core';import { SignalModule, ComputedModule, EffectModule, BatchModule } from '@rimitive/signals/extend';import { createDOMAdapter } from '@rimitive/view/adapters/dom';import { createElModule } from '@rimitive/view/el';import { createMatchModule } from '@rimitive/view/match';import { OnModule } from '@rimitive/view/deps/addEventListener';import { MountModule } from '@rimitive/view/deps/mount';
const adapter = createDOMAdapter();const svc = compose( SignalModule, ComputedModule, EffectModule, BatchModule, createElModule(adapter), createMatchModule(adapter), OnModule, MountModule);
const { el, match, on, signal, computed, effect, mount } = svc;
// Stateconst query = signal('');const results = signal<string[]>([]);const loading = signal(false);
// Debounced searchlet debounceTimer: number;const debouncedSearch = (q: string) => { clearTimeout(debounceTimer); if (!q.trim()) { results([]); return; }
loading(true); debounceTimer = setTimeout(() => { // Simulate API call const mockResults = ['Apple', 'Banana', 'Cherry', 'Date'] .filter(item => item.toLowerCase().includes(q.toLowerCase()));
// Batched update results(mockResults); loading(false); }, 300);};
// Watch query changeseffect(() => { debouncedSearch(query());});
// Appconst App = () => el('div').props({ className: 'search-app' })( el('h1')('Search'),
// Search input el('input') .props({ type: 'text', placeholder: 'Type to search...', value: query, }) .ref( on('input', (e) => query((e.target as HTMLInputElement).value)), on('keydown', (e) => { if (e.key === 'Escape') query(''); }) )(),
// Loading indicator match(loading, (isLoading) => isLoading ? el('div').props({ className: 'loading' })('Searching...') : null ),
// Results el('ul')( ...[] // Would use map() here for dynamic results ),
// Result count el('div').props({ className: 'count' })( computed(() => { const r = results(); return r.length === 0 ? 'No results' : `${r.length} result${r.length === 1 ? '' : 's'}`; }) ));
const app = mount(App());document.body.appendChild(app.element!);Event handling, batching, cleanup—all handled. Next: Loading Data for async data fetching.