EventAction
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:
- A Json Array.
- 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
- 
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.
- 
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.
- 
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
- EventActionis designed to be performant, you only add one listener on the parent.
- EventActionWill memoize parsed action strings and action arrays after the first time it is parsed, so it doesn’t have to do that every time.
- EventActionwill 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
- #preventcalls- event.preventDefault().
- #stopcalls- event.stopPropagation().
- #debuglogs data about the current event, action, and switches to the console.
- #logis passed a param it will be logged otherwise it will log info about the current event, action, and switches.
- #nothingdoes nothing, you can thing of this as a return now and do nothing, or- stopPropagationbut unlike- stopPropagationit allows other listeners to run.
Default Switches
- 
#keyruns forKeyboardEventsand expects a param of the keys to check if any of them are pressed.- paramcan be a comma separated list of keys or an array of keys (for action array syntax).
- Spaceis a replacement for- ' '(space) (only for string syntax).
 
- 
#special-keyruns forKeyboardEventsand expects a param of special keys to check if any of them are pressed.- special keys are ctrlaltshiftmeta
 
- special keys are 
Workflow
- 
Start by creating a new EventActioninstance.const eventAction = new EventAction()
- 
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,}})
- 
Register the parent element to listen to events. eventAction.addListeners(document.body, ['pointerdown', 'pointerup', 'click'])
- 
Register your html elements as event action targets <buttondata-click="action-name:btn-click"data-pointerdown="action-name:btn-pointerdown"data-pointerup="action-name:btn-pointerup">Click Me</button><buttondata-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>
- 
Now whenever a one of registered event fires on the body, EventActionwill 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.
- 
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 | undefinedParameters
-  datatype 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 | undefinedReturns
type T = HTMLElement | undefined;getEventAttributeName static
This function is used to get the event attribute name for an event.
Definition
function getEventAttributeName(eventName: string): stringParameters
-  eventNametype 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, ifeventNameisclick, the function will returndata-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
-  nametype name = string;- The action name string to parse.
 
Returns
type T = {  name: string,  hasOr: boolean};-  nameis the action name without the||(if any)
-  hasOris 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): ParsedActionParameters
-  actionStringtype actionString = string;- The action string to parse.
- See action string syntax and roles of actions for more details.
 
]}
Returns
type T = ParsedAction;- See ParsedAction for more details
addListeners
A quick way to add multiple event action listeners to an element.
Definition
function addListeners(  element: HTMLElement,  eventsNames: (string)[]): thisParameters
-  elementtype element = HTMLElement;- The element to register the event listeners on.
 
-  eventNamestype 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)[]): thisParameters
-  elementtype element = HTMLElement;- The element to remove event listeners from.
 
-  eventNamestype 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>): thisParameters
-  switchestype 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  }): thisParameters
-  actionstype 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.
 
- An object where the keys are action names and the values are either an action handler or a custom action object.
-  optionstype options = { generateDataHandler?: (data: GenerateData) => T };- options.generateDataHandlerA default- generateDataHandlerfor 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
-  actionStringtype actionString = string;- The action string to parse.
 See HowToWriteActions for more details.
 
- The action string to parse.
Returns
type T = ParsedAction[];- See ParsedAction for more details
_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 | undefinedParameters
-  datatype 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
-  datatype 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[]};- undefinedis returned if there is no- matchedTargetor- attributeNamehas 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.
- handlerThe Action handler.
 It receives a GenerateData object as a parameter if- generateDataHandleris not provided otherwise it receives the return value of- generateDataHandler.
- generateDataHandlerA 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.
- overrideA boolean that is required when overriding existing action and it is overridable.
- overridableA 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}- eventThe current event
- matchedTargetThe target of the event returned by- _getMatchedTarget
- eventNameThe name of the event
- actionParamThe parameter passed to the action
- _parsedActionThe parsed action object
ParsedAction
export type ParsedAction = {  name: string,  param: unknown,  hasOr: boolean,  switches: {    name: string,    param: unknown  }[]}- nameThe name of the action
- paramThe parameter passed to the action
- hasOrA boolean that indicates if the action has or
- switchesAn 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}- switchParamThe parameter passed to the switch.
- actionNameThe 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}- handlerThe switch handler.
 Receives a SwitchHandlerData object as a parameter and return a boolean indicates if the action should be executed
- overrideA boolean that is required when overriding existing switch and it is overridable.
- overridableA boolean that indicates if the switch can be overridden.
- dynamicA 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'] | CustomSwitchRegisteredSwitchData
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.
- actionsAdd actions at initialization.
 See registerActions for more details.
- switchesAdd switches at initialization.
 See registerSwitches for more details.
- generateDataHandlerA function that return the custom data which will be passed to actions handlers.
 See- generateDataHandleron CustomAction for more details.
- currTargetSelectorThe selector for the current target.
 If exists and- getMatchedTargetis not provided, getMatchedTargetPierce will be used instead of getMatchedTarget which is required for working on shadow dom elements.
- getEventAttributeNameA 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- EventActioninstances or just to override the default attribute name.
 See getEventAttributeName for more details.
- getMatchedTargetA 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 EventActionwill warn for any issues like unregistered actions or switches.
- 
Check that you are adding EventActionlistener to the correct element or forgot to do so.
- 
Make sure to add currentTargetSelectorto 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 getEventAttributeNameoption in the constructor
- 
Make sure that switches are returning truewhen the action should be executed.
- 
Try removing all of the actions and switches from the target and use the default action #debugto 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.