Contribute to help us improve!
Are there edge cases or problems that we didn't consider? Is there a technical pitfall that we should add? Did we miss a comma in a sentence?
If you have any input for us, we would love to hear from you and appreciate every contribution. Our goal is to learn from projects for projects such that nobody has to reinvent the wheel.
Let's collect our experiences together to make room to explore the novel!
To contribute click on Contribute to this page on the toolbar.
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:
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.
Building with ngx-build-plus
(Recommended)
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:
-
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.
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 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. |
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:
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>
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.