Log In

Angular Web Components


by Yariv Katz

In angular we are building components that expend the angular templating language and we can use this fact to create new html tags that will map to our components. Can we use the new tags we are creating outside the angular environment. What if we want a tag to use in a regular application with no angular in it. In this article we will demonstrate how to create a web component from our angular component. A component we can embed in a web app regardless of the framework we are using.

What are web components

Web components are custom html tags we are creating. Those tags can encapsulate a full template in them, they can recieve attributes, and have an encapsulated logic in them. Let's create a web component that contains a login form. What we want to achieve is that when we place the tag <nz-login></nz-login> a full login form will be displayed with an email input and a password input. When creating our web component we will first create the template for that component, then we will create class of the component and then register that class. We will start by creating the index.html file holding the template for our component.

<html>
    <head>
        <title>Web Components</title>        
    </head>
    <body>
        <nz-login></nz-login>

        <template id="login-template">
            <form>
                <div class="form-group">
                    <label>Email</label>
                    <input type="email" name="email" class="form-control" />
                </div>
                <div class="form-group">
                    <label>Password</label>
                    <input type="password" name="password" class="form-control" />
                </div>
                <div class="form-group">
                    <button type="submit">Login</button>
                </div>
            </form>
        </template>

        <script src="login.js" ></script>
    </body>
</html>

We are loading a js file called login.js which we will contain soon and will contain the logic of our web component. Also note the template tag, this template tag represent the visual part of our component, placing a template tag in an html file will not display that dom elements, you can consider this as similar to create a template for our comopnent in angular. We also place the tag nz-login which will be translated to our login form. Let's create the login.js file:

class Login extends HTMLElement {
    constructor() {
        super();
        const template = document.getElementById('login-template');
        this.attachShadow({mode: 'open'}).appendChild(template.content.cloneNode(true));
    }
}

customElements.define('nz-login', Login);

Our web component is a class which attach the template in the dom. note that the shadow dom can be attached in a mode open or closed which determines if the dom is accessible from outside the element we are creating. Launching the index.html you will see the login form in our browser. Now let's extend our component and add an event for the submition of the form. In the event we will display an alert if the login is successful or not. Modify the Login class:

class Login extends HTMLElement {
    constructor() {
        super();
        const template = document.getElementById('login-template');
        this.attachShadow({mode: 'open'}).appendChild(template.content.cloneNode(true));

        const form = this.shadowRoot.querySelector('form');
        form.addEventListener('submit', this.formSubmit.bind(this));
    }

    formSubmit(event) {
        const emailInput = this.shadowRoot.querySelector("input[name='email']");
        const passwordInput = this.shadowRoot.querySelector("input[name='password']");
        const email = emailInput.value;
        const password = passwordInput.value;
        
        if (email === 'info@nerdeez.com' && password === '12345678') {
            alert('success');
        } else {
            alert('fail');
        }
        event.preventDefault();
    }
}

note that we are grabbing the form element and placing the login of the login in the component. This make sure that we encapsulate the login of the component in itself which makes this entire component more reusable. Let's say our component is sending a request to a server with the login credentials. We want this component to be reusable and to get the url of the server in the component attributes. Modify the index.html change the nz-login tag to this:

<nz-login server-url="https://nztodo.herokuapp.com/api/login/"></nz-login>

Now let's try and grab the attribute in the js class.

class Login extends HTMLElement {
    constructor() {
        super();
        const template = document.getElementById('login-template');
        this.attachShadow({mode: 'open'}).appendChild(template.content.cloneNode(true));

        const form = this.shadowRoot.querySelector('form');
        form.addEventListener('submit', this.formSubmit.bind(this));

        this.serverUrl = 'https://some-default-value/api/login/';
        if (this.hasAttribute('server-url')) {
            this.serverUrl = this.getAttribute('server-url');
        }
    }

    formSubmit(event) {
        const emailInput = this.shadowRoot.querySelector("input[name='email']");
        const passwordInput = this.shadowRoot.querySelector("input[name='password']");
        const email = emailInput.value;
        const password = passwordInput.value;
        
        console.log(`sending request to: ${this.serverUrl}`);
        event.preventDefault();
    }
}

customElements.define('nz-login', Login);

this kinds of reminds us of using @Input in our angular components. we get the attributes with this.getAttribute. Now let's say we want to call an event when the login is a success, kind of like we want to create an @Output and an option to define an event for our element where whomever uses our tag can connect to that event. Modify the js file formSubmit event:

formSubmit(event) {
    const emailInput = this.shadowRoot.querySelector("input[name='email']");
    const passwordInput = this.shadowRoot.querySelector("input[name='password']");
    const email = emailInput.value;
    const password = passwordInput.value;
    
    console.log(`sending request to: ${this.serverUrl}`);
    this.dispatchEvent( new CustomEvent('success'))
    event.preventDefault();
}

and now to connect to that event we can do:

document.getElementsByTagName('nz-login')[0].addEventListener('success', () => {
    console.log('connecting to custom events');
})

Similar to the @Output in angular we can define events that our components sends out.

Creating web components with angular

Our goal now is to create the login component we created before with angular. We want to use that component regardless if we have angular in our page or not, just use it as a vanilla web component. This means wrapping angular in our js file and distributing everything in our js file. Create a new angular cli project:

> npx @angular/cli new ng-web-component

Create a new login component using the cli

> ng g c Login

To wrap our component as a web component we will need to install @angular/elements. We will also need to install certain polyfills in order for our component to be supported in older browsers. We can add @angular/elements with angular cli:

> ng add @angular/elements

let's create our login component, similar to the login component we created before. Modify the file login.component.ts:

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
    selector: 'nz-login',
    template: `
        <form (ngSubmit)="formSubmit()">
        <div class="form-group">
            <label>Email</label>
            <input [(ngModel)]="email" type="email" name="email" class="form-control" />
        </div>
        <div class="form-group">
            <label>Password</label>
            <input [(ngModel)]="password" type="password" name="password" class="form-control" />
        </div>
        <div class="form-group">
            <button type="submit">Login</button>
        </div>
        </form>
    `
})
export class LoginComponent {
    @Input() serverUrl = 'https://some-default-value/api/login/';
    @Output() success : EventEmitter<void> = new EventEmitter();

    email : string;
    password : string;

    formSubmit = () => {
        console.log(`sending request to: ${this.serverUrl}`);
        this.success.emit();
    }
}

We created similar component to the login before. We are getting here an input of serverUrl. We are defining an event called success when the logins happens. To get the submit event we are using ngSubmit directive. To get the email and password we are using 2 way binding with ngModel. For ngSubmit and ngModel we will have to add the FormsModule. To create the component we just made as a web component we will also need to add the component as an entryComponent in our module. Modify the file app.module.ts

import { NgModule, Injector } from '@angular/core';
import { LoginComponent } from './login.component';
import { FormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { createCustomElement } from '@angular/elements';

@NgModule({
    declarations: [LoginComponent],
    imports: [
        BrowserModule,
        FormsModule
    ],
    exports: [LoginComponent],
    entryComponents: [LoginComponent]
})
export class AppModule { 
    constructor(injector: Injector) {
        const Login = createCustomElement(LoginComponent, {injector});
        customElements.define('nz-login', Login)
    }

    ngDoBootstrap() {}
}

Our module when creating will create a custom element from our login component. We need to also pass the injector when creating the custom element so the injected services could be resolved. we then call the browser customElements to define a new tag for our element. We also need our module to dynamically bootstrap so the constructor will be called, so we can just implement an empty ngDoBootstrap. Now we need to build our application with angular cli:

> ng build --prod --output-hashing=none

You can now grab all the js files in the dist folder and create the following index.html

<html>
    <head>
        <title>Web Components</title>   
        <script src="runtime.js" ></script>
        <script src="polyfills.js" ></script>
        <script src="scripts.js" ></script>
        <script src="main.js" ></script>     
    </head>
    <body>
        <nz-login server-url="https://nztodo.herokuapp.com/api/login/"></nz-login>
    </body>
</html>

We included all the scripts here and when we run this index.html we will see our login form. Entire angular app is wrapped in a web component completly framework agnostic.

Summary

The use case for this feature is quite common, you no longer need to worry about creating all your apps in angular, you can still implement an angular component and re use it in other apps no matter what framework you are using. Also if you have a big app and you don't want to rebuild and publish it every time this is a good method to split you angular app to multiple apps where every one is maintained and deployed on it's own.