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.
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 )
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.
In a generalized way, a simple Angular component
could be transformed to an Angular Element
with this steps:
The first step is going to be install the library using our prefered packet manager:
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);
}
}
....
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.
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:
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.
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
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"/scripts.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.
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 --prod --bundle-styles false
This command will generate three things:
-
The main js bundle
-
The script js
-
The css
These files will be used later instead of the single js generated in the last step.
Here are some extra useful parameters that ngx-build-plus
provides:
-
--keep-polyfills
: This paremeter 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 ourAngular Elements
project with the name of different libraries. Usingwebpack
these libraries will not be included in theAngular Element
. This is useful to lower the size of ourAngular 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. |
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>
<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 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.
In order to use an Angular Element
within another Angular
project the following steps need to be followed:
First copy the generated .js
and .css
inside assets in the corresponding folder.
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.
There are two ways that Angular Element
can be used:
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);
}
....
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>