Order Service page
Writing the Order Service
Overview
In this part, we will:
- Create a new order service
 - Write interfaces to describe orders and items in orders
 - Add data methods to our service
 - Import our new order service to our component & create an order
 - Show completed order in UI
 
Problem 1: Create Order Service and Export Items and Order interfaces
We need to create a new service to handle creating and updating orders. We’ll need three interfaces - one to describe the order form data, one to describe the order, and one to describe items in the order.
P1: Technical requirements
Create a new service order in the order directory, and write and export CreateOrderDto, Order and Item interfaces representing these objects in the new service:
const createOrderDto = {
  restaurant: '12345',
  name: 'Jennifer',
  address: '123 Main st',
  phone: '867-5309',
  items: [
    {
      name: 'tacos',
      price: 6.99,
    },
  ],
};
const order = {
  _id: 'a123123bdd',
  restaurant: '12345',
  name: 'Jennifer',
  address: '123 Main st',
  phone: '867-5309',
  status: 'new',
  items: [
    {
      name: 'tacos',
      price: 6.99,
    },
  ],
};
P1: What you Need to Know
How to create services (you learned this in previous sections! ✔️)
How to write interfaces (you learned this in previous sections! ✔️)
ng g service order/order
P1: Solution
Click to see the solution
✏️ Update src/app/order/order.service.ts
import { Injectable } from '@angular/core';
export interface Item {
  name: string;
  price: number;
}
export interface CreateOrderDto {
  restaurant: string;
  name: string;
  address: string;
  phone: string;
  items: Item[];
}
export interface Order {
  _id: string;
  restaurant: string;
  name: string;
  address: string;
  phone: string;
  status: string;
  items: Item[];
}
@Injectable({
  providedIn: 'root'
})
export class OrderService {
  constructor() { }
}
Problem 2: Finish the Order Service
With our order service we’ll want to be able to create new orders, updating existing orders, delete orders, and view all orders.
P2: Setup
✏️ Paste the following into src/app/order/order.service.spec.ts:
import { HttpRequest } from '@angular/common/http';
import {
  HttpClientTestingModule,
  HttpTestingController,
} from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { OrderService } from './order.service';
describe('OrderService', () => {
  let httpTestingController: HttpTestingController;
  let orderService: OrderService;
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [OrderService],
    });
    httpTestingController = TestBed.inject(HttpTestingController);
    orderService = TestBed.inject(OrderService);
  });
  it('should make a GET request to get orders', () => {
    const mockOrders = {
      data: [
        {
          _id: 'adsfsdf',
          name: 'Jennifer',
          restaurant: 'FoodLand',
          address: '123 main',
          phone: '555-555-5555',
          status: 'new',
          items: [
            {
              name: 'nummy fries',
              price: 2.56,
            },
          ],
        },
      ],
    };
    orderService.getOrders().subscribe((orders) => {
      expect(orders).toEqual(mockOrders);
    });
    const url = 'http://localhost:7070/orders';
    const req = httpTestingController.expectOne(url);
    expect(req.request.method).toEqual('GET');
    req.flush(mockOrders);
    httpTestingController.verify();
  });
  it('should make a post request to create an order', () => {
    const mockOrder = {
      _id: 'adsfsdf',
      restaurant: '12345',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'new',
      items: [
        {
          name: 'nummy fries',
          price: 2.56,
        },
      ],
    };
    const orderForm = {
      restaurant: '12345',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      items: [
        {
          name: 'nummy fries',
          price: 2.56,
        },
      ],
    };
    orderService.createOrder(orderForm).subscribe((order) => {
      expect(order).toEqual(mockOrder);
    });
    const url = 'http://localhost:7070/orders';
    httpTestingController
      .expectOne(
        (request: HttpRequest<any>) =>
          request.method == 'POST' &&
          request.url == url &&
          JSON.stringify(request.body) ==
            JSON.stringify({ ...orderForm, status: 'new' })
      )
      .flush(mockOrder);
    httpTestingController.verify();
  });
  it('should make a put request to update an order', () => {
    const mockOrder = {
      _id: 'adsfsdf',
      restaurant: '12345',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'old',
      items: [
        {
          name: 'nummy fries',
          price: 2.56,
        },
      ],
    };
    const updatedOrder = {
      _id: 'adsfsdf',
      restaurant: '12345',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'delivered',
      items: [
        {
          name: 'nummy fries',
          price: 2.56,
        },
      ],
    };
    orderService.updateOrder(mockOrder, 'delivered').subscribe((order) => {
      expect(order).toEqual(updatedOrder);
    });
    const url = 'http://localhost:7070/orders/adsfsdf';
    httpTestingController
      .expectOne(
        (request: HttpRequest<any>) =>
          request.method == 'PUT' &&
          request.url == url &&
          JSON.stringify(request.body) == JSON.stringify(updatedOrder)
      )
      .flush(updatedOrder);
    httpTestingController.verify();
  });
  it('should make a delete request to delete an order', () => {
    const mockOrder = {
      _id: 'adsfsdf',
      restaurant: '12345',
      name: 'Jennifer',
      address: '123 main',
      phone: '555-555-5555',
      status: 'old',
      items: [
        {
          name: 'nummy fries',
          price: 2.56,
        },
      ],
    };
    orderService.deleteOrder('adsfsdf').subscribe((order) => {
      expect(order).toEqual(mockOrder);
    });
    const url = 'http://localhost:7070/orders/adsfsdf';
    const req = httpTestingController.expectOne(url);
    expect(req.request.method).toEqual('DELETE');
    req.flush(mockOrder);
    httpTestingController.verify();
  });
});
Now, try to understand what this test is doing and
implement the remainder of order.service.ts to
get the tests to pass.
✏️ Run your tests with:
ng test
P2: What you need to know
- The method signatures for the methods you’ll be adding to 
OrderService:getOrders(): Observable<{data: Order[]}>should make aGETrequestcreateOrder(orderForm: CreateOrderDto): Observable<Order>should make aPOSTrequestupdateOrder(order: Order, status: string): Observable<Order>should make aPUTrequest to/orders/<order-id>deleteOrder(orderId: string): Observable<Order>should make aDELETErequest to/orders/<order-id>
 - You will need to make sure 
HttpClientis imported and added as a property in theOrderServiceconstructor. - You can pass a request 
bodyusing the second argument ofHttpClientpostandputmethods. 
P2: Solution
Click to see the solution
✏️ Update src/app/order/order.service.ts
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from '../../environments/environment';
export interface Item {
  name: string;
  price: number;
}
export interface CreateOrderDto {
  restaurant: string;
  name: string;
  address: string;
  phone: string;
  items: Item[];
}
export interface Order {
  _id: string;
  restaurant: string;
  name: string;
  address: string;
  phone: string;
  status: string;
  items: Item[];
}
@Injectable({
  providedIn: 'root'
})
export class OrderService {
  constructor(private httpClient: HttpClient) {}
  getOrders(): Observable<{ data: Order[] }> {
    return this.httpClient.get<{ data: Order[] }>(
      environment.apiUrl + '/orders'
    );
  }
  createOrder(order: CreateOrderDto): Observable<Order> {
    const orderData = {
      ...order,
      status: 'new',
    };
    return this.httpClient.post<Order>(
      environment.apiUrl + '/orders',
      orderData
    );
  }
  updateOrder(order: Order, action: string): Observable<Order> {
    const orderData = {
      ...order,
      status: action,
    };
    return this.httpClient.put<Order>(
      environment.apiUrl + '/orders/' + orderData._id,
      orderData
    );
  }
  deleteOrder(id: string): Observable<Order> {
    return this.httpClient.delete<Order>(environment.apiUrl + '/orders/' + id);
  }
}
 
Problem 3: Use the OrderService in the OrderComponent to Create an Order
P3: Technical requirements
For this problem, we will:
- Create an order when the user submits the form using the service.
 - Disable the button while the order is being processed.
 - Show the completed order to the user.
 - Let the user start a new order.
 
How we will solve this:
- We will import the order service, and save it as 
orderServicein theOrderComponent’sconstructor. - Call 
orderService’screateOrderwith theorderForm’s values. - While the order is being created 
orderProcessingshould betrue. - Once complete, 
orderCompleteshould be set totrueand set back tofalsewhenstartNewOrder()is called. - We will save the completed order in 
completedOrder. 
A FormGroup’s
valueproperty is wrapped inPartialtype because controls are removed from the form’s value when disabled. For our case, we don’t need to disable controls. We can use a FormGroup’sgetRawValue()method to access its value with the full type.
P3: Setup
Before starting:
1. ✏️ Update src/app/order/order.component.html to show the completed order:
<div class="order-form">
  <ng-container *ngIf="orderComplete && completedOrder; else showOrderForm">
    <h3>Thanks for your order {{ completedOrder.name }}!</h3>
    <div>
      <label class="control-label">
        Confirmation Number: {{ completedOrder._id }}</label
      >
    </div>
    <h4>Items ordered:</h4>
    <ul class="list-group panel">
      <li class="list-group-item" *ngFor="let item of completedOrder.items">
        <label>
          {{ item.name }} <span class="badge">${{ item.price }}</span>
        </label>
      </li>
      <li class="list-group-item">
        <label>
          Total <span class="badge">${{ orderTotal }}</span>
        </label>
      </li>
    </ul>
    <div>
      <label class="control-label"> Phone: {{ completedOrder.phone }} </label>
    </div>
    <div>
      <label class="control-label">
        Address: {{ completedOrder.address }}
      </label>
    </div>
    <p>
      <button (click)="startNewOrder()">Place another order</button>
    </p>
  </ng-container>
  <ng-template #showOrderForm>
    <h2>Order here</h2>
    <form
      *ngIf="orderForm && restaurant"
      [formGroup]="orderForm"
      (ngSubmit)="onSubmit()"
    >
      <tabset>
        <tab heading="Lunch Menu" *ngIf="restaurant.menu.lunch">
          <ul class="list-group">
            <pmo-menu-items
              [items]="restaurant.menu.lunch"
              formControlName="items"
            ></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
          <ul class="list-group">
            <pmo-menu-items
              [items]="restaurant.menu.dinner"
              formControlName="items"
            ></pmo-menu-items>
          </ul>
        </tab>
      </tabset>
      <div class="form-group">
        <label class="control-label">Name:</label>
        <input name="name" type="text" formControlName="name" />
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" formControlName="address" />
        <p class="help-text">Please enter your address.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Phone:</label>
        <input
          name="phone"
          type="text"
          pmoOnlyNumbers
          formControlName="phone"
        />
        <p class="help-text">Please enter your phone number.</p>
      </div>
      <div class="submit">
        <h4>Total: ${{ orderTotal }}</h4>
        <div class="loading" *ngIf="orderProcessing"></div>
        <button
          type="submit"
          [disabled]="!orderForm.valid || orderProcessing"
          class="btn"
        >
          Place My Order!
        </button>
      </div>
    </form>
  </ng-template>
</div>
 
2. ✏️ Update src/app/order/order.component.ts to have a onSubmit method and
a startNewOrder that will start a new order.
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormControl,
  FormGroup,
  ValidationErrors,
  ValidatorFn,
  Validators,
} from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Restaurant } from '../restaurant/restaurant';
import { RestaurantService } from '../restaurant/restaurant.service';
export interface Item {
  name: string;
  price: number;
}
export interface OrderForm {
  restaurant: FormControl<string>;
  name: FormControl<string>;
  address: FormControl<string>;
  phone: FormControl<string>;
  items: FormControl<Item[]>;
}
// CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM.
function minLengthArray(min: number): ValidatorFn {
  return (c: AbstractControl): ValidationErrors | null => {
    if (c.value.length >= min) {
      return null;
    }
    return { minLengthArray: { valid: false } };
  };
}
@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrl: './order.component.css',
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm?: FormGroup<OrderForm>;
  restaurant?: Restaurant;
  isLoading = true;
  orderTotal = 0.0;
  completedOrder: any;
  orderComplete = false;
  orderProcessing = false;
  private onDestroy$ = new Subject<void>();
  constructor(
    private route: ActivatedRoute,
    private restaurantService: RestaurantService,
    private formBuilder: FormBuilder
  ) {}
  ngOnInit(): void {
    // GETTING THE RESTAURANT FROM THE ROUTE SLUG
    const slug = this.route.snapshot.paramMap.get('slug');
    if (slug) {
      this.restaurantService
        .getRestaurant(slug)
        .pipe(takeUntil(this.onDestroy$))
        .subscribe((data: Restaurant) => {
          this.restaurant = data;
          this.isLoading = false;
          this.createOrderForm();
        });
    }
  }
  ngOnDestroy(): void {
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }
  createOrderForm(): void {
    this.orderForm = this.formBuilder.nonNullable.group({
      restaurant: [this.restaurant?._id ?? '', Validators.required],
      name: ['', Validators.required],
      address: ['', Validators.required],
      phone: ['', Validators.required],
      // PASSING OUR CUSTOM VALIDATION FUNCTION TO THIS FORM CONTROL
      items: [[] as Item[], minLengthArray(1)],
    });
    this.onChanges();
  }
  getChange(newItems: Item[]): void {
    this.orderForm?.controls.items.patchValue(newItems);
  }
  onChanges(): void {
    // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL
    this.orderForm?.controls.items.valueChanges
      .pipe(takeUntil(this.onDestroy$))
      .subscribe((value) => this.calculateTotal(value));
  }
  calculateTotal(items: Item[]): void {
    let total = 0.0;
    if (items.length) {
      for (const item of items) {
        total += item.price;
      }
      this.orderTotal = Math.round(total * 100) / 100;
    } else {
      this.orderTotal = total;
    }
  }
  onSubmit(): void {
    this.orderProcessing = true;
    // call createOrder here
  }
  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}
 
P3: How to verify your solution is correct
If you’ve implemented everything correctly, you should now be able to create an order from the UI and see a record of your completed order once it’s created.
✏️ Update the menu-items spec file src/app/order/order.component.spec.ts to be:
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { RestaurantService } from '../restaurant/restaurant.service';
import { MenuItemsComponent } from './menu-items/menu-items.component';
import { OrderComponent, OrderForm } from './order.component';
import { CreateOrderDto, OrderService } from './order.service';
class MockRestaurantService {
  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',
    });
  }
}
class MockOrderService {
  createOrder(order: CreateOrderDto) {
    return of({
      address: null,
      items: [
        { name: 'Onion fries', price: 15.99 },
        { name: 'Roasted Salmon', price: 23.99 },
      ],
      name: 'Jennifer Hungry',
      phone: null,
      restaurant: 'uPkA2jiZi24tCvXh',
      status: 'preparing',
      _id: '0awcHyo3iD6CpvhX',
    });
  }
}
const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace';
      },
    },
  },
};
describe('OrderComponent', () => {
  let component: OrderComponent;
  let fixture: ComponentFixture<OrderComponent>;
  let orderService: OrderService;
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [OrderComponent, MenuItemsComponent],
      imports: [ReactiveFormsModule, RouterTestingModule],
      providers: [
        {
          provide: RestaurantService,
          useClass: MockRestaurantService,
        },
        {
          provide: ActivatedRoute,
          useValue: MockActivatedRoute,
        },
        {
          provide: OrderService,
          useClass: MockOrderService,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
    orderService = TestBed.inject(OrderService);
  });
  beforeEach(() => {
    fixture = TestBed.createComponent(OrderComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  it('should get a restaurant based on route slug', () => {
    const mockRestaurant = {
      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',
    };
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant);
  });
  it('should have an orderForm formGroup', () => {
    expect(
      fixture.componentInstance.orderForm?.controls['restaurant']
    ).toBeTruthy();
    expect(
      fixture.componentInstance.orderForm?.controls['address']
    ).toBeTruthy();
    expect(fixture.componentInstance.orderForm?.controls['phone']).toBeTruthy();
    expect(fixture.componentInstance.orderForm?.controls['items']).toBeTruthy();
  });
  it('should have a validator on items formControl', () => {
    const itemFormControl =
      fixture.componentInstance.orderForm?.controls['items'];
    expect(itemFormControl?.valid).toEqual(false);
  });
  it('should update items FormControl when setUpdatesItems is called', () => {
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    const childInput = compiled
      .getElementsByTagName('pmo-menu-items')[0]
      .getElementsByTagName('input')[0];
    const formItems = fixture.componentInstance.orderForm?.get('items');
    childInput.click();
    fixture.detectChanges();
    expect(formItems?.value).toEqual([
      { name: 'Crab Pancakes with Sorrel Syrup', price: 35.99 },
    ]);
  });
  it('should update the order total when the items FormControl value changes', () => {
    fixture.detectChanges();
    const compiled = fixture.nativeElement as HTMLElement;
    const childInput1 = compiled
      .getElementsByTagName('pmo-menu-items')[0]
      .getElementsByTagName('input')[0];
    const childInput2 = compiled
      .getElementsByTagName('pmo-menu-items')[0]
      .getElementsByTagName('input')[1];
    childInput1.click();
    childInput2.click();
    fixture.detectChanges();
    const orderText = compiled.querySelector('.submit h4');
    expect(orderText?.textContent).toEqual('Total: $57.98');
  });
  it('should call the orderService createOrder on form submit with form values', () => {
    const createOrderSpy = spyOn(orderService, 'createOrder').and.callThrough();
    const expectedOrderValue: ReturnType<FormGroup<OrderForm>['getRawValue']> =
      {
        restaurant: '12345',
        name: 'Jennifer Hungry',
        address: '123 Main St',
        phone: '555-555-5555',
        items: [
          { name: 'Onion fries', price: 15.99 },
          { name: 'Roasted Salmon', price: 23.99 },
        ],
      };
    const compiled = fixture.nativeElement as HTMLElement;
    fixture.componentInstance.orderForm?.setValue(expectedOrderValue);
    fixture.detectChanges();
    (
      compiled.querySelector('button[type="submit"]') as HTMLButtonElement
    ).click();
    expect(createOrderSpy).toHaveBeenCalledWith(expectedOrderValue);
  });
  it('should show completed order when order is complete', () => {
    const expectedOrderValue: ReturnType<FormGroup<OrderForm>['getRawValue']> =
      {
        restaurant: '12345',
        name: 'Jennifer Hungry',
        address: '123 Main St',
        phone: '555-555-5555',
        items: [
          { name: 'Onion fries', price: 15.99 },
          { name: 'Roasted Salmon', price: 23.99 },
        ],
      };
    const compiled = fixture.nativeElement as HTMLElement;
    fixture.componentInstance.orderForm?.setValue(expectedOrderValue);
    fixture.detectChanges();
    (
      compiled.querySelector('button[type="submit"]') as HTMLButtonElement
    ).click();
    fixture.detectChanges();
    const displayedOrder = compiled.querySelector('h3');
    expect(displayedOrder?.textContent).toEqual(
      'Thanks for your order Jennifer Hungry!'
    );
  });
  it('should clear the form values when create new order is clicked', () => {
    const expectedOrderValue: ReturnType<FormGroup<OrderForm>['getRawValue']> =
      {
        restaurant: '12345',
        name: 'Jennifer Hungry',
        address: '123 Main St',
        phone: '555-555-5555',
        items: [
          { name: 'Onion fries', price: 15.99 },
          { name: 'Roasted Salmon', price: 23.99 },
        ],
      };
    const compiled = fixture.nativeElement as HTMLElement;
    fixture.componentInstance.orderForm?.setValue(expectedOrderValue);
    fixture.detectChanges();
    (
      compiled.querySelector('button[type="submit"]') as HTMLButtonElement
    ).click();
    fixture.detectChanges();
    (
      compiled.querySelector('button:nth-child(1)') as HTMLButtonElement
    ).click();
    const emptyform = {
      restaurant: '3ZOZyTY1LH26LnVw',
      name: '',
      address: '',
      phone: '',
      items: [],
    };
    expect(fixture.componentInstance.orderForm?.value).toEqual(emptyform);
  });
});
 
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
 - How to call a method on a service and get the result
 - How to show/hide content using *ngIf
 
P3: Solution
Click to see the solution
✏️ Update src/app/order/order.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormControl,
  FormGroup,
  ValidationErrors,
  ValidatorFn,
  Validators,
} from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { Restaurant } from '../restaurant/restaurant';
import { RestaurantService } from '../restaurant/restaurant.service';
import { Order, OrderService } from './order.service';
export interface Item {
  name: string;
  price: number;
}
export interface OrderForm {
  restaurant: FormControl<string>;
  name: FormControl<string>;
  address: FormControl<string>;
  phone: FormControl<string>;
  items: FormControl<Item[]>;
}
// CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM.
function minLengthArray(min: number): ValidatorFn {
  return (c: AbstractControl): ValidationErrors | null => {
    if (c.value.length >= min) {
      return null;
    }
    return { minLengthArray: { valid: false } };
  };
}
@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrl: './order.component.css',
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm?: FormGroup<OrderForm>;
  restaurant?: Restaurant;
  isLoading = true;
  orderTotal = 0.0;
  completedOrder?: Order;
  orderComplete = false;
  orderProcessing = false;
  private onDestroy$ = new Subject<void>();
  constructor(
    private route: ActivatedRoute,
    private restaurantService: RestaurantService,
    private orderService: OrderService,
    private formBuilder: FormBuilder
  ) {}
  ngOnInit(): void {
    // GETTING THE RESTAURANT FROM THE ROUTE SLUG
    const slug = this.route.snapshot.paramMap.get('slug');
    if (slug) {
      this.restaurantService
        .getRestaurant(slug)
        .pipe(takeUntil(this.onDestroy$))
        .subscribe((data: Restaurant) => {
          this.restaurant = data;
          this.isLoading = false;
          this.createOrderForm();
        });
    }
  }
  ngOnDestroy(): void {
    this.onDestroy$.next();
    this.onDestroy$.complete();
  }
  createOrderForm(): void {
    this.orderForm = this.formBuilder.nonNullable.group({
      restaurant: [this.restaurant?._id ?? '', Validators.required],
      name: ['', Validators.required],
      address: ['', Validators.required],
      phone: ['', Validators.required],
      // PASSING OUR CUSTOM VALIDATION FUNCTION TO THIS FORM CONTROL
      items: [[] as Item[], minLengthArray(1)],
    });
    this.onChanges();
  }
  getChange(newItems: Item[]): void {
    this.orderForm?.controls.items.patchValue(newItems);
  }
  onChanges(): void {
    // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL
    this.orderForm?.controls.items.valueChanges
      .pipe(takeUntil(this.onDestroy$))
      .subscribe((value) => this.calculateTotal(value));
  }
  calculateTotal(items: Item[]): void {
    let total = 0.0;
    if (items.length) {
      for (const item of items) {
        total += item.price;
      }
      this.orderTotal = Math.round(total * 100) / 100;
    } else {
      this.orderTotal = total;
    }
  }
  onSubmit(): void {
    if (!this.orderForm?.valid) {
      return;
    }
    this.orderProcessing = true;
    this.orderService
      .createOrder(this.orderForm.getRawValue())
      .pipe(takeUntil(this.onDestroy$))
      .subscribe((res: Order) => {
        this.completedOrder = res;
        this.orderComplete = true;
        this.orderProcessing = false;
      });
  }
  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    this.orderTotal = 0.0;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}