Angular
Posted By Sebastian

Angular Elements – A Practical Introduction To Web Components With Angular 6


DemoCode

With the release of Angular 6 the new Angular Elements functionality is now fully available. By using Angular Elements you can package Angular components as custom elements, a web standard for defining new HTML elements in a framework-agnostic way.

The custom elements standard is currently supported by browsers like Chrome, Opera, and Safari. To be able to use it Firefox and Edge polyfills are available. With a custom element you can extend the set of available HTML tags. The content of this tag is then controlled by JavaScript code which is included in the page.

In order to keep track of all available custom elements the browser maintains a registry in which every elements needs to be registered first. In this registry the name of the tag is mapped to the JavaScript class which controls the behavior and the output of that element.

The Angular Elements functionality is available with the package @angular/elements. This packages exposes the createCustomElement() function which can be used to create a custom element (web component) from an Angular component class. Therewith it provides a bridge from Angular component interface and change detection functionality to the build-in DOM API.

In this tutorial we’ll explore the Angular Elements functionality from scratch by implementing a practical example from start to finish.

What We’re Going To Build In This Tutorial

In this tutorial we’ll be building a Web Component with Angular Elements which should have a similar functionality as the CodingTheSmartWay.com Framework Voter which is available at https://vote.codingthesmartway.com/. This web component can then be used in any HTML-based web application by including the custom HTML element in the body section of a HTML page.

Let’s take a look at the final result. On the first screen the user is asked to vote for his favorite front-end framework by clicking on one of the logos:


Having clicked on one of the logos takes you to the result page which displays the voting results in the following way:


As the back-end of the web component Firebase’s Firestore database is used. This makes sure that the application is connected to a real-time data store. The result page is updated automatically (once there are new votes available) without the need to manually refresh the page.

Creating An Angular 6 Project

First we need to create a new Angular project. Therefore we’ll be using Angular CLI. If you haven’t installed Angular CLI yet, you first need to install it on your systems by following the steps listed at https://cli.angular.io/.

Once Angular CLI is available a new project is initiated by using the following command:

$ ng new angularElements

This creates a new folder angularElements with the default Angular 6 project inside.

Adding Angular Elements

With the release of Angular 6 the new ng add command is available which makes it easy to add new capabilities to the project. This command will use the package manager to download new dependencies and invoke corresponding installation scripts. This is making sure that the project is updated with dependencies, configuration changes and that package-specific initialization code is executed.

In the following we’ll use the ng add command to add Angular Elements functionality to the previously created Angular 6 application:

$ ng add @angular/elements

By using this command we’re adding the needed document-register-element.js polyfill and dependencies for Angular Elements at the same time. Now the we’re ready to make use of Angular Elements functionality within our project. However, before doing so, we need to add further dependencies.

Adding Further Dependencies

Because we’ll be using Bootstrap’s CSS classes in our application we first need to make sure that the Bootstrap library is added to the project:

$ npm install bootstrap

Because our Angular application will be using Firebase as a back-end we need to install the following dependencies:

$ npm install firebase angularfire2

Because RxJS 5 is used in AngularFire2 and there have been some major changes in RxJS 6 we need to install the rxjs-compat package in addition to make AngularFire2 work.

$ npm install rxjs-compat@6

Of course, this is only a temporary issue and is expected to be fixed with one of the next releases of AngularFire2.

Adding A New Component

The implementation of Angular Elements is based on components. So we need to add a new component first:

$ ng generate component framework-poll

By using this command inside the project folder a new sub-folder is created: src/app/framework-poll. Inside that newly created folder you’ll be able to find the following four new files:

  • framework-poll.component.css
  • framework-poll.component.html
  • framework-poll.component.spec.ts
  • framework-poll.component.ts

The new component is added to AppModule as well.

In framework-poll.component.css add the following two lines of code to make sure that Bootstrap’s CSS classes are available:

@import '~bootstrap/dist/css/bootstrap.min.css';

Setting Up Firebase

Because we’ll be using Firestore as our real-time back-end we’re going to configure Firebase next.

First let’s add the import statements for AngularFireModule and AngularFirestoreModule in app.module.ts:

import { AngularFireModule } from 'angularfire2';
import { AngularFirestoreModule } from 'angularfire2/firestore';

Furthermore the Firebase configuration settings (which can be retrieved from the Firebase Console once a new Firebase project is created) are stored in an object named config:

const config = {
  apiKey: '...',
  authDomain: '...',
  databaseURL: '...',
  projectId: '...',
  storageBucket: '...',
  messagingSenderId: '...'
};

Next make sure to add AngularFireModule and AngularFirestoreModule to the array which is assigned to the imports property of the @NgModule decorator. For AngularFireModule the initialize method needs to be called and the configuration object has to be provided as an argument:

imports: [
    BrowserModule,
    AngularFireModule.initializeApp(config),
    AngularFirestoreModule
  ],

Creating The Custom Element

Next let’s create the custom element based on the FrameworkPollComponent. Therefore we need to import Injector from the @angular/core package and createCustomElement from the @angular/elements package:

import { NgModule, Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

Because FrameworkPollComponent will not be used in the Angular application itself we need to add the entryComponents property to @NgModule decorator and add FrameworkPollComponent to the assigned array:

entryComponents: [
    FrameworkPollComponent
]

Now we’re ready to create the custom element by using the createCustomElement function:

export class AppModule { 
  constructor(private injector: Injector) {}

  ngDoBootstrap() {
    const el = createCustomElement(FrameworkPollComponent, { injector: this.injector });
    customElements.define('framework-poll', el);
  }
}

This function is called inside the ngDoBootstrap method which has to be added to the AppModule class. The createCustomElement function is expecting to get two parameter:

  • First, the Angular component which should be used to create the element.
  • Second, a configuration object. This object needs to include the injector property which is set to the current Injector instance.

The next step is to register the newly created custom element in the browser. This is done by calling customElements.define(). Please note that this is not Angular. The customElements read-only property belongs to the Window interface and returns a reference to the CustomElementRegistry object, which can be used to register new custom elements and get information about previously registered custom elements in the browser.

The customElements.define() method needs two parameter. The first parameter is of type string and contains the name of the element. Passing the string framework-poll means that the custom element <framework-poll> will be registered and can be used in the HTML code. The second parameter is the custom element which has been created before.

Implement FrameworkPollComponent

Now that the custom element is created and registered in the browser we need to complete the implementation of FrameworkPollComponent next.

Setting View Encapsulation To Native

First we need to make sure that the native shadow DOM is used for the component, so that style encapsulation is done. This is done by setting the encapsulation property of the @Component decorator to ViewEncapsulation.Native.

import { Component, OnInit, ViewEncapsulation } from '@angular/core';
import { AngularFirestore, AngularFirestoreDocument } from 'angularfire2/firestore';

@Component({
  selector: 'app-framework-poll',
  templateUrl: './framework-poll.component.html',
  styleUrls: ['./framework-poll.component.css'],
  encapsulation: ViewEncapsulation.Native
})

Adding Class Members, Constructor And ngOnInit

Next step is to add class members, constructor and the implementation of the component lifecycle method ngOnInit:

export class FrameworkPollComponent implements OnInit {

  angularVoteCount: number;
  reactVoteCount: number;
  vueVoteCount: number;
  hasVoted = false;
  updating = false;
  fsRef: AngularFirestoreDocument<any>;

  constructor(private afs: AngularFirestore) {
    afs.firestore.settings({ timestampsInSnapshots: true });
  }

  ngOnInit() {
    this.fsRef = this.afs.doc('frameworkPoll/current');

    this.fsRef.valueChanges().subscribe(doc => {
      this.angularVoteCount = doc.angularVoteCount;
      this.reactVoteCount = doc.reactVoteCount;
      this.vueVoteCount = doc.vueVoteCount;
    });
  }

Dependency injection is used to inject the AngularFirestore service into the component. This is done by adding a private constructor parameter of that type.

Inside the constructor the the firestore.settings method is called to set the configuration option timestampsInSnapshots to true. This is needed to ensure that timestamps are returned and no Date values (needed to get AngularFire2 working correctly).

Inside the ngOnInit method we’re fist retrieving a reference to the Firestore document which must be available in frameworkPoll/current and must contain the following numeric properties: angularVoteCount, reactVoteCount and vueVoteCount.

Having obtained the reference of the Firestore document we’re able to subscribe to value changes by calling the method valueChanges() and subscribe to the returned Observable. The callback method will then make sure that the call properties angularVoteCount, reactVoteCount and vueVoteCount are updated with the updated values from the Firestore document.

Adding Method vote

The most important part of the FrameworkPollComponent implementation is the vote method:

vote(framework: string) {
    this.updating = true;
    this.afs.firestore.runTransaction(t => {
      return t.get(this.fsRef.ref).then(doc => {
        const newVoteCount = doc.data()[framework] + 1;
        t.update(this.fsRef.ref, { [framework]: newVoteCount });
      });
    })
      .then(() => {
        this.hasVoted = true;
        this.updating = false;
        console.log('Transaction successfully committed');
      })
      .catch(error => console.log('Transaction failed: ' + error));
  }

This event handler method will be executed each time the user clicks on one of the framework logos to vote. First the updating class property is set to true. Later on we’re going to use this flag to display a message to the user that the update is taking place.

The The firestore.runTransaction() method is called to executed a transaction which

  • first reads out the old voting value of the specific framework from Firestore
  • Increments the value by one and updates the the voting count in Firestore accordingly

After the transaction has been completed successfully the hasVoted property is set to true and updating is set to false. Both flags will be used in the template code later on.

Calculating Voting Results In Terms Of Percentages

To calculate the voting results in percentage terms three getters are added to the component class:

  get angularVotePercent() {
    return (this.angularVoteCount / (this.angularVoteCount + this.reactVoteCount + this.vueVoteCount)) * 100;
  }

  get reactVotePercent() {
    return (this.reactVoteCount / (this.angularVoteCount + this.reactVoteCount + this.vueVoteCount)) * 100;
  }

  get vueVotePercent() {
    return (this.vueVoteCount / (this.angularVoteCount + this.reactVoteCount + this.vueVoteCount)) * 100;
  }

The getters will be used in the template code to include the computed values in the HTML output of the component.

Component Template

To complete the component implementation the template code needs to be added to file framework-poll.component.html next:

<div class="container" [style.textAlign]="'center'" [style.padding.px]="10">
  <div class="card">
    <div>
        <a href="https://codingthesmartway.com/" target="_blank"><img src="../../assets/ctsw_logo.png" height="37" class="animated tada" alt="CodingTheSmartWay.com" [style.marginTop]="'20px'" /></a>
    </div>
    <hr>
    <div class="card-body">

      <div *ngIf="!hasVoted && !updating">
        <h2>Which is your favorite frontend framework?</h2>
        <h4>Click on the logos below to vote!</h4>

        <br />
        <div class="row">
          <div class="offset-md-3 col-md-2">
            <img src="../../assets/angular_logo.png" height="96" alt="Angular" (click)="vote('angularVoteCount')">
          </div>
          <div class="col-md-2">
            <img src="../../assets/react_logo.png" height="96" alt="React" (click)="vote('reactVoteCount')">
          </div>
          <div class="col-md-2">
            <img src="../../assets/vuejs_logo.png" height="96" alt="Vue.js" (click)="vote('vueVoteCount')">
          </div>
        </div>
      </div>

      <div *ngIf="updating">
        <h3>Thanks. Redirecting to voting results ...</h3>
      </div>

      <div *ngIf="hasVoted">
        <h3>Results</h3>
        
        <div [style.textAlign]="'left'">
          <span class="badge badge-pill badge-danger mb-1" >Angular: {{angularVoteCount}} ( {{angularVotePercent}} %)</span>
          <div class="progress">
            <div class="progress-bar progress-bar-striped bg-danger" role="progressbar" [style.width.%]="angularVotePercent"></div>
          </div>
          <br/>
          <span class="badge badge-pill badge-info mb-1">React: {{reactVoteCount}} ( {{reactVotePercent}} %)</span>
          <div class="progress">
            <div class="progress-bar progress-bar-striped bg-info" role="progressbar" [style.width.%]="reactVotePercent"></div>
          </div>
          <br/>
          <span class="badge badge-pill badge-success mb-1">Vue.js: {{vueVoteCount}} ( {{vueVotePercent}} %)</span>
          <div class="progress">
            <div class="progress-bar progress-bar-striped bg-success" role="progressbar" [style.width.%]="vueVotePercent"></div>
          </div>
        </div>
      </div>
    </div>

  </div>
</div>

As you can see the template code is split up into three sections:

<div *ngIf="!hasVoted && !updating">
    ...
</div>

<div *ngIf="updating">
   ...
</div>

<div *ngIf="hasVoted">
  ...
</div>

The content of the first div-section is displayed if the user has not yet voted (flags hasVoted and updated needs to be false). In this case the voting options are displayed so that the voting decision can be made by clicking on one of the framework logos. The click event is connected to the vote event handler method:

(click)="vote('angularVoteCount')"

The second section is displayed once the user has voted and the update process of the Firestore document has not been finished yet. In this case a confirmation message is printed out informing the user that the redirect to the result page will be done next.

The third section is displayed if the voting has taken place and the update process has been concluded. In this case the voting results are displayed as progress bars to the user.

Build Process

The web component is now ready and in theory we’re now able to use this custom HTML custom in any HTML page outside of our Angular project. Unfortunately the current release of Angular / Angular CLI is not offering special build functionality for Angular Elements. However in the following we’ll implement a custom build script that will close that gap. For the future it is expected that Angular CLI will include build options for Angular Elements, so that this process will be easier.

Building For Production

The standard build for production is done by using the following command inside the Angular project folder:

$ ng build —prod

If we use this command with the option --output-hashing none we’re making sure that no hash value is included in the file names and you should be able to see the following content inside the dist folder:

Here you can see that a bunch of JS files are created for the application. The custom build script we’re going to implement in the next step will make sure that these JS assets are bundles into one file so that it’s easier to include the JS code which is needed to make the web component available on your page.

Installing Further Dependencies

To be able to implement the custom build script we need two more dependencies:

$ npm install fs-extra concat

  • fs-extra: adds file system methods that aren’t included in the native fs module and adds promise support to the fs methods.
  • concat: concatenate multiple files

Creating A Build Script

In the top level project folder create a new file with name elements-build.js and insert the following code:

const fs = require('fs-extra');
const concat = require('concat');

(async function build() {
    const files = [
        './dist/angularElements/runtime.js',
        './dist/angularElements/polyfills.js',
        './dist/angularElements/scripts.js',
        './dist/angularElements/main.js',
    ]

    await fs.ensureDir('elements')

    await concat(files, 'elements/framework-poll.js');

    await fs.copyFile('./dist/angularElements/styles.css', 'elements/styles.css')

    await fs.copy('./dist/angularElements/assets/', 'elements/assets/' )
    
})()

With this code we’re making sure that:

  • that a new sub-folder elements is create inside the project folder
  • that the JS files runtime.js, polyfills.js, scripts.js and main.js are concatenated into a new file framework-poll.js inside the elements folder
  • that styles.css from the production build is copied to the elements folder
  • that files from the assets folder are copied to the elements folder

Add Script To Package.jSON

In order to be able to invoke the custom build script from the command line by using the NPM command the following line needs to be added to the scripts section in package.json:

  "scripts": {
    [...],
    "build:elements": "ng build --prod --output-hashing none && node elements-build.js"
  },

Running The Elements Build

Now the build process and the custom build script can be executed by using the NPM command in the following way:

$ npm run build:elements

The output in the elements folder should now comprise the following:

Using The Element

With that build result available we’re now able to make use of our framework-poll element in any HTML page by including the framework-poll.js file in the body section and the styles.css in the header section:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Test Angular Elements - Framework Poll</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <framework-poll></framework-poll>
    <script type="text/javascript" src="framework-poll.js"></script>
</body>
</html>

Conclusion

Angular Elements is an exciting and promising new feature which lets you bridge the gap between Angular components and the new custom elements browser standard. By using Angular elements functionality you will have many more possibilities of integration Angular applications in existing web applications / web pages. Transforming an Angular component to a custom element provides an easy way to create dynamic HTML content outside your Angular application.

ONLINE COURSE: Angular - The Complete Guide

Check out the great Angular – The Complete Guide with thousands of students already enrolled:

Angular – The Complete Guide

  • This course covers Angular 6
  • Develop modern, complex, responsive and scalable web applications with Angular
  • Use their gained, deep understanding of the Angular  fundamentals to quickly establish themselves as frontend developers
  • Fully understand the architecture behind an Angular application and how to use it
  • Create single-page applications with on of the most modern JavaScript frameworks out there

Go To Course


Using and writing about best practices and latest technologies in web design & development is my passion.