Creating a Pizza Ordering Form

Now that users have an account set up, they need the ability to order an actual pizza. Unlike the registration form, the order form has several parts that need to interact with each other. RxPizza wants to sell specialty pizzas and allow for trivial one-click ordering. Different sizes and toppings will change the price as the user fills out the form. We’ll need to subscribe to select parts of our form and update other values on the page (sound familiar?).

You’re already chomping at the bit to build this, so start off by generating the component with ng g component pizza-order and add a route to the route module:

 {
  path: ​'pizza'​,
  component: PizzaOrderComponent
 },

Let’s move on to the data model.

Building a Pizza Model

The core data model for our pizza form is thankfully simpler than the registration form. Add it to the component you just generated.

 // FormBuild can be instantiated outside of a component
 const​ _fb = ​new​ FormBuilder();
 class​ Pizza {
  size = ​'medium'​;
  toppings = _fb.group({
  pepperoni: ​false​,
  ham: ​false​,
  pineapple: ​false​,
  chicken: ​false​,
  hotSauce: ​false
  });
  cheese = ​'mozzarella'​;
 // For specialty pizzas
  name = ​''​;
 }

Instead of a plain JavaScript object, this data model is represented as a class. With this class, you can easily create new instances of the model, which will come in handy when adding new pizzas to the form.

Joe asks:
Joe asks:
Why Are Toppings Stored as an Object?

One would expect toppings to be stored as an array—after all, they’re a collection. However, the pizzaModel stores toppings as a key/value object. This may be a bit confusing on the JavaScript side of things, but once you take a look at the view, things start clearing up. Each topping is represented by a single true/false checkbox. We need to reference toppings directly, and it’s easier to do that with an object than constantly iterating through an array.

This model can be included in the root form group for the component, along with form controls for the address and credit card. The form will allow users to order multiple pizzas, so that’s represented by a FormArray.

 this​.pizzaForm = ​this​.fb.group({
  address: [​''​, Validators.required],
  creditCard: [​''​, Validators.required],
  pizzas: ​this​.fb.array([​this​.fb.group(​new​ Pizza())])
 });

Creating the Pizza View

The pizzaModel is where the meat of this form exists (as well as the dough, tomato sauce, and other toppings). It’s set up at the root as an array (who orders just one pizza?), so we’ll clone the formArray work from the previous form:

 get​ pizzas(): FormArray {
 return​ ​this​.pizzaForm.​get​(​'pizzas'​) ​as​ FormArray;
 }
 
 addPizza() {
 this​.pizzas.push(​this​.fb.group(​new​ Pizza()));
 }

Now that the boilerplate is out of the way, it’s time to start writing the HTML for this form. For the first step, insert the all-important headline and a form element linked to pizzaForm. Everything after this will go in the form element.

 <h1>Pizza Order Form</h1>
 <form ​[​formGroup​]="​pizzaForm​"​>
 </form>

Now on to the most important part—the pizza! This section of the form will iterate over all of the pizzas stored in the form (you’ve already initialized it with a single, empty pizza object).

 <div ​*​ngFor=​"let pizza of pizzas.controls; let i = index"
 [​formGroupName​]="​i​"​>
 <!-- TODO: specials -->
  <h3>Make it a Special</h3>
  <button ​*​ngFor=​"let s of specials"​ ​(​click​)="​selectSpecial​(​i​,​ s​)"​>
  {{ s.name }}
  </button>
  <h3>Size:</h3>
  <label>
  Small
  <input formControlName=​"size"​ type=​"radio"​ value=​"small"​>
  </label>
  <label>
  Medium
  <input formControlName=​"size"​ type=​"radio"​ value=​"medium"​>
  </label>
  <label>
  Large
  <input formControlName=​"size"​ type=​"radio"​ value=​"large"​>
  </label>
  <h3>Toppings:</h3>
 
  <div formGroupName=​"toppings"​>
  <span ​*​ngFor=​"let t of toppingNames"​>
  <label>
  {{ t }}
  <input type=​"checkbox"​ ​[​formControlName​]="​t​"​>
  </label>
  </span>
  </div>
  <button type=​"button"​ ​(​click​)="​pizzas​.​removeAt​(​i​)"​>
  Remove this Pizza</button>
  <hr>
  </div>
 </div>
 <button type=​"button"​ ​(​click​)="​addPizza​()"​>Add Pizza</button>

The root of this section iterates over all the pizza objects stored in the FormArray, creating a new subsection for each one. Inside are elements asking the user to select a size and toppings for their pizza. The attribute [formGroupName]="i" binds these select elements to an individual pizza.

Finally, a pair of buttons provide the option to add a new pizza or remove an existing one. These buttons work just like the buttons in the address section.

If your editor is smart enough, it may have noticed that something’s missing. Otherwise, you’ll see an error in the JavaScript console: toppingNames is used in the view, but can’t be found. Add it as a property on the component class:

 toppingNames = [
 'pepperoni'​,
 'ham'​,
 'pineapple'​,
 'chicken'​,
 'hotSauce'
 ];

Now that basic pizza selection is handled, the next step is to add pickers for the address and credit card.

Fetching Component Data in the Route

The address and credit card inputs are simplified to just dropdowns in this example, filled in with data fetched from the server. While you could add a few ajax calls in the component itself, Angular’s routing allows you to define the async data a component needs and load it alongside all of the other data that component needs. This can be used to gracefully handle errors and redirect as the component loads.

To fetch custom data during the routing, you need to create a special type of service called a Resolver. A resolver is just a fancy name for a service with a resolve method that gets called when Angular needs to route to a given page. Our resolve method will make two AJAX calls and return an observable of the resulting data. Generate a service with ng g service user-detail-resolver and fill in the details with the following:

 import​ { Injectable } ​from​ ​'@angular/core'​;
 import​ {
  Router, Resolve
 } ​from​ ​'@angular/router'​;
 import​ { combineLatest } ​from​ ​'rxjs'​;
 import​ { ajax } ​from​ ​'rxjs/ajax'​;
 import​ { pluck, map } ​from​ ​'rxjs/operators'​;
 
 @Injectable()
export​ ​class​ UserDetailResolver ​implements​ Resolve<any> {
 constructor​() { }
 
  resolve() {
 return​ combineLatest(
  ajax(​'http://localhost:3000/ng2/reactiveForms/userData/addresses'​)
  .pipe(pluck(​'response'​)),
  ajax(​'http://localhost:3000/ng2/reactiveForms/userData/creditCards'​)
  .pipe(pluck(​'response'​))
  )
  .pipe(
  map(responses => {
 return​ {
  addresses: responses[0],
  creditCards: responses[1]
  };
  })
  );
  }
 }

The snippet implements Resolve<any> tells us two things. One, that this class is intended to be used any place a resolver should be (if it’s used outside of the router, something’s wrong). Second, the <any> can be used to indicate the type of data the resolver will return. In this case, it’s not important, so any is used to declare that the resolver could return any type of data (or nothing at all).

Once that’s done, you can create a route for the pizza ordering component and include the resolve parameter:

 {
  path: ​'pizza'​,
  component: PizzaOrderComponent,
  resolve: {
  userDetails: UserDetailResolver
  }
 },

Finally, add private route: ActivatedRoute to the constructor of the pizza form component and listen in to the result of resolver in ngOnInit. You also need to add a userDetails: any to the top of the component class.

 this​.route.data
 .subscribe(data => {
 this​.userDetails = data.userDetails;
 });

Adding Reactive Selectors

Thankfully, dropdown selectors are as simple as can be with reactive forms. Bind to the form control as usual, and iterate over the data provided in userDetails to create an <option> element for each one.

 <div>
  <h3>Payment</h3>
  <select formControlName=​"creditCard"​>
  <option ​*​ngFor=​"let card of userDetails.creditCards"​ ​[​ngValue​]="​card​"​>
  {{ card }}
  </option>
  </select>
 </div>

Use the same pattern to add an address selector to the form.

Reacting to Change

At some point, the customer needs to know how much all these pizzas will cost them. Observables let us react to user input to keep an updated total price on the page, but how do we recalculate the price only when relevant information is updated? We know the entire form can be subscribed to with .valueChanges—and that’s what we’ll use for the individual form controls as well. We can extract the properties of the form with this.pizzaForm.get(nameOfProperty). We’ll need an observable stream of all changes to pizzas, mapped through a function that calculates the total cost:

 this​.price$ = ​this​.pizzaForm.​get​(​'pizzas'​).valueChanges
 .pipe(
  map((pizzaList: any[]) => pizzaList
  .reduce((total, pizza) => {
 let​ price;
 switch​ (pizza.size) {
 case​ ​'small'​: price = 10; ​break​;
 case​ ​'medium'​: price = 14; ​break​;
 case​ ​'large'​: price = 19; ​break​;
  }
 
 const​ numToppings = ​this​.toppingNames
  .filter(x => pizza.toppings[x]).length;
  price += (numToppings * 0.5);
 return​ total + price;
  }, 0)
  )
 );

Inside calculatePrice, we implement the cost logic, charging a base price for pizza size and adding $0.50 for each topping. Remember, toppings is an object, so we need to do a bit of fancy logic to determine just how many toppings the user has selected.

Now the form should display the latest price to the user using the async pipe you learned about before:

 <div>
  <h3>Price: ${{ price$ | async }}</h3>
  <button type=​"button"​>Submit</button>
 </div>

Specialty Pizzas

Now we have a relatively functional (if plain) order form. Time to zest that up with some specials. In our case, the specials will be pre-configured collections of toppings (nothing too crazy). This also let’s the user customize the specials (maybe you’re not a fan of hot sauce, but love everything else about buffalo chicken). The traditional form story doesn’t provide much comfort here, but through patchValue, reactive forms make this easy.

First, let’s set up two specials for today—Hawaiian and Buffalo Chicken. Add the following code as a property declaration on the component class:

 specials = [{
  name: ​'Buffalo Chicken'​,
  toppings: {
  pepperoni: ​false​,
  ham: ​false​,
  pineapple: ​false​,
  chicken: ​true​,
  hotSauce: ​true
  }
 }, {
  name: ​'Hawaiian'​,
  toppings: {
  pepperoni: ​false​,
  ham: ​true​,
  pineapple: ​true​,
  chicken: ​false​,
  hotSauce: ​false
  }
 }];

Both of these are partial models—that is, they define only part of a pizza model. A special shouldn’t define the size of a pizza, so we’ll leave that up to whatever the user chooses. The view is simple—just iterate over the specials and display a button for each. Add the following where the <!-- TODO: specials --> comment is:

 <h3>Make it a Special</h3>
 <button ​*​ngFor=​"let s of specials"​ ​(​click​)="​selectSpecial​(​i​,​ s​)"​>
  {{ s.name }}
 </button>

The selectSpecial method is trivial to implement—it’s just a wrapper around patchValue:

 selectSpecial(pizzaIndex, special) {
 this​.pizzas.controls[pizzaIndex].patchValue(special);
 }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.26.221