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:
- 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?
- 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.
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
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);
});
}
}