H5P Guides

Using the Event Dispatcher

Events are a modular way of connecting pieces of code together, or more specifically in H5P's case: class instances. In this short guide, we'll have a look at how experienced developers use events to communicate between the instances of different classes.

The What & Why

The Event Dispatcher allows you to listen for events and add callbacks which are invoked once the event occurs. You may recognise the same principles from a model called publish/subscribe, or pub/sub.

The event system in H5P must not be mistaken for JavaScript DOM Events, even though they do work much in the same way. 

Why not just call a function directly you ask? It's simply not as elegant and loosely coupled as using events before you know it you get complex relationships and dependencies between your classes and adding and removing components suddenly becomes a pain. With events, you can have multiple listeners so that it's no trouble for a third party or new components to hook into these communications. I.e. the different parties doesn't have to know as much about each other, they only need to know about the events they care about.

We can compare this to bus drivers not having to know where each of their passengers is going, they simply stop when someone pushes(triggers) the stop button(event).

Using it

In H5P, there's an H5P.EventDispatcher class which you can extend in your class. In JavaScript, this is done in two steps. First you must call the constructor of the class you wish to extend in your constructor.

H5P.EventDispatcher.call(this);

Secondly after the definition of your constructor you must copy the prototype object of the class you're extending.

H5P.MyClass.prototype = Object.create(H5P.EventDispatcher.prototype);
H5P.MyClass.prototype.constructor = H5P.MyClass;

This will give your class the ability to trigger events and listen to them.

Listening for events can be done on any class that has extended the H5P.EventDispatcher.

this.on('buttonPressed', function (event) {
  console.log('Someone pressed a button!');
});

Triggering events are usually done after something has happened in your class. Listening for events that are never triggered gives no meaning, but there's no harm in doing so. Likewise, there is no harm triggering events that have no listeners, but it could be useful if you know other classes will want to respond to that event.

this.trigger('buttonPressed', buttonText);

The second parameter used when triggering an event is called the event data, which is an optional parameter. However, it can be useful when you want to pass data to the listeners.

var buttonText = event.data;
console.log('Someone pressed the ' + buttonText + ' button!');

When registering event listeners it is worth noticing that inside the listening callback the this variable is registered to the instance triggering the event. 

this.disable();

The above code will call the disable function of the instance triggering the event. In this case, the button.

When you're creating a runnable library and extending the event dispatcher, H5P will automatically try to trigger some events on your class.

this.on('resize', function () {
  console.log('Someone is trying to resize me, I should adjust my HTML elements!');
});

Read more about these events in the Events Overview.

Putting it all together

Below we have (hopefully) put together a self-explanatory example.

bus.js

H5P.Bus = (function ($) {

  /**
   * @class H5P.Bus
   * @param {Object} params
   */
  function Bus(params) {
    var self = this;
    var $body;

    /**
     * Stops the bus.
     *
     * @private
     */
    var stop = function (event) {
      var $msg = $('<div/>', {
        html: event.data + ' is getting off...',
        appendTo: $body
      })
      $body.addClass('h5p-bus-stopped');

      // Start the bus again after 10 secs
      setTimeout(function () {
        $body.removeClass('h5p-bus-stopped');
        $msg.remove();
      }, 10000);
    };

    /**
     * Fills the bus with passengers.
     *
     * @private
     */
    var fill = function () {
      for (var i = 0; i < params.passengers.length; i++) {
        // Create new passenger from parameters data (from editor)
        var passenger = new Bus.Passenger(params.passengers[i]);

        // Listen for stop signals
        passenger.on('wantToGetOff', stop);

        // Sit down
        passenger.appendTo($body);
      }
    };

    /**
     * Gets the bus on the road.
     *
     * @param {H5P.jQuery} $container road
     */
    self.attach = function ($container) {
      if ($body === undefined) {
        // Build HTML the first time we are attached
        $body = $('<div>', {
          html: 'The ' + params.name + ' bus!'
        });

        // Create our passengers
        fill();
      }

      $container.html('').addClass('h5p-bus').append($body);
    };
  }

  return Bus;
})(H5P.jQuery);

passenger.js

(function ($, EventDispatcher, Bus) {

  /**
   * @class H5P.Bus.Passenger
   * @extends H5P.EventDispatcher
   * @param {Object} personalia Passenger data
   */
  Bus.Passenger = function (personalia) {
    var self = this;
    var $face;

    // Inherit functionality
    EventDispatcher.call(self);

    /**
     * Try to get off the bus by triggering the stop signal.
     * @private
     */
    var tryToGetOff = function () {
      $face.addClass('h5p-nervous');
      self.trigger('wantToGetOff', personalia.name);
    };

    /**
     * Makes sure the passenger takes a seat.
     *
     * @param {H5P.jQuery} $seats
     */
    self.appendTo = function ($seats) {
      if ($face === undefined) {
        // Create the passengers face
        $face = $('<button/>', {
          html: personalia.name,
          on: {
            // The passenger's face can be clicked to make him or her want
            // to get off the bus.
            click: tryToGetOff
          }
        });
      }

      $face.appendTo($seats);
    };
  };

  // Inherit prototype properties
  Bus.Passenger.prototype = Object.create(EventDispatcher.prototype);
  Bus.Passenger.prototype.constructor = Bus.Passenger;

})(H5P.jQuery, H5P.EventDispatcher, H5P.Bus);