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 5 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 />}>
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
<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 add button if custom addButton is passed', () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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 remove button if custom removeButton is passed', () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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 custom add button if custom addButton is passed', () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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 custom remove button if custom removeButton is passed', () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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 onClick method, when custom add button is clicked', async () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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 onClick method, when custom remove button is clicked', async () => {
Luwangel marked this conversation as resolved.
Show resolved Hide resolved
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();
});
});