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

text
on-<eventType>="actionId[:payload][; modifier1,modifier2,…]"
html
<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.

ts
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

ModifierEffect
preventCalls event.preventDefault() before dispatching.
stopCalls event.stopPropagation().
validateRuns the nearest <form>'s checkValidity() first; dispatch is skipped if it fails.
onceRemoves the attribute after the first successful dispatch.
html
<button on-click="ui_track_impression:hero; once">Learn More</button>

Payload resolvers

ResolverResolves to
$valueThe triggering element's .value.
$formdataThe nearest ancestor <form>, serialized to a plain object.
$checkedThe checkbox/radio's .checked boolean.
$datasetAll 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.

html
<section action_context="volume">
  <input type="range" on-input="ui_slider_change:$value" />
</section>
ts
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:

ts
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

MethodPurpose
.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.

Tip

Naming convention: prefix UI-originated action types with ui_ and leave programmatic ones unprefixed — see Architecture & UDF.