Shipping Angular Elements as Web Components

Angular elements are Angular components packaged as custom elements (also called Web Components), a web standard for defining new HTML elements in a framework-agnostic way. - Angular.io


Shipping Angular Components as Web Components is easy as pie. You just have to add the @angular/elements package and use the createCustomElement() API.

ng add @angular/elements

Next up is to create an actual component. It's a subscription component and somewhat configurable. Users can enter their email addresses and submit to get registered with a subscription plan.

The following goes into the script file i.e. the component file,

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { SubscriptionService } from '../shared/subscriber.service';

@Component({
  selector: 'app-subscriber',
  templateUrl: './subscriber.component.html',
  styleUrls: ['./subscriber.component.scss']
})
export class SubscriberComponent implements OnInit {
  @Input() url: string;
  @Output() subscribed: EventEmitter<string> = new EventEmitter<string>();

  subscriptionForm: FormGroup;

  get email() {
    return this.subscriptionForm.get('email');
  }

  constructor(
    private fb: FormBuilder,
    private subscriptionSvc: SubscriptionService
  ) {}

  ngOnInit() {
    this.createForm();
  }

  createForm() {
    this.subscriptionForm = this.fb.group(
      {
        email: ['', [Validators.required, Validators.email]]
      },
      { updateOn: 'submit' }
    );
  }

  onSubmit() {
    if (this.subscriptionForm.status === 'VALID') {
      this.subscriptionSvc
        .register(this.url, this.email.value)
        .subscribe(res => this.subscribed.emit(res));
    } else {
      this.subscriptionForm.updateValueAndValidity();
    }
  }
}
subscriber.component.ts

And the markup is simple as this,

<div class="card">
  <div class="card-content">
    <div class="content">
      <form [formGroup]="subscriptionForm" novalidate (ngSubmit)="onSubmit()">
        <div class="field is-grouped">
          <p class="control is-expanded">
            <input
              class="input is-light is-warning"
              [ngClass]="{
                'is-danger': email.invalid && (email.dirty || email.touched)
              }"
              type="text"
              placeholder="Your email address"
              id="email"
              formControlName="email"
            />
          </p>
          <p class="control">
            <input
              class="button is-warning is-light is-outlined"
              value="Subscribe"
              type="submit"
            />
          </p>
        </div>
      </form>
    </div>
  </div>
</div>
subscriber.component.html

Singleton service class is doing a mock delay request and returning a string containing a notification. You can use the HttpClient to make a post request to your preferred server. Of course, the URL is configurable here, and it can be set by the consumer of the component.

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class SubscriptionService {
  constructor() {}

  // Create a POST request using the url and register email in the backend
  register(url: string, email: string): Observable<string | undefined> {
    return of(
      `Please check your email (${email}) and confirm subscription`
    ).pipe(delay(500));
  }
}

The bridging of Angular component to web component happens in the app.module.ts.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';

import { SubscriberComponent } from './subscriber/subscriber.component';

@NgModule({
  declarations: [SubscriberComponent],
  imports: [BrowserModule, ReactiveFormsModule],
  providers: [],
  entryComponents: [SubscriberComponent]
})
export class AppModule {
  constructor(injector: Injector) {
    const SubscriberElement = createCustomElement(SubscriberComponent, {
      injector
    });

    customElements.define('subscriber-element', SubscriberElement);
  }

  ngDoBootstrap() {}
}
app.module.ts

Things to notice are:

  • How the createCustomeElement API is used to create a custom element.
  • customElements.define() is used to register the element to browser's CustomElementRegistry.
  • Components are registered using the entryComponents option so that they can be dynamically loaded into the view
  • ngDoBootstrap is used for manual bootstrapping of the application instead of using the bootstrap array.
Transforming a component to a custom element makes all of the required Angular infrastructure available to the browser - Angular.io

You can do a production build of the app and get the minified styles and scripts from the dist folder. You can reference these files in any other HTML project and start using the subscriber-element.

For quick testing, you can also use the index.html file that resides in your Angular app. The following markup shows how to use the element in plain HTML

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>NgPlayground</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>
    <div class="section">
      <div class="container">
        <h1 class="title">Subscription Form</h1>
        <h2 class="subtitle">
          A web component created with <code>@angular/elements</code> project
        </h2>
        <subscriber-element
          url="https://api.lorem.com/subscribe"
        ></subscriber-element>
      </div>
    </div>
    <div class="section">
      <div class="container">
        <h2 class="subtitle">Console</h2>
        <pre></pre>
      </div>
    </div>
    <script>
      var el = document.querySelector('subscriber-element');
      el.addEventListener('subscribed', evt => {
        const pre = document.querySelector('pre');
        pre.innerText += `Log: ${evt.detail}\n`;
      });
    </script>
  </body>
</html>

Repository:

https://github.com/fiyazbinhasan/ng-playground/tree/ng-web-components

fiyazbinhasan/ng-playground
Contribute to fiyazbinhasan/ng-playground development by creating an account on GitHub.

Links:

https://angular.io/api/core/NgModule#entryComponents

https://angular.io/api/elements/NgElementConfig

https://angular.io/guide/elements

https://angular.io/api/core/DoBootstrap

Image Upload in Angular with Preview & Progress
In Angular, you can upload a file and at the same time get the progress by enabling the reportProgress flag on HttpRequest object. Source code available in Github.
Angular Form Validation on Blur and Submit
updateOn: ‘blur’ and updateOn: ‘submit’ are the options we were really craving for while working with Angular Forms. Now it’s available out of the box and you don’t need any custom implementations.
Asynchronous Validation in Angular
It’s very easy to setup asynchronous validation in Angular Form. You can check wheather provided email/date/name etc. already exists in the server with a HTTP call.