diff --git a/docs/src/api/useField.mdx b/docs/src/api/useField.mdx index 16b14eef..026eff58 100644 --- a/docs/src/api/useField.mdx +++ b/docs/src/api/useField.mdx @@ -4,24 +4,23 @@ menu: Api route: /api/usefield --- - # useField _Hook to mount a field to the form._ ```tsx -import { useField } from "fielder" +import { useField } from 'fielder'; ``` ## Example usage ```tsx const [passwordProps, passwordMeta] = useField({ - name: "password", - validate: passwordValidation, -}) + name: 'password', + validate: passwordValidation +}); -return +return ; ``` ## Return type @@ -66,7 +65,9 @@ _Whether the initial value should be considered valid._ Type: `boolean` -Default: `false` +Default: `false` (see comment below) + +> When no `validate` argument is passed to `useField`, the default value is `true` ### initialError @@ -132,11 +133,11 @@ Props which can be passed to a form element / component. ```ts type UseFieldProps = { - readonly name: string - readonly value: T - readonly onChange: ChangeEventHandler - readonly onBlur: () => void -} + readonly name: string; + readonly value: T; + readonly onChange: ChangeEventHandler; + readonly onBlur: () => void; +}; ``` ### UseFieldMeta @@ -145,9 +146,9 @@ Additional information about a field and it's validation state. ```ts type UseFieldMeta = { - readonly touched: boolean - readonly error?: Error | string - readonly isValid: boolean - readonly isValidating: boolean -} + readonly touched: boolean; + readonly error?: Error | string; + readonly isValid: boolean; + readonly isValidating: boolean; +}; ``` diff --git a/src/useField.spec.tsx b/src/useField.spec.tsx index 270b8bf4..714b3f6e 100644 --- a/src/useField.spec.tsx +++ b/src/useField.spec.tsx @@ -1,8 +1,8 @@ -import React, { FC } from "react"; -import { mount } from "enzyme"; -import { useField, UseFieldResponse } from "./useField"; -import { FielderContext } from "./context"; -import { FieldConfig } from "./types"; +import React, { FC } from 'react'; +import { mount } from 'enzyme'; +import { useField, UseFieldResponse } from './useField'; +import { FielderContext } from './context'; +import { FieldConfig } from './types'; const context = { fields: {}, @@ -30,16 +30,16 @@ const Fixture: FC = ({ children }) => { beforeEach(jest.clearAllMocks); -describe("on mount", () => { - it("calls mountField", () => { - args = { name: "someField" }; +describe('on mount', () => { + it('calls mountField with default values', () => { + args = { name: 'someField' }; mount(); expect(context.mountField).toBeCalledTimes(1); expect(context.mountField).toBeCalledWith({ name: args.name, initialError: undefined, - initialValid: false, + initialValid: true, initialValue: undefined, initialTouched: false, validate: undefined, @@ -49,12 +49,30 @@ describe("on mount", () => { }); }); - it("calls mountField with default overrides", () => { + it('calls mountField with default values (validate function provided)', () => { + args = { name: 'someField', validate: jest.fn() }; + mount(); + + expect(context.mountField).toBeCalledTimes(1); + expect(context.mountField).toBeCalledWith({ + name: args.name, + initialError: undefined, + initialValid: false, + initialValue: undefined, + initialTouched: false, + validate: args.validate, + validateOnBlur: true, + validateOnChange: true, + validateOnUpdate: false + }); + }); + + it('calls mountField with default overrides', () => { args = { - name: "someField", - initialError: "aaa", + name: 'someField', + initialError: 'aaa', initialValid: true, - initialValue: "hello", + initialValue: 'hello', initialTouched: true, validate: jest.fn(), validateOnBlur: false, @@ -70,9 +88,9 @@ describe("on mount", () => { }); }); -describe("on unmount", () => { - it("calls unmountField", () => { - args = { name: "someField" }; +describe('on unmount', () => { + it('calls unmountField', () => { + args = { name: 'someField' }; const wrapper = mount(); wrapper.unmount(); @@ -83,8 +101,8 @@ describe("on unmount", () => { }); }); - it("calls unmountField and destroys value", () => { - args = { name: "someField", destroyOnUnmount: true }; + it('calls unmountField and destroys value', () => { + args = { name: 'someField', destroyOnUnmount: true }; const wrapper = mount(); wrapper.unmount(); @@ -96,9 +114,9 @@ describe("on unmount", () => { }); }); -describe("on blur", () => { - it("calls blurField", () => { - args = { name: "someField" }; +describe('on blur', () => { + it('calls blurField', () => { + args = { name: 'someField' }; mount(); response[0].onBlur(); @@ -109,16 +127,16 @@ describe("on blur", () => { }); }); -describe("on change", () => { - describe("basic input", () => { - it("calls setFieldValue", () => { - const value = "newval"; - args = { name: "someField" }; +describe('on change', () => { + describe('basic input', () => { + it('calls setFieldValue', () => { + const value = 'newval'; + args = { name: 'someField' }; mount(); response[0].onChange({ currentTarget: { - tagName: "INPUT", + tagName: 'INPUT', getAttribute: () => undefined, value } @@ -132,16 +150,16 @@ describe("on change", () => { }); }); - describe("radio input", () => { - it("calls setFieldValue", () => { - const value = "newval"; - args = { name: "someField" }; + describe('radio input', () => { + it('calls setFieldValue', () => { + const value = 'newval'; + args = { name: 'someField' }; mount(); response[0].onChange({ currentTarget: { - tagName: "INPUT", - getAttribute: () => "radio", + tagName: 'INPUT', + getAttribute: () => 'radio', value } } as any); @@ -154,22 +172,22 @@ describe("on change", () => { }); }); - describe("checkbox input", () => { - const value = "newval"; - args = { name: "someField" }; + describe('checkbox input', () => { + const value = 'newval'; + args = { name: 'someField' }; beforeEach(() => { mount(); response[0].onChange({ currentTarget: { - tagName: "INPUT", - getAttribute: () => "checkbox", + tagName: 'INPUT', + getAttribute: () => 'checkbox', value } } as any); }); - it("calls setFieldValue", () => { + it('calls setFieldValue', () => { expect(context.setFieldValue).toBeCalledTimes(1); expect(context.setFieldValue).toBeCalledWith( expect.objectContaining({ @@ -178,37 +196,37 @@ describe("on change", () => { ); }); - describe("on setFieldValue value function", () => { + describe('on setFieldValue value function', () => { let valueFn: Function; - const otherVals = ["other", "again"]; + const otherVals = ['other', 'again']; beforeEach(() => { valueFn = context.setFieldValue.mock.calls[0][0].value; }); - it("instantiates array with value", () => { + it('instantiates array with value', () => { expect(valueFn()).toEqual([value]); }); - it("removes value from array", () => { + it('removes value from array', () => { expect(valueFn([...otherVals, value])).toEqual(otherVals); }); - it("appends value to array", () => { + it('appends value to array', () => { expect(valueFn(otherVals)).toEqual([...otherVals, value]); }); }); }); - describe("select input", () => { - it("calls setFieldValue", () => { - const value = "newval"; - args = { name: "someField" }; + describe('select input', () => { + it('calls setFieldValue', () => { + const value = 'newval'; + args = { name: 'someField' }; mount(); response[0].onChange({ currentTarget: { - tagName: "SELECT", + tagName: 'SELECT', value, getAttribute: () => undefined } diff --git a/src/useField.ts b/src/useField.ts index 082fbda3..e4a19081 100644 --- a/src/useField.ts +++ b/src/useField.ts @@ -7,9 +7,9 @@ import React, { useCallback, useMemo, useLayoutEffect -} from "react"; -import { FielderContext } from "./context"; -import { FormState, FormError, FieldState, FieldConfig } from "./types"; +} from 'react'; +import { FielderContext } from './context'; +import { FormState, FormError, FieldState, FieldConfig } from './types'; export type UseFieldProps = { readonly name: string; @@ -19,10 +19,10 @@ export type UseFieldProps = { }; export type UseFieldMeta = { - readonly touched: FieldState["touched"]; - readonly error: FieldState["error"]; - readonly isValid: FieldState["isValid"]; - readonly isValidating: FieldState["isValidating"]; + readonly touched: FieldState['touched']; + readonly error: FieldState['error']; + readonly isValid: FieldState['isValid']; + readonly isValidating: FieldState['isValidating']; }; type SupportedElements = @@ -36,7 +36,7 @@ export type UseFieldResponse = [UseFieldProps, UseFieldMeta]; export const useField = ({ name: initialName, validate, - initialValid = false, + initialValid = !validate, initialError = undefined, initialValue = undefined, initialTouched = false, @@ -68,7 +68,7 @@ export const useField = ({ useLayoutEffect(() => { if (fields[name] && fields[name]._isActive) { - throw Error("Duplicate field mounted."); + throw Error('Duplicate field mounted.'); } mountField({ @@ -95,7 +95,7 @@ export const useField = ({ const type = getElementType(e); const newVal = e.currentTarget.value; - if (type !== "checkbox") { + if (type !== 'checkbox') { return setFieldValue({ name, value: newVal }); } @@ -130,23 +130,23 @@ const getElementType = (e: ChangeEvent) => { const target = e.currentTarget || e.target; const tagName = target.tagName.toLowerCase(); - if (tagName === "select") { - return "select"; + if (tagName === 'select') { + return 'select'; } - const type = target.getAttribute("type"); + const type = target.getAttribute('type'); - if (tagName === "input" && type === "checkbox") { - return "checkbox"; + if (tagName === 'input' && type === 'checkbox') { + return 'checkbox'; } - if (tagName === "input" && type === "radio") { - return "radio"; + if (tagName === 'input' && type === 'radio') { + return 'radio'; } - if (tagName === "input") { - return "input"; + if (tagName === 'input') { + return 'input'; } - throw Error("Unsupported input element"); + throw Error('Unsupported input element'); };