@plone/volto
Version:
Volto
361 lines (343 loc) • 9.73 kB
JSX
/**
* Modal form component.
* @module components/manage/Form/ModalForm
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import keys from 'lodash/keys';
import map from 'lodash/map';
import {
Button,
Form as UiForm,
Header,
Menu,
Message,
Modal,
Dimmer,
Loader,
} from 'semantic-ui-react';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import FormValidation from '@plone/volto/helpers/FormValidation/FormValidation';
import Icon from '@plone/volto/components/theme/Icon/Icon';
import { Field } from '@plone/volto/components/manage/Form';
import aheadSVG from '@plone/volto/icons/ahead.svg';
import clearSVG from '@plone/volto/icons/clear.svg';
const messages = defineMessages({
required: {
id: 'Required input is missing.',
defaultMessage: 'Required input is missing.',
},
minLength: {
id: 'Minimum length is {len}.',
defaultMessage: 'Minimum length is {len}.',
},
uniqueItems: {
id: 'Items must be unique.',
defaultMessage: 'Items must be unique.',
},
save: {
id: 'Save',
defaultMessage: 'Save',
},
cancel: {
id: 'Cancel',
defaultMessage: 'Cancel',
},
});
/**
* Modal form container class.
* @class ModalForm
* @extends Component
*/
class ModalForm extends Component {
/**
* Property types.
* @property {Object} propTypes Property types.
* @static
*/
static propTypes = {
schema: PropTypes.shape({
fieldsets: PropTypes.arrayOf(
PropTypes.shape({
fields: PropTypes.arrayOf(PropTypes.string),
id: PropTypes.string,
title: PropTypes.string,
}),
),
properties: PropTypes.objectOf(PropTypes.any),
required: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.objectOf(PropTypes.any),
formData: PropTypes.objectOf(PropTypes.any),
submitError: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
onCancel: PropTypes.func,
onChangeFormData: PropTypes.func,
open: PropTypes.bool,
submitLabel: PropTypes.string,
loading: PropTypes.bool,
loadingMessage: PropTypes.string,
className: PropTypes.string,
};
/**
* Default properties.
* @property {Object} defaultProps Default properties.
* @static
*/
static defaultProps = {
submitLabel: null,
onCancel: null,
formData: {},
open: true,
loading: null,
loadingMessage: null,
submitError: null,
className: null,
dimmer: null,
};
/**
* Constructor
* @method constructor
* @param {Object} props Component properties
* @constructs ModalForm
*/
constructor(props) {
super(props);
this.state = {
currentTab: 0,
errors: {},
isFormPristine: true,
formData: props.formData,
};
this.selectTab = this.selectTab.bind(this);
this.onChangeField = this.onChangeField.bind(this);
this.onBlurField = this.onBlurField.bind(this);
this.onClickInput = this.onClickInput.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
/**
* Change field handler
* @method onChangeField
* @param {string} id Id of the field
* @param {*} value Value of the field
* @returns {undefined}
*/
onChangeField(id, value) {
this.setState({
formData: {
...this.state.formData,
[id]: value,
},
});
}
/**
* If user clicks on input, the form will be not considered pristine
* this will avoid onBlur effects without interaction with the form
* @param {Object} e event
*/
onClickInput(e) {
this.setState({ isFormPristine: false });
}
/**
* Validate fields on blur
* @method onBlurField
* @param {string} id Id of the field
* @param {*} value Value of the field
* @returns {undefined}
*/
onBlurField(id, value) {
if (!this.state.isFormPristine) {
const errors = FormValidation.validateFieldsPerFieldset({
schema: this.props.schema,
formData: this.state.formData,
formatMessage: this.props.intl.formatMessage,
touchedField: { [id]: value },
});
this.setState({
errors,
});
}
}
/**
* Submit handler
* @method onSubmit
* @param {Object} event Event object.
* @returns {undefined}
*/
onSubmit(event) {
event.preventDefault();
const errors = FormValidation.validateFieldsPerFieldset({
schema: this.props.schema,
formData: this.state.formData,
formatMessage: this.props.intl.formatMessage,
});
if (keys(errors).length > 0) {
this.setState({
errors,
});
} else {
let setFormDataCallback = (formData) => {
this.setState({ formData: formData, errors: {} });
};
this.props.onSubmit(this.state.formData, setFormDataCallback);
}
}
/**
* Select tab handler
* @method selectTab
* @param {Object} event Event object.
* @param {number} index Selected tab index.
* @returns {undefined}
*/
selectTab(event, { index }) {
this.setState({
currentTab: index,
});
}
/**
* Component did update lifecycle handler
* @param {Object} prevProps
* @param {Object} prevState
*/
async componentDidUpdate(prevProps, prevState) {
if (this.props.onChangeFormData) {
if (!isEqual(prevState?.formData, this.state.formData)) {
this.props.onChangeFormData(this.state.formData);
}
}
if (!isEqual(prevProps.formData, this.props.formData)) {
let newFormData = {};
map(keys(this.props.formData), (field) => {
if (!isEqual(prevProps.formData[field], this.props.formData[field])) {
newFormData[field] = this.props.formData[field];
}
});
this.setState({
formData: {
...this.state.formData,
...newFormData,
},
});
}
}
/**
* Render method.
* @method render
* @returns {string} Markup for the component.
*/
render() {
const { schema, onCancel, description } = this.props;
const currentFieldset = schema.fieldsets[this.state.currentTab];
const fields = currentFieldset
? map(currentFieldset.fields, (field) => ({
...schema.properties[field],
id: field,
value: this.state.formData[field],
required: schema.required.indexOf(field) !== -1,
onChange: this.onChangeField,
onBlur: this.onBlurField,
onClick: this.onClickInput,
}))
: [];
const state_errors = keys(this.state.errors).length > 0;
return (
<Modal
dimmer={this.props.dimmer}
open={this.props.open}
className={this.props.className}
>
<Header>{this.props.title}</Header>
<Dimmer active={this.props.loading}>
<Loader>
{this.props.loadingMessage || (
<FormattedMessage id="Loading" defaultMessage="Loading." />
)}
</Loader>
</Dimmer>
<Modal.Content scrolling>
<UiForm
method="post"
onSubmit={this.onSubmit}
error={state_errors || Boolean(this.props.submitError)}
>
{description}
<Message error>
{state_errors ? (
<FormattedMessage
id="There were some errors."
defaultMessage="There were some errors."
/>
) : (
''
)}
<div>{this.props.submitError}</div>
</Message>
{schema.fieldsets?.length > 1 && (
<Menu tabular stackable>
{map(schema.fieldsets, (item, index) => (
<Menu.Item
name={item.id}
index={index}
key={item.id}
active={this.state.currentTab === index}
onClick={this.selectTab}
>
{item.title}
</Menu.Item>
))}
</Menu>
)}
{fields.map((field) => (
<Field
{...field}
key={field.id}
onBlur={this.onBlurField}
onClick={this.onClickInput}
error={this.state.errors[field.id]}
/>
))}
</UiForm>
</Modal.Content>
<Modal.Actions>
<Button
basic
circular
primary
floated="right"
aria-label={
this.props.submitLabel
? this.props.submitLabel
: this.props.intl.formatMessage(messages.save)
}
title={
this.props.submitLabel
? this.props.submitLabel
: this.props.intl.formatMessage(messages.save)
}
onClick={this.onSubmit}
loading={this.props.loading}
>
<Icon name={aheadSVG} className="contents circled" size="30px" />
</Button>
{onCancel && (
<Button
type="button"
basic
circular
secondary
aria-label={this.props.intl.formatMessage(messages.cancel)}
title={this.props.intl.formatMessage(messages.cancel)}
floated="right"
onClick={onCancel}
>
<Icon name={clearSVG} className="circled" size="30px" />
</Button>
)}
</Modal.Actions>
</Modal>
);
}
}
export default injectIntl(ModalForm);