Обновление 12.05.2016: После некоторого обсуждения в комментариях было написано второе сообщение, посвященное недостаткам этого — Как сделать доступные веб-компоненты . Пожалуйста, не забудьте прочитать это тоже.
Эта статья была рецензирована Райаном Льюисом . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Веб-приложения с каждым днем становятся все сложнее и требуют много разметки, скриптов и стилей. Для управления и поддержки сотен килобайт HTML, JS и CSS мы пытаемся разделить наше приложение на повторно используемые компоненты. Мы стараемся инкапсулировать компоненты и предотвращать конфликты стилей и скриптов.
В конце исходный код компонента распределяется между несколькими файлами: файлом разметки, файлом сценария и таблицей стилей. Еще одна проблема, с которой мы можем столкнуться — это длинная разметка, загроможденная div
s и span
s. Этот вид кода слабо выражен и также трудно обслуживаем. Чтобы решить и попытаться решить все эти проблемы, W3C представила веб-компоненты.
В этой статье я собираюсь объяснить, что такое веб-компоненты и как их можно создать самостоятельно.
Встречайте веб-компоненты
Веб-компоненты решают все эти вопросы, обсуждаемые во введении. Используя веб-компоненты, мы можем связать один HTML-файл, содержащий реализацию компонента, и использовать его на странице с пользовательским элементом HTML. Они упрощают создание компонентов, усиливают инкапсуляцию и делают разметку более выразительной.
Веб-компоненты определяются набором спецификаций:
- Пользовательские элементы : позволяют зарегистрировать пользовательский значимый HTML-элемент для компонента
- HTML-шаблоны : определить разметку компонента
- Shadow DOM : инкапсулирует внутренние компоненты компонента и скрывает его от страницы, на которой он используется
- HTML Imports : предоставляет возможность включить компонент на целевую страницу.
Описав, что такое веб-компоненты, давайте посмотрим на них в действии.
Как создать готовый к работе веб-компонент
В этом разделе мы собираемся создать полезный многоэлементный виджет, который будет готов к использованию в производстве. Результат можно найти на этой демонстрационной странице, а весь исходный код можно найти на GitHub .
Требования
Прежде всего, давайте определим некоторые требования к нашему многоэлементному виджету.
Разметка должна иметь следующую структуру:
<x-multiselect placeholder="Select Item"> <li value="1" selected>Item 1</li> <li value="2">Item 2</li> <li value="3" selected>Item 3</li> </x-multiselect>
Пользовательский элемент <x-multiselect>
имеет атрибут placeholder
для определения заполнителя пустого множественного выбора. Элементы определяются элементами <li>
поддерживающими value
и selected
атрибуты.
Множественный выбор должен иметь метод API selectedItems
возвращающий массив выбранных элементов.
// returns an array of values, eg [1, 3] var selectedItems = multiselect.selectedItems();
Кроме того, виджет должен инициировать change
события каждый раз, когда change
выбранные элементы.
multiselect.addEventListener('change', function() { // print selected items to console console.log('Selected items:', this.selectedItems()); });
Наконец, виджет должен работать во всех современных браузерах.
шаблон
Мы начинаем создавать файл multiselect.html
который будет содержать весь исходный код нашего компонента: разметку HTML, стили CSS и код JS.
HTML-шаблоны позволяют нам определять шаблон компонента в специальном HTML-элементе <template>
. Вот шаблон нашего мультиселектора:
<template id="multiselectTemplate"> <style> /* component styles */ </style> <!-- component markup --> <div class="multiselect"> <div class="multiselect-field"></div> <div class="multiselect-popup"> <ul class="multiselect-list"> <content select="li"></content> </ul> </div> </div> </template>
Разметка компонента содержит поле множественного выбора и всплывающее окно со списком элементов. Мы хотим, чтобы multiselect получал элементы прямо из пользовательской разметки. Мы можем сделать это с помощью нового HTML-элемента <content>
( вы можете найти больше информации об элементе content
в MDN ). Он определяет точку вставки разметки от теневого хоста (объявление компонента в пользовательской разметке) в теневой DOM (инкапсулированная разметка компонента).
Атрибут select
принимает CSS-селектор и определяет, какие элементы выбрать из теневого хоста. В нашем случае мы хотим взять все элементы <li>
и установить select="li"
.
Создать компонент
Теперь давайте создадим компонент и зарегистрируем пользовательский элемент HTML. Добавьте следующий скрипт создания в файл multiselect.html
:
<script> // 1. find template var ownerDocument = document.currentScript.ownerDocument; var template = ownerDocument.querySelector('#multiselectTemplate'); // 2. create component object with the specified prototype var multiselectPrototype = Object.create(HTMLElement.prototype); // 3. define createdCallback multiselectPrototype.createdCallback = function() { var root = this.createShadowRoot(); var content = document.importNode(template.content, true); root.appendChild(content); }; // 4. register custom element document.registerElement('x-multiselect', { prototype: multiselectPrototype }); </script>
Создание веб-компонента включает четыре этапа:
- Найдите шаблон в документе владельца.
- Создайте новый объект с указанным объектом-прототипом. В этом случае мы наследуем от существующего элемента HTML, но любой доступный элемент может быть расширен.
- Определите
createdCallback
который вызывается при создании компонента. Здесь мы создаем теневой корень для компонента и добавляем содержимое шаблона внутри. - Зарегистрируйте пользовательский элемент для компонента с помощью метода
document.registerElement
.
Render Multiselect Field
Следующим шагом является рендеринг поля множественного выбора в зависимости от выбранных элементов.
Точкой входа является метод createdCallback
. Давайте определим два метода, init
и render
:
multiselectPrototype.createdCallback = function() { this.init(); this.render(); };
Метод init
создает теневой корень и находит все внутренние компоненты (поле, всплывающее окно и список):
multiselectPrototype.init = function() { // create shadow root this._root = this.createRootElement(); // init component parts this._field = this._root.querySelector('.multiselect-field'); this._popup = this._root.querySelector('.multiselect-popup'); this._list = this._root.querySelector('.multiselect-list'); }; multiselectPrototype.createRootElement = function() { var root = this.createShadowRoot(); var content = document.importNode(template.content, true); root.appendChild(content); return root; };
Метод render
выполняет фактический рендеринг. Поэтому он вызывает метод refreshField
который перебирает выбранные элементы и создает теги для каждого выбранного элемента:
multiselectPrototype.render = function() { this.refreshField(); }; multiselectPrototype.refreshField = function() { // clear content of the field this._field.innerHTML = ''; // find selected items var selectedItems = this.querySelectorAll('li[selected]'); // create tags for selected items for(var i = 0; i < selectedItems.length; i++) { this._field.appendChild(this.createTag(selectedItems[i])); } }; multiselectPrototype.createTag = function(item) { // create tag text element var content = document.createElement('div'); content.className = 'multiselect-tag-text'; content.textContent = item.textContent; // create item remove button var removeButton = document.createElement('div'); removeButton.className = 'multiselect-tag-remove-button'; removeButton.addEventListener('click', this.removeTag.bind(this, tag, item)); // create tag element var tag = document.createElement('div'); tag.className = 'multiselect-tag'; tag.appendChild(content); tag.appendChild(removeButton); return tag; };
У каждого тега есть кнопка удаления. Обработчик нажатия кнопки «Удалить» удаляет выделение из элементов и обновляет поле множественного выбора:
multiselectPrototype.removeTag = function(tag, item, event) { // unselect item item.removeAttribute('selected'); // prevent event bubbling to avoid side-effects event.stopPropagation(); // refresh multiselect field this.refreshField(); };
Откройте всплывающее окно и выберите элемент
Когда пользователь нажимает на поле, мы должны показать всплывающее окно. Когда он / она нажимает на элемент списка, он должен быть помечен как выбранный, а всплывающее окно должно быть скрыто.
Для этого мы обрабатываем клики по полю и списку товаров. Давайте добавим метод attachHandlers
в render
:
multiselectPrototype.render = function() { this.attachHandlers(); this.refreshField(); }; multiselectPrototype.attachHandlers = function() { // attach click handlers to field and list this._field.addEventListener('click', this.fieldClickHandler.bind(this)); this._list.addEventListener('click', this.listClickHandler.bind(this)); };
В поле обработчика щелчков мы переключаем видимость всплывающего окна:
multiselectPrototype.fieldClickHandler = function() { this.togglePopup(); }; multiselectPrototype.togglePopup = function(show) { show = (show !== undefined) ? show : !this._isOpened; this._isOpened = show; this._popup.style.display = this._isOpened ? 'block' : 'none'; };
В списке обработчиков щелчков мы находим выбранный элемент и помечаем его как выбранный. Затем мы скрываем всплывающее окно и обновляем поле множественного выбора:
multiselectPrototype.listClickHandler = function(event) { // find clicked list item var item = event.target; while(item && item.tagName !== 'LI') { item = item.parentNode; } // set selected state of clicked item item.setAttribute('selected', 'selected'); // hide popup this.togglePopup(false); // refresh multiselect field this.refreshField(); };
Добавить атрибут заполнителя
Еще одна функция множественного выбора — это атрибут- placeholder
. Пользователь может указать текст, который будет отображаться в поле, когда ни один элемент не выбран. Чтобы решить эту задачу, давайте прочитаем значения атрибутов при инициализации компонента (в методе init
):
multiselectPrototype.init = function() { this.initOptions(); ... }; multiselectPrototype.initOptions = function() { // save placeholder attribute value this._options = { placeholder: this.getAttribute("placeholder") || 'Select' }; };
Метод refreshField
покажет заполнитель, когда ни один элемент не выбран:
multiselectPrototype.refreshField = function() { this._field.innerHTML = ''; var selectedItems = this.querySelectorAll('li[selected]'); // show placeholder when no item selected if(!selectedItems.length) { this._field.appendChild(this.createPlaceholder()); return; } ... }; multiselectPrototype.createPlaceholder = function() { // create placeholder element var placeholder = document.createElement('div'); placeholder.className = 'multiselect-field-placeholder'; placeholder.textContent = this._options.placeholder; return placeholder; };
Но это не конец истории. Что, если значение атрибута заполнителя изменяется? Нам нужно обработать это и обновить поле. Здесь обратный вызов attributeChangedCallback
пригодится. Этот обратный вызов вызывается каждый раз, когда изменяется значение атрибута. В нашем случае мы сохраняем новое значение заполнителя и обновляем поле множественного выбора:
multiselectPrototype.attributeChangedCallback = function(optionName, oldValue, newValue) { this._options[optionName] = newValue; this.refreshField(); };
Добавить метод selectedItems
элементов
Все, что нам нужно сделать, это добавить метод в прототип компонента. Реализация метода selectedItems
тривиальна — зацикливание на выбранных элементах и чтение значений. Если элемент не имеет значения, вместо него возвращается текст элемента:
multiselectPrototype.selectedItems = function() { var result = []; // find selected items var selectedItems = this.querySelectorAll('li[selected]'); // loop over selected items and read values or text content for(var i = 0; i < selectedItems.length; i++) { var selectedItem = selectedItems[i]; result.push(selectedItem.hasAttribute('value') ? selectedItem.getAttribute('value') : selectedItem.textContent); } return result; };
Добавить пользовательское событие
Теперь давайте добавим событие change
которое будет запускаться каждый раз, когда пользователь меняет выбор. Чтобы запустить событие, нам нужно создать экземпляр CustomEvent
и отправить его:
multiselectPrototype.fireChangeEvent = function() { // create custom event instance var event = new CustomEvent("change"); // dispatch event this.dispatchEvent(event); };
На этом этапе нам нужно запустить событие, когда пользователь выбирает или отменяет выбор элемента. В обработчике щелчков списка мы запускаем событие именно тогда, когда элемент был фактически выбран:
multiselectPrototype.listClickHandler = function(event) { ... if(!item.hasAttribute('selected')) { item.setAttribute('selected', 'selected'); this.fireChangeEvent(); this.refreshField(); } ... };
В обработчике кнопки удаления тега нам также нужно запустить событие change
так как элемент не выбран:
multiselectPrototype.removeTag = function(tag, item, event) { ... this.fireChangeEvent(); this.refreshField(); };
стайлинг
Стилизация внутренних элементов Shadow DOM довольно проста. Мы прикрепляем несколько определенных классов, таких как multiselect-field
или multiselect-popup
и добавляем для них необходимые правила CSS.
Но как мы можем стилизовать элементы списка? Проблема в том, что они исходят от теневого хоста и не относятся к теневому DOM. Специальный селектор ::content
приходит нам на помощь.
Вот стили для наших элементов списка:
::content li { padding: .5em 1em; min-height: 1em; list-style: none; cursor: pointer; } ::content li[selected] { background: #f9f9f9; }
Веб-компоненты представили несколько специальных селекторов, и вы можете узнать больше о них здесь .
использование
Большой! Наша многофункциональная функциональность завершена, поэтому мы готовы ее использовать. Все, что нам нужно сделать, это импортировать HTML-файл с множественным выбором и добавить пользовательский элемент в разметку:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <link rel="import" href="multiselect.html"> </head> <body> <x-multiselect placeholder="Select Value"> <li value="1" selected>Item 1</li> <li value="2">Item 2</li> <li value="3" selected>Item 3</li> <li value="4">Item 4</li> </x-multiselect> </body> </html>
Давайте подпишемся на change
события и будем печатать выбранные элементы на консоли каждый раз, когда пользователь меняет выбор:
<script> var multiselect = document.querySelector('x-multiselect'); multiselect.addEventListener('change', function() { console.log('Selected items:', this.selectedItems()); }); </script>
Перейдите на демонстрационную страницу и откройте консоль браузера, чтобы видеть выбранные элементы при каждом изменении выбора.
Поддержка браузеров
Если мы посмотрим на поддержку браузера , то увидим, что веб-компоненты полностью поддерживаются только Chrome и Opera. Тем не менее, мы по-прежнему можем использовать веб-компоненты с пакетом polyfills webcomponentjs , который позволяет использовать веб-компоненты в последней версии всех браузеров.
Давайте применим этот полифилл, чтобы иметь возможность использовать наш множественный выбор во всех браузерах. Он может быть установлен вместе с Bower, а затем включен в вашу веб-страницу.
bower install webcomponentsjs
Если мы откроем демонстрационную страницу в Safari, мы увидим в консоли ошибку «null не является объектом» . Проблема в том, что document.currentScript
не существует. Чтобы решить эту проблему, нам нужно получить ownerDocument
из полизаполненной среды (используя document._currentScript
вместо document.currentScript
).
var ownerDocument = (document._currentScript || document.currentScript).ownerDocument;
Оно работает! Но если вы откроете множественный выбор в Safari, вы увидите, что элементы списка не имеют стиля. Чтобы исправить эту другую проблему, нам нужно изменить стиль содержимого шаблона. Это можно сделать с WebComponents.ShadowCSS.shimStyling
метода WebComponents.ShadowCSS.shimStyling
. Мы должны вызвать его перед добавлением содержимого теневого корня:
multiselectPrototype.createRootElement = function() { var root = this.createShadowRoot(); var content = document.importNode(template.content, true); if (window.ShadowDOMPolyfill) { WebComponents.ShadowCSS.shimStyling(content, 'x-multiselect'); } root.appendChild(content); return root; };
Поздравляем! Теперь наш мультиселекторный компонент работает правильно и выглядит так, как ожидается во всех современных браузерах.
Веб-компоненты polyfills великолепны! Очевидно, потребовались огромные усилия, чтобы эти спецификации работали во всех современных браузерах. Размер исходного скрипта polyfill составляет 258Kb. Хотя версия в сжатом и сжатом виде составляет 38 Кб, мы можем представить, сколько логики скрыто за кулисами. Это неизбежно влияет на выступления. Хотя авторы делают прокладку лучше и лучше, акцентируя внимание на производительности.
Полимер и X-Tag
Говоря о веб-компонентах, я должен упомянуть Polymer . Polymer — это библиотека, созданная поверх веб-компонентов, которая упрощает создание компонентов и предоставляет множество готовых к использованию элементов. webcomponents.js
webcomponents.js был частью Polymer и назывался platform.js
. Позже он был извлечен и переименован .
Создание веб-компонентов с помощью Polymer намного проще. В этой статье от Pankaj Parashar показано, как использовать Polymer для создания веб-компонентов.
Если вы хотите углубить тему, вот список статей, которые могут быть полезны:
- Создание пользовательских веб-компонентов с помощью X-Tag
- Создание компонента галереи изображений с помощью полимера
- Обеспечение компонентизации в Интернете: обзор веб-компонентов
Существует еще одна библиотека, которая может упростить работу с веб-компонентами, а именно X-Tag . Он был разработан Mozilla и теперь поддерживается Microsoft.
Выводы
Веб-компоненты являются огромным шагом вперед в области веб-разработки. Они помогают упростить извлечение компонентов, укрепить инкапсуляцию и сделать разметку более выразительной.
В этом уроке мы увидели, как создать готовый виджет с множественными выборками с помощью веб-компонентов. Несмотря на отсутствие поддержки браузера, сегодня мы можем использовать веб-компоненты благодаря высококачественному полифилу webcomponentsjs Такие библиотеки, как Polymer и X-Tag, предоставляют возможность создавать веб-компоненты более простым способом.
Теперь обязательно ознакомьтесь с последующим сообщением: Как сделать веб-компоненты доступными .
Вы уже использовали веб-компоненты в своих веб-приложениях? Не стесняйтесь поделиться своим опытом и мыслями в разделе ниже.