7

The subject
I'm building an abstract table component to which I pass what pipe it should use in certain columns. As the data passed may vary, so the pipes should vary as well.

The goal
To use whatever pipe is passed to the table

The project
Here's how it should look like in html in my opinion

<!-- html --> 

<span *ngIf="element.pipe">{{ row[element.column] | <<here_inject_an_appropriate_pipe>> }}</span>

The column settings are passed through an object and have form of

//typescript  

columnSettings: [
    ...
    {column: 'fruitExpDate', caption: 'Best before', pipe: 'date: \"' + PIPE_DATE_FORMAT + '\"' },
    ...
]

and PIPE_DATE_FORMAT holds the string 'yyyy-MM-dd'

What I tried

  1. Passing the pipe directly through a variable like
<!-- html --> 

<span *ngIf="element.pipe">{{ row[element.column] | element.pipe }}</span>
  1. Creating custom pipe which takes another pipe as an argument
@Pipe({
    name: 'dynamicPipe',
})
export class DynamicPipe implements PipeTransform {
    // constructor(private abstractTableService: AbstractTableService) {}

    transform(value: any, pipe: string): any {
        const pipeToken: any = pipe.split(':')[0].replace(/[\s]+/g, '');
        const pipeArgs: any = pipe.split(':')[1].replace(/[\s]+/g, '');

        console.log(value);
        console.log(pipe);

        // return pipeToken.transform(value, ...pipeArgs);
        return 'check pipe';
    }
}

and here I tried many different things to call the requested pipe but eventually didn't figure out how to do this. Here's my html with the custom pipe:

<!-- html --> 

<span *ngIf="element.pipe">{{ row[element.column] | dynamicPipe: element.pipe }}</span>
  1. Creating a custom service to call imported pipes
@Injectable()
export class AbstractTableService {
    constructor(
        private date: DatePipe,
    ) {}

    getDatePipe(): DatePipe {
        return this.date;
    }
}

but here I had no idea how to use this service effectively.

big_OS
  • 381
  • 7
  • 20

5 Answers5

12

You need to create an instance of the selected pipe inside a dynamic pipe. To do that, you can utilize Angular injector. The dynamic pipe (what I call it) can be something like this:

import { Pipe, PipeTransform, Injector, Type } from '@angular/core';

@Pipe({
  name: 'dynamicPipe'
})
export class DynamicPipe implements PipeTransform {

  constructor(private injector: Injector) {}

  transform(value: any, requiredPipe: Type<any>, pipeArgs: any): any {
    const injector = Injector.create({
      name: 'DynamicPipe',
      parent: this.injector,
      providers: [
        { provide: requiredPipe }
      ]
    })
    const pipe = injector.get(requiredPipe)
    return pipe.transform(value, pipeArgs);
  }

}

Make sure to pass the pipe class (type) as args not a string representation of its name. If you are going to pass a string, let's say the data comes from server-side, you might need to consider creating a mapping for that.

A fully working example can be found here: https://stackblitz.com/edit/angular-ivy-evzwnh

This is a rough implementation. I am not sure about Tree-Shaking. It needs more testing and optimization.

Aboodz
  • 1,549
  • 12
  • 23
  • That's exactly what I need! Can you tell me one more thing - how do I pass and later handle pipe argument like for example the format string in case of DatePipe? With `providers: [{provide: args, useClass: args, useValue: pipeArgs}],` or somehow else? – big_OS Jul 20 '20 at 09:51
  • I have updated the answer and stackblitz to include pipeArgs. You only need to pass a the args to the dynamicPipe. Simply, we pass those args along with the value `transform(value, pipeArgs)`. – Aboodz Jul 20 '20 at 10:18
1

pipe is NOT a string, so you can't use pipe:'date: \"' + PIPE_DATE_FORMAT + '\"'

Your second aproach is closed to you want get it, but you need use a switch case

NOTE 1: From Angular 9 you can use directly the functions: formatDate, formatNumber, formatCurrency and formatPercent

import { formatDate,formatNumber,formatCurrency,formatPercent } from '@angular/common';


transform(value: any, pipe: string): any {
    const pipeToken: any = pipe.split(':')[0].replace(/[\s]+/g, '');
    const pipeArgs: any = pipe.split(':')[1].replace(/[\s]+/g, '');
    let result=value;
    switch (pipeToken)
    {
       case "date":
         result=formatDate(value,pipeArgs) //(*)
         break
       case "number"
         result=formatNumber(value,pipeArgs) //(*)
         break
       ...
    }
    return result;
}

(*) check the docs to know how use the functions, I write "pseudo-code"

NOTE 2: perhafs if you create your "columns objects" with two properties, pipeKing and args- this one as an array-, your pipe becomes more confortable

e.g.

  {
   column: 'fruitExpDate', 
   caption: 'Best before', 
   pipeKind: 'date'
   pipeArgs:[PIPE_DATE_FORMAT]
  }
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • That sounds good. But I wonder if you know about any way to call a specific pipe in ts file by its name just like we do this in html? – big_OS Jul 20 '20 at 06:57
1
import { Pipe, PipeTransform } from '@angular/core';

export type OmitFirstArg<T extends unknown[]> = T extends [unknown, ...infer U] ? U : never;

@Pipe({
  name: 'dynamicPipe',
  pure: true
})
export class DynamicPipe<P extends PipeTransform> implements PipeTransform {
  public transform(
    value: Parameters<P['transform']>[1],
    pipeTransform: P,
    pipeArgs?: OmitFirstArg<Parameters<P['transform']>>): ReturnType<P['transform']> | unknown {
    if (!('transform' in pipeTransform)) {
      return value;
    }
    return pipeTransform.transform(value, ...(pipeArgs || []));
  }
}
Londeren
  • 3,202
  • 25
  • 26
0

Try to change your original model and separate the pipe type and the pipe parameters. It would help a lot, let's say, you have this:

columnSettings: [
    ...
    {
      column: 'fruitExpDate', 
      caption: 'Best before', 
      pipeType: 'date', 
      pipeParams: PIPE_DATE_FORMAT 
    },
    ...
]

Then you could write your 'dynamic pipe' logic in the html, and using existing pipes directly:

<ng-container [ngSwitch]="element.pipeType">
   <ng-container *ngSwitchCase="'date'">{{ row[element.column] | date:element.pipeParams }}</ng-container>
   <ng-container *ngSwitchCase="'...'">...</ng-container>
   <some-element *ngSwitchDefault>...</some-element>
</ng-container>
Marcell Kiss
  • 538
  • 3
  • 11
0

For conditional piping you can map those items and transform accordingly.

columnSettings: [
...
 {
   column: 'fruitExpDate', 
   caption: 'Best before', 
   pipeType: 'date', 
   pipeParams: PIPE_DATE_FORMAT 
 },
 ...
]


columnSettings = columnSettings.map((col) => {
   if(col.pipeType === 'date) {
      col.pipeVal = (new DatePipe).transform(col) // or whatever you want to pass
   } else if(col.pipeType === 'blah) {
      col.pipeVal = (new BlahPipe).transform(col) // or whatever you want to pass
   }
  return col;
})

Now in the template you won't need any conditional statement but rather just print col.pipeVal

Basheer Kharoti
  • 4,202
  • 5
  • 24
  • 50