Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix global validation not firing after submit with ArrayInput #8118

23 changes: 23 additions & 0 deletions docs/ArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,26 @@ Check [the `<SimpleFormIterator>` documentation](./SimpleFormIterator.md) for de

`<ArrayInput>` also accepts the [common input props](./Inputs.md#common-input-props) (except `format` and `parse`).

## Global validation

If you are using an `<ArrayInput>` inside a form with global validation, you need to shape the errors object returned by the `validate` function like an array too.

For instance, to display the following errors:

![ArrayInput global validation](./img/ArrayInput-global-validation.png)

You need to return an errors object shaped like this:

```js
{
authors: [
{},
{
name: 'A name is required',
role: 'ra.validation.required' // translation keys are supported too
},
],
}
```

**Tip:** You can find a sample `validate` function that handles arrays in the [Form Validation documentation](./Validation.md#global-validation).
30 changes: 28 additions & 2 deletions docs/Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,40 @@ const validateUserCreation = (values) => {
args: { min: 18 }
};
}
// You can add a message for a whole ArrayInput
if (!values.children || !values.children.length) {
errors.children = 'ra.validation.required';
} else {
// Or target each child of an ArrayInput by returning an array of error objects
errors.children = values.children.map(child => {
const childErrors = {};
if (!child || !child.firstName) {
childErrors.firstName = 'The firstName is required';
}
if (!child || !child.age) {
childErrors.age = 'ra.validation.required'; // Translation keys are supported here too
}
return childErrors;
});
}
return errors
};

export const UserCreate = () => (
<Create>
<SimpleForm validate={validateUserCreation}>
<TextInput label="First Name" source="firstName" />
<TextInput label="Age" source="age" />
{/*
We need to add `validate={required()}` on required fields to append a '*' symbol
to the label, but the real validation still happens in `validateUserCreation`
*/}
<TextInput label="First Name" source="firstName" validate={required()} />
<TextInput label="Age" source="age" validate={required()} />
<ArrayInput label="Children" source="children" fullWidth validate={required()}>
<SimpleFormIterator>
<TextInput label="First Name" source="firstName" validate={required()} />
<TextInput label="Age" source="age" validate={required()} />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</Create>
);
Expand Down
Binary file added docs/img/ArrayInput-global-validation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
162 changes: 100 additions & 62 deletions packages/ra-core/src/form/getSimpleValidationResolver.spec.ts
Original file line number Diff line number Diff line change
@@ -1,94 +1,79 @@
import {
getSimpleValidationResolver,
flattenErrors,
} from './getSimpleValidationResolver';
import { getSimpleValidationResolver } from './getSimpleValidationResolver';

describe('getSimpleValidationResolver', () => {
const validator = getSimpleValidationResolver(values => values);

it('should return a flattened object', async () => {
const result = flattenErrors({
it('should resolve array values as nested keys', async () => {
const result = await validator({
title: 'title too short',
backlinks: [
{ url: 'url too short', id: 'missing id' },
{ url: 'url too short', id: 'missing id' },
],
});

expect(result).toEqual({
title: 'title too short',
'backlinks.0.url': 'url too short',
'backlinks.0.id': 'missing id',
'backlinks.1.url': 'url too short',
'backlinks.1.id': 'missing id',
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
backlinks: [
{
url: {
type: 'manual',
message: 'url too short',
},
id: {
type: 'manual',
message: 'missing id',
},
},
{
url: {
type: 'manual',
message: 'url too short',
},
id: {
type: 'manual',
message: 'missing id',
},
},
],
},
});
});

it('should support complex translation messages', async () => {
const result = flattenErrors({
it('should treat an empty array value as no error', async () => {
const result = await validator({
title: 'title too short',
body: {
message: 'Not good for %{variable}',
args: {
variable: 'you',
},
},
backlinks: [
{ url: 'url too short', id: 'missing id' },
{ url: 'url too short', id: 'missing id' },
],
backlinks: [],
});

expect(result).toEqual({
title: 'title too short',
body: {
message: 'Not good for %{variable}',
args: {
variable: 'you',
},
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
},
'backlinks.0.url': 'url too short',
'backlinks.0.id': 'missing id',
'backlinks.1.url': 'url too short',
'backlinks.1.id': 'missing id',
});
});

it('should resolve array values as nested keys', async () => {
it('should treat an array with empty objects as no error', async () => {
const result = await validator({
title: 'title too short',
backlinks: [
{ url: 'url too short', id: 'missing id' },
{ url: 'url too short', id: 'missing id' },
],
backlinks: [{}, {}],
});

expect(result).toEqual({
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
'backlinks.0.url': {
type: 'manual',
message: 'url too short',
},
'backlinks.0.id': {
type: 'manual',
message: 'missing id',
},
'backlinks.1.url': {
type: 'manual',
message: 'url too short',
},
'backlinks.1.id': {
type: 'manual',
message: 'missing id',
},
},
});
});

it('should treat an empty array value as no error', async () => {
it('should treat an empty object value as no error', async () => {
const result = await validator({
title: 'title too short',
backlinks: [],
backlinks: {},
});

expect(result).toEqual({
Expand All @@ -99,35 +84,88 @@ describe('getSimpleValidationResolver', () => {
});
});

it('should treat an empty object value as no error', async () => {
it('should resolve nested error objects', async () => {
const result = await validator({
title: 'title too short',
backlinks: {},
comment: {
author: 'author is required',
},
});

expect(result).toEqual({
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
comment: {
author: {
type: 'manual',
message: 'author is required',
},
},
},
});
});

it('should use nested values keys', async () => {
it('should handle RA translation objects', async () => {
const result = await validator({
title: 'title too short',
'backlinks.0.url': 'url too short',
average_note: {
message: 'ra.validation.minValue',
args: { min: 2 },
},
});

expect(result).toEqual({
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
'backlinks.0.url': {
average_note: {
type: 'manual',
message: 'url too short',
message: {
message: 'ra.validation.minValue',
args: { min: 2 },
},
},
},
});
});

it('should handle RA translation objects in arrays', async () => {
const result = await validator({
title: 'title too short',
backlinks: [
{
average_note: {
message: 'ra.validation.minValue',
args: { min: 2 },
},
},
{ id: 'missing id' },
],
});

expect(result).toEqual({
values: {},
errors: {
title: { type: 'manual', message: 'title too short' },
backlinks: [
{
average_note: {
type: 'manual',
message: {
message: 'ra.validation.minValue',
args: { min: 2 },
},
},
},
{
id: {
type: 'manual',
message: 'missing id',
},
},
],
},
});
});
});
Loading