React-select: Add support for pasting a list of values to Creatable

Created on 18 Apr 2017  路  15Comments  路  Source: JedWatson/react-select

Can we have support for pasting a list of values to the creatable component? I'm working on this component that uses AsyncCreatable, but sometimes the user may want to create more than one value at a time, so the user may paste a whole list of values separated by a comma.

Most helpful comment

Forgot to update this thread, but I found a working solution that serves my purpose so I'm just gonna share it with you.

I basically created a new version of the component Creatable that is called PasteCreatable. This one will handle the onPaste event (this event is already implemented by default by React). Then I created a version of AsynCreatable that will use the PasteCreatable component. Here is the code:

PasteCreatable:

import React, { Component, Element } from 'react';
import _ from 'lodash';

import { Creatable } from 'react-select';

class PasteCreatable extends Component {
  render(): Element<any> {
    return (
      <Creatable
        {...this.props}
        ref={(ref) => this.pasteInput = ref}
        inputProps={{
          onPaste: (evt) => {
            evt.preventDefault();
            const clipboard = evt.clipboardData.getData('Text');
            if (!clipboard) {
              return;
            }
            const values = _.uniq(clipboard.split(/[\s,]+/), (value) =>
               value.trim()
            );
            const options = values
              .filter((value) =>
                this.pasteInput.props.isValidNewOption({ label: value })
              )
              .map((value) => ({
                [this.pasteInput.labelKey]: value,
                [this.pasteInput.valueKey]: value,
                className: 'Select-create-option-placeholder'
              }));
            this.pasteInput.select.selectValue(options);
          }
        }}
      />
    );
  }
}

AsyncPasteCreatable:

import React, { Component, Element } from 'react';

import Select from 'react-select';

import PasteCreatable from './PasteCreatable';

const reduce = (obj, childProps = {}) => {
  return Object.keys(obj)
  .reduce((props, key) => {
    const value = obj[key];
    if (value !== undefined) props[key] = value;
    return props;
  }, childProps);
};

class AsyncPasteCreatable extends Component {
  render(): Element<any> {
    return (
      <Select.Async {...this.props}>
        {(asyncProps) => (
          <PasteCreatable {...this.props}>
            {(creatableProps) => (
              <Select
                {...reduce(asyncProps, reduce(creatableProps, {}))}
                onInputChange={(input) => {
                  creatableProps.onInputChange(input);
                  return asyncProps.onInputChange(input);
                }}
                ref={(ref) => {
                  this.select = ref;
                  creatableProps.ref(ref);
                  asyncProps.ref(ref);
                }}
              />
            )}
          </PasteCreatable>
        )}
      </Select.Async>
    );
  }
}

All 15 comments

This is similar to a problem I'm facing.
It would be nice to have an "MS Outlook" style "To address" box.
Where you can paste stuff and once you lose focus, the emails are resolved.
If not, you can start typing and the relevant emails show up as prompts.

You should be able to do this without adding a library feature. Just split the input string in your onChange handler. You could even use the onInputKeyDown to check for commas and call your change handler to add tags as you're typing.

@Stenerson Could you show me how to call the change handler from the onInputKeyDown
I'm able to detect the conditions in the onInputKeyDown but I'm unable to invoke the onChange Handler with the new input.
Even if i try to set the state from within the keydown handler, it doesn't seem to work.

@asdkalluri I'd probably have some time to play around with the idea this weekend. Can you include an example of what you have so far as a starting point for me?

Here's what I have right now.
I'd like to change the input on the onInputKeyDownitself. Somehow, I'm not able to do this and the control goes out and I'm unable to override the pasted value.

Here's the Async Creatable

  var Selector = <AsyncCreatable
            name="form-lots"
            value={this.state.selectValues}
            multi={true}
            autoload={false}
            valueRenderer={this.renderValue.bind(this)}
            placeholder="Start Typing or Paste Here"
            promptTextCreator={label => label}
            onInputKeyDown={((event) => {
                //capture the Ctrl+V key stroke
                if (event.ctrlKey && event.keyCode == 86) {
                    //if IE only
                    if (isIE) {
                        pastedValues = (window as any).clipboardData.getData('Text').replace(/(\r\n|\n|\r)/gm, " ");
                        this.changeHandler(this.state.selectLotValues);
                        //pass on the keycode as Enter/Return Key
                        event.keyCode = 13;
                    }
                }

            }).bind(this)}
            loadOptions={this.getOptions.bind(this)}
            isLoading={this.state.isLotAsyncLoadingExternally}
            onChange={this.changeHandler.bind(this)}
            />;

Here's the changeHandler

changeHandler(input) {
        //Users can paste stuff here. Loop through the input and create multiple options if there are commas from the paste
        let massagedInput = input?input:[];
        let latestValue = {};
        if (pastedValues != null) {
            latestValue['value'] = pastedValues;
            pastedValues = null;
        }
        else if (input && input.length > 0)
            latestValue = input[input.length - 1];

        if (Object.keys(latestValue).length >0) {
            //split on comma
            const separators = [',', ';', '\\(', '\\)', '\\*', '/', ':', '\\?', '\n', '\r', ' '];
            if (latestValue['value'].includes(' ') || latestValue['value'].includes(',') || latestValue['value'].includes('\r') || latestValue['value'].includes('\n')) {
                //let splitArray = latestValue['value'].split(' ');
                if(!isIE)
                    massagedInput.pop();
                //massagedInput = massagedInput.concat(splitArray.map(x => { return { 'value': x, 'label': x } }));
                massagedInput = massagedInput.concat(latestValue['value'].split(new RegExp(separators.join('|'))).map(x => { return { 'value': x.trim(), 'label': x.trim() }; }));
            }

            let scope = this;
            axios({
                method: 'post',
                url: '/Home/ValidateEntry',
                data: {
                    data: massagedInput.map(x => { return x.value })
                }
            }).then(function (response) {
                scope.setState({
                    selectValues: response.data
                }, scope.validate);
            });
        }
        else {
            this.setState({ selectLotValues: [] }, this.validate);
        }
    }

@asdkalluri I created a naive but (mostly) working example below. I used the MultiSelect Example as a starting point so pardon the use of createClass, etc. 馃槃

This is more or less what I was thinking when I gave my first answer but there are a few issues with it. Mostly that onInputChange appears to be broken for Creatable at the moment (#1349, #1411) 馃槩

import React from 'react';
import Select from 'react-select';

const FLAVOURS = [
  { label: 'Chocolate', value: 'chocolate' },
  { label: 'Vanilla', value: 'vanilla' },
  { label: 'Strawberry', value: 'strawberry' },
  { label: 'Caramel', value: 'caramel' },
  { label: 'Cookies and Cream', value: 'cookiescream' },
  { label: 'Peppermint', value: 'peppermint' },
];

const WHY_WOULD_YOU = [
  { label: 'Chocolate (are you crazy?)', value: 'chocolate', disabled: true },
].concat(FLAVOURS.slice(1));

var MultiSelectField = React.createClass({
  displayName: 'MultiSelectField',
  propTypes: {
    label: React.PropTypes.string,
  },
  getInitialState() {
    return {
      options: FLAVOURS,
      value: [],
    };
  },
  handleInputChange(value) {
    if (value.includes(',')) {
      let valuesToSelect = [];
      const existingValues = this.state.value.map(v => v.value);
      const splitValues = value.split(',').map(v => { return { label: v.trim(), value: v.trim().toLowerCase() }; });

      splitValues.forEach(v => {
        // Ignore currently selected values
        if (existingValues.includes(v)) {
          return;
        }

        // Do something to validate and/or actually create any new options "creatables" here
        // ...

        // Add this value to our array that we'll send to change handler
        valuesToSelect.push(v);
      });

      // Concatenate new values with current state and send to change handler
      this.handleSelectChange([...this.state.value, ...valuesToSelect]);

      // Clear the input
      // THIS ISN'T WORKING BECAUSE OF https://github.com/JedWatson/react-select/issues/1349
      // Would be great if it did though!
      // Looks like https://github.com/JedWatson/react-select/pull/1411 fixes it
      return '';
    }
    // if there are no commas to process, just pass through the input value
    return value;
  },
  handleSelectChange(value) {
    this.setState({ value });
  },

  render() {
    return (
      <div className="section">
        <h3 className="section-heading">{this.props.label}</h3>
        <Select.Creatable
          multi
          disabled={this.state.disabled}
          value={this.state.value}
          newOptionCreator={o => { return { label: o.label, value: o.label.toLowerCase() }; }}
          placeholder="Select your favourite(s)"
          options={this.state.options}
          onInputChange={this.handleInputChange}
          onChange={this.handleSelectChange}
        />
      </div>
    );
  },
});

module.exports = MultiSelectField;

Forgot to update this thread, but I found a working solution that serves my purpose so I'm just gonna share it with you.

I basically created a new version of the component Creatable that is called PasteCreatable. This one will handle the onPaste event (this event is already implemented by default by React). Then I created a version of AsynCreatable that will use the PasteCreatable component. Here is the code:

PasteCreatable:

import React, { Component, Element } from 'react';
import _ from 'lodash';

import { Creatable } from 'react-select';

class PasteCreatable extends Component {
  render(): Element<any> {
    return (
      <Creatable
        {...this.props}
        ref={(ref) => this.pasteInput = ref}
        inputProps={{
          onPaste: (evt) => {
            evt.preventDefault();
            const clipboard = evt.clipboardData.getData('Text');
            if (!clipboard) {
              return;
            }
            const values = _.uniq(clipboard.split(/[\s,]+/), (value) =>
               value.trim()
            );
            const options = values
              .filter((value) =>
                this.pasteInput.props.isValidNewOption({ label: value })
              )
              .map((value) => ({
                [this.pasteInput.labelKey]: value,
                [this.pasteInput.valueKey]: value,
                className: 'Select-create-option-placeholder'
              }));
            this.pasteInput.select.selectValue(options);
          }
        }}
      />
    );
  }
}

AsyncPasteCreatable:

import React, { Component, Element } from 'react';

import Select from 'react-select';

import PasteCreatable from './PasteCreatable';

const reduce = (obj, childProps = {}) => {
  return Object.keys(obj)
  .reduce((props, key) => {
    const value = obj[key];
    if (value !== undefined) props[key] = value;
    return props;
  }, childProps);
};

class AsyncPasteCreatable extends Component {
  render(): Element<any> {
    return (
      <Select.Async {...this.props}>
        {(asyncProps) => (
          <PasteCreatable {...this.props}>
            {(creatableProps) => (
              <Select
                {...reduce(asyncProps, reduce(creatableProps, {}))}
                onInputChange={(input) => {
                  creatableProps.onInputChange(input);
                  return asyncProps.onInputChange(input);
                }}
                ref={(ref) => {
                  this.select = ref;
                  creatableProps.ref(ref);
                  asyncProps.ref(ref);
                }}
              />
            )}
          </PasteCreatable>
        )}
      </Select.Async>
    );
  }
}

Nice @MateusDantas, thanks for sharing! 馃帀

Hi @MateusDantas this would be a great addition to the Creatable!! Would you like to contribute with a PR?

Sure. I will prepare the code and send a PR then.

thanks!

Thanks for saving me a ton of time, @MateusDantas!

How's that PR coming along @MateusDantas ? :)

I would love to have this in as I need exactly this feature. I could help if @agirton can indicate how he would like to see it integrated with Creatable

Hey guys. Had a lot of things to do at work, plus I went on vacations during July, so I couldn't send the PR. I will try to do it asap!

How's PR @MateusDantas ? 馃憤

Hello -

In an effort to sustain the react-select project going forward, we're closing old issues / pull requests.

We understand this might be inconvenient but in the best interest of supporting the broader community we have to direct our limited efforts to maintain the latest version.

If you aren't using the latest version of react-select please consider upgrading to see if it resolves any issues you're having.

If you feel this issue / pull request is still relevant and you'd like us to review it, please leave a comment and we'll do our best to get back to you.

Was this page helpful?
0 / 5 - 0 ratings