DEV Community

loading...
Cover image for Dynamic Component Generation in Lazy-Loaded Routes

Dynamic Component Generation in Lazy-Loaded Routes

ng-conf
Home to the largest gathering of Angular developers world wide!
・6 min read

Jim Armstrong | ng-conf | Jun 2019

— Exploit Data Driven Component Layout, Loaded On-Demand in Angular

One of the fun things about being an applied mathematician in front-end development is the never-ending stream of complex, interactive problems that users wish to solve. These range from gamification of business applications to data-driven user experiences. Two interesting applications of the latter topic include dynamic routing through an application and dynamic component layout. Both are examples of user experiences that vary based on factors such as user role and prior uses of the application. In other words, two different users may be presented with an entirely different experience based on both a priori knowledge of the user and how the user interacts with the application in real time.

The general solution I’ve applied to dynamic routing is a data-driven, adaptive decision tree. This is, unfortunately, a client-proprietary solution and can not be shared in public. However, I built the solution on top of two projects that I have released to the public in my Github, a general tree data structure and a lightweight expression engine. Both are written in Typescript. I believe any enterprising developer with some fundamental knowledge of decision trees could duplicate my work. So, here is the best I can do for you at present:

theAlgorithmist/TSTree - Typescript Math Toolkit General Tree Data Structure on github.com

theAlgorithmist/TSExpressionEngine - Typescript Math Toolkit Expression Engine on github.com

Dynamic, Data-Driven Component Layout

This article discusses how to layout Angular components programmatically, based on a simple JSON data structure. A simple example I’ve worked on in the past is where components are vertically stacked in an order that is server-generated. Consider a case where three components, A, B, and C could be presented to a user. They might be displayed in the order A, B, C, or A, C, B, or perhaps C, B, A. In general, there are n! display permutations of n components (displayed n at a time). One might be willing to struggle with a layout that could accommodate all possible scenarios for three components, but what about when the client later indicates there could be anywhere from three to eight components? And, we know how clients think, so that 3–8 range will not stay constant for very long. In short, this is a situation that is much better managed with an imperative instead of declarative approach.

Thankfully, the Angular team has provided everything we need to dynamically generate components at runtime. But, before we move on, here is the repo for this article so that you can follow along with the deconstruction, and have the code for experimentation and future usage in projects.

theAlgorithmist/Angular8LazyLoadDynamic - Angular 8 Dynamic Component Generation in a Lazy-Loaded Route on github.com

The Scenario

This demo simulates a scenario where a user logs into an application and then selects a navigational element that routes to another area of the application. The user experience, however, is tailored to each specific user based on information that is known about the user after login. A service call is to be made before activating the route. The service returns some JSON data that describes the order in which child components are to be displayed in the Angular Component associated with the selected route. The JSON data also provides some simple textual and numerical data that is used for binding within each of the child components.

Since the order of the components is not known in advance and the number of components can also vary, the child components are dynamically created and then rendered into the parent component’s template. For demonstration purposes, the number of components in the code distribution is limited to three. The technique, however, is easily scaled to any number of components in any arbitrary order.

The Process

I’ll treat the explanation in a cookbook fashion since additional information on each step in the process is readily available online for subsequent study.

1 — Each child component that could be rendered into a parent component must be provided as an EntryComponent into the Module associated with the route. This is why the route should be lazy-loaded as there is no guarantee that every user will navigate to this route.

2 — A route Resolver is used to ensure that the JSON data is loaded before the route activates. This is the mechanism a server would use to dynamically alter the experience for each individual user.

3 — In order for an Angular component to be dynamically displayed in a template, it must be added to a ViewContainerRef associated with a DOM container after creating the component. An Attribute Directive is used for this purpose.

4 — Each child component is to be generated by two Factories. One factory (that we write) generates component type and raw data instances based on a symbolic code and a known number of components. So, if the component range is changed from 3–8 to 2–12 at a later point, the four new items must be added to the factory. Another factory (provided by Angular and discussed below) is used to create the actual Angular component at runtime.

5 — The template for the lazy-loaded component consists of a ng-container as the primary container with a ngFor that loops over the number of dynamic components specified in the JSON data.

6 — Each dynamic component is associated with a ng-template by using an attribute directive.

7 — A QueryList of dynamic item attribute directives is processed by the parent component. Each child component is created by an Angular Component Factory (provided by a factory resolver) and then added to the ViewContainerRef of the ng-template. Data for each component is then added to the newly created component for binding. This requires some handshaking between the parent component code and the attribute directive. The actual separation of concerns can be experimented with and adjusted to suit your specific desires.

Application Layout

The application structure for this demo is rather simple. There is a single application module and component. The main app component displays a button whose markup contains a routerLink. That is used to route the user to the single feature module, appropriately named ‘feature’ :)

The main app module provides a single route resolver that is used to ensure the JSON data for dynamic layout is loaded before the route is activated.

All libraries, directives, and components for the single feature are provided in the feature folder.

The model for dynamically generated components is provided in src/app/models.

There is no relevant code in the main app component and the only item worth deconstructing is the main app routing module. Relevant code from the routing module is provided below.

/src/app/app-route-module.ts

const routes: Routes = [
  {
    path: `feature`,
    resolve: { model: AppRouteResolver },
    loadChildren: () => import(`./feature/feature.module`).then(m => m.FeatureModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  providers: [AppRouteResolver],
  exports: [RouterModule]
})
export class AppRoutingModule { }
Enter fullscreen mode Exit fullscreen mode

Note the new Angular 8 dynamic import syntax for lazy-loaded routes. This module also provides the route resolver, AppRouteResolver. The model property is used when loading data from the activated route.

Now, we can deconstruct each of the above-listed steps.

(1) Feature Module

Look at /src/app/feature/feature.module.ts. The important code is shown below.

export const DYNAMIC_COMPONENTS: Array<any> = [
  BaseComponent, Component1Component, Component2Component, Component3Component
];

@NgModule({
  declarations: [
    DynamicItemDirective, FeatureComponent, ...DYNAMIC_COMPONENTS
  ],
  imports: [
    CommonModule,
    RouterModule,
    RouterModule.forChild(localRoutes),
  ],
  providers: [],
  entryComponents: DYNAMIC_COMPONENTS
  exports: [
    DynamicItemDirective, ...DYNAMIC_COMPONENTS
  ]
})
Enter fullscreen mode Exit fullscreen mode

The three dynamic components in this example are Component1Component, Component2Component, and Component3Component. Yes, those are stupid names, but slightly better than my original choice of Moe, Larry, and Curly :) Each of these components extends BaseComponent.

In particular, note the declaration of all of dynamic components in the entryComponents property of NgModule. Since there is no direct reference to any of these components in a template, Angular needs this information directly for compilation purposes. Without entryComponents Angular will tree-shake out those components because they are never referenced in a template.

The attribute directive, DynamicItemDirective, is used to associate a ViewContainerRef with a specific DOM element (ng-template in this example).

(2) Route Resolver

The resolver is used by the main app component and is provided in /src/app/app-route.resolver.ts. This code implements the Resolve interface by providing a concrete implementation of the resolve() method.

@Injectable({providedIn: 'root'})
export class AppRouteResolver implements Resolve<LayoutModel>
{
  constructor(@Inject(DataService) protected _service: DataService)
  {
    // empty
  }

  resolve(): Observable<LayoutModel>
  {
    // load layout model
    return < Observable<LayoutModel> > this._service.getData('/assets/layout-model.json');
  }
}
Enter fullscreen mode Exit fullscreen mode

ng-conf: Join us for the Reliable Web Summit

Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/

Discussion (0)