Angular is an application framework favored by many in the JavaScript community. Angular provides a library for building encapsulated components, dependency injection, a templating language with data binding, an application router built on observables, and a command line interface that lowers the barrier to entry. While being slightly less flexible than some frameworks, Angular’s opinionated nature helps larger teams to code to an existing standard rather than developing their own. In addition, Angular makes it easy to separate display logic (components) from business logic (services and logic) so multiple teams can work on different aspects of the same application. To see a more complete analysis of frameworks, see our series on choosing a framework.

Web components

Web components are a set of standard APIs that make it possible to natively create custom HTML tags that have their own functionality and component lifecycle. The APIs include a custom elements specification, the shadow DOM, which allows the internals of a component to be isolated, and HTML imports, which describe how components can be loaded and used in any web application. The main goal of web components is to encapsulate the code for the components into a nice, reusable package for maximum interoperability.

Angular

Angular 2+ does a lot of the heavy lifting for you by compiling a component’s template to a JavaScript renderer and keeping the data in sync between it and the component instance. It does this by using a unidirectional data flow and a lifecycle manager that will detect changes to a component’s properties and determine which templates need to re-render. Because Angular’s templating language is essentially HTML with some syntactic sugar, the compiler already understands how to create DOM nodes based on tags in a template. By default, though, the compiler only understands standard HTML tags and any Angular components that have been registered with your application. However, once the Angular compiler knows to expect web components, any web components that have been registered with the browser can be used throughout an application as if they were native HTML elements.

We’ll be starting with a project generated with angular-cli (full source) using the same web components we used in our React web components tutorial. Let’s get started and show you how.

1. Enable web components

First, let’s enable web components in our project in src/app/app.module.ts:

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

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

import '../web-components/Tabs';
import '../web-components/Tab';

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

We’ve started by importing CUSTOM_ELEMENTS_SCHEMA from @angular/core and added it to the application’s @NgModule declaration in the schemas property. This tells Angular’s template compiler to allow web components and their attributes. We have also imported our web components so they are registered with the browser. However, because these web components are JavaScript files, we will need to tell TypeScript to allow importing JavaScript files in tsconfig.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "allowJs": true,
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2017",
      "dom"
    ]
  }
}

Here we have added the compilerOptions.allowJs property and set it to true. We’re now able to start using our web components with our Angular application and components.

2. Use Components in a template

Now that the Angular compiler knows to allow our custom element tag names, we can start using our web components in any template in our project. We’ll start by adding a very simple static tabs component to src/app/app.component.html:

<x-tabs>
  <x-tab title="Tab 01" closable="true">
    <div>
      <h3>Tab 01 Content</h3>
    </div>
  </x-tab>
  <x-tab title="Tab 02">
    <div>
      <h3>Tab 02 Content</h3>
    </div>
  </x-tab>
</x-tabs>

This is a simple tabs component using version 1 of the web components recommendation. The x-tabs component will create an unordered list with each of the x-tab titles that will control which tab has its content shown. As you can see, there is nothing in the markup that specifies where or how the ul will be rendered, which makes this a good component to discuss as portions of the component are dynamically generated. This component has events that will be fired, dynamically generates markup within the component, and modifies its DOM structure.

3. Handling events and collections

When a closable tab is closed, the x-tabs component emits a tabclosed event. The handling of web component events follows the same approach used for handling Angular component events. We’ll change our template (src/app/app.component.html) to hook up the event:

<x-tabs (tabclosed)="onTabClosed($event)">
  <x-tab title="Tab 01" closable="true">
    <div>
      <h3>Tab 01 Content</h3>
    </div>
  </x-tab>
  <x-tab title="Tab 02">
    <div>
      <h3>Tab 02 Content</h3>
    </div>
  </x-tab>
</x-tabs>

And we’ll implement the onTabClosed method on our component (src/app/app.component.ts):

  onTabClosed({ detail: component }) {
    console.log(component);
  }

When we run our application, clicking the close button on the first tab runs our event handler and logs out the x-tab node that was just removed.

Because Angular treats web components as first-class citizens, we can also use the @ViewChildren decorator to get a collection of tabs from our template. We’ll need to change our template to add template ids to the x-tab nodes:

<x-tabs (tabclosed)="onTabClosed($event)">
  <x-tab title="Tab 01" #tab closable="true">
    <div>
      <h3>Tab 01 Content</h3>
    </div>
  </x-tab>
  <x-tab #tab title="Tab 02">
    <div>
      <h3>Tab 02 Content</h3>
    </div>
  </x-tab>
</x-tabs>

And we’ll add a property decorated with the @ViewChildren decorator to our component:

  @ViewChildren('tab') tabs: QueryList<ElementRef>;

Now when we look at our application in Augury, we can see that the tabs property is populated with two ElementRef objects which hold references to the x-tab components:

However, one problem arises: when we click on the close button for the first tab, the tabs collection is not updated. This stems from the fact that the application template is not updated when the underlying DOM changes underneath it (as our x-tabs component does). Triggering a re-render of the component would cause our close tab to appear again.

4. Manage the tabs

For a simple application with no closable tabs, the x-tabs component will work out of the box. However, if we need closable tabs that Angular can track, we’ll need to manage some sort of state and let Angular keep track of rendering the x-tab components. First, we’ll modify our application template to add a new set of tabs (for the purposes of this tutorial) and use ngFor to generate them from an array of objects (tabObjects):

<x-tabs (tabclosed)="onStatefulTabClosed($event)">
  <x-tab *ngFor="let tab of tabObjects; let i = index" #statefulTab
         [attr.data-index]="i" [title]="tab.title" [closable]="tab.closable">
    <div>
      <h3>{{ tab.pane.title }}</h3>
    </div>
  </x-tab>
</x-tabs>

Then, we’ll modify the component to add the tabObjects array, the collection to hold our statefulTab elements, and the onStatefulTabClosed event handler:

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  tabObjects = [
    {
      title: 'Tab 01',
      closable: true,
      pane: {
        title: 'Tab 01 Content'
      }
    },
    {
      title: 'Tab 02',
      closable: false,
      pane: {
        title: 'Tab 02 Content'
      }
    }
  ];

  @ViewChildren('tab') tabs: QueryList<ElementRef>;
  @ViewChildren('statefulTab') statefulTabs: QueryList<ElementRef>;

  onTabClosed({ detail: component }) {
    console.log(component);
  }

  onStatefulTabClosed(event) {
    event.preventDefault();
    const component = event.detail;
    this.tabObjects.splice(component.dataset.index, 1);
  }
}

Because we are generating the x-tab components based on the tabObjects array, any change to it in our event handler will cause a re-render. However, we also need to prevent the default behavior of the tabclosed event (removing the corresponding x-tab and li) because Angular will handle this automatically. Taking a look in Augury after clicking the close button of the first tab in our stateful tabs reveals that Angular updates the statefulTabs collection to match the current state of our component:

Conclusion

Using web components in Angular applications and components is as straightforward as using Angular components. The template syntax, property binding, and event handling mechanisms that are familiar to Angular developers will work just as well with third-party web components as with Angular components, and wrappers will rarely need to be created.

Web components offer substantial potential for component interoperability across frameworks. It is our hope that all modern frameworks will eventually make it as easy to import web components as Angular does, allowing application developers to easily leverage components in many different modern application contexts.