UNPKG

react-object-form

Version:
317 lines (272 loc) 10.5 kB
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 };