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

Custom Elements из Angular в Angular

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 (если их нет, см. первую попытку).

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

Источник: habr.com
К списку статей
Опубликовано: 07.02.2021 18:17:06
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Разработка веб-сайтов

Программирование

Angular

Web-components

Angular elements

Категории

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

  • Имя: Макс
    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