Item Total Pipe page

Write an Item Total Pipe to help manage item total calculation across our application.

Overview

In this part, we will:

  • Create a new Pipe called Item Totals
  • Use Angular Default Currency Pipe

Problem 1: Implementing and Using Item Total Pipe

Now that our application is coming together nicely, you might have noticed we are repeating the same code to calculate the total prices of our items. We can simplify this by creating an Item Total Pipe which we will use in the list.component.html template file and call directly using transform within order.component.ts file. The Pipe will transform an array of items and return the total sum of the price of each item.

After implementing this Pipe, you should be able to remove the total method from list.component.ts.

P1: What you need to know

How to:

  • Generate a Pipe using Angular CLI (you previously learned this in the Creating Pipes section! ✔️)
    ng g pipe itemTotal
  • Provide a Pipe globally to the entire application, so it can be used in any component

Provide Pipe Globally

Unlike services, Pipes are not readily injectable into our components. In order to be able to use a Pipe in a component’s .ts file, the Pipe has to be provided by a module. To provide the Pipe just created in the app module, simply add the Pipe to the providers array in the app.module.ts file. Learn More.

By including the Pipe in a component’s constructor, you gain the ability to run the Pipe using its transform method.

✏️ Update src/app/app.module.ts

    BrowserAnimationsModule,
    TabsModule.forRoot()
  ],
  providers: [ItemTotalPipe],
  bootstrap: [AppComponent]
})
export class AppModule { }

Now the Pipe is available to be used in components across the application.

The example below shows how to provide a Pipe globally in Angular. The Pipe being created will transform two separate words into one compound word. It will take a string value, then a parameter, that is used as a prefix to the string value.

// app.js
const { Component, VERSION, Pipe, PipeTransform} = ng.core;

@Pipe({ name: 'prefix' })
class PrefixPipe implements PipeTransform {
  transform(value: string, prefix: string): string {
    if (prefix && value) {
      return `${prefix}-${value}`;
    }
    return value;
  }
}

@Component({
  selector: 'my-app',
  template: `
    <h2>{{ word }}</h2>
    <br />
    <h2>{{ word2 | prefix: 'long' }}</h2>
  `,
})
class AppComponent implements OnInit {
  constructor(private prefixPipe: PrefixPipe) {}

  ngOnInit(): void {
    this.word = this.prefixPipe.transform(this.word, 'seventy');
  }
}


@NgModule({
  imports: [BrowserModule, CommonModule],
  declarations: [AppComponent, PrefixPipe],
  bootstrap: [AppComponent],
  providers: [PrefixPipe]
})
class AppModule {}

P1: How to verify your solution is correct

✏️ Update the following spec files to include and provide the new Pipe:

  • src/app/order/order.component.spec.ts
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { ItemTotalPipe } from '../item-total.pipe';
import { RestaurantService } from '../restaurant/restaurant.service';
import { MenuItemsComponent } from './menu-items/menu-items.component';
import { OrderComponent, OrderForm } from './order.component';
          provide: OrderService,
          useClass: MockOrderService,
        },
        ItemTotalPipe,
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  • src/app/app.component.spec.ts
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ImageUrlPipe } from './image-url.pipe';
import { ItemTotalPipe } from './item-total.pipe';
import { HistoryComponent } from './order/history/history.component';
import { OrderComponent } from './order/order.component';
import { DetailComponent } from './restaurant/detail/detail.component';
        DetailComponent,
        OrderComponent,
        HistoryComponent,
        ItemTotalPipe,
      ],
      providers: [
        { provide: RestaurantService, useClass: MockRestaurantService },
        ItemTotalPipe,
      ],
      schemas: [NO_ERRORS_SCHEMA],
    })
  • src/app/order/history/history.component.spec.ts
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { ItemTotalPipe } from '../../item-total.pipe';
import { ListComponent } from '../list/list.component';
import { Order, OrderService } from '../order.service';


  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [HistoryComponent, ListComponent, ItemTotalPipe],
      providers: [
        {
          provide: OrderService,
          useClass: MockOrderService,
        },
        ItemTotalPipe,
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  • src/app/order/list/list.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { ItemTotalPipe } from '../../item-total.pipe';
import { Order, OrderService } from '../order.service';

import { ListComponent } from './list.component';

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [ListComponent, ItemTotalPipe],
      providers: [
        {
          provide: OrderService,
          useClass: MockOrderService,
        },
        ItemTotalPipe,
      ],
    }).compileComponents();
    injectedOrderService = TestBed.inject(OrderService);

✏️ Update the spec file item-total.pipe.spec.ts to be:

import { ItemTotalPipe } from './item-total.pipe';
import { Item } from './order/order.service';

describe('ItemTotalPipe', () => {
  const mockItemList: Item[] = [
    { name: 'Truffle Noodles', price: 14.99 },
    { name: 'Garlic Fries', price: 15.99 },
  ];
  const pipe = new ItemTotalPipe();

  it('create an instance', () => {
    expect(pipe).toBeTruthy();
  });

  it('transform Item[] and return the sum of each item price, total price should be 30.98', () => {
    const total = pipe.transform(mockItemList);
    expect(total).toEqual(30.98);
  });

  it('expect empty array to be 0', () => {
    const total = pipe.transform([]);
    expect(total).toEqual(0);
  });
});

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

P1: Solution

Click to see the solution ✏️ Update src/app/item-total.pipe.ts to:

import { Pipe, PipeTransform } from '@angular/core';
import { Item } from './order/order.service';

@Pipe({
  name: 'itemTotal',
})
export class ItemTotalPipe implements PipeTransform {
  transform(items: Item[]): number {
    let itemTotal = 0.0;
    if (items.length) {
      for (const item of items) {
        itemTotal += item.price;
      }
      return Math.round(itemTotal * 100) / 100;
    }
    return itemTotal;
  }
}

✏️ Update src/app/order/order.component.ts to use the item total Pipe:

import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ItemTotalPipe } from '../item-total.pipe';
import { Restaurant } from '../restaurant/restaurant';
import { RestaurantService } from '../restaurant/restaurant.service';
import { Order, OrderService } from './order.service';
    private route: ActivatedRoute,
    private restaurantService: RestaurantService,
    private orderService: OrderService,
    private formBuilder: FormBuilder,
    private itemTotal: ItemTotalPipe
  ) {}

  ngOnInit(): void {
    // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL
    this.orderForm?.controls.items.valueChanges
      .pipe(takeUntil(this.onDestroy$))
      .subscribe((value) => {
        this.orderTotal = this.itemTotal.transform(value);
      });
  }

        this.completedOrder = res;
        this.orderComplete = true;
        this.orderProcessing = false;
      });
  }

  startNewOrder(): void {

✏️ Update src/app/order/list/list.component.html to use the item total Pipe:

      </ul>
    </div>

    <div class="total">${{ order.items | itemTotal }}</div>

    <div class="actions">
      <span class="badge">{{ statusTitle }}</span>

Problem 2: Using Currency Pipe

Now that we have finished implementing our Item Total Pipe, this is a good time for us to talk about using Currency Pipe. You might have noticed that our prices are in USD, which is a currency, and is a perfect use for the Currency Pipe. Angular provides us with a Currency Pipe that formats a number as currency using locale rules. Locale Rules refers to a user’s country or region specific details like currency, language and more.

Add the Currency Pipe to the order.component.html , menu-items.component.html and list.component.html template files:

P2: What you need to know

How to:

  • Apply a Pipe (you previously learned this ✔️)

Like all the other Pipes we have created, the Currency Pipe can be used the exact same way.

P2: Solution

Click to see the solution ✏️ Update src/app/order/order.component.html to use the Currency Pipe:

    <ul class="list-group panel">
      <li class="list-group-item" *ngFor="let item of completedOrder.items">
        <label>
          {{ item.name }}
          <span class="badge">{{ item.price | currency }}</span>
        </label>
      </li>

      <li class="list-group-item">
        <label>
          Total <span class="badge">{{ orderTotal | currency }}</span>
        </label>
      </li>
    </ul>
        <p class="help-text">Please enter your phone number.</p>
      </div>
      <div class="submit">
        <h4>Total: {{ orderTotal | currency }}</h4>
        <div class="loading" *ngIf="orderProcessing"></div>
        <button
          type="submit"

✏️ Update src/app/order/list/list.component.html to use the currency Pipe:

      </ul>
    </div>

    <div class="total">{{ order.items | itemTotal | currency }}</div>

    <div class="actions">
      <span class="badge">{{ statusTitle }}</span>

✏️ Update src/app/order/menu-items/menu-items.component.html to use the Currency Pipe:

<li class="list-group-item" *ngFor="let item of items">
  <label>
    <input type="checkbox" (change)="updateItem(item)" />
    {{ item.name }} <span class="badge">{{ item.price | currency }}</span>
  </label>
</li>

Set Default Currency Code

If we want to display a different currency than the one provided by the application’s locale, which defaults to en-US and therefore USD, we may override the locale’s currency by providing DEFAULT_CURRENCY_CODE in app.module.ts.

✏️ Update app.module.ts to provide currency code:

import { HttpClientModule } from '@angular/common/http';
import { DEFAULT_CURRENCY_CODE, NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    BrowserAnimationsModule,
    TabsModule.forRoot()
  ],
  providers: [
    ItemTotalPipe,
    { provide: DEFAULT_CURRENCY_CODE, useValue: 'USD' },
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
Previous Lesson: Order History ComponentNext Lesson: Real Time Connections