React-select clear value while keeping filter

2019-05-18 04:02发布

问题:

I'm working on a permissions system to let users control who can access/comment/edit a resource, much like what you can find on Google Drive. I'm using a React-Select multi to let the owner of the resource pick users he wants to give access to the resource to.

When I click on an option displayed by react-select, I want this option to be added to my list of allowed users (a list that is handled by another component). This part works, I just used an onChange handler on the select (as you can see on the code below).

export default class AddUsersForm extends Component {

  PropTypes = {
    onSubmit: PropTypes.func.isRequired,
    usersList: PropTypes.array.isRequired, // List of all users in the company
    usersInPermissions: PropTypes.array.isRequired, // Users who already have access to the resource
  }

  handleChange(users){
    // Adds new user to the list of allowed users, an updated value for this.props.usersInPermissions will be received
    this.props.onSubmit(users);
  }

  render() {
    return (
      <form>
        <Select
          name="users"
          multi={true}
          options={this.props.usersList.filter(user => !this.props.usersInPermissions.includes(user.id))}
          onChange={(users) => this.handleChange(users)}
        />
      </form>
    );
  }

}

This is where I am stuck: once the option has been added, I would like to keep displaying the filter that the user was potentially using while searching for the first option in the text field. The way it works now, the filter is removed and all the options are shown in the dropdown.

Is there any simple way of achieving this with React-Select?

Many thanks!

回答1:

This code is working. Maybe there are better ways.

// ManageUsers
import React, { PropTypes } from 'react';
import AddUserForm from './AddUserForm'

export default class NoMatch extends React.Component {
  constructor(props) {
    super(props)
    this.handleChange = this.handleChange.bind(this);

    let selectedUsers = [ { value: 3, label: 'Three' },
      { value: 4, label: 'Four' } ];

    this.state = {
      selectedUsers: selectedUsers
    }
  }

  handleChange(selected) {
    this.setState({ selectedUsers: selected })
  }

  render() {
    let usersList = [
      { value: 1, label: 'One' },
      { value: 2, label: 'Two' }
    ];

    return (
      <div>Users
        <AddUserForm usersList={usersList} 
         selectedUsers={this.state.selectedUsers} 
         handleChange={this.handleChange} />
      </div>
    );
  }
}
// AddUsersForm
import React, { PropTypes } from 'react';
import Select from 'react-select';
import 'react-select/dist/react-select.css';

export default class AddUsersForm extends React.Component {
 PropTypes = {
  usersList: PropTypes.array.isRequired,
  selectedUsers: PropTypes.array.isRequired,
  handleChange: PropTypes.func.isRequired
 }

 render() {
  return (
   <form>
    <Select
     multi={true}
     options={this.props.usersList}
     value={this.props.selectedUsers}
     onChange={(users) => this.props.handleChange(users)}
    />
   </form>
  );
 }
}

If you want to keep the typed text then you have to set the text of the input on the handleChange. There is no build in function to keep the typed text.

 onChange={(users) => this.props.handleChange(users, event)}
handleChange(selected, event) {
let selectedFilter = event.target;
 // then navigated to the input element with Javascript or jQuery
 // and set the value of the input

this.setState({ selectedUsers: selected })
}


回答2:

My way:

  1. Replaced Option component with own component (several components from Material-UI library).
  2. Overrided onClick event handler - here is some logic and call onChange handler from ReactSelect props. At the end of theonClick handler added event.stopPropagation()
import React from 'react';

import MenuItem from '@material-ui/core/MenuItem/MenuItem';
import Checkbox from '@material-ui/core/Checkbox/Checkbox';
import ListItemText from '@material-ui/core/ListItemText/ListItemText';

const MultiOption = props => (
  <MenuItem
    buttonRef={props.innerRef}
    {...props.innerProps}
    onClick={event => {
      let values = [];
      if (props.isSelected) {
        values = props.selectProps.value.filter(
          item => item.value !== props.value,
        );
      } else {
        values = [props.data].concat(props.selectProps.value);
      }
      props.selectProps.onChange(values);
      event.stopPropagation();
    }}
    style={{
      overflow: 'initial',
      padding: 0,
    }}
  >
    <Checkbox
      checked={props.isSelected}
      style={{
        padding: 4,
      }}
    />
    <ListItemText
      primary={props.label}
      classes={{
        root: props.selectProps.classes.optionRoot,
      }}
    />
  </MenuItem>
);

export default MultiOption;
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';

import { withStyles } from '@material-ui/core/styles';

import { getComponents } from './components';

import { styles, getSelectStyles } from './styles';

class Combobox extends React.Component {
  handleChange = () => value => {
    const { onChange } = this.props;
    onChange(value);
  };

  render() {
    const {
      classes,
      theme,
      options,
      label,
      rootStyle,
      value,
      error,
      isInner,
      isMulti,
      fullWidth,
      ...props
    } = this.props;

    return (
      <div className={classes.root} style={{ ...rootStyle }}>
        <Select
          {...props}
          isClearable
          classes={classes}
          styles={getSelectStyles({
            theme,
            fullWidth,
          })}
          options={options}
          menuPortalTarget={document.body}
          menuPlacement="auto"
          value={value || null}
          onChange={this.handleChange()}
          components={getComponents({
            isInner,
            isMulti,
          })}
          textFieldProps={{
            label,
            error: !!error,
            helperText: error,
            InputLabelProps: { shrink: true },
          }}
          isMulti={isMulti}
          hideSelectedOptions={!isMulti}
          closeMenuOnSelect={!isMulti}
          loadingMessage={() => 'Loading...'}
        />
      </div>
    );
  }
}

Combobox.propTypes = {
  options: PropTypes.arrayOf(PropTypes.shape({})),
  label: PropTypes.string,
  classes: PropTypes.shape({}).isRequired,
  theme: PropTypes.shape({}).isRequired,
  value: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.bool,
    PropTypes.arrayOf(PropTypes.any),
    PropTypes.shape({}),
  ]),
  error: PropTypes.string,
  onChange: PropTypes.func.isRequired,
  isInner: PropTypes.bool,
  isMulti: PropTypes.bool,
  fullWidth: PropTypes.bool,
};

Combobox.defaultProps = {
  options: [],
  label: '',
  value: null,
  error: '',
  isInner: false,
  isMulti: false,
  fullWidth: false,
};

export default withStyles(styles, { withTheme: true })(({ ...props }) => (
  <Combobox {...props} />
));
import Control from './Control';
import InnerControl from './InnerControl';

import InputComponent from './InputComponent';
import MenuList from './MenuList';

import Option from './Option';
import MultiOption from './MultiOption';

import SingleValue from './SingleValue';
import MultiValue from './MultiValue';

import NoOptionsMessage from './NoOptionsMessage';

import Placeholder from './Placeholder';

import ValueContainer from './ValueContainer';

const getComponents = ({ isInner, isMulti }) => ({
  Control: isInner ? InnerControl : Control,
  ...(isMulti && { MenuList }),
  MultiValue,
  NoOptionsMessage,
  Option: isMulti ? MultiOption : Option,
  Placeholder,
  SingleValue,
  ValueContainer,
});

export {
  Control,
  InnerControl,
  InputComponent,
  MenuList,
  Option,
  MultiOption,
  SingleValue,
  MultiValue,
  NoOptionsMessage,
  Placeholder,
  ValueContainer,
  getComponents,
};