Skip to content

Commit

Permalink
Add option to skip validation for controlled form attrs assignment
Browse files Browse the repository at this point in the history
Description:
- Add `onSet` property to `FormProvider` component of controlled form
functionality. This allows to trigger "set form attributes" action with
additional options. At this point, the only option is `validate`, which
can be used to skip validation when form attributes change in some
way, and form is in "validate on change" state.
  • Loading branch information
akuzko committed May 23, 2021
1 parent 3b43b48 commit 842b09f
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 10 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,39 @@ function OrderEditor() {
}
```

#### `FormProvider` props

- `config` - additional config that will be merged into the one specified in
`makeForm` call.

- `attrs` - form attributes that are stored and provided from external source.

- `onChange(attrs)` - function that will be called whenever form attributes
are requested to be changed (via `set` function call, for instance).

- `onSet(setAttrs, { attrs, nextAttrs })` - function that will be called whenever
`attrs` are assigned to the form from external source. This function received
`setAttrs` function as first parameter and `{ attrs, nextAttrs }` object as
second one. `setAttrs` function has to be called to complete the sync of external
attributes and form attributes. This function can be called with additional _options_
object. The only supported option is `validate`:

```jsx
const onSet = useCallback((setAttrs) => {
setAttrs({ validate: false });
})

return (
<FormProvider
attrs={attrs}
onChange={onChange}
onSet={onSet}
>
<OrderForm />
</FormProvider>
);
````

#### Additional Helpers

In `makeForm` use-case scenarios there might also be a need in some additional
Expand Down
19 changes: 18 additions & 1 deletion examples/src/ControlledForm/ControlledForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ export default function ControlledForm() {
setAttrs(attrs);
}, []);

const onSet = useCallback((setAttrs, { attrs, nextAttrs }) => {
console.log({ attrs, nextAttrs });
setAttrs({ validate: false });
}, []);

const addItem = useCallback(() => {
setAttrs((attrs) => {
return {
...attrs,
items: [...attrs.items, {}]
};
});
}, []);

const config = useMemo(() => ({
helpers: () => ({
fillForm: () => {
Expand Down Expand Up @@ -47,8 +61,11 @@ export default function ControlledForm() {
}), []);

return (
<FormProvider config={config} attrs={attrs} onChange={onChange}>
<FormProvider config={config} attrs={attrs} onChange={onChange} onSet={onSet}>
<OrderForm />
<div>
<button onClick={addItem}>Add item</button>
</div>
</FormProvider>
);
}
10 changes: 7 additions & 3 deletions src/makeForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function makeForm(mainConfig = {}) {
const Context = createContext({ attrs: mainConfig.initial });

// eslint-disable-next-line react/prop-types
function FormProvider({ config, children, attrs, onChange }) {
function FormProvider({ config, children, attrs, onChange, onSet }) {
if (attrs && !mainConfig.initial) {
mainConfig.initial = attrs;
}
Expand All @@ -24,9 +24,9 @@ export default function makeForm(mainConfig = {}) {
_action._processed = true;
onChange(formAttrs);
} else if (attrs && attrs !== formAttrs) {
setFormAttrs(attrs);
onSet((...args) => setFormAttrs(attrs, ...args), { attrs: formAttrs, nextAttrs: attrs });
}
}, [attrs, formAttrs, _action, onChange]);
}, [attrs, formAttrs, _action, onChange, onSet]);

return (
<Context.Provider value={helpers}>
Expand All @@ -35,6 +35,10 @@ export default function makeForm(mainConfig = {}) {
);
}

FormProvider.defaultProps = {
onSet: (setAttrs) => setAttrs()
};

function useContextForm() {
return useContext(Context);
}
Expand Down
8 changes: 4 additions & 4 deletions src/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,9 @@ export default function reducer(state, action) {
return { ...state, attrs: nextAttrs, errors: compact(nextErrors), isPristine: false, action };
}
case 'setFullAttrs': {
const { attrs } = action;
const { attrs, options = {} } = action;

if (shouldValidateOnChange) {
if (shouldValidateOnChange && options.validate !== false) {
// When all attributes are set at once and validation should be
// executed on set, run all validation routines.
const nextErrors = {};
Expand Down Expand Up @@ -299,8 +299,8 @@ export function setAttrs(attrs, prefix) {
return { type: 'setAttrs', attrs, prefix, isAttrUpdate: true };
}

export function setFullAttrs(attrs) {
return { type: 'setFullAttrs', attrs };
export function setFullAttrs(attrs, options) {
return { type: 'setFullAttrs', attrs, options };
}

export function validate(path, resolve, reject) {
Expand Down
4 changes: 2 additions & 2 deletions src/useForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function useForm(config = DEFAULT_CONFIG, secondaryConfig) {
}, []);

// Used in a useEffect to apply external attributes to a form.
const setFormAttrs = useCallback((attrs) => {
return dispatch(setFullAttrs(attrs));
const setFormAttrs = useCallback((attrs, options) => {
return dispatch(setFullAttrs(attrs, options));
}, []);

const validate = useCallback((name) => {
Expand Down
43 changes: 43 additions & 0 deletions test/useForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -957,5 +957,48 @@ describe('useForm', () => {
expect(wrapper.find('.address .error')).to.have.lengthOf(1);
expect(wrapper.find('.items-0 .error')).to.have.lengthOf(1);
});

it('allows to skip validation via onSet property', () => {
function Page() {
const [attrs, setAttrs] = useState({
username: 'foo',
address: '',
guest: false,
items: [{}]
});

const addItem = () => {
setAttrs({ ...attrs, items: [...attrs.items, {}] });
};

const config = useMemo(() => ({
validations: {
'items.*.name': 'presence'
}
}), []);

const onSet = (setAttrs) => {
setAttrs({ validate: false });
};

return (
<div>
<FormProvider config={config} attrs={attrs} onChange={setAttrs} onSet={onSet}>
<OrderForm />
</FormProvider>
<button className="helper-add-item" onClick={addItem}>Add item</button>
</div>
);
}

const wrapper = mount(<Page />);

wrapper.find('.validate').simulate('click');
wrapper.find('.helper-add-item').simulate('click');

expect(wrapper.find('.address .error')).to.have.lengthOf(1);
expect(wrapper.find('.items-0 .error')).to.have.lengthOf(1);
expect(wrapper.find('.items-1 .error')).to.have.lengthOf(0);
});
});
});

0 comments on commit 842b09f

Please sign in to comment.