Skip to content

Commit

Permalink
feat(Dropdown): add support for allowAdditions (#356)
Browse files Browse the repository at this point in the history
  • Loading branch information
dylankiss authored and levithomason committed Aug 18, 2016
1 parent 4557289 commit ba1c9b3
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 3 deletions.
72 changes: 72 additions & 0 deletions docs/app/Examples/modules/Dropdown/Types/AllowAdditions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { Component } from 'react'
import { Dropdown, Grid, Header } from 'stardust'

const options = [
{ text: 'English', value: 'English' },
{ text: 'French', value: 'French' },
{ text: 'Spanish', value: 'Spanish' },
{ text: 'German', value: 'German' },
{ text: 'Chinese', value: 'Chinese' },
]

export default class AllowAdditionsExample extends Component {
state = { optionsSingle: options, optionsMultiple: options }

handleAdditionSingle = value => {
// validate, save to server, show a message to the user ...
this.setState({
optionsSingle: [{ text: value, value }, ...this.state.optionsSingle],
})
}

handleAdditionMultiple = value => {
// validate, save to server, show a message to the user ...
this.setState({
optionsMultiple: [{ text: value, value }, ...this.state.optionsMultiple],
})
}

handleChangeSingle = (e, value) => this.setState({ currentValue: value })

handleChangeMultiple = (e, values) => this.setState({ currentValues: values })

render() {
const { currentValue, currentValues } = this.state

return (
<Grid>
<Grid.Column width={8}>
<Header>Single Option</Header>
<pre>Current value: {currentValue}</pre>
<Dropdown
options={this.state.optionsSingle}
placeholder='Choose Language'
search
selection
fluid
allowAdditions
onAddItem={this.handleAdditionSingle}
onChange={this.handleChangeSingle}
/>
</Grid.Column>
<Grid.Column width={8}>
<Header>Multiple Options</Header>
<pre>Current values: {JSON.stringify(currentValues)}</pre>
<Dropdown
options={this.state.optionsMultiple}
placeholder='Choose Languages'
search
selection
fluid
multiple
allowAdditions
additionPosition='top'
additionLabel=''
onAddItem={this.handleAdditionMultiple}
onChange={this.handleChangeMultiple}
/>
</Grid.Column>
</Grid>
)
}
}
10 changes: 10 additions & 0 deletions docs/app/Examples/modules/Dropdown/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ const DropdownExamples = () => (
Selection dropdowns must use the <code>options</code> prop, instead of sub component markup.
</Message>
</ComponentExample>
<ComponentExample
title='Allow Additions'
description='A dropdown that allows user additions'
examplePath='modules/Dropdown/Types/AllowAdditions'
>
<Message className='info'>
The <code>allowAdditions</code> prop requires you to handle updating the options list
with the new value via the <code>onAddItem</code> prop.
</Message>
</ComponentExample>
</ExampleSection>
<ExampleSection title='States'>
<ComponentExample
Expand Down
42 changes: 39 additions & 3 deletions src/modules/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const _meta = {
type: META.TYPES.MODULE,
props: {
pointing: ['bottom left', 'bottom right'],
additionPosition: ['top', 'bottom'],
},
}

Expand Down Expand Up @@ -101,6 +102,24 @@ export default class Dropdown extends Component {
/** Name of the hidden input which holds the value. */
name: PropTypes.string,

/**
* Allow user additions to the list of options (boolean).
* Requires the use of `selection`, `options` and `search`.
*/
allowAdditions: customPropTypes.every([
customPropTypes.demand(['options', 'selection', 'search']),
PropTypes.bool,
]),

/** Called with the new value added by the user. Use this to update the options list. */
onAddItem: PropTypes.func,

/** Position of the `Add: ...` option in the dropdown list ('top' or 'bottom'). */
additionPosition: PropTypes.oneOf(_meta.props.additionPosition),

/** Label prefixed to an option added by a user. */
additionLabel: PropTypes.string,

// ------------------------------------
// Callbacks
// ------------------------------------
Expand Down Expand Up @@ -177,6 +196,7 @@ export default class Dropdown extends Component {

static defaultProps = {
icon: 'dropdown',
additionLabel: 'Add:',
}

static autoControlledProps = [
Expand Down Expand Up @@ -347,12 +367,15 @@ export default class Dropdown extends Component {
if (keyboardKey.getCode(e) !== keyboardKey.Enter) return
e.preventDefault()

const { multiple } = this.props
const { multiple, onAddItem, options } = this.props
const value = this.getSelectedItemValue()

// prevent selecting null if there was no selected item value
if (!value) return

// notify the onAddItem prop if this is a new value
if (onAddItem && !_.some(options, { text: value })) onAddItem(value)

// notify the onChange prop that the user is trying to change value
if (multiple) {
// state value may be undefined
Expand Down Expand Up @@ -408,7 +431,7 @@ export default class Dropdown extends Component {
handleItemClick = (e, value) => {
debug('handleItemClick()')
debug(value)
const { multiple } = this.props
const { multiple, onAddItem, options } = this.props

// prevent toggle() in handleClick()
e.stopPropagation()
Expand All @@ -417,6 +440,9 @@ export default class Dropdown extends Component {
e.nativeEvent.stopImmediatePropagation()
}

// notify the onAddItem prop if this is a new value
if (onAddItem && !_.some(options, { value })) onAddItem(value)

// notify the onChange prop that the user is trying to change value
if (multiple) {
const newValue = _.union(this.state.value, [value])
Expand Down Expand Up @@ -488,7 +514,7 @@ export default class Dropdown extends Component {
// ----------------------------------------

getMenuOptions = () => {
const { multiple, options, search } = this.props
const { multiple, search, allowAdditions, additionPosition, additionLabel, options } = this.props
const { searchQuery, value } = this.state

let filteredOptions = options
Expand All @@ -504,6 +530,16 @@ export default class Dropdown extends Component {
filteredOptions = _.filter(filteredOptions, (opt) => re.test(opt.text))
}

// insert the "add" item
if (allowAdditions && search && searchQuery && !_.some(filteredOptions, { text: searchQuery })) {
const addItem = {
text: additionLabel ? `${additionLabel} ${searchQuery}` : searchQuery,
value: searchQuery,
}
if (additionPosition === 'top') filteredOptions.unshift(addItem)
else filteredOptions.push(addItem)
}

return filteredOptions
}

Expand Down
Loading

0 comments on commit ba1c9b3

Please sign in to comment.