Vulcan/packages/vulcan-forms/lib/components/FormComponent.jsx

303 lines
9 KiB
React
Raw Normal View History

2017-05-19 14:42:43 -06:00
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
2017-06-01 11:42:30 +09:00
import { intlShape } from 'meteor/vulcan:i18n';
import classNames from 'classnames';
import { Components } from 'meteor/vulcan:core';
import { registerComponent } from 'meteor/vulcan:core';
import debounce from 'lodash.debounce';
import get from 'lodash/get';
2018-03-26 14:27:45 +09:00
import { isEmptyValue } from '../modules/utils.js';
2017-05-19 14:42:43 -06:00
class FormComponent extends PureComponent {
constructor(props) {
super(props);
const value = this.getValue(props);
2018-03-26 17:50:03 +09:00
if (this.showCharsRemaining(props)) {
const characterCount = value ? value.length : 0;
this.state = {
2018-03-26 17:50:03 +09:00
charsRemaining: props.max - characterCount,
};
}
}
handleChange = (name, value) => {
2018-03-26 14:27:45 +09:00
if (!!value) {
// if this is a number field, convert value before sending it up to Form
if (this.getType() === 'number') {
value = Number(value);
}
this.context.updateCurrentValues({ [this.props.path]: value });
} else {
this.context.updateCurrentValues({ [this.props.path]: null });
}
// for text fields, update character count on change
2018-03-26 17:50:03 +09:00
if (this.showCharsRemaining()) {
this.updateCharacterCount(value);
}
};
2018-03-24 11:16:11 +09:00
/*
Note: not currently used because when function is debounced
some changes might not register if the user submits form too soon
*/
handleChangeDebounced = debounce(this.handleChange, 500, { leading: true });
2018-03-26 18:00:26 +09:00
updateCharacterCount = value => {
2018-03-26 17:50:03 +09:00
const characterCount = value ? value.length : 0;
this.setState({
charsRemaining: this.props.max - characterCount,
});
};
/*
Get value from Form state through document and currentValues props
*/
getValue = (props) => {
const p = props || this.props;
const { document, currentValues, defaultValue } = p;
2018-03-26 18:00:26 +09:00
// note: value has to default to '' to make component controlled
let value = currentValues[p.path] || get(document, p.path) || '';
2018-03-26 14:27:45 +09:00
// replace empty value, which has not been prefilled, by the default value from the schema
if (isEmptyValue(value)) {
if (defaultValue) value = defaultValue;
2018-03-26 14:27:45 +09:00
}
2018-03-26 18:00:26 +09:00
2018-03-26 14:27:45 +09:00
return value;
};
/*
2018-03-26 17:50:03 +09:00
Whether to keep track of and show remaining chars
*/
2018-03-26 18:00:26 +09:00
showCharsRemaining = props => {
2018-03-26 17:50:03 +09:00
const p = props || this.props;
return p.max && ['url', 'email', 'textarea', 'text'].includes(this.getType(p));
2018-03-26 18:00:26 +09:00
};
2018-03-26 17:50:03 +09:00
/*
Get errors from Form state through context
*/
getErrors = () => {
const fieldErrors = this.props.errors.filter(error => error.data.name === this.props.path);
return fieldErrors;
};
/*
Get form control type, either based on control props, or by guessing
based on form field type
*/
2018-03-26 18:00:26 +09:00
getType = props => {
2018-03-26 17:50:03 +09:00
const p = props || this.props;
const fieldType = p.datatype && p.datatype[0].type;
const autoType =
fieldType === Number ? 'number' : fieldType === Boolean ? 'checkbox' : fieldType === Date ? 'date' : 'text';
return p.control || autoType;
};
renderComponent() {
const {
control,
beforeComponent,
afterComponent,
2018-03-26 17:50:03 +09:00
options,
name,
label,
form,
2018-03-26 14:27:45 +09:00
formType,
2018-03-26 17:50:03 +09:00
} = this.props;
2018-03-26 18:00:26 +09:00
const value = this.getValue();
2018-03-26 17:50:03 +09:00
// these properties are whitelisted so that they can be safely passed to the actual form input
// and avoid https://facebook.github.io/react/warnings/unknown-prop.html warnings
const inputProperties = {
name,
options,
label,
2018-03-24 11:16:11 +09:00
onChange: this.handleChange,
2018-03-26 18:00:26 +09:00
value,
2018-03-26 17:50:03 +09:00
...form,
};
2018-03-26 18:00:26 +09:00
// note: we also pass value on props directly
const properties = { ...this.props, value, errors: this.getErrors(), inputProperties };
2016-04-07 15:24:38 +09:00
// if control is a React component, use it
2018-03-26 17:50:03 +09:00
if (typeof control === 'function') {
const ControlComponent = control;
return <ControlComponent {...properties} />;
} else {
// else pick a predefined component
switch (this.getType()) {
case 'nested':
2018-03-26 18:00:26 +09:00
return <Components.FormNested {...properties} />;
2018-03-22 19:22:54 +09:00
2017-08-19 16:17:52 +09:00
case 'number':
return <Components.FormComponentNumber {...properties} />;
2017-08-19 16:17:52 +09:00
case 'url':
return <Components.FormComponentUrl {...properties} />;
2017-08-19 16:17:52 +09:00
case 'email':
return <Components.FormComponentEmail {...properties} />;
2017-08-19 16:17:52 +09:00
case 'textarea':
return <Components.FormComponentTextarea {...properties} />;
2017-08-19 16:17:52 +09:00
case 'checkbox':
2018-01-25 13:36:20 -06:00
// formsy-react-components expects a boolean value for checkbox
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox.js#L20
properties.value = !!properties.value;
return <Components.FormComponentCheckbox {...properties} />;
2017-08-19 16:17:52 +09:00
case 'checkboxgroup':
2018-01-25 13:36:20 -06:00
// formsy-react-components expects an array value
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/checkbox-group.js#L42
if (!Array.isArray(properties.value)) {
properties.value = [properties.value];
}
2018-03-26 14:27:45 +09:00
// in case of checkbox groups, check "checked" option to populate value if this is a "new document" form
const checkedValues = _.where(properties.options, { checked: true }).map(option => option.value);
2018-03-26 17:50:03 +09:00
if (checkedValues.length && !properties.value && formType === 'new') {
2018-03-26 14:27:45 +09:00
properties.value = checkedValues;
}
return <Components.FormComponentCheckboxGroup {...properties} />;
2017-08-19 16:17:52 +09:00
case 'radiogroup':
// TODO: remove this?
2018-01-25 13:36:20 -06:00
// formsy-react-compnents RadioGroup expects an onChange callback
// https://github.com/twisty/formsy-react-components/blob/v0.11.1/src/radio-group.js#L33
// properties.onChange = (name, value) => {
// this.context.updateCurrentValues({ [name]: value });
// };
return <Components.FormComponentRadioGroup {...properties} />;
2018-01-25 13:36:20 -06:00
2017-08-19 16:17:52 +09:00
case 'select':
const noneOption = {
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
value: '',
disabled: true,
};
console.log(properties.options)
properties.options = [noneOption, ...properties.options];
return <Components.FormComponentSelect {...properties} />;
case 'selectmultiple':
properties.multiple = true;
return <Components.FormComponentSelect {...properties} />;
2018-01-25 13:36:20 -06:00
2017-08-19 16:17:52 +09:00
case 'datetime':
return <Components.FormComponentDateTime {...properties} />;
2018-01-25 13:36:20 -06:00
case 'date':
return <Components.FormComponentDate {...properties} />;
case 'time':
return <Components.FormComponentTime {...properties} />;
2018-01-25 13:36:20 -06:00
2017-08-19 16:17:52 +09:00
case 'text':
return <Components.FormComponentDefault {...properties} />;
2018-01-25 13:36:20 -06:00
default:
2018-03-26 17:50:03 +09:00
const CustomComponent = Components[control];
return CustomComponent ? (
<CustomComponent {...properties} />
) : (
<Components.FormComponentDefault {...properties} />
);
2016-04-04 16:50:07 +09:00
}
}
}
2017-10-18 20:07:43 +09:00
showClear = () => {
2018-03-03 11:09:58 +09:00
return ['datetime', 'time', 'select', 'radiogroup'].includes(this.props.control);
};
2017-10-18 20:07:43 +09:00
clearField = e => {
2017-10-18 20:07:43 +09:00
e.preventDefault();
2018-03-26 18:00:26 +09:00
this.context.updateCurrentValues({ [this.props.path]: null });
};
2017-10-18 20:07:43 +09:00
renderClear() {
return (
<a
href="javascript:void(0)"
className="form-component-clear"
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
onClick={this.clearField}
>
<span></span>
</a>
);
2017-10-18 20:07:43 +09:00
}
render() {
2018-03-26 17:50:03 +09:00
const { beforeComponent, afterComponent, max, name, control } = this.props;
2018-03-25 10:54:45 +09:00
const hasErrors = this.getErrors() && this.getErrors().length;
2018-03-26 18:00:26 +09:00
const inputClass = classNames('form-input', `input-${name}`, `form-component-${control || 'default'}`, {
'input-error': hasErrors,
});
return (
<div className={inputClass}>
2018-03-26 17:50:03 +09:00
{beforeComponent ? beforeComponent : null}
{this.renderComponent()}
{hasErrors ? <Components.FieldErrors errors={this.getErrors()} /> : null}
2017-10-18 20:07:43 +09:00
{this.showClear() ? this.renderClear() : null}
2018-03-26 18:00:26 +09:00
{this.showCharsRemaining() && (
<div className={classNames('form-control-limit', { danger: this.state.charsRemaining < 10 })}>
{this.state.charsRemaining}
</div>
)}
2018-03-26 17:50:03 +09:00
{afterComponent ? afterComponent : null}
</div>
);
}
}
FormComponent.propTypes = {
2017-05-19 14:42:43 -06:00
document: PropTypes.object,
name: PropTypes.string,
label: PropTypes.string,
value: PropTypes.any,
placeholder: PropTypes.string,
prefilledValue: PropTypes.any,
options: PropTypes.any,
control: PropTypes.any,
datatype: PropTypes.any,
2018-03-26 18:00:26 +09:00
path: PropTypes.string,
2017-05-19 14:42:43 -06:00
disabled: PropTypes.bool,
2018-03-26 18:00:26 +09:00
nestedSchema: PropTypes.object,
currentValues: PropTypes.object,
errors: PropTypes.array,
};
FormComponent.contextTypes = {
2017-10-18 20:07:43 +09:00
intl: intlShape,
addToDeletedValues: PropTypes.func,
2018-03-25 10:54:45 +09:00
errors: PropTypes.array,
autofilledValues: PropTypes.object,
deletedValues: PropTypes.array,
getDocument: PropTypes.func,
updateCurrentValues: PropTypes.func,
};
registerComponent('FormComponent', FormComponent);