Компоненты, хоть и каждый сам по себе, обычно как-то общаются с остальной частью страницы
Есть несколько способов, при помощи которых компоненты сообщают друг другу о важных событиях, которые в них произошли.
Колбэки
Колбэк (от англ. callback) – это функция, которую мы передаём куда-либо и ожидаем, что она будет вызвана при наступлении события.
Например, мы можем добавить в options
для Menu
новый параметр – функцию onselect
, которая будет вызываться при выборе пункта меню:
var menu = new Menu({
title: "Сладости",
template: _.template(document.getElementById('menu-template').innerHTML),
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML,
items: {
"donut": "Пончик",
"cake": "Пирожное",
"chocolate": "Шоколадка"
},
onselect: showSelected
});
function showSelected(href) {
alert(href);
}
В коде меню нужно будет вызывать её, например так:
...
function select(link) {
options.onselect(link.getAttribute('href').slice(1));
...
}
...
Полный пример:
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
var html = options.template({
title: options.title
});
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.onmousedown = function() {
return false;
}
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
if (event.target.closest('a')) {
event.preventDefault();
select(event.target.closest('a'));
}
}
}
function renderItems() {
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({
items: options.items
});
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function select(link) {
options.onselect(link.getAttribute('href').slice(1));
}
function open() {
renderItems();
elem.classList.add('open');
};
function close() {
elem.classList.remove('open');
};
function toggle() {
if (elem.classList.contains('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
<script type="text/template" id="menu-template">
<div class="menu">
<span class="title"><%-title%></span>
</div>
</script>
<!--
встроенная браузерная функция encodeURIComponent кодирует спец-символы для URL,
например русские буквы и пробелы
в этом примере русских букв в ключах items нет, но потенциально они возможны
-->
<script type="text/template" id="menu-list-template">
<ul>
<% for(var name in items) { %>
<li>
<a href="#<%=encodeURIComponent(name)%>">
<%-items[name]%>
</a>
</li>
<% } %>
</ul>
</script>
<script>
var menu = new Menu({
title: "Сладости",
template: _.template(document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML.trim()),
items: {
cake: "Торт", // cake <a href="#cake">Торт</a>
donut: "Пончик", // donut
chocolate: "Шоколадка" // chocolate
},
onselect: showSelected
});
function showSelected(itemName) {
alert(itemName);
}
document.body.appendChild(menu.getElem());
</script>
</body>
</html>
Свои события
Как мы уже знаем, в современных браузерах DOM-элементы могут генерировать произвольные события при помощи встроенных методов, а в IE8- это возможно с использованием фреймворка, к примеру, jQuery.
Воспользуемся ими, чтобы корневой элемент меню генерировал событие, которое мы назовём select
, при выборе элемента, и передавал в объект события выбранное значение.
Для этого модифицируем функцию select
:
function Menu(options) {
...
function select(link) {
var widgetEvent = new CustomEvent("select", {
bubbles: true,
// detail - стандартное свойство CustomEvent для произвольных данных
detail: link.getAttribute('href').slice(1)
});
elem.dispatchEvent(widgetEvent);
}
...
}
Код, который заинтересован в том, чтобы узнавать, что выбрано в меню, подписывается на событие select
его корневого элемента:
var menu = new Menu(...);
var elem = menu.getElem();
elem.addEventListener('select', function(event) {
alert( event.detail );
});
Вместо detail
можно было бы выбрать и другое название свойства, но тогда нужно позаботиться о том, чтобы оно не конфликтовало со стандартными. Кроме того, в конструкторе CustomEvent
разрешено только detail
, другое свойство понадобилось бы присваивать в отдельной строке.
Полный пример:
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
var html = options.template({
title: options.title
});
elem = document.createElement('div');
elem.innerHTML = html;
elem = elem.firstElementChild;
elem.onmousedown = function() {
return false;
}
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
if (event.target.closest('a')) {
event.preventDefault();
select(event.target.closest('a'));
}
}
}
function renderItems() {
if (elem.querySelector('ul')) return;
var listHtml = options.listTemplate({
items: options.items
});
elem.insertAdjacentHTML("beforeEnd", listHtml);
}
function select(link) {
var widgetEvent = new CustomEvent("select", {
bubbles: true,
detail: link.getAttribute('href').slice(1)
});
elem.dispatchEvent(widgetEvent);
}
function open() {
renderItems();
elem.classList.add('open');
};
function close() {
elem.classList.remove('open');
};
function toggle() {
if (elem.classList.contains('open')) close();
else open();
};
this.getElem = getElem;
this.toggle = toggle;
this.close = close;
this.open = open;
}
.menu ul {
display: none;
margin: 0;
}
.menu .title {
font-weight: bold;
cursor: pointer;
}
.menu .title:before {
content: '▶';
padding-right: 6px;
color: green;
}
.menu.open ul {
display: block;
}
.menu.open .title:before {
content: '▼';
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="menu.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
<script src="https://cdn.polyfill.io/v1/polyfill.js?features=CustomEvent,Element.prototype.closest"></script>
<script src="menu.js"></script>
</head>
<body>
<script type="text/template" id="menu-template">
<div class="menu">
<span class="title"><%-title%></span>
</div>
</script>
<script type="text/template" id="menu-list-template">
<ul>
<% for(var name in items) { %>
<li>
<a href="#<%=encodeURIComponent(name)%>">
<%-items[name]%>
</a>
</li>
<% } %>
</ul>
</script>
<script>
var menu = new Menu({
title: "Сладости",
template: _.template(document.getElementById('menu-template').innerHTML.trim()),
listTemplate: _.template(document.getElementById('menu-list-template').innerHTML.trim()),
items: {
cake: "Торт", // cake <a href="#cake">Торт</a>
donut: "Пончик", // donut
chocolate: "Шоколадка" // chocolate
}
});
var elem = menu.getElem();
document.body.appendChild(elem);
elem.addEventListener('select', function(event) {
alert(event.detail);
});
</script>
</body>
</html>
Очень важно, что внешний код ставит обработчик на корневой элемент, но не на внутренние элементы меню.
Строго говоря, он вообще не знает про то, как устроено меню, есть ли там ссылки и какие, или там вообще всё реализовано через кнопки.
Меню для него – «чёрный ящик». Корневой элемент – точка доступа к его функциональности. Событие – не то, которое произошло на ссылке, а «переработанный вариант», интерпретация действия со стороны меню.
Такое правило позволяет нам не опасаться проблем при оптимизации, расширении и даже полной переделке DOM-структуры меню. Коль скоро события и методы сохраняются, внешний код будет работать как прежде.
Ещё раз – внешний код не имеет права залезать внутрь DOM-структуры меню, ставить там обработчики и так далее.
Итого
Для того, чтобы внешний код мог узнавать о важных событиях, произошедших внутри компонента, используются:
- Колбэки – функции, которые передаются «снаружи» при создании компонента, и которые он обязуется вызвать при наступлении событий.
- События – компонент генерирует их на корневом элементе при помощи
dispatchEvent
, а внешний код ставит обработчики при помощиaddEventListener
. Такие события всплывают, если указан флагbubbles
, поэтому с ними можно использовать делегирование.