UNPKG

@eccenca/gui-elements

Version:

Collection of low-level GUI elements like Buttons, Icons or Alerts. Also includes core styles for those elements.

322 lines (286 loc) 10.6 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; import Select from 'react-select/lib/Select'; import Creatable from 'react-select/lib/Creatable'; import Async from 'react-select/lib/Async'; import AsyncCreatable from 'react-select/lib/AsyncCreatable'; import _ from 'lodash'; import UniqueIdWrapper from '../../utils/uniqueId'; import Button from '../Button/Button'; // format value to lowercase string const stringCompare = function (value) { return _.toLower(_.toString(value)); }; const clearRenderer = () => ( <Button iconName="clear" className="mdl-button--clearance" tabIndex={-1} /> ); /** The SelectBox wraps [react-select](https://github.com/JedWatson/react-select) to use mixed content of strings and numbers as well as the default object type. The SelectBox behaves like a [controlled input](https://facebook.github.io/react/docs/forms.html#controlled-components). Please refer to all available properties in the linked documentations. ```js import { SelectBox } from '@eccenca/gui-elements'; const Page = React.createClass({ getInitialState(){ return { value: 8, }; }, selectBoxOnChange(value){ this.setState({ value }); }, // template rendering render() { return ( <SelectBox placeholder="Label for SelectBox" options={['label1', 3]} optionsOnTop={true} // option list opens up on top of select input (default: false) value={this.state.value} onChange={this.selectBoxOnChange} creatable={true} // allow creation of new values promptTextCreator={(newLabel) => ('New stuff: ' + newLabel)} // change default "Create option 'newLabel'" to "New stuff: 'newLabel'" multi={true} // allow multi selection clearable={false} // hide 'remove all selected values' button searchable={true} // whether to behave like a type-ahead or not reducedSize={false} // remove vertical whitespace around element, default: false /> ) }, }); ``` Note: - if objects are used in multi selectable options you can add {"clearableValue": false} to it to hide delete button for this specifc object - if "creatable" is set new values will be applied on Enter, Tab and Comma (",") - ``placeholder`` label is used within MDL floating label layout */ class SelectBox extends Component { static displayName = 'SelectBox'; static propTypes = { /** * contains values which are available in dropdown list * options is an array of objects or strings and/or numbers */ options: PropTypes.arrayOf( (propValue, key, componentName, location, propFullName) => { const containObjects = _.isPlainObject(_.head(propValue)); const isObject = _.isPlainObject(propValue[key]); const isNumberOrString = _.isString(propValue[key]) || _.isNumber(propValue[key]); if ( (!containObjects && !isNumberOrString) || (containObjects && !isObject) ) { return new Error( `Invalid prop \`${propFullName}\` supplied to` + ` \`${componentName}\`. No mixed content (object vs string/number) allowed.` ); } return false; } ).isRequired, /** * contains selected value * value is an object or a strings a numbers */ value: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.object, // only needed for multiple inputs PropTypes.array, ]), // onChange handler onChange: PropTypes.func.isRequired, // allow creation of new values creatable: PropTypes.bool, /** * remove vertical whitespace around element */ reducedSize: PropTypes.bool, }; static defaultProps = { creatable: false, reducedSize: false, }; constructor(props) { super(props); this.onChange = this.onChange.bind(this); this.uniqueOptions = this.uniqueOptions.bind(this); this.onFocus = this.onFocus.bind(this); this.onBlur = this.onBlur.bind(this); } onChange(newValue) { // If the options consist of plainvalues, we just want to return the plain value if (_.get(newValue, '$plainValue', false)) { return this.props.onChange(newValue.value, this.props.name); } return this.props.onChange(newValue, this.props.name); } // default check for value creation // prevent double values (check case insensitive, and handle numbers as string) uniqueOptions({ option: newObject, options }) { return !_.some( options, ({ value, label }) => stringCompare(value) === stringCompare(newObject.value) && stringCompare(label) === stringCompare(newObject.label) ); } onFocus() { this.setState({ focused: true, }); } onBlur() { this.setState({ focused: false, }); } render() { const { autofocus, className, creatable, placeholder = '', reducedSize, optionsOnTop, value, async = false, uniqueId, ...passProps } = this.props; // we do not want to pass onChange, as we wrap onChange ourselves delete passProps.onChange; // we do not want to pass name, as we use it ourselves delete passProps.name; passProps.onFocus = this.onFocus; passProps.onBlur = this.onBlur; passProps.clearable = _.isUndefined(passProps.clearable) ? true : passProps.clearable; if (passProps.clearable) { passProps.clearRenderer = clearRenderer; } const focused = this.state && typeof this.state.focused !== 'undefined' ? this.state.focused : autofocus; const classes = classNames( { 'mdl-textfield mdl-js-textfield mdl-textfield--full-width': true, // use always 'mdl-textfield--floating-label': true, // use always 'mdl-textfield--reduced': reducedSize === true, 'mdl-textfield--missinglabel': !placeholder, 'is-dirty': !_.isNil(value) && (_.isNumber(value) || !_.isEmpty(value)), 'is-focused': focused === true, 'Select--optionsontop': optionsOnTop === true, }, className ); let parsedValue = null; // if value is not empty or a number check for formatting if (!_.isEmpty(value) || _.isNumber(value)) { // in case of multi select is used if (_.isArray(value)) { parsedValue = _.map( value, it => (_.isPlainObject(it) ? it : { value: it, label: it }) ); } else { parsedValue = _.isPlainObject(value) ? value : { value, label: value }; } } let component = null; if (async) { const { ignoreCase = false, ignoreAccents = false, ...passAsyncProps } = passProps; if (creatable) { component = ( <AsyncCreatable {...passAsyncProps} value={parsedValue} id={uniqueId} onChange={this.onChange} isOptionUnique={ this.props.isOptionUnique || this.uniqueOptions } ignoreAccents={ignoreAccents} ignoreCase={ignoreCase} placeholder="" /> ); } else { component = ( <Async {...passAsyncProps} id={uniqueId} value={parsedValue} onChange={this.onChange} ignoreAccents={ignoreAccents} ignoreCase={ignoreCase} placeholder="" /> ); } } else { const { options, ...passSyncProps } = passProps; // parse values to object format if needed const parsedOptions = _.isPlainObject(options[0]) ? options : _.map(options, it => ({ value: it, label: it, $plainValue: true, })); if (creatable) { component = ( <Creatable {...passSyncProps} id={uniqueId} value={parsedValue} options={parsedOptions} onChange={this.onChange} isOptionUnique={ this.props.isOptionUnique || this.uniqueOptions } placeholder="" /> ); } else { component = ( <Select {...passSyncProps} id={uniqueId} value={parsedValue} options={parsedOptions} onChange={this.onChange} placeholder="" /> ); } } return ( <div className={classes} data-test-id={`ecc-gui-elements-${uniqueId}`} > {component} <label className="mdl-textfield__label" htmlFor={uniqueId}> {placeholder} </label> </div> ); } } export default UniqueIdWrapper(SelectBox, { prefix: 'selectBox', targetProp: 'uniqueId', });