basic/form/GenericField.js
/**
* Created by kinneretzin on 15/11/2016.
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Input, Checkbox, Dropdown, Form } from '../index';
import { getToolbox } from '../../../utils/Toolbox';
/**
* GenericField is a generic component which can be used as different input fields in {@link Form} component
*
* It is used widely in widget configuration modal. Constant values used for defining field type are described below.
*
* ## Access
* `Stage.Basic.Form.GenericField`
*
* ## Usage
*
* ### String input field
* ![GenericField](manual/asset/form/GenericField_1.png)
* ```
* <GenericField name="stringTest" type={GenericField.STRING_TYPE}
* label="STRING_TYPE" icon="rocket" placeholder="Write text..." />
* ```
*
* ### Password input field
* ![GenericField](manual/asset/form/GenericField_2.png)
* ```
* <GenericField name="passwordTest" type={GenericField.PASSWORD_TYPE}
* label="PASSWORD_TYPE" icon="key" value="" />
* ```
*
* ### Number input field
* ![GenericField](manual/asset/form/GenericField_3.png)
* ```
* <GenericField name="numberTest" type={GenericField.NUMBER_TYPE}
* label="NUMBER_TYPE" value="5" min={1} max={10} />
* ```
*
* ### Boolean input field
* ![GenericField](manual/asset/form/GenericField_4.png)
* ```
* <GenericField name="booleanTest" type={GenericField.BOOLEAN_TYPE}
* label="BOOLEAN_TYPE" value="true" />
* ```
*
* ### List input field
* ![GenericField](manual/asset/form/GenericField_5.png)
* ```
* <GenericField name="listTest" type={GenericField.LIST_TYPE}
* label="LIST_TYPE" items={['a','b','c']} value='b' />
* ```
*
* ### Number list input field
* ![GenericField](manual/asset/form/GenericField_6.png)
* ```
* <GenericField name="numberListTest" type={GenericField.NUMBER_LIST_TYPE}
* label="NUMBER_LIST_TYPE" items={[1,2,3]} value={2} />
* ```
*
* ### Multi select list input field
* ![GenericField](manual/asset/form/GenericField_7.png)
* ```
* <GenericField name="multiSelectListTest" type={GenericField.MULTI_SELECT_LIST_TYPE}
* label="MULTI_SELECT_LIST_TYPE" value={[2,3,4]} items={[1,2,3,{value:4, name:'four'}, {value:5, name:'five'}]} />
* ```
*
* ### Editable list input field
* ![GenericField](manual/asset/form/GenericField_8.png)
* ```
* <GenericField name="editableListTest" type={GenericField.EDITABLE_LIST_TYPE}
* label="EDITABLE_LIST_TYPE" value='b' items={['a','b','c']}/>
* ```
*
* ### Editable number list input field
* ![GenericField](manual/asset/form/GenericField_9.png)
* ```
* <GenericField name="numberEditableListTest" type={GenericField.NUMBER_EDITABLE_LIST_TYPE}
* label="NUMBER_EDITABLE_LIST_TYPE" items={[1,2,3]} value={2}/>
* ```
*
* ### Custom field - Editable table
* ![GenericField](manual/asset/form/GenericField_10.png)
* ```
* <GenericField name="editableTable" type={GenericField.CUSTOM_TYPE} component={Stage.Basic.Form.Table}
* label="EDITABLE_TABLE_TYPE"
* columns={[
* {name: "metric", label: 'Metric', default: "", type: Stage.Basic.GenericField.EDITABLE_LIST_TYPE, description: "Name of the metric to be presented on the graph",
* items: ["", "cpu_total_system", "cpu_total_user", "memory_MemFree", "memory_SwapFree", "loadavg_processes_running"]},
* {name: 'label', label: 'Label', default: "", type: Stage.Basic.GenericField.STRING_TYPE, description: "Chart label"},
* {name: 'unit', label: 'Unit', default: "", type: Stage.Basic.GenericField.STRING_TYPE, description: "Chart data unit"}
* ]}
* rows={3} />
* ```
*
* ### Custom filed - Time filter
* ![GenericField](manual/asset/form/GenericField_11.png)
* ```
* <GenericField name="timeFilterTest" type={GenericField.CUSTOM_TYPE} component={Stage.Basic.TimeFilter}
* label="TIME_FILTER_TYPE" value={Stage.Basic.TimeFilter.DEFAULT_VALUE} />
* ```
*/
export default class GenericField extends Component {
/**
* alphanumeric input field
*/
static STRING_TYPE = 'string';
/**
* password input field
*/
static PASSWORD_TYPE = 'password';
/**
* numeric input field
*/
static NUMBER_TYPE = 'number';
/**
* two-state input field
*/
static BOOLEAN_TYPE = 'boolean';
/**
* boolean with no default
*/
static BOOLEAN_LIST_TYPE = 'booleanList';
/**
* dropdown alphanumeric list field
*/
static LIST_TYPE = 'list';
/**
* dropdown numeric list field
*/
static NUMBER_LIST_TYPE = 'numberList';
/**
* dropdown multiselection list
*/
static MULTI_SELECT_LIST_TYPE = 'multiSelectList';
/**
* dropdown editable list
*/
static EDITABLE_LIST_TYPE = 'editableList';
/**
* dropdown editable numeric list
*/
static NUMBER_EDITABLE_LIST_TYPE = 'numberEditableList';
/**
* custom input field
*/
static CUSTOM_TYPE = 'custom';
static isListType(type) {
return (
type === GenericField.LIST_TYPE ||
type === GenericField.NUMBER_LIST_TYPE ||
type === GenericField.MULTI_SELECT_LIST_TYPE ||
type === GenericField.BOOLEAN_LIST_TYPE ||
type === GenericField.EDITABLE_LIST_TYPE ||
type === GenericField.NUMBER_EDITABLE_LIST_TYPE
);
}
/**
* propTypes
*
* @property {string} label field's label to show above the field
* @property {string} name name of the input field
* @property {string} [placeholder=''] specifies a short hint that describes the expected value of an input field
* @property {string} [error=false] specifies if a field should be marked as field with error
* @property {string} [type=GenericField.STRING_TYPE] specifies type of the field
* @property {string} [icon=null] additional icon in right side of the input field
* @property {string|element} [description=''] fields description showed in popup when user hovers field
* @property {object} [value=''] specifies the value of an <input> element
* @property {boolean} [required={true}] define if a field is required adding a red star icon to label
* @property {object[]} [items=[]] list of items (for list types)
* @property {Function} [onChange=()=>{}] function called on input value change
* @property {number} [max=null] maximal value (only for {@link GenericField.NUMBER_TYPE} type)
* @property {number} [min=null] minimal value (only for {@link GenericField.NUMBER_TYPE} type)
*/
static propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
placeholder: PropTypes.string,
error: PropTypes.bool,
type: PropTypes.string,
icon: PropTypes.string,
description: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
value: PropTypes.any,
required: PropTypes.bool,
onChange: PropTypes.func,
storeValueInContext: PropTypes.bool,
// field specific configuration
items: PropTypes.array,
max: PropTypes.number,
min: PropTypes.number
};
static defaultProps = {
placeholder: '',
error: false,
type: GenericField.STRING_TYPE,
icon: null,
description: '',
value: '',
onChange: () => {},
storeValueInContext: false,
// field specific configuration
items: [],
max: null,
min: null
};
constructor(props, context) {
super(props, context);
this.toolbox = getToolbox(() => {}, () => {}, null);
this.state = GenericField.isListType(props.type) ? { options: [] } : {};
}
_initOptions(props) {
if (props.type === GenericField.BOOLEAN_LIST_TYPE) {
this.setState({
options: [{ text: 'false', value: false }, { text: 'true', value: true }]
});
} else if (GenericField.isListType(props.type) && props.items) {
let valueAlreadyInOptions = false;
const options = _.map(props.items, item => {
if (!_.isObject(item)) {
item = { name: item, value: item };
}
if (item.value === props.value) {
valueAlreadyInOptions = true;
}
return { text: item.name, value: item.value };
});
if (props.type !== GenericField.MULTI_SELECT_LIST_TYPE && !valueAlreadyInOptions) {
options.push({ text: props.value, value: props.value });
}
this.setState({ options });
}
}
_storeValueInContext(name, value) {
this.toolbox.getContext().setValue([name], value);
this.toolbox.getEventBus().trigger(`${name}:change`);
}
_handleInputChange(proxy, field) {
if (this.props.storeValueInContext) {
this._storeValueInContext(field.name, field.value);
}
this.props.onChange(proxy, { ...field, genericType: this.props.type });
}
componentDidMount() {
if (this.props.storeValueInContext) {
this._storeValueInContext(this.props.name, this.props.value);
}
this._initOptions(this.props);
}
componentDidUpdate(prevProps) {
if (this.props.items !== prevProps.items) {
this._initOptions(this.props);
}
}
shouldComponentUpdate(nextProps, nextState) {
return (
!_.isEqual(JSON.stringify(this.props), JSON.stringify(nextProps)) ||
!_.isEqual(JSON.stringify(this.state), JSON.stringify(nextState))
);
}
static formatValue(type, value) {
if (type === GenericField.MULTI_SELECT_LIST_TYPE) {
value = _.split(value, ',');
} else if (type === GenericField.BOOLEAN_TYPE) {
value = (_.isBoolean(value) && value) || (_.isString(value) && value === 'true');
} else if (
type === GenericField.NUMBER_TYPE ||
type === GenericField.NUMBER_LIST_TYPE ||
type === GenericField.NUMBER_EDITABLE_LIST_TYPE
) {
value = parseInt(value) || 0;
}
return value;
}
render() {
let field;
if (
this.props.type === GenericField.STRING_TYPE ||
this.props.type === GenericField.NUMBER_TYPE ||
this.props.type === GenericField.PASSWORD_TYPE
) {
field = (
<Input
icon={this.props.icon}
name={this.props.name}
type={this.props.type === GenericField.STRING_TYPE ? 'text' : this.props.type}
placeholder={this.props.placeholder}
value={this.props.value === null ? '' : this.props.value}
onChange={this._handleInputChange.bind(this)}
max={this.props.type === GenericField.NUMBER_TYPE ? this.props.max : null}
min={this.props.type === GenericField.NUMBER_TYPE ? this.props.min : null}
/>
);
} else if (this.props.type === GenericField.BOOLEAN_TYPE) {
field = (
<Checkbox
name={this.props.name}
toggle
checked={
(_.isBoolean(this.props.value) && this.props.value) ||
(_.isString(this.props.value) && this.props.value === 'true')
}
onChange={this._handleInputChange.bind(this)}
/>
);
} else if (GenericField.isListType(this.props.type)) {
field = (
<Dropdown
fluid
selection
value={this.props.value}
name={this.props.name}
multiple={this.props.type === GenericField.MULTI_SELECT_LIST_TYPE}
allowAdditions={
this.props.type === GenericField.EDITABLE_LIST_TYPE ||
this.props.type === GenericField.NUMBER_EDITABLE_LIST_TYPE
}
search={
this.props.type === GenericField.EDITABLE_LIST_TYPE ||
this.props.type === GenericField.NUMBER_EDITABLE_LIST_TYPE
}
placeholder={this.props.placeholder || 'Please select'}
options={this.state.options}
onAddItem={(e, { value }) => {
this.setState({ options: [{ text: value, value }, ...this.state.options] });
}}
onChange={this._handleInputChange.bind(this)}
clearable={false}
/>
);
} else if (this.props.type === GenericField.CUSTOM_TYPE) {
const optionalProps = _.keys(GenericField.defaultProps);
const requiredProps = ['name', 'label', 'component'];
const componentProps = _.omit(this.props, [...optionalProps, ...requiredProps]);
const CustomComponent = this.props.component;
if (_.isUndefined(CustomComponent)) {
return new Error(`For \`${this.props.type}\` type \`component\` prop have to be supplied.`);
}
field = (
<CustomComponent
name={this.props.name}
value={_.isUndefined(this.props.value) ? this.props.default : this.props.value}
onChange={this._handleInputChange.bind(this)}
{...componentProps}
/>
);
}
return (
<Form.Field
className={this.props.name}
help={this.props.description}
label={this.props.label}
required={this.props.required}
error={this.props.error}
>
{field}
</Form.Field>
);
}
}