Documentation
Actions & Events
Declarative, O(1) global event delegation with a fully typed action bus.
@alwatr/action bridges declarative HTML attributes to a typed, O(1) action bus (a ChannelSignal under the hood). One capture-phase listener per event type lives on document.body — boot cost and memory are constant regardless of element count, and elements inserted after boot work immediately, with no re-registration.
Attribute syntax
on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"<button on-click="ui_open_drawer:menu">Menu</button>
<input on-input="ui_search_query:$value" />
<form on-submit="ui_submit_form:$formdata; prevent,validate" novalidate>
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>Setup
Call once, at bootstrap. ActionService.DEFAULT_DELEGATED_EVENTS covers click, submit, input, and change — pass your own array to delegate additional event types.
import {actionService, ActionService} from '@alwatr/flux';
declare module '@alwatr/flux' {
interface ActionRecord {
ui_add_to_cart: {productId: number; qty: number};
}
}
actionService.on('ui_add_to_cart', (action) => {
cartService.add(action.payload.productId, action.payload.qty);
});
actionService.setupDelegation(ActionService.DEFAULT_DELEGATED_EVENTS);
actionService.dispatch({type: 'ui_add_to_cart', payload: {productId: 42, qty: 1}});Modifiers
| Modifier | Effect |
|---|---|
prevent | Calls event.preventDefault() before dispatching. |
stop | Calls event.stopPropagation(). |
validate | Runs the nearest <form>'s checkValidity() first; dispatch is skipped if it fails. |
once | Removes the attribute after the first successful dispatch. |
<button on-click="ui_track_impression:hero; once">Learn More</button>Payload resolvers
| Resolver | Resolves to |
|---|---|
$value | The triggering element's .value. |
$formdata | The nearest ancestor <form>, serialized to a plain object. |
$checked | The checkbox/radio's .checked boolean. |
$dataset | All of the element's data-* attributes, as an object. |
action_context
Wrap a region in [action_context] to scope the same action type to different UI instances without duplicating handler logic — the ancestor's value lands on action.context.
<section action_context="volume">
<input type="range" on-input="ui_slider_change:$value" />
</section>actionService.on('ui_slider_change', (action) => {
console.log(action.context); // 'volume'
console.log(action.payload); // the range input's string value
});Extending the bus
Register your own modifiers and payload resolvers when the built-ins aren't enough:
actionService.registerModifier('trace', (_event, _element, action) => {
action.meta ??= {};
action.meta['traceId'] = crypto.randomUUID();
return true; // return false to abort dispatch
});
actionService.registerPayloadResolver('$data-id', (_event, element) => element.dataset.id);The ActionService API
| Method | Purpose |
|---|---|
.on(type | type[], handler, options?) | Subscribe. Returns a SubscribeResult with .unsubscribe(). |
.dispatch(action) | Dispatch a full AFSA action object programmatically. |
.setupDelegation(eventTypes?) | Attach the delegated body-level listeners. Defaults to ['click', 'submit', 'input', 'change']. |
.teardownDelegation() | Remove the delegated listeners. |
.registerModifier(name, handler) | Add a custom modifier. |
.registerPayloadResolver(name, resolver) | Add a custom payload resolver. |
actionService is a pre-instantiated singleton — the class ActionService can also be instantiated directly for isolated buses (tests, micro-frontends, or a Shadow DOM island with its own event scope).
Migration notes
Older code may still use onAction and dispatchAction — thin wrappers kept for backward compatibility that delegate to the singleton. Their signatures changed from positional (id, payload) arguments to a single typed Action object; prefer actionService.on() / actionService.dispatch() directly in new code.
Naming convention: prefix UI-originated action types with ui_ and leave programmatic ones unprefixed — see Architecture & UDF.