Skip to content

Commit

Permalink
feat(core/presentation): Introduce Validation class for reusable vali…
Browse files Browse the repository at this point in the history
…dation fns. (spinnaker#5913)

* feat(core/presentation): Introduce Validation class for reusable validation fns.
  - Support multiple validators and `required` flag in FormikFormField
* feat(core/presentation): Support custom validation messages via ValidationFunctionFactory signature
* Fix typing errors, default "required" error message to `${label} is required` where label comes from the FormField
  • Loading branch information
christopherthielen committed Oct 31, 2018
1 parent 9eacd0c commit f22fecd
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 9 deletions.
12 changes: 12 additions & 0 deletions app/scripts/modules/core/src/presentation/forms/Validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type ValidationFunctionFactory = (message?: string) => ValidationFunction;
export type ValidationFunction = (value: any) => string | Function | Promise<any>;

export class Validation {
public static compose = (...validationFns: ValidationFunction[]): ValidationFunction => {
return (value: any) => validationFns.reduce((error, validationFn) => error || validationFn(value), null);
};

public static isRequired: ValidationFunctionFactory = (message = 'This field is required') => (val: any) => {
return (val === undefined || val === null || val === '') && message;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import * as React from 'react';

import { noop } from 'core/utils';

import { Validation, ValidationFunction } from '../Validation';

import {
ICommonFormFieldProps,
IControlledInputProps,
Expand All @@ -12,7 +14,7 @@ import { StandardFieldLayout } from '../layouts';
import { renderContent } from './renderContent';

export interface IFormFieldValidationProps {
validate?: (value: any) => string;
validate?: ValidationFunction | ValidationFunction[];
}

export type IFormFieldProps = IFormFieldValidationProps &
Expand All @@ -30,20 +32,34 @@ export class FormField extends React.Component<IFormFieldProps> {
name: null,
};

/** Returns validation function composed of all the `validate` functions (and `isRequired` if `required` is truthy) */
/** Returns validation function composed of all the `validate` functions (and `isRequired` if `required` is truthy) */
private composedValidation(
label: IFormFieldProps['label'],
required: boolean,
validate: IFormFieldProps['validate'],
): ValidationFunction {
const labelStr = typeof label === 'string' ? label : 'This Field';
const requiredFn = !!required && Validation.isRequired(`${labelStr} is required`);
const validationFns = [requiredFn].concat(validate).filter(x => !!x);

return validationFns.length ? Validation.compose(...validationFns) : null;
}

public render() {
const { input, layout } = this.props; // ICommonFormFieldProps

const { validate } = this.props; // IFormFieldValidationProps
const { label, help, required, actions } = this.props; // IFieldLayoutPropsWithoutInput
const { touched, validationMessage: message, validationStatus: status } = this.props; // IValidationProps
const { onChange, onBlur, value, name } = this.props; // IControlledInputProps

const fieldLayoutPropsWithoutInput: IFieldLayoutPropsWithoutInput = { label, help, required, actions };
const controlledInputProps: IControlledInputProps = { onChange, onBlur, value, name };

const { touched, validationMessage: message, validationStatus: status } = this.props; // IValidationProps
const validationMessage = message || this.props.validate(value);
const validationMessage = message || this.composedValidation(label, required, validate)(value);
const validationStatus = status || !!validationMessage ? 'error' : null;
const validationProps: IValidationProps = { touched, validationMessage, validationStatus };

const { label, help, required, actions } = this.props; // IFieldLayoutPropsWithoutInput
const fieldLayoutPropsWithoutInput: IFieldLayoutPropsWithoutInput = { label, help, required, actions };

const inputElement = renderContent(input, { field: controlledInputProps, validation: validationProps });
return renderContent(layout, { ...fieldLayoutPropsWithoutInput, ...validationProps, input: inputElement });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { isUndefined } from 'lodash';
import { ICommonFormFieldProps, IFieldLayoutPropsWithoutInput, IValidationProps } from '../interface';
import { StandardFieldLayout } from '../layouts';
import { renderContent } from './renderContent';
import { Validation, ValidationFunction } from '../Validation';

export interface IFormikFieldProps {
name: string;
validate?: (value: any) => string | Function | Promise<any>;
validate?: ValidationFunction | ValidationFunction[];
}

export type IFormikFormFieldProps = IFormikFieldProps & ICommonFormFieldProps & IFieldLayoutPropsWithoutInput;
Expand All @@ -18,6 +19,19 @@ export class FormikFormField extends React.Component<IFormikFormFieldProps> {
layout: StandardFieldLayout,
};

/** Returns validation function composed of all the `validate` functions (and `isRequired` if `required` is truthy) */
private composedValidation(
label: IFormikFormFieldProps['label'],
required: boolean,
validate: IFormikFieldProps['validate'],
): ValidationFunction {
const labelStr = typeof label === 'string' ? label : 'This Field';
const requiredFn = !!required && Validation.isRequired(`${labelStr} is required`);
const validationFns = [requiredFn].concat(validate).filter(x => !!x);

return validationFns.length ? Validation.compose(...validationFns) : null;
}

public render() {
const { input, layout, name, validate } = this.props; // ICommonFieldProps & name & validate
const { label, help, required, actions } = this.props; // IFieldLayoutPropsWithoutInput
Expand All @@ -27,7 +41,7 @@ export class FormikFormField extends React.Component<IFormikFormFieldProps> {
return (
<Field
name={name}
validate={validate}
validate={this.composedValidation(label, required, validate)}
render={(props: FieldProps<any>) => {
const { field, form } = props;

Expand Down

0 comments on commit f22fecd

Please sign in to comment.