Skip to content
Open
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
7 changes: 5 additions & 2 deletions docs/Forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ const OrderEdit = () => (
);
```

**Tip**: When used inside an `ArrayInput`, `<FormDataConsumer>` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
**Tip**: When used inside an `ArrayInput`, `<FormDataConsumer>` provides two additional properties to its child function: `scopedFormData` and `index`. `scopedFormData` is an object containing the current values of the *currently rendered item*. `index` is the index of the current item in the array. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:

```tsx
import { FormDataConsumer } from 'react-admin';
Expand All @@ -329,6 +329,7 @@ const PostEdit = () => (
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
index, // The index of the current item in the ArrayInput
...rest
}) =>
scopedFormData && scopedFormData.name ? (
Expand All @@ -347,7 +348,9 @@ const PostEdit = () => (
);
```

**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, this parameter will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that this parameter will be defined.
**Tip:** TypeScript users will notice that `scopedFormData` and `index` are typed as optional parameters. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, these parameters will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that these parameters will be defined.

**Tip:** The `index` parameter is useful when you need to know the position of the current item in the array, e.g. to display a row number or to apply conditional logic based on the item's position.

## Hiding Inputs Based On Other Inputs

Expand Down
7 changes: 5 additions & 2 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,7 @@ const OrderEdit = () => (
);
```

**Tip**: When used inside an `<ArrayInput>`, `<FormDataConsumer>` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
**Tip**: When used inside an `<ArrayInput>`, `<FormDataConsumer>` provides two additional properties to its child function: `scopedFormData` and `index`. `scopedFormData` is an object containing the current values of the *currently rendered item*. `index` is the index of the current item in the array. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:

```tsx
import { FormDataConsumer } from 'react-admin';
Expand All @@ -688,6 +688,7 @@ const PostEdit = () => (
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
index, // The index of the current item in the ArrayInput
...rest
}) =>
scopedFormData && scopedFormData.name ? (
Expand All @@ -706,7 +707,9 @@ const PostEdit = () => (
);
```

**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, this parameter will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that this parameter will be defined.
**Tip:** TypeScript users will notice that `scopedFormData` and `index` are typed as optional parameters. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, these parameters will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that these parameters will be defined.

**Tip:** The `index` parameter is useful when you need to know the position of the current item in the array, e.g. to display a row number or to apply conditional logic based on the item's position.

**Tip:** If you need to access the *effective* source of an input inside an `<ArrayInput>`, for example to change the value programmatically using `setValue`, you will need to leverage the [`useSourceContext` hook](./ArrayInput#changing-an-items-value-programmatically).

Expand Down
7 changes: 5 additions & 2 deletions docs/SimpleFormIterator.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ A list of Input elements, that will be rendered on each row.

By default, `<SimpleFormIterator>` renders one input per line, but they can be displayed inline with the `inline` prop.

`<SimpleFormIterator>` also accepts `<FormDataConsumer>` as child. In this case, `<FormDataConsumer>` provides one additional property to its child function called `scopedFormData`. It's an object containing the current values of the *currently rendered item*. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:
`<SimpleFormIterator>` also accepts `<FormDataConsumer>` as child. In this case, `<FormDataConsumer>` provides two additional properties to its child function: `scopedFormData` and `index`. `scopedFormData` is an object containing the current values of the *currently rendered item*. `index` is the index of the current item in the array. This allows you to create dependencies between inputs inside a `<SimpleFormIterator>`, as in the following example:

```jsx
import { FormDataConsumer } from 'react-admin';
Expand All @@ -150,6 +150,7 @@ const PostEdit = () => (
{({
formData, // The whole form data
scopedFormData, // The data for this item of the ArrayInput
index, // The index of the current item in the ArrayInput
}) =>
scopedFormData && scopedFormData.name ? (
<SelectInput
Expand All @@ -166,7 +167,9 @@ const PostEdit = () => (
);
```

**Tip:** TypeScript users will notice that `scopedFormData` is typed as an optional parameter. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, this parameter will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that this parameter will be defined.
**Tip:** TypeScript users will notice that `scopedFormData` and `index` are typed as optional parameters. This is because the `<FormDataConsumer>` component can be used outside of an `<ArrayInput>` and in that case, these parameters will be `undefined`. If you are inside an `<ArrayInput>`, you can safely assume that these parameters will be defined.

**Tip:** The `index` parameter is useful when you need to know the position of the current item in the array, e.g. to display a row number or to apply conditional logic based on the item's position.

**Note**: `<SimpleFormIterator>` only accepts `Input` components as children. If you want to use some `Fields` instead, you have to use a `<FormDataConsumer>`, as follows:

Expand Down
74 changes: 74 additions & 0 deletions packages/ra-core/src/form/FormDataConsumer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,78 @@ describe('FormDataConsumerView', () => {
});
});
});

it('calls its children with the index when inside an ArrayInput', async () => {
let globalIndex;

render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm>
<ArrayInput source="authors">
<SimpleFormIterator>
<FormDataConsumer>
{({ index }) => {
globalIndex = index;
return null;
}}
</FormDataConsumer>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);

expect(globalIndex).toEqual(undefined);

fireEvent.click(screen.getByLabelText('ra.action.add'));

expect(globalIndex).toEqual(0);

fireEvent.click(screen.getByLabelText('ra.action.add'));

expect(globalIndex).toEqual(1);
});

it('calls its children with the correct index when inside nested ArrayInputs', async () => {
let innerIndex: number | undefined;

render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm
defaultValues={{
authors: [
{
books: [{ title: 'Book 1' }],
},
],
}}
>
<ArrayInput source="authors">
<SimpleFormIterator>
<ArrayInput source="books">
<SimpleFormIterator>
<FormDataConsumer>
{({ index }) => {
innerIndex = index;
return null;
}}
</FormDataConsumer>
</SimpleFormIterator>
</ArrayInput>
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);

await waitFor(() => {
// The inner array's first item should have index 0,
// not the outer array's index (also 0 in this case)
expect(innerIndex).toEqual(0);
});
});
});
5 changes: 4 additions & 1 deletion packages/ra-core/src/form/FormDataConsumer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export const FormDataConsumerView = <
// If we have an index, we are in an iterator like component (such as the SimpleFormIterator)
if (arraySource) {
const scopedFormData = get(formData, arraySource);
result = children({ formData, scopedFormData });
const matches = [...arraySource.matchAll(/\d+/g)];
const index = Number(matches[matches.length - 1]?.[0]);
result = children({ formData, scopedFormData, index });
} else {
result = children({ formData });
}
Expand All @@ -98,6 +100,7 @@ export interface FormDataConsumerRenderParams<
> {
formData: TFieldValues;
scopedFormData?: TScopedFieldValues;
index?: number;
}

export type FormDataConsumerRender<
Expand Down