From 65ac7d0d844f38ca2eed8ccb6c937e5ed231773c Mon Sep 17 00:00:00 2001 From: Frontendr Date: Tue, 2 Jul 2019 15:05:55 +0200 Subject: [PATCH] Added the `formState.setFields` method which allows to set multiple field values, their errors and their touched and validity state. --- README.md | 62 +++++++++ src/index.d.ts | 7 + src/useState.js | 58 ++++++++ test/useFormState-manual-updates.test.js | 167 +++++++++++++++++++++++ 4 files changed, 294 insertions(+) diff --git a/README.md b/README.md index afeb1b3..844bff3 100644 --- a/README.md +++ b/README.md @@ -387,6 +387,65 @@ Please note that when `formState.setField` is called, any existing errors that m It's also possible to set the error value for a single input using `formState.setFieldError` and to clear a single input's value using `formState.clearField`. +### Updating multiple fields at once + +Updating the value of multiple fields in the form at once is possible via the `formState.setFields` method. + +This could come in handy if you're, for example, loading data from the server. + +```js +function Form() { + const [formState, { text, email }] = useFormState(); + + React.useEffect(function loadDataFromServer() { + // we'll simulate some delay with a setTimeout. This could be your fetch() request: + const timer = setTimeout(() => { + formState.setFields({ + name: "John", + age: 24, + email: "john@example.com" + }); + }, 1000); + return () => clearTimeout(timer); + }, []); + + return ( + <> + + + + + ) +} + +``` + +`formState.setFields` has a second `options` argument which can be used to update the `touched`, `validity` and `errors` in the state. + +`touched` and `validity` can be a boolean value which applies that value to all fields for which a value is provided. + +```js +// mark all fields as valid (clears all errors): +formState.setFields(newValues, { + validity: true +}); + +// marks only "name" as invalid: +formState.setFields(newValues, { + validity: { + name: false + }, + errors: { + name: "Your name is required!" + } +}); + +// marks all fields as not touched: +formState.setFields(newValues, { + touched: false +}); +``` + ### Resetting The From State All fields in the form can be cleared all at once at any time using `formState.clear`. @@ -601,6 +660,9 @@ formState = { // updates the value of an input setField(name: string, value: string): void, + // updates multiple field values and (optionally) sets touched, validity and errors: + setFields(values: object, [options]: object): void, + // sets the error of an input setFieldError(name: string, error: string): void, } diff --git a/src/index.d.ts b/src/index.d.ts index b029603..00e041e 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -18,6 +18,12 @@ interface UseFormStateHook { export const useFormState: UseFormStateHook; +type SetFieldsOptions = { + touched?: StateValidity; + validity?: StateValidity; + errors?: StateErrors; +}; + interface FormState> { values: StateValues; validity: StateValidity; @@ -25,6 +31,7 @@ interface FormState> { errors: E; clear(): void; setField(name: K, value: T[K]): void; + setFields(fieldValues: StateValues, options?: SetFieldsOptions): void; setFieldError(name: keyof T, error: string): void; clearField(name: keyof T): void; } diff --git a/src/useState.js b/src/useState.js index d726ef4..1253bbb 100644 --- a/src/useState.js +++ b/src/useState.js @@ -23,6 +23,10 @@ export function useState({ initialState, onClear }) { const clearField = name => setField(name); + function setAll(fields, value) { + return fields.reduce((obj, name) => Object.assign(obj, {[name]: value}), {}); + } + return { /** * @type {{ values, touched, validity, errors }} @@ -43,6 +47,60 @@ export function useState({ initialState, onClear }) { setField(name, value) { setField(name, value, true, true); }, + setFields(fieldValues, options = {touched: false, validity: true}) { + setValues(fieldValues); + + if (options) { + const fields = Object.keys(fieldValues); + + if (options.touched !== undefined) { + // We're setting the touched state of all fields at once: + if (typeof options.touched === "boolean") { + setTouched(setAll(fields, options.touched)); + } else { + setTouched(options.touched); + } + } + + if (options.validity !== undefined) { + if (typeof options.validity === "boolean") { + // We're setting the validity of all fields at once: + setValidity(setAll(fields, options.validity)); + if (options.validity) { + // All fields are valid, clear the errors: + setError(setAll(fields, undefined)); + } + } else { + setValidity(options.validity); + + if (options.errors === undefined) { + // Clear the errors for valid fields: + const errorFields = Object.entries(options.validity).reduce((errorsObj, [name, isValid]) => { + if (isValid) { + return Object.assign({}, errorsObj || {}, {[name]: undefined}); + } + return errorsObj; + }, null); + + if (errorFields) { + setError(errorFields); + } + } + } + } + + if (options.errors) { + // Not logical to set the same error for all fields so has to be an object. + setError(options.errors); + + if (options.validity === undefined) { + // Fields with errors are not valid: + setValidity(setAll(Object.keys(options.errors), false)); + } + } + } + + }, setFieldError(name, error) { setValidity({ [name]: false }); setError({ [name]: error }); diff --git a/test/useFormState-manual-updates.test.js b/test/useFormState-manual-updates.test.js index f5112eb..9086745 100644 --- a/test/useFormState-manual-updates.test.js +++ b/test/useFormState-manual-updates.test.js @@ -56,6 +56,173 @@ describe('useFormState manual updates', () => { expect(formState.current.values.name).toBe('waseem'); }); + it('sets the values of multiple inputs using formState.setFields', () => { + const { formState } = renderWithFormState(([, input]) => ( + <> + + + + + )); + + const values1 = { + firstName: "John", + lastName: "Doe", + age: 33 + }; + + formState.current.setFields(values1); + + expect(formState.current.values).toMatchObject(values1); + expect(Object.values(formState.current.validity)).toMatchObject([true, true, true]); + expect(Object.values(formState.current.touched)).toMatchObject([false, false, false]); + expect(Object.values(formState.current.errors)).toMatchObject([undefined, undefined, undefined]); + + const values2 = { + firstName: "Barry", + lastName: "" + }; + + formState.current.setFields(values2); + + const expected2 = Object.assign({}, values1, values2); + expect(formState.current.values).toMatchObject(expected2); + }); + + it('sets validity when provided in options of formState.setFields', () => { + const { formState } = renderWithFormState(([, input]) => ( + <> + + + + )); + + const values = { + firstName: "John", + lastName: "Doe" + }; + + formState.current.setFields(values, { + validity: true + }); + expect(formState.current.values).toMatchObject(values); + expect(formState.current.validity).toMatchObject({ + firstName: true, + lastName: true + }); + + formState.current.setFields({firstName: "test", lastName: "foo"}, { + validity: false + }); + expect(formState.current.validity).toMatchObject({ + firstName: false, + lastName: false + }); + + formState.current.setFields(values, { + validity: { + firstName: true, + lastName: false + } + }); + expect(Object.values(formState.current.validity)).toMatchObject([true, false]); + }); + + it('sets touched when provided in options of formState.setFields', () => { + const { formState } = renderWithFormState(([, input]) => ( + <> + + + + )); + + const values = { + firstName: "John", + lastName: "Doe" + }; + + formState.current.setFields(values, { + touched: true + }); + expect(formState.current.values).toMatchObject(values); + expect(formState.current.touched).toMatchObject({ + firstName: true, + lastName: true + }); + + formState.current.setFields(values, { + touched: false + }); + expect(formState.current.touched).toMatchObject({ + firstName: false, + lastName: false + }); + + formState.current.setFields(values, { + touched: { + firstName: true, + lastName: false + } + }); + expect(formState.current.touched).toMatchObject({ + firstName: true, + lastName: false + }); + }); + + it('sets the errors of the specified fields when provided in options of formState.setFields', () => { + const { formState } = renderWithFormState(([, input]) => ( + <> + + + + )); + + const values = { + firstName: "John", + lastName: "" + }; + const errors = { + lastName: "This field cannot be empty" + }; + + formState.current.setFields(values, {errors}); + + expect(formState.current.errors).toMatchObject(errors); + expect(formState.current.validity).toMatchObject({ + lastName: false + }); + }); + + it ('automatically clears errors when marking fields as valid via formState.setFields', () => { + const { formState } = renderWithFormState(([, input]) => ( + <> + + + )); + + const values = { + firstName: "#$%^&" + }; + const errors = { + firstName: "That's not a name" + }; + formState.current.setFields(values, {errors}); + expect(formState.current.values).toMatchObject(values); + expect(formState.current.errors).toMatchObject(errors); + + const newValues = { + firstName: "John" + }; + const newValidity = { + firstName: true + }; + formState.current.setFields(newValues, {validity: newValidity}); + + expect(formState.current.values).toMatchObject(newValues); + expect(formState.current.errors).toMatchObject({firstName: undefined}); + }); + it('sets the error of an input and invalidates the input programmatically using from.setFieldError', () => { const { formState } = renderWithFormState(([, input]) => (