Angular 6 Elements

10 minute read

Angular Elements library provides a method to extend the HTML tag library writing components that can be used with any framework. The new components can be used everywhere without any Angular knowledge and regardless the technology used to implement a web site:

  • CMS
  • HTML and VanillaJS
  • React, Vue, etc
  • legacy AngularJS application as an alternative to ngUpdate for the migration.

Angular Elements project has been revealed at the AngularConnect 2017 by Rob Worlmald

Presentation. A presentation about Angular Elements based on this post is also available.

World of Custom Components

Each framework provides a specific tailored mechanism to smoothly extend the HTML tag library w.r.t. its own way to configure, interact and update the DOM element.

For instance in AngularJS, the developer can implement new HTML tags via the directive approach: a powerful technique to link together a controller and a template encapsulating everything in a new DOM element.

Framework specific components libraries cannot be used neither by another framework nor in a CMS nor in a website implemented in HTML and JavaScript with standard Web API.

Nitty Gritty. Web Components allow to extend the HTML developing new cross browser DOM elements. Encapsulation and private features enpower reusability avoiding collision with other elements (Shadow DOM).

Web Components

Web components are a set of Web Platform APIs to create new custom, reusable, encapsulated HTML tags that can be used everywhere.

Developers can then extend HTML tag library writing new elements that encapsule style and behavior.

Web Components include four different technologies:

  • Custom Elements: JavaScript APIs to define new DOM elements.
  • Shodow DOM: JavaScript APIs to render and attach and encapsulated Shadow DOM tree to an element whose features stay private and not collide with the rest of the document.
  • HTML templates: write HTML templates that can be reused by custom elements using <template> and <slot> tags.
  • HTML Imports: how to include and reuse HTML templates.

Pro Tip. Angular Elements are powered by Custom Elements.

Watch out. Web Components specification has moved from V0 to V1.

Extend the HTML with v1 Custom Elements

Let’s introduce an example to show how to implement a new custom element.


Custom Element. Web API that allow to extend the HTML tag system creating new custom DOM elements that encapsulate functinalities away from the rest of the code. It refers to encapsulation principle of the software engineering.

To define a new v1 Custom Element:

  • create a ES6 class extending HTMLElement;
  • register the new class with the browser via the CustomElementRegistry Web API: use [window.customElements] property of the Window interface to get an instance of it;
  • use lifecycle hooks or reactions
    • constructor: initialize the state, set up event listeners or create a Shadow DOM
    • connectedCallback: called when the element is attached to the DOM, useful for setting up, fetching some resources or rendering
    • disconnectedCallback: called when the element is removed from the DOM, useful for cleaning up
    • attributeChangedCallback(attrName, oldVal, newVal): called when an attribute registered in the observedAttributes property has been added, removed, updated or replaced
    • adoptedCallback(): called when the element has been moved into a new document (document.adoptNode(el))
    • more examples
  • events can be associated, as any other DOM element, using EventTarget method addEventListener.

Shadow DOM

Shadow DOM is more or less like a scoped subtree inside the Custom Element that helps to build a new DOM element.

class RocketParagraph extends HTMLElement {
  constructor() {

    super()

    const shadowRoot = this.attachShadow({ mode: 'open' })
    shadowRoot.innerHTML = `<link rel="stylesheet" href="fontawesome.com/all.css">`

    const span = document.createElement('span')
    const icon = document.createElement('i')

    ...

    shadowRoot.appendChild(span)
    shadowRoot.appendChild(icon)
  }
}
window.customElements.define('rocket-paragraph', RocketParagraph)

A Shadow Root is the document fragment that gets attached to a “host” element. The act of attaching a shadow root is how the element gains its shadow DOM.

Chrome debugger for Shadow DOM
Shadow Root in the Chrome debugger.

Building Libraries

There are many libraries that help to build custom Web Components providing features such as:

  • boilerplate to define the project scaffolding
    • Polymer Boilerplate
    • ValillaJS Boilerplate
    • etc
  • polyfills to have web components work in all major browsers
  • automatic build systems
    • Yeoman generator to automate scaffolding and building web components.
    • Polymer CLI to automate scaffolding and build Polymer-based web components.

Once created the component it can be published and the downloaded and installed via a package manager like Bower.

Look for a component, for instance emoji-rain, install and use it in your web site.

More examples are available on the Web Components Github MDN repo.

Angular Elements

Angular Component on the inside, standards on the outside. Rob Wormald

Pro Tip. Angular Elements are Angular Components packed as Custom Elements, part of the Web Components, a suite of Web platform APIs.

Angular Components act as a hosts for Custom Elements mapping properties from the Angular world to the Web Platform API standard:

  • @Input mapped to properties
  • @Output mapped to events
  • @HostBinding mapped to observed attributes
  • lifecycle hooks mapped to Custom Elements lifecycle hooks

Whereas Inputs rely on the Custom Elements lifecycle hook, Outputs rely on the Custom Event API (spec). Both APIs are entirely separate from each other.

Pro Tip. Angular Elements are self-bootstrapping, they host an Angular Component inside a Custom Element and their usage is not anymore tied to a specific framework.

Dependency Injection. Angular Elements also let us take advantage of Angular’s dependency injection. When we create a custom element using Angular Elements, we pass in a reference to the current module’s injector. This injector lets us share context across multiple elements or use shared services.

Content Projection. We also get content projection (transclusion) with Angular Elements, but not with dynamic content projection yet. Content projection we’ll have the ability to use slots and the shadow DOM as browser support increases.

Angular Elements: not production ready yet

Angular Elements is one of the Angular Labs projects where new experimental ideas are developed. The first release became part of Angular 6 and a more refined version should arrive along with Angular 7. For the time being Angular Elements is not ready for production yet:

  • element bundle size: even if a small widget is delivered a great amount of Angular code comes along with. Angular Yvi, next Angular compiler generation, should solve this problem removing from the bundle unused Angular code;
  • laborius setup: the set up required to use Angular Elements is a bit cumbersome, better way should come;
  • browser support: native browser support for Custom Elements has been not fully implemented everywhere as can be seen from this table. For the time being:

Angular Elements In Action

The code used in this post is available on GitHub. Following all the steps to reproduce the full workflow if you start from scratch.

Angular Elements Component Description

The Angular Elements component will allow the user to select a CSS shape and, once the button will be pressed, an alert message will be displayed with the info of the selection made.

The goal is to show how the input/output between an Angular Elements component and the vanilla JavaScript of the hosting page works. Basically the selection is captured by the component, the action connected with the submit button generates an event that communicate the selection made by the user to the external world.

The final result: the user can select the desired CSS shape and then press the button submit.

Setup the Project

npm install -g @angular/cli@latest
ng new angular-geometry-elements
cd angular-geometry-elements
ng add @angular/elements --project=angular-geometry-elements

The latest command add both Angular Elements and the polyfill called document-register-element to the configuration file angular.json.

Create the Component

Open and modify the file app.component.ts as follows:

import { Component, ViewEncapsulation, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  encapsulation: ViewEncapsulation.Native
})
export class AppComponent implements OnInit {

  @Input() defaultValue = 'square';

  @Output() selectedValue = new EventEmitter<string>();

  form: FormGroup;

  ngOnInit(): void {
    this.form = new FormGroup({
      shape: new FormControl(this.defaultValue)
    });
  }

  submit() {
    this.selectedValue.emit(this.form.value);
  }
}
  • ViewEncapulation: change the encapsulation strategy w.r.t. the styles are applied to the component:
    • Native: use the real Shadow DOM if natively available, otherwise the web-components polyfills are required to shim the behavior.
    • Emulated: scoped styling emulating the Shadow DOM.
  • @Input for a property with a default value.
  • Some other properties.

In the file app.module.ts rename bootstrap to entryComponents to avoid the component bootstrap along with the module:

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

import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { createCustomElement } from '@angular/elements';

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

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [],
  entryComponents: [AppComponent]
})
export class AppModule {

  // The bridge between Angular and the Web API.
  constructor(private injector: Injector) {
    const el = createCustomElement(AppComponent, {injector});
    customElements.define('geometry-elements', el);
  }

  ngDoBootstrap() {}

}

Now focus on the constructor of the module:

constructor(private injector: Injector) {
  const el = createCustomElement(AppComponent, {injector});
  customElements.define('geometry-elements', el);
}

this function returns a special class that can be used with the Custom Element Web API to define a new custom element as the pair selector and ES6 class.

Pro Tip. The Angular Component now act as a host for the Custom Element.

Build and Pack the Angular Element Component

So fare there is not a straight forward way to pack an Angular Element component, here some approaches proposed by Sam Julien:

  • Use a bash script, like in this article by Tomek Sułkowski.
  • Use Gulp, like in this made-with-love component by Nitay Neeman.
  • Use Manfred Steyer’s CLI tool ngx-build-plus.
  • Use Node with a script like in this article by Sebastian Eschweiler.

Following the Sam Julien’s advice the Node approach is choosen:

npm install --save-dev concat fs-extra

Create a file called elements-build.js at the root of the project with the following script:

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

(async function build() {
  const files = [
    './dist/angular-geometry-elements/runtime.js',
    './dist/angular-geometry-elements/polyfills.js',
    './dist/angular-geometry-elements/scripts.js',
    './dist/angular-geometry-elements/main.js'
  ];

  await fs.ensureDir('elements');
  await concat(files, 'elements/geometry-elements.js');
  await fs.copyFile(
    './dist/angular-geometry-elements/styles.css',
    'elements/styles.css'
  );
})();

The script will combine what the Angular CLI has generated in a single file.

In the package.json add

"build:elements": "ng build --prod --output-hashing none && node elements-build.js"

Then create the file index.html in the previously created folder elements to have a page where to test the new created component:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Angular Elements</title>
    <base href="/">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" type="image/x-icon" href="favicon.ico">
    <link rel="stylesheet" href="styles.css"></head>
  </head>
  <body>
    <geometry-elements id="el" default-value="circle"></geometry-elements>
    <script type="text/javascript" src="geometry-elements.js"></script>
    <script>
      document.getElementById("el").addEventListener("selectedValue", function(value){
        alert("Output from the component " + JSON.stringify(value.detail));
      })
    </script>
  </body>
</html>

Fireup the build

npm run build:elements

The output of the build is put in the elements folder:

.
├── angular-geometry-elements.js
├── index.html
└── styles.css

Note. Style file is empty because of the Native strategy choice.

Test The Angular Elements Component

Install the static-server to serve the elements/index.html

npm install -g static-server

From the afrementioned folder type

static-server

and connect to localhost:9080.

Check The Registered Element

Following this advice, in the browser console type:

String.prototype.wasRegistered = function() { 
  switch(document.createElement(this).constructor) {
    case HTMLElement: return false; 
    case HTMLUnknownElement: return undefined; 
  }
  return true;
}
'element-cake'.wasRegistered()
//⇒ true
'element-my-cake'.wasRegistered()
//⇒ false
'xx'.wasRegistered()
//⇒ undefined

Browser Support

Recent News