1
votes

i'm using:

  • react 15.6.2
  • redux-form 7.2.0
  • react-select 1.2.1

i don't have a codepen set up for this yet, it's complicated. hoping someone will see the problem from the source alone. i can dream, can't i?

i have a redux-form displaying a invoice object:

invoice = { client: { email: '[email protected]' } }

summary: i'm using react-select.creatable as an autocomplete component for an email dropdown. selecting an email sets the client on the invoice.

typing and hitting enter creates and selects the new option

when i create a new email address and select it, then blur out of the input field, the input value gets cleared.

enter image description here

the new client on the invoice form still gets set correctly in the redux store, it just gets cleared from the Field's input value. why?

  • please note i am already manually calling input.onBlur() from the Select with the correct value, as per other answers. i think this has something to do with creating a new option.

    // redux-form field wrapping the custom autocomplete component

    const clientOptions = clients.map(client => ({ label: client.email, value: client })) const isValidNewOption = ({ label }) => email.REG_EMAIL.test(label) const promptTextCreator = (label) => (Send to ${label}) const newOptionCreator = ({label, labelKey, valueKey}) => { return { label: label, value: { email: label } } }

// RFReactSelect, a react-select Creatable to act as an autocomplete with the ability to create a new option

import React, { PropTypes } from 'react';
import Select from 'react-select';
import 'react-select/dist/react-select.css';

RFReactSelect.defaultProps = {
  multi: false,
  className: ""
};

RFReactSelect.propTypes = {
  input: PropTypes.shape({
    name: PropTypes.string.isRequired,

    onBlur: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    onFocus: PropTypes.func.isRequired,
  }).isRequired,
  options: PropTypes.array.isRequired,
  multi: PropTypes.bool,
  className: PropTypes.string,
  onNewOptionClick: PropTypes.func,
};

export default function RFReactSelect(props) {
  const { input , options, multi, className,
    newOptionCreator, promptTextCreator, isValidNewOption } = props
  const { name, value, onBlur, onChange, onFocus } = input;
  const transformedValue = transformValue(value, options, multi);

  return (
    <Select.Creatable
      className={className}
      isValidNewOption={isValidNewOption}
      name={name}
      multi={multi}
      newOptionCreator={newOptionCreator}
      onSelectResetsInput={false}
      onBlurResetsInput={false}
      options={options}
      onChange={multi
        ? multiChangeHandler(onChange)
        : singleChangeHandler(onChange)
      }
      onBlur={() => onBlur(value)}
      onFocus={onFocus}
      promptTextCreator={promptTextCreator}
      value={transformedValue}
      valueKey='value'
    />
  );
}

/**
 * onChange from Redux Form Field has to be called explicity.
 */
function singleChangeHandler(func) {
  return function handleSingleChange(value) {
    func(value ? value.value : '');
  };
}

/**
 * onBlur from Redux Form Field has to be called explicity.
 */
function multiChangeHandler(func) {
  return function handleMultiHandler(values) {
    func(values.map(value => value.value));
  };
}

/**
 * For single select, Redux Form keeps the value as a string, while React Select
 * wants the value in the form { value: "grape", label: "Grape" }
 *
 * * For multi select, Redux Form keeps the value as array of strings, while React Select
 * wants the array of values in the form [{ value: "grape", label: "Grape" }]
 */
function transformValue(value, options, multi) {
  if (multi && typeof value === 'string') return [];

  const filteredOptions = options.filter(option => {
    return multi
      ? value.indexOf(option.value) !== -1
      : option.value === value;
  });

  return multi ? filteredOptions : filteredOptions[0];
}
1

1 Answers

0
votes

annnnd i solved it. the issue was that you have to keep track of the newly created options in local state, and concat them with props.options of the component, otherwise the new ones are lost on re-render. so the value passed back to the onBlur of the Field was null. here's the source:

import React, { Component, PropTypes } from 'react';
import Select from 'react-select';
import 'react-select/dist/react-select.css';

class RFReactSelect extends Component {
  constructor() {
    super()

    this.state = {
      createdOptions: []
    }

    this.onNewOptionClick = this.onNewOptionClick.bind(this)
  }

  render() {
    const { input , options, multi, className,
      newOptionCreator, promptTextCreator, isValidNewOption } = this.props
    const { name, value, onBlur, onChange, onFocus } = input;
    const allOptions = options.concat(this.state.createdOptions)
    const transformedValue = this.transformValue(value, allOptions, multi);

    return (
      <Select.Creatable
        className={className}
        isValidNewOption={isValidNewOption}
        multi={multi}
        name={name}
        newOptionCreator={newOptionCreator}
        onSelectResetsInput={false}
        onBlurResetsInput={false}
        options={allOptions}
        onChange={multi
          ? this.multiChangeHandler(onChange)
          : this.singleChangeHandler(onChange)
        }
        onBlur={() => onBlur(value)}
        onFocus={onFocus}
        onNewOptionClick={this.onNewOptionClick}
        promptTextCreator={promptTextCreator}
        ref='creatable'
        value={transformedValue}
        valueKey='value'
      />
    );
  }

  /**
   * Keep created options in local state or they will be lost
   * on re-render
   */
  onNewOptionClick(option) {
    const { props, select } = this.refs.creatable
    const { options } = props

    options.unshift(option)
    select.selectValue(option)

    this.setState({
      createdOptions: [option]
    })
  }

  /**
   * onChange from Redux Form Field has to be called explicity.
   */
  singleChangeHandler(func) {
    return function handleSingleChange(option) {
      func(option ? option.value : '');
    };
  }

  /**
   * onBlur from Redux Form Field has to be called explicity.
   */
  multiChangeHandler(func) {
    return function handleMultiHandler(values) {
      func(values.map(value => value.value));
    };
  }

  /**
   * For single select, Redux Form keeps the value as a string, while React Select
   * wants the value in the form { value: "grape", label: "Grape" }
   *
   * * For multi select, Redux Form keeps the value as array of strings, while React Select
   * wants the array of values in the form [{ value: "grape", label: "Grape" }]
   */
  transformValue(value, options, multi) {
    if (multi && typeof value === 'string') return [];

    const filteredOptions = options.filter(option => {
      return multi
        ? value.indexOf(option.value) !== -1
        : option.value === value;
    });

    return multi ? filteredOptions : filteredOptions[0];
  }
}

RFReactSelect.defaultProps = {
  multi: false,
  className: ""
};

RFReactSelect.propTypes = {
  input: PropTypes.shape({
    name: PropTypes.string.isRequired,
    onBlur: PropTypes.func.isRequired,
    onChange: PropTypes.func.isRequired,
    onFocus: PropTypes.func.isRequired,
  }).isRequired,
  options: PropTypes.array.isRequired,
  multi: PropTypes.bool,
  className: PropTypes.string,
  onNewOptionClick: PropTypes.func,
}

export default RFReactSelect