1. Introduction to Angular Elements
Welcome to this comprehensive guide on Angular Elements! In today’s diverse web development landscape, the ability to reuse UI components across different frameworks is incredibly valuable. Angular Elements provides a powerful solution by allowing you to package your Angular components as native Web Components, also known as Custom Elements. This means you can take a component built with the full power of Angular and seamlessly integrate it into any web project—whether it’s built with React, Vue, plain HTML, or even other Angular applications.
What is Angular Elements?
Angular Elements is a feature within the Angular framework that enables you to compile Angular components into standard Web Components. These resulting Web Components can then be used like any other native HTML tag in any web page, regardless of the underlying JavaScript framework (or lack thereof).
At its core, Web Components leverage several native browser technologies:
- Custom Elements: Allows you to define new HTML tags (e.g.,
<my-custom-element>) with encapsulated behavior. - Shadow DOM: Provides encapsulation for the component’s internal DOM structure and styles, preventing them from bleeding out or being affected by external CSS.
- HTML Templates (
<template>and<slot>): Enables you to write reusable markup structures that can be rendered dynamically. - ES Modules: A standardized module system for JavaScript, allowing for modular development and efficient loading of component code.
Angular Elements acts as a bridge, wrapping Angular’s powerful features like data binding, dependency injection, and the component lifecycle into these highly compatible Web Components.
Why Learn Angular Elements? (Benefits, Use Cases, Industry Relevance)
Learning Angular Elements offers several significant advantages for modern web development:
- Cross-Framework Reusability: This is the primary benefit. If you have a suite of well-crafted Angular components, you can share them with teams using other frameworks (React, Vue, etc.) without requiring them to adopt Angular for their entire project. This promotes consistency and reduces development effort.
- Micro-Frontend Architectures: Angular Elements is an excellent fit for micro-frontend strategies. You can build independent, deployable UI modules as Web Components, allowing different teams to work on distinct parts of an application using their preferred technologies, then integrate them seamlessly.
- Embedding in Non-Angular Projects: Need to embed a dynamic Angular form, a data grid, or a complex widget into a static website, a CMS like WordPress, or an application built with an older framework? Angular Elements makes this possible and straightforward.
- Design Systems: For organizations building comprehensive design systems, Web Components can serve as the foundational, framework-agnostic building blocks, ensuring a consistent user experience across various products and teams.
- Future-Proofing: By adhering to web standards, Web Components offer a degree of future-proofing. They are supported natively by browsers, reducing reliance on framework-specific implementations that might evolve rapidly.
Setting Up Your Development Environment
To begin working with Angular Elements, you’ll need a standard Angular development environment.
Prerequisites:
- Node.js: Ensure you have Node.js installed (version 18.x or later is recommended). You can download it from nodejs.org.
- Angular CLI: The Angular Command Line Interface is crucial for creating, developing, and building Angular applications.
Step-by-step instructions:
Install Angular CLI globally:
npm install -g @angular/cliVerify the installation by checking the version:
ng versionCreate a new Angular project:
We’ll create a minimal Angular project for our demonstration.
ng new angular-elements-demo --minimal=true --prefix=awc --skip-tests=true --routing=false --experimental-zoneless=true cd angular-elements-demo--minimal=true: Creates a project with minimal initial files.--prefix=awc: Sets the prefix for generated components toawc.--skip-tests=true: Skips generating test files for simplicity in this guide.--routing=false: No routing module is generated.--experimental-zoneless=true: Utilizes the experimental zoneless architecture in Angular, which can simplify Web Component integration by removing the need forzone.jspolyfills and improving performance. Note that in Angular v20.2, zoneless is stable.
This command will create a new directory
angular-elements-demowith a basic Angular project structure.Run the default application:
ng serveOpen your browser to
http://localhost:4200. You should see a basic “Welcome” message, confirming your environment is set up correctly.
With your environment ready, let’s dive into the core concepts of creating and using Angular Elements!
2. Core Concepts and Fundamentals
In this section, we’ll break down the fundamental building blocks of Angular Elements. You’ll learn how to create a simple Angular component and transform it into a reusable Web Component.
2.1. Installing @angular/elements
The first step is to add the @angular/elements package to your Angular project. This package provides the necessary tools to compile Angular components into Web Components.
Code Example:
ng add @angular/elements
This command will install the package and make some necessary configurations in your angular.json file.
Exercise/Mini-Challenge:
After running the ng add command, inspect your package.json file. Can you locate @angular/elements under dependencies?
2.2. Creating a Simple Angular Component
Before we can convert an Angular component into a Web Component, we need an Angular component to work with. Let’s create a basic GreetingComponent that accepts a name as input and displays a personalized message.
Code Example:
Generate the component:
ng generate component greeting --standaloneWe use the
--standaloneflag as standalone components are the recommended approach in modern Angular, eliminating the need forNgModulesand making components self-contained.Modify
src/app/greeting/greeting.component.ts:// src/app/greeting/greeting.component.ts import { Component, Input, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; // Import CommonModule for directives like *ngIf, etc. @Component({ selector: 'awc-greeting', // Notice the 'awc' prefix as defined during project creation standalone: true, imports: [CommonModule], template: ` <div class="greeting-card"> <h2>Hello, {{ name }}!</h2> <p>Welcome to our Angular Elements demo!</p> <button (click)="sayHello()">Say Hello</button> </div> `, styles: [` .greeting-card { padding: 20px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9; text-align: center; font-family: Arial, sans-serif; margin: 20px; } h2 { color: #007bff; } button { padding: 10px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; } button:hover { background-color: #218838; } `] }) export class GreetingComponent implements OnInit { @Input() name: string = 'Guest'; // Default value for the name input ngOnInit() { console.log(`Greeting component initialized for ${this.name}`); } sayHello() { alert(`Greetings from ${this.name}!`); } }@Input() name: string = 'Guest';: This defines an input propertyname. When this component is used as a Web Component,namewill correspond to an HTML attribute (e.g.,<awc-greeting name="Alice"></awc-greeting>).sayHello(): A simple method that will be triggered when the button inside the component is clicked.standalone: trueandimports: [CommonModule]: Essential for standalone components.CommonModuleprovides access to common Angular directives likengIf,ngFor, etc.
Exercise/Mini-Challenge:
Modify the GreetingComponent to accept another input property, message, and display it below the “Welcome” paragraph. Give it a default value of “Hope you’re having a great day!”.
2.3. Converting to a Web Component with createCustomElement
Now, let’s take our GreetingComponent and transform it into a Web Component using Angular Elements’ createCustomElement function. This function creates a class that can be registered with the browser’s CustomElementRegistry.
Code Example:
We will modify src/main.ts (the application’s entry point) to define our custom element.
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';
import { GreetingComponent } from './app/greeting/greeting.component';
import { ApplicationRef, DoBootstrap } from '@angular/core';
// This is where the magic happens:
// 1. We create the custom element constructor.
// 2. We register it with the browser.
(async () => {
// We don't bootstrap a root Angular component for the entire app,
// as we only want to expose our custom element.
// Instead, we create an application instance which provides the injector.
const app = await bootstrapApplication(class App implements DoBootstrap {
ngDoBootstrap(appRef: ApplicationRef): void {
// No need to bootstrap any root component here,
// as our purpose is solely to define the custom element.
// However, we need this structure to get access to the injector.
}
}).catch(err => console.error(err));
if (app) {
const greetingElement = createCustomElement(GreetingComponent, { injector: app.injector });
// Define the custom element tag. It's recommended to use a unique prefix
// and avoid using the component's original selector to prevent conflicts.
customElements.define('my-greeting-card', greetingElement);
console.log('Custom element <my-greeting-card> defined!');
}
})();
// For a simple Web Component, you often don't need a top-level Angular application.
// This structure primarily serves to obtain an Injector.
Explanation:
bootstrapApplication(...): Even though we’re not bootstrapping a full Angular application for display, we need to bootstrap something to get anApplicationRefand, more importantly, itsinjector. Theinjectoris crucial for Angular Elements to manage dependencies within your component.createCustomElement(GreetingComponent, { injector: app.injector }): This function takes ourGreetingComponentand the application’sInjectorto create a Web Component constructor. This constructor encapsulates the Angular component’s logic, template, and styles.customElements.define('my-greeting-card', greetingElement);: This is a native browser API. It registers ourgreetingElementconstructor with the browser, associating it with the custom HTML tag<my-greeting-card>. Now, whenever the browser encounters<my-greeting-card>in the DOM, it will instantiate our Angular component.
Important Note: It’s crucial not to use your Angular component’s selector (e.g., awc-greeting) directly as the custom element tag name (e.g., my-greeting-card). This can lead to the browser creating a custom element instance and Angular also potentially creating a component instance, resulting in two instances for the same DOM element and unexpected behavior.
Exercise/Mini-Challenge:
Change the custom element’s tag name to something different, like my-welcome-widget. Rebuild and verify that the element still works.
2.4. Testing the Web Component in a Plain HTML File
Once registered, your Angular component, now a Web Component, can be used in any HTML file. We’ll build the Angular project and then include the generated JavaScript file(s) in a simple index.html.
Code Example:
Build your Angular project for production:
We’ll build it with specific options to make the output easier to manage for Web Components.
ng build --output-hashing=none --configuration production--output-hashing=none: Prevents Angular from adding hash strings to the output filenames (e.g.,main.jsinstead ofmain-es2015.f2e3a4b.js). This makes it easier to reference the files.--configuration production: Applies production optimizations, resulting in smaller bundle sizes.
After the build, check the
dist/angular-elements-demofolder. You’ll find several JavaScript files (e.g.,main.js,polyfills.js).Create a simple
test.htmlfile in your project’s root directory (not insidesrcordist):<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Angular Web Component</title> <style> body { font-family: sans-serif; margin: 40px; } </style> <!-- Include the polyfills for older browsers if needed. Modern browsers generally have good Web Component support. --> <!-- <script src="./dist/angular-elements-demo/polyfills.js"></script> --> <!-- Include the main bundle generated by Angular --> <script src="./dist/angular-elements-demo/main.js"></script> </head> <body> <h1>My Application with a Reusable Angular Web Component</h1> <my-greeting-card name="World"></my-greeting-card> <my-greeting-card name="Framework Agnostic App"></my-greeting-card> <div id="dynamic-host"></div> <script> // You can even create and add them dynamically with JavaScript const dynamicGreeting = document.createElement('my-greeting-card'); dynamicGreeting.setAttribute('name', 'Dynamically Added'); document.getElementById('dynamic-host').appendChild(dynamicGreeting); </script> </body> </html>Serve the
test.htmlfile:You can use a simple HTTP server to serve your files. If you don’t have one,
http-serveris a good option:npm install -g http-server http-server .Then, open
http://localhost:8080/test.htmlin your browser. You should see three greeting cards, all rendered by your Angular component, demonstrating its reusability and encapsulation.
Exercise/Mini-Challenge:
Add a few more my-greeting-card elements to test.html with different name attributes. Try opening your browser’s developer tools and inspecting the Shadow DOM of these elements. Can you see how the component’s internal structure and styles are encapsulated?
3. Intermediate Topics
Now that you have a solid grasp of the basics, let’s explore more advanced aspects of Angular Elements, focusing on how they interact with their host environment.
3.1. Handling Inputs and Outputs (Custom Events)
Angular components communicate with inputs (@Input()) and outputs (@Output()). When converted to Web Components:
- Inputs map directly to HTML attributes on the custom element.
- Outputs are dispatched as native browser Custom Events.
Let’s modify our GreetingComponent to emit an event when the “Say Hello” button is clicked and then listen for that event in our plain HTML file.
Code Example:
Modify
src/app/greeting/greeting.component.ts:// src/app/greeting/greeting.component.ts import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'awc-greeting', standalone: true, imports: [CommonModule], template: ` <div class="greeting-card"> <h2>Hello, {{ name }}!</h2> <p>Welcome to our Angular Elements demo!</p> <button (click)="greet()">Say Hello</button> </div> `, styles: [` .greeting-card { padding: 20px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9; text-align: center; font-family: Arial, sans-serif; margin: 20px; } h2 { color: #007bff; } button { padding: 10px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; } button:hover { background-color: #218838; } `] }) export class GreetingComponent implements OnInit { @Input() name: string = 'Guest'; @Output() greeted = new EventEmitter<string>(); // Define an output event ngOnInit() { console.log(`Greeting component initialized for ${this.name}`); } greet() { const message = `Hi from ${this.name}!`; this.greeted.emit(message); // Emit the custom event console.log(message); } }@Output() greeted = new EventEmitter<string>();: Declares an output property namedgreetedthat emits a string.this.greeted.emit(message);: When thegreet()method is called, it emits thegreetedevent with a specific message.
Rebuild your Angular project:
ng build --output-hashing=none --configuration productionUpdate
test.htmlto listen for thegreetedevent:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Angular Web Component</title> <style> body { font-family: sans-serif; margin: 40px; } </style> <script src="./dist/angular-elements-demo/main.js"></script> </head> <body> <h1>My Application with a Reusable Angular Web Component</h1> <my-greeting-card name="World"></my-greeting-card> <my-greeting-card name="Framework Agnostic App"></my-greeting-card> <div id="dynamic-host"></div> <script> const dynamicGreeting = document.createElement('my-greeting-card'); dynamicGreeting.setAttribute('name', 'Dynamically Added'); document.getElementById('dynamic-host').appendChild(dynamicGreeting); // Get references to our custom elements const worldGreeting = document.querySelector('my-greeting-card[name="World"]'); const dynamicGreetingElement = document.getElementById('dynamic-host').querySelector('my-greeting-card'); // Add event listeners to the custom elements if (worldGreeting) { worldGreeting.addEventListener('greeted', (event) => { alert(`Event from 'World' component: ${event.detail}`); console.log('Event details:', event.detail); }); } if (dynamicGreetingElement) { dynamicGreetingElement.addEventListener('greeted', (event) => { alert(`Event from 'Dynamically Added' component: ${event.detail}`); console.log('Event details:', event.detail); }); } </script> </body> </html>element.addEventListener('greeted', (event) => { ... });: We attach a standard JavaScript event listener to our custom element, listening for thegreetedevent.event.detail: The data emitted by an@Output()in an Angular component is available through thedetailproperty of theCustomEventobject.
Run http-server . and open http://localhost:8080/test.html. Click the “Say Hello” buttons on the custom elements, and you should see JavaScript alert messages corresponding to the emitted events.
Exercise/Mini-Challenge:
Add another button to the GreetingComponent that emits a different event, for example, farewell, with a message like “Goodbye from [name]!”. Listen for this new event in test.html and log its details to the console.
3.2. Shadow DOM Encapsulation and Styling
One of the most powerful features of Web Components is Shadow DOM, which provides CSS and DOM encapsulation. This means the styles defined within your Angular component will not leak out and affect the rest of your page, and external styles won’t easily penetrate and affect your component. Angular Elements uses Shadow DOM by default.
Code Example (Verification):
Our GreetingComponent already uses encapsulated styles because Angular Elements automatically configures the Shadow DOM. To explicitly set the ViewEncapsulation (though it’s usually automatic for custom elements), you can modify the component:
// src/app/greeting/greeting.component.ts (no change needed for default behavior, but for explicit understanding)
import { Component, Input, Output, EventEmitter, OnInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'awc-greeting',
standalone: true,
imports: [CommonModule],
template: `
<div class="greeting-card">
<h2>Hello, {{ name }}!</h2>
<p>Welcome to our Angular Elements demo!</p>
<button (click)="greet()">Say Hello</button>
</div>
`,
styles: [`
/* These styles are encapsulated within the Shadow DOM */
.greeting-card {
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
background-color: #f9f9f9;
text-align: center;
font-family: Arial, sans-serif;
margin: 20px;
}
h2 {
color: #007bff;
}
button {
padding: 10px 15px;
background-color: #28a745;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin-top: 10px;
}
button:hover {
background-color: #218838;
}
`],
// Angular Elements uses ViewEncapsulation.ShadowDom by default for custom elements
// You can explicitly set it, but it's often not necessary.
encapsulation: ViewEncapsulation.ShadowDom
})
export class GreetingComponent implements OnInit {
// ... (rest of the component code)
@Input() name: string = 'Guest';
@Output() greeted = new EventEmitter<string>();
ngOnInit() {
console.log(`Greeting component initialized for ${this.name}`);
}
greet() {
const message = `Hi from ${this.name}!`;
this.greeted.emit(message);
console.log(message);
}
}
To demonstrate the encapsulation, add some conflicting global styles in test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Test Angular Web Component</title>
<style>
body { font-family: sans-serif; margin: 40px; }
/* Global styles that should NOT affect the web component */
h1 {
color: purple;
border-bottom: 2px solid purple;
}
h2 { /* This global H2 style should not override the component's H2 style */
color: orange !important;
font-style: italic;
}
button { /* This global button style should not override the component's button style */
background-color: blue !important;
color: yellow !important;
}
</style>
<script src="./dist/angular-elements-demo/main.js"></script>
</head>
<body>
<h1>My Application with a Reusable Angular Web Component</h1>
<my-greeting-card name="World"></my-greeting-card>
<script>
const worldGreeting = document.querySelector('my-greeting-card[name="World"]');
if (worldGreeting) {
worldGreeting.addEventListener('greeted', (event) => {
alert(`Event from 'World' component: ${event.detail}`);
});
}
</script>
</body>
</html>
Serve test.html and observe the output. The global h1 will be purple, but the h2 inside my-greeting-card will remain blue, and the button will remain green, demonstrating that the component’s styles are protected by Shadow DOM.
Exercise/Mini-Challenge:
Experiment with :host and ::part selectors (if you define them) to style your custom element from the outside. While Shadow DOM encapsulates styles, Web Components provide ways for consumers to customize parts of the component. (Hint: For ::part, you would need to add part="some-name" to elements within your component’s template.)
3.3. Content Projection with <ng-content> and Slots
Angular’s ng-content is used for content projection, allowing you to insert content from the parent component into a specified location in the child component’s template. When compiled into a Web Component, ng-content maps to the native Web Components <slot> element.
Code Example:
Modify
src/app/greeting/greeting.component.tsto use<ng-content>:// src/app/greeting/greeting.component.ts import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'awc-greeting', standalone: true, imports: [CommonModule], template: ` <div class="greeting-card"> <h2>Hello, {{ name }}!</h2> <p>Welcome to our Angular Elements demo!</p> <ng-content></ng-content> <!-- Default slot --> <ng-content select=".extra-info"></ng-content> <!-- Named slot --> <button (click)="greet()">Say Hello</button> </div> `, styles: [` .greeting-card { padding: 20px; border: 1px solid #ccc; border-radius: 8px; background-color: #f9f9f9; text-align: center; font-family: Arial, sans-serif; margin: 20px; } h2 { color: #007bff; } button { padding: 10px 15px; background-color: #28a745; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 10px; } button:hover { background-color: #218838; } ::slotted(p.projected-content) { /* Style for content projected into the default slot */ font-style: italic; color: #555; } ::slotted(div.extra-info) { /* Style for content projected into the named slot */ background-color: #e0f7fa; padding: 10px; border-left: 3px solid #00bcd4; margin-top: 10px; } `] }) export class GreetingComponent implements OnInit { @Input() name: string = 'Guest'; @Output() greeted = new EventEmitter<string>(); ngOnInit() { console.log(`Greeting component initialized for ${this.name}`); } greet() { const message = `Hi from ${this.name}!`; this.greeted.emit(message); console.log(message); } }<ng-content></ng-content>: This is the default slot. Any content placed directly inside<my-greeting-card>that doesn’t have aslotattribute will be projected here.<ng-content select=".extra-info"></ng-content>: This is a named slot. Content with the classextra-infoand theslotattribute will be projected here. Note: When projecting to Web Components,ng-content selectmaps to nativeslotattributes.
Rebuild your Angular project:
ng build --output-hashing=none --configuration productionUpdate
test.htmlto project content:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Angular Web Component</title> <style> body { font-family: sans-serif; margin: 40px; } </style> <script src="./dist/angular-elements-demo/main.js"></script> </head> <body> <h1>My Application with a Reusable Angular Web Component</h1> <my-greeting-card name="Alice"> <p class="projected-content">This content is projected into the default slot!</p> <div class="extra-info" slot="extra-info"> <span>This is additional information specific to Alice.</span> </div> </my-greeting-card> <my-greeting-card name="Bob"> <p class="projected-content">Just a simple greeting for Bob.</p> </my-greeting-card> <script> const aliceGreeting = document.querySelector('my-greeting-card[name="Alice"]'); if (aliceGreeting) { aliceGreeting.addEventListener('greeted', (event) => { alert(`Event from 'Alice' component: ${event.detail}`); }); } </script> </body> </html><p class="projected-content">This content is projected...</p>: This element will be projected into the default slot.<div class="extra-info" slot="extra-info">...</div>: This element, with theslot="extra-info"attribute, will be projected into the named slot defined by<ng-content select=".extra-info">.
Serve test.html and observe how the content inside the custom element tags is projected into the respective slots within the GreetingComponent’s Shadow DOM. The ::slotted() CSS pseudo-element is used to style the projected content from within the component’s styles.
Exercise/Mini-Challenge:
Add another named slot to GreetingComponent for a “footer” section. Then, project some unique footer content into each of your my-greeting-card instances in test.html.
4. Advanced Topics and Best Practices
As you become more comfortable with Angular Elements, consider these advanced topics and best practices for robust and efficient component development.
4.1. Bundling Strategies for Production
For production deployments, the default Angular build might produce multiple JavaScript files (e.g., runtime.js, polyfills.js, main.js). For truly standalone Web Components, you often want a single JavaScript file that contains everything.
Code Example: Merging JavaScript Files
While Angular CLI doesn’t have a direct “single-file Web Component build” option, you can achieve this by concatenating the output files.
Ensure
outputHashingisnoneinangular.json: (You should have already done this in previous steps). Inangular.json, underprojects -> [your-project-name] -> architect -> build -> optionsandconfigurations -> production, ensureoutputHashing: "none"is set.{ "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", // Disable hashing in production too "namedChunks": false, "extractLicenses": true, "vendorChunk": false, "buildOptimizer": true, "budgets": [ { "type": "initial", "maximumWarning": "2mb", "maximumError": "5mb" } ] } } } } } } }Build the project:
ng build --configuration productionConcatenate the output files. You can use a tool like
concat(installable via npm) or a simple script.First, install
concatglobally (or locally and usenpx):npm install -g concatThen, concatenate the files (adjust paths based on your
distdirectory structure and Angular version):# For Angular versions <= v15 # concat -o dist/angular-elements-demo/my-web-component.js \ # dist/angular-elements-demo/runtime.js \ # dist/angular-elements-demo/polyfills.js \ # dist/angular-elements-demo/main.js # For Angular versions >= v16 (with esbuild builder, if polyfills are not separate) # The output structure might vary. A common scenario for standalone elements # is that `main.js` contains most of what's needed, possibly with a small `polyfills.js` if you still need them. # Check your `dist` folder carefully! concat -o dist/angular-elements-demo/my-greeting-card.js \ dist/angular-elements-demo/polyfills.js \ dist/angular-elements-demo/main.jsImportant: The order of concatenation matters!
polyfills.js(if present) should generally come first, followed bymain.js(which contains your application code). Inspect yourdistfolder after a build to verify the actual file names.
Now, you have a single my-greeting-card.js file that you can include in any HTML page:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standalone Angular Web Component</title>
<script src="./dist/angular-elements-demo/my-greeting-card.js"></script>
</head>
<body>
<h1>My Application with a Fully Packaged Angular Web Component</h1>
<my-greeting-card name="Packaged"></my-greeting-card>
</body>
</html>
Exercise/Mini-Challenge:
Create a small shell script or a package.json script (e.g., npm run build:element) that automates the ng build and concat steps into a single command.
4.2. Best Practices for Developing Angular Elements
- Keep Elements Focused and Small: Design your Angular Elements to be as lean and self-contained as possible. Avoid including large, unnecessary libraries if the component doesn’t strictly need them.
- Use Standalone Components: As demonstrated, standalone components (available since Angular 14) are ideal for Angular Elements as they reduce boilerplate and are inherently self-contained, simplifying the conversion process.
- Decouple from Root Angular Application: If your goal is truly reusable Web Components, try to keep the element’s logic and dependencies separate from your main Angular application’s architecture (if you have one).
- Clear Input/Output Contracts: Define clear and well-documented
@Input()properties and@Output()events. This forms the public API of your Web Component. - Naming Conventions:
- Custom Element Tags: Always use a hyphenated name (e.g.,
my-custom-element). This is a Web Components specification requirement. - Avoid Collisions: Choose unique tag names to prevent conflicts, especially if you plan to use multiple Angular Elements or other Web Components in the same application.
- Custom Element Tags: Always use a hyphenated name (e.g.,
- Accessibility (A11y): Ensure your Angular components are built with accessibility in mind. Web Components are just HTML elements, so standard ARIA attributes and best practices apply.
- Error Handling: Implement robust error handling within your Angular component logic, as consumers of your Web Component might not be aware of Angular-specific errors.
- Lifecycle Hooks: Understand how Angular lifecycle hooks (
OnInit,OnDestroy, etc.) map to Web Component lifecycle callbacks (e.g.,connectedCallback,disconnectedCallback).connectedCallbackanddisconnectedCallbackfire when the element is added or removed from the DOM, respectively. These often align with Angular’sngOnInitandngOnDestroy.
4.3. Common Pitfalls and Solutions
- Zone.js Overhead: Older Angular Elements deployments included
zone.jsin the bundle, potentially increasing file size.- Solution: With Angular v19/v20 and the
experimental-zoneless=trueflag (now stable), you can create zoneless Angular applications. This often removes the need forzone.js, significantly reducing bundle size and improving performance for Web Components.
- Solution: With Angular v19/v20 and the
- Styling Conflicts: While Shadow DOM prevents styles from leaking out, global styles with
!importantor universal selectors can sometimes bleed in if not handled carefully.- Solution: Leverage CSS Custom Properties (CSS Variables) to allow consumers to customize styles without breaking encapsulation. Inside your component, use
var(--my-custom-color). Consumers can then set--my-custom-color: red;on the custom element itself or its parent.
- Solution: Leverage CSS Custom Properties (CSS Variables) to allow consumers to customize styles without breaking encapsulation. Inside your component, use
- Bundle Size: Angular applications can be large. Compiling a full Angular app into a single Web Component can result in a significant bundle.
- Solution:
- Tree-shaking: Angular’s build process performs tree-shaking, removing unused code. Ensure your components are as lean as possible.
- Lazy Loading (Advanced): For very complex elements, you might consider dynamically importing parts of your Angular Element if certain features are only needed on demand.
- Shared Dependencies: If you have multiple Angular Elements on a page, try to share common Angular runtime code among them to avoid duplication. This usually involves custom build configurations.
- Solution:
- Dependency Injection in Web Components: While Angular’s DI works within the custom element, it doesn’t directly extend to the host application.
- Solution: For services or data shared with the host, use inputs, outputs (events), or potentially global state management solutions (though often, inputs/outputs are sufficient for component-level interaction).
- Change Detection Issues: In rare scenarios, especially with very complex interactions or when manually manipulating the DOM outside of Angular’s control, change detection might not fire as expected.
- Solution: For zoneless applications, ensure you are using signals appropriately or manually triggering change detection when necessary (e.g.,
ChangeDetectorRef.detectChanges()), though with signals this is less frequent.
- Solution: For zoneless applications, ensure you are using signals appropriately or manually triggering change detection when necessary (e.g.,
4.4. Real-World Context: Micro Frontends
Angular Elements are a powerful enabler for Micro Frontends. Imagine a large enterprise application composed of several independent teams, each responsible for a distinct part of the UI. One team might prefer Angular, another React, and a third Vue.
Scenario: A company needs to build an e-commerce platform. The “Product Catalog” team uses React, the “User Profile” team uses Vue, and the “Shopping Cart” team uses Angular.
Angular Elements Solution for the Shopping Cart:
The “Shopping Cart” team develops their entire shopping cart experience as an Angular application.
They identify key reusable components within this (e.g.,
CartItemDisplayComponent,CheckoutButtonComponent).Using Angular Elements, they compile their
CartSummaryComponentinto a Web Component:<ecom-cart-summary>.The React “Product Catalog” application can now simply embed this component:
// React Component rendering the Angular Element function ProductPage() { const handleCheckout = (event) => { console.log('Checkout initiated from Angular cart!', event.detail); // Navigate to checkout or process order }; // In React, you use lowercase for HTML attributes (inputs) // and on<EventName> for custom events. return ( <div> <h1>Product Listing</h1> {/* ... product list ... */} <ecom-cart-summary userId="123" oncheckoutinitiated={handleCheckout} ></ecom-cart-summary> </div> ); }And the Angular
CartSummaryComponentcould have:// Inside CartSummaryComponent @Input() userId: string = ''; @Output() checkoutInitiated = new EventEmitter<any>(); checkout() { this.checkoutInitiated.emit({ userId: this.userId, itemsInCart: this.cartService.getItems() }); }
This approach allows teams to maintain autonomy over their technology stack while providing a cohesive user experience. The host application simply treats the Angular Element as a standard HTML tag, abstracting away the Angular-specific implementation details.
5. Guided Projects
In this section, we’ll work through two guided projects to solidify your understanding of Angular Elements by building practical, reusable components.
Project 1: An Interactive User Avatar Widget
Objective: Create an Angular Element that displays a user’s avatar and name. When clicked, it should show a small popup with more user details and emit an event when the details are viewed.
Concepts Covered:
- Inputs (
@Input()) for user data. - Outputs (
@Output()) for user interaction. - Conditional rendering (
*ngIf). - Basic component styling.
- Shadow DOM encapsulation.
Steps:
Create a New Angular Component
UserAvatarComponent:ng generate component user-avatar --standaloneDefine Inputs and Outputs in
src/app/user-avatar/user-avatar.component.ts:We’ll need inputs for
imageUrl,userName, anduserEmail. We’ll also need an outputdetailsViewed.// src/app/user-avatar/user-avatar.component.ts import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; // For *ngIf @Component({ selector: 'awc-user-avatar', standalone: true, imports: [CommonModule], template: ` <div class="avatar-container" (click)="toggleDetails()"> <img [src]="imageUrl" alt="{{ userName }}" class="avatar-img" /> <span class="user-name">{{ userName }}</span> <div *ngIf="showDetails" class="details-popup"> <h3>{{ userName }}</h3> <p>Email: {{ userEmail }}</p> <p><small>Click anywhere to close</small></p> </div> </div> `, styles: [` :host { display: inline-block; font-family: Arial, sans-serif; margin: 10px; cursor: pointer; } .avatar-container { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 25px; background-color: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.1); position: relative; } .avatar-img { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; } .user-name { font-weight: bold; color: #333; } .details-popup { position: absolute; top: 100%; left: 50%; transform: translateX(-50%); background-color: #f9f9f9; border: 1px solid #eee; border-radius: 8px; padding: 15px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); z-index: 10; min-width: 200px; text-align: left; } .details-popup h3 { margin-top: 0; color: #007bff; } .details-popup p { margin-bottom: 5px; color: #555; } .details-popup small { color: #888; font-size: 0.8em; } `] }) export class UserAvatarComponent implements OnInit { @Input() imageUrl: string = 'https://via.placeholder.com/40'; @Input() userName: string = 'Unknown User'; @Input() userEmail: string = 'no-email@example.com'; @Output() detailsViewed = new EventEmitter<string>(); showDetails: boolean = false; ngOnInit() { console.log(`UserAvatarComponent initialized for ${this.userName}`); } toggleDetails() { this.showDetails = !this.showDetails; if (this.showDetails) { this.detailsViewed.emit(this.userName); } } }Convert and Register as a Custom Element in
src/main.ts:Use
createCustomElementandcustomElements.defineto turnUserAvatarComponentinto<my-user-avatar>.// src/main.ts (Append to existing logic or replace for this specific project focus) import { bootstrapApplication } from '@angular/platform-browser'; import { createCustomElement } from '@angular/elements'; import { ApplicationRef, DoBootstrap } from '@angular/core'; // Import the new component import { UserAvatarComponent } from './app/user-avatar/user-avatar.component'; // ... (other imports like GreetingComponent if you're keeping it) (async () => { const app = await bootstrapApplication(class App implements DoBootstrap { ngDoBootstrap(appRef: ApplicationRef): void {} }).catch(err => console.error(err)); if (app) { // Register GreetingComponent (if you still want it) const greetingElement = createCustomElement(GreetingComponent, { injector: app.injector }); customElements.define('my-greeting-card', greetingElement); console.log('Custom element <my-greeting-card> defined!'); // Register UserAvatarComponent const userAvatarElement = createCustomElement(UserAvatarComponent, { injector: app.injector }); customElements.define('my-user-avatar', userAvatarElement); console.log('Custom element <my-user-avatar> defined!'); } })();Build the Project:
ng build --output-hashing=none --configuration productionTest in
test.html:Create a new
project1-test.htmlor updatetest.html.<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>User Avatar Widget Demo</title> <script src="./dist/angular-elements-demo/main.js"></script> </head> <body> <h1>Interactive User Avatar Widgets</h1> <my-user-avatar imageUrl="https://randomuser.me/api/portraits/women/1.jpg" userName="Alice Smith" userEmail="alice@example.com" ></my-user-avatar> <my-user-avatar imageUrl="https://randomuser.me/api/portraits/men/2.jpg" userName="Bob Johnson" userEmail="bob@example.com" ></my-user-avatar> <div id="dynamic-avatars"></div> <script> document.querySelectorAll('my-user-avatar').forEach(avatar => { avatar.addEventListener('detailsViewed', (event) => { console.log(`User ${event.detail} details were viewed!`); alert(`Event: ${event.detail}'s details have been viewed.`); }); }); // Dynamically add another avatar const dynamicAvatar = document.createElement('my-user-avatar'); dynamicAvatar.setAttribute('image-url', 'https://randomuser.me/api/portraits/women/3.jpg'); dynamicAvatar.setAttribute('user-name', 'Carol White'); dynamicAvatar.setAttribute('user-email', 'carol@example.com'); document.getElementById('dynamic-avatars').appendChild(dynamicAvatar); dynamicAvatar.addEventListener('detailsViewed', (event) => { console.log(`Dynamically added user ${event.detail} details were viewed!`); alert(`Event: ${event.detail}'s details have been viewed.`); }); // Notice that HTML attribute names are dash-cased (kebab-case) // while Angular @Input properties are camelCase. // Angular Elements handles this conversion automatically. </script> </body> </html>Serve this HTML file and interact with the avatars. Click on them to reveal details and see the event pop up.
Encourage Independent Problem-Solving:
Try to add a “Follow” button to the user details popup. When this button is clicked, it should emit a new custom event, userFollowed, with the userName as its detail. Update test.html to listen for this new event and log a message.
Project 2: A Reusable Progress Bar
Objective: Create a Web Component that displays a customizable progress bar. It should take a progress value (0-100) and label as inputs and update its display dynamically.
Concepts Covered:
- Inputs (
@Input()) for dynamic data. - Reactive updates based on input changes.
- Styling for visual representation.
Steps:
Create a New Angular Component
ProgressBarComponent:ng generate component progress-bar --standaloneDefine Inputs in
src/app/progress-bar/progress-bar.component.ts:The
progressinput will determine the width of the filled bar.// src/app/progress-bar/progress-bar.component.ts import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'; import { CommonModule } from '@angular/common'; @Component({ selector: 'awc-progress-bar', standalone: true, imports: [CommonModule], template: ` <div class="progress-container"> <div class="progress-bar-label">{{ label }}: {{ currentProgress }}%</div> <div class="progress-bar-wrapper"> <div class="progress-bar-fill" [style.width.%]="currentProgress" [class.complete]="currentProgress === 100" ></div> </div> </div> `, styles: [` :host { display: block; font-family: Arial, sans-serif; margin: 15px 0; width: 100%; max-width: 400px; } .progress-container { background-color: #f0f0f0; border-radius: 5px; overflow: hidden; padding: 10px; border: 1px solid #e0e0e0; } .progress-bar-label { font-size: 0.9em; color: #555; margin-bottom: 5px; text-align: center; } .progress-bar-wrapper { width: 100%; height: 20px; background-color: #e9ecef; border-radius: 4px; overflow: hidden; } .progress-bar-fill { height: 100%; background-color: #007bff; width: 0%; /* Initial width */ transition: width 0.5s ease-in-out; border-radius: 4px; } .progress-bar-fill.complete { background-color: #28a745; /* Green when complete */ } `] }) export class ProgressBarComponent implements OnInit, OnChanges { @Input() progress: number = 0; @Input() label: string = 'Progress'; currentProgress: number = 0; ngOnInit() { this.updateProgress(this.progress); } ngOnChanges(changes: SimpleChanges): void { if (changes['progress']) { this.updateProgress(changes['progress'].currentValue); } } private updateProgress(newProgress: number): void { // Ensure progress is between 0 and 100 this.currentProgress = Math.max(0, Math.min(100, newProgress)); console.log(`Progress Bar "${this.label}" updated to: ${this.currentProgress}%`); } }@Input() progress: number = 0;: The current progress value.@Input() label: string = 'Progress';: A label for the progress bar.ngOnChanges(changes: SimpleChanges): This lifecycle hook is crucial for reacting to changes in@Input()properties when the component is used as a Web Component.[style.width.%]="currentProgress": Dynamically sets the width of the fill bar.
Convert and Register as a Custom Element in
src/main.ts:// src/main.ts (Append to existing logic or replace for this specific project focus) // ... (existing imports and registration for GreetingComponent and UserAvatarComponent) import { ProgressBarComponent } from './app/progress-bar/progress-bar.component'; (async () => { const app = await bootstrapApplication(class App implements DoBootstrap { ngDoBootstrap(appRef: ApplicationRef): void {} }).catch(err => console.error(err)); if (app) { // ... (Register GreetingComponent and UserAvatarComponent if desired) // Register ProgressBarComponent const progressBarElement = createCustomElement(ProgressBarComponent, { injector: app.injector }); customElements.define('my-progress-bar', progressBarElement); console.log('Custom element <my-progress-bar> defined!'); } })();Build the Project:
ng build --output-hashing=none --configuration productionTest in
test.html:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Progress Bar Web Component Demo</title> <script src="./dist/angular-elements-demo/main.js"></script> </head> <body> <h1>Dynamic Progress Bars</h1> <my-progress-bar label="Loading Assets" progress="25"></my-progress-bar> <my-progress-bar label="Task Completion" progress="70"></my-progress-bar> <my-progress-bar label="Download" progress="100"></my-progress-bar> <hr> <h2>Interactive Progress Bar</h2> <my-progress-bar id="interactive-progress" label="Interactive Demo" progress="0"></my-progress-bar> <button id="increment-progress">Increase Progress</button> <button id="reset-progress">Reset</button> <script> const interactiveProgressBar = document.getElementById('interactive-progress'); const incrementButton = document.getElementById('increment-progress'); const resetButton = document.getElementById('reset-progress'); let currentDemoProgress = 0; incrementButton.addEventListener('click', () => { currentDemoProgress += 10; if (currentDemoProgress > 100) { currentDemoProgress = 100; } // Update the 'progress' attribute to reflect changes interactiveProgressBar.setAttribute('progress', currentDemoProgress); }); resetButton.addEventListener('click', () => { currentDemoProgress = 0; interactiveProgressBar.setAttribute('progress', currentDemoProgress); }); // Observe changes on the 'progress' attribute for demonstration const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'progress') { console.log(`Attribute 'progress' changed to: ${interactiveProgressBar.getAttribute('progress')}`); } } }); observer.observe(interactiveProgressBar, { attributes: true }); </script> </body> </html>Serve this HTML file. You’ll see several static progress bars. Use the buttons to interact with the “Interactive Demo” progress bar and watch it update dynamically.
Encourage Independent Problem-Solving:
Add a new input, color, to the ProgressBarComponent that allows you to customize the background-color of the .progress-bar-fill element. If color is provided, it should override the default blue. If the progress is 100, the color should always be green, regardless of the color input.
6. Bonus Section: Further Learning and Resources
Congratulations on making it this far! You’ve learned the essentials of creating and using Angular Elements. The journey of web development is continuous, so here are some resources to help you dive deeper.
Recommended Online Courses/Tutorials
- Official Angular Documentation: The best place to start and stay up-to-date.
- Web Components.org: Comprehensive resources for native Web Components.
- Udemy/Coursera: Search for advanced Angular courses that might include sections on Angular Elements or Micro Frontends. Look for recent courses (late 2024 - 2025) to ensure relevance.
Official Documentation
- Angular Elements Documentation: angular.dev/guide/elements
- MDN Web Docs - Web Components: In-depth explanation of the underlying web standards.
Blogs and Articles
- Angular Blog: Stay informed about the latest Angular updates, which often include news on Angular Elements.
- Medium: Many independent developers share valuable articles and tutorials on Angular Elements and Web Components. Search for terms like “Angular Elements tutorial,” “Angular Web Components,” “Micro Frontends Angular.”
- Angular Elements Using Angular Components as Web Components (Published Aug 2025)
- Angular Web Elements — A coded gold (Published Jul 2025)
- Create Powerful Web Components with Angular 19 (Published Feb 2025)
YouTube Channels
- Angular Channel: The official channel often posts updates and deep dives.
- Fireship: Excellent, concise videos on web development topics, often including Angular and Web Components.
- Individual Creators: Many Angular experts have channels with practical tutorials. Search for “Angular Elements tutorial 2025” for the most recent content.
Community Forums/Groups
- Stack Overflow: A vast resource for specific programming questions. Use tags like
angular-elements,web-components,angular. - Angular Discord Server: Many Angular communities exist on Discord for real-time help and discussions.
- GitHub Discussions: The Angular GitHub repository often has discussions on specific features, including Angular Elements.
Next Steps/Advanced Topics
- Advanced Build Optimizations: Explore custom Webpack/Rollup configurations for more granular control over bundling and code splitting if you’re building a library of Web Components.
- Micro Frontend Orchestration: Investigate tools and patterns for orchestrating multiple Micro Frontends built with different technologies, often using Web Components as the integration layer (e.g., using single-spa, module federation).
- Web Component Performance: Deep dive into optimizing the loading and rendering performance of your Web Components, especially for large applications.
- Server-Side Rendering (SSR) with Web Components: Explore how Angular Elements interact with SSR strategies, particularly if your host application uses SSR.
- Web Component Testing: Learn about testing strategies specifically for Web Components, ensuring their robustness across different environments.
- Integrating with Specific Frameworks: Understand the nuances of integrating Angular Elements into React, Vue, or other frameworks. For instance, how to handle data flow when a framework has its own reactivity system.
Keep building, keep experimenting, and happy coding!