Angular Custom Elements
Introduction
Angular Custom Elements provide a powerful way to use Angular components outside of an Angular application. Based on the Web Components standards, custom elements allow you to create reusable components that work across modern browsers and frameworks. This feature enables you to leverage your Angular expertise while integrating with other technologies or frameworks like React, Vue, or even vanilla JavaScript.
In this tutorial, we'll explore how to create, build, and use Angular Custom Elements, also known as Angular Elements, in various environments.
What Are Angular Custom Elements?
Angular Custom Elements (or Angular Elements) are standard HTML elements generated from Angular components. When you create an Angular Custom Element:
- You transform an Angular component into a self-bootstrapping Web Component
- The resulting component can be used in any HTML page without the Angular framework being present
- It's packaged with all its dependencies, styles, and functionality in a single bundle
This functionality is powered by the @angular/elements
package, which was introduced in Angular 6.
Prerequisites
Before we start, ensure you have:
- Node.js and npm installed (Node 14.x or later recommended)
- Angular CLI installed (
npm install -g @angular/cli
) - Basic knowledge of Angular components
Setting Up Angular Elements
First, let's create a new Angular project and add the required dependencies:
# Create a new Angular project
ng new angular-elements-demo
cd angular-elements-demo
# Install @angular/elements
npm install @angular/elements --save
# Install the polyfills for older browsers (optional)
npm install @webcomponents/custom-elements --save
Next, we need to modify our polyfills.ts
file to support custom elements in all browsers:
// polyfills.ts
// Add these imports:
import 'zone.js';
import '@webcomponents/custom-elements/src/native-shim';
import '@webcomponents/custom-elements/custom-elements.min';
Creating Your First Custom Element
Let's create a simple component that we'll convert into a custom element:
// src/app/hello-world/hello-world.component.ts
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-hello-world',
template: `
<div class="hello-world">
<h2>Hello, {{name}}!</h2>
<p>This is an Angular Custom Element</p>
</div>
`,
styles: [`
.hello-world {
border: 2px solid #3f51b5;
border-radius: 8px;
padding: 16px;
margin: 16px 0;
color: #3f51b5;
font-family: Arial, sans-serif;
}
`]
})
export class HelloWorldComponent implements OnInit {
@Input() name = 'World';
constructor() { }
ngOnInit(): void { }
}
Now, register this component in the module and convert it to a custom element:
// src/app/app.module.ts
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { AppComponent } from './app.component';
import { HelloWorldComponent } from './hello-world/hello-world.component';
@NgModule({
declarations: [
AppComponent,
HelloWorldComponent
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent],
entryComponents: [HelloWorldComponent]
})
export class AppModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
// Convert Angular Component to Custom Element
const HelloWorldElement = createCustomElement(HelloWorldComponent, {
injector: this.injector
});
// Register the custom element with the browser
customElements.define('hello-world-element', HelloWorldElement);
}
}
Using Your Custom Element
Now let's modify our app.component.html
to use our custom element:
<!-- src/app/app.component.html -->
<div class="container">
<h1>Angular Custom Elements Demo</h1>
<!-- Using the custom element within Angular -->
<hello-world-element name="Angular Developer"></hello-world-element>
<!-- We can add it dynamically too -->
<button (click)="addElement()">Add Another Element</button>
<div id="container"></div>
</div>
And update the component class:
// src/app/app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'angular-elements-demo';
addElement() {
const element = document.createElement('hello-world-element');
element.setAttribute('name', 'Dynamically Added');
document.getElementById('container')?.appendChild(element);
}
}
Building for External Use
The real power of Angular Custom Elements comes when using them outside of Angular. To do this, we need to build our component as a standalone bundle.
First, let's create a separate module just for our custom element:
// src/app/elements.module.ts
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { HelloWorldComponent } from './hello-world/hello-world.component';
@NgModule({
declarations: [
HelloWorldComponent
],
imports: [
BrowserModule
],
entryComponents: [HelloWorldComponent]
})
export class ElementsModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
const HelloWorldElement = createCustomElement(HelloWorldComponent, {
injector: this.injector
});
customElements.define('hello-world-element', HelloWorldElement);
}
}
Next, create a new file to serve as the entry point for our custom element bundle:
// src/elements.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { ElementsModule } from './app/elements.module';
platformBrowserDynamic()
.bootstrapModule(ElementsModule)
.catch(err => console.error(err));
Update the Angular configuration to build this new entry point:
// angular.json (partial)
{
"projects": {
"angular-elements-demo": {
"architect": {
"build": {
"configurations": {
"elements": {
"main": "src/elements.ts",
"output": {
"path": "dist/elements"
},
"optimization": true,
"outputHashing": "none",
"sourceMap": false,
"namedChunks": false,
"aot": true,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true
}
}
}
}
}
}
}
Now we can build our custom element bundle:
ng build --configuration=elements
Using the Custom Element in a Non-Angular Application
Now that we have our bundle, let's create a simple HTML file to use our custom element:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Using Angular Custom Elements</title>
</head>
<body>
<h1>This is a plain HTML page</h1>
<!-- Using our Angular Custom Element -->
<hello-world-element name="Web Developer"></hello-world-element>
<button id="addMore">Add Another Element</button>
<div id="container"></div>
<!-- Include our bundled element -->
<script src="dist/elements/main.js"></script>
<script>
document.getElementById('addMore').addEventListener('click', function() {
const element = document.createElement('hello-world-element');
element.setAttribute('name', 'New User');
document.getElementById('container').appendChild(element);
});
</script>
</body>
</html>
Real-World Example: Interactive Widget
Let's create a more complex example - a feedback widget that can be embedded in any website:
// src/app/feedback-widget/feedback-widget.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-feedback-widget',
template: `
<div class="feedback-container">
<h3>{{ title }}</h3>
<div class="rating">
<span *ngFor="let star of [1, 2, 3, 4, 5]"
[class.active]="star <= rating"
(click)="setRating(star)">
★
</span>
</div>
<textarea
placeholder="Tell us what you think..."
[(ngModel)]="comment"
rows="3">
</textarea>
<button (click)="submitFeedback()">Submit Feedback</button>
<p *ngIf="submitted" class="thank-you">Thank you for your feedback!</p>
</div>
`,
styles: [`
.feedback-container {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
max-width: 400px;
font-family: Arial, sans-serif;
}
.rating span {
font-size: 24px;
color: #ddd;
cursor: pointer;
margin-right: 5px;
}
.rating span.active {
color: gold;
}
textarea {
width: 100%;
margin: 10px 0;
padding: 8px;
border-radius: 4px;
border: 1px solid #ddd;
}
button {
background: #4285f4;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
.thank-you {
color: green;
font-weight: bold;
}
`]
})
export class FeedbackWidgetComponent {
@Input() title = 'Please Rate Your Experience';
@Output() feedback = new EventEmitter<{ rating: number, comment: string }>();
rating = 0;
comment = '';
submitted = false;
setRating(value: number) {
this.rating = value;
}
submitFeedback() {
if (this.rating > 0) {
this.feedback.emit({ rating: this.rating, comment: this.comment });
this.submitted = true;
// Reset form after submission
setTimeout(() => {
this.rating = 0;
this.comment = '';
this.submitted = false;
}, 3000);
}
}
}
Don't forget to add this component to your elements module and update your build configuration. You'll also need to add the FormsModule to your imports:
// src/app/elements.module.ts
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { FormsModule } from '@angular/forms';
import { HelloWorldComponent } from './hello-world/hello-world.component';
import { FeedbackWidgetComponent } from './feedback-widget/feedback-widget.component';
@NgModule({
declarations: [
HelloWorldComponent,
FeedbackWidgetComponent
],
imports: [
BrowserModule,
FormsModule
],
entryComponents: [
HelloWorldComponent,
FeedbackWidgetComponent
]
})
export class ElementsModule {
constructor(private injector: Injector) {}
ngDoBootstrap() {
// Register HelloWorldElement
const HelloWorldElement = createCustomElement(HelloWorldComponent, {
injector: this.injector
});
customElements.define('hello-world-element', HelloWorldElement);
// Register FeedbackWidgetElement
const FeedbackWidgetElement = createCustomElement(FeedbackWidgetComponent, {
injector: this.injector
});
customElements.define('feedback-widget', FeedbackWidgetElement);
}
}
Using the feedback widget in an HTML page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feedback Widget Demo</title>
</head>
<body>
<h1>Product Landing Page</h1>
<p>This is information about our amazing product...</p>
<!-- Using our feedback widget element -->
<feedback-widget title="How would you rate our product?"></feedback-widget>
<!-- Include our bundled element -->
<script src="dist/elements/main.js"></script>
<script>
// Listen for feedback events
document.querySelector('feedback-widget').addEventListener('feedback', function(event) {
console.log('Feedback received:', event.detail);
// Here you could send this data to your server
});
</script>
</body>
</html>
Best Practices for Angular Custom Elements
- Keep components focused: Each custom element should have a single responsibility
- Handle attributes carefully: Remember that HTML attributes can only be strings, so use property binding for complex data
- Optimize bundle size: Use tools like ngx-build-plus to create smaller bundles
- Style encapsulation: Use ViewEncapsulation.ShadowDom for true isolation
- Events: Use @Output() decorators to define custom events
- Browser compatibility: Include necessary polyfills for older browsers
Challenges and Considerations
When working with Angular Custom Elements, be aware of:
- Bundle size: Angular elements include the Angular runtime, which can make bundles large
- Browser support: Some browsers require polyfills for Web Components
- Two-way binding: Angular's
[(ngModel)]
doesn't work directly; you'll need to handle both property and event binding - Shadow DOM: Style encapsulation can make styling more complex
- Lazy loading: Custom elements might require special handling for lazy loading scenarios
Summary
Angular Custom Elements provide a powerful way to leverage your Angular knowledge in non-Angular environments. They allow you to:
- Create reusable components that work across different frameworks
- Gradually migrate applications from one framework to another
- Build widget libraries that can be used in any web application
- Create micro frontends with different technology stacks
By transforming Angular components into standard HTML elements, you can share your Angular code with any web application, regardless of the framework or library it uses.
Additional Resources
- Official Angular Elements Documentation
- Web Components on MDN
- Custom Elements Specification
- ngx-build-plus - Enhanced build process for Angular Elements
Exercises
- Create a custom element that displays a countdown timer with configurable duration
- Build a reusable data table custom element that accepts data as a JSON string attribute
- Create a chart component using a library like Chart.js and expose it as a custom element
- Build a form component with validation and expose it as a custom element
- Create a custom element that uses the Geolocation API to display the user's current location on a map
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)