Skip to content

Commit

Permalink
Add isPristine helper property
Browse files Browse the repository at this point in the history
- Add "isPristine" helper property which designates whether or not form
values were touched in any way. Form is in "pristine" state when it
is initialized or has been reset.
- Update tests, README, demo app.
- Also, slightly refactor code responsible for controlled form behavior,
reducing overall amount of used hooks by 2.
  • Loading branch information
akuzko committed Sep 22, 2020
1 parent 33eedfc commit a7fc4c4
Show file tree
Hide file tree
Showing 7 changed files with 39 additions and 23 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,8 @@ export function Form() {
Essentially calls `setError(name, undefined)`.
- `isValid` - boolean flag indicating whether or not there are any errors
currently set.
- `isPristine` - boolean flag indicating whether or not form attributes were
changed. Gets back to `true` on `reset` helper call.
- `validate()` - performs form validations. Return a promise-like object that
responds to `then` and `catch` methods. On successful validation, resolves
promise with form attributes. On failed validation, rejects promise with
Expand Down
9 changes: 8 additions & 1 deletion examples/src/ControlledForm/FormControls.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,16 @@ function fakeSubmit() {
}

export default function FormControls() {
const { reset: doReset, isValid, validate, withValidation, setErrors, fillForm } = useOrderForm();
const { set, reset: doReset, isValid, validate, withValidation, setErrors, fillForm } = useOrderForm();

const reset = useCallback(() => doReset(), []);

const randomizeName = useCallback(() => {
set({
username: `random-${(Math.random() * 1e6).toFixed(0)}`
});
}, [set]);

const handleSubmit = useCallback(withValidation((attrs) => {
fakeSubmit(attrs).catch((errors) => {
setErrors(errors)
Expand All @@ -35,6 +41,7 @@ export default function FormControls() {
<button onClick={reset}>Reset</button>
<button onClick={validate}>Validate</button>
<button onClick={fillForm}>Quick Fill</button>
<button onClick={randomizeName}>Randomize Name</button>
</div>
);
}
3 changes: 2 additions & 1 deletion examples/src/ControlledForm/OrderForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ItemForm from './ItemForm';
import FormControls from './FormControls';

export default function OrderForm() {
const { $, attrs, set, errors, getError, useConfig, isFreeDelivery } = useOrderForm();
const { $, attrs, isPristine, isValid, set, errors, getError, useConfig, isFreeDelivery } = useOrderForm();
const { guest, items } = attrs;

useConfig(() => {
Expand Down Expand Up @@ -51,6 +51,7 @@ export default function OrderForm() {

<div>{ JSON.stringify(attrs) }</div>
<div>{ JSON.stringify(errors) }</div>
<div>{ JSON.stringify({ isPristine, isValid }) }</div>
</Fragment>
);
}
21 changes: 6 additions & 15 deletions src/makeForm.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useEffect, useRef } from 'react';
import React, { createContext, useContext, useEffect } from 'react';
import { useForm } from './useForm';

export default function makeForm(mainConfig = {}) {
Expand All @@ -10,7 +10,6 @@ export default function makeForm(mainConfig = {}) {
mainConfig.initial = attrs;
}

const skipSetFormAttrsRef = useRef(false);
const helpers = useForm(mainConfig, config);
const { attrs: formAttrs, setFormAttrs, _amendInitialConfig, _action } = helpers;

Expand All @@ -21,21 +20,13 @@ export default function makeForm(mainConfig = {}) {
}, [config]);

useEffect(() => {
if (_action?.isAttrUpdate && onChange) {
skipSetFormAttrsRef.current = true;
if (_action?.isAttrUpdate && onChange && !_action._processed) {
_action._processed = true;
onChange(formAttrs);
} else if (attrs && attrs !== formAttrs) {
setFormAttrs(attrs);
}
}, [formAttrs, _action, onChange]);

useEffect(() => {
if (attrs && attrs !== formAttrs) {
if (skipSetFormAttrsRef.current) {
skipSetFormAttrsRef.current = false;
} else {
setFormAttrs(attrs);
}
}
}, [attrs, formAttrs]);
}, [attrs, formAttrs, _action, onChange]);

return (
<Context.Provider value={helpers}>
Expand Down
11 changes: 7 additions & 4 deletions src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,11 @@ export default function reducer(state, action) {
...errors,
...nextErrors
},
isPristine: false,
action
};
} else {
return update(state, { [`attrs.${path}`]: value, action });
return update(state, { [`attrs.${path}`]: value, isPristine: false, action });
}
}
case 'setAttrs': {
Expand All @@ -135,7 +136,7 @@ export default function reducer(state, action) {
}
}

return { ...state, attrs: nextAttrs, errors: nextErrors, action };
return { ...state, attrs: nextAttrs, errors: nextErrors, isPristine: false, action };
}
case 'setFullAttrs': {
const { attrs } = action;
Expand All @@ -150,10 +151,10 @@ export default function reducer(state, action) {
validateRule(validations, fullOpts, rule, nextErrors);
});

return { ...state, attrs, errors: nextErrors, action };
return { ...state, attrs, errors: nextErrors, isPristine: false, action };
}

return { ...state, attrs, action };
return { ...state, attrs, isPristine: false, action };
}
case 'validate': {
const { resolve, reject } = action;
Expand Down Expand Up @@ -213,6 +214,7 @@ export default function reducer(state, action) {
...state,
errors: {},
attrs: action.attrs || state.initialAttrs,
isPristine: true,
action
};
}
Expand All @@ -231,6 +233,7 @@ export function init(config, secondaryConfig) {
attrs,
errors: {},
configs: [fullConfig],
isPristine: true,
...fullConfig
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/useForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { resolveConfig, DEFAULT_CONFIG } from './config';

export function useForm(config = DEFAULT_CONFIG, secondaryConfig) {
const initial = useMemo(() => init(config, secondaryConfig), []);
const [{ attrs, errors, pureHandlers, helpers, action }, dispatch] = useReducer(reducer, initial);
const [{ attrs, errors, isPristine, pureHandlers, helpers, action }, dispatch] = useReducer(reducer, initial);
const isValid = !Object.values(errors).some(Boolean);

const handlersCache = useMemo(() => new HandlersCache(pureHandlers), []);
Expand Down Expand Up @@ -107,6 +107,7 @@ export function useForm(config = DEFAULT_CONFIG, secondaryConfig) {
attrs,
get,
set,
isPristine,
setFormAttrs,
errors,
getError,
Expand Down
13 changes: 12 additions & 1 deletion test/useForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('useForm', () => {
});

function Form() {
const { $, isBar } = useForm({
const { $, reset, isBar, isPristine } = useForm({
initial: { foo: 'foo' },
helpers: ({ attrs }) => ({ isBar: attrs.foo === 'bar' })
});
Expand All @@ -36,6 +36,8 @@ describe('useForm', () => {
{ isBar &&
<div className="is-bar">{ 'value of "foo" is "bar"' }</div>
}
<div className="is-pristine">{ isPristine.toString() }</div>
<button className="reset" onClick={() => reset()}>Reset</button>
</div>
);
}
Expand All @@ -58,6 +60,15 @@ describe('useForm', () => {
expect(wrapper.find('.is-bar')).to.have.lengthOf(1);
});

it('tracks pristine state', () => {
const wrapper = mount(<Form />);
expect(wrapper.find('.is-pristine').text()).to.eq('true');
wrapper.find('input.foo').simulate('change', { target: { value: 'bar' } });
expect(wrapper.find('.is-pristine').text()).to.eq('false');
wrapper.find('.reset').simulate('click');
expect(wrapper.find('.is-pristine').text()).to.eq('true');
});

describe('custom onChange handlers', () => {
function Form() {
const { $, set } = useForm({ initial: { foo: 'foo' } });
Expand Down

0 comments on commit a7fc4c4

Please sign in to comment.