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 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 the value to be transformed and a parameter to use as an exponential multiplier.

<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: 'exponentialStrength'})
class ExponentialStrengthPipe implements PipeTransform {
  transform(value: number, exponent: number): number {
    return Math.pow(value, isNaN(exponent) ? 1 : exponent);
  }
}

@Component({
  selector: 'my-app',
  template: `
  <h2>Power Booster</h2>
  <p>Super power boost: {{2 | exponentialStrength: 10}}</p>
  `
})
class AppComponent {
  constructor() {
  }
}

// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;

@NgModule({
  imports: [ BrowserModule,
  CommonModule],
  declarations: [AppComponent, ExponentialStrengthPipe],
  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 src="{{ restaurant.images.thumbnail | imageUrl }}" />
  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

✏️ 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
        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>

✏️ Run the following to generate the pipe and the pipe's tests:

ng g pipe imageUrl

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

If you've implemented the solution correctly, when you run npm run test all tests will pass!

Solution

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