Order History Component page

Writing the Order History Component

Overview

In this part, we will:

  • Create a new order history component
  • Get all orders from our order service
  • Create a child component to handle different states of orders
  • Create ways to update and delete orders in the view
  • Add order history link to our main navigation

Problem 1: Generate a HistoryComponent and create a route for it

We want to create a component that will show the app's order history.

P1: Technical Requirements

  1. Generate a HistoryComponent in src/app/order/history/history.component.ts
  2. Show HistoryComponent when we navigate to /order-history

P1: How to Verify Your Solution is Correct

If you've implemented the solution correctly you should be able to navigate to http://localhost:4200/order-history and see 'history works!'.

✏️ Update the spec file src/app/app.component.spec.ts to be:

import { TestBed, async, fakeAsync, tick, flush } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs'; 

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ImageUrlPipe } from './image-url.pipe';
import { HttpClientModule } from '@angular/common/http';
import { RestaurantService } from './restaurant/restaurant.service';
import { ReactiveFormsModule } from '@angular/forms';
import { DetailComponent } from './restaurant/detail/detail.component';
import { OrderComponent } from './order/order.component';
import { HistoryComponent } from './order/history/history.component';

class MockRestaurantService {
  getRestaurants() {
    return of({
      data: [{
        "name": "Poutine Palace",
        "slug": "poutine-palace",
        "images": {
          "thumbnail": "node_modules/place-my-order-assets/images/4-thumbnail.jpg",
          "owner": "node_modules/place-my-order-assets/images/3-owner.jpg",
          "banner": "node_modules/place-my-order-assets/images/2-banner.jpg"
        },
        "menu": {
          "lunch": [
            {
              "name": "Crab Pancakes with Sorrel Syrup",
              "price": 35.99
            },
            {
              "name": "Steamed Mussels",
              "price": 21.99
            },
            {
              "name": "Spinach Fennel Watercress Ravioli",
              "price": 35.99
            }
          ],
          "dinner": [
            {
              "name": "Gunthorp Chicken",
              "price": 21.99
            },
            {
              "name": "Herring in Lavender Dill Reduction",
              "price": 45.99
            },
            {
              "name": "Chicken with Tomato Carrot Chutney Sauce",
              "price": 45.99
            }
          ]
        },
        "address": {
          "street": "230 W Kinzie Street",
          "city": "Green Bay",
          "state": "WI",
          "zip": "53205"
        },
        "_id": "3ZOZyTY1LH26LnVw"
      },
      {
        "name": "Cheese Curd City",
        "slug": "cheese-curd-city",
        "images": {
          "thumbnail": "node_modules/place-my-order-assets/images/2-thumbnail.jpg",
          "owner": "node_modules/place-my-order-assets/images/3-owner.jpg",
          "banner": "node_modules/place-my-order-assets/images/2-banner.jpg"
        },
        "menu": {
          "lunch": [
            {
              "name": "Ricotta Gnocchi",
              "price": 15.99
            },
            {
              "name": "Gunthorp Chicken",
              "price": 21.99
            },
            {
              "name": "Garlic Fries",
              "price": 15.99
            }
          ],
          "dinner": [
            {
              "name": "Herring in Lavender Dill Reduction",
              "price": 45.99
            },
            {
              "name": "Truffle Noodles",
              "price": 14.99
            },
            {
              "name": "Charred Octopus",
              "price": 25.99
            }
          ]
        },
        "address": {
          "street": "2451 W Washburne Ave",
          "city": "Green Bay",
          "state": "WI",
          "zip": "53295"
        },
        "_id": "Ar0qBJHxM3ecOhcr"
      }]}
            )
  }

  getStates() {
    return of({
      data: [
        {"short":"MO","name":"Missouri"},
        {"short":"CA  ","name":"California"},
        {"short":"MI","name":"Michigan"}]
    });
  }

  getCities(state:string) {
    return of({
      data: [{"name":"Sacramento","state":"CA"},{"name":"Oakland","state":"CA"}]
    });
  }

  getRestaurant(slug:string) {
    return of({
      "name": "Poutine Palace",
      "slug": "poutine-palace",
      "images": {
        "thumbnail": "node_modules/place-my-order-assets/images/4-thumbnail.jpg",
        "owner": "node_modules/place-my-order-assets/images/3-owner.jpg",
        "banner": "node_modules/place-my-order-assets/images/2-banner.jpg"
      },
      "menu": {
        "lunch": [
          {
            "name": "Crab Pancakes with Sorrel Syrup",
            "price": 35.99
          },
          {
            "name": "Steamed Mussels",
            "price": 21.99
          },
          {
            "name": "Spinach Fennel Watercress Ravioli",
            "price": 35.99
          }
        ],
        "dinner": [
          {
            "name": "Gunthorp Chicken",
            "price": 21.99
          },
          {
            "name": "Herring in Lavender Dill Reduction",
            "price": 45.99
          },
          {
            "name": "Chicken with Tomato Carrot Chutney Sauce",
            "price": 45.99
          }
        ]
      },
      "address": {
        "street": "230 W Kinzie Street",
        "city": "Green Bay",
        "state": "WI",
        "zip": "53205"
      },
      "_id": "3ZOZyTY1LH26LnVw"
    })
  }
}

describe('AppComponent', () => {
  let router: Router;
  let location: Location;
  let fixture;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        AppRoutingModule, HttpClientModule, ReactiveFormsModule
      ],
      declarations: [
        AppComponent, HomeComponent, RestaurantComponent, 
        ImageUrlPipe, DetailComponent, OrderComponent, HistoryComponent
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).overrideComponent(RestaurantComponent, {
      set: {
        providers: [
          { provide: RestaurantService, useClass: MockRestaurantService }
        ],
        template: '<p> I am a fake restaurant component</p>'
      }
    })
    .compileComponents();

    router = TestBed.get(Router);
    location = TestBed.get(Location);

    fixture = TestBed.createComponent(AppComponent);
  }));

  it('should create the app', () => {
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'place-my-order'`, () => {
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('place-my-order');
  });

  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('place-my-order.com');
  });

  it('should render the HomeComponent with router navigates to "/" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['']).then(() => {
      expect(location.path()).toBe('');
      expect(compiled.querySelector('pmo-home')).not.toBe(null);
    });
  });

  it('should render the RestaurantsComponent with router navigates to "/restaurants" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants']).then(() => {
      expect(location.path()).toBe('/restaurants');
      expect(compiled.querySelector('pmo-restaurant')).not.toBe(null);
    });
  });

  it('should render the DetailComponent with router navigates to "/restaurants/slug" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants/crab-shack']).then(() => {
      expect(location.path()).toBe('/restaurants/crab-shack');
      expect(compiled.querySelector('pmo-detail')).not.toBe(null);
    });
  });

  it('should render the OrderComponent with router navigates to "/restaurants/slug/order" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants/crab-shack/order']).then(() => {
      expect(location.path()).toBe('/restaurants/crab-shack/order');
      expect(compiled.querySelector('pmo-order')).not.toBe(null);
    });
  });

  it('should render the HistoryComponent with router navigates to "/order-history" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['order-history']).then(() => {
      expect(location.path()).toBe('/order-history');
      expect(compiled.querySelector('pmo-history')).not.toBe(null);
    });
  });

  it('should have the home navigation link href set to ""', () => {
    fixture.detectChanges();
    let homeLink = fixture.debugElement.query(By.css('li a'));
    let href = homeLink.nativeElement.getAttribute('href');
    expect(href).toEqual('/');
  });

  it('should have the restaurants navigation link href set to ""', () => {
    fixture.detectChanges();
    let restaurantsLink = fixture.debugElement.query(By.css('li:nth-child(2) a'));
    let href = restaurantsLink.nativeElement.getAttribute('href');
    expect(href).toEqual('/restaurants');
  });

  it('should make the home navigation link class active when the router navigates to "/" path', fakeAsync(() => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['']).then(() => {
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      let homeLinkLi = fixture.debugElement.query(By.css('li'));
      expect(homeLinkLi.nativeElement.classList).toContain('active');
      expect(compiled.querySelectorAll('.active').length).toBe(1);
      fixture.destroy();
      flush();
    });
  }));

  it('should make the restaurants navigation link class active when the router navigates to "/restaurants" path', fakeAsync(() => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants']).then(() => {
      expect(location.path()).toBe('/restaurants');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      let restaurantsLinkLi = fixture.debugElement.query(By.css('li:nth-child(2)'));
      expect(restaurantsLinkLi.nativeElement.classList).toContain('active');
      expect(compiled.querySelectorAll('.active').length).toBe(1);
      fixture.destroy();
      flush();
    });
  }));

});

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

P1: What you need to know

You got this already, but just in case, here's some hints:

  • How to generate a component
    ng g component PATH
    
  • Update app-routing.module.ts to import the component you want and create a path to it.

P1: solution

✏️ First, run:

ng g component order/history

Then route to the component:

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

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
import { DetailComponent } from './restaurant/detail/detail.component';
import { OrderComponent } from './order/order.component';
import { HistoryComponent } from './order/history/history.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent,
  },
  {
    path: 'restaurants',
    component: RestaurantComponent,
  },
  {
    path: 'restaurants/:slug',
    component: DetailComponent,
  },
  {
    path: 'restaurants/:slug/:order',
    component: OrderComponent,
  },
  {
    path: 'order-history',
    component: HistoryComponent
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Problem 2: Add HistoryComponent to navigation

We want a user to be able to navigate to the HistoryComponent via a link in the main navigation.

P2: Technical Requirements

  1. Add a Order History link to the navigation bar at the top of the page.
  2. Add the class name active to the link if we are on the OrderHistory page.

P2: What You Need to Know

You've seen this before. Checkout how the Home link works in app.component.html.

P2: How to Verify Your Solution is Correct

If you've implemented the solution correctly you should now be able to navigate to http://localhost:4200/order-history and see a list of all orders.

P2: Solution

✏️ Update src/app/app.component.html

<header>
  <nav>
    <h1>place-my-order.com</h1>
    <ul>
      <li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
        <a routerLink="/" >Home</a>
      </li>
      <li [ngClass]="{'active' : rla.isActive} ">
        <a routerLink="/restaurants" routerLinkActive #rla="routerLinkActive">Restaurants</a>
      </li>
      <li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
        <a routerLink="/order-history">Order History</a>
      </li>
    </ul>
  </nav>
</header>
<router-outlet></router-outlet>

Problem 3: List All Orders

We want to be able to see a list of all created orders and their varying statuses of "new", "preparing", "delivery", and "delivered".

P3: Technical Requirements

  1. List all orders in the HistoryComponent.
  2. Make sure the <div> for each order has a class name of 'order' and a class name that is the order.status value. Make sure you've created a new order.

P3: Setup

1. ✏️ Copy the following into src/app/order/history.component.ts. You will fill out its getOrders method. The getters newOrders, preparingOrders, deliveryOrders, and deliveredOrders will be used later.

import { Component, OnInit } from '@angular/core';

interface Data<T> {
  value: Array<T>;
  isPending: boolean;
}

@Component({
  selector: 'pmo-history',
  templateUrl: './history.component.html',
  styleUrls: ['./history.component.less']
})
export class HistoryComponent implements OnInit {

  constructor() {}

  ngOnInit() {
    this.getOrders();
  }

  getOrders() {
   //get orders here
  }

  get newOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "new";
    });
    return orders;
  }

   get preparingOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "preparing";
    });
    return orders;
   }

   get deliveryOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "delivery";
    });
    return orders;
   }

   get deliveredOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "delivered";
    });
    return orders;
   }

}

2. ✏️ Copy the following into src/app/order/history.component.html. You will need to iterate through orders and add the right class names to the outer <div> for each order.

<div class="order-history">
  <div class="order header">
    <address>Name / Address / Phone</address>
    <div class="items">Order</div>
    <div class="total">Total</div>
    <div class="actions">Action</div>
  </div>

  <ng-container LOOP_HERE>
    <div class="ADD RIGHT CLASS NAMES">
      <address>
        {{order.name}} <br />{{order.address}} <br />{{order.phone}}
      </address>

      <div class="items">
        <ul>
          <li *ngFor="let item of order.items">{{item.name}}</li>
        </ul>
      </div>

      <div class="total">$order total?</div>

      <div class="actions">
        <span class="badge">Status title?</span>
          <p class="action" *ngIf="action">
            Mark as:
            <a href="javascript://">
              next step
            </a>
          </p>

        <p class="action">
          <a href="javascript://" >Delete</a>
        </p>
      </div>
    </div>
  </ng-container>
</div>

P3: How to Verify Your Solution is Correct

✏️ Update the menu-items spec file src/app/order/history/history.component.spec.ts to be:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs'; 

import { HistoryComponent } from './history.component';
import { OrderService } from '../order.service';

class MockOrderService {
  getOrders() {
    return of({
      data: [{
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Steak Tacos", "price": 15.99}, {"name": "Guacamole", "price": 3.99}],
      "name": "Client 2",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "preparing",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Mac & Cheese", "price": 15.99}, {"name": "Grilled chicken", "price": 23.99}],
      "name": "Client 3",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivery",
      "_id": "0awcHyo8iD7XjahX",
      },
      {
      "address": null,
      "items": [{"name": "Eggrolls", "price": 5.99}, {"name": "Fried Rice", "price": 18.99}],
      "name": "Client 4",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivered",
      "_id": "1awcJyo3iD6CpvhZ",
      }
    ]
    })
  }
}
describe('HistoryComponent', () => {
  let component: HistoryComponent;
  let fixture: ComponentFixture<HistoryComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HistoryComponent ],
      providers: [{
        provide: OrderService,
        useClass: MockOrderService
      }]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HistoryComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should set response from getOrders service to orders member', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let expectedOrders = [{
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Steak Tacos", "price": 15.99}, {"name": "Guacamole", "price": 3.99}],
      "name": "Client 2",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "preparing",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Mac & Cheese", "price": 15.99}, {"name": "Grilled chicken", "price": 23.99}],
      "name": "Client 3",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivery",
      "_id": "0awcHyo8iD7XjahX",
      },
      {
      "address": null,
      "items": [{"name": "Eggrolls", "price": 5.99}, {"name": "Fried Rice", "price": 18.99}],
      "name": "Client 4",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivered",
      "_id": "1awcJyo3iD6CpvhZ",
      }
    ];
    let orders = fixture.componentInstance.orders;
    expect(orders.value).toEqual(expectedOrders);
  });

  it('should display orders in UI', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let orderDivs = compiled.querySelectorAll('.order:not(.header):not(.empty)');
    expect(orderDivs.length).toEqual(4);
  })

  it('should display orders with appropriate classes', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let newOrder = compiled.getElementsByClassName('new');
    let preparingOrder = compiled.getElementsByClassName('preparing');
    let deliveryOrder = compiled.getElementsByClassName('delivery');
    let deliveredOrder = compiled.getElementsByClassName('delivered');
    expect(newOrder.length).toEqual(1);
    expect(preparingOrder.length).toEqual(1);
    expect(deliveryOrder.length).toEqual(1);
    expect(deliveredOrder.length).toEqual(1);
  });
});

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

P3: What You Need to Know

  • How to import a service and get data out of it. Hint: Import it and create a property in the constructor.
  • How to loop through values in HTML. Hint: *ngFor.

For this step, you'll need to know how to add multiple class names. You can do this with [ngClass] and setting it to an array like:

<div [ngClass]="['first','second']">

P3: Solution

✏️ Update src/app/order/history.component.ts

import { Component, OnInit } from '@angular/core';
import { OrderService, Order } from '../order.service';
import { ResponseData } from '../../restaurant/restaurant.service';

interface Data<T> {
  value: Array<T>;
  isPending: boolean;
}

@Component({
  selector: 'pmo-history',
  templateUrl: './history.component.html',
  styleUrls: ['./history.component.less']
})
export class HistoryComponent implements OnInit {
  public orders: Data<Order> = {
    value: [],
    isPending: true
  }

  constructor(
    private orderService: OrderService
    ) {
    }

  ngOnInit() {
    this.getOrders();

  }

  getOrders() {
    this.orderService.getOrders().subscribe((res: ResponseData<Order>) => {
      this.orders.value = res.data;
    });
  }

  get newOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "new";
    });
    return orders;
  }

   get preparingOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "preparing";
    });
    return orders;
   }

   get deliveryOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "delivery";
    });
    return orders;
   }

   get deliveredOrders() {
    let orders =  this.orders.value.filter((order) => {
      return order.status === "delivered";
    });
    return orders;
   }

}

✏️ Update src/app/order/history.component.html

<div class="order-history">
  <div class="order header">
    <address>Name / Address / Phone</address>
    <div class="items">Order</div>
    <div class="total">Total</div>
    <div class="actions">Action</div>
  </div>

  <ng-container *ngFor="let order of orders.value">
    <div [ngClass]="['order', order.status]">
      <address>
        {{order.name}} <br />{{order.address}} <br />{{order.phone}}
      </address>

      <div class="items">
        <ul>
          <li *ngFor="let item of order.items">{{item.name}}</li>
        </ul>
      </div>

      <div class="total">$order total?</div>

      <div class="actions">
        <span class="badge">Status title?</span>
          <p class="action" *ngIf="action">
            Mark as:
            <a href="javascript://">
              next step
            </a>
          </p>

        <p class="action">
          <a href="javascript://" >Delete</a>
        </p>
      </div>
    </div>
  </ng-container>
</div>

Problem 4: Creating a Child Component to Handle Order States

We want to create a child component that will take a list of orders by status and display them, as well as actions a user can perform on an order.

P4: Technical Requirements

  1. Group the orders by status.
  2. Allow the user to change the status of an order.
  3. Allow the user to delete an order.

NOTE!! To see that an order has changed, you will have to refresh the page!

To solve this problem:

  • Create a ListComponent in src/app/order/list/list.component.ts that will take a list of orders and other values like:
    <pmo-list
      [orders]="newOrders"
      listTitle="New Orders"
      status="new"
      statusTitle="New Order!"
      action="preparing"
      actionTitle="Preparing"
      emptyMessage="No new orders">
      </pmo-list>
    
  • ListComponent will take the following values:
    • orders - an array of orders based on status property
    • listTitle - A title for the list: "NewOrders" , "Preparing" , "Out for Delivery" , "Delivery"
    • status - Which status the list is "new", "preparing", "delivery", "delivered"
    • statusTitle - Another title for the status: "New Order!", "Preparing", "Out for delivery", "Delivered"
    • action - What status items can be moved to: "preparing", "delivery", "delivered"
    • actionTitle - A title for the action: "Preparing", "Out for delivery", "Delivered"
    • emptyMessage - What to show when there are no orders in the list: "No new orders", "No orders preparing", "No orders are being delivered", "No delivered orders"
  • ListComponent will have the following methods:
    • markAs(order, action) that will update an order based on the action
    • delete(order._id) that will delete an order
    • total(items) that will return the order total

P4: Setup

1. ✏️ Create the ListComponent:

ng g component order/list

2. ✏️ Update src/app/order/history.component.html to use <pmo-list>:

<div class="order-history">
  <div class="order header">
    <address>Name / Address / Phone</address>
    <div class="items">Order</div>
    <div class="total">Total</div>
    <div class="actions">Action</div>
  </div>

  <pmo-list
    [orders]="newOrders"
    listTitle="New Orders"
    status="new"
    statusTitle="New Order!"
    action="preparing"
    actionTitle="Preparing"
    emptyMessage="No new orders">
    </pmo-list>

  <pmo-list
    [orders]="preparingOrders"
    listTitle="Preparing"
    status="preparing"
    statusTitle="Preparing"
    action="delivery"
    actionTitle="Out for delivery"
    emptyMessage="No orders preparing">
    </pmo-list>

  <pmo-list
    [orders]="deliveryOrders"
    listTitle="Out for delivery"
    status="delivery"
    statusTitle="Out for delivery"
    action="delivered"
    actionTitle="Delivered"
    emptyMessage="No orders are being delivered">
    </pmo-list>

  <pmo-list
    [orders]="deliveredOrders"
    listTitle="Delivery"
    status="delivered"
    statusTitle="Delivered"
    emptyMessage="No delivered orders">
    </pmo-list>
</div>

3. ✏️ Update src/app/order/list/list.component.html to its final html:

<h4>{{listTitle}}</h4>
<ng-container *ngFor="let order of orders">
  <div [ngClass]="['order', order.status? order.status: '']">
    <address>
      {{order.name}} <br />{{order.address}} <br />{{order.phone}}
    </address>
  
    <div class="items">
      <ul>
        <li *ngFor="let item of order.items">{{item.name}}</li>
      </ul>
    </div>
  
    <div class="total">${{total(order.items)}}</div>
  
    <div class="actions">
      <span class="badge">{{statusTitle}}</span>
        <p class="action" *ngIf="action">
          Mark as:
          <a href="javascript://" (click)="markAs(order, action)">
            {{actionTitle}}
          </a>
        </p>
  
      <p class="action">
        <a href="javascript://"  (click)="delete(order._id)">Delete</a>
      </p>
    </div>
  </div>
</ng-container>

<div class="order empty">{{emptyMessage}}</div>

P4: How to Verify Your Solution is Correct

✏️ Update src/app/order/history/history.component.spec.ts to be:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs'; 
import { NO_ERRORS_SCHEMA } from '@angular/core';

import { HistoryComponent } from './history.component';
import { OrderService } from '../order.service';
import { ListComponent } from '../list/list.component';

class MockOrderService {
  getOrders() {
    return of({
      data: [{
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Steak Tacos", "price": 15.99}, {"name": "Guacamole", "price": 3.99}],
      "name": "Client 2",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "preparing",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Mac & Cheese", "price": 15.99}, {"name": "Grilled chicken", "price": 23.99}],
      "name": "Client 3",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivery",
      "_id": "0awcHyo8iD7XjahX",
      },
      {
      "address": null,
      "items": [{"name": "Eggrolls", "price": 5.99}, {"name": "Fried Rice", "price": 18.99}],
      "name": "Client 4",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivered",
      "_id": "1awcJyo3iD6CpvhZ",
      }
    ]
    })
  }
}
describe('HistoryComponent', () => {
  let component: HistoryComponent;
  let fixture: ComponentFixture<HistoryComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HistoryComponent, ListComponent ],
      providers: [{
        provide: OrderService,
        useClass: MockOrderService
      }],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HistoryComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should set response from getOrders service to orders member', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let expectedOrders = [{
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Steak Tacos", "price": 15.99}, {"name": "Guacamole", "price": 3.99}],
      "name": "Client 2",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "preparing",
      "_id": "0awcHyo3iD6CpvhX",
      },
      {
      "address": null,
      "items": [{"name": "Mac & Cheese", "price": 15.99}, {"name": "Grilled chicken", "price": 23.99}],
      "name": "Client 3",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivery",
      "_id": "0awcHyo8iD7XjahX",
      },
      {
      "address": null,
      "items": [{"name": "Eggrolls", "price": 5.99}, {"name": "Fried Rice", "price": 18.99}],
      "name": "Client 4",
      "phone": null,
      "restaurant": "uPkA2jiZi24tCvXh",
      "status": "delivered",
      "_id": "1awcJyo3iD6CpvhZ",
      }
    ];
    let orders = fixture.componentInstance.orders;
    expect(orders.value).toEqual(expectedOrders);
  });

  it('should display orders in UI', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let orderDivs = compiled.querySelectorAll('.order:not(.header):not(.empty)');
    expect(orderDivs.length).toEqual(4);
  })

  it('should display orders with appropriate classes', () => {
    const fixture = TestBed.createComponent(HistoryComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let newOrder = compiled.getElementsByClassName('new');
    let preparingOrder = compiled.getElementsByClassName('preparing');
    let deliveryOrder = compiled.getElementsByClassName('delivery');
    let deliveredOrder = compiled.getElementsByClassName('delivered');
    expect(newOrder.length).toEqual(1);
    expect(preparingOrder.length).toEqual(1);
    expect(deliveryOrder.length).toEqual(1);
    expect(deliveredOrder.length).toEqual(1);
  });
});

✏️ Update src/app/order/list/list.component.spec.ts to be:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs'; 

import { ListComponent } from './list.component';
import { OrderService } from '../order.service';

class MockOrderService {
  updateOrder(order, status) {
    return of({})
  }

  deleteOrder(orderId) {
    return of({})
  }
}

describe('ListComponent', () => {
  let component: ListComponent;
  let fixture: ComponentFixture<ListComponent>;
  let injectedOrderService;

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

  beforeEach(() => {
    fixture = TestBed.createComponent(ListComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should display the order total', () => {
    const fixture = TestBed.createComponent(ListComponent);
    fixture.componentInstance.orders = [
      {
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      }
    ]
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;

    expect(compiled.querySelector('.total').textContent).toEqual('$39.98')
  });

  it('should call orderService updateOrder with order and action when "mark as" is clicked', () => {
    const fixture = TestBed.createComponent(ListComponent);
    let updateOrderSpy = spyOn(injectedOrderService, 'updateOrder').and.callThrough();
    let order1 =  {
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      }
    fixture.componentInstance.orders = [
      order1
    ];
    fixture.componentInstance.action = 'preparing';
    fixture.componentInstance.actionTitle = 'Preparing';
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let markAsLink = compiled.querySelector('.actions .action a');
    markAsLink.click();
    expect(updateOrderSpy).toHaveBeenCalledWith(order1, 'preparing');
  });

  it('should call orderService deleteOrder with id when delete item is clicked', () => {
    const fixture = TestBed.createComponent(ListComponent);
    let deleteOrderSpy = spyOn(injectedOrderService, 'deleteOrder').and.callThrough();
    let order1 =  {
      "address": null,
      "items": [{"name": "Onion fries", "price": 15.99}, {"name": "Roasted Salmon", "price": 23.99}],
      "name": "Client 1",
      "phone": null,
      "status": "new",
      "_id": "0awcHyo3iD6CpvhX",
      }
    fixture.componentInstance.orders = [
      order1
    ]
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let deleteLink = compiled.querySelector('.actions .action a');
    deleteLink.click();
    expect(deleteOrderSpy).toHaveBeenCalledWith('0awcHyo3iD6CpvhX');
  });
});

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

P4: What You Need to Know

  • How to add @Input()s to a component so it can be passed values.
  • How to call methods on a service that you get from the constructor.

P4: Solution

✏️ Update src/app/order/list.component.ts

import { Component, OnInit, Input } from '@angular/core';
import { Order, Item, OrderService } from '../order.service';


@Component({
  selector: 'pmo-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.less']
})
export class ListComponent implements OnInit {
  @Input() orders: Order[];
  @Input() listTitle: string;
  @Input() status: string;
  @Input() statusTitle: string;
  @Input() action: string;
  @Input() actionTitle: string;
  @Input() emptyMessage: string;

  constructor(private orderService: OrderService) { }

  ngOnInit() {}

  markAs(order: Order, action: string) {
    this.orderService.updateOrder(order, action).subscribe(() => {
    });
  }

  delete(id:string) {
    this.orderService.deleteOrder(id).subscribe(() => {
    })
  }

  total(items: Item[]) {
    let total = 0.0;
      items.forEach((item: Item) => {
        total += item.price;
      });
    return Math.round(total * 100) / 100;
  }
}