Its about Angular with TypeScript.
I am coming from the WPF world and now I am tring some Angular development.
There is this place in my app, where I want to establish a tab control to contain all sorts of 'open' documents (Angular components). Maybe I apporach this totally wrong but let's considder there is an injectable that contains an Array of TabItem
s one of the properties of a TabItem
is either a string of a class, a factory or a typename of a component of my application (To be decided on ease).
export class TabItem {
public title : string;
public disabled : boolean;
public active : boolean;
public factory: any; // or class name or object
}
@Injectable()
export class OpenDocumentService {
openTabs: Array<TabItem> = [];
addTab(t:TabItem){ openTabs.push(t); }
}
In the WPF wolrd, I would create a content presenter and bind it to the name or the object to be displayed.
What would I do in the Angular world. Remark: The to be displayed component may be in a different module.
How can I *ngFor over it and display arbitrary components when added to the service? (replace ng-contentpresenter)
<tabset>
<tab *ngFor="let tabz of tabservice.openTabs"
[heading]="tabz.titel">
<ng-contentpresenter use={{tabz?.factory}}/>
</tab>
</tabset>
For anyone who ended up here:
Short answer - angular does not approve such shinenigans, so you better stick with recommended ways to construct ui - like templates injection, correct usage of routing, ngSwitch, for complex cases with item-browser with tree add @ngrx/store, etc.
Long answer - look here. You will have to make infrastructure first thou:
import { Injectable, Type } from '@angular/core';
/**
* This service allows dynamically bind viewModel and component in configuration stage and then resolve it in render stage.
* Service for dynamic component registration and detection. Component to be used are determined based on view model they have to render.
*/
@Injectable()
export class DataTemplateService {
private dictionary: Map<Type<any>, Type<any>>;
constructor() {
this.dictionary = new Map<Type<any>, Type<any>>();
}
/**
* Determines component class, searching in registered components.
* @param data ViewModel that will be used for component, returns undefined if not found.
*/
public determine(data: any): Type<any> | undefined {
return data ? this.dictionary.get(data.constructor) : undefined;
}
/**
* Registers binding of certain view model towards certain component.
* @param viewModelType Type of ViewModel to be registered.
* @param componentType Type of Component to be registered.
*/
public register(viewModelType: Type<any>, componentType: Type<any>) {
this.dictionary.set(viewModelType, componentType);
}
}
import { ComponentFactoryResolver, Injectable } from '@angular/core';
/**
* Service fro rendering dynamic components.
*/
@Injectable()
export class ComponentRendererService {
constructor(private componentFactoryResolver: ComponentFactoryResolver,
private dataTemplateService: DataTemplateService) {
}
public render(data: any, containerComponent: any) {
setTimeout(() => {
this.doRender(data, containerComponent);
}, 0);
}
/**
* Renders dynamic components based on ViewModel they have to use.
* @param data Collection of ViewModels that have to be used to render all child components.
* @param containerComponent Parent component, that have to host dynamic child components.
*/
public renderAll(data: Array<any>, containerComponent: any) {
setTimeout(() => {
if (data) {
data.forEach(item => {
this.doRender(item, containerComponent);
});
}
}, 0);
}
private doRender(data: any, containerComponent: any) {
if (!data) {
console.debug('No data (viewModel) for ComponentRendererService to render.');
return;
}
const viewContainerRef = containerComponent.viewContainerRef;
const dataItem = data;
const component = this.dataTemplateService.determine(dataItem);
if (component) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
const componentRef = viewContainerRef.createComponent(componentFactory);
(<any>componentRef.instance).data = data;
} else {
console.warn('Failed to find component for viewmodel of type' + dataItem.constructor);
}
}
}
import { Directive, ViewContainerRef, Component, Input, OnChanges, SimpleChanges, ViewChild } from '@angular/core';
/**
* Directive that enables construction of dynamic child components.
*/
@Directive({
selector: '[dynamic-component-host]',
})
export class DynamicComponentHostDirective {
constructor(public viewContainerRef: ViewContainerRef) { }
}
/**
* Component that represents extention point for rendering dynamic child components.
*/
@Component({
selector: 'ext-point-single-host',
template: `
<div class="ext-point-host">
<ng-template dynamic-component-host></ng-template>
</div>
`
})
export class ExtPointSingleHostComponent implements OnChanges {
@ViewChild(DynamicComponentHostDirective) public hostDirective: DynamicComponentHostDirective;
@Input() public viewModel: any;
constructor(private componentRenderer: ComponentRendererService) { }
/**
* Loads nested components.
*/
public loadComponent() {
const viewModel = this.viewModel;
this.componentRenderer.render(viewModel, this.hostDirective);
}
public ngOnChanges(changes: SimpleChanges) {
this.hostDirective.viewContainerRef.clear();
this.loadComponent();
}
}
After that you can bind model to component in module:
@Component({
template: '<button type="button" class="">Custom style 2</button>'
})
export class CustomButton1Component {
public data: CustomButton1ViewModel;
}
export class CustomButton1ViewModel {
}
@Component({
template: '<button type="button" class="">Custom style 2</button>'
})
export class CustomButton2Component {
public data: CustomButton2ViewModel;
}
export class CustomButton2ViewModel {
}
@NgModule({
...
providers: [..., DataTemplateService]
})
export class DemoModule {
constructor(dataTemplateService: DataTemplateService) {
dataTemplateService.register(CustomButton2ViewModel, CustomButton2Component);
dataTemplateService.register(CustomButton1ViewModel, CustomButton1Component);
}
}
And that's it!
Now we can use and bind it's viewModel property to CustomButton1ViewModel or CustomButton2ViewModel to actually render CustomButton2Component or CustomButton1Component. Why so much code? Well, look for short answer :(