I have an AppComponent
which holds a list of ShapeComponents
. I implemented some Components which extends the ShapeComponent
like LineComponent
, CircleComponent
, RectangleComponent
. Each of them has its own ng-template
with #shapeTemplate
.
In my app.component.html
I want to iterate over the list of ShapeComponents
and display each ng-template
(from LineComponent
, CircleComponent
etc).
so I have
shapes: ShapeComponent[] = []
which hold a LineComponent, CircleComponent etc
I want to something like this
<div *ngFor="let shape of shapes">
<!-- How to display the ng-template from for example LineComponent or Circle -->
</div>
I thought using @ViewChildren or @ContentChildren would be useful but no idea how to deal with that
I've done something similar recently. Here is the final stackblitz
First, I create a ShapeComponent
import {
Component,
Input,
ViewChild,
TemplateRef
} from '@angular/core';
@Component({
selector: 'shape',
template: `<ng-template><ng-content></ng-content></ng-template>`,
})
export class ShapeComponent {
@ViewChild(TemplateRef) template: TemplateRef<any>;
}
It's template has a ng-template
so that we can ref to it, and ng-content
so consumers of this component can project their content in.
With @ViewChild(TemplateRef)
you can get a reference of ng-template
and whatever is inside of it because of ng-content
.
Let's create a LineComponent
@Component({
selector: 'line',
template: `<ng-template>
This is line and its content: <ng-content></ng-content>
</ng-template>`,
providers: [{
provide: ShapeComponent,
useExisting: forwardRef(() => LineComponent)
}]
})
export class LineComponent extends ShapeComponent {}
and CircleComponent
@Component({
selector: 'circle',
template: `<ng-template>
This is circle and its content: <ng-content></ng-content>
</ng-template>`,
providers: [{
provide: ShapeComponent,
useExisting: forwardRef(() => CircleComponent)
}]
})
export class CircleComponent extends ShapeComponent {}
Both components extend ShapeComponent
and provide it according to themselves. So that whenever someone tries to inject ShapeComponent
, they will get a LineComponent
or a ShapeComponent
.
Finally, let's create a ShapeHolderComponent
which will glue all this together
@Component({
selector: 'shape-holder',
template: `
<div *ngFor="let child of children">
<ng-container *ngTemplateOutlet="child.template"></ng-container>
</div>
`,
})
export class ShapeHolderComponent {
@ContentChildren(ShapeComponent) children: QueryList<ShapeComponent>;
}
You can list of ShapeComponent
s with ContentChildren
. Since, every ShapeComponent
provides themselves, we can get a list of them and use their template
s.
Finally, let's use all of this within AppComponent
<shape-holder>
<circle>
Custom Circle content
</circle>
<line>
Custom Line content
</line>
</shape-holder>
The output is
This is circle and its content: Custom Circle content
This is line and its content: Custom Line content
There is a solution to display components, but it is quite complex and not my recommendation.
The "angular-style" solution for your issue is this:
- Use a list of model objects (not components), containing all information that the sub-components need. Let's call it
models
in this example.
- Store the type of the shape in a property of each of these models (avoids having to use
typeOf
). Let's call if shape
. You could also use an enumeration, if you prefer.
- Iterate over the models in a
ngFor
and create a component for each one of them.
The HTML-Template might look like this
<div *ngFor="let model of models">
<!-- display the ng-template from for example LineComponent or Circle -->
<line [model]="model" *ngIf="model.shape === 'Line'"></line>
<circle [model]="model" *ngIf="model.shape === 'Circle'"></circle>
</div>
See the full, working example on stackblitz.
In this case the best and also officially suggested approach is to use Dynamic Forms.
In the documentation you'll find some useful tips
I found the solution. Actually I found an excellent post on github
https://github.com/shivs25/angular5-canvas-drawer. I took this solution to implement my own.
So all the credits go to Billy Shivers. Well done.
Here is the solution
The settings for line and circle can be dynamic set, below is just an example of a line and circle
CircleComponent and HTML template
import { Component } from '@angular/core';
import { ShapeComponent } from '../shape/shape.component';
@Component({
selector: 'app-circle',
templateUrl: './circle.component.html',
styleUrls: ['./circle.component.css']
})
export class CircleComponent extends ShapeComponent {
constructor() {
super('circle');
}
}
html
<ng-template #elementTemplate>
<svg:circle [attr.cx]="50" [attr.cy]="50" [attr.r]="40" stroke="black" stroke-width="3" fill="red" />
</ng-template>>
LineComponent and HTML template
import { Component } from '@angular/core';
import { ShapeComponent } from '../shape/shape.component';
@Component({
selector: 'app-line',
templateUrl: './line.component.html',
styleUrls: ['./line.component.css']
})
export class LineComponent extends ShapeComponent {
constructor() {
super('line');
console.log('linecomponent:constructor');
}
}
html
<ng-template #elementTemplate>
<svg:line [attr.x1]="100" [attr.y1]="100" [attr.x2]="200" [attr.y2]="200" style="stroke:#006600; stroke-width:1px" />
</ng-template>>
The ShapeComponent and HTML
import { Component, OnInit, ViewChild, TemplateRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-shape',
templateUrl: './shape.component.html',
styleUrls: ['./shape.component.css']
})
export class ShapeComponent implements OnInit, AfterViewInit {
shapeType: string;
visible: boolean = true;
id: string = 'unknown';
@ViewChild('elementTemplate')
elementTemplate: TemplateRef<any>;
constructor(shapeType: string) {
console.log('shapecomponent constructor :', shapeType);
this.shapeType = shapeType;
}
setid(value: string): void {
this.id = value;
}
ngOnInit() {
console.log('ShapeComponent ngOnInit()');
}
ngAfterViewInit(): void {
console.log('!!!!!!!!! ShapeComponent ngAfterViewInit: ', this.elementTemplate);
}
}
html : none
The enum for component types
export enum ShapeTypes {
Line,
Circle,
Rectangle
}
The ShapeHolderComponent
import { Component, OnInit, ViewChild, TemplateRef, AfterViewInit } from '@angular/core';
import { ShapeComponent } from '../shape/shape.component';
import { LineComponent } from '../line/line.component';
import { CircleComponent } from '../circle/circle.component';
import { ShapeTypes } from '../model/shape-types';
@Component({
selector: 'app-shapeholder',
templateUrl: './shapeholder.component.html',
styleUrls: ['./shapeholder.component.css']
})
export class ShapeholderComponent implements OnInit, AfterViewInit {
@ViewChild('elementTemplate')
elementTemplate: TemplateRef<any>;
shapes: ShapeTypes[];
constructor() {
this.shapes = [];
this.shapes.push(ShapeTypes.Line);
this.shapes.push(ShapeTypes.Circle);
console.log('shapeholder shapes :', this.shapes);
}
ngOnInit() {
console.log('ShapeHolderComponent : ngOnInit()');
}
ngAfterViewInit(): void {
console.log('!!!!!!!!! ShapeHolder ngAfterViewInit: ', this.elementTemplate);
}
}
html, set height in width in css for the svg
<svg>
<ng-container *ngFor="let shape of shapes; let i = index">
<ng-container svg-dynamic [componentData]="shape">
</ng-container>
</ng-container>
</svg>
And the most import part of it, the directive
import { Directive, Input, ViewContainerRef, Injector, ComponentFactoryResolver } from '@angular/core';
import { ShapeComponent } from './shape/shape.component';
import { LineComponent } from './line/line.component';
import { CircleComponent } from './circle/circle.component';
import { ShapeTypes } from './model/shape-types';
@Directive({
selector: '[svg-dynamic]'
})
export class SvgDynamicDirective {
constructor(private _viewContainerRef: ViewContainerRef, private _resolver: ComponentFactoryResolver) {
}
@Input() set componentData(data: ShapeTypes) {
console.log('set componentdata : ', data);
let injector = Injector.create([], this._viewContainerRef.parentInjector);
console.log('injector:', injector);
let factory = this._resolver.resolveComponentFactory(this.buildComponent(data));
console.log('factory:', factory);
let component = factory.create(injector);
console.log('component:', component);
let c: ShapeComponent = <ShapeComponent>component.instance;
console.log('viewContainerRef:', this._viewContainerRef);
console.log('elementTemplate:', c.elementTemplate);
this._viewContainerRef.clear();
this._viewContainerRef.createEmbeddedView(c.elementTemplate);
}
private buildComponent(data: ShapeTypes): any {
switch (data) {
case ShapeTypes.Line:
return LineComponent;
case ShapeTypes.Circle:
return CircleComponent;
}
return null;
}
}
And the app.component html
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<app-shapeholder></app-shapeholder>
</div>
The app.component
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'demo1';
}
And the app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ShapeComponent } from './shape/shape.component';
import { LineComponent } from './line/line.component';
import { ShapeholderComponent } from './shapeholder/shapeholder.component';
import { SvgDynamicDirective } from './svg-dynamic.directive';
import { CircleComponent } from './circle/circle.component';
@NgModule({
entryComponents: [
LineComponent,
ShapeComponent,
CircleComponent
],
declarations: [
AppComponent,
LineComponent,
ShapeComponent,
CircleComponent,
ShapeholderComponent,
SvgDynamicDirective,
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
And a final screen shot of my app
I hope you find this answer usefull and can use it in your own app. The idea is to create dynamic templates views