JedWatson / react-select

The Select Component for React.js
https://react-select.com/
MIT License
27.56k stars 4.12k forks source link

Create new option on blur with Creatable #1764

Closed frfroes closed 3 years ago

frfroes commented 7 years ago

First of of all, thanks for the great work on react-select.

So, I needed to create a new option on blur with the Creatable component, even if the the user didn't executed one of the defaults triggers for the creation (e.g: click on"Create option 'foo'", hit TAB or ENTER).

I was able to achieve this using refs and invoking createNewOption in my parent component, something like this:

...
handleBlur(){
    let { inputValue } = this.selector;
    this.selector.createNewOption(inputValue)
}

render(){
   <Creatable props={...this.props} onBlur={this.handleBlur} onBlurResetsInput={false} 
   ref={s => this.selector = s}/>
}

I'd like to know if there is any intention of adding a more intuitive support for this, like a createNewOptionOnBlur prop for the Creatable. I will be glad to open a PR if that's the case.

agirton commented 7 years ago

Hi @frfroes, this behavior could be confusing for some. For example what if the user blurred from the input, but did not want to create an option? With your proposal they would have to delete the entry before they could blur or they would have to remove the entry after it being selected.

frfroes commented 7 years ago

Well, @agirton the default behavior would still be to blur from the input without creating an option. The implementer would have to explicitly pass the createNewOptionOnBlur={true} prop to the Creatable element in question, therefore taking the responsibility of creating a new option on every blur.

In this case he would only do so if his businesses logic required such behavior, as mine required.

lewandowskia commented 7 years ago

I would also love to see this implemented, I currently use autocomplete just as nice addition, not main feature. user should be able to enter anything he likes, blur the field and just forget about it

haikezegwaard commented 7 years ago

Same usecase here, the autocomplete is used as an addition. Love to see this implemented. Thoughts on how to proceed would be welcome, i could then contribute on this.

githubdoramon commented 7 years ago

Plus one for this... would also use it

samvk commented 6 years ago

A strange minor bug in @frfroes 's solution: if you enter an exact value found in the dropdown, it won't add it. (possibly due to the isOptionUnique check?)

jamcreencia commented 6 years ago

@frfroes On your code above, what is this.selector? I'd like to implement this in my code too. Thanks!

martimarkov commented 6 years ago

This doesn't seem to work on v2 or is it just me? @jamcreencia Did you use v1 or v2?

jamcreencia commented 6 years ago

Hi @martimarkov , I'm using v1.

frfroes commented 6 years ago

Hi guys, sorry for the absence. I have been very busy with work and college these last months and I have not given the proper attention to the issue which I opened myself. Anyway, I would like to be more engaged with the community from now on and I guess this is a good place to start.

@agirton This issue have gotten some popularity over time, so I guess it's worth opening to PR. I do not have experience contributing to open source, so I have some doubts on how to get started. Should I just clone the repo, do the changes, test everything and open the PR? At least that was what I understood from reading the CONTRIBUTING.md, please correct me if I'm missing something.

@jamcreencia Really sorry for letting you hang. You probably figured it out or got it working somehow by now. Just in case, the answer to your question is: In React, when you're doing something like ref = {s => this.selector = s} it's called [callback refs] (https://reactjs.org/docs/refs-and-the-dom.html # callback-refs). Basically you're passing the component instance which you are using the callback ref to attribute in the parent component. So this.selector is the reference for the instance of Creatable, that way I can invokecreateNewOption ()imperatively from the instance's API.

andoq commented 6 years ago

Just a note on this, in V2 the onBlur event arg is a synthetic event that has a handle to the input, so you should be able to do this easily on your own with a thin wrapper over your onCreateOption handler: (I haven't fully verified this): The handleBlur functions in your component needs to be:

handleCreate = (value) => {
   //Do what you need here to create an new object and set your state
}

handleBlur(event){
    this.handleCreate(event.target.value)
}

<Creatable onBlur={this.handleBlur} onBlurResetsInput={false} onCreateOption={this.handleCreate} />

Assuming this works, I wouldn't see the need to add anything more to the new API.

bertho-zero commented 4 years ago

Are there plans to have an option for that?

bladey commented 4 years ago

Hi all,

Thank you everyone who had a part in addressing this question.

I'm now closing this issue as it appears to have been resolved via community comments.

However, if you feel this issue is still relevant and you'd like us to review it, or have any suggestions regarding this going forward - please leave a comment and we'll do our best to get back to you!

ro-savage commented 4 years ago

@bladey - The option suggested by @andoq doesn't handle use cases of multi-select.

There are a bunch of edge cases around it (when the value already exists, the fact null is returned instead of an empty array, etc).

It would be great if onBlur was passed was also passed the current values (or better yet the same params as onChange). This would make it much easier to implement in user land.

ro-savage commented 4 years ago

FYI for anyone using multiselect and create on blur. This is our solution, although there are still some edge cases that may be missed

  const onBlur = (event) => {
    const name = event.target.value
    if (name) {
      const newValue = {label: name, value: name}
      if (Array.isArray(props.value) ) {
        // Dont allow duplciate values
        if (props.value.find(v => v.value === name)) { return props.onChange(props.value) }
        props.onChange([ ...props.value, newValue ])
      } else if (props.isMulti) {
        props.onChange([ newValue ])
      } else {
        props.onChange(newValue)
      }
    }
  }
bladey commented 4 years ago

Thanks for your feedback @ro-savage, I appreciate it.

ebonow commented 3 years ago

It would be great if onBlur was passed was also passed the current values (or better yet the same params as onChange). This would make it much easier to implement in user land.

Greetings @ro-savage ,

When using onInputChange, there is an action meta event named input-blur which would enable you to do exactly as you are suggesting.

  const onInputChange = (textInput, { action }) => {
    if (action === "input-blur" && !!textInput) {
      handleChange(textInput);
    }
  };

  const handleChange = (label) => {
    if (options.find((opt) => opt.value === label)) {
      return;
    }

    const option = { label, value: label };
    setOptions([...options, option]);
    isMulti ? setValue([...(value || []), option]) : setValue(option);
  };

Here is the example as a codesandbox: Create Option onBlur

Here is a similar example: Create Option on comma input

Given that the issue is over 3 yrs old and that we have an adequate api to solve this use case as of v2, I will close this issue. If anyone has any other questions or concerns please feel free to share and we can always reopen this and/or work through anything that is unresolved for you.

sagar-ranglani commented 3 years ago

@ebonow shared a great solution but it does not work with the new version as textInput is blank when the action is "input-blur"

In order to achieve this, one option is to save the textInput in the state (a redux state in my case) on the input-change action and use that state to call handleChange on input-blur action.

Sample Code might look like this:

onInputChange = (textInput, { action }) => {
  if (action === 'input-blur') {
    const { inputValue } = this.state;
    if (inputValue) {
      handleChange(inputValue)
      this.setState({ inputValue: '' });
    }
  }
  if (action === 'input-change') {
    this.setState({ inputValue: textInput });
  }
};
ebonow commented 3 years ago

@sagar-ranglani

My mistake, I was blurring by tabbing out of the field which of course, selects the value. Here is an updated codesandbox based on your observations.

Codesandbox demo

  const [options, setOptions] = useState([
    { label: "Option 1", value: 1 },
    { label: "Option 2", value: 2 },
    { label: "Option 3", value: 3 }
  ]);

  const [value, setValue] = useState();
  const [inputValue, setInputValue] = useState();

  const handleBlur = () => {
    const label = inputValue?.trim() || "";
    const optionExists = options.find((opt) => opt.label === label);

    if (!label || optionExists) {
      return;
    }

    const option = { label, value: label };

    // Add new option to menu list
    setOptions([...options, option]);
    // Add value to selected options
    setValue([...(value || []), option]);
    // Clear input value
    setInputValue("");
  };

  const onInputChange = (textInput, { action }) => {
    if (action === "input-change") {
      setInputValue(textInput);
    }
    if (action === "input-blur") {
      handleBlur();
    }
  };

  const onChange = (selected) => {
    setValue(selected);
    setInputValue("");
  };

  return (
    <Createable
      isMulti
      inputValue={inputValue}
      value={value}
      options={options}
      onChange={onChange}
      onInputChange={onInputChange}
    />
  );