Spec-Zone.ru › Angular

Обзор элементов Angular

Элементы Angular — это компоненты Angular, упакованные в виде пользовательских элементов (также называемых веб-компонентами), веб-стандарта для определения новых элементов HTML независимым от фреймворка способом.

Пользовательские элементы — это функция веб-платформы, доступная во всех браузерах, поддерживаемых Angular. Пользовательский элемент расширяет HTML, позволяя определить тег, содержимое которого создаётся и контролируется кодом JavaScript. Браузер поддерживает CustomElementRegistry определённых пользовательских элементов, в котором класс JavaScript, допускающий создание экземпляров, сопоставляется с тегом HTML.

Пакет @angular/elements экспортирует API createCustomElement(), который обеспечивает связь между интерфейсом компонента Angular и функцией обнаружения изменений и встроенным API DOM.

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

Использование пользовательских элементов

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

Чтобы добавить пакет @angular/elements в рабочую область, выполните следующую команду:

npm install @angular/elements
yarn add @angular/elements
pnpm add @angular/elements
bun add @angular/elements

Принцип работы

Функция createCustomElement() преобразует компонент в класс, который можно зарегистрировать в браузере как пользовательский элемент. Зарегистрировав настроенный класс в реестре пользовательских элементов браузера, используйте новый элемент так же, как встроенный элемент HTML, в содержимом, которое добавляете непосредственно в DOM:

<my-popup message="Use Angular!"></my-popup>

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

Преобразование компонентов в пользовательские элементы

Angular предоставляет функцию createCustomElement() для преобразования компонента Angular вместе с его зависимостями в пользовательский элемент.

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

Используйте встроенную функцию браузера customElements.define(), чтобы зарегистрировать настроенный конструктор и связанный с ним тег пользовательского элемента в CustomElementRegistry браузера. Когда браузер встречает тег зарегистрированного элемента, он использует конструктор для создания экземпляра пользовательского элемента.

ВАЖНО: Не используйте селектор компонента в качестве имени тега пользовательского элемента. Это может привести к неожиданному поведению, поскольку Angular создаст два экземпляра компонента для одного элемента DOM: один обычный компонент Angular и второй — с использованием пользовательского элемента.

Сопоставление

Пользовательский элемент содержит компонент Angular и служит связующим звеном между данными и логикой, определёнными в компоненте, и стандартными API DOM. Свойства и логика компонента напрямую сопоставляются с атрибутами HTML и системой событий браузера.

  • API создания анализирует компонент, чтобы найти входные свойства, и определяет соответствующие атрибуты пользовательского элемента. Имена свойств преобразуются в формат, совместимый с пользовательскими элементами, которые не различают регистр. В полученных именах атрибутов используются строчные буквы, разделённые дефисами. Например, для компонента с inputProp = input({alias: 'myInputProp'}) соответствующий пользовательский элемент определяет атрибут my-input-prop.

  • Выходные данные компонента отправляются как HTML-события пользовательских событий, причём имя пользовательского события совпадает с именем выходного параметра. Например, для компонента с valueChanged = output() соответствующий пользовательский элемент отправляет события с именем "valueChanged", а отправленные данные сохраняются в свойстве detail события. Если указать псевдоним, используется он; например, clicks = output<string>({alias: 'myClick'}); приводит к отправке событий с именем "myClick".

Дополнительные сведения см. в документации по веб-компонентам в разделе Создание пользовательских событий.

Пример: сервис всплывающих окон

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

Использование пользовательского элемента Angular упрощает и делает более прозрачным этот процесс, автоматически предоставляя всю инфраструктуру и средства фреймворка: вам остаётся только определить нужный тип обработки событий. (Если вы не собираетесь использовать компонент в приложении, его всё же нужно исключить из компиляции.)

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

Файлы Описание
popup.ts Определяет простой всплывающий элемент, который отображает входное сообщение и содержит анимацию и стили.
popup.service.ts Создаёт внедряемый сервис, предоставляющий два способа вызвать Popup: как динамический компонент или как пользовательский элемент. Обратите внимание, насколько больше настроек требуется для динамической загрузки.
app.ts Определяет корневой компонент приложения, который использует PopupService для добавления всплывающего окна в DOM во время выполнения. При запуске приложения конструктор корневого компонента преобразует Popup в пользовательский элемент.

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

import {Component, computed, input, output} from '@angular/core';
import {animate, state, style, transition, trigger} from '@angular/animations';

@Component({
  selector: 'my-popup',
  template: `
    <span>Popup: {{ message() }}</span>
    <button type="button" (click)="closed.emit()">&#x2716;</button>
  `,
  animations: [
    trigger('state', [
      state('opened', style({transform: 'translateY(0%)'})),
      state('void, closed', style({transform: 'translateY(100%)', opacity: 0})),
      transition('* => *', animate('100ms ease-in')),
    ]),
  ],
  styles: [
    `
      :host {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        background: #009cff;
        height: 48px;
        padding: 16px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        border-top: 1px solid black;
        font-size: 24px;
      }

      button {
        border-radius: 50%;
      }
    `,
  ],
  host: {
    '[@state]': 'state()',
  },
})
export class Popup {
  readonly message = input('');
  readonly closed = output<void>();

  readonly state = computed(() => (this.message() ? 'opened' : 'closed'));
}
import {
  ApplicationRef,
  createComponent,
  EnvironmentInjector,
  inject,
  Injectable,
} from '@angular/core';
import {NgElement, WithProperties} from '@angular/elements';
import {Popup} from './popup';

@Injectable()
export class PopupService {
  private readonly injector = inject(EnvironmentInjector);
  private readonly applicationRef = inject(ApplicationRef);

  // Previous dynamic-loading method required you to set up infrastructure
  // before adding the popup to the DOM.
  showAsComponent(message: string) {
    // Create element
    const popup = document.createElement('popup-component');

    // Create the component and wire it up with the element
    const popupComponentRef = createComponent(Popup, {
      environmentInjector: this.injector,
      hostElement: popup,
    });

    // Attach to the view so that the change detector knows to run
    this.applicationRef.attachView(popupComponentRef.hostView);

    // Listen to the close event
    popupComponentRef.instance.closed.subscribe(() => {
      document.body.removeChild(popup);
      this.applicationRef.detachView(popupComponentRef.hostView);
    });

    // Set the message
    popupComponentRef.setInput('message', message);

    // Add to the DOM
    document.body.appendChild(popup);
  }

  // This uses the new custom-element method to add the popup to the DOM.
  showAsElement(message: string) {
    // Create element
    const popupEl: NgElement & WithProperties<Popup> = document.createElement(
      'popup-element',
    ) as any;

    // Listen to the close event
    popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));

    // Set the message
    popupEl.setAttribute('message', message);
    // Add to the DOM
    document.body.appendChild(popupEl);
  }
}
import {Component, Injector} from '@angular/core';
import {createCustomElement} from '@angular/elements';
import {Popup} from './popup';
import {PopupService} from './popup.service';

@Component({
  selector: 'app-root',
  template: `
    <input #input value="Message" />
    <button type="button" (click)="popup.showAsComponent(input.value)">Show as component</button>
    <button type="button" (click)="popup.showAsElement(input.value)">Show as element</button>
  `,
  providers: [PopupService],
  imports: [Popup],
})
export class App {
  constructor(
    injector: Injector,
    public popup: PopupService,
  ) {
    // Convert `PopupComponent` to a custom element.
    const PopupElement = createCustomElement(Popup, {injector});
    // Register the custom element with the browser.
    customElements.define('popup-element', PopupElement);
  }
}

Типизация пользовательских элементов

Общие API DOM, такие как document.createElement() или document.querySelector(), возвращают тип элемента, соответствующий указанным аргументам. Например, вызов document.createElement('a') возвращает HTMLAnchorElement, у которого, как знает TypeScript, есть свойство href. Аналогично, document.createElement('div') возвращает HTMLDivElement, у которого, как знает TypeScript, нет свойства href.

При вызове для неизвестных элементов, например пользовательского элемента (в нашем примере это popup-element), методы возвращают общий тип, например HTMLElement, поскольку TypeScript не может определить правильный тип возвращаемого элемента.

Пользовательские элементы, созданные с помощью Angular, расширяют NgElement (который, в свою очередь, расширяет HTMLElement). Кроме того, у таких пользовательских элементов будет свойство для каждого входного параметра соответствующего компонента. Например, у нашего popup-element есть свойство message типа string.

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

@Component(/* ... */)
class MyDialog {
  content = input('');
}

Самый простой способ получить точную типизацию — привести возвращаемое значение соответствующих методов DOM к правильному типу. Для этого используйте типы NgElement и WithProperties (оба экспортируются из @angular/elements):

const aDialog = document.createElement('my-dialog') as NgElement &
  WithProperties<{content: string}>;
aDialog.content = 'Hello, world!';
aDialog.content = 123; // <-- ERROR: TypeScript knows this should be a string.
aDialog.body = 'News'; // <-- ERROR: TypeScript knows there is no `body` property on `aDialog`.

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

Альтернативный способ, при котором тип каждого пользовательского элемента нужно определить только один раз, — расширить HTMLElementTagNameMap. TypeScript использует его, чтобы определить тип возвращаемого элемента по имени его тега (для таких методов DOM, как document.createElement(), document.querySelector() и т. д.):

declare global {
  interface HTMLElementTagNameMap {
    'my-dialog': NgElement & WithProperties<{content: string}>;
    'my-other-element': NgElement & WithProperties<{foo: 'bar'}>;
    …
  }
}

Теперь TypeScript может определять правильный тип так же, как и для встроенных элементов:

document.createElement('div'); //--> HTMLDivElement (built-in element)
document.querySelector('foo'); //--> Element        (unknown element)
document.createElement('my-dialog'); //--> NgElement & WithProperties<{content: string}> (custom element)
document.querySelector('my-other-element'); //--> NgElement & WithProperties<{foo: 'bar'}>      (custom element)

Ограничения

При уничтожении и последующем повторном добавлении пользовательских элементов, созданных с помощью @angular/elements, следует соблюдать осторожность из-за проблем с обратным вызовом disconnect(). С этой проблемой можно столкнуться в следующих случаях:

  • При отображении компонента в ng-if или ng-repeat в AngularJS
  • При ручном отсоединении элемента от DOM и его повторном добавлении

Super-powered by Google ©2010–2025.
Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0.
https://angular.dev/guide/elements

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API