How to load a component dynamically
Component templates are not always fixed. An application may need to load new components at runtime.
Build a component to be loaded dynamically
Feature - creating a new component to display current time with a random background-color when clicking the button each time.
A component to display current time
it('should display current time', () => {
fixture = createGenericTestComponent(`
<display-time></display-time>
`, TestComponent);
const element = fixture.debugElement.query(By.css('.create-time')).nativeElement;
expect(element.textContent).toBeTruthy();
});
Set background-color with bgColor
property
it('should set background-color with bgColor property', () => {
fixture = createGenericTestComponent(`
<display-time bgColor="green"></display-time>
`, TestComponent);
const element = fixture.debugElement.query(By.css('.create-time')).nativeElement;
expect(element.style.backgroundColor).toBe('green');
});
Fix error specs and finish the component
import { Component, OnInit, Input } from '@angular/core';
import * as moment from 'moment';
@Component({
selector: 'display-time',
templateUrl: './display-time.component.html',
styleUrls: ['./display-time.component.scss']
})
export class DisplayTimeComponent implements OnInit {
createTime: string;
@Input() bgColor: string;
ngOnInit() {
this.createTime = moment().format();
}
}
<p class="create-time" [style.backgroundColor]="bgColor">
</p>
Build a parent component with a button
Feature - component with a button and a place holder to load DisplayTimeComponent
Displaying a button
it('should display with a button', () => {
fixture = createGenericTestComponent(`
<holder></holder>
`, TestComponent);
const buttonElement = fixture.debugElement.query(By.css('button'));
expect(buttonElement).toBeTruthy('display a button');
});
Loading DisplayTimeComponent instance in the placeholder
it('should load DisplayTimeComponent in the placeholder', () => {
fixture = createGenericTestComponent(`
<holder></holder>
`, TestComponent);
const placeholder = fixture.debugElement.query(By.css('.create-time'));
expect(placeholder.nativeElement.textContent).toBeTruthy('display create time');
});
Fix error specs
Fill the template file with a button and a placeholder
<button type="button" class="btn btn-primary">button</button>
<ng-template></ng-template>
How can we get reference to the placeholder
? - We create a directive to do this.
The directive will be added to ng-template
element.
It will be injected with a ViewContainerRef
that reference to the placeholder element.
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[dtPlaceholder]'
})
export class DtPlaceholderDirective {
constructor(viewContainerRef: ViewContainerRef) { }
}
Add the directive selector to ng-template
element.
<ng-template class="placeholder" dtPlaceholder></ng-template>
We get the reference of DtPlaceholderDirective
with @ViewChild
.
@ViewChild(DtPlaceholderDirective) dtPlaceholder: DtPlaceholderDirective;
Then we can get the reference of placeholder
with dtPlaceholder.viewContainerRef
When the hold component is created, we use ComponentFactoryResolver
to load a DisplayTimeComponent
instance inside placeholder
Add entryComponents
to the module decorator first.
Error: No component factory found for DisplayTimeComponent. Did you add it to @NgModule.entryComponents
A TestModule
need to be create for define entryComponents
for TestBed, because TestBed.configureTestingModule
do not support entryComponents option.
@NgModule({
declarations: [DisplayTimeComponent],
entryComponents: [
DisplayTimeComponent,
]
})
class TestModule {}
TestBed.configureTestingModule({
imports: [TestModule],
declarations: [TestComponent, HolderComponent]
});
Finally, tests passed and the whole implement is:
import { Component, OnInit, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { DtPlaceholderDirective } from 'app/demo-components/load-component/dt-placeholder.directive';
import { DisplayTimeComponent } from 'app/demo-components/load-component/display-time/display-time.component';
@Component({
selector: 'holder',
templateUrl: './holder.component.html',
styleUrls: ['./holder.component.scss']
})
export class HolderComponent implements OnInit {
@ViewChild(DtPlaceholderDirective) dtPlaceholder: DtPlaceholderDirective;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngOnInit() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(DisplayTimeComponent);
const placeholderReference = this.dtPlaceholder.viewContainerRef;
const componentRef = placeholderReference.createComponent(componentFactory);
componentRef.instance.bgColor = 'yellow';
}
}
- Inject
ComponentFactoryResolver
for creating component instance. - Get child
DtPlaceholderDirective
instance with@ViewChild
. - Get
componentFactory
with methodcomponentFactoryResolver.resolveComponentFactory
- Get the reference to
ng-template
that contains the attribute directive throughdtPlaceholder.viewContainerRef
. - Load component dynamically with
placeholderReference.createComponent(componentFactory)