react-object-form
Version:
React Object Form
317 lines (272 loc) • 10.5 kB
JSX
import React from 'react';
import Select from 'react-select';
import GenericValueInput from './GenericValueInput';
import PropTypes from 'prop-types';
//Workaround. See -> https://phabricator.babeljs.io/T6777
typeof undefined;
let console = console ? console : {};
console.log = console.log ? console.log : () => {};
//TODO: Make all components configurable by checking for component override via config
const InternalObjectValuePropType = PropTypes.oneOfType([
PropTypes.object,
PropTypes.bool,
PropTypes.number,
PropTypes.string,
PropTypes.arrayOf([
PropTypes.bool,
PropTypes.number,
PropTypes.string])]);
//Base shape for property config
const PropertyConfig =
{
//Property key in Object
name: PropTypes.string.isRequired,
//If property is a child-object configs for its properties can be supplied
config: PropTypes.arrayOf(PropTypes.shape(PropertyConfig)),
//Possible values for this property
options: PropTypes.array,
//true if property can contain values which are not listed in "options"
allowCustomValues: PropTypes.bool,
//true if form field should be read-only
disabled: PropTypes.bool,
clearable: PropTypes.bool,
resetValue: PropTypes.shape({label: PropTypes.string, option: InternalObjectValuePropType}),
placeholder: PropTypes.string,
label: PropTypes.string,
//TODO: concept / implement
validator: PropTypes.func,
caption: PropTypes.string,
changeHandler: PropTypes.func,
hide: PropTypes.bool,
trim: PropTypes.bool
};
class ReactObjectForm extends React.Component {
static propTypes = {
object: PropTypes.object.isRequired,
config: PropTypes.arrayOf(PropTypes.shape(PropertyConfig)),
changeHandler: PropTypes.func,
id: PropTypes.string
};
render(){
let {object, config, id, ...rest } = this.props;
let configtmp = {};
configtmp.config = config;
//obey PropTypes
configtmp.name = id ? id : 'react-object';
configtmp = Object.assign({},config, configtmp);
return(
<form className={`${configtmp.name}-form`}>
<BaseFormRenderer {...rest} {...configtmp} id={configtmp.name} object={object} />
</form>
);
}
}
export const BooleanValueInput = ({value, id, name, changeHandler,disabled, ...rest}) => {
let internalChangeHandler = (event) => changeHandler(event.target.checked);
return(
<input {...rest}
id={id+'-input'}
className={`${disabled ? 'disabled': ''} form-control boolean-value-input`}
type="checkbox"
checked={value ? 'checked' : null}
value={name}
name={name}
onChange={internalChangeHandler}
disabled={disabled ? 'disabled': null}/>);
};
BooleanValueInput.propTypes = {
...PropertyConfig,
value: PropTypes.bool,
config: PropTypes.shape(PropertyConfig)
};
export class NumberValueInput extends React.Component {
constructor(props){
super(props);
this.state = {value: props.value};
this.numberParsingChangeHandler = this.numberParsingChangeHandler.bind(this);
}
componentWillReceiveProps(next){
if (next.value != this.state.value){
this.setState({value: next.value});
}
}
numberParsingChangeHandler(value){
this.setState({value: value});
const parsed = Number.parseFloat(value);
if (Number.isFinite(parsed)){
this.props.changeHandler(parsed);
} else if (value === ''){
this.setState({value: Number.NaN});
this.props.changeHandler(Number.NaN);
}
}
render(){
const { value, ...rest } = this.props;
return(<GenericValueInput {...rest}
value={Number.isNaN(value) ? '' : this.state.value}
changeHandler={this.numberParsingChangeHandler}/>);
}
}
NumberValueInput.propTypes = {
...PropertyConfig,
value: PropTypes.bool,
config: PropTypes.shape(PropertyConfig)
};
export const FieldRenderer = ({name,id, object, caption, label, ...rest}) => {
//Use capitalized field name as label if not set
let labelString;
if (label){
labelString = label;
} else {
const [firstLetter, ...rest] = name;
labelString = firstLetter.toLocaleUpperCase()+rest.join('');
}
return(
<div className="form-group">
<label>{labelString}</label>
<div>
<BaseFormRenderer {...rest} id={id} name={name} object={object} />
<span>{caption}</span>
</div>
</div>
);
};
FieldRenderer.propTypes = {
config: PropTypes.arrayOf(PropTypes.shape(PropertyConfig)),
object: InternalObjectValuePropType,
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
caption: PropTypes.string,
label: PropTypes.string
};
export const ObjectFormRenderer = ({object, config, changeHandler, id, ...rest}) => {
const childConfig = (name) => {
return config ? config.find((currentConfig) => currentConfig.name === name) : null;
};
const createChildChangeHandler = (name) => (newObjectValue) => {
if (childConfig(name) && childConfig(name).hasOwnProperty('changeHandler') && 'function' == typeof childConfig(name).changeHandler){
childConfig(name).changeHandler(newObjectValue, changeHandler);
} else {
let changedObject = Object.assign({}, object);
changedObject[name] = newObjectValue;
changeHandler(changedObject);
}
};
//add configured fields in configured order
const objectFields = Object.keys(object);
let fields = config ? config.map(c => c.name) : [];
//add remaining fields without config
objectFields.forEach(f => {
if (!fields.includes(f)){
fields.push(f);
}
});
fields = fields.filter((name) => {
let currchildConfig = childConfig(name);
if(! currchildConfig){
return true;
}
return !currchildConfig.hide;
})
.map((childPropertyName) => {
const childPropertyConfig = childConfig(childPropertyName);
const prefix = id && id != '' ? id+'-' : id;
return(
<FieldRenderer {...rest} {...childPropertyConfig}
key={childPropertyName}
id={prefix+childPropertyName}
name={childPropertyName}
object={object[childPropertyName]}
changeHandler={createChildChangeHandler(childPropertyName)}/>);
});
return(
<fieldset id={id+'-fieldset'}>
{fields}
</fieldset>
);
};
ObjectFormRenderer.propTypes = {
object:PropTypes.object,
config: PropTypes.arrayOf(PropTypes.shape(PropertyConfig)),
changeHandler: PropTypes.func,
id: PropTypes.string.isRequired
};
export const SelectRenderer = ({value, options, id, changeHandler, allowCustomValues, multi, placeholder, resetValue, ...rest}) => {
let internalChangeHandler = (values) => {
if (values){
if (Array.isArray(values)){
changeHandler(values.map((value) => {return value.value ? value.value : null;} ));
} else {
changeHandler(values.value ? values.value : null);
}
}
};
//transform options if not in correct form
const transformedOptions = options.map(option => typeof option === 'object' ? option : {label: option, value: option});
//default behaviour of react-select is to use clearable = true, but to make this work a resetValue
//in the correct format is needed
return(
<Select
{...rest}
options={transformedOptions}
name={id+'-select'}
value={value}
onChange={internalChangeHandler}
multi={multi}
resetValue={resetValue ? resetValue : {label: '', value: null}}
allowCreate={allowCustomValues}
placeholder={placeholder ? placeholder : 'Select..'}
/>
);
};
SelectRenderer.propTypes = {...PropertyConfig};
export const MultiSelectRenderer = ({value, ...rest}) => {
return(<SelectRenderer {...rest} value={value} multi={true}/>);
};
MultiSelectRenderer.propTypes = {...PropertyConfig};
export const BaseFormRenderer = ({object,config, name, options, component: Component, ...rest}) => {
//TODO: handle explicitly configured inputs
if (Component){
return(<Component {...rest} value={object} name={name} config={config} />);
}
//handle generic cases
const valueType = typeof object;
if (options && Array.isArray(options)){
return(<SelectRenderer {...rest} {...config} value={object} name={name} options={options} />);
}
if (object === null && !options){
return(<GenericValueInput trim {...rest} {...config} value={''} name={name}/>);
}
switch (valueType){
case 'number':
return(<NumberValueInput {...rest} {...config} value={object} name={name}/>);
case 'object':
if (Array.isArray(object)){
return (<MultiSelectRenderer {...rest} {...config} value={object} name={name} options={options ? options : []} />);
}
return(<ObjectFormRenderer
{...rest}
config={config}
object={object} name={name} />);
case 'boolean':
return(<BooleanValueInput {...rest} {...config} value={object} name={name}/>);
case 'string':
//if there are options configured, use SelectRenderer
if (options && Array.isArray(options)){
return(<SelectRenderer {...rest} {...config} value={object} name={name} options={options} />);
}
return(<GenericValueInput trim {...rest} {...config} value={object} name={name}/>);
default:
return(<GenericValueInput trim {...rest} {...config} value={object} name={name}/>);
}
};
BaseFormRenderer.propTypes = {
...PropertyConfig,
object: InternalObjectValuePropType,
config: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.shape(PropertyConfig)),
PropTypes.shape(PropertyConfig)
])
};
export default ReactObjectForm;
export { PropertyConfig };