Русский
Русский
English
Статистика
Реклама

Web-components

От библиотеки компонентов к дизайн-системе

23.06.2020 12:07:45 | Автор: admin


Разработка UI-компонентов одна из самых затратных задач при разработке фронтенда. Чтобы оптимизировать процессы, некоторые компании поручают её выделенной команде. Мы же решили пойти другим путём: сделать библиотеку, чтобы компоненты туда могли добавлять разработчики из разных команд. В этой статье расскажу, как наша библиотека организована.


Задачи библиотеки компонентов


В компании, где я работаю, создают несколько разных продуктов. У каждого своя команда, но интерфейс выполнен в одном стиле.

Интерфейсы продуктов для управления инфраструктурой и сайтами от ISPsystem

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


  • Поддержка версионности
  • Простое добавление компонентов
  • Совмещение кода компонента с визуальными требованиями к нему.

Как мы эти требования реализовали, подробно расскажу дальше. Если коротко, то использовали Verdaccio, Stencil, импорт svg-файлов из Фигмы.


А давайте сделаем не одну, а две библиотеки


Несколько лет назад мы начали писать новый интерфейс для всей линейки продуктов ISPsystem. Переделку начали с биллинга, а когда встал вопрос о старте ещё одного проекта, задумались о переиспользовании написанного кода. Тогда и пришла мысль о библиотеке компонентов.


Один из наших разработчиков был покорен идеей веб-компонентов, в ту пору не так известных, и заразил нас. Идея и правда отличная: веб-компоненты можно использовать в любых веб-приложениях и на любых фреймворках. А Angular мы только начинали использовать и не были уверены (или душу грела мысль о возможности выбора), что новый проект будем делать именно на нем. Поэтому мы решили сделать две библиотеки: одну на веб-компонентах, вторую на Angular. Первую назвали ispui, вторую ngispui.


Задумали, что компоненты из ispui будем использовать где угодно, как на сайте, так и в проектах на разных фреймворках; а ngispui будет состоять из оберток для веб-компонентов, для удобства использования в ангуляр-проектах.



Светлая идея разбилась о реальность. Писать на чистых веб-компонентах не так удобно и быстро, как на Angular, поэтому мы их не разрабатывали. К тому же поддерживать три проекта (основной, плюс две библиотеки) было не очень удобно. Добавили в ispui пару веб-компонентов и несколько CSS-компонентов, и оставили. Сосредоточились на компонентах на ngispui.


Так основная работа стала вестись в библиотеке для Angular. За время существования мы добавили туда более 50 компонентов. И это действительно принесло плоды: новые проекты стартовали меньше, чем за год, тогда как первые мы делали почти два года.


Библиотека компонентов на Angular: как работает


Первая версия библиотеки публиковалась в Git. О её устройстве мырассказывалиещегодназад. Схема рабочая, но не идеальная: все версии были прибиты железно, поэтому воспользоваться преимуществами версионности было нельзя. Как следствие, возникали проблемы при сборке Angular, если у пакетов из библиотеки был один и тот же зависимый пакет, но разных версий.


В комментариях под той статьей нам посоветовали локальный npm-регистр пакетов verdaccio. Мы развернули его локально, с авторизацией через GitLab. Жить стало проще: можно пользоваться всеми фишками версионности. И скрипт публикации был переписан.


При публикации в verdaccio мы смотрим на бранч, из которого происходит публикация, если это не master, то в момент публикации к версии добавляем суффикс dev-текущая дата и время (например 1.0.0-dev12.12.2019). В package.json в репозитории версия остается без префикса. Поднимаем версию и пишем changelog руками. А когда бранч с новой версией компонента мёржится в master, в CI запускается публикация стабильной версии. И ничего в этот момент не нужно править в исходниках. Что удобно.


Требования к добавлению компонентов в библиотеку просты, это тесты + демо-страница. Для быстрого добавления компонентов в библиотеку у нас есть скрипт на plop, который генерирует шаблон демо-страницы и добавляет его в демо-приложение. Разработчику остаётся только сделать пример UI-компонента с описанием API.


Получается, от продуктового разработчика требуется минимум усилий, чтобы добавить компонент в библиотеку. Но всё же это чуть сложнее, чем просто сделать компонент внутри продукта.


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


Каждому компоненту своя версия


Библиотека сделана как одно приложение с демо, но каждый компонент публикуется отдельно. То есть каждый компонент это отдельный пакет со своими версиями.


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


Помимо плюсов, такой подход несёт и минусы. Получается, что при публикации одного компонента надо:


  • собирать всю библиотеку, проходить все тесты;
  • если есть зависимые пакеты, добавлять их в демо-приложение;
  • прописывать зависимости компонента в package.json.

Зависимости установлены в демо-приложении и не влияют на сборку и публикацию компонента, поэтому о них легко забыть. Но если их не прописать, это сломает сборку продуктового приложения.




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


При мерж-реквесте новый компонент должен пройти ревью другого разработчика и дизайнера. Если разработчик еще может собрать приложение, то дизайнеру это вообще ни к чему. Чтобы ревьюер мог сразу посмотреть и код, и то, как он выглядит в браузере, в каждом мерж-реквесте должна быть ссылка на демо-версию компонента.


Сборку демо-версии мы тоже автоматизировали: во время мерж-реквеста в CI запускается задача, которая собирает демо-приложение и загружает его в папку на веб-сервере с nginx. Папка создается по названию бранча, это позволяет хранить много демо-версий компонента одновременно. Разработчик получает готовую ссылку и добавляет к описанию мерж-реквеста.


Превращаем библиотеку в дизайн-систему


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


Дизайнеры сделали в Фигме страницы с описанием компонентов. Мы договорились об едином наименовании. И теперь при сборке библиотеки мы просто скачиваем страницы из Фигмы и вставляем их в виде SVG-файлов в демо страницы с компонентами. Решение не идеальное: на SVG-картинке текст не выделишь и поиском по странице не найдёшь, но прочитать можно.


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



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

Считаем, как часто используется компонент


С библиотекой компонентов работает четыре команды. Каждая может вносить изменения, а это чревато: можно кому-то что-то сломать нарочно или нет.


Чтобы это обойти, решили прикрутить статистику. Исходили из мысли, что если человек знает, сколько людей помимо него используют компонент из библиотеки, он будет осторожнее. Или не будет если компонент никто не использует.


Когда к нам в очередной раз пришли практиканты, мы попросили их написать сервис сбора статистики по использованию компонентов из библиотеки. Ребята сделали сервис из нескольких частей:


  1. первый скрипт забирает информацию о зависимостях из package.json;
  2. второй скрипт парсит html на использование UI-компонентов и атрибутов этих компонентов;
  3. компонент-виджет отображает собранную статистику.


Чем больше пользователей у компонента, тем страшнее разработчику с ним хулиганить

Обновляем библиотеку на новую версию Angular


Библиотека компонентов на Angular получилось хорошей: много компонентов, рабочая инфраструктура, понятная схема работы. Но есть у нее и больные места. Одно из них обновление на актуальную версию Angular.


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


Кроме этого, сам процесс обновления и публикации 50+ компонентов тяжелый, даже с учетом автоматизации. Он отнимает время, которое можно было потратить на что-то более полезное и интересное.


Возрождаем библиотеку веб-компонентов


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


Так мы вернулись к библиотеке веб-компонентов ispui. Чтобы не писать на чистых веб-компонентах, мы стали искать библиотеки и фреймворки, которые могли упростить нашу задачу. Angular Elements отпал сразу, из-за привязки к версии Angular, и сама реализация пока сыровата для использования в качестве библиотеки. LitElement довольно простой, но на тот момент не был совместим с Typescript, а Typescript мы любим. Наши ребята в свободное время написали свой класс для написания веб-компонентов AbstractElement, но ресурсов на его разработку и поддержку у нас нет, а на сдачу делать не вариант. А вот Stencil нас покорил: поддержка Typescript, TSX, ангуляр-подобный синтаксис с декораторами, готовая экосистема с тестами, генерацией документации и лоадер для использования в Angular и в других фреймворках.


Сейчас активно разрабатываем библиотеку ispui, учитывая предыдущий опыт и пожелания. Что уже сделали:


  • Монорепозиторий управляемый lerna. С ним можно независимо запускать сборку, тесты и публикацию отдельных пакетов.
  • Для каждого компонента своя демка в виде html-файла. При сборке она добавляется к демо приложению.
  • Для stencil-компонентов генерируется документация, которая вставляется в демо-приложение с описанием API компонента, используемых CSS-переменных.
  • Для автоматического поднятия версий и генерации чейнджлога используем написание коммитов по конвенции коммитов. Для этого удобно использовать утилиту git-cz

Заключение: как разрабатывать дизайн-систему, когда ресурсы ограничены


Проект в активной разработке, но подвести промежуточные итоги уже можно. Вот, на что стоит обращать внимание, если вам нужна дизайн-система, а ресурсы ограничены.


  • Используйте готовые инструменты по управлению инфраструктурой. Например, lerna.
  • Используйте автоматический листинг, настроенные tslint/eslint, stylelint, commitlint. Они позволяют валидировать проект автоматически.
  • Автоматизируйте рутинные процессы. Например, создание демо-страниц.
  • Используйте фреймворки и библиотеки с развитой инфраструктурой с настроенной тестовой средой, и автоматической генерации документации.
Подробнее..

Веб-компоненты в реальном мире (часть 2)

16.08.2020 16:17:46 | Автор: admin

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


rusty carPhoto by Brandon Molitwenik on Unsplash


Сломанный HTML


В HTML есть много полезных возможностей, которые позволяют реализовывать функциональность без использования JavaScript. Одной из таких фич является возможность отправки формы при нажатии на клавишу Enter в любом поле ввода. Вот пример:


<form>  <label>First name: <input type="text"></label>  <label>Last name: <input type="text"></label>  <button>Send!</button></form>

Вводим текст, нажимаем Enter, данные отправляются на сервер, никакого JavaScript. При желании можно избежать перезагрузки страницы, и сделать отправку данных через AJAX, но и в этом случае количество JS будет минимальным.


А теперь попробуем заменить обычную кнопку на веб-компонент:


<form>  <label>First name: <input type="text"></label>  <label>Last name: <input type="text"></label>  <my-button>Send!</my-button></form>

Веб-компонент my-button внутри себя содержит всё ту же кнопку, визуально никаких отличий нет. А вот отправка формы по нажатию Enter сломалась! Вот демо, можете убедиться в этом сами.


В чем причина такого поведения? Это недоработка спецификации веб-компонентов, вот тикет. В библиотеках разработчики обходят эту проблему с помощью вот такого костыля, например. Сам код выглядит не очень страшно, но давайте на секунду задумаемся: мы пишем кастомный Javascript, чтобы починить поведение, которое сломал веб-стандарт. На всякий случай напомню, что спецификации веб-компонентов уже 8 лет и изо всех утюгов трубят, что она уже production-ready.


Но это ещё не всё, что умудрились сломать в веб-компонентах. В HTML есть такая фича, автоматический фокус поля ввода при нажатии на соседний label. Очень удобно, не обязательно целиться в маленький квадрат, можно нажать на текст рядом. Но не в случае веб-компонентов! Вот пример:


<label>First name: <input type="text"></label><label>Last name: <my-input></label>

На демо видно, что обычный тэг input можно выделить нажатием на "First name", а вот нажатие на "Last name" веб-компонент выделить не может. Проблема! На эту тему есть открытый тикет с последним комментарием 2 года назад, так что скорого разрешения тут ждать не стоит. У разработчиков пока есть только один способ объединить label и input в один компонент. А как быть, если дизайн этого не позволяет? Тут два варианта, либо уговаривать дизайнеров придумать что-то совместимое с веб-компонентами, либо отказаться от веб-компонентов в своем проекте (по крайней мере, от ShadowDOM).


CSP


В своё время нашумел "Рассказ о том, как я ворую номера кредиток и пароли у посетителей ваших сайтов". В качестве одной из мер защиты там упоминается CSP возможность указать белый список доменов, на которые разрешено делать запросы с вашей страницы. Одним из побочных эффектов внедрения CSP является невозможность использовать <style></style> тэги, только внешние файлы через <link rel="stylesheet"> (конечно, можно разрешить style-тэги обратно, через директиву 'unsafe-inline', но как видно из её названия, это будет ослабление вашей защиты).


При чем здесь веб-компоненты? Дело в том, что содержимое ShadowDOM полностью изолированно от внешних стилей, загруженных на страницу, поэтому для стилизации внутри ShadowDOM обычно используются style-тэги, что противоречит CSP. Два самых популярных веб-компонент фреймворка имеют с этим проблемы: Stencil (тикет) и LitElement (тикет).


Свет в конце туннеля есть планируется новое Constructable Stylesheets API, которое позволит создавать стили для ShadowDOM в безопасной форме без необходимости в unsafe-inline. А пока разработчикам придется делать выбор либо CSP, либо веб-компоненты.


Lifecycle-хаос


В хорошей архитектуре компоненты должны выполнять роль кирпичиков, из которых собирается большой проект. Например, мы можем получить такую комбинацию (по аналогии с material-web-components):


<my-menu>    <my-menu-item />    <my-menu-item /></my-menu>

В этой ситуации два веб-компонента должны взаимодействовать друг с другом. Обычно это делается в connectedCallback. Веб-компонент подключается в DOM и осматривается вокруг. В случае подобных композитных компонентов может иметь значение, на каком компоненте этот метод вызовется первым. Проведем тест:


class MyMenu extends HTMLElement {    connectedCallback() {        console.log('my menu')    }}class MyMenuItem extends HTMLElement {    connectedCallback() {        console.log('my menu item')    }}// регистрацияcustomElements.define('my-menu', MyMenu)customElements.define('my-menu-item', MyMenuItem)

Запускаем демо, смотрим в консоль и видим:


"my menu""my menu item""my menu item"

Можно предположить что connectedCallback вызывается на родительском элементе, потом на дочерних. Звучит логично, почему нет. А что, если мы сделаем маленькое изменение и откроем второе демо:


"my menu item""my menu item""my menu"

Как это получилось? Почему my-menu теперь опаздывает? В HTML изменений нет, но мы переставили эти две строки местами


// былоcustomElements.define('my-menu', MyMenu)customElements.define('my-menu-item', MyMenuItem)// сталоcustomElements.define('my-menu-item', MyMenuItem)customElements.define('my-menu', MyMenu)

Оказывается, порядок регистрации элементов влияет на порядок вызова connectedCallback. В практическом смысле это означает то, что мы не можем знать порядок вызова методов, и наш код должен быть готов обработать оба варианта. С вариантом "нас вызвали слишком рано" все просто, добавляем window.setTimeout делаем нашу инициализацию попозже. В случае "нас вызвали слишком поздно" ситуация хуже, мы уже не сможем отменить начатые операции. Поэтому на веб-компонентах не получится сделать нормально работающий компонент спойлера


Пример спойлера

Спасибо что заглянули, вот вам котик:


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


Выводы


В веб-компонентах повсюду раскиданы грабли, грамотно присыпанные маркетингом от Гугла. В стандарте еще много неразрешенных вопросов, которые могут оказаться непреодолимым препятствием для ваших проектов. Было бы полезно знать о потенциальных граблях заранее, чтобы принять более взвешенное решение, использовать ли веб-компоненты и фреймворки на их основе, или остаться с простым старым подходом на HTML/JS/CSS. Надеюсь, эта статья была полезной, спасибо за внимание!

Подробнее..

Перевод Веб-компоненты руководство для начинающих

21.10.2020 14:12:03 | Автор: admin

Узнайте о преимуществах использования веб-компонентов, о том, как они работают, а также о том, как начать их использовать

С помощью веб-компонентов (далее компоненты) разработчики могут создавать собственные HTML-элементы. В данном руководстве вы узнаете все, что должны знать о компонентах. Мы начнем с того, что такое компоненты, каковы их преимущества, а также из чего они состоят.

После этого мы приступим к созданию компонентов, сначала с помощью шаблонов HTML (HTML templates) и интерфейса теневого DOM (shadow DOM), затем немного углубимся в тему и посмотрим как создать кастомизированный встроенный элемент (customized build-in element).

Что такое компоненты?


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

XML Binding Language от Mozilla и спецификации HTML Component от Microsoft для Internet Explorer 5 появились около 20 лет назад. К сожалению, обе реализации были очень сложными и не смогли заинтересовать производителей других браузеров, а потому вскоре были забыты. Несмотря на это, именно они заложили основы того, что мы имеем в этой сфере сегодня.

JavaScript-фреймворки, такие как React, Vue и Angular используют аналогичный подход. Одной из главных причин их успеха является возможность инкапсуляции общей логики приложения в некие шаблоны, легко переходящие из одной формы в другую.

Хотя данные фреймворки улучшают опыт разработки, за все приходится платить. Особенности языка, такие как JSX, нуждаются в компиляции, и большинство фреймворков используют движок JavaScript для управления своими абстракциями. Существует ли другой подход к решению проблемы разделения кода на компоненты? Ответ веб-компоненты.

4 столпа компонентов


Компоненты состоит из трех интерфейсов (API) кастомные элементы (custom elements), шаблоны HTML (HTML templates) и теневой DOM (shadow DOM), а также из лежащих в их основе модулей JavaScript (ES6 modules). С помощью инструментов, предоставляемых этими интерфейсами, можно создавать кастомные HTML-элементы, которые ведут себя подобно нативным аналогам.

Компоненты используются также, как и обычные элементы HTML. Их можно настраивать с помощью атрибутов, получать с помощью JavaScript, стилизовать с помощью CSS. Главное уведомить браузер о том, что они существуют.

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

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

Рассмотрим каждую спецификацию по-отдельности.



1. Кастомные элементы


Ключевые особенности:

  • Определение поведения элемента
  • Реагирование на изменения атрибутов
  • Расширение существующих элементов

Часто при разговоре о компонентах, люди имеют ввиду интерфейс кастомных элементов.

Этот API позволяет расширять элементы, определяя их поведение при добавлении, обновлении и удалении.

class ExampleElement extends HTMLElement {  static get observedAttributes() {      return [...]  }  attributeChangedCallback(name, oldValue, newValue) {}  connectedCallback() {}}customElements.define('example-element', ExampleElement)

Каждый кастомный элемент имеет похожую структуру. Он расширяет функционал существующего класса HTMLElements.

Внутри кастомного элемента содержится несколько методов, которые называются реакциями (reactions), отвечающих за обработку того или иного изменения элемента. Например, connectedCallback вызывается при добавлении элемента на страницу. Это похоже на стадии жизненного цикла, используемые в фреймворках (componentDidMount в React, mounted в Vue).

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

Элемент должен быть определен перед тем, как браузер сможет его использовать. Метод define принимает два аргумента название тега и его класс. Все теги должны содержать символ "-" во избежание конфликтов с существующими и будущими нативными элементами.

<example-element>Content</example-element>

Элемент может использовать как обычный тег HTML. При обнаружении такого элемента, браузер связывает его поведение с указанным классом. Данный процесс называется обновлением (upgrading).

Существует два типа элементов автономные (autonomous) и кастомизированные встроенные (customized build-in). До сих пор мы рассматривали автономные элементы. Это такие элементы, которые не связаны с существующими HTML-элементами. Подобно тегам div и span, которые не имеют определенного семантического значения.

Кастомизированные встроенные элементы как следует из их названия расширяют функционал существующих элементов HTML. Они наследуют семантическое поведение этих элементов и могут его изменять. Например, если элемент input был кастомизирован, он все равно останется полем для ввода данных и частью формы при ее отправке.

class CustomInput extends HTMLInputElement {}customElements.define('custom-input', CustomInput, { extends: 'input' })

Класс кастомизированного встроенного элемента расширяет класс кастомизируемого элемента. При определении (define) встроенного элемента в качестве третьего аргумента передается расширяемый элемент.

<input is="custom-input" />

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

В то время, как автономные элементы поддерживаются большинством современных браузеров, кастомизированные встроенные элементы поддерживаются только Chrome и Firefox. При использовании последних в браузере, который их не поддерживает, они будут рассматриваться как обычные HTML-элементы, так что, по большему счету, их использование является безопасным даже в таких браузерах.

2. Шаблоны HTML


  • Создание готовых структур
  • Не отображаются на странице до вызова
  • Содержат HTML, CSS и JS

Исторически создание шаблонов на стороне клиента предполагало конкатенацию строк в JavaScript или использование таких библиотек, как Handlebars, разбирающих блоки специальной разметки. Недавно в спецификации появился тег template, который может содержать все, что мы хотим использовать.

<template id="tweet">  <div class="tweet">    <span class="message"></span>      Written by @    <span class="username"></span>  </div></template>

Сам по себе он никак не влияет на страницу, т.е. он не парсится движком, запросы на получение ресурсов (аудио, видео) не отправляются. JavaScript не может получить к нему доступ, а для браузеров это пустой элемент.

const template = document.getElementById('tweet')const node = document.importNode(template.content, true)document.body.append(node)

Сначала мы получаем элемент template. Метод importNode создает копию его содержимого, второй аргумент (true) означает глубокое копирование. Наконец, мы добавляем его на страницу, как любой другой элемент.

Шаблоны могут содержать все, что может содержать обычный HTML, включая CSS и JavaScript. При добавлении элемента на страницу, к нему будут применены стили и запущены скрипты. Помните, что стили и скрипты являются глобальными, а, значит, могут перезаписать другие стили и значения, используемые скриптами.

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

3. Теневой DOM


  • Позволяет избежать конфликта стилей
  • Придумывать названия (классов, например) становится проще
  • Инкапсуляция логики реализации

Объектная модель документа (Document Object Model, DOM) это то, как браузер интерпретирует структуру страницы. Читая разметку, браузер определяет какие элементы какой контент содержат и на основе этого принимает решение о том, что следует отображать на странице. При использовании document.getElemetById(), например, браузер обращается к DOM в поисках нужного элемента.

Для макета (layout) страницы такой подход является приемлемым, но что насчет деталей, скрытых внутри элемента? Например, страницу не должно беспокоить то, какой интерфейс содержится внутри элемента video. Вот где теневой DOM приходит на помощь.

<div id="shadow-root"></div><script>  const host = document.getElementById('shadow-root')  const shadow = host.attachShadow({ mode: 'open' })</script>

Теневой DOM создается в момент применения к элементу. В теневой DOM можно добавлять любой контент, как и в обычный (светлый, light) DOM. На теневой DOM не влияет то, что происходит снаружи, т.е. за его пределами. Обычный DOM также не может получить доступ к теневому напрямую. Это означает, что в теневом DOM мы можем использовать любые названия классов, стили и скрипты и не переживать о возможных конфликтах.

Наилучший результат дает использование теневого DOM вкупе с кастомными элементами. Благодаря теневому DOM при повторном использовании компонента его стили и структура никак не влияют на другие элементы на странице.

ES и HTML модули

  • Добавление при необходимости
  • Предварительная генерация не требуется
  • Все хранится в одном месте

В то время, как три предыдущие спецификации прошли долгий путь в своем развитии, способ их упаковки и переиспользования остается предметом напряженных дискуссий.

Спецификация импортов HTML (HTML Imports) определяет способ экспорта и импорта HTML документов, а также CSS и JavaScript. Это позволило бы кастомным элементам вместе с шаблонами и теневым DOM находится в другом месте и использоваться по необходимости.

Однако, Firefox отказался от реализации данной спецификации в своем браузере и предложил иной способ на основе JavaScript-модулей.

export class ExampleElement external HTMLElement {}import { ExampleElement } from 'ExampleElement.js'

Модули по умолчанию имеют собственное пространство имен, т.е. их содержимое не является глобальным. Экспортируемые переменные, функции и классы, могут импортироваться где и когда угодно и использоваться как локальные ресурсы.

Это прекрасно подходит для компонентов. Кастомные элементы, содержащие шаблон и теневой DOM могут экспортироваться из одного файла и использовать в другом.

import { ExampleElement } from 'ExampleElement.html'

Microsoft выдвинула предложение о расширении спецификации JavaScript-модулей экспортом/импортом HTML. Это позволит создавать компоненты с помощью декларативного и семантического HTML. Данная возможность скоро появится в Chrome и Edge.

Создание собственного компонента


Несмотря на множество вещей, связанных с компонентами, которые могут показаться вам сложными, создание и использование простого компонента занимает всего несколько строк кода. Рассмотрим примеров.


Компоненты позволяют отображать комментарии пользователей с помощью интерфейсов шаблонов HTML и теневого DOM.

Создадим компонент для отображения комментариев пользователей с помощью шаблонов HTML и теневой DOM.

1. Создание шаблона

Компоненту нужен шаблон для копирования перед генерацией разметки. Шаблон может находиться где угодно на странице, класс кастомного элемента получает к нему доступ через идентификатор.

Добавляем элемент template на страницу. Любые стили, определенные в этом элементе, будут влиять только на него.

<template id="user-comment-template">  <style>      ...  </style></template>

2. Добавление разметки

Кроме стилей, компонент может содержать макет (структуру). Для этих целей используется элемент div.

Динамический контент передается через слоты (slots). Добавим слоты для аватара, имени и сообщения пользователя с соответствующими атрибутами name:

<div class="container">  <div class="avatar-container">    <slot name="avatar"></slot>  </div>  <div class="comment">    <slot name="username"></slot>    <slot name="comment"></slot>  </div></div>

Содержимое слота по умолчанию


Контент по умолчанию будет отображаться при отсутствии переданной слоту информации

Данные, переданные слоту, перезаписывают данные в шаблоне. Если слоту не передается информации, отображается контент по умолчанию.

В данном случае, если имя пользователя не было передано, на его месте отображается сообщение No name:

<slot name="username">  <span class="unknown">No name</span></slot>

3. Создание класса

Создание кастомного элемента начинается с расширения класса HTMLElement. Частью процесса настройки является создание теневого корневого узла (shadow root) для рендеринга контента элемента. Открываем его для получения доступа на следующем этапе.

Наконец, сообщаем браузеру о новом классе UserComment.

class UserComment extends HTMLElement {  constructor() {      super()      this.attachShadow({ mode: 'open' })  }}customElements.define('user-comment', UserComment)

4. Применение теневого контента

Когда браузер встречает элемент user-comment, он обращается к теневому корневому узлу для получения его содержимого. Второй аргумент указывает браузеру копировать весь контент, а не только первый слой (элементы верхнего уровня).

Добавляем разметку в теневой корневой узел, что приводит к немедленному обновлению внешнего вида компонента.

connectedCallback() {  const template = document.getElementById('user-comment-template')  const node = document.importNode(template.content, true)  this.shadowRoot.append(node)}

5. Использование компонента

Теперь компонент готов к использованию. Добавляем тег user-comment и передаем ему необходимую информацию.

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

<user-comment>  <img alt="" slot="avatar" src="avatar.png" />  <span slot="username">Matt Crouch</span>  <div slot="comment">This is an example of a comment</div></user-comment>

Рсширенный код примера:
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Web Components Example</title>    <style>      body {        display: grid;        place-items: center;      }      img {        width: 80px;        border-radius: 4px;      }    </style>  </head>  <body>    <template id="user-comment-template">      <div class="container">        <div class="avatar-container">          <slot name="avatar">            <slot class="unknown"></slot>          </slot>        </div>        <div class="comment">          <slot name="username">No name</slot>          <slot name="comment"></slot>        </div>      </div>      <style>        .container {          width: 320px;          clear: both;          margin-bottom: 1rem;        }        .avatar-container {          float: left;          margin-right: 1rem;        }        .comment {          height: 80px;          display: flex;          flex-direction: column;          justify-content: center;        }        .unknown {          display: block;          width: 80px;          height: 80px;          border-radius: 4px;          background: #ccc;        }      </style>    </template>    <user-comment>      <img alt="" slot="avatar" src="avatar1.jpg" />      <span slot="username">Matt Crouch</span>      <div slot="comment">Fisrt comment</div>    </user-comment>    <user-comment>      <img alt="" slot="avatar" src="avatar2.jpg" />      <!-- no username -->      <div slot="comment">Second comment</div>    </user-comment>    <user-comment>      <!-- no avatar -->      <span slot="username">John Smith</span>      <div slot="comment">Second comment</div>    </user-comment>    <script>      class UserComment extends HTMLElement {        constructor() {          super();          this.attachShadow({ mode: "open" });        }        connectedCallback() {          const template = document.getElementById("user-comment-template");          const node = document.importNode(template.content, true);          this.shadowRoot.append(node);        }      }      customElements.define("user-comment", UserComment);    </script>  </body></html>





Создание кастомизированного встроенного элемента


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

1. Создание класса

Встроенные элементы, как и автономные, появляются в момент расширения класса, но вместо общего класса HTMLElement, они расширяют конкретный класс.

В нашем случае таким классом является HTMLTimeElement класс, используемый элементами time. Он включает поведение, связанное с атрибутом datetime, включая формат данных.

class RelativeTime extends HTMLTimeElement {}

2. Определение элемента

Элемент регистрируется браузером с помощью метода define. Однако, в отличие от автономного элемента, при регистрации встроенного элемента методу define необходимо передать третий аргумент объект с настройками.

Наш объект будет содержать один ключ со значением кастомизируемого элемента. Он принимает название тега. В случае отсутствия такого ключа будет выброшено исключение.

customElements.define('relative-time', RelativeTime, { extends: 'time' })

3. Установка времени

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

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

setTime() {  this.innerHTML = timeago().format(this.getAttribute('datetime'))  this.setAttribute('title', this.getAttribute('datetime'))}

4. Обновление соединения

Компонент может использовать метод сразу после отображения на странице. Поскольку встроенные компоненты не имеют теневого DOM, они не нуждаются в конструкторе.

connectedCAllback() {  this.setTime()}

5. Слежение за изменением атрибутов

Если программно обновить время, компонент не отреагирует. Он не знает о том, что должен следить за изменениями атрибута datetime.

После определения наблюдаемых атрибутов (observed attributes), attributeChangedCallback будет вызываться при каждом их изменении.

static get observedAttributes() {  return ['datetime']}attributeChangedCallback() {  this.setTime()}

6. Добавление на страницу

Поскольку наш элемент является расширением нативного элемента, его имплементация немного отличается. Для его использования добавляем на страницу тег time со специальным атрибутом is, значением которого является название встроенного элемента, определенного при регистрации. Браузеры, которые не поддерживают компоненты, будут отображать запасной контент.

<time is="relative-time" datetime="2020-09-20T12:00:00+0000">  20 сентября 2020 г. 12:00</time>

Расширенный код примера:
<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <meta name="viewport" content="width=device-width, initial-scale=1.0" />    <title>Web Components Another Example</title>    <!-- timeago.js -->    <script      src="http://personeltest.ru/aways/cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"      integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="      crossorigin="anonymous"    ></script>    <style>      body {        display: flex;        flex-direction: column;        align-items: center;      }      input,      button {        margin-bottom: 0.5rem;      }      time {        font-size: 2rem;      }    </style>  </head>  <body>    <input type="text" placeholder="2020-10-20" value="2020-08-19" />    <button>Set Time</button>    <time is="relative-time" datetime="2020-09-19">      19 сентября 2020 г.    </time>    <script>      class RelativeTime extends HTMLTimeElement {        setTime() {          this.innerHTML = timeago.format(this.getAttribute("datetime"));          this.setAttribute("title", this.getAttribute("datetime"));        }        connectedCallback() {          this.setTime();        }        static get observedAttributes() {          return ["datetime"];        }        attributeChangedCallback() {          this.setTime();        }      }      customElements.define("relative-time", RelativeTime, { extends: "time" });      const button = document.querySelector("button");      const input = document.querySelector("input");      const time = document.querySelector("time");      button.onclick = () => {        const { value } = input;        time.setAttribute("datetime", value);      };    </script>  </body></html>





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

Нарушает ли React DOM-стандарты?

13.03.2021 18:22:01 | Автор: admin

Существует довольно популярный сайт https://custom-elements-everywhere.com где показывается как работают веб-компоненты в разных фреймворках. Почти у всех фреймворков там красивый 100% результат, но у React там очень настораживающие 71%:

Рейтинг React на custom-elements-everywhere.comРейтинг React на custom-elements-everywhere.com

Многие пользователи смотрят на эту страничку и делают вывод, что React плохо поддерживает не только веб-компоненты, но и DOM API в принципе. Так ли это? Действительно ли все плохо?

Давайте разбираться!

Анализируем тесты

Рейтинг высчитывается на основании тестов. Вот тут показывается результат. Всего 15 тестов, 7 из них сломаны, отсюда получаем такой неважный рейтинг. Сломаны следующие тесты:

  • attributes and properties

    • will pass array data as a property

    • will pass object data as a property

  • events

    • can declaratively listen to a lowercase DOM event dispatched by a Custom Element

    • can declaratively listen to a kebab-case DOM event dispatched by a Custom Element

    • can declaratively listen to a camelCase DOM event dispatched by a Custom Element

    • can declaratively listen to a CAPScase DOM event dispatched by a Custom Element

    • can declaratively listen to a PascalCase DOM event dispatched by a Custom Element

Можно заметить, что все это тесты с похожими именами, то есть они тестируют разные аспекты одной и той же штуки. Скорее всего все эти падения вызваны одной проблемой и можно исправить их все одним изменением. Поэтому цифра в 71% по сути ничего не значит, это могло быть и 90% и 15% в зависимости от фантазии автора, и сколько тест-кейсов он написал.

Однако факт остается, что-то не работает, и с этим нужно разбираться. Вся ситуация сводится к двум баг-репортам на Github:

Какие же сложности мешают "просто взять и починить" фреймворк? Давайте сначала разберемся с событиями.

События

В современных фреймворках принято добавлять обработчики событий сразу рядом со обычными атрибутами:

<input type="checkbox" checked={checked} onChange={handleChange} />

Это позволяет сразу видеть что подается на вход и выход элемента в одном месте. С веб-компонентами хотелось бы делать то же самое:

<custom-checkbox checked={checked} onChange={handleChange} />

Однако это не работает, потому что не каждый атрибут начинающийся с on* React превращает в обработчик события. Вместо этого они поддерживают избранный (пусть и довольно большой) список событий. Почему они так сделали, объясняет Sebastian Markbge (один из разработчиков React) в этом комментарии. Вот мой перевод:

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

Конечно, мы можем добавить простую реализацию, которая оставляет эти вопросы открытыми. Однако, мы уже знаем, что даже если мы добавим такую простую модель, мы захотим задепрекейтить ее и удалить в будущем в пользу наших текущих изысканий. Нам кажется, что работа не стоит того ради небольшого синтаксического улучшения. Экосистема будет вынуждена поддерживать как ручные ссылки на DOM-элементы, _так и_ этот новый синтаксис, пока мы разрабатываем новую систему. Вместо этого вы можете уже сейчас пользоваться ручным управлением через ссылки на DOM-элементы (refs) и это будет рабочий вариант.

Таким образом, React не предоставляет прямого доступа к DOM-событиям, потому что они хотят их обернуть в свои абстракции, чтобы лучше поддержать будущие фичи React (вроде Concurrent mode), у разработчиков уже сейчас есть вариант добавлять события вручную через Refs API:

function CustomCheckbox({ checked, handleChange }) {const ref = useRef();useEffect(() => {ref.current.addEventListener("change", handleChange);return () => ref.current.removeEventListener("change", handleChange);  }, [handleChange]);  return <custom-checkbox ref={ref} />;}

Если возможность добавления событий есть, то почему же тогда это не делается в тестах custom-elements-everywhere? Автор этой странички считает это хаком и настаивает на том, что поддержка должна быть встроена во фреймворк. Получается, что результаты этого теста основаны на субъективном мнении, а не технической возможности/невозможности решить задачу.

Свойства или атрибуты?

Вторая причина так называемой "несовместимости в веб-компонентами" это невозможность передать в них свойства. Все, что вы передаете веб-компоненту в JSX будет превращено в атрибут. Атрибуты могут быть только строками, поэтому передать сложный объект не получится (хаки с JSON.stringify) в расчет не берем:

<user-view user="{user}" /><!-- рендерит <user-view user="[object Object]" /> -->

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

function UserView({ user }) {const ref = useRef();// обновлять свойство при новом объекте useruseEffect(() => (ref.current.user = user), [user]);return <user-view ref={ref} />;}

Более того, если написание обертки для каждого свойства и компонента может утомить, то можно взять вот этот маленький модуль, который научит JSX работать и со свойствами, и с событиями как надо:

/** @jsx h */import { createElement } from "react";import val from "@skatejs/val";const h = val(createElement);function Checkbox({ checked, handleChange }) {// работает!return <custom-checkbox checked={checked} onChange={handleChange} />;}

Таким образом, технически задача решается, есть и переиспользуемый плагин, но Google Developer Advocates недовольны тем, что решение это не такое, как им бы хотелось. Ради этого они готовы дезинформировать публику о том, что у React есть проблемы с совместимостью с DOM (которых на самом деле нет).

Достоверность 100% рейтинга

Еще есть интересная ситуация со 100% результатами. Действительно ли это гарантия отсутствия проблем совместимости? Как бы не так!. Имена событий могут быть любой строкой и содержать любые символы (вы же всегда хотели сделать new CustomEvent('клик!'))?

Иногда это вызывает проблемы совместимости с синтаксисом шаблонов. Например, в Angular нельзя использовать двоеточие при назначении обработчика событий в шаблонах. При этом materials-components-web использует такую систему именования событий: MDCSlider:change. Возникает ироничная ситуация когда один проект Google (Angular) несовместим с другим проектом той же компании (Material design). Решение всё то же, нам уже знакомое добавим обертку и назначим обработчик через прямое обращение к DOM-элементу.

Таким образом, необходимость создания оберток, из-за которой React влепили его 71% рейтинга, не помешала дать Angular 100%. Вот такой вот непредвзятый рейтинг.

На всякий случай замечу, что это не является проблемой ни одного из фреймворков. Имена свойств и событий могут быть самыми разными и не вписываться в синтаксис шаблонов. Явное лучше неявного, и брать ручное управление DOM в особых случаях это нормально. Не очень понятно, какую цель преследует автор custom-elements-everywhere своим рейтингом.

Реальная ситуация

После того как мы разобрались c custom-elements-everywhere, давайте посмотрим на реальные проблемы совместимости между React и DOM API за пределами этой истории с веб-компонентами. Будем честны, они есть:

  1. onChange обработчик в React совсем не равен DOM-событию change. Это действительно проблема, причины такого поведения объясняются в этом [Github issue](https://github.com/facebook/react/issues/9657). Это вызывает сложности как при изучении React, так и при миграции с React на что-то другое, когда выясняется, что onChange в React не настоящий.

  2. onFocus/onBlur события всплывают. В привычном нам DOM API, событие focus, вызывается только на том элементе, который получает фокус. В React же это событие всплывает по дереву компонентов, по сути работает как событие focusin. Больше об этом можно почитать в этом issue.

  3. События всплывают по порталам. Возможно, многие посчитают это фичей, но для полноты картины написать об этом стоит. Portal API позволяет рендерить элементами за пределами основного контейнера с React-приложением. При этом события продолжат всплывать по дереву компонентов а не DOM-дереву, как будто портала никакого и нет.

Список далеко не полный, но это те моменты с которыми довелось лично мне столкнуться и которые я бы хотел исправить. Как видно, все не так уж плохо, и несмотря на эти недостатки, с помощью React можно строить большие, но при этом доступные (семантичные) веб-интерфейсы и использовать полную мощь DOM API в тех местах, где React недостаточно (например, декларативно управлять фокусом через react-focus-lock).

Послесловие

Одним из фундаментальных требований по доступности к веб-сайтам является правильное оформление текстовых полей. Каждому полю должен быть назначен соответствующий label. Часто это делается через for и id атрибуты:

<label for="name">Ваше имя</label><input id="name" name="firstName" />

В реальных проектах обычно создаются переиспользуемые компоненты для единообразного дизайна. На React наш пример может превратиться в такой код:

const id = useUniqueId();<Label for={id}>Ваше имя</Label><Input id={id} name="firstName" />

При этом компоненты Label и Input рендерят соответствующие html-тэги и наше текстовое поле остается семантичным и доступным.

Попробуем сделать то же самое без React, но на веб-компонентах:

<custom-label for="name">Ваше имя</custom-label><custom-input id="name" name="firstName" />

Наше текстовое поле сломалось! Внутренний тэг label не смог связаться с тэгом input, потому что они находятся в разных ShadowDOM инстансах. Существует proposal призванный решить эту проблему, но он еще в зачаточной стадии и не работает ни в одном из браузеров (напоминаю, веб-компоненты разрабатываются без малого уже 10 лет). А в настоящий момент реализовать custom-label и custom-input в виде веб-компонентов, соблюдая требования доступности, не получится.

Вот и думайте сами, какая технология тут является настоящим нарушителем веб-стандартов.

Подробнее..

Custom Elements из Angular в Angular

07.02.2021 18:17:06 | Автор: admin

Angular позволяет создавать кастомные веб-компоненты на основе своих компонентов.

Зачем?

Код, собранный в веб-компонент, можно использовать где угодно, достаточно только подключить сгенерированный скрипт. Компонент можно внедрить в код страницы, причём, всё равно каким образом: можно в HTML (<my-custom-component></my-custom-component>), можно через DOM-API (document.body.append(document.createElement('my-custom-component')) ), после этого получим работающее микро-приложение c Angular под капотом.

Задача

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

Итого, задача: динамически создать и отобразить зарегистрированные в браузере веб-компоненты.

Странно, но готовое решение нагуглить не получилось. Пришлось городить велосипед. Пост пишется в том числе и с целью получить правильное решение в комментариях.

Процесс решения

Внимание! Весь код в статье не тестировался и даже не собирался. Приводится исключительно для примера.

Angular позволяет динамически создавать компоненты и в документации описано, как это делать. Проблема в том, что для этого на этапе сборки необходимо знать тип компонента. Не подходит.

NB: По-моему, раньше можно было создавать элементы по селектору, но задепрекейтили.

Первая попытка

Хорошо, там сверху написано, что достаточно просто сгенерировать правильный HTML, попробуем сделать это влоб, через динамически создаваемый компонент-обёртку (и не забыть про санитайзинг):

this.componentTagName = '<my-custom-component></my-custom-component>';
<div [innerHtml]="componentTagName"></div>

Странно, работает.

А как насчёт @Input/@Output в этом HTML?

this.componentTagName = '<my-custom-component [someInput]="someInputVariable"></my-custom-component>';

Не работает. Что там с dependency injection, я думаю, тоже понятно.

Вторая попытка

Так, у нас есть Renderer2, попробуем через него.

<div #componentContainerRef>  Component '{{componentTagName}}' is not registered in the browser's component registry yet.</div>

и

customElements.whenDefined(this.componentTagName).then(() => {  const element = this.renderer.createElement(this.componentTagName);  this.renderer.appendChild(this.componentContainerRef.nativeElement, element);});

Работает. Снова, как насчёт @Input? Естественно, если отображаемый веб-компонент его поддерживает:

customElements.whenDefined(this.componentTagName).then(() => {  const element = this.renderer.createElement(this.componentTagName);  this.renderer.setProperty(element, 'someInput', this.componentInput);  this.renderer.appendChild(this.componentContainerRef.nativeElement, element);});

Работает. А @Output? Только не через setProperty (я его ещё не тестировал, потому что, теоретически, компоненты могут быть абсолютно любыми, не только Angular'овскими), а просто поймаем какое-нибудь событие (типа CustomEvent), которое отправил веб-компонент:

customElements.whenDefined(this.componentTagName).then(() => {  const element = this.renderer.createElement(this.componentTagName);  this.renderer.setProperty(element, 'someInput', this.componentInput);  this.renderer.listen(    element,    'customComponentEvent',     event => this.onEventReceivedFromComponent  );  this.renderer.appendChild(this.componentContainerRef.nativeElement, element);});onEventReceivedFromComponent(event: CustomEvent) {  console.info(`${this.componentTagName} received event: `, event);}

Хм, не работает, теряется this, тогда попробуем вот так:

this.renderer.listen(  element,  'customComponentEvent',  event => this.onEventReceivedFromComponent.apply(this, [event]));

Работает.

А что с dependency injection?

Можно инжектить всё что хочешь в компонент-обёртку и реализовать логику в нём. Как передать/получить данные в/из веб компонента мы уже знаем. Например, вот код компонента и его обёртки с подключением к общей шине событий:

// Angular Elements component import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';const tagName = 'my-custom-component';@Component({  selector: tagName,  templateUrl: '<input [(ngModel)]="messageText" type="text"><button (click)="onSendMessageClick()">Send message</button>'})export class ExternalWidgetComponent implements OnInit {  @Input('messageReceiver') messageReceiver: EventEmitter<any>;  @Output('customComponentMessage') messageSender = new EventEmitter<any>();  static readonly tagName = tagName;  messageText: string = '';  constructor() {  }  ngOnInit(): void {    if (this.messageReceiver) {      this.messageReceiver.subscribe(this.onMessageReceived);    }  }  onSendMessageClick(): void {    this.messageSender.emit({text: this.messageText});  }  onMessageReceived(message) {    console.log(`${tagName} received the message: `, message);  }}// Wrapper componentimport { Component, ElementRef, EventEmitter, Input, Renderer2, ViewChild } from '@angular/core';import { EventBusService } from '../event-bus.service';@Component({  selector: 'web-component-wrapper',  template: `<div #componentContainerRef>Component '{{componentTagName}}' is not registered in the browser's component registry yet.</div>`})export class WebComponentWrapperComponent {  @ViewChild('componentContainerRef') componentContainerRef: ElementRef;  componentTagName = '';  messageSender: EventEmitter<any> = new EventEmitter<any>();  // message name for the event bus  private messageType = 'customComponentMessage';  constructor(private renderer: Renderer2, private eventBus: EventBusService) {  }  renderComponentWhenDefined() {    customElements.whenDefined(this.componentTagName).then(() => {      const element = this.renderer.createElement(this.componentTagName);      this.renderer.setProperty(element, 'messageReceiver', this.messageSender);      this.renderer.listen(        element,        'customComponentEvent',        event => this.onEventReceivedFromComponent.apply(this, [event])      );      this.renderer.appendChild(this.componentContainerRef.nativeElement, element);    });    // re-route messages from the event bus to the web-component    this.eventBus      .observe(this.messageType)      .subscribe(this.sendMessageToComponent(message));  }  set componentName(value: string) {    this.componentTagName = value;    this.renderComponentWhenDefined();  }  // re-route messages from the component to the event bus  onMessageReceivedFromComponent(message: CustomEvent) {    this.eventBus.publish(this.messageType, message.detail);  }  sendMessageToComponent(message: any) {    this.messageSender.emit(message);  }}

Итог

Таким образом можно интегрировать в Angular-приложение доступные в браузере веб-компоненты, при условии, что известно имя компонента и сигнатуры его @Input/@Output (если их нет, см. первую попытку).

Спасибо за внимание.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru