diff --git a/Makefile b/Makefile
index 2fdbfaf480a..2d90341142a 100644
--- a/Makefile
+++ b/Makefile
@@ -120,7 +120,7 @@ build-create-react-admin:
@echo "Transpiling create-react-admin files...";
@cd ./packages/create-react-admin && yarn build
-build: build-ra-core build-ra-ui-materialui build-ra-data-fakerest build-ra-data-json-server build-ra-data-localforage build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-ra-i18n-i18next build-react-admin build-ra-no-code build-create-react-admin ## compile ES6 files to JS
+build: build-ra-core build-ra-data-fakerest build-ra-ui-materialui build-ra-data-json-server build-ra-data-localforage build-ra-data-localstorage build-ra-data-simple-rest build-ra-data-graphql build-ra-data-graphql-simple build-ra-i18n-polyglot build-ra-input-rich-text build-data-generator build-ra-language-english build-ra-language-french build-ra-i18n-i18next build-react-admin build-ra-no-code build-create-react-admin ## compile ES6 files to JS
doc: ## compile doc as html and launch doc web server
@yarn doc
diff --git a/docs/Inputs.md b/docs/Inputs.md
index d83e2bf2c91..e40015ace00 100644
--- a/docs/Inputs.md
+++ b/docs/Inputs.md
@@ -43,7 +43,8 @@ All input components accept the following props:
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
| `className` | Optional | `string` | - | Class name (usually generated by JSS) to customize the look and feel of the field element itself |
| `defaultValue` | Optional | `any` | - | Default value of the input. |
-| `disabled` | Optional | `boolean` | - | If true, the input is disabled. |
+| `readOnly` | Optional | `boolean` | `false` | If true, the input is in read-only mode. |
+| `disabled` | Optional | `boolean` | `false` | If true, the input is disabled. |
| `format` | Optional | `Function` | `value => value == null ? '' : value` | Callback taking the value from the form state, and returning the input value. |
| `fullWidth` | Optional | `boolean` | `false` | If `true`, the input will expand to fill the form width |
| `helperText` | Optional | `string` | - | Text to be displayed under the input (cannot be used inside a filter) |
@@ -150,21 +151,29 @@ export const PostCreate = () => (
);
```
+## `readOnly`
+
+The `readOnly` prop set to true makes the element not mutable, meaning the user can not edit the control.
+
+```tsx
+
+```
+
+Contrary to disabled controls, read-only controls are still focusable and are submitted with the form.
+
## `disabled`
-If `true`, the input is disabled and the user can't change the value.
+The `disabled` prop set to true makes the element not mutable, focusable, or even submitted with the form.
```tsx
```
-**Tip**: The form framework used by react-admin, react-hook-form, [considers](https://github.com/react-hook-form/react-hook-form/pull/10805) that a `disabled` input shouldn't submit any value. So react-hook-form sets the value of all `disabled` inputs to `undefined`. As a consequence, a form with a `disabled` input is always considered `dirty` (i.e. react-hook-form considers that the form values and the initial record values are different), and it triggers [the `warnWhenUnsavedChanges` feature](./EditTutorial.md#warning-about-unsaved-changes) when leaving the form, even though the user changed nothing. The workaround is to set the `disabled` prop on the underlying input component, as follows:
+Contrary to read-only controls, disabled controls can not receive focus and are not submitted with the form.
-{% raw %}
-```jsx
-
-```
-{% endraw %}
+**Warning:** Note that `disabled` inputs are **not** included in the form values, and hence may trigger `warnWhenUnsavedChanges` if the input previously had a value in the record.
+
+**Tip:** To include the input in the form values, you can use `readOnly` instead of `disabled`.
## `format`
diff --git a/docs/SimpleFormIterator.md b/docs/SimpleFormIterator.md
index 8fa004b39e1..95876339aa0 100644
--- a/docs/SimpleFormIterator.md
+++ b/docs/SimpleFormIterator.md
@@ -86,6 +86,8 @@ const OrderEdit = () => (
| `inline` | Optional | `boolean` | `false` | When true, inputs are put on the same line |
| `removeButton` | Optional | `ReactElement` | - | Component to render for the remove button |
| `reOrderButtons` | Optional | `ReactElement` | - | Component to render for the up / down button |
+| `readOnly` | Optional | `boolean` | `false` | If true, all inputs are in read-only mode. |
+| `disabled` | Optional | `boolean` | `false` | If true, all inputs are disabled. |
| `sx` | Optional | `SxProps` | - | Material UI shortcut for defining custom styles |
## `addButton`
@@ -354,6 +356,34 @@ const OrderEdit = () => (
);
```
+## `readOnly`
+
+The `readOnly` prop set to true makes the children input not mutable, meaning the user can not edit them.
+
+```jsx
+
+
+
+
+
+```
+
+Contrary to disabled controls, read-only controls are still focusable and are submitted with the form.
+
+## `disabled`
+
+The `disabled` prop set to true makes the children input not mutable, focusable, or even submitted with the form.
+
+```jsx
+
+
+
+
+
+```
+
+Contrary to read-only controls, disabled controls can not receive focus and are not submitted with the form.
+
## `sx`
You can override the style of the root element (a `
` element) as well as those of the inner components thanks to the `sx` property (see [the `sx` documentation](./SX.md) for syntax and examples).
diff --git a/docs/TranslatableInputs.md b/docs/TranslatableInputs.md
index c624872ac33..49a1ba96820 100644
--- a/docs/TranslatableInputs.md
+++ b/docs/TranslatableInputs.md
@@ -197,4 +197,4 @@ You can add validators to any of the inputs inside a `TranslatableInputs`. If an
-```
\ No newline at end of file
+```
diff --git a/packages/ra-core/src/form/useInput.ts b/packages/ra-core/src/form/useInput.ts
index 85decdc4108..b78feff2edf 100644
--- a/packages/ra-core/src/form/useInput.ts
+++ b/packages/ra-core/src/form/useInput.ts
@@ -148,6 +148,8 @@ export type InputProps = Omit<
resource?: string;
source: string;
validate?: Validator | Validator[];
+ readOnly?: boolean;
+ disabled?: boolean;
};
export type UseInputValue = {
diff --git a/packages/ra-input-rich-text/src/RichTextInput.stories.tsx b/packages/ra-input-rich-text/src/RichTextInput.stories.tsx
index 92ded1603de..6ff51f2b453 100644
--- a/packages/ra-input-rich-text/src/RichTextInput.stories.tsx
+++ b/packages/ra-input-rich-text/src/RichTextInput.stories.tsx
@@ -79,6 +79,19 @@ export const Disabled = (props: Partial) => (
);
+export const ReadOnly = (props: Partial) => (
+
+ {}}
+ {...props}
+ >
+
+
+
+
+);
+
export const Small = (props: Partial) => (
(
+
+
+
+
+
+ );
+ }}
+ />
+
+);
+
+export const ReadOnly = () => (
+
+ {
+ return (
+ {
+ console.log(data);
+ },
+ }}
+ >
+
+
+
+
+
+
+
@@ -408,8 +440,8 @@ const BookEditGlobalValidation = () => {
}}
>
- {/*
- We still need `validate={required()}` to indicate fields are required
+ {/*
+ We still need `validate={required()}` to indicate fields are required
with a '*' symbol after the label, but the real validation happens in `globalValidator`
*/}
@@ -438,8 +470,8 @@ const CreateGlobalValidationInFormTab = () => {
}}
>
- {/*
- We still need `validate={required()}` to indicate fields are required
+ {/*
+ We still need `validate={required()}` to indicate fields are required
with a '*' symbol after the label, but the real validation happens in `globalValidator`
*/}
diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
index 35e7af10547..196c1dbd55c 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/ArrayInput.tsx
@@ -83,6 +83,7 @@ export const ArrayInput = (props: ArrayInputProps) => {
validate,
variant,
disabled,
+ readOnly,
margin = 'dense',
...rest
} = props;
@@ -185,7 +186,8 @@ export const ArrayInput = (props: ArrayInputProps) => {
source,
variant,
margin,
- disabled,
+ disabled: children.props.disabled || disabled,
+ readOnly: children.props.readOnly || readOnly,
})}
{renderHelperText ? (
diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx
index e3dac119c2c..701f67d9b0f 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx
@@ -147,7 +147,23 @@ describe('', () => {
render(
-
+
+
+
+
+
+
+
+ );
+
+ expect(screen.queryAllByLabelText('ra.action.add').length).toBe(0);
+ });
+
+ it('should not display add button if readOnly is truthy', () => {
+ render(
+
+
+
diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx
index 068402a70bd..04767455021 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.stories.tsx
@@ -108,6 +108,38 @@ export const Inline = () => (
);
+export const ReadOnly = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const Disabled = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
export const DisableAdd = () => (
diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx
index e2adf566de3..3fc87585479 100644
--- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx
+++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx
@@ -48,6 +48,7 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => {
className,
resource,
source,
+ readOnly,
disabled,
disableAdd = false,
disableClear,
@@ -164,7 +165,7 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => {
className={clsx(
className,
fullWidth && 'fullwidth',
- disabled && 'disabled'
+ (disabled || readOnly) && 'disabled'
)}
sx={sx}
>
@@ -172,7 +173,7 @@ export const SimpleFormIterator = (props: SimpleFormIteratorProps) => {
{fields.map((member, index) => (
{
))}
- {!disabled && !(disableAdd && (disableClear || disableRemove)) && (
-