Using Web Components in Angular

The History of Components in Web Development

If you’re a JavaScript developer, you’ve probably heard of Web Components. In case you’re not familiar with them, Web Components make it possible to define custom HTML components using JavaScript. Have you ever wanted to create a <my-cat /> tag that will wrap its contents with a border of animated dancing cats? With Web Components, you can (and it’ll work in any browser) whether or not you’re using a framework.

The idea of component-based web development isn’t new. It’s been around nearly as long as the web. It’s an alluring proposition; instead of generating bespoke HTML for every site you generate, components offer the opportunity to create reusable building blocks.

The first widespread use of components to build web apps was NeXT Software’s WebObjects, which appeared in 1996. A few years later, both JavaServer Faces and ASP.NET Web Forms hit the market. Although all of these frameworks generated HTML server-side and sent it to the browser, users of modern front-end frameworks would find the concepts in WebObjects, JSF, and Web Forms very familiar.

Eventually, as browsers evolved and JavaScript engines became more capable, developers started experimenting with the creation of single-page applications (SPA). Instead of generating HTML server-side, developers started to generate its client-side, in the browser. One of the first component-based client-side frameworks to appear was the Google Web Toolkit (usually just called GWT) in 2006. GWT was a couple of things in one: it was a component-based framework and was also a Java-to-JavaScript compiler.

In 2010, more popular component frameworks appeared: BackboneJS and KnockoutJS. Unlike GWT, these were pure JavaScript frameworks. In 2012, AngularJS appeared, and this is when the popularity of client-side component frameworks accelerated. Within a couple of years, React and Vue showed up, and for many web developers, a component-based SPA became the default way of developing new web apps.

All of these client-side frameworks have a drawback — components created in one framework can’t be used easily in another framework. In many cases, this isn’t a problem; a company that settles on a framework usually uses that framework for all of its applications. But for companies, open-source projects, and individuals who want to create reusable front-end component libraries, the wide variety of popular frameworks presents a challenge: how do you make components that anyone can use without duplicating your work for every framework you want to support?

Web Components offer a solution. They’re a browser-native way to define custom HTML elements that can be used in any framework, or even without any front-end framework at all. We’ll be exploring how to use Web Components in Angular 8. To begin, we’ll walk through the creation of a couple of simple Web Components.

Prerequisites

The rest of this article assumes you’re familiar with modern Angular development. We’ll be using Angular 8, but if you’re familiar with Angular 4+, you’ll have no trouble following along.

We also assume you’re familiar with how Web Components work. Although we’ll be creating our very own web components to use in an Angular app, we’re not going to have time to do a deep-dive into Web Components before we start.

Fortunately, the Web Components specification site provides a great introduction to the basics of creating and using Web Components. Unlike most software specs, this one is easy to follow; it explains everything in plain English and includes plenty of code samples.

Creating Web Components

Before we can put Web Components into our Angular app, we’ll need some Web Components to use. We could import something from an existing component library, but where's the fun in that?

We’re going to create two simple counter components. One will be an imperative component. This component will have an object-oriented API that we’ll have to call to increment or decrement a counter.

The second component will be a declarative counter component. This component has its count value passed in via an attribute. It relies on this external value and can't increment or decrement its value. While this might sound useless, declarative components are common in cases where a component is only responsible for displaying data passed to it. The Wijmo Gauge is a good example of this type of component.

Now let’s dive into our custom component code.

To make this exercise easy to follow, we’ve put all the code into a StackBlitz so you can view and run it from your web browser.

To get started, let’s take a look at the ImperativeCounter.ts file, located in the src/app/web-components folder.

class ImperativeCounter extends HTMLElement {  
  private readonly shadow: ShadowRoot;  
  private currentCount: number = 0;  

  constructor() {  
    super();  
    this.shadow = this.attachShadow({ mode: 'open'});  
    this.update();  
  }

  update() {  
    const template = `  
      <style>  
        .counter {  
          font-size: 25px;  
        }  
      </style>  
      <div class="counter">  
        <b>Count:</b> ${this.currentCount}  
      </div>  
    `;  
    this.shadow.innerHTML = template;  
  }

  increment(){  
    this.currentCount++;  
    this.update();  
  }

  decrement() {  
    this.currentCount--;  
    this.update();  
  }  
}

window.customElements.define('i-counter', ImperativeCounter);

Like all Web Components, this one starts by extending HTMLElement. We must do this, or else the browser won’t let us register the component.

Next, we create two instance variables: shadow, which holds the Web Component’s shadow DOM, and currentCount, which stores the current value of our counter.

Then, our constructor creates the shadow DOM and stores it in shadow. The constructor finishes up by calling the update method.

In update, we define an HTML template for our element that includes the value of currentCount. We then assign our template string to our shadow DOM’s innerHTML property.

Our imperative counter class finishes up by defining its increment and decrement methods. These methods simply increase or decrease the value of currentCount, and then call update to ensure that we show the new value of currentCount in our component’s HTML.

Outside the class declaration, we call window.customElements.define to register our shiny new component with the browser. Without this step, nobody would be able to use our counter component.

Now, let’s take a look at the declarative counter component. Notice that it’s similar to the imperative counter:

class DeclarativeCounter extends HTMLElement {  
  private readonly shadow: ShadowRoot;  
  private currentCount: number = 0;

  constructor() {  
    super();  
    this.shadow = this.attachShadow({ mode: 'open'});  
  }  

  static get observedAttributes() {  
      return ['count']  
  }

  connectedCallback() {  
      this.currentCount = parseInt(this.getAttribute('count')) || 0;  
      this.update();  
  }

  attributeChangedCallback(attrName, oldVal, newVal) {  
      this.currentCount = newVal;  
      this.update();  
  }

  update() {  
    const template = `  
      <style>  
        .counter {  
          font-size: 25px;  
        }  
      </style>  
      <div class="counter">  
        <b>Count:</b> ${this.currentCount}  
    `;  
    this.shadow.innerHTML = template;  
  }  
}

window.customElements.define('d-counter', DeclarativeCounter);

Just as in the imperative counter, we'll start by defining instance variables for our shadow DOM root and the counter’s current count value.

The constructor is similar, but we don’t call the update method. We want to wait and see if a value was passed into the component via the count attribute. This value won’t be available at the time the component’s constructor is called. Instead, we use a Web Component lifecycle method: connectedCallback.

This method gets called after a component has been inserted in the DOM. If we look in our component’s connectedCallback method, we’ll see that it reads the value of the component’s count attribute and uses it to set the value of currentCount. It then calls update to render the component.

Looking through the component, we’ll see two more things that differ from the imperative counter: a static property named observedAttributes, and an attributeChangedCallback method. These are both parts of the Web Components API.

observedAttributes provides the browser a list of attribute names for which the component would like to receive a notification when they change. This is helpful because end-users of the component could add as many attributes to it as they want to, and it would be a waste of resources for the browser to send the component change notifications it isn’t going to use.

The attributeChangedCallback method is called by the browser whenever one of the attributes listed in attributeChangedCallback changes. In our case, count is the only attribute we’ll receive notifications for. So, when the method is called, we update currentCount with the new value and then call update to re-render the component.

Using Web Components in Angular

Now that we’ve created our components, it’s time to use them in Angular. After all the work we’ve done so far, using our Web Components in Angular will be so easy, it’s going to seem anti-climactic!

In the StackBlitz project, open up app.module.ts. You’ll see the following:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';  
import { BrowserModule } from '@angular/platform-browser';  
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({  
  imports:      [ BrowserModule, FormsModule ],  
  declarations: [ AppComponent ],  
  bootstrap:    [ AppComponent ],  
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ]  
})  
export class AppModule { }

Overall, this is very similar to a bare-bones app module that Angular CLI would generate for you. There are a couple of important differences, though, so let’s examine those.

The first thing to note is that we’re importing CUSTOM_ELEMENTS_SCHEMA from @angular/core. Angular uses schemas to determine what element names are allowed inside a module. Importing the custom elements schema is necessary because, without it, the Angular template compiler will report an error when it encounters an element name it doesn’t understand.

Also, notice that we’ve added a schemas property to the NgModule decorator to tell Angular to use the custom elements schema in our app module.

Next, look in main.ts and note that we’ve added two imports:

import "./app/web-components/ImperativeCounter.ts";  
import "./app/web-components/DeclarativeCounter.ts";

These lines import the counter components we created and registers them with the browser. This step is important because, without it, we wouldn’t be able to use the components in our Angular app!

Let’s take a look in app.component.html to see how our Web Components are used:

<h5>Imperative Counter</h5>  
<div class="counter-box">  
  <i-counter #iCounter></i-counter>  
</div>  
<h5>Declarative Counter</h5>  
<div class="counter-box">  
  <d-counter [count]="count" #dCounter></d-counter>  
</div>  
<button (click)="increment()" class="btn btn-primary">Increment</button>  
<button (click)="decrement()"class="btn btn-primary">Decrement</button>

As you can see, we render our Web Components using the tag names we registered with the browser: i-counter and d-counter. For the declarative counter, we also bind its initial count attribute to the value of our component’s count variable.

You’ll see that we’ve also added something extra to our components: #iCounter and #dCounter.

As we’ll see in a moment, these tags allow our Angular component to obtain direct references to these elements so we can update them.

Finally, we have increment and decrement buttons that call methods in the app component to change the value of our Web Component counters.

Now, let’s move on to look at app.component.ts:

import { Component, ElementRef, ViewChild  } from '@angular/core';

@Component({  
  selector: 'my-app',  
  templateUrl: './app.component.html',  
  styleUrls: [ './app.component.css' ]  
})  
export class AppComponent  {  
  private count: number = 0;  
  @ViewChild("iCounter") iCounter: ElementRef;  
  @ViewChild("dCounter") dCounter: ElementRef;

  increment() {  
    this.count++;  
    this.iCounter.nativeElement.increment();  
    this.dCounter.nativeElement.setAttribute("count", this.count);  
  }

  decrement() {  
    this.count--;  
    this.iCounter.nativeElement.decrement();  
    this.dCounter.nativeElement.setAttribute("count", this.count);  
  }  
}

We start by importing Component, ElementRef, and ViewChild from @angular/core. We need ElementRef and ViewChild because we’ll need to manipulate our Web Components directly to change their values. Although this is a bit more awkward than working with native Angular components, it’s easy to do.

Inside the class, we add an instance variable to store our current counter value; then we use the ViewChild decorator to acquire ElementRefs to our two web components by using the tags we just saw in the template. ElementRef instances make it possible to use underlying native elements directly. In our case, these elements are DOM nodes.

Next, we have increment and decrement methods. This is where the magic happens. In both methods, we change the component’s count variable, and then update our Web Components to use the new value. In the case of the imperative counter, we call its increment and decrement methods. For the declarative counter, we use the DOM’s setAttribute method to update the counter’s value.

That’s all there is! We have a fully functioning Angular app, using our hand-crafted Web Components. You can see the final result in action here.

Using Web Components in Angular Apps

And with that, we’ve completed our tour of Web Components and how to use them in an Angular app.

Now, you can add mix Web Components into all of your Angular applications, and your users will love you even more than they do already! You’re no longer limited to using Angular components in your apps. Although Angular Material is great, you might find that one of Google’s Material Web Components is a better fit for your app.

GrapeCity has a complete set of JavaScript UI components and powerful Excel-like JavaScript spreadsheet components. We have deep support for Angular (as well as React and Vue) and are dedicated to extending our components for use in modern JavaScript frameworks.

Here's a look at several of our Angular demos.

Wijmo's UI controls support all versions of Angular

Download the latest version of Wijmo

Download Now!

Try SpreadJS's Angular spreadsheet components

Download the latest version of SpreadJS

Download Now!