Building the Order Form 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 3rd 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 once item has been selected, and calculate the order total every time a new item is selected or unselected.

Problem 1: Create New Route for Ordering From a Restaurant

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 { TestBed, async, fakeAsync, tick, flush } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs'; 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

});

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

P1: What You Need to Know

  • How to create new components
    ng g component order
    
  • You've created routes before! You got this!

P1: Solution

src/app/app-routing.module.ts

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

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

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: 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/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" [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" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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 { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray, AbstractControl } from '@angular/forms';

import { RestaurantService } from '../restaurant/restaurant.service';
import { Restaurant } from '../restaurant/restaurant';
import { Subscription } from 'rxjs';

//CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM. 
function minLengthArray(min: number) {
  return (c: AbstractControl): {[key: string]: any} => {
      if (c.value.length >= min)
          return null;
      return { 'minLengthArray': {valid: false }};
  }
}

@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.less']
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm: FormGroup;
  restaurant: Restaurant;
  isLoading: boolean = true;
  items: FormArray;
  orderTotal: number = 0.0;
  completedOrder: any;
  orderComplete: boolean = false;
  orderProcessing: boolean = false;
  private subscription: Subscription;

  constructor(
    private route: ActivatedRoute, 
    private restaurantService: RestaurantService,
    private formBuilder: FormBuilder 
  ) { 
  }

  ngOnInit() {
    //GET THE RESTAURANT FROM THE ROUTE SLUG
    
  }

  ngOnDestroy(): void {
  }

  createOrderForm() {
    //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.group({
    });
    this.onChanges();
  }

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

  startNewOrder() {
    this.orderComplete = false;
    this.completedOrder = this.orderForm.value;
    //CLEAR THE ORDER FORM
    this.createOrderForm();
  }

}

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. Here's an example of a form with form controls with different validation, and one that's value is set to an array.

    function coolKidsChecker(isACoolKid: string) {
      return (c: AbstractControl): {[key: string]: any} => {
          if (c.value === isACoolKid)
              return null;
          return { 'coolKidsChecker': {valid: false }};
      }
    }
    
    this.myValidatingForm = this.formBuilder.group({
      aRequiredField: [null, Validators.required]
      aCustomRequiredField: [null, coolKidsChecker('yes')],
      anArrayField: [[]]
    });
    

P2: How to Verify Your Solution is Correct

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

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

import { OrderComponent } from './order.component';
import { ReactiveFormsModule } from '@angular/forms';
import { RestaurantService } from '../restaurant/restaurant.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"
      })
  }
}

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

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

  beforeEach(async(() => {
    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', () => {
    let 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"
    }
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant)
  });

  it('should have an orderForm formGroup', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    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 fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let itemFormControl = fixture.componentInstance.orderForm.controls.items;
    expect(itemFormControl.valid).toEqual(false);
  });

});

P2: Solution

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

import { Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray, AbstractControl } from '@angular/forms';

import { RestaurantService } from '../restaurant/restaurant.service';
import { Restaurant } from '../restaurant/restaurant';
import { Subscription } from 'rxjs';

//CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM. 
function minLengthArray(min: number) {
  return (c: AbstractControl): {[key: string]: any} => {
      if (c.value.length >= min)
          return null;
      return { 'minLengthArray': {valid: false }};
  }
}

@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.less']
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm: FormGroup;
  restaurant: Restaurant;
  isLoading: boolean = true;
  items: FormArray;
  orderTotal: number = 0.0;
  completedOrder: any;
  orderComplete: boolean = false;
  orderProcessing: boolean = false;
  private subscription: Subscription;

  constructor(
    private route: ActivatedRoute, 
    private restaurantService: RestaurantService,
    private formBuilder: FormBuilder 
  ) { 
  }

  ngOnInit() {
    //GETTING THE RESTAURANT FROM THE ROUTE SLUG
    const slug = this.route.snapshot.paramMap.get('slug');

    this.restaurantService.getRestaurant(slug).subscribe((data:Restaurant) => {
      this.restaurant = data;
      this.isLoading = false;      
      this.createOrderForm();
    })
  }

  ngOnDestroy(): void {
    if(this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  createOrderForm() {
    this.orderForm = this.formBuilder.group({
      restaurant: [this.restaurant._id],
      name: [null],
      address:  [null],
      phone: [null],
      //PASSING OUR CUSTOM VALIDATION FUNCTION TO THIS FORM CONTROL
      items: [[], minLengthArray(1)] 
    });
    this.onChanges();
  }

  onChanges() {
    // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL
    this.subscription = this.orderForm.get('items').valueChanges.subscribe(val => {
      let total = 0.0;
      if(val.length) {
        val.forEach((item: any) => {
          total += item.price;
        });
        this.orderTotal = Math.round(total * 100) / 100;
      }
      else {
        this.orderTotal = total;
      }
    });
  }

  startNewOrder() {
    this.orderComplete = false;
    this.completedOrder = this.orderForm.value;
    //CLEAR THE ORDER FORM
    this.createOrderForm();
  }

}

Importing 3rd 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:

✏️ Run:

ng add ngx-bootstrap  --component tabs

Ng add is a convenient way to import 3rd party libs that will update angular.json and package.json with any changes we need, as well as automatically import the 3rd party module into our root app module. Don't forget to restart the client server!

Your root app module should now look like this:

src/app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { ReactiveFormsModule } from '@angular/forms';
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,
    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" [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" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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

Problem 3: Create Custom 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: Technical Requirements

We want the menu-items component take an array of menu items and iterate through them in the template.

Each menu item should have this markup:

<li class="list-group-item" >
    <label>
        <input type="checkbox">
        ITEM_NAME <span class="badge">$ ITEM_PRICE</span>
    </label>
</li>

P3: Setup

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

✏️ Run:

ng g component order/menu-items

Go ahead and put your new component in the order history 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" [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" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs'; 

import { OrderComponent } from './order.component';
import { ReactiveFormsModule } from '@angular/forms';
import { RestaurantService } from '../restaurant/restaurant.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';

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(() => {
    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', () => {
    let 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"
    }
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant)
  });

  it('should have an orderForm formGroup', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    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 fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';

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

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

  beforeEach(async(() => {
    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', () => {
    const fixture = TestBed.createComponent(MenuItemsComponent);
    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;
    let itemLabels = compiled.getElementsByTagName('label');
    expect(itemLabels.length).toEqual(3)
  });
});

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 and @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 {
  public momName: string = '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: 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, OnInit, Input } from '@angular/core';

interface Item {
  name: string;
  price: number;
}

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrls: ['./menu-items.component.less']
})
export class MenuItemsComponent implements OnInit {
  @Input() items: Item[];

  constructor() { }

  ngOnInit() {
  }

}

✏️ 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" [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" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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, and update an array called selectedItems containing all checked items.

P4: Technical Requirements

Create a function in the MenuItemsComponent called updateItems that fires whenever a checkbox is checked and takes a parameter of the item that has been checked. In the updateItems function use the following code to update the selectedItems array:

 let index = this.selectedItems.indexOf(item);
  if(index > -1) {
    this.selectedItems.splice(index, 1);
  }
  else {
    this.selectedItems.push(item);
  }

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 {
  public  name = new FormControl('');

  constructor() {}

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

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

  willCallOnBlur(event) {
    console.log('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: 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';

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

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

  beforeEach(async(() => {
    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', () => {
    const fixture = TestBed.createComponent(MenuItemsComponent);
    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;
    let itemLabels = compiled.getElementsByTagName('label');
    expect(itemLabels.length).toEqual(3)
  });

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

P4: Solution

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

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

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

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

interface Item {
  name: string;
  price: number
}

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

  constructor() { }

  ngOnInit() {
  }

  updateItems(item: Item) {
    let index = this.selectedItems.indexOf(item);
    if(index > -1) {
      this.selectedItems.splice(index, 1);
    }
    else {
      this.selectedItems.push(item);
    }
  }

}

Problem 5: Update OrderFormComponent with selectedItems Array 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: Technical Requirements

Create an itemsChanged EventEmitter property that emits the selectedItems value every time it changes, and in the parent OrderForm component update the items FormControl with the value.

P5: How to Verify Your Solution is Correct

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

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

import { OrderComponent } from './order.component';
import { ReactiveFormsModule } from '@angular/forms';
import { RestaurantService } from '../restaurant/restaurant.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuItemsComponent } from './menu-items/menu-items.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(() => {
    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', () => {
    let 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"
    }
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant)
  });

  it('should have an orderForm formGroup', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    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 fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let itemFormControl = fixture.componentInstance.orderForm.controls.items;
    expect(itemFormControl.valid).toEqual(false);
  });

  it('should get updated selected Items when child component changes', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let changeSpy = spyOn(fixture.componentInstance, 'getChange');
    const compiled = fixture.debugElement.nativeElement;
    let childInput = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    childInput.click();
    expect(changeSpy).toHaveBeenCalled();
  });

  it('should update items FormControl when updateItems is called', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let childInput = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let 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', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let childInput1 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let childInput2 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[1];
    childInput1.click();
    childInput2.click();
    fixture.detectChanges();
    let orderText = compiled.querySelector('.submit h4');
    expect(orderText.textContent).toEqual('Total: $57.98')
  })

});

P5: What you need to know

Emitting Data to Parent Components

To pass data to parent components in Angular, the EventEmitter class is used in combination with the Output decorator. 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 {
  public message: string;
  constructor() {}

  ngOnInit() {

  }

  getMessage(ev) {
    this.message = ev;
  }
}

@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 {
  public childMessage = new FormControl('');
  @Output() sendMessage: EventEmitter<any> = new EventEmitter();
 
  constructor() {}

  ngOnInit() {

  }

  callToEmit() {
    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>


Programmatically Updating FormControl Values

When we have a formControl we need to update programmatically with a value we can use the patchValue method on the FormControl class. This method must be called on a FormControl instance and with a parameter of the new 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="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION, OnInit } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { ReactiveFormsModule, FormGroup, FormBuilder } = ng.forms;

@Component({
  selector: 'my-app',
  template: `
    <p>A home component!</p>
    <label>
      Custom input:
      <input type="text" (change)="customChange($event)">
    </label>
    
    <form [formGroup]="myQuickForm">
      <label>
        First name:
        <input type="text" formControlName="firstName">
      </label>
      <label>
        Last name:
        <input type="text" formControlName="lastName">
      </label>
      <label>
        Email:
        <input type="text" formControlName="email">
      </label>
    </form>
  `
})
class AppComponent implements OnInit {
  myQuickForm: FormGroup;

  constructor(private fb:FormBuilder) {}

  ngOnInit() {
    this.myQuickForm = this.fb.group({
      firstName: {value: '', disabled: false},
      lastName: {value: '', disabled: false},
      email: {value: '', disabled: false}
    });
  }

  customChange(ev) {
    let newVal = ev.currentTarget.value;
    let formControl = this.myQuickForm.get('firstName');
    formControl.patchValue(newVal);
  }
}
//THIS IS A HACK JUST FOR CODEPEN TO WORK
AppComponent.parameters = [FormBuilder];

@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>


  • How to emit a value to a parent component (you learned this in the section above! ✔️)
  • How to programmatically update a FormControl's value (you learned this in the section above! ✔️)

P5: Solution

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

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

interface Item {
  name: string;
  price: number
}

@Component({
  selector: 'pmo-menu-items',
  templateUrl: './menu-items.component.html',
  styleUrls: ['./menu-items.component.less']
})
export class MenuItemsComponent implements OnInit {
  @Input() items: Item[] = [];
  selectedItems: Item[] = [];
  @Output() itemsChanged: EventEmitter<any> = new EventEmitter();

  constructor() { }

  ngOnInit() {
  }

  updateItems(item: Item) {
    let index = this.selectedItems.indexOf(item);
    if(index > -1) {
      this.selectedItems.splice(index, 1);
    }
    else {
      this.selectedItems.push(item);
    }
    this.itemsChanged.emit(this.selectedItems);
  }

}

✏️ 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" [formGroup]="orderForm" (ngSubmit)="onSubmit()">
      <tabset>
        <tab heading="Lunch Menu">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.lunch" (itemsChanged)="getChange($event)" ></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner menu">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.dinner" (itemsChanged)="getChange($event)" ></pmo-menu-items>
          </ul>
        </tab>
      </tabset>
      <div class="form-group">
        <label class="control-label">Name:</label>
        <input name="name" type="text" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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 { ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, FormArray, AbstractControl } from '@angular/forms';

import { RestaurantService } from '../restaurant/restaurant.service';
import { Restaurant } from '../restaurant/restaurant';
import { Subscription } from 'rxjs';

//CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM. 
function minLengthArray(min: number) {
  return (c: AbstractControl): {[key: string]: any} => {
      if (c.value.length >= min)
          return null;
      return { 'minLengthArray': {valid: false }};
  }
}

@Component({
  selector: 'pmo-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.less']
})
export class OrderComponent implements OnInit, OnDestroy {
  orderForm: FormGroup;
  restaurant: Restaurant;
  isLoading: boolean = true;
  items: FormArray;
  orderTotal: number = 0.0;
  completedOrder: any;
  orderComplete: boolean = false;
  orderProcessing: boolean = false;
  private subscription: Subscription;

  constructor(
    private route: ActivatedRoute, 
    private restaurantService: RestaurantService,
    private formBuilder: FormBuilder
  ) { 
  }

  ngOnInit() {
    let slug = this.route.snapshot.paramMap.get('slug');
    this.restaurantService.getRestaurant(slug).subscribe((res:Restaurant) => {
      this.restaurant = res;
      this.createOrderForm();
    });    
  }

  ngOnDestroy(): void {
    if(this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  createOrderForm() {
    //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.group({
      restaurant: [{value: this.restaurant._id}],
      name: [''],
      address: [''],
      phone: [''],
      items: [[], minLengthArray(1)]
    });
    this.onChanges();
  }

  getChange(newItems: []) {
    let currentItems = this.orderForm.get('items').value;

    for (let i = 0; i < newItems.length; i++) {
      let item = newItems[i];
      let idx = currentItems.indexOf(item);
      if (idx === -1) {
        currentItems.push(item);
      }
      
    }
    this.orderForm.get('items').patchValue(newItems);
  }

  onChanges() {
    // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL
    this.subscription = this.orderForm.get('items').valueChanges.subscribe(val => {
      let total = 0.0;
      if(val.length) {
        val.forEach((item: any) => {
          total += item.price;
        });
        this.orderTotal = Math.round(total * 100) / 100;
      }
      else {
        this.orderTotal = total;
      }
    });
  }

  startNewOrder() {
    this.orderComplete = false;
    this.completedOrder = this.orderForm.value;
    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 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, Input, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

interface Item {
  name: string;
  price: number;
}

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

  constructor() { }

  @Input('value') _value: Item[];

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

  get value() {
    return this._value;
  }

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

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

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

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

  updateItems(item: Item) {
    let index = this._value.indexOf(item);
    if(index !== -1) {
      this._value.splice(index, 1)
    }
    else {
      this._value.push(item);
    }
    this.writeValue(this._value);
  }

}

Other concepts used here:

forwardRef

https://angular.io/api/core/forwardRef Used to reference a token that may not be defined when we need it.

NG_VALUE_ACCESSOR

https://angular.io/api/forms/NG_VALUE_ACCESSOR 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" [formGroup]="orderForm" (ngSubmit)="onSubmit()">
      <tabset>
        <tab heading="Lunch Menu">
          <ul class="list-group">
            <pmo-menu-items [items]="restaurant.menu.lunch" formControlName="items"></pmo-menu-items>
          </ul>
        </tab>
        <tab heading="Dinner menu">
          <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" class="form-control" formControlName="name">
        <p>Please enter your name.</p>
      </div>
      <div class="form-group">
        <label class="control-label">Address:</label>
        <input name="address" type="text" class="form-control" 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" class="form-control" 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs'; 

import { OrderComponent } from './order.component';
import { ReactiveFormsModule } from '@angular/forms';
import { RestaurantService } from '../restaurant/restaurant.service';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuItemsComponent } from './menu-items/menu-items.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(() => {
    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', () => {
    let 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"
    }
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurant).toEqual(mockRestaurant)
  });

  it('should have an orderForm formGroup', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    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 fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let itemFormControl = fixture.componentInstance.orderForm.controls.items;
    expect(itemFormControl.valid).toEqual(false);
  });

  it('should update items FormControl when setUpdatesItems is called', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let childInput = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let 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', () => {
    const fixture = TestBed.createComponent(OrderComponent);
    fixture.detectChanges();
    let compiled = fixture.debugElement.nativeElement;
    let childInput1 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[0];
    let childInput2 = compiled.getElementsByTagName('pmo-menu-items')[0].getElementsByTagName('input')[1];
    childInput1.click();
    childInput2.click();
    fixture.detectChanges();
    let orderText = compiled.querySelector('.submit h4');
    expect(orderText.textContent).toEqual('Total: $57.98')
  })

});