Creation of Custom Web Components with Angular 6 Elements

One of the introductory innovations, which pleased Angular 6, was the package of Angular Elements. The usage of this package, will help you to easily create reusable native HTML elements with custom functionality that can be used in any environment - like React, Vue, native JavaScript, etc.

The main features of the functioning of custom Web components, generated with the help of Angular Elements:

  1. All of them are self-bootstrapped;

  2. Processing Angular application inside the custom web element;

  3. The connecting link between the DOM API and Angular Component;

  4. Can be used by developers who do not have experience with the Angular framework.

So, let's start creating the simple web component. Let it be a button that accepts a label as a parameter and returns the total number of clicks on a click event. The message about the total number of clicks will be output to the console.

Create a new project with Angular CLI (install it globally if required)...

ng new angular-custom-button

...and package Angular Elements

ng add @angular/elements

Next, create a new component - the Custom Button. For convenience, we will not create separate files for the template and component styles. In addition, in the case of "ViewEncapsulation.Native", the styles, template, and class of the component will be placed by the builder in one file.

ng g component custom-button --inline-style
--inline-template -v Native

Change custom-button.component.ts code:

import { Component, EventEmitter, Input, OnInit, Output, ViewEncapsulation } from '@angular/core';
@Component({
  selector: 'app-custom-button',
  template: `
        <button (click)="clickHandler()">{{ label }}</button>
    `,
  styles: [`
    button {
      min-width: 120px;
      min-height: 40px;
      font-size: 22px;
      outline: none;
      border: none;
      background-color: #3F92D2;
      color: #FFF;
      cursor: pointer;
      transition: all .2s ease-out;
    }
    button:hover {
      color: #FFCF40;
    }
  `],
  encapsulation: ViewEncapsulation.Native
})
export class CustomButtonComponent implements OnInit {
  counter: number;
  @Input() label: string;
  @Output() onButtonClick = new EventEmitter<number>();
  ngOnInit() {
    this.counter = 0;
  }
  clickHandler(): void {
    this.counter++;
    this.onButtonClick.emit(this.counter);
  }
}

The component template is represented by a button with a label that is an incoming parameter. By tapping the button, the value of the counter is increased by 1 and an output event is emitting the current value of the counter. We also added some styles to display the button.

Now the most important things. As the Custom Button component is not directly used in our application and is not a root component, we need to add it to the root module entryComponents array. In turn, the App component is no longer needed, and we can remove it completely, including from the declarations array of the root module.

rm -rf src/app/app.component.ts src/app/app.component.css
src/app/app.component.html
src/app/app.component.spec.ts

To create the custom element itself, we have to use the createCustomElement method from @angular/elements package, and also Injector from @angular/core. In order for Angular could use this module for bootstrapping, we should declare its method ngDoBootstrap.

@NgModule({
  declarations: [CustomButtonComponent],
  imports: [BrowserModule],
  entryComponents: [CustomButtonComponent]
})
export class AppModule {
  constructor(private _injector: Injector) {}
  ngDoBootstrap() {
    const element = createCustomElement(CustomButtonComponent, {injector: this._injector});
    customElements.define('custom-button', element);
  }
}

Inside the ngDoBootstrap method of App module, we call the imported createCustomElement method in order to create a new custom element, passing the Angular component (namely our Custom Button component) to it, and the object with options, which has the injector property with the current injector entity. Next, using the customElements.define (the method of window object), we register the created component in the browser.

This method takes two parameters:

  1. The selector of the custom component;

  2. The custom element itself.

Since Custom Elements are still limitedly supported by browsers, we need to connect the polyfills.

npm i @webcomponents/custom-elements --save

src/polyfills.ts

import
'@webcomponents/custom-elements/src/native-shim';
import
'@webcomponents/custom-elements/custom-elements.min';

This concludes the development of the custom component and we begin to configure its building process. The logic of it is: when the project is built, 4 compiled files appear in the distribution directory: runtime.js, scripts.js, polyfills.js, main.js. For ease of distribution, all these files must be concatenated into one. To test the operation of our custom component, you need a simple web document, in which we connect a script with the component code. To do this, create a testing directory and add an index.html to it.

mkdir testing
touch testing/index.html

testing/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Angular Elements - Button</title>
</head>
<body>
<custom-button label="Counter"></custom-button>
<script type="text/javascript" src="./custom-button.js"></script>
<script>
    const button = document.querySelector('custom-button');
    button.addEventListener('onButtonClick', (event) => {
        console.log(`Custom button output - ${event.detail}`);
    });
</script>
</body>
</html>

Next, we add a script to package.json to build the project with the concatenation of the files we need. In addition to the assembly, we need a local server to test the work of the custom component in the test document environment. Within the framework of this demonstration project, http-server is used.

"build:element": "ng build --prod
--output-hashing=none && cat
dist/angular-custom-button/{runtime,polyfills,scripts
,main}.js > ./testing/custom-button.js",
"serve": "http-server ./testing"

After the project is successfully built (npm run build:element), the .js file of our component appears in the testing directory. After executing the command "npm run serve" in the browser, we will see our test document with the custom component. Using Chrome Dev Tools, we can see that inside our web component there is a Shadow DOM node, inside which the button and its styles are located. Shadow DOM encapsulates the styles of the custom component within itself, so they do not affect the styles of the other document elements.

Conclusion

Today we looked at the process of developing a custom web component using a new feature from the Angular team - the Angular Element package. As an example, an Angular component with an input parameter and an output event emitting was used. As a result, we got a web component whose internal Angular environment is isolated from the external document and interacts with it through the described incoming and outgoing parameters. In fact, Angular Elements make it easy to modify applications for work outside the Angular environment.

In case, you got left any questions or suggestions, feel free to contact us and we will assist you in any inquiry!