DEV Community

Cover image for Dynamic components right way
Marko Berger
Marko Berger

Posted on • Edited on

Dynamic components right way

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.
Old TV
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

GitHub logo 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)

Collapse
 
banderberg profile image
Barry Anderberg

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.

Collapse
 
blackmamba profile image
The Black Mamba🔥

Thanks for this, I was searching for dynamic components a week back

Collapse
 
bergermarko profile image
Marko Berger

You are welcome.

Collapse
 
bobscoe profile image
bobscoe

is it possible to load an entire Angular module from a remote url using this same approach?