Документация на русском
Github репозиторий
Всем привет!
Функциональность событий в Matreshka.js стала настолько богатой, что она, без сомнения, заслужила отдельной статьи.
Основы: произвольные события
Начнем с самого простого. события во фреймворке добавляются методом on.
const handler = () => {
alert('"someeevent" is fired');
};
this.on('someevent', handler);
В который можно передать список событий, разделенных пробелами.
this.on('someevent1 someevent2', handler);
Для объявления обработчика события в произвольном объекта (являющегося или не являющегося экземпляром Matreshka
), используется статичный метод Matreshka.on (разница только в том, что целевой объект — первый аргумент, а не this
).
const object = {};
Matreshka.on(object, 'someevent', handler);
События можно генерировать методом trigger.
this.trigger('someevent');
Для произвольных объектов можно воспользоваться статичным аналогом метода.
Matreshka.trigger(object, 'someevent');
При этом, можно передать какие-нибудь данные в обработчик, указав первый и последующие аргументы.
this.on('someevent', (a, b, c) => {
alert([a, b, c]); // 1,2,3
});
this.trigger('someevent', 1, 2, 3);
Или
Matreshka.on(object, 'someevent', (a, b, c) => {
alert([a, b, c]); // 1, 2, 3
});
Matreshka.trigger(object, 'someevent', 1, 2, 3);
Здесь вы можете углядеть синтаксис Backbone. Всё верно: первые строки кода Matreshka.js писались под впечатлением от Backbone (даже код изначально был позаимствован оттуда, хотя и перетерпел большие изменения в дальнейшем).
Дальше, в этом посте, буду приводить вариант методов, использующих ключевое слово this
(за исключением примеров делегированных событий). Просто помните, что on, once, onDebounce, trigger, set, bindNode и прочие методы Matreshka.js имеют статичные аналоги, принимаемые произвольный целевой объект в качестве первого аргумента.
Кроме метода on
, есть еще два: once
и onDebounce
. Первый навешивает обработчик, который может быть вызван только однажды.
this.once('someevent', () => {
alert('yep');
});
this.trigger('someevent'); // yep
this.trigger('someevent'); // nothing
Второй "устраняет дребезжание" обработчика. Когда срабатывает событие, запускается таймер с заданной программистом задержкой. Если по истечению таймера не вызвано событие с таким же именем, запускается обработчик. Если событие сработало перед окончанием задержки, таймер обновляется и снова ждет. Это реализация очень популярного микропаттерна debounce, о котором можно прочесть на Хабре, на англоязычном ресурсе.
this.onDebounce('someevent', () => {
alert('yep');
});
for(let i = 0; i < 1000; i++) {
this.trigger('someevent');
}
// через минимальный промежуток времени один раз покажет 'yep'
Не забывайте, что метод может принимать задержку.
this.onDebounce('someevent', handler, 1000);
События изменения свойства
Когда свойство меняется, Matreshka.js генерирует обытие change:KEY
.
this.on('change:x', () => {
alert('x is changed');
});
this.x = 42;
В случае, если вы хотите передать какую-нибудь информацию в обработчик события или же изменить значение свойства не вызывая при этом события "change:KEY"
, вместо обычного присваивания воспользуйтесь методом Matreshka#set (или статичным методом Matreshka.set), принимающим три аргумента: ключ, значение и объект с данными или флагами.
this.on('change:x', evt => {
alert(evt.someData);
});
this.set('x', 42, { someData: 'foo' });
А вот как можно изменить свойство, не вызывая обработчик события:
this.set('x', 9000, { silent: true }); // изменение не вызывает событие
Метод set
поддерживает еще несколько флагов, описание которых заставило бы выйти за рамки темы статьи, поэтому, прошу обратиться к документации к методу.
События, генерирующиеся перед изменением свойства
В версии 1.1 появилась еще одно событие: "beforechange:KEY"
, генерирующееся перед изменением свойства. Событие может быть полезно в случаях, когда вы определяете событие "change:KEY"
и хотите вызвать код, предшествующий этому событию.
this.on('beforechange:x', () => {
alert('x will be changed in few microseconds');
});
В обработчик можно передать какие-нибудь данные или отменить генерацию события.
this.on('beforechange:x', evt => {
alert(evt.someData);
});
this.set('x', 42, { someData: 'foo' });
this.set('x', 9000, { silent: true }); // изменение не генерирует событие
События удаления свойства
При удалении свойств методом remove, генерируются события delete:KEY
и delete
.
this.on('delete:x', () => {
alert('x is deleted');
});
this.on('delete', evt => {
alert(`${evt.key} is deleted`);
});
this.remove('x');
События байндинга
При объявлении привязки генерируется два события: "bind"
и "bind:KEY"
, где KEY — ключ связанного свойства.
this.on('bind:x', () => {
alert('x is bound');
});
this.on('bind', evt => {
alert(`${evt.key} is bound`);
});
this.bindNode('x', '.my-node');
Это событие может быть полезно, например, тогда, когда байндинги контролирует другой класс, и вам нужно запустить свой код после какой-нибудь привязки (например, привязки песочницы).
События добавления и удаления событий
Когда добавляется событие, генерируются события "addevent"
и "addevent:NAME"
, когда удаляются — "removeevent"
и "removeevent:NAME"
, где NAME — имя события.
this.on('addevent', handler);
this.on('addevent:someevent', handler);
this.on('removeevent', handler);
this.on('removeevent:someevent', handler);
Одним из способов применения можно назвать использование событий фреймворка в связке с движком событий сторонней библиотеки. Скажем, вы хотите разместить все обработчики для класса в одном единственном вызове on, сделав код читабельнее и комапактнее. С помощью "addevent"
вы перехватываете все последующие инициализации событий, а в обработчике проверяете имя события на соответствие каким-нибудь условиям и инициализируете событие, используя API сторонней библиотеки. В примере ниже код из проекта, который юзает Fabric.js. Обработчик "addevent"
проверяет имя события на наличие префикса "fabric:"
и, если проверка пройдена, добавляет холсту соответствующий обработчик с помощью Fabric API.
this.canvas = new fabric.Canvas(node);
this.on({
'addevent': evt => {
const { name, callback } = evt;
const prefix = 'fabric:';
if(name.indexOf(prefix) == 0) {
const fabricEventName = name.slice(prefix.length);
// add an event to the canvas
this.canvas.on(fabricEventName, callback);
}
},
'fabric:after:render': evt => {
this.data = this.canvas.toObject();
},
'fabric:object:selected': evt => { /* ... */ }
});
Делегированные события
Теперь приступим к самому интересному: к делегированием событий. Синтаксис делегированных событий таков: PATH@EVENT_NAME
, где PATH — это путь (свойства разделены точкой) к объекту, на который навешивается событие EVENT_NAME. Давайте разберемся на примерах.
Пример 1
Вы хотите навешать обработчик события в свойстве "a"
, которое является объектом.
this.on('a@someevent', handler);
Обработчик будет вызван тогда, когда в "a"
произошло событие "someevent"
.
this.a.trigger('someevent'); // если a - экземпляр Matreshka
Matreshka.trigger(this.a, 'someevent'); // если a - обычный объект или экземпляр Matreshka
При этом, обработчик можно объявить и до того, как свойство "a"
объявлено. Если свойство "a"
перезаписать другим объектом, внутренний механизм Matreshka.js отловит это изменение, удалит обработчик у предыдущего значения свойства и навешает новому значению.
this.a = new Matreshka();
this.a.trigger('someevent');
//или
this.a = {};
Matreshka.trigger(this.a, 'someevent');
Обработчик handler
снова будет вызван.
Пример 2
А что если наш объект — коллекция, унаследованная от Matreshka.Array или Matreshka.Object (Matreshka.Object
— это коллекция, типа ключ-значение)? Мы заранее не знаем, в каком элементе коллекции произойдет событие (в первом или десятом). Поэтому, вместо имени свойства, для этих классов, можно использовать звездочку "*", говорящую о том, что обработчик события должен вызываться тогда, когда событие вызвано на одном из входящих в коллекцию элементов.
this.on('*@someevent', handler);
Если входящий элемент — экземпляр Matreshka
:
this.push(new Matreshka());
this[0].trigger('someevent');
Или, в случае, если входящий элемент либо обычный объект либо экземпляр Matreshka
:
this.push({});
Matreshka.trigger(this[0], 'someevent');
Пример 3
Идем глубже. Скажем, у нас есть свойство "a"
, которое содержит объект со свойством "b"
, в котором должно произойти событие "someevent"
. В этом случае, свойства разделаются точкой:
this.on('a.b@someevent', handler);
this.a.b.trigger('someevent');
//или
Matreshka.trigger(this.a.b, 'someevent');
Пример 4
У нас есть свойство "a"
, которое является коллекцией. Мы хотим отловить событие "someevent"
, которое должно возникнуть у какого-нибудь элемента входящего в эту коллекцию. Совмещаем примеры (2) и (3).
this.on('a.*@someevent', handler);
this.a[0].trigger('someevent');
//или
Matreshka.trigger(this.a[0], 'someevent');
Пример 5
У нас есть коллекция объектов, содержащих свойство "a"
, являющееся объектом. Мы хотим навешать обработчик, на все объекты, содержащиеся под ключем "a"
у каждого элемента коллекции:
this.on('*.a@someevent', handler);
this[0].a.trigger('someevent');
//или
Matreshka.trigger(this[0].a, 'someevent');
Пример 6
У нас есть коллекция, элементы которой содержат свойство "a"
, являющееся коллекцией. В свою очередь, последняя включает в себя элементы, содержащие свойство "b"
, являющееся объектом. Мы хотим отловить "someevent"
у всех объектов "b"
:
this.on('*.a.*.b@someevent', handler);
this[0].a[0].b.trigger('someevent');
//или
Matreshka.trigger(this[0].a[0].b, 'someevent');
Пример 7. Различные комбинации
Кроме произвольных событий, можно использовать и встроенные в Matreshka.js. Вместо "someevent"
можно воспользоваться событием "change:KEY"
, описанное выше или "modify"
, которое позволяет слушать любые изменения в Matreshka.Object
и Matreshka.Array
.
// в объекте "a" есть объект "b", в котором мы слушаем изменения свойства "c".
this.on('a.b@change:c', handler);
// объект "a" - коллекция коллекций
// мы хотим отловить изменения (добавление/удаление/пересортировку элементов) последних.
this.on('a.*@modify', handler);
Напоминаю, что делегированные события навешиваются динамически. При объявлении обработчика, любая ветвь пути может отсутствовать. Если что-то в дереве объектов переопределено, связь со старым значением разрывается и создается связь с новым значением:
this.on('a.b.c.d@someevent', handler);
this.a.b = {c: {d: {}}};
Matreshka.trigger(this.a.b.c.d, 'someevent');
DOM события
Как известно, Matreshka.js позволяет связать DOM элемент на странице с каким-нибудь свойством экземпляра Matreshka
или обычного объекта, реализуя одно или двух-стороннее связывание:
this.bindNode('x', '.my-node');
//или
Matreshka.bindNode(object, 'x', '.my-node');
До или после объявления привязки можно создать обработчик, слушающий DOM события привязанного элемента. Синтаксис таков: DOM_EVENT::KEY
, где DOM_EVENT
— DOM или jQuery событие (если jQuery используется), а KEY
— ключ привязанного свойства. DOM_EVENT
и KEY
разделены двойным двоеточием.
this.on('click::x', evt => {
evt.preventDefault();
});
В объект оригинального DOM события находится под ключём domEvent
объекта события, переданного в обработчик. Кроме этого, в объекте доступно несколько свойств и методов, для того чтобы не обращаться каждый раз к domEvent
: preventDefault
, stopPropagation
, which
, target
и несколько других свойств.
Эта возможность — синтаксический сахар, над обычными DOM и jQuery событиями, а код ниже делает то же самое, что и предыдущий:
document.querySelector('.my-node').addEventListener('click', evt => {
evt.preventDefault();
});
Делегированные DOM события
Объявление событий из примера выше требует объявления привязки. Вы должны совершить два шага: вызвать метод bindNode
и, собственно, объявить событие. Это не всегда удобно, так как часто бывают случаи, когда DOM узел нигде не используется, кроме одного-единственного DOM события. Для такого случая предусмотрен еще один вариант синтаксиса DOM событий, выглядящий, как DOM_EVENT::KEY(SELECTOR)
. KEY
, в данном случае — некий ключ, связанный с неким DOM элементом. а SELECTOR
— это селектор DOM элемента, который входит в элемент, связанный с KEY
.
<div class="my-node">
<span class="my-inner-node"></span>
</div>
this.bindNode('x', '.my-node');
this.on('click::x(.my-inner-node)', handler);
Делегированные DOM события внутри песочницы
Если нам нужно создать обработчик для некоегого элемента, входящего в песочницу, используется немного упрощенный синтаксис DOM_EVENT::(SELECTOR)
.
Напомню, песочница ограничивает влияние экземпляра Matreshka
или произвольного объекта одним элементом в веб приложении. Например, если на странице есть несколько виджетов, и каждый виджет управляется своим классом, очень желательно задать песочницу для каждого класса, указывающую на корневой элемент виджета, на который влияет этот класс.
this.bindNode('sandbox', '.my-node');
this.on('click::(.my-inner-node)', handler);
Этот код делает совершенно то же самое:
this.on('click::sandbox(.my-inner-node)', handler);
События класса Matreshka.Object
Напомню, Matreshka.Object
— это класс, отвечающий за данные, типа ключ-значение. Подробнее об этом классе прочтите в документации.
При каждом измении свойств, отвечающих за данные, генерируется событие "set"
.
this.on('set', handler);
При каждом удалении свойств, отвечающих за данные, генерируется событие "remove"
.
this.on('remove', handler);
При каждом измении или удалении свойств, отвечающих за данные, генерируется событие "modify"
.
this.on('modify', handler);
Таким нехитрым способом можно слушать все изменения данных, вместо ручной прослушки свойств.
События класса Matreshka.Array
С массивом всё намного интереснее. Matreshka.Array включает массу полезных событий, дающих возможность узнать что произошло в коллекции: вставка элемента, удаление элемента, пересортировка.
Напомню, Matreshka.Array
— это класс, отвечающий за реализацию коллекций во фреймворке. Класс полностью повторяет методы встроенного Array.prototype
, а программисту не нужно думать о том, какой метод вызвать, чтоб что-то добавить или удалить. Что нужно знать о событиях Matreshka.Array
:
- При вызове методов, позаимствованных у
Array.prototype
вызывается соответствующее событие ("push"
,"splice"
,"pop"
...) - При вставке элементов в массив генерируются события
"add"
и"addone"
. Используя первое, в свойство"added"
попадает массив из вставленных элементов. Используя второе в свойство"addedItem"
попадает вставленный элемент, а событие генерируется столько раз, сколько элементов добавленно. - При удалении элементов используется та же логика:
"remove"
генерируется, передавая в свойство"removed"
объекта события массив удаленных элемеентов, а"removeone"
генерируется на каждом удаленном элементе, передавая в свойство"removedItem"
удаленный элемент. - При любых модификациях коллекции генерируется событие
"modify"
. Т. е. отлавливать события"remove"
и"add"
по отдельности не обязательно.
Несколько примеров из документации:
this.on('add', function(evt) {
console.log(evt.added); // [1,2,3]
});
// обработчик запустится трижды,
// так как в массив добавили три новых элемента
this.on('addone', function(evt) {
console.log(evt.addedItem); // 1 … 2 … 3
});
this.push(1, 2, 3);
Чтоб не копировать содержимое документации полностью, предлагаю ознакомиться с документацией к Matreshka.Array самостоятельно.
Спасибо всем, кто остаётся с проектом. Всем добра.