Dynamic pipe in Angular 2
Asked Answered
H

8

23

I'm trying to create a component where you can pass which pipe that should be used for a list inside the component. From what I could find by testing and looking around for answers the only solution appears to create something like:

<my-component myFilter="sortByProperty"></my-component>

my-component template:

<li *ngFor="#item of list | getPipe:myFilter"></li>

Which then maps myFilter to the correct pipe logic and runs it, but this seems a bit dirty and not optimal.

I thought they would have come up with a better solution to this problem since Angular 1 where you would also do something along these lines.

Is there not a better way to do this in Angular 2?

Hanway answered 12/4, 2016 at 6:17 Comment(2)
is getpipe your custom filter?Irenics
Not only could you do this in AngularJS, you can also do this in Angular +2. The technique is similar to what @Balu posted below.Quartana
I
59

Building on borislemke's answer, here's a solution which does not need eval() and which I find rather clean:

dynamic.pipe.ts:

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


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

    public constructor(private injector: Injector) {
    }

    transform(value: any, pipeToken: any, pipeArgs: any[]): any {
        if (!pipeToken) {
            return value;
        }
        else {
            let pipe = this.injector.get(pipeToken);
            return pipe.transform(value, ...pipeArgs);
        }
    }
}

app.module.ts:

// …
import { DynamicPipe } from './dynamic.pipe';

@NgModule({
  declarations: [
    // …
    DynamicPipe,
  ],
  imports: [
    // …
  ],
  providers: [
    // list all pipes you would like to use
    PercentPipe,
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.ts:

import { Component, OnInit } from '@angular/core';
import { PercentPipe } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
    The following should be a percentage: 
    {{ myPercentage | dynamicPipe: myPipe:myPipeArgs }}
    `,
  providers: []
})

export class AppComponent implements OnInit {
  myPercentage = 0.5;
  myPipe = PercentPipe;
  myPipeArgs = [];
}
Insistency answered 24/10, 2017 at 12:32 Comment(7)
I can't get this to work. I get the following error Error: StaticInjectorError(AppModule)[date]: StaticInjectorError(Platform: core)[date]: NullInjectorError: No provider for date!Irizarry
@Irizarry Did you add the DatePipe to your app module's providers?Insistency
All my pipes are exported through a shared.module which is provided in the app.module. When I implement the code as done above it works. However I made myPipe an @Input() pipe in my code so that it can be used genericallyIrizarry
@Irizarry if you figured out the answer, please comment or add an answer with what you did to figure it out. Other people want to know, too.Vig
I had to add a check to pipeArgs to ensure it was an array before applying the spread (...) to it, otherwise it was not providing my non-dynamic pipe with the args. if (Array.isArray(pipeArgs)) { return pipe.transform(value, ...pipeArgs); } else { return pipe.transform(value, pipeArgs); } It used to work fine in Angular 7 with Typescript 3.1.6, but since upgrading to Angular 8 with Typescript 3.5.3 I've encountered issues.Uzbek
@Shawn I'm afraid I've been out of the Angular business for too long, so someone else will have to weigh in.Insistency
If you have your pipes in a shared module and you are getting NullInjection... you need to add the pipes to the providers inside your shared module and not in appModuleBaroque
G
4

The easiest way to tackle this would be to not use the pipes in the HTML templates, but instead, inject the pipe into a component's constructor (using DI), then apply the transform functionally. This works quite well with an Observable map or similar rxjs streams.

Gallardo answered 19/3, 2017 at 23:23 Comment(1)
Good suggestion, however it would still require a wrapper service if it's to be used the way I want.Hanway
B
3

Unfortunately I don't think so. It's the same as in angular1 where you have a function return a string for the dynamic Pipe you want.

Looking at the docs that's exactly how they show it as well.

https://angular.io/docs/ts/latest/guide/pipes.html

template: `
   <p>The hero's birthday is {{ birthday | date:format }}</p>
   <button (click)="toggleFormat()">Toggle Format</button>
`

Then in the controller:

get format()   { return this.toggle ? 'shortDate' : 'fullDate'}

Alas, it could be worse! :)

Bolter answered 12/4, 2016 at 8:32 Comment(0)
P
3

I managed to get something working, it's a bit dirty and evil (with eval) but it does the trick for me. In my case, I have a table component with different data types in each row (e.g title, url, date, status). In my database, status is marked as either 1 as enabled or 0 for disabled. Of course, it is more preferable to be showing enabled/disabled to my user. Also, my title column is multilingual, which makes it an object with either en or id as it's key.

// Example row object:
title: {
    "en": "Some title in English",
    "id": "Some title in Indonesian"
},
status: 1 // either 1 or 0

Ideally, I need 2 different pipes to convert my data to show to my app's user. Something like translateTitle and getStatus will do fine. Let's call the parent's pipe dynamicPipe.

/// some-view.html
{{ title | dynamicPipe:'translateTitle' }}
{{ status | dynamicPipe:'getStatus' }}


/// dynamic.pipe.ts
//...import Pipe and PipeTransform

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

    transform(value:string, modifier:string) {
        if (!modifier) return value;
        return eval('this.' + modifier + '(' + value + ')')
    }

    getStatus(value:string|number):string {
        return value ? 'enabled' : 'disabled'
    }

    translateTitle(value:TitleObject):string {
        // defaultSystemLanguage is set to English by default
        return value[defaultSystemLanguage]
    }
}

I'll probably get a lot of hate on using eval. Hope it helps!

Update: when you might need it

posts = {
    content: [
        {
            title:
                {
                    en: "Some post title in English",
                    es: "Some post title in Spanish"
                },
            url: "a-beautiful-post",
            created_at: "2016-05-15 12:21:38",
            status: 1
        },
        {
            title:
                {
                    en: "Some post title in English 2",
                    es: "Some post title in Spanish 2"
                },
            url: "a-beautiful-post-2",
            created_at: "2016-05-13 17:53:08",
            status: 0
        }
    ],
    pipes: ['translateTitle', null, 'humanizeDate', 'getStatus']
}

<table>
    <tr *ngFor="let row in posts">
        <td *ngFor="let column in row; let i = index">{{ column | dynamicPipe:pipes[i] }}</td>
    </tr>
</table>

Will return:

| title          | url            | date           | status         |
| Some post t...   a-beautiful...   an hour ago      enabled
| Some post ...2   a-beautifu...2   2 days ago       disabled
Prisoner answered 10/7, 2016 at 20:18 Comment(0)
C
2

Building on @Balu answer this what I had to do to get it working with Angular 9

import { Injector, Pipe, PipeTransform } from '@angular/core';
import { PercentPipe, CurrencyPipe, DecimalPipe } from '@angular/common';

@Pipe({
    name: 'dynamicPipe'
})

export class DynamicPipe implements PipeTransform {

    public constructor(private injector: Injector, private percentPipe: PercentPipe) {
    }

    transform(value: any, pipeToken: any, pipeArgs: any[]): any {

        const MAP = { 'currency': CurrencyPipe, 'decimal': DecimalPipe, 'percent': PercentPipe }

        if (pipeToken && MAP.hasOwnProperty(pipeToken)) {
            var pipeClass = MAP[pipeToken];
            var pipe = this.injector.get(pipeClass);
            if (Array.isArray(pipeArgs)) {
                return pipe.transform(value, ...pipeArgs);
            } else {
                return pipe.transform(value, pipeArgs);
            }
        }
        else {
            return value;
        }
    }
}
Conley answered 17/4, 2020 at 15:51 Comment(0)
D
0

I handled this by sending the pipe provider to the component and it runs the transform method. And It works with Angular 9. I hope it helps someone! Demo: https://stackblitz.com/edit/angular-kdqc5e

pipe-injector.component.ts:

import { Component, OnInit, Input, PipeTransform } from '@angular/core';
    @Component({
      selector: 'pipe-injector',
      template: `
        Should inject my pipe provider 
        {{ getText() }}
        `,
      providers: []
    })


    export class PipeInjectorComponent {
      @Input() pipeProvider: PipeTransform;
      @Input() pipeArgs: Array<any>;
      @Input() textToFormat: string;

      getText() {
        return this.pipeProvider.transform(this.textToFormat, ...this.pipeArgs);
      }
    }

app-component.ts:

import { Component, OnInit } from '@angular/core';
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-root',
  template: `
    <pipe-injector [pipeProvider]="pipeProvider" [pipeArgs]="pipeArgs" textToFormat='05-15-2020'> 
    </pipe-injector>
    `,
  providers: []
})

export class AppComponent implements OnInit {
  pipeArgs = ['dd/MM/yyyy'];
  constructor(public pipeProvider: DatePipe) {}
}

app.module.ts:

import { DatePipe } from '@angular/common';
import { PipeInjectorComponent } from './pipe-injector.component';

@NgModule({
  declarations: [

    PipeInjectorComponent,
  ],
  imports: [
  ],
  providers: [
    DatePipe,
    ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Dacey answered 15/5, 2020 at 13:18 Comment(0)
V
0

I've added some typings to the @balu's answer

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 || []));
  }
}
Vivianna answered 1/2, 2022 at 11:41 Comment(0)
U
0

if you want to pass multiple pipes dynamically you can use this implementation

(extended solution from @shawn)

import { Injector, Pipe, PipeTransform } from '@angular/core';
import { DecimalPipe, SlicePipe } from '@angular/common';
import { TitlecasePipe } from './titlecase.pipe';

@Pipe({
  name: 'dynamicPipe',
})
export class DynamicPipe implements PipeTransform {
  public constructor(private injector: Injector) {}

  transform(value: any, pipeToken: any, pipeArgs: any[]): any {
    console.log(value, pipeToken);

    const MAP = {
      titlecase: TitlecasePipe,
      decimal: DecimalPipe,
      slice: SlicePipe,
    };

    const func = (pipe: any, args: any, val) => {
      if (pipe && MAP.hasOwnProperty(pipe)) {
        var pipeClass = MAP[pipe];
        var pipe = this.injector.get<any>(pipeClass);
        return Array.isArray(args)
          ? pipe.transform(val, ...args)
          : pipe.transform(val, args);
      } else {
        return val;
      }
    };

    if (!Array.isArray(pipeToken)) {
      return func(pipeToken, pipeArgs, value);
    } else {
      return pipeToken.reduce((acc, curr, index) => {
        return func(curr, pipeArgs[index], acc);
      }, value);
    }
  }
}

in html pass pipeToken as array and maintain pipeArgs array with same index

 {{ title | dynamicPipe: ["slice" , "titlecase"] :[6]}}
Unequal answered 27/7, 2022 at 17:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.