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

  1. 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"
/>
  1. 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, when you run npm run test all tests will pass!

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');
  }

}