Skip to content

Commit

Permalink
feat: ERM Form
Browse files Browse the repository at this point in the history
Split out ERM Form into a proper component, and leave the hook implementation as is. Cleans up implementation in our apps
  • Loading branch information
EthanFreestone committed Mar 12, 2024
1 parent 6b36b5a commit ff751b6
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 103 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { default as DocumentFilter } from './lib/DocumentFilter';
export { default as DocumentsFieldArray } from './lib/DocumentsFieldArray';
export { default as DuplicateModal } from './lib/DuplicateModal';
export { default as EditCard } from './lib/EditCard';
export { default as ERMForm } from './lib/ERMForm';
export { default as Embargo } from './lib/Embargo';
export { default as EResourceType } from './lib/EResourceType';
export { default as FileUploader } from './lib/FileUploader';
Expand Down
97 changes: 97 additions & 0 deletions lib/ERMForm/ERMForm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import PropTypes from 'prop-types';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import createDecorator from 'final-form-focus';
import arrayMutators from 'final-form-arrays';
import { FormattedMessage, useIntl } from 'react-intl';
import { LastVisitedContext } from '@folio/stripes/core';
import { ConfirmationModal } from '@folio/stripes/components';

import { useErmForm } from '../hooks';

const focusOnErrors = createDecorator();

const ERMForm = ({
children,
decorators = [],
initialValues,
navigationCheck = true,
mutators = [],
onSubmit,
subscription = {},
...formOptions // Any other options we sould pass to final form
}) => {
const {
openModal,
continueNavigation,
closeModal,
formSpyRef,
_isMounted,
} = useErmForm({ navigationCheck });
const intl = useIntl();

return (
<LastVisitedContext.Consumer>
{(ctx) => (
<>
<FinalForm
{...formOptions}
decorators={[focusOnErrors, ...decorators]}
initialValues={initialValues}
mutators={{ ...mutators, ...arrayMutators }}
onSubmit={onSubmit}
render={(formProps) => (
<>
{children(formProps)}
<FormSpy
onChange={(state) => {
if (_isMounted.current) {
formSpyRef.current = state;
}
}}
subscription={{
dirty: true,
submitSucceeded: true,
invalid: true,
submitting: true,
}}
{...formProps}
/>
</>
)}
subscription={{
dirty: true,
submitSucceeded: true,
invalid: true,
submitting: true,
initialValues: true,
pristine: true,
...subscription,
}}
/>
<ConfirmationModal
cancelLabel={<FormattedMessage id="stripes-erm-components.closeWithoutSaving" />}
confirmLabel={<FormattedMessage id="stripes-erm-components.keepEditing" />}
heading={intl.formatMessage({ id: 'stripes-erm-components.areYouSure' })}
id="cancel-editing-confirmation"
message={<FormattedMessage id="stripes-erm-components.unsavedChanges" />}
onCancel={() => continueNavigation(ctx)}
onConfirm={closeModal}
open={openModal}
/>
</>
)}
</LastVisitedContext.Consumer>
);
};

ERMForm.propTypes = {
children: PropTypes.elementType.isRequired,
decorators: PropTypes.arrayOf(PropTypes.object),
initialValues: PropTypes.object,
navigationCheck: PropTypes.bool,
mutators: PropTypes.object,
onSubmit: PropTypes.func,
subscription: PropTypes.object,
};

export default ERMForm;
1 change: 1 addition & 0 deletions lib/ERMForm/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './ERMForm';
142 changes: 39 additions & 103 deletions lib/hooks/useErmForm.js
Original file line number Diff line number Diff line change
@@ -1,136 +1,72 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import PropTypes from 'prop-types';
import { Form as FinalForm, FormSpy } from 'react-final-form';
import { useState, useEffect, useRef } from 'react';
import { useHistory } from 'react-router-dom';
import createDecorator from 'final-form-focus';
import arrayMutators from 'final-form-arrays';
import { FormattedMessage, useIntl } from 'react-intl';
import { LastVisitedContext } from '@folio/stripes/core';
import { ConfirmationModal } from '@folio/stripes/components';

const focusOnErrors = createDecorator();

const useErmForm = ({ navigationCheck = true } = {}) => {
const [openModal, setOpenModal] = useState(false);
const [nextLocation, setNextLocation] = useState(null);

const unblock = useRef();
const formSpyRef = useRef();
const _isMounted = useRef(false);

const history = useHistory();
const intl = useIntl();

useEffect(() => {
if (navigationCheck) {
// Is this whole history.unblock a stale function? Maybe have the whole thing in state or ref?
const unblock = history.block((nextLoc) => {
// Due to stale closure probolems, grab current state from "state updator" pattern
console.log("FORMSPYREF (FIRST): %o", formSpyRef);
const shouldPrompt = !!formSpyRef.current && formSpyRef.current.dirty && !formSpyRef.current.submitSucceeded && !formSpyRef.current.submitting;
console.log("FORMSPYREF: %o", formSpyRef);
console.log("shouldPrompt: %o", shouldPrompt);
_isMounted.current = true;

if (shouldPrompt) {
setOpenModal(true);
setNextLocation(nextLoc);
}
const handleBlockedNavigation = (nextLoc) => {
const shouldPrompt =
!!formSpyRef.current &&
formSpyRef.current.dirty &&
!formSpyRef.current.submitSucceeded &&
!formSpyRef.current.submitting;

return !shouldPrompt;
});
return unblock;
if (shouldPrompt) {
setOpenModal(true);
setNextLocation(nextLoc);
}

return !shouldPrompt;
};

// Set up the history.block listener
if (navigationCheck) {
unblock.current = history.block(handleBlockedNavigation);
}

// Clean up the history.block listener on unmount
return () => {
_isMounted.current = false;
if (unblock.current) {
unblock.current();
unblock.current = null;
}
};
}, [history, navigationCheck]);

// const continueNavigation = (ctx) => {
// const { pathname, search } = nextLocation;

// ctx.cachePreviousUrl();
// setOpenModal(false);
// history.push(`${pathname}${search}`);
// };

const continueNavigation = useCallback((ctx) => {
const continueNavigation = (ctx) => {
const { pathname, search } = nextLocation;

ctx.cachePreviousUrl();
if (unblock.current) {
unblock.current();
unblock.current = null;
}
setOpenModal(false);
history.push(`${pathname}${search}`);
}, [nextLocation, history]);
};

const closeModal = () => {
setOpenModal(false);
};

// useCallback applied to ERMForm
const ERMForm = useCallback(({ FormComponent, initialValues, onSubmit, formOptions }) => {
return (
<LastVisitedContext.Consumer>
{(ctx) => (
<>
<FinalForm
{...formOptions}
decorators={[focusOnErrors, ...(formOptions.decorators || [])]}
initialValues={initialValues}
mutators={{ ...formOptions.mutators, ...arrayMutators }}
onSubmit={onSubmit}
render={(formProps) => (
<>
<FormComponent {...formProps} />
<FormSpy
onChange={state => {
formSpyRef.current = state;
}}
subscription={{
dirty: true,
submitSucceeded: true,
invalid: true,
submitting: true,
}}
{...formProps}
/>
</>
)}
subscription={{
initialValues: true,
submitting: true,
pristine: true,
...formOptions.subscription,
}}
/>
<ConfirmationModal
cancelLabel={<FormattedMessage id="stripes-erm-components.closeWithoutSaving" />}
confirmLabel={<FormattedMessage id="stripes-erm-components.keepEditing" />}
heading={intl.formatMessage({ id: 'stripes-erm-components.areYouSure' })}
id="cancel-editing-confirmation"
message={<FormattedMessage id="stripes-erm-components.unsavedChanges" />}
onCancel={() => continueNavigation(ctx)}
onConfirm={closeModal}
open={openModal}
/>
</>
)}
</LastVisitedContext.Consumer>
);
}, [intl, openModal, continueNavigation]);

ERMForm.propTypes = {
FormComponent: PropTypes.elementType.isRequired,
initialValues: PropTypes.object,
onSubmit: PropTypes.func,
formOptions: PropTypes.shape({
decorators: PropTypes.arrayOf(PropTypes.object),
mutators: PropTypes.object,
subscription: PropTypes.object,
}),
return {
openModal,
continueNavigation,
closeModal,
formSpyRef,
_isMounted,
};

return { ERMForm };
};

useErmForm.propTypes = {
navigationCheck: PropTypes.bool,
};

export default useErmForm;

0 comments on commit ff751b6

Please sign in to comment.