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.
Table of contents
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!
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
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!