Skip to content

EventAction

type: class
since: v2.7.0

A problem with event handling

When working on my UI library (Which I didn’t publish yet), I always put customization in mind, so users can easily build on top of it. But that left me with a big event handling challenge, specially when working on a complex component like AutoComplete.

To solve it:

  • I first created a trigger component, which solved the problem but introduced a couple of new problems. Because I had to add a lot of triggers and multiple triggers to the same component where some of them needed to only stop propagation so parents don’t get the event. so for a toggle button component I had to add a click trigger and a pointerdown trigger to stop propagation and that is not good for performance as I had to do this for a lot of components.

  • I also thought of adding a one listener on the parent, but that will not be customizable, and I will have to check for multiple targets (like toggle button, input, etc) for each event.

  • Finally I came up with the idea of using EventAction and it solved all my problems in a simple and customizable way. Now I only add one listener on the parent (don’t matter which event triggered) and add actions to the matched target for that event. The parent instantiate EventAction, add the actions to it, then register the event names to listen to, and that’s it for event handling (very easy to implement, maintain, and more importantly customizable and performant).


An EventAction is simply a way of describing what to do when an event is triggered on a registered parent element through the html. This is done by declaring actions and switches through html attributes for that event and register the parent to listen to that event, and define what happens when that action is triggered after verifying the switches —if any—, then let EventAction do the rest.

A very simple example:

<!-- if parent is registered to listen to click events through EventAction it will log 'btn clicked' and prevent default when button is clicked -->
<button data-click="#log:btn clicked && #prevent">Click me</button>

You can think of the previous example as this: if this button clicked log 'btn clicked' and prevent default. where #log and #prevent are actions, btn clicked is an action param, and && is used to chain actions.

Usage

<script type="module">
import { EventAction } from '@mustib/utils';
new EventAction({
actions: {
'action-name'(data) {
console.log('action-name triggered with param', data.actionParam)
},
},
switches: {
'switch-name': {
handler: (data) => Math.random() > 0.5,
dynamic: true
},
}
}).addListeners(document.body, ['click']);
</script>
<button data-click="switch-name? action-name:1">Btn 1</button>
<button data-click="switch-name? action-name:2">Btn 2</button>

Roles of actions

  • Each action must have a predefined name, an optional param, and possibly some switches.
  • Each switch must have a predefined name and an optional param.
  • Actions are executed in the order they are declared.
  • Switches on the same action are executed in the order they are declared.
  • Switches are like if statements, can be used to conditionally execute actions.
  • If any switch is false, then other switches are not executed, and we move to the next action (if any).
  • Switches belong to an action, can only be used on that action, but they can be repeated on different actions to do different things if they are true.
  • Switches can be repeated for the same action with the same param or different params.
  • Each action should be responsible for only one thing, and you can use action params to pass data to the action.
  • If || precedes the action name (has or) it means to stop executing the rest of the actions for the current event if that action gets executed
  • When using || there must be no spaces between it and the action name

How to write actions

Actions are defined through a normal html attribute value which can be one of two things:

  1. A Json Array.
  2. A String.

Action String Syntax

switch1:switch1Param? switch2:switch2Param? ||action:actionParam &&
switch3:switch3Param? ||action:actionParam
  • Actions are separated by && and Switches must end with ? after the param (if any) to indicate that it is a switch not an action.
  • Use : to separate the action name from the param.
  • Use : to separate the switch name from the param.
  • You can add as many spaces as you want before and after the :, ?, &&.

Action Array Syntax

  1. An array of action strings.

    [
    "switch1:switch1Param? switch2:switch2Param? ||action:actionParam",
    "switch3:switch3Param? ||action:actionParam"
    ]

    Which follows the same rules as the action string syntax except that the actions are not separated by && but they are an array of actions.

  2. An array of action arrays.

    [
    ["||action", "actionParam", "switch1:switch1Param", "switch2:switch2Param"],
    ["||action", "actionParam", ["switch3", "switch3Param"]]
    ]

    This is useful if you want to pass action params or switch params other than a string.

    Each array of actions must have at least the action name as the first index, and an optional param as the second index, then the rest is switches.

    Each switch can be a string which can has : to separate the switch name from the param, or an array which has the switch name as the first index and an optional param as the second index.

  3. An array of action strings and action arrays.

    [
    "switch1:switch1Param? switch2:switch2Param? ||action:actionParam",
    ["||action", "actionParam", ["switch3", "switch3Param"]]
    ]

Roles of naming actions

  • Action names must be unique.
  • They should not start with # as that is reserved for default actions.
  • They should not contain ’?’ or ’&&’ or ’:’
  • Its recommended to name them in kebab-case, but you can use any naming convention as long as it doesn’t break how-to-write-actions.

Performance

  • EventAction is designed to be performant, you only add one listener on the parent.
  • EventAction Will memoize parsed action strings and action arrays after the first time it is parsed, so it doesn’t have to do that every time.
  • EventAction will memoize the switch result through the current event cycle, if it is not dynamic and the switch param does not changed, so even if you are using the same switch multiple times for the same action or different actions, it will only execute the switch once.

Default Actions

  • #prevent calls event.preventDefault().
  • #stop calls event.stopPropagation().
  • #debug logs data about the current event, action, and switches to the console.
  • #log is passed a param it will be logged otherwise it will log info about the current event, action, and switches.
  • #nothing does nothing, you can thing of this as a return now and do nothing, or stopPropagation but unlike stopPropagation it allows other listeners to run.

Default Switches

  • #key runs for KeyboardEvents and expects a param of the keys to check if any of them are pressed.

  • #special-key runs for KeyboardEvents and expects a param of special keys to check if any of them are pressed.

    • special keys are ctrl alt shift meta

Workflow

  1. Start by creating a new EventAction instance.

    const eventAction = new EventAction()
  2. Add your custom actions and switches (you can do that in the constructor too)

    eventAction.registerActions({
    'action-name': (data) => {console.log('dispatched action param', data.actionParam)},
    });
    eventAction.registerSwitches({
    'switch-name': {
    handler: (data) => {return Math.random() > Number(data.switchParam) ?? 0.5},
    dynamic: true,
    }
    })
  3. Register the parent element to listen to events.

    eventAction.addListeners(document.body, ['pointerdown', 'pointerup', 'click'])
  4. Register your html elements as event action targets

    <button
    data-click="action-name:btn-click"
    data-pointerdown="action-name:btn-pointerdown"
    data-pointerup="action-name:btn-pointerup"
    >
    Click Me
    </button>
    <button
    data-click="switch-name? action-name:btn-click-switch"
    data-pointerdown="switch-name:.2? action-name:btn-pointerdown-switch with param .2"
    data-pointerup="switch-name? action-name:btn-pointerup-switch"
    >
    Click Me With Switch
    </button>
  5. Now whenever a one of registered event fires on the body, EventAction will get the closest element that has the right attribute that belongs to that event name, and starts parsing the action string for the first time then memoizes the result and then calls the respective actions and switches.

  6. If things doesn’t work as intended, See the troubleshooting section

This approach offers a clean and efficient way to handle multiple targets with different behaviors, as you can define what should happen for each target and on which event directly on the target itself. This way, you can keep your code concise, maintainable, and easy to understand, without having to repeat the same logic for multiple listeners.

Definition

export class EventAction<T = GenerateData> {
constructor(options?: ConstructorOptions<T>) { }
}
T is the generic type passed to the EventAction<T> on instantiation to improve intellisense for the action handler and generateDataHandler methods.
See CustomAction for more details about these two methods.

See ConstructorOptions for all available options

getMatchedTarget static

Finds the first matched element from the event.composedPath() that is contained in the event.currentTarget and matches the given attribute.

Definition

function getMatchedTarget(data: {
event: Event,
attributeName: string,
}): HTMLElement | undefined

Parameters

  1. data
    type data = {
    attributeName: string,
    event: Event
    };
    • data.attributeName
      • The name of the attribute to match (without brackets).
    • data.event
      • The Event whose composedPath and currentTarget are used for the search.

Returns

type T = HTMLElement | undefined;

getMatchedTargetPierce static

This function is similar to getMatchedTarget, but instead it allows a custom currTargetSelector to be specified in the data object, which will be passed to closestPierce function under the hood to determine if the target matches the attributeName is contained in the closest element that matches currTargetSelector.

Definition

function getMatchedTargetPierce(data: {
event: Event,
attributeName: string,
currTargetSelector: string
}): HTMLElement | undefined

Returns

type T = HTMLElement | undefined;

getEventAttributeName static

This function is used to get the event attribute name for an event.

Definition

function getEventAttributeName(eventName: string): string

Parameters

  1. eventName
    type eventName = string;
    • The name of the event to get the attribute name for.

Returns

type T = string;
  • The event name prefixed with data-. For example, if eventName is click, the function will return data-click.

parseActionName static

This method parses the action name string to detect if it has ||

Definition

function parseActionName(name: string): {
name: string,
hasOr: boolean
}

Parameters

  1. name
    type name = string;
    • The action name string to parse.

Returns

type T = {
name: string,
hasOr: boolean
};
  • name is the action name without the || (if any)
  • hasOr is a boolean indicating whether the action name starts with ||

parseActionString static

this method parses the action string part like that switch1:param1? switch2:param2? ||action:param and returns the parsed action object as shown below.

{
name: 'action',
param: 'param',
hasOr: true,
switches: [
{name: 'switch1', param: 'param1'},
{name: 'switch2', param: 'param2'},
]
}

Definition

function parseActionString(actionString: string): ParsedAction

Parameters

  1. actionString
    type actionString = string;

]}

Returns

type T = ParsedAction;

addListeners

A quick way to add multiple event action listeners to an element.

Definition

function addListeners(
element: HTMLElement,
eventsNames: (string)[]
): this

Parameters

  1. element
    type element = HTMLElement;
    • The element to register the event listeners on.
  2. eventNames
    type eventNames = string[];
    • An array of event names to register listeners for element

removeListeners

A quick way to remove multiple event action listeners from an element.

Definition

function removeListeners(
element: HTMLElement,
eventsNames: (string)[]
): this

Parameters

  1. element
    type element = HTMLElement;
    • The element to remove event listeners from.
  2. eventNames
    type eventNames = string[];
    • An array of event names to remove their listeners from element

registerSwitches

Adds switches to the event action instance

Definition

function registerSwitches(switches: Record<
string,
SwitchHandlerOrCustomSwitch
>): this

Parameters

  1. switches
    type switches = Record<string, SwitchHandlerOrCustomSwitch>;
    • An object where the keys are switch names and the values are either a switch handler or a custom switch object.
    • See SwitchHandlerOrCustomSwitch for more details.

registerActions

Adds actions to the event action.

Definition

function registerActions(
actions: Record<string, ActionHandlerOrCustomAction<T>>,
options?: {
generateDataHandler?: (data: GenerateData) => T
}
): this

Parameters

  1. actions
    type actions = Record<string, ActionHandlerOrCustomAction<T>>;
    • An object where the keys are action names and the values are either an action handler or a custom action object.
      See ActionHandlerOrCustomAction for more details.
  2. options
    type options = { generateDataHandler?: (data: GenerateData) => T };
    • options.generateDataHandler A default generateDataHandler for all provided actions instead of adding it to each action

parseActionsString

Parses an attribute string and returns a parsed actions array.

Definition

function parseActionsString(actionString: string): ParsedAction[]

Parameters

  1. actionString
    type actionString = string;

Returns

type T = ParsedAction[];

_getMatchedTarget protected

Used to get the matched target for an event. Decides whether to use the static getMatchedTarget or the static getMatchedTargetPierce function based on the currTargetSelector property of the instance or the custom getMatchedTarget handler passed to the constructor options.

Definition

function _getMatchedTarget(data: {
attributeName: string,
event: Event
}): HTMLElement | undefined

Parameters

  1. data
    type data = { attributeName: string, event: Event };
    • An object containing the attributeName and the event.

executeParsedActions

Executes the actions and switches from the parsed actions array

Definition

function executeParsedActions(data: {
parsedActions: ParsedAction[],
matchedTarget: HTMLElement,
event: Event,
eventName: string
}): { executedActions: ParsedAction[] }

Parameters

  1. data
    type data = {
    parsedActions: ParsedAction[],
    matchedTarget:HTMLElement,
    event: Event,
    eventName: string
    };
    • An object containing the parsed actions, the matched target, the event and the event name.

Returns

type T = {
executedActions: ParsedAction[]
};

listener

The event listener handler

Definition

function listener(e: Event): undefined | {
matchedTarget: HTMLElement,
attributeName: string,
parsedActions: ParsedAction[],
executedActions: ParsedAction[]
}

Returns

type T = undefined | {
matchedTarget: HTMLElement,
attributeName: string,
parsedActions: ParsedAction[],
executedActions: ParsedAction[]
};
  • undefined is returned if there is no matchedTarget or attributeName has no value
  • The returned object can be useful for debugging purposes, and can also be used when the listener is not the actual event listener handler if that is needed.

Types

CustomAction

type CustomAction<T> = {
handler(data: T): void
generateDataHandler?(data: GenerateData): T
override?: boolean,
overridable?: boolean
}
T is the generic type passed to the EventAction<T> on instantiation to improve intellisense for the action handler and generateDataHandler methods.
See CustomAction for more details about these two methods.
  • handler The Action handler.
    It receives a GenerateData object as a parameter if generateDataHandler is not provided otherwise it receives the return value of generateDataHandler.
  • generateDataHandler A function that will be called to generate the data for the action handler.
    It receives a GenerateData object as a parameter, and it's return value will be passed as the first parameter to the action handler.
  • override A boolean that is required when overriding existing action and it is overridable.
  • overridable A boolean that indicates if the action can be overridden.

ActionHandlerOrCustomAction

A custom action or a custom action handler. See CustomAction

type ActionHandlerOrCustomAction<T> = CustomAction<T>['handler'] | CustomAction<T>
T is the generic type passed to the EventAction<T> on instantiation to improve intellisense for the action handler and generateDataHandler methods.
See CustomAction for more details about these two methods.

GenerateData

export type GenerateData<CurrTarget = HTMLElement> = {
readonly event: Omit<Event, 'currentTarget'> & { currentTarget: CurrTarget },
matchedTarget: HTMLElement,
eventName: string,
actionParam: unknown,
readonly _parsedAction: ParsedAction
}
  • event The current event
  • matchedTarget The target of the event returned by _getMatchedTarget
  • eventName The name of the event
  • actionParam The parameter passed to the action
  • _parsedAction The parsed action object

ParsedAction

export type ParsedAction = {
name: string,
param: unknown,
hasOr: boolean,
switches: {
name: string,
param: unknown
}[]
}
  • name The name of the action
  • param The parameter passed to the action
  • hasOr A boolean that indicates if the action has or
  • switches An array of parsed switches object with `name` and `param`

RegisteredActionData

type RegisteredActionData<T> = {
handler: (data: T) => void
generateDataHandler: ((data: GenerateData) => T) | undefined
overridable: boolean
}
T is the generic type passed to the EventAction<T> on instantiation to improve intellisense for the action handler and generateDataHandler methods.
See CustomAction for more details about these two methods.

See CustomAction for properties details.

SwitchHandlerData

type SwitchHandlerData = GenerateData & {
switchParam: unknown,
actionName: string
}
  • switchParam The parameter passed to the switch.
  • actionName The name of the current action that is being executed.
  • See GenerateData for other properties.

CustomSwitch

export type CustomSwitch = {
handler: (data: SwitchHandlerData) => boolean
override?: boolean
overridable?: boolean,
dynamic?: boolean
}
  • handler The switch handler.
    Receives a SwitchHandlerData object as a parameter and return a boolean indicates if the action should be executed
  • override A boolean that is required when overriding existing switch and it is overridable.
  • overridable A boolean that indicates if the switch can be overridden.
  • dynamic A boolean that indicates if the switch is dynamic.
    When true, the returned switch handler value will not be cashed through the current event actions

SwitchHandlerOrCustomSwitch

A custom switch or a custom switch handler. See CustomSwitch

type SwitchHandlerOrCustomSwitch = CustomSwitch['handler'] | CustomSwitch

RegisteredSwitchData

type RegisteredSwitchData = {
handler: (data: SwitchHandlerData) => boolean
overridable: boolean,
dynamic: boolean
}

See CustomSwitch for properties details.

ConstructorOptions

type ConstructorOptions<T> = {
actions?: Record<string, ActionHandlerOrCustomAction<T>>,
switches?: Record<string, SwitchHandlerOrCustomSwitch>,
generateDataHandler?: (data: GenerateData) => T,
currTargetSelector?: string
getEventAttributeName?: typeof EventAction.getEventAttributeName
getMatchedTarget?: typeof EventAction.getMatchedTarget
}
T is the generic type passed to the EventAction<T> on instantiation to improve intellisense for the action handler and generateDataHandler methods.
See CustomAction for more details about these two methods.
  • actions Add actions at initialization.
    See registerActions for more details.
  • switches Add switches at initialization.
    See registerSwitches for more details.
  • generateDataHandler A function that return the custom data which will be passed to actions handlers.
    See generateDataHandler on CustomAction for more details.
  • currTargetSelector The selector for the current target.
    If exists and getMatchedTarget is not provided, getMatchedTargetPierce will be used instead of getMatchedTarget which is required for working on shadow dom elements.
  • getEventAttributeName A custom function to get the event attribute name.
    This allows the same target element to have multiple actions for the same event but for different EventAction instances or just to override the default attribute name.
    See getEventAttributeName for more details.
  • getMatchedTarget A custom function to get the matched target.
    See getMatchedTarget and getMatchedTargetPierce for more details.

troubleshooting

If things doesn’t work as expected, you can try the following:

  • Check the browser console as EventAction will warn for any issues like unregistered actions or switches.

  • Check that you are adding EventAction listener to the correct element or forgot to do so.

  • Make sure to add currentTargetSelector to the constructor options if you are working on shadow dom elements.

  • Make sure the target element has the right attribute name that belongs to that event specially when using getEventAttributeName option in the constructor

  • Make sure that switches are returning true when the action should be executed.

  • Try removing all of the actions and switches from the target and use the default action #debug to see if it works.

  • Try to manually add the listener and log it’s returned data to see if it works.

    document.body.addEventListener('click', (event) => {
    console.log('manual listener')
    console.log(eventAction.listener(event))
    })
  • If all of the above fails, please open an issue on GitHub.