Today, let’s dive into Angular Elements, a powerful tool that lets you package Angular components as Web Components, ready to be dropped into any web project—whether it’s React, Vue, or plain HTML. Sounds pretty cool, right? We’ll break down the principles, usage, and real-world scenarios with detailed code examples, guiding you step-by-step from scratch. Our focus is on practical, technical details to help you master turning Angular components into Web Components!
What Are Angular Elements?
Angular Elements is a feature of the Angular framework that compiles Angular components into standard Web Components (Custom Elements). These can be used like HTML tags in any web page. Web Components are a native browser technology built on standards like Custom Elements, Shadow DOM, and HTML Templates, enabling cross-framework and cross-project reuse. Angular Elements shines by wrapping Angular’s powerful features—data binding, dependency injection, and componentization—into lightweight, highly compatible Web Components.
Why use Angular Elements? Simply put:
- You have an Angular project and want to share components with teams using React or Vue.
- You need to embed Angular components in non-Angular projects like WordPress, Joomla, or static pages.
- You’re building a micro-frontend architecture, embedding Angular components as standalone modules.
Enough talk—let’s get hands-on, starting with a simple Angular component and turning it into a Web Component.
Environment Setup
To use Angular Elements, you need an Angular development environment. We’ll assume you have Node.js (recommended v18.x) and Angular CLI (latest version, 18.x at the time of writing).
Install Angular CLI:
npm install -g @angular/cli
Create a new project:
ng new angular-elements-demo
cd angular-elements-demo
Choose default settings (CSS, no SSR). The project structure looks like:
angular-elements-demo/
├── src/
│ ├── app/
│ │ ├── app.component.ts
│ │ ├── app.module.ts
│ ├── main.ts
│ ├── index.html
├── angular.json
├── package.json
Run ng serve
to see the default welcome page at localhost:4200
. Now, let’s add Angular Elements support.
Adding Angular Elements
Angular Elements is part of Angular, available via the @angular/elements
package. Install it:
ng add @angular/elements
This adds @angular/elements
to package.json
and configures the project. Let’s create a simple Angular component and convert it into a Web Component.
Creating a Greeting Component
Generate a new component in the app
directory:
ng generate component greeting
This creates greeting.component.ts
and related files. Modify greeting.component.ts
to accept a name
input and display a personalized greeting:
// src/app/greeting/greeting.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `
<div>
<h2>Hello, {{ name }}!</h2>
<p>Welcome to our Angular Elements demo!</p>
</div>
`,
styles: [`
div {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
}
h2 {
color: #007bff;
}
`]
})
export class GreetingComponent {
@Input() name: string = 'Guest';
}
This component accepts a name
input and displays a styled greeting. Next, we’ll turn it into a Web Component.
Converting to a Web Component
Register the GreetingComponent
as a Web Component in app.module.ts
:
// src/app/app.module.ts
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { GreetingComponent } from './greeting/greeting.component';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent, GreetingComponent],
imports: [BrowserModule],
bootstrap: [], // Remove default bootstrap
})
export class AppModule {
constructor(private injector: Injector) {
const greetingElement = createCustomElement(GreetingComponent, { injector });
customElements.define('greeting-element', greetingElement);
}
ngDoBootstrap() {}
}
Key points:
-
createCustomElement
convertsGreetingComponent
into a Web Component, withinjector
providing Angular’s dependency injection. -
customElements.define
registers the Web Component with the taggreeting-element
. - Remove
bootstrap: [AppComponent]
since Web Components don’t need default bootstrapping. -
ngDoBootstrap
is left empty, as Angular Elements handles component initialization.
Testing the Web Component
Update index.html
to use the <greeting-element>
tag:
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Angular Elements Demo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<greeting-element name="Alice"></greeting-element>
</body>
</html>
Run ng serve
, and at localhost:4200
, you’ll see “Hello, Alice!” with the welcome message and styles applied. This confirms GreetingComponent
is now a Web Component, usable like an HTML tag.
Packaging as a Single JS File
Web Components shine when deployed standalone. Let’s bundle greeting-element
into a single JS file for use in other projects. Angular Elements generates multiple files (main.js
, polyfills.js
, etc.), so we’ll merge them.
Update angular.json
for single-file bundling:
{
"projects": {
"angular-elements-demo": {
"architect": {
"build": {
"options": {
"outputPath": "dist/angular-elements-demo",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"assets": [],
"styles": ["src/styles.css"],
"scripts": [],
"outputHashing": "none" // Disable hashing for fixed filenames
},
"configurations": {
"production": {
"optimization": true,
"outputHashing": "none",
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
}
]
}
}
}
}
}
}
}
Build the project:
ng build --configuration production
This generates files in dist/angular-elements-demo
. Merge JS files using concat
:
npm install -g concat
concat -o dist/angular-elements-demo/greeting-element.js dist/angular-elements-demo/polyfills.js dist/angular-elements-demo/main.js
Now, greeting-element.js
is a standalone Web Component file. Test it in a plain HTML page:
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title>Test Greeting Element</title>
</head>
<body>
<greeting-element name="Bob"></greeting-element>
<script src="greeting-element.js"></script>
</body>
</html>
Serve with http-server
:
npm install -g http-server
http-server dist/angular-elements-demo
Open localhost:8080/test.html
to see “Hello, Bob!”—the Web Component works in a non-Angular environment.
Handling Inputs and Events
Web Components should support interaction. Let’s add a button to GreetingComponent
that triggers a custom event.
Update greeting.component.ts
:
// src/app/greeting/greeting.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `
<div>
<h2>Hello, {{ name }}!</h2>
<p>Welcome to our Angular Elements demo!</p>
<button (click)="greet()">Say Hi</button>
</div>
`,
styles: [`
div {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
}
h2 {
color: #007bff;
}
button {
padding: 10px 20px;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
`]
})
export class GreetingComponent {
@Input() name: string = 'Guest';
@Output() greeted = new EventEmitter<string>();
greet() {
this.greeted.emit(`Hi from ${this.name}!`);
}
}
The @Output
defines a greeted
event triggered by the button. Update test.html
to listen for it:
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title>Test Greeting Element</title>
</head>
<body>
<greeting-element name="Charlie"></greeting-element>
<script src="greeting-element.js"></script>
<script>
const element = document.querySelector('greeting-element');
element.addEventListener('greeted', (event) => {
alert(event.detail);
});
</script>
</body>
</html>
Rebuild (ng build --configuration production
), merge JS, and run test.html
. Clicking the button shows “Hi from Charlie!”. This confirms the Web Component supports inputs (name
) and outputs (greeted
event), enabling interaction with external JS.
Shadow DOM Encapsulation
A key feature of Web Components is Shadow DOM, which encapsulates a component’s styles and DOM, preventing external CSS interference. Angular Elements uses Shadow DOM by default; let’s verify.
Update greeting.component.ts
to explicitly enable Shadow DOM:
import { Component, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `...`, // Same as above
styles: [`...`],
encapsulation: ViewEncapsulation.ShadowDom // Explicitly specify
})
export class GreetingComponent {
@Input() name: string = 'Guest';
@Output() greeted = new EventEmitter<string>();
greet() {
this.greeted.emit(`Hi from ${this.name}!`);
}
}
Test external CSS interference:
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title>Test Shadow DOM</title>
<style>
h2 {
color: red !important;
}
</style>
</head>
<body>
<greeting-element name="Dave"></greeting-element>
<script src="greeting-element.js"></script>
</body>
</html>
Run test.html
. The <h2>
in <greeting-element>
remains blue (#007bff
), unaffected by the external color: red
. Shadow DOM isolates the component’s styles.
Shadow DOM Notes
- Style Isolation: Shadow DOM styles don’t leak out, and external styles don’t affect the component.
-
DOM Isolation: The component’s internal DOM is hidden;
document.querySelector
can’t access internal elements. - Performance: Shadow DOM adds minor rendering overhead, negligible for complex components.
To disable Shadow DOM (e.g., to allow external styling), use ViewEncapsulation.None
, but this sacrifices encapsulation.
Dynamic Data Binding
Angular excels at data binding. Let’s make GreetingComponent
support dynamic name
updates:
// src/app/greeting/greeting.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-greeting',
template: `
<div>
<h2>Hello, {{ name }}!</h2>
<input [(ngModel)]="name" placeholder="Enter your name">
</div>
`,
styles: [`
div {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
}
h2 {
color: #007bff;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class GreetingComponent {
@Input() name: string = 'Guest';
}
Add FormsModule
for ngModel
support in app.module.ts
:
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { createCustomElement } from '@angular/elements';
import { GreetingComponent } from './greeting/greeting.component';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent, GreetingComponent],
imports: [BrowserModule, FormsModule],
bootstrap: [],
})
export class AppModule {
constructor(private injector: Injector) {
const greetingElement = createCustomElement(GreetingComponent, { injector });
customElements.define('greeting-element', greetingElement);
}
ngDoBootstrap() {}
}
Update test.html
:
<!DOCTYPE html>
<html>
<head>
<title>Dynamic Binding</title>
</head>
<body>
<greeting-element name="Eve"></greeting-element>
<script src="greeting-element.js"></script>
</body>
</html>
Rebuild and run test.html
. The input updates name
, and <h2>
reflects changes in real-time, proving Angular’s reactive data binding works in Web Components.
Using in a React Project
Angular Elements excels at cross-framework reuse. Let’s embed greeting-element
in a React project.
Create a React project:
npx create-react-app react-host
cd react-host
Copy greeting-element.js
to react-host/public
. Update src/App.js
:
// react-host/src/App.js
import React, { useEffect } from 'react';
import './App.css';
function App() {
useEffect(() => {
const script = document.createElement('script');
script.src = '/greeting-element.js';
document.body.appendChild(script);
}, []);
return (
<div className="App">
<h1>React Host</h1>
<greeting-element name="Frank"></greeting-element>
</div>
);
}
export default App;
/* react-host/src/App.css */
.App {
text-align: center;
padding: 20px;
}
Run npm start
. At localhost:3000
, the React page embeds <greeting-element>
, showing “Hello, Frank!”. Clicking the button triggers the event, proving seamless integration with React, with Shadow DOM preventing style conflicts.
React Interaction
Dynamically update the name
attribute from React:
import React, { useState, useEffect } from 'react';
function App() {
const [name, setName] = useState('Frank');
useEffect(() => {
const script = document.createElement('script');
script.src = '/greeting-element.js';
document.body.appendChild(script);
const element = document.querySelector('greeting-element');
element.addEventListener('greeted', (e) => {
alert(e.detail);
});
}, []);
return (
<div className="App">
<h1>React Host</h1>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<greeting-element name={name}></greeting-element>
</div>
);
}
The input updates name
, and <greeting-element>
reflects changes, confirming dynamic attribute binding in React.
Using in a Vue Project
Web Components work in Vue too. Create a Vue project:
npm install -g @vue/cli
vue create vue-host
cd vue-host
Copy greeting-element.js
to vue-host/public
. Update src/App.vue
:
<!-- vue-host/src/App.vue -->
<template>
<div id="app">
<h1>Vue Host</h1>
<input v-model="name" placeholder="Enter name" />
<greeting-element :name="name"></greeting-element>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
name: 'Grace',
};
},
mounted() {
const script = document.createElement('script');
script.src = '/greeting-element.js';
document.body.appendChild(script);
const element = document.querySelector('greeting-element');
element.addEventListener('greeted', (e) => {
alert(e.detail);
});
},
};
</script>
<style>
#app {
text-align: center;
padding: 20px;
}
</style>
Run npm run serve
. At localhost:8080
, the Vue page shows <greeting-element>
with “Hello, Grace!”, and the input updates the name in real-time, proving Angular Elements works well in Vue.
Complex Component: With Services and HTTP
Angular Elements supports complex Angular features like dependency injection and HTTP. Let’s create a component that fetches a user list from an API.
Generate a new component:
ng generate component user-list
Update user-list.component.ts
:
// src/app/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
template: `
<div>
<h2>Users</h2>
<ul>
<li *ngFor="let user of users">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</div>
`,
styles: [`
div {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
h2 {
color: #007bff;
}
ul {
list-style: none;
padding: 0;
}
li {
padding: 10px;
border-bottom: 1px solid #eee;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class UserListComponent implements OnInit {
users: any[] = [];
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('https://jsonplaceholder.typicode.com/users')
.subscribe((data: any) => {
this.users = data;
});
}
}
Add HttpClientModule
to app.module.ts
:
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { createCustomElement } from '@angular/elements';
import { GreetingComponent } from './greeting/greeting.component';
import { UserListComponent } from './user-list/user-list.component';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent, GreetingComponent, UserListComponent],
imports: [BrowserModule, HttpClientModule],
bootstrap: [],
})
export class AppModule {
constructor(private injector: Injector) {
const greetingElement = createCustomElement(GreetingComponent, { injector });
customElements.define('greeting-element', greetingElement);
const userListElement = createCustomElement(UserListComponent, { injector });
customElements.define('user-list-element', userListElement);
}
ngDoBootstrap() {}
}
Update test.html
:
<!DOCTYPE html>
<html>
<head>
<title>User List Element</title>
</head>
<body>
<user-list-element></user-list-element>
<script src="greeting-element.js"></script>
</body>
</html>
Rebuild, merge JS, and run test.html
. You’ll see a user list from JSONPlaceholder, showing names and emails, proving Angular Elements supports complex features like HTTP and *ngFor
.
Micro-Frontend Scenario
Angular Elements is ideal for micro-frontends. Embed user-list-element
in a React-based main app:
import React, { useEffect } from 'react';
function App() {
useEffect(() => {
const script = document.createElement('script');
script.src = '/greeting-element.js';
document.body.appendChild(script);
}, []);
return (
<div className="App">
<h1>React Micro Frontend</h1>
<user-list-element></user-list-element>
</div>
);
}
Run the React app. The user-list-element
displays the user list, seamlessly integrated with the React app, making Angular Elements perfect for micro-frontends.
Complex Interactions
Add filtering to user-list-element
:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-user-list',
template: `
<div>
<h2>Users</h2>
<input [(ngModel)]="filter" placeholder="Filter by name" (input)="filterUsers()">
<ul>
<li *ngFor="let user of filteredUsers" (click)="selectUser(user)">
{{ user.name }} ({{ user.email }})
</li>
</ul>
</div>
`,
styles: [`
div {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
input {
padding: 8px;
margin-bottom: 10px;
width: 100%;
border: 1px solid #ccc;
border-radius: 4px;
}
`],
encapsulation: ViewEncapsulation.ShadowDom
})
export class UserListComponent implements OnInit {
@Input() filter: string = '';
@Output() userSelected = new EventEmitter<any>();
users: any[] = [];
filteredUsers: any[] = [];
constructor(private http: HttpClient) {}
ngOnInit() {
this.http.get('https://jsonplaceholder.typicode.com/users')
.subscribe((data: any) => {
this.users = data;
this.filteredUsers = data;
});
}
filterUsers() {
this.filteredUsers = this.users.filter(user =>
user.name.toLowerCase().includes(this.filter.toLowerCase())
);
}
selectUser(user: any) {
this.userSelected.emit(user);
}
}
Ensure FormsModule
is in app.module.ts
(already added). Update test.html
:
<!DOCTYPE html>
<html>
<head>
<title>User List with Filter</title>
</head>
<body>
<user-list-element filter="Jo"></user-list-element>
<script src="greeting-element.js"></script>
<script>
const element = document.querySelector('user-list-element');
element.addEventListener('userSelected', (e) => {
alert(`Selected: ${e.detail.name}`);
});
</script>
</body>
</html>
Rebuild and run test.html
. The input filters users (e.g., “Jo” shows only John), and clicking a user triggers the userSelected
event, showing their name. This confirms Angular Elements handles complex interactions.
Using in Static HTML
Angular Elements is great for static pages like WordPress. Update test.html
:
<!DOCTYPE html>
<html>
<head>
<title>Static Page</title>
</head>
<body>
<h1>My Blog</h1>
<p>Some static content...</p>
<user-list-element filter="Le"></user-list-element>
<script src="greeting-element.js"></script>
</body>
</html>
Run it, and the user list embeds in the static page with filtering, proving Angular Elements is CMS-friendly.
Conclusion (Technical Details)
Angular Elements transforms Angular components into Web Components for cross-framework and cross-project reuse. We started with a simple GreetingComponent
, implementing inputs, events, and Shadow DOM, then built a complex UserListComponent
with HTTP and dynamic filtering. The code demonstrated:
- Converting components to Web Components with
@angular/elements
. - Packaging into a single JS file.
- Embedding in React, Vue, and static HTML.
- Handling dynamic binding, events, and HTTP requests.
Try running these examples, embed greeting-element
and user-list-element
in your projects, and experience the cross-platform power of Angular Elements!
Top comments (0)