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 SelectInput to use useInput #3526

Merged
merged 4 commits into from
Aug 14, 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
14 changes: 7 additions & 7 deletions examples/simple/src/comments/PostReferenceInput.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Fragment, useState, useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { Field } from 'react-final-form';
import { FormSpy } from 'react-final-form';

import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
Expand Down Expand Up @@ -93,10 +93,10 @@ const PostReferenceInput = props => {
>
{translate('ra.action.create')}
</Button>
<Field
name="post_id"
component={({ input }) =>
input.value && (
<FormSpy
subscription={{ values: true }}
render={({ values }) =>
values.post_id ? (
<Fragment>
<Button
data-testid="button-show-post"
Expand All @@ -117,7 +117,7 @@ const PostReferenceInput = props => {
</DialogTitle>
<DialogContent>
<PostPreview
id={input.value}
id={values.post_id}
basePath="/posts"
resource="posts"
/>
Expand All @@ -132,7 +132,7 @@ const PostReferenceInput = props => {
</DialogActions>
</Dialog>
</Fragment>
)
) : null
}
/>
<Dialog
Expand Down
14 changes: 14 additions & 0 deletions packages/ra-core/src/form/useInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
useField as useFinalFormField,
FieldProps as FinalFormFieldProps,
FieldRenderProps,
FieldInputProps,
} from 'react-final-form';
import { Validator, composeValidators } from './validate';
import isRequired from './isRequired';
Expand All @@ -21,6 +22,8 @@ export interface InputProps<T = any>
onChange?: (event: ChangeEvent | any) => void;
onFocus?: (event: FocusEvent) => void;
options?: T;
input?: FieldInputProps<any, HTMLElement>;
meta?: any;
}

interface ComputedInputProps extends FieldRenderProps<any, HTMLElement> {
Expand Down Expand Up @@ -86,6 +89,17 @@ const useInput = ({
[onFocus, customOnFocus]
);

// If there is an input prop, this input has already been enhanced by final-form
// This is required in for inputs used inside other inputs (such as the SelectInput inside a ReferenceInput)
if (options.input) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Not sure of the performances implications of calling the useField from final-form at L51 but ignoring its result here. Final-form should be optimized and we don't have any other choice to comply to the hooks rules anyway

return {
id: id || source,
input: options.input,
meta: options.meta,
isRequired: isRequired(validate),
};
}

return {
id: id || source,
input: {
Expand Down
5 changes: 0 additions & 5 deletions packages/ra-ui-materialui/src/input/DateTimeInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,7 @@ export const DateTimeInput: FunctionComponent<
};

DateTimeInput.propTypes = {
classes: PropTypes.object,
className: PropTypes.string,
input: PropTypes.object,
isRequired: PropTypes.bool,
label: PropTypes.string,
meta: PropTypes.object,
options: PropTypes.object,
resource: PropTypes.string,
source: PropTypes.string,
Expand Down
103 changes: 37 additions & 66 deletions packages/ra-ui-materialui/src/input/SelectInput.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { useState, useCallback, useEffect } from 'react';
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
import get from 'lodash/get';
import MenuItem from '@material-ui/core/MenuItem';
import { withStyles, createStyles } from '@material-ui/core/styles';
import compose from 'recompose/compose';
import { addField, translate, FieldTitle, useTranslate } from 'ra-core';
import { makeStyles } from '@material-ui/core/styles';
import { useInput, FieldTitle, useTranslate } from 'ra-core';

import ResettableTextField from './ResettableTextField';
import InputHelperText from './InputHelperText';
Expand Down Expand Up @@ -50,12 +49,11 @@ const sanitizeRestProps = ({
...rest
}) => rest;

const styles = theme =>
createStyles({
input: {
minWidth: theme.spacing(20),
},
});
const useStyles = makeStyles(theme => ({
input: {
minWidth: theme.spacing(20),
},
}));

/**
* An Input component for a select box, using an array of objects for the options
Expand Down Expand Up @@ -131,60 +129,50 @@ const styles = theme =>
* <SelectInput source="gender" choices={choices} disableValue="not_available" />
*
*/
export const SelectInput = ({
const SelectInput = ({
allowEmpty,
choices,
classes,
className,
disableValue,
emptyText,
emptyValue,
helperText,
input,
isRequired,
label,
meta,
onBlur,
onChange,
onFocus,
options,
optionText,
optionValue,
resource,
source,
translateChoice,
validate,
...rest
}) => {
/*
* Using state to bypass a redux-form comparison but which prevents re-rendering
* @see https://github.com/erikras/redux-form/issues/2456
*/
const [value, setValue] = useState(input.value);
const translate = useTranslate();
const classes = useStyles({});

useEffect(() => {
setValue(input.value);
}, [input]);

const handleChange = useCallback(
eventOrValue => {
const value = eventOrValue.target
? eventOrValue.target.value
: eventOrValue;
input.onChange(value);

// HACK: For some reason, redux-form does not consider this input touched without calling onBlur manually
input.onBlur(value);
setValue(value);
},
[input, setValue]
);
const {
id,
input,
isRequired,
meta: { error, touched },
} = useInput({
onBlur,
onChange,
onFocus,
resource,
source,
validate,
...rest,
});

const renderEmptyItemOption = useCallback(
emptyText => {
return React.isValidElement(emptyText)
? React.cloneElement(emptyText)
: translate(emptyText, { _: emptyText });
},
[emptyText, translate]
);
const renderEmptyItemOption = useCallback(() => {
return React.isValidElement(emptyText)
? React.cloneElement(emptyText)
: translate(emptyText, { _: emptyText });
}, [emptyText, translate]);

const renderMenuItemOption = useCallback(
choice => {
Expand All @@ -206,18 +194,12 @@ export const SelectInput = ({
[optionText, translate, translateChoice]
);

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;

return (
<ResettableTextField
id={id}
{...input}
select
margin="normal"
value={value}
label={
<FieldTitle
label={label}
Expand All @@ -226,7 +208,6 @@ export const SelectInput = ({
isRequired={isRequired}
/>
}
name={input.name}
className={`${classes.input} ${className}`}
clearAlwaysVisible
error={!!(touched && error)}
Expand All @@ -239,11 +220,10 @@ export const SelectInput = ({
}
{...options}
{...sanitizeRestProps(rest)}
onChange={handleChange}
>
{allowEmpty ? (
<MenuItem value={emptyValue} key="null">
{renderEmptyItemOption(emptyText)}
{renderEmptyItemOption()}
</MenuItem>
) : null}
{choices.map(choice => (
Expand All @@ -266,10 +246,7 @@ SelectInput.propTypes = {
choices: PropTypes.arrayOf(PropTypes.object),
classes: PropTypes.object,
className: PropTypes.string,
input: PropTypes.object,
isRequired: PropTypes.bool,
label: PropTypes.string,
meta: PropTypes.object,
options: PropTypes.object,
optionText: PropTypes.oneOfType([
PropTypes.string,
Expand All @@ -280,15 +257,13 @@ SelectInput.propTypes = {
disableValue: PropTypes.string,
resource: PropTypes.string,
source: PropTypes.string,
translate: PropTypes.func.isRequired,
translateChoice: PropTypes.bool,
};

SelectInput.defaultProps = {
allowEmpty: false,
emptyText: '',
emptyValue: '',
classes: {},
choices: [],
options: {},
optionText: 'name',
Expand All @@ -297,8 +272,4 @@ SelectInput.defaultProps = {
disableValue: 'disabled',
};

export default compose(
addField,
translate,
withStyles(styles)
)(SelectInput);
export default SelectInput;
Loading