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

[RFR] Migrate SelectArrayInput to hooks #3576

Merged
merged 2 commits into from
Aug 23, 2019
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
7 changes: 6 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -784,4 +784,9 @@ const OrderEdit = (props) => (
</SimpleForm>
</Edit>
);
```
```

## helperText is handled the same way in all components

Some components (such as `<SelectArrayInput />`) accepted the `helperText` in their `meta` prop. They now receive it directly in their props.
Besides, all components now display their error or their helperText, but not both at the same time.
307 changes: 145 additions & 162 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import React, { Component } from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
import InputLabel from '@material-ui/core/InputLabel';
import Input from '@material-ui/core/Input';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Chip from '@material-ui/core/Chip';
import { withStyles, createStyles } from '@material-ui/core/styles';
import compose from 'recompose/compose';
import {
makeStyles,
Select,
MenuItem,
InputLabel,
Input,
FormHelperText,
FormControl,
Chip,
} from '@material-ui/core';
import classnames from 'classnames';
import { addField, translate, FieldTitle } from 'ra-core';
import { FieldTitle, useInput, useTranslate } from 'ra-core';
import InputHelperText from './InputHelperText';

const sanitizeRestProps = ({
Expand Down Expand Up @@ -54,21 +55,20 @@ const sanitizeRestProps = ({
...rest
}) => rest;

const styles = theme =>
createStyles({
root: {},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: theme.spacing(1 / 4),
},
select: {
height: 'auto',
overflow: 'auto',
},
});
const useStyles = makeStyles(theme => ({
root: {},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: theme.spacing(1 / 4),
},
select: {
height: 'auto',
overflow: 'auto',
},
}));

/**
* An Input component for a select box allowing multiple selections, using an array of objects for the options
Expand Down Expand Up @@ -122,149 +122,140 @@ const styles = theme =>
* { id: 'photography', name: 'myroot.tags.photography' },
* ];
*/
export class SelectArrayInput extends Component {
/*
* Using state to bypass a redux-form comparison but which prevents re-rendering
* @see https://github.com/erikras/redux-form/issues/2456
*/
state = {
value: this.props.input.value || [],
};

componentWillReceiveProps(nextProps) {
if (nextProps.input.value !== this.props.input.value) {
this.setState({ value: nextProps.input.value || [] });
}
}
const SelectArrayInput = ({
choices,
classes: classesOverride,
className,
label,
helperText,
onBlur,
onChange,
onFocus,
options,
optionText,
optionValue,
resource,
source,
translateChoice,
validate,
...rest
}) => {
const classes = useStyles({ classes: classesOverride });

handleChange = event => {
this.props.input.onChange(event.target.value);
// HACK: For some reason, redux-form does not consider this input touched without calling onBlur manually
this.props.input.onBlur(event.target.value);
this.setState({ value: event.target.value });
};
const translate = useTranslate();

renderMenuItemOption = choice => {
const { optionText, translate, translateChoice } = this.props;
if (React.isValidElement(optionText)) {
return React.cloneElement(optionText, {
record: choice,
});
}
const {
id,
input,
isRequired,
meta: { error, touched },
} = useInput({
onBlur,
onChange,
onFocus,
resource,
source,
validate,
...rest,
});

const choiceName =
typeof optionText === 'function'
? optionText(choice)
: get(choice, optionText);
const renderMenuItemOption = useCallback(
choice => {
if (React.isValidElement(optionText)) {
return React.cloneElement(optionText, {
record: choice,
});
}

return translateChoice
? translate(choiceName, { _: choiceName })
: choiceName;
};
const choiceName =
typeof optionText === 'function'
? optionText(choice)
: get(choice, optionText);

renderMenuItem = choice => {
const { optionValue } = this.props;
return choice ? (
<MenuItem
key={get(choice, optionValue)}
value={get(choice, optionValue)}
>
{this.renderMenuItemOption(choice)}
</MenuItem>
) : null;
};
return translateChoice
? translate(choiceName, { _: choiceName })
: choiceName;
},
[optionText, translate, translateChoice]
);

render() {
const {
choices,
classes,
className,
isRequired,
label,
meta,
options,
resource,
source,
optionText,
optionValue,
helperText,
...rest
} = this.props;
if (typeof meta === 'undefined') {
throw new Error(
"The SelectInput component wasn't called within a redux-form <Field>. Did you decorate it and forget to add the addField prop to your component? See https://marmelab.com/react-admin/Inputs.html#writing-your-own-input-component for details."
);
}
const { touched, error } = meta;
const renderMenuItem = useCallback(
choice => {
return choice ? (
<MenuItem
key={get(choice, optionValue)}
value={get(choice, optionValue)}
>
{renderMenuItemOption(choice)}
</MenuItem>
) : null;
},
[optionValue, renderMenuItemOption]
);

return (
<FormControl
margin="normal"
className={classnames(classes.root, className)}
error={touched && !!error}
{...sanitizeRestProps(rest)}
return (
<FormControl
margin="normal"
className={classnames(classes.root, className)}
error={touched && !!error}
{...sanitizeRestProps(rest)}
>
<InputLabel htmlFor={id}>
<FieldTitle
label={label}
source={source}
resource={resource}
isRequired={isRequired}
/>
</InputLabel>
<Select
autoWidth
multiple
input={<Input id={id} />}
error={!!(touched && error)}
renderValue={selected => (
<div className={classes.chips}>
{selected
.map(item =>
choices.find(
choice => get(choice, optionValue) === item
)
)
.map(item => (
<Chip
key={get(item, optionValue)}
label={renderMenuItemOption(item)}
className={classes.chip}
/>
))}
</div>
)}
data-testid="selectArray"
{...input}
value={input.value || []}
{...options}
>
<InputLabel htmlFor={source}>
<FieldTitle
label={label}
source={source}
resource={resource}
isRequired={isRequired}
{choices.map(renderMenuItem)}
</Select>
{helperText || (touched && error) ? (
<FormHelperText>
<InputHelperText
touched={touched}
error={error}
helperText={helperText}
/>
</InputLabel>
<Select
autoWidth
multiple
input={<Input id={source} />}
value={this.state.value}
error={!!(touched && error)}
renderValue={selected => (
<div className={classes.chips}>
{selected
.map(item =>
choices.find(
choice =>
get(choice, optionValue) === item
)
)
.map(item => (
<Chip
key={get(item, optionValue)}
label={this.renderMenuItemOption(item)}
className={classes.chip}
/>
))}
</div>
)}
data-testid="selectArray"
{...options}
onChange={this.handleChange}
>
{choices.map(this.renderMenuItem)}
</Select>
{helperText || (touched && error) ? (
<FormHelperText>
<InputHelperText
touched={touched}
error={error}
helperText={helperText}
/>
</FormHelperText>
) : null}
</FormControl>
);
}
}
</FormHelperText>
) : null}
</FormControl>
);
};

SelectArrayInput.propTypes = {
choices: PropTypes.arrayOf(PropTypes.object),
classes: PropTypes.object,
className: PropTypes.string,
children: PropTypes.node,
input: PropTypes.object,
isRequired: PropTypes.bool,
label: PropTypes.string,
meta: PropTypes.object,
options: PropTypes.object,
optionText: PropTypes.oneOfType([
PropTypes.string,
Expand All @@ -274,23 +265,15 @@ SelectArrayInput.propTypes = {
optionValue: PropTypes.string.isRequired,
resource: PropTypes.string,
source: PropTypes.string,
translate: PropTypes.func.isRequired,
translateChoice: PropTypes.bool,
};

SelectArrayInput.defaultProps = {
classes: {},
choices: [],
options: {},
optionText: 'name',
optionValue: 'id',
translateChoice: true,
};

const EnhancedSelectArrayInput = compose(
addField,
translate,
withStyles(styles)
)(SelectArrayInput);

export default EnhancedSelectArrayInput;
export default SelectArrayInput;
Loading