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
To get started using template reference variables, simply create a new Angular component or visit an existing one. To create a template reference variable, locate the HTML element that you want to reference and then tag it like so: #myVarName .
ng-template should be used along with structural directives like [ngIf],[ngFor],[NgSwitch] or custom structural directives. That is why in the above example the contents of ng-template are not displayed. ng-template never meant to be used like other HTML elements.
ng-template is an Angular element that is used for rendering HTML in a template. However, it is not rendered directly on DOM. If you include an ng-template tag to a template, the tag and the content inside it will be replaced by comment upon render.
In order to have a template rendered in that container, we use the *ngTemplateOutlet to pass a reference to an existing template, such as the one we created earlier. We use @Input to pass a TemplateRef to our component.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With