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

Add automated empty values sanitization #8188

Merged
merged 17 commits into from
Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions docs/AutocompleteInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,43 @@ When used inside a `<ReferenceInput>`, `<AutocompleteInput>` doesn't need a `cho

See [Using in a `ReferenceInput>`](#using-in-a-referenceinput) below for more information.

## `emptyText`

If the input isn't required (using `validate={required()}`), users can select an empty choice with an empty text `''` as label.

You can override that label with the `emptyText` prop.

```jsx
<AutocompleteInput
source="author_id"
emptyText="No author"
choices={[
{ id: 123, name: 'Leo Tolstoi' },
{ id: 456, name: 'Jane Austen' },
]}
/>
```

The `emptyText` prop accepts either a string or a React Element.

And if you want to hide that empty choice, make the input required.

```jsx
<AutocompleteInput
source="author_id"
choices={[
{ id: 123, name: 'Leo Tolstoi' },
{ id: 456, name: 'Jane Austen' },
]}
validate={required()}
/>
```

## `emptyValue`

An empty choice is always added (with a default `''` value, which you can override with the `emptyValue` prop) on top of the options. You can furthermore customize the empty choice by using the `emptyText` prop, which can receive a string or a React Element.
If the input isn't required (using `validate={required()}`), users can select an empty choice. The default value for that empty choice is the empty string (`''`), or `null` if the input is inside a [`<ReferenceInput>`](./ReferenceInput.md).

You can override this value with the `emptyValue` prop.

```jsx
<AutocompleteInput
Expand All @@ -113,8 +146,7 @@ An empty choice is always added (with a default `''` value, which you can overri
/>
```

**Note**: `emptyValue` cannot be set to `undefined` or `null` since the `dataProvider` method will receive an empty string on submit due to the nature of HTML inputs.

You can furthermore customize the empty choice by using [the `emptyText` prop](#emptytext).

## `optionText`

Expand Down
27 changes: 27 additions & 0 deletions docs/Form.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Here are all the props you can set on the `<Form>` component:
* [`id`](#id)
* [`noValidate`](#novalidate)
* [`onSubmit`](#onsubmit)
* [`sanitizeEmptyValues`](#sanitizeemptyvalues)
fzaninotto marked this conversation as resolved.
Show resolved Hide resolved
* [`validate`](#validate)
* [`warnWhenUnsavedChanges`](#warnwhenunsavedchanges)

Expand Down Expand Up @@ -133,6 +134,32 @@ export const PostCreate = () => {
};
```

## `sanitizeEmptyValues`

In HTML, the value of empty form inputs is the empty string (`''`) by default. React-hook-form doesn't sanitize these values. This leads to unexpected `create` and `update` payloads like:

```jsx
{
id: 123,
title: '',
author: '',
}
```

To avoid that, set the `sanitizeEmptyValues` prop to `true`. This will remove empty strings from the form state on submit, unless the record actually had a value for that field.

```jsx
const PostCreate = () => (
<Create>
<Form sanitizeEmptyValues>
...
</Form>
</Create>
);
```

If you need a more fine-grained control over the sanitization, you can use [the `transform` prop](./Edit.md#transform) of `<Edit>` or `<Create>` components, or [the `parse` prop](./Inputs.md#parse) of individual inputs.

## `validate`

The value of the form `validate` prop must be a function taking the record as input, and returning an object with error messages indexed by field. For instance:
Expand Down
4 changes: 3 additions & 1 deletion docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,15 @@ import { TextInput } from 'react-admin';
const TextInputWithNullEmptyValue = props => (
<TextInput
{...props}
parse={ v => v === '' ? null : v }
parse={v => v === '' ? null : v}
/>
);

export default TextInputWithNullEmptyValue;
```

**Tip**: If you need to do that for every input, use [the `sanitizeEmptyValues` prop of the `<Form>` component](./Form.md#sanitizeemptyvalues) instead.

Let's look at another usage example. Say the user would like to input values of 0-100 to a percentage field but your API (hence record) expects 0-1.0. You can use simple `parse()` and `format()` functions to archive the transform:

```jsx
Expand Down
52 changes: 52 additions & 0 deletions docs/ReferenceInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,32 @@ You can filter the query used to populate the possible values. Use the `filter`
```
{% endraw %}

## `format`

By default, children of `<ReferenceInput>` transform `null` values from the `dataProvider` into empty strings.

If you want to change this behavior, you have to pass a custom `format` prop to the `<ReferenceInput>` *child component*, because **`<ReferenceInput>` doesn't have a `format` prop**. It is the responsibility of the child component to format the input value.

For instance, if you want to transform an option value before rendering, and the selection control is an `<AutocompleteInput>` (the default), set [the `<AutocompleteInput format>` prop](./Inputs.md#format) as follows:

```jsx
import { ReferenceInput, AutocompleteInput } from 'react-admin';

<ReferenceInput source="post_id" reference="posts">
<AutocompleteInput format={value => value == null ? 'not defined' : value} />
</ReferenceInput>
```

The same goes if the child is a `<SelectInput>`:

```jsx
import { ReferenceInput, SelectInput } from 'react-admin';

<ReferenceInput source="post_id" reference="posts">
<SelectInput format={value => value === undefined ? 'not defined' : null} />
</ReferenceInput>
```

## `label`

In an `<Edit>` or `<Create>` view, the `label` prop has no effect. `<ReferenceInput>` has no label, it simply renders its child (an `<AutocompleteInput>` by default). If you need to customize the label, set the `label` prop on the child element:
Expand All @@ -116,6 +142,32 @@ const filters = [
];
```

## `parse`

By default, children of `<ReferenceInput>` transform the empty form value (an empty string) into `null` before passing it to the `dataProvider`.

If you want to change this behavior, you have to pass a custom `parse` prop to the `<ReferenceInput>` *child component*, because **`<ReferenceInput>` doesn't have a `parse` prop**. It is the responsibility of the child component to parse the input value.

For instance, if you want to transform an option value before submission, and the selection control is an `<AutocompleteInput>` (the default), set [the `<AutocompleteInput parse>` prop](./Inputs.md#parse) as follows:

```jsx
import { ReferenceInput, AutocompleteInput } from 'react-admin';

<ReferenceInput source="post_id" reference="posts">
<AutocompleteInput parse={value => value === 'not defined' ? null : value} />
</ReferenceInput>
```

The same goes if the child is a `<SelectInput>`:

```jsx
import { ReferenceInput, SelectInput } from 'react-admin';

<ReferenceInput source="post_id" reference="posts">
<SelectInput parse={value => value === 'not defined' ? undefined : null} />
</ReferenceInput>
```

## `perPage`

By default, `<ReferenceInput>` fetches only the first 25 values. You can extend this limit by setting the `perPage` prop.
Expand Down
53 changes: 45 additions & 8 deletions docs/SelectInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,56 @@ const choices = [
<SelectInput source="contact_id" choices={choices} optionText="full_name" optionValue="_id" disableValue="not_available" />
```

## `emptyValue`
## `emptyText`

If the input isn't required (using `validate={required()}`), users can select an empty choice with an empty text `''` as label.

An empty choice is always added (with a default `''` value, which you can override with the `emptyValue` prop) on top of the options. You can furthermore customize the `MenuItem` for the empty choice by using the `emptyText` prop, which can receive either a string or a React Element, which doesn't receive any props.
You can override that label with the `emptyText` prop.

```jsx
<SelectInput source="category" emptyValue={0} choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
]} />
<SelectInput
source="category"
emptyText="No category selected"
choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
]}
/>
```

The `emptyText` prop accepts either a string or a React Element.

And if you want to hide that empty choice, make the input required.

```jsx
<SelectInput
source="category"
choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
]}
validate={required()}
/>
```

**Note**: `emptyValue` cannot be set to `undefined` or `null` since the `dataProvider` method will receive an empty string on submit due to the nature of HTML inputs.
## `emptyValue`

If the input isn't required (using `validate={required()}`), users can select an empty choice. The default value for that empty choice is the empty string (`''`), or `null` if the input is inside a [`<ReferenceInput>`](./ReferenceInput.md).

You can override this value with the `emptyValue` prop.

```jsx
<SelectInput
source="category"
emptyValue={0}
choices={[
{ id: 'programming', name: 'Programming' },
{ id: 'lifestyle', name: 'Lifestyle' },
{ id: 'photography', name: 'Photography' },
]} />
```

## `options`

Expand Down
75 changes: 38 additions & 37 deletions docs/SimpleForm.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Here are all the props you can set on the `<SimpleForm>` component:
* [`id`](#id)
* [`noValidate`](#novalidate)
* [`onSubmit`](#onsubmit)
* [`sanitizeEmptyValues`](#sanitizeemptyvalues)
* [`sx`](#sx-css-api)
* [`toolbar`](#toolbar)
* [`validate`](#validate)
Expand Down Expand Up @@ -141,6 +142,43 @@ export const PostCreate = () => {
};
```

## `sanitizeEmptyValues`

As a reminder, HTML form inputs always return strings, even for numbers and booleans. So the empty value for a text input is the empty string, not `null` or `undefined`. This means that the data sent to the form handler will contain empty strings:

```jsx
{
id: 1234,
title: 'Lorem Ipsum',
is_published: '',
body: '',
// etc.
}
```

React-hook-form doesn't sanitize these values. If you prefer to omit the keys for empty values, set the `sanitizeEmptyValues` prop to `true`. This will sanitize the form data before passing it to the `dataProvider`, i.e. remove empty strings from the form state, unless the record actually had a value for that field before edition.

```jsx
const PostCreate = () => (
<Create>
<SimpleForm sanitizeEmptyValues>
...
</SimpleForm>
</Create>
);
```

For the previous example, the data sent to the `dataProvider` will be:

```jsx
{
id: 1234,
title: 'Lorem Ipsum',
}
```

If you need a more fine-grained control over the sanitization, you can use [the `transform` prop](./Edit.md#transform) of `<Edit>` or `<Create>` components, or [the `parse` prop](./Inputs.md#parse) of individual inputs.

## `sx`: CSS API

Pass an `sx` prop to customize the style of the main component and the underlying elements.
Expand Down Expand Up @@ -334,43 +372,6 @@ export const TagEdit = () => (
);
```

## Cleaning Up Empty Strings

As a reminder, HTML form inputs always return strings, even for numbers and booleans. So the empty value for a text input is the empty string, not `null` or `undefined`. This means that the data sent to the form handler will contain empty strings:

```js
{
title: '',
average_note: '',
body: '',
// etc.
}
```

If you prefer to have `null` values, or to omit the key for empty values, use `transform` prop of the parent component ([`<Edit>`](./Edit.md#transform) or [`<Create>`](./Create.md#transform)) to sanitize the form data before passing it to the `dataProvider`:

```jsx
export const UserEdit = (props) => {
const transform = (data) => {
const sanitizedData = {};
for (const key in data) {
if (typeof data[key] === "string" && data[key].trim().length === 0) continue;
sanitizedData[key] = data[key];
}
return sanitizedData;
};
return (
<Edit {...props} transform={transform}>
<SimpleForm>
...
</SimpleForm>
</Edit>
);
}
```

As an alternative, you can clean up empty values at the input level, using [the `parse` prop](./Inputs.md#transforming-input-value-tofrom-record).

## Using Fields As Children

The basic usage of `<SimpleForm>` is to pass [Input components](./Inputs.md) as children. For non-editable fields, you can pass `disabled` inputs, or even [Field components](./Fields.md). But since `<Field>` components have no label by default, you'll have to wrap your inputs in a `<Labeled>` component in that case:
Expand Down
Loading