Categories javascript

WordPress-like action handling in javascript

I really like the concept of events in javascript. How they can be dispatched and handled, how they bubble and how custom events can have additional details. However, at some point I needed to control the order, in which the handlers got executed. And I could not get it done with events, so I decided to implements a mini wodrpress-like system to handle that case.

Post date September 21, 2020

I really like the concept of events in javascript. How they can be dispatched and handled, how they bubble and how custom events can have additional details. However, at some point I needed to control the order, in which the handlers got executed. And I could not get it done with events, so I decided to implements a mini wodrpress-like system to handle that case.

Assumptions

The assumptions looked more or less like this:

– at any point I can call do_action to run all associated handlers’ code. If there are no handlers registered, nothing happens.
– I can add new handlers with add_action and set priority (order) to control the order handlers are run in.
– I can remove handlers with remove_action if I need to override some part of their code or I want remove and re-add code with different priority.

Implementation

To achieve all this I will need three global functions (do_action, add_action, remove_action) and one global variable (handlers) to keep references to functions, that should run when specific actions are called.

Adding handlers

To add a handler I need to know to which action it belongs (action) to, how to identify it later (identifier), reference to the function to be run (handler) and priority (priority) with default value of 10.

The code look like this:

let handlers = {};

const add_action = function(action, identifier, handler, priority = 10) {
    if (!handlers.hasOwnProperty(action)) {
    handlers[action] = {};
  }

  if (!handlers[action].hasOwnProperty(priority)) {
    handlers[action][priority] = [];
  }

  handlers[action][priority][identifier] = handler;
}

The most important part is the last line (handlers[action][priority][identifier] = handler). It explains the whole structure of the handlers variable and saves the reference to handler.

Because we don’t have associative arrays in javascript we need to use objects to have word-like identifier for a other data. On top level those keys are action names. When we later run do_action these values will be search through. On the next level the keys are numeral-looking strings. They will eventually be sorted like numbers, but in fact these are strings. Last level in this structure is built by pairs identifierhandling function.

The structure of handlers with some handlers added
looks like this:

handlers = {
  add_item: {
    10: {
      id1: handleNewItem 
    },
    15: {
      id2: updateInfo
    },
    20: { 
      id3: saveItem
    },
  },
  remove_item: {
    10: {
      id1: updateInfo
    }
  }
}

Where add_item and remove_item are actions, 10, 15 and 20 are priorities and handleNewItem, updateInfo and saveItem are functions, that will be executed.

Removing handlers

To remove handlers we run remove_action. The implementation is rather straightforward:

const remove_action = function(action, identifier, priority = 10) {
  if (handlers.hasOwnProperty(action)
  &&
  handlers[action].hasOwnProperty(priority)
  &&
  handlers[action][priority].hasOwnProperty(identifier)
  ) {
    delete handlers[action][priority][identifier];
  }
}

In javascript to remove a key from an object we use delete. If the handler was not there, nothing happens.

Running actions

Now, that we have our handlers registered, it’s time to make use of them. To do it we need do_action:

const do_action = function(action, ...args) {
  if (!handlers.hasOwnProperty(action)) {
    return;
  }

  let priorities = Object.keys(handlers[action]);

  priorities = priorities.sort((a, b) => a - b);

  for (let i = 0; i < priorities.length; i++) {
    let priority = priorities[i];

    for (let identifier in handlers[action][priority]) {
      handlers[action][priority][identifier](...args);
    }
  }
}

This function takes one or more parameters. The first one (action) is a string – the key we will be looking for in handlers object. After that parameter we can add any number of additional parameters, that will be passed to handling functions.

If there are no handlers registered for our action, nothing happens. It’s like an event without a handler. However, if we have any handlers, we want to run them in right order. First we get all registered priorities with Object.keys(handlers[action]). Then we sort them using Array.sort function. Default sorting is alphabetical, which means 10 comes before 5, because 1 is earlier in alphabet than 5. To change this behavior we define a callback ((a, b) => a - b) telling the browser to treat values as numbers and not as strings.

When we have our priorities sorted, we can run the handlers (handlers[action][priority][identifier](...args)). eclaring ...args will pass second, third and so on parameter passed to do_action forward to handlers. To understand it better, let’s have a look at an example with one additional parameter (item):

const handle_remove = function(item) {
  console.log('Remove: ', item);
}

add_action('remove_item', 'id1', handle_remove);

let item = "Example string";
do_action('remove_item', item);

And that’s it. It works like a charm.

Note on using this

One of the trickiest parts of javascript (right after getting variable types right) is using keyword this, which changes depending on context without any warning causing lots of headache. If we want our handler to have access to it’s class’ properties and methods we use bind. It takes care of our this being the right this.

class Example {
  init() {
    add_action('remove_item', 'id1',
    this.handle_remove.bind(this));
  }

  handle_remove = function(item) {
    this.log('Removed an item.');
  }

  log_change(messaget) {
    console.log(message);
  }
}

let example = new Example();
example.init();

do_action('remove_item', item);

If we tried to add a handler with this code:

add_action('remove_item', 'id1', this.handle_remove);

it would be added without any errors, but when called, this inside handle_remove method would be undefined and throw an error.

Leave a Reply

Your email address will not be published. Required fields are marked *