diff --git a/src/Wrapper.ts b/src/Wrapper.ts index 6e94239e..c7c01a65 100644 --- a/src/Wrapper.ts +++ b/src/Wrapper.ts @@ -3,12 +3,19 @@ import PropTypes from 'prop-types'; import * as utils from './utils'; import FormsyContext from './FormsyContext'; -import { ComponentWithStaticAttributes, RequiredValidation, Validations, WrappedComponentClass } from './interfaces'; +import { + ComponentWithStaticAttributes, + RequiredValidation, + ValidationError, + Validations, + WrappedComponentClass, +} from './interfaces'; +import { isString } from './utils'; /* eslint-disable react/default-props-match-prop-types */ const convertValidationsToObject = (validations: false | Validations): Validations => { - if (typeof validations === 'string') { + if (isString(validations)) { return validations.split(/,(?![^{[]*[}\]])/g).reduce((validationsAccumulator, validation) => { let args: string[] = validation.split(':'); const validateMethod: string = args.shift(); @@ -49,8 +56,8 @@ export interface WrapperProps { innerRef?: (ref: React.Ref) => void; name: string; required?: RequiredValidation; - validationError?: string; - validationErrors?: { [key: string]: string }; + validationError?: ValidationError; + validationErrors?: { [key: string]: ValidationError }; validations?: Validations; value?: V; } @@ -63,13 +70,13 @@ export interface WrapperState { isRequired: boolean; isValid: boolean; pristineValue: V; - validationError: string[]; + validationError: ValidationError[]; value: V; } export interface InjectedProps { - errorMessage: string; - errorMessages: string[]; + errorMessage: ValidationError; + errorMessages: ValidationError[]; hasValue: boolean; isFormDisabled: boolean; isFormSubmitted: boolean; @@ -85,10 +92,13 @@ export interface InjectedProps { showRequired: boolean; } -export interface WrapperInstanceMethods { +export interface WrapperInstanceMethods { + getErrorMessage: () => null | ValidationError; + getErrorMessages: () => ValidationError[]; + getValue: () => V; + isFormDisabled: () => boolean; isValid: () => boolean; - getValue: () => any; - getErrorMessage: () => null | string; + setValue: (value: V) => void; } export type PassDownProps = WrapperProps & InjectedProps; @@ -102,7 +112,7 @@ function getDisplayName(component: WrappedComponentClass) { export default function( WrappedComponent: React.ComponentType>, ): React.ComponentType, keyof InjectedProps>> { - return class extends React.Component, WrapperState> implements WrapperInstanceMethods { + return class extends React.Component, WrapperState> implements WrapperInstanceMethods { // eslint-disable-next-line react/sort-comp public static contextType = FormsyContext; @@ -155,11 +165,10 @@ export default function( public shouldComponentUpdate(nextProps, nextState, nextContext) { const { props, state, context } = this; - const isPropsChanged = Object.keys(props).some(k => props[k] !== nextProps[k]); - - const isStateChanged = Object.keys(state).some(k => state[k] !== nextState[k]); - - const isFormsyContextChanged = Object.keys(context).some(k => context[k] !== nextContext[k]); + const isChanged = (a: object, b: object): boolean => Object.keys(a).some(k => a[k] !== b[k]); + const isPropsChanged = isChanged(props, nextProps); + const isStateChanged = isChanged(state, nextState); + const isFormsyContextChanged = isChanged(context, nextContext); return isPropsChanged || isStateChanged || isFormsyContextChanged; } @@ -182,18 +191,17 @@ export default function( } // Detach it when component unmounts - // eslint-disable-next-line react/sort-comp public componentWillUnmount() { const { detachFromForm } = this.context; detachFromForm(this); } - public getErrorMessage = () => { + public getErrorMessage = (): ValidationError | null => { const messages = this.getErrorMessages(); return messages.length ? messages[0] : null; }; - public getErrorMessages = () => { + public getErrorMessages = (): ValidationError[] => { const { externalError, validationError } = this.state; if (!this.isValid() || this.showRequired()) { @@ -218,9 +226,7 @@ export default function( const { validate: validateForm } = this.context; if (!validate) { - this.setState({ - value, - }); + this.setState({ value }); } else { this.setState( { @@ -237,7 +243,7 @@ export default function( // eslint-disable-next-line react/destructuring-assignment public hasValue = () => { const { value } = this.state; - if (typeof value === 'string') { + if (isString(value)) { return value !== ''; } return value !== undefined; diff --git a/src/interfaces.ts b/src/interfaces.ts index bd89aa4f..ef3c0d79 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -10,7 +10,9 @@ export type IResetModel = (model?: IModel) => void; export type IUpdateInputsWithValue = (values: { [key: string]: V }, validate?: boolean) => void; export type IUpdateInputsWithError = (errors: { [key: string]: string }, invalidate?: boolean) => void; -export type ValidationFunction = (values: Values, value: V, extra?: any) => boolean | string; +export type ValidationError = string | React.ReactNode; + +export type ValidationFunction = (values: Values, value: V, extra?: any) => boolean | ValidationError; export type Validation = string | boolean | ValidationFunction; diff --git a/src/utils.ts b/src/utils.ts index bac5cd85..a176e624 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import isPlainObject from 'lodash.isplainobject'; -import { Validations, Values } from './interfaces'; +import { ValidationError, Validations, Values } from './interfaces'; export function isArray(value: unknown): value is unknown[] { return Array.isArray(value); @@ -86,7 +86,7 @@ export function isSame(a: unknown, b: unknown) { } interface RulesResult { - errors: string[]; + errors: ValidationError[]; failed: string[]; success: string[]; }