3

I have an array of data objects:

const arr = [
   {type: 'CustomA', id: 1, label: 'foo'},
   {type: 'CustomB', src: './images/some.jpg'}
   {type: 'CustomX', firstName: 'Bob', secondName:'Smith'}
]

These objects will always have a type but the rest of the properties on each will be specific to that type.
I have a component set up that can accept and display each of the possible data objects, doing whatever parsing and internal logic is required.
I want to look through that list, check the type of each object that I find and add the relevant component to the page. So, if I find type: 'CustomA I can add a CustomADisplay component to the page and pass it the data object to work with.

How best to achieve this when I don't know in advance how many items will be in that list or what their types will be?
Basically, who can I dynamically create and add components to page while the app is running.
I am currently playing around with ViewContainerRef as per the docs but this seems to require a target element in the template and until I get my data I'm not going to know how many of these are needed.

Hope that makes sense. Any advice very gratefully received.

2

3 Answers 3

7

The above answer gets the job done but just like with any other switch case you will need to keep on adding more switch conditions for each type. Like talha mentioned in the comments it might be better to use dynamic component rendering. Although this solution requires more configuration but it should be easier to manage in the long run. Essentially something like this. heres the working stackblitz link.

const datas = [
   {type: 'CustomA', id: 1, label: 'foo'},
   {type: 'CustomB', src: './images/some.jpg'}
   {type: 'CustomX', firstName: 'Bob', secondName:'Smith'}
]
import AComponent from 'wherever';
import BComponent from 'wherever';
import XComponent from 'wherever';
// constants file
export const TypeMappedComponents = {
  CustomA: AComponent,
  CustomB: BComponent,
  CustomX: XComponent
}
// dynamic-renderer.component.html
<ng-template appRenderDynamically></ng-template>
//dynamic-renderer.component.ts
import { TypeMappedComponents } from '../constants';
import { RenderDynamicallyDirective } from '../render-dynamically.directive';

@Component(...all the decorator stuff)
export class DynamicRendererComponent {
  _data: Record<string, unknown> = {};
  componentType = '';
  @Input() set data(data: Record<string, unknown>) {
    this._data = data;
    this.componentType = data.type as string;
    this.componentType && this.createDynamicComponent(this.componentType);
  }
  @ViewChild(RenderDynamicallyDirective, { static: true })
  renderDynamic: RenderDynamicallyDirective;

  constructor(private cfr: ComponentFactoryResolver) {}

  createDynamicComponent(type: string) {
    const component = this.cfr.resolveComponentFactory(
      TypeMappedComponents[type]
    );
    this.renderDynamic.vcr.clear();
    const componentRef = this.renderDynamic.vcr.createComponent(
      component
    ) as any;
    componentRef.instance.data = this._data;
  }
}

and then you just loop it and pass the data to dynamic-renderer. This should dynamically create the component according to the type.

<ng-container *ngFor="let data of datas">
  <app-dynamic-renderer [data]="data"></app-dynamic-renderer>
</ng-container>

Heres the renderDynamicallyDirective which places the component into the view.

@Directive({
  selector: '[appRenderDynamically]'
})
export class RenderDynamicallyDirective {
  constructor(public vcr: ViewContainerRef) { }
}

Although this code is longer than the switch case, its more of a one time effort. Since if you have a new type and new component to go along with it you just need to created the required component. then add the new type and the component associated with it to the TypeMappedComponent.

Sign up to request clarification or add additional context in comments.

2 Comments

I think this solution is the best approach! Maybe you could try also something with ViewChildren to avoid the individual wrapper component but it's already correct!
Yes, this is a great solution. One question for @KZoeps : this will clear old components. Let's say we want user to switch between components, is there a way to keep them all in the DOM and display only the one required? Or is that a bad idea to begin with? If HTML components are in the DOM, the switching is smooth and fast. Thanks!
4

I guess that you are thinking off in something like this:

HTML for your array

arr = [
      {type: 'CustomA', id: 1, label: 'foo'},
      {type: 'CustomB', src: './images/some.jpg'}
      {type: 'CustomX', firstName: 'Bob', secondName:'Smith'}
];

 <ng-container *ngFor="let item of arr" [ngSwitch]="item.type">

    <ng-container *ngSwitchCase="CustomA">
       <app-custom-a-display [data]="item"><app-custom-a-display>
    </ng-container>

    <ng-container *ngSwitchCase="CustomB">
       <app-custom-b-display [data]="item"><app-custom-b-display>
    </ng-container>

    <ng-container *ngSwitchDefault>
       <app-custom-x-display [data]="item"><app-custom-x-display>
    </ng-container>

  </ng-container>

TS example for your CustomADisplay:

...
@Component({
  selector: 'app-custom-a-display',
  templateUrl: './custom-a-display.component.html',
  styleUrls: ['./custom-a-display.component.scss'],
})
export class CustomADisplayComponent {
private _localData;
@Input()
get data() {
   return _localData;
}
set data(data) {
    this._localData= data;   
}

constructor() {
// you will have your {type: 'CustomA', id: 1, label: 'foo'}
// accesible in 'data' variable
}

   
}

3 Comments

I honestly don't know but that looks promising. I've not come across ngSwitch or <ng-container> before so I'll look into this and hopefully come back and present you with a nice green tick later
Yep, this has done the job. Cheers :)
You're welcome! Glad it helped
1

This is a modification of the answer provided by @KZoeps,

// dynamic-renderer.component.html
<ng-template #container></ng-template>
//dynamic-renderer.component.ts
import { TypeMappedComponents } from '../constants';
import { RenderDynamicallyDirective } from '../render-dynamically.directive';

@Component(...all the decorator stuff)
export class DynamicRendererComponent {
  _data: Record<string, unknown> = {};
  componentType = '';
  @Input() set data(data: Record<string, unknown>) {
    this._data = data;
    this.componentType = data.type as string;
    this.componentType && this.createDynamicComponent(this.componentType);
  }

   @ViewChild('container', { read: ViewContainerRef, static: false })

  
  component!: ViewContainerRef;

 
  createDynamicComponent(type: string) {
    setTimeout(() => {
      const componentRef = this.component.createComponent(
        TypeMappedComponents[type]
      ) as any;
     componentRef.instance.data = this._data;
    }, 10);
  }
}

and then you just loop it and pass the data to dynamic-renderer. This should dynamically create the component according to the type.

<ng-container *ngFor="let data of datas">
  <app-dynamic-renderer [data]="data"></app-dynamic-renderer>
</ng-container>

1 Comment

Thanks it works. I only replace the timeout by a call in ngAfterViewInit.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.