For all of you young people, there was a time when TV had no remote. And you had to press some mechanical buttons to switch the channel.
In those times there were only 3 channels and Tv had 8 channel buttons. Now the time has passed a few years. Now we have 8 channels. No problem but we see there are no more slots anymore. That's right “Houston we have a problem”.
Now imagine we have a component like that. Component that by some condition switches and loads other presenter components in a template with ngSwitch. By adding new presenter components your template will become unreadable and just ugly. So what to do. We need to load as many different kinds of presenter components that we like in our parent component and make our template clean and readable. Not to mention that we need communication between them in both ways. The solution is angular dynamic components.Official documentation
Let's start
You can pull working an example project from this GitHub repo.
I will not go over how to create a new angular project but jump to the interesting part and try to explain it. Life is to short :)
First things first. We have to create a directive that will expose our placeholder reference.
ng c d placeholder
import { Directive, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appPlaceholder]'
})
export class PlaceholderDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
The main thing in this directive is that we are making ViewContainerRef public so that we can access it from outside.
Magic time
ng g c dynamic
dynmaic.component.ts --> Explanations are in the comments
import { Component, OnInit, Input, ComponentFactoryResolver, ViewChild, OnChanges, ChangeDetectionStrategy, SimpleChanges, Output, EventEmitter } from '@angular/core';
import { Component1Component } from '../component1/component1.component';
import { PlaceholderDirective } from '../placeholder.directive';
import { Component2Component } from '../component2/component2.component';
import { IComp } from '../icomp';
@Component({
selector: 'app-dynamic',
templateUrl: './dynamic.component.html',
styleUrls: ['./dynamic.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DynamicComponent implements OnInit, OnChanges{
/**Here we grab reference placeholder directive */
@ViewChild(PlaceholderDirective, {static: true}) placeholder: PlaceholderDirective;
@Input() inputData: number;
@Output() changeEmit: EventEmitter<string> = new EventEmitter<string>();
/**An array where we register what component we want to load */
components = [Component1Component, Component2Component];
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
/**
* We are using this hook for change detection and invoking loadComponent() method
* There are more ways to do this but for the simplicity of this example I have decided on this way
*/
if(changes.inputData.currentValue != undefined){ // We need to check if inputData has some value
this.loadComponent();
}
}
/**
* A method that loads and creates instances of components
*/
loadComponent(){
/** Preparing our component for creation */
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.components[this.inputData]);
/** Grabbing reference of our view placeholder */
const viewContainerRef = this.placeholder.viewContainerRef;
/** Clearing our placeholder */
viewContainerRef.clear();
/** Magic of creating a component instance */
const componentRef = viewContainerRef.createComponent(componentFactory);
/**
* @Input data into our instance.
*/
(componentRef.instance as IComp).text = ''+this.inputData;
/** @Output data from our instance */
(componentRef.instance as IComp).event.subscribe(
data => this.changeEmit.emit(data)
);
}
}
Important -- you have to import presenter components as entryComponents to work.
dynamaic.component.html
<ng-template appPlaceholder></ng-template>
icomp.ts --> interface for this example
export interface IComp {
text: string;
event: any;
}
Now let us see the component that we want to load.
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { IComp } from '../icomp';
@Component({
selector: 'app-component1',
templateUrl: './component1.component.html',
styleUrls: ['./component1.component.css']
})
export class Component1Component implements OnInit, IComp {
@Output() event = new EventEmitter<string>();
@Input() text: string;
textbox: string;
constructor() { }
ngOnInit() {
}
onEmit(){
this.event.emit(this.textbox);
}
}
Nothing special. Right. Only that we are implementing our interface for standardization of output and inputs.
That's it.
Github Repo
ikonezg / angular-dynamic-example
Angular Dynamic example
DynamicExample
This project was generated with Angular CLI version 7.3.7.
Development server
Run ng serve
for a dev server. Navigate to http://localhost:4200/
. The app will automatically reload if you change any of the source files.
Code scaffolding
Run ng generate component component-name
to generate a new component. You can also use ng generate directive|pipe|service|class|guard|interface|enum|module
.
Build
Run ng build
to build the project. The build artifacts will be stored in the dist/
directory. Use the --prod
flag for a production build.
Running unit tests
Run ng test
to execute the unit tests via Karma.
Running end-to-end tests
Run ng e2e
to execute the end-to-end tests via Protractor.
Further help
To get more help on the Angular CLI use ng help
or go check out the Angular CLI README.
Top comments (4)
This is only useful for a dynamic component with a single changing input. If the component has more than one changing input then ngOnChanges will keep firing and you will end up re-creating the component over and over.
Thanks for this, I was searching for dynamic components a week back
You are welcome.
is it possible to load an entire Angular module from a remote url using this same approach?