54. Angular Elements
57. Angular Elements (Web Components) 🧩
Заголовок раздела «57. Angular Elements (Web Components) 🧩»Привет! Яша здесь. Angular Elements — это способ «запаковать» Angular-компонент в стандартный Custom Element (Web Component). Это значит, что твой компонент можно использовать в React, Vue, или даже в обычном HTML без Angular! Разберём как это работает 🚀
Что такое Web Components
Заголовок раздела «Что такое Web Components»Web Components — набор нативных браузерных стандартов:
- Custom Elements — создание кастомных HTML-тегов (
<my-button>) - Shadow DOM — инкапсуляция стилей и разметки
- HTML Templates —
<template>и<slot>
<!-- После упаковки в Angular Elements: --><!-- Используется в любом HTML без Angular! --><script src="my-angular-element.js"></script>
<my-rating-widget value="4" max="5"></my-rating-widget><my-notification type="success" message="Saved!"></my-notification>Создание Angular Element
Заголовок раздела «Создание Angular Element»npm install @angular/elements// rating.component.ts — обычный Angular компонент@Component({ selector: 'app-rating', standalone: true, template: ` <div style="display: flex; gap: 4px;"> @for (star of stars; track star) { <span [style.color]="star <= value ? '#fbbf24' : '#475569'" style="font-size: 24px; cursor: pointer;" (click)="onRate(star)"> ★ </span> } </div> `,})export class RatingComponent { @Input() value = 0; @Input() max = 5; @Output() rated = new EventEmitter<number>();
get stars(): number[] { return Array.from({ length: this.max }, (_, i) => i + 1); }
onRate(star: number): void { this.value = star; this.rated.emit(star); }}// app.module.ts — регистрация как Custom Elementimport { NgModule, Injector } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { createCustomElement } from '@angular/elements';import { RatingComponent } from './rating/rating.component';
@NgModule({ imports: [BrowserModule, RatingComponent],})export class AppModule { constructor(private injector: Injector) {}
ngDoBootstrap(): void { // createCustomElement — превращает Angular компонент в Web Component const RatingElement = createCustomElement(RatingComponent, { injector: this.injector, });
// Регистрируем в браузере под своим тегом customElements.define('app-rating', RatingElement); }}Standalone компоненты как Elements (Angular 14+)
Заголовок раздела «Standalone компоненты как Elements (Angular 14+)»// main.ts — без NgModuleimport { createApplication } from '@angular/platform-browser';import { createCustomElement } from '@angular/elements';import { RatingComponent } from './rating.component';import { NotificationComponent } from './notification.component';
(async () => { const app = await createApplication({ providers: [/* провайдеры */] });
const Rating = createCustomElement(RatingComponent, { injector: app.injector }); const Notification = createCustomElement(NotificationComponent, { injector: app.injector });
customElements.define('app-rating', Rating); customElements.define('app-notification', Notification);})();@Input и @Output маппинг
Заголовок раздела «@Input и @Output маппинг»Angular автоматически маппит:
@Input()→ атрибут/свойство Custom Element@Output()→ CustomEvent
<!-- В HTML атрибуты — всегда строки --><app-user-card user-id="42" show-avatar="true"></app-user-card>
<!-- В JavaScript можно передавать объекты через свойства --><script> const card = document.querySelector('app-user-card');
// Слушаем Angular @Output как DOM событие card.addEventListener('userSelected', (event) => { console.log('Выбран пользователь:', event.detail); });</script>// Angular компонент — без изменений!@Component({ selector: 'app-user-card', ... })export class UserCardComponent { @Input() userId!: number; @Input() user!: User; @Input() showAvatar = true; @Output() userSelected = new EventEmitter<User>();}Shadow DOM
Заголовок раздела «Shadow DOM»// Включение Shadow DOM для инкапсуляции стилей@Component({ selector: 'app-isolated-widget', encapsulation: ViewEncapsulation.ShadowDom, // ← Shadow DOM! template: ` <div class="widget"> <h2>Изолированный виджет</h2> <slot></slot> <!-- слот для контента от родителя --> </div> `, styles: [` /* Эти стили НЕ утекут за пределы Shadow DOM */ .widget { background: #1e293b; padding: 16px; border-radius: 8px; } h2 { color: #dd0031; margin: 0 0 8px; } `]})export class IsolatedWidgetComponent {}Сборка Angular Element в один файл
Заголовок раздела «Сборка Angular Element в один файл»// angular.json — конфигурация для сборки элемента{ "projects": { "elements": { "architect": { "build": { "options": { "outputPath": "dist/elements", "main": "projects/elements/src/main.ts", "polyfills": [], "optimization": true } } } } }}ng build elements --configuration=production
# Объединить в один файл (устаревший способ)cat dist/elements/runtime.js dist/elements/polyfills.js dist/elements/main.js > elements.js
# Современный способ — один бандл через esbuildng build --single-bundle trueИспользование в React
Заголовок раздела «Использование в React»import { useEffect, useRef } from 'react';
// Декларация для TypeScriptdeclare global { namespace JSX { interface IntrinsicElements { 'app-rating': { value?: number; max?: number; onRated?: (event: CustomEvent<number>) => void; }; } }}
function AngularRating({ value, max, onRate }: Props) { const ref = useRef<HTMLElement>(null);
useEffect(() => { const el = ref.current; if (!el) return;
const handler = (e: Event) => onRate?.((e as CustomEvent).detail); el.addEventListener('rated', handler); return () => el.removeEventListener('rated', handler); }, [onRate]);
return <app-rating ref={ref} value={value} max={max} />;}NgElement: типизированный API
Заголовок раздела «NgElement: типизированный API»import { NgElement, WithProperties } from '@angular/elements';import { RatingComponent } from './rating.component';
// Типизированный элементtype RatingElement = NgElement & WithProperties<RatingComponent>;
// Программное созданиеconst rating = document.createElement('app-rating') as RatingElement;rating.value = 3; // TypeScript знает тип!rating.max = 5;document.body.appendChild(rating);
// Получение существующего элементаconst el = document.querySelector('app-rating') as RatingElement;el.value = 5; // полная типизацияMicro-frontend с Angular Elements
Заголовок раздела «Micro-frontend с Angular Elements»// shell/main.ts — загрузка Angular Elements динамическиasync function loadMicroFrontend(url: string, elementName: string): Promise<void> { // Проверяем, не загружен ли уже элемент if (customElements.get(elementName)) { return; }
// Динамическая загрузка скрипта await new Promise<void>((resolve, reject) => { const script = document.createElement('script'); script.src = url; script.onload = () => resolve(); script.onerror = reject; document.head.appendChild(script); });}
// Использованиеawait loadMicroFrontend('https://team-a.example.com/elements.js', 'team-a-widget');// Теперь <team-a-widget> работает везде!Playground 🎮
Заголовок раздела «Playground 🎮»Симуляция Custom Element в не-Angular окружении: