Implementing The Strategy Pattern Using Lazy-Loaded Components in Angular Version 9
By: Jim Armstrong
Tags: Math, Typescript, Angular 9

Implementing The Strategy Pattern Using Lazy-Loaded Components in Angular Version 9

Simply stated, the Strategy Pattern is a behavioral pattern that governs the selection of different algorithms or procedures for solving a common problem at runtime. Many times, this pattern is implemented by loading a computational library that conforms to an Interface and is applied uniformly by one or more application components.

In front-end applications, a strategy may involve more than just an algorithm or library API. There is often need to load one of several different implementations of an entire view based on runtime criteria. In an Angular setting, we may think of this as needed to lazy-load a Module, but it is even more helpful if we can load individual components, outside any Module definition. Enter Angular Version 9.

In the article below, I covered dynamic component generation inside lazy-loaded routes, with the ability to control component display via JSON data,

Three components were dynamically generated inside a lazy-loaded route and displayed in an order dictated by a data file. However, since components prior to Angular Version 9 were required to exist in a Module definition, each component was defined in the entryComponents section of the route’s Module. Each component was loaded into the application when the route was lazy-loaded, even if the component was not actually required.

Angular Version 9 provides the ability to lazy-load individual components that are outside the scope of any Module. This allows for a very powerful implementation of the Strategy Pattern.

The Application

The sample application for this article is a mashup of multiple requirements from past applications across Flash, Flex, and Angular. The application illustrates two different solutions to the point-in-circle problem, i.e. identify the circles in a collection for which a single point is in the strict interior.

Now, if it was just a matter of selecting an algorithm to solve the problem and then display the results in a single view, the strategy could be implemented with a lazy-loaded JavaScript library (a topic for another article). In this demo, the results are viewed differently, not only in terms of layout, but with different render environments. Each view renders a simulation in which the point moves inside the view area and the simulation updates the point/circle intersections. However, one algorithm is rendered into a Canvas context and the other into SVG. The Canvas and SVG displays also differ based on algorithm characteristics.

Because of the differing views and dramatically different dependencies between views, this problem is best solved by lazy-loading an individual component (and its dependencies).

Before deconstructing the application, I wish to point out that is is not an article on ‘how to’ lazy-load Components in Angular Version 9. A Google search will return about half a dozen good articles on the steps involved in this process. Instead, this article discusses how to build a complete application around the technique.

And, on that note, here is the GitHub for the complete application.

Algorithms and Geometry

Well, let’s get this out of the way early. This is an Angular article, not a math/algorithms article. As a consequence, the two algorithms I chose for the demonstration are rather rudimentary and easy to follow. As far as the necessary math, here is everything we need.

Blah, blah … math … blah, blah … geometry … blah blah … API.

There, we’re finished :). Everything you need is encapsulated away into Typescript libraries in the /src/app/shared/libs folder. I even gave you a bonus — a number of pure functions for operations involving circles from my private Typescript Math Toolkit library.

The two algorithms implemented in this demo were chosen to illustrate techniques you may find helpful in future applications. In one case, there could be an arbitrary (and large) number of circles, but the circles do not move (only the point moves). In the second case, there are a modest number of circles (hundreds) and a small percentage of them are expected to move each simulation step along with the point.

Both the point and circles move according to a random walk. Don’t worry — remember the power of the API — it’s all done for you :)

The first algorithm (simulation rendered into a Canvas) uses a simple quad map. The viewable area is divided into quadrants. The Typescript Math Toolkit basic circle class automatically computes quadrant locations given bounds, so a quad map is very easy to implement. Since the circles never move, the map need only be initialized once before the simulation begins.

The second algorithm (simulation rendered into SVG) is designed for a situation where there are a modest number of circles (hundreds), and a small percentage of them move at each time step. All circles are tested for point intersection, but the test is optimized to return false quickly. It is more expensive to test for full intersection, so instead of testing ‘if something is true, then do the following operation,’ it is more efficient to test ‘if something is false, skip and go to the next one.’ It’s very easy to quickly return false for the intersection test and that is the majority expectation for most tests.

Common (visual) operations for each display include indicating the specific circles that contain the test point at each simulation step. Previous visual indications from a prior simulation step must be reset to default display at the next time step. Beyond that, the Canvas display shows the quadrants and highlights the current quadrant in which the test point lies. The SVG display only shows the circles and test point.

It’s only necessary to understand the algorithms employed in both simulations at a very high level. In fact, the only reason to dive deeper into their implementation is if you want to improve the point-circle intersection algorithm further.

Application Deconstruction

Let’s look at each of the application requirements, and then discuss how they are all integrated together. We need

1 — Two components to implement each algorithm and each set of display requirements for a single simulation. Commonality among these components should be specified in an Interface.

2 — Lazy-load only one of the components based on some runtime criteria (an injected algorithm ID, for example).

3 — Create a means to provide inputs and handle component outputs.

4 — Run the simulation, i.e. advance one step at regular time intervals.

For demonstration purposes, the main app component, /src/app/app.component.ts, serves as the smart component that lazy-loads one of the two point-in-circle simulation components. The main app module allows specification of an algorithm ID (1 for the first algorithm and Canvas render, 2 for the second algorithm and SVG render).

import { BrowserModule } from '@angular/platform-browser';

import { NgModule } from '@angular/core';



import { AppComponent } from './app.component';

import { CircleService } from './shared/services/circle-service';



// Injectable constants

import { ALGORITHM_ID } from './tokens';



@NgModule({

declarations: [

AppComponent,

],

imports: [

BrowserModule

],

providers: [

CircleService,

{ provide: ALGORITHM_ID, useValue: 1 },

],

bootstrap: [AppComponent]

})

export class AppModule { }

This ID is injected into the main app component and then used to select the run-time strategy for the point-in-circles simulation. Change the value from 1 to 2 in order to lazy-load a different simulation component.

The two point-in-circle simulation components are

Algorithm 1 (Canvas): /src/app/components/point-in-circle-1/pic-1.component.ts

Algorithm 2 (SVG): /src/app/components/point-in-circle-2/pic-2.component.ts

Since everything is driven by the main app component, let’s look at that one in detail.

Main App Component

This component, in /src/app/app.component.ts is the smart component that lazy-loads one of two presentational components based on an algorithm ID. Although the algorithms and the display employed by each of these presentational components is different, as far as the rest of the application is concerned, they have a common set of inputs, outputs, and an exposed API. These are defined in the Interface in /src/app/shared/interfaces/pic-component.ts,

import { OnChanges } from '@angular/core';

import { Subject } from 'rxjs';



export interface IPointInCircle extends OnChanges

{

intersect$: Subject<string>;



next: () => void;



step: number;

}

The simulation step (controlled by the smart component) is an Input to the point-in-circle component. This component is not statically defined in a template (there is no known binding to that Input), so Angular does not know to call the ngOnChanges lifecycle method. As a result, the smart component must do this manually. So, each lazy-loaded component must implement the OnChanges interface. Each of these components must also provide a next() method in order to advance the simulation one step.

While there are several approaches to wiring up component outputs, I personally prefer a reactive approach. Each of the lazy-loaded components must expose a Subject<string> to indicate that the point intersected a circle with a particular id. The smart component that lazy-loads one of the two point-in-circle components simply subscribes to intersect$.

This interface is the only knowledge the smart component has about the two lazy-loaded presentational components.

Typical practice for lazy-loaded components is to place them into an ng-container, and this is the case for the main view in this application, /src/app/app.component.html,

<p>Algorithm ({{algorithm}} Render into: {{render}})</p>

<p>Total intersections: {{intersections}}</p>



<ng-container #picContainer></ng-container>

The template variable is used to assign a ViewChild in /src/app/app.component.ts,

@ViewChild('picContainer', {static: true, read: ViewContainerRef})

private picContainer: ViewContainerRef;

The main app component receives the algorithm ID and all other classes necessary for lazy-loading through injection,

constructor(@Inject(ALGORITHM_ID) public algorithm: number,

private _compFactoryResolver: ComponentFactoryResolver,

private _injector: Injector)

{

this.render = RenderTargetEnum.CANVAS;



this.ComponentInstance = null;



this._duration = 1000;

this._delta = 500;

this._currentStep = 0;

this.intersections = 0;

}

The render variable indicates whether the rendered output is Canvas or SVG. The ComponentInstance variable is a ComponentRef<IPointInCircle>. The instance property of that variable allows direct calls to the presentational component after lazy-loading.

A duration of 1000 time steps at increments of 500 msec is set in the constructor along with the current step number of zero and a total intersection count of zero.

On initialization, the component begins the simulation via a call to the private __initSimulation() method, whose relevant code is shown below.

You could place this code in the then clause of a Promise, but I personally prefer asyc/await (although beware there is a known problem with this if your target is ES2017). Component lazy-loading is shown below,

Following is the relevant block of code for lazy-loading the component associated with algorithm #1,

const {Pic1Component} = await import('./components/point-in-circle-1/pic-1.component'); factory = this._compFactoryResolver.resolveComponentFactory(Pic1Component); this.ComponentInstance = this.picContainer.createComponent(factory, null, this._injector);

It is very important to place the precise path of the lazy-loaded component as a string literal inside the import. For example,

const url: string = './components/point-in-circle-1/pic-1.component'; .

.

. const {Pic1Component} = await import(url);

compiles with a warning, and the application will not run.

The destructuring assignment must reference an exported Object from the referenced file, in this case, Pic1Component.

Once a point-in-circle simulation component is lazy-loaded, the following blocks of code in __initSimulation() handle outputs, inputs, and the actual simulation,

this.ComponentInstance.instance.intersect$.subscribe( (id: string) => this.__updateIntersection(id) );

This subscribes to the output from the simulation component and executes a handler every time the simulation detects a point-circle intersection.

The simulation is run within an RxJs timer.

timer(100, this._delta)

.pipe(

map( (msec: number): void => {

this._currentStep++;



if (this._currentStep > this._duration) {

this.destroy$.next();

this.destroy$.complete();

}

}),

takeUntil(this.destroy$)

)

.subscribe( () => {

this.ComponentInstance.instance.ngOnChanges({

step: new SimpleChange(this._currentStep-1, this._currentStep, this._currentStep === 1)

});



// run the next simulation step

this.ComponentInstance.instance.next();

});

The variable destroy$ is a local Subject that is used to indicate the end of simulation. This happens when the duration limit is reached or if the component is destroyed.

The parent (smart) component controls the step count of the simulation as it may later be desired to run a simulation forward or backward. The presentational (simulation) component accepts this step as an Input and reflects it in its own UI.

The simulation (lazy-loaded) component Input is accepted and processed through that component’s ngOnChanges() method. That handler is called each time step with a new SimpleChange object. Note that the call is made on the component reference’s instance property.

this.ComponentInstance.instance.ngOnChanges({

step: new SimpleChange(this._currentStep-1, this._currentStep, this._currentStep === 1)

and then, each iteration of the timer executes the next simulation step,

this.ComponentInstance.instance.next();

That concludes the deconstruction of the smart component that lazy-loads one of the point-in-circle simulation (or presentational) components. The simulation components are largely like any other Angular presentational component, but with one important exception. We’ll look at that next by deconstructing the Canvas-rendered component.

Lazy-Loaded Component Structure

The lazy-loaded simulation component for the first algorithm looks largely like any other component. This is the template,

/src/app/components/point-in-circle-1/pic-1.component.html

<div pic-canvas

class="circlesContainer"

[strokeWidth]="1"

[strokeColor]="'0x0000ff'"

[gridColor]="'0xff0000'"

(onIntersect)="onIntersection($event)">

</div>

<p>Simulation Step: {{step}}</p>

An attribute directive is used to delegate Canvas creation (via PIXI.js), computations, and rendering. In addition to defining class properties necessary to fulfill the IPointInCircle interface, a ViewChild is defined for the attribute directive,

export class Pic1Component implements IPointInCircle, OnChanges

{

public intersect$: Subject<string>;



@Input()

public step: number;



@ViewChild(PicCanvasDirective, {static: true})

protected _picContainer: PicCanvasDirective;



constructor()

{

this.intersect$ = new Subject<string>();

}

.

.

.

The next two methods complete the IPointInCircle interface contract,

public ngOnChanges(changes: SimpleChanges): void

{

let prop: string;

let change: SimpleChange;



for (prop in changes)

{

change = changes[prop];



if (prop === 'step')

{

this.step = +change.currentValue;

}

}

}



public next(): void

{

if (this._picContainer) {

this._picContainer.next();

}

}

Note that this is the next() method that is called from the RxJs timer subscription after Pic1Component is lazy-loaded.

Note that Pic1Component has an important dependency, the PicCanvasDirective. While Pic1Component may exist outside any NgModule in the application, the directive still needs to be tied to the component of which it is a ViewChild via a module definition. The cleanest way to do this and ensure that the directive is only loaded when Pic1Component is loaded is to use a local (non-exported) module,

@NgModule({

imports: [CommonModule],

declarations: [

Pic1Component,

PicCanvasDirective

]

})

class PicCanvasModule {}

You will see a similar module in /src/app/components/point-in-circle-2/pic-2.component.ts for the second simulation component.

Note that Pic1Component (and Pic2Component, for that matter) have another dependency, the injected CircleService. This service currently contains read-only variables, but is not listed as a provider in the above NgModule, nor has an injector been configured for it. In this application, the lazy-loaded component is loaded into the application root, so we are using the root injector. Note that a provider for this service is contained in /src/app/app.module.ts. It is often the case that either the root or a route-level injector can be used for such purposes. Other tutorials on the mechanics of lazy-loading components describe how to configure an injector just for services used by a lazy-loaded component.

This is all the structure that is necessary to dynamically load and instantiate a complete simulation for the problem at hand using only an algorithm id to differentiate between the two simulations.

The final section is optional and deconstructs the PicCanvasDirective. It is not necessary to understand the details of this directive in order to build your own application with lazy-loaded components. So, free free to quit reading here :)

Inside A Simulation

The reference file for this deconstruction is /src/app/shared/libs/render/canvas-render/pic-canvas.directive.ts

This is an attribute directive that creates a PIXI.js-generated Canvas inside some DOM container (typically a DIV). The directive employs a number of computational helper functions, as can be seen in the imports,

import { TSMT$Circle } from '../../circle';



import { CircleService } from '../../../services/circle-service';



import { canvasRenderCircle } from '../render-circle-canvas';

import { pointRandomWalk } from '../../point-random-walk';

import { TSMT$PointInCircle } from '../../circle-util-functions';

import { TSMT$getQuadrant } from '../../geom-util-functions';

import { RandomIntInRange } from '../../random/RandomIntInRange';



@Directive({

selector: '[pic-canvas]'

})

export class PicCanvasDirective implements OnInit, OnChanges

.

.

.

TSMT$Circle is the Typescript Math Toolkit (a private library for which I have open-sourced some of the contents) basic circle class. This class is optimized for a quad-map as it automatically updates the quadrant location of a circle every time its coordinates are mutated (provided that bounds are set in advance). The class also handles the case where a circle may lie in more than one quadrant.

Quadrants in which circles are located are referenced by four Typescript Record structures,

protected _quad1: Record<string, boolean>;

protected _quad2: Record<string, boolean>;

protected _quad3: Record<string, boolean>;

protected _quad4: Record<string, boolean>;

Relevant information about the DOM container for the Canvas (specifically width and height) is available in the ngOnInit() handler, which is where most of the PIXI.js initialization is performed. The initial set of circles are also drawn at that time in the __initCircles() method.

There are four class variables that store circle-related information. The first is _circleRefs, an array that stores are reference to each TSMT$Circle in the simulation. Every circle has a visual representation, a graphics context or display object in PIXI.js, which is stored in another array, _circleDO.

When a circle is identified as containing the test point during a simulation step, the circle reference is placed into the _indentified array and its corresponding graphics context into the _identifiedDO array.

Here is the code for the circle initialization, which is responsible for placing circles throughout the simulation area. PIXI.js drawing has been covered in a number of my past articles and will not be discussed in the current deconstruction.

The most important code (for this algorithm) occurs after placing the circles ‘randomly’ inside the display area.

c.setBounds(0, 0, this._width, this._height);



if (c.inQuadrant(1)) {

this._quad1[c.id] = true;

}



if (c.inQuadrant(2)) {

this._quad2[c.id] = true;

}



if (c.inQuadrant(3)) {

this._quad3[c.id] = true;

}



if (c.inQuadrant(4)) {

this._quad4[c.id] = true;

}

Each circle instance contains internal bounds that may be set by the caller. Bound setting and each subsequent coordinate mutation alters the quadrant in which the circle lies. That quadrant may be queried and is used to set the primitive quad-map (currently four separate Records in Pic1Directive).

Most of the simulation work is performed in the next() method that advances the simulation one step.

It seems like a long method, but it performs a relatively simple number of steps. First, the _indentified array is used to redraw any circles identified as containing the point in the prior simulation step with their default visual properties.

The random walk for the point is executed and the coordinates updated in a destructuring assignment,

[this._px, this._py] = pointRandomWalk(this._px, this._py, CircleService.LOW_RADIUS, CircleService.HIGH_RADIUS);

The current quadrant occupied by the point (stored in the _curQuad class variable) is updated with a utility method,

this._curQuad = TSMT$getQuadrant(this._px, this._py, 0, 0, this._width, this._height);

The current quadrant is checked vs the prior quadrant from the previous simulation step. If the quadrant number changes, then it is necessary to adjust the subset of circles that are checked for point-circle intersection. For example, if the current quadrant is one and the prior quadrant is one, then the check is made against keys in the local _quad1 Record. This variable might be unchanged for several simulation steps. If it changes to three, for example, tests for point-circle intersection should to be made against keys in the local _quad3 Record. This is handled by the block of code,

if (this._curQuad != this._prevQuad)

{

this._prevQuad = this._curQuad;

this._check = Object.keys(this[`_quad${this._curQuad}`]);

}

The statement,

this[`_quad${this._curQuad}`]

creates a reference to one of the local class variables (Records), _quad1, _quad2, _quad3, or _quad4, depending on the current quadrant number. A forEach function runs the point-circle intersection test against only the circles that lie in the current quadrant,

this._check.forEach( (id: string): void =>

{

circ = this._circleRefs[+id];

g = this._circleDO[+id];



if (TSMT$PointInCircle(this._px, this._py, circ.x, circ.y, circ.radius))

{

this._identified.push(circ);

this._identifiedDO.push(g);



g.clear();

g.lineStyle(this.strokeWidth, '0xff0000');

g.drawCircle(circ.x, circ.y, circ.radius);



this.onIntersect.emit(id);

}

Note that an Angular EventEmitter is used for the directive to convey an intersection to a containing component (Pic1Component). That component in turn executes the next() method of an RxJs Subject, which is subscribed to in the main app component (the primary smart component in this application).

Here is a screenshot from one time step of the Canvas-rendered simulation,

Note that the quadrant currently containing the test point is highlighted in green. Run the application and change the algorithm id to 2 to see how the SVG simulation display differs.

Summary

This was a long article, but I hope it illustrates how lazy-loaded components can be applied in an actual application. In this hypothetical situation, two different simulations with different algorithms, dependencies, and output are differentiated by an algorithm id. A run-time decision is made as to which simulation to execute. The simulation component and its dependencies are lazy-loaded.

This is in contrast to an alternate scenario where a simulation with identical dependencies and visual display is differentiated by algorithm only. This situation can be handled by a lazy-loaded a computational library instead of a component.

Good luck with your Angular efforts!

EnterpriseNG is coming

EnterpriseNG is a two-day conference from the ng-conf folks coming on November 19th and 20th. Check it out at ng-conf.org