Skip to content

Added formState.setFields method to be able to update multiple fields at once #91

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<input {...text('name')} readOnly />
<input {...text('age')} readOnly />
<input {...email('email')} readOnly />
</>
)
}

```

`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`.
Expand Down Expand Up @@ -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,
}
Expand Down
7 changes: 7 additions & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,20 @@ interface UseFormStateHook {

export const useFormState: UseFormStateHook;

type SetFieldsOptions = {
touched?: StateValidity<boolean>;
validity?: StateValidity<boolean>;
errors?: StateErrors<string, string>;
};

interface FormState<T, E = StateErrors<T, string>> {
values: StateValues<T>;
validity: StateValidity<T>;
touched: StateValidity<T>;
errors: E;
clear(): void;
setField<K extends keyof T>(name: K, value: T[K]): void;
setFields(fieldValues: StateValues<T>, options?: SetFieldsOptions): void;
setFieldError(name: keyof T, error: string): void;
clearField(name: keyof T): void;
}
Expand Down
58 changes: 58 additions & 0 deletions src/useState.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 });
Expand Down
167 changes: 167 additions & 0 deletions test/useFormState-manual-updates.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]) => (
<>
<input {...input.text('firstName')} />
<input {...input.text('lastName')} required />
<input {...input.number('age')} />
</>
));

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]) => (
<>
<input {...input.text('firstName')} />
<input {...input.text('lastName')} required />
</>
));

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]) => (
<>
<input {...input.text('firstName')} />
<input {...input.text('lastName')} required />
</>
));

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]) => (
<>
<input {...input.text('firstName')} />
<input {...input.text('lastName')} required />
</>
));

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]) => (
<>
<input {...input.text('firstName')} />
</>
));

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]) => (
<input {...input.text('name')} />
Expand Down