UNPKG

react-select

Version:

A Select control built with and for ReactJS

192 lines (180 loc) 6.07 kB
// @flow import React, { Component, type Config, type Node, type AbstractComponent, type ElementRef, type ElementConfig, } from 'react'; import Select, { type Props as SelectProps } from './Select'; import type { OptionType, OptionsType, ValueType, ActionMeta } from './types'; import { cleanValue } from './utils'; import manageState from './stateManager'; export type DefaultCreatableProps = {| /* Allow options to be created while the `isLoading` prop is true. Useful to prevent the "create new ..." option being displayed while async results are still being loaded. */ allowCreateWhileLoading: boolean, /* Sets the position of the createOption element in your options list. Defaults to 'last' */ createOptionPosition: 'first' | 'last', /* Gets the label for the "create new ..." option in the menu. Is given the current input value. */ formatCreateLabel: string => Node, /* Determines whether the "create new ..." option should be displayed based on the current input value, select value and options array. */ isValidNewOption: (string, OptionsType, OptionsType) => boolean, /* Returns the data for the new option when it is created. Used to display the value, and is passed to `onChange`. */ getNewOptionData: (string, Node) => OptionType, |}; export type CreatableProps = { ...DefaultCreatableProps, /* If provided, this will be called with the input value when a new option is created, and `onChange` will **not** be called. Use this when you need more control over what happens when new options are created. */ onCreateOption?: string => void, /* Sets the position of the createOption element in your options list. Defaults to 'last' */ createOptionPosition: 'first' | 'last', /* Name of the HTML Input (optional - without this, no input will be rendered) */ name?: string, options?: OptionsType, inputValue: string, value: ValueType, isLoading?: boolean, isMulti?: boolean, onChange: (ValueType, ActionMeta) => void, }; export type Props = SelectProps & CreatableProps; const compareOption = (inputValue = '', option) => { const candidate = String(inputValue).toLowerCase(); const optionValue = String(option.value).toLowerCase(); const optionLabel = String(option.label).toLowerCase(); return optionValue === candidate || optionLabel === candidate; }; const builtins = { formatCreateLabel: (inputValue: string) => `Create "${inputValue}"`, isValidNewOption: ( inputValue: string, selectValue: OptionsType, selectOptions: OptionsType ) => !( !inputValue || selectValue.some(option => compareOption(inputValue, option)) || selectOptions.some(option => compareOption(inputValue, option)) ), getNewOptionData: (inputValue: string, optionLabel: Node) => ({ label: optionLabel, value: inputValue, __isNew__: true, }), }; export const defaultProps: DefaultCreatableProps = { allowCreateWhileLoading: false, createOptionPosition: 'last', ...builtins, }; type State = { newOption: OptionType | void, options: OptionsType, }; export const makeCreatableSelect = <C: {}>( SelectComponent: AbstractComponent<C> ): AbstractComponent<C & Config<CreatableProps, DefaultCreatableProps>> => class Creatable extends Component<CreatableProps & C, State> { static defaultProps = defaultProps; select: ElementRef<*>; constructor(props: CreatableProps & C) { super(props); const options = props.options || []; this.state = { newOption: undefined, options: options, }; } UNSAFE_componentWillReceiveProps(nextProps: CreatableProps & C) { const { allowCreateWhileLoading, createOptionPosition, formatCreateLabel, getNewOptionData, inputValue, isLoading, isValidNewOption, value, } = nextProps; const options = nextProps.options || []; let { newOption } = this.state; if (isValidNewOption(inputValue, cleanValue(value), options)) { newOption = getNewOptionData(inputValue, formatCreateLabel(inputValue)); } else { newOption = undefined; } this.setState({ newOption: newOption, options: (allowCreateWhileLoading || !isLoading) && newOption ? createOptionPosition === 'first' ? [newOption, ...options] : [...options, newOption] : options, }); } onChange = (newValue: ValueType, actionMeta: ActionMeta) => { const { getNewOptionData, inputValue, isMulti, onChange, onCreateOption, value, name, } = this.props; if (actionMeta.action !== 'select-option') { return onChange(newValue, actionMeta); } const { newOption } = this.state; const valueArray = Array.isArray(newValue) ? newValue : [newValue]; if (valueArray[valueArray.length - 1] === newOption) { if (onCreateOption) onCreateOption(inputValue); else { const newOptionData = getNewOptionData(inputValue, inputValue); const newActionMeta = { action: 'create-option', name }; if (isMulti) { onChange([...cleanValue(value), newOptionData], newActionMeta); } else { onChange(newOptionData, newActionMeta); } } return; } onChange(newValue, actionMeta); }; focus() { this.select.focus(); } blur() { this.select.blur(); } render() { const { options } = this.state; return ( <SelectComponent {...this.props} ref={ref => { this.select = ref; }} options={options} onChange={this.onChange} /> ); } }; // TODO: do this in package entrypoint const SelectCreatable = makeCreatableSelect<ElementConfig<typeof Select>>( Select ); export default manageState<ElementConfig<typeof SelectCreatable>>( SelectCreatable );