Calling the transform method of an Angular Pipe in the component class code

Calling the transform method of an Angular Pipe in the component class code

Posted on

Target audience

This article is intended to help anybody that is looking for ways to call the transform method of any Angular Pipe in a component class code or in other TypeScript code.

We will also learn about some other simple trick on how to design Angular pipes and how to test them in those particular cases.

Introduction

There are several cases where you would want to reuse the transform functionality of a pipe in your component class code. The most simple example is formatting dates in an Angular component's template. The DatePipe, provided by the Angular framework itself, is imported in the providers of the component and then used by piping the value in the template.

For example, {{currentDate | date : "fullDate"}} would print out something like "Friday, September 29, 2023" in the template. If you would like to use the same functionality in the component class code, you can inject the DatePipe and then call the transform method manually.

Component example:

import { DatePipe } from '@angular/common';

@Component({
  ...
  template: `{{currentDate | date : "fullDate"}}`
  imports: [DatePipe],
  standalone: true,
})
export class ExampleComponent {
  constructor(public currentDate = new Date()) {
    const formattedDate = inject(DatePipe).transform(this.currentDate, 'fullDate');
    console.log(formattedDate);
  }
}

Note: This is not the way I would personally recommend to reuse the functionality of any (Angular build-in) pipe. Continue reading to find out why there are better ways.

Injection Context

This method can only be used wherever you are in your code that has an Injection Context. You could use the runInInjectionContext function to make sure you have access to the right providers, but still you would need access to the right injector.

Another way would be to manually instantiate the DatePipe class. In that case you probably need the applicable LOCALE_ID of your application, leading to again, needing the right injection context. 🤷🏼

A better way to reuse the functionality of a pipe

Angular build-in Pipes

Next to providing common functionality like formatting dates, percentages, currencies or numbers through pipes, Angular also exposes the inner working of those pipes directly as pure functions. For example, considering the use case from above, where we injected the DatePipe to be able to use the transform method, can be rewritten as follows:

Component example, with formatDate:

import { formatDate } from '@angular/common';

@Component({
    ...
})
export class ExampleComponent {
  constructor(){
    const formattedDate = formatDate(currentDate, 'fullDate');
    console.log(formattedDate);
  }
}

We don't even need to inject the DatePipe anymore, because the formatDate is exposed as a pure function directly. But we still need to get the applicable LOCALE_ID somehow. So in this particular case, we can't get around needing the injection context.

Custom Pipes

When creating your own custom pipes for your applications and having the same need to reuse the functionality of the transform method, it's better to start with an external function first. This way you can easily use and test this function on its own:

sum-of-numbers.ts

export function sumOfNumbers(listOfNumbers: number[]): number {
  return listOfNumbers.reduce((a, b) => a + b, 0);
}

sum-of-numbers.spec.ts

describe('sumOfNumbers', () => {
  it('should return the sum of all numbers in the list', () => {
    const listOfNumbers = [1, 2, 3, 4, 5];
    const result = sumOfNumbers(listOfNumbers);
    expect(result).toBe(15);
  });
});

sum-of-numbers.pipe.ts

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

@Pipe({
  name: 'sumOfNumbers',
  standalone: true,
})
export class SumOfNumbersPipe implements PipeTransform {
    transform(listOfNumbers: number[]): number {
    return sumOfNumbers(listOfNumbers);
  }
}
// no need to test this pipe, as it's just a wrapper around the function
// and you would be testing instantiating a class, which is not necessary

Now that we have the function and its test covered, we can wrap it in a pipe. This way we can still use the function in our component class code, but also in our templates.

Example component:

import { SumOfNumbersPipe } from './sum-of-numbers.pipe';
import { sumOfNumbers } from './sum-of-numbers';

@Component({
  ...
  template: `{{listOfNumbers | sumOfNumbers}}`
  imports: [SumOfNumbersPipe],
  standalone: true,
})
export class ExampleComponent {
    constructor(public listOfNumbers = [1, 2, 3]) {
    console.log(sumOfNumbers(this.listOfNumbers));
  }
}

Conclusion

There is no wrong or right way of reusing the functionality of any (Angular build-in) pipe. It all depends on personal or project-specific preference and how your applications are structured. But at least now you know how I would do it, and I honestly believe it's a better way :). Thanks for reading!

Further reading

  1. Angular Documentation: Understanding Pipes
  2. Angular Pipe Decorator
  3. Angular Style Guide: Pipe names
  4. Angular Documentation: Injection Context

Contents

  1. Target audience
  2. Introduction
  3. Injection Context
  4. A better way to reuse the functionality of a pipe
  5. Angular build-in Pipes
  6. Custom Pipes
  7. Conclusion
  8. Further reading

By reading this article I hope you can find a solution for your problem. If it still seems a little bit unclear, you can hire me for helping you solve your specific problem or use case. Sometimes even just a quick code review or second opinion can make a great difference.