TL;DR: I’m trying to use Angular Elements as plugins for an Angular application. If I build the element with --prod
it works with ng serve
on my app (development setup), but it goes into infinite reloading when I use it with ng serve --prod
on my app or after the ng build --prod
of my app (production setup).
Though, if I build the element adding --optimization=false
, works with my productive app, but not in my development setup.
The thing is, I was expecting that building an Angular Element with --prod
would be fine for both cases.
Question: Is there a way to solve this?
Now, the long read.
At work we are trying to use configurable plugins in our Angular site where the server is the one that tells which plugin is active or not.
We tried to load Angular modules dynamically, but that's a totally different headache we pushed aside for another day.
So, the next thing we wanted to try was Angular Elements and it kinda works, unless we build everything the way it should.
First, I started following this tutorial https://scotch.io/tutorials/build-a-reusable-component-with-angular-elements/amp and ignored everything about okta
because my functionality is something different.
Creation:
I created my core application using the next command, this will be the application that hosts plugins:
ng new core --routing --skip-git --style scss --skip-tests --minimal
Then I created a plugin/angular-element using this command:
ng new plugin --skip-git --style scss --skip-tests --minimal
Plugin:
After all the creation I went into my plugin and commented this line in polyfills.ts
, I read somewhere in this site that it solve the problem of NgZone
already loaded and it was true:
// import 'zone.js/dist/zone'; // Included with Angular CLI.
In tsconfig.json
I changed "target": "es5"
to "target": "es2015"
in order to fix an issue with how Angular creates elements. Not so sure how this works, but stackoverflow suggested it and it did the trick.
I edited the app.module.ts
into something like this following ideas from the tutorial and some other to which I lost the link:
import { BrowserModule } from '@angular/platform-browser';
import { CUSTOM_ELEMENTS_SCHEMA, Injector, NgModule } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
],
providers: [
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,
],
entryComponents: [
AppComponent,
],
})
export class AppModule {
constructor(private injector: Injector) {
const elem = createCustomElement(AppComponent, { injector: this.injector });
customElements.define('my-plugin', elem);
}
ngDoBootstrap() {
}
}
Note: I added CUSTOM_ELEMENTS_SCHEMA
'cause I found it somewhere, but it didn't solve it (also, I'm not sure what it does).
In the app.component.ts
I did this to have some property to show in its template:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
public content: any = {
a: 10,
b: '20',
}
}
And the app.component.html
looks like this:
Some content:
<pre>{{content | json}}</pre>
In the package.json
file I have three script to build all:
{
"scripts": {
"build": "npm run build:opt && npm run build:noopt",
"build:opt": "ng build --prod --output-hashing none && node build-elements.js",
"build:noopt": "ng build --prod --output-hashing none --optimization=false && node build-elements.noopt.js"
}
}
The file build-elements.js
looks like this (build-elements.noopt.js
is the same with a different destination name):
'use strict';
const concat = require('concat');
const fs = require('fs-extra');
const path = require('path');
(async function build() {
const files = [
'./dist/plugin/runtime.js',
'./dist/plugin/polyfills.js',
'./dist/plugin/scripts.js',
'./dist/plugin/main.js',
];
const destinationDir = path.join(__dirname, '../core/src/assets');
await fs.ensureDir(destinationDir);
await concat(files, path.join(destinationDir, 'my-plugin.js'));
})();
Core:
For the host app I added a component called embedded
and the default route goes to it.
Then I changed the embedded.component.html
into something like this using some Bootstrap classes:
<div id="embedded-container" class="container border border-primary rounded mt-5 p-3" #container>
<pre>Loading...</pre>
</div>
Finally, the embedded.component.ts
ended up like this to show the actual loading mechanism:
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Router, ActivatedRoute, Params } from '@angular/router';
import { environment } from '../../environments/environment';
@Component({
selector: 'app-embedded',
templateUrl: './embedded.component.html',
})
export class EmbeddedComponent implements OnInit {
@ViewChild('container') public container: ElementRef;
constructor(protected activatedRoute: ActivatedRoute) {
}
ngOnInit() {
this.activatedRoute.queryParams.subscribe((params: Params) => {
const script = document.createElement('script');
if (params['how-it-should-be'] !== undefined) {
script.src = environment.production ? '/assets/my-plugin.js' : '/assets/my-plugin-no-optimization.js';
} else {
script.src = environment.production ? '/assets/my-plugin-no-optimization.js' : '/assets/my-plugin.js';
}
document.body.appendChild(script);
const div = document.createElement('div');
div.innerHTML = '<my-plugin></my-plugin>';
this.container.nativeElement.innerHTML = '';
this.container.nativeElement.appendChild(div);
});
}
}
Running:
If I run ng serve
and browse to http://localhost:4200
, the page loads without problem, injects the plugin, adds the new element to the DOM and displays the message from my plugin.
And if you debug the application you'll see that it loads /assets/my-plugin.js
, the one built for production. This wouldn't be a problem except maybe for debugging.
Then, If I run ng serve --prod
(or build it for production) it also works fine, but it loads /assets/my-plugin-no-optimization.js
, the one built for "debugging".
This is the solution I ended up using on our real application, but as you can see, I'm not using optimized code in my plugin for production and that's not nice... at all.
To prove my point, if I browse to http://localhost:4200/?how-it-should-be
, it will try to load the optimized plugin for ng serve --prod
and the debugging one for ng serve
. Be aware that this will put you into infinite reload, open you browser developer tools to see it.
The final product we are using is much more complex, but these examples have the basic logic that's not really working.
I also created a GitHub repository with this where you can see these codes, and try the problem yourselves, or use as example for your own ideas.
If you're wondering how I found out that using --optimization=false
kinda fixed it, well, I was trying to debug this problem (which proved to be impossible) and suddenly it loaded.
I looked at the time and I was two hours too late for a production deployment, so I added that ugly mechanism to load different build depending on the environment. It's working, both in development and production, but I don't feel proud about it.
Sorry if my English is bad... no no no, my English IS bad, sorry ^__^