js/controllers/Form/FormConstraint.js
import ErrorManager from '~/helpers/ErrorManager';
import isEmail from 'validator/lib/isEmail';
import { combineLatest } from 'rxjs/index';
import { map, startWith, share } from 'rxjs/operators';
export const NoElementWithId = Symbol('Form.FormConstraint.NoElementWithId');
function leadingToLower(text) {
return text[0].toLowerCase() + text.substring(1)
}
function flatttenValidatorDescription(validator) {
return validator._validators.map((validator, index) => {
if (index > 0) {
return leadingToLower(validator.error);
} else {
return validator.error;
}
}).join(" and ");
}
/**
* @typedef {Object} Validator
* @property {Function} callback - Runs validator given element.
* @property {string} error - String to display on error.
*/
/**
* @typedef {Object} ValidationError
* @property {HTMLElement} node - Node with error.
* @property {string} error - String describing error
* @property {Validator} sourceValidator - The 'validator' object.
*/
/**
* Specifies what constraints a given form field must satisfy
*/
export default class FormConstraint {
/**
* Takes list of observers over which to observe validation.
* @param {...Observable<ValidationError[]>} validationObservers
* @return {Observable<boolean>} emits beginning with false
*/
static observeValidation(...validationObservers) {
return combineLatest(
validationObservers,
(...errors) => [].concat(...errors))
.pipe(
map(errors => errors.length === 0),
startWith(false),
share());
}
/**
* Creates a form constraint (doing nothing).
*/
constructor() {
this._validators = [];
}
/**
* Adds a validator to execute
* @param {Function} callback Validator to call. Passed element for first
* arg.
* @param {string} error String to print on error if the validation fails.
* @param {Object} opts additional options
*/
addValidator(callback, error, { } = {}) {
this._validators.push({ callback, error });
return this;
}
/**
* Specifies the length must be between (inclusive) bounds between a min
* and a max. For input elements.
*
* @param {number} min a positive integer representing the minimum length.
* @param {number} max a positive integer representing the maximum length.
* @return {FormConstraint} chainable object.
*/
length(min, max) {
return this.addValidator(
(value) => value.length >= min && value.length <= max,
`Must be at least ${min} and at most ${max} characters long`
);
}
/**
* Allows the value to either follow validations or be empty. Empty is
* defined as null or having no length.
*
* @return {FormConstraint} chainable object.
*/
isEmpty() {
return this.addValidator(
(value) => value === null || value.length === 0,
`Is empty`
);
}
/**
* Adds an OR validator.
* @param {FormConstraint} caseA - The first case
* @param {FormConstraint} caseB - The second case
* @return {FormConstraint} chainable object.
*/
or(caseA, caseB) {
// Create the text
const caseAText = flatttenValidatorDescription(caseA);
const caseBText = flatttenValidatorDescription(caseB);
const text = `${caseAText} or ${leadingToLower(caseBText)}`;
return this.addValidator(
(value) => caseA.validate(value).length === 0 || caseB.validate(value).length === 0,
text
);
}
/**
* Flatten a validator into a single one
* @param {FormConstraint} constraint
* @return {FormConstraint} chainable object
*/
flatten(constraint) {
return this.addValidator(
(value) => constraint.validate(value).length === 0,
flatttenValidatorDescription(constraint)
);
}
/**
* Checks if a field is an email
* @return {FormConstraint} chainable object.
*/
isEmail() {
return this.addValidator(
(value) => isEmail(value),
`Provide a valid email`
)
}
/**
* Makes sure a form value is not empty
* @param {string} error - error string to show.
* @return {FormConstraint} chainable object.
*/
notEmpty(error = `Must specify a value`) {
return this.addValidator(
(value) => value.length > 0,
error
);
}
/**
* Checks if has a value
* @param {string} error - Error to show
* @return {FormConstraint} chainable object
*/
hasValue(error = `Must specify a value`) {
return this.addValidator(
value => !!value,
error
);
}
/**
* Requires a field to match a certain regex.
* @param {RegExp} regex - Regex to match `elem.value` to.
* @return {FormConstraint} chainable object.
*/
regex(regex) {
return this.addValidator(
(value) => regex.test(value),
`Must match pattern ${regex.source}`
)
}
/**
* Runs validation on the element.
* @param {any} value - Any value to run validator on
* @return {ValidationError[]} list of form errors. Empty array if none.
*/
validate(value) {
let errors = [];
for (let i = 0; i < this._validators.length; i++) {
let validator = this._validators[i];
let res = validator.callback(value);
if (res === false) {
errors.push({
node: this._elem,
error: validator.error,
sourceValidator: validator
})
}
}
return errors;
}
}