Loading Dynamic Components with Ionic & Angular ComponentFactoryResolver Last update: 2018-04-03
Recently one member of the Ionic Academy asked for help regarding a tough situation: How to display dynamic components based on a JSON object structure you might get from an API?
One solution to this problem is the use of the Angular ComponentFactoryResolver. Imagine you have a process with different steps that can be ordered by users how they need them - now you want to display the right process step to the user inside your Ionic app based on his decision.
This case is what we will build inside this tutorial so we can finally create dynamic components.
App Structure
To get started we create a blank new Ionic app and add a few components and a provider. However, using the ComponentFactoryResolver we need to directly reference our components at the top level of our app and not only inside a dedicated module like normally.
Therefore, we can remove the components.module.ts file and later add everything to our main module. Also, we need two more files which we will simply create inside the components/ folder but which don’t need a template file. For all this you could run these commands:
ionic start devdacticDynamic blank
cd devdacticDynamic
ionic g provider process
ionic g component stepOne
ionic g component stepTwo
ionic g component stepThree
rm src/components/components.module.ts
touch src/components/process.ts
touch src/components/process-item.ts
We need to directly add our components to both the declarations and entryComponents array of our main module so Angular knows about them and we can dynamically create them later.
Go ahead and change your app/app.module.ts to:
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { ProcessProvider } from '../providers/process/process';
import { StepOneComponent } from '../components/step-one/step-one';
import { StepTwoComponent } from '../components/step-two/step-two';
import { StepThreeComponent } from '../components/step-three/step-three';
@NgModule({
declarations: [
MyApp,
HomePage,
StepOneComponent,
StepTwoComponent,
StepThreeComponent
],
imports: [
BrowserModule,
IonicModule.forRoot(MyApp)
],
bootstrap: [IonicApp],
entryComponents: [
MyApp,
HomePage,
StepOneComponent,
StepTwoComponent,
StepThreeComponent
],
providers: [
StatusBar,
SplashScreen,
{provide: ErrorHandler, useClass: IonicErrorHandler},
ProcessProvider
]
})
export class AppModule {}
That’s it for the basic setup. If you have an existing app just make sure that you reference the components accordingly in both arrays so they are available when we need them later.
Creating the Components
First now we add some basic stuff to our components and keep them under one interface. By doing this we can later easily cast our objects and reference their inputs and know exactly what we are working with!
Open the components/process.ts and add this interface:
export interface ProcessComponent {
data: any;
}
Now all our 3 created components will implement this interface so all of them have one input which is data
. You can go to all 3 of your components and change them like this one example, but of course keep their original class names and annotation definition:
import { ProcessComponent } from './../process';
import { Component, Input } from '@angular/core';
@Component({
selector: 'step-one',
templateUrl: 'step-one.html'
})
export class StepOneComponent implements ProcessComponent {
@Input() data: any;
constructor() {
}
}
Finally, add some super simple HTML to all of the 3 views of our components like this:
<ion-slide>
<h1>{{ data }}</h1>
</ion-slide>
If you want to distinguish them even better, you can change the CSS on the different components as well.
Working with the Process Provider
Now we get to the difficult stuff, but first let’s create another class which will represent a ProcessItem
in our app. This item consists of an actual component and some description, and we can use this class again to get some more control over typings (yeah, TypeScript!).
Open your components/process-item.ts and change it to:
import { Type } from '@angular/core';
export class ProcessItem {
constructor(public component: Type<any>, public desc: string) {}
}
Our provider acts as if it would get a JSON response from an API which consists some information about the process steps. This is just one example, there are various cases in which you might want to dynamically create components.
To get an array of ProcessItem
we need to transform this response into the according information of components using getProcessSteps
.
The function getPageOrder
will then resolve the different steps and either insert a new item or call itself again if the step contains some children (expert mode here).
Finally, the resolveComponentsName
needs to resolve the steps to the actual component names. This means, you can’t simply send a string from the API and translate it to a component - there need to be some kind of mapping or logic inside your app and you need to reference the components and imports.
Imagine different teams working on different components - in the end they just need to add their mapping and the backend team can send out new items which the app can resolve as well. So this can help to separate concerns inside your project or distribute the work on multiple teams.
Now open your providers/process/process.ts and change it to:
import { ProcessItem } from './../../components/process-item';
import { Injectable } from '@angular/core';
import { StepOneComponent } from '../../components/step-one/step-one';
import { StepTwoComponent } from '../../components/step-two/step-two';
import { StepThreeComponent } from '../../components/step-three/step-three';
@Injectable()
export class ProcessProvider {
private dummyJsonResponse = {
items: [
{
step: 1,
desc: 'Mighty first step'
},
{
step: 2,
desc: 'Always first looser'
},
{
step: 3,
items: [
{
step: 3,
desc: 'I am the best step'
},
{
step: 1,
desc: 'Mighty first step'
}
]
},
{
step: 2,
desc: 'Always first looser'
}
]
}
constructor() { }
getProcessSteps() : ProcessItem[] {
return this.getPageOrder(this.dummyJsonResponse.items);
}
private getPageOrder(steps) : ProcessItem[] {
let result : ProcessItem[] = [];
for (let item of steps) {
if (item.items) {
result = result.concat(this.getPageOrder(item.items));
} else {
let comp = this.resolveComponentsName(item.step);
let newItem = new ProcessItem(comp, item.desc);
result.push(newItem);
}
}
return result;
}
private resolveComponentsName(step) {
if (step === 1) {
return StepOneComponent;
} else if (step === 2) {
return StepTwoComponent;
} else if (step === 3) {
return StepThreeComponent;
}
}
}
Now we just need a way to create and display our components, and that’s the last step for today.
Loading Dynamic Components
To render the dynamically created components inside our view we need a parent element hosting all of it. Therefore, change your pages/home/home.html to this:
<ion-header>
<ion-navbar color="primary">
<ion-title>
Dynamic Templates
</ion-title>
</ion-navbar>
</ion-header>
<ion-content padding>
<ion-slides>
<div #processContainer>
</div>
</ion-slides>
</ion-content>
In our case we wrapped our components in slides so we can swipe through our pages and see the different process steps. The processContainer
is what we will be fill with all of our components now.
Inside the class we can rely completely on the logic of our provider which has already translated JSON info to component info. We now get the array of steps along with their component and factory. The resolveComponentFactory
then gives us a factory that can be used to create a new component and that’s what we do.
Finally, we can cast the new componentRef
using our interface ProcessComponent
and then actually set the data input of the component!
The “add this component to the view” step actually also already took place when we created the component on our container, which is the ViewChild we created at the top and which references the hosting element inside our view.
Wrap up your code by changing the pages/home/home.ts to:
import { ProcessComponent } from './../../components/process';
import { ProcessProvider } from './../../providers/process/process';
import { Component, ViewChild, ViewContainerRef, ComponentFactoryResolver } from '@angular/core';
import { NavController } from 'ionic-angular';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
@ViewChild('processContainer', { read: ViewContainerRef }) container;
constructor(public navCtrl: NavController, private processProvider: ProcessProvider, private resolver: ComponentFactoryResolver) { }
ionViewDidLoad() {
let steps = this.processProvider.getProcessSteps();
for (let step of steps) {
const factory = this.resolver.resolveComponentFactory(step.component);
let componentRef = this.container.createComponent(factory);
(<ProcessComponent>componentRef.instance).data = step.desc;
}
}
}
Now you can see that your final view is dynamically creating components but actually has none of the imports! Only our provider keeps a reference to all of them, so we don’t have to touch the view and logic here later on and can apply all changes to the provider and main module.
Conclusion
Some ideas can be tricky to implement, but many times there’s an Angular way of doing things - sometimes it’s just a bit hidden and not so popular!
In this tutorial you’ve learned to generate components dynamically using the factory pattern. This was not one of the easier tutorials here, so congrats if you’ve finished all of it!
You can also find a video version of this article below.