UNPKG

10 kBPlain TextView Raw
1import PropTypes from 'prop-types';
2import React from 'react';
3import FormsyContext from './FormsyContext';
4import {
5 ComponentWithStaticAttributes,
6 FormsyContextInterface,
7 RequiredValidation,
8 ValidationError,
9 Validations,
10 WrappedComponentClass,
11} from './interfaces';
12
13import * as utils from './utils';
14import { isString } from './utils';
15import { isDefaultRequiredValue } from './validationRules';
16
17/* eslint-disable react/default-props-match-prop-types */
18
19const convertValidationsToObject = <V>(validations: false | Validations<V>): Validations<V> => {
20 if (isString(validations)) {
21 return validations.split(/,(?![^{[]*[}\]])/g).reduce((validationsAccumulator, validation) => {
22 let args: string[] = validation.split(':');
23 const validateMethod: string = args.shift();
24
25 args = args.map((arg) => {
26 try {
27 return JSON.parse(arg);
28 } catch (e) {
29 return arg; // It is a string if it can not parse it
30 }
31 });
32
33 if (args.length > 1) {
34 throw new Error(
35 'Formsy does not support multiple args on string validations. Use object format of validations instead.',
36 );
37 }
38
39 // Avoid parameter reassignment
40 const validationsAccumulatorCopy: Validations<V> = { ...validationsAccumulator };
41 validationsAccumulatorCopy[validateMethod] = args.length ? args[0] : true;
42 return validationsAccumulatorCopy;
43 }, {});
44 }
45
46 return validations || {};
47};
48
49export const propTypes = {
50 innerRef: PropTypes.func,
51 name: PropTypes.string.isRequired,
52 required: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.string]),
53 validations: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
54 value: PropTypes.any, // eslint-disable-line react/forbid-prop-types
55};
56
57export interface WrapperProps<V> {
58 innerRef?: (ref: React.Ref<any>) => void;
59 name: string;
60 required?: RequiredValidation<V>;
61 validationError?: ValidationError;
62 validationErrors?: { [key: string]: ValidationError };
63 validations?: Validations<V>;
64 value?: V;
65}
66
67export interface WrapperState<V> {
68 [key: string]: unknown;
69 formSubmitted: boolean;
70 isPristine: boolean;
71 isRequired: boolean;
72 isValid: boolean;
73 pristineValue: V;
74 validationError: ValidationError[];
75 value: V;
76}
77
78export interface InjectedProps<V> {
79 errorMessage: ValidationError;
80 errorMessages: ValidationError[];
81 hasValue: boolean;
82 isFormDisabled: boolean;
83 isFormSubmitted: boolean;
84 isPristine: boolean;
85 isRequired: boolean;
86 isValid: boolean;
87 isValidValue: (value: V) => boolean;
88 ref?: React.Ref<any>;
89 resetValue: () => void;
90 setValidations: (validations: Validations<V>, required: RequiredValidation<V>) => void;
91 setValue: (value: V, validate?: boolean) => void;
92 showError: boolean;
93 showRequired: boolean;
94}
95
96export interface WrapperInstanceMethods<V> {
97 getErrorMessage: () => null | ValidationError;
98 getErrorMessages: () => ValidationError[];
99 getValue: () => V;
100 isFormDisabled: () => boolean;
101 isFormSubmitted: () => boolean;
102 isValid: () => boolean;
103 isValidValue: (value: V) => boolean;
104 setValue: (value: V, validate?: boolean) => void;
105}
106
107export type PassDownProps<V> = WrapperProps<V> & InjectedProps<V>;
108
109function getDisplayName(component: WrappedComponentClass) {
110 return component.displayName || component.name || (utils.isString(component) ? component : 'Component');
111}
112
113export default function withFormsy<T, V>(
114 WrappedComponent: React.ComponentType<T & PassDownProps<V>>,
115): React.ComponentType<Omit<T & WrapperProps<V>, keyof InjectedProps<V>>> {
116 class WithFormsyWrapper
117 extends React.Component<T & WrapperProps<V> & FormsyContextInterface, WrapperState<V>>
118 implements WrapperInstanceMethods<V> {
119 public validations?: Validations<V>;
120
121 public requiredValidations?: Validations<V>;
122
123 public static displayName = `Formsy(${getDisplayName(WrappedComponent)})`;
124
125 public static propTypes: any = propTypes;
126
127 public static defaultProps: any = {
128 innerRef: null,
129 required: false,
130 validationError: '',
131 validationErrors: {},
132 validations: null,
133 value: (WrappedComponent as ComponentWithStaticAttributes).defaultValue,
134 };
135
136 public constructor(props) {
137 super(props);
138 const { runValidation, validations, required, value } = props;
139
140 this.state = { value } as any;
141
142 this.setValidations(validations, required);
143
144 this.state = {
145 formSubmitted: false,
146 isPristine: true,
147 pristineValue: props.value,
148 value: props.value,
149 ...runValidation(this, props.value),
150 };
151 }
152
153 public componentDidMount() {
154 const { name, attachToForm } = this.props;
155
156 if (!name) {
157 throw new Error('Form Input requires a name property when used');
158 }
159
160 attachToForm(this);
161 }
162
163 public shouldComponentUpdate(nextProps, nextState) {
164 const { props, state } = this;
165 const isChanged = (a: object, b: object): boolean => Object.keys(a).some((k) => a[k] !== b[k]);
166 const isPropsChanged = isChanged(props, nextProps);
167 const isStateChanged = isChanged(state, nextState);
168
169 return isPropsChanged || isStateChanged;
170 }
171
172 public componentDidUpdate(prevProps) {
173 const { value, validations, required, validate } = this.props;
174
175 // If the value passed has changed, set it. If value is not passed it will
176 // internally update, and this will never run
177 if (!utils.isSame(value, prevProps.value)) {
178 this.setValue(value);
179 }
180
181 // If validations or required is changed, run a new validation
182 if (!utils.isSame(validations, prevProps.validations) || !utils.isSame(required, prevProps.required)) {
183 this.setValidations(validations, required);
184 validate(this);
185 }
186 }
187
188 // Detach it when component unmounts
189 public componentWillUnmount() {
190 const { detachFromForm } = this.props;
191 detachFromForm(this);
192 }
193
194 public getErrorMessage = (): ValidationError | null => {
195 const messages = this.getErrorMessages();
196 return messages.length ? messages[0] : null;
197 };
198
199 public getErrorMessages = (): ValidationError[] => {
200 const { validationError } = this.state;
201
202 if (!this.isValid() || this.showRequired()) {
203 return validationError || [];
204 }
205 return [];
206 };
207
208 // eslint-disable-next-line react/destructuring-assignment
209 public getValue = (): V => this.state.value;
210
211 public setValidations = (validations: Validations<V>, required: RequiredValidation<V>): void => {
212 // Add validations to the store itself as the props object can not be modified
213 this.validations = convertValidationsToObject(validations) || {};
214 this.requiredValidations =
215 required === true ? { isDefaultRequiredValue: required } : convertValidationsToObject(required);
216 };
217
218 // By default, we validate after the value has been set.
219 // A user can override this and pass a second parameter of `false` to skip validation.
220 public setValue = (value: V, validate = true): void => {
221 const { validate: validateForm } = this.props;
222
223 if (!validate) {
224 this.setState({ value });
225 } else {
226 this.setState(
227 {
228 value,
229 isPristine: false,
230 },
231 () => {
232 validateForm(this);
233 },
234 );
235 }
236 };
237
238 // eslint-disable-next-line react/destructuring-assignment
239 public hasValue = () => {
240 const { value } = this.state;
241 return isDefaultRequiredValue(value);
242 };
243
244 // eslint-disable-next-line react/destructuring-assignment
245 public isFormDisabled = (): boolean => this.props.isFormDisabled;
246
247 // eslint-disable-next-line react/destructuring-assignment
248 public isFormSubmitted = (): boolean => this.state.formSubmitted;
249
250 // eslint-disable-next-line react/destructuring-assignment
251 public isPristine = (): boolean => this.state.isPristine;
252
253 // eslint-disable-next-line react/destructuring-assignment
254 public isRequired = (): boolean => !!this.props.required;
255
256 // eslint-disable-next-line react/destructuring-assignment
257 public isValid = (): boolean => this.state.isValid;
258
259 // eslint-disable-next-line react/destructuring-assignment
260 public isValidValue = (value: V) => this.props.isValidValue(this, value);
261
262 public resetValue = () => {
263 const { pristineValue } = this.state;
264 const { validate } = this.props;
265
266 this.setState(
267 {
268 value: pristineValue,
269 isPristine: true,
270 },
271 () => {
272 validate(this);
273 },
274 );
275 };
276
277 public showError = (): boolean => !this.showRequired() && !this.isValid();
278
279 // eslint-disable-next-line react/destructuring-assignment
280 public showRequired = (): boolean => this.state.isRequired;
281
282 public render() {
283 const { innerRef } = this.props;
284 const propsForElement: T & PassDownProps<V> = {
285 ...this.props,
286 errorMessage: this.getErrorMessage(),
287 errorMessages: this.getErrorMessages(),
288 hasValue: this.hasValue(),
289 isFormDisabled: this.isFormDisabled(),
290 isFormSubmitted: this.isFormSubmitted(),
291 isPristine: this.isPristine(),
292 isRequired: this.isRequired(),
293 isValid: this.isValid(),
294 isValidValue: this.isValidValue,
295 resetValue: this.resetValue,
296 setValidations: this.setValidations,
297 setValue: this.setValue,
298 showError: this.showError(),
299 showRequired: this.showRequired(),
300 value: this.getValue(),
301 };
302
303 if (innerRef) {
304 propsForElement.ref = innerRef;
305 }
306
307 return React.createElement(WrappedComponent, propsForElement);
308 }
309 }
310
311 // eslint-disable-next-line react/display-name
312 return (props) =>
313 React.createElement(FormsyContext.Consumer, null, (contextValue) => {
314 return React.createElement(WithFormsyWrapper, { ...props, ...contextValue });
315 });
316}