This content originally appeared on Bits and Pieces - Medium and was authored by Kamil Konopka
Find out if the Angular v13 update actually simplified dynamic component creation.
With the Angular v13 release, the revamped concept of dynamic component usage was introduced. The aim of the update was to simplify the actual approach.
Is it really simplified? Let us have a closer look! I will be using Angular v16, so stay tuned for most feature usage!
👉 Also, check out some Angular dev tools you could try in 2023.
The idea is to leverage Dynamic Parameters to determine which Angular Component should be dynamically created. Therefore our route configuration should look like this:
import { Routes } from '@angular/router';
export const rootRoutes: Routes = [
... // other routes definition
{
path: 'services/:id',
loadComponent: () => import('./offerings/index')
.then(x => x.ServicesComponent),
},
... //other routes definition
];
I am using lazy loading with a standalone component, which is why I am using loadComponent with import syntax (in lines 7–8). Our actual focus should be on line 6, where dynamic :id is being declared. This way we are declaring a dynamic parameter named “id”, which will be available with ActivatedRoute class injected into constructor (via dependency injection mechanism) like in the code example below:
import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { distinctUntilChanged, mergeMap, Subject, takeUntil, tap } from 'rxjs';
@Component({
selector: 'app-services',
standalone: true,
templateUrl: './services.component.html',
styleUrls: ['./services.component.scss'],
})
export class ServicesComponent implements OnInit, OnDestroy {
constructor(private readonly route: ActivatedRoute) {}
destroy: Subject<void> = new Subject<void>();
ngOnInit(): void {
this.route.params
.pipe(
takeUntil(this.destroy.asObservable()),
)
.subscribe();
}
ngOnDestroy(): void {
this.destroy.next();
}
}
To get the most recent parameters from the URL you can subscribe to this.route.params stream (line 18). If there’s no parameter declared within the URL (`/services`), you will receive an empty object ({}).
Remember:
ALWAYS unsubscribe when your component is being destroyed! (There are plenty of ways to achieve this! But it’s a topic for another time). If you don’t, the garbage collector will not know that you’re not using subscription anymore and you will face memory leak!
Now we are all set to get to the bottom of our main topic. Angular Team suggests creating a separate directive, which will contain ViewContainerRef class injected, so it can be reused anywhere without the need of passing additional dependency injection to every component where dynamic components might be needed. GOOD IDEA! Here’s what the example directive implementation might look like:
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appComponentHost]',
standalone: true,
})
export class ComponentHostDirective {
constructor(public readonly viewContainerRef: ViewContainerRef) {}
}
Now you need to simply use it within the html template of services.component.
<ng-template appComponentHost />
I am using the newly introduced (in v16) self-closing tag syntax, which you might have known from React already. Looks a little bit clearer right? It is all about making our code a little bit cleaner and easier to read.
Ok, my directive is already in place, so now I am able to use @ViewChild decorator to get to the directive within our services.component class and access our publicly available viewContainerRef! Here’s how services.component.ts class should look now:
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { distinctUntilChanged, mergeMap, Subject, takeUntil, tap } from 'rxjs';
@Component({
selector: 'app-services',
standalone: true,
imports: [ComponentHostDirective],
templateUrl: './services.component.html',
styleUrls: ['./services.component.scss'],
})
export class ServicesComponent implements OnInit, OnDestroy {
@ViewChild(ComponentHostDirective, { static: true }) componentHost!: ComponentHostDirective;
constructor(private readonly route: ActivatedRoute) {}
destroy: Subject<void> = new Subject<void>();
ngOnInit(): void {
this.route.params
.pipe(
takeUntil(this.destroy.asObservable()),
)
.subscribe();
}
ngOnDestroy(): void {
this.destroy.next();
}
}
ComponentHostDirective has been imported to our standalone component and the actual instance from the HTML template is already available under this.componentHost attribute.
Let us now create a method which will handle component creation:
loadComponent(service: Service): void {
const { viewContainerRef }: ComponentHostDirective = this.componentHost;
viewContainerRef.clear();
const { instance } = viewContainerRef.createComponent<ServiceComponentType>(
servicesComponentFactory[service.id as ServiceTypes]
);
instance.data = service;
}
I am destructuring viewContainerRef so I can use it for all my needs. Firstly, I need to make sure, that there’s no leftover from previous emissions. That is why I am calling the viewContainerRef.clear() method, to remove any previously created component instance.
Secondly, I am creating a brand new instance of my component based on the service id, taken from the servicesComponentFactory definition.
This is my own implementation, has nothing to do with actual dynamic component creation. It is more like a convenience technique to ensure proper mapping between parameters and component type. You can also ensure some default behavior for the factory on your own. We’re not preparing bulletproof solutions now, just showing examples!
Here’s what the servicesComponentFactory definition looks like:
import {
ConsultancyComponent,
SoftwareEngineeringComponent,
TrainingsComponent,
} from '../components';
import { Type } from '@angular/core';
import { ServiceTypes } from './service-types';
export type ServiceComponentType = ConsultancyComponent
| TrainingsComponent
| SoftwareEngineeringComponent;
export const servicesComponentFactory:
Record<ServiceTypes, Type<ServiceComponentType>>
= {
[ServiceTypes.consultancy]: ConsultancyComponent,
[ServiceTypes.trainings]: TrainingsComponent,
[ServiceTypes.engineering]: SoftwareEngineeringComponent,
};
Need to highlight, how our data passed as an argument of the method looks like. It’s an interface which represents the data structure being passed to the created component instance:
export interface Service {
id: number;
title: string;
description: string;
}
I’ve also declared an enum which represents types of available services:
export enum ServiceTypes {
consultancy, // 0
trainings, // 1
engineering, // 2
}
Now, let us hook the method into the parameters stream subscription:
ngOnInit(): void { // subscribing to the stream when component has been created
this.route.params
.pipe(
switchMap(({ id }: Params) => this.servicesDataService.getOne(Number(ServiceTypes[id]))),
tap((service: Service) => this.loadComponent(service)),
takeUntil(this.destroy.asObservable()),
)
.subscribe();
}
Compared to our initial stream, I have added switchMap operator to switch every emission into another stream which will allow me to fetch data representing my service object.
Just to remind you: my expected parameters should be represented by ServiceTypes, and look like:
'consultancy' | 'trainings' | 'engineering'
That is why I need to transform those into the actual id representation, of type number (according to our Service interface).
I use switchMap, because of its nature. Once new parameter arrives and my data is still not there, I will not need my data anymore. I need to request new data, based on newly emitted id immediately. This is what switchMap operator does. Cancels previous request and sends a new one.
To summarize, the entire services.component.ts class should look like this:
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { distinctUntilChanged, mergeMap, Subject, takeUntil, tap } from 'rxjs';
import { ComponentHostDirective } from '../../directives';
import { Service, ServiceComponentType, servicesComponentFactory, ServiceTypes } from '../../models';
import { ServicesDataService } from '../../../services';
@Component({
selector: 'app-services',
standalone: true,
imports: [ComponentHostDirective],
templateUrl: './services.component.html',
styleUrls: ['./services.component.scss'],
})
export class ServicesComponent implements OnInit, OnDestroy {
@ViewChild(ComponentHostDirective, { static: true }) componentHost!: ComponentHostDirective;
constructor(
private readonly route: ActivatedRoute,
private readonly servicesDataService: ServicesDataService,
) {}
destroy: Subject<void> = new Subject<void>();
ngOnInit(): void {
this.route.params
.pipe(
mergeMap(({ id }: Params) => this.servicesDataService.getOne(Number(ServiceTypes[id]))),
tap((service: Service) => this.loadComponent(service)),
takeUntil(this.destroy.asObservable()),
)
.subscribe();
}
loadComponent(service: Service): void {
const { viewContainerRef }: ComponentHostDirective = this.componentHost;
viewContainerRef.clear();
const { instance } = viewContainerRef.createComponent<ServiceComponentType>(
servicesComponentFactory[service.id as ServiceTypes]
);
instance.data = service;
}
ngOnDestroy(): void {
this.destroy.next();
}
}
Just have a look at the imports at the top of the file! Barrel exports rock right?
That’s it! Our dynamic component creation is up and running! Looks really simple! Thanks for the update Angular Team!
💡 Do you find yourself using the same Angular component for multiple projects? Consider using a component-management hub such as Bit. With Bit, you can harvest reusable Angular components from your codebase and reuse across all your projects. Bit comes with a component development environment for Angular that provides proper tools, configurations and runtime environment for your components, providing versioning, testing, and dependency management features to simplify the process.
Learn more here:
Build Angular Apps with reusable components, just like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want:
→ Micro-Frontends
→ Design System
→ Code-Sharing and reuse
→ Monorepo
Learn more:
- Introducing Angular Component Development Environment
- 10 Useful Angular Features You’ve Probably Never Used
- Top 8 Tools for Angular Development in 2023
- Getting Started with a New Angular Project in 2023
- How We Build Micro Frontends
- How to Share Angular Components Between Projects and Apps
- How we Build a Component Design System
- Creating a Developer Website with Bit components
Dynamic Components in Angular 16 was originally published in Bits and Pieces on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Bits and Pieces - Medium and was authored by Kamil Konopka
Kamil Konopka | Sciencx (2023-05-22T05:45:35+00:00) Dynamic Components in Angular 16. Retrieved from https://www.scien.cx/2023/05/22/dynamic-components-in-angular-16/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.