1 | import PropTypes from 'prop-types';
|
2 | import React from 'react';
|
3 | import FormsyContext from './FormsyContext';
|
4 | import {
|
5 | ComponentWithStaticAttributes,
|
6 | FormsyContextInterface,
|
7 | RequiredValidation,
|
8 | ValidationError,
|
9 | Validations,
|
10 | WrappedComponentClass,
|
11 | } from './interfaces';
|
12 |
|
13 | import * as utils from './utils';
|
14 | import { isString } from './utils';
|
15 | import { isDefaultRequiredValue } from './validationRules';
|
16 |
|
17 |
|
18 |
|
19 | const 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;
|
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 |
|
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 |
|
49 | export 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,
|
55 | };
|
56 |
|
57 | export 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 |
|
67 | export 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 |
|
78 | export 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 |
|
96 | export 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 |
|
107 | export type PassDownProps<V> = WrapperProps<V> & InjectedProps<V>;
|
108 |
|
109 | function getDisplayName(component: WrappedComponentClass) {
|
110 | return component.displayName || component.name || (utils.isString(component) ? component : 'Component');
|
111 | }
|
112 |
|
113 | export 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 |
|
176 |
|
177 | if (!utils.isSame(value, prevProps.value)) {
|
178 | this.setValue(value);
|
179 | }
|
180 |
|
181 |
|
182 | if (!utils.isSame(validations, prevProps.validations) || !utils.isSame(required, prevProps.required)) {
|
183 | this.setValidations(validations, required);
|
184 | validate(this);
|
185 | }
|
186 | }
|
187 |
|
188 |
|
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 |
|
209 | public getValue = (): V => this.state.value;
|
210 |
|
211 | public setValidations = (validations: Validations<V>, required: RequiredValidation<V>): void => {
|
212 |
|
213 | this.validations = convertValidationsToObject(validations) || {};
|
214 | this.requiredValidations =
|
215 | required === true ? { isDefaultRequiredValue: required } : convertValidationsToObject(required);
|
216 | };
|
217 |
|
218 |
|
219 |
|
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 |
|
239 | public hasValue = () => {
|
240 | const { value } = this.state;
|
241 | return isDefaultRequiredValue(value);
|
242 | };
|
243 |
|
244 |
|
245 | public isFormDisabled = (): boolean => this.props.isFormDisabled;
|
246 |
|
247 |
|
248 | public isFormSubmitted = (): boolean => this.state.formSubmitted;
|
249 |
|
250 |
|
251 | public isPristine = (): boolean => this.state.isPristine;
|
252 |
|
253 |
|
254 | public isRequired = (): boolean => !!this.props.required;
|
255 |
|
256 |
|
257 | public isValid = (): boolean => this.state.isValid;
|
258 |
|
259 |
|
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 |
|
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 |
|
312 | return (props) =>
|
313 | React.createElement(FormsyContext.Consumer, null, (contextValue) => {
|
314 | return React.createElement(WithFormsyWrapper, { ...props, ...contextValue });
|
315 | });
|
316 | }
|