Web components are a way of creating custom elements that work alongside the standard HTML elements. The goal for web browsers, such as Google and Mozilla, is to provide a standardized way for developers to create custom elements with any framework. The idea is that you can put something like <app-dashboard></app-dashboard>
next to standard elements, like <h1>
and <ul>
, and it will just work.
With Angular, you can already do that through templates where component selectors are used to place the Angular component into the DOM.
In this article, I’ll show you how to use Angular to build an embeddable newsletter signup form that uses Web Components. You can view the finished code on Github.
First, we’re going to create a new Angular app and a simple widget. After that, we’ll make sure the widget works with the usual Angular build process.
We start by installing the Angular CLI globally:
npm install -g @angular/cli
Creating the Angular project
Then, we create a new Angular workspace which will contain our app:
ng new angular-web-component --routing=false --skip-tests=true --style=css
We don’t need any routing for our demonstration. We’re also going to skip out on generating tests. For styling the component, we’re going to keep things simple and use CSS (alternatives that Angular allows are SCSS, Sass, Less, and Stylus).
Creating the newsletter signup form component
We will add a new component which we will turn into an embeddable widget. Run the following command in the root project directory:
ng generate component info-box
This will generate the component’s TypeScript code, HTML template, and CSS stylesheet.
We’re going to change the selector from the generated “app-info-box” to “info-box”, and add two input attributes to the info box component’s code:
// src/app/info-box/info-box.component.ts
import { Component, Input } from '@angular/core'; @Component({ selector: 'info-box', templateUrl: './info-box.component.html', styleUrls: ['./info-box.component.css']
})
export class InfoBoxComponent implements OnInit { @Input() signupTitle = 'Sign up for our newsletter'; @Input() thankyouMessage = 'Thanks!'; formData = { name: '', email: '' }; formSubmitted = false; constructor() { } onSubmit() { this.formSubmitted = true; }
}
Then, we’re going to update the template to display a signup form and a thank you message after the user has signed up for the newsletter. The thank you message and sign up title are both input attributes to the component, which we will be able to modify whenever we embed the component as a widget.
<!-- src/app/info-box/info-box.component.html -->
<div *ngIf="formSubmitted; else formEntry"> <h4>{{ thankyouMessage }}</h4> <p>{{ formData.name }}, our newsletter is on its way to your inbox!</p>
</div>
<ng-template #formEntry> <h4>{{ signupTitle }}</h4> <p>Get cool news and tips on the latest blockchain Angular-powered AI 5G stuff</p> <form (ngSubmit)="onSubmit()" #emailForm="ngForm"> <div> <label>Name:</label> <input type="text" name="name" [(ngModel)]="formData.name"> </div> <div> <label>Email:</label> <input type="text" name="email" [(ngModel)]="formData.email"> </div> <div> <button type="submit" [disabled]="!emailForm.form.valid">Sign up!</button> </div> </form>
</ng-template>
The logic is the basic template-driven form logic that Angular provides through the FormsModule.
We’re going to use the info box component in the template twice: once with defaults where no input attributes are provided, and another time where we provide the sign up title and the thank you message:
<!-- src/app/app.component.html →
<h1>Welcome to Angular Web Component Example</h1>
<p><info-box></info-box></p>
<p><info-box signupTitle="Amazing newsletter!" thankyouMessage="Thanks for signing up!"></info-box></p>
Building the project
Now we can build and serve the project locally using ng serve
. That command will compile the code, templates, and stylesheets, and serve them in a local web development server. You can open your browser to localhost:4200 to see the Angular app.
Making a build for production is as easy as running the command ng build --prod
Configuring Angular for web components
Now that we have the app working, we can configure it to build a web component. This will let us embed the component into a static site, such as those generated by static site generators, or embed the widget into a WordPress or Shopify website.
Adding the libraries needed
Currently, only Firefox (version 63+), Chrome, and Opera support web components. To support all other browsers, we will need to add a polyfill library called “document-register-element”. This adds the registerElement function to the global document object so we can use Web Components with Angular.
Install the document-register-element package with this command:
npm install --save document-register-element@">=1.7.2 <=1.8.1"
Note: Due to a bug in the latest version of the library, that is the package version range that will work. You should update the package.json to include the specific version range.
We also need the @angular/elements library, as it provides the Angular function that uses the polyfill to render Angular components:
ng add @angular/elements
Updating the index.html file and using the Web Component
Now we’re going to update index.html
to use the Angular component with the “info-box” tag, which replaces the “app-root”. Note that, at this point, you could also entirely delete the app component since the info box component can be rendered directly into the index.html
file.
<!-- src/index.html →
<!doctype html>
<html lang="en">
<head> <meta charset="utf-8"> <title>AngularWebComponent</title> <base href="/"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body> <p><info-box></info-box></p> <p><info-box signupTitle="Amazing newsletter!" thankyouMessage="Thanks for signing up!"></info-box></p>
</body>
</html>
The attributes of a component are important, but they become even more so for web components, as they are the only way the component can be configured. If you need to display more complex data, you can pass in a JSON string or a URL. You can also download additional configuration JSON data by using Angular HttpClient.
Adjusting the build process
If you run the Angular dev web server now, it will render nothing and show an error that looks something like this:
Error: "The selector "app-root" did not match any elements"
This means that the app-root component is no longer in the index.html
. This is because we still have the AppComponent as part of the app module bootstrap configuration.
To fix this, we’ll first need to completely remove the app component and its template and style:
rm src/app/app.component.*
Next, we have to update app.module.ts
to remove the app component.
We also need to add the info box component as an entry component. The difference is that bootstrap components are rendered as soon as the Angular app loads, while entry components are not rendered immediately.
We’re going to use the app module constructor to inject the info box component and use the registerElement polyfill to define our web component as a custom element:
// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements'; import { InfoBoxComponent } from './info-box/info-box.component'; @NgModule({ declarations: [InfoBoxComponent], imports: [BrowserModule, FormsModule], providers: [], entryComponents: [InfoBoxComponent]
})
export class AppModule { constructor(injector: Injector) { const el = createCustomElement(InfoBoxComponent, { injector }); customElements.define('info-box', el); } ngDoBootstrap() {}
}
The constructor code is the replacement for the bootstrap configuration. Also notice that we specify the CUSTOM_ELEMENTS_SCHEMA, which is provided by @angular/elements. The ngDoBootstrap method is also required to kickstart the Angular rendering process.
Building the project
Building a web component project is similar to what we do for a typical Angular project. We run ng serve
for local development and ng build --prod
. The major difference is that the app component no longer exists and it is the index.html
that renders our embeddable web component.
So let’s build the project:
ng build --prod
Using the component in another project
Now that we have a production build of our Angular component, we can use it as a web component in another project.
We begin our new standalone project by creating a new example
directory at the project’s top-level. In this directory, we’re going to have an index.html
file and it’s going to contain the info-box.js
JavaScript file that we produced, along with the info-box.css
stylesheet.
Let’s copy the stylesheet:
cp dist/angular-web-component/styles.*.css example/info-box.css
Angular builds multiple JavaScript files and a style file. We need the JavaScript code to be in one file so that it can be embedded more easily into another project.
The way we do this is by simply concatenating the JavaScript files from the dist
directory:
cat dist/angular-web-component/runtime-es2015*.js dist/angular-web-component/polyfills-es2015*.js dist/angular-web-component/main-es2015*.js > example/info-box.js
To make the build process easier, we can update the scripts
in package.json
:
"scripts": { "ng": "ng", "start": "ng serve", "build": "ng build --prod && cat dist/angular-web-component/runtime-es2015*.js dist/angular-web-component/polyfills-es2015*.js dist/angular-web-component/main-es5*.js > example/info-box.js && cp dist/angular-web-component/styles.*.css example/info-box.css", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" },
Then we can run npm run-script build
.
We’re going to create a basic index.html
that uses a precompiled version of Bootstrap for styling.
In this new project we will have the following files:
- index.html
- Info-box.js
- info-box.css
For the index.html
, this is the HTML code we will have:
<!-- example/index.html →
<!DOCTYPE html>
<html>
<head> <title>Example Website with Embedded Angular Web Component</title> <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script> <script src="info-box.js"></script> <link rel="stylesheet" href="info-box.css">
</head>
<body> <div class="jumbotron"> <h1 class="display-4">Hello, world!</h1> <p class="lead">This is a simple hero unit, a simple jumbotron-style component for calling extra attention to featured content or information.</p> <hr class="my-4"> <p><info-box></info-box></p> <p><info-box signup-title="Amazing newsletter!" thankyou-message="Thanks for signing up!"></info-box></p> <p>It uses utility classes for typography and spacing to space content out within the larger container.</p> <a class="btn btn-primary btn-lg" href="#" role="button">Learn more</a> </div>
</body>
</html>
It includes the Bootstrap CSS framework and you can see that underneath the heading and above the button, we have the info-box
Angular component embedded. Notice that the attributes given to info-box
use dashes instead of kebab case, signup-title
instead of signupTitle
.
To see this working, we have to install http-server
, which is a basic zero-configuration web server and run it from the example
directory:
npm install --save-dev http-server
npx http-server example
Open the browser to http://localhost:8080 and you’ll see the example page show up.
Conclusion
Web components are a great way to make reusable widgets that can be embedded within other web apps or websites. If you want more interactivity on your statically generated website, or need an interactive and engaging widget on your WordPress site, you are no longer constrained by whatever build process, language, or framework that the project was using.
You can use Angular with TypeScript and create well-crafted widgets that can be used anywhere. Check out the code from this tutorial on Github.