Skip to content

Matreshka.js: Events

Andrey Gubanov edited this page Dec 4, 2015 · 7 revisions

Basics

Custom events

Let’s start with the simplest thing. In Matreshka events can be added with the help of on method.

var handler = function() {
  alert('"someeevent" is fired'); 
};
this.on('someevent', handler);

Where the list of events separated by spaces can be passed to.

this.on('someevent1 someevent2', handler);

Static MK.on method is used to declare an event handler for a custom object (which is or is not Matreshka instance) (the difference is only that the target object is the first argument but not this).

var object = {};
MK.on(object, 'someevent', handler);

Events can be fired with trigger method.

this.trigger('someevent');

Use the static alternative of the method for custom objects.

MK.trigger(object, 'someevent');

At the same time, you can pass some data to the handler having determined the first and the following arguments.

this.on('someevent', function(a, b, c) {
  alert([a, b, c]); // 1,2,3
});
this.trigger('someevent', 1, 2, 3);

Or

MK.on(object, 'someevent', function(a, b, c) {
  alert([a, b, c]); // 1, 2, 3
});
MK.trigger(object, 'someevent', 1, 2, 3);

You can notice Backbone syntax here. That’s right: the first lines of Matreshka’s code were being written under the (negative) impression of Backbone (even the code has originally been borrowed from it, though it has undergone substantial transformation later).

Hereafter, in this post, I will show alternative methods which use this key-word (except the examples of delegated events). Just remember that on, once, onDebounce, trigger, set, bindNode and other Matreshka’s methods have got static alternatives which accept a custom target object as the first argument.

Besides on method, there are two more: once and onDebounce. The first one adds a handler that can be called only once.

this.once('someevent', function() {
  alert('yep');
});
this.trigger('someevent'); // yep
this.trigger('someevent'); // nothing

The second one "debounces" the handler. When an event fires out, the timer with the specified delay by a programmer starts. If no event with the same name is called upon the expiry of the timer, a handler is called. If an event fires before the delay is over, the timer updates and waits again. This is the implementation of a very popular "debounce" micropattern which you can read about on this page or on Matreshka website.

this.onDebounce('someevent', function() {
  alert('yep');
});
for(var i = 0; i < 1000; i++) {
  this.trigger('someevent');
}
// it will show ‘yep’ once in a very short space of time

Remember the method can accept a delay.

this.onDebounce('someevent', handler, 1000);

Events of property changing

When a property is changed, Matreshka fires an event: "change:KEY".

this.on('change:x', function() {
  alert('x is changed');
});
this.x = 42;

In case you want to pass some data to the event handler or change a property value without calling "change:KEY" event, instead of a usual assignment use Matreshka#set method (or static Matreshka.set method) which accepts three arguments: a key, a value and an object with data or special flags.

this.on('change:x', function(evt) {
  alert(evt.someData);
});
this.set('x', 42, {someData: 'foo'});

You can change a property without calling an event handler in this way:

// changing doesn’t fire an event
this.set('x', 9000, {silent: true});

set method supports some more flags, the description of which would make us go beyond the theme of the article, so I refer you to the documentation of the method.

Events which are being fired before a property changing

In 1.1 version another event: "beforechange:KEY" has appeared which is being fired before a property changing. The event can be useful in cases you define "change:KEY" event and want to call the code which precedes this event.

this.on('beforechange:x', funtion() {
  alert('x will be changed in few microseconds'); 
});

You can pass some data to the handler or cancel an event triggering.

this.on('beforechange:x', function(evt) {
  alert(evt.someData);
});
this.set('x', 42, {someData: 'foo'});
// changing doesn’t fire an event
this.set('x', 9000, {silent: true});

Events of a property removing

On removing properties with remove method, "delete:KEY" and delete events are fired.

this.on('delete:x', function() {
  alert('x is deleted');
});
this.on('delete', function(evt) {
  alert(evt.key + ' is deleted');
});
this.remove('x');

Binding events

On the binding declaration two events: "bind" and "bind:KEY" are fired, where KEY is a key of a bound property.

this.on('bind:x', function() {
  alert('x is bound');
});
this.on('bind', function(evt) {
  alert(evt.key + ' is bound');
});
this.bindNode('x', '.my-node');

This event can be of use, for example, when another class controls bindings and you need to launch your code after some binding (for instance, sandbox binding).

The events of event adding/removing

That’s right. When an event is added, "addevent" and "addevent:NAME" events are fired, and when an event is removed, "removeevent" and "removeevent:NAME" events are fired, where NAME is an event name.

this.on('addevent', handler);
this.on('addevent:someevent', handler);
this.on('removeevent', handler);
this.on('removeevent:someevent', handler);

Originally, "addevent:NAME" event was implemented for inner optimizations, but then it was decided to put it into the public API.

One of the ways of its application can be the use of Matreshka and the event engine of the third-party library together. Let’s say, you want to place all handlers for the class only in one on call, having made the code more readable and compact. With the help of addevent you catch all the following event initializations, and in the handler you check an event name against some conditions and initialize an event using API of the third-party library. In the example below there’s a code from a project written in ECMAScript 2015, which uses Fabric.js. "addevent" handler checks an event name for the presence of "fabric:" prefix and if checking is passed, it adds the corresponding handler to the canvas with the help of Fabric API.

this.canvas = new fabric.Canvas(node);
this.on({
  'addevent': evt => {
    let {name, callback} = evt,
        prefix = 'fabric:';
    if(name.indexOf(prefix) == 0) {
      name = name.slice(prefix.length);
      // add an event to the canvas
      this.canvas.on(name, callback);
    }
  },
  'fabric:after:render': evt => this.data = this.canvas.toObject(),
  'fabric:object:selected': evt => 
}

Hidden events

For the sake of completeness, it should be mentioned that Matreshka’s core also uses events for implementing cool features. On property changing, for example, two events are fired: "_runbindings:KEY" which calls the mechanism of synchronization with DOM element and "_rundependencies:KEY" which calls linkProps mechanism.

Delegated events

Now let’s get down to the most interesting: event delegations. The syntax of delegated events is as follows: "PATH@EVENT_NAME", where PATH is the way (properties are separated by a dot) to the object which EVENT_NAME event needs to be added to. Let’s consider the examples.

Example 1

You want to add an event handler in "a" property which is an object.

this.on('a@someevent', handler);

The handler will be called only when "someevent" event has fired in the "a" object.

// if a is an instance of Matreshka
this.a.trigger('someevent'); 
// if a is an ordinary object or an instance of Matreshka
MK.trigger(this.a, 'someevent');

Also, the handler can be declared before "a" property is declared. If "a" property is rewritten into another object, Matreshka’s inner mechanism will catch this change, remove the handler from the previous property value and add it to a new value (if the new value is an object as well).

this.a = new MK();
this.a.trigger('someevent');
//or
this.a = {};
MK.trigger(this.a, 'someevent');

The handler will be called again.

Example 2

What if our object is a collection inherited from Matreshka.Array or Matreshka.Object (MK.Object is a collection of a key-value type , instances of this class have each method which iterates over all properties responsible for data and a support of for..of cycles)? We don’t know beforehand in which item of the collection an event will be fired (in the first or in the tenth one). That’s why, instead of a property name for these classes we can use an asterisk "*" meaning that an event handler must be called only when an event is fired upon one of the elements included into the collection.

this.on('*@someevent', handler);

If the included element is an instance of Matreshka:

this.push(new Matreshka());
this[0].trigger('someevent');

Or, in case the included element is an ordinary object or an instance of Matreshka:

this.push({});
MK.trigger(this[0], 'someevent');

Example 3

Let’s go deeper. Suppose we have "a" property that contains an object with "b" property, in which "someevent" event must be fired. In this case properties are separated by a dot:

this.on('a.b@someevent', handler);
this.a.b.trigger('someevent');
//or
MK.trigger(this.a.b, 'someevent');

Example 4

We have "a" property which is a collection. We want to listen to "someevent" event that must be fired in some element included in this collection. Combine examples (2) and (3).

this.on('a.*@someevent', handler);
this.a[0].trigger('someevent');
//or
MK.trigger(this.a[0], 'someevent');

Example 5

We have a collection containing objects with "a" property which is an object. We want to add a handler to all objects we have with "a" key in each element of the collection:

this.on('*.a@someevent', handler);
this[0].a.trigger('someevent');
//or
MK.trigger(this[0].a, 'someevent');

Example 6

We have a collection which items have "a" property that is a collection. In its turn, the "a" includes items containing "b" property which is an object. We want to catch "someevent" in all "b" objects:

this.on('*.a.*.b@someevent', handler);
this[0].a[0].b.trigger('someevent');
//or
MK.trigger(this[0].a[0].b, 'someevent');

Example 7. Various combinations

Besides custom events, you can use the ones which are built in Matreshka as well. Instead of "someevent" you can use "change:KEY" event described above or "modify" which allows to listen to any changes in MK.Object or MK.Array.

// in “a” object there’s “b” object,
// in which we listen to changes of “c” property.
this.on('a.b@change:c', handler);
// “a” object is a collection of collections
// we want to catch changes
//(adding/removing/resorting of elements) of the last-named.
this.on('a.*@modify', handler);

Let me remind you that delegated events are added dynamically. On declaring a handler any branch of the way may be absent. If anything is overridden in the object tree, the binding to the old value is disrupted and a new one is created with a new value:

this.on('a.b.c.d@someevent', handler);
this.a.b = {c: {d: {}}};
MK.trigger(this.a.b.c.d, 'someevent');

DOM events

Matreshka is known to allow the binding of DOM element on the page to some Matreshka’s instance property or an ordinary object implementing one-way or two-way data binding:

this.bindNode('x', '.my-node');
//or
MK.bindNode(object, 'x', '.my-node');

More detailed information about bindNode method.

Before or after the declaration of the binding you can create a handler that listens to DOM events of the bound element. The syntax is as follows: "DOM_EVENT::KEY", where DOM_EVENT is DOM or jQuery event, and KEY is a key of a bound property. DOM_EVENT and KEY are separated by a double colon.

this.on('click::x', function(evt) {
  evt.preventDefault();
});

The object of original DOM event is under "domEvent" key of the event object passed to the handler. Besides, there are several properties and methods available in the object so as not to address "domEvent" every time: "preventDefault", "stopPropagation", "which", "target" and some other properties ("key", "node", "$nodes", "self").

This opportunity is syntactic sugar over ordinary DOM and jQuery events and the code below does the same things as the previous one:

$('.my-node').on('click', function(evt) {
  evt.preventDefault();
});

Delegated DOM events

Event declaring from the example above requires binding declaring. You must take two steps: call bindNode method and declare the event as such. It isn’t always convenient because there are often some cases when DOM node isn’t used anywhere except the only DOM event. For this case there is another syntax variant of DOM events which looks like "DOM_EVENT::KEY(SELECTOR)". In this case KEY is some key bound to some DOM node. And SELECTOR is a selector of DOM node that is included into the one bound to KEY.

HTML:

<div class="my-node">
 <span class="my-inner-node"></span>
</div>

JS:

this.bindNode('x', '.my-node');
this.on('click::x(.my-inner-node)', handler);

Delegated DOM events inside a sandbox

If we need to create a handler for some element included into a sandbox, a little simplified "DOM_EVENT::(SELECTOR)" syntax is used.

Let me remind you that a sandbox limits the influence of Matreshka’s instance or a custom object to one element in your web application. For example, if there are several widgets on the page and each widget is driven by its class, it’s highly recommended to set a sandbox for each class referring to the root element of the widget which this class has influence on.

this.bindNode('sandbox', '.my-node');
this.on('click::(.my-inner-node)', handler);

This code does absolutely the same thing:

this.on('click::sandbox(.my-inner-node)', handler);

Events of Matreshka.Object class

Remember MK.Object is a class that is responsible for data of a key-value type. Besides, it can be used as a model in MK.Array class (as well as MK.Array which allows to create collections of collections). You can read more about this class in documentation.

On every changing of properties which are responsible for data, "modify" event is fired.

this.on('modify', handler);

In this easy way you can listen to all data changing instead of a manual property listening.

Events of Matreshka.Array class

Everything is much more interesting with an array. MK.Array includes lots of useful events which give an opportunity to find out what has happened in the collection: an item insertion, an item removing, resorting, which method has been called...

Let me remind you that MK.Array is a class which is responsible for the implementation of collections in Matreshka framework. The class completely duplicates the methods of built-in Array.prototype, and a programmer doesn’t have to think which method to call in order to add or remove something. The information you should know about MK.Array events:

  • On calling the methods which have been borrowed from Array.prototype, the corresponding event ("push", "splice", "pop"...) is fired
  • On inserting items into the array, "add" and "addone" events are fired. Using the first one, an array of inserted items gets to "added" property of an event object. Using the second one, one inserted item gets to "added" property, and the event is fired so many times according to how many items have been added.
  • On removing items the same logic is used: "remove" is fired passing an array of removed items to "removed" property of an event object, and "removeone" is fired on each removed item, passing one removed element to property "removed".
  • On any modifications of collection, "modify" event is fired. I. е. to catch "remove" and "add" events separately isn’t obligatory.

Some examples from the documentation:

this.on('push', function(evt) {
  console.log(evt.args); // [1,2,3]
});

this.on('add', function(evt) {
  console.log(evt.added); // [1,2,3]
});
// the handler will launch thrice,
// as three new elements have been added to the array
this.on('addone', function(evt) {
  console.log(evt.added); // 1 … 2 … 3
});
this.push(1, 2, 3);

In order not to copy the whole documentation here, please refer to MK.Array documentation on your own.

Clone this wiki locally