11
votes

I'm developping a little UI components framework for my personal needs and for fun. I'm developping a Tab component and for testing purpose, I need to inject dynamically a component (TabContainerComponent) inside another component (TabComponent). Below, the code of my two components:

tab.component.ts:

import {Component, ContentChildren} from "@angular/core";
import {TabContainerComponent} from "./tabContainer.component";

@Component({
    selector: 'tab',
    templateUrl: 'tab.component.html'
})
export class TabComponent {

    @ContentChildren(TabContainerComponent)
    tabs: TabContainerComponent[];
}

tab.component.html:

<ul>
    <li *ngFor="let tab of tabs">{{ tab.title }}</li>
</ul>
<div>
    <div *ngFor="let tab of tabs">
        <ng-container *ngTemplateOutlet="tab.template"></ng-container>
    </div>
    <ng-content></ng-content>
</div>

tabContainer.component.ts:

import {Component, Input} from "@angular/core";

@Component({
    selector: 'tab-container',
    template: '<ng-container></ng-container>'
})
export class TabContainerComponent {

    @Input()
    title: string;

    @Input()
    template;
}

I used a ComponentFactoryResolver and a ComponentFactory to create dynamically my new component (TabContainerComponent), and inject it in a placeholder inside my other component (TabContainer), in the addTab method:

app.component.ts:

import {
    Component, ViewChild, ComponentFactoryResolver, ComponentFactory,
    ComponentRef, TemplateRef, ViewContainerRef
} from '@angular/core';
import {TabContainerComponent} from "./tabContainer.component";
import {TabComponent} from "./tab.component";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent {

    title = 'app';

    @ViewChild(TabComponent)
    tab: TabComponent;

    @ViewChild('tabsPlaceholder', {read: ViewContainerRef})
    public tabsPlaceholder: ViewContainerRef;

    @ViewChild('newTab')
    newTab: TemplateRef<any>;

    constructor(private resolver: ComponentFactoryResolver) {
    }

    addTab(): void {
        let factory: ComponentFactory<TabContainerComponent> = this.resolver.resolveComponentFactory(TabContainerComponent);
        let tab: ComponentRef<TabContainerComponent> = this.tabsPlaceholder.createComponent(factory);
        tab.instance.title = "New tab";
        tab.instance.template = this.newTab;
        console.log('addTab() triggered');
    }
}

The addMethod is triggered by clicking on the "Add tab" button:

app.component.html:

<button (click)="addTab()">Add tab</button>
<tab>
    <tab-container title="Tab 1" [template]="tab1"></tab-container>
    <tab-container title="Tab 2" [template]="tab2"></tab-container>
    <ng-container #tabsPlaceholder></ng-container>
</tab>
<ng-template #tab1>T1 template</ng-template>
<ng-template #tab2>T2 template</ng-template>
<ng-template #newTab>
    This is a new tab
</ng-template>

app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';


import { AppComponent } from './app.component';
import {TabContainerComponent} from "./tabContainer.component";
import {TabComponent} from "./tab.component";


@NgModule({
    declarations: [
        AppComponent,
        TabComponent,
        TabContainerComponent
    ],
    imports: [
        BrowserModule
    ],
    providers: [],
    bootstrap: [AppComponent],
    entryComponents: [
        TabContainerComponent
    ]
})
export class AppModule { }

When I click on the "Add tab" button, I'm able to see the console.log message and I'm able to see a new <tab-container> tag (but without any attribute, which is strange) inside the <tab> tag but Angular doesn't update the Tab component view (there is no <li> and <div> created). I tried also to check changes by implementing OnChanges interface in TabComponent class but without success.

Is anyone have an idea to solve my problem ?

P.S.: I don't want to use an array of TabContainer components in order to test the createComponent method.

Update:

Demo:

https://stackblitz.com/edit/angular-imeh71?embed=1&file=src/app/app.component.ts

2
Given that you will inject the same component every time, why not work around this issue with a ngFor ?user4676340
I inject the same component only for testing purpose. The injected component could be created by any logic in the app-componentJustin C.

2 Answers

3
votes

Here is the way I could make it work -

Tab.component.ts

Changed "tabs" from TabContainerComponent array to QueryList.

import { Component, ContentChildren, QueryList } from '@angular/core';
import { TabContainerComponent } from '../tab-container/tab-container.component';

@Component({
  selector: 'app-tab',
  templateUrl: 'tab.component.html'
})
export class TabComponent {
  @ContentChildren(TabContainerComponent)
  tabs: QueryList<TabContainerComponent>;

  constructor() {}
}

Added a new template in app.component.html

<ng-template #tabContainerTemplate>
  <app-tab-container title="New Tab" [template]="newTab"></app-tab-container>
</ng-template>

app.component.ts

import {
  Component,
  ViewChild,
  TemplateRef,
  ViewContainerRef,
  AfterViewInit,
  ViewChildren,
  QueryList,
  ChangeDetectorRef
} from '@angular/core';
import { TabContainerComponent } from './tab-container/tab-container.component';
import { TabComponent } from './tab/tab.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
  title = 'app';
  changeId: string;
  @ViewChild(TabComponent) tab: TabComponent;
  @ViewChild('tabsPlaceholder', { read: ViewContainerRef })
  public tabsPlaceholder: ViewContainerRef;
  @ViewChild('tabContainerTemplate', { read: TemplateRef })
  tabContainerTemplate: TemplateRef<null>;
  @ViewChildren(TabContainerComponent)
  tabList: QueryList<TabContainerComponent>;

  constructor(private changeDetector: ChangeDetectorRef) {}

  ngAfterViewInit() {}

  addTab(): void {
    this.tabsPlaceholder.createEmbeddedView(this.tabContainerTemplate);
    this.tab.tabs = this.tabList;
    this.changeDetector.detectChanges();
    console.log('addTab() triggered');
  }
}

Added a ViewChildren query for TabContainerComponent. In addTab() used createEmbeddedView to add new tab container component.

I thought that "ContentChildren" query in TabComponent should be updated by the newly added component but it doesn't. I have tried to subscribe to "changes" for the query list in TabCompoent but it doesn't get triggered.

But I observed that "ViewChildren" query in AppComponent got updated every time a new component is added. So I have assigned updated QueryList of app component to the QueryList of TabComponent.

The working demo is available here

2
votes

I have modified your code check this it is showing in view as you expected.

Stackblitz Demo

For Dynamic component Creating You need to create Embedded View using templateRef.

View Container provides API to create, manipulate and remove dynamic views.

For More Info About Dynamic Component Manipulation check this: https://blog.angularindepth.com/working-with-dom-in-angular-unexpected-consequences-and-optimization-techniques-682ac09f6866

  import {
    Component, ViewChild, ComponentFactoryResolver, ComponentFactory,
    ComponentRef, TemplateRef, ViewContainerRef,AfterViewInit
} from '@angular/core';
import {TabContainerComponent} from "./hello.component";
import {TapComponent} from "./tap/tap.component";

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {

    title = 'app';
   @ViewChild('vc',{read:ViewContainerRef}) vc:ViewContainerRef;
    @ViewChild(TapComponent)
    tab: TapComponent;

    @ViewChild('tabsPlaceholder', {read: ViewContainerRef})
    public tabsPlaceholder: ViewContainerRef;

    @ViewChild('newTab')
    newTab: TemplateRef<any>;

    constructor(private resolver: ComponentFactoryResolver) {
    }

  ngAfterViewInit(){


  }

    addTab(): void {
        let factory: ComponentFactory<TabContainerComponent> = this.resolver.resolveComponentFactory(TabContainerComponent);
        let tab: ComponentRef<TabContainerComponent> = this.tabsPlaceholder.createComponent(factory);
        tab.instance.title = "New tab";
        tab.instance.template = this.newTab;
        this.vc.createEmbeddedView(this.newTab);
        console.log('addTab() triggered');
    }
}