85

The current official docs only shows how to dynamically change components within an <ng-template> tag. https://angular.io/guide/dynamic-component-loader

What I want to achieve is, let's say I have 3 components: header, section, and footer with the following selectors:

<app-header>
<app-section>
<app-footer>

And then there are 6 buttons that will add or remove each component: Add Header, Add Section, and Add Footer

and when I click Add Header, the page will add <app-header> to the page that renders it, so the page will contain:

<app-header>

And then if I click Add Section twice, the page will now contain:

<app-header>
<app-section>
<app-section>

And if I click Add Footer, the page will now contain all these components:

<app-header>
<app-section>
<app-section>
<app-footer>

Is it possible to achieve this in Angular? Note that ngFor is not the solution I'm looking for, as it only allows to add the same components, not different components to a page.

EDIT: ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily.

EDIT 2: To make it more clear, let's have another example of why ngFor does not work. Let's say we have the following components:

<app-header>
<app-introduction>
<app-camera>
<app-editor>
<app-footer>

Now here comes a new component, <app-description>, which the user wants to insert in between and <app-editor>. ngFor works only if there is one same component that I want to loop over and over. But for different components, ngFor fails here.

6
  • If they are known, as in your example, you could use *ngIf on the header and footer and *ngFor on the sections. Commented Jul 6, 2017 at 4:57
  • @DeborahK ngIf and ngFor is not the solution I'm looking for as the templates are already predetermined. What I am looking for is something like a stack of components or an array of components where we can add, remove, and change any index of the array easily. Commented Jul 6, 2017 at 6:38
  • This might also help if you want to add different kinds of components stackoverflow.com/questions/36325212/… Commented Jul 6, 2017 at 6:57
  • @GünterZöchbauer Thanks! This might be especially useful as I get to know ngComponentOutlet Commented Jul 6, 2017 at 8:10
  • ngComponentOutlet is still lacking some basic features. There is a pull request to address these since a long time but it wasn't yet merged AFAIK. So reading and passing values from/to dynamically added components doesn't work well. Commented Jul 6, 2017 at 8:11

2 Answers 2

97

What you're trying to achieve can be done by creating components dynamically using the ComponentFactoryResolver and then injecting them into a ViewContainerRef. One way to do this dynamically is by passing the class of the component as an argument of your function that will create and inject the component.

See example below:

import {
  Component,
  ComponentFactoryResolver, Type,
  ViewChild,
  ViewContainerRef
} from '@angular/core';

// Example component (can be any component e.g. app-header app-section)
import { DraggableComponent } from './components/draggable/draggable.component';

@Component({
  selector: 'app-root',
  template: `
    <!-- Pass the component class as an argument to add and remove based on the component class -->
    <button (click)="addComponent(draggableComponentClass)">Add</button>
    <button (click)="removeComponent(draggableComponentClass)">Remove</button>

    <div>
      <!-- Use ng-template to ensure that the generated components end up in the right place -->
      <ng-template #container>

      </ng-template>
    </div>

  `
})
export class AppComponent {
  @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef;

  // Keep track of list of generated components for removal purposes
  components = [];

  // Expose class so that it can be used in the template
  draggableComponentClass = DraggableComponent;

  constructor(private componentFactoryResolver: ComponentFactoryResolver) {
  }

  addComponent(componentClass: Type<any>) {
    // Create component dynamically inside the ng-template
    const componentFactory = this.componentFactoryResolver.resolveComponentFactory(componentClass);
    const component = this.container.createComponent(componentFactory);

    // Push the component so that we can keep track of which components are created
    this.components.push(component);
  }

  removeComponent(componentClass: Type<any>) {
    // Find the component
    const component = this.components.find((component) => component.instance instanceof componentClass);
    const componentIndex = this.components.indexOf(component);

    if (componentIndex !== -1) {
      // Remove component from both view and array
      this.container.remove(this.container.indexOf(component));
      this.components.splice(componentIndex, 1);
    }
  }
}

Notes:

  1. If you want to make it easier to remove the components later on, you can keep track of them in a local variable, see this.components. Alternatively you can loop over all the elements inside the ViewContainerRef.

  2. You have to register your component as an entry component. In your module definition register your component as an entryComponent (entryComponents: [DraggableComponent]).

Running example: https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5

For more information: https://angular.io/guide/dynamic-component-loader

Sign up to request clarification or add additional context in comments.

7 Comments

Thanks! This might be the solution I am looking for but I haven't tried it out yet. Would you be so kind as to provide a Plunker?
I have added a plunker to the answer. Please see https://plnkr.co/edit/mrXtE1ICw5yeIUke7wl5. Hope this helps.
I edited the Plunker to include different components (foo and bar) and it works! Thank you very much! plnkr.co/edit/tMR2ahkb5VRpWCIh2Jvb?p=preview
Excellent example and code. I was searching for exactly this. I would just suggest editing this "const component = this.components.find…" to "const component = this.components.reverse().find…" in order to remove last inserted component. Or add id's to them to track specific ones.
There is also a tutorial about this here: courses.ultimateangular.com/courses/angular-pro/lectures/…
|
65

Angular v13 or above - simple way to add dynamic components to DOM

parent.component.html

<ng-template #viewContainerRef></ng-template>

parent.component.ts

@ViewChild("viewContainerRef", { read: ViewContainerRef }) vcr!: ViewContainerRef;
ref!: ComponentRef<YourChildComponent>

addChild() {
  this.ref = this.vcr.createComponent(YourChildComponent)
}

removeChild() {
  const index = this.vcr.indexOf(this.ref.hostView)
  if (index != -1) this.vcr.remove(index)
}

Angular v12 or below

I have created a demo to show the dynamic add and remove process. The parent component creates the child components dynamically and removes them.

Click for demo

Parent Component

// .ts
export class ParentComponent {
  @ViewChild("viewContainerRef", { read: ViewContainerRef })
  VCR: ViewContainerRef;

  child_unique_key: number = 0;
  componentsReferences = Array<ComponentRef<ChildComponent>>()

  constructor(private CFR: ComponentFactoryResolver) {}

  createComponent() {
    let componentFactory = this.CFR.resolveComponentFactory(ChildComponent);

    let childComponentRef = this.VCR.createComponent(componentFactory);

    let childComponent = childComponentRef.instance;
    childComponent.unique_key = ++this.child_unique_key;
    childComponent.parentRef = this;

    // add reference for newly created component
    this.componentsReferences.push(childComponentRef);
  }

  remove(key: number) {
    if (this.VCR.length < 1) return;

    let componentRef = this.componentsReferences.filter(
      x => x.instance.unique_key == key
    )[0];

    let vcrIndex: number = this.VCR.indexOf(componentRef as any);

    // removing component from container
    this.VCR.remove(vcrIndex);

    // removing component from the list
    this.componentsReferences = this.componentsReferences.filter(
      x => x.instance.unique_key !== key
    );
  }
}

// .html
<button type="button" (click)="createComponent()">
    I am Parent, Create Child
</button>
<div>
    <ng-template #viewContainerRef></ng-template>
</div>

Child Component

// .ts
export class ChildComponent {

  public unique_key: number;
  public parentRef: ParentComponent;

  constructor() {
  }

  remove_me() {
    console.log(this.unique_key)
    this.parentRef.remove(this.unique_key)
  }
}

// .html
<button (click)="remove_me()">I am a Child {{unique_key}}, click to Remove</button>

6 Comments

You could replace .filter(x => x.instance.index == index)[0] by .find(...)
Yeah, you're right but that were old days buddy when I wrote this ;)
This solution not work with Ivy=enabled . See stackblitz.com/edit/angular-ivy-tdqan6 . It is fails on removing the component
I made it work: stackblitz.com/edit/angular-ivy-k6nqph . I changed in remove method let vcrIndex: number = this.VCR.indexOf(componentRef) with let vcrIndex: number = this.VCR.indexOf(componentRef.hostView);
I would use .destroy() on the component over using index of to remove it from the container.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.