diff --git a/README.md b/README.md index 27f19880ef..ef236c0c79 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ You can enable multi-value selection by setting `multi={true}`. In this mode: * The `onChange` event provides an array of the selected options as the second argument * The first argument to `onChange` is always a string, regardless of whether the values of the selected options are numbers or strings * By default, only options in the `options` array can be selected. Setting `allowCreate` to true allows new options to be created if they do not already exist. +* You can set a `multiSum={true}` if you would like to have the list of selected options summarized. There is a default option that can also be sent as a prop `multiSumLimit={number}` (default is set to 3. If the select items is less than the limit then it just prints out an ordered list, if it is more it will say x of y selected for example: "7 of 20 selected". If all items are selected it will say "All", and if all but one or two it will say "All except, {valueLabel}". ### Async options @@ -168,6 +169,8 @@ For multi-select inputs, when providing a custom `filterOptions` method, remembe matchPos | string | (any, start) match the start or entire string when filtering matchProp | string | (any, label, value) which option property to filter on multi | bool | multi-value input + multiSum | bool | multi-value text summary option + multiSumLimit | bool | number of values until it stops listing them out one by one name | string | field name, for hidden tag noResultsText | string | placeholder displayed when there are no matching search results onBlur | func | onBlur handler: function(event) {} @@ -197,4 +200,4 @@ See our [CONTRIBUTING.md](https://github.com/JedWatson/react-select/blob/master/ # License -MIT Licensed. Copyright (c) Jed Watson 2015. +MIT Licensed. Copyright (c) Jed Watson 2015. \ No newline at end of file diff --git a/examples/src/app.js b/examples/src/app.js index cb06945ba6..dd382d42a3 100644 --- a/examples/src/app.js +++ b/examples/src/app.js @@ -11,6 +11,7 @@ import StatesField from './components/StatesField'; import UsersField from './components/UsersField'; import ValuesAsNumbersField from './components/ValuesAsNumbersField'; import DisabledUpsellOptions from './components/DisabledUpsellOptions'; +import MultiSelectSummaryField from './components/MultiSelectSummaryField'; var FLAVOURS = [ { label: 'Chocolate', value: 'chocolate' }, @@ -40,6 +41,7 @@ React.render( + , document.getElementById('example') -); +); \ No newline at end of file diff --git a/examples/src/components/MultiSelectSummaryField.js b/examples/src/components/MultiSelectSummaryField.js new file mode 100644 index 0000000000..36d4ccc222 --- /dev/null +++ b/examples/src/components/MultiSelectSummaryField.js @@ -0,0 +1,41 @@ +import React from 'react'; +import Select from 'react-select'; + +function logChange() { + console.log.apply(console, [].concat(['Select value changed:'], Array.prototype.slice.apply(arguments))); +} + +var MultiSelectField = React.createClass({ + displayName: 'MultiSelectField', + propTypes: { + label: React.PropTypes.string, + }, + getInitialState () { + return { + value: [] + }; + }, + handleSelectChange (value, values) { + logChange('New value:', value, 'Values:', values); + this.setState({ value: value }); + }, + + render () { + var ops = [ + { 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' } + ]; + return ( +
+

{this.props.label}

+ tag newOptionCreator: React.PropTypes.func, // factory to create new options when allowCreate set noResultsText: React.PropTypes.string, // placeholder displayed when there are no matching search results @@ -74,6 +76,7 @@ var Select = React.createClass({ inputProps: {}, matchPos: 'any', matchProp: 'any', + multiSumLimit: 3, name: undefined, newOptionCreator: undefined, noResultsText: 'No results found', @@ -269,7 +272,7 @@ var Select = React.createClass({ if (typeof values === 'string') { values = values === '' ? [] - : this.props.multi + : this.props.multi ? values.split(this.props.delimiter) : [ values ]; } else { @@ -631,9 +634,11 @@ var Select = React.createClass({ focusAdjacentOption: function(dir) { this._focusedOptionReveal = true; + var ops = this.state.filteredOptions.filter(function(op) { return !op.disabled; }); + if (!this.state.isOpen) { this.setState({ isOpen: true, @@ -675,6 +680,7 @@ var Select = React.createClass({ } }, + buildMenu: function() { var focusedValue = this.state.focusedOption ? this.state.focusedOption.value : null; var renderLabel = this.props.optionRenderer || function(op) { @@ -695,24 +701,55 @@ var Select = React.createClass({ }; options.unshift(newOption); } - var ops = Object.keys(options).map(function(key) { - var op = options[key]; + + if (this.props.multi && this.props.multiSum){ + options = options.map(function(opt){ + opt.type = 'opt'; + opt.isMulti = false; + opt.renderLabel = undefined; + opt.selectValue = undefined; + return opt; + }); + + if (this.state.values.length > 0){ + var multiValues = this.state.values.map(function(val){ + val.type = 'multiSum'; + val.isMulti = true; + var optionRenderer = this.props.optionRenderer; + val.renderLabel = function(op){ + var label = op.label; + if (optionRenderer){ + label = optionRenderer(op); + } + return 'x ' + label; + }; + + val.selectValue = this.removeValue.bind(this, val); + return val; + }, this); + + options = multiValues.concat(options); + } + } + + var ops = options.map(function(op) { var isSelected = this.state.value === op.value; var isFocused = focusedValue === op.value; var optionClass = classes({ 'Select-option': true, 'is-selected': isSelected, 'is-focused': isFocused, - 'is-disabled': op.disabled + 'is-disabled': op.disabled, + 'is-multiSum': op.isMulti }); var ref = isFocused ? 'focused' : null; var mouseEnter = this.focusOption.bind(this, op); var mouseLeave = this.unfocusOption.bind(this, op); - var mouseDown = this.selectValue.bind(this, op); + var mouseDown = op.selectValue || this.selectValue.bind(this, op); var optionResult = React.createElement(this.props.optionComponent, { - key: 'option-' + op.value, + key: 'option-' + op.value + '-' + op.type, className: optionClass, - renderFunc: renderLabel, + renderFunc: ( op.renderLabel || renderLabel), mouseEnter: mouseEnter, mouseLeave: mouseLeave, mouseDown: mouseDown, @@ -724,6 +761,8 @@ var Select = React.createClass({ return optionResult; }, this); + + if (ops.length) { return ops; } else { @@ -753,6 +792,30 @@ var Select = React.createClass({ } }, + summarizeValues: function(values){ + var summary = ''; + + if (values.length < this.props.multiSumLimit){ + this.state.values.forEach( function(opt, i){ + summary = summary + opt.label; + if (i < (values.length - 1) ){ + summary = summary + ', '; + } + }); + return summary; + + } else if (values.length === this.props.options.length){ + return 'All'; + } else if (values.length >= (this.props.options.length - 2) ){ + this.state.filteredOptions.forEach( function(opt){ + summary = summary + ', ' + opt.label; + }); + return 'All except' + summary; + } + + return summary = values.length + ' of ' + this.props.options.length + ' selected'; + }, + render: function() { var selectClass = classes('Select', this.props.className, { 'is-multi': this.props.multi, @@ -765,6 +828,7 @@ var Select = React.createClass({ }); var value = []; if (this.props.multi) { + this.state.values.forEach(function(val) { var onOptionLabelClick = this.handleOptionLabelClick.bind(this, val); var onRemove = this.removeValue.bind(this, val); @@ -779,6 +843,10 @@ var Select = React.createClass({ }); value.push(valueComponent); }, this); + + if (this.props.multiSum && value.length > 0){ + value = this.summarizeValues(value); + } } if (!this.state.inputValue && (!this.props.multi || !value.length)) { @@ -799,6 +867,7 @@ var Select = React.createClass({ } } + var loading = this.state.isLoading ?