src/applications/_mock-form-ae-design-patterns/patterns/pattern2/TaskBlue/SchemaForm.jsx
import PropTypes from 'prop-types';
import React from 'react';
import { merge, once } from 'lodash';
import classNames from 'classnames';
import Form from '@department-of-veterans-affairs/react-jsonschema-form';
import { deepEquals } from '@department-of-veterans-affairs/react-jsonschema-form/lib/utils';
import set from 'platform/utilities/data/set';
import { connect } from 'react-redux';
import {
uiSchemaValidate,
transformErrors,
} from 'platform/forms-system/src/js/validation';
import FieldTemplate from 'platform/forms-system/src/js/components/FieldTemplate';
import * as reviewWidgets from 'platform/forms-system/src/js/review/widgets';
import ReviewFieldTemplate from 'platform/forms-system/src/js/review/ReviewFieldTemplate';
import StringField from 'platform/forms-system/src/js/review/StringField';
import widgets from 'platform/forms-system/src/js/widgets/index';
import ObjectField from 'platform/forms-system/src/js/fields/ObjectField';
import ArrayField from 'platform/forms-system/src/js/fields/ArrayField';
import ReadOnlyArrayField from 'platform/forms-system/src/js/review/ReadOnlyArrayField';
import BasicArrayField from 'platform/forms-system/src/js/fields/BasicArrayField';
import TitleField from 'platform/forms-system/src/js/fields/TitleField';
import ReviewObjectField from 'platform/forms-system/src/js/review/ObjectField';
import { scrollToFirstError } from 'platform/forms-system/src/js/utilities/ui/index';
import getFormDataFromSchemaId from 'platform/forms-system/src/js/utilities/data/getFormDataFromSchemaId';
import YesNoWidget from './YesNoWidget';
import { updateSaveToProfile } from '../../../actions/actions';
import content from '../../../shared/locales/en/content.json';
/*
* Each page uses this component and passes in config. This is where most of the page level
* form logic should live.
*/
class SchemaForm extends React.Component {
constructor(props) {
super(props);
this.validate = this.validate.bind(this);
this.onError = this.onError.bind(this);
this.getEmptyState = this.getEmptyState.bind(this);
this.transformErrors = this.transformErrors.bind(this);
this.onBlur = this.onBlur.bind(this);
this.setTouched = this.setTouched.bind(this);
this.fields = {
ObjectField,
ArrayField,
BasicArrayField,
TitleField,
};
this.state = {
...this.getEmptyState(props),
saveToProfile: null,
};
this.onChangeProfile = this.onChangeProfile.bind(this);
this.reviewFields = {
ObjectField: ReviewObjectField,
ArrayField: ReadOnlyArrayField,
BasicArrayField,
address: ReviewObjectField,
StringField,
};
}
// componentDidMount() {
// this.onChangeProfile(this.state.saveToProfile);
// }
/* eslint-disable-next-line camelcase */
UNSAFE_componentWillReceiveProps(newProps) {
if (
newProps.name !== this.props.name ||
newProps.pagePerItemIndex !== this.props.pagePerItemIndex
) {
this.setState(this.getEmptyState(newProps)); // No need for prevState here
} else if (newProps.title !== this.props.title) {
this.setState(prevState => ({
formContext: set('pageTitle', newProps.title, prevState.formContext),
}));
} else if (!!newProps.reviewMode !== !!this.state.formContext.reviewMode) {
this.setState(this.getEmptyState(newProps)); // No need for prevState here
} else if (newProps.formContext !== this.props.formContext) {
this.setState(this.getEmptyState(newProps)); // No need for prevState here
}
}
/*
* If we’re in review mode, we can short circuit updating
* by making sure the schemas are the same and the data
* displayed on this particular page hasn’t changed
*/
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.reviewMode &&
!nextProps.editModeOnReviewPage &&
nextProps.reviewMode === this.props.reviewMode &&
deepEquals(this.state, nextState) &&
nextProps.schema === this.props.schema &&
typeof nextProps.title !== 'function' &&
nextProps.uiSchema === this.props.uiSchema
) {
return !Object.keys(nextProps.schema.properties).every(
objProp => this.props.data[objProp] === nextProps.data[objProp],
);
}
return true;
}
onChangeProfile = value => {
const saveToProfileValue = value;
this.setState({ saveToProfile: saveToProfileValue }, () => {
const newFormData = {
...this.props.data,
saveToProfile: saveToProfileValue,
};
this.props.dispatch(updateSaveToProfile(saveToProfileValue));
this.props.onChange(newFormData);
});
};
onError(hasSubmitted = true) {
this.setState(
prevState => {
const formContext = set(
'submitted',
!!hasSubmitted,
prevState.formContext,
);
return { formContext };
},
() => {
scrollToFirstError();
},
);
}
onBlur(id) {
if (!this.state.formContext.touched[id]) {
const data = getFormDataFromSchemaId(id, this.props.data);
const isEmpty = data === undefined || data === null || data === '';
// Prefer to only set as touched if the field is NOT empty,
// so that we won't show an error message prematurely.
// If data is not found for some reason (e.g. schema uses snake case
// properties which can't be parsed in a 'root_' string) then go
// ahead and mark as touched which will show a potential error message.
if (!isEmpty || data === 'FORM_DATA_NOT_FOUND') {
this.setState(prevState => {
const formContext = set(['touched', id], true, prevState.formContext);
return { formContext };
});
}
}
}
getEmptyState(props) {
const {
onEdit,
hideTitle,
title,
reviewMode,
reviewTitle,
pagePerItemIndex,
uploadFile,
hideHeaderRow,
formContext,
trackingPrefix,
saveToProfile,
} = props;
return {
formContext: {
touched: {},
submitted: false,
onEdit,
hideTitle,
setTouched: this.setTouched,
reviewTitle,
pageTitle: title,
pagePerItemIndex,
reviewMode,
hideHeaderRow,
uploadFile,
onError: this.onError,
trackingPrefix,
saveToProfile,
...formContext,
},
};
}
setTouched(touchedItem, setStateCallback) {
const touched = merge({}, this.state.formContext.touched, touchedItem);
const formContext = set('touched', touched, this.state.formContext);
this.setState({ formContext }, setStateCallback);
}
/*
* This gets the list of JSON Schema errors whenever validation
* is run
*/
transformErrors(errors) {
return transformErrors(errors, this.props.uiSchema);
}
validate(formData, errors) {
const { schema, uiSchema, appStateData } = this.props;
if (uiSchema) {
uiSchemaValidate(
errors,
uiSchema,
schema,
formData,
'',
null,
appStateData,
);
}
return errors;
}
render() {
const {
data,
schema,
uiSchema,
idSchema,
reviewMode,
editModeOnReviewPage,
children,
onSubmit,
onChange,
safeRenderCompletion,
name,
addNameAttribute,
} = this.props;
const useReviewMode = reviewMode && !editModeOnReviewPage;
return (
<Form
safeRenderCompletion={safeRenderCompletion}
FieldTemplate={useReviewMode ? ReviewFieldTemplate : FieldTemplate}
formContext={this.state.formContext}
liveValidate
noHtml5Validate
onError={this.onError}
onBlur={this.onBlur}
onChange={({ formData }) => onChange(formData)}
onSubmit={onSubmit}
schema={schema}
uiSchema={uiSchema}
idSchema={idSchema}
validate={once(this.validate)}
showErrorList={false}
formData={data}
widgets={useReviewMode ? reviewWidgets : widgets}
fields={useReviewMode ? this.reviewFields : this.fields}
transformErrors={this.transformErrors}
name={addNameAttribute ? name : null}
>
<legend
className={classNames('vads-u-margin-top--3', {
'schemaform-label': true,
'usa-input-error-label': this.state.error,
})}
>
Do you also want to update this information in your VA.gov profile?
<span className="schemaform-required-span">
{content['validation-required-label']}
</span>
</legend>
<div className="schemaform-widget-wrapper vads-u-margin-bottom--3">
<YesNoWidget
id="saveToProfile"
value={this.state.saveToProfile}
onChange={this.onChangeProfile}
/>
</div>
{children}
</Form>
);
}
}
const mapStateToProps = state => ({
profile: state.user.profile,
});
SchemaForm.propTypes = {
name: PropTypes.string.isRequired,
schema: PropTypes.object.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
uiSchema: PropTypes.object.isRequired,
addNameAttribute: PropTypes.bool,
appStateData: PropTypes.object,
children: PropTypes.array,
data: PropTypes.any,
dispatch: PropTypes.func,
editModeOnReviewPage: PropTypes.bool,
formContext: PropTypes.object,
hideTitle: PropTypes.bool,
idSchema: PropTypes.object,
pagePerItemIndex: PropTypes.number,
profile: PropTypes.object,
reviewMode: PropTypes.bool,
safeRenderCompletion: PropTypes.bool,
saveToProfile: PropTypes.bool,
onChange: PropTypes.func,
onSubmit: PropTypes.func,
};
SchemaForm.defaultProps = {
// This is required when running tests, but we'd prefer not to force
// everyone to be aware of it when writing tests that use SchemaForm
safeRenderCompletion: navigator.userAgent === 'node.js',
// When `true` the rendered `Form` is passed a `name` prop so that the `form`
// that's rendered to the DOM will have a `name` attribute. A `form` without a
// `name` attribute will have its implicit `role="form"` disabled. More info
// re: the implicit role:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
addNameAttribute: false,
};
export default connect(mapStateToProps)(SchemaForm);