@alwatr/action 9.14.0 → 9.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js.map CHANGED
@@ -3,11 +3,11 @@
3
3
  "sources": ["../src/lib.ts", "../src/registry.ts", "../src/method.ts", "../src/delegate.ts"],
4
4
  "sourcesContent": [
5
5
  "import {createLogger} from '@alwatr/logger';\nimport {createChannelSignal} from '@alwatr/signal';\n\n/**\n * Module-scoped logger for `@alwatr/action`.\n * Scoped to `'alwatr-action'` so log lines are easy to filter in the console.\n *\n * @internal\n */\nexport const logger_ = createLogger('alwatr-action');\n\n/**\n * The action channel — a `ChannelSignal` strictly typed by `ActionRecord`.\n *\n * Only action names declared in `ActionRecord` (via declaration merging) are\n * accepted at compile time. Passing an unknown action name to `onAction` or\n * `dispatchAction` is a **compile error** — there is no string fallback.\n *\n * Uses `ChannelSignal` for O(1) routing: dispatching action `'A'` performs a\n * single `Map.get('A')` lookup and invokes only the handlers registered for\n * that specific action — never handlers for `'B'`, `'C'`, etc.\n *\n * @internal — not part of the public API; use `onAction` / `dispatchAction` instead.\n */\nexport const internalChannel_ = createChannelSignal<Record<string, unknown>>({name: 'alwatr-action'});\n",
6
- "// ─── Type Definitions ────────────────────────────────────────────────────────\n\n/**\n * A modifier handler used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. Return `true` (or any truthy value) to allow the\n * action to proceed, or `false` to cancel the dispatch.\n *\n * Using explicit parameters instead of `this` binding makes handlers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A modifier that only allows the action when the element is not disabled\n * const notDisabledHandler: ModifierHandler = (_event, element) => {\n * return !(element as HTMLButtonElement).disabled;\n * };\n * ```\n */\nexport type ModifierHandler = (event: Event, element: HTMLElement) => boolean;\n\n/**\n * A payload resolver used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. The return value becomes the `actionPayload` passed\n * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.\n *\n * Using explicit parameters instead of `this` binding makes resolvers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A resolver that returns the element's dataset id\n * const dataIdResolver: PayloadResolver = (_event, element) => {\n * return (element as HTMLElement).dataset.id ?? null;\n * };\n * ```\n */\nexport type PayloadResolver = (event: Event, element: HTMLElement) => unknown;\n\n// ─── Registries ──────────────────────────────────────────────────────────────\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-action` attribute syntax\n * (e.g. `click.prevent->action-id`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-action` attribute syntax\n * (e.g. `click->action-id:$value`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-action=\"submit.prevent->submit-form\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `.prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\" novalidate>`\n */\nmodifierRegistry.set('validate', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-action=\"input->search-query:$value\" />`\n */\npayloadRegistry.set('$value', (_event, element) => {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-action=\"submit.prevent.validate->submit-form:$formdata\">`\n * ```ts\n * onAction('submit-form', (data) => {\n * console.log(data); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form).entries()) : null;\n});\n",
7
- "import type {Awaitable} from '@alwatr/type-helper';\nimport {internalChannel_, logger_} from './lib.js';\nimport type {SubscribeResult} from '@alwatr/signal';\nimport {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';\nimport type {ActionRecord} from './action-record.js';\n\n// Re-export extension types so consumers can import them from the package root.\nexport type {ModifierHandler, PayloadResolver};\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter\n * is automatically typed to the corresponding `ActionRecord` value — no manual\n * generic annotation needed:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * onAction('add-to-cart', (item) => {\n * cartService.add(item.productId, item.qty); // fully typed, no `!` needed\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'open-drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param handler - Callback invoked with the typed payload on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('page-ready', (pageId) => {\n * router.setPage(pageId); // pageId: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n actionId: K,\n handler: (payload: ActionRecord[K]) => Awaitable<void>,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {actionId});\n return internalChannel_.on(actionId, handler as (payload: unknown) => Awaitable<void>);\n}\n\n/**\n * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.\n *\n * `actionId` must be a key of `ActionRecord`. The `payload` parameter is\n * automatically typed — passing the wrong type is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}\n * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅\n * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error\n * dispatchAction('unknown-action', 'x'); // ❌ compile error\n * ```\n *\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'navigate': string;\n * 'logout': void;\n * }\n * }\n * ```\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-action` HTML attribute with `setupActionDelegation`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param actionPayload - The payload; type is enforced by `ActionRecord`.\n *\n * @example — with payload\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction('page-ready', 'home');\n * dispatchAction('navigate', '/dashboard');\n * ```\n *\n * @example — void payload (no second argument)\n * ```ts\n * dispatchAction('logout');\n * ```\n */\nexport function dispatchAction<K extends keyof ActionRecord>(\n ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]\n): void {\n const [actionId, actionPayload] = args;\n logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});\n internalChannel_.dispatch(actionId, actionPayload);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-action` attribute syntax.\n *\n * A modifier is a dot-chained token placed after the event type\n * (e.g. `click.mymod->action-id`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always\n * available. This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no dots or arrows).\n * @param handler - A `ModifierHandler` receiving `(event, element)`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', () => window.confirm('Are you sure?'));\n * ```\n * ```html\n * <button on-action=\"click.confirm->delete-item:42\">Delete</button>\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-action` attribute syntax.\n *\n * A payload resolver is a colon-suffixed token in the attribute value\n * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time\n * with an `ActionContext` as `this` and the DOM event as the argument.\n * The return value becomes the `actionPayload` passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`) are always available. This function\n * lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - A `PayloadResolver` receiving `(event, element)`.\n *\n * @example — a `$checked` resolver for checkbox state\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$checked', (_event, element) => {\n * return (element as HTMLInputElement).checked;\n * });\n * ```\n * ```html\n * <input type=\"checkbox\" on-action=\"change->toggle-feature:$checked\" />\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n",
8
- "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the **Qwik-inspired global delegation** pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-action`\n * attribute whose event type matches.\n * - Modifiers and payload resolvers run in the same pipeline as before.\n * - `dispatchAction` is called with the resolved payload.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by delete attribute elements after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib.js';\nimport {modifierRegistry, payloadRegistry} from './registry.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-action` attribute value into its three segments.\n *\n * Full syntax: `eventType[.modifier…]->actionId[:payload]`\n *\n * | Capture group | Matches | Example |\n * | ------------- | ------------------------------------------- | -------------------- |\n * | 1 | Event type + optional dot-chained modifiers | `click.prevent.once` |\n * | 2 | Action identifier | `open-drawer` |\n * | 3 | Optional payload token or literal | `main` / `$value` |\n *\n * @example\n * ```\n * 'click.prevent.once->open-drawer:main' → ['click.prevent.once', 'open-drawer', 'main']\n * 'input->search-query:$value' → ['input', 'search-query', '$value']\n * 'submit.prevent->submit-form' → ['submit.prevent', 'submit-form', undefined]\n * ```\n */\nconst syntaxRegex = /^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-action` attribute value.\n *\n * Caching avoids re-parsing the same attribute string on every event fire.\n * The cache is keyed by the raw attribute string so identical values share\n * the same descriptor object.\n */\ninterface ActionDescriptor {\n /** The DOM event type to listen for (e.g. `'click'`, `'input'`). */\n readonly eventType: string;\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or `$`-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * LRU-style cache for parsed `on-action` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-action` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * Using a plain `Map` here is intentional — attribute strings are short-lived\n * keys and the map size is bounded by the number of distinct `on-action` values\n * in the page, which is typically small.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-action` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const [eventType, ...modifierList] = match[1].split('.');\n if (!eventType) {\n logger_.accident('parseDescriptor__', 'missing_event_type', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const modifiers = new Set(modifierList);\n const actionId = match[2];\n const payload: string | undefined = match[3];\n\n const descriptor: ActionDescriptor = {\n eventType,\n modifiers,\n actionId,\n payload,\n };\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\nconst onActionAttrib__ = 'on-action';\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-action` attribute whose event type matches the current event.\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Run each modifier in order; if any returns `false`, abort.\n * 4. Resolve the payload token (literal or `$`-resolver).\n * 5. Call `dispatchAction(actionId, payload)`.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n // Walk up the DOM to find the closest element with a matching on-action attribute.\n // We use `closest` on the composedPath target to support Shadow DOM correctly.\n const target = event.target as Element | null;\n if (!target) return;\n\n // Find the nearest ancestor (or self) that has an on-action attribute\n const actionElement = target.closest?.(`[${onActionAttrib__}^=${eventType}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(onActionAttrib__)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, attributeValue, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) {\n logger_.accident('handleDelegatedEvent__', 'invalid_attribute', {eventType, attributeValue, actionElement});\n return;\n }\n\n if (descriptor.eventType !== eventType) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', descriptor);\n\n // Step 1: handle once modifier\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(onActionAttrib__); // remove on-action to prevent repeat the action\n descriptorCache__.delete(attributeValue); // free memory for once\n }\n\n // Step 2: run modifiers\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled separately\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {modifier, attributeValue});\n return; // unknown modifier — abort to avoid silent misbehaviour\n }\n if (handler(event, actionElement) === false) return;\n }\n\n // Step 3: resolve payload\n let payload: unknown = descriptor.payload;\n if (payload) {\n const resolver = payloadRegistry.get(payload as string);\n if (resolver) payload = resolver(event, actionElement);\n }\n\n // Step 4: dispatch\n internalChannel_.dispatch(descriptor.actionId, payload);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-action` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All `on-action` processing — modifier execution,\n * payload resolution, and `dispatchAction` — happens inside that one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('open-drawer', (panel) => openDrawer(panel));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n"
6
+ "// ─── Type Definitions ────────────────────────────────────────────────────────\n\n/**\n * A modifier handler used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. Return `true` (or any truthy value) to allow the\n * action to proceed, or `false` to cancel the dispatch.\n *\n * Using explicit parameters instead of `this` binding makes handlers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A modifier that only allows the action when the element is not disabled\n * const notDisabledHandler: ModifierHandler = (_event, element) => {\n * return !(element as HTMLButtonElement).disabled;\n * };\n * ```\n */\nexport type ModifierHandler = (event: Event, element: HTMLElement) => boolean;\n\n/**\n * A payload resolver used in `on-action` attribute syntax.\n *\n * Receives the triggering DOM `event` and the `element` that owns the\n * `on-action` attribute. The return value becomes the `actionPayload` passed\n * to `onAction` subscribers. Use this to compute dynamic payloads from DOM state.\n *\n * Using explicit parameters instead of `this` binding makes resolvers\n * compatible with arrow functions and easier to test in isolation.\n *\n * @example\n * ```ts\n * // A resolver that returns the element's dataset id\n * const dataIdResolver: PayloadResolver = (_event, element) => {\n * return (element as HTMLElement).dataset.id ?? null;\n * };\n * ```\n */\nexport type PayloadResolver = (event: Event, element: HTMLElement) => unknown;\n\n// ─── Registries ──────────────────────────────────────────────────────────────\n\n/**\n * Registry of all named modifier handlers.\n *\n * Keys are modifier names used in the `on-<eventType>` attribute syntax\n * (e.g. `on-click=\"action-id; prevent\"`). Values are `ModifierHandler` functions.\n * Populated at module load with built-in modifiers; extended at runtime via\n * `registerModifier`.\n *\n * @internal\n */\nexport const modifierRegistry = new Map<string, ModifierHandler>();\n\n/**\n * Registry of all named payload resolvers.\n *\n * Keys are resolver tokens used in the `on-<eventType>` attribute syntax\n * (e.g. `on-input=\"search_query:$value\"`). Values are `PayloadResolver` functions.\n * Populated at module load with built-in resolvers; extended at runtime via\n * `registerPayloadResolver`.\n *\n * @internal\n */\nexport const payloadRegistry = new Map<string, PayloadResolver>();\n\n// ─── Built-in Modifiers ───────────────────────────────────────────────────────\n\n/**\n * `prevent` — calls `event.preventDefault()` before dispatching.\n *\n * Use it to suppress the browser's default behaviour (e.g. form submission,\n * link navigation, context menu).\n *\n * @example `<form on-submit=\"submit-form; prevent\">`\n */\nmodifierRegistry.set('prevent', (event) => {\n event.preventDefault();\n return true;\n});\n\n/**\n * `validate` — cancels the dispatch if the nearest `<form>` fails validation.\n *\n * Looks for a `<form>` ancestor (or the element itself if it is a form) and\n * calls `checkValidity()`. If the form is invalid the action is not dispatched,\n * allowing native constraint-validation UI to surface errors. If no form is\n * found the dispatch is also cancelled.\n *\n * Pair with `.prevent` on `submit` events to avoid page reloads:\n *\n * @example `<form on-submit=\"submit_form:$formdata; prevent,validate\" novalidate>`\n */\nmodifierRegistry.set('validate', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n if (!form) return false;\n return form.checkValidity();\n});\n\n// ─── Built-in Payload Resolvers ───────────────────────────────────────────────\n\n/**\n * `$value` — resolves to the element's `.value` property at dispatch time.\n *\n * Works with any element that exposes a `value` property: `<input>`,\n * `<textarea>`, `<select>`. Returns `null` for elements without `.value`.\n *\n * @example `<input on-input=\"search_query:$value\" />`\n */\npayloadRegistry.set('$value', (_event, element) => {\n return 'value' in element ? (element as {value: unknown}).value : null;\n});\n\n/**\n * `$formdata` — resolves to a plain object of all fields in the nearest `<form>`.\n *\n * Collects entries via `FormData` and converts them to a `Record<string, FormDataEntryValue>`.\n * Looks for a `<form>` ancestor (or the element itself). Returns `null` when no\n * form is found.\n *\n * @example `<form on-submit=\"submit_form:$formdata; prevent,validate\">`\n * ```ts\n * onAction('submit_form', (data) => {\n * console.log(data); // {username: 'ali', password: '…'}\n * });\n * ```\n */\npayloadRegistry.set('$formdata', (_event, element) => {\n const form = element instanceof HTMLFormElement ? element : element.closest('form');\n return form ? Object.fromEntries(new FormData(form)) : null;\n});\n\n/**\n * `$checked` — resolves to the `.checked` boolean property of a checkbox or radio input.\n *\n * Works with `<input type=\"checkbox\">` and `<input type=\"radio\">`.\n * Returns `null` for elements that do not have a `checked` property.\n *\n * @example `<input type=\"checkbox\" on-change=\"toggle_feature:$checked\" />`\n * ```ts\n * onAction('toggle_feature', (isChecked) => {\n * featureSignal.set(isChecked as boolean);\n * });\n * ```\n */\npayloadRegistry.set('$checked', (_event, element) => {\n return 'checked' in element ? (element as HTMLInputElement).checked : null;\n});\n",
7
+ "import type {Awaitable} from '@alwatr/type-helper';\nimport {internalChannel_, logger_} from './lib.js';\nimport type {SubscribeResult} from '@alwatr/signal';\nimport {modifierRegistry, payloadRegistry, type ModifierHandler, type PayloadResolver} from './registry.js';\nimport type {ActionRecord} from './action-record.js';\n\n// Re-export extension types so consumers can import them from the package root.\nexport type {ModifierHandler, PayloadResolver};\n\n// ─── Core Action API ──────────────────────────────────────────────────────────\n\n/**\n * Subscribes to a named action dispatched anywhere in the application.\n *\n * `actionId` must be a key of `ActionRecord`. The handler's `payload` parameter\n * is automatically typed to the corresponding `ActionRecord` value — no manual\n * generic annotation needed:\n *\n * ```ts\n * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}\n * onAction('add_to_cart', (item) => {\n * cartService.add(item.productId, item.qty); // fully typed, no `!` needed\n * });\n * ```\n *\n * Passing an action name not declared in `ActionRecord` is a **compile error**.\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'open_drawer': string;\n * }\n * }\n * ```\n *\n * Internally delegates to `ChannelSignal.on()` for **O(1) routing** — dispatching\n * action `'A'` never invokes handlers registered for action `'B'`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param handler - Callback invoked with the typed payload on each dispatch.\n * @returns A `SubscribeResult` with an `unsubscribe()` method for cleanup.\n *\n * @example\n * ```ts\n * import {onAction} from '@alwatr/action';\n *\n * const sub = onAction('page_ready', (pageId) => {\n * router.setPage(pageId); // pageId: string — inferred from ActionRecord\n * });\n *\n * sub.unsubscribe(); // stop listening when no longer needed\n * ```\n */\nexport function onAction<K extends keyof ActionRecord>(\n actionId: K,\n handler: (payload: ActionRecord[K]) => Awaitable<void>,\n): SubscribeResult {\n logger_.logMethodArgs?.('onAction', {actionId});\n return internalChannel_.on(actionId, handler as (payload: unknown) => Awaitable<void>);\n}\n\n/**\n * Dispatches a named action to all `onAction` subscribers with a matching `actionId`.\n *\n * `actionId` must be a key of `ActionRecord`. The `payload` parameter is\n * automatically typed — passing the wrong type is a **compile error**:\n *\n * ```ts\n * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}\n * dispatchAction('add_to_cart', {productId: 42, qty: 1}); // ✅\n * dispatchAction('add_to_cart', 'wrong'); // ❌ compile error\n * dispatchAction('unknown_action', 'x'); // ❌ compile error\n * ```\n *\n * Register new actions by extending `ActionRecord` via declaration merging:\n *\n * ```ts\n * // src/action-record.ts\n * declare module '@alwatr/action' {\n * interface ActionRecord {\n * 'navigate': string;\n * 'logout': void;\n * }\n * }\n * ```\n *\n * Use `dispatchAction` when triggering an action from code — e.g. after an\n * async operation, from a service layer, or in tests. For DOM-driven actions,\n * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.\n *\n * @param actionId - A key of `ActionRecord`.\n * @param actionPayload - The payload; type is enforced by `ActionRecord`.\n *\n * @example — with payload\n * ```ts\n * import {dispatchAction} from '@alwatr/action';\n *\n * dispatchAction('page_ready', 'home');\n * dispatchAction('navigate', '/dashboard');\n * ```\n *\n * @example — void payload (no second argument)\n * ```ts\n * dispatchAction('logout');\n * ```\n */\nexport function dispatchAction<K extends keyof ActionRecord>(\n ...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]\n): void {\n const [actionId, actionPayload] = args;\n logger_.logMethodArgs?.('dispatchAction', {actionId, actionPayload});\n internalChannel_.dispatch(actionId, actionPayload);\n}\n\n// ─── Extension API ────────────────────────────────────────────────────────────\n\n/**\n * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.\n *\n * A modifier is a comma-separated token placed after the `;` separator\n * (e.g. `on-click=\"action-id; mymod\"`). Its handler runs before the payload is\n * resolved and the action is dispatched. Returning `false` cancels the dispatch.\n *\n * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always\n * available. This function lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * handler — avoid duplicate registrations in production code.\n *\n * @param name - The modifier token (lowercase, no special characters).\n * @param handler - A `ModifierHandler` receiving `(event, element)`.\n *\n * @example — a `confirm` modifier that shows a browser dialog\n * ```ts\n * import {registerModifier} from '@alwatr/action';\n *\n * registerModifier('confirm', () => window.confirm('Are you sure?'));\n * ```\n * ```html\n * <button on-click=\"delete_item:42; confirm\">Delete</button>\n * ```\n */\nexport function registerModifier(name: string, handler: ModifierHandler): void {\n logger_.logMethodArgs?.('registerModifier', {name});\n if (modifierRegistry.has(name)) {\n logger_.accident('registerModifier', 'modifier_already_registered', {name});\n }\n modifierRegistry.set(name, handler);\n}\n\n/**\n * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.\n *\n * A payload resolver is a colon-prefixed token in the attribute value\n * (e.g. `on-click=\"action-id:$mytoken\"`). Its function is called at dispatch time\n * with an `ActionContext` as `this` and the DOM event as the argument.\n * The return value becomes the `actionPayload` passed to `onAction` subscribers.\n *\n * Built-in resolvers (`$value`, `$formdata`) are always available. This function\n * lets you add domain-specific ones.\n *\n * Registering the same name twice logs an accident and overwrites the previous\n * resolver — avoid duplicate registrations in production code.\n *\n * @param name - The resolver token (should start with `$` by convention).\n * @param resolver - A `PayloadResolver` receiving `(event, element)`.\n *\n * @example — a `$checked` resolver for checkbox state\n * ```ts\n * import {registerPayloadResolver} from '@alwatr/action';\n *\n * registerPayloadResolver('$checked', (_event, element) => {\n * return (element as HTMLInputElement).checked;\n * });\n * ```\n * ```html\n * <input type=\"checkbox\" on-change=\"toggle_feature:$checked\" />\n * ```\n */\nexport function registerPayloadResolver(name: string, resolver: PayloadResolver): void {\n logger_.logMethodArgs?.('registerPayloadResolver', {name});\n if (payloadRegistry.has(name)) {\n logger_.accident('registerPayloadResolver', 'payload_resolver_already_registered', {name});\n }\n payloadRegistry.set(name, resolver);\n}\n",
8
+ "/**\n * @file delegate.ts\n *\n * Global Event Delegation engine for `@alwatr/action`.\n *\n * ## Why delegation instead of per-element listeners?\n *\n * The classic directive approach attaches one `addEventListener` per element.\n * With 100 buttons on a page, that means 100 listener registrations at boot\n * time — O(N) initialization cost, O(N) memory for listener references, and\n * zero support for elements added after bootstrap.\n *\n * This module implements the global delegation pattern:\n * - A single listener per event type is attached to `document.body` with\n * `capture: true` (so it fires even for non-bubbling events).\n * - When an event fires, the handler walks up the DOM from `event.target`\n * using `closest()` to find the nearest element with an `on-<eventType>`\n * attribute (e.g. `on-click`, `on-submit`).\n * - Modifiers and payload resolvers run in the same pipeline as before.\n * - `dispatchAction` is called with the resolved payload.\n *\n * ## Complexity\n *\n * | Metric | Per-element listeners | Global delegation |\n * | --------------- | --------------------- | ----------------- |\n * | Boot time | O(N elements) | O(1) — 1 loop |\n * | Memory | O(N listeners) | O(1) — 1 handler |\n * | Dynamic content | Requires re-bootstrap | Works out-of-box |\n * | `once` modifier | Native option | Manual tracking |\n *\n * ## Trade-offs vs. the directive approach\n *\n * - `passive` is not supported as a per-element option (all delegated listeners\n * are non-passive so that `prevent` can call `preventDefault()`).\n * - `stop` stops further bubbling but the delegation handler has already\n * captured the event at `body` level — it does not prevent other delegation\n * handlers from running on the same element.\n * - `once` is emulated by removing the attribute after first fire.\n */\n\nimport {internalChannel_, logger_} from './lib.js';\nimport {modifierRegistry, payloadRegistry} from './registry.js';\n\n// ─── Syntax Parser ────────────────────────────────────────────────────────────\n\n/**\n * Parses the `on-<eventType>` attribute value into its segments.\n *\n * Syntax: `actionId[:payload][; modifier1,modifier2,…]`\n *\n * The event type is encoded in the **attribute name** itself (`on-click`,\n * `on-submit`, etc.) rather than inside the value. This makes the HTML more\n * readable and aligns with native event attribute conventions.\n *\n * | Capture group | Matches | Example |\n * | ------------- | -------------------------------------- | ---------------------- |\n * | 1 | Action identifier | `open_drawer` |\n * | 2 | Optional payload token or literal | `main_menu` / `$value` |\n * | 3 | Optional comma-separated modifier list | `prevent,validate` |\n *\n * @example\n * ```\n * 'close_drawer'\n * → actionId='close_drawer', payload=undefined, modifiers={}\n *\n * 'open_drawer:main_menu'\n * → actionId='open_drawer', payload='main_menu', modifiers={}\n *\n * 'my_submit_handler:$formdata; prevent,validate'\n * → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}\n * ```\n */\nconst syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\\s*([a-z0-9_,-]+))?$/;\n\n// ─── Parsed Action Descriptor ─────────────────────────────────────────────────\n\n/**\n * Parsed and cached representation of a single `on-<eventType>` attribute value.\n *\n * Does not store `eventType` — the caller always has it from `event.type`,\n * and the attribute name already encodes it (e.g. `on-click`), so storing it\n * here would be redundant. This also keeps the cache key simple: just the raw\n * attribute value string, with no composite key needed.\n */\ninterface ActionDescriptor {\n /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */\n readonly modifiers: ReadonlySet<string>;\n /** The action identifier dispatched to `onAction` subscribers. */\n readonly actionId: string;\n /** Raw payload token from the attribute (literal string or $-resolver key). */\n readonly payload: string | undefined;\n}\n\n/**\n * Cache for parsed `on-<eventType>` attribute values.\n *\n * Attribute strings are typically repeated across many elements (e.g. every\n * \"add to cart\" button shares the same `on-click` value). Caching the parsed\n * descriptor avoids redundant regex work on every event.\n *\n * The cache key is the raw attribute value string. No composite key with\n * event type is needed because the attribute name already encodes the event\n * type — `on-click=\"open_drawer\"` and `on-submit=\"open_drawer\"` are two\n * separate attributes with the same value string, but they are read from\n * different attribute names and never collide in this cache.\n *\n * @internal\n */\nconst descriptorCache__ = new Map<string, ActionDescriptor | null>();\n\n/**\n * Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.\n *\n * Returns `null` when the syntax is invalid. Results are cached by the raw\n * attribute value string so repeated calls for the same value are O(1).\n *\n * @internal\n */\nfunction parseDescriptor__(attributeValue: string): ActionDescriptor | null {\n logger_.logMethodArgs?.('parseDescriptor__', {attributeValue});\n\n const cached = descriptorCache__.get(attributeValue);\n // Explicit `undefined` check: `null` means \"already parsed and invalid\".\n if (cached !== undefined) return cached;\n\n const match = attributeValue.match(syntaxRegex);\n if (!match) {\n logger_.accident('parseDescriptor__', 'invalid_syntax', {attributeValue});\n descriptorCache__.set(attributeValue, null);\n return null;\n }\n\n const actionId = match[1];\n const payload: string | undefined = match[2];\n // match[3] is the raw modifier list string, e.g. \"prevent,validate\"\n const modifierString = match[3];\n const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();\n const descriptor: ActionDescriptor = {modifiers, actionId, payload};\n\n descriptorCache__.set(attributeValue, descriptor);\n return descriptor;\n}\n\n// ─── Core Delegation Handler ──────────────────────────────────────────────────\n\n/**\n * Central event handler attached to `document.body` for every delegated event type.\n *\n * Execution flow for each incoming event:\n * 1. Walk up from `event.target` to find the nearest element with an\n * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).\n * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.\n * 3. Run each modifier in order; if any returns `false`, abort.\n * 4. Resolve the payload token (literal or $-resolver).\n * 5. Call `dispatchAction(actionId, payload)`.\n *\n * @internal\n */\nfunction handleDelegatedEvent__(event: Event): void {\n const eventType = event.type;\n logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});\n\n const target = event.target as Element | null;\n if (!target) return;\n\n // Attribute name encodes the event type: on-click, on-submit, etc.\n const actionAttrib = `on-${eventType}`;\n\n // Walk up the DOM to find the closest element with the matching on-<eventType> attribute.\n const actionElement = target.closest?.(`[${actionAttrib}]`);\n if (!actionElement) return;\n\n const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();\n if (!attributeValue) {\n logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});\n return;\n }\n\n if (!(actionElement instanceof HTMLElement)) {\n logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});\n return;\n }\n\n const descriptor = parseDescriptor__(attributeValue);\n if (!descriptor) return;\n\n logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});\n\n // Step 1: handle once modifier — remove attribute before running other modifiers\n // so that even if a modifier aborts, the element will not fire again.\n if (descriptor.modifiers.has('once')) {\n actionElement.removeAttribute(actionAttrib);\n }\n\n // Step 2: run modifiers\n for (const modifier of descriptor.modifiers) {\n if (modifier === 'once') continue; // handled above\n const handler = modifierRegistry.get(modifier);\n if (!handler) {\n logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});\n return; // unknown modifier — abort to avoid silent misbehavior\n }\n if (handler(event, actionElement) === false) return;\n }\n\n // Step 3: resolve payload\n let payload: unknown = descriptor.payload;\n if (payload) {\n const resolver = payloadRegistry.get(payload as string);\n if (resolver) payload = resolver(event, actionElement);\n }\n\n // Step 4: dispatch\n internalChannel_.dispatch(descriptor.actionId, payload);\n}\n\n// ─── Setup ────────────────────────────────────────────────────────────────────\n\n/**\n * The set of event types currently delegated to `document.body`.\n *\n * Tracked so that `setupActionDelegation` is idempotent — calling it multiple\n * times with the same event types does not register duplicate listeners.\n *\n * @internal\n */\nconst delegatedEventTypes__ = new Set<string>();\n\n/**\n * Default DOM event types that cover the vast majority of interactive elements.\n *\n * - `click` — buttons, links, checkboxes, custom interactive elements\n * - `submit` — form submission\n * - `input` — live text input, range sliders\n * - `change` — select boxes, checkboxes, radio buttons (fires on commit)\n *\n * Pass additional types to `setupActionDelegation` when your app uses other\n * events (e.g. `'keydown'`, `'pointerup'`).\n */\nexport const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];\n\n/**\n * Registers global event delegation for `on-<eventType>` attributes.\n *\n * Attaches a single `capture`-phase listener on `document.body` for each\n * event type in `eventTypes`. All processing — modifier execution, payload\n * resolution, and `dispatchAction` — happens inside that one handler.\n *\n * **Call this once at application bootstrap**, before any user interaction.\n * Subsequent calls with the same event types are no-ops (idempotent).\n *\n * ### Why `capture: true`?\n *\n * Capture-phase listeners fire before bubble-phase listeners and also catch\n * events that do not bubble (e.g. `focus`, `blur`). This ensures the delegation\n * handler always runs, even when a child element calls `stopPropagation()`.\n *\n * ### Dynamic content\n *\n * Because the listener lives on `document.body`, any element added to the DOM\n * after this call — via `innerHTML`, `lit-html`, a framework renderer, or\n * server-sent HTML — is automatically covered. No re-bootstrap is needed.\n *\n * @param eventTypes - Event types to delegate. Defaults to `DEFAULT_DELEGATED_EVENTS`.\n *\n * @example — minimal bootstrap\n * ```ts\n * import {setupActionDelegation, onAction} from '@alwatr/action';\n *\n * // One call activates the entire page.\n * setupActionDelegation();\n *\n * onAction('open_drawer', (panel) => openDrawer(panel));\n * ```\n *\n * @example — with extra event types\n * ```ts\n * import {setupActionDelegation, DEFAULT_DELEGATED_EVENTS} from '@alwatr/action';\n *\n * setupActionDelegation([...DEFAULT_DELEGATED_EVENTS, 'keydown', 'pointerup']);\n * ```\n */\nexport function setupActionDelegation(eventTypes: readonly string[] = DEFAULT_DELEGATED_EVENTS): void {\n logger_.logMethodArgs?.('setupActionDelegation', {eventTypes});\n\n for (const eventType of eventTypes) {\n if (delegatedEventTypes__.has(eventType)) continue; // already registered — skip\n delegatedEventTypes__.add(eventType);\n // capture: true — fires before bubble-phase listeners and catches non-bubbling events.\n document.body.addEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n}\n\n/**\n * Removes all global delegation listeners registered by `setupActionDelegation`.\n *\n * Useful in test environments where each test needs a clean slate, or in\n * micro-frontend setups where a sub-app is unmounted.\n *\n * After calling this, `setupActionDelegation` can be called again to re-register.\n */\nexport function teardownActionDelegation(): void {\n logger_.logMethod?.('teardownActionDelegation');\n for (const eventType of delegatedEventTypes__) {\n document.body.removeEventListener(eventType, handleDelegatedEvent__, {capture: true});\n }\n delegatedEventTypes__.clear();\n descriptorCache__.clear();\n}\n"
9
9
  ],
10
- "mappings": ";AAAA,uBAAQ,uBACR,8BAAQ,uBAQD,IAAM,EAAU,EAAa,eAAe,EAetC,EAAmB,EAA6C,CAAC,KAAM,eAAe,CAAC,EC8B7F,IAAM,EAAmB,IAAI,IAYvB,EAAkB,IAAI,IAYnC,EAAiB,IAAI,UAAW,CAAC,IAAU,CAEzC,OADA,EAAM,eAAe,EACd,GACR,EAcD,EAAiB,IAAI,WAAY,CAAC,EAAQ,IAAY,CACpD,IAAM,EAAO,aAAmB,gBAAkB,EAAU,EAAQ,QAAQ,MAAM,EAClF,GAAI,CAAC,EAAM,MAAO,GAClB,OAAO,EAAK,cAAc,EAC3B,EAYD,EAAgB,IAAI,SAAU,CAAC,EAAQ,IAAY,CACjD,MAAO,UAAW,EAAW,EAA6B,MAAQ,KACnE,EAgBD,EAAgB,IAAI,YAAa,CAAC,EAAQ,IAAY,CACpD,IAAM,EAAO,aAAmB,gBAAkB,EAAU,EAAQ,QAAQ,MAAM,EAClF,OAAO,EAAO,OAAO,YAAY,IAAI,SAAS,CAAI,EAAE,QAAQ,CAAC,EAAI,KAClE,EC7EM,SAAS,CAAsC,CACpD,EACA,EACiB,CAEjB,OADA,EAAQ,gBAAgB,WAAY,CAAC,UAAQ,CAAC,EACvC,EAAiB,GAAG,EAAU,CAAgD,EAgDhF,SAAS,CAA4C,IACvD,EACG,CACN,IAAO,EAAU,GAAiB,EAClC,EAAQ,gBAAgB,iBAAkB,CAAC,WAAU,eAAa,CAAC,EACnE,EAAiB,SAAS,EAAU,CAAa,EA+B5C,SAAS,CAAgB,CAAC,EAAc,EAAgC,CAE7E,GADA,EAAQ,gBAAgB,mBAAoB,CAAC,MAAI,CAAC,EAC9C,EAAiB,IAAI,CAAI,EAC3B,EAAQ,SAAS,mBAAoB,8BAA+B,CAAC,MAAI,CAAC,EAE5E,EAAiB,IAAI,EAAM,CAAO,EAgC7B,SAAS,CAAuB,CAAC,EAAc,EAAiC,CAErF,GADA,EAAQ,gBAAgB,0BAA2B,CAAC,MAAI,CAAC,EACrD,EAAgB,IAAI,CAAI,EAC1B,EAAQ,SAAS,0BAA2B,sCAAuC,CAAC,MAAI,CAAC,EAE3F,EAAgB,IAAI,EAAM,CAAQ,EC3HpC,IAAM,EAAc,0CAmCd,EAAoB,IAAI,IAU9B,SAAS,CAAiB,CAAC,EAAiD,CAC1E,EAAQ,gBAAgB,oBAAqB,CAAC,gBAAc,CAAC,EAE7D,IAAM,EAAS,EAAkB,IAAI,CAAc,EAEnD,GAAI,IAAW,OAAW,OAAO,EAEjC,IAAM,EAAQ,EAAe,MAAM,CAAW,EAC9C,GAAI,CAAC,EAGH,OAFA,EAAQ,SAAS,oBAAqB,iBAAkB,CAAC,gBAAc,CAAC,EACxE,EAAkB,IAAI,EAAgB,IAAI,EACnC,KAGT,IAAO,KAAc,GAAgB,EAAM,GAAG,MAAM,GAAG,EACvD,GAAI,CAAC,EAGH,OAFA,EAAQ,SAAS,oBAAqB,qBAAsB,CAAC,gBAAc,CAAC,EAC5E,EAAkB,IAAI,EAAgB,IAAI,EACnC,KAGT,IAAM,EAAY,IAAI,IAAI,CAAY,EAChC,EAAW,EAAM,GACjB,EAA8B,EAAM,GAEpC,EAA+B,CACnC,YACA,YACA,WACA,SACF,EAGA,OADA,EAAkB,IAAI,EAAgB,CAAU,EACzC,EAKT,IAAM,EAAmB,YAczB,SAAS,CAAsB,CAAC,EAAoB,CAClD,IAAM,EAAY,EAAM,KACxB,EAAQ,gBAAgB,yBAA0B,CAAC,WAAS,CAAC,EAI7D,IAAM,EAAS,EAAM,OACrB,GAAI,CAAC,EAAQ,OAGb,IAAM,EAAgB,EAAO,UAAU,IAAI,MAAqB,IAAY,EAC5E,GAAI,CAAC,EAAe,OAEpB,IAAM,EAAiB,EAAc,eAAe,CAAgB,GAAG,KAAK,EAC5E,GAAI,CAAC,EAAgB,CACnB,EAAQ,SAAS,yBAA0B,kBAAmB,CAAC,YAAW,iBAAgB,eAAa,CAAC,EACxG,OAGF,GAAI,EAAE,aAAyB,aAAc,CAC3C,EAAQ,SAAS,yBAA0B,0BAA2B,CAAC,YAAW,iBAAgB,eAAa,CAAC,EAChH,OAGF,IAAM,EAAa,EAAkB,CAAc,EACnD,GAAI,CAAC,EAAY,CACf,EAAQ,SAAS,yBAA0B,oBAAqB,CAAC,YAAW,iBAAgB,eAAa,CAAC,EAC1G,OAGF,GAAI,EAAW,YAAc,EAAW,OAKxC,GAHA,EAAQ,gBAAgB,gCAAiC,CAAU,EAG/D,EAAW,UAAU,IAAI,MAAM,EACjC,EAAc,gBAAgB,CAAgB,EAC9C,EAAkB,OAAO,CAAc,EAIzC,QAAW,KAAY,EAAW,UAAW,CAC3C,GAAI,IAAa,OAAQ,SACzB,IAAM,EAAU,EAAiB,IAAI,CAAQ,EAC7C,GAAI,CAAC,EAAS,CACZ,EAAQ,SAAS,yBAA0B,mBAAoB,CAAC,WAAU,gBAAc,CAAC,EACzF,OAEF,GAAI,EAAQ,EAAO,CAAa,IAAM,GAAO,OAI/C,IAAI,EAAmB,EAAW,QAClC,GAAI,EAAS,CACX,IAAM,EAAW,EAAgB,IAAI,CAAiB,EACtD,GAAI,EAAU,EAAU,EAAS,EAAO,CAAa,EAIvD,EAAiB,SAAS,EAAW,SAAU,CAAO,EAaxD,IAAM,EAAwB,IAAI,IAarB,EAA8C,CAAC,QAAS,SAAU,QAAS,QAAQ,EA2CzF,SAAS,CAAqB,CAAC,EAAgC,EAAgC,CACpG,EAAQ,gBAAgB,wBAAyB,CAAC,YAAU,CAAC,EAE7D,QAAW,KAAa,EAAY,CAClC,GAAI,EAAsB,IAAI,CAAS,EAAG,SAC1C,EAAsB,IAAI,CAAS,EAEnC,SAAS,KAAK,iBAAiB,EAAW,EAAwB,CAAC,QAAS,EAAI,CAAC,GAY9E,SAAS,CAAwB,EAAS,CAC/C,EAAQ,YAAY,0BAA0B,EAC9C,QAAW,KAAa,EACtB,SAAS,KAAK,oBAAoB,EAAW,EAAwB,CAAC,QAAS,EAAI,CAAC,EAEtF,EAAsB,MAAM,EAC5B,EAAkB,MAAM",
11
- "debugId": "7E5E853AFB39285764756E2164756E21",
10
+ "mappings": "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",
11
+ "debugId": "C5F4214484C292C364756E2164756E21",
12
12
  "names": []
13
13
  }
package/dist/method.d.ts CHANGED
@@ -11,8 +11,8 @@ export type { ModifierHandler, PayloadResolver };
11
11
  * generic annotation needed:
12
12
  *
13
13
  * ```ts
14
- * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
15
- * onAction('add-to-cart', (item) => {
14
+ * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
15
+ * onAction('add_to_cart', (item) => {
16
16
  * cartService.add(item.productId, item.qty); // fully typed, no `!` needed
17
17
  * });
18
18
  * ```
@@ -24,7 +24,7 @@ export type { ModifierHandler, PayloadResolver };
24
24
  * // src/action-record.ts
25
25
  * declare module '@alwatr/action' {
26
26
  * interface ActionRecord {
27
- * 'open-drawer': string;
27
+ * 'open_drawer': string;
28
28
  * }
29
29
  * }
30
30
  * ```
@@ -40,7 +40,7 @@ export type { ModifierHandler, PayloadResolver };
40
40
  * ```ts
41
41
  * import {onAction} from '@alwatr/action';
42
42
  *
43
- * const sub = onAction('page-ready', (pageId) => {
43
+ * const sub = onAction('page_ready', (pageId) => {
44
44
  * router.setPage(pageId); // pageId: string — inferred from ActionRecord
45
45
  * });
46
46
  *
@@ -55,10 +55,10 @@ export declare function onAction<K extends keyof ActionRecord>(actionId: K, hand
55
55
  * automatically typed — passing the wrong type is a **compile error**:
56
56
  *
57
57
  * ```ts
58
- * // ActionRecord declares: 'add-to-cart': {productId: number; qty: number}
59
- * dispatchAction('add-to-cart', {productId: 42, qty: 1}); // ✅
60
- * dispatchAction('add-to-cart', 'wrong'); // ❌ compile error
61
- * dispatchAction('unknown-action', 'x'); // ❌ compile error
58
+ * // ActionRecord declares: 'add_to_cart': {productId: number; qty: number}
59
+ * dispatchAction('add_to_cart', {productId: 42, qty: 1}); // ✅
60
+ * dispatchAction('add_to_cart', 'wrong'); // ❌ compile error
61
+ * dispatchAction('unknown_action', 'x'); // ❌ compile error
62
62
  * ```
63
63
  *
64
64
  * Register new actions by extending `ActionRecord` via declaration merging:
@@ -75,7 +75,7 @@ export declare function onAction<K extends keyof ActionRecord>(actionId: K, hand
75
75
  *
76
76
  * Use `dispatchAction` when triggering an action from code — e.g. after an
77
77
  * async operation, from a service layer, or in tests. For DOM-driven actions,
78
- * use the `on-action` HTML attribute with `setupActionDelegation`.
78
+ * use the `on-<eventType>` HTML attribute with `setupActionDelegation`.
79
79
  *
80
80
  * @param actionId - A key of `ActionRecord`.
81
81
  * @param actionPayload - The payload; type is enforced by `ActionRecord`.
@@ -84,7 +84,7 @@ export declare function onAction<K extends keyof ActionRecord>(actionId: K, hand
84
84
  * ```ts
85
85
  * import {dispatchAction} from '@alwatr/action';
86
86
  *
87
- * dispatchAction('page-ready', 'home');
87
+ * dispatchAction('page_ready', 'home');
88
88
  * dispatchAction('navigate', '/dashboard');
89
89
  * ```
90
90
  *
@@ -95,10 +95,10 @@ export declare function onAction<K extends keyof ActionRecord>(actionId: K, hand
95
95
  */
96
96
  export declare function dispatchAction<K extends keyof ActionRecord>(...args: ActionRecord[K] extends void | undefined ? [actionId: K] : [actionId: K, actionPayload: ActionRecord[K]]): void;
97
97
  /**
98
- * Registers a custom modifier that can be used in `on-action` attribute syntax.
98
+ * Registers a custom modifier that can be used in `on-<eventType>` attribute syntax.
99
99
  *
100
- * A modifier is a dot-chained token placed after the event type
101
- * (e.g. `click.mymod->action-id`). Its handler runs before the payload is
100
+ * A modifier is a comma-separated token placed after the `;` separator
101
+ * (e.g. `on-click="action-id; mymod"`). Its handler runs before the payload is
102
102
  * resolved and the action is dispatched. Returning `false` cancels the dispatch.
103
103
  *
104
104
  * Built-in modifiers (`prevent`, `stop`, `validate`, `once`) are always
@@ -107,7 +107,7 @@ export declare function dispatchAction<K extends keyof ActionRecord>(...args: Ac
107
107
  * Registering the same name twice logs an accident and overwrites the previous
108
108
  * handler — avoid duplicate registrations in production code.
109
109
  *
110
- * @param name - The modifier token (lowercase, no dots or arrows).
110
+ * @param name - The modifier token (lowercase, no special characters).
111
111
  * @param handler - A `ModifierHandler` receiving `(event, element)`.
112
112
  *
113
113
  * @example — a `confirm` modifier that shows a browser dialog
@@ -117,15 +117,15 @@ export declare function dispatchAction<K extends keyof ActionRecord>(...args: Ac
117
117
  * registerModifier('confirm', () => window.confirm('Are you sure?'));
118
118
  * ```
119
119
  * ```html
120
- * <button on-action="click.confirm->delete-item:42">Delete</button>
120
+ * <button on-click="delete_item:42; confirm">Delete</button>
121
121
  * ```
122
122
  */
123
123
  export declare function registerModifier(name: string, handler: ModifierHandler): void;
124
124
  /**
125
- * Registers a custom payload resolver that can be used in `on-action` attribute syntax.
125
+ * Registers a custom payload resolver that can be used in `on-<eventType>` attribute syntax.
126
126
  *
127
- * A payload resolver is a colon-suffixed token in the attribute value
128
- * (e.g. `click->action-id:$mytoken`). Its function is called at dispatch time
127
+ * A payload resolver is a colon-prefixed token in the attribute value
128
+ * (e.g. `on-click="action-id:$mytoken"`). Its function is called at dispatch time
129
129
  * with an `ActionContext` as `this` and the DOM event as the argument.
130
130
  * The return value becomes the `actionPayload` passed to `onAction` subscribers.
131
131
  *
@@ -147,7 +147,7 @@ export declare function registerModifier(name: string, handler: ModifierHandler)
147
147
  * });
148
148
  * ```
149
149
  * ```html
150
- * <input type="checkbox" on-action="change->toggle-feature:$checked" />
150
+ * <input type="checkbox" on-change="toggle_feature:$checked" />
151
151
  * ```
152
152
  */
153
153
  export declare function registerPayloadResolver(name: string, resolver: PayloadResolver): void;
@@ -39,8 +39,8 @@ export type PayloadResolver = (event: Event, element: HTMLElement) => unknown;
39
39
  /**
40
40
  * Registry of all named modifier handlers.
41
41
  *
42
- * Keys are modifier names used in the `on-action` attribute syntax
43
- * (e.g. `click.prevent->action-id`). Values are `ModifierHandler` functions.
42
+ * Keys are modifier names used in the `on-<eventType>` attribute syntax
43
+ * (e.g. `on-click="action-id; prevent"`). Values are `ModifierHandler` functions.
44
44
  * Populated at module load with built-in modifiers; extended at runtime via
45
45
  * `registerModifier`.
46
46
  *
@@ -50,8 +50,8 @@ export declare const modifierRegistry: Map<string, ModifierHandler>;
50
50
  /**
51
51
  * Registry of all named payload resolvers.
52
52
  *
53
- * Keys are resolver tokens used in the `on-action` attribute syntax
54
- * (e.g. `click->action-id:$value`). Values are `PayloadResolver` functions.
53
+ * Keys are resolver tokens used in the `on-<eventType>` attribute syntax
54
+ * (e.g. `on-input="search_query:$value"`). Values are `PayloadResolver` functions.
55
55
  * Populated at module load with built-in resolvers; extended at runtime via
56
56
  * `registerPayloadResolver`.
57
57
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alwatr/action",
3
- "version": "9.14.0",
3
+ "version": "9.16.0",
4
4
  "description": "Declarative DOM action-dispatch — bridge HTML attributes to typed signal handlers.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
@@ -21,12 +21,12 @@
21
21
  },
22
22
  "sideEffects": false,
23
23
  "dependencies": {
24
- "@alwatr/logger": "9.14.0",
25
- "@alwatr/signal": "9.14.0"
24
+ "@alwatr/logger": "9.16.0",
25
+ "@alwatr/signal": "9.16.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@alwatr/nano-build": "9.14.0",
29
- "@alwatr/standard": "9.14.0",
29
+ "@alwatr/standard": "9.16.0",
30
30
  "@alwatr/type-helper": "9.14.0",
31
31
  "typescript": "^6.0.3"
32
32
  },
@@ -79,5 +79,5 @@
79
79
  "vanilla-js",
80
80
  "web-development"
81
81
  ],
82
- "gitHead": "4e499b23191d4460ea60f34cde8a99b472741f1a"
82
+ "gitHead": "c210044f6e8ab444ec2f9e600f095761cbd279bd"
83
83
  }
@@ -13,8 +13,8 @@
13
13
  * // In your package: src/action-record.ts
14
14
  * declare module '@alwatr/action' {
15
15
  * interface ActionRecord {
16
- * 'open-drawer': string;
17
- * 'add-to-cart': {productId: number; qty: number};
16
+ * 'open_drawer': string;
17
+ * 'add_to_cart': {productId: number; qty: number};
18
18
  * 'logout': void;
19
19
  * }
20
20
  * }
@@ -43,8 +43,8 @@
43
43
  * // pkg/my-feature/src/action-record.ts
44
44
  * declare module '@alwatr/action' {
45
45
  * interface ActionRecord {
46
- * 'open-drawer': string;
47
- * 'add-to-cart': {productId: number; qty: number};
46
+ * 'open_drawer': string;
47
+ * 'add_to_cart': {productId: number; qty: number};
48
48
  * 'logout': void;
49
49
  * }
50
50
  * }
package/src/delegate.ts CHANGED
@@ -10,12 +10,12 @@
10
10
  * time — O(N) initialization cost, O(N) memory for listener references, and
11
11
  * zero support for elements added after bootstrap.
12
12
  *
13
- * This module implements the **Qwik-inspired global delegation** pattern:
13
+ * This module implements the global delegation pattern:
14
14
  * - A single listener per event type is attached to `document.body` with
15
15
  * `capture: true` (so it fires even for non-bubbling events).
16
16
  * - When an event fires, the handler walks up the DOM from `event.target`
17
- * using `closest()` to find the nearest element with an `on-action`
18
- * attribute whose event type matches.
17
+ * using `closest()` to find the nearest element with an `on-<eventType>`
18
+ * attribute (e.g. `on-click`, `on-submit`).
19
19
  * - Modifiers and payload resolvers run in the same pipeline as before.
20
20
  * - `dispatchAction` is called with the resolved payload.
21
21
  *
@@ -35,7 +35,7 @@
35
35
  * - `stop` stops further bubbling but the delegation handler has already
36
36
  * captured the event at `body` level — it does not prevent other delegation
37
37
  * handlers from running on the same element.
38
- * - `once` is emulated by delete attribute elements after first fire.
38
+ * - `once` is emulated by removing the attribute after first fire.
39
39
  */
40
40
 
41
41
  import {internalChannel_, logger_} from './lib.js';
@@ -44,65 +44,75 @@ import {modifierRegistry, payloadRegistry} from './registry.js';
44
44
  // ─── Syntax Parser ────────────────────────────────────────────────────────────
45
45
 
46
46
  /**
47
- * Parses the `on-action` attribute value into its three segments.
47
+ * Parses the `on-<eventType>` attribute value into its segments.
48
48
  *
49
- * Full syntax: `eventType[.modifier…]->actionId[:payload]`
49
+ * Syntax: `actionId[:payload][; modifier1,modifier2,…]`
50
50
  *
51
- * | Capture group | Matches | Example |
52
- * | ------------- | ------------------------------------------- | -------------------- |
53
- * | 1 | Event type + optional dot-chained modifiers | `click.prevent.once` |
54
- * | 2 | Action identifier | `open-drawer` |
55
- * | 3 | Optional payload token or literal | `main` / `$value` |
51
+ * The event type is encoded in the **attribute name** itself (`on-click`,
52
+ * `on-submit`, etc.) rather than inside the value. This makes the HTML more
53
+ * readable and aligns with native event attribute conventions.
54
+ *
55
+ * | Capture group | Matches | Example |
56
+ * | ------------- | -------------------------------------- | ---------------------- |
57
+ * | 1 | Action identifier | `open_drawer` |
58
+ * | 2 | Optional payload token or literal | `main_menu` / `$value` |
59
+ * | 3 | Optional comma-separated modifier list | `prevent,validate` |
56
60
  *
57
61
  * @example
58
62
  * ```
59
- * 'click.prevent.once->open-drawer:main' → ['click.prevent.once', 'open-drawer', 'main']
60
- * 'input->search-query:$value' ['input', 'search-query', '$value']
61
- * 'submit.prevent->submit-form' → ['submit.prevent', 'submit-form', undefined]
63
+ * 'close_drawer'
64
+ * actionId='close_drawer', payload=undefined, modifiers={}
65
+ *
66
+ * 'open_drawer:main_menu'
67
+ * → actionId='open_drawer', payload='main_menu', modifiers={}
68
+ *
69
+ * 'my_submit_handler:$formdata; prevent,validate'
70
+ * → actionId='my_submit_handler', payload='$formdata', modifiers={'prevent','validate'}
62
71
  * ```
63
72
  */
64
- const syntaxRegex = /^([a-z0-9.-]+)->([a-z0-9-]+)(?::(.+))?$/;
73
+ const syntaxRegex = /^([a-z0-9_-]+)(?::([^;]+))?(?:;\s*([a-z0-9_,-]+))?$/;
65
74
 
66
75
  // ─── Parsed Action Descriptor ─────────────────────────────────────────────────
67
76
 
68
77
  /**
69
- * Parsed and cached representation of a single `on-action` attribute value.
78
+ * Parsed and cached representation of a single `on-<eventType>` attribute value.
70
79
  *
71
- * Caching avoids re-parsing the same attribute string on every event fire.
72
- * The cache is keyed by the raw attribute string so identical values share
73
- * the same descriptor object.
80
+ * Does not store `eventType` — the caller always has it from `event.type`,
81
+ * and the attribute name already encodes it (e.g. `on-click`), so storing it
82
+ * here would be redundant. This also keeps the cache key simple: just the raw
83
+ * attribute value string, with no composite key needed.
74
84
  */
75
85
  interface ActionDescriptor {
76
- /** The DOM event type to listen for (e.g. `'click'`, `'input'`). */
77
- readonly eventType: string;
78
86
  /** Set of active modifier names (e.g. `{'prevent', 'once'}`). */
79
87
  readonly modifiers: ReadonlySet<string>;
80
88
  /** The action identifier dispatched to `onAction` subscribers. */
81
89
  readonly actionId: string;
82
- /** Raw payload token from the attribute (literal string or `$`-resolver key). */
90
+ /** Raw payload token from the attribute (literal string or $-resolver key). */
83
91
  readonly payload: string | undefined;
84
92
  }
85
93
 
86
94
  /**
87
- * LRU-style cache for parsed `on-action` attribute values.
95
+ * Cache for parsed `on-<eventType>` attribute values.
88
96
  *
89
97
  * Attribute strings are typically repeated across many elements (e.g. every
90
- * "add to cart" button shares the same `on-action` value). Caching the parsed
98
+ * "add to cart" button shares the same `on-click` value). Caching the parsed
91
99
  * descriptor avoids redundant regex work on every event.
92
100
  *
93
- * Using a plain `Map` here is intentional attribute strings are short-lived
94
- * keys and the map size is bounded by the number of distinct `on-action` values
95
- * in the page, which is typically small.
101
+ * The cache key is the raw attribute value string. No composite key with
102
+ * event type is needed because the attribute name already encodes the event
103
+ * type `on-click="open_drawer"` and `on-submit="open_drawer"` are two
104
+ * separate attributes with the same value string, but they are read from
105
+ * different attribute names and never collide in this cache.
96
106
  *
97
107
  * @internal
98
108
  */
99
109
  const descriptorCache__ = new Map<string, ActionDescriptor | null>();
100
110
 
101
111
  /**
102
- * Parses an `on-action` attribute value into an `ActionDescriptor`.
112
+ * Parses an `on-<eventType>` attribute value into an `ActionDescriptor`.
103
113
  *
104
114
  * Returns `null` when the syntax is invalid. Results are cached by the raw
105
- * attribute string so repeated calls for the same value are O(1).
115
+ * attribute value string so repeated calls for the same value are O(1).
106
116
  *
107
117
  * @internal
108
118
  */
@@ -120,23 +130,12 @@ function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
120
130
  return null;
121
131
  }
122
132
 
123
- const [eventType, ...modifierList] = match[1].split('.');
124
- if (!eventType) {
125
- logger_.accident('parseDescriptor__', 'missing_event_type', {attributeValue});
126
- descriptorCache__.set(attributeValue, null);
127
- return null;
128
- }
129
-
130
- const modifiers = new Set(modifierList);
131
- const actionId = match[2];
132
- const payload: string | undefined = match[3];
133
-
134
- const descriptor: ActionDescriptor = {
135
- eventType,
136
- modifiers,
137
- actionId,
138
- payload,
139
- };
133
+ const actionId = match[1];
134
+ const payload: string | undefined = match[2];
135
+ // match[3] is the raw modifier list string, e.g. "prevent,validate"
136
+ const modifierString = match[3];
137
+ const modifiers: Set<string> = modifierString ? new Set(modifierString.split(',').filter(Boolean)) : new Set();
138
+ const descriptor: ActionDescriptor = {modifiers, actionId, payload};
140
139
 
141
140
  descriptorCache__.set(attributeValue, descriptor);
142
141
  return descriptor;
@@ -144,16 +143,15 @@ function parseDescriptor__(attributeValue: string): ActionDescriptor | null {
144
143
 
145
144
  // ─── Core Delegation Handler ──────────────────────────────────────────────────
146
145
 
147
- const onActionAttrib__ = 'on-action';
148
146
  /**
149
147
  * Central event handler attached to `document.body` for every delegated event type.
150
148
  *
151
149
  * Execution flow for each incoming event:
152
150
  * 1. Walk up from `event.target` to find the nearest element with an
153
- * `on-action` attribute whose event type matches the current event.
151
+ * `on-<eventType>` attribute (e.g. `on-click`, `on-submit`).
154
152
  * 2. Parse (or retrieve from cache) the `ActionDescriptor` for that attribute.
155
153
  * 3. Run each modifier in order; if any returns `false`, abort.
156
- * 4. Resolve the payload token (literal or `$`-resolver).
154
+ * 4. Resolve the payload token (literal or $-resolver).
157
155
  * 5. Call `dispatchAction(actionId, payload)`.
158
156
  *
159
157
  * @internal
@@ -162,49 +160,45 @@ function handleDelegatedEvent__(event: Event): void {
162
160
  const eventType = event.type;
163
161
  logger_.logMethodArgs?.('handleDelegatedEvent__', {eventType});
164
162
 
165
- // Walk up the DOM to find the closest element with a matching on-action attribute.
166
- // We use `closest` on the composedPath target to support Shadow DOM correctly.
167
163
  const target = event.target as Element | null;
168
164
  if (!target) return;
169
165
 
170
- // Find the nearest ancestor (or self) that has an on-action attribute
171
- const actionElement = target.closest?.(`[${onActionAttrib__}^=${eventType}]`);
166
+ // Attribute name encodes the event type: on-click, on-submit, etc.
167
+ const actionAttrib = `on-${eventType}`;
168
+
169
+ // Walk up the DOM to find the closest element with the matching on-<eventType> attribute.
170
+ const actionElement = target.closest?.(`[${actionAttrib}]`);
172
171
  if (!actionElement) return;
173
172
 
174
- const attributeValue = actionElement.getAttribute?.(onActionAttrib__)?.trim();
173
+ const attributeValue = actionElement.getAttribute?.(actionAttrib)?.trim();
175
174
  if (!attributeValue) {
176
- logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, attributeValue, actionElement});
175
+ logger_.accident('handleDelegatedEvent__', 'empty_attribute', {eventType, actionElement});
177
176
  return;
178
177
  }
179
178
 
180
179
  if (!(actionElement instanceof HTMLElement)) {
181
- logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, attributeValue, actionElement});
180
+ logger_.accident('handleDelegatedEvent__', 'target_not_html_element', {eventType, actionElement});
182
181
  return;
183
182
  }
184
183
 
185
184
  const descriptor = parseDescriptor__(attributeValue);
186
- if (!descriptor) {
187
- logger_.accident('handleDelegatedEvent__', 'invalid_attribute', {eventType, attributeValue, actionElement});
188
- return;
189
- }
190
-
191
- if (descriptor.eventType !== eventType) return;
185
+ if (!descriptor) return;
192
186
 
193
- logger_.logMethodArgs?.('handleDelegatedEvent__.action', descriptor);
187
+ logger_.logMethodArgs?.('handleDelegatedEvent__.action', {eventType, descriptor});
194
188
 
195
- // Step 1: handle once modifier
189
+ // Step 1: handle once modifier — remove attribute before running other modifiers
190
+ // so that even if a modifier aborts, the element will not fire again.
196
191
  if (descriptor.modifiers.has('once')) {
197
- actionElement.removeAttribute(onActionAttrib__); // remove on-action to prevent repeat the action
198
- descriptorCache__.delete(attributeValue); // free memory for once
192
+ actionElement.removeAttribute(actionAttrib);
199
193
  }
200
194
 
201
195
  // Step 2: run modifiers
202
196
  for (const modifier of descriptor.modifiers) {
203
- if (modifier === 'once') continue; // handled separately
197
+ if (modifier === 'once') continue; // handled above
204
198
  const handler = modifierRegistry.get(modifier);
205
199
  if (!handler) {
206
- logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {modifier, attributeValue});
207
- return; // unknown modifier — abort to avoid silent misbehaviour
200
+ logger_.accident('handleDelegatedEvent__', 'unknown_modifier', {eventType, modifier, attributeValue, descriptor});
201
+ return; // unknown modifier — abort to avoid silent misbehavior
208
202
  }
209
203
  if (handler(event, actionElement) === false) return;
210
204
  }
@@ -246,11 +240,11 @@ const delegatedEventTypes__ = new Set<string>();
246
240
  export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', 'input', 'change'];
247
241
 
248
242
  /**
249
- * Registers global event delegation for `on-action` attributes.
243
+ * Registers global event delegation for `on-<eventType>` attributes.
250
244
  *
251
245
  * Attaches a single `capture`-phase listener on `document.body` for each
252
- * event type in `eventTypes`. All `on-action` processing — modifier execution,
253
- * payload resolution, and `dispatchAction` — happens inside that one handler.
246
+ * event type in `eventTypes`. All processing — modifier execution, payload
247
+ * resolution, and `dispatchAction` — happens inside that one handler.
254
248
  *
255
249
  * **Call this once at application bootstrap**, before any user interaction.
256
250
  * Subsequent calls with the same event types are no-ops (idempotent).
@@ -276,7 +270,7 @@ export const DEFAULT_DELEGATED_EVENTS: readonly string[] = ['click', 'submit', '
276
270
  * // One call activates the entire page.
277
271
  * setupActionDelegation();
278
272
  *
279
- * onAction('open-drawer', (panel) => openDrawer(panel));
273
+ * onAction('open_drawer', (panel) => openDrawer(panel));
280
274
  * ```
281
275
  *
282
276
  * @example — with extra event types