Handling Forms in Angular Apps - Reactive Approach
Reactive: setup
in app.component import FormGroup and add property of type FormGroup
    import { Component } from '@angular/core';
    import { FormGroup } from '@angular/forms';

    @Component({
      ...
    })
    export class AppComponent {
      genders = ['male', 'female'];
      signupForm: FormGroup;
    }
        
n app.module import ReactiveFormsModule and add it to the imports property of NgModule
    import { BrowserModule } from '@angular/platform-browser';
    import { NgModule } from '@angular/core';
    import { ReactiveFormsModule } from '@angular/forms';
    import { HttpModule } from '@angular/http';

    import { AppComponent } from './app.component';

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

Top

Index

Reactive: creating a form in code
in app.component import OnInit
in ngOnInit instantiate the FormGroup property, the c'tor arg is a JSON object
import FormControl
add a property named 'username' to the JSON object and instantiate it as a FormControl
FormControl's c'tor arguments
  1. initial value of form
  2. array of validators to be applied to the control (optional)
  3. array of async validators to be applied to the control (optional)
add properties for email and gender setting the gender's default value to male
    import { Component, OnInit } from '@angular/core';
    import { FormControl, FormGroup } from '@angular/forms';

    @Component({
      ...
    })
    export class AppComponent implements OnInit{
      genders = ['male', 'female'];
      signupForm: FormGroup;

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

Top

Index

Reactive: syncing HTML and form
add directives to markup form tag
use property binding with FormGroup referencing the FormGroup created programmatically
add formControlName directive to the inputs in the markup with the control name fron the code
    <form [formGroup]="signupForm">
        <div class="form-group">
            <label for="username">Username</label>
            <input
            type="text"
            id="username"
            formControlName="username"
            class="form-control">
        </div>
        <div class="form-group">
            <label for="email">email</label>
            <input
            type="text"
            id="email"
            formControlName="email"
            class="form-control">
        </div>
        <div class="radio" *ngFor="let gender of genders">
            <label>
            <input
                type="radio"
                formControlName="gender"
                [value]="gender">{{ gender }}
            </label>
        </div>
        <button class="btn btn-primary" type="submit">Submit</button>
    </form>
        

Top

Index

Reactive: submitting the form
in the markup bind the form's ngSubmit property to an event handler
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        ...
    </form>
        
in the code add the event handler
    ...
    export class AppComponent implements OnInit {
      ...
      signupForm: FormGroup;

      ngOnInit() {
        this.signupForm = new FormGroup({
          'username': new FormControl(null),
          'email': new FormControl(null),
          'gender': new FormControl('male')
        });
      }
      onSubmit() {
        console.log(this.signupForm);
      }
    }  
        
no local references are needed in the markup because the HTML is synchronized with with the form created programmatically

Top

Index

Reactive: adding validation
import Validators
add required validator as username control's c'tor second arg
can also pass an array of Validators to the email control add the required and email validators
    import { Component, OnInit } from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    ...
    export class AppComponent implements OnInit {
      ...
      ngOnInit() {
        this.signupForm = new FormGroup({
          'username': new FormControl(null, Validators.required),
          'email': new FormControl(null, [Validators.email, Validators.required]),
          'gender': new FormControl('male')
        });
      }
      ...
    }  
        

Top

Index

Reactive: getting access to controls
the form has a get method which takes a string as an arg
the string is the control name or the path & control name
use the method and ngIf to display/hide error messages
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div class="form-group">
            <label for="username">Username</label>
            <input type="text" id="username" formControlName="username" class="form-control">
            <span 
                *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched" 
                class="help-block">
                Please enter a valid username.
            </span>
        </div>
        <div class="form-group">
            <label for="email">email</label>
            <input type="text" id="email" formControlName="email" class="form-control">
            <span 
                *ngIf="!signupForm.get('email').valid && signupForm.get('email').touched" 
                class="help-block">
                Please enter a email address.
            </span>
        </div>
        <div class="radio" *ngFor="let gender of genders">
            <label>
            <input
                type="radio"
                formControlName="gender"
                [value]="gender">{{ gender }}
            </label>
        </div>
        <span 
            *ngIf="!signupForm.valid && signupForm.touched" 
            class="help-block">
            Please enter valid data.
            </span> 
    </form>
    <button class="btn btn-primary" type="submit">Submit</button>
        
edit the css adding a class which turns the inputs' border red when the control has been touched and is not valid
    .container {
      margin-top: 30px;
    }

    input.ng-invalid.ng-touched {
      border: 1px solid red;
    } 
        

Top

Index

Reactive: grouping controls
in app.component's ngOnInit method add a FormGroup and nest the username and email FormControls within
    ...
    export class AppComponent implements OnInit {
      ...

      ngOnInit() {
        this.signupForm = new FormGroup({
          'userData': new FormGroup({
            'username': new FormControl(null, Validators.required),
            'email': new FormControl(null, [Validators.email, Validators.required])       
          }),
          'gender': new FormControl('male')
        });
      }
      ...
    }
        
in the app.component markup add a div with the formControlName attribute assigned to the name of the new FormGroup
move the username and email form-groups inside the new div
app will fail because the ngIf statements are unable to locate the username and email controls
to add the path to the ngIf statements use the dot operator prepended with the value assigned to the formControlName attribute of the new div
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userData">
            <div class="form-group">
                <label for="username">Username</label>
                <input type="text" id="username" formControlName="username" class="form-control">
                <span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched" class="help-block">Please enter a valid username.</span>
            </div>
            <div class="form-group">
                <label for="email">email</label>
                <input type="text" id="email" formControlName="email" class="form-control">
                <span *ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched" class="help-block">Please enter a email address.</span>
            </div>
        </div>
    ...
    </form>
        

Top

Index

Reactive: arrays of form controls (FormArray)
in app.component's ngOnInit below the gender control add an object named hobbies of type FormArray
the FormArray's c'tor takes an array as an arg so pass an empty array
add a method named onAddHobby which creates a new FormControl and pushes the control into the hobbies array
note the cast
    import { Component, OnInit } from '@angular/core';
    import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
    ...
    export class AppComponent implements OnInit {
      ...
      ngOnInit() {
        this.signupForm = new FormGroup({
          'userData': new FormGroup({
            'username': new FormControl(null, Validators.required),
            'email': new FormControl(null, [Validators.email, Validators.required])       
          }),
          'gender': new FormControl('male'),
          'hobbies': new FormArray([])
        });
      }
      onAddHobby() {
         const control = new FormControl(null, Validators.required);
         // cast object to FormArray before pushing control to array
         (<FormArray>this.signupForm.get('hobbies')).push(control);
      }
      ...
    }
        
in the app.compoent markup add a div below the radio buttons
assign the FormArray name to the formArrayName directive
inside the div add a header and a button
assign the button's click event to the onAddHobby method
below the button add a div with the CSS class form-group
in the div tag add a ngFor directive to loop through the hobbies array
also introduce a local variable to which the current array index is set within the newset div add an input of CSS class form-control
use property binding to bind formControlName to the current index of the array
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        ...        
        <div formArrayName="hobbies">
            <h4>Your Hobbies</h4>
            <button class="btn btn-default" type="button" (click)="onAddHobby()">Add Hobby</button>
            <div 
                class="form-group" 
                *ngFor="let hobbyControl of signupForm.get('hobbies').controls; let i = index">
            <input type="text" class="form-control" [formControlName]="i">
            </div>
        </div>
        ...
    </form>
        
clicking the Add Hobby button dynamically creates an input control where the user can enter a hobby

Top

Index

Reactive: creating custom validators
example considers preventing users from selecting certain user names
a validator is just a function which gets executed automatically by Angular
method arg is the control to be validated
return value is a JSON object containing key value pair where value is a boolean
if validation is successful return null
change the second arg of the FormControl c'tor for username to an array and add the new validator
because Angular invokes the method an error will occur because the this pointer is an unknown
bind the this pointer to the validator
at this point a name in the array is considered valid because indexOf returns -1 if the name is not found because Angular considers -1 to be true
to make comparison work check that indexOf is not the same as -1
    ...
    export class AppComponent implements OnInit {
      ...
      forbiddenUsernames = ['Chris', 'Anna'];

      ngOnInit() {
        this.signupForm = new FormGroup({
          'userData': new FormGroup({
            'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
            ...
          }),
          ...
        });
      }
      ...
      forbiddenNames(control: FormControl): { [s: string]: boolean } {
        if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
          return { 'name is forbidden': true };
        }
        return null;
      }
    } 
        

Top

Index

Reactive: using error codes
now two different errors are possible with the username remove error message within the span in username form-group
add two spans within the span where the error message was
the two spans check the control's error code to determine which error message is shown
    <form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
        <div formGroupName="userData">
            <div class="form-group">
            <label for="username">Username</label>
            <input type="text" id="username" formControlName="username" class="form-control">
            <span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched" class="help-block">
                <span *ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">The username is invalid.</span>
                <span *ngIf="signupForm.get('userData.username').errors['required']">This field is required.</span>
            </span>
            </div>
            ...
        </div>
        ...
    </form>
        

Top

Index

Reactive: creating a custom async validator
add method to validate email addresses
function returns a Promise or an Observable
the timeout represents the time used to send a value down the wire and receive a response
if control's value is invalid the resolve method's arg is a JSON object otherwise the arg should be null
while the async method runs the control's validity will be pending
    ...
    export class AppComponent implements OnInit {
      ...
      ngOnInit() {
        this.signupForm = new FormGroup({
          'userData': new FormGroup({
            'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
            'email': new FormControl(null, [Validators.email, Validators.required], this.forbiddenEmails)
          }),
          'gender': new FormControl('male'),
          'hobbies': new FormArray([])
        });
      }
      ...
      forbiddenEmails(control: FormControl): Promise
 | Observable {
        const promise = new Promise((resolve, reject) => {
          setTimeout(() => {
            if(control.value === '[email protected]') {
              resolve({'emailIsForbidden': true});
            } else {
              resolve(null);
            }      
          }, 1500);
        });
        return promise;
      }
    }   
     

Top

Index

Reactive: reacting to status or value changes
can subscribe to valueChanges of form or individual controls
can subscribe to statusChanges of form or individual controls
    ...
    export class AppComponent implements OnInit {
      ...
      ngOnInit() {
        this.signupForm = new FormGroup({
          'userData': new FormGroup({
            'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),
            'email': new FormControl(null, [Validators.email, Validators.required], this.forbiddenEmails)
          }),
          'gender': new FormControl('male'),
          'hobbies': new FormArray([])
        });
        this.signupForm.valueChanges.subscribe(
          (value) => console.log(value)
        ); 
        this.signupForm.statusChanges.subscribe(
          (status) => console.log(status)
        );
      }
      ...
    }
    

Top

Index

Reactive setting and patching values
setValue, patchValue, and reset methods work the same in both reactive and template-driven forms
  ...
  ngOnInit() {
    ...
    this.signupForm.valueChanges.subscribe(
      (value) => console.log(value)
    );
    this.signupForm.statusChanges.subscribe(
      (status) => console.log(status)
    );
    this.signupForm.setValue({
      'userData': {
        'username': 'flip',
        'email': '[email protected]'
      },
      'gender': 'male',
      'hobbies': []
    });
    this.signupForm.patchValue({
      'userData': {
        'username': 'flipper'
      }
    });
  }
  ... 
    

Top

Index

n4jvp.com