Step by Step Custom Pipes in Angular blog post

Step by Step Custom Pipes in Angular

Todd Motto

19 Sep, 2019

Angular

6 minutes read

Angular has many Pipes built-in – but they only take us so far and can be fairly limiting when expanding out more complex logic in our applications. This is where the concept of creating a Custom Pipe in Angular comes into play, allowing us to nicely extend our applications.

What are Pipes in Angular?

Before we get started, if you’re new to Angular and the concept of Pipes, let’s demonstrate what a Pipe is before we move on to showing a Custom Pipe.

Angular has a few built-in Pipes that ship with the framework’s CommonModule, allowing us to make use of them in any module we’re writing.

Here are a few usual suspects we could encounter with Angular’s built-in Pipes:

  • DatePipe (for parsing Date objects)
  • UpperCasePipe (for uppercase-ing Strings)
  • LowerCasePipe (for lowercase-ing Strings)
  • CurrencyPipe (for formatting currencies)
  • AsyncPipe (for unwrapping asynchronous values, such as Observables!)

You can think of Pipes in Angular just like you would a function. A function can take parameters and return us something new – and that’s solely what Pipes do! We could pass in a valid Date and be given back a String value that’s nicely formatted for the UI. And here, the word UI is key as Pipes are typically for transforming data between our Model and View (the UI)!

That’s the essence of a Pipe!

So, how do we use a Pipe? Let’s assume some simple component HTML with a binding of a Date stamp:

<div>
  <!-- Renders: 21/10/2019 -->
  <p>{{ myDateValue | date:'M/d/yy' }}</p>
</div>

This could render out as above with the formatted Date. So that’s a valid use case for Pipes! We don’t really want to fetch data and then loop through it all and convert each date from a Date object to a String, as we’d lose native Date object functionality and be duplicating values. It’s super convenient to use a Pipe and let it parse out for us!

Now you’re ready to start venturing into Custom Pipes! This will allow us to use a function to create our own input and output based on what you’re supplying. Let’s dive in!

Custom Pipes in Angular

The most basic of pipe transforms a single value, into a new value. This value can be anything you like, a string, array, object, etc.

For the demonstration of this, we’ll be converting numeric filesizes into more human readable formats, such as "2.5MB" instead of something like "2120109". But first, let’s start with the basics – how we’ll use the Pipe.

Using Custom Pipes

Let’s assume an image was just uploaded via a drag and drop zone – and we’re getting some of the information from it. A simplified file object we’ll work with:

export class FileComponent {
  file = { name: 'logo.svg', size: 2120109, type: 'image/svg' };
}

Properties name and type aren’t what we’re really interested in to learn about Pipes – however size is the one we’d like. Let’s put a quick example together for how we’ll define the usage of our pipe (which will convert numbers into filesizes):

<div>
  <p>{{ file.name }}</p>
  <p>{{ file.size | filesize }}</p>
</div>

Creating a Custom Pipe

To create a Pipe definition, we need to first create a class (which would live in its own file). We’ll call this our FileSizePipe, as we are essentially transforming a numeric value into a string value that’s more human readable:

export class FileSizePipe {}

Now we’ve got this setup, we need to name our Pipe. In the above HTML, we did this:

<p>{{ file.size | filesize }}</p>

So, we need to name the pipe "filesize". This is done via another TypeScript decorator, the @Pipe:

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

@Pipe({ name: 'filesize' })
export class FileSizePipe {}

All we need to do is supply a name property that corresponds to our template code name as well (as you’d imagine).

Don’t forget to register the Pipe in your @NgModule as well, under declarations:

// ...
import { FileSizePipe } from './filesize.pipe';

@NgModule({
  declarations: [
    //...
    FileSizePipe,
  ],
})
export class AppModule {}

Pipes tend to act as more "utility" classes, so it’s likely you’ll want to register a Pipe inside a shared module. If you want to use your custom Pipe elsewhere, simply use exports: [YourPipe] on the @NgModule.

Pipe and PipeTransform

Once we’ve got our class setup, registered, and the @Pipe decorator added – the next step is implementing the PipeTransform interface:

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

@Pipe({ name: 'filesize' })
export class FileSizePipe implements PipeTransform {
  transform() {}
}

This creates a required contract that our FileSizePipe must adhere to the following structure:

export interface PipeTransform {
  transform(value: any, ...args: any[]): any;
}

Which is why we added the transform() {} method to our class above.

Pipe Transform Value

As we’re using our Pipe via interpolation, this is the magic on how we’re given arguments in a Pipe.

{{ file.size | filesize }}

The file.size variable is passed straight through to our transform method, as the first argument.

We can call this our size and type it appropriately:

//...
export class FileSizePipe implements PipeTransform {
  transform(size: number) {}
}

From here, we can implement the logic to convert the numeric value into a more readable format of megabytes.

//...
export class FileSizePipe implements PipeTransform {
  transform(size: number): string {
    return (size / (1024 * 1024)).toFixed(2) + 'MB';
  }
}

We’re returning a type string as we’re appending 'MB' on the end. This will then give us:

<!-- 2.02MB -->
{{ file.size | filesize }}

We can now demonstrate how to add your own custom arguments to custom Pipes.

Pipes with Arguments

So let’s assume that, for our use case, we want to allow us to specify the extension slightly differently than advertised.

Before we hit up the template, let’s just add the capability for an extension:

//...
export class FileSizePipe implements PipeTransform {
  transform(size: number, extension: string = 'MB'): string {
    return (size / (1024 * 1024)).toFixed(2) + extension;
  }
}

I’ve used a default parameter value instead of appending the 'MB' to the end of the string. This allows us to use the default 'MB', or override it when we use it. Which takes us to completing our next objective of passing an argument into our Pipe:

<!-- 2.02megabyte -->
{{ file.size | filesize:'megabyte' }}

And that’s all you need to supply an argument to your custom Pipe. Multiple arguments are simply separated by :, for example:

{{ value | pipe:arg1 }}
{{ value | pipe:arg1:arg2 }}
{{ value | pipe:arg1:arg3 }}

Don’t forget you can chain these pipes alongside others, like you would with dates and so forth.

Here’s the final assembled code:

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

@Pipe({ name: 'filesize' })
export class FileSizePipe implements PipeTransform {
  transform(size: number, extension: string = 'MB') {
    return (size / (1024 * 1024)).toFixed(2) + extension;
  }
}

Want a challenge? Extend this custom Pipe that allows you to represent the Pipe in Gigabyte, Megabyte, and any other formats you might find useful. It’s always a good exercise to learn from a starting point!

To learn more techniques, best practices and real-world expert knowledge I’d highly recommend checking out my Angular courses – they will guide you through your journey to mastering Angular to the fullest!

About the author

Todd Motto profile picture

Todd Motto

GDE Google Developer Expert

Todd is the Founder of Ultimate Courses. With a passion for Angular, TypeScript and JavaScript, Todd leads the online courses creation and has written hundreds of articles on front-end web development and beyond. He specialises in breaking down complex topics and understands the critical mission of learning new technology fast, comprehensively and the right way.

Love the post? Share it!

Lots of time and effort go into all our blogs, resources and demos,
we'd love it if you'd spare a moment to share them!

Explore our Angular courses

Get started today and join over 60,000 developers.