Skip to content

Commit

Permalink
feat(core/presentation): Add a formik hook to save/restore mutually e…
Browse files Browse the repository at this point in the history
…xclusive form fields when toggling between two or more modes in a form
  • Loading branch information
christopherthielen authored and mergify[bot] committed May 6, 2020
1 parent 21387a7 commit 3156dff
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 0 deletions.
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',
});
});
});
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]);
}

0 comments on commit 3156dff

Please sign in to comment.