I'm trying to setup a tab system that allows for components to register themselves (with a title). The first tab is like an inbox, there's plenty of actions/link items to choose from for the users, and each of these clicks should be able to instantiate a new component, on click. The actions / links comes in from JSON.

The instantiated component will then register itself as a new tab.

I'm not sure if this is the 'best' approach? Sofar the only guides I've seen are for static tabs, which doesn't help.

So far I've only got the tabs service which is bootstrapped in main to persist throughout the app, looks ~something like this.

export interface ITab { title: string; }

@Injectable()
export class TabsService {
    private tabs = new Set<ITab>();

    addTab(title: string): ITab {
        let tab: ITab = { title };
        this.tabs.add(tab);
        return tab;
    }

    removeTab(tab: ITab) {
        this.tabs.delete(tab);
    }
}

Questions:

1) How can I have a dynamic list in the inbox that creates new (different) tabs? I am sort of guessing the DynamicComponentBuilder would be used?

2) How can the components created from the inbox (on click) register themselves as tabs and also be shown? I'm guessing ng-content but I can't find much info on how to use it

Edit: Try to clarify

Think of the inbox as a mail inbox, items are fetched as JSON and displays several items. Once one of the items is clicked, a new tab is created with that items action 'type'. The type is then a component

Edit2: Image

http://i.imgur.com/yzfMOXJ.png

upvote
  flag
If the components shown in the tabs aren't known at build time, then DCL is the right approach. – Günter Zöchbauer
5 upvote
  flag
I dont understand your requirement clearly so hard tell you anything without working code/plunker. Look this if it can help you somewhere plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (I don't know if its relevant or not) – micronyks
upvote
  flag
@micronyks I think you got the wrong link – Cuel
upvote
  flag
check now....... – micronyks
upvote
  flag
@Cuel, you got the solution? – Sunil Garg
upvote
  flag
@SunilGarg yes, see the accepted answer – Cuel

3 Answers 11

up vote 183 down vote accepted

update

ngComponentOutlet was added to 4.0.0-beta.3

update

There is a NgComponentOutlet work in progress that does something similar https://github.com/angular/angular/pull/11235

RC.7

Plunker example RC.7

// Helper component to add dynamic components
@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target: ViewContainerRef;
  @Input() type: Type<Component>;
  cmpRef: ComponentRef<Component>;
  private isViewInitialized:boolean = false;

  constructor(private componentFactoryResolver: ComponentFactoryResolver, private compiler: Compiler) {}

  updateComponent() {
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      // when the `type` input changes we destroy a previously 
      // created component before creating the new one
      this.cmpRef.destroy();
    }

    let factory = this.componentFactoryResolver.resolveComponentFactory(this.type);
    this.cmpRef = this.target.createComponent(factory)
    // to access the created instance use
    // this.compRef.instance.someProperty = 'someValue';
    // this.compRef.instance.someOutput.subscribe(val => doSomething());
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Usage example

// Use dcl-wrapper component
@Component({
  selector: 'my-tabs',
  template: `
  <h2>Tabs</h2>
  <div *ngFor="let tab of tabs">
    <dcl-wrapper [type]="tab"></dcl-wrapper>
  </div>
`
})
export class Tabs {
  @Input() tabs;
}
@Component({
  selector: 'my-app',
  template: `
  <h2>Hello {{name}}</h2>
  <my-tabs [tabs]="types"></my-tabs>
`
})
export class App {
  // The list of components to create tabs from
  types = [C3, C1, C2, C3, C3, C1, C1];
}
@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App, DclWrapper, Tabs, C1, C2, C3],
  entryComponents: [C1, C2, C3],
  bootstrap: [ App ]
})
export class AppModule {}

See also angular.io DYNAMIC COMPONENT LOADER

older versions xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

This changed again in Angular2 RC.5

I will update the example below but it's the last day before vacation.

This Plunker example demonstrates how to dynamically create components in RC.5

Update - use ViewContainerRef.createComponent()

Because DynamicComponentLoader is deprecated, the approach needs to be update again.

@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target;
  @Input() type;
  cmpRef:ComponentRef;
  private isViewInitialized:boolean = false;

  constructor(private resolver: ComponentResolver) {}

  updateComponent() {
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
   this.resolver.resolveComponent(this.type).then((factory:ComponentFactory<any>) => {
      this.cmpRef = this.target.createComponent(factory)
      // to access the created instance use
      // this.compRef.instance.someProperty = 'someValue';
      // this.compRef.instance.someOutput.subscribe(val => doSomething());
    });
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Plunker example RC.4
Plunker example beta.17

Update - use loadNextToLocation

export class DclWrapper {
  @ViewChild('target', {read: ViewContainerRef}) target;
  @Input() type;
  cmpRef:ComponentRef;
  private isViewInitialized:boolean = false;

  constructor(private dcl:DynamicComponentLoader) {}

  updateComponent() {
    // should be executed every time `type` changes but not before `ngAfterViewInit()` was called 
    // to have `target` initialized
    if(!this.isViewInitialized) {
      return;
    }
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }
    this.dcl.loadNextToLocation(this.type, this.target).then((cmpRef) => {
      this.cmpRef = cmpRef;
    });
  }

  ngOnChanges() {
    this.updateComponent();
  }

  ngAfterViewInit() {
    this.isViewInitialized = true;
    this.updateComponent();  
  }

  ngOnDestroy() {
    if(this.cmpRef) {
      this.cmpRef.destroy();
    }    
  }
}

Plunker example beta.17

original

Not entirely sure from your question what your requirements are but I think this should do what you want.

The Tabs component gets an array of types passed and it creates "tabs" for each item in the array.

@Component({
  selector: 'dcl-wrapper',
  template: `<div #target></div>`
})
export class DclWrapper {
  constructor(private elRef:ElementRef, private dcl:DynamicComponentLoader) {}
  @Input() type;

  ngOnChanges() {
    if(this.cmpRef) {
      this.cmpRef.dispose();
    }
    this.dcl.loadIntoLocation(this.type, this.elRef, 'target').then((cmpRef) => {
      this.cmpRef = cmpRef;
    });
  }
}

@Component({
  selector: 'c1',
  template: `<h2>c1</h2>`

})
export class C1 {
}

@Component({
  selector: 'c2',
  template: `<h2>c2</h2>`

})
export class C2 {
}

@Component({
  selector: 'c3',
  template: `<h2>c3</h2>`

})
export class C3 {
}

@Component({
  selector: 'my-tabs',
  directives: [DclWrapper],
  template: `
  <h2>Tabs</h2>
  <div *ngFor="let tab of tabs">
    <dcl-wrapper [type]="tab"></dcl-wrapper>
  </div>
`
})
export class Tabs {
  @Input() tabs;
}


@Component({
  selector: 'my-app',
  directives: [Tabs]
  template: `
  <h2>Hello {{name}}</h2>
  <my-tabs [tabs]="types"></my-tabs>
`
})
export class App {
  types = [C3, C1, C2, C3, C3, C1, C1];
}

Plunker example beta.15 (not based on your Plunker)

There is also a way to pass data along that can be passed to the dynamically created component like (someData would need to be passed like type)

    this.dcl.loadIntoLocation(this.type, this.elRef, 'target').then((cmpRef) => {
  cmpRef.instance.someProperty = someData;
  this.cmpRef = cmpRef;
});

There is also some support to use dependency injection with shared services.

For more details see https://angular.io/docs/ts/latest/cookbook/dynamic-component-loader.html

upvote
  flag
Thanks, this helps a lot. Here's an image of what I need to create i.imgur.com/yzfMOXJ.png Note that item1,2,3 are also dynamic / loaded with JSON What I need to figure out at the moment is the link between clicking on something in the inbox and instantiating that component – Cuel
upvote
  flag
In that case, how about a service that 'take'/registers components, and then are mounted as per the above solution? Would that be a plausible solution? – Cuel
1 upvote
  flag
Sure, you just need to get the components type to the DclWrapper to make it create an actual instance. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer when using wrapper, the layer is being created between container and nested component. And there won't be direct communication between container and nested component. Seems like the wrapper will always be link between two, isn't it? – Ammar Khan
upvote
  flag
@AmmarKhan Not sure what you mean. With DynamicComponentLoader there isn't any communication anyway. The only way is a shared service. The wrapper doesn't make a difference here. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer Let me brief you what I meant, The wrapper is responsible for loading component of all tabs. Let say if any of the tab require an input from Tabs component, how would that be pass? And if any of the nested component (C1, C2 or C3) want to notify about something to Tabs component. How that could be done? – Ammar Khan
upvote
  flag
You just can't have inputs or outputs on components added using DynamicComponentLoader no matter if it is wrapped or not. As mentioned in my previous comment. The only option are services angular.io/docs/ts/latest/cookbook/… – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer right, for my specific scenario, I am trying to create hierarchical grid myself similar to this one demos.telerik.com/kendo-ui/grid/hierarchy. And the problem I am having right now, how to load nested component inside my container component since that require input and output both. So definitely DynamicComponentLoader won't be a solution. Any suggestion? – Ammar Khan
upvote
  flag
I don't think there is a solution. Either you add it statically than you can use @Input()/@Output() or you use DynamicComponentLoader with services. – Günter Zöchbauer
upvote
  flag
why do you destroy component within updateComponent()? – Namek
upvote
  flag
How would you pass data through to the dynamic component? – Michael
upvote
  flag
There are two ways. You can use a shared service or you can pass it to the wrapper and set it on the dynamically added comonent using this.cmpRef.instance.someProperty = this.someInput (similar for observables (this.cmpRef.instance.someObservable.subscribe(val => this.someOutput.emit(val))). The later approach only works if all dynamically added components have the same properties to set. – Günter Zöchbauer
upvote
  flag
@Namek I destroy the one previously added before I add a new one. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer will you update your answer? Also I wanted to ask, is it possible to load component with either ComponentResolver or ComponentFactoryResolver by having only components metadata. I mean for example only selector element ? – kuldarim
upvote
  flag
Sure, I'll update the answer but I have first to investigate myself. I'm pretty sure there is no way. You would probably need to maintain some map where you can lookup the component type by selector name. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer, thank you for your great answer! You're like omnipresent regarding this issue on here and GitHub :) – Kel
upvote
  flag
Can you explain what this line does? this.target.createComponent. How does it bind the component to the dom? – dopatraman
upvote
  flag
this.target is a ViewContainerRef acquired from a DOM element (the element with the template variable #target). The created component will be added as a sibling to this element (not a child). – Günter Zöchbauer
upvote
  flag
Is it possible to add animations between the component transitions? Can I load a new component dynamically with animations? – galvan
upvote
  flag
I haven't tried myself but I'm pretty sure with the new animation module this will work. You have to maintain two components at the same time. When one component is added, you add the 2nd one and then animate the transition, then you remove the first one. Something like that. – Günter Zöchbauer
upvote
  flag
Is there a way to remove the extra div that floats inside of the wrapper (the div that the #target is attached to). I end up with <div></div><my-dynamic-component>contents</my-dynamic-compon‌​ent> – Joseph
1 upvote
  flag
@Joseph You can inject ViewContainerRef instead of using ViewChild, then <dcl-wrapper> itself becomes the target. Elements are added as siblings of the target and will therefore be outside <dcl-wrapper> this way. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer Do you have a quick code snippet for this? I'm not following something. I was able to get the Div removed from the wrapper by using <template #target></template, but its still a child of dcl-wrapper. I could live with this, but replacing dcl-wrapper with the dynamic component would be ideal. – Joseph
1 upvote
  flag
Replacing is not supported. You can change the template to '' (empty string)` and change the constructor to constructor(private target:ViewContainerRef) {}, then dynamically added components become siblings of <dcl-wrapper> – Günter Zöchbauer
1 upvote
  flag
I am using RC4 and the example was quite useful. Only thing that i wanted to mention is i had to add below code for binding to work properly this.cmpRef.changeDetectorRef.detectChanges(); – Rajee
upvote
  flag
I my case I cannot declare the @ViewChild beforehand since some of the content is fetched after user interaction and replaced in the DOM, this replacement code can contain angular components that then need to be initialised. My old approach was to fetch the elements with jquery and then use DynamicComponentLoader to attach angular components to them. How would that work now? – Tom
upvote
  flag
There is some way to pass a CSS selector as string but I haven't seen it working. Did you check if this post contains anything useful //allinonescript.com/a/37044960/217408 ? – Günter Zöchbauer
upvote
  flag
How would you modify the instance to pass in data to the component types? For instance, if C1, C2, or C3 had their own @Inputs... – Jason Wall
upvote
  flag
@Input() is ignored in this case. They are just class properties. I don't think there is a general solution. It depends on your use case. You can for example pass a single complex objects of any shape customized for the target component or you can make the dynamic component provide some meta-data about what properties it supports that you read when you create the component. – Günter Zöchbauer
1 upvote
  flag
upvote
  flag
@lqbweb thanks a lot :) – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer any idea on how would you apply dynamically Directives to the view container ("#target in your example")? I mean it is clear that for Components you have the ComponentFactoryResolver, and createComponent, but I do not think they work for just Directives right? – lqbweb
1 upvote
  flag
Dynamicylly adding/removing directives is (currently?) not possible. Only components. – Günter Zöchbauer
3 upvote
  flag
I got an error when the dynamic component had another dynaimc component when using ngAfterViewInit. Changed to ngAfterContentInit instead and now it is working with nested dynamic components – Abris
upvote
  flag
I don't really understand how to use this in my case. Can you show an example where you receive a message with links from internet and then you create a list with messages and links (binding the click event)? :S – Eusthace
upvote
  flag
@Eusthace there is no need for this approach if you want to show the same thing for each link you receive. Just use <a *ngFor="let link of links" [href]="link"> or similar (where links contians the linkes you received from internet). – Günter Zöchbauer
upvote
  flag
No, its not the case. Its a chat app. Users can send messages with links. I need to detect links and then create the specific links, so they can open the links in a external browser. :( – Eusthace
upvote
  flag
upvote
  flag
How would be something like a service that loads components on demand to the dlcWraper? – Mijail Dan Cohen
upvote
  flag
@MijailDanCohen I'd suggest you create a new question with more details about what you try to accomplish. – Günter Zöchbauer
1 upvote
  flag
just to note that mentioned PR has been taken over and superseded by a newer one, currently waiting for reviews – superjos
upvote
  flag
@GünterZöchbauer ngComponentOutlet is saving my life. I am midway implementing it, but I think it can be the right solution to my problem. I was trying to approach this with @Pipe. If you are interested check this: //allinonescript.com/questions/43143956/… – SrAxi
upvote
  flag
I just saw your question and answer. I think it's the right approach. – Günter Zöchbauer
1 upvote
  flag
@GünterZöchbauer Thanks! :D I will be updating my answer to be as precise as possible! – SrAxi
upvote
  flag
@GünterZöchbauer Thanks for the answer. This approach works fine for me. But I have a problem in accessing back the component's instances once its created and added. For e.g, say I want to save all the information entered in components C1, C2, C3. How I will access back these component's instances? – smhnkmr
upvote
  flag
@smhnkmr The commented out code in the example below RC.7 shows how you can read from or write to such components. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer Thanks. Yes, the commented code shows how I can access the component's instance immediately after creating it. My clarification is mainly on, how we get the instances after sometime, say I have added 5 components dynamically. Now I let the user to add/edit the component's controls say input box. Now I have to retrive all the components to get the values entered by user. How to achieve that? – smhnkmr
upvote
  flag
@smhnkmr I think you should create a new question that contains the code that demonstrates what you try to accomplish, what you tried and where you failed. – Günter Zöchbauer
upvote
  flag
"Because DynamicComponentLoader is deprecated, the approach needs to be update again," translates to that Angular is still immature, and has no place in production work. – David A. Gray
upvote
  flag
@DavidA.Gray it's not only deprecated, it's long removed and the example on the top does use the new way. – Günter Zöchbauer
upvote
  flag
"NgComponentOutlet requires a component type, if a falsy value is set the view will clear and any existing component will get destroyed." How to load a component on click but not destroy the previous one? – Chris Tarasovs
upvote
  flag
Is there anyway to add directive to dynamically created components? as this is what I need to achieve <someselector (someMethod)="doSomething()" [someProperty]="property"></someselector> – hngdev
upvote
  flag
@hngdev no, directives can only be added statically to a components template. You can create a component at runtime like explained in//allinonescript.com/questions/38888008/how-can-i-use‌​-create-dynamic-temp‌​late-to-compile-dyna‌​mic-component-with-a‌​ngular. I don't seeadirective in your example code. – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer sorry, I mean the property and method in the component. – hngdev
upvote
  flag
@hngdev event and value bindings are not supported. The commented-out code in the first code block shows how you can work around – Günter Zöchbauer
upvote
  flag
@GünterZöchbauer I tried that but it doesn't work for me, so I tried a different approach using nd-dynamic-component – hngdev

I'm not cool enough for comments. I fixed the plunker from the accepted answer to work for rc2. Nothing fancy, links to the CDN were just broken is all.

'@angular/core': {
  main: 'bundles/core.umd.js',
  defaultExtension: 'js'
},
'@angular/compiler': {
  main: 'bundles/compiler.umd.js',
  defaultExtension: 'js'
},
'@angular/common': {
  main: 'bundles/common.umd.js',
  defaultExtension: 'js'
},
'@angular/platform-browser-dynamic': {
  main: 'bundles/platform-browser-dynamic.umd.js',
  defaultExtension: 'js'
},
'@angular/platform-browser': {
  main: 'bundles/platform-browser.umd.js',
  defaultExtension: 'js'
},

https://plnkr.co/edit/kVJvI1vkzrLZJeRFsZuv?p=preview

there is component ready to use (rc5 compatible) ng2-steps which uses Compiler to inject component to step container and service for wiring everything together (data sync)

    import { Directive , Input, OnInit, Compiler , ViewContainerRef } from '@angular/core';

import { StepsService } from './ng2-steps';

@Directive({
  selector:'[ng2-step]'
})
export class StepDirective implements OnInit{

  @Input('content') content:any;
  @Input('index') index:string;
  public instance;

  constructor(
    private compiler:Compiler,
    private viewContainerRef:ViewContainerRef,
    private sds:StepsService
  ){}

  ngOnInit(){
    //Magic!
    this.compiler.compileComponentAsync(this.content).then((cmpFactory)=>{
      const injector = this.viewContainerRef.injector;
      this.viewContainerRef.createComponent(cmpFactory, 0,  injector);
    });
  }

}

Not the answer you're looking for? Browse other questions tagged or ask your own question.