Статьи

Создание компонента Multiselect в качестве веб-компонента

Обновление 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> 

Создание веб-компонента включает четыре этапа:

  1. Найдите шаблон в документе владельца.
  2. Создайте новый объект с указанным объектом-прототипом. В этом случае мы наследуем от существующего элемента HTML, но любой доступный элемент может быть расширен.
  3. Определите createdCallback который вызывается при создании компонента. Здесь мы создаем теневой корень для компонента и добавляем содержимое шаблона внутри.
  4. Зарегистрируйте пользовательский элемент для компонента с помощью метода 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 . Он был разработан Mozilla и теперь поддерживается Microsoft.

Выводы

Веб-компоненты являются огромным шагом вперед в области веб-разработки. Они помогают упростить извлечение компонентов, укрепить инкапсуляцию и сделать разметку более выразительной.

В этом уроке мы увидели, как создать готовый виджет с множественными выборками с помощью веб-компонентов. Несмотря на отсутствие поддержки браузера, сегодня мы можем использовать веб-компоненты благодаря высококачественному полифилу webcomponentsjs Такие библиотеки, как Polymer и X-Tag, предоставляют возможность создавать веб-компоненты более простым способом.

Теперь обязательно ознакомьтесь с последующим сообщением: Как сделать веб-компоненты доступными .

Вы уже использовали веб-компоненты в своих веб-приложениях? Не стесняйтесь поделиться своим опытом и мыслями в разделе ниже.