10

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

Justin C.
  • 161
  • 1
  • 1
  • 9
  • Given that you will inject the same component every time, why not work around this issue with a `ngFor` ? –  Jul 24 '18 at 10:11
  • I inject the same component only for testing purpose. The injected component could be created by any logic in the app-component – Justin C. Jul 24 '18 at 13:15

2 Answers2

3

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

byj
  • 228
  • 1
  • 7
  • Thanks for your solution. It's working as expected. The key is to force the update the tabs property of the TabComponent by replacing with the one from the AppComponent and do not forget to detect changes using the ChangeDetectorRef (otherwise, the view will not be updated each time you click on the button). It is not the more elegant solution on my opinion but it solves my issue and it seems to be the only existing solution ... – Justin C. Jul 30 '18 at 14:21
  • 2
    Honestly, I did not want to force update from AppComponent to TabComponent. I was expecting the ContentChildren query to be updated each time after new component is added but it was not getting updated. I have tried some other ways in TabComponent, like in nOnAfterContentInit() subscribe to tabList changes (`this.tabs.changes.subscribe()`) but nothing worked. So finally I had to force the assignment from App Component to TabComponent. – byj Jul 30 '18 at 14:35
2

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');
    }
}
Chellappan வ
  • 15,213
  • 2
  • 16
  • 42
  • it is bcoz we are not creating new embedded view after every click now it is working fine check – Chellappan வ Jul 24 '18 at 13:02
  • The purpose is not to create a view after the Tab component. I would like to inject a new component (TabContainer) which is only a kind of data container only (its template is almost empty, just a because the template cannot be empty). This injected component should be detected by the Tab component and, this latest should update its view in order to create the corresponding
  • and
    .
  • – Justin C. Jul 24 '18 at 13:14
  • if you want to show dynamically created component you need to use view unless it will create unwanted result. check that blog you will understand – Chellappan வ Jul 24 '18 at 13:38
  • @Chellappan I read the blog you posted the link but unfortunately, it doesn't help me to solve my issue. Maybe it doesn't work because I inject the TabContainerComponent through a ng-container ? As it is not injected directly via the TabComponent, maybe this latest doesn't detect the new TabContainerComponent ? I have to use a ng-container because, if I use directly a reference to the TabComponent, with the createComponent method, the TabContainerComponent are injected after the TabComponent – Justin C. Jul 25 '18 at 12:37