---
title: Bitovi Academy - Binding Component Data
description: Building the Order Form
---

- ![AI implementation](https://www.bitovi.com/hubfs/AIConsultingIcon.svg)
  
  [AI implementation](https://www.bitovi.com/services/ai-consulting?hsLang=en-us)
- ![Systems engineering](https://www.bitovi.com/hubfs/icon%20-%20backend.svg)
  
  [Systems engineering](https://www.bitovi.com/services/systems-engineering-consulting?hsLang=en-us)
- ![Project management](https://www.bitovi.com/hubfs/icon%20-%20PM.svg)
  
  [Project management](https://www.bitovi.com/services/agile-project-management-consulting?hsLang=en-us)
- ![Product design](https://www.bitovi.com/hubfs/icon%20-%20design.svg)
  
  [Product design](https://www.bitovi.com/services/product-design-consulting?hsLang=en-us)
- ![Frontend development](https://www.bitovi.com/hubfs/icon%20-%20frontend.svg)
  
  [Frontend development](https://www.bitovi.com/services/frontend-development-consulting?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/digital-consulting-services?hsLang=en-us)

We're Experts in...

- [JavaScript](https://www.bitovi.com/services/frontend/javascript-consulting?hsLang=en-us)
- [AI training](https://www.bitovi.com/ai-training-for-software-engineers?hsLang=en-us)
- [Angular](https://www.bitovi.com/services/frontend/angular-consulting?hsLang=en-us)
- [Design Systems](https://www.bitovi.com/services/axure-figma-migration?hsLang=en-us)
- [React](https://www.bitovi.com/services/frontend/react-consulting?hsLang=en-us)
- [Temporal](https://www.bitovi.com/services/backend/temporal-consulting?hsLang=en-us)
- [React Native](https://www.bitovi.com/services/frontend/react-consulting/react-native?hsLang=en-us)
- [Node.js](https://www.bitovi.com/services/backend/nodejs-consulting-1?hsLang=en-us)

Showcase

![Yum! Brands](https://www.bitovi.com/hubfs/yum-showcase-link-1.png)

[View case study](https://www.bitovi.com/en/bitovi-yum-case-study?hsLang=en-us)

More Projects

- [![Levi's](https://www.bitovi.com/hubfs/levis.svg)](https://www.bitovi.com/web-application-consulting-work/levis-ecommerce-responsive-redesign?hsLang=en-us)
- [![Christie's International Real Estate](https://www.bitovi.com/hubfs/christies.svg)](https://design.bitovi.com/christies)
- [![BAFS](https://www.bitovi.com/hubfs/bafs.svg)](https://www.bitovi.com/ux-design-consulting/ux-case-studies/bafs-ppp?hsLang=en-us)
- [View more
  
  →
  
  ](https://www.bitovi.com/our-software-consulting-work?hsLang=en-us)

Open Source Tools

We build powerful tools and open source them to support the community.

[See what we've built →](https://www.bitovi.com/open-source?hsLang=en-us)

- [![Blog](https://www.bitovi.com/hubfs/icon%20-%20blog.svg)
  
  BlogWe post about perfecting software delivery
  
  ](https://bitovi.com/blog)
- [![Partnerships](https://www.bitovi.com/hubfs/Handshake-1.svg)
  
  PartnershipsLearn about Bitovi's technology partners
  
  ](https://www.bitovi.com/partnerships?hsLang=en-us)
- [![Academy](https://www.bitovi.com/hubfs/limbo/icons/icon-academy.svg)
  
  AcademyFree courses to build delivery skills
  
  ](https://www.bitovi.com/academy/?hsLang=en-us)
- [![Open source tools](https://www.bitovi.com/hubfs/icon%20-%20open%20source.svg)
  
  Open source toolsUse our tools or contribute to our open source mission.
  
  ](https://www.bitovi.com/open-source?hsLang=en-us)

Let's Connect

- [![Discord](https://www.bitovi.com/hubfs/DiscordLogo.svg)
  
  Discord
  
  ](https://discord.gg/J7ejFsZnJ4)
- [![LinkedIn](https://www.bitovi.com/hubfs/LinkedinLogo.svg)
  
  LinkedIn
  
  ](https://www.linkedin.com/company/bitovi/)
- [![GitHub](https://www.bitovi.com/hubfs/GithubLogo.svg)
  
  GitHub
  
  ](https://github.com/bitovi/)

![Eggbot](https://www.bitovi.com/hubfs/build_assets/bitovi-limbo-cms-react/338/js_client_assets/assets/eggbot-LTGhdSGL.png)

Name *

Work Email *

Phone

What's your project?

Send

### Contact Us

(312) 620-0386contact@bitovi.com

- [ Advanced JavaScript ](https://www.bitovi.com/academy/learn-advanced-javascript.html?hsLang=en-us)
- [ DOM and jQuery ](https://www.bitovi.com/academy/learn-dom-jquery.html?hsLang=en-us)
- [ Debug JavaScript ](https://www.bitovi.com/academy/learn-to-debug-javascript.html?hsLang=en-us)
- [ Learn TypeScript ](https://www.bitovi.com/academy/learn-typescript.html?hsLang=en-us)
- [ Learn React Native ](https://www.bitovi.com/academy/learn-react-native.html?hsLang=en-us)
- [ Learn Web Components ](https://www.bitovi.com/academy/learn-web-components.html?hsLang=en-us)
- [ Agile Program Management With Jira ](https://www.bitovi.com/academy/learn-agile-program-management-with-jira.html?hsLang=en-us)
- [ Learn RxJS ](https://www.bitovi.com/academy/learn-rxjs.html?hsLang=en-us)
- [ Giving a Training ](https://www.bitovi.com/academy/training.html?hsLang=en-us)
- [ Learn Technology Consulting ](https://www.bitovi.com/academy/learn-technology-consulting.html?hsLang=en-us)
- [ Learn React ](https://www.bitovi.com/academy/learn-react.html?hsLang=en-us)
- [ Learn NgRx ](https://www.bitovi.com/academy/learn-ngrx.html?hsLang=en-us)
- [ Learn Graphql ](https://www.bitovi.com/academy/learn-graphql.html?hsLang=en-us)
- [ Learn Docker ](https://www.bitovi.com/academy/learn-docker.html?hsLang=en-us)
- [ Learn CanJS ](https://www.bitovi.com/academy/learn-canjs.html?hsLang=en-us)
- [ Learn Angular ](https://www.bitovi.com/academy/learn-angular.html?hsLang=en-us) 
    - [ Why Angular? ](https://www.bitovi.com/academy/learn-angular/why-angular.html?hsLang=en-us)
    - [ Generate an App ](https://www.bitovi.com/academy/learn-angular/building-our-first-app.html?hsLang=en-us)
    - [ Creating Components ](https://www.bitovi.com/academy/learn-angular/creating-components.html?hsLang=en-us)
    - [ Creating Pipes ](https://www.bitovi.com/academy/learn-angular/creating-pipes.html?hsLang=en-us)
    - [ Adding Routing ](https://www.bitovi.com/academy/learn-angular/adding-routing.html?hsLang=en-us)
    - [ Creating Navigation ](https://www.bitovi.com/academy/learn-angular/creating-navigation.html?hsLang=en-us)
    - [ Fetching Data with Services ](https://www.bitovi.com/academy/learn-angular/restaurant-service.html?hsLang=en-us)
    - [ Rendering Data in Components ](https://www.bitovi.com/academy/learn-angular/pull-restaurant-data-into-view.html?hsLang=en-us)
    - [ Reactive Forms Data Binding ](https://www.bitovi.com/academy/learn-angular/state-city-options.html?hsLang=en-us)
    - [ Listening to Form Changes ](https://www.bitovi.com/academy/learn-angular/form-value-changes.html?hsLang=en-us)
    - [ Multi-parameter Services ](https://www.bitovi.com/academy/learn-angular/updating-service-params.html?hsLang=en-us)
    - [ Optional: Declarative State ](https://www.bitovi.com/academy/learn-angular/declarative-state.html?hsLang=en-us)
    - [ Writing Unit Tests ](https://www.bitovi.com/academy/learn-angular/writing-unit-tests.html?hsLang=en-us)
    - [ Creating Nested Routes ](https://www.bitovi.com/academy/learn-angular/nested-routes.html?hsLang=en-us)
    - [ Binding Component Data ](https://www.bitovi.com/academy/learn-angular/building-order-form.html?hsLang=en-us)
    - [ Creating Directives ](https://www.bitovi.com/academy/learn-angular/creating-directive.html?hsLang=en-us)
    - [ Order Service ](https://www.bitovi.com/academy/learn-angular/order-service.html?hsLang=en-us)
    - [ Order History Component ](https://www.bitovi.com/academy/learn-angular/order-history-component.html?hsLang=en-us)
    - [ Item Total Pipe ](https://www.bitovi.com/academy/learn-angular/item-total-pipe.html?hsLang=en-us)
    - [ Real Time Connections ](https://www.bitovi.com/academy/learn-angular/real-time-connection.html?hsLang=en-us)
    - [ Deploy App ](https://www.bitovi.com/academy/learn-angular/deploy-app.html?hsLang=en-us)
- [ Advanced TypeScript ](https://www.bitovi.com/academy/advanced-typescript.html?hsLang=en-us)

[View More Courses](https://www.bitovi.com/academy/#courses) [ ]

# Binding Component Data `page`

Building the Order Form

## Overview

In this part, we will:

- Create a new order component
- Get the restaurant from route params
- Add new route for ordering from a restaurant
- Import a third-party lib
- Create a custom component to handle item selection

## Creating an order form component

Our order form is how we can create new orders. We’ll use a reactive form to get data from the users, use a custom validation function to make sure at least one item has been selected, and calculate the order total every time a new item is selected or unselected.

## Problem 1: Create a new route for ordering from a restaurant

## P1: What you need to know

- How to create new components
  
  ```
  ng g component order
  ```
- You’ve created routes before! You got this!

## P1: Technical requirements

Create a new `order` component, and create a route for our new component! The path should be `/restaurants/{{slug}}/order`.

## P1: How to verify your solution is correct

When you navigate to the `/order` path from a restaurant detail page you should see your new order component.

✏️ 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 { 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,
      ],
      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 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: Solution

Click to see the solution ✏️ 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 { 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,
  },
];

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

```

## Problem 2: Build out the order component

We’ve covered a few concepts, like how to get the slug from the route, how to get a restaurant, how to create a form and subscribe to its changes. Let’s practice those concepts.

We’ve provided some starting code to get through this section to help you get the restaurant based on the route slug, create a new reactive form to collect order information, and update the order total whenever the `items` FormControl value changes.

## P2: What you need to know

- How to get the restaurant from the route slug (you learned this in previous sections! ✔️)
- Create a reactive form (you learned this in previous sections! ✔️)
- Listen to form value changes (you learned this in previous sections! ✔️)
- Add validation:
  
  This time, our form will require [validation](https://angular.io/guide/form-validation#reactive-form-validation). Here’s an example of a form with form controls with different validation, and one whose value is set to an array.

```
function validateLessThan(maxValue: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (control.value < maxValue) {
      return null;
    }
    return { belowMax: { valid: false } };
  };
}

this.myValidatingForm = this.formBuilder.group({
  name: [null, Validators.required],
  age: [null, validateLessThan(18)],
  hobbies: [[]]
});
```

## P2: Technical requirements

The order form component needs to get the restaurant from the route slug, and needs a reactive form to collect `restaurant`, `name`, `address`, `phone`, and `items`, and a way to update the order total when the items form control changes.

## P2: Setup

✏️ Update the **src/app/order/order.component.html** file to be:

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></ng-container>
  <ng-template #showOrderForm>
    <h2>Order here</h2>
    <form
      *ngIf="orderForm && restaurant"
      [formGroup]="orderForm"
      (ngSubmit)="onSubmit()"
    >
      <ng-container *ngIf="restaurant?.menu?.lunch">
        <h4>Lunch Menu</h4>
        <ul class="list-group">
          <li
            class="list-group-item"
            *ngFor="let item of restaurant.menu.lunch"
          >
            <label>
              <input type="checkbox" formControlName="items" />
              {{ item.name }} <span class="badge">${{ item.price }}</span>
            </label>
          </li>
        </ul>
      </ng-container>
      <ng-container *ngIf="restaurant.menu.dinner">
        <h4>Dinner Menu</h4>
        <ul class="list-group">
          <li
            class="list-group-item"
            *ngFor="let item of restaurant.menu.dinner"
          >
            <label>
              <input type="checkbox" formControlName="items" />
              {{ item.name }} <span class="badge">${{ item.price }}</span>
            </label>
          </li>
        </ul>
      </ng-container>
      <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" 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>

```

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

```
import { Component, OnInit, OnDestroy } from '@angular/core';
import {
  AbstractControl,
  FormBuilder,
  FormControl,
  FormGroup,
  ValidationErrors,
  ValidatorFn,
} 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 {
    // GET THE RESTAURANT FROM THE ROUTE SLUG
  }

  ngOnDestroy(): void {}

  createOrderForm(): void {
    // CREATE AN ORDER FORM TO COLLECT: RESTAURANT ID, NAME, ADDRESS, PHONE, AND ITEMS
    // ITEMS SHOULD USE THE CUSTOM MINLENGTH ARRAY VALIDATION
    this.orderForm = this.formBuilder.nonNullable.group({});
    this.onChanges();
  }

  onChanges(): void {
    // SUBSCRIBE TO THE ITEMS FORMCONTROL CHANGE TO CALCULATE A NEW TOTAL
  }

  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 {}

  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}

```

## P2: How to verify your solution is correct

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

```
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { 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 { OrderComponent } from './order.component';

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',
    });
  }
}

const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace';
      },
    },
  },
};

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [OrderComponent],
      imports: [ReactiveFormsModule, RouterTestingModule],
      providers: [
        {
          provide: RestaurantService,
          useClass: MockRestaurantService,
        },
        {
          provide: ActivatedRoute,
          useValue: MockActivatedRoute,
        },
      ],
    }).compileComponents();
  });

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

```

## P2: 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';

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

  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 {}

  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}

```

## Importing third-party plugins

In our markup we would like to display our lunch and dinner menus in tabs. Instead of creating our own library, let’s import a well supported one, [ngx-bootstrap](https://valor-software.com/ngx-bootstrap/#/documentation#getting-started):

✏️ Run:

```
ng add ngx-bootstrap
```

The `ng add` command is a convenient way to import third-party libs that will update `angular.json` and `package.json` with any changes we need.

✏️ Update **src/app/app.module.ts**. Once you’re done, don’t forget to restart the server!

```
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TabsModule } from 'ngx-bootstrap/tabs';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
import { ImageUrlPipe } from './image-url.pipe';
import { DetailComponent } from './restaurant/detail/detail.component';
import { OrderComponent } from './order/order.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    RestaurantComponent,
    ImageUrlPipe,
    DetailComponent,
    OrderComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    ReactiveFormsModule,
    BrowserAnimationsModule,
    TabsModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

```

Now let’s add the markup to our order component implementing the tabs widget.

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

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></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">
            <li
              class="list-group-item"
              *ngFor="let item of restaurant?.menu?.lunch"
            >
              <label>
                <input type="checkbox" formControlName="items" />
                {{ item.name }} <span class="badge">${{ item.price }}</span>
              </label>
            </li>
          </ul>
        </tab>
        <tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
          <ul class="list-group">
            <li
              class="list-group-item"
              *ngFor="let item of restaurant.menu.dinner"
            >
              <label>
                <input type="checkbox" formControlName="items" />
                {{ item.name }} <span class="badge">${{ item.price }}</span>
              </label>
            </li>
          </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" 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>

```

Now when we view the order form of our route, we’ll see a nice form and tabs for lunch and dinner menu options.

![Place My Order App tabs](https://bitovi.github.io/academy/static/img/angular/pmo-tabs-working.gif)

## Problem 3: Create a `menu-items` component

We’re going to build another component to use in our form to handle selecting order items. We use data-binding to pass data between components. We’ll use the `@Input()` to get our list of items from the restaurant to display in our child component, and eventually hook it into our Reactive Form using the `formControlName` attribute as shown below.

```
<pmo-menu-items
  [data]="restaurant.menu.lunch"
  formControlName="items"
></pmo-menu-items>
```

## P3: What you need to know

## Component interaction

Components in Angular can pass data back and forth to each other through the use of [@Input](https://angular.io/api/core/Input) and [@Output](https://angular.io/api/core/Output) decorations.

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION, Input } = ng.core;

@Component({
  selector: 'my-child',
  template: `
    My parent’s name is {{parentName}}
  `
})
class ChildComponent {
  @Input() parentName: string;
  constructor() {
  }
}

@Component({
  selector: 'my-app',
  template: `
    <my-child parentName="Bob"></my-child>

    <my-child [parentName]="momName"></my-child>
  `
})
class AppComponent {
  momName = 'Sandy';

  constructor() {
  }
}

// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;

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

const { platformBrowserDynamic } = ng.platformBrowserDynamic;

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));
</script>

```

- How to use *ngFor (you learned this in previous sections! ✔️)
- How to use @Input to pass properties (you learned this in the section above! ✔️)

## P3: Setup

Create the new menu-items component inside the order component folder

✏️ Run:

```
ng g component order/menu-items
```

✏️ Update **src/app/order/menu-items/menu-items.component.html**

```
<!--  Iterate over items and render the markup below for each item -->
<li class="list-group-item">
  <label>
    <input type="checkbox" />
    ITEM_NAME <span class="badge">$ ITEM_PRICE</span>
  </label>
</li>
```

Go ahead and put your new component in the order component.

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

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></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></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
          <ul class="list-group">
            <pmo-menu-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" 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>

```

## P3: How to verify your solution is correct

✏️ Update the order 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 { 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 { OrderComponent } from './order.component';

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',
    });
  }
}

const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace';
      },
    },
  },
};

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [OrderComponent],
      imports: [ReactiveFormsModule, RouterTestingModule],
      providers: [
        {
          provide: RestaurantService,
          useClass: MockRestaurantService,
        },
        {
          provide: ActivatedRoute,
          useValue: MockActivatedRoute,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  });

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

```

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

```
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { MenuItemsComponent } from './menu-items.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MenuItemsComponent],
    }).compileComponents();
  });

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

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

  it('should display a list of inputs', () => {
    fixture.componentInstance.items = [
      { name: 'Charred Octopus', price: 25.99 },
      { name: 'Steamed Mussels', price: 21.99 },
      { name: 'Ricotta Gnocchi', price: 15.99 },
    ];
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement as HTMLElement;
    const itemLabels = compiled.getElementsByTagName('label');
    expect(itemLabels.length).toEqual(3);
  });
});

```

## P3: Solution

Click to see the solution ✏️ Update **src/app/order/menu-items.component.html**

```
<li class="list-group-item" *ngFor="let item of items">
  <label>
    <input type="checkbox" />
    {{ item.name }} <span class="badge">${{ item.price }}</span>
  </label>
</li>

```

✏️ Update **src/app/order/menu-items.component.ts**

```
import { Component, Input } from '@angular/core';
import { Item } from '../order.component';

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrl: './menu-items.component.css',
})
export class MenuItemsComponent {
  @Input() items: Item[] = [];
}

```

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

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></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"></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.dinner"></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" 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>

```

## Problem 4: Attaching event handlers to item checkboxes

Next, we want to know when a checkbox has been checked or unchecked.

Before communicating between `MenuItemsComponent` and its parent `OrderComponent`, `MenuItemsComponent` will need to attach an event handler - a function - to the `change` event of the item checkboxes.

## P4: What you need to know

## Event handlers in Angular

Event binding in Angular follows a simple pattern - the event name in parenthesis and a function to call in quotes on the other side of an equal sign. `(event)="functionToCall()"`. Any parameter(s) can be passed to the event function, but to capture the event itself use the parameter `$event`

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="https://unpkg.com/@angular/forms@7.2.0/bundles/forms.umd.js"></script>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION, OnInit, Injectable } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { ReactiveFormsModule, FormControl } = ng.forms;

@Component({
  selector: 'my-app',
  template: `
    <p>A home component!</p>

    <button (click)="willCallOnClick()">
      Click me!
    </button>
    {{ clickMessage }}
    <button (click)="willCallWithParam(clickMessage)">
      Log click message
    </button>
    <p>
      Value: {{ name.value }}
    </p>
    <label>
      Name:
      <input
        type="text"
        [formControl]="name"
        (change)="willCallOnBlur($event)"
      />
    </label>
  `
})
class AppComponent {
  name = new FormControl('');

  constructor() {}

  willCallOnClick(): void {
    this.clickMessage = 'You are my hero!';
  }

  willCallWithParam(message: string): void {
    console.info(message + ' from my 1st click')
  }

  willCallOnBlur(event): void {
    console.info('value', event.target.value);
  }
}

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

const { platformBrowserDynamic } = ng.platformBrowserDynamic;

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));
</script>

```

## P4: Technical requirements

Create a function in the `MenuItemsComponent` called `updateItem` that fires whenever a checkbox is checked and takes a parameter of the item that has been checked.

## P4: How to verify your solution is correct

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

```
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { MenuItemsComponent } from './menu-items.component';

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [MenuItemsComponent],
    }).compileComponents();
  });

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

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

  it('should display a list of inputs', () => {
    fixture.componentInstance.items = [
      { name: 'Charred Octopus', price: 25.99 },
      { name: 'Steamed Mussels', price: 21.99 },
      { name: 'Ricotta Gnocchi', price: 15.99 },
    ];
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement as HTMLElement;
    const itemLabels = compiled.getElementsByTagName('label');
    expect(itemLabels.length).toEqual(3);
  });

  it('should call an updateItem function when a checkbox is selected or unselected', () => {
    fixture.componentInstance.items = [
      { name: 'Charred Octopus', price: 25.99 },
      { name: 'Steamed Mussels', price: 21.99 },
      { name: 'Ricotta Gnocchi', price: 15.99 },
    ];
    fixture.detectChanges();
    const changeSpy = spyOn(fixture.componentInstance, 'updateItem');
    const compiled = fixture.debugElement.nativeElement;
    const input = compiled.getElementsByTagName('input')[0];
    input.click();
    fixture.detectChanges();
    expect(changeSpy).toHaveBeenCalled();
  });
});

```

## P4: Solution

Click to see the solution ✏️ Update **src/app/order/menu-items/menu-items.component.html**

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

```

✏️ Update **src/app/order/menu-items/menu-items.component.ts**

```
import { Component, Input } from '@angular/core';
import { Item } from '../order.component';

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrl: './menu-items.component.css',
})
export class MenuItemsComponent {
  @Input() items: Item[] = [];

  updateItem(item: Item): void {

  }
}

```

## Problem 5: Update `OrderFormComponent` with the emitted `Item` from `MenuItemsComponent`

Now we want to let the form know what the selected items are as they change so we can update the order total accordingly.

## P5: What you need to know

- How to emit a value to a parent component
- How to programmatically update a `FormControl`’s value (you learned this in previous sections! ✔️)

## Emitting Data to Parent Components

To pass data to parent components in Angular, the [EventEmitter](https://angular.io/api/core/EventEmitter) class is used in combination with the [Output decorator](https://angular.io/api/core/Output). The Output decorator marks a property to be listened to during change detection, and we call the `emit` method to broadcast the property’s new value.

The parent component is listening for a change on the child component’s property and calls a function on that change that takes a parameter of the updated value.

```
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/router@7.2.0/bundles/router.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<script src="https://unpkg.com/@angular/forms@7.2.0/bundles/forms.umd.js"></script>
<base href="/">
<app-parent></app-parent>
<script type="typescript">
const { Component, NgModule, VERSION, OnInit, Input, Output, EventEmitter } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { ReactiveFormsModule, FormControl } = ng.forms;

@Component({
  selector: 'app-parent',
  styles: [ `
    :host {
      border: solid 2px deeppink;
      display: block;
      overflow: hidden;
      padding: 2em;
    }`
  ],
  template: `
    <p>Parent component!</p>
    <p>Message from child: {{ message }}</p>
    <child-component (sendMessage)="getMessage($event)">
    </child-component>
  `
})
class ParentComponent implements OnInit {
  message: string;
  constructor() {}

  ngOnInit(): void {

  }

  getMessage(event): void {
    this.message = event;
  }
}

@Component({
  selector: 'child-component',
  styles: [ `
    :host {
      border: solid 2px blue;
      display: block;
      overflow: hidden;
      padding: 2em;
    }`
  ],
  template: `
    <p>Child component</p>
    <label>
      Name:
      <input type="text" [formControl]="childMessage">
    </label>
    <button (click)="callToEmit()">Send child message</button>
  `
})
class ChildComponent implements OnInit {
  childMessage = new FormControl('');
  @Output() sendMessage: EventEmitter<any> = new EventEmitter();
 
  constructor() {}

  ngOnInit(): void {

  }

  callToEmit(): void {
    this.sendMessage.emit(this.childMessage.value);
  }
}

@NgModule({
  declarations: [ParentComponent, ChildComponent],
  imports: [
    BrowserModule,
    CommonModule, 
    ReactiveFormsModule
  ],
  bootstrap: [ParentComponent],
  providers: []
})
class AppModule {}

const { platformBrowserDynamic } = ng.platformBrowserDynamic;

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));
</script>

```

## P5: Technical requirements

Create an `itemChanged` EventEmitter property that emits the checked/unchecked `Item` value every time it changes, and in the parent OrderComponent update the `items` FormControl with the updated `Item`s array using the provided `getChange` function.

## P5: Setup

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

```
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(item: Item): void {
    if (!this.orderForm) {
      return;
    }
    const items = this.orderForm.controls.items.value;
    const index = items.indexOf(item);
    if (index > -1) {
      items.splice(index, 1);
    } else {
      items.push(item);
    }

    // TO-DO: UPDATE ITEMS FORMCONTROL WITH ITEMS VALUE
  }

  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 {}

  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}

```

## P5: How to verify your solution is correct

✏️ 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 { 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 } from './order.component';

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',
    });
  }
}

const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace';
      },
    },
  },
};

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [OrderComponent, MenuItemsComponent],
      imports: [ReactiveFormsModule, RouterTestingModule],
      providers: [
        {
          provide: RestaurantService,
          useClass: MockRestaurantService,
        },
        {
          provide: ActivatedRoute,
          useValue: MockActivatedRoute,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  });

  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 get updated selected Items when child component changes', () => {
    fixture.detectChanges();
    const changeSpy = spyOn(fixture.componentInstance, 'getChange');
    const compiled = fixture.nativeElement as HTMLElement;
    const childInput = compiled
      .getElementsByTagName('pmo-menu-items')[0]
      .getElementsByTagName('input')[0];
    childInput.click();
    expect(changeSpy).toHaveBeenCalled();
  });

  it('should update items FormControl when updateItem 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');
  });
});

```

## P5: Solution

Click to see the solution ✏️ Update **src/app/order/menu-items.component.ts**

```
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Item } from '../order.component';

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrl: './menu-items.component.css',
})
export class MenuItemsComponent {
  @Input() items: Item[] = [];
  @Output() itemChanged: EventEmitter<Item> = new EventEmitter();

  updateItem(item: Item): void {
    this.itemChanged.emit(item);
  }
}

```

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

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></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"
              (itemChanged)="getChange($event)"
            ></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner Menu" *ngIf="restaurant.menu.dinner">
          <ul class="list-group">
            <pmo-menu-items
              [items]="restaurant.menu.dinner"
              (itemChanged)="getChange($event)"
            ></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" 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>

```

✏️ 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';

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(item: Item): void {
    if (!this.orderForm) {
      return;
    }
    const items = this.orderForm.controls.items.value;
    const index = items.indexOf(item);
    if (index > -1) {
      items.splice(index, 1);
    } else {
      items.push(item);
    }

    this.orderForm.controls.items.setValue(items);
  }

  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 {}

  startNewOrder(): void {
    this.orderComplete = false;
    this.completedOrder = undefined;
    // CLEAR THE ORDER FORM
    this.createOrderForm();
  }
}

```

## Control Value Accessor

Using inputs and event emitters is a great way to pass data between components in a general sense. However this can be a very messy way to approach handling custom form situations. Some times a better approach can be to write a custom component that implements the [Control Value Accessor](https://angular.io/api/forms/ControlValueAccessor) interface to just write the value straight to the form. Classes implementing the CVA must have 3 methods - `onChange`, `onTouched`, `setValue`. We call these methods when the user interacts with our checkboxes to let the parent form know that values have been touched, when they change, and what the value is.

✏️ Update **src/app/order/menu-items.component.ts**

```
import { Component, forwardRef, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Item } from '../order.component';

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrl: './menu-items.component.css',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => MenuItemsComponent),
      multi: true,
    },
  ],
})
export class MenuItemsComponent implements ControlValueAccessor {
  @Input() items: Item[] = [];
  @Input('value') _value: Item[] = [];

  onChange: any = () => {};
  onTouched: any = () => {};

  get value() {
    return this._value;
  }

  set value(value) {
    this._value = value;
    this.onChange(value);
    this.onTouched();
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  writeValue(value: Item[]) {
    this.value = value;
  }

  updateItem(item: Item): void {
    const index = this._value?.indexOf(item) ?? -1;
    if (index !== -1) {
      this._value?.splice(index, 1);
    } else {
      this._value?.push(item);
    }
    this.writeValue(this._value);
  }
}

```

Other concepts used here:

### forwardRef

[forwardRef](https://angular.io/api/core/forwardRef) is used to reference a token that may not be defined when we need it.

### NG_VALUE_ACCESSOR

[NG_VALUE_ACCESSOR](https://angular.io/api/forms/NG_VALUE_ACCESSOR) is used to provide the control value accessor for a form control.

### Use New Menu Items Component in Order Form

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

```
<div class="order-form">
  <ng-container *ngIf="orderComplete; else showOrderForm"></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" 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>

```

We now have a form that updates the `items` formControl when items are selected and shows the user an updated total!

### Update order component Tests

✏️ Update the order 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 { 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 } from './order.component';

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',
    });
  }
}

const MockActivatedRoute = {
  snapshot: {
    paramMap: {
      get() {
        return 'poutine-palace';
      },
    },
  },
};

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

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      declarations: [OrderComponent, MenuItemsComponent],
      imports: [ReactiveFormsModule, RouterTestingModule],
      providers: [
        {
          provide: RestaurantService,
          useClass: MockRestaurantService,
        },
        {
          provide: ActivatedRoute,
          useValue: MockActivatedRoute,
        },
      ],
      schemas: [NO_ERRORS_SCHEMA],
    }).compileComponents();
  });

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

```

###### On this page...

###### Get help

If you need [help](https://discord.gg/J7ejFsZnJ4) with an exercise, please reach out to us on [Discord](https://discord.gg/J7ejFsZnJ4)

If you find a bug, please [create an issue](https://github.com/bitovi/academy/issues/new) or email [contact@bitovi.com](mailto:contact@bitovi.com?Subject=Training%20Suggestion)

## Join the Bitovi Newsletter

 Do you want to receive updates about new Bitovi Academy courses, Bitovi open source projects, and dev community news? If so, please provide your email. We won’t spam you or give it away for any reason!  
*By providing your email you agree to receive communications from Bitovi*

![](https://bitovi.github.io/academy/static/img/close.svg)

```json
{
     "@context": "http://schema.org",
     "@type": "Organization",
     "mainEntityOfPage":{
          "@type":"WebPage",
          "@id":"https://www.bitovi.com/academy/learn-angular/building-order-form.html",
          "description": "Building the Order Form 
"
     },
     "url": "http://bitovi.com",
     "logo": "https://www.bitovi.com/hubfs/bitovi-logo-23-1.svg",
     "image": "https://www.bitovi.com/hubfs/bitovi-logo-x2.png",
     "name": "Bitovi Web App Consulting",
     "alternateName": "Bitovi",
      "naics": "541511",
      "description": "Bitovi is a UX, UI design and front-end JavaScript development consulting company",
     "address": {
          "@type": "PostalAddress",
          "streetAddress": "1134 Pine Tree Lane ",
          "addressLocality": "Libertyville",
          "addressRegion": "IL",
          "addressCountry": "United States",
          "postalCode": "60048"
     },
     "areaServed": {
          "@type": "GeoCircle",
          "geoMidpoint": {
               "@type": "GeoCoordinates",
               "latitude": "41.8781",
               "longitude": "87.6298"
          },
          "geoRadius": "5000 km"
     },
     "telephone": "312-620-0386",
     "email": "contact@bitovi.com",
     "sameAs": [
          "https://www.facebook.com/BitoviLLC/",
          "https://twitter.com/bitovi",
          "https://www.linkedin.com/company/bitovi"
     ]
}
```