en.javascript.info/2-ui/5-widgets/2-widgets-structure/article.md
2015-02-18 21:23:40 +03:00

309 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Графические компоненты
Первый и главный шаг в наведении порядка -- это оформить код в объекты, каждый из которых будет решать свою задачу.
Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами".
В браузерах есть встроенные виджеты, например `<select>`, `<input>` и другие элементы, о которых мы даже и не думаем, "как они работают". Они "просто работают": показывают значение, вызывают события...
Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними.
## Виджет Menu
Мы начнём работу с виджета, который предусматривает уже готовую разметку.
То есть, в нужном месте HTML находится DOM-структура для меню -- заголовок и список опций:
```html
<div class="menu" id="sweets-menu">
<span class="title">Сладости</span>
<ul>
<li>Торт</li>
<li>Пончик</li>
<li>...</li>
</ul>
</div>
```
Далее она может дополняться, изменяться, но в начале -- она такая.
Обратим внимание на важные соглашения виджета:
<dl>
<dt>Вся разметка заключена в корневой элемент `<div class="menu" id="sweeties-menu">`.</dt>
<dd>Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.</dd>
<dt>Внутри корневого элемента -- только классы, не `id`.</dt>
<dd>Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы.
Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.</dd>
</dl>
# Класс виджета
Для работы с разметкой будем создавать объект `new Menu` и передавать ему корневой элемент. В конструкторе он поставит необходимые обработчики:
```js
function Menu(options) {
var elem = options.elem;
elem.onmousedown = function() { return false; }
elem.onclick = function(event) {
if (event.target.closest('.title')) {
elem.classList.toggle('open');
}
};
}
// использование
var menu = new Menu({
elem: document.getElementById('sweets-menu')
});
```
Меню:
[codetabs src="menu"]
Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
<dl>
<dt>У конструктора только один аргумент -- объект `options`.</dt>
<dd>Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения "по умолчанию". Если передавать аргументы через запятую -- их будет слишком много.</dd>
<dt>Обработчики назначаются через делегирование.</dt>
<dd>Вместо того, чтобы найти элемент и поставить обработчик на него:
```js
var titleElem = elem.querySelector('.title');
titleElem.onclick = function() {
elem.classList.toggle('open');
}
```
...Мы ставим обработчик на корневой `elem` и используем делегирование:
```js
elem.onclick = function(event) {
if (event.target.closest('.title')) {
elem.classList.toggle('open');
}
};
```
Это ускоряет инициализацию, так как не надо искать элементы, и даёт возможность в любой момент менять DOM внутри, в том числе через `innerHTML`, без необходимости переставлять обработчик.
</dd>
</dl>
В этот код лучше добавить дополнительную проверку на то, что найденный `.title` находится внутри `elem`:
```js
elem.onclick = function(event) {
var closestTitle = event.target.closest('.title');
if (closestTitle && elem.contains(closestTitle)) {
elem.classList.toggle('open');
}
};
```
## Публичные методы
Уважающий себя компонент обычно имеет публичные методы, которые позволяют управлять им снаружи.
Рассмотрим повнимательнее этот фрагмент:
```js
if (event.target.closest('.title')) {
elem.classList.toggle('open');
}
```
Здесь в обработчике события сразу код работы с элементом. Пока одна строка -- всё понятно, но если их будет много, то при чтении понадобится долго и упорно вникать: "А что же, всё-таки, такое делается при клике?"
Для улучшения читаемости выделим обработчик в отдельную функцию `toggle`, которая к тому же станет полезным публичным методом:
```js
function Menu(options) {
var elem = options.elem;
elem.onmousedown = function() { return false; }
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
};
function toggle() {
elem.classList.toggle('open');
}
this.toggle = toggle;
}
```
Теперь метод `toggle` можно использовать и снаружи:
```js
var menu = new Menu(...);
menu.toggle();
```
## Генерация DOM-элемента
До этого момента меню "оживляло" уже существующий HTML.
Но далеко не всегда в HTML уже есть готовая разметка. В сложных интерфейсах намного чаще её нет, а есть данные, на основе которых компонент генерирует разметку.
В случае меню, данные -- это набор пунктов меню, которые передаются конструктору.
Для генерации DOM добавим меню три метода:
<ul>
<li>`render()` -- генерирует корневой DOM-элемент и заголовок меню.</li>
<li>`renderItems()` -- генерирует DOM для списка опций `ul/li`.</li>
<li>`getElem()` -- возвращает DOM-элемент меню, при необходимости запуская генерацию, публичный метод.</li>
</ul>
Функция генерации корневого элемента с заголовком `render` отделена от генерации списка `renderItems`. Почему -- будет видно чуть далее.
Новый способ использования меню:
```js
*!*
// создать объект меню с данным заголовком и опциями
*/!*
var menu = new Menu({
title: "Сладости",
items: [
"Торт",
"Пончик",
"Пирожное",
"Шоколадка",
"Мороженое"
]
});
*!*
// получить сгенерированный DOM-элемент меню
*/!*
var elem = menu.getElem();
*!*
// вставить меню в нужное место страницы
*/!*
document.body.appendChild(elem);
```
Код `Menu` с новыми методами:
```js
function Menu(options) {
var elem;
function getElem() {
if (!elem) render();
return elem;
}
function render() {
elem = document.createElement('div');
elem.className = "menu";
var titleElem = document.createElement('span');
elem.appendChild(titleElem);
titleElem.className = "title";
titleElem.textContent = options.title;
elem.onmousedown = function() {
return false;
};
elem.onclick = function(event) {
if (event.target.closest('.title')) {
toggle();
}
}
}
function renderItems() {
var items = options.items || [];
var list = document.createElement('ul');
items.forEach(function(item) {
var li = document.createElement('li');
li.textContent = item;
list.appendChild(li);
});
elem.appendChild(list);
}
function open() {
if (!elem.querySelector('ul')) {
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;
}
```
Отметим некоторые особенности этого кода.
<dl>
<dt>Обработчики отделяются от реальных действий.</dt>
<dd>В обработчике `onclick` мы "ловим" событие и выясняем, что именно произошло. Возможно, нужно проверить `event.target`, координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же.
Выяснив, что нужно сделать, обработчик `onclick` не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика.
Здесь есть ряд важных плюсов:
<ul>
<li>Обработчик `onclick` не "распухает" чрезмерно.</li>
<li>Код гораздо лучше читается.</li>
<li>Метод можно повторно использовать, в том числе и сделать публичным, как в коде выше.</li>
</ul>
</dd>
<dt>Генерация DOM, по возможности, должна быть "ленивой".</dt>
<dd>Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()` функцией `render()`.
Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытиии `open()` вызовет функцию `renderItems()`, которая специально для этого выделена отдельно от `render()`.
**Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.**
Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить долгий старт может быть сложно. Тем более, что инициализация -- это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода.
Конечно, здесь, как и везде в оптимизации -- без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен -- то лучше отложить.
</dd>
</dl>
В действии:
[codetabs src="menu-dom" height="200"]
## Итого
Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек.
Основные принципы:
<ul>
<li>Виджет -- это объект, который либо контролирует готовое дерево DOM, либо создаёт своё.</li>
<li>В конструктор виджета передаётся объект аргументов `options`.</li>
<li>Виджет при необходимости создаёт элемент или "оживляет" готовый. Внутре в разметке не используются `id`.</li>
<li>Обработчики назначаются через делегирование -- для производительности и упрощения виджета.</li>
<li>Обработчики событий вызывают соответствующий метод, не пытаются делать всё сами.</li>
<li>При инициализации, если существенный участок работы можно отложить до реального задействования виджета -- откладываем его.</li>
</ul>