To allow new developments to be made in Angular and used in the Plain-Old-Javascript V1 application, we’ve worked with something Angular calls “Elements”. Angular Elements builds as WebComponents, a standard package importable in any page and fully responsible for its rendering and internal logic.
Modular Design
As their goal is to be injected in a foreign application, the Elements must be very autonomous. This helped a lot to design actual modules, with very few dependencies. The first Elements created (Automatic Export Parameter Setting and Phyc Export) don’t even use the Store, which is too coupled to the rest of the whole Superviseur V2 application, and handles their own data privately. A way should be thought, in needed, to use light-weight version of the Store within the Elements, and for them to be able to exchange data with a Store, if any.
Development organization
The easiest way to develop-test-debug the Elements is to develop them directly within an Angular environment. To accelerate the process, we don’t add them to the whole Superviseur but instead create mini-development-applications. In addition to the files of the Element itself (contained in, for example, MyFeatureModule), we create/edit these files:
- angular.json: declaration of the new dev-app
- package.json: npm command to start the dev-app
- tsconfig.elements-dev.ts: adding the dev-app start file (note: there could also be one tsconfig file per AngularElement)
- dev-myFeature.ts: a bootstrap start file
- appMyFeature.module.ts: a module including MyFeatureModule but also some other modules we want to use within the Angular context, such as BootstrapModule and UserMessageModule.
This allows to test the application with ease in the Angular context and code most of the code.
When done, we want to build the WebComponents from the Angular Elements. Note that we’ll produce only one package containing all the Components we want to inject in the old application V1. To do so, we’ve created a new Angular project called ‘elements’ in angular.json with associated build commands in package.json. In src/elements, lie:
- elements.ts: the project start file
- elements.module.ts: in which we have to add our new Elements and define the new HTML tag for this one
- concatenate-elements.js: a handy script to move generated files and concatenate them in the output directory (script used within package.json commands)
- test.html: a simple plain-old page which imports the generated files, in which we want to add the new defined HTML tag for our Component
Note that test.html must be called from your web server (example: http://localhost/angular/src/elements/test.html) and not directly (not: file:///D:/Ceneau/angular/src/elements/test.html) for the API server to be reached properly !
With all that in mind, we’re set to go.
Inputs, Outputs
WebComponents can certainly handle Inputs and Outputs, just as an Angular Component can. But we’re in plain HTML, so inputs will simply given as simple HTML attribute. Note that they have to be kebab-case and not camelCase (and that the translation is automatically done under the hood by the generated WebComponent)! So if my Angular Component have an @Input() clientId: number, the HTML tag to summon it would be:
<sv-aeps-pilot-ccgst id=”aepspilot” client-id=”8″></sv-aeps-pilot-ccgst>
If your input must change, I’m not too sure the changing of the attribute will be detected by the Components… As an alternative, we can totally get a reference on the Components itself and call one of its methods:
var component = document.getElementById("aepspilot");
comment.clientId = 2;
Note that you must use camelCase now !
For the output, to subscribe to them from a plain-old page:
var node = document.getElementById('myFeature');
node.addEventListener("messageInfo", function(event) {
var message = event.detail;
// ...
});
By convention, our Elements have 3 output emitters ( EventEmitter<string>) handled by the old application:
- messageInfo
- messageWarning
- messageError
Limitation: Dev vs Prod
Certain parameters, such as the root URL of the API server, changes drastically between the dev environment and the staging/production/etc. The way it’s done now, parameters are included in the generated Component package, so a package generated in dev will only work in dev, and so on. As for now, we only commit the production package in the source control, along with the V1 files which import it.
Limitation: Internationalization (i18n)
I struggled with my first Component package size when I tried to include i18n in it, and reverted to use localized string directly in my components at the time. Now, Angular 9 has come with a new i18n system, and maybe it would be easy to use with Angular Elements.
Limitation: External CSS
The fact that the resulting WebComponent will be injected in an existing application does not protect this component from the default CSS rules applied by the user agent, which as we know can very a great deal from one browser to another (it should be protected from the global CSS styling of the existing application, though, if the encapsulation works well). Therefore, it is paramount to test the Component in-situ, meaning in the target application, and correct the Component CSS accordingly. The use of CSS resetter would also be a good practice within the Component, as this article describes: https://blog.jiayihu.net/css-resets-in-shadow-dom/.
Limitation: Error handling
Warning: I want to throw Exceptions in my service, to catch them in my Controller and display a user message accordingly. I tried to do so with a custom Error class, as follows:
export enum ExportPhycErrorType {
SensorCodeCanNotByEmpty = 1,
SiteMeteoCodeCanNotBeEmpty = 2,
StationCodeCanNotBeEmpty = 3,
StationHasActiveMeteoQuantityButNoMeteoCode = 4,
StationHasActiveCorrelatedQuantityButNoStationCode = 5,
OnlyOneCorrelatedAllowed = 6,
OnlyOneMeteoAllowed = 7,
}
export class ExportPhycError extends Error {
type: ExportPhycErrorType;
constructor( errorType: ExportPhycErrorType )
{
super();
this.type = errorType;
}
}
In my service:
return throwError( new ExportPhycError( ExportPhycErrorType.OnlyOneMeteoAllowed ) );
In my controller:
obs.subscribe(
()=>{
//...
}, (err) => {
if (err instanceof ExportPhycError) {
let msg;
switch (err.type) {
case ExportPhycErrorType.SensorCodeCanNotByEmpty:
msg = 'Une grandeur hydrométrique doit avoir un code capteur valide si son export est activé.';
break;
}
//...
}
});
This works well within an Angular application, as well as when an Angular application uses the generated Web Component. However, it does not work within an plain old JS app like V1. Indeed, the ‘err’ passed during the error handling is a callstack, and does not match ExportPhycError at all.
A replacement solution is to not use ExportPhycError and directly pass a ExportPhycErrorType.xxx (a number) to throwError(), and switch on the number value.