Когда-то давным давно, когда js был совсем молод и еще ходили Netscape-динозавры, то все думали, что мощь-js заканчивается следующими сценариями:
- создание мигающих ссылок;
- скрытие/расскрытие элементов;
- ...
Метеориты падали и падали, многие динозавры вымерли, и JS начал изменяться под потребности работадателей и других 'зверей':
- Исполнения кода не только в браузере;
- Запуск js на сервере;
- Firefox Os;
- Phonegap;
- ...
Но все 'мутации', которым подвергся js, не изменили его главную цель, а именно, создание динамических страниц в браузере. Конечно со времен динозавров много воды утекло и потребности изменились:
- Календари;
- Частичное заполнение форм;
- Саггесты(подсказки при вводе);
- Динамическая отрисовка страниц;
- И многое другое...
В этой лекции мы поговорим о том, что делает фронтенд разработчика - фронтенд разработчиком.
P.S. Все примеры, который написаны в данной главе работают в относительно современных браузерах. Так как лекции готовятся с расчетом на смерть ie10 <=
Запустить js в браузере можно множеством способов:
- Через консоль разработки
- Через сервисы-песочницы
- Через html документ
Давай-те рассмотрим каждый из вариантов:
- Запускаем Яндекс.Браузер
- Идем в Settings
- Идем в Advanced
- Идем в More Tools
- Идем в Developer Tools
- Затем идем в таб Console
- Пишем в строке код 2+2
- Жмем enter // 4
P.S. Для запуска Developer Tools есть хоткей
- идем на сайт jsfiddle.net
- во вкладке js пишем: console.log(2+2)
- жмем кнопку run
- идем в Developer tools
- Создадим простую html-страницу
<html>
<head></head>
<body>
<h1>Hello world!</h1>
</boby>
</html>
- Вставляем тэг-script
<html>
<head></head>
<body>
<h1>Hello world!</h1>
<script>
console.log(2+2);
</script>
</boby>
</html>
- Открываем ваш документ в браузере
- открываем Developer Tools
DOM - это API, которое предоставляет браузер js-интерпретатору для взаимодействия с html-документов:
- чтение данных из документа;
- изменение документа;
- реакция на действия пользователя;
Очень важно понимать, что DOM - это API, которое удоволетворяет стандарту, то есть каждый браузер старается реализовать стандарт, но бывает возникают некоторые 'проблемы'.
Стандарт можно почитать здесь: http://www.w3.org/DOM/
<html>
<body>
<form>
<input type='text' placeholder='Имя'/>
<input type='text' placeholder='Фамилия'/>
<button type='submit'>Отправить</button>
</form>
</body>
</html>
DOM - это Document Object Model, то есть браузер предоставляет интерпретатору не html страницу ввиде строки, а некоторый object, который связан с документом, отображаемым в браузере. То есть при изменении модели меняется документ, а при изменении документа меняется модель.
Грубо говоря модель - это граф, который состоит из узлов(Node).
Node - это, объект со следующими полями:
- тип узла;
- набор атрибутов;
- ссылка на левого/правого соседа;
- ссылка на родительский узел;
- ссылка на массив 'детей';
Каждый узел имеет тип. Типов очень много, но не все из них одинакого полезные, поэтому укажу только важные:
- ELEMENT_NODE=1 - элемент;
- TEXT_NODE=3 - текст;
- COMMENT_NODE=8 - комментарий
- DOCUMENT_NODE=9 - документ(ссылка на тэг-html)
- DOCUMENT_TYPE_NODE=10 - DOCTYPE
- DOCUMENT_FRAGMENT_NODE=11 - фрагмент
Из списка типов элементов можно понять следующее:
- DOM сохраняет информацию о тексте;
- DOM сохраняет информацию о комментарии;
- DOM содержит особый элемент DOCUMENT_NODE, то есть на корень графа;
Пока оставим за сценой, что такое DOCUMENT_FRAGMENT_NODE, но в конце лекции мы вернемся к нему.
DOCUMENT_NODE - в документе ровно один. Для получение ссылки на DOCUMENT_NODE нужно обратиться к переменную document.
document.nodeType === document.DOCUMENT_NODE;
Давай-те вернемся к нашему основному примеру:
<html>
<body>
<form>
<input type='text' placeholder='Имя'/>
<input type='text' placeholder='Фамилия'/>
<button type='submit'>Отправить</button>
</form>
</body>
</html>
Сколько узлов будет в DOM?
- 6 - элементарных узлов;
- 1 - элемент текста;
Но не все так просто мы забыли посчитать пробельные символы между элементами:
- 6 - элементарных узлов;
- 9 - текстовых узлов;
То есть соседний элемент у 'Фамилия' - не 'Имя', а некоторый текстовый элемент. Поэтому многие разработчики 'минифицируют' свой html перед отправкой его клиенту, удаляя все лишние пробелы и переводы строк.
<html>
<body>
<form>
<input type='text' placeholder='Имя'/>
<input type='text' placeholder='Фамилия'/>
<button type='submit'>Отправить</button>
</form>
</body>
</html>
Прежде чем перейти к чему-то сложному, нужно научиться находить какие-то узлы.
Поставим для себя следующие задачи:
- Найти форму
- Найти текстовые поля
Давай-те разметим наш документ id-ми:
<html>
<body>
<form id='form'>
<input id='name' type='text' placeholder='Имя'/>
<input id='surname' type='text' placeholder='Фамилия'/>
<button id='submit' type='submit'>Отправить</button>
</form>
</body>
</html>
Для поиска по id нужно воспользоваться методом getElementById, который есть только у document.
var form = document.getElementById('form');
var name = document.getElementById('name');
- Подходит для поиска уникальных элементов;
- Очень быстрый;
- Поведение не гарантированно при наличии дупликатов;
Возьмем наш html.
<html>
<body>
<form>
<input type='text' placeholder='Имя'/>
<input type='text' placeholder='Фамилия'/>
<button type='submit'>Отправить</button>
</form>
</body>
</html>
Для поиска по тэгу необходимо воспользоваться методом getElementsByTagName.
var forms = document.getElementsByTagName('form');
var input = document.getElementByTagName('input');
Метод возвращает не массив элементов, а некоторый array-like объект NodeList. NodeList - это живая-коллекция, то есть она меняется при изменении DOM
Из-за своей особенности(живучести), NodeList очень сильно не любят => от него очень часто избавляются по средством превращения array-like объекта в массив:
var inputs = document.getElementByTagName('input');
inputs = [].slice.call(inputs);
Но если вы готовы:
- Помнить, что коллекция может неожиданно измениться
- Кэш коллекции сбрасывается при изменении DOM
- Вы не сможете пользоваться методами массива
Метод getElementsByTagName есть не только у document, но и у всех других элементов, если вы вызываете этот метод не у document, то метод ищет в контексте элемента.
- вы можете найти все тэги за один запрос;
- можно искать в контексте определенного элемента;
- редко нужно искать по тэгам, так как такой код сложно поддерживать
Возьмем наш html и добавим все элементам классы.
<html>
<body>
<form class='b-form'>
<input class='b-form__name' type='text' placeholder='Имя'/>
<input class='b-form__surname' type='text' placeholder='Фамилия'/>
<button class='b-form__submmit' type='submit'>Отправить</button>
</form>
</body>
</html>
Для поиска элементов по классам можно воспользоваться методом getElementsByClassName, который есть так же у всех узлов.
var form = document.getElementsByClassName('b-form')[0];
var name = form.getElementsByClassName('form__name')[0];
- Можно искать сразу много элементов удоволетворяющих одному css селектору;
- Можно гибко размечать нужные элементы;
- Метод не такой быстрый, как getElementById, но не критично.
Для поиска по DOM интерпретатор предоставляет два очень удобных метода:
- querySelector - поиск первого элемента по css-селектору
- querySelectorAll - поиск всех элементов удоволетворяющих css-селектору
var form = document.querySelector('.b-form');
var items = form.querySelectorAll('input');
- метод очень удобный
- метод помедленне всех остальных, но не критично
Мы рекомендуем вам пользоваться им.
Для поиска родительского элемента нужно воспользоваться свойствами:
- parentNode
- parentElement
parentNode - ищет родительский Node узел произвольного типа, а parentElement ищет родительский узел типа отличного от комментарий, текст или DOCTYPE.
Лучше использовать всегда parentElement, или обфуцировать свой html и использовать parentNode.
var formsName = document.querySelector('.b-form__name');
var form = formsName.parentElement;
Метод closest, который есть у всех элементов, позволяет найти ближайшего родителя или самого себя, удоволетворяющего css-селектору.
Если нет отца/самого_себя удоволетворяющего условию, то вернется null
<div class='b-page'>
<div class='controller'></div>
</div>
<div class='b-page'>
<div class='another-controller'></div>
</div>
var controller = document.querySelector('.controller');
var page = controller.closest('.b-page');
Для поиска соседей существует 4ре свойства:
- nextSibling
- nextElementSibling
- previousSibling
- previousElementSibling
next - ищет правого соседа, а previous - левого.
var formsName = document.querySelector('.b-form__name');
var formsSurName = formsName.nextSibling;
formsName === formsSurName.previousSibling;
Для поиска детей есть следующие свойства:
- firstChild - возвращает ссылку на первого ребенка
- firstElementChild
- lastChild - возвращает ссылку на последнего ребенка
- lastElementChild
- childNodes - возвращает live-array со списком детей
- children
var form = document.querySelector('.b-form__name');
var name = form.firsElementChild;
var submit = form.lastElemenChild;
Метод, который есть у всех элементов, проверяет что dom-element подходит под css-селектор.
<div class='b-page'>
<div class='controller current'></div>
</div>
<div class='b-page'>
<div class='another-controller'></div>
</div>
var controller = document.querySelector('.controller');
controller.matches('.controller.current')
атрибуты нужны для двух вещей:
- для настройки нативной работы некоторых html элементов(таких как ссылки)
- для передачи данных на клиент и работы с ними
<html>
<body>
<form class='b-form'>
<input class='b-form__name' type='text' placeholder='Имя'/>
<input class='b-form__surname' type='text' placeholder='Фамилия'/>
<button class='b-form__submmit' type='submit'>Отправить</button>
</form>
</body>
</html>
Для чтения из атрибутов нужно воспользоваться методом getAttribute()
var name = document.querySelector('.b-form__name');
name.getAttribute('placeholder');
Для чтения из атрибутов нужно воспользоваться методом setAttribute()
var name = document.querySelector('.b-form__name');
name.setAttribute('placeholder', 'Имя вашего соседа');
Для чтения из атрибутов нужно воспользоваться методом removeAttribute()
var name = document.querySelector('.b-form__name');
name.removeAttribute('placeholder');
Многие атрибуты отражены в свойствах Node, например:
var name = document.querySelector('.b-form__name');
name.placeholder;
name.className;
name.id;
НО:
- Не у всех атрибутов совпадает имя со свойством. Например, class и className
- значение, находящиеся в атрибуте, может не совпадать со свойством
- при изменении атрибута/свойства синхронизация не всегда гарантирована
- id - Идентификатор объекта, который используется для поиска уникальных элементов.
- className - В js 'class' зарезервированное имя, поэтому разработчики стандарта решили использовать имя className
style - Это свойство используется для задания стилей конкретным элементам. Обычно используется для динамического изменения: width, height, left, right... Если у вас есть какой-то конечный(разумный) набор состояний у html элемента, то лучше менять класс, а style использовать только в крайнем случаи. 4. и другие
Веб-стандарты выделелили под бизнес-лапшу namespace атрибутов. Все атрибуты с префиксом 'data-' считаются пользовательскими => веб-стандарты никогда не выпустят атрибут, который начинается с 'data-' => их можно безопасно использовать для своих нужд.
<div class='page' data-server-time='01/09/2007'>
</div>
var page = document.querySearch('page');
page.getAttribute('data-search-time');
Более того, веб-стандарты создали для нас специальное свойство dataset, которое содержит все пользовательские атрибуты.
Если бы пользовательские атрибуты в словаре dataset именовались так же как в html-разметке, то тогда ими было бы не удобно пользоваться:
var domElement = document.querySelector('.element');
domElement.dataset['data-track-id'];
Поэтому разработчики стандарта решили нам упростить жизнь, и создали отображение html-атрибутов в js-свойства и обратно.
Отображение html-атрибута в js-свойство:
- Берем имя атрибута
- Откусываем префикс 'data-'
- Преобразовываем к верхнему регистру символы следующие за '-'
- Удаляем символы '-'
Отображение js-свойства в html
- Берем имя свойства
- Ищем все символы в верхнем регистре (*с 1ой позиции)
- Вставляем перед ним '-'
- Опускаем строку в нижний регистр и прибавляем префикс 'data-
<div class="track-list">
<div class="track-list__item" data-duration="2m30s">...</div>
</div>
var trackList = document.querySelector('.track-list');
var tracks = [].slice.apply(trackList.querySelectorAll('.trackList__item'));
tracks.forEach(function (track) {
console.log(track.dataset.duration);
});
Между data-атрибутами и dataset существует 'связь', то есть при изменении атрибута меняется свойство, а при изменении свойства изменяется атрибут.
var item = document.querySelector('.track-list__item');
item.dataset.url = 'https://music.yandex.ru/album/2754/track/21168'
item.getAttribute('data-url') === item.dataset.url;
item.setAttribute('data-artist') = 'Marilyn Manson'
item.getAttribute('data-artist') === item.dataset.artist;
При разработке интерфейсов многие компоненты имеют визуальные состояния:
- Модальное окно открыто/закрыто
- Элемент когда-то нажимали или нет
- Любимый трэк пользователя или нет
Для изменение визуального состояния страницы можно поступать следующими способами:
- Отправить на другую страницу с измененным отображением(старая школа)
- Удалить часть элементов из html
- Изменить style у нужных элементов
- Изменить class
Способы 1-3 хороши, но давай-те поговорим о 4.
class - это тоже атрибут, для его изменения вы можете изменить этот атрибут:
var track = document.querySelector('.track');
track.setAttribute('class', 'track track_lovely_yes');
Тут нужно обратить внимание, что при изменении class через атрибут вы работаете со строкой => вам все операции по работе с классами нужно писать самим:
- удаление класса
- замена класса
- добавление класса
В качестве домашнего упражнения можете написать мальнькие функции, которые это делают.
Так же менять класс можно через свойство className
var track = document.querySelector('.track');
track.className = 'track track_lovely_yes';
Тут такая же ситуация, как и с setAttribute.
После такой работы с классами кажется, что веб очень-очень странный, но функции для изменения класса пишутся довольно легко.
Шли годы и разработчики стандарта решили уделить время созданию более удобному интерфейсу для работы с классами, так как пользовательский сценарий по изменению класса довольно частый.
classList - это свойство, которое есть у всех dom-element.
Для добавления нового класса у элемента нужно воспользоваться методом add:
var track = document.querySelector('.track');
track.classList.add('track_lovely_yes');
track.className;
Для удаления класса из элемента нужно воспользоваться методом remove:
var track = document.querySelector('.track');
track.classList.remove('track_lovely_yes');
track.className;
Для замены класса из элемента нужно воспользоваться методом toggle:
var track = document.querySelector('.track');
track.classList.toggle('hidden', 'shown');
track.className;
Для проверки на наличия класса нужно воспользоваться методом contain:
var track = document.querySelector('.track');
track.classList.contain('track');
Когда-то давным давно чтобы изменить страницу нужно было перейти по url с измененной страницей.
Но темные времена прошли и веб стал очень динамичным. При работе с сайтом dom частенько меняется:
- открытие модальных окон
- создание таблиц
- бесконечные ленты
- обновление страниц без перезагрузки данных
Поэтому очень важно уметь менять dom.
У объекта страницы есть метод createElement, который принимает название tag нового элемента и создает новый element с вашим тэгом.
var track = document.createElement('div');
track.className = 'track'
Когда вы создаете новый элемент, то он создается в "вакууме" и нигде не отображается. Чтобы он стал виден на странице его нужно добавить к одному из элементу, который уже отображен на странице.
Для создание текста нужно воспользоваться методом createTextNode
var text = document.createTextNode('Ваша любимая песня');
Мало создать элемент, но нужно его еще добавить к существующему dom, иначе от него мало толку.
Для этого существует несколько способоов.
Находим какой-то элемент и добавляем справа к его детям новый элемент.
var track = document.createElement('div');
var label = document.createElement('div');
track.appendChild(label);
Находим элемент и вставляем перед ним.
var track = document.createElement('div');
var label = document.createElement('div')
var duration = document.createElement('div')
track.appendChild(duration);
track.insertBefore(label, duration);
Находим старый элемент, заменяем на новый
var track = document.createElement('div');
var label = document.createElement('div');
track.appendChild(track);
// какой-то код
track.replaceChild(document.createTextNode('Ваша любимая песня'), label);
Все способы до этого кажутся очень кропотливыми и если нам предстоит задача по json нарисовать страничку на клиенте, то такая задача кажется непосильной. Поэтому есть свойство innerHTML, которое позволяет создавать новые элементы проще.
Это свойство позволяет получить html детей в виде строки.
var html = document.innerHTML;
Так же в свойство innerHTML можно присваивать новый html:
var track = document.createElement('div');
track.innerHTML = "<div class='label'>Marilyn Manson</div><div class='star'>5</div>"
Но innerHTML опасное свойство, так как это свойство не экранирует html, что очень даже логично, но это является уязвимостью. Например, XSS.
DOM элементы содержат в себе некоторый аналог innerHTML, а именно, innerText.
innerText - это свойство, которое позволяет заменить все содержимое элемента на заэкранированный текст, то есть если вы вставить некий html-код, то вы на странице увидете код, а не html-элементы.
var track = document.createElement('div');
track.innerText = 'Пользовательский ввод';
Все предыдущие способы по генерации нового html содержимого кажутся 'неприятными'. И я с вами полностью согласен. Поэтому умные js разработчики написали кучу шаблонизаторов, которые позволяют создавать html ввиде строки значительно приятнее, а затем вы можете вставить этот html 5 способом.
https://garann.github.io/template-chooser/
Маленькое домашнее задание придумать свой шаблонизатор! Больше шаблонизаторов!
Для удаления html элементов надо воспользоваться функцией removeChild
var track = document.querySelector('.track');
var label = track.querySelector('.label');
track.removeChild(label);
Бывает, нам нужно добавить не одного ребенка, а сразу N
Берем метод appendChild и цикл for
var track = document.querySelector('.track');
for(var i = 0; i < 10; i++) {
var label = document.createElement('div');
track.appendChild(label)
}
Но важно помнить, что каждый раз когда вы изменяете dom у вас запускаются внутренние механихмы браузера по обновлению страницы. Следовательно, есть правило хорошего тона: 'Трогать дом можно, но чуть-чуть';
Создаем какой-то dom-элемент, вставляем в него новые элементы и, вставляем в dom.
var track = document.querySelector('.track');
var container = document.createElement('div');
for(var i = 0; i < 10; i++) {
var label = document.createElement('div');
container.appendChild(label);
}
track.appendChild(container);
Этот способ хорош с точки зрения вставки пачки элементов в DOM, но у вас создается лишний элемент, а мусор в доме держать плохо.
Для решение этой проблемы есть специальный тип Node - DOCUMENT_FRAGMENT.
DocumentFragment - растворяется в DOM при присоединении его к отображаемым элементамю.
Для создание DocumentFragment нам нужно воспользоваться методом createDocumentFragment у document
var track = document.querySelector('.track');
var container = document.createDocumentElement();
for(var i = 0; i < 10; i++) {
var label = document.createElement('div');
container.appendChild(label);
}
track.appendChild(container);
Когда-то давным давно люди думали, что html-страницы нужны только для публикации научных статей, но время шло и людям захотелось не только смотреть на текст + картинки, но и как-то взаимодействовать со страницей. Для этого браузер оснастили механизмом событий, а именно, способностью сообщать клиентскому коду, что на странице что-то произошло. Например:
- пользователь тыкнул на кнопку
- страница загрузилась
- пользователь начал скролить и так далее.
Сам механизм события довольно сложный, поэтому мы о нем поговорим позже.
Как обычно в js существует несколько способов
Подписать на событие можно с помощью html-атрибута. Для всех основных событий имя атрибута: on + имя событие(onclick, onblur, ...) .Обработчик события записываем в виде строки в разметку или записываем имя глобальной функции
<button onclick='onClickByButton()'>First</button>
<button onclick='(function(){alert(2);})()'>Second</button>
function onClickByButton() {
alert(1);
}
- Можно добавить только один обработчик;
- Смешиваем отображение с реализацией;
- Глобальные функции;
- Самый простой вариант;
В js у всех dom-element есть свойства on + имя событие. Чтобы подписаться на событие через js нужно присвоить функцию обработчик в атрибут.
Чтобы отписаться от события нужно присвоить в атрибут null.
<button >First</button>
var button = document.querySelector('button');
button.onclick = function () {
console.log('Click!!!');
}
- Можно добавить только один обработчик;
- Самый простой вариант;
Разработчики веб стандарта предлагают пользоваться методом addEventListener, который есть у всех dom-element. Метод принимает три аргумента:
- eventName имя событие
- listener - ваш обработчик
- useCapture - стадия активации обработчика, о которой мы поговорим позже.
<button >First</button>
var button = document.querySelector('button');
var callback = function (event) {
alert('Click')
}
button.addEventListener('click', callback, false);
Обработчик событий принимает объект event, который в себе содержит всю информацию о произошедшем событии.
- target - на каком элементе сработало событие
- currentTarget - на каком элементе в данный момент обрабатывается событие
- type - название события
- и другие свойства, которые специфичны для различных событий
У метода addEventListener есть напарник removeEventListener(), который имеет такую же сигнатуру.
<button >First</button>
var button = document.querySelector('button');
var callback = function () {
alert('Click')
};
button.addEventListener('click', callback, false);
button.removeEventListener('click', callback, false);
Важно обратить внимание, что при отписке от события нужно передать туже функцию.
Следующий код не будет работать:
var button = document.querySelector('button');
button.addEventListener('click', function () {
alert('Click')
}, false);
button.removeEventListener('click', function () {
alert('Click')
}, false);
Зачем может понадобиться отписываться:
- при изменении состояния приложения(например, контрол заблокирован)
Вот и настал момент, когда мы примерно представляем интерфейс и мы можем поговорить, о том как устроен механизм обработки событий.
Когда событие возникает на dom елементе, то события вовзникает не только на нем но и на всех родителях причем. Но обо все по порядку.
Механизм:
- Кто-то создал событие (например, тыкнул мышкой по кнопке)
- Берем спискок родителей
- Начинаем стадию захвата
- Бежим в цикле по родителям начиная с самого дальнего
-
- Вызываем обработчики, которые подписаны на стадию захвата
- Начинаем стадию цели
- Вызываем все обработчики, которые подписаны не важно на какую стадию
- Начинаем стадию всплытия
- Бежим в цикле по родителям начиная с самого ближнего
-
- Вызываем обработчики, которые подписаны на стадию всплытия
Что мы узнали из механизма:
- addEventListener - последним параметром принимает стадию захвата/всплытие
- Список элементов на которых будут вызваны обработчики фиксируется при возникновении события
- Событие на стадии захвата происходят чуть быстрее(но по историческим причинам все используют стадию всплытия)
- На цели обработчики вызываются в произвольном порядке.
10 - пунктов в алгоритме очень тяжело поэтому давайте рассмотрим пример
http://jsfiddle.net/851kwgnv/1/
Так давай-те тыкнем по 'красному квадрату':
- target=red
- parents = [html, body, container]
- Вызываем обработчики захвата на html -> body -> container
- Вызываем обработчики цели на red
- Вызываем обработчики стадии всплытия container -> body -> html
Затем давай-те тыкнем по 'синему квадрату':
- target=blue
- parents = [html, body, container, red]
- Вызываем обработчики захвата на html -> body -> container->red
- Вызываем все обработчики на blue
- Вызываем обработчики стадии всплытия red -> container -> body -> html
Затем давай-те тыкнем по 'зеленому квадрату':
- target=green
- parents = [html, body, container, red, blue]
- Вызываем обработчики захвата на html -> body -> container->red-> blue
- Вызываем все обработчики на blue
- Вызываем обработчики стадии всплытия blue -> red -> container -> body -> html
Механизм событий позволяет делегировать обработку событий родителю. Для этого нам нужно сделать следующее:
- Найти родителя;
- подписаться на нужное событие в родителе на стадию всплытия;
- в обработчике отсеять не нужные элементы;
Как будем отсеивать. Все обработчики события вызывают с параметром event. Параметр event - это объект в котором есть куча полезных вещей:
- currentTarget - dom-elem на котором вызван обработчик (совпадает с this)
- target - элемент, по которому реально тыкнули
- и другие полезные параметры
Тогда делегирование событий может быть реализованно как-то так:
<form>
<button data-number=1>Первый</button>
<button data-number=2>Второй</button>
<button data-number=3>Третий</button>
</form>
var button = document.querySelector('form');
button.addEventListener('click', function (event) {
if (event.target.tagName !== 'BUTTON') {
return;
}
console.log(event.target.dataset.number);
}, false);
Но возникаем проблема, когда в элементе с которого мы хотим делигировать событие есть вложенные элементы
<form>
<button data-number=1><span>Первый</span></button>
<button data-number=2><span>Второй</span></button>
<button data-number=3><span>Третий</span></button>
</form>
var button = document.querySelector('form');
button.addEventListener('click', function (event) {
if (event.target.tagName !== 'BUTTON') {
return;
}
console.log(event.target.dataset.number);
}, false);
Проблема в том, что target будет указывать на кликнутый элемент, то есть на самый вложенные, в нашем случае span. Чтобы эту проблему решить нужно маленько 'прокачать' наш обработчик:
<form>
<button data-number=1><span>Первый</span></button>
<button data-number=2><span>Второй</span></button>
<button data-number=3><span>Третий</span></button>
</form>
var button = document.querySelector('form');
button.addEventListener('click', function (event) {
var ourTarget = event.target.closest('button');
if (!ourTarget) {
return;
}
console.log(ourTarget.dataset.number);
}, false);
Что мы сделали:
- взяли элемент по которому тыкнули (event.target)
- нашли от него ближайший родитель/самого себя, нашей реальной цели
- если реальная цель не нашлась, то беда и тыкнули по чему-то другому
- иначе мы нашли что надо.
Ясное дело, что постоянно писать такие обработчики накладно, поэтому рекондую вам написать функцию delegate, которая принимает:
-
parentTarget - элемент на который делегируют ответственность за обработку событий
-
selector - селектор детей с которых делегируется ответственность
-
listener - обработчик
-
Не нужно подписывать на каждый элемент при добавлении.
-
Не нужно отписываться от событий при удалении элементов.
-
Один обработчик вместо N.
-
Обработчик становится чуть сложнее.
-
Обработчик будет срабатывать, когда мог бы не срабатывать.
Я рекомендую иметь на вооружении этот инструмент, так как он помогает избавиться от кучи проблем.
Так же вам может показаться, что было бы не плохо повешать delegate на body, чтобы иметь минимальное количество обработчиков или вообще один. Но при таком подходе обработчик будет срабатывать очень часто и это может повлиять на производитьльность. Но такой подход используют некоторые framework.
Бывает в одном из обработчиков мы понимаем, что мы все обработали и вызов дальнейших обработчиков не нужен. Тогда нам на помощь приходитм метод stopPropagation, который есть у объекта event.
<div class='container'>
<div class='block'></div>
</div>
var container = document.querySelector('.container');
var block = document.querySelector('.block');
block.addEventListener('click', function (event) {
console.log(1);
event.stopPropagation();
}, false);
container.addEventListener('click', function (event) {
console.log('этот текст никогда не распечатается');
}, false);
!!!stopPropagation полность останавливает обработку события.
Некоторые html-элементы имеют некоторое действие по умолчанию, при возникновении определенных событий. Например, ссылка при клике осуществляет переход по ссылке.
Чтобы этого избежать у объекта event есть метод preventDefault.
<a class='link' href='yadnex.ru'>
какая классная ссылка
</a>
var link = document.querySelector('.link');
link.addEventListener('click', function (event) {
event.preventDefault();
}, false);