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

Allow custom add and remove buttons in SimpleFormIterator #4818

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
13 changes: 13 additions & 0 deletions docs/Inputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admi
</ArrayInput>
```

You can also use `addButton` and `removeButton` props to pass your custom add and remove buttons to `SimpleFormIterator`.

```jsx
import { ArrayInput, SimpleFormIterator, DateInput, TextInput } from 'react-admin';

<ArrayInput source="backlinks">
<SimpleFormIterator addButton={<CustomAddButton />} addButton={<CustomRemoveButton />}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a typo here, I read addButton twice

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, we missed it. I opened the PR #5095 to fix it.

<DateInput source="date" />
<TextInput source="url" />
</SimpleFormIterator>
</ArrayInput>
```

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

```jsx
Expand Down
81 changes: 60 additions & 21 deletions packages/ra-ui-materialui/src/form/SimpleFormIterator.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,32 @@ const useStyles = makeStyles(
{ name: 'RaSimpleFormIterator' }
);

const DefaultAddButton = props => {
const classes = useStyles(props);
const translate = useTranslate();
return (
<Button size="small" {...props}>
<AddIcon className={classes.leftIcon} />
{translate('ra.action.add')}
</Button>
);
};

const DefaultRemoveButton = props => {
const classes = useStyles(props);
const translate = useTranslate();
return (
<Button size="small" {...props}>
<CloseIcon className={classes.leftIcon} />
{translate('ra.action.remove')}
</Button>
);
};

const SimpleFormIterator = props => {
const {
addButton = <DefaultAddButton />,
removeButton = <DefaultRemoveButton />,
basePath,
children,
fields,
Expand All @@ -78,7 +102,6 @@ const SimpleFormIterator = props => {
TransitionProps,
defaultValue,
} = props;
const translate = useTranslate();
const classes = useStyles(props);

// We need a unique id for each field for a proper enter/exit animation
Expand Down Expand Up @@ -120,6 +143,25 @@ const SimpleFormIterator = props => {
fields.push(undefined);
};

// add field and call the onClick event of the button passed as addButton prop
const handleAddButtonClick = originalOnClickHandler => event => {
addField();
if (originalOnClickHandler) {
originalOnClickHandler(event);
}
};

// remove field and call the onClick event of the button passed as removeButton prop
const handleRemoveButtonClick = (
originalOnClickHandler,
index
) => event => {
removeField(index)();
if (originalOnClickHandler) {
originalOnClickHandler(event);
}
};

const records = get(record, source);
return fields ? (
<ul className={classes.root}>
Expand Down Expand Up @@ -186,19 +228,16 @@ const SimpleFormIterator = props => {
disableRemove
) && (
<span className={classes.action}>
<Button
className={classNames(
{cloneElement(removeButton, {
onClick: handleRemoveButtonClick(
removeButton.props.onClick,
index
),
className: classNames(
'button-remove',
`button-remove-${source}-${index}`
)}
size="small"
onClick={removeField(index)}
>
<CloseIcon
className={classes.leftIcon}
/>
{translate('ra.action.remove')}
</Button>
),
})}
</span>
)}
</li>
Expand All @@ -208,17 +247,15 @@ const SimpleFormIterator = props => {
{!disableAdd && (
<li className={classes.line}>
<span className={classes.action}>
<Button
className={classNames(
{cloneElement(addButton, {
onClick: handleAddButtonClick(
addButton.props.onClick
),
className: classNames(
'button-add',
`button-add-${source}`
)}
size="small"
onClick={addField}
>
<AddIcon className={classes.leftIcon} />
{translate('ra.action.add')}
</Button>
),
})}
</span>
</li>
)}
Expand All @@ -233,6 +270,8 @@ SimpleFormIterator.defaultProps = {

SimpleFormIterator.propTypes = {
defaultValue: PropTypes.any,
addButton: PropTypes.element,
removeButton: PropTypes.element,
basePath: PropTypes.string,
children: PropTypes.node,
classes: PropTypes.object,
Expand Down
109 changes: 109 additions & 0 deletions packages/ra-ui-materialui/src/form/SimpleFormIterator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,4 +211,113 @@ describe('<SimpleFormIterator />', () => {
).toEqual([{ email: 'bar@foo.com' }]);
});
});

it('should not display the default add button if a custom add button is passed', () => {
const { queryAllByText } = renderWithRedux(
<SimpleForm>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
addButton={<button>Custom Add Button</button>}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);
expect(queryAllByText('ra.action.add').length).toBe(0);
});

it('should not display the default remove button if a custom remove button is passed', () => {
const { queryAllByText } = renderWithRedux(
<SimpleForm record={{ emails: [{ email: '' }] }}>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
removeButton={<button>Custom Remove Button</button>}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);

expect(queryAllByText('ra.action.remove').length).toBe(0);
});

it('should display the custom add button', () => {
const { getByText } = renderWithRedux(
<SimpleForm>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
addButton={<button>Custom Add Button</button>}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);

expect(getByText('Custom Add Button')).toBeDefined();
});

it('should display the custom remove button', () => {
const { getByText } = renderWithRedux(
<SimpleForm record={{ emails: [{ email: '' }] }}>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
removeButton={<button>Custom Remove Button</button>}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);

expect(getByText('Custom Remove Button')).toBeDefined();
});

it('should call the onClick method when the custom add button is clicked', async () => {
const onClick = jest.fn();
const { getByText } = renderWithRedux(
<SimpleForm>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
addButton={
<button onClick={onClick}>Custom Add Button</button>
}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);
fireEvent.click(getByText('Custom Add Button'));
expect(onClick).toHaveBeenCalled();
});

it('should call the onClick method when the custom remove button is clicked', async () => {
const onClick = jest.fn();
const { getByText } = renderWithRedux(
<SimpleForm record={{ emails: [{ email: '' }] }}>
<ArrayInput source="emails">
<SimpleFormIterator
translate={x => x}
removeButton={
<button onClick={onClick}>
Custom Remove Button
</button>
}
>
<TextInput source="email" />
</SimpleFormIterator>
</ArrayInput>
</SimpleForm>
);
fireEvent.click(getByText('Custom Remove Button'));
expect(onClick).toHaveBeenCalled();
});
});