- Быстрый старт с WebComponents
- Быстрый старт с WebComponents
- Веб-компоненты: обзор и использование в продакшне
- Вступление
- Теория
- Обзор
- Пользовательские элементы (Custom Elements)
- Регистрация пользовательского элемента
- Жизненный цикл пользовательского элемента
- Взаимодействие с пользовательским элементом
- Ещё немного о customElements
- Расширение пользовательских элементов
- Расширение стандартных элементов
- Стилизация элементов до регистрации
- Итого
- Теневой DOM (Shadow DOM)
- Инкапсуляция DOM
- Инкапсуляция стилей
- Взаимодействие с теневым DOM
- Что ещё?
- Шаблоны (HTML Templates)
- Ограничения полифила теневого DOM
- Ограничения полифила шаблонов
Быстрый старт с WebComponents
Веб-компоненты это набор стандартов определяющих программные интерфейсы для организации компонентной архитектуры. Все они реализованы в современных версиях браузеров, т.е. не требуют подключения библиотек или транспиляторов кода, однако, если нужна совместимость например с Internet Explorer 11, то и библиотеки и транспиляторы использовать видимо все-таки придется.
Данная статья ориентирована на начальный уровень подготовки и разработчиков имеющих опыт с тем или иным фронтенд фреймворком, но возможно, благодаря некоторым фокусам, будет интересна и многоопытным специалистам.
Все эксперименты приводимые далее проверялись в Chrome и Firefox может быть даже не самых новых версий.
Для начала создадим директорию для проекта и перейдем в него.
В этом каталоге ответив на все вопросы по умолчанию.
Создадим в каталоге файл index.html с самым простым содержимым.
Добавим тег для элемента, имя должно обязательно содержать дефис, это сигнал для подсистемы CusomElements для попытки определения этого элемента как надстраивающего стандартные.
Добавим класс обработчик в теге script .
В модульном теге script , мы определили новый класс который c помощью метода customElements.define() определили за тегом my-webcomp . А добавив код в метод connectedCallback() мы обеспечили его вызов при добавлении реализации компонента в дерево. Результат уже можно посмотреть в браузере:
Однако, размещать html верстку прямо в код если и удобно для небольших кусочков, то вообще не правильно, что особенно сказывается когда кусочки разрастаются до приличных размеров. Например на форме с 20ю элементами, которую побить на подкомпоненты тоже не всегда может быть удобно. Поэтому мы для начала вынесем верстку в шаблон, который будет располагаться в том же html, хотя ничто не помешает его нам загрузить из отдельного файла при необходимости.
В коде компонента вставку строки c html мы заменили на получение элемента шаблона по id. Импорт, т.е. создание копии этого элемента и присоединение к содержимому текущего.
id назван в нотации camelCase т.к. все айдишники элементов прокидываются в глобальное пространство имен и при использовании дефисов или других спец. символов доступ к ним может быть менее элегантен. Т.е. мы могли бы вместо:
Источник
Быстрый старт с WebComponents
Веб-компоненты это набор стандартов определяющих программные интерфейсы для организации компонентной архитектуры. Все они реализованы в современных версиях браузеров, т.е. не требуют подключения библиотек или транспиляторов кода, однако, если нужна совместимость например с Internet Explorer 11, то и библиотеки и транспиляторы использовать видимо все-таки придется.
Данная статья ориентирована на начальный уровень подготовки и разработчиков имеющих опыт с тем или иным фронтенд фреймворком, но возможно, благодаря некоторым фокусам, будет интересна и многоопытным специалистам.
Все эксперименты приводимые далее проверялись в Chrome и Firefox может быть даже не самых новых версий.
Итак начнем.
Для начала создадим директорию для проекта и перейдем в него.
в этом каталоге ответив на все вопросы по умолчанию.
Создадим в каталоге файл index.html с самым простым содержимым.
Добавим тег для элемента, имя должно обязательно содержать дефис, это сигнал для подсистемы CusomElements для попытки определения этого элемента как надстраивающего стандартные.
Добавим класс обработчик в теге script.
В модульном теге script, мы определили новый класс который c помощью метода customElements.define() определили за тегом my-webcomp. А добавив код в метод connectedCallback() мы обеспечили его вызов при добавлении реализации компонента в дерево. Результат уже можно посмотреть в браузере:
Однако, размещать html верстку прямо в код если и удобно для небольших кусочков, то вообще не правильно, что особенно сказывается когда кусочки разрастаются до приличных размеров. Например на форме с 20ю элементами, которую побить на подкомпоненты тоже не всегда может быть удобно. Поэтому мы для начала вынесем верстку в шаблон, который будет располагаться в том же html, хотя ничто не помешает его нам загрузить из отдельного файла при необходимости.
В коде компонента вставку строки c html мы заменили на получение элемента шаблона по id. Импорт, т.е. создание копии этого элемента и прицепление к содержимому текущего.
id назван в нотации camelCase т.к. все айдишники элементов прокидываются в глобальное пространство имен и при использовании дефисов или других спец. символов доступ к ним может быть менее элегантен. Т.е. мы могли бы вместо:
написать в одну строчку:
и этот код работал бы точно так же, но это считается не очень безопасным. Также если мы присвоим id нашему элементу, то сможем обращаться к нему из любой точки контекста как к экземпляру из глобального пространства имен вызывая методы и получая значения свойств.
Например вот так:
При таком раскладе алерт будет показываться сразу при загрузке страницы.
Теперь повесим обработчик клика мышки для нашего компонента который будет выводить алерт сообщение.
Теперь при нажатии на сообщение у нас будет реакция на действия пользователя.
Аргументом метода showMessage() также объявляется объект event, который хранит данные о событии, например координаты клика или ссылку на сам элемент.
Часто каждый конкретный элемент надо сконфигурировать уникальным для него образом, это можно сделать используя атрибуты.
Добавим второй экземпляр элемента и определим для каждого из них разные свойства greet-name значения которых будут выводиться при нажатии на элемент.
Теперь при нажатии на первый будет выводиться “This is the message for John”, а на второй “This is the message for Josh”.
Может так случиться что атрибут надо будет использовать не в обработке события, а прямо отрендерить в шаблон, для этого мы добавим id целевому элементу и подставим значение из апи сразу после рендеринга копии объекта шаблона.
Получается вот так:
Вместо .textContent может быть .innerHTML или можно вызвать у объекта из селектора тот же метод .insertAdjacentHTML() мы делали в самом начале.
Долгое время использование айдишников считалось дурным тоном, потому что на значительных объемах кода они могли дублироваться, что приводило к коллизиям. Однако, с появлением технологии теневого дерева можно внутреннее содержимое элемента изолировать использовать айдишники, стили и прочие ресурсы без опасений. Для веб-компонентов включается теневое дерево следующим образом:
Теперь правда все DOM обращения к this придется заменить на this.shadowRoot благо их пока не так много.
Визуально работа этого кода опять никак не изменится, но теперь в глобальном пространстве имен не будет никакого helloLabel, а у на странице уже 2 элемента с таким идентификатором. А получить доступ к ним можно будет например вот так:
и то если вы не закроете дерево передав соответствующий атрибут в методе .attachShadow().
У нас получилось довольно много кода и размещать его прямо в html файле тоже не очень правильно. Поэтому создадим файл my-webcomp.js и перенесем в него наш класс предварив инструкцией export, а в теге script добавим импорт этого класса, чтобы получилось вот такое:
На работоспособности это никак не скажется, но теперь вся бизнес логика у нас отдельно в .js, конфигурация осуществляется в html атрибутами, а модульную асинхронную инициализацию берут на себя механизмы модульной и компонентной системы браузера.
Правда с этого момента открывать index.html как локальный для разработки не получится, т.к. браузер заблокирует загрузку скрипта с файловой системы. Если у вас есть nodejs можно поставить простейший веб-сервер:
и запускать его командой http-server в каталоге с проектом, при запуске он подскажет хост и порт с которого можно открывать страницу
Которая и будет отныне адресом отладочной страницы с нашим элементом.
Немаловажным для разработки является написание тестов, они помогают удерживать код работоспособным в ходе развития его компонентов. Для тестирования веб-компонентов можно использовать mocha в режиме запуска из браузера.
Для этого создадим каталог test и добавим в него файл all.html такого содержимого:
Скопируем наш index.html в test/ задав ему имя my-webcomp.tests.html и добавим такое же содержимое head как и в all.html.
Однако, нам обычную инициализацию и манипуляции теперь надо будет заменить на производимую в рамках запуска тестов:
Теперь при заходе на
будет показан отчет о выполнении тестов.
Но может потребоваться запускать тесты автоматизировано и в разных браузеров для этого надо поставить специальную утилиту:
и запускать вот так:
Эту строчку можно добавить в секцию test файла package.json и запускать как:
Рекомендуется тестировать каждый значимый метод класса в рамках отдельного теста. Если надо сделать общую для каждого инициализацию ее следует определить в методе suiteSetup():
На этом наверное хватит для первого раза, мы получили некоторый минимальный проект которому решай он конкретную задачу не стыдно было бы сделать.
Книг на тему пока не так уж много, но вся расширенная документация легко находится по словам Web Components, CustomElements, ShadowDOM, Native Template Tag, Custom Events.
А продолжение темы демонстрирующее способы взаимодействия между компонентами за счет использования событий можно найти вот тут
Источник
Веб-компоненты: обзор и использование в продакшне
Привет! Я использую веб-компоненты в разработке фронтэнда. В данной статье я опишу все возможности веб-компонентов, а также то, как их можно использовать сегодня с учётом пока ещё не высокой поддержки.
Кратко про веб-компоненты: это набор технологий, которые позволяют использовать компонентный подход с инкапсуляцией стилей и скриптов в вебе нативно, без подключения каких-либо библиотек или фейрмворков. Если вам интересно, что предлагают стандарты вместо привычных уже React или Angular, и как это использовать при разработке под старые браузеры, прошу под кат.
Список материалов для детального изучения — в конце статьи.
Вступление
Я работаю фронтэнд-разработчиком сервисов в одной из крупных международных кампаний и в настоящий момент второй раз переписываю фронтэнд проекта.
В первой версии, написанной согласно канонам 1C-Bitrix, меня ожидало мясо из скриптов и стилей во множестве шаблонов. Скрипты, конечно, были на jQuery, а стили, разумеется, были абсолютно хаотичны, без какой-либо структуры или порядка. В связи с переездом на новую платформу выпал шанс полностью переписать проект. Для наведения порядка была разработана своя компонентная система с использованием БЭМ-методологии. На каждый экземпляр БЭМ-блока в разметке после загрузки страницы создаётся один объект соответствующего класса, который начинает управлять логикой. Таким образом, всё довольно строго систематизировано — блоки (логика и стили) реиспользуемы и изолированы друг от друга.
По прошествии года поддержки и доработки проекта обнаружился ряд недостатков такой системы. Разметка, на основе которой работают мои псевдокомпоненты, держится на внимательности и «честном слове» разработчика: JS надеется, что верстальщик правильно расставил все нужные элементы и прописал к ним классы. Если компонент модифицирует свой DOM, который содержит другие БЭМ-блоки, либо же разметка подгружается через ajax, компоненты этой разметки приходится инициализировать вручную. Это всё казалось простым при ежедневной работе до тех пор, пока на проекте не появился второй человек. Документация, к сожалению, хоть и была довольно объёмной, но охватывала только базовые принципы (объём в данном случае стал минусом). В жизни же шаг влево или шаг вправо ломал установленные «принципы», а чтение готовых компонентов только вносили сумбур, так как они были написаны по разному и в разное время.
Всё это, а также потенциальное увеличение количества проблем в разработке при запланированном переходе к SPA/PWA подтолкнули к очередной переработке фронта. Велосипеды, которым, в том числе, является моя компонентная система, очень полезны во время обучения чему-либо (в моём случае — JS), но в качественном проекте с несколькими разработчиками необходимо что-то более надёжное и структурированное. В настоящее время (впрочем, уже давно) в вебе мы имеем массу фреймворков, среди которых есть, что выбрать: Preact предлагает маленький размер и максимальное сходство в разработке с уже знакомым мне React-ом, Angular манит встроенным прелестным TypeScript-ом, Vue выглядывает из-за угла и хвастается своей простотой, и много другого. Особняком держится стандарт: оказывается, можно писать реиспользуемые веб-компоненты, с зашитой внутри логикой и стилями, которые не надо будет искусственно (за счёт БЭМ и дополнительного JS-а) связывать с уже написанной разметкой. И всё это должно работать из коробки. Чудо, не правда ли?
Из-за моей дикой любви к стандартам и вере в светлое будущее, а также схожести имеющейся системы компонентов с веб-компонентами (один JS-класс на один «компонент» с изолированной логикой), решено было попробовать использовать именно веб-компоненты. Больше всего напрягала поддержка этого дела в браузерах: веб-компоненты слишком молоды, чтобы охватывать сколько-нибудь широкий круг браузеров, а уж тем более охватывать браузеры, которые необходимо поддерживать коммерческому продукту (например, Android 4.4 stock browser и Internet Explorer 11). Мысленно был принят некоторый уровень боли и ограничений, который могли меня ожидать, и рамок, в которые я согласен вписываться при разработке, и я погрузился в изучение теории и практические эксперименты: как писать фронт на веб-компонентах и выкатывать это в продакшн так, чтобы оно работало.
Необходимый теоретический минимум для чтения статьи: чистый JavaScript на уровне базовых манипуляций с DOM-деревом, понимание синтаксиса классов в ES2015, плюсом будет знакомство с каким-либо из фреймворков из разряда React.js/Angular/Vue.js.
Теория
Обзор
Веб-компоненты — это совокупность стандартов, которые позволяют делать декларативно описываемые, реиспользуемые «виджеты» с изолированными стилями и скриптами в виде собственных тегов. Стандарты развиваются независимо, и связываются в веб-компоненты довольно условно — в принципе, можно использовать каждую из используемых технологий отдельно. Но именно вместе они максимально эффективны.
Обычно все четыре стандарта — пользовательские элементы (Custom Elements), теневой DOM (Shadow DOM), шаблоны (HTML Templates) и HTML-импорты (HTML Imports) — рассматриваются отдельно, а уже потом соединяются вместе. Так как по отдельности они слабо полезны, мы рассмотрим все возможности стандартов кумулятивно, прибавляя их к уже изученному ранее.
Стоит напомнить, что веб-компоненты — довольно молодая технология, и стандарты претерпевали множество изменений. Для нас в основном это выражается в нескольких версиях стандартов Custom Elements и Shadow DOM — v0 и v1 . v0 в настоящий момент не актуальны. Мы будем рассматривать только v1 . Будьте внимательны при поиске дополнительных материалов! Версии v1 сформировались только в 2016 году, а значит, все статьи и видео до 2016 года гарантированно говорят именно о старой версии спецификации.
Пользовательские элементы (Custom Elements)
Пользовательские элементы — это возможность создавать новые HTML-теги с произвольными именами и поведением, например, или .
Регистрация пользовательского элемента
Конечно, мы и так можем «создать» собственный теги (например, браузеры вполне корректно обработают тег ), но при этом в DOM-дереве элемент регистрируется как объект класса HTMLUnknownElement и не имеет никакого поведения по умолчанию. «Оживлять» каждый такой элемент придётся вручную.
Спецификация пользовательских элементов позволяет регистрировать новые теги и задавать их поведение в соответствии с жизненным циклом — создание, вставка в DOM, изменение атрибутов, удаление из DOM. Для того чтобы предотвратить возможный конфликт новых тегов стандарта HTML и пользовательских тегов, имена последних обязаны содержать как минимум один дефис — например, или . Также пользовательские теги на текущий момент не могут быть самозакрывающимися, даже теги без содержимого должны быть парными.
Так как лучший способ обучения это практика, напишем элемент, схожий по функционалу с элементом . Назовём его .
При добавлении такого элемента в DOM-дерево он также станет объектом класса HTMLUnknownElement . Для того, чтобы зарегистрировать элемент, как пользовательский, и добавить ему своё поведение, нужно воспользоваться методом define глобального объекта customElements . Первым аргументом передаётся имя тега, вторым — класс, описывающий поведение. Класс при этом должен расширять класс HTMLElement , чтобы наш элемент обладал всеми качествами и возможностями других HTML элементов. Итого:
После этого браузер пересоздаст все имеющиеся в разметке теги x-spoiler как объекты класса XSpoiler , а не HTMLUnknownElement . Все новые теги x-spoiler , которые добавляются в документ через innerHTML , insertAdjacentHTML , append или другие методы для работы с HTML, сразу создаются на основе класса XSpoiler . Также такие DOM-элементы можно создавать и через document.createElement .
Если попытаться зарегистрировать элемент с уже зарегистрированным именем или на основе уже зарегистрированного класса, мы получим исключение.
Имя тега и имя класса совсем не обязательно должны совпадать. Поэтому мы можем при необходимости зарегистрировать два пользовательских элемента с одинаковым именем класса под разными тегами.
Теперь пользовательский элемент, конечно, зарегистрирован, но ничего полезного он не делает. Чтобы действительно оживить наш пользовательский элемент, рассмотрим его жизненный цикл.
Жизненный цикл пользовательского элемента
Мы можем добавить методы-коллбэки на создание элемента, добавление его в DOM, на изменение атрибутов, на удаление элемента из DOM и на изменение родительского документа. Мы воспользуемся этим, чтобы реализовать логику работы спойлера: компонент будет содержать кнопку с текстом «Свернуть»/«Развернуть» и секцию с изначальным содержимым тега. Видимость секции будет управляться кликом по кнопке или значением атрибута. Текст кнопок также можно будет настроить через атрибуты.
Коллбэком на создание элемента является конструктор класса. Чтобы он корректно отработал, сперва необходимо вызвать родительский конструктор через super . В конструкторе можно задать разметку, навесить обработчики событий, сделать какую-то другую подготовительную работу. В конструкторе, как и в других методах, this будет ссылаться на сам DOM-элемент, а благодаря тому, что наш пользовательский элемент расширяет HTMLElement , у this есть такие методы как querySelector и такие свойства как classList .
Добавим в конструкторе значения для текстов кнопки, разметку компонента и навесим обработчик на клик по кнопке, который будет менять наличие атрибута opened .
Подробно разберём каждую часть конструктора.
super() вызывает конструктор класса HTMLElement . Это в данном случае обязательное действие, если нам нужен конструктор элемента.
this.text — так как this это объект, мы можем добавить и свои свойства. В данном случае я буду хранить в объекте text вспомогательные тексты, которые выводятся на кнопке.
this.innerHTML установит разметку нашего DOM-элемента. При этом мы используем текст, заданный чуть выше.
this.querySelector(«button»).addEventListener добавит обработчик события click по кнопке, который будет устанавливать или снимать атрибут opened . Мы будем работать с ним как с логическим значением — спойлер либо открыт, либо закрыт, следовательно, атрибут либо есть, либо нет. В обработчике мы будем проверять наличие атрибута через сравнение с null , а затем либо устанавливать, либо удалять атрибут.
Теперь при клике по созданной кнопке будет меняться атрибут opened . Пока что изменение атрибута ничего не даёт. Прежде чем перейти к этой теме, немного модифицируем код.
Помните, что у тега есть атрибут disabled , который отключает кнопку? Если мы пропишем его в разметке, кнопка перестанет быть активной, если удалим — вновь станет кликабельной. Работать с атрибутом мы можем и из JavaScript-кода, используя методы getAttribute , setAttribute и removeAttribute . Но это не очень удобно, нам нужно целых три метода для работы с атрибутами, они длинные, и, к тому же, работают только со строками (значение атрибута — всегда строка). Поэтому в DOM-элементах зачастую используется «отражение» атрибутов в одноимённые свойства. Так, свойство button.disabled вернёт наличие или отсутствие атрибута. А теперь сравнените два подхода, через прямую работу с атрибутами и через свойства:
Согласитесь, работа через свойства гораздо удобнее? Реализуем такой же механизм и с нашим атрибутом opened , чтобы его значение можно было легко получить и установить. Для этого используем возможность геттеров и сеттеров свойств в классах:
Для строковых свойств (как для встроенных id у любых элементов и href у ссылок) геттер и сеттер будут выглядеть немного проще, но идея сохраняется.
Можно ещё добавить, что такое «отражение» может быть не всегда полезным с точки зрения производительности. Например, у элементов формы атрибут value работает иначе.
Теперь у нас есть атрибут, он может менять свое значение при клике по кнопке, но больше никакого полезного действия не происходит. Мы могли бы добавить полезный код по скрытию и отображению элемента прямо в обработчик клика, но тогда было бы очень проблемно изменить видимость элемента, например, другим, внешним JS-кодом.
Вместо этого можно навесить обработчик на изменение значения атрибутов, используя метод attributeChangedCallback . Он будет вызываться каждый раз при изменении атрибута, таким образом, через атрибуты можно будет управлять компонентом как изнутри, так и снаружи.
Метод применяет три параметра: имя атрибута, старое значение, новое значение. Так как вызывать этот метод на изменение абсолютно ВСЕХ атрибутов было бы нерационально с точки зрения производительности, срабатывает он только при изменении свойств, которые перечислены в статическом массиве observedAttributes текущего класса.
Наш компонент должен реагировать на изменение трёх атрибутов — opened , text-when-open и text-when-close . Первый будет влиять на отображение спойлера, а два других будут управлять текстом кнопки. Первым делом добавим к нашему классу имена этих атрибутов в статический массив observedAttributes :
Теперь добавим сам метод attributeChangedCallback , который в зависимости от изменённого атрибута будет менять либо видимость контента и выводить текст кнопки, либо менять текст кнопки и выводить его при необходимости. Для этого используем switch по первому аргументу метода.
Обратите внимание, что метод attributeChangedCallback сработает даже тогда, когда требуемые атрибуты изначально присутствуют у элемента. То есть, если наш компонент будет вставлен в разметку сразу с атрибутом opened , спойлер действительно будет открыт, т.к. attributeChangedCallback сработает сразу после constructor . Поэтому никакой дополнительной работы по обработке начального значения атрибутов в конструкторе производить не нужно (если, конечно, атрибут — отслеживаемый).
Теперь наш компонент действительно работает! При клике по кнопке меняется значение атрибута opened , после этого срабатывает коллбэк attributeChangedCallback , который в свою очередь управляет видимостью содержимого. Управление состоянием именно через атрибуты и attributeChangedCallback позволяет управлять изначальным состоянием (мы можем добавить opened в разметку сразу, если хотим показать открытый спойлер) или управлять состоянием снаружи (любой другой JS-код может установить или снять атрибут с нашего элемента и это будет корректно обработано). В качестве бонуса мы можем настроить текст управляющей кнопки. Демо результата, смотреть в свежем Chrome!
Основной функционал готов, это самые часто используемые возможности пользовательских элементов. Теперь рассмотрим те коллбэки, которые используются реже.
На вставку элемента в DOM-дерево срабатывает метод connectedCallback . Если элемент уже находился в разметке на момент регистрации, или он создаётся через вставку HTML-строки, последовательно сработают constructor , при необходимости — attributeChangedCallback , а уже затем — connectedCallback . Этот коллбэк можно использовать, если, например, необходимо знать информацию о родителе в DOM-дереве, или мы хотим оптимизировать наш компонент и отложить какой-то тяжёлый код именно до момента использования элемента. Однако, при этом стоит помнить две вещи: во-первых, если constructor срабатывает единожды для одного элемента, то connectedCallback срабатывает каждый раз, когда элемент вставляется в DOM, и во-вторых, attributeChangedCallback может сработать раньше connectedCallback , поэтому, если отложить создание разметки с конструктора до connectedCallback , это может привести к ошибке. Метод можно использовать для назначения обработчиков событий или для других тяжёлых операций, например, соединения с сервером.
Точно так же, как можно отследить вставку элемента в DOM, можно отследить и удаление. За это отвечает метод disconnectedCallback . Он срабатывает, например, при удалении элемента из DOM методом remove() . Обратите внимание: если элемент удалён из DOM-дерева, но у вас есть ссылка на элемент, он опять может быть вставлен в DOM, и при этом повторно сработает connectedCallback . При удалении можно, например, прекратить обновлять данные в компоненте, удалить таймеры, удалить назначенные в connectedCallback обработчики событий или закрыть соединение с сервером. Обратите внимание, что disconnectedCallback не гарантирует выполнение своего кода — например, при закрытии пользователем страницы метод вызван не будет.
Самым редко используемым коллбэком является метод adoptedCallback . Он срабатывает, когда элемент меняет свойство ownerDocument . Это происходит, если, например, создать новое окно и переместить элемент в него.
Взаимодействие с пользовательским элементом
Управляется компонент, как мы уже поняли, значением атрибутов напрямую или через свойства. А вот для того чтобы передать данные из компонента наружу, можно использовать CustomEvents . В случае нашего компонента будет рационально добавить событие изменения состояния, чтобы его можно было прослушать и отреагировать. Для этого добавим в конструкторе свойство events с двумя объектами CustomEvent :
Также отредактируем attributeChangedCallback , чтобы при изменении opened отправлялось то или иное событие:
Теперь мы можем прослушивать интересующие нас события, чтобы узнавать об изменении состояния спойлера.
Ещё немного о customElements
При регистрации элемента мы использовали метод define глобального объекта customElements . Помимо этого у него есть ещё два полезных метода.
customElements.get(name) вернёт конструктор пользовательского элемента, зарегистрированного под именем name , если такой есть, либо undefined .
customElements.whenDefined(name) вернёт промис, который будет выполнен успешно тогда, когда элемент с именем name будет зарегистрирован, или незамедлительно, если элемент уже зарегистрирован. Особенно удобно это с использванием await , но это уже другая тема.
Расширение пользовательских элементов
Мы можем наследоваться от класса пользовательского элемента, чтобы создавать новые пользовательские элементы на основе существующих. Например, мы можем расширить наш спойлер и добавить ему при необходимости какой-либо функционал. При этом важно не забыть вызвать аналогичный метод родительского класса через super.methodName() , если это необходимо (в случае конструктора и super() это обязательно).
Расширение стандартных элементов
Спецификация разрешает расширять и стандартные HTML-теги. Например, вам нужна своя реализация кнопки, но при этом вам желательно сохранить уже имеющийся функционал браузерных кнопок, например, правильную работу с атрибутами disabled , tabindex , type и прочими. Однако, тут есть ряд особенностей.
Во-первых, при объявлении класса расширять необходимо класс нужного вам тега. В случае с кнопкой, это будет класс HTMLButtonElement . Полный список классов можно найти в спецификации.
Во-вторых, при регистрации элемента третьим параметром необходимо передать объект опций, в котором указывается, какой именно тег вы хотите расширить (нескольким тегам может соответствовать один и тот же класс).
В-третьих, создаётся такой пользовательский элемент как обычный тег, который нужно было расширить, но с атрибутом is , равным имени пользовательского элемента. Если же элемент создается через document.createElement , is передаётся как свойство второго аргумента.
Выглядеть это будет так:
Стилизация элементов до регистрации
Между тем, как пользователь получит HTML-разметку, и тем, как будет скачан и выполнен JavaScript-код, пройдёт некоторое время. Чтобы как-то стилизовать пользовательские элементы, которые отрисованы в DOM, но ещё не зарегистрированы и не работают должным образом, можно использовать псевдокласс :defined . Самый простой пример использования — скрыть все незарегистрированные пользовательские элементы:
Итого
Мы сделали реиспользуемый веб-компонент на основе технологии пользовательских элементов. Однако, у него есть много минусов. Так, например, у нас нет изоляции стилей: правило section
Теневой DOM (Shadow DOM)
Спецификация теневого DOM позволит решить проблемы с изоляцией стилей и разметки от окружения и внутреннего содержимого.
Инкапсуляция DOM
Стандартная модель DOM, к которой мы привыкли, предполагает, что все потомки элемента доступны через childNodes , их можно найти через querySelector() , и так далее. DOM — сквозной, где бы не находился параграф текста, он всегда будет найден через document.querySelectorAll(«p») . Однако, есть возможность отображать не то, что находится в DOM-дереве, а какую-либо другую разметку, причём так, чтобы она была игнорировалась привычными childNode и querySelector . Самым простым примером такого поведения будет тег с несколькими внутри. Мы добавляем в DOM только , а видим полноценный видеопроигрыватель, с собственной изолированной разметкой (блоками, кнопками и прочим). Всё, что мы видим на экране, как раз располагается в теневом DOM. Как это работает?
В любой элемент времени мы можем добавить теневой DOM с помощью метода attachShadow() . В этот момент вместо обычного DOM-дерева у элемента появляется сразу три других: Shadow DOM, Light DOM и Flattened DOM. Рассмотрим их по отдельности.
Light DOM — это то, что раньше являлось обычным DOM-деревом элемента: все элементы, которые доступны через обычные innerHTML , childNodes или по которым ищет метод querySelectorAll .
Shadow DOM — это DOM-дерево, которое лежит в свойстве shadowRoot элемента. Сразу после вызова attachShadow свойство shadowRoot пустое, но мы можем задать какую-либо разметку через стандартные innerHTML , appendChild или другие способы работы с DOM, вызывая их относительно this.shadowRoot .
Flattened DOM — это результат объединения Shadow DOM и Light DOM. Это то, что пользователь в действительности видит на экране. Это искусственное понятие, необходимое только для понимания механизма работы Shadow DOM. Если получить разметку Light DOM можно через element.innerHTML , разметку Shadow DOM можно через element.shadowRoot.innerHTML , то на Flattened DOM можно только посмотреть в окне браузера. Flattened DOM строится на основе Shadow DOM и в самом простом варианте использования строго ему равен. Таким образом, Light DOM может вообще не отображаться на экране. Например:
До момента регистрации элемента пользователь будет видеть обычный DOM, т.е. в нашем случае текст «Привет!». Но как только элемент будет зарегистрирован, текст пропадёт, вместо него появится фраза «Привет из тени. ».
Обратите внимание: при добавлении Shadow DOM нужно в объекте опций указать режим, в котором этот Shadow DOM будет создан. Для пользовательских элементов рекомендуется использовать open . Это позволит при необходимости взаимодействовать с Shadow DOM через свойство shadowRoot .
Пока не очень полезно. Гораздо больше возможностей нам дают тег и его атрибут name в Shadow DOM и атрибут slot в Light DOM. Они объяснят, где именно во Flattened DOM нужно отображать содержимое Lignt DOM. Работает это так: элементы (их может быть как 0, так и 1 и более) с атрибутом slot и каким-либо значением из Light DOM отображаются в качестве содержимого тега с соответствующим значением атрибута name в Shadow DOM. Если для какого-либо тега не нашлось подходящего содержимого, отображается его содержимое из Shadow DOM. Если есть тег без атрибута name , в нём отображается всё содержимое Light DOM без атрибута slot .
Таким образом, самый простой вариант комбинации Shadow DOM и Light DOM это когда Shadow DOM содержит только один тег без атрибутов, тогда всё содержимое Light DOM будет отображаться как содержимое тега .
Этот вариант хорошо подходит для нашего компонента спойлера: попробуем убрать всю «обвязку» (секцию и кнопку) в Shadow DOM, и отобразим содержимое Light DOM с помощью тега . Для этого исправим конструктор:
Так как кнопка для открытия теперь находится в Shadow DOM, необходимо также заменить this.querySelector(«button») на this.shadowRoot.querySelector(«button») . Также нужно поступить и с поиском тега section .
Теперь мы можем безопасно менять текст спойлера, например, через textContent пользовательского элемента. Наша разметка инкапсулирована и никак не мешает работать с содержимым элемента. Разработчику не нужно знать, как устроен элемент изнутри, чтобы работать с ним. Демо.
Рассмотрим пример посложнее: это будет искусственный пример, который будет раскидывать по четырём цветным «коробкам» разных цветов DOM-элементы из Light DOM. Можете попробовать поменять содержимое тега, чтобы посмотреть, как оно работает: демо.
В этом примере видно, что если в Light DOM находится несколько элементов с одинаковым значением атрибута slot , все они попадут в с аналогичным name (красная секция). Если у элемента в Light DOM не указан slot , он попадёт в Shadow DOM в тег , который не имеет атрибута name (белая секция). Ну и если у нас вообще нет элементов с атрибутом slot с каким-либо значением, соответствующий тег останется со своей собственной разметкой (желтая секция).
Инкапсуляция стилей
С изоляцией DOM и соотношением Light DOM / Shadow DOM вроде разобрались, теперь поговорим об изоляции стилей.
В последнем примере я уже начал использовать изоляцию стилей. Если в Shadow DOM находится тег
Вероятно, вам нужно предоставить какую-то возможность по стилизации вашего пользовательского элемента снаружи, помимо предустановленных через :host и :host-context стилей.
В случае с самим пользовательским элементом всё просто — стили на нём при совпадении со стилями в :host будут перебиты. Таким образом, стилизация по тегу будет перебивать стилизацию через :host . Это позволит управлять контейнером пользовательского элемента.
Для управления элементами в Shadow DOM можно использовать пользовательские свойства CSS, если, конечно, вы поддерживаете только те браузеры, которые позволяют использовать пользовательские свойства.
Взаимодействие с теневым DOM
Пользовательские элементы с теневым DOM могут быть использованы в теневом DOM другого компонента, на это нет никаких ограничений. Также можно использовать и браузерные теги, которые используют теневой DOM, например, тег .
Доступ к теневому DOM открыт любому JavaScript-коду при условии, что теневой DOM был добавлен с опцией
Что ещё?
У вас есть возможность создавать так называемый «закрытый» Shadow DOM, передав опцию mode со значением closed . При этом Shadow DOM будет недоступен через свойство shadowRoot , оно будет возвращать null . Ссылку на настоящий shadowRoot вернёт сам метод attachShadow() , и если сохранить её в переменную, можно работать с shadowRoot в пределах конструтора и нельзя как-то повлиять в последующем. Это сильно ограничивает работу с Shadow DOM, поэтому не рекомендуется к использованию в пользовательских элементах.
Теги генерируют событие slotchange , если после инициализации пользовательского элемента произошло изменение в Light DOM, которое затронуло этот слот. Таким образом, можно отслеживать изменения и реагировать на них.
Также теги имеют метод assignedNodes , который возвращает массив DOM-узлов из Light DOM, которые были помещены в этот слот. Если в слот не попал ни один DOM-узел и в качестве аргумента переданы опции
В свою очередь у помещённых в слоты элементов из Light DOM есть метод assignedSlot , который вернёт слот, в который попал элемент.
Шаблоны (HTML Templates)
Наш компонент спойлера уже сейчас довольно хорош, но его можно улучшить с помощью спецификации шаблонов HTML.
В текущий момент разметка Shadow DOM хранится прямо в конструкторе. Веб-компоненты не приветствуют такой путь, в частности потому что установка innerHTML для каждого компонента является довольно трудозатратной операцией, а также из-за принципа разделения сущностей.
Вместо этого предлагается использовать тег , который предлагает спецификация HTML Templates. Данный тег может содержать в себе любую разметку, которая будет парситься наравне с остальными тегами, но при это не будет выполняться. Это значит, что разметка не будет добавлена в DOM-дерево, а значит, содержимое тега
Выглядит довольно громоздко, но — работает. Самый главный минус — мы вынуждены подключать ещё один файл даже тогда, когда он нам не нужен. Ещё один минус — файл всё равно будет скачиваться, даже если он не исполняется в итоге.
Альтернативой является использование babel-plugin-transform-custom-element-classes. При подключении данного плагина вам больше не нужно использовать es5-adapter , но при этом мы получаем нерабочий код в IE11 и многих других браузерах, так как решение плагина построено на использовании Reflect.construct . Возможно, вы тоже, как и я, до этого о таком не слышали. То есть, решая эту проблему, мы породили другую — теперь нам для старых браузеров нужен ещё один полифил. Причём с ним не всё так просто: готовый, работающий в данной ситуации полифил я нашёл только в babel-polyfill . А это просто огромный файл. Спасибо, но пока что нет.
Ещё одна альтернатива — babel-plugin-transform-builtin-classes от упомянутого выше WebReflection . С ним всё гораздо лучше и легче, за исключением того, что код для IE11 опять-таки ломается, но теперь по иной причине. Дело в том, что этот плагин не совместим с полифилом пользовательских элементов от WebComponents . Точка зрения автора здесь — надо было ставить линукс надо использовать полифил пользовательских элементов от WebReflection, а не от WebComponents. Поэтому этот вариант нам также не подходит.
И ещё одна альтернатива, которая выглядит многообещающе, но опять таки имеет ограничения — использовать Babel 7, который сейчас находится в статусе бета. Совсем недавно там появилась встроенная возможность расширять нативные элементы. Несмотря на то что выглядит это очень привлекательно, на практике я получил сломанный код и в Chrome (есть проблемы с наследованием и полифилами), и в IE11, так как в основе опять лежит Reflect.construct .
Итого: в текущий момент решение с удалением из DOM ES5-адаптера, если он не нужен, будет для нас самым приемлемым. Со временем мы его улучшим, а пока — будем использовать в таком виде.
Есть ограничения по свойству constructor : в IE и Safari конструктор может быть равен HTMLUnknownElementConstructor , а не настоящему конструктору пользовательского элемента. Поэтому небезопасно полагаться на это свойство.
Ограничения полифила теневого DOM
Чтобы симулировать инкапсуляцию стилей, необходимо немного дополнительного кода. Во-первых, единожды для каждого шаблона необходимо вызвать метод ShadyCSS.prepareTemplate() , передав туда первым аргументом сам шаблон, вторым — название пользовательского элемента, к которому этот шаблон относится. В случае с шаблоном и пользовательским элементом , необходимо вызвать строчку ShadyCSS.prepareTemplate(document.getElementById(«x-spoiler»), «x-spoiler») .
Перед использованием же шаблона необходимо также вызвать дополнительный код: ShadyCSS.styleElement(this) ;
Есть ограничения на уровень вложенности селекторов со скобками при использовании вместе с :host() : :host(.zot) и :host(.zot:not(.bar)) будут работать, а :host(.zot:not(.bar:nth-child(2))) — уже нет.
Есть ограничения на использование селектора ::slotted : слева от селектора обязательно должен быть родитель. Так, селектор ::slotted(span) в тестах не будет работать с полифилом, поэтому там указан селектор .header ::slotted(span) .
Ограничения полифила шаблонов
Следующая проблема — это инициализация полифила тега . Дело в том, что он отрабатывает только после события DOMContentLoaded , а это значит, что customElements.define() , запущенный до завершения построения DOM-дерева, столкнётся с невозможностью использования свойства content у тегов template . Решения два: либо запускать customElements.define() также по событию DOMContentLoaded , либо принудительно инициализировать полифил до первого customElements.define() . Второй вариант я вижу менее костыльным. Выглядеть это будет так:
Чтобы полифил и теневого DOM корректно отработал, на нас накладывается ещё одно ограничение: все теги уже должны быть в DOM-дереве на момент инициализации пользовательских элементов. Помогать нам с этим будет система сборки.
Шаблоны не могут быть вложенными и не должны содержать теги
Источник