diff --git a/src/components/input/autocomplete-select/consult.js b/src/components/input/autocomplete-select/consult.js index a9e3389bd..805985020 100644 --- a/src/components/input/autocomplete-select/consult.js +++ b/src/components/input/autocomplete-select/consult.js @@ -1,25 +1,58 @@ -import React, { Component } from 'react'; +import React, { Component, PropTypes } from 'react'; import ComponentBaseBehaviour from '../../../behaviours/component-base'; @ComponentBaseBehaviour +/** + * Autcomplete select component consultation view. + */ class AutocompleteSelectConsult extends Component { + /** DisplayName. */ + static displayName = 'AutocompleteSelectConsult'; + + /** PropTypes. */ + static propTypes = { + keyName: PropTypes.string, + keyResolver: PropTypes.func.isRequired, + label: PropTypes.string.isRequired, + labelName: PropTypes.string, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, + wholeItem: PropTypes.bool + }; + + /** DefaultProps. */ + static defaultProps = { + keyName: 'key', + labelName: 'label', + wholeItem: false + }; + + /** Initial state. */ state = {}; + /** @inheritdoc */ componentDidMount() { this._callKeyResolver(this.props.value); } + /** @inheritdoc */ componentWillReceiveProps({ value }) { if (value !== this.props.value) { this._callKeyResolver(value); } } + /** + * Callback to resolve value into label. + * @param {string} value value. + */ _callKeyResolver(value) { - const { keyResolver } = this.props; + const { keyResolver, keyName, wholeItem } = this.props; if (keyResolver && value !== undefined && value !== null) { - keyResolver(value).then(label => { + const key = wholeItem ? value[keyName] : value; + keyResolver(key).then(label => { this.setState({ resolvedLabel: label }); }).catch(err => { console.error(err.message); }); } else { @@ -27,9 +60,10 @@ class AutocompleteSelectConsult extends Component { } } + /** @inheritdoc */ render() { - const { label, name, type, value } = this.props; - const { resolvedLabel = value } = this.state; + const { label, name, type, value, wholeItem, labelName } = this.props; + const { resolvedLabel = wholeItem ? value[labelName] : value } = this.state; return (
{ - this.setState({ inputValue, fromKeyResolver: true }); - }).catch(error => this.setState({ customError: error.message })); + if (value !== undefined && value !== null) { + this._setValue(this.props); } document.addEventListener('click', this._handleDocumentClick); this._debouncedQuerySearcher = debounce(this._querySearcher, inputTimeout); } - componentWillReceiveProps({ value, customError, error, keyResolver }) { + /** @inheritdoc */ + componentWillReceiveProps(nextProps) { + const { value, customError, error, labelName, wholeItem } = nextProps; if (value !== this.props.value && value !== undefined && value !== null) { // value is defined, call the keyResolver to get the associated label - this.setState({ inputValue: value, customError }, () => keyResolver(value).then(inputValue => { - this.setState({ inputValue, fromKeyResolver: true }); - }).catch(error => this.setState({ customError: error.message }))); + const inputValue = wholeItem ? value[labelName] : value; + this.setState({ inputValue, customError }, () => this._setValue(nextProps)); } else if (customError !== this.props.customError) { this.setState({ customError }); } @@ -90,6 +116,7 @@ class Autocomplete extends Component { } } + /** @inheritdoc */ componentDidUpdate() { if (this.props.customError) { this.refs.inputText.classList.add('is-invalid'); @@ -98,26 +125,54 @@ class Autocomplete extends Component { } } + /** @inheritdoc */ componentWillUnmount() { document.removeEventListener('click', this._handleDocumentClick); } + + /** + * Set value. + * + * @param {object} object containing props + * @memberof AutocompleteSelectEdit + */ + _setValue({ value, keyResolver, labelName, wholeItem }) { + + if (wholeItem) { + this.setState({ inputValue: value[labelName], fromKeyResolver: true }); + } else { + keyResolver(value).then(inputValue => { + this.setState({ inputValue, fromKeyResolver: true }); + }).catch(error => this.setState({ customError: error.message })); + } + } + + /** + * Get value. + * @returns {string} value. + */ getValue() { - const { labelName, keyName, value } = this.props; + const { labelName, value, wholeItem } = this.props; const { inputValue, selected, options, fromKeyResolver } = this.state; - const resolvedLabel = options.get(selected); + const item = options.get(selected) || {}; + const resolvedLabel = item[labelName]; if (inputValue === '') { // The user cleared the field, return a null return null; - } else if (fromKeyResolver) { // Value was received from the keyResolver, give it firectly + } else if (fromKeyResolver) { // Value was received from the keyResolver, give it directly return value; } else if (resolvedLabel !== inputValue && selected !== inputValue) { // The user typed something without selecting any option, return a null return null; } else { // The user selected an option (or no value was provided), return it - return selected || null; + return wholeItem ? item : selected || null; } } - _handleDocumentClick = ({ target }) => { + /** + * Handle document click. + * @param {object} event event. + */ + _handleDocumentClick({ target }) { const { focus, inputValue } = this.state; const { onBadInput } = this.props; if (focus) { @@ -130,40 +185,56 @@ class Autocomplete extends Component { }); } } - }; + } - _handleQueryChange = ({ target: { value } }) => { + /** + * Handle query change. + * @param {object} event event. + */ + _handleQueryChange({ target: { value } }) { if (value === '') { // the user cleared the input, don't call the querySearcher const { onChange } = this.props; this.setState({ inputValue: value, fromKeyResolver: false }); - if (onChange) onChange(null); + if (onChange) { + onChange(null); + } } else { this.setState({ inputValue: value, fromKeyResolver: false, isLoading: true }); this._debouncedQuerySearcher(value); } - }; + } - _querySearcher = value => { - const { querySearcher, keyName, labelName } = this.props; + /** + * Query searcher. + * @param {string} value value. + */ + _querySearcher(value) { + const { querySearcher, keyName } = this.props; querySearcher(value).then(({ data, totalCount }) => { - // TODO handle the incomplete option list case const options = new Map(); data.forEach(item => { - options.set(item[keyName], item[labelName]); + options.set(item[keyName], item); }); this.setState({ options, isLoading: false, totalCount }); }).catch(error => this.setState({ customError: error.message })); - }; + } - _handleQueryFocus = () => { + /** + * Handle query field focus. + */ + _handleQueryFocus() { this.refs.options.scrollTop = 0; if (this.props.onFocus) { this.props.onFocus.call(this); } this.setState({ active: '', focus: true }); - }; + } - _handleQueryKeyDown = (event) => { + /** + * Handle query field key down. + * @param {object} event event. + */ + _handleQueryKeyDown(event) { event.stopPropagation(); const { which } = event; const { active, options } = this.state; @@ -188,16 +259,25 @@ class Autocomplete extends Component { } this.setState({ active: optionKeys[newIndex] }); } - }; + } - _handleSuggestionHover = key => { + /** + * Handle suggestion hover. + * @param {number} key key. + */ + _handleSuggestionHover(key) { this.setState({ active: key }); - }; + } + /** + * Handle selection of result. + * @param {number} key key. + */ _select(key) { const { options } = this.state; - const { onChange } = this.props; - const resolvedLabel = options.get(key) || ''; + const { onChange, labelName, wholeItem } = this.props; + const item = options.get(key) || {}; + const resolvedLabel = item[labelName] || ''; this.refs.htmlInput.blur(); let newState = { inputValue: this.i18n(resolvedLabel), selected: key, focus: false }; if (this.props.onSelectClear === true) { @@ -205,25 +285,30 @@ class Autocomplete extends Component { } this.setState(newState, () => { if (onChange) { - onChange(key); + onChange(wholeItem ? item : key); } }); } - _renderOptions = () => { + /** + * Render options. + * @returns {JSXElement} options. + */ + _renderOptions() { + const { labelName } = this.props; const { active, options, focus } = this.state; const renderedOptions = []; - for (let [key, value] of options) { + for (let [key, item] of options) { const isActive = active === key; renderedOptions.push(
  • this._select(key)} + onMouseOver={() => this._handleSuggestionHover(key)} > - {this.i18n(value)} + {this.i18n(item[labelName])}
  • ); } @@ -233,8 +318,9 @@ class Autocomplete extends Component { {renderedOptions} ); - }; + } + /** @inheritdoc */ render() { const { inputValue, isLoading } = this.state; const { customError, renderOptions } = this.props; @@ -270,8 +356,4 @@ class Autocomplete extends Component { } } -Autocomplete.displayName = 'Autocomplete'; -Autocomplete.defaultProps = defaultProps; -Autocomplete.propTypes = propTypes; - -export default Autocomplete; +export default AutocompleteSelectEdit; diff --git a/src/components/input/autocomplete-select/field.js b/src/components/input/autocomplete-select/field.js index d6acb14d8..15d5b9822 100644 --- a/src/components/input/autocomplete-select/field.js +++ b/src/components/input/autocomplete-select/field.js @@ -1,44 +1,82 @@ import React, { Component, PropTypes } from 'react'; import AutocompleteSelectEdit from './edit'; import AutocompleteSelectConsult from './consult'; -import translation from 'focus-core/translation'; +import { translate } from 'focus-core/translation'; +/** + * Autocomplete select component. + */ class AutocompleteSelectField extends Component { - state = {}; + static displayName = 'AutocompleteSelectField'; + /** Proptypes. */ static propTypes = { isEdit: PropTypes.bool.isRequired, keyResolver: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, querySearcher: PropTypes.func.isRequired }; + /** Initial state. */ + state = {}; + + /** + * AutocompleteSelectField constructor. + * @param {object} props props. + */ + constructor(props) { + super(props); + + this._handleAutocompleteBadInput = this._handleAutocompleteBadInput.bind(this); + this._handleAutocompleteChange = this._handleAutocompleteChange.bind(this); + } + + /** @inheritdoc */ componentWillReceiveProps({ error }) { this.setState({ customError: error }); } - getValue = () => { + /** + * Get value. + * @returns {string} value. + */ + getValue() { const { isEdit, value } = this.props; if (isEdit) { return this.refs.autocomplete.getValue(); } else { return value; } - }; + } - _handleAutocompleteBadInput = value => { - this.setState({ customError: translation.translate('autocomplete.error.badInput', { value }) }) - }; + /** + * Handle bad input and display error message. + * @param {string} value value. + */ + _handleAutocompleteBadInput(value) { + this.setState({ customError: translate('autocomplete.error.badInput', { value }) }) + } - _handleAutocompleteChange = value => { + /** + * Handle for value selection. + * @param {object} value value. + */ + _handleAutocompleteChange(value) { const { onChange } = this.props; this.setState({ customError: null }, () => { - if (onChange) onChange(value); + if (onChange) { + onChange(value); + } }); - }; + } - _renderEdit = () => { + /** + * Renders component in edition mode. + * @returns {JSXElement} Edit component. + */ + _renderEdit() { const { customError } = this.state; return ( ); - }; + } - _renderConsult = () => { + /** + * Renders component in consultation mode. + * @returns {JSXElement} Consult component. + */ + _renderConsult() { return ( ); - }; + } + /** @inheritdoc */ render() { const { isEdit } = this.props; return isEdit ? this._renderEdit() : this._renderConsult();