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 a GET request
    • createOrder(orderForm: CreateOrderDto): Observable<Order> should make a POST request
    • updateOrder(order: Order, status: string): Observable<Order> should make a PUT request to /orders/<order-id>
    • deleteOrder(orderId: string): Observable<Order> should make a DELETE request to /orders/<order-id>
  • You will need to make sure HttpClient is imported and added as a property in the OrderService constructor.
  • You can pass a request body using the second argument of HttpClient post and put methods.

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:

  1. We will import the order service, and save it as orderService in the OrderComponent’s constructor.
  2. Call orderService’s createOrder with the orderForm’s values.
  3. While the order is being created orderProcessing should be true.
  4. Once complete, orderComplete should be set to true and set back to false when startNewOrder() is called.
  5. We will save the completed order in completedOrder.

A FormGroup’s value property is wrapped in Partial type 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’s getRawValue() 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, when you run npm run test all tests will pass!

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