How to display ng-templates from list How to display ng-templates from list angular angular

How to display ng-templates from list


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 ShapeComponents with ContentChildren. Since, every ShapeComponent provides themselves, we can get a list of them and use their templates.

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 contentThis 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:

  1. Use a list of model objects (not components), containing all information that the sub-components need. Let's call it models in this example.
  2. 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.
  3. 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.


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

enter image description here

I hope you find this answer usefull and can use it in your own app. The idea is to create dynamic templates views