Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
07fffeb
Introduce `<ArrayInputBase>`, `<SimpleFomIteratorBase>` and `<SimpleF…
djhi Sep 23, 2025
2bd511d
Fix build
djhi Sep 23, 2025
fb34fc0
Improve API
djhi Sep 23, 2025
ecaa065
Add some comments [no ci]
djhi Sep 23, 2025
51b93be
Improve API
djhi Sep 24, 2025
ff63ccb
Introduce minimal test ui
djhi Sep 24, 2025
ad72de4
Add ArrayInputBase story
djhi Sep 24, 2025
afacca7
Add headless documentation
djhi Sep 24, 2025
882aa5e
Fix test-ui export
djhi Sep 24, 2025
94f18d4
Rename `onAddItem` to `getItemDefaults`
djhi Sep 25, 2025
62fd603
Fix test-ui exports
djhi Sep 29, 2025
b305858
Cleanup test-ui `<TextInput>`
djhi Sep 29, 2025
76cfb03
Improve `<SimpleFormIteratorItem>` JSDoc
djhi Sep 29, 2025
2187d9c
Use `<ArrayInputBase>` in test-ui
djhi Sep 29, 2025
1b68259
Avoid breaking change in `<SimpleFormIteratorItem>`
djhi Oct 2, 2025
044d631
FIx `<SimpleFormIteratorClearButton>` className prop
djhi Oct 2, 2025
9bc7fea
Merge branch 'next' into array-input-base
djhi Oct 3, 2025
744bd66
Improve documentation
djhi Oct 7, 2025
c2b8f79
Merge branch 'next' into array-input-base
djhi Oct 7, 2025
1ed9b9a
Improve documentation
djhi Oct 7, 2025
69bbd27
Ensure users can override `getItemDefaults` in `<SimpleFormIterator>`
djhi Oct 7, 2025
18e8882
Add a basic story for `SimpleFormIteratorBase`
djhi Oct 7, 2025
78da761
Improve documentation
djhi Oct 7, 2025
6be804c
Remove unnecessary props
djhi Oct 7, 2025
37232dd
Improve SimpleFormIteratorBase documentation
djhi Oct 7, 2025
a6cfd80
Fix ArrayInputBase documentation
djhi Oct 7, 2025
4f55868
Fix breaking change in SimpleFormIterator for clear button
djhi Oct 7, 2025
9476d68
FIx documentation and JSDoc
djhi Oct 7, 2025
84808da
[no ci] remove dead link
slax57 Oct 7, 2025
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
7 changes: 6 additions & 1 deletion docs_headless/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,12 @@ export default defineConfig({
},
{
label: 'Inputs',
items: ['inputs', 'useinput'],
items: [
'inputs',
'useinput',
'arrayinputbase',
'simpleformiteratorbase',
],
},
{
label: 'Preferences',
Expand Down
177 changes: 177 additions & 0 deletions docs_headless/src/content/docs/ArrayInputBase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
layout: default
title: "<ArrayInputBase>"
---

`<ArrayInputBase>` allows editing of embedded arrays, like the `items` field in the following `order` record:

```json
{
"id": 1,
"date": "2022-08-30",
"customer": "John Doe",
"items": [
{
"name": "Office Jeans",
"price": 45.99,
"quantity": 1,
},
{
"name": "Black Elegance Jeans",
"price": 69.99,
"quantity": 2,
},
{
"name": "Slim Fit Jeans",
"price": 55.99,
"quantity": 1,
},
],
}
```

## Usage

`<ArrayInputBase>` expects a single child, which must be a *form iterator* component. A form iterator is a component rendering a field array (the object returned by react-hook-form's [`useFieldArray`](https://react-hook-form.com/docs/usefieldarray)). You can build such component using [the `<SimpleFormIteratorBase>`](./SimpleFormIteratorBase.md).

```tsx
import { ArrayInputBase, EditBase, Form } from 'ra-core';
import { MyFormIterator } from './MyFormIterator';
import { DateInput } from './DateInput';
import { NumberInput } from './NumberInput';
import { TextInput } from './TextInput';

export const OrderEdit = () => (
<EditBase>
<Form>
<DateInput source="date" />
<div>
<div>Items:</div>
<ArrayInputBase source="items">
<MyFormIterator>
<TextInput source="name" />
<NumberInput source="price" />
<NumberInput source="quantity" />
</MyFormIterator>
</ArrayInputBase>
</div>
<button type="submit">Save</button>
</Form>
</EditBase>
)
```

**Note**: Setting [`shouldUnregister`](https://react-hook-form.com/docs/useform#shouldUnregister) on a form should be avoided when using `<ArrayInputBase>` (which internally uses `useFieldArray`) as the unregister function gets called after input unmount/remount and reorder. This limitation is mentioned in the react-hook-form [documentation](https://react-hook-form.com/docs/usecontroller#props). If you are in such a situation, you can use the [`transform`](./EditBase.md#transform) prop to manually clean the submitted values.

## Props

| Prop | Required | Type | Default | Description |
|-----------------| -------- |---------------------------| ------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `source` | Required | `string` | - | Name of the entity property to use for the input value |
| `defaultValue` | Optional | `any` | - | Default value of the input. |
| `validate` | Optional | `Function` &#124; `array` | - | Validation rules for the current property. See the [Validation Documentation](./Validation.md#per-input-validation-built-in-field-validators) for details. |

## Global validation

If you are using an `<ArrayInputBase>` 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).

## Disabling The Input

`<ArrayInputBase>` does not support the `disabled` and `readOnly` props.

If you need to disable the input, make sure the children are either `disabled` and `readOnly`:

```jsx
import { ArrayInputBase, EditBase, Form } from 'ra-core';
import { MyFormIterator } from './MyFormIterator';
import { DateInput } from './DateInput';
import { NumberInput } from './NumberInput';
import { TextInput } from './TextInput';

const OrderEdit = () => (
<EditBase>
<Form>
<TextInput source="customer" />
<DateInput source="date" />
<div>
<div>Items:</div>
<ArrayInputBase source="items">
<MyFormIterator inline disabled>
<TextInput source="name" readOnly/>
<NumberInput source="price" readOnly />
<NumberInput source="quantity" readOnly />
</MyFormIterator>
</ArrayInputBase>
</div>
<button type="submit">Save</button>
</Form>
</EditBase>
);
```

## Changing An Item's Value Programmatically

You can leverage `react-hook-form`'s [`setValue`](https://react-hook-form.com/docs/useform/setvalue) method to change an item's value programmatically.

However you need to know the `name` under which the input was registered in the form, and this name is dynamically generated depending on the index of the item in the array.

To get the name of the input for a given index, you can leverage the `SourceContext` created by react-admin, which can be accessed using the `useSourceContext` hook.

This context provides a `getSource` function that returns the effective `source` for an input in the current context, which you can use as input name for `setValue`.

Here is an example where we leverage `getSource` and `setValue` to change the role of an user to 'admin' when the 'Make Admin' button is clicked:

```tsx
import { ArrayInputBase, useSourceContext } from 'ra-core';
import { useFormContext } from 'react-hook-form';
import { MyFormIterator } from './MyFormIterator';

const MakeAdminButton = () => {
const sourceContext = useSourceContext();
const { setValue } = useFormContext();

const onClick = () => {
// sourceContext.getSource('role') will for instance return
// 'users.0.role'
setValue(sourceContext.getSource('role'), 'admin');
};

return (
<button onClick={onClick}>
Make admin
</button>
);
};

const UserArray = () => (
<ArrayInputBase source="users">
<MyFormIterator inline>
<TextInput source="name" helperText={false} />
<TextInput source="role" helperText={false} />
<MakeAdminButton />
</MyFormIterator>
</ArrayInputBase>
);
```

**Tip:** If you only need the item's index, you can leverage the `useSimpleFormIteratorItem` hook instead.
70 changes: 70 additions & 0 deletions docs_headless/src/content/docs/SimpleFormIteratorBase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
layout: default
title: "<SimpleFormIteratorBase>"
---

`<SimpleFormIteratorBase>` helps building a component that lets users edit, add, remove and reorder sub-records. It is designed to be used as a child of [`<ArrayInputBase>`](./ArrayInputBase.md) or [`<ReferenceManyInputBase>`](https://react-admin-ee.marmelab.com/documentation/ra-core-ee#referencemanyinputbase). You can also use it within an `ArrayInputContext` containing a *field array*, i.e. the value returned by [react-hook-form's `useFieldArray` hook](https://react-hook-form.com/docs/usefieldarray).

## Usage

Here's how one could implement a minimal `SimpleFormIterator` using `<SimpleFormIteratorBase>`:

```tsx
import {
SimpleFormIteratorBase,
SimpleFormIteratorItemBase,
useArrayInput,
useFieldValue,
useSimpleFormIterator,
useSimpleFormIteratorItem,
useWrappedSource,
type SimpleFormIteratorBaseProps
} from 'ra-core';

export const SimpleFormIterator = ({ children, ...props }: SimpleFormIteratorBaseProps) => {
const { fields } = useArrayInput(props);
// Get the parent source by passing an empty string as source
const source = useWrappedSource('');
const records = useFieldValue({ source });

return (
<SimpleFormIteratorBase {...props}>
<ul>
{fields.map((member, index) => (
<SimpleFormIteratorItemBase
key={member.id}
index={index}
record={record}
>
<li>
{children}
<RemoveItemButton />
</li>
</SimpleFormIteratorItemBase>
))}
</ul>
<AddItemButton />
</SimpleFormIteratorBase>
)
}

const RemoveItemButton = () => {
const { remove } = useSimpleFormIteratorItem();
return (
<button type="button" onClick={() => remove()}>Remove</button>
)
}

const AddItemButton = () => {
const { add } = useSimpleFormIterator();
return (
<button type="button" onClick={() => add()}>Add</button>
)
}
```

## Props

| Prop | Required | Type | Default | Description |
|-------------------|----------|----------------|-----------------------|-----------------------------------------------|
| `children` | Optional | `ReactElement` | - | List of inputs to display for each array item |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions packages/ra-core/src/controller/input/ArrayInputBase.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react';
import fakeRestDataProvider from 'ra-data-fakerest';
import { TestMemoryRouter } from '../../routing';
import { EditBase } from '../edit';
import {
Admin,
DataTable,
TextInput,
SimpleFormIterator,
SimpleForm,
} from '../../test-ui';
import { ListBase } from '../list';
import { Resource } from '../../core';
import { ArrayInputBase } from './ArrayInputBase';

export default { title: 'ra-core/controller/input/ArrayInputBase' };

export const Basic = () => (
<TestMemoryRouter initialEntries={['/posts/1']}>
<Admin
dataProvider={fakeRestDataProvider({
posts: [
{
id: 1,
title: 'Post 1',
tags: [
{ name: 'Tag 1', color: 'red' },
{ name: 'Tag 2', color: 'blue' },
],
},
{ id: 2, title: 'Post 2' },
],
})}
>
<Resource
name="posts"
list={
<ListBase>
<DataTable>
<DataTable.Col source="title" />
<DataTable.Col
label="Tags"
render={record =>
record.tags
? record.tags
.map(tag => tag.name)
.join(', ')
: ''
}
/>
</DataTable>
</ListBase>
}
edit={
<EditBase>
<SimpleForm>
<TextInput source="title" />
<div>
<div>Tags:</div>
<ArrayInputBase source="tags">
<SimpleFormIterator>
<TextInput source="name" />
<TextInput source="color" />
</SimpleFormIterator>
</ArrayInputBase>
</div>
</SimpleForm>
</EditBase>
}
/>
</Admin>
</TestMemoryRouter>
);
Loading