Angular 2 - Form validation for warnings/hints

Ok

its can be easy by angular.io for form validation hinting you can read documents on https://angular.io/docs/ts/latest/cookbook/form-validation.html

but the similar way that can help you better may be in my mind.

first we create a abstract class named Form it contains some common function and properties.

import {FormGroup} from "@angular/forms";

export abstract class Form {
    form: FormGroup;

    protected abstract formErrors: Object;

    protected abstract validationMessages: Object;

    onValueChanged(data?: any) {
        if (!this.form) { return; }
        const form = this.form;

        for (const field in this.formErrors) {
            this.formErrors[field] = '';
            const control = form.get(field);

            if (control && control.dirty && !control.valid) {
                const messages = this.validationMessages[field];

                for (const key in control.errors) {
                    this.formErrors[field] = messages[key];
                    break;
                }
            }
        }
    }
}

then you should to create a form component for example named LoginComponent like below

import {Component, OnInit} from "@angular/core";
import {Form} from "./form";
import {Validators, FormBuilder} from "@angular/forms";


@Component({
    templateUrl: '...'
})
export class LoginComponent extends Form implements OnInit {

    protected formErrors = {
        'email': '',
        'password': ''
    }

    protected validationMessages = {
        'email': {
            'required': 'email required message',
            'email':  'email validation message'
        },
        'password': {
            'required': 'password required message',
            'minlength': 'password min length message',
            'maxlength': 'password max length message',
        }
    }

    constructor(private _fb: FormBuilder) { }

    ngOnInit() {
        this.buildForm();
    }

    buildForm() {
        this.form = this._fb.group({
            'email': ['', [
                Validators.required,
                // emailValidator
            ]],
            'password': ['', [
                Validators.required,
                Validators.minLength(8),
                Validators.maxLength(30)
            ]]
        });

        this.form.valueChanges
            .subscribe(data => this.onValueChanged(data));

        this.onValueChanged(); //
    }

}

first we should inject FormBuilder for reactive forms (do not forgot to import ReactiveFormModule in your main module) and then in [buildForm()] method we build a group of form on form property that was inherited from abstract class Form.

then in next we create a subscribe for form value changes and on value change we call [onValueChanged()] method.

in [onValueChanged()] method we check the fields of form has valid or not, if not we get the message from protected validationMessages property and show it in formErrors property.

then your template should be similar this

<div class="col-md-4 col-md-offset-4">
    <form [formGroup]="form" novalidate>


        <div class="form-group">
            <label class="control-label" for="email">email</label>
            <input type="email" formControlName="email" id="email" class="form-control" required>
            <div class="help help-block" *ngIf="formErrors.email">
                <p>{{ formErrors.email }}</p>
            </div>
        </div>

        <div class="form-group">
            <label class="control-label" for="password">password</label>
            <input type="password" formControlName="password" id="password" class="form-control" required>
            <div class="help help-block" *ngIf="formErrors.password">
                <p>{{ formErrors.password }}</p>
            </div>
        </div>

        <div class="form-group">
            <button type="submit" class="btn btn-block btn-primary" [disabled]="!form.valid">login</button>
        </div>
    </form>
</div>

the template is so easy, inner you check the field has error or not if has the error bind.

for bootstrap you can do something else like below

<div class="form-group" [ngClass]="{'has-error': form.controls['email'].dirty && !form.controls['email'].valid, 'has-success': form.controls['email'].valid}">
    <label class="control-label" for="email">email</label>
    <input type="email"
           formControlName="email"
           id="email"
           class="form-control"
           required>
    <div class="help help-block" *ngIf="formErrors.email">
        <p>{{ formErrors.email }}</p>
    </div>
</div>

UPDATE: I attempt to create a very simple but almost complete sample for you certainly you can develop it for wilder scale : https://embed.plnkr.co/ExRUOtSrJV9VQfsRfkkJ/

but a little describe for that, you can create a custom validations like below

import {ValidatorFn, AbstractControl} from '@angular/forms';

function isEmptyInputValue(value: any) {
  return value == null || typeof value === 'string' && value.length === 0;
}

export class MQValidators {

  static age(max: number, validatorName: string = 'age'): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} => {
      if (isEmptyInputValue(control.value)) return null;

      const value = typeof control.value == 'number' ? control.value : parseInt(control.value);

      if (isNaN(value)) return null;

      if (value <= max) return null;

      let result = {};
      result[validatorName] = {valid: false};

      return result;
    }
  }

}

this custom validator get a optional param named validatorName, this param cause you designate multi similar validation as in your example the formComponent should be like below :

buildForm () {

    this.form = this._fb.group({
        'age': ['', [
            Validators.required,
            Validators.pattern('[0-9]*'),
            MQValidators.age(90, 'abnormalAge'),
            MQValidators.age(120, 'incredibleAge')
        ]]
    });

    this.form.valueChanges
        .subscribe(data => this.onValueChanged(data));

    this.onValueChanged();
}

onValueChanged(data?: any): void {
    if (!this.form) { return; }
    const form = this.form;

    for (const field in this.formErrors) {
        this.formErrors[field] = '';
        const control = form.get(field);

        if (control && control.dirty && !control.valid) {
            const messages = this.validationMessages[field];

            for (const key in control.errors) {
                this.formErrors[field] = messages[key];
            }
        }
    }
}

formErrors = {
    age: ''
}

validationMessages = {
    'age': {
        'required': 'age is required.',
        'pattern': 'age should be integer.',
        'abnormalAge': 'age higher than 90 is abnormal !!!',
        'incredibleAge': 'age higher than 120 is incredible !!!'
    }
}

Hope I helped.


This is probably how I would have done it.

<form #form="ngForm" (ngSubmit)="save()">

  <input formControlName="controlName">

  <span *ngIf="form.pristine && form.controls.controlName.value > 90 && form.controls.controlName.value < 120">
   Warning: Age limit is high..
  </span>

  <span *ngIf="form.pristine && form.controls.controlName.value > 120">
   Error: Age limit exceeded..
  </span>

<form>

I have done it by creating custom validator, which always return null. Also this validator creates additional property warnings. Then just simple check this property from your view.

export interface AbstractControlWarn extends AbstractControl { warnings: any; }

export function tooBigAgeWarning(c: AbstractControlWarn) {
  if (!c.value) { return null; }
  let val = +c.value;
  c.warnings = val > 90 && val <= 120 ? { tooBigAge: {val} } : null;
  return null;
}

export function impossibleAgeValidator(c: AbstractControl) {
  if (tooBigAgeWarning(c) !== null) { return null; }
  let val = +c.value;
  return val > 120 ? { impossibleAge: {val} } : null;
}

@Component({
  selector: 'my-app',
  template: `
    <div [formGroup]="form">
      Age: <input formControlName="age"/>
      <div *ngIf="age.errors?.required" [hidden]="age.pristine">
      Error! Age is required
      </div>
      <div *ngIf="age.errors?.impossibleAge" [hidden]="age.pristine">
      Error! Age is greater then 120
      </div>
      <div *ngIf="age.warnings?.tooBigAge" [hidden]="age.pristine">
      Warning! Age is greater then 90
      </div>
      <p><button type=button [disabled]="!form.valid">Send</button>
    </div>
  `,
})
export class App {
  age: FormControl;
  constructor(private _fb: FormBuilder) { }
  ngOnInit() {
    this.form = this._fb.group({
      age: ['', [
        Validators.required,
        tooBigAgeWarning,
        impossibleAgeValidator]]
    })
    this.age = this.form.get("age");
  }
}

Example: https://plnkr.co/edit/me0pHePkcM5xPQ7nzJwZ?p=preview


The accepted answer from Sergey Voronezhskiy works perfect in development mode but if you try build in --prod mode you will get this error.

...  Property 'warnings' does not exist on type 'FormControl'.

In order to fix this error I did adjustments to the original code (App class), basically to fix loosely typed variables. This is the new version:

export class FormControlWarn extends FormControl { warnings: any; }

export function tooBigAgeWarning(c: FormControlWarn ) {
    if (!c.value) { return null; }
    let val = +c.value;
    c.warnings = val > 90 && val <= 120 ? { tooBigAge: {val} } : null;
    return null;
}
export function impossibleAgeValidator(c: AbstractControl) {
   if (tooBigAgeWarning(c) !== null) { return null; }
   let val = +c.value;
   return val > 120 ? { impossibleAge: {val} } : null;
}

@Component({
  selector: 'my-app',
  template: `
    <div [formGroup]="form">
      Age: <input formControlName="age"/>
      <div *ngIf="age.errors?.required" [hidden]="age.pristine">
      Error! Age is required
      </div>
      <div *ngIf="age.errors?.impossibleAge" [hidden]="age.pristine">
      Error! Age is greater then 120
      </div>
      <div *ngIf="age.warnings?.tooBigAge" [hidden]="age.pristine">
      Warning! Age is greater then 90
      </div>
      <p><button type=button [disabled]="!form.valid">Send</button>
    </div>
  `,
})
    export class App {
      get age(): FormControlWarn{
         return <FormControlWarn>this.form.get("age");
      }
      constructor(private _fb: FormBuilder) { }
      ngOnInit() {
        this.form = this._fb.group({
          age: new FormControlWarn('', [
            Validators.required,
            tooBigAgeWarning,
            impossibleAgeValidator])
        });       
      }
    }