Skip to content

Commit 7f6b3ca

Browse files
authored
Merge pull request #380 from data-driven-forms/async-validator
breaking(renderer): Allow combining async validators with normal ones.
2 parents 174b076 + de2a21d commit 7f6b3ca

File tree

7 files changed

+148
-59
lines changed

7 files changed

+148
-59
lines changed

packages/react-form-renderer/demo/form-fields-mapper.js

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@ import useFieldApi from '../src/hooks/use-field-api';
77
const TextField = (props) => (
88
<FieldProvider
99
{...props}
10-
render={({ input, meta, isVisible, label, helperText, isRequired, dataType, isDisabled, isReadOnly, ...rest }) => (
11-
<div>
12-
<label>{label} &nbsp;</label>
13-
<input {...input} {...rest} />
14-
{meta.error && (
15-
<div>
16-
<span>{meta.error}</span>
17-
</div>
18-
)}
19-
</div>
20-
)}
10+
render={({ input, meta, isVisible, label, helperText, isRequired, dataType, isDisabled, isReadOnly, ...rest }) => {
11+
return (
12+
<div>
13+
<label>{label} &nbsp;</label>
14+
<input {...input} {...rest} />
15+
{meta.error && (
16+
<div>
17+
<span>{meta.error}</span>
18+
</div>
19+
)}
20+
</div>
21+
);
22+
}}
2123
/>
2224
);
2325

packages/react-form-renderer/demo/index.js

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,65 @@
11
/* eslint-disable camelcase */
2-
import React, { useState } from 'react';
2+
import React from 'react';
33
import ReactDOM from 'react-dom';
4-
import FormRenderer from '../src';
4+
import FormRenderer, { validatorTypes } from '../src';
55
import componentMapper from './form-fields-mapper';
66
import FormTemplate from './form-template';
7-
import sandboxSchema from './sandbox';
7+
// import sandboxSchema from './sandbox';
88

99
const intl = (name) => `translated ${name}`;
1010

1111
const actionMapper = {
12-
loadData: (data) => () => new Promise((resolve) => setTimeout(() => resolve({ custom: 'ererewr', ...data }), 1700)),
12+
loadData: (data) => (...args) =>
13+
new Promise((resolve) => {
14+
setTimeout(() => resolve({ custom: 'ererewr', ...data }), 1700);
15+
}),
1316
loadLabel: intl
1417
};
1518

19+
const validatorMapper = {
20+
asyncValidator: (url, attributes) => (value, allValues) =>
21+
new Promise((resolve, reject) =>
22+
setTimeout(() => {
23+
if (value === 'error') {
24+
reject('Async validation failed');
25+
}
26+
27+
resolve('hola');
28+
}, 1700)
29+
)
30+
};
31+
32+
const asyncValidatorSchema = {
33+
fields: [
34+
{
35+
component: 'text-field',
36+
name: 'async-validation-field',
37+
label: 'Async validation field',
38+
validate: [
39+
{ type: 'asyncValidator' },
40+
{ type: 'required-validator' },
41+
{
42+
type: validatorTypes.PATTERN_VALIDATOR,
43+
pattern: '^Foo$',
44+
flags: 'i'
45+
}
46+
]
47+
}
48+
]
49+
};
50+
1651
const App = () => {
17-
const [values, setValues] = useState({});
52+
// const [values, setValues] = useState({});
1853
return (
1954
<div style={{ padding: 20 }}>
2055
<FormRenderer
21-
initialValues={{
22-
text_box_1: 'hue',
23-
text_box_3: 'initial'
24-
}}
25-
clearedValue={'bla'}
56+
validatorMapper={validatorMapper}
2657
componentMapper={componentMapper}
2758
onSubmit={(values) => console.log(values)}
28-
onCancel={console.log}
29-
canReset
30-
onReset={() => console.log('i am resseting')}
31-
schema={sandboxSchema}
32-
debug={(state) => setValues(state.values)}
59+
schema={asyncValidatorSchema}
3360
FormTemplate={FormTemplate}
3461
actionMapper={actionMapper}
3562
/>
36-
<div>{JSON.stringify(values, null, 2)}</div>
3763
</div>
3864
);
3965
};
Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1-
const composeValidators = (validators = []) => (value, allValues) =>
2-
validators.reduce((error, validator) => error || (typeof validator === 'function' ? validator(value, allValues) : undefined), undefined);
1+
const composeValidators = (validators = []) => (value, allValues) => {
2+
const [initialValidator, ...sequenceValidators] = validators;
3+
const resolveValidator = (error, validator) => error || (typeof validator === 'function' ? validator(value, allValues) : undefined);
4+
if (initialValidator && typeof initialValidator === 'function') {
5+
const result = initialValidator(value, allValues);
6+
if (result && result.then) {
7+
return result.then(() => sequenceValidators.reduce(resolveValidator, undefined)).catch((error) => error);
8+
}
9+
}
10+
11+
return validators.reduce((error, validator) => error || (typeof validator === 'function' ? validator(value, allValues) : undefined), undefined);
12+
};
313

414
export default composeValidators;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import composeValidators from '../../components/compose-validators';
2+
3+
describe('Compose validators', () => {
4+
const syncValidator = (value) => (value === 'sync-error' ? 'sync-error' : undefined);
5+
const asyncValidator = (value, allValues) =>
6+
new Promise((resolve, reject) =>
7+
setTimeout(() => {
8+
if (value === 'error') {
9+
reject('Async validation failed');
10+
}
11+
12+
resolve('hola');
13+
})
14+
);
15+
16+
it('should pass async validator', () => {
17+
return composeValidators([asyncValidator])('Good value').then((result) => {
18+
expect(result).toBeUndefined();
19+
});
20+
});
21+
22+
it('should fail async validator', () => {
23+
return composeValidators([asyncValidator])('error').then((result) => {
24+
expect(result).toBe('Async validation failed');
25+
});
26+
});
27+
28+
it('should pass async validator but fail sync sequence', () => {
29+
return composeValidators([asyncValidator, syncValidator])('sync-error').then((result) => {
30+
expect(result).toBe('sync-error');
31+
});
32+
});
33+
34+
it('should fail async validator before running sync', () => {
35+
return composeValidators([asyncValidator, syncValidator])('error').then((result) => {
36+
expect(result).toBe('Async validation failed');
37+
});
38+
});
39+
40+
it('should pass async and sync validation', () => {
41+
return composeValidators([asyncValidator, syncValidator])('good').then((result) => {
42+
expect(result).toBeUndefined();
43+
});
44+
});
45+
});

packages/react-renderer-demo/src/app/pages/renderer/migration-guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,10 @@ import { componentMapper } from '@data-driven-forms/pf4-component-mapper'
141141
- "select-field" -> "select"
142142
- removed duplicate constants SELECT_COMPONENT etc.
143143
144+
### Async validators changed
145+
- Failed async validator must now throw an error. Error must be a string! Thrown string will be shown as a validation message in form.
146+
- Succesfully resolved promise with message is ignored.
147+
144148
### PF4/PF3/MUI Wizard doesn't use 'stepKey' anymore
145149
146150
- stepKey prop is replaced by name

packages/react-renderer-demo/src/app/pages/renderer/validators.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ The function takes `value` as an argument and should return undefined when pases
4646

4747
## Async validator
4848

49-
You can use a Async function as a validator. However, the returned promise will overwrite all other validators
50-
(because it is returned last),
51-
so you need combine all validators into one function.
49+
You can use a Async function as a validator. But it **must be first in the validate array**. Other async validators will be ignored. This rule was created to prevent long asynchronous validation sequences.
50+
51+
You can either use custom function, or custom validator from validator mapper.
5252

5353
<RawComponent source="validators/async-validator" />
5454

@@ -83,7 +83,7 @@ const schema = {
8383

8484
```
8585

86-
It is designed to return functions returning functions, so you can easily cached or debounce results.
86+
Validator in a mapper must be a function which returns a function. This makes validator easily configurable (different messages for same validator).
8787

8888
The higher order function receives the whole validator object.
8989

packages/react-renderer-demo/src/app/src/doc-components/validators/async-validator.js

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,43 @@
11
import React from 'react';
22
import FormRenderer, { componentTypes } from '@data-driven-forms/react-form-renderer';
33
import { FormTemplate, componentMapper } from '@data-driven-forms/pf4-component-mapper';
4-
const mockEndpoint = value => new Promise((resolve, reject) => {
5-
setTimeout(() => {
6-
if (value === 'John') {
7-
reject({ message: 'John is not allowed' });
8-
}
4+
const mockEndpoint = (value) =>
5+
new Promise((resolve, reject) => {
6+
setTimeout(() => {
7+
if (value === 'John') {
8+
reject({ message: 'John is not allowed' });
9+
}
910

10-
resolve({ message: 'validation sucesfull' });
11-
}, 2000);
12-
});
11+
resolve({ message: 'validation sucesfull' });
12+
}, 2000);
13+
});
1314

14-
const asyncValidator = value => mockEndpoint(value).then(response => {
15-
//do something with response but do not return any value!
16-
}).catch(error => {
17-
return error.message;
18-
});
15+
const asyncValidator = (value) =>
16+
mockEndpoint(value)
17+
.catch(({ message }) => {
18+
// error must only throw valid react child eg: string, number, node, etc.
19+
throw message;
20+
})
21+
.then(() => {
22+
// possible success handler
23+
});
1924

2025
const schema = {
2126
title: 'Start typing',
22-
fields: [{
23-
component: componentTypes.TEXT_FIELD,
24-
name: 'async-validator',
25-
label: 'Async validator',
26-
helperText: 'Type name John to fail validation. Validation will take 2 seconds.',
27-
validate: [ asyncValidator ],
28-
}],
27+
fields: [
28+
{
29+
component: componentTypes.TEXT_FIELD,
30+
name: 'async-validator',
31+
label: 'Async validator',
32+
helperText: 'Type name John to fail validation. Validation will take 2 seconds.',
33+
validate: [asyncValidator]
34+
}
35+
]
2936
};
3037

3138
const AsyncValidator = () => (
3239
<div className="pf4">
33-
<FormRenderer
34-
FormTemplate={ FormTemplate }
35-
componentMapper={ componentMapper }
36-
schema={ schema }
37-
onSubmit={ console.log }
38-
/>
40+
<FormRenderer FormTemplate={FormTemplate} componentMapper={componentMapper} schema={schema} onSubmit={console.log} />
3941
</div>
4042
);
4143

0 commit comments

Comments
 (0)