Skip to main content

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:

  1. You transform an Angular component into a self-bootstrapping Web Component
  2. The resulting component can be used in any HTML page without the Angular framework being present
  3. 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:

bash
# 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

html
<!-- 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

json
// 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:

bash
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:

html
<!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:

typescript
// 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:

typescript
// 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:

html
<!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

  1. Keep components focused: Each custom element should have a single responsibility
  2. Handle attributes carefully: Remember that HTML attributes can only be strings, so use property binding for complex data
  3. Optimize bundle size: Use tools like ngx-build-plus to create smaller bundles
  4. Style encapsulation: Use ViewEncapsulation.ShadowDom for true isolation
  5. Events: Use @Output() decorators to define custom events
  6. Browser compatibility: Include necessary polyfills for older browsers

Challenges and Considerations

When working with Angular Custom Elements, be aware of:

  1. Bundle size: Angular elements include the Angular runtime, which can make bundles large
  2. Browser support: Some browsers require polyfills for Web Components
  3. Two-way binding: Angular's [(ngModel)] doesn't work directly; you'll need to handle both property and event binding
  4. Shadow DOM: Style encapsulation can make styling more complex
  5. 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

Exercises

  1. Create a custom element that displays a countdown timer with configurable duration
  2. Build a reusable data table custom element that accepts data as a JSON string attribute
  3. Create a chart component using a library like Chart.js and expose it as a custom element
  4. Build a form component with validation and expose it as a custom element
  5. 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! :)