mirror of
https://github.com/vale981/Vulcan
synced 2025-03-09 20:16:39 -04:00

- Moved UI portions of FormComponent to FormComponentInner.jsx in vulcan-ui-bootstrap - Added user alert when the user navigates to another route while there are unsaved changed in the form (disabled by default) - Added setting forms.warnUnsavedChanges and SmartForm property warnUnsavedChanges to enable user alert - Added optional Revert button in FormSubmits to allow the user to discard any changes to the form; this is activated by passing a "revertCallback" property to SmartForm (which can be empty: () => {}) - Added two functions that form components can access in the child context: refetchForm() to refetch the document from the database (in case it was updated by a background process), isChanged() to determine if there are any unsaved changes - For any phrases I have added to en_US.js I also added it to es_ES.js and fr_FR.js with the comment // TODO: translate - Updated Form.clearForm and Form.mutationSuccessCallback so that the user can continue working on the document after submitting it - The form now scrolls the flash message into view when a submit results in errors - Fixed bugs in FormComponent.shouldComponentUpdate() and Form.getDocument() - Fixed bug in FormComponent.handleChange() - number fields could not be cleared, only set to 0 - Fixed a bug in FormComponent.getValue() - it returned the initial value of a checkbox even after it was set to false, and a number even after it was set to 0
208 lines
6.8 KiB
JavaScript
208 lines
6.8 KiB
JavaScript
import React, { PureComponent } from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import { intlShape } from 'meteor/vulcan:i18n';
|
|
import { Components, registerComponent, instantiateComponent } from 'meteor/vulcan:core';
|
|
import classNames from 'classnames';
|
|
|
|
|
|
class FormComponentInner extends PureComponent {
|
|
|
|
renderClear = () => {
|
|
if (['datetime', 'time', 'select', 'radiogroup'].includes(this.props.input)) {
|
|
return (
|
|
<a
|
|
href="javascript:void(0)"
|
|
className="form-component-clear"
|
|
title={this.context.intl.formatMessage({ id: 'forms.clear_field' })}
|
|
onClick={this.props.clearField}
|
|
>
|
|
<span>✕</span>
|
|
</a>
|
|
);
|
|
}
|
|
};
|
|
|
|
getProperties = () => {
|
|
const { name, options, label, onChange, value } = this.props;
|
|
|
|
// 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,
|
|
onChange,
|
|
value,
|
|
...this.props.inputProperties,
|
|
};
|
|
|
|
return {
|
|
...this.props,
|
|
inputProperties,
|
|
};
|
|
};
|
|
|
|
renderExtraComponent = (extraComponent) => {
|
|
if (!extraComponent) return null;
|
|
|
|
const properties = this.getProperties();
|
|
|
|
return instantiateComponent(extraComponent, properties);
|
|
};
|
|
|
|
|
|
renderComponent = () => {
|
|
const { input, inputType, formType } = this.props;
|
|
const properties = this.getProperties();
|
|
|
|
// if input is a React component, use it
|
|
if (typeof input === 'function') {
|
|
const InputComponent = input;
|
|
return <InputComponent {...properties} />;
|
|
} else {
|
|
// else pick a predefined component
|
|
|
|
switch (inputType) {
|
|
case 'nested':
|
|
return <Components.FormNested {...properties} />;
|
|
|
|
case 'number':
|
|
return <Components.FormComponentNumber {...properties} />;
|
|
|
|
case 'url':
|
|
return <Components.FormComponentUrl {...properties} />;
|
|
|
|
case 'email':
|
|
return <Components.FormComponentEmail {...properties} />;
|
|
|
|
case 'textarea':
|
|
return <Components.FormComponentTextarea {...properties} />;
|
|
|
|
case 'checkbox':
|
|
// 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.inputProperties.value = !!properties.inputProperties.value;
|
|
return <Components.FormComponentCheckbox {...properties} />;
|
|
|
|
case 'checkboxgroup':
|
|
// 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.inputProperties.value)) {
|
|
properties.inputProperties.value = [properties.inputProperties.value];
|
|
}
|
|
// 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);
|
|
if (checkedValues.length && !properties.inputProperties.value && formType === 'new') {
|
|
properties.inputProperties.value = checkedValues;
|
|
}
|
|
return <Components.FormComponentCheckboxGroup {...properties} />;
|
|
|
|
case 'radiogroup':
|
|
// TODO: remove this?
|
|
// 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} />;
|
|
|
|
case 'select':
|
|
const noneOption = {
|
|
label: this.context.intl.formatMessage({ id: 'forms.select_option' }),
|
|
value: '',
|
|
disabled: true,
|
|
};
|
|
properties.inputProperties.options = [noneOption, ...properties.inputProperties.options];
|
|
|
|
return <Components.FormComponentSelect {...properties} />;
|
|
|
|
case 'selectmultiple':
|
|
properties.inputProperties.multiple = true;
|
|
return <Components.FormComponentSelect {...properties} />;
|
|
|
|
case 'datetime':
|
|
return <Components.FormComponentDateTime {...properties} />;
|
|
|
|
case 'date':
|
|
return <Components.FormComponentDate {...properties} />;
|
|
|
|
case 'time':
|
|
return <Components.FormComponentTime {...properties} />;
|
|
|
|
case 'text':
|
|
return <Components.FormComponentDefault {...properties} />;
|
|
|
|
default:
|
|
const CustomComponent = Components[input];
|
|
return CustomComponent ? (
|
|
<CustomComponent {...properties} />
|
|
) : (
|
|
<Components.FormComponentDefault {...properties} />
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
render () {
|
|
const {
|
|
inputClassName,
|
|
name,
|
|
input,
|
|
beforeComponent,
|
|
afterComponent,
|
|
errors,
|
|
showCharsRemaining,
|
|
charsRemaining,
|
|
} = this.props;
|
|
|
|
const hasErrors = errors && errors.length;
|
|
|
|
const inputName = typeof input === 'function' ? input.name : input;
|
|
const inputClass = classNames('form-input', inputClassName, `input-${name}`,
|
|
`form-component-${inputName || 'default'}`, { 'input-error': hasErrors, });
|
|
|
|
return (
|
|
<div className={inputClass}>
|
|
{this.renderExtraComponent(beforeComponent)}
|
|
{this.renderComponent()}
|
|
{hasErrors ? <Components.FieldErrors errors={errors}/> : null}
|
|
{this.renderClear()}
|
|
{showCharsRemaining &&
|
|
<div className={classNames('form-control-limit', { danger: charsRemaining < 10 })}>
|
|
{charsRemaining}
|
|
</div>
|
|
}
|
|
{this.renderExtraComponent(afterComponent)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
FormComponentInner.propTypes = {
|
|
inputClassName: PropTypes.string,
|
|
name: PropTypes.string.isRequired,
|
|
input: PropTypes.any,
|
|
beforeComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
|
afterComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
|
clearField: PropTypes.func.isRequired,
|
|
errors: PropTypes.array.isRequired,
|
|
help: PropTypes.node,
|
|
onChange: PropTypes.func.isRequired,
|
|
showCharsRemaining: PropTypes.bool.isRequired,
|
|
charsRemaining: PropTypes.number,
|
|
charsCount: PropTypes.number,
|
|
charsMax: PropTypes.number,
|
|
};
|
|
|
|
FormComponentInner.contextTypes = {
|
|
intl: intlShape,
|
|
};
|
|
|
|
FormComponentInner.displayName = 'FormComponentInner';
|
|
|
|
|
|
registerComponent('FormComponentInner', FormComponentInner);
|