-
Notifications
You must be signed in to change notification settings - Fork 903
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(core/validation): move FormValidator classes to separate fil…
…es (#7460) Create a file for FormValidator and FormValidatorField. Keep interfaces and helpers in validation.ts.
- Loading branch information
1 parent
479e9d6
commit 53300b1
Showing
5 changed files
with
227 additions
and
214 deletions.
There are no files selected for viewing
162 changes: 162 additions & 0 deletions
162
app/scripts/modules/core/src/presentation/forms/validation/FormValidator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { get, isBoolean, set, startCase } from 'lodash'; | ||
import { FormValidatorField } from './FormValidatorField'; | ||
import { | ||
FORM_VALIDATION_VALIDATABLE_FIELD_IS_VALID_SHORT_CIRCUIT, | ||
IArrayItemValidationBuilder, | ||
IArrayItemValidator, | ||
IFormValidator, | ||
IFormValidatorField, | ||
IValidator, | ||
} from './validation'; | ||
import { Validators } from './validators'; | ||
|
||
interface INamedValidatorResult { | ||
name: string; | ||
error: string; | ||
} | ||
|
||
const runValidators = (validators: IValidator[], value: any, label: string) => { | ||
return validators.reduce( | ||
(result, next) => (result ? result : next(value, label)), | ||
'', // Need a falsy ValidatorResult other than undefined, which will trip out Array.reduce() | ||
); | ||
}; | ||
/** Transforms a list of INamedValidatorResults into a Formik-compatible errors object */ | ||
const expandErrors = (errors: INamedValidatorResult[], isArray: boolean) => { | ||
return errors.reduce((acc, curr) => set(acc, curr.name, curr.error), isArray ? [] : {}); | ||
}; | ||
const isError = (maybeError: any): boolean => { | ||
if (!maybeError) { | ||
return false; | ||
} else if (maybeError === FORM_VALIDATION_VALIDATABLE_FIELD_IS_VALID_SHORT_CIRCUIT) { | ||
return false; | ||
} else if (typeof maybeError === 'string') { | ||
return true; | ||
} else if (Array.isArray(maybeError)) { | ||
return !!maybeError.length; | ||
} else if (typeof maybeError === 'object') { | ||
return !!Object.keys(maybeError).length; | ||
} | ||
return !!maybeError; | ||
}; | ||
|
||
const createItemBuilder = (arrayBuilder: IFormValidator, index: number): IArrayItemValidationBuilder => { | ||
return { | ||
item(itemLabel) { | ||
return arrayBuilder.field(`[${index}]`, itemLabel); | ||
}, | ||
field(name, itemLabel) { | ||
return arrayBuilder.field(`[${index}].${name}`, itemLabel); | ||
}, | ||
validateForm: arrayBuilder.validateForm, | ||
arrayForEach: arrayBuilder.arrayForEach, | ||
}; | ||
}; | ||
|
||
// Utility to provide a builder for array items. The provided iteratee will be invoked for every array item. | ||
// This allows the error aggregation to ignore nested non-errors (i.e. [] or {}) | ||
const arrayForEach = (builder: (values: any) => IFormValidator, iteratee: IArrayItemValidator) => { | ||
return (array: any[], arrayLabel?: string) => { | ||
// Silently ignore non-arrays (usually undefined). If strict type checking is desired, it should be done by a previous validator. | ||
if (!Array.isArray(array)) { | ||
return false; | ||
} | ||
const arrayBuilder = builder(array); | ||
array.forEach((item: any, index: number) => { | ||
const itemBuilder = createItemBuilder(arrayBuilder, index); | ||
iteratee && iteratee(itemBuilder, item, index, array, arrayLabel); | ||
}); | ||
return arrayBuilder.validateForm(); | ||
}; | ||
}; | ||
|
||
export class FormValidator implements IFormValidator { | ||
constructor(private values: any) {} | ||
|
||
private fields: FormValidatorField[] = []; | ||
private isSpelAwareDefault = false; | ||
|
||
/** | ||
* Defines a new form field to validate | ||
* | ||
* @param name the name of the field in the Formik Form | ||
* @param label (optional) the label of this field. | ||
* If no label is provided, it will be inferred based on the name. | ||
*/ | ||
public field(name: string, label?: string): IFormValidatorField { | ||
label = label || startCase(name); | ||
|
||
const field = new FormValidatorField(name, label); | ||
this.fields.push(field); | ||
return field; | ||
} | ||
|
||
/** | ||
* Makes this FormValidator default to being spel-aware or not | ||
* | ||
* When true, all fields in this FormValidator will default to allowing spel values to pass validation. | ||
* Individual fields may override this default by calling field().spelAware() | ||
*/ | ||
public spelAware(isSpelAware = true): FormValidator { | ||
this.isSpelAwareDefault = isSpelAware; | ||
return this; | ||
} | ||
|
||
private getFieldValidators(field: FormValidatorField, isSpelAwareDefault: boolean): IValidator[] { | ||
const isSpelAware = isBoolean(field.isSpelAware) ? field.isSpelAware : isSpelAwareDefault; | ||
|
||
const requiredValidator = field.isRequired ? Validators.isRequired(field.isRequiredMessage) : null; | ||
const optionalValidator = !field.isRequired ? isOptionalValidator() : null; | ||
const spelValidator = isSpelAware ? spelAwareValidator() : null; | ||
|
||
return [requiredValidator, optionalValidator, spelValidator, ...field.validators].filter(x => !!x); | ||
} | ||
|
||
/** | ||
* This runs validation on all the ValidatableField(s) in this FormValidator. | ||
* | ||
* It aggregate all the field validation errors into an object compatible with Formik Errors. | ||
* Each field error is stored in the resulting object using the field's 'name' as a path. | ||
*/ | ||
public validateForm(): any { | ||
const results: INamedValidatorResult[] = this.fields.map(field => { | ||
const { name, label } = field; | ||
|
||
const value = get(this.values, name); | ||
const fieldValidators = this.getFieldValidators(field, this.isSpelAwareDefault); | ||
const error = runValidators(fieldValidators, value, label); | ||
|
||
return { name, error }; | ||
}); | ||
|
||
const errors = results.filter(fieldResult => isError(fieldResult.error)); | ||
|
||
return expandErrors(errors, Array.isArray(this.values)); | ||
} | ||
|
||
public arrayForEach(iteratee: IArrayItemValidator): any { | ||
return arrayForEach(values => new FormValidator(values), iteratee); | ||
} | ||
} | ||
|
||
/** | ||
* Not exported because it uses.short circuiting, so it only works inside of a FormValidator | ||
* Use via ValidatableField.optional(). | ||
*/ | ||
function isOptionalValidator(): IValidator { | ||
return value => { | ||
const isValueMissing = value === undefined || value === null || value === ''; | ||
return isValueMissing ? FORM_VALIDATION_VALIDATABLE_FIELD_IS_VALID_SHORT_CIRCUIT : null; | ||
}; | ||
} | ||
|
||
/** | ||
* Not exported because it uses.short circuiting, so it only works inside of a FormValidator | ||
* Use via ValidatableField.spelAware(). | ||
*/ | ||
function spelAwareValidator(): IValidator { | ||
return value => { | ||
const isSpelContent = typeof value === 'string' && value.includes('${'); | ||
return isSpelContent ? FORM_VALIDATION_VALIDATABLE_FIELD_IS_VALID_SHORT_CIRCUIT : null; | ||
}; | ||
} |
44 changes: 44 additions & 0 deletions
44
app/scripts/modules/core/src/presentation/forms/validation/FormValidatorField.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { IFormValidatorField, IValidator } from './validation'; | ||
|
||
/** | ||
* Encapsulates a form field and the validation rules defined for that field. | ||
* | ||
* By default a ValidatableField is optional. | ||
*/ | ||
export class FormValidatorField implements IFormValidatorField { | ||
constructor(public name: string, public label: string) {} | ||
|
||
public isSpelAware: boolean; | ||
public isRequired: boolean; | ||
public isRequiredMessage: string; | ||
public validators: IValidator[] = []; | ||
|
||
/** Causes the field to fail validation if the value is undefined, null, or empty string. */ | ||
public required(message?: string): FormValidatorField { | ||
this.isRequired = true; | ||
this.isRequiredMessage = message; | ||
return this; | ||
} | ||
|
||
/** | ||
* Causes the field to pass validation if the value is undefined, null, or empty string. | ||
* Fields are default by default. | ||
*/ | ||
public optional(): FormValidatorField { | ||
this.isRequired = false; | ||
this.isRequiredMessage = undefined; | ||
return this; | ||
} | ||
|
||
/** Causes the field to pass validation if the value contains SpEL */ | ||
public spelAware(isSpelAware = true): FormValidatorField { | ||
this.isSpelAware = isSpelAware; | ||
return this; | ||
} | ||
|
||
/** Adds additional validators */ | ||
public withValidators(...validators: IValidator[]): FormValidatorField { | ||
this.validators = this.validators.concat(validators); | ||
return this; | ||
} | ||
} |
2 changes: 2 additions & 0 deletions
2
app/scripts/modules/core/src/presentation/forms/validation/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
export * from './validators'; | ||
export * from './validation'; | ||
export * from './FormValidator'; | ||
export * from './FormValidatorField'; |
5 changes: 3 additions & 2 deletions
5
app/scripts/modules/core/src/presentation/forms/validation/validation.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.