Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to display ng-templates from list

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

like image 920
baliman Avatar asked Sep 18 '18 04:09

baliman


People also ask

How do I get ng-template reference in component?

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 .

Where do you put ng templates?

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.

How do you call a ng-template?

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.

How do I pass a ng-template?

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.


2 Answers

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 content
This is line and its content: Custom Line content
like image 109
Bunyamin Coskuner Avatar answered Oct 07 '22 18:10

Bunyamin Coskuner


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

like image 37
baliman Avatar answered Oct 07 '22 20:10

baliman