All files / src/app/shared/forms cd-validators.ts

97.44% Statements 76/78
93.75% Branches 30/32
100% Functions 22/22
97.18% Lines 69/71

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 27327x               27x 27x 27x   27x 177x         27x         27x     53x   2x                     27x     16x 16x     5x   6x   5x                 51x   23x   11x                 27x   11x   2x                                                 27x 187x       523x     479x   40x     40x                     625x 625x 6294x   48x   6246x                             292x       539x 538x   292x   292x           670x   269x     45x   356x       292x 134x 25x                           27x 17x 139x 139x   7x   132x         2x 2x 2x     139x                                       27x   55x 104x   52x       57x       4x       2x   2x                           27x 10x 10x   3x           4x     27x  
import {
  AbstractControl,
  AsyncValidatorFn,
  ValidationErrors,
  ValidatorFn,
  Validators
} from '@angular/forms';
 
import * as _ from 'lodash';
import { Observable, of as observableOf, timer as observableTimer } from 'rxjs';
import { map, switchMapTo, take } from 'rxjs/operators';
 
export function isEmptyInputValue(value: any): boolean {
  return value == null || value.length === 0;
}
 
export type existsServiceFn = (value: any) => Observable<boolean>;
 
export class CdValidators {
  /**
   * Validator that performs email validation. In contrast to the Angular
   * email validator an empty email will not be handled as invalid.
   */
  static email(control: AbstractControl): ValidationErrors | null {
    // Exit immediately if value is empty.
    if (isEmptyInputValue(control.value)) {
      return null;
    }
    return Validators.email(control);
  }
 
  /**
   * Validator function in order to validate IP addresses.
   * @param {number} version determines the protocol version. It needs to be set to 4 for IPv4 and
   * to 6 for IPv6 validation. For any other number (it's also the default case) it will return a
   * function to validate the input string against IPv4 OR IPv6.
   * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
   * if the validation failed, otherwise `null`.
   */
  static ip(version: number = 0): ValidatorFn {
    // prettier-ignore
    const ipv4Rgx =
      /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/i;
    const ipv6Rgx = /^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i;
 
    if (version === 4) {
      return Validators.pattern(ipv4Rgx);
    } else if (version === 6) {
      return Validators.pattern(ipv6Rgx);
    } else {
      return Validators.pattern(new RegExp(ipv4Rgx.source + '|' + ipv6Rgx.source));
    }
  }
 
  /**
   * Validator function in order to validate numbers.
   * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
   * if the validation failed, otherwise `null`.
   */
  static number(allowsNegative: boolean = true): ValidatorFn {
    if (allowsNegative) {
      return Validators.pattern(/^-?[0-9]+$/i);
    } else {
      return Validators.pattern(/^[0-9]+$/i);
    }
  }
 
  /**
   * Validator function in order to validate decimal numbers.
   * @returns {ValidatorFn} A validator function that returns an error map containing `pattern`
   * if the validation failed, otherwise `null`.
   */
  static decimalNumber(allowsNegative: boolean = true): ValidatorFn {
    if (allowsNegative) {
      return Validators.pattern(/^-?[0-9]+(.[0-9]+)?$/i);
    } else {
      return Validators.pattern(/^[0-9]+(.[0-9]+)?$/i);
    }
  }
 
  /**
   * Validator that requires controls to fulfill the specified condition if
   * the specified prerequisites matches. If the prerequisites are fulfilled,
   * then the given function is executed and if it succeeds, the 'required'
   * validation error will be returned, otherwise null.
   * @param {Object} prerequisites An object containing the prerequisites.
   *   ### Example
   *   ```typescript
   *   {
   *     'generate_key': true,
   *     'username': 'Max Mustermann'
   *   }
   *   ```
   *   Only if all prerequisites are fulfilled, then the validation of the
   *   control will be triggered.
   * @param {Function | undefined} condition The function to be executed when all
   *   prerequisites are fulfilled. If not set, then the {@link isEmptyInputValue}
   *   function will be used by default. The control's value is used as function
   *   argument. The function must return true to set the validation error.
   * @return {ValidatorFn} Returns the validator function.
   */
  static requiredIf(prerequisites: Object, condition?: Function | undefined): ValidatorFn {
    return (control: AbstractControl): ValidationErrors | null => {
      // Check if all prerequisites matches.
      if (
        !Object.keys(prerequisites).every((key) => {
          return control.parent && control.parent.get(key).value === prerequisites[key];
        })
      ) {
        return null;
      }
      const success = _.isFunction(condition)
        ? condition.call(condition, control.value)
        : isEmptyInputValue(control.value);
      return success ? { required: true } : null;
    };
  }
 
  /**
   * Custom validation by passing a name for the error and a function as error condition.
   *
   * @param {string} error
   * @param {Function} condition - a truthy return value will trigger the error
   * @returns {ValidatorFn}
   */
  static custom(error: string, condition: Function): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } => {
      const value = condition.call(this, control.value);
      if (value) {
        return { [error]: value };
      }
      return null;
    };
  }
 
  /**
   * Validate form control if condition is true with validators.
   *
   * @param {AbstractControl} formControl
   * @param {Function} condition
   * @param {ValidatorFn[]} conditionalValidators List of validators that should only be tested
   * when the condition is met
   * @param {ValidatorFn[]} permanentValidators List of validators that should always be tested
   * @param {AbstractControl[]} watchControls List of controls that the condition depend on.
   * Every time one of this controls value is updated, the validation will be triggered
   */
  static validateIf(
    formControl: AbstractControl,
    condition: Function,
    conditionalValidators: ValidatorFn[],
    permanentValidators: ValidatorFn[] = [],
    watchControls: AbstractControl[] = []
  ) {
    conditionalValidators = conditionalValidators.concat(permanentValidators);
 
    formControl.setValidators(
      (
        control: AbstractControl
      ): {
        [key: string]: any;
      } => {
        const value = condition.call(this);
        if (value) {
          return Validators.compose(conditionalValidators)(control);
        }
        if (permanentValidators.length > 0) {
          return Validators.compose(permanentValidators)(control);
        }
        return null;
      }
    );
 
    watchControls.forEach((control: AbstractControl) => {
      control.valueChanges.subscribe(() => {
        formControl.updateValueAndValidity({ emitEvent: false });
      });
    });
  }
 
  /**
   * Validator that requires that both specified controls have the same value.
   * Error will be added to the `path2` control.
   * @param {string} path1 A dot-delimited string that define the path to the control.
   * @param {string} path2 A dot-delimited string that define the path to the control.
   * @return {ValidatorFn} Returns a validator function that always returns `null`.
   *   If the validation fails an error map with the `match` property will be set
   *   on the `path2` control.
   */
  static match(path1: string, path2: string): ValidatorFn {
    return (control: AbstractControl): { [key: string]: any } => {
      const ctrl1 = control.get(path1);
      const ctrl2 = control.get(path2);
      if (ctrl1.value !== ctrl2.value) {
        ctrl2.setErrors({ match: true });
      } else {
        const hasError = ctrl2.hasError('match');
        if (hasError) {
          // Remove the 'match' error. If no more errors exists, then set
          // the error value to 'null', otherwise the field is still marked
          // as invalid.
          const errors = ctrl2.errors;
          _.unset(errors, 'match');
          ctrl2.setErrors(_.isEmpty(_.keys(errors)) ? null : errors);
        }
      }
      return null;
    };
  }
 
  /**
   * Asynchronous validator that requires the control's value to be unique.
   * The validation is only executed after the specified delay. Every
   * keystroke during this delay will restart the timer.
   * @param serviceFn {existsServiceFn} The service function that is
   *   called to check whether the given value exists. It must return
   *   boolean 'true' if the given value exists, otherwise 'false'.
   * @param serviceFnThis {any} The object to be used as the 'this' object
   *   when calling the serviceFn function. Defaults to null.
   * @param {number|Date} dueTime The delay time to wait before the
   *   serviceFn call is executed. This is useful to prevent calls on
   *   every keystroke. Defaults to 500.
   * @return {AsyncValidatorFn} Returns an asynchronous validator function
   *   that returns an error map with the `notUnique` property if the
   *   validation check succeeds, otherwise `null`.
   */
  static unique(
    serviceFn: existsServiceFn,
    serviceFnThis: any = null,
    EdueTime = 500
  ): AsyncValidatorFn {
    return (control: AbstractControl): Observable<ValidationErrors | null> => {
      // Exit immediately if user has not interacted with the control yet
      // or the control value is empty.
      if (control.pristine || isEmptyInputValue(control.value)) {
        return observableOf(null);
      }
      // Forgot previous requests if a new one arrives within the specified
      // delay time.
      return observableTimer(dueTime).pipe(
        switchMapTo(serviceFn.call(serviceFnThis, control.value)),
        map((resp: boolean) => {
          if (!resp) {
            return null;
          } else {
            return { notUnique: true };
          }
        }),
        take(1)
      );
    };
  }
 
  /**
   * Validator function for UUIDs.
   * @param required - Defines if it is mandatory to fill in the UUID
   * @return Validator function that returns an error object containing `invalidUuid` if the
   * validation failed, `null` otherwise.
   */
  static uuid(Erequired = false): ValidatorFn {
    const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
    return (control: AbstractControl): { [key: string]: any } | null => {
      if (control.pristine && control.untouched) {
        return null;
      } else if (!required && !control.value) {
        return null;
      } else if (uuidRe.test(control.value)) {
        return null;
      }
      return { invalidUuid: 'This is not a valid UUID' };
    };
  }
}