Jerry's Blog

Recording what I learned everyday

View on GitHub


30 June 2019

Angular (17) -- Reactive Forms

by Jerry Zhang

Day 25: Reactive Forms

Setup

  1. Create an attribute of type FormGroup.
  2. Import ReactiveFormsModule in the app.module.

Create a new form with typescript

Create a new FormGroup and some FormControl

ngOnInit() {
    this.signupForm = new FormGroup({
      'username': new FormControl(null),
      'email': new FormControl(null),
      'gender': new FormControl('male')
    });
  }

Connect the HTML form with our signupForm

<form [formGroup]="signupForm">
    <input
        type="text"
        id="username3"
        formControlName="username"
        class="form-control">

First, we bind the formGroup property with the form.

Second, we specify the formControlName as the same name in the typescript file.

Submitting the Form

To submit the form, we still need to bind the ngSubmit event with the form like before.

<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">

However, this time, we do not need a reference to our form, because we have created the form in the typescript file. To access it, simply use the created attribute.

onSubmit() {
    console.log(this.signupForm);
}

Validation

In the Template-driven approach, we added a required on the input. This does not work in the reactive approach. Instead, we add a second parameter in the FormControl constructor.

ngOnInit() {
    this.signupForm = new FormGroup({
      'username': new FormControl(null, Validators.required),
      'email': new FormControl(null, [Validators.required, Validators.email]),
      'gender': new FormControl('male')
    });
  }

Getting Access to Controls

We use the signupForm attribute to get access to the form directly.

<span
*ngIf="!signupForm.get('username').valid && signupForm.get('username').touched"
class="help-block">Please enter a valid username!</span>

<span
*ngIf="!signupForm.get('email').valid && signupForm.get('email').touched"
class="help-block">Please enter a valid email!</span>

<span
*ngIf="!signupForm.valid && signupForm.touched"
class="help-block">Please enter valid data!</span>

Grouping Controls

We can put a FormGroup inside a FormGroup, and then put form controls in it.

ngOnInit() {
    this.signupForm = new FormGroup({
      'userData': new FormGroup({
        'username': new FormControl(null, Validators.required),
        'email': new FormControl(null, [Validators.required, Validators.email]),
      }),
      'gender': new FormControl('male')
    });
  }

To update the corresponding html file, we need to wrap the two controls in another div, and add a directive formGroupName.

<div formGroupName="userData">

Further more, we have to pass the path in the get method

<span
  *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched"
  class="help-block">Please enter a valid username!</span>

FormArray

Create a new button. Make the type button so that it does not accidentally submit the form.

<button class="btn btn-light" type="button">Add Hobby</button>

Then, when the user clicks this button, I want to dynamically add a control to my form.

<button
    class="btn btn-light"
    type="button"
    (click)="onAddHobby()">Add Hobby</button>
ngOnInit() {
    this.signupForm = new FormGroup({
      'userData': new FormGroup({
        'username': new FormControl(null, Validators.required),
        'email': new FormControl(null, [Validators.required, Validators.email]),
      }),
      'gender': new FormControl('male'),
      'hobbies': new FormArray([])
    });
  }
onAddHobby() {
    const control = new FormControl(null, Validators.required);
    (this.signupForm.get('hobbies') as FormArray).push(control);
  }
<div formArrayName="hobbies">
  <h4>Your Hobbies</h4>
  <button
    class="btn btn-light"
    type="button"
    (click)="onAddHobby()">Add Hobby</button>
  <div
    class="form-group"
    *ngFor="let hobbyControl of getHobbies(); let i = index">
    <input type="text" class="form-control" [formControlName]="i">
  </div>
</div>
getHobbies() {
    return (this.signupForm.get('hobbies') as FormArray).controls;
  }

Notice that we need to specify the formArrayName in the outside div.

Also notice that we need to get the index for a temporary name of the newly created control.

Create Custom Validators

For example, we want to forbidden some specific user names.

forbiddenUsernames = ['Chris', 'Anna'];
forbiddenNames(control: FormControl): {[s: string]: boolean} {
    if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
      return {'nameIsForbidden': true};
    }
    return null;
  }

A validator should return an Object if it is valid, or null if it is invalid.

'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),

Using Error Codes

<span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">This name is invalid!</span>
<span *ngIf="signupForm.get('userData.username').errors['required']">This field is required!</span>

Creating a Custom Async Validator

forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise<any>((resolve, reject) => {
      setTimeout(() => {
        if (control.value === 'test@test.com') {
          resolve({'emailIsForbidden': true});
        } else {
          resolve(null);
        }
      }, 1500);
    });
    return promise;
  }
'email': new FormControl(null, [Validators.required, Validators.email], this.forbiddenEmails),

Reacting to status or value changes

 this.signupForm.valueChanges.subscribe(
   (value) => console.log(value)
 );
this.signupForm.statusChanges.subscribe(
  (status) => console.log(status)
);

Setting, Patching, and Resetting Values

this.signupForm.setValue({
      'userData': {
        'username': 'Max',
        'email': 'sdf@test.com'
      },
      'gender': 'male',
      'hobbies': []
    });
    this.signupForm.patchValue({
      'userData': {
        'username': 'Anna'
      }
    });
this.signupForm.reset();

Do not forget to add ReactiveFormModule in the app.module!!!

Pattern validator

<input
type="number"
id="amount"
class="form-control"
name="amount"
ngModel
required
pattern="^[1-9]+[0-9]*$"
>
tags: Angular