Skip to content

Implement allowCreate and newOptionCreator in 1.x#1187

Merged
bvaughn merged 2 commits intomasterfrom
custom-create-option
Sep 4, 2016
Merged

Implement allowCreate and newOptionCreator in 1.x#1187
bvaughn merged 2 commits intomasterfrom
custom-create-option

Conversation

@bvaughn
Copy link
Collaborator

@bvaughn bvaughn commented Sep 3, 2016

(Note that 1 test is failing in this branch but I'm fairly confident it is unrelated to this change set.)

Party time!🎈😎 Resolves issues #343, #586, #658, #712, #725, #795, #811, #815, #838, #897, #960, #987, #1007, and #1088.

TODO items remaining for this PR:

  • Write unit tests for new Creatable HOC.

High level overview

  • Added new Creatable HOC that wraps Select and adds support for creating new options. This HOC provides reasonable default behavior (eg create a new option on ENTER/TAB/comma) but also allows users to override defaults via props. No create-option-specific logic is inside of the base Select component and the logic within Creatable is fully customizable.
  • Added new onInputKeyDown prop to Select (a mirror to onInputChange) that allows users to tap into key-down events and prevent default Select handling. (This is key to how new options are created.)
  • Pulled default filterOptions and menuRenderer props out of Select and into separate modules. I think this lines up with @JedWatson's long-term goals. It was also necessary to enable better sharing between the base Select and the new Creatable HOC.

Documentation (snippet added to README)

The Creatable component enables users to create new tags within react-select. It decorates a Select and so it supports all of the default properties (eg single/multi mode, filtering, etc) in addition to a couple of custom ones (shown below). The easiest way to use it is like so:

import { Creatable } from 'react-select';

function render (selectProps) {
  return <Creatable {...selectProps} />;
};
Creatable properties
Property Type Description
isOptionUnique function Searches for any matching option within the set of options. This function prevents duplicate options from being created. By default this is a basic, case-sensitive comparison of label and value. Expected signature: ({ option: Object, options: Array, labelKey: string, valueKey: string }): boolean
isValidNewOption function Determines if the current input text represents a valid option. By default any non-empty string will be considered valid. Expected signature: ({ label: string }): boolean
newOptionCreator function Factory to create new option. Expected signature: ({ label: string, labelKey: string, valueKey: string }): Object
shouldKeyDownEventCreateNewOption function Decides if a keyDown event (eg its keyCode) should result in the creation of a new option. ENTER, TAB and comma keys create new options by dfeault. Expected signature: ({ keyCode: number }): boolean

Demo

untitled screencast - edited

@Reggino
Copy link

Reggino commented Sep 3, 2016

Wow, this is looking awesome!

@JedWatson
Copy link
Owner

This looks fantastic @bvaughn!

Wanted to throw a couple of more advanced cases at you to see how we could support them (not required for now, just wondering how they might impact the design)

1. Integration with Async

Would be useful to be able to combine Creatable and Async, my best guess on what this would mean from a UX / requirements perspective is:

  • wait for async results to load before presenting the "create ..." option in the menu
  • (optionally) perform an async call to isValidNewOption (i.e. could check for a duplicate value in the database, or perform some server-side validation) - I think this is pretty low value considering matching values would be returned in the loaded options and could be filtered out synchronously, but it's an edge case that might exist
  • (optionally) perform an async call to newOptionCreator - this would put the control into a loading state and wait for a value to be returned before firing off the onChange handler

Without actually writing any code to test this assumption, I think what you've got here would support all of the above, or at least allow for workarounds at a higher level in the application (e.g. for the last point, when onChange fires with new values in it, perform an async action to create the new value and replace it when it's ready)

2. Hooking into newOptionCreator with a UI interrupt

Use case here is if you wanted to use the "create ..." item in the menu to invoke a more complete UI to collect more data before adding a new option. e.g. if you were linking a post to an author, and wanted to allow for inline creation of new authors by prompting for full name / email / photo in a modal.

By hooking into the newOptionCreator method you could cause the new UI to be displayed, and then when the process is complete the application could set the new value of the Select field directly rather than calling back to the component.

I think the only thing we'd need to do to support this is close the menu and clear the input when the newOptionCreator is called, then not do anything if it returns a falsy value.

The other option would be to support an async newOptionCreator method, not sure if that complicates the Creatable component more than is necessary though, for the trade off with decreased implementation complexity when using the component. Let me know what you think.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 3, 2016

Thanks @JedWatson! Glad to hear it lines up with what you were thinking more or less.

Both of the use cases you brought up are interesting. I haven't dug into Async much yet to be honest so I'm not totally confident about that combination off the top of my head. I'd feel better about composing the two if we were using a child-function pattern but that's something to re-visit for version 2.x perhaps. 😁 In the meanwhile maybe we could modify one (or both) of these HOCs to support a new prop for their inner Select component; this prop would get defaulted to the base Select for the common case.

Async calls to isValidNewOption and newOptionCreator are a little tricky since I'm using both during the filtering pass as well. We could relax the use of newOptionCreator but I'd be a little concerned about the subsequent uniqueness check if any fancy custom logic is done in terms of creating a new option.

The UI interrupt for newOptionCreator is a pretty cool story I think! I believe it would almost work- (I'd have to update createNewOption to handle a possible null option)- except again for the fact that I'm also using that callback in filterOptions. I suppose we could either... support a separate callback for creating the temporary options in that method or pass an additional flag param to newOptionCreator to let it know which context it was being invoked in- but both seem a bit hackish.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 3, 2016

Gut feeling regarding async isValidNewOption is that it's tricky and may be a weird user experience considering it's something we'd have to fire off in response to user input (eg on a debounce as a user types). If we think it would add significant value though, here's how I'd tackle it:

  • Keep the current, synchronous UX the same as it currently is.
  • Support a Promise return type as well. If this is returned, insert a different kind of placeholder option at the start (eg "Checking option...") that then gets converted to a regular placed (eg "Create option 'foo'") or hidden upon resolution.

@Stenerson
Copy link
Contributor

@bvaughn 👏 this is awesome to 💯!

I noticed that when a new option is selected the selectValue is set to newOption[valueKey], which is a string. I think it would be useful to return the entire newOption object created in newOptionCreator. This way your onChange handler is always looking for an object and custom attributes like create: true can be used in the handler. Thoughts?

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 3, 2016

Hey @Stenerson. Nice catch. I'll fix that momentarily. :)

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 4, 2016

@JedWatson I'm going to try to finish up writing Creatable tests ASAP so we can get this branch merged. (Hopefully can finish them this weekend.) I think I'm going to have to pass on the 2 additional options for the time being because it's going to be hard to find time to tackle anything meaty while traveling.

I fly back to the states in 2 weeks though so maybe I can pick up one or both of them on the return trip. Sound reasonable? (Do you think what's here- once test coverage is added- is okay to proceed with for now?)

@bvaughn bvaughn force-pushed the custom-create-option branch from 19b11a1 to cd37659 Compare September 4, 2016 02:14
Added new Creatable HOC that wraps Select and adds support for creating new options. This HOC provides reasonable default behavior (eg create a new option on ENTER/TAB/comma) but also allows users to override defaults via props. No create-option-specific logic is inside of the base Select component and the logic within Creatable is fully customizable.

Added new onInputKeyDown prop to Select (a mirror to onInputChange) that allows users to tap into key-down events and prevent default Select handling. (This is key to how new options are created.)

Pulled default filterOptions and menuRenderer props out of Select and into separate modules. I think this lines up with @JedWatson's long-term goals. It was also necessary to enable better sharing between the base Select and the new Creatable HOC.
@bvaughn bvaughn force-pushed the custom-create-option branch from cd37659 to 7fe723f Compare September 4, 2016 02:19
@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 4, 2016

Okay! Tests are in. Rebased for cleanliness. This branch should be good to go. Any objections? 😄

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 4, 2016

Confirmed that the only test failing in this branch also fails for me on master. Given that- and my limited availability in the next few days- I'm going to roll forward with this change. Cheers!

@JedWatson
Copy link
Owner

🎉 Thanks @bvaughn!

@takashi
Copy link

takashi commented Sep 7, 2016

Hi, so is there any workaround to implement Async and Creatable together?
I think 0.9.x can implement this but 1.0.0-rc.1 is not. (I strongly wanted to upgrade to 1.0 because of my peerdeps)
Should I create my own HOC from Select ?

If there are any solutions, please tell me

thanks.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 7, 2016

@takashi Unfortunately the two HOCs aren't composable at the moment but I'd like to create a follow up PR that adds support for that. With RC1 though, your best bet is to fork Creatable and use the default props (attached as statics on it) to reduce your forked code.

@takashi
Copy link

takashi commented Sep 7, 2016

@bvaughn Thanks for replying quickly. I'm glad to hear you are planning to follow up the implementation in the future. I'll fork and try to create my own from Creatable module for work around atm.

thanks.

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 7, 2016

Hoping that the solution is as simple as allowing users to specify which inner Select impl Creatable should decoarate/compose.

@Stenerson
Copy link
Contributor

@takashi I'm not sure how @bvaughn or @JedWatson feel about continuing this approach but until the future PR, if you're not keen on forking to make these changes, you can implement a creatable solution using a custom filter function as discussed in #1007 and in the wiki.

<Select
  options={options}
  onChange={ props.handleChange }
  filterOptions={(options, filterValue, excludeOptions) => {
    let lowerFilterValue = filterValue.toLowerCase();
    let filteredOptions = options.filter(option => {
      let optionValue = String(option.value).toLowerCase();
      let optionLabel = String(option.label).toLowerCase();
      return (optionValue.indexOf(lowerFilterValue) > -1 || optionLabel.indexOf(lowerFilterValue) > -1);
    });

    // This is the important part
    if (filterValue.length > 0) {
      filteredOptions = filteredOptions.concat({create: true, value: filterValue, label: `Add "${filterValue}"?`});
    }

    return filteredOptions;
  }}
/>

Then in your change handler

function handleChange(option) {
  if (option.create) {
    // Do something here for the added option
    console.log(option); // {create: true, value: "New Option", label: 'Add "New Option"?'}
  } else {
    // Do something here for a "normal" selection
    console.log(option); // {value: 121, label: "Existing Option"}
  }
}

@deepakbansal1010
Copy link

@bvaughn Great Job!
I'm using react-select component in one of my angular project via referencing standalone ecma5 script file of this component. How I can get the updated version of the same with this PR merged?
(probably the build process or direct link to the file)

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 16, 2016

Hi @deepakbansal1010. Thanks!

The simplest thing would be to wait for RC2 to be released and then you can snag it directly from unpkg.com/react-select. Alternately you can check out this project and build it yourself to get a copy sooner:

# shallow clone
git clone git@github.com:JedWatson/react-select.git --depth 1

cd react-select

# install dependencies
npm i

# build project
npm run build

# built source will be available in the 'dist' folder

@deepakbansal1010
Copy link

@bvaughn Thank you for your quick response. This will definitely help.
Also to add to my question, will it be possible to use this creatable component in 'async' mode. If not, by what time we can expect an update?

@bvaughn
Copy link
Collaborator Author

bvaughn commented Sep 16, 2016

@deepakbansal1010 RC2 will also include a new AsyncCreatable HOC. Learn more about that here: #1210

@deepakbansal1010
Copy link

Ohh that's really nice! Thanks Again 🙂 @bvaughn

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants