23

I have a "dashboard" that loads configured elements. Dashboard template has this:

  <div class="dash-container" [ngGrid]="gridConfig">
    <div *ngFor="let box of boxes; let i = index"
       [(ngGridItem)]="box.config"
       (onItemChange)="updateItem(i, $event)"
       (onResize)="onResize(i, $event)"
       (onDrag)="onDrag(i, $event)"
       (onDragStop)="onDragStop(i,$event)"
       [ngClass]="box.class"
     >
      <div class="handle"><h4>{{box.title}}</h4></div>
      <div [innerHTML]= "box.content"></div>
    </div>
  </div>

Now <div [innerHTML]= "box.content"></div> will not work because non standard elements get sanitised. Running latest Angular 2.4.6 (RC 6).

I look at the examples i could find for dynamic components - but all i see is that they just add components to the current component - but i need them in a very specific divs like in the example above.

ComponentFactoryResolver is often used together with @ViewChild. But i can't just do this inside a loop:

ngAfterViewInit() {
    const dashWidgetsConf = this.widgetConfigs();
    for (var i = 0; i < dashWidgetsConf.length; i++) {
      const conf = dashWidgetsConf[i];

      @ViewChild(conf.id, {read: ViewContainerRef}) var widgetTarget: ViewContainerRef;

      var widgetComponent = this.componentFactoryResolver.resolveComponentFactory(UnitsComponent);
      widgetTarget.createComponent(widgetComponent);
    }
  }

The @viewchild gives 'Decorators are not valid here'. How can i load components from a conf list (in a loop) and add them inside a specific div (divs got #{{conf.id}}) in my component?

Agony
  • 845
  • 2
  • 9
  • 23
  • 1
    Looks quite similar to http://stackoverflow.com/questions/36325212/angular-2-dynamic-tabs-with-user-click-chosen-components/36325468#36325468 with components created dynamically like shown in http://stackoverflow.com/questions/34784778/equivalent-of-compile-in-angular-2/37044960#37044960 – Günter Zöchbauer Feb 16 '17 at 12:42
  • 1
    But in those examples - the "target" is fixed - already known element defined as @ViewChild in the very beginning. I need to add them to x number of elements with id-s i cannot predefine in template. – Agony Feb 16 '17 at 12:56
  • You can predefine everything in a template of a component that is created at runtime as show in the 2nd link - and I mean the whole component is created at runtime, not only dynamically added. – Günter Zöchbauer Feb 16 '17 at 13:00
  • End up usin @viewChildren - the items would be there in the same order as the temp #dynamic id elements i created with *ngFor. Unfortunately this still adds them after this div (so i have some junk in dom now) but at least i can add them to the proper location and use createComponent ref it returns. – Agony Mar 06 '17 at 15:06

4 Answers4

33

After some research, this is the solution i came up with (works in angular 4.0.0).

Load all the ViewContainerRef targets by id:

@ViewChildren('dynamic', {read: ViewContainerRef}) public widgetTargets: QueryList<ViewContainerRef>;

Then loop over them to get the target, create a factory for the component and call createComponent.
Also can use the component reference to subscribe or set other component properties.

ngAfterViewInit() {
    const dashWidgetsConf = this.widgetConfigs();
    const widgetComponents = this.widgetComponents();
    for (let i = 0; i < this.widgetTargets.toArray().length; i++) {
        let conf = dashWidgetsConf[i];
        let component = widgetComponents[conf.id];
        if(component) {
            let target = this.widgetTargets.toArray()[i];
            let widgetComponent = this.componentFactoryResolver.resolveComponentFactory(component);
            let cmpRef: any = target.createComponent(widgetComponent);

            if (cmpRef.instance.hasOwnProperty('title')) {
                cmpRef.instance.title = conf.title;
            }
        }
    }
}

The widgetComponents is a object {key: component} and widgetConfigs is where i store specific component info - like title, component id etc.

Then in template:

<div *ngFor="let box of boxes; let i = index" >
    <ng-template #dynamic></ng-template>
</div>

And the order of targets is the same as in my conf ( boxes is generated from it) - which is why i can loop through them in order and use i as index to get the correct conf and component.

Agony
  • 845
  • 2
  • 9
  • 23
  • 3
    @Agony what if the value of 'boxes' changes in future? eg. new boxes are added to the existing boxes. I could see that the DOM is not updated with the new boxes in case of components loaded dynamically. – Tejas Sherdiwala Mar 10 '18 at 04:19
  • @Tejas Yes, this one won't update after. I believe in later iteration i moved some code around when that requirement came. I created component factories separately and added new ones to the factory when needed. And moved createcomponent part to a separate method aswell. – Agony Jul 27 '18 at 08:07
  • Thanks for showing how to do this! Works perfectly! Also, to watch for changes: ```this.widgetTargets.changes.subscribe(() => {/* do something */});``` – 6utt3rfly Nov 07 '19 at 17:24
  • How do I unit test this? Because it is throwing `Expression changed` error and `widgetTargets` would be `undefined` if I call `ngAfterViewInit` explicitly. – Suyash Gupta Jan 02 '20 at 12:25
6

If you have this pattern:

<div *ngFor="for item in items">
  <!-- needs to be one per item -->
  <ng-template details-directive></ng-template>
</div>

I suggest wrapping the directive in a component:

@Component({
  selector: 'details-wrapper',
  template: '<ng-template details-directive></ng-template>'
})
export class DetailsWrapper {
  @Input item?: Item;
  // Dynamically load details using the regular solution.
}

And making this your for loop:

<div *ngFor="for item in items">
  <details-wrapper [item]="item"></details-wrapper>
</div>
djechlin
  • 59,258
  • 35
  • 162
  • 290
  • This looks like it might be a good solution, but is just not complete enough for me to follow. – peterc Aug 29 '19 at 05:12
  • @peterc can you be more specific about what information it's missing? – djechlin Aug 29 '19 at 18:49
  • I liked the look of this pattern, but I am not sure what goes in the `details-directive` and what to put in the section `// Dynamically load details using the regular solution`. I actually tried to use this pattern in my case but could not get it to work.. infact I now have my [own question on this](https://stackoverflow.com/questions/57705694/angular-material-stepper-how-to-dynamically-create-components-to-load-into-ste) where I attempted (and failed) to use the wrapper and directive. – peterc Aug 30 '19 at 00:12
1

After

if (cmpRef.instance.hasOwnProperty('title')) {
   cmpRef.instance.title = conf.title;
}

You can add this.cd.detectChanges(); or you will have the error "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked" in your children component with Angular 6x.

Jbz797
  • 13
  • 2
  • this is actually very helpful, as I have experienced exactly the same problem after adding dynamic components. Reason is that as you have dynamic components that use `@Input` and `@Output` attributes those will not get evaluated by angular automatically in ngAfterViewInit(), so you have to trigger change detection manually – Ivan Hušnjak Jul 31 '18 at 12:14
0

I resolved the issue by adding 'static: false'.

@ViewChild(conf.id, {read: ViewContainerRef, static: false}) var widgetTarget: ViewContainerRef

https://stackoverflow.com/a/41095677/6329980

Jordyz96
  • 1
  • 1