# Графические компоненты Первый и главный шаг в наведении порядка -- это оформить код в объекты, каждый из которых будет решать свою задачу. Здесь мы сосредоточимся на графических компонентах, которые также называют "виджетами". В браузерах есть встроенные виджеты, например `` и другие элементы, о которых мы даже и не думаем, "как они работают". Они "просто работают": показывают значение, вызывают события... Наша задача -- сделать то же самое на уровне выше. Мы будем создавать объекты, которые генерируют меню, диалог или другие компоненты интерфейса, и дают возможность удобно работать с ними. ## Виджет Menu Мы начнём работу с виджета, который предусматривает уже готовую разметку. То есть, в нужном месте HTML находится DOM-структура для меню -- заголовок и список опций: ```html ``` Далее она может дополняться, изменяться, но в начале -- она такая. Обратим внимание на важные соглашения виджета:
Вся разметка заключена в корневой элемент `
Это очень удобно: вынул этот элемент из DOM -- нет меню, вставил в другое место -- переместил меню. Кроме того, можно удобно искать подэлементы.
Внутри корневого элемента -- только классы, не `id`.
Документ вполне может содержать много различных меню. Они не должны конфликтовать между собой, поэтому для разметки везде используются классы. Исключение -- корневой элемент. В данном случае мы предполагаем, что данное конкретное "меню сладостей" в документе только одно, поэтому даём ему `id`.
# Класс виджета Для работы с разметкой будем создавать объект `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"] Это, конечно, только первый шаг, но уже здесь видны некоторые важные соглашения в коде.
У конструктора только один аргумент -- объект `options`.
Это удобно, так как у графических компонентов обычно много настроек, большинство из которых имеют разумные значения "по умолчанию". Если передавать аргументы через запятую -- их будет слишком много.
Обработчики назначаются через делегирование.
Вместо того, чтобы найти элемент и поставить обработчик на него: ```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`, без необходимости переставлять обработчик.
В этот код лучше добавить дополнительную проверку на то, что найденный `.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 добавим меню три метода: Функция генерации корневого элемента с заголовком `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; } ``` Отметим некоторые особенности этого кода.
Обработчики отделяются от реальных действий.
В обработчике `onclick` мы "ловим" событие и выясняем, что именно произошло. Возможно, нужно проверить `event.target`, координаты, клавиши-модификаторы, и т.п. Это всё можно делать здесь же. Выяснив, что нужно сделать, обработчик `onclick` не делает это сам, а вызывает для этого соответствующий метод. Этот метод уже не знает ничего о событии, он просто делает что-то с виджетом. Его можно вызвать и отдельно, не из обработчика. Здесь есть ряд важных плюсов:
Генерация DOM, по возможности, должна быть "ленивой".
Мы стараемся откладывать работу до момента, когда она реально нужна. Например, когда `new Menu` создаётся, то переменная `elem` лишь объявляется. DOM-дерево будет сгенерировано только при вызове `getElem()` функцией `render()`. Более того! Пока меню закрыто -- достаточно заголовка. Кроме того, возможно, посетитель вообще никогда не раскроет это меню, так зачем генерировать список раньше времени? А при первом открытиии `open()` вызовет функцию `renderItems()`, которая специально для этого выделена отдельно от `render()`. **Фаза инициализации очень чувствительна к производительности, так как обычно в сложном интерфейсе создаётся много всего.** Если изначально подходить к оптимизации на этой фазе "спустя рукава", то потом поправить долгий старт может быть сложно. Тем более, что инициализация -- это фундамент, начало работы виджета, её оптимизация в будущем может потребовать сильных изменений кода. Конечно, здесь, как и везде в оптимизации -- без фанатизма. Бывают ситуации, когда гораздо удобнее что-то сделать сразу. Если это один элемент, то оптимизация здесь ни к чему. А если большой фрагмент DOM, который, как в случае с меню, прямо сейчас не нужен -- то лучше отложить.
В действии: [codetabs src="menu-dom" height="200"] ## Итого Мы начали создавать компонент "с чистого листа", пока без дополнительных библиотек. Основные принципы: