Создание пользовательских элементов с помощью веб-компонентов для выборов 2020 года

Не знаю, знали ли вы об этом, но сейчас выборы. В демократической гонке участвуют около 89 человек, и предстоит много праймериз, которые могут определить будущее нашей страны. Поэтому, когда PBS NewsHour обратился к нам в Upstatement, чтобы построить графики и диаграммы, которые они могли бы использовать на своем веб-сайте и транслировать, мы знали, что будущее нации лежит исключительно на наших плечах.

NewsHour нуждается в четкой, достоверной и живой графике выборов.

Начиная с Кавказа в Айове и кончая генералом в ноябре, выборы имеют быстро разворачивающиеся результаты и постоянно меняющиеся взгляды. Команде PBS NewsHour требовалась графика, которая могла бы ясно рассказать эти истории с последними данными и минимальным рабочим процессом; они не могли рассчитывать на создание трудоемких и медленных графиков вручную. Наша работа заключалась в создании графики, которая могла бы получать самые свежие данные, отображать их и обновлять в реальном времени.

Эти графические изображения должны были появиться в программе PBS NewsHour, на их веб-сайтах, на сайтах местных филиалов и в лентах социальных сетей. Редакторам нужен был способ быстро просматривать данные, фильтровать их, переключаться между гонками и ставить изображения в очередь, чтобы рассказать о них в трансляции. Как мы могли создать один инструмент, который бы делал все это и работал бы на любом веб-сайте?

Решение: веб-компоненты

Первое, что мы поняли, это то, что мы не собираемся создавать что-либо во фреймворке или в плагине WordPress. Эту графику нужно было легко перенести на любой веб-сайт без размещения приложения узла или управления конвейером сборки. Все в NewsHour очень заняты, и попросить их управлять еще одной системой было бы слишком много для их тарелок.

Поэтому мы остановились на Веб-компонентах. Мы просто собирали все наши компоненты в один файл JavaScript, и все, что им нужно было сделать, это добавить один тег script на свой сайт, чтобы разместить любой компонент на любой странице.

<script src="http://someurl.biz/components.js"></script>

Веб-компоненты также решили проблему правил локального стиля на любом конкретном веб-сайте с Shadow DOM. Это означало, что мы могли стилизовать наши компоненты и не беспокоиться о каком-либо глобальном CSS с веб-сайта-потребителя. Конечным результатом был элемент HTML, который они могли разместить где угодно, чтобы получить самые свежие результаты и составить для них график:

Создание веб-компонентов с нуля

Все это красиво звучит на бумаге. Один файл JavaScript, и у нас есть стилизованный компонент, который мы можем разместить где угодно! Но как мы на самом деле приступим к его созданию? Как это сделать без использования большой библиотеки (которая может значительно увеличить размер нашего пакета)?

Начнем с простого примера создания веб-компонента с нуля. Поскольку выборы являются достаточно напряженным и запутанным делом, мы сделаем что-нибудь простое: компонент, отображающий портрет единственного кандидата! (Все, что мы сделали для PBS NewsHour, соответствует контурам этого примера)

Итак, с чего мы начнем? Допустим, мы хотели создать компонент, который позволил бы нам добавлять фотографии кандидатов на наш сайт. Сначала мы начнем с создания собственного класса в JavaScript:

class CandidateImage extends HTMLElement {}

HTMLElement является частью спецификации JavaScript по умолчанию и имеет четыре обратных вызова жизненного цикла, которыми мы можем воспользоваться (кроме значения по умолчанию constructor).

  • connectedCallback - вызывается, когда элемент добавляется к основному документу.
  • disconnectedCallback - вызывается, когда элемент отключается от DOM.
  • adoptedCallback - вызывается, когда элемент перемещается в новый документ (мы не будем рассматривать это в этом сообщении в блоге)
  • attributeChangedCallback - вызывается при обновлении, добавлении или удалении атрибута.

При создании компонентов вы будете чаще всего использовать connectedCallback. Обычно вы хотите иметь здесь что-нибудь, связанное с обновлением разметки. Для нашего CandidateImage компонента это будет место, где мы создаем наше изображение:

Это настраивает innerHTML нашего элемента как изображение Берни Сандерса. Но чтобы использовать это в HTML, мы должны фактически зарегистрировать это как настраиваемый элемент. Мы можем сделать это с помощью API customElements, вызвав метод define. Однако мы хотим, чтобы это произошло после загрузки документа. Мы можем обновить код нашего компонента:

Теперь мы можем добавить элемент candidate-image на любую страницу, которая загружает наш component.js файл (обратите внимание, что все настраиваемые элементы должны иметь тире в своем имени).

<candidate-image></candidate-image>

Бац! У нас есть кастомный компонент. Но есть проблема: глобальные стили по-прежнему будут применяться к нашему компоненту. Нравится:

img { display: none; }

Это правило скроет нашу фотографию Берни Сандерса, даже если оно может быть написано для чего-то совершенно другого.

Тень DOM

Именно тогда в игру вступает Shadow DOM. По сути, это позволяет нам создать документ внутри текущего документа и скрыть его от любого глобального CSS. И для этого требуется всего лишь один дополнительный шаг в нашем компоненте:

Все, что нам нужно сделать, это вызвать this.attachShadow(), и наш HTMLElement получит собственный теневой DOM! Мы сохраняем его как this.shadow, чтобы мы могли получить к нему доступ позже (при необходимости). Это очень похоже на работу с обычным документом. Мы можем запрашивать элементы с помощью this.shadow.querySelector('img') или использовать appendChild и другие аналогичные методы, которые вы ожидаете от документа. Это буквально документ внутри документа.

Теперь это глобальное правило CSS, которое скрывает все изображения, не повлияет на наш компонент! Наша фотография Берни в безопасности.

Установка атрибутов

Наш компонент идеально отображает портрет Берни, но на самом деле ничего не делает. Было бы неплохо, если бы мы могли использовать этот компонент для отображения любого кандидата, который нам нужен. Вот когда в игру вступают атрибуты. Мы могли бы передать нашему компоненту name, и он найдет для нас подходящее изображение.

Как и любой элемент HTML, мы можем вызвать getAttribute в нашем классе, чтобы получить определенный атрибут, установленный для нашего элемента. Например, скажем, мы добавляем это на нашу страницу:

<candidate-image name="pete"></candidate-image>

Затем мы могли бы посмотреть значение атрибута type в нашем компоненте и получить правильное изображение.

Теперь мы можем загрузить изображение Пита Буттигига.

Обработка внутренних стилей

Мы рассмотрели, как скрыть наш компонент от глобальных стилей, но как именно мы добавляем стили к нашим компонентам? Изучая Shadow DOM, мы обнаружили свойство под названием adoptedStyleSheets, которое показалось многообещающим. Он принимает объект таблицы стилей и прикрепляет его к нашей Shadow DOM. Примерно так:

const styleSheet = new CSSStyleSheet()
styleSheet.replaceSync('img { backgroundColor: 'white' }');
...
this.shadow.adoptedStyleSheets = [styleSheet];

Проблема в том, что конструируемые [sic] таблицы стилей работают только в Chrome на момент написания этой статьи. Итак, нам пришлось выбрать решение, которое будет работать для всех браузеров (особенно потому, что многие пользователи PBS NewsHour используют IE11).

Вместо этого мы просто создаем style элемент и сами добавляем его в теневой документ. В нашем примере с изображением кандидата давайте сделаем так, чтобы мы могли отметить кандидата как «победителя», указав status.

Теперь мы можем указать, когда кто-то выиграл!

<candidate-image name="pete" status="winner"></candidate-image>

Он не очень хорошо оформлен, но суть вы поняли.

Кроме того, я хотел бы отметить, что @font-face правила CSS не работают в Shadow DOM. Нам нужно было что-то написать, чтобы добавить наши шрифты в настоящий документ, чтобы они были соблюдены. Как ни странно, единственным браузером, который поддерживал @font-face в компонентах с ограниченной областью видимости, было мобильное сафари… ну разберись 🤷‍♀️

Наблюдение за изменениями в реальном времени

Итак, у нас есть стилизованный веб-компонент, работающий, как и ожидалось, для первоначального рендеринга. Но что, если мы обновим status вручную, не обновляя страницу?

document
  .querySelector('candidate-image')
  .setAttribute('status', 'not winner');

Компонент на самом деле здесь ничего не делает, потому что не знает, как наблюдать за этими изменениями. Вот когда в игру вступает attributeChangedCallback. По умолчанию наш компонент не замечает никаких изменений - в конце концов, у компонента может быть много логики, и он не может знать, что делать, без нашего ведома. Итак, нам нужно указать, что происходит, когда пользователь вручную изменяет status в нашем компоненте.

Для этого мы должны сначала указать, какие свойства мы хотим наблюдать. Это делается в статическом геттере observedAttributes, который должен возвращать массив, в котором перечислены свойства, которые мы хотим наблюдать:

static get observedAttributes() {
  return ['status'];
}

Затем мы можем сказать attributeChangedCallback, что делать, когда появятся новые изменения:

attributeChangedCallback(name, oldValue, newValue) {
  if (name === 'status') { 
    // do something 
  }
}

Все, что нам нужно сделать, это удалить текст победителя, если статус не является победителем. Наш обновленный компонент будет выглядеть так:

Теперь наш компонент позволяет нам указывать status кандидата, и он будет обновляться при любых изменениях!

Примеры из реального мира для PBS NewsHour

Компонент candidate-image - это упрощенный пример, но он демонстрирует все, что мы сделали для PBS NewsHour при создании графики выборов. Вы можете увидеть пару из них на практике на их страницах «результатов»:



Или вы можете увидеть их использование прямо в середине статьи:



Они даже использовали их в прямом эфире!

Все они были созданы с использованием технологий по умолчанию, доступных нам в браузере (специальной библиотеки не требуется). Мы просто делаем запрос к их API, чтобы получить самые свежие данные, а затем заполняем некоторый HTML, который мы создали в компонентах. Никаких уловок или волшебства - это просто JavaScript, HTML и CSS.