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
- Generate a
HistoryComponent
insrc/app/order/history/history.component.ts
- 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 { Location } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import {
ComponentFixture,
fakeAsync,
flush,
TestBed,
tick,
} from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { Router } from '@angular/router';
import { of } from 'rxjs';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { ImageUrlPipe } from './image-url.pipe';
import { HistoryComponent } from './order/history/history.component';
import { OrderComponent } from './order/order.component';
import { DetailComponent } from './restaurant/detail/detail.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
import { RestaurantService } from './restaurant/restaurant.service';
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 fixture: ComponentFixture<AppComponent>;
let location: Location;
let router: Router;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppRoutingModule, HttpClientModule, ReactiveFormsModule],
declarations: [
AppComponent,
HomeComponent,
RestaurantComponent,
ImageUrlPipe,
DetailComponent,
OrderComponent,
HistoryComponent,
],
providers: [
{ provide: RestaurantService, useClass: MockRestaurantService },
],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideComponent(RestaurantComponent, {
set: { template: '<p>I am a fake restaurant component</p>' },
})
.compileComponents();
fixture = TestBed.createComponent(AppComponent);
location = TestBed.inject(Location);
router = TestBed.inject(Router);
});
it('should create the app', () => {
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'place-my-order'`, () => {
const app = fixture.componentInstance;
expect(app.title).toEqual('place-my-order');
});
it('should render title in a h1 tag', () => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain(
'place-my-order.com'
);
});
it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
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', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
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', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
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', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
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', fakeAsync(() => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
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();
const compiled = fixture.nativeElement as HTMLElement;
const homeLink = compiled.querySelector('li a');
const href = homeLink?.getAttribute('href');
expect(href).toEqual('/');
});
it('should have the restaurants navigation link href set to "/restaurants"', () => {
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const restaurantsLink = compiled.querySelector('li:nth-child(2) a');
const href = restaurantsLink?.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.nativeElement as HTMLElement;
router.navigate(['']);
fixture.detectChanges();
tick();
fixture.detectChanges();
const homeLinkLi = compiled.querySelector('li');
expect(homeLinkLi?.classList).toContain('active');
expect(compiled.querySelectorAll('.active').length).toBe(1);
flush();
}));
it('should make the restaurants navigation link class active when the router navigates to "/restaurants" path', fakeAsync(() => {
const compiled = fixture.nativeElement as HTMLElement;
router.navigate(['restaurants']);
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(location.path()).toBe('/restaurants');
const restaurantsLinkLi = compiled.querySelector('li:nth-child(2)');
expect(restaurantsLinkLi?.classList).toContain('active');
expect(compiled.querySelectorAll('.active').length).toBe(1);
flush();
}));
});
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
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
Click to see the 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 { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { HistoryComponent } from './order/history/history.component';
import { OrderComponent } from './order/order.component';
import { DetailComponent } from './restaurant/detail/detail.component';
import { RestaurantComponent } from './restaurant/restaurant.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
- Add a Order History link to the navigation bar at the top of the page.
- Add the class name
active
to the link if we are on theOrderHistory
page.
P2: What you need to know
You’ve seen this before. Check out 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
Click to see the 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 />
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
- List all orders in the
HistoryComponent
. - Make sure the
<div>
for each order has a class name of 'order' and a class name that is theorder.status
value. Make sure you’ve created a new order.
P3: Setup
1. ✏️ Copy the following into src/app/order/history/history.component.ts. You will fill out its
getOrders
method. The getters newOrders
, preparingOrders
, deliveryOrders
, and deliveredOrders
will be used later.
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Order } from '../order.service';
interface Data<T> {
value: T[];
isPending: boolean;
}
@Component({
selector: 'pmo-history',
templateUrl: './history.component.html',
styleUrl: './history.component.css',
})
export class HistoryComponent implements OnInit, OnDestroy {
constructor() {}
ngOnInit(): void {
this.getOrders();
}
ngOnDestroy(): void {
// unsubscribe here
}
getOrders(): void {
// get orders here
}
get newOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'new';
});
return orders;
}
get preparingOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'preparing';
});
return orders;
}
get deliveryOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'delivery';
});
return orders;
}
get deliveredOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'delivered';
});
return orders;
}
}
2. ✏️ Copy the following into src/app/order/history/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="false">
Mark as:
<button class="btn-link">next step</button>
</p>
<p class="action">
<button class="btn-link">Delete</button>
</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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { Order, OrderService } from '../order.service';
import { HistoryComponent } from './history.component';
class MockOrderService {
getOrders(): Observable<{ data: Order[] }> {
return of({
data: [
{
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Steak Tacos', price: 15.99 },
{ name: 'Guacamole', price: 3.99 },
],
name: 'Client 2',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'preparing',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Mac & Cheese', price: 15.99 },
{ name: 'Grilled chicken', price: 23.99 },
],
name: 'Client 3',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivery',
_id: '0awcHyo8iD7XjahX',
},
{
address: '',
items: [
{ name: 'Eggrolls', price: 5.99 },
{ name: 'Fried Rice', price: 18.99 },
],
name: 'Client 4',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivered',
_id: '1awcJyo3iD6CpvhZ',
},
],
});
}
}
describe('HistoryComponent', () => {
let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>;
beforeEach(async () => {
await 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 expectedOrders: Order[] = [
{
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Steak Tacos', price: 15.99 },
{ name: 'Guacamole', price: 3.99 },
],
name: 'Client 2',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'preparing',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Mac & Cheese', price: 15.99 },
{ name: 'Grilled chicken', price: 23.99 },
],
name: 'Client 3',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivery',
_id: '0awcHyo8iD7XjahX',
},
{
address: '',
items: [
{ name: 'Eggrolls', price: 5.99 },
{ name: 'Fried Rice', price: 18.99 },
],
name: 'Client 4',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivered',
_id: '1awcJyo3iD6CpvhZ',
},
];
const orders = fixture.componentInstance.orders;
expect(orders.value).toEqual(expectedOrders);
});
it('should display orders in UI', () => {
const compiled = fixture.nativeElement as HTMLElement;
const orderDivs = compiled.querySelectorAll(
'.order:not(.header):not(.empty)'
);
expect(orderDivs.length).toEqual(4);
});
it('should display orders with appropriate classes', () => {
const compiled = fixture.nativeElement as HTMLElement;
const newOrder = compiled.getElementsByClassName('new');
const preparingOrder = compiled.getElementsByClassName('preparing');
const deliveryOrder = compiled.getElementsByClassName('delivery');
const 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, the tests will pass when you run npm run test
!
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']"></div>
P3: Solution
Click to see the solution
✏️ Update src/app/order/history.component.tsimport { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { ResponseData } from '../../restaurant/restaurant.service';
import { Order, OrderService } from '../order.service';
interface Data<T> {
value: T[];
isPending: boolean;
}
@Component({
selector: 'pmo-history',
templateUrl: './history.component.html',
styleUrl: './history.component.css',
})
export class HistoryComponent implements OnInit, OnDestroy {
orders: Data<Order> = { value: [], isPending: true };
private onDestroy$ = new Subject<void>();
constructor(private orderService: OrderService) {}
ngOnInit(): void {
this.getOrders();
}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
}
getOrders(): void {
this.orderService
.getOrders()
.pipe(takeUntil(this.onDestroy$))
.subscribe((res: ResponseData<Order>) => {
this.orders.value = res.data;
});
}
get newOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'new';
});
return orders;
}
get preparingOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'preparing';
});
return orders;
}
get deliveryOrders(): Order[] {
const orders = this.orders.value.filter((order) => {
return order.status === 'delivery';
});
return orders;
}
get deliveredOrders(): Order[] {
const 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="false">
Mark as:
<button class="btn-link">next step</button>
</p>
<p class="action">
<button class="btn-link">Delete</button>
</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
- Group the orders by status.
- Allow the user to change the status of an order.
- 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 propertylistTitle
- 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 actiondelete(order._id)
that will delete an ordertotal(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:
<button class="btn-link" (click)="markAs(order, action)">
{{ actionTitle }}
</button>
</p>
<p class="action">
<button class="btn-link" (click)="deleteOrder(order._id)">
Delete
</button>
</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 { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { ListComponent } from '../list/list.component';
import { Order, OrderService } from '../order.service';
import { HistoryComponent } from './history.component';
class MockOrderService {
getOrders(): Observable<{ data: Order[] }> {
return of({
data: [
{
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Steak Tacos', price: 15.99 },
{ name: 'Guacamole', price: 3.99 },
],
name: 'Client 2',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'preparing',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Mac & Cheese', price: 15.99 },
{ name: 'Grilled chicken', price: 23.99 },
],
name: 'Client 3',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivery',
_id: '0awcHyo8iD7XjahX',
},
{
address: '',
items: [
{ name: 'Eggrolls', price: 5.99 },
{ name: 'Fried Rice', price: 18.99 },
],
name: 'Client 4',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivered',
_id: '1awcJyo3iD6CpvhZ',
},
],
});
}
}
describe('HistoryComponent', () => {
let component: HistoryComponent;
let fixture: ComponentFixture<HistoryComponent>;
beforeEach(async () => {
await 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 expectedOrders: Order[] = [
{
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Steak Tacos', price: 15.99 },
{ name: 'Guacamole', price: 3.99 },
],
name: 'Client 2',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'preparing',
_id: '0awcHyo3iD6CpvhX',
},
{
address: '',
items: [
{ name: 'Mac & Cheese', price: 15.99 },
{ name: 'Grilled chicken', price: 23.99 },
],
name: 'Client 3',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivery',
_id: '0awcHyo8iD7XjahX',
},
{
address: '',
items: [
{ name: 'Eggrolls', price: 5.99 },
{ name: 'Fried Rice', price: 18.99 },
],
name: 'Client 4',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'delivered',
_id: '1awcJyo3iD6CpvhZ',
},
];
const orders = fixture.componentInstance.orders;
expect(orders.value).toEqual(expectedOrders);
});
it('should display orders in UI', () => {
const compiled = fixture.nativeElement as HTMLElement;
const orderDivs = compiled.querySelectorAll(
'.order:not(.header):not(.empty)'
);
expect(orderDivs.length).toEqual(4);
});
it('should display orders with appropriate classes', () => {
const compiled = fixture.nativeElement as HTMLElement;
const newOrder = compiled.getElementsByClassName('new');
const preparingOrder = compiled.getElementsByClassName('preparing');
const deliveryOrder = compiled.getElementsByClassName('delivery');
const 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 { ComponentFixture, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';
import { Order, OrderService } from '../order.service';
import { ListComponent } from './list.component';
class MockOrderService {
updateOrder(order: Order, status: string) {
return of({});
}
deleteOrder(orderId: string) {
return of({});
}
}
describe('ListComponent', () => {
let component: ListComponent;
let fixture: ComponentFixture<ListComponent>;
let injectedOrderService: OrderService;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ListComponent],
providers: [
{
provide: OrderService,
useClass: MockOrderService,
},
],
}).compileComponents();
injectedOrderService = TestBed.inject(OrderService);
});
beforeEach(() => {
fixture = TestBed.createComponent(ListComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display the order total', () => {
fixture.componentInstance.orders = [
{
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
},
];
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.total')?.textContent).toEqual('$39.98');
});
it('should call orderService updateOrder with order and action when "mark as" is clicked', () => {
const updateOrderSpy = spyOn(
injectedOrderService,
'updateOrder'
).and.callThrough();
const order1 = {
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
};
fixture.componentInstance.orders = [order1];
fixture.componentInstance.action = 'preparing';
fixture.componentInstance.actionTitle = 'Preparing';
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const markAsLink = compiled.querySelector(
'.actions .action button'
) as HTMLButtonElement;
markAsLink?.click();
expect(updateOrderSpy).toHaveBeenCalledWith(order1, 'preparing');
});
it('should call orderService deleteOrder with id when delete item is clicked', () => {
const deleteOrderSpy = spyOn(
injectedOrderService,
'deleteOrder'
).and.callThrough();
const order1 = {
address: '',
items: [
{ name: 'Onion fries', price: 15.99 },
{ name: 'Roasted Salmon', price: 23.99 },
],
name: 'Client 1',
phone: '',
restaurant: 'uPkA2jiZi24tCvXh',
status: 'new',
_id: '0awcHyo3iD6CpvhX',
};
fixture.componentInstance.orders = [order1];
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const deleteLink = compiled.querySelector(
'.actions .action button'
) as HTMLButtonElement;
deleteLink.click();
expect(deleteOrderSpy).toHaveBeenCalledWith('0awcHyo3iD6CpvhX');
});
});
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
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
Click to see the solution
✏️ Update src/app/order/list.component.tsimport { Component, Input, OnDestroy } from '@angular/core';
import { Subject, takeUntil } from 'rxjs';
import { Item, Order, OrderService } from '../order.service';
@Component({
selector: 'pmo-list',
templateUrl: './list.component.html',
styleUrl: './list.component.css',
})
export class ListComponent implements OnDestroy {
@Input() orders?: Order[];
@Input() listTitle?: string;
@Input() status?: string;
@Input() statusTitle?: string;
@Input() action?: string;
@Input() actionTitle?: string;
@Input() emptyMessage?: string;
private onDestroy$ = new Subject<void>();
constructor(private orderService: OrderService) {}
ngOnDestroy(): void {
this.onDestroy$.next();
this.onDestroy$.complete();
}
markAs(order: Order, action: string): void {
this.orderService
.updateOrder(order, action)
.pipe(takeUntil(this.onDestroy$))
.subscribe();
}
deleteOrder(id: string): void {
this.orderService
.deleteOrder(id)
.pipe(takeUntil(this.onDestroy$))
.subscribe();
}
total(items: Item[]): number {
let total = 0.0;
for (const item of items) {
total += item.price;
}
return Math.round(total * 100) / 100;
}
}