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

349 lines
9.7 KiB
React
Raw Normal View History

import React, { Component } from 'react';
2017-05-19 14:42:43 -06:00
import PropTypes from 'prop-types';
import { Components } from 'meteor/vulcan:core';
import { registerComponent } from 'meteor/vulcan:core';
import get from 'lodash/get';
2018-03-28 11:51:18 +09:00
import merge from 'lodash/merge';
import find from 'lodash/find';
import isObjectLike from 'lodash/isObjectLike';
import isEqual from 'lodash/isEqual';
2018-03-26 14:27:45 +09:00
import { isEmptyValue } from '../modules/utils.js';
class FormComponent extends Component {
2018-05-11 09:52:04 +09:00
constructor(props) {
super(props);
this.state = {};
}
2018-05-11 09:52:04 +09:00
componentWillMount() {
if (this.showCharsRemaining()) {
const value = this.getValue();
this.updateCharacterCount(value);
}
}
2018-05-11 09:52:04 +09:00
shouldComponentUpdate(nextProps, nextState) {
// allow custom controls to determine if they should update
if (this.isCustomInput(this.getType(nextProps))) {
return true;
}
2018-05-11 09:52:04 +09:00
const { currentValues, deletedValues, errors } = nextProps;
const { path } = this.props;
2018-05-11 09:52:04 +09:00
const valueChanged = get(currentValues, path) !== get(this.props.currentValues, path);
const errorChanged = !isEqual(this.getErrors(errors), this.getErrors());
const deleteChanged = deletedValues.includes(path) !== this.props.deletedValues.includes(path);
const charsChanged = nextState.charsRemaining !== this.state.charsRemaining;
2018-06-30 09:23:49 +02:00
const disabledChanged = nextProps.disabled !== this.props.disabled;
2018-05-11 09:52:04 +09:00
2018-06-30 09:23:49 +02:00
return valueChanged || errorChanged || deleteChanged || charsChanged || disabledChanged;
}
2018-05-11 09:52:04 +09:00
/*
If this is an intl input, get _intl field instead
*/
2018-05-11 09:52:04 +09:00
getPath = props => {
const p = props || this.props;
return p.intlInput ? `${p.path}_intl` : p.path;
2018-05-11 09:52:04 +09:00
};
/*
Returns true if the passed input type is a custom
*/
2018-05-11 09:52:04 +09:00
isCustomInput = inputType => {
const isStandardInput = [
'nested',
'number',
'url',
'email',
'textarea',
'checkbox',
'checkboxgroup',
'radiogroup',
'select',
'selectmultiple',
'datetime',
'date',
'time',
'text',
].includes(inputType);
return !isStandardInput;
};
2018-05-11 09:52:04 +09:00
/*
Function passed to form controls (always controlled) to update their value
*/
handleChange = (name, value) => {
2018-04-06 17:56:25 +09:00
// if value is an empty string, delete the field
if (value === '') {
value = null;
}
// if this is a number field, convert value before sending it up to Form
if (this.getType() === 'number' && value != null) {
value = Number(value);
}
const updateValue = this.props.locale ? { locale: this.props.locale, value } : value;
this.props.updateCurrentValues({ [this.getPath()]: updateValue });
// 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
/*
Updates the state of charsCount and charsRemaining as the users types
2018-03-24 11:16:11 +09:00
*/
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,
charsCount: characterCount,
2018-03-26 17:50:03 +09:00
});
};
/*
Get value from Form state through document and currentValues props
*/
getValue = props => {
2018-03-28 11:51:18 +09:00
let value;
const p = props || this.props;
const { document, currentValues, defaultValue, datatype } = p;
// for intl field fetch the actual field value by adding .value to the path
const path = p.locale ? `${this.getPath(p)}.value` : this.getPath(p);
2018-03-28 11:51:18 +09:00
const documentValue = get(document, path);
const currentValue = get(currentValues, path);
2018-03-28 11:51:18 +09:00
const isDeleted = p.deletedValues.includes(path);
2018-04-06 17:56:25 +09:00
2018-03-28 11:51:18 +09:00
if (isDeleted) {
value = '';
} else {
if (p.locale) {
2018-05-11 09:52:04 +09:00
// note: intl fields are of type Object but should be treated as Strings
value = currentValue || documentValue || '';
} else if (Array.isArray(currentValue) && find(datatype, ['type', Array])) {
// for object and arrays, use lodash's merge
// if field type is array, use [] as merge seed to force result to be an array as well
value = merge([], documentValue, currentValue);
} else if (isObjectLike(currentValue) && find(datatype, ['type', Object])) {
value = merge({}, documentValue, currentValue);
2018-03-28 11:51:18 +09:00
} else {
// note: value has to default to '' to make component controlled
value = '';
if (typeof currentValue !== 'undefined' && currentValue !== null) {
value = currentValue;
} else if (typeof documentValue !== 'undefined' && documentValue !== null) {
value = documentValue;
}
2018-03-28 11:51:18 +09:00
}
// replace empty value, which has not been prefilled, by the default value from the schema for new forms only
if (isEmptyValue(value) && p.formType === 'new') {
2018-03-28 11:51:18 +09:00
if (defaultValue) value = defaultValue;
}
2018-03-26 14:27:45 +09:00
}
return this.cleanValue(p, value);
};
2018-05-11 09:52:04 +09:00
/*
For some input types apply additional normalization
*/
cleanValue = (props, value) => {
const p = props || this.props;
2018-05-11 09:52:04 +09:00
if (p.input === 'checkbox') {
value = !!value;
} else if (p.input === 'checkboxgroup') {
if (!Array.isArray(value)) {
value = [value];
}
2018-05-11 09:52:04 +09:00
// in case of checkbox groups, check "checked" option to populate value
// if this is a "new document" form
2018-05-11 09:52:04 +09:00
const checkedValues = _.where(p.options, { checked: true }).map(option => option.value);
if (checkedValues.length && !value && p.formType === 'new') {
value = checkedValues;
}
}
2018-05-11 09:52:04 +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
Note: we use `includes` to get all errors from nested components, which have longer paths
*/
2018-05-11 09:52:04 +09:00
getErrors = errors => {
errors = errors || this.props.errors;
const fieldErrors = errors.filter(error => error.path.includes(this.props.path));
return fieldErrors;
};
/*
Get form input type, either based on input 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 =
2018-05-11 09:52:04 +09:00
fieldType === Number ? 'number' : fieldType === Boolean ? 'checkbox' : fieldType === Date ? 'date' : 'text';
return p.input || autoType;
};
/*
Function passed to form controls to clear their contents (set their value to null)
*/
clearField = event => {
event.preventDefault();
event.stopPropagation();
2018-04-06 17:56:25 +09:00
this.props.updateCurrentValues({ [this.props.path]: null });
if (this.showCharsRemaining()) {
this.updateCharacterCount(null);
}
};
2018-05-11 09:52:04 +09:00
/*
Function passed to FormComponentInner to help with rendering the component
*/
getFormInput = () => {
const inputType = this.getType();
2018-05-11 09:52:04 +09:00
// if input is a React component, use it
if (typeof this.props.input === 'function') {
const InputComponent = this.props.input;
return InputComponent;
} else {
// else pick a predefined component
2018-05-11 09:52:04 +09:00
switch (inputType) {
case 'text':
return Components.FormComponentDefault;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'number':
return Components.FormComponentNumber;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'url':
return Components.FormComponentUrl;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'email':
return Components.FormComponentEmail;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'textarea':
return Components.FormComponentTextarea;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'checkbox':
return Components.FormComponentCheckbox;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'checkboxgroup':
return Components.FormComponentCheckboxGroup;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'radiogroup':
return Components.FormComponentRadioGroup;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'select':
return Components.FormComponentSelect;
2018-05-11 09:52:04 +09:00
case 'selectmultiple':
return Components.FormComponentSelectMultiple;
2018-05-11 09:52:04 +09:00
2017-08-19 16:17:52 +09:00
case 'datetime':
return Components.FormComponentDateTime;
2018-05-11 09:52:04 +09:00
case 'date':
return Components.FormComponentDate;
2018-05-11 09:52:04 +09:00
case 'time':
return Components.FormComponentTime;
2018-05-11 09:52:04 +09:00
default:
const CustomComponent = Components[this.props.input];
return CustomComponent ? CustomComponent : Components.FormComponentDefault;
2016-04-04 16:50:07 +09:00
}
}
};
2018-05-11 09:52:04 +09:00
render() {
if (this.props.intlInput) {
return <Components.FormIntl {...this.props} />;
} else if (this.props.nestedInput){
return <Components.FormNested {...this.props} />;
}
2017-10-18 20:07:43 +09:00
return (
<Components.FormComponentInner
{...this.props}
{...this.state}
inputType={this.getType()}
value={this.getValue()}
errors={this.getErrors()}
document={this.context.getDocument()}
showCharsRemaining={!!this.showCharsRemaining()}
onChange={this.handleChange}
clearField={this.clearField}
formInput={this.getFormInput()}
/>
);
2017-10-18 20:07:43 +09:00
}
}
FormComponent.propTypes = {
2017-05-19 14:42:43 -06:00
document: PropTypes.object,
name: PropTypes.string.isRequired,
2017-05-19 14:42:43 -06:00
label: PropTypes.string,
value: PropTypes.any,
placeholder: PropTypes.string,
prefilledValue: PropTypes.any,
options: PropTypes.any,
input: PropTypes.any,
2017-05-19 14:42:43 -06:00
datatype: PropTypes.any,
path: PropTypes.string.isRequired,
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.isRequired,
deletedValues: PropTypes.array.isRequired,
throwError: PropTypes.func.isRequired,
updateCurrentValues: PropTypes.func.isRequired,
errors: PropTypes.array.isRequired,
2018-04-06 17:56:25 +09:00
addToDeletedValues: PropTypes.func,
clearFieldErrors: PropTypes.func.isRequired,
currentUser: PropTypes.object,
};
FormComponent.contextTypes = {
getDocument: PropTypes.func.isRequired,
};
registerComponent('FormComponent', FormComponent);