DEV Community

Cover image for Angular Elements Using Angular Components as Web Components
Tianya School
Tianya School

Posted on

Angular Elements Using Angular Components as Web Components

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
Enter fullscreen mode Exit fullscreen mode

Create a new project:

ng new angular-elements-demo
cd angular-elements-demo
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • createCustomElement converts GreetingComponent into a Web Component, with injector providing Angular’s dependency injection.
  • customElements.define registers the Web Component with the tag greeting-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>
Enter fullscreen mode Exit fullscreen mode

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"
                }
              ]
            }
          }
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Build the project:

ng build --configuration production
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Serve with http-server:

npm install -g http-server
http-server dist/angular-elements-demo
Enter fullscreen mode Exit fullscreen mode

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}!`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}!`);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
/* react-host/src/App.css */
.App {
  text-align: center;
  padding: 20px;
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

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() {}
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)