Wednesday, August 17, 2016

19. Building Forms with Complex Validation

We learned about template driven forms. But limited to few basic validators. For more advanced validation, need...

Will extend this file.

signup-form.component.html

Creating Controls Explicitly

Upgrade form to angular form

signup-form.component.ts

import {Component} from '@angular/core';
import {Control, ControlGroup, Validators} from '@angular/common';

@Component({
    selector: 'signup-form',
    templateUrl: 'app/signup-form.component.html'
})
export class SignUpFormComponent {
    // model driven forms
    form = new ControlGroup({
        username: new Control('',Validators.required),
        password: new Control('',Validators.required)
    });

    signup(){
        console.log(this.form.value); // returns a json object that contains all value in the form
    }
}

signup-form.component.html

<form [ngFormModel]="form" (ngSubmit)="signup()">
    <div class="form-group">
        <label for="username">Username</label>
        <input 
            id="username" 
            type="text" 
            class="form-control"            
            ngControl="username"
            #username="ngForm">
        <div
            *ngIf="!username.valid" 
            class="alert alert-danger">Username is required</div>
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input 
            id="password" 
            type="text" 
            class="form-control"
            ngControl="password"
            #password="ngForm">
        <div
            *ngIf="!password.valid" 
            class="alert alert-danger">Password is required</div>
    </div>
    <button class="btn btn-primary" type="submit">Sign Up</button>

</form>

app.component.ts

import { Component } from '@angular/core';
import {SignUpFormComponent} from './signup-form.component'

@Component({
  moduleId: module.id,
  selector: 'app-root',
  directives:[SignUpFormComponent],
  template: `
        <signup-form></signup-form>
    `       
})
export class AppComponent {


}

Using FormBuilder

in Signup-form.component.ts

export class SignUpFormComponent {

    form: ControlGroup;

    constructor(fb:FormBuilder){
        // cleaner? compact?
        fb.group({
            username:['',Validators.required],
            password:['',Validators.required]
        });
    }

    /*
    form = new ControlGroup({
        username: new Control('',Validators.required),
        password: new Control('',Validators.required)
    });
    */

    signup(){
        console.log(this.form.value); // returns a json object that contains all value in the form
    }

}

Implementing Custom Validation

// assume username cannot contain a space

Create usernameValidators.ts

import {Control} from '@angular/common'


// can put this method anywhere, but better to put in a separate class
export class UsernameValidators{

// validation passes: return null
// validation fails: return {<validationRule>:<value>} key-value pair e.g. minlength

    static cannotContainSpace(control: Control){
        if(control.value.indexOf(' ') >= 0)
            return { cannotContainSpace: true};

        return null;
    }
}

signup-form.component.html

<form [ngFormModel]="form" (ngSubmit)="signup()">
    <div class="form-group">
        <label for="username">Username</label>
        <input 
            id="username" 
            type="text" 
            class="form-control"            
            ngControl="username"
            #username="ngForm">
        <div *ngIf="username.touched && username.errors">
            <div
                *ngIf="!username.errors.valid" 
                class="alert alert-danger">Username is required</div>
            <div
                *ngIf="username.errors.cannotContainSpace" 
                class="alert alert-danger">Username cannot contain spaces
            </div>
        </div>            
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input 
            id="password" 
            type="text" 
            class="form-control"
            ngControl="password"
            #password="ngForm">
        <div
            *ngIf="!password.valid" 
            class="alert alert-danger">Password is required</div>
    </div>
    <button class="btn btn-primary" type="submit">Sign Up</button>
</form>


Async Validation: server side validation e.g. check if username is unique

add in usernameValidators.ts

    // method returns a promise. A promise represents the result of a asynochronous operation. 
    static shouldBeUnique(control: Control){ // promises
        return new Promise((resolve,reject) => {
            // call to server
            setTimeout(function(){
                if(control.value == "mosh")
                    resolve({shouldBeUnique: true})
                else
                    resolve(null);
            },1000)
        });
    }

Add in signup-form.component.html

            <div
                *ngIf="username.errors.shouldBeUnique" 
                class="alert alert-danger">This username is already taken.
            </div>

Showing a loader image

<form [ngFormModel]="form" (ngSubmit)="signup()">
    <div class="form-group">
        <label for="username">Username</label>
        <input 
            id="username" 
            type="text" 
            class="form-control"            
            ngControl="username"
            #username="ngForm">
        <div *ngIf="username.control.pending">Checking for uniqueness..</div>

        <div *ngIf="username.touched && username.errors">
            <div
                *ngIf="!username.errors.valid" 
                class="alert alert-danger">Username is required</div>

            <div

Validating Upon Form Submit

    signup(){
        // var result = authService.login(this.form.value)
        this.form.find('username').setErrors({invalidLogin:true});

        console.log(this.form.value); // returns a json object that contains all value in the form

    }


Add in signup-form.component.html

<div *ngIf="username.touched && username.errors">
            <div
                *ngIf="username.errors.invalidLogin" 
                class="alert alert-danger">Username or password is invalid</div>

            <div

Password Change Form

change-password-form.component.html

<form [ngFormModel]="form" (ngSubmit)="changePassword()">
    <div class="form-group">
        <label for="oldPassword">Current Password</label>
        <input 
            id="oldPassword" 
            type="password" 
            class="form-control"
            ngControl="oldPassword"
            #oldPassword="ngForm">
        <div *ngIf="oldPassword.touched && oldPassword.errors">
            <div
                *ngIf="oldPassword.errors.required" 
                class="alert alert-danger">Old password is required.</div>
            <div
                *ngIf="oldPassword.errors.validOldPassword"
                class="alert alert-danger">Old password is incorrect.</div>
        </div>
    </div>
    <div class="form-group">
        <label for="newPassword">New Password</label>
        <input 
            id="newPassword" 
            type="password" 
            class="form-control"
            ngControl="newPassword"
            #newPassword="ngForm">
        <div *ngIf="newPassword.touched && newPassword.errors">
            <div 
                *ngIf="newPassword.errors.required"
                class="alert alert-danger">
                New password is required.</div>
            <div 
                *ngIf="newPassword.errors.complexPassword"
                class="alert alert-danger">
                New password should be minimum {{ newPassword.errors.complexPassword.minLength }} characters.</div>
        </div>
    </div>
    <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input 
            id="confirmPassword" 
            type="password" 
            class="form-control"
            ngControl="confirmPassword"
            #confirmPassword="ngForm">
        <div 
            *ngIf="confirmPassword.touched && !confirmPassword.valid"
            class="alert alert-danger">
            Confirm the password.</div>
        <!-- 
            Note that here I'm checking for form.errors.passwordShouldMatch
            because this validation is applied at the form itself. 
         -->
        <div 
            *ngIf="confirmPassword.touched && form.errors && form.errors.passwordsShouldMatch"
            class="alert alert-danger">
            Passwords don't match.</div>
    </div>
    <button class="btn btn-primary" type="submit" [disabled]="!form.valid">Change Password</button>

</form>

change-password-form.component.ts

import {Component} from '@angular/core';
import {ControlGroup, Validators, FormBuilder} from '@angular/common';

import {PasswordValidators} from './passwordValidators';

@Component({
    selector: 'change-password-form',
    templateUrl: 'app/change-password-form.component.html'
})
export class ChangePasswordFormComponent {
    form: ControlGroup;
    
    constructor(fb: FormBuilder){
        this.form = fb.group({
            oldPassword: ['', Validators.required],
            newPassword: ['', Validators.compose([
                Validators.required,
                PasswordValidators.complexPassword
            ])],
            // Note that here is no need to apply complexPassword validator
            // to confirm password field. It's sufficient to apply it only to
            // new password field. Next, passwordsShouldMatch validator
            // will compare confirm password with new password and this will
            // implicitly enforce that confirm password should match complexity
            // rules. 
            confirmPassword: ['', Validators.required]
        }, { validator: PasswordValidators.passwordsShouldMatch });
    }
    
    changePassword(){
        // Validating the oldPassword on submit. In a real-world application
        // here, we'll use a service to call the server. The server would
        // tell us that the old password does not match. 
        var oldPassword = this.form.find('oldPassword');
        if (oldPassword.value != '1234') 
            oldPassword.setErrors({ validOldPassword: true });

        if (this.form.valid)
            alert("Password successfully changed.");
    }

}

passwordValidators.ts

import {Control, ControlGroup} from '@angular/common';

export class PasswordValidators {

    static complexPassword(control: Control){
        const minLength = 5;
        
        // We bypass this validation rule if the field is empty, assuming
        // it is valid. At this point, the required validator will kick in
        // and asks the user to type a value. With this technique, we'll 
        // display only a single validation message at a time, rather than:
        // 
        // * This field is required.
        // * Password doesn't match complexity rules.
        //
        if (control.value == '')
            return null; 
     
        if (control.value.length < minLength)
            // Note that I'm returning an object, instead of:
            // 
            // return { complexPassword: true }
            //
            // This will give the client additional data about the 
            // validation and why it failed. 
            return { 
                complexPassword: {
                    minLength: minLength
                } 
            };
            
        return null;
    }
    
    static passwordsShouldMatch(group: ControlGroup){
        var newPassword = group.find('newPassword').value;
        var confirmPassword = group.find('confirmPassword').value;
        
        // If either of these fields is empty, the validation 
        // will be bypassed. We expect the required validator to be 
        // applied first. 
        if (newPassword == '' || confirmPassword == '')
            return null;
        
        if (newPassword != confirmPassword)
            return { passwordsShouldMatch: true };
            
        return null;
    }

}

No comments:

Post a Comment