Search code examples
angularangular2-templateangular2-directivesangular2-components

Angular 2: Component to render table with static <td>s and dynamic <td>s from another component


I have an Angular 2 component which renders a nice table with kitten object data.

Since some of the columns are going to be reused in a different component, I'm looking for a way to extract the <td> into a separate component (dynamic-kitten-tds). I can not move <td>s which render kitten.name and kitten.lastWashed since they are unique to cat-o-base component:

cat-o-base.component.html

<table>
<tbody>
    <tr *ngFor="let kitten of kittenBasket">
        <td>{{ kitten.name }}</td>

        <dynamic-kitten-tds [value]="kitten"></dynamic-kitten-tds>

        <td>{{ kitten.lastWashed | date }}</td>
    </tr>
</tbody>
<table>

dynamic-kitten-tds.component.html

The entire template of the dynamic-kitten-tds component looks like this:

<td *ngFor="let preference of kitten.preferences">{{ preference | json }}</td>

Limitation 1

I may not use the *ngFor like this:

<td>{{ kitten.name }}</td>
<td *ngFor="let preference of kitten.preferences" [value]="preference"></td>
<td>{{ kitten.lastWashed | date }}</td>

This limitation comes from the business logic that must be implemented as a part of dynamic-kitten-tds component.

Limitation 2

The code must result in a valid DOM emission.

Question

How do I achieve it? Using auxiliary components is fine. Using special structural directives is fine too.

P.S

I looked through some other SO questions (like this one) however didn't find quite matching problem definition.


Solution

  • If using auxiliary components is fine here is my thought:

    dynamic-outlet.ts

    @Directive({selector: '[dynamicOutlet]'})
    export class DynamicOutlet implements OnChanges, OnDestroy {
      @Input() dynamicOutlet: Type<any>;
      @Input() dynamicOutletModel: any;
    
      private componentRef: ComponentRef<any> = null;
    
      constructor(private vcRef: ViewContainerRef) {}
    
      ngOnChanges(changes: SimpleChanges) {
        this.vcRef.clear();
        this.componentRef = null;
    
        if (this.dynamicOutlet) {
          const elInjector = this.vcRef.parentInjector;
          const componentFactoryResolver = elInjector.get(ComponentFactoryResolver);
    
          const componentFactory = componentFactoryResolver.resolveComponentFactory(this.dynamicOutlet);
          this.componentRef = componentFactory.create(elInjector);
    
          this.componentRef.changeDetectorRef.detectChanges();
          this.componentRef.instance.model = this.dynamicOutletModel;
          this.vcRef.createEmbeddedView(this.componentRef.instance.template, { $implicit: this.dynamicOutletModel });
        }
      }
    
      ngOnDestroy() {
        if(this.componentRef) {
          this.vcRef.clear();
          this.vcRef = null;
        }
      }
    }
    

    kitten.ts

    @Component({
        selector: 'kitten-component',
        template: `
          <ng-template let-model>
            <td *ngFor="let preference of model.preferences">{{ preference | json }}</td>
          </ng-template>
        `
    })
    export class Kitten {
      @ViewChild(TemplateRef) template: TemplateRef<any>;
    
      model: any;
    }
    

    and then you can use it like

    view

    <ng-container *dynamicOutlet="kittenComp; model: kitten"></ng-container>
    

    component

    kittenComp = Kitten;
    

    Don't forget to add Kitten component to entryComponents array.

    Here is Plunker Example