Angular Elements 🧩
Angular Elements allows you to package Angular components as custom elements (web components) that work in any HTML page, even outside Angular applications. Build once, use everywhere!
🎯 What are Angular Elements?
Section titled “🎯 What are Angular Elements?”Angular Elements are Angular components packaged as custom elements - standard web components that work with any JavaScript framework or vanilla HTML.
Key Features:
- 🌐 Framework Agnostic - Use in React, Vue, or vanilla JS
- 📦 Self-Contained - All dependencies bundled
- 🔄 Reusable - Share across projects
- ⚡ Lightweight - Tree-shakeable bundles
Use Cases:
- Widget libraries for multiple frameworks
- Micro frontends architecture
- CMS content widgets
- Third-party embeddable components
🚀 Getting Started
Section titled “🚀 Getting Started”Installation
Section titled “Installation”ng add @angular/elementsThis installs:
@angular/elements- Angular Elements packagedocument-register-element- Polyfill for older browsers
Basic Custom Element
Section titled “Basic Custom Element”import { Component, input } from '@angular/core';import { createCustomElement } from '@angular/elements';
@Component({ selector: 'app-hello', standalone: true, template: `<h1>Hello, {{ name() }}!</h1>`})export class HelloComponent { name = input<string>('World');}
// Register as custom elementconst HelloElement = createCustomElement(HelloComponent, { injector: inject(Injector)});
customElements.define('hello-widget', HelloElement);Usage in HTML:
<hello-widget name="Angular"></hello-widget>🎨 Real-World Examples
Section titled “🎨 Real-World Examples”Note: Angular Elements work with both standalone and module-based components.
1. Counter Widget
Section titled “1. Counter Widget”Let’s create a reusable counter widget.
import { Component, signal, Injector, inject } from '@angular/core';import { createCustomElement } from '@angular/elements';
@Component({ selector: 'app-counter', standalone: true, template: ` <div class="counter"> <button (click)="decrement()">-</button> <span>{{ count() }}</span> <button (click)="increment()">+</button> </div> `, styles: [` .counter { display: flex; gap: 10px; align-items: center; } button { padding: 8px 16px; font-size: 18px; cursor: pointer; } span { font-size: 24px; min-width: 40px; text-align: center; } `]})export class CounterComponent { count = signal(0);
increment() { this.count.update(c => c + 1); }
decrement() { this.count.update(c => c - 1); }}
// Register in main.ts or app configexport function registerCounterElement() { const injector = inject(Injector); const CounterElement = createCustomElement(CounterComponent, { injector }); customElements.define('counter-widget', CounterElement);}Usage:
<!DOCTYPE html><html><head> <script src="counter-widget.js"></script></head><body> <h1>My Page</h1> <counter-widget></counter-widget></body></html>2. User Card Widget
Section titled “2. User Card Widget”Let’s create a user card component with inputs and outputs.
import { Component, input, output, Injector, inject } from '@angular/core';import { createCustomElement } from '@angular/elements';
interface User { name: string; email: string; avatar: string;}
@Component({ selector: 'app-user-card', standalone: true, template: ` <div class="user-card"> <img [src]="user().avatar" [alt]="user().name"> <div class="info"> <h3>{{ user().name }}</h3> <p>{{ user().email }}</p> <button (click)="onContact()">Contact</button> </div> </div> `, styles: [` .user-card { display: flex; gap: 16px; padding: 16px; border: 1px solid #ddd; border-radius: 8px; max-width: 400px; } img { width: 80px; height: 80px; border-radius: 50%; } .info { flex: 1; } button { margin-top: 8px; padding: 8px 16px; background: #1976d2; color: white; border: none; border-radius: 4px; cursor: pointer; } `]})export class UserCardComponent { user = input.required<User>(); contact = output<User>();
onContact() { this.contact.emit(this.user()); }}
// Register elementexport function registerUserCardElement() { const injector = inject(Injector); const UserCardElement = createCustomElement(UserCardComponent, { injector }); customElements.define('user-card', UserCardElement);}Usage:
<user-card id="userCard"></user-card>
<script> const userCard = document.getElementById('userCard');
// Set input userCard.user = { name: 'John Doe', email: 'john@example.com', avatar: '/avatar.jpg' };
// Listen to output userCard.addEventListener('contact', (event) => { console.log('Contact user:', event.detail); });</script>3. Chart Widget
Section titled “3. Chart Widget”Let’s create a chart widget that can be embedded anywhere.
import { Component, input, effect, ElementRef, inject } from '@angular/core';import { createCustomElement } from '@angular/elements';
@Component({ selector: 'app-chart', standalone: true, template: `<canvas #canvas></canvas>`, styles: [` canvas { max-width: 100%; height: 300px; } `]})export class ChartComponent { private el = inject(ElementRef);
data = input.required<number[]>(); type = input<'bar' | 'line'>('bar');
constructor() { effect(() => { this.renderChart(); }); }
private renderChart() { const canvas = this.el.nativeElement.querySelector('canvas'); const ctx = canvas.getContext('2d');
// Simple chart rendering (use Chart.js in production) const data = this.data(); const max = Math.max(...data);
ctx.clearRect(0, 0, canvas.width, canvas.height);
data.forEach((value, index) => { const height = (value / max) * canvas.height; const x = (index * canvas.width) / data.length; const width = canvas.width / data.length - 10;
ctx.fillStyle = '#1976d2'; ctx.fillRect(x, canvas.height - height, width, height); }); }}
// Register elementexport function registerChartElement() { const injector = inject(Injector); const ChartElement = createCustomElement(ChartComponent, { injector }); customElements.define('chart-widget', ChartElement);}Usage:
<chart-widget id="chart" type="bar"></chart-widget>
<script> const chart = document.getElementById('chart'); chart.data = [10, 20, 30, 25, 40];</script>🏗️ Building for Production
Section titled “🏗️ Building for Production”Build Configuration
Section titled “Build Configuration”import { createApplication } from '@angular/platform-browser';import { createCustomElement } from '@angular/elements';import { CounterComponent } from './counter.component';
createApplication({ providers: []}).then(appRef => { const CounterElement = createCustomElement(CounterComponent, { injector: appRef.injector });
customElements.define('counter-widget', CounterElement);});Angular.json Configuration
Section titled “Angular.json Configuration”{ "projects": { "elements": { "architect": { "build": { "options": { "outputHashing": "none", "scripts": [], "styles": [] }, "configurations": { "production": { "optimization": true, "buildOptimizer": true } } } } } }}Bundle Single File
Section titled “Bundle Single File”# Build the projectng build --configuration production
# Concatenate files (using concat or similar tool)cat dist/elements/runtime.js \ dist/elements/polyfills.js \ dist/elements/main.js > widget.js🔧 Advanced Patterns
Section titled “🔧 Advanced Patterns”Multiple Elements in One Package
Section titled “Multiple Elements in One Package”import { Injector, inject } from '@angular/core';import { createCustomElement } from '@angular/elements';import { CounterComponent } from './counter.component';import { UserCardComponent } from './user-card.component';import { ChartComponent } from './chart.component';
export function registerAllElements() { const injector = inject(Injector);
const elements = [ { component: CounterComponent, tag: 'counter-widget' }, { component: UserCardComponent, tag: 'user-card' }, { component: ChartComponent, tag: 'chart-widget' } ];
elements.forEach(({ component, tag }) => { const element = createCustomElement(component, { injector }); customElements.define(tag, element); });}Lazy Loading Elements
Section titled “Lazy Loading Elements”export async function loadCounterWidget() { const { CounterComponent } = await import('./counter.component'); const injector = inject(Injector);
const CounterElement = createCustomElement(CounterComponent, { injector }); customElements.define('counter-widget', CounterElement);}
// Usagedocument.getElementById('loadBtn').addEventListener('click', () => { loadCounterWidget();});Communication Between Elements
Section titled “Communication Between Elements”// Event bus service@Injectable({ providedIn: 'root' })export class EventBusService { private events = new Subject<{ type: string; data: any }>();
emit(type: string, data: any) { this.events.next({ type, data }); }
on(type: string) { return this.events.pipe( filter(event => event.type === type), map(event => event.data) ); }}
// Widget A@Component({ selector: 'app-sender', standalone: true})export class SenderComponent { private eventBus = inject(EventBusService);
sendMessage() { this.eventBus.emit('message', { text: 'Hello!' }); }}
// Widget B@Component({ selector: 'app-receiver', standalone: true})export class ReceiverComponent { private eventBus = inject(EventBusService);
constructor() { this.eventBus.on('message').subscribe(data => { console.log('Received:', data); }); }}✅ Best Practices
Section titled “✅ Best Practices”1. Keep Elements Self-Contained
Section titled “1. Keep Elements Self-Contained”// ✅ Good - All dependencies included@Component({ selector: 'app-widget', standalone: true, imports: [CommonModule, FormsModule], template: `...`})export class WidgetComponent {}2. Use Attribute Inputs for Primitives
Section titled “2. Use Attribute Inputs for Primitives”// ✅ Good - Simple attributes<my-widget count="5" title="Hello"></my-widget>
// For complex objects, use propertiesconst widget = document.querySelector('my-widget');widget.data = { complex: 'object' };3. Emit Custom Events
Section titled “3. Emit Custom Events”// ✅ Good - Use CustomEventthis.clicked.emit(data);
// Translates to:element.dispatchEvent(new CustomEvent('clicked', { detail: data }));4. Handle Styles Properly
Section titled “4. Handle Styles Properly”// ✅ Good - Encapsulated styles@Component({ selector: 'app-widget', standalone: true, styles: [` :host { display: block; /* Styles scoped to element */ } `]})5. Provide Fallback Content
Section titled “5. Provide Fallback Content”<!-- ✅ Good - Fallback for unsupported browsers --><my-widget> <p>Your browser doesn't support custom elements.</p></my-widget>🎯 Common Use Cases
Section titled “🎯 Common Use Cases”- Widget Libraries - Reusable UI components
- Micro Frontends - Independent deployable modules
- CMS Integration - Embeddable content widgets
- Third-Party Widgets - Analytics, chat, forms
- Cross-Framework Components - Use in React/Vue apps
- Legacy App Migration - Gradual Angular adoption
- Marketing Widgets - Embeddable promotional content
🎓 Learning Checklist
Section titled “🎓 Learning Checklist”- Understand Angular Elements and web components
- Create custom elements from Angular components
- Handle inputs and outputs in custom elements
- Build and bundle elements for production
- Register multiple elements in one package
- Implement lazy loading for elements
- Handle cross-element communication
- Deploy elements to CDN or npm
🚀 Next Steps
Section titled “🚀 Next Steps”- Micro Frontends - Build micro frontend architectures
- Performance Optimization - Optimize your applications
- Bundle Optimization - Reduce bundle sizes
Pro Tip: Angular Elements are perfect for creating framework-agnostic widgets! Use them to build reusable components that work anywhere. Keep elements self-contained and well-documented for maximum reusability! 🧩