RouteReuseStrategy: advantages and pitfalls

The Angular philosophy is to destroy a Component as soon as it’s not in use anymore, meaning: not in the DOM anymore, and this happens quickly when RouterOutlet is used to display such or such content, depending on the URL route. The Component will be created again, if the user navigates back to a route that contains it.

Recreating Components can be trouble (depending on the usecase)

This is useful for large applications (to reduce the memory) where Components are kept simple or when they will not be met again often enough to care about their destruction/reconstruction time.

But for applications where the user is expected to go back and forth the same Components, this can create several uncomfortable side-effects for the user experience:

  • a clipping may result, especially if some somewhat heavy initialization process occurs in the Component (= in ngOnInit); though, this might reveal a design smell. For instance, all data fetching (typically, from the API server) and calculation should be done and kept in a sidecar service, whose responsability will be to handle the ‘data state’ related to the Component, which can fetch it from there very quickly when initializing again.
  • the ‘UI state’ of the Component is lost, and that might go against the user experience. By ‘UI state’ (versus ‘data state’), I mean all the little interface details, not related to any business data, that evolves when the user plays with the Component. Examples: a selection in a Select node; the beginning of some text in a TextArea node; the expanding of a collapsable widget; the scrolling in a large list; the navigation in a map. For form-related elements, it would be possible to store the Form state in a sidecar, but for the rest of the myriads of possible little things, it wouldn’t be possible (too much effort) to store them all and restore them when the Component is met again, and the user would face a ‘default UI state’ each time. Again, this depends on the complexity and nature of the Components, and if we care at all about the UI to reset everytime.
  • when depending on third-party Component, we may still face an initialization time, resulting in clippings and annoying waitings. It will most likely be very difficult to save the internal ‘UI state’ of such Components, and for some of them, it would need some unwanted hacking. A third-party map component, for instance, is likely to request its tiles again if destroyed and recreated.

Angular RouteReuseStrategy to the rescue!

Angular offers a mechanism to keep a Component on the side when navigating away and use it again – unchanged – when the same route is met again, called RouteReuseStrategy. The set up is not sexy; it feels more like a toolbox than an integrated part of the framework, but it’s not difficult, and soon enough you can specify the different routes of your applications, whose related Components will be preserved for later (note that you define reuseable routes; you can’t flag a Component itself as reusable). And that’s it! The navigation is lightning flash again, your Components are met again as they were, hooray!

Notice: be wary though that your Component, when kept aside for later, is still very much alive. Its subscription will still be active, so be sure that your Component is not subscribed to anything exterior to itself (which should be very much the case anyway!).

RouteReuseStrategy: caveats

Sadly, no solution is ever perfect straight away.

In my design, there’s a little piece missing from the RouteReuseStrategy (SO users still agrees that it feels more like a bug), which is: some Component lifecycle hooks, to be alerted of its detaching (when it’s kept aside for later) and reattaching (reused).

Again, it might not be a problem for a lot of usecases. For mine, I couldn’t think about a proper alternative to what I have.

Description:

  • My Component depends on a part of the URL to identify which entity to display. For instance, the route /station/14 is linked to StationComponent (via RouterOutlet) and indicates the Station #14 should be displayed within it.
  • The Component should initialize depending on the route (and not some external state), because we want the same behavior whether the user has come to this route by navigating the application, or by launching this URL directly. For that purpose, subscribing to ActivatedRoute.params seems the most logical way, as this Observable emits some ParamMap, in which the interesting bit of the URL (defined in the routes given to the RouterModule) is directly proposed as paramMap.get(‘stationId’). Awesome!
  • Some other Components in my application rely on the displayed Station (if any). These Components can not subscribe to ActivatedRoute.params as they’re not connected to the route (from the RouterModule point of view). They could listen to URL changes by other means, but in a messy way, most likely very coupled to the route definitions. To resolve this, simple: we use a state service to keep the information of the displayed Station, on which my other Components can subscribe, and which is modified by my StationComponent whenever it detects a change in the route parameters.
  • Now if the user navigates to another Component, say, a Digest, then my state service will be alerted that now a Digest is displayed, and not a Station anymore. If he navigates back to a Station – one different than the first – then StationComponent is reused, it detects the change in the route parameters and notifies the state service that now a Station is displayed, this very new Station. Good!
  • BUT! If the user navigates away and come back to the same Station, then StationComponent is reused, but this time it does not see any changes in the route parameters (if it was 14, from /station/14 before, and now it’s still /station/14, then no changes have occured, as the observed ActivatedRoute is only the one related to StationComponent). No event is emitted by ActivatedRoute.params, and the notifying of the state service is not done, resulting in a unsynced state within my application. Curses !

That’s where I lack of an elegant way to resolve this. To me, the Component should be able to react to its reattaching, during which it would have an extra opportunity to notify the state service.

But RouteReuseStrategy does not call any Component lifecycle hooks. : /

RouteReuseStrategy’s missing Component Lifecyle hooks: a fix

A workaround (found on SO) consists on replacing the default RouterOutlet by a custom one, which will trigger some lifecycle hooks in its related Component.

sv-router-outlet.directive.ts

import { ComponentRef, Directive } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router';

@Directive({
  selector: 'sv-router-outlet',
})
export class SvRouterOutletDirective extends RouterOutlet {

  detach(): ComponentRef<any> {
    const instance: any = this.component;
    if (instance && typeof instance.onDetach === 'function') {
      instance.onDetach();
    }
    return super.detach();
  }

  attach(ref: ComponentRef<any>, activatedRoute: ActivatedRoute): void {
    super.attach(ref, activatedRoute);
    if (ref.instance && typeof ref.instance.onAttach === 'function') {
      ref.instance.onAttach(ref, activatedRoute);
    }
  }
}

To use, define a Module for this new outlet, and load the module wherever you’ll replace the default <router-outlet> by <sv-router-outlet>.

sv-router-outlet.module.ts

import { NgModule } from '@angular/core';
import { SvRouterOutletDirective } from './sv-router-outlet.directive';

@NgModule({
  declarations: [
    SvRouterOutletDirective,
  ],
  exports: [
    SvRouterOutletDirective,
  ],
})
export class SvRouterOutletModule { }

Angular Elements

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.

HttpClient and cold Observables

TL;DR: use obs.toPromise() with the Observable returned by the HttpClient library if you condiser the end-consumer will attach callbacks to the Promise before the API answers; otherwise, use .shareReplay(1) and subscribe directly a first time to the Observable.

HttpClient library, whether in NestJS or Angular, uses cold Observables.

A cold Observable will only “activate” (here: do the HTTP call) when it is subscribed to, for the simple reason that when it emits (here: when we receive response from the call), we want at least someone to listen, so that the result don’t go unheard straight to the void.

Yet, I find that in a lot of usecases involving API calls, the user which want to make the call will or will not deal with the result, depending on the situation (it could be updating a stock, which returns the resulting inventory, which we want to store somewhere or not). I’m talking about a situation where you’re developping a module to place calls, used by consumers services.

consumer <-> HTTP module <-> remote API

My first trial involved subscribing to the Observable directly in my HTTP module, before handing the Observable to the consumer.

const obs = httpClient.get(url);
obs.subscribe( () => {}, () => {} );
return obs;

Note that I provide two empty callbacks in .subscribe to avoid nuclear mole bomb (see previous post) ?

This works, but it works too much: if you observe API calls in the Network tab of your dev tools, you’ll see 2 API calls at once. Why that? Because the way it’s done, the cold Observable given by HttpClient will activate n times if you subscribe n times to it. So here, it’s fine if the consumer does not subscribe it, but possibly problematic if it does.

For that matter, there’s a nice Rxjs operator: .share(). (Btw, don’t let people who say it’s equivalent to .publish().refCount(): it’s not.)

const obs = httpClient.get(url).pipe( share() );
obs.subscribe( () => {}, () => {} );
return obs;

This operator will share the result of one call with all subscribers at the time of result coming: nice! But actually not enough. If the call is real fast, and the consumer takes time to subscribe, it will miss the first result and another call will be made (resulting again in two calls, which we really want to avoid).

So there’s another Rxjs operator for that: .shareReplay(1).

const obs = httpClient.get(url).pipe( shareReplay(1) );
obs.subscribe( () => {}, () => {} );
return obs;

This operator will share the last result it got (and HttpClient will only ever emit one result, so we’re fine with that) with any present or future subscribers. Awesome !

Now all that seems a bit far-fetched. There’s a way simpler solution, but note that it’s not appropriate if you think the consumer will attach callbacks later on, after the API has sent results (in which case, error-handling seems to be simply impossible). It’s to use the Observable .toPromise() function. Why that? Because due to the nature of the 2 structures (Observable and Promise), .toPromise() has to subscribe to the Observable, activating it in the same time, so we don’t have to take care of forcing the call anymore. Then, you handle a Promise to the consumer. This Promise will receive the first emission of the Observable (and there will be maximum one, so that’s fine), and keep it to show it to any callback the consumer will attach to .then().

const obs = httpClient.get(url);
return obs.toPromise();

Only caveat: if the API call gets into an error, and if the consumer does not handle error on the Promise before that, then you’re doomed, and the error is likely to bubble up and crash the process. At the time of writing, I found no way to handle this situation, except by interecepting the error with the Observable .catchError() operator and providing a default replacement value, which is probably not the best on the consumer side…

Byebye, old HttpModule

Welcome, HttpClientModule

HttpModule has been deprecated since Angular 4.3, but hadn’t been replaced yet. Now to prepare for the coming version 6 where it’s not supported anymore, we just got rid of it to welcome the newer HttpClientModule.

All objects from the “@angular/http” package are obsolete and have been replaced by objects from “@angular/common/http”. It was also the opportunity to have a good spring cleaning of those areas of code.

As we’re still receiving mixed types of server response (the version is still hybrid with legacy Symfony mechanisms), we indicate in the HttpClient calls options: observe = ‘response’, so the response we receive is not directly json-decoded, but returned as is.

Especially, the @ngx-translate/http-loader, which is responsible for loading the i18n translation files, has been upgraded from 0.1.0 to 2.0.1 to support the new HttpClient.

Tests

Tests were a bit verbose before when it got to Http. They are now simplified by the use and import in TestBed of the newer module:

import { HttpClientTestingModule } from ‘@angular/common/http/testing’;

Angular – Accessing services from the browser console

Though it is possible to access the properties of an Angular Component by selecting its corresponding DOM Element (in the WebTools DOM Explorer) and using the magical “ngprobe(0)” object in the console, it’s quite laborious and not really glamour.

For that reason comes to the rescue new service ConsoleUtilsService, which you will never use because it doesn’t provide any methods, but which is responsible for making some selected services directly available from the console. Everything provided that way will be packed in the global variable “sv“.

Example of direct use from the console:

sv.storage.getInstantEntity('Station', 10);

This feature is naturally delivered only in development environment and will not be accessible in production.

Upgrade to Angular 5.2 and Angular-CLI 1.7

It’s time to upgrade the application, and it goes with few changes to take in consideration !

For you to get a grasp at the new features of each release of Angular or its CLI, this blog is most interesting: http://blog.ninja-squad.com/tags.html#Angular 5-ref

Steps to do after pulling the new sources

Note: npm has been upgraded to 5.6.0 and yarn to 1.3.2.

  • mise à jour du package @angular/cli en global
npm update -g @angular/cli@1.7.1
  • mise à jour de typescript en global
npm update -g typescript
  • mise à jour des packages locaux (dans répertoire angular)
yarn install

Angular-CLI

Configuration has been updated for this new version. Note that test.ts has also received modifications.

Angular

The new command “ng update” of the latest CLI allows to update all Angular modules, here to version 5.2.6.

In addition, have been updated via “yarn upgrade”:

  • yarn upgrade @angular/material –latest
  • yarn upgrade localize-router –latest
  • yarn upgrade ng-dynamic-component –latest
  • yarn upgrade ng2-google-charts –latest
  • yarn upgrade codelyzer –latest
  • yarn upgrade @angular/cdk –latest
  • yarn upgrade @ngx-translate/core –latest
  • yarn upgrade typescript@~2.5.3
  • yarn upgrade @angular/cdk@5.2.2
  • yarn upgrade tslib@^1.0.0

The 3 last ones are for compatibility with used tools.

Note that because we upgraded Typescript, you might need to upgrade your IDE plugin for Typescript, for it to understand the lattest evolutions (especially the new options in tsconfig.json).

Netbeans user: this way !

New options have been added to the compiler:

  • fullTemplateTypeCheck: true
  • preserveWhitespaces: false