angular2-json-schema-form
Version:
Angular 2 JSON Schema Form builder
710 lines (678 loc) • 30.6 kB
text/typescript
import { AbstractControl } from '@angular/forms';
import {
_convertToPromise, _executeValidators, _executeAsyncValidators, _mergeObjects,
_mergeErrors, isEmpty, isDefined, hasValue, isString, isNumber, isBoolean,
isArray, getType, isType, toJavaScriptType, xor,
SchemaPrimitiveType, PlainObject, IValidatorFn, AsyncIValidatorFn
} from './validator.functions';
import { forEachCopy } from './utility.functions';
/**
* 'JsonValidators' module
*
* Provides an extended set of validators to be used by form controls,
* compatible with standard JSON Schema validation options.
* http://json-schema.org/latest/json-schema-validation.html
*
* Note: This library is designed as a drop-in replacement for the Angular 2
* Validators library, and except for one small breaking change to the 'pattern'
* validator (described below) it can even be imported as a substitute, like so:
*
* import { JsonValidators as Validators } from 'json-validators';
*
* and it should work with existing code as a complete replacement.
*
* The one exception is the 'pattern' validator, which has been changed to
* matche partial values by default (the standard 'pattern' validator wrapped
* all patterns in '^' and '$', forcing them to always match an entire value).
* However, the old behavior can be restored by simply adding '^' and '$'
* around your patterns, or by passing an optional second parameter of TRUE.
* This change is to make the 'pattern' validator match the behavior of a
* JSON Schema pattern, which allows partial matches, rather than the behavior
* of an HTML input control pattern, which does not.
*
* This library replaces Angular 2's 4 validators and 1 validator combination
* function with the following 16 validators and 4 transformation functions:
*
* Validators:
* For all formControls: required (*), type, enum
* For text formControls: minLength (*), maxLength (*), pattern (*), format
* For numeric formControls: minimum, maximum, multipleOf
* For formGroup objects: minProperties, maxProperties, dependencies
* For formArray arrays: minItems, maxItems, uniqueItems
* (Validators originally included with Angular 2 are maked with (*).)
*
* NOTE: The dependencies validator is not complete.
* NOTE: The enum validator does not yet work with objects.
*
* Validator transformation functions:
* composeAnyOf, composeOneOf, composeAllOf, composeNot
* (Angular 2's original combination funciton, 'compose', is also included for
* backward compatibility, though it is effectively equivalent to composeAllOf,
* though with a more generic error message.)
*
* All validators have also been extended to accept an optional second argument
* which, if passed a TRUE value, causes the validator to perform the opposite
* of its original finction. (This is used internally to enable 'not' and
* 'composeOneOf' to function and return useful error messages.)
*
* The 'required' validator has also been overloaded so that if called with
* a boolean parameter (or no parameters) it returns the original validator
* function (rather than executing it). However, if it is called with an
* AbstractControl parameter (as was previously required), it behaves
* exactly as before.
*
* This enables all validators (including 'required') to be constructed in
* exactly the same way, so they can be automatically applied using the
* equivalent key names and values taken directly from a JSON Schema.
*
* This source code is partially derived from Angular 2,
* which is Copyright (c) 2014-2016 Google, Inc.
* Use of this source code is therefore governed by the same MIT-style license
* that can be found in the LICENSE file at https://angular.io/license
*/
export class JsonValidators {
/**
* Validator functions:
*
* For all formControls: required, type, enum
* For text formControls: minLength, maxLength, pattern, format
* For numeric formControls: minimum, maximum, multipleOf
* For formGroup objects: minProperties, maxProperties, dependencies
* For formArray arrays: minItems, maxItems, uniqueItems
*
* TODO: finish dependencies validator
* TODO: update enum to work with formGroup objects
*/
/**
* 'required' validator
*
* This validator is overloaded, compared to the default required validator.
* If called with no parameters, or TRUE, this validator returns the
* 'required' validator function (rather than executing it). This matches
* the behavior of all other validators in this library.
*
* If this validator is called with an AbstractControl parameter
* (as was previously required) it behaves the same as Angular 2's default
* required validator, and returns an error if the control is empty.
*
* Old behavior: (if input type = AbstractControl)
* @param {AbstractControl} control - required control
* @return {{[key: string]: boolean}} - returns error message if no input
*
* New behavior: (if no input, or input type = boolean)
* @param {boolean = true} required? - true to validate, false to disable
* @return {IValidatorFn} - returns the 'required' validator function itself
*/
static required(input: AbstractControl): PlainObject;
static required(input?: boolean): IValidatorFn;
static required(input?: AbstractControl | boolean): PlainObject | IValidatorFn {
if (input === undefined) { input = true; }
switch (input) {
case true: // Return required function (do not execute it yet)
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (invert) { return null; } // if not required, always return valid
return hasValue(control.value) ? null : { 'required': true };
};
case false: // Do nothing
return (control: AbstractControl): PlainObject => null;
default: // Execute required function
return hasValue((<AbstractControl>input).value) ?
null : { 'required': true };
}
};
/**
* 'type' validator
*
* Requires a control to only accept values of a specified type,
* or one of an array of types.
*
* Note: SchemaPrimitiveType = 'string'|'number'|'integer'|'boolean'|'null'
*
* @param {SchemaPrimitiveType | SchemaPrimitiveType[]} type - type(s) to accept
* @return {IValidatorFn}
*/
static type(type: SchemaPrimitiveType | SchemaPrimitiveType[]): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualValue: any = control.value;
let typeArray: SchemaPrimitiveType[] = isArray(type) ?
<SchemaPrimitiveType[]>type : [<SchemaPrimitiveType>type];
let isValid: boolean = false;
for (let typeValue of typeArray) {
if (isType(actualValue, typeValue) === true) {
isValid = true; break;
}
}
return xor(isValid, invert) ?
null : { 'type': { type, actualValue } };
};
}
/**
* 'enum' validator
*
* Requires a control to have a value from an enumerated list of values.
*
* Converts types as needed to allow string inputs to still correctly
* match number, boolean, and null enum values.
* (toJavaScriptType() can be used later to convert these string values.)
*
* TODO: modify to work with objects
*
* @param {any[]} enumList - array of acceptable values
* @return {IValidatorFn}
*/
static enum(enumList: any[]): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let isValid: boolean = true;
let actualValues: any | any[] = (isArray(control.value)) ?
control.value : [control.value];
for (let i1 = 0, l1 = actualValues.length; i1 < l1; i1++) {
let actualValue: any = actualValues[i1];
let itemIsValid: boolean = false;
for (let i2 = 0, l2 = enumList.length; i2 < l2; i2++) {
let enumValue: any = enumList[i2];
if (actualValue === enumValue) {
itemIsValid = true; break;
} else if (isNumber(enumValue) && +actualValue === +enumValue) {
itemIsValid = true; break;
} else if (
isBoolean(enumValue, 'strict') &&
toJavaScriptType(actualValue, 'boolean') === enumValue
) {
itemIsValid = true; break;
} else if (enumValue === null && !hasValue(actualValue)) {
itemIsValid = true; break;
}
}
if (!itemIsValid) { isValid = false; break; }
}
return xor(isValid, invert) ?
null : { 'enum': { 'enum': enumList, 'actualValue': control.value } };
};
}
/**
* 'minLength' validator
*
* Requires a control's text value to be greater than a specified length.
*
* @param {number} requiredLength - minimum allowed string length
* @param {boolean = false} invert - instead return error object only if valid
* @return {IValidatorFn}
*/
static minLength(requiredLength: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualLength: number = isString(control.value) ? control.value.length : 0;
let isValid: boolean = actualLength >= requiredLength;
return xor(isValid, invert) ?
null :
{ 'minlength': { requiredLength, actualLength } };
};
};
/**
* 'maxLength' validator
*
* Requires a control's text value to be less than a specified length.
*
* @param {number} requiredLength - maximum allowed string length
* @param {boolean = false} invert - instead return error object only if valid
* @return {IValidatorFn}
*/
static maxLength(requiredLength: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let actualLength: number = isString(control.value) ? control.value.length : 0;
let isValid: boolean = actualLength <= requiredLength;
return xor(isValid, invert) ?
null :
{ 'maxlength': { requiredLength, actualLength } };
};
};
/**
* 'pattern' validator
*
* Note: NOT the same as Angular 2's default pattern validator.
* Requires a control's value to match a specified regular expression pattern.
*
* This validator changes the behavior of default pattern validator
* by replacing RegExp(`^${pattern}$`) with RegExp(`${pattern}`),
* which allows for partial matches.
*
* To return to the default funcitonality, and match the entire string,
* pass TRUE as the optional second parameter.
*
* @param {string} pattern - regular expression pattern
* @param {boolean = false} wholeString - match whole value string?
* @return {IValidatorFn}
*/
static pattern(pattern: string, wholeString: boolean = false): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualValue: string = control.value;
let requiredPattern: string = (wholeString) ? `^${pattern}$` : pattern;
let regex = new RegExp(requiredPattern);
let isValid: boolean = isString(actualValue) ? regex.test(actualValue) : false;
return xor(isValid, invert) ?
null : { 'pattern': { requiredPattern, actualValue } };
};
}
/**
* 'format' validator
*
* Requires a control to have a value of a certain format.
*
* This validator currently checks the following formsts:
* 'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'
*
* TODO: add 'regex' and 'color' formats
*
* @param {'date-time'|'email'|'hostname'|'ipv4'|'ipv6'|'uri'} format - format to check
* @return {IValidatorFn}
*/
static format(
format: 'date-time' | 'email' | 'hostname' | 'ipv4' | 'ipv6' | 'uri' | 'url' | 'color'
): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let isValid: boolean;
let actualValue: string = control.value;
if (!isString(actualValue)) {
isValid = false;
} else {
switch (format) {
case 'date-time':
isValid = !!actualValue.match(/^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))$/);
break;
case 'email':
let parts: string[] = actualValue.split('@');
isValid =
!!parts && parts.length === 2 &&
!!parts[0].match(/^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")$/)
&&
!!parts[1].match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
break;
case 'hostname':
isValid = !!actualValue.match(/(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\b-){0,61}[0-9A-Za-z])?)*\.?/);
break;
case 'ipv4':
isValid = !!actualValue.match(/^((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]?)$/);
break;
case 'ipv6':
isValid = !!actualValue.match(/(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))/);
break;
case 'uri': case 'url':
isValid = !!actualValue.match(/^((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)$/);
break;
case 'color':
isValid = !!actualValue.match(/^#[A-Fa-f0-9]{6}$/);
break;
default:
console.error('format validator error: "' + format +
'" is not a recognized format.');
isValid = true;
}
}
return xor(isValid, invert) ?
null : { 'format': { format, actualValue } };
};
}
/**
* 'minimum' validator
*
* Requires a control to have a numeric value not greater than
* a specified minimum amount.
*
* The optional second parameter indicates whether the valid range excludes
* the minimum value. It defaults to false, and includes the minimum.
*
* @param {number} minimum - minimum allowed value
* @param {boolean = false} exclusiveMinimum - include minimum value itself?
* @return {IValidatorFn}
*/
static minimum(minimum: number, exclusiveMinimum: boolean = false): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualValue: number = control.value;
let isValid: boolean = isNumber(actualValue) &&
exclusiveMinimum ? actualValue > minimum : actualValue >= minimum;
return xor(isValid, invert) ?
null : { 'minimum': { minimum, exclusiveMinimum, actualValue } };
};
}
/**
* 'maximum' validator
*
* Requires a control to have a numeric value not less than
* a specified maximum amount.
*
* The optional second parameter indicates whether the valid range excludes
* the maximum value. It defaults to false, and includes the maximum.
*
* @param {number} maximum - maximum allowed value
* @param {boolean = false} exclusiveMaximum - include maximum value itself?
* @return {IValidatorFn}
*/
static maximum(maximum: number, exclusiveMaximum: boolean = false): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualValue: number = control.value;
let isValid: boolean = isNumber(actualValue) &&
exclusiveMaximum ? actualValue < maximum : actualValue <= maximum;
return xor(isValid, invert) ?
null : { 'maximum': { maximum, exclusiveMaximum, actualValue } };
};
}
/**
* 'multipleOf' validator
*
* Requires a control to have a numeric value that is a multiple
* of a specified number.
*
* @param {number} multipleOf - number value must be a multiple of
* @return {IValidatorFn}
*/
static multipleOf(multipleOf: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualValue: number = control.value;
let isValid: boolean = isNumber(actualValue) && actualValue % multipleOf === 0;
return xor(isValid, invert) ?
null : { 'multipleOf': { multipleOf, actualValue } };
};
}
/**
* 'minProperties' validator
*
* Requires a form group to have a minimum number of properties (i.e. have
* values entered in a minimum number of controls within the group).
*
* @param {number} minProperties - minimum number of properties allowed
* @return {IValidatorFn}
*/
static minProperties(minProperties: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualProperties: number = Object.keys(control.value).length || 0;
let isValid: boolean = actualProperties >= minProperties;
return xor(isValid, invert) ?
null : { 'minProperties': { minProperties, actualProperties } };
};
}
/**
* 'maxProperties' validator
*
* Requires a form group to have a maximum number of properties (i.e. have
* values entered in a maximum number of controls within the group).
*
* Note: Has no effect if the form group does not contain more than the
* maximum number of controls.
*
* @param {number} maxProperties - maximum number of properties allowed
* @return {IValidatorFn}
*/
static maxProperties(maxProperties: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let actualProperties: number = Object.keys(control.value).length || 0;
let isValid: boolean = actualProperties <= maxProperties;
return xor(isValid, invert) ?
null : { 'maxProperties': { maxProperties, actualProperties } };
};
}
/**
* 'dependencies' validator
*
* Requires the controls in a form group to meet additional validation
* criteria, depending on the values of other controls in the group.
*
* Examples:
* https://spacetelescope.github.io/understanding-json-schema/reference/object.html#dependencies
*
* @param {any} dependencies - required dependencies
* @return {IValidatorFn}
*/
static dependencies(dependencies: any): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
if (getType(dependencies) !== 'object' || isEmpty(dependencies)) { return null; }
let allErrors: PlainObject = _mergeObjects(
forEachCopy(dependencies, (value, requiringField) => {
if (!hasValue(control.value[requiringField])) { return null; }
let requiringFieldErrors: PlainObject = { };
let requiredFields: string[];
let properties: PlainObject = { };
if (getType(dependencies[requiringField]) === 'array') {
requiredFields = dependencies[requiringField];
} else if (getType(dependencies[requiringField]) === 'object') {
requiredFields = dependencies[requiringField]['required'] || [];
properties = dependencies[requiringField]['properties'] || { };
}
// Validate property dependencies
for (let requiredField of requiredFields) {
if (xor(!hasValue(control.value[requiredField]), invert)) {
requiringFieldErrors[requiredField] = { 'required': true };
}
}
// Validate schema dependencies
requiringFieldErrors = _mergeObjects(requiringFieldErrors,
forEachCopy(properties, (requirements, requiredField) => {
let requiredFieldErrors: PlainObject = _mergeObjects(
forEachCopy(requirements, (requirement, parameter) => {
let validator: IValidatorFn = null;
if (requirement === 'maximum' || requirement === 'minimum') {
let exclusive: boolean =
!!requirements['exclusiveM' + requirement.slice(1)];
validator = JsonValidators[requirement](parameter, exclusive);
} else if (typeof JsonValidators[requirement] === 'function') {
validator = JsonValidators[requirement](parameter);
}
return !isDefined(validator) ?
null : validator(control.value[requiredField]);
})
);
return isEmpty(requiredFieldErrors) ?
null : { [requiredField]: requiredFieldErrors };
})
);
return isEmpty(requiringFieldErrors) ?
null : { [requiringField]: requiringFieldErrors };
})
);
return isEmpty(allErrors) ? null : allErrors;
};
}
/**
* 'minItems' validator
*
* Requires a form array to have a minimum number of values.
*
* @param {number} minItems - minimum number of items allowed
* @return {IValidatorFn}
*/
static minItems(minItems: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let actualItems: number = isArray(control.value) ? control.value.length : 0;
let isValid: boolean = actualItems >= minItems;
return xor(isValid, invert) ?
null : { 'minItems': { minItems, actualItems } };
};
}
/**
* 'maxItems' validator
*
* Requires a form array to have a maximum number of values.
*
* @param {number} maxItems - maximum number of items allowed
* @return {IValidatorFn}
*/
static maxItems(maxItems: number): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let actualItems: number = isArray(control.value) ? control.value.length : 0;
let isValid: boolean = actualItems <= maxItems;
return xor(isValid, invert) ?
null : { 'maxItems': { maxItems, actualItems } };
};
}
/**
* 'uniqueItems' validator
*
* Requires values in a form array to be unique.
*
* @param {boolean = true} unique? - true to validate, false to disable
* @return {IValidatorFn}
*/
static uniqueItems(unique: boolean = true): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (!unique) { return null; }
if (isEmpty(control.value)) { return null; }
let sorted: any[] = control.value.slice().sort();
let duplicateItems = [];
for (let i = 1, l = sorted.length; i < l; i++) {
if (sorted[i - 1] === sorted[i] &&
duplicateItems.indexOf(sorted[i]) !== -1
) {
duplicateItems.push(sorted[i]);
}
}
let isValid: boolean = !duplicateItems.length;
return xor(isValid, invert) ?
null : { 'uniqueItems': { duplicateItems } };
};
}
/**
* No-op validator. Included for backward compatibility.
*/
static nullValidator(c: AbstractControl): PlainObject { return null; }
/**
* Validator transformation functions:
* composeAnyOf, composeOneOf, composeAllOf, composeNot,
* compose, composeAsync
*
* TODO: Add composeAnyOfAsync, composeOneOfAsync,
* composeAllOfAsync, composeNotAsync
*/
/**
* 'composeAnyOf' validator combination function
*
* Accepts an array of validators and returns a single validator that
* evaluates to valid if any one or more of the submitted validators are
* valid. If every validator is invalid, it returns combined errors from
* all validators.
*
* @param {IValidatorFn[]} validators - array of validators to combine
* @return {IValidatorFn} - single combined validator function
*/
static composeAnyOf(validators: IValidatorFn[]): IValidatorFn {
if (!validators) { return null; }
let presentValidators: IValidatorFn[] = validators.filter(isDefined);
if (presentValidators.length === 0) { return null; }
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let arrayOfErrors: PlainObject[] =
_executeValidators(control, presentValidators, invert).filter(isDefined);
let isValid: boolean = validators.length > arrayOfErrors.length;
return xor(isValid, invert) ?
null : _mergeObjects.apply(arrayOfErrors.concat({ 'anyOf': !invert }));
};
}
/**
* 'composeOneOf' validator combination function
*
* Accepts an array of validators and returns a single validator that
* evaluates to valid only if exactly one of the submitted validators
* is valid. Otherwise returns combined information from all validators,
* both valid and invalid.
*
* @param {IValidatorFn[]} validators - array of validators to combine
* @return {IValidatorFn} - single combined validator function
*/
static composeOneOf(validators: IValidatorFn[]): IValidatorFn {
if (!validators) { return null; }
let presentValidators: IValidatorFn[] = validators.filter(isDefined);
if (presentValidators.length === 0) { return null; }
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let arrayOfErrors: PlainObject[] =
_executeValidators(control, presentValidators);
let validControls: number =
validators.length - arrayOfErrors.filter(isDefined).length;
let isValid: boolean = validControls === 1;
if (xor(isValid, invert)) { return null; }
let arrayOfValids: PlainObject[] =
_executeValidators(control, presentValidators, invert);
return _mergeObjects.apply(
arrayOfErrors.concat(arrayOfValids).concat({ 'oneOf': !invert })
);
};
}
/**
* 'composeAllOf' validator combination function
*
* Accepts an array of validators and returns a single validator that
* evaluates to valid only if all the submitted validators are individually
* valid. Otherwise it returns combined errors from all invalid validators.
*
* @param {IValidatorFn[]} validators - array of validators to combine
* @return {IValidatorFn} - single combined validator function
*/
static composeAllOf(validators: IValidatorFn[]): IValidatorFn {
if (!validators) { return null; }
let presentValidators: IValidatorFn[] = validators.filter(isDefined);
if (presentValidators.length === 0) { return null; }
return (control: AbstractControl, invert: boolean = false): PlainObject => {
let combinedErrors = _mergeErrors(
_executeValidators(control, presentValidators, invert)
);
let isValid: boolean = combinedErrors === null;
return (xor(isValid, invert)) ?
null : _mergeObjects(combinedErrors, { 'allOf': !invert });
};
}
/**
* 'composeNot' validator inversion function
*
* Accepts a single validator function and inverts its result.
* Returns valid if the submitted validator is invalid, and
* returns invalid if the submitted validator is valid.
* (Note: this function can itself be inverted
* - e.g. composeNot(composeNot(validator)) -
* but this can be confusing and is therefore not recommended.)
*
* @param {IValidatorFn[]} validators - validator to invert
* @return {IValidatorFn} - new validator function that returns opposite result
*/
static composeNot(validator: IValidatorFn): IValidatorFn {
return (control: AbstractControl, invert: boolean = false): PlainObject => {
if (isEmpty(control.value)) { return null; }
let error: PlainObject = validator(control, !invert);
let isValid: boolean = error === null;
return (xor(isValid, invert)) ?
null : _mergeObjects(error, { 'not': !invert });
};
}
/**
* 'compose' validator combination function
*
* @param {IValidatorFn[]} validators - array of validators to combine
* @return {IValidatorFn} - single combined validator function
*/
static compose(validators: IValidatorFn[]): IValidatorFn {
if (!validators) { return null; }
let presentValidators = validators.filter(isDefined);
if (presentValidators.length === 0) { return null; }
return (control: AbstractControl, invert: boolean = false): PlainObject =>
_mergeErrors(_executeValidators(control, presentValidators, invert));
};
/**
* 'composeAsync' async validator combination function
*
* @param {AsyncIValidatorFn[]} async validators - array of async validators
* @return {AsyncIValidatorFn} - single combined async validator function
*/
static composeAsync(validators: AsyncIValidatorFn[]): AsyncIValidatorFn {
if (!validators) { return null; }
let presentValidators = validators.filter(isDefined);
if (presentValidators.length === 0) { return null; }
return (control: AbstractControl, invert: boolean = false) => Promise.all(
_executeAsyncValidators(control, presentValidators).map(_convertToPromise)
).then(_mergeErrors);
}
}