Skip to content

guide angular elements

devonfw-core edited this page Jan 5, 2022 · 11 revisions

Angular Elements

What are Angular Elements?

Angular elements are Angular components packaged as custom elements, a web standard for defining new HTML elements in a framework-agnostic way.

Custom elements are a Web Platform feature currently supported by Chrome, Firefox, Opera, and Safari, and available in other browsers through Polyfills. A custom element extends HTML by allowing you to define a tag whose content is created and controlled by JavaScript code. The browser maintains a CustomElementRegistry of defined custom elements (also called Web Components), which maps an instantiable JavaScript class to an HTML tag.

Why use Angular Elements?

Angular Elements allows Angular to work with different frameworks by using input and output elements. This allows Angular to work with many different frameworks if needed. This is an ideal situation if a slow transformation of an application to Angular is needed or some Angular needs to be added in other web applications(For example. ASP.net, JSP etc )

Negative points about Elements

Angular Elements is really powerful but since, the transition between views is going to be handled by another framework or HTML/JavaScript, using Angular Router is not possible. the view transitions have to be handled manually. This fact also eliminates the possibility of just porting an application completely.

How to use Angular Elements?

In a generalized way, a simple Angular component could be transformed to an Angular Element with this steps:

Installing Angular Elements

The first step is going to be install the library using our preferred packet manager:

NPM
npm install @angular/elements
YARN
yarn add @angular/elements

Preparing the components in the modules

Inside the app.module.ts, in addition to the normal declaration of the components inside declarations, the modules inside imports and the services inside providers, the components need to added in entryComponents. If there are components that have their own module, the same logic is going to be applied for them, only adding in the app.module.ts the components that do not have their own module. Here is an example of this:

....
@NgModule({
  declarations: [
    DishFormComponent,
    DishViewComponent
  ],
  imports: [
    CoreModule,  // Module containing Angular Materials
    FormsModule
  ],
  entryComponents: [
    DishFormComponent,
    DishViewComponent
  ],
  providers: [DishShareService]
})
....

After that is done, the constructor of the module is going to be modified to use injector and bootstrap the application defining the components. This is going to allow the Angular Element to get the injections and to define a component tag that will be used later:

....
})
export class AppModule {
  constructor(private injector: Injector) {

  }

  ngDoBootstrap() {
    const el = createCustomElement(DishFormComponent, {injector: this.injector});
    customElements.define('dish-form', el);

    const elView = createCustomElement(DishViewComponent, {injector: this.injector});
    customElements.define('dish-view', elView);
  }
}
....

A component example

In order to be able to use a component, @Input() and @Output() variables are used. These variables are going to be the ones that will allow the Angular Element to communicate with the framework/JavaScript:

Component html

<mat-card>
    <mat-grid-list cols="1" rowHeight="100px" rowWidth="50%">
				<mat-grid-tile colspan="1" rowspan="1">
					<span>{{ platename }}</span>
				</mat-grid-tile>
				<form (ngSubmit)="onSubmit(dishForm)" #dishForm="ngForm">
					<mat-grid-tile colspan="1" rowspan="1">
						<mat-form-field>
							<input matInput placeholder="Name" name="name" [(ngModel)]="dish.name">
						</mat-form-field>
					</mat-grid-tile>
					<mat-grid-tile colspan="1" rowspan="1">
						<mat-form-field>
							<textarea matInput placeholder="Description" name="description" [(ngModel)]="dish.description"></textarea>
						</mat-form-field>
					</mat-grid-tile>
					<mat-grid-tile colspan="1" rowspan="1">
						<button mat-raised-button color="primary" type="submit">Submit</button>
					</mat-grid-tile>
				</form>
		</mat-grid-list>
</mat-card>

Component ts

@Component({
  templateUrl: './dish-form.component.html',
  styleUrls: ['./dish-form.component.scss']
})
export class DishFormComponent implements OnInit {

  @Input() platename;

  @Input() platedescription;

  @Output()
  submitDishEvent = new EventEmitter();

  submitted = false;
  dish = {name: '', description: ''};

  constructor(public dishShareService: DishShareService) { }

  ngOnInit() {
    this.dish.name = this.platename;
    this.dish.description = this.platedescription;
  }

  onSubmit(dishForm: NgForm): void {
    this.dishShareService.createDish(dishForm.value.name, dishForm.value.description);
    this.submitDishEvent.emit('dishSubmited');
  }

}

In this file there are definitions of multiple variables that will be used as input and output. Since the input variables are going to be used directly by html, only lowercase and underscore strategies can be used for them. On the onSubmit(dishForm: NgForm) a service is used to pass this variables to another component. Finally, as a last thing, the selector inside @Component has been removed since a tag that will be used dynamically was already defined in the last step.

Solving the error

In order to be able to use this Angular Element a Polyfills/Browser support related error needs to solved. This error can be solved in two ways:

Changing the target

One solution is to change the target in tsconfig.json to es2015. This might not be doable for every application since maybe a specific target is required.

Installing Polyfaces

Another solution is to use AutoPollyfill. In order to do so, the library is going to be installed with a packet manager:

Yarn

yarn add @webcomponents/webcomponentsjs

Npm

npm install @webcomponents/webcomponentsjs

After the packet manager has finished, inside the src folder a new file polyfills.ts is found. To solve the error, importing the corresponding adapter (custom-elements-es5-adapter.js) is necessary:

....
/***************************************************************************************************
 * APPLICATION IMPORTS
 */

import '@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js';
....

If you want to learn more about polyfills in angular you can do it here

Building the Angular Element

First, before building the Angular Element, every element inside that app component except the module need to be removed. After that, a bash script is created in the root folder,. This script will allow to put every necessary file into a JS.

ng build "projectName" --configuration production --output-hashing=none && cat dist/"projectName"/runtime.js dist/"projectName"/polyfills.js dist/"projectName"/scripts.js dist/"projectName"/main.js > ./dist/"projectName"/"nameWantedAngularElement".js

After executing the bash script, it will generate inside the path dist/"projectName" (or dist/apps/projectname in a Nx workspace) a JS file named "nameWantedAngularElement".js and a css file.

The library ngx-build-plus allows to add different options when building. In addition, it solves some errors that will occur when trying to use multiple angular elements in an application. In order to use it, yarn or npm can be used:

Yarn

yarn add ngx-build-plus

Npm

npm install ngx-build-plus

If you want to add it to a specific sub project in your projects folder, use the --project:

.... ngx-build-plus --project "project-name"

Using this library and the following command, an isolated Angular Element which won’t have conflict with others can be generated. This Angular Element will not have a polyfill so, the project where we use them will need to include a poliyfill with the Angular Element requirements.

ng build "projectName" --output-hashing none --single-bundle true --configuration production --bundle-styles false

This command will generate three things:

  1. The main JS bundle

  2. The script JS

  3. The css

These files will be used later instead of the single JS generated in the last step.

Extra parameters

Here are some extra useful parameters that ngx-build-plus provides:

  • --keep-polyfills: This parameter is going to allow us to keep the polyfills. This needs to be used with caution, avoiding using multiple different polyfills that could cause an error is necessary.

  • --extraWebpackConfig webpack.extra.js: This parameter allows us to create a JavaScript file inside our Angular Elements project with the name of different libraries. Using webpack these libraries will not be included in the Angular Element. This is useful to lower the size of our Angular Element by removing libraries shared. Example:

const webpack = require('webpack');

module.exports = {
    "externals": {
        "rxjs": "rxjs",
        "@angular/core": "ng.core",
        "@angular/common": "ng.common",
        "@angular/common/http": "ng.common.http",
        "@angular/platform-browser": "ng.platformBrowser",
        "@angular/platform-browser-dynamic": "ng.platformBrowserDynamic",
        "@angular/compiler": "ng.compiler",
        "@angular/elements": "ng.elements",
        "@angular/router": "ng.router",
        "@angular/forms": "ng.forms"
    }
}
ℹ️
If some libraries are excluded from the `Angular Element` you will need to add the bundled UMD files of those libraries manually.

Using the Angular Element

The Angular Element that got generated in the last step can be used in almost every framework. In this case, the Angular Element is going to be used in html:

Listing 1. Sample index.html version without ngx-build-plus
<html>
    <head>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>
        <div id="container">

        </div>
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
        <script src="./devon4ngAngularElements.js"> </script>
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
    </body>
</html>
Listing 2. Sample index.html version with ngx-build-plus
<html>
    <head>
        <link rel="stylesheet" href="styles.css">
    </head>
    <body>
        <div id="container">

        </div>
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
         <script src="./polyfills.js"> </script> <!-- Created using --keep-polyfills options -->
        <script src="./scripts.js"> </script>
         <script src="./main.js"> </script>
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
    </body>
</html>

In this html, the css generated in the last step is going to be imported inside the <head> and then, the JavaScript element is going to be imported at the end of the body. After that is done, There is two uses of Angular Elements in the html, one directly with use of the @input() variables as parameters commented in the html:

....
        <!--Use of the element non dynamically-->
        <!--<plate-form platename="test" platedescription="test"></plate-form>-->
....

and one dynamically inside the script:

....
        <script>
                var elContainer = document.getElementById('container');
                var el= document.createElement('dish-form');
                el.setAttribute('platename','test');
                el.setAttribute('platedescription','test');
                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });
                elContainer.appendChild(el);
        </script>
....

This JavaScript is an example of how to create dynamically an Angular Element inserting attributed to fill our @Input() variables and listen to the @Output() that was defined earlier. This is done with:

                el.addEventListener('submitDishEvent',(ev)=>{
                    var elView= document.createElement('dish-view');
                    elContainer.innerHTML = '';
                    elContainer.appendChild(elView);
                });

This allows JavaScript to hook with the @Output() event emitter that was defined. When this event gets called, another component that was defined gets inserted dynamically.

Angular Element within another Angular project

In order to use an Angular Element within another Angular project the following steps need to be followed:

Copy bundled script and css to resources

First copy the generated .js and .css inside assets in the corresponding folder.

Add bundled script to angular.json

Inside angular.json both of the files that were copied in the last step are going to be included. This will be done both, in test and in build. Including it on the test, will allow to perform unitary tests.

{
....
  "architect": {
    ....
    "build": {
      ....
      "styles": [
        ....
          "src/assets/css/devon4ngAngularElements.css"
        ....
      ]
      ....
      "scripts": [
        "src/assets/js/devon4ngAngularElements.js"
      ]
      ....
    }
    ....
    "test": {
      ....
      "styles": [
        ....
          "src/assets/css/devon4ngAngularElements.css"
        ....
      ]
      ....
      "scripts": [
        "src/assets/js/devon4ngAngularElements.js"
      ]
      ....
    }
  }
}

By declaring the files in the angular.json angular will take care of including them in a proper way.

ℹ️
If you are using Nx, the configuration file `angular.json` might be named as `workspace.json`, depending on how you had setup the workspace. The structure of the file remains similar though.

Using Angular Element

There are two ways that Angular Element can be used:

Create component dynamically

In order to add the component in a dynamic way, first adding a container is necessary:

app.component.html

....
<div id="container">
</div>
....

With this container created, inside the app.component.ts a method is going to be created. This method is going to find the container, create the dynamic element and append it into the container.

app.component.ts

export class AppComponent implements OnInit {
  ....
  ngOnInit(): void {
    this.createComponent();
  }
  ....
  createComponent(): void {
    const container = document.getElementById('container');
    const component = document.createElement('dish-form');
    container.appendChild(component);
  }
  ....
Using it directly

In order to use it directly on the templates, in the app.module.ts the CUSTOM_ELEMENTS_SCHEMA needs to be added:

....
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
....
@NgModule({
  ....
  schemas: [ CUSTOM_ELEMENTS_SCHEMA ],

This is going to allow the use of the Angular Element in the templates directly:

app.component.html

....
<div id="container">
  <dish-form></dish-form>
</div>

You can find a working example of Angular Elements in our devon4ts-samples repo by referring the samples named angular-elements and angular-elements-test.

Clone this wiki locally