6

I'm trying to build a dynamic table where i wand to decide in run time which pipe to use (If Any).

I'm trying to achieve something similar to (Simplified):

export class CellModel {
     public content: any;
     public pipe: string
}

Table

<tbody>
     <tr *ngFor="let row of data">
         <template ngFor let-cell [ngForOf]=row>
           <td *ngIf="cell.pipe">{{cell.content | cell.pipe}}</td>
           <td *ngIf="!cell.pipe">{{cell.content}}</td>
     </tr>
</tbody>

I understand that this example gives an error. Can i use Reflect is some manner or some other solution?

royB
  • 12,779
  • 15
  • 58
  • 80

2 Answers2

6

You can't apply pipes dynamically. What you can do is to build a "meta" pipe that decides what transformation(s) to do.

@Pipe({
  name: 'meta'
})
class MetaPipe implements PipeTransform {
  transform(val, pipes:any[]) {
    var result = val;
    for(var pipe of pipes) {
      result = pipe.transform(result);
    }
    return result;
  }
}

and then use it like

<td *ngIf="cell.pipe">{{cell.content | meta:[cell.pipe]}}</td>
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • Thanks @Gunter. great answer. There is a small problem. `cell.pipe' is a type of string - the name of the pipe in the `@Pipe` meta data. so In The `MetaPipe` how can i use Reflect (or something better) in order to inject the correct `pipe` according to the name? – royB Aug 31 '16 at 15:55
  • You could provide an object (map from name to pipe) like `{provide: 'pipes', useFactory: (pipe1, pipe2) => {pipe1Name: pipe1, pipe2Name: pipe2}, deps: [Pipe1, Pipe2]}` and then inject it to the meta pipe `constructor(@Inject('pipes') private pipes) {};` and use it like `result = this.pipes[pipe].transform(result);` – Günter Zöchbauer Aug 31 '16 at 16:02
  • your solution works perfect but i've did it with pipe injection. I'm trying now your solution with PipesProvider. I couldn't find any example where `useFactory` is a map between label and a class. I'm getting an error 'ts7028 unused label` no matter what i'm doing. Can you please add a more concrete example please – royB Sep 08 '16 at 11:40
  • Please create a new question where you post the code that demonstrates what you try to accomplish and where you failed. – Günter Zöchbauer Sep 08 '16 at 11:41
  • http://stackoverflow.com/questions/39390759/usefactory-get-value-results-in-undefined. thanks – royB Sep 08 '16 at 12:20
4

For runtime compile only you can create a directive which will compile template dynamically.

UPDATE:

Using compileModuleAndAllComponentsAsync for RC.6^

dynamic-pipe.ts

  ngAfterViewInit() {
    const data = this.data.content;
    const pipe = this.data.pipe;

    @Component({
      selector: 'dynamic-comp',
      template: '{{ data | ' + pipe  + '}}'
    })
    class DynamicComponent  {
        @Input() public data: any;
    };

    @NgModule({
      imports: [BrowserModule],
      declarations: [DynamicComponent]
    })
    class DynamicModule {}

    this.compiler.compileModuleAndAllComponentsAsync(DynamicModule)
      .then(({moduleFactory, componentFactories}) => {
        const compFactory = componentFactories.find(x => x.componentType === DynamicComponent);
        const injector = ReflectiveInjector.fromResolvedProviders([], this.vcRef.parentInjector);
        const cmpRef = this.vcRef.createComponent(compFactory, 0, injector, []);
        cmpRef.instance.data = data;
      });
  }

Plunker sample RC.6^


OBSOLETE SOLUTION

In RC.5 you can use Compiler.compileComponentSync/Async to do that:

dynamic-pipe.ts

@Directive({
  selector: 'dynamic-pipe' 
})
export class DynamicPipe {
  @Input() data: CellModel;

  constructor(private vcRef: ViewContainerRef, private compiler: Compiler) {}

  ngAfterViewInit() {
    const metadata = new ComponentMetadata({
      template: '{{ data | ' + this.data.pipe  + '}}'
    });

    const data = this.data.content;
    const decoratedCmp = Component(metadata)(class DynamicComponent {  data = data; });

    this.compiler.compileComponentAsync(decoratedCmp)
      .then(factory => {
        const injector = ReflectiveInjector.fromResolvedProviders([], 
           this.vcRef.parentInjector);
        this.vcRef.createComponent(factory, 0, injector, []);
      });
  }
}

And use it this way:

<template ngFor let-cell [ngForOf]="row">
   <td><dynamic-pipe [data]="cell"></dynamic-pipe></td>
</template>

See also the plunker sample RC.5 that demonstrates this feature.

Anyway i think Günter's solution is preferable

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Perfect!. Any known performance hits? using `ReflectiveInjector`? – royB Aug 31 '16 at 17:19
  • Your rc.6 Plunker doesn't work any longer because it isn't pointing specifically to rc.6 Angular packages. Now that rc.7 is out, it doesn't work, but only one quick fix, see updated Plunker: https://plnkr.co/edit/mi85w6QnCeq7zY1VDoaQ?p=preview – Jeremy Zerr Sep 14 '16 at 17:15
  • A version of the Plunker that has the Angular version fixed at rc.6 is here: https://plnkr.co/edit/pvfijMMOfUinMRwwvDde?p=preview – Jeremy Zerr Sep 14 '16 at 17:16