2

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.

Community
  • 1
  • 1
Igor Soloydenko
  • 11,067
  • 11
  • 47
  • 90

1 Answers1

2

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

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • I actually solved it differently (will post my solution later), but your post works too, so I accept it. Thanks for your time and effort @yurzui ! – Igor Soloydenko Apr 11 '17 at 22:44