Skip to content

Commit dc0f79e

Browse files
authored
Merge pull request #836 from rvsia/warningsValidation
feat(renderer): add useWarnings functionality
2 parents dd4ca01 + 64c3302 commit dc0f79e

File tree

11 files changed

+251
-27
lines changed

11 files changed

+251
-27
lines changed
Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
const composeValidators = (validators = []) => (value, allValues, meta) => {
22
const [initialValidator, ...sequenceValidators] = validators;
3-
const resolveValidator = (error, validator) => error || (typeof validator === 'function' ? validator(value, allValues, meta) : undefined);
4-
if (initialValidator && typeof initialValidator === 'function') {
5-
const result = initialValidator(value, allValues, meta);
6-
if (result && result.then) {
7-
return result.then(() => sequenceValidators.reduce(resolveValidator, undefined)).catch((error) => error);
3+
const resolveValidator = (error, validator) => {
4+
if (error) {
5+
return error;
86
}
7+
8+
if (typeof validator !== 'function') {
9+
return undefined;
10+
}
11+
12+
return validator(value, allValues, meta);
13+
};
14+
15+
const result = resolveValidator(undefined, initialValidator);
16+
17+
if (result?.then) {
18+
return result.then(() => sequenceValidators.reduce(resolveValidator, undefined)).catch((error) => error);
919
}
1020

11-
return validators.reduce(
12-
(error, validator) => error || (typeof validator === 'function' ? validator(value, allValues, meta) : undefined),
13-
undefined
14-
);
21+
return sequenceValidators.reduce(resolveValidator, result);
1522
};
1623

1724
export default composeValidators;

packages/react-form-renderer/src/files/field.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { DataType } from "./data-types";
44
import { AnyObject } from "./common";
55
import { FieldMetaState, FieldInputProps } from "react-final-form";
66
import { FormOptions } from "./renderer-context";
7+
import { Meta } from "./use-field-api";
78

89
export type FieldAction = [string, ...any[]];
910

@@ -12,7 +13,7 @@ export interface FieldActions {
1213
}
1314

1415
export interface FieldApi<FieldValue, T extends HTMLElement = HTMLElement> {
15-
meta: FieldMetaState<FieldValue>;
16+
meta: Meta<FieldValue>;
1617
input: FieldInputProps<FieldValue, T>;
1718
}
1819

packages/react-form-renderer/src/files/use-field-api.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ export interface ValidatorType extends Object {
1010
export interface UseFieldApiConfig extends AnyObject {
1111
name: string;
1212
validate?: ValidatorType[];
13+
useWarnings?: boolean;
1314
}
1415
export interface UseFieldApiComponentConfig extends UseFieldConfig<any> {
1516
name: string;
1617
}
1718

19+
export interface Meta<FieldValue> extends FieldMetaState<FieldValue> {
20+
warning?: any;
21+
}
22+
1823
export interface UseFieldApiProps<
1924
FieldValue,
2025
T extends HTMLElement = HTMLElement> extends AnyObject {
2126
input: FieldInputProps<FieldValue, T>;
22-
meta: FieldMetaState<FieldValue>;
27+
meta: Meta<FieldValue>;
2328
}
2429

2530
export default function<T = any>(options: UseFieldApiConfig): UseFieldApiProps<T>;

packages/react-form-renderer/src/files/use-field-api.js

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useContext, useRef, useReducer } from 'react';
1+
import { useEffect, useContext, useRef, useReducer, useState } from 'react';
22
import { useField } from 'react-final-form';
33
import enhancedOnChange from '../form-renderer/enhanced-on-change';
44
import RendererContext from './renderer-context';
@@ -21,16 +21,34 @@ const calculateArrayValidator = (props, validate, component, validatorMapper) =>
2121
}
2222
};
2323

24-
const calculateValidate = (props, validate, component, validatorMapper) => {
24+
const calculateValidate = (props, validate, component, validatorMapper, setWarning, useWarnings) => {
2525
if ((validate || props.dataType) && componentTypes.FIELD_ARRAY !== component) {
26-
return composeValidators(getValidate(validate, props.dataType, validatorMapper));
26+
const validateFn = composeValidators(getValidate(validate, props.dataType, validatorMapper));
27+
28+
if (useWarnings) {
29+
return async (...args) => {
30+
setWarning(undefined);
31+
32+
const result = await validateFn(...args);
33+
34+
if (result?.type === 'warning') {
35+
setWarning(result.error);
36+
37+
return;
38+
}
39+
40+
return result;
41+
};
42+
}
43+
44+
return validateFn;
2745
}
2846
};
2947

30-
const init = ({ props, validate, component, validatorMapper }) => ({
48+
const init = ({ props, validate, component, validatorMapper, setWarning, useWarnings }) => ({
3149
initialValue: calculateInitialValue(props),
3250
arrayValidator: calculateArrayValidator(props, validate, component, validatorMapper),
33-
validate: calculateValidate(props, validate, component, validatorMapper),
51+
validate: calculateValidate(props, validate, component, validatorMapper, setWarning, useWarnings),
3452
type: assignSpecialType(component)
3553
});
3654

@@ -66,8 +84,9 @@ const createFieldProps = (name, formOptions) => {
6684
};
6785
};
6886

69-
const useFieldApi = ({ name, initializeOnMount, component, render, validate, resolveProps, ...props }) => {
87+
const useFieldApi = ({ name, initializeOnMount, component, render, validate, resolveProps, useWarnings, ...props }) => {
7088
const { validatorMapper, formOptions } = useContext(RendererContext);
89+
const [warning, setWarning] = useState();
7190

7291
const { validate: resolvePropsValidate, ...resolvedProps } = resolveProps
7392
? resolveProps(props, createFieldProps(name, formOptions), formOptions) || {}
@@ -77,7 +96,7 @@ const useFieldApi = ({ name, initializeOnMount, component, render, validate, res
7796

7897
const [{ type, initialValue, validate: stateValidate, arrayValidator }, dispatch] = useReducer(
7998
reducer,
80-
{ props: { ...props, ...resolvedProps }, validate: finalValidate, component, validatorMapper },
99+
{ props: { ...props, ...resolvedProps }, validate: finalValidate, component, validatorMapper, setWarning, useWarnings },
81100
init
82101
);
83102

@@ -108,7 +127,7 @@ const useFieldApi = ({ name, initializeOnMount, component, render, validate, res
108127
if (mounted.current) {
109128
dispatch({
110129
type: 'setValidators',
111-
validate: calculateValidate(enhancedProps, finalValidate, component, validatorMapper),
130+
validate: calculateValidate(enhancedProps, finalValidate, component, validatorMapper, setWarning, useWarnings),
112131
arrayValidator: calculateArrayValidator(enhancedProps, finalValidate, component, validatorMapper)
113132
});
114133
}
@@ -193,7 +212,13 @@ const useFieldApi = ({ name, initializeOnMount, component, render, validate, res
193212
return {
194213
...cleanProps,
195214
...fieldProps,
196-
...(arrayValidator ? { arrayValidator } : {}),
215+
...(arrayValidator && { arrayValidator }),
216+
...(useWarnings && {
217+
meta: {
218+
...fieldProps.meta,
219+
warning
220+
}
221+
}),
197222
input: {
198223
...fieldProps.input,
199224
value:

packages/react-form-renderer/src/files/validator-mapper.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { ValidatorFunction } from "./validators";
2+
13
export interface ValidatorMapper {
2-
[key: string]: (options?: object) => (value: any, allValues: object) => string | undefined;
4+
[key: string]: (options?: object) => ValidatorFunction;
35
}
46

57
declare const validatorMapper: ValidatorMapper;

packages/react-form-renderer/src/files/validators.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface ValidatorConfiguration extends AnyObject {
99
type: string;
1010
message?: string;
1111
msg?: string;
12+
warning?: boolean;
1213
}
1314

1415
export type Validator = ValidatorConfiguration | ValidatorFunction;

packages/react-form-renderer/src/form-renderer/validator-helpers.d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { ValidatorType } from "../files/use-field-api";
22
import { ReactNode } from "react";
33
import { ValidatorMapper } from "../files/validator-mapper";
44
import { DataType } from "../files/data-types";
5+
import { ValidatorFunction } from "../files/validators";
56

6-
export type ValidatorFunction = (value: any, allValues: object) => ReactNode | undefined;
7+
export type convertToWarning = (validator: ValidatorType) => ValidatorFunction;
78

89
export function prepareValidator(
910
validator: ValidatorFunction | ValidatorType,

packages/react-form-renderer/src/form-renderer/validator-helpers.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,22 @@ import { memoize } from '../validators/helpers';
22
import { dataTypeValidator } from '../validators';
33
import composeValidators from '../files/compose-validators';
44

5-
export const prepareValidator = (validator, mapper) =>
6-
typeof validator === 'function' ? memoize(validator) : mapper[validator.type]({ ...validator });
5+
export const convertToWarning = (validator) => (...args) => ({
6+
type: 'warning',
7+
error: validator(...args)
8+
});
9+
10+
export const prepareValidator = (validator, mapper) => {
11+
if (typeof validator === 'function') {
12+
return memoize(validator);
13+
}
14+
15+
if (validator.warning) {
16+
return convertToWarning(mapper[validator.type]({ ...validator }));
17+
}
18+
19+
return mapper[validator.type]({ ...validator });
20+
};
721

822
export const getValidate = (validate, dataType, mapper = {}) => [
923
...(validate ? validate.map((validator) => prepareValidator(validator, mapper)) : []),

packages/react-form-renderer/src/tests/form-renderer/validator.test.js

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
22
import { mount } from 'enzyme';
3+
import { act } from 'react-dom/test-utils';
34

45
import FormRenderer from '../../files/form-renderer';
56
import componentTypes from '../../files/component-types';
67
import FormTemplate from '../../../../../__mocks__/mock-form-template';
78
import useFieldApi from '../../files/use-field-api';
8-
import { act } from 'react-dom/test-utils';
99

1010
describe('FormRenderer validator', () => {
1111
const TextField = (props) => {
@@ -18,11 +18,12 @@ describe('FormRenderer validator', () => {
1818
);
1919
};
2020

21+
const VALUE = 'some-value';
22+
const NAME = 'field1';
23+
2124
it('pass value, allvalues, meta to custom validator func', async () => {
2225
expect.assertions(3);
2326

24-
const VALUE = 'some-value';
25-
const NAME = 'field1';
2627
const META = expect.any(Object);
2728

2829
const validator = (value, allValues, meta) => {
@@ -55,4 +56,106 @@ describe('FormRenderer validator', () => {
5556
wrapper.find('input').simulate('change', { target: { value: VALUE } });
5657
});
5758
});
59+
60+
describe('warning validators', () => {
61+
const TextFieldWarning = (props) => {
62+
const { input, meta, ...rest } = useFieldApi(props);
63+
return (
64+
<div>
65+
<input {...input} {...rest} />
66+
{meta.warning && <div id="warning">{meta.warning}</div>}
67+
</div>
68+
);
69+
};
70+
71+
let wrapper;
72+
73+
it('should not convert object validator to warning when warnings are not used', async () => {
74+
await act(async () => {
75+
wrapper = mount(
76+
<FormRenderer
77+
FormTemplate={FormTemplate}
78+
componentMapper={{
79+
[componentTypes.TEXT_FIELD]: TextFieldWarning
80+
}}
81+
schema={{
82+
fields: [{ component: 'text-field', name: NAME, validate: [{ type: 'required', warning: true }] }]
83+
}}
84+
onSubmit={jest.fn()}
85+
/>
86+
);
87+
});
88+
wrapper.update();
89+
90+
expect(wrapper.find('#warning')).toHaveLength(0);
91+
});
92+
93+
it('should convert object validator to warning', async () => {
94+
await act(async () => {
95+
wrapper = mount(
96+
<FormRenderer
97+
FormTemplate={FormTemplate}
98+
componentMapper={{
99+
[componentTypes.TEXT_FIELD]: TextFieldWarning
100+
}}
101+
schema={{
102+
fields: [{ useWarnings: true, component: 'text-field', name: NAME, validate: [{ type: 'required', warning: true }] }]
103+
}}
104+
onSubmit={jest.fn()}
105+
/>
106+
);
107+
});
108+
wrapper.update();
109+
110+
expect(wrapper.find('#warning').text()).toEqual('Required');
111+
});
112+
113+
it('should convert function validator to warning', async () => {
114+
const ERROR = 'SOME-ERROR';
115+
116+
const customValidator = () => ({ type: 'warning', error: ERROR });
117+
118+
await act(async () => {
119+
wrapper = mount(
120+
<FormRenderer
121+
FormTemplate={FormTemplate}
122+
componentMapper={{
123+
[componentTypes.TEXT_FIELD]: TextFieldWarning
124+
}}
125+
schema={{
126+
fields: [{ useWarnings: true, component: 'text-field', name: NAME, validate: [customValidator] }]
127+
}}
128+
onSubmit={jest.fn()}
129+
/>
130+
);
131+
});
132+
wrapper.update();
133+
134+
expect(wrapper.find('#warning').text()).toEqual(ERROR);
135+
});
136+
137+
it('should convert async function validator to warning', async () => {
138+
const ERROR = 'SOME-ERROR';
139+
140+
const customValidator = () => new Promise((res, rej) => setTimeout(() => rej({ type: 'warning', error: ERROR })));
141+
142+
await act(async () => {
143+
wrapper = mount(
144+
<FormRenderer
145+
FormTemplate={FormTemplate}
146+
componentMapper={{
147+
[componentTypes.TEXT_FIELD]: TextFieldWarning
148+
}}
149+
schema={{
150+
fields: [{ useWarnings: true, component: 'text-field', name: NAME, validate: [customValidator] }]
151+
}}
152+
onSubmit={jest.fn()}
153+
/>
154+
);
155+
});
156+
wrapper.update();
157+
158+
expect(wrapper.find('#warning').text()).toEqual(ERROR);
159+
});
160+
});
58161
});

packages/react-renderer-demo/src/components/navigation/schemas/schema.schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const schemaNav = [
7777
component: 'validator-mapper',
7878
linkText: 'Validator mapper'
7979
},
80+
{
81+
component: 'validator-warnings',
82+
linkText: 'Warnings'
83+
},
8084
{
8185
subHeader: true,
8286
title: 'Condition',

0 commit comments

Comments
 (0)