Creating Pipes page
Learn how to create a custom pipe in Angular that returns a modified version of a string.
Problem
You may have noticed an image error in our rendered html page. We’re using an API in this demo that wasn’t built for our exact purposes, and we need a different image path for our app to serve.
In this exercise, we will fix the path of the thumbnail images in src/app/restaurant/restaurant.component.html.
Currently the path is written out like:
<img
alt=""
src="{{ restaurant.images.thumbnail }}"
width="100"
height="100"
/>
restaurant.images.thumbnail
will be a path like node_modules/place-my-order-assets/image.png
. We need to change that path to be more like ./assets/image.png
. Once
the path rewriting is fixed, images will show up correctly.
What you need to know
- How to generate a pipe
- How to use a pipe to transform data
How to Generate a Pipe via the CLI
Generate a pipe with the following command:
ng g pipe imageUrl
This will generate a pipe file: image-url.pipe.ts
How to Build a Pipe
Angular Pipes come in handy to transform content in our templates. Pipes allow us to transform data to display to the user in our HTML without modifying the original source.
Angular comes with several built-it pipes like DatePipe, UpperCasePipe, LowerCasePipe, CurrencyPipe, and PercentPipe. These pipes can be used in templates to modify the way data displays. We can build custom pipes as well. Pipes require one parameter - the value we want to change, but can take an additional parameters as well.
This example takes a price to be transformed and a parameter to use as the currency symbol.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION, Pipe, PipeTransform } = ng.core;
@Pipe({ name: 'currencyFormat' })
export class CurrencyFormatPipe implements PipeTransform {
transform(value: number, symbol: string = '$'): string {
return `${symbol}${value / 100}`;
}
}
@Component({
selector: 'my-app',
template: `
<h2>Prices</h2>
<p>USD price: {{ 1522 | currencyFormat:'$' }}</p>
`
})
class AppComponent {
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [ BrowserModule,
CommonModule],
declarations: [AppComponent, CurrencyFormatPipe],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Technical requirements
- Use an
imageUrl
pipe in src/app/restaurant/restaurant.component.html to rewrite the path. Using a pipe looks like the following:
<img
alt=""
src="{{ restaurant.images.thumbnail }}"
width="100"
height="100"
/>
- Generate and implement the
imageUrl
pipe.
The pipe will take an image url and transform it to the path we actually want to serve the image from. For example, from node_modules/place-my-order-assets
to ./assets
. This pipe will be used on our restaurant image thumbnail.
Hint: Use String.prototype.replace to create the new path with image name.
Setup
✏️ Run the following to generate the pipe and the pipe’s tests:
ng g pipe imageUrl
✏️ Update src/app/restaurant/restaurant.component.html file to use the pipe we will create:
<div class="restaurants">
<h2 class="page-header">Restaurants</h2>
<ng-container *ngIf="restaurants.length">
<div class="restaurant" *ngFor="let restaurant of restaurants">
<img
alt=""
src="{{ restaurant.images.thumbnail | imageUrl }}"
width="100"
height="100"
/>
<h3>{{ restaurant.name }}</h3>
<div class="address" *ngIf="restaurant.address">
{{ restaurant.address.street }}<br />{{ restaurant.address.city }},
{{ restaurant.address.state }} {{ restaurant.address.zip }}
</div>
<div class="hours-price">
$$$<br />
Hours: M-F 10am-11pm
<span class="open-now">Open Now</span>
</div>
<a class="btn" [routerLink]="['/restaurants', restaurant.slug]">
Details
</a>
<br />
</div>
</ng-container>
</div>
Having issues with your local setup?
You can get through most of this tutorial by using an online code editor. You won’t be able to run our tests to verify your solution, but you will be able to make changes to your app and see them live.
You can use one of these two online editors:
How to verify your solution is correct
✏️ Update the restaurant spec file src/app/restaurant/restaurant.component.spec.ts to include the new pipe:
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ImageUrlPipe } from '../image-url.pipe';
import { RestaurantComponent } from './restaurant.component';
describe('RestaurantComponent', () => {
let fixture: ComponentFixture<RestaurantComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [RestaurantComponent, ImageUrlPipe],
}).compileComponents();
fixture = TestBed.createComponent(RestaurantComponent);
});
it('should create', () => {
const component: RestaurantComponent = fixture.componentInstance;
expect(component).toBeTruthy();
});
it('should render title in a h2 tag', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h2')?.textContent).toContain('Restaurants');
});
it('should not show any restaurants markup if no restaurants', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.restaurant')).toBe(null);
});
it('should have two .restaurant divs', fakeAsync((): void => {
fixture.detectChanges();
tick(501);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const restaurantDivs = compiled.getElementsByClassName('restaurant');
const hoursDivs = compiled.getElementsByClassName('hours-price');
expect(restaurantDivs.length).toEqual(2);
expect(hoursDivs.length).toEqual(2);
}));
it('should display restaurant information', fakeAsync((): void => {
fixture.detectChanges();
tick(501);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.restaurant h3')?.textContent).toContain(
'Poutine Palace'
);
}));
});
✏️ Update the spec file src/app/image-url.pipe.spec.ts to be:
import { ImageUrlPipe } from './image-url.pipe';
describe('ImageUrlPipe', () => {
it('create an instance', () => {
const pipe = new ImageUrlPipe();
expect(pipe).toBeTruthy();
});
it('returns a string with proper assets path', () => {
const pipe = new ImageUrlPipe();
expect(pipe.transform('node_modules/place-my-order-assets/tacos.png')).toBe(
'./assets/tacos.png'
);
});
});
Solution
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/app/image-url.pipe.ts to:import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'imageUrl'
})
export class ImageUrlPipe implements PipeTransform {
transform(value: string): string {
return value.replace('node_modules/place-my-order-assets', './assets');
}
}