Skip to content

Commit b7459b1

Browse files
committed
Fix nested defaults in SimpleFormIterator
1 parent 28f518c commit b7459b1

File tree

3 files changed

+199
-4
lines changed

3 files changed

+199
-4
lines changed

packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Children, isValidElement, useRef, type ReactNode } from 'react';
2+
import set from 'lodash/set.js';
23
import { FormDataConsumer } from '../../form/FormDataConsumer';
34
import type { ArrayInputContextValue } from './ArrayInputContext';
45
import { useEvent } from '../../util';
@@ -31,15 +32,18 @@ export const useGetArrayInputNewItemDefaults = (
3132

3233
// ArrayInput used for an array of objects
3334
// (e.g. authors: [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Doe' }])
34-
const defaultValue = initialDefaultValue.current;
35+
const defaultValue = { ...initialDefaultValue.current };
3536
Children.forEach(inputs, input => {
3637
if (
3738
isValidElement(input) &&
3839
input.type !== FormDataConsumer &&
3940
input.props.source
4041
) {
41-
defaultValue[input.props.source] =
42-
input.props.defaultValue ?? null;
42+
set(
43+
defaultValue,
44+
input.props.source,
45+
input.props.defaultValue ?? null
46+
);
4347
}
4448
});
4549
return defaultValue;

packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,193 @@ describe('<SimpleFormIterator />', () => {
505505
expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1);
506506
});
507507

508+
it('should not reuse removed values for nested sources when adding a new item', async () => {
509+
render(
510+
<Wrapper>
511+
<SimpleForm
512+
record={{
513+
id: 1,
514+
venueList: [
515+
{
516+
venue: 'Madison Square Garden, New York',
517+
details: {
518+
stageManagerId: '101',
519+
ticketTier: null,
520+
language: null,
521+
},
522+
},
523+
{
524+
venue: 'Wembley Stadium, London',
525+
details: {
526+
stageManagerId: '102',
527+
ticketTier: 'premium',
528+
language: 'en',
529+
},
530+
},
531+
{
532+
venue: 'Tokyo Dome, Tokyo',
533+
details: {
534+
stageManagerId: '103',
535+
ticketTier: 'vip',
536+
language: 'ja',
537+
},
538+
},
539+
],
540+
}}
541+
>
542+
<ArrayInput source="venueList">
543+
<SimpleFormIterator>
544+
<TextInput source="venue" label="Venue" />
545+
<TextInput
546+
source="details.stageManagerId"
547+
label="Stage Manager ID"
548+
/>
549+
<TextInput
550+
source="details.ticketTier"
551+
label="Ticket Tier"
552+
/>
553+
<TextInput
554+
source="details.language"
555+
label="Language"
556+
/>
557+
</SimpleFormIterator>
558+
</ArrayInput>
559+
</SimpleForm>
560+
</Wrapper>
561+
);
562+
563+
await waitFor(() => {
564+
expect(
565+
screen
566+
.queryAllByLabelText('Stage Manager ID')
567+
.map(
568+
inputElement => (inputElement as HTMLInputElement).value
569+
)
570+
).toEqual(['101', '102', '103']);
571+
});
572+
573+
const removeLastButton = getByLabelText(
574+
// @ts-ignore
575+
screen.queryAllByLabelText('Venue')[2].closest('li'),
576+
'ra.action.remove'
577+
).closest('button') as HTMLButtonElement;
578+
579+
fireEvent.click(removeLastButton);
580+
await waitFor(() => {
581+
expect(screen.queryAllByLabelText('Venue').length).toEqual(2);
582+
});
583+
584+
fireEvent.click(
585+
screen
586+
.getByLabelText('ra.action.add')
587+
.closest('button') as HTMLButtonElement
588+
);
589+
590+
await waitFor(() => {
591+
expect(screen.queryAllByLabelText('Venue').length).toEqual(3);
592+
});
593+
594+
expect(
595+
screen
596+
.queryAllByLabelText('Stage Manager ID')
597+
.map(inputElement => (inputElement as HTMLInputElement).value)
598+
).toEqual(['101', '102', '']);
599+
expect(
600+
screen
601+
.queryAllByLabelText('Ticket Tier')
602+
.map(inputElement => (inputElement as HTMLInputElement).value)
603+
).toEqual(['', 'premium', '']);
604+
expect(
605+
screen
606+
.queryAllByLabelText('Language')
607+
.map(inputElement => (inputElement as HTMLInputElement).value)
608+
).toEqual(['', 'en', '']);
609+
});
610+
611+
it('should create nested null defaults for nested sources when adding a new item', async () => {
612+
const save = jest.fn();
613+
614+
render(
615+
<Wrapper>
616+
<SimpleForm
617+
onSubmit={save}
618+
record={{
619+
id: 1,
620+
venueList: [
621+
{
622+
venue: 'Tokyo Dome, Tokyo',
623+
details: {
624+
stageManagerId: '103',
625+
ticketTier: 'vip',
626+
language: 'ja',
627+
},
628+
},
629+
],
630+
}}
631+
>
632+
<ArrayInput source="venueList">
633+
<SimpleFormIterator>
634+
<TextInput source="venue" label="Venue" />
635+
<TextInput
636+
source="details.stageManagerId"
637+
label="Stage Manager ID"
638+
/>
639+
<TextInput
640+
source="details.ticketTier"
641+
label="Ticket Tier"
642+
/>
643+
<TextInput
644+
source="details.language"
645+
label="Language"
646+
/>
647+
</SimpleFormIterator>
648+
</ArrayInput>
649+
</SimpleForm>
650+
</Wrapper>
651+
);
652+
653+
const removeFirstButton = getByLabelText(
654+
// @ts-ignore
655+
screen.queryAllByLabelText('Venue')[0].closest('li'),
656+
'ra.action.remove'
657+
).closest('button') as HTMLButtonElement;
658+
659+
fireEvent.click(removeFirstButton);
660+
await waitFor(() => {
661+
expect(screen.queryAllByLabelText('Venue').length).toEqual(0);
662+
});
663+
664+
fireEvent.click(
665+
screen
666+
.getByLabelText('ra.action.add')
667+
.closest('button') as HTMLButtonElement
668+
);
669+
670+
await waitFor(() => {
671+
expect(screen.queryAllByLabelText('Venue').length).toEqual(1);
672+
});
673+
674+
fireEvent.click(screen.getByText('ra.action.save'));
675+
676+
await waitFor(() => {
677+
expect(save).toHaveBeenCalled();
678+
});
679+
680+
expect(save.mock.calls[0][0]).toEqual({
681+
id: 1,
682+
venueList: [
683+
{
684+
venue: null,
685+
details: {
686+
stageManagerId: null,
687+
ticketTier: null,
688+
language: null,
689+
},
690+
},
691+
],
692+
});
693+
});
694+
508695
it('should remove children row on remove button click', async () => {
509696
const emails = [{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }];
510697

packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react';
22
import type { ReactNode } from 'react';
3+
import { useWatch } from 'react-hook-form';
34
import {
45
type ComponentsOverrides,
56
styled,
@@ -63,7 +64,10 @@ export const SimpleFormIterator = (inProps: SimpleFormIteratorProps) => {
6364
}
6465
const { fields } = useArrayInput(props);
6566
const record = useRecordContext(props);
66-
const records = get(record, finalSource);
67+
const records =
68+
useWatch({
69+
name: finalSource,
70+
}) ?? get(record, finalSource);
6771
const getArrayInputNewItemDefaults =
6872
useGetArrayInputNewItemDefaults(fields);
6973

0 commit comments

Comments
 (0)