From 0770c495b33444775c5e799faf7a8a7ee5791f0e Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Wed, 16 Nov 2022 16:57:17 +0200 Subject: [PATCH] feat: array value support --- .vscode/tasks.json | 13 ++++++++++ src/types.ts | 24 ++++++++++++++++-- src/use-form.ts | 61 +++++++++++++++++++++++++++------------------- 3 files changed, 71 insertions(+), 27 deletions(-) create mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..dba1d91 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "build", + "group": "build", + "problemMatcher": [], + "label": "npm: build", + "detail": "webpack --mode=production --node-env=production" + } + ] +} diff --git a/src/types.ts b/src/types.ts index bbdb7d7..4497414 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,5 @@ +import React from 'react' + /** * Options for the `useForm` hook * @@ -77,7 +79,7 @@ export interface UseFormReturn { /** * The current form data, before parsing. */ - rawState: Partial> + rawState: Partial> /** * Indicates whether the form is valid. @@ -132,6 +134,13 @@ export interface FieldOptions { */ required?: boolean + /** + * If `true`, handlers will treat the field as an array and not a single value. + * + * If you supply an array as the initial value, this will be set to `true` automatically. + */ + multiple?: boolean + /** * Minimum length (in characters) for the field. * @@ -196,7 +205,8 @@ export interface FieldOptions { * @see {@link UseFormReturn.state} for the parsed form state * @see {@link UseFormReturn.rawState} for the raw form state */ - parse?(value: string): T[K] + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parse?(value: InputType | any): T[K] /** * A callback for changing the input, which also contains the parsed value. @@ -314,9 +324,16 @@ export interface ErrorStrings { maxLength: string | MessageResolver } +export type InputType = unknown + /** @hidden */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export type FieldReturn = { + /** + * The name of the field. + */ + name: string + /** * The value of the field. */ @@ -343,6 +360,9 @@ export type FieldReturn = { export type ChangeEvent = { // eslint-disable-next-line @typescript-eslint/no-explicit-any target: any + defaultPrevented: boolean + persist?(): void } + /** @hidden */ export type BlurEvent = ChangeEvent diff --git a/src/use-form.ts b/src/use-form.ts index 1b20951..1ce5482 100644 --- a/src/use-form.ts +++ b/src/use-form.ts @@ -6,6 +6,7 @@ import { ErrorStrings, FieldOptions, FieldReturn, + InputType, UseFormOptions, UseFormReturn, } from './types' @@ -36,12 +37,10 @@ export function useForm({ {} as unknown as Record>, ) const [state, setState] = React.useState>(initialState ?? {}) - const [rawState, setRawState] = React.useState< - Partial> - >( + const [rawState, setRawState] = React.useState>>( Object.entries(state).reduce( - (acc, [key, value]) => ({ ...acc, [key]: String(value) }), - {} as Partial>, + (acc, [key, value]) => ({ ...acc, [key]: value }), + {} as Partial>, ), ) const [errors, setErrors] = React.useState>>({}) @@ -49,8 +48,8 @@ export function useForm({ function setValue( key: K, - value: T[K] | string | string[] | number, - raw: T[K] | string | string[] | number = value, + value: T[K] | InputType, + raw: T[K] | InputType = value, ) { setRawState((prev) => ({ ...prev, [key]: raw })) setState((s) => ({ ...s, [key]: value })) @@ -93,10 +92,10 @@ export function useForm({ // eslint-disable-next-line @typescript-eslint/no-unused-vars function parseValue( - value: string | number | string[] | null | undefined, + value: InputType | null | undefined, options?: FieldOptions, - ): string | number | string[] | undefined | T[K] { - const parsed = options?.parse ? options.parse(value as string) : value + ): InputType | InputType[] | undefined | T[K] | T[K][] { + const parsed = options?.parse ? options.parse(value) : value if (value !== '' && options?.pattern) { if (!new RegExp(options.pattern).test(parsed as string)) { @@ -108,38 +107,50 @@ export function useForm({ } function fieldProps(key: K, options?: FieldOptions): FieldReturn { - fields.current = { ...fields.current, [key]: options } + const isArrayField = options.multiple || (initialState[key] && Array.isArray(initialState[key])) + options = { ...options, multiple: options?.multiple ?? isArrayField } + + fields.current = { + ...fields.current, + [key]: options, + } if (autoValidateBehavior === 'immediate') { setErrorsFromRaw(key, rawState[key], options) } return { - value: rawState[key] ?? '', + name: key as string, + value: rawState[key] ?? (isArrayField ? [] : ''), onChange: (e: ChangeEvent) => { + e.persist?.() const value = parseValue(e.target.value, options) if (value === undefined) { return } - setValueFromRaw(key, e.target.value, options) - - if (autoValidateBehavior === 'onChange') { - if (setErrorsFromRaw(key, e.target.value, options)) { - options?.onChange?.(e, value as T[K]) - } + const isValid = + autoValidateBehavior !== 'onChange' || !setErrorsFromRaw(key, value, options) + if (isValid) { + options?.onChange?.(e, value as T[K]) + } + if (!e.defaultPrevented) { + setValueFromRaw(key, e.target.value, options) } }, onBlur: (e: BlurEvent) => { + e.persist?.() const value = parseValue(e.target.value, options) if (value === undefined) { return } - setValueFromRaw(key, e.target.value, options) - if (autoValidateBehavior === 'onBlur') { - if (setErrorsFromRaw(key, e.target.value, options)) { - options?.onChange?.(e, value as T[K]) - } + const isValid = + autoValidateBehavior !== 'onBlur' || !setErrorsFromRaw(key, value, options) + if (isValid) { + options?.onBlur?.(e, value as T[K]) + } + if (!e.defaultPrevented) { + setValueFromRaw(key, e.target.value, options) } }, } @@ -147,7 +158,7 @@ export function useForm({ function setErrorsFromRaw( key: K, - value: string | string[] | number, + value: InputType, options: FieldOptions, ): boolean { const parsed = parseValue(value, options) @@ -167,7 +178,7 @@ export function useForm({ function setValueFromRaw( key: K, - value: string | string[] | number, + value: InputType, options: FieldOptions, ) { const parsed = parseValue(value, options)