Angular:Passing data in and out of Dynamic Components created using ngComponentOutlet
I have already written a story which explores how to pass data in and out of Dynamic Components created using ComponentFactoryResolver. Below is the link:
This story provides the same example implemented via ngComponentOutlet.
We begin with creating 2 Tabs which are actually 2 components: Tab1Component and Tab2Component loaded dynamically inside a parent TabContainerComponent
TabContainerComponent Template:
<ul class=”nav”>
<li *ngFor=”let x of tabs;let ind=index” class=”nav-item”>
<a [ngClass]="x.isActive ? 'activeTab' : ''" class=”nav-link” (click)=”ldTab(x.component, x.data,ind)”>{{x.tab}}</a>
</li>
</ul><ng-container
[ngComponentOutlet]="activeComponent"[ngComponentOutletInjector]="myInjector"
></ng-container><p>Data received from Tab Component: {{ receivedData }}</p>
We are iterating over a variable tabs, which is an array of 2 objects containing each Tab’s data,Component details and active status.
On clicking on a particular tab, we are calling a method ldTab(), passing the component name,the tab’s data and tab index as arguments.
<ng-container> has accepted 2 properties: ngComponentOutlet and ngComponentOutletInjector.
activeComponent variable contains the component name which has to be dynamically created. This variable is passed as value to the ngComponentOutlet property.
myInjector is a custom injector created for the purpose of passing data to the dynamic component. This injector is passed as value to the ngComponentOutletInjector property.
We shall see shortly how the injector is created,passed to this property and how it passes data into the Dynamic Component.
export const token = new InjectionToken<string>(‘’);@Component({
selector: ‘app-tabs-container’,
templateUrl: ‘./tabs-container.component.html’,
styleUrls: [‘./tabs-container.component.css’],
})export class TabsContainerComponent implements OnInit {
constructor(private serv: DynamicService, private injector: Injector) {}public tabs: any = [{
tab: ‘Tab1’,
component: Tab1Component,
data: ‘This is the first Tab’,
isActive:true
},
{
tab: ‘Tab2’,
component: Tab2Component,
data: ‘This is the second tab’,
isActive:false
},
];public activeComponent: any;
public activeComponentData: string = ‘’;
public receivedData: string;
public myInjector: Injector;ngOnInit() {
this.serv.getObservable().subscribe((data) => {
this.receivedData = data;
});this.ldTab(this.tabs[0].component, this.tabs[0].data,0);
}setActiveTab(tabInd: number) {
this.tabs.forEach((tab, tabIndex) => {
tab.isActive = tabIndex === tabInd;
});
}ldTab(tabComponent: any, tabData: string,tabIndex:number) {
setTimeout(() => {this.setActiveTab(tabIndex);
this.receivedData = ‘’;
this.activeComponent = tabComponent;
this.activeComponentData = tabData;
this.createInjector();}, 0);
}createInjector() {
this.myInjector = Injector.create({
providers: [{ provide: token, useValue: this.activeComponentData }],
parent: this.injector,
});
}}
In this class, we have defined the array tabs which is an array of 2 objects. Each object contains 4 properties: tab,component,data and isActive.
tab contains the tab name to display in the template; component contains the component name corresponding to the tab i.e Tab1Component and Tab2Component ; data contains a small string which will be passed as input to the component; isActive tells us whether the tab is currently selected or not.
activeComponent is the dynamic component which is currently active.Eg: If I click on Tab1, then Tab1Component is the activeComponent.
activeComponentData is the data associated with the dynamic component which we will pass from the TabsContainerComponent to the dynamic component. This data is available in the data property of each object in the tabs array.
ngOnInit() {
this.serv.getObservable().subscribe((data) => {
this.receivedData = data;
});this.ldTab(this.tabs[0].component, this.tabs[0].data,0);
}
In the ngOnInit() lifecycle hook, we have subscribed to a Subject named output in the Dynamic service.
@Injectable()
export class DynamicService {
private output = new Subject<string>();
constructor() {}getObservable() {
return this.output.asObservable();
}outputFromDynamicComponent(data: string) {
this.output.next(data);
}
}
The purpose of the Subject is to assist the parent TabsContainerComponent to receive data from the Dynamic component.
<p>Data received from Tab Component: {{ receivedData }}</p>
receivedData contains the data which is sent from the dynamic component to the TabContainerComponent. We are displaying the receivedData as above in the TabContainerComponent.
On app load, we want Tab1Component to be created and its data to be displayed as well. Hence within the same lifecycle hook, we have called ldTab() method,passing the component,data properties of the first object of the tabs array and the tab index as arguments.
this.ldTab(this.tabs[0].component, this.tabs[0].data,0);
Now let’s jump to the ldTab() method.
ldTab(tabComponent: any, tabData: string) {
setTimeout(() => {this.setActiveTab(tabIndex);
this.receivedData = ‘’;
this.activeComponent = tabComponent;
this.activeComponentData = tabData;
this.createInjector();}, 0);
}
The purpose of enclosing the remaining code within a setTimeout() is to avoid the ExpressionChangedAfterChecked errors.
We are first calling setActiveTab() passing the tab index as argument in order to set the tab’s isActive property value. Based on this property value, we are adding or removing the CSS class activeTab using ngClass as shown below.
This CSS class just adds a background color to the active tab in order to differentiate.
<li *ngFor=”let x of tabs;let ind=index” class=”nav-item”>
<a [ngClass]="x.isActive ? 'activeTab' : ''" class=”nav-link” (click)=”ldTab(x.component, x.data,ind)”>{{x.tab}}</a>
</li>
We are next setting the receivedData property to an empty string so that on switching tabs, the previous tab’s data will be removed.
this.receivedData = ‘’;
activeComponent variable is set to the component name, which needs to be dynamically created.
activeComponentData variable contains the data associated with the component which will be dynamically created.
We are making a call to the createInjector() where we create our custom injector myInjector.
createInjector() {
this.myInjector = Injector.create({
providers: [{ provide: token, useValue: this.activeComponentData }],
parent: this.injector,
});
}
create() of the Injector class, creates an Injector instance myInjector.
What is the purpose of the injector?
A dependency is basically a class or an object that any class requires to perform its function. Angular creates a map of internal dependencies.
The key of this map is the Dependency Injection(DI) Token and the value of this map is the runtime value of the dependency. The injector first uses the DI token to locate the dependency value. The injector then instantiates the dependency and injects it into the class that requires the dependency.
providers: [{ provide: token, useValue: this.activeComponentData }]
The provide property contains the DI token. The 2nd property basically tells the injector how to create the dependency value. The 2nd property used here is useValue, but it could be useClass,useExisting etc depending on the scenario.
I would like to take your attention to the below line of code which is written before the @Component annotation.
export const token = new InjectionToken<string>(‘’);
We have created an InjectionToken object named token as the DI token. InjectionToken is useful to create DI tokens when no class dependency is required.
Thus the injector will associate token with the variable activeComponentData. This injector will be used in the dynamic component to access the data stored in activeComponentData.
Lets now check the Tab1Component and Tab2Component to see how we extract the dependency value from the injector instance and how the dynamic component sends data to the parent TabsContainerComponent.
Tab1Component Template:
<p>Data received from Parent Component:{{dataIn}}</p><button (click)=”sendData()”>Send data from Tab1 to Parent</button>
Tab1Component Class:
export class Tab1Component implements OnInit {public dataIn: string;constructor(private inject: Injector, private serv: DynamicService) {}ngOnInit() {
this.dataIn = this.inject.get(token);
}sendData() {
this.serv.outputFromDynamicComponent(‘Sent data from Tab1’);
}}
dataIn is the data received from the TabsContainerComponent. We have used the Injector instance inject to obtain the data(dependency value) based on the DI token(key), passed as argument to the get().
When we click on the Send Data from Tab1 to Parent button, we are calling the outputFromDynamicComponent() in the Dynamic service,passing the data as argument. This method passes data to the subject named output. The subject is subscribed to within the TabsContainerComponent to receive the data.
Tab2Component is just a replica of Tab1Component with small changes.
Tab2Component Template:
<p>Data received from Parent Component:{{dataIn}}</p><button (click)=”sendData()”>Send data from Tab2 to Parent</button>
Tab2Component Class:
export class Tab2Component implements OnInit {public dataIn: string;constructor(private inject: Injector, private serv: DynamicService) {}ngOnInit() {
this.dataIn = this.inject.get(token);
}sendData() {
this.serv.outputFromDynamicComponent(‘Sent data from Tab2’);
}}
You can find the working code at the below link: