- Пример «Ба Гуа»
- На примере меню
- Пример со вложенным меню
- Применение делегирования: действия в разметке
- Вспомогательная функция
delegate - Итого
Если у вас есть много элементов, события на которых нужно обрабатывать похожим образом, то не стоит присваивать отдельный обработчик каждому.
Вместо этого, назначьте один обработчик общему родителю. Из него можно получить целевой элемент event.target, понять на каком потомке произошло событие и обработать его.
Эта техника называется делегированием и очень активно применяется в современном JavaScript.
Пример «Ба Гуа»
Вот, например, диаграмма «Ба Гуа». Это таблица, отражающая древнюю китайскую философию.
Если кликнуть по ячейке, то она подсветится.
В таблице 9 ячеек. В каждой ячейке есть текст и тег STRONG для форматирования направлений: South, North etc. Этот пример в песочнице tutorial/browser/events/delegation/bagua/index.html.
В этом примере важно то, как реализована подсветка элементов.
Для этого используется делегирование событий. Вместо того, чтобы назначать обработчик для каждой ячейки, назначен один обработчик для всей таблицы. Он использует event.target, чтобы получить элемент, на котором произошло событие, и подсветить его.
table.onclick = function(event) {
event = event || window.event;
var target = event.target || event.srcElement;
// ... подсветить ...
}
Клик может произойти на любом теге внутри таблицы. Например, на теге <STRONG>. А затем он всплывает наверх:

Для того, чтобы найти ячейку, нам нужно пройти цепочку parentNode:
table.onclick = function(event) {
event = event || window.event;
var target = event.target || event.srcElement;
while(target != this) { // ( ** )
if (target.tagName == 'TD') { // ( * )
toggleHighlight(target);
}
target = target.parentNode;
}
}
Этот код следует основному принципу делегирования:
- В обработчике мы можем получить
целевой элемент, на котором произошел клик. Он всегда самый вложенный, поэтому может быть какTD, так иSTRONGвнутри него. А еще клик может попасть в область между ячейками, если присутствует атрибутcellspacing. В этом случае целевым элементом будетTRилиTABLE. - Поднимаемся вверх по цепочке родителей
target = target.parentNode, пока не встретимTDили не дойдем до TABLE. - ( * ) Если мы
TD- обрабатываем его.
( ** ) Если мы дошли вверх до текущего элемента (таблицы), значит клик каким-то образом попал внеTD, и нам он не интересен.
А теперь представьте себе, что в таблице не 9, а 1000 или 10.000 ячеек. Делегирование позволяет обойтись всего одним обработчиком для любого количества ячеек.
На примере меню
Делегирование событий позволяет удобно организовывать деревья и вложенные меню.
Давайте для начала обсудим одноуровневое меню:
<ul id="menu"> <li><a class="button" href="/php">PHP</a></li> <li><a class="button" href="/html">HTML</a></li> <li><a class="button" href="/javascript">JavaScript</a></li> <li><a class="button" href="/flash">Flash</a></li> </ul>
Данный пример - это просто семантичный HTML/CSS. Клики по меню будут обрабатываться с помощью JavaScript, но пункты меню представлены тегом A, чтобы обеспечить работоспособность меню для пользователей с отключенным JavaScript (и для поисковых систем).
Ссылки должны стать кликабельными, как в примере:
Поведение меню при наведении на него мыши реализовано на чистом CSS, при помощи псевдоселектора :hover.
В IE6 такой прием работает только для тега A.
С помощью делегирования, мы можем назначить один обработчик для всего меню:
document.getElementById('menu').onclick = function(e) {
var target = e && e.target || event.srcElement; // целевой элемент
if (target.tagName != 'A') return; // если не ссылка - не интересует!
var href = target.getAttribute('href');
alert(href); // обработать клик по элементу
return false; // отменить переход по ссылке
}
Здесь мы не поднимаемся от target вверх по цепочке parentNode, так как предполагаем, что у тега A в меню заведомо нет потомков.
if..elseВ первой строке мы получаем target при помощи логических операторов.
Дело в том, что есть два варианта:
- Если это не IE<9, то есть первый аргумент
eи свойствоe.target. При этом сработает левая часть оператора ИЛИ||. - Если это старый IE, то первого аргумента нет, левая часть сразу становится
falseи вычисляется правая часть ИЛИ||, которая в этом случае и является аналогом свойстваtarget.
Код target = e && e.target || event.srcElement можно также прочитать так:
target = if e then e.target else event.srcElement.
Полный код примера: tutorial/browser/events/delegation/menu/index.html.
Пример со вложенным меню
Обычное меню при использовании делегирования легко и непринужденно превращается во вложенное.
У вложенного меню остается похожая семантичная структура:
<ul id="menu">
<li><a class="button" href="/php">PHP</a>
<ul>
<li><a href="/php/manual">Manual</a></li>
<li><a href="/php/snippets">Snippets</a></li>
</ul>
</li>
<li><a class="button" href="/html">HTML</a>
<ul>
<li><a href="/html/information">Information</a></li>
<li><a href="/html/examples">Examples</a></li>
</ul>
</li>
</ul>
С помощью CSS, можно спрятать вложенный тег UL до того момента, пока на LI не наведут курсор.
Для отображения вложенного меню достаточно составить правильный CSS. В принципе, скрытие-появление элементов можно реализовать и при помощи JavaScript, но если что-то можно сделать в CSS — лучше использовать CSS.
Полный код примера: tutorial/browser/events/delegation/menu-nested/index.html.
Меню может быть не только вложенным, но и динамическим. Можно добавлять новые пункты меню или удалять ненужные пункты. Так как применено делегирование, то обработчик подхватит новые элементы автоматически.
Дан список сообщений. Добавьте каждому сообщению кнопку для его удаления.
Используйте делегирование событий. Один обработчик для всего.
В результате, должно работать вот так(кликните на крестик):
Исходный документ: tutorial/browser/events/messages-delegate-src/index.html.
Поставьте обработчик click на контейнере. Он должен проверять, произошел ли клик на кнопке удаления (target), и если да, то удалять соответствующий ей DIV.
Решение задачи: tutorial/browser/events/messages-delegate/index.html.
Создайте дерево, которое по клику на заголовок скрывает-показывает детей:
Исходный документ: tutorial/browser/events/tree-src/index.html.
Требования:
- Использовать делегирование.
- Клик вне текста заголовка (на пустом месте) ничего делать не должен.
- При наведении на заголовок — он становится жирным, реализовать CSS.
- При двойном клике на заголовке — его текст не должен становиться выделенным.
P.S. При необходимости HTML/CSS дерева можно изменить.
Дерево устроено как вложенный список.
Клики на все элементы можно поймать, повесив единый обработчик onclick на внешний UL.
Как поймать клик на заголовке? Элемент LI является блочным, поэтому нельзя понять, был ли клик на тексте, или справа от него.
Например, ниже — участок дерева с выделенными рамкой узлами. Кликните справа от любого заголовка. Видите, клик ловится? А лучше бы такие клики (не на тексте) игнорировать.
<style>
li { border: 1px solid green; }
</style>
<ul onclick="alert(event.target || event.srcElement)">
<li>Млекопетающие
<ul>
<li>Коровы</li>
<li>Ослы</li>
<li>Собаки</li>
<li>Тигры</li>
</ul>
</li>
</ul>
В примере выше видно, что проблема в верстке, в том что LI занимает всю ширину. Можно кликнуть справа от текста, это все еще LI.
Один из способов это поправить — обернуть заголовки в дополнительный элемент SPAN, и обрабатывать только клики внутри SPAN'ов.
Мы могли бы это сделать в HTML, но давайте для примера используем JavaScript. Следующий код ищет все LI и оборачивает текстовые узлы в SPAN.
var treeUl = document.getElementsByTagName('ul')[0];
var treeLis = treeUl.getElementsByTagName('li');
for(var i=0; i<treeLis.length; i++) {
var li = treeLis[i];
var span = document.createElement('span');
li.insertBefore(span, li.firstChild); // добавить пустой SPAN
span.appendChild(span.nextSibling); // переместить в него заголовок
}
Теперь можно отслеживать клики на заголовках.
Так выглядит дерево с обёрнутыми в SPAN заголовками и делегированием:
<style>
span { border: 1px solid red; }
</style>
<ul onclick="alert((event.target||event.srcElement).tagName)">
<li><span>Млекопетающие</span>
<ul>
<li><span>Коровы</span></li>
<li><span>Ослы</span></li>
<li><span>Собаки</span></li>
<li><span>Тигры</span></li>
</ul>
</li>
</ul>
Так как SPAN — инлайновый элемент, он всегда такого же размера как текст. Да здравствует SPAN!
В реальной жизни дерево должно быть сразу со SPAN, чтобы не нужно было исправлять структуру. Если HTML-код дерева генерируется на сервере, то это несложно. Если дерево генерируется в JavaScript — тем более просто.
Получение узла по SPAN
Для делегирования нужно по клику понять, на каком узле он произошел.
В нашем случае у SPAN нет детей-элементов, поэтому не нужно подниматься вверх по цепочке родителей. Достаточно просто проверить event.target.tagName == 'SPAN', чтобы понять, где был клик, и спрятать потомков.
var tree = document.getElementsByTagName('ul')[0];
tree.onclick = function(e) {
e = e || event;
var target = e.target || e.srcElement;
if (target.tagName != 'SPAN') {
return; // клик был не на заголовке
}
var li = target.parentNode; // получить родительский LI
// получить UL с потомками -- это первый UL внутри LI
var node = li.getElementsByTagName('ul')[0];
if (!node) return; // потомков нет -- ничего не надо делать
// спрятать/показать
node.style.display = node.style.display ? '' : 'none';
}
Жирные узлы при наведении
Узел выделяется при наведении при помощи CSS-селектора :hover.
Невыделяемость при клике
На всё дерево можно поставить обработчик, отменяющий выделение при клике
tree.onselectstart = tree.onmousedown = function() {
return false; // делаем узлы невыделяемыми
}
Полное решение вы можете увидеть здесь: tutorial/browser/events/tree/index.html.
Создайте галерею изображений, в которой основное изображение изменяется при клике на уменьшенный вариант.
Результат должен выглядеть так:
Для обработки событий используйте делегирование, т.е. не более одного обработчика.
Исходный документ: tutorial/browser/events/gallery-src/index.html.
P.S. Если получится — сделайте предзагрузку изображений, чтобы при клике они появлялись сразу.
P.P.S. Всё ли в порядке с семантической вёрсткой в HTML исходного документа? Если нет — сделайте, как нужно.
Решение состоит в том, чтобы добавить обработчик на контейнер #thumbs и отслеживать клики на ссылках.
Когда происходит событие, обработчик должен изменять src #largeImg на href ссылки и заменять alt на ее title.
Код решения:
var largeImg = document.getElementById('largeImg');
document.getElementById('thumbs').onclick = function(e) {
e = e || window.event;
var target = e.target || e.srcElement;
while(target != this) {
if (target.nodeName == 'A') {
showThumbnail(target.href, target.title);
return false;
}
target = target.parentNode;
}
}
function showThumbnail(href, title) {
largeImg.src = href;
largeImg.alt = title;
}
Рабочий пример tutorial/browser/events/gallery/index.html.
Предзагрузка картинок
Для того, чтобы картинка загрузилась, достаточно создать новый элемент IMG и указать ему src, вот так:
var imgs = thumbs.getElementsByTagName('img');
for(var i=0; i<imgs.length; i++) {
var url = imgs[i].parentNode.href;
*!*
var img = document.createElement('img');
img.src = url;
*/!*
}
Как только элемент создан и ему назначен src, браузер сам начинает скачивать файл картинки.
При правильных настройках сервера как-то использовать этот элемент не обязательно — картинка уже закеширована.
Семантичная верстка
Для списка картинок используется DIV. С точки зрения семантики более верный вариант — список UL/LI.
Полное решение: tutorial/browser/events/gallery/index.html.
Сделайте так, чтобы при клике на ссылки внутри <DIV id="content"> пользователю выводился вопрос о том, действительно ли он хочет покинуть страницу и если он не хочет, то прерывать переход по ссылке.
Как это должно работать:
Детали:
- Содержимое блока
DIVможет быть загружено динамически и присвоено при помощиinnerHTML. Так что обработчики на ссылки ставить нельзя. Используйте делегирование. - Содержимое может содержать вложенные теги, в том числе внутри ссылок, например,
<a href=".."><i>...</i></a>.
Исходный документ: tutorial/browser/events/links-src.html
Это - классическая задача на тему делегирования.
В реальной жизни, мы можем перехватить событие и создать AJAX-запрос к серверу, который сохранит информацию о том, по какой ссылке ушел посетитель.
Мы перехватываем событие на contents и поднимаемся до parentNode пока не получим A или не упремся в контейнер.
document.getElementById('contents').onclick = function(evt) {
var evt = evt || event;
var target = evt.target || evt.srcElement;
function handleLink(href) {
var isLeaving = confirm('Уйти на '+href+'?');
if (!isLeaving) return false;
}
while(target != this) {
if (target.nodeName == 'A') {
*!*
return handleLink(target.getAttribute('href')); // (*)
*/!*
}
target = target.parentNode;
}
}
В строке (*) используется атрибут, а не свойство href, т.к. свойство обязано содержать полный валидный адрес, а атрибут — в точности что указано в HTML.
Полное решение: tutorial/browser/events/links.html.
Сделать сортировку таблицы при клике на заголовок.
Демо:
Требования:
- Использовать делегирование.
- Код не меняется при увеличении количества столбцов или строк.
Исходный код: tutorial/browser/events/grid-sort-src/index.html.
P.S. Обратите внимание, тип столбца задан атрибутом у заголовка. Числа сортируются иначе чем строки!
P.P.S. Вам помогут дополнительные навигационные ссылки по таблицам.
- Обработчик
onclickможно повесить один, на всю таблицу илиTHEAD. Он будет игнорировать клики не наTH. - При клике на
THобработчик будет получать номер изTH, на котором кликнули (TH.cellIndex) и вызывать функциюsortColumn, передавая ей номер колонки и тип. - Функция
sortColumn(colNum, type)будет сортировать.
Функция сортировки:
- Переносит все
TRизTBODYв массивrowsArr - Сортирует массив, используя
rowsArr.sort(compare), функцияcompareзависит от типа столбца. - Добавляет
TRиз массива обратно вTBODY
Применение делегирования: действия в разметке
Ячейки таблицы и пункты меню - это примеры обработки схожих элементов. Эти способы работают хорошо, потому что действия для потомков были одни и те же.
Но делегирование событий позволяет использовать обработчик для абсолютно разных действий.
Например, нам нужно сделать меню с разными кнопками: «Сохранить», «Загрузить», «Поиск» и т.д.
Первое, что приходит в голову - это найти каждую кнопку и назначить ей свой обработчик.
Но более изящно решить задачу можно путем добавления одного обработчика на всё меню. Все клики внутри меню попадут в обработчик.
Но как нам узнать, какую кнопку нажали и как обработать это событие? Эту задачу мы можем решить, добавив каждой кнопке нужный нам метод в специальный атрибут, который назовем data-action (можно придумать любое название, но data-* является валидным в HTML5):
<button *!*data-action="save"*/!*>Нажмите, чтобы Сохранить</button>
HTML-атрибуты, которые начинаются с data-... являются валидными в HTML5.
В спецификации есть раздел об этом. Еще есть API, который помогает взаимодействовать с такими атрибутами, но он еще не поддерживается браузерами.
Поэтому, основные плюсы использования атрибутов data-* - это совместимость в будущем и прохождение валидатора HTML5 в настоящем.
Обработчик считывает содержимое атрибута и выполняет метод. Взгляните на рабочий пример:
<div id="menu">
<button data-action="save">Нажмите, чтобы Сохранить</button>
<button data-action="load">Нажмите, чтобы Загрузить</button>
</div>
<script>
function Menu(elem) {
this.save = function() { alert('сохраняю'); };
this.load = function() { alert('загружаю'); };
var self = this;
elem.onclick = function(e) {
var target = e && e.target || event.srcElement; // (*)
*!*
var action = target.getAttribute('data-action');
if (action) {
self[action]();
}
*/!*
};
}
new Menu(document.getElementById('menu'));
</script>
Обратите внимание, как используется трюк с var self = this, чтобы сохранить ссылку на объект Menu. Иначе обработчик просто бы не смог вызвать методы Menu, потому что его собственный this ссылается на элемент.
Что в этом случае нам дает использование делегирования событий?
- Не нужно писать код, чтобы присвоить обработчик каждой кнопке. Меньше кода, меньше времени, потраченного на инициализацию.
- Структура HTML становится по-настоящему гибкой. Мы можем добавлять/удалять кнопки в любое время.
- Данный подход является семантичным. Мы можем использовать классы «action-save», «action-load» вместо
data-action. Обработчик найдёт классaction-*и вызовет соответствующий метод. Это действительно очень удобно.
Качество кода в примере выше можно повысить, если поменять названия методов объекта с save, load на onClickSave, onClickLoad, так как это не просто методы, а методы-обработчики событий. И вызывать их, соответственно, как self["onClick"+...](). Это сделает смысл методов более понятным и упростит чтение и поддержку кода.
Вспомогательная функция delegate
Алгоритм делегирования можно вынести в отдельную функцию, которая будет искать ближайший элемент и вызывать на нём обработчик.
Подобные функции есть во многих фреймворках, но легко можно написать и свою.
Напишите функцию для удобного делегирования.
Способ вызова:
delegate(container, eventName, // элемент, событие
function(elem) { ... }, // селектор
function(e) { ... } // действие
);
Функция должна вешать обработчик container.oneventName, при срабатывании искать ближайший elem, для которого функция-селектору возвратит true, и вызывать на нём функцию-действие с правильным this.
Пример использования:
delegate(table, 'click', // элемент, событие
function(elem) { return elem.tagName == 'TD'; }, // селектор
function(e) { randomizeColor(this); } // изменить цвет TD
);
В примере ниже функция использована для обработки кликов на ячейках таблицы. Она меняет цвет ячейки на красный и обратно:
Обратите внимание, ячейка может содержать вложенные элементы.
Исходный документ (есть всё, кроме вашей функции delegate): tutorial/browser/events/delegate-src.html.
Данное решение поддерживает только один обработчик на элемент, т.к. реализовано через onсобытие:
function delegate(elem, eventName, selectorFunc, handler) {
elem['on'+eventName] = function(e) {
var target = e && e.target || e.srcElement;
while(target != this) {
if (selectorFunc(target)) {
return handler.call(target, e); // (*)
}
target = target.parentNode;
}
}
}
Важно:
- Обработчик в строке
(*)вызывается в контекстеtarget, ему передаётся объект событияe, содержащий информацию о произошедшем. - Возвращаем результат обработчика, так что
return falseиз него сработает.
Итоговый документ с ним: tutorial/browser/events/delegate.html.
Если нужно более одного события одного типа на элемент, то delegate можно переписать с использованием addEventListener/attachEvent, например так: tutorial/browser/events/delegate2.html
Итого
Делегирование событий — это здорово.
Оно возможно, если существует контейнер с элементами, на который можно повесить обработчик.
Алгоритм:
- Вешаем обработчик на контейнер.
- В обработчике: получаем
event.target. - В обработчике: если необходимо, проходим вверх цепочку
target.parentNode, пока не найдем нужный подходящий элемент (и обработаем его), или пока не упремся в контейнер (this).
Зачем использовать:
- Один обработчик для одинаковых действий над всеми потомками
- Упрощает архитектуру для разных действий, если действие можно узнать от элемента.
Обобщая преимущества:
- Упрощает инициализацию, экономит память.
- Упрощает изменения.
- Позволяет добавлять или удалять элементы, в том числе через
innerHTML, без изменения обработчика.
Конечно, у делегирования событий есть свои ограничения.
- Во-первых, событие должно всплывать. Большинство событий всплывают, но не все.
- Во-вторых, теоретически, делегирование создает дополнительную нагрузку на браузер, ведь обработчик запускается, когда событие происходит в любом месте контейнера, в том числе на элементах, которые нам не интересны. Но обычно это не является проблемой, так как эти лишние расходы — небольшие.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.