-
Notifications
You must be signed in to change notification settings - Fork 901
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core/presentation): Add a formik hook to save/restore mutually e…
…xclusive form fields when toggling between two or more modes in a form
- Loading branch information
1 parent
21387a7
commit 3156dff
Showing
2 changed files
with
199 additions
and
0 deletions.
There are no files selected for viewing
130 changes: 130 additions & 0 deletions
130
...les/core/src/presentation/forms/hooks/useSaveRestoreMutuallyExclusiveFields.hook.spec.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { mount } from 'enzyme'; | ||
import { useSaveRestoreMutuallyExclusiveFields } from './useSaveRestoreMutuallyExclusiveFields.hook'; | ||
import { Formik, FormikProps } from 'formik'; | ||
import React from 'react'; | ||
import { FormikFormField } from '../fields'; | ||
import { SelectInput } from '../inputs'; | ||
import { SpinFormik } from '../SpinFormik'; | ||
|
||
function PizzaComponent() { | ||
return ( | ||
<> | ||
<FormikFormField name="topping" input={props => <SelectInput {...props} options={['peppers', 'mushrooms']} />} /> | ||
<FormikFormField name="crust" input={props => <SelectInput {...props} options={['thin', 'deepdish']} />} /> | ||
<FormikFormField name="sauce" input={props => <SelectInput {...props} options={['red', 'white']} />} /> | ||
</> | ||
); | ||
} | ||
|
||
function SandwichComponent() { | ||
return ( | ||
<> | ||
<FormikFormField name="bread" input={props => <SelectInput {...props} options={['white', 'wheat']} />} /> | ||
<FormikFormField name="meat" input={props => <SelectInput {...props} options={['ham', 'turkey']} />} /> | ||
<FormikFormField name="cheese" input={props => <SelectInput {...props} options={['cheddar', 'swiss']} />} /> | ||
</> | ||
); | ||
} | ||
|
||
function OrderComponent({ formik }: { formik: FormikProps<any> }) { | ||
useSaveRestoreMutuallyExclusiveFields(formik, formik.values.pizzaOrSandwich, { | ||
pizza: ['topping', 'crust', 'sauce'], | ||
sandwich: ['bread', 'meat', 'cheese'], | ||
}); | ||
|
||
// Note: none of the FormikFormField components are necessary for these unit tests to work. | ||
// However, they provide clarity to the reader regarding what the hook intends to do. | ||
return ( | ||
<> | ||
<FormikFormField | ||
name="pizzaOrSandwich" | ||
input={props => <SelectInput {...props} options={['pizza', 'sandwich']} />} | ||
/> | ||
|
||
{formik.values.pizzaOrSandwich === 'pizza' && <PizzaComponent />} | ||
{formik.values.pizzaOrSandwich === 'sandwich' && <SandwichComponent />} | ||
</> | ||
); | ||
} | ||
|
||
const initialValues = { | ||
pizzaOrSandwich: 'pizza', | ||
topping: 'pepperoni', | ||
crust: 'thin', | ||
sauce: 'red', | ||
}; | ||
|
||
const setupTest = (formikRef: React.MutableRefObject<any>) => { | ||
return mount( | ||
<SpinFormik | ||
ref={formikRef} | ||
onSubmit={null} | ||
initialValues={initialValues} | ||
render={formik => <OrderComponent formik={formik} />} | ||
/>, | ||
); | ||
}; | ||
|
||
describe('useSaveRestoreMutuallyExclusiveFields hook', () => { | ||
it(`clears out previously entered 'pizza' fields when the user chooses 'sandwich'`, () => { | ||
const formikRef = React.createRef<Formik>(); | ||
const wrapper = setupTest(formikRef); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'sandwich'); | ||
wrapper.setProps({}); | ||
// cleared out the pizza field from the formik values | ||
expect(formikRef.current.getFormikBag().values).toEqual({ pizzaOrSandwich: 'sandwich' }); | ||
}); | ||
|
||
it(`restores previously saved 'pizza' fields when toggling back to 'pizza'`, () => { | ||
const formikRef = React.createRef<Formik>(); | ||
const wrapper = setupTest(formikRef); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'sandwich'); | ||
wrapper.setProps({}); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'pizza'); | ||
wrapper.setProps({}); | ||
|
||
// restored the pizza fields | ||
expect(formikRef.current.getFormikBag().values).toEqual({ | ||
pizzaOrSandwich: 'pizza', | ||
topping: 'pepperoni', | ||
crust: 'thin', | ||
sauce: 'red', | ||
}); | ||
}); | ||
|
||
it(`restores previously saved 'pizza' and 'sandwich' fields when toggling back and forth`, () => { | ||
const formikRef = React.createRef<Formik>(); | ||
const wrapper = setupTest(formikRef); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'sandwich'); | ||
wrapper.setProps({}); | ||
|
||
formikRef.current.setFieldValue('bread', 'wheat'); | ||
formikRef.current.setFieldValue('meat', 'ham'); | ||
formikRef.current.setFieldValue('cheese', 'cheddar'); | ||
wrapper.setProps({}); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'pizza'); | ||
wrapper.setProps({}); | ||
// restored the pizza fields | ||
expect(formikRef.current.getFormikBag().values).toEqual({ | ||
pizzaOrSandwich: 'pizza', | ||
topping: 'pepperoni', | ||
crust: 'thin', | ||
sauce: 'red', | ||
}); | ||
|
||
formikRef.current.setFieldValue('pizzaOrSandwich', 'sandwich'); | ||
wrapper.setProps({}); | ||
// restored the sandwich fields | ||
expect(formikRef.current.getFormikBag().values).toEqual({ | ||
pizzaOrSandwich: 'sandwich', | ||
bread: 'wheat', | ||
meat: 'ham', | ||
cheese: 'cheddar', | ||
}); | ||
}); | ||
}); |
69 changes: 69 additions & 0 deletions
69
...s/modules/core/src/presentation/forms/hooks/useSaveRestoreMutuallyExclusiveFields.hook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { FormikProps } from 'formik'; | ||
import { useEffect, useState } from 'react'; | ||
import { usePrevious } from '../../hooks'; | ||
import { get } from 'lodash'; | ||
|
||
/** | ||
* Sometimes a form allows the user to choose between multiple mutually exclusive sets of fields. | ||
* This hook saves and restores mutually exclusive fields when the user interactively toggles between them. | ||
* | ||
* For example, an order form may have separate fields for ordering a pizza or a sandwich. When the user selects | ||
* "pizza", this hook saves a copy of the user's currently entered "sandwich" data and then clears the "sandwich" fields. | ||
* If the user then switches back to "sandwich", the hook restores the "sandwich" data and saves/clears the "pizza" fields. | ||
* | ||
* Note: this hook does not explicitly attempt to manage default values for form fields | ||
* | ||
* @param formik formik props (typically from the Formik render function) | ||
* @param currentFieldSetKey the current form mode (e.g., the value of the selected radio button) | ||
* @param mutuallyExclusiveFieldSets an object with: | ||
* - keys: one for each form mode | ||
* - values: lodash paths to the form fields exclusively owned by that form mode | ||
* | ||
* Example: | ||
* | ||
* // Restores previous "pizza" form data ('toppings', 'crust', 'sauce') when the form mode switches back to 'pizza' | ||
* // Restores previous "sandwich" form data ('bread', 'meat', 'cheese') when the form mode switches back to 'sandwich' | ||
* | ||
* useSaveRestoreMutuallyExclusiveFields(formik, pizzaOrSandwich, { | ||
* { pizza: ["toppings", "crust", "sauce" ] }, | ||
* { sandwich: ["bread", "meat", "cheese" ] }, | ||
* }); | ||
* | ||
* <FormikFormField name="pizzaOrSandwich" input={props => <SelectInput {...props} options={['pizza', 'sandwich']}>} /> | ||
* | ||
* { formik.values.pizzaOrSandwich === 'pizza' && (<> | ||
* <FormikFormField name="toppings" input={props => <SelectInput {...props} options={toppingsOptions}>} /> | ||
* <FormikFormField name="crust" input={props => <SelectInput {...props} options={crustOptions}>} /> | ||
* <FormikFormField name="sauce" input={props => <SelectInput {...props} options={sauceOptions}>} /> | ||
* </>)} | ||
* | ||
* { formik.values.pizzaOrSandwich === 'sandwich' && (<> | ||
* <FormikFormField name="bread" input={props => <SelectInput {...props} options={breadOptions}>} /> | ||
* <FormikFormField name="meat" input={props => <SelectInput {...props} options={meatOptions}>} /> | ||
* <FormikFormField name="cheese" input={props => <SelectInput {...props} options={cheeseOptions}>} /> | ||
* </>)} | ||
*/ | ||
export function useSaveRestoreMutuallyExclusiveFields( | ||
formik: FormikProps<any>, | ||
currentFieldSetKey: string, | ||
mutuallyExclusiveFieldSets: { [fieldSetId: string]: string[] }, | ||
) { | ||
const previousFormMode = usePrevious(currentFieldSetKey); | ||
const [savedData, setSavedData] = useState({} as { [path: string]: any }); | ||
|
||
// Whenever the user switches the form mode source, clear out the | ||
// previous form mode's exclusive field values and restore the new mode's values. | ||
useEffect(() => { | ||
if (!!previousFormMode && currentFieldSetKey !== previousFormMode) { | ||
const fieldsToSave = mutuallyExclusiveFieldSets[previousFormMode] ?? []; | ||
const fieldsToRestore = mutuallyExclusiveFieldSets[currentFieldSetKey] ?? []; | ||
|
||
const savedDataCopy = { ...savedData }; | ||
fieldsToSave.forEach(path => (savedDataCopy[path] = get(formik.values, path))); | ||
setSavedData(savedDataCopy); | ||
|
||
fieldsToSave.forEach(field => formik.setFieldValue(field, undefined)); | ||
fieldsToRestore.forEach(path => formik.setFieldValue(path, savedData[path])); | ||
} | ||
}, [currentFieldSetKey]); | ||
} |