2

I'm trying to display a list of items (components) according to their types:

I have an array of components. All inherits from a base class. The array type is defined as that of the base class. I want to display the array (let say as a list of items) each with its own template, rather than the base template.

I have tried that:

In the app.component.html:

<app-shape *ngFor="let shape of shapes"></app-shape>

In the app.component.ts:

shapes: ShapeComponent[] = [new CircleComponent(), new SquareComponent()];

I have defined 3 Components:

export class ShapeComponent {
}

export class CircleComponent extends ShapeComponent{
}

export class SquareComponent extends ShapeComponent{
}

And the result is that I get a list of shapes.

Does Angular support such a thing?

Thanks!

Eibi
  • 402
  • 4
  • 17

2 Answers2

4

Declarative approach

You can use ngComponentOutlet.

Code:

shapes: ShapeComponent[] = [CircleComponent, SquareComponent];

template:

<ng-container *ngFor="let shape of shapes">
    <ng-container *ngComponentOutlet="shape">
    </ng-container>
</ng-container>

ngComponentOutlet - Instantiates a single Component type and inserts its Host View into current View. NgComponentOutlet provides a declarative approach for dynamic component creation.

NgComponentOutlet requires a component type, if a falsy value is set the view will clear and any existing component will get destroyed.

So, no hard code needed in template. *ngFor will iterate over component types array in your code

Update

Don't remember add dynamic rendering component to entryComponents of AppModule:

@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent, HelloComponent, AComponent, BComponent ],
  entryComponents: [
    AComponent, BComponent 
  ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

StackBlitz Demo

Imperative approach with setting data

app-component.template:

<ng-container #comps>

</ng-container>

Get access to #comps(ng-container) view by ViewChild decorator and create components. So you can't initilize component like b = new BComponent().

  1. First need to resolve component factory.
  2. Initialize component via viewContainerRef's createComponent method. It returns reference to instantiated component
  3. By the reference, get access to instance property and update any data as you need

app-component.ts:

 @ViewChild('comps', { read: ViewContainerRef }) comps: ViewContainerRef;
  constructor(private componentFactoryResolver: ComponentFactoryResolver) {

  }

ngOnInit() {
    this.comps.clear();
    let aComponentFactory = this.componentFactoryResolver.resolveComponentFactory(this.compArr[0]);
    let aComponentRef = this.comps.createComponent(aComponentFactory);
    (<AComponent>aComponentRef.instance).name = 'A name';

    let bComponentFactory = this.componentFactoryResolver.resolveComponentFactory(this.compArr[1]);

    let bComponentRef = this.comps.createComponent(bComponentFactory);
    (<BComponent>bComponentRef.instance).name = 'B name';
  }

StackBlitzh Demo

Yerkon
  • 4,548
  • 1
  • 18
  • 30
  • Doe's not work, I get: "[Angular] Can't have multiple template bindings on one element. Use only one attribute named 'template' or prefixed with *" – Eibi Apr 15 '18 at 10:50
  • Thanks @Yerkon, it worked. Although since I have array of instances, not array of types, I just needed to add a method getType(ShapeComponent) that returned the inheriting type and set it to the ngComponentOutlet. Furthermore, I got an exception when I have created a constructor with a parameter, saying that the base class cannot be instantiated. I removed it (sadly..) and it worked like a charm. – Eibi Apr 15 '18 at 16:10
  • Sorry @Yerkon, I have just realized that there is a problem with this solution, new instances are being created instead of using my instances from the 'shapes' array and this misses the point completely. I am losing all my data :|. Am I missing something? – Eibi Apr 15 '18 at 16:30
  • @Eibi, `new instances are being created instead of using my instances` Right. Creating component by `new Component()` doesn't mean that component is creating in angular. If you want to create component and pass data to them. I recommend you to use **imperative** approach https://angular.io/guide/dynamic-component-loader#resolving-components – Yerkon Apr 15 '18 at 16:36
  • How to access `ViewContainerRef` from inherited class? Since decorators do not inherit, I have problem here (Cannot read property 'clear' of undefined): `@ViewChild('licenceContent', { read: ViewContainerRef }) protected licenceContent: ViewContainerRef;` is a declaration in base class, error at `loadLicenceComponent() { this.licenceContent.clear();...` in inherited component class. Markup is SHARED by inherited classes - this is where #licenceContent is needed. Calling `loadLicenceContent` from `ngAfterViewInit` of the inherited class - but it does not matter, since the problem is decorators. – Alexander Nov 17 '20 at 16:52
0

Each component must have separate selector.

import {Component} from "@angular/core";

@Component({
  selector: "app-shape" //maybe even is not needed here
})
class Shape {

}

@Component({
  selector: "app-rect"
})
class Rect extends Shape {

}

So you can do something like this.

<div *ngFor="let shape of shapes">
  <app-rect *ngIf="//check if shape is rect"></app-rect>
  <app-circle *ngIf="//check if shape is circle"></app-circle>
</div>
Vayrex
  • 1,417
  • 1
  • 11
  • 13
  • I do indeed have separate selectors. But I would rather not to add code each time I am adding a new type. I am basically looking for the equivalent of DataTemplate in WPF which knows to select the inherited class DataTemplate without using extra code that explicitly does it. – Eibi Apr 15 '18 at 07:26