How to display ng-templates from list

2019-05-07 04:06发布

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

4条回答
一纸荒年 Trace。
2楼-- · 2019-05-07 04:23

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

查看更多
欢心
3楼-- · 2019-05-07 04:29

In this case the best and also officially suggested approach is to use Dynamic Forms.

In the documentation you'll find some useful tips

查看更多
女痞
4楼-- · 2019-05-07 04:35

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
查看更多
淡お忘
5楼-- · 2019-05-07 04:37

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.

查看更多
登录 后发表回答