Using Advanced Validation

Form validation is more than just ensuring the user entered something into each element. It can be a cruical asset for users on phones (where a contextual keyboard will pop up on the phone), folks who use screen readers, or people who are just in a hurry and don’t notice minor mistakes. In all of these cases, having the form pop up reminders, as soon as it knows what’s wrong, will be a boon for the user. Let’s dive into validation beyond a basic phone number and brainstorm a few rules about our form.

Inline Error Messages

images/aside-icons/note.png For the remainder of this chapter, I’ll skip adding the error messages to the form to keep focus on the topic of the section. The CSS you added in the previous section still applies here, so you can easily tell whether an input is invalid or not.

Validating Account Information

The first section of the form involves all the basic account information (username, email, phone, password). You could probably write out all the rules for this form in your sleep, but why do that, when we can just borrow from the Angular team?

The first element in the form is the username. There’ll be the standard set of validations for any username—a minimum and maximum length, a character whitelist, and an asynchronous validator ensuring the username hasn’t been taken yet. If you want to ensure this async functionality works, the three usernames already in the system are rkoutnik, taken, anotheruser.

 username: [​''​, [
  Validators.required,
  Validators.maxLength(20),
  Validators.minLength(5)
 ],
  [(control) => {
 return​ ​this​.http.​get​(​this​.endpoint + ​'reactiveForms/usernameCheck/'
  + control.value)
  .pipe(
  map((res: any) => {
 if​ (res.taken) {
 return​ { usernameTaken: ​true​ };
  }
  })
  );
  }]
 ],

The next item, email, is so standard that Angular has a built-in validator:

 email: [​''​, [
  Validators.required,
  Validators.email
 ]],

There’s no built-in tool for validating a phone number, but we can use Validators.pattern to ensure our element passes a regex test. In this case we’re just checking for a valid U.S. phone number with the pattern of 123-456-7890 for simplicity, as more complicated regexes are outside the scope of this book. For bonus points, reuse the phone number validator you wrote in the previous section.

 phoneNumber: [​''​, [
  Validators.required,
  Validators.pattern(​/^​​[​​1-9​​]d{2}​​-​​d{3}​​-​​d{4}​​/​)
 ]],

The final pair of items in the signup form asks for a password and confirmation of that password. This is the first time in form validation that we’ve had to consider the state of the form outside of an individual element. First, we need to ensure the password meets our length and complexity requirements.

 password: [​''​, [
  Validators.required,
  Validators.minLength(12),
  (ac: AbstractControl) => {
 const​ currentVal: string = ac.value;
 // Password must contain at least three of the four options
 // Uppercase, lowercase, number, special symbol
 let​ matches = 0;
 if​ (currentVal.match(​/​​[​​A-Z​​]​​+/​)) {
  matches++;
  }
 if​ (currentVal.match(​/​​[​​a-z​​]​​+/​)) {
  matches++;
  }
 if​ (currentVal.match(​/​​d​​+/​)) {
  matches++;
  }
 if​ (currentVal.replace(​/​​[​​A-Za-z0-9​​]​​/g​, ​''​)) {
  matches++;
  }
 if​ (matches < 3) {
 return​ { passwordComplexityFailed: ​true​ };
  }
  }
 ]],
 confirmPassword: [​''​, [
  Validators.required
 ]],

If we want to add global validators to our form, we can’t add them element-by-element. Instead, fb.group takes a second parameter, an object with two properties: validator and asyncValidator. This is where we add validation logic that requires checking multiple elements at once. Each value is a function that takes an abstract control representing the entire form.

 validator: (ac: AbstractControl) => {
 const​ pw = ac.​get​(​'password'​).value;
 const​ cpw = ac.​get​(​'confirmPassword'​).value;
 if​ (pw !== cpw) {
  ac.​get​(​'confirmPassword'​).setErrors({passwordMismatch: ​true​});
  }
 }

Validating an Address

The address section has two layers of validation. First, each item needs to have its own individual set of validators, but the address as a whole needs to be checked against the backend to ensure that it’s a valid address. The individual validations are simple:

 const​ addressModel = {
  street: [​''​, Validators.required],
  apartment: [​''​],
  city: [​''​, Validators.required],
  state: [​''​, Validators.required],
  zip: [​''​, [
  Validators.required,
  Validators.pattern(​/​​d{5}​​/​)
  ]]
 };

The backend check assembles the current value of the entire form and checks it against the backend. Async validators only run when all of the synchronous validators pass, so we don’t need to worry about sending a half-completed address to the backend. We also add in a debounceTime operator to keep the overall number of requests low.

 const​ checkAddress = (control: AbstractControl) => {
 const​ address = {
  street: control.​get​(​'street'​).value,
  apartment: control.​get​(​'apartment'​).value,
  city: control.​get​(​'city'​).value,
  state: control.​get​(​'state'​).value,
  zip: control.​get​(​'zip'​).value
  };
 return​ ​this​.http.​get​(​this​.endpoint + ​'reactiveForms/addressCheck/'​ + address)
  .pipe(
  debounceTime(333),
  map((res: any) => {
 if​ (!res.validAddress) {
 return​ { invalidAddress: ​true​ };
  }
  })
  );
 };

Finally, the address subform is attached to the main registration form:

 addresses: ​this​.fb.array([
 this​.fb.group(addressModel, {
  asyncValidator: checkAddress
  })
 ]),

Validating a Credit Card

The credit card section contains our first bit of complicated, custom validation. All credit card numbers follow the Luhn algorithm[7] for validation, which works like so:

  • Double the second digit from the right, and every other digit after that (working from right to left). If the result of the doubling is greater than nine, add the digits together.

  • Take the sum of all the resulting digits.

  • If the final result is divisible by 10, the credit card is valid.

While you can’t ever be sure a card is valid without first checking it with your payment processor, simple checks like the Luhn algorithm help catch errors where a customer might have accidentally entered a typo.

 const​ ccModel = {
  cc: [​''​, [
  Validators.required,
  (ac: AbstractControl) => {
 // Convert string to array of digits
 const​ ccArr: number[] = ac.value.split(​''​).map(digit => Number(digit));
 // double every other digit, starting from the right
 let​ shouldDouble = ​false​;
 const​ sum = ccArr.reduceRight((accumulator, item) => {
 if​ (shouldDouble) {
  item = item * 2;
 // sum the digits, tens digit will always be one
 if​ (item > 9) {
  item = 1 + (item % 10);
  }
  }
  shouldDouble = !shouldDouble;
 return​ accumulator + item;
  }, 0);
 
 if​ (sum % 10 !== 0) {
 return​ { ccInvalid: ​true​ };
  }
  }
  ]],
  cvc: [​''​, Validators.required],
  expirationMonth: [​''​, [
  Validators.required,
  Validators.min(1),
  Validators.max(12)
  ]],
  expirationYear: [​''​, [
  Validators.required,
  Validators.min((​new​ Date()).getFullYear())
  ]]
 };

This is only the penultimate step on our journey of creating a form group—the final step is to collect all of these requirements together into a data model for your form.

Connecting the Model to the View

At a certain abstract level, any form has a data model attached to it, describing the properties of the form and the values they contain. For most forms, the parts of this data model are scattered around the view, several JavaScript files, and sometimes even partially stored on the backend. Making changes to the form involves checking for conflicts at all layers. One little mistake, and now you’ve brought prod down (or worse, prod’s still up but you’re capturing the wrong data).

Reactive forms instead use the concept of a central, class-based data model. Everything in the form is routed through this model—even to the point of throwing errors when you try to modify properties that don’t exist on the model (a huge and welcome change from JavaScript’s typical stance of silently allowing such changes, leading to developers thinking everything’s OK). Data models are based on plain old JavaScript objects. At its simplest, a model for the registration form would look like this:

 {
  username: [​''​],
  phoneNumber: [​''​],
  password: [​''​],
  confirmPassword: [​''​],
  address: {
  street: [​''​],
  apartment: [​''​],
  city: [​''​],
  state: [​''​],
  zip: [​''​]
  },
  creditCard: {
  cc: [​''​],
  cvc: [​''​],
  expirationMonth: [​''​],
  expirationYear: [​''​]
  }
 }

The entire form is defined in this one object, showing the requirements for each element and how subgroups relate to each other. Angular uses this definition to validate the view—if ever there’s an input that attempts to connect to a property that doesn’t exist in the model, you will get an error. You have already built most of the data model by building out the validators and nested formGroups. Now let’s link that model to the view and add a way to save the whole thing.

First, we’ll deliberately break the view to demonstrate how the validation provided by reactive forms can save us from ourselves. Update the registration-form.component.html to be a form element with one input. [formGroup]="registrationForm" will hook up the form to the form element. The input is given a form control that doesn’t exist on registrationForm:

 <form ​[​formGroup​]="​registrationForm​"​>
  <input formControlName=​"notReal"​>
 </form>

If your editor’s smart enough, it might catch the error here. If not, open the page, and the console should have something like the screenshot.

images/missingFormControl.png

Presto! Angular has already figured out there’s an error with our form and alerted us. Now that we’ve proven that Angular’s aware of when we do the wrong thing, let’s fill out the rest of the form inputs. First is the section containing all the formControls that aren’t part of a subgroup:

 <form ​[​formGroup​]="​registrationForm​"​>
  <label>Username:
  <input formControlName=​"username"​>
  </label>
  <label>Phone Number:
  <input formControlName=​"phoneNumber"​>
  </label>
  <label>Password:
  <input formControlName=​"password"​ type=​"password"​>
  </label>
  <label>Confirm Password:
  <input formControlName=​"confirmPassword"​ type=​"password"​>
  </label>
 </form>

There is nothing terribly new here, but do not forget those type="password" attributes.

Next up is the address section. First, add a convenience helper to the controller to get the addresses attribute::

 get​ addresses() {
 return​ ​this​.registrationForm.​get​(​'addresses'​) ​as​ FormArray;
 }
 addAddress() {
 this​.addresses.push(​this​.fb.group(addressModel));
 }

This addresses isn’t a regular method. Rather, it defines what happens whenever anything tries to access the addresses property of this component. This is necessary because we want to pull the addresses property off of the registrationForm using the getter, but that getter returns an AbstractControl. To take full advantage of the type hinting provided by Angular, the as FormArray is required. After you add the as FormArray, the rest of the component and view can access the form array without any worry.

The address is a nested formGroup, so the view uses a nested <form> element to represent that. It’s not connected by binding the [formGroup] property. Rather, the directive formGroupName is used to indicate that this form tag relates to a subgroup. Inside the form tag, you refer to each form control directly (no need to add address.whatever to each input):

 <form ​[​formGroup​]="​registrationForm​"​>
 <!-- Previously-created inputs hidden -->
  <form formGroupName=​"address"​>
  <label>Street:
  <input formControlName=​"street"​>
  </label>
  <label>Apartment (optional):
  <input formControlName=​"apartment"​>
  </label>
  <label>City:
  <input formControlName=​"city"​>
  </label>
  <label>State:
  <input formControlName=​"state"​>
  </label>
  <label>Zip:
  <input formControlName=​"zip"​>
  </label>
  </form>
 </form>

That’s the address section settled. Use the same technique to repeat this process on your own for the credit card section, using the same technique.

At this point, we have a fully functional pizza signup form. Time to add some fancy features. First of all, this is a rather large form. If the user filled it out, then hit a network error upon submitting and need to refresh the page, they’d be pretty frustrated. Let’s subscribe to all changes on the form and save them to localStorage. Add the following to the end of the ngOnInit call:

 this​.registrationForm.valueChanges
 .subscribe(newForm => {
  window.localStorage.registrationForm = JSON.stringify(newForm);
 });

This snippet doesn’t change anything from the user’s perspective. Once the form’s saved, we need to restore up to the previous state on component init. To do that, you need to know how to programatically modify a given form group.

Updating Form Groups

Any FormGroup object has two methods that let us update the value of the form in bulk: setValue and patchValue. Both take an object and update the value of the form to match the properties of that object. setValue is the stricter of the two—the object passed in must exactly map to the object definition passed into form builder (it does not need to be recreated as FormControls, however). For example:

 let​ myForm = fb.group({
  name: ​''​,
  favoriteFood: ​''
 });
 
 // This fails, we need to also provide favoriteFood
 myForm.setValue({ name: ​'Randall'​ });
 
 // This works
 myForm.setValue({
  name: ​'Randall'​,
  favoriteFood: ​'pizza'
 });

The patchValue method doesn’t care if the object passed in matches the form’s requirements. Properties that don’t match are ignored. In general, if you want to update part of a form or use an object that has superfluous properties, go with patchValue. If you have a representation of the form, use the superior error-checking of setValue. In the localStorage case, we have a representation of the form, so we grab the latest from localStorage and update the form with setValue. This snippet goes right above the subscribe call you added earlier:

 if​ (window.localStorage.registrationForm) {
 this​.registrationForm.setValue(
  JSON.parse(window.localStorage.registrationForm));
 }

Fill out part of the form and refresh the page. The parts you filled out should still be there. The form will survive through any network disaster.

Submitting the Form

One last thing for this section—the user needs to be able to save the form (and clear out that saved state). Add a save method to the controller and an accompanying button to the view:

 save() {
 return​ ​this​.http.post(​this​.endpoint + ​'reactiveForms/user/save'​,
 this​.registrationForm.value)
  .subscribe(
  next => window.localStorage.registrationForm = ​''​,
  err => console.log(err),
  () => console.log(​'done'​)
  );
 }

The save method looks at the invalid property of the form (there’s also an accompanying valid property) and enables the button only when the form is fully valid, to prevent accidental submissions.

 <button
  class=​"btn btn-default"
 (​click​)="​save​()"
 [​disabled​]="​registrationForm​.​invalid​"
 >Save</button>

The save method also resets the locally saved form value only when the save is successful. However, if the form’s invalid, the user won’t be able to submit it until the problems are corrected.

Now that the main thrust of the form has been completed, you can add more advanced features, such as allowing the user to input an arbitrary number of addresses.

Handling Multiple Addresses

This form is all well and good for users who use exactly one credit card and never leave the house. If we want to expand our target market beyond such a select group, the form needs to accomodate multiple inputs. The tool we use for this is FormArray, which represents a collection of FormGroups. First, take the extracted address model definition:

 const​ addressModel = {
  street: [​''​, Validators.required],
  apartment: [​''​],
  city: [​''​, Validators.required],
  state: [​''​, Validators.required],
  zip: [​''​, [
  Validators.required,
  Validators.pattern(​/​​d{5}​​/​)
  ]]
 };

Then update the actual declaration of addressForm to build a formArray, with a default of a single address:

 addresses: ​this​.fb.array([
 this​.fb.group(addressModel, {
  asyncValidator: checkAddress
  })
 ]),

Now we need to figure out how to iterate over a formArray in the model. Counterintuitively, the formArray actually isn’t an array. To access the collection of form controls, we use the controls property. Inside that iteration, we create a form element for each item in the address collection. The form’s formGroupName is set by index, because each formGroup doesn’t have a specific name.

 <h3>Addresses</h3>
 <div formArrayName=​"addresses"​ ​*​ngFor=​"let addr of addresses.controls;
  let i = index"​>
  <form ​[​formGroupName​]="​i​"​>
  <div class=​"form-group"​>
  <label>Street:
  <input class=​"form-control"​ formControlName=​"street"​>
  </label>
  </div>
  <div class=​"form-group"​>
  <label>Apartment (optional):
  <input class=​"form-control"​ formControlName=​"apartment"​>
  </label>
  </div>
  <div class=​"form-group"​>
  <label>City:
  <input class=​"form-control"​ formControlName=​"city"​>
  </label>
  </div>
  <div class=​"form-group"​>
  <label>State:
  <input class=​"form-control"​ formControlName=​"state"​>
  </label>
  </div>
  <div class=​"form-group"​>
  <label>Zip:
  <input class=​"form-control"​ formControlName=​"zip"​>
  </label>
  </div>
  <button
  type=​"button"
 [​disabled​]="​addresses​.​controls​.​length =​==​ 1​"
  class=​"btn btn-default"
 (​click​)="​addresses​.​removeAt​(​i​)"
  >Remove</button>
  <hr>
  </form>
 </div>
 <button type=​"button"​ class=​"btn btn-default"​ ​(​click​)="​addAddress​()"​>
  Add Address</button>

The Remove button calls the handy method removeAt to remove whatever address is stored at that index. At least one address is required, so the button is disabled, unless there are multiple addresses already. The Add button will require us to modify the component class. Add another method to the component that creates a new (empty) address and adds it to the form group:

 get​ addresses() {
 return​ ​this​.registrationForm.​get​(​'addresses'​) ​as​ FormArray;
 }
 addAddress() {
 this​.addresses.push(​this​.fb.group(addressModel));
 }

This method takes advantage of the getter defined earlier. In this case, it adds a new, empty address to the form. Unfortunately, FormBuilder is not built into FormArray’s push method, so we need to convert the address model into a group before passing it in.

When these two methods have been added to the controller, your registration form is complete. Click through to make sure everything works. When you’re satisfied with the registration form, it’s time to dig into ordering a pizza.

..................Content has been hidden....................

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