Skip to content

guide angular elements

Cristian edited this page Feb 26, 2019 · 11 revisions

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 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 prefered 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 dont 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 boostrap 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 {
    console.log('SUBMIT');
    console.log(dishForm.value);
    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" --prod --output-hashing=none && cat dist/"projectName"/runtime.js dist/"projectName"/polyfills.js dist/"projectName"/script.js dist/"projectName"/main.js > ./dist/"projectName"/"nameWantedAngularElement".js

After executing the bash script, it will generate inside the path dist/"projectName" a js file named "nameWantedAngularElement".js and a css file.

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:

<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>

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 whith 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 we need to do the following steps:

Copy bundled script and css to resources

First we need to 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 we 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.

Using Angular Element

There are two ways that Angular Element can be used:

Create component dynamicly

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 we need to add CUSTOM_ELEMENTS_SCHEMA:

....
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>
Clone this wiki locally