241

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? So far, 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. It 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 be 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: An attempt to clarify.

Think of the inbox as a mail inbox. Items are fetched as JSON and it 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.

EDIT 2: Image.

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
Cuel
  • 2,620
  • 3
  • 13
  • 13
  • 1
    If the components shown in the tabs aren't known at build time, then DCL is the right approach. – Günter Zöchbauer Mar 31 '16 at 05:56
  • 8
    I dont understand your requirement clearly so hard tell you anything without working code/plunker. Look this if it can help you somewhere http://plnkr.co/edit/Ud1x10xee7BmtUaSAA2R?p=preview (I don't know if its relevant or not) – micronyks Mar 31 '16 at 06:03
  • 1
    @micronyks I think you got the wrong link – Cuel Mar 31 '16 at 06:04
  • 1
    Hi! I'm trying to do what you asked for. So far I managed to create the tab with dynamic content but I didn't find a satisfying way to persist the component state when tab is changed (loaded components can be very different). How did you managed it? – gipinani May 08 '19 at 06:13

3 Answers3

280

update

Angular 5 StackBlitz example

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

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thanks, this helps a lot. Here's an image of what I need to create http://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 Mar 31 '16 at 06:48
  • 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 Mar 31 '16 at 08:48
  • 2
    Sure, you just need to get the components type to the `DclWrapper` to make it create an actual instance. – Günter Zöchbauer Mar 31 '16 at 08:52
  • @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 Apr 19 '16 at 17:49
  • @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 Apr 19 '16 at 17:59
  • @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 Apr 19 '16 at 18:05
  • 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 https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#bidirectional-service – Günter Zöchbauer Apr 19 '16 at 18:08
  • @GünterZöchbauer right, for my specific scenario, I am trying to create hierarchical grid myself similar to this one http://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 Apr 19 '16 at 18:15
  • 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 Apr 19 '16 at 18:20
  • why do you destroy component within `updateComponent()`? – Namek Jun 02 '16 at 09:41
  • How would you pass data through to the dynamic component? – Michael Jun 13 '16 at 05:03
  • 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 Jun 13 '16 at 05:11
  • @Namek I destroy the one previously added before I add a new one. – Günter Zöchbauer Jun 13 '16 at 05:14
  • @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 Jul 20 '16 at 10:22
  • 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 Jul 20 '16 at 10:53
  • @GünterZöchbauer, thank you for your great answer! You're like omnipresent regarding this issue on here and GitHub :) – Kel Jul 23 '16 at 17:05
  • Can you explain what this line does? `this.target.createComponent`. How does it bind the component to the dom? – dopatraman Jul 29 '16 at 17:52
  • `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 Jul 29 '16 at 17:54
  • Is it possible to add animations between the component transitions? Can I load a new component dynamically with animations? – galvan Aug 06 '16 at 19:49
  • 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 Aug 06 '16 at 20:07
  • 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 `
    contents`
    – Joseph Aug 15 '16 at 19:46
  • 2
    @Joseph You can inject `ViewContainerRef` instead of using `ViewChild`, then `` itself becomes the target. Elements are added as siblings of the target and will therefore be outside `` this way. – Günter Zöchbauer Aug 16 '16 at 03:00
  • @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 ` – Joseph Aug 16 '16 at 16:01
  • 2
    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 `` – Günter Zöchbauer Aug 16 '16 at 16:04
  • 2
    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 Aug 17 '16 at 11:06
  • 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 Sep 02 '16 at 09:16
  • 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 http://stackoverflow.com/a/37044960/217408 ? – Günter Zöchbauer Sep 02 '16 at 09:43
  • 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 Sep 12 '16 at 20:29
  • `@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 Sep 13 '16 at 05:05
  • @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 Sep 15 '16 at 10:38
  • 1
    Dynamicylly adding/removing directives is (currently?) not possible. Only components. – Günter Zöchbauer Sep 15 '16 at 10:43
  • 5
    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 Oct 28 '16 at 11:38
  • 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 Nov 28 '16 at 20:08
  • @Eusthace there is no need for this approach if you want to show the same thing for each link you receive. Just use `` or similar (where `links` contians the linkes you received from internet). – Günter Zöchbauer Nov 28 '16 at 20:11
  • 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 Nov 28 '16 at 22:08
  • I guess you need something like http://stackoverflow.com/questions/34784778/equivalent-of-compile-in-angular-2/37044960#37044960 – Günter Zöchbauer Nov 29 '16 at 06:51
  • How would be something like a service that loads components on demand to the dlcWraper? – Mijail Dan Cohen Nov 30 '16 at 19:28
  • @MijailDanCohen I'd suggest you create a new question with more details about what you try to accomplish. – Günter Zöchbauer Nov 30 '16 at 20:32
  • 1
    just to note that mentioned PR has been taken over and superseded by [a newer one](https://github.com/angular/angular/pull/13383), currently waiting for reviews – superjos Dec 27 '16 at 17:15
  • @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: http://stackoverflow.com/questions/43143956/angular2-use-pipe-to-render-templates-dynamically/43144634#43144634 – SrAxi Mar 31 '17 at 15:49
  • I just saw your question and answer. I think it's the right approach. – Günter Zöchbauer Mar 31 '17 at 15:59
  • 1
    @GünterZöchbauer Thanks! :D I will be updating my answer to be as precise as possible! – SrAxi Mar 31 '17 at 16:08
  • @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? – Mohan Kumar Apr 10 '17 at 15:10
  • @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 Apr 10 '17 at 15:11
  • @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? – Mohan Kumar Apr 11 '17 at 11:33
  • @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 Apr 11 '17 at 12:31
  • "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 Apr 28 '17 at 21:02
  • @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 Apr 28 '17 at 21:06
  • "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 May 20 '17 at 16:37
  • Is there anyway to add directive to dynamically created components? as this is what I need to achieve `` – looooongname Dec 19 '17 at 14:14
  • @hngdev no, directives can only be added statically to a components template. You can create a component at runtime like explained inhttps://stackoverflow.com/questions/38888008/how-can-i-use-create-dynamic-template-to-compile-dynamic-component-with-angular. I don't seeadirective in your example code. – Günter Zöchbauer Dec 19 '17 at 14:24
  • @GünterZöchbauer sorry, I mean the property and method in the component. – looooongname Dec 19 '17 at 14:58
  • @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 Dec 19 '17 at 15:02
  • @GünterZöchbauer I tried that but it doesn't work for me, so I tried a different approach using nd-dynamic-component – looooongname Dec 21 '17 at 15:54
  • 1
    @GünterZöchbauer Somehow `directives: [DclWrapper]` returns an error in VSC: _[ts] Argument of type '{ selector: string; directives: (typeof DclWrapper)[]; template: string; }' is not assignable to parameter of type 'Component'. Object literal may only specify known properties, and 'directives' does not exist in type 'Component'. (property) directives: (typeof DclWrapper)[]_ – mpro Feb 13 '18 at 10:10
  • @mpro Thanks a lot for notifying me. I'll have a look probably later today. It might only be an issue with type annotations. – Günter Zöchbauer Feb 13 '18 at 10:12
  • @GünterZöchbauer did you check what is above error about? I've managed to implement [different solution](https://stackoverflow.com/questions/41148129/angular-2-functions-methods-on-click-inside-dynamic-template-dynamic-component) for my project, but because facing some problems, I would like to try your approach :) – mpro Feb 21 '18 at 15:52
  • @mpro sorry, have a lot on my platter currently. I haven't forgotten. – Günter Zöchbauer Feb 21 '18 at 16:01
  • @GünterZöchbauer thanks for the response, will wait for an update, call me if possible please, thanks. – mpro Feb 21 '18 at 16:15
  • 1
    @mpro I added an updated example at the top of my answer. – Günter Zöchbauer Feb 26 '18 at 07:17
22

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

davimusprime
  • 277
  • 4
  • 11
17

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);
    });
  }

}
neuronet
  • 1,139
  • 8
  • 19