mirror of
https://github.com/chenasraf/formplex-react.git
synced 2026-05-17 17:48:11 +00:00
feat: add validate method
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## v0.1.3
|
||||
|
||||
- feat: add `validate` method
|
||||
- fix: remove unnecessary `pattern` from `ErrorString`
|
||||
|
||||
## v0.1.2
|
||||
|
||||
135
src/types.ts
135
src/types.ts
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Options for the `useForm` hook
|
||||
*
|
||||
* @typeParam T - The type of the form state.
|
||||
*/
|
||||
export interface UseFormOptions<T> {
|
||||
/**
|
||||
@@ -15,7 +17,12 @@ export interface UseFormOptions<T> {
|
||||
|
||||
/**
|
||||
* Map of custom error messages for the default validation methods.
|
||||
* @see {ErrorStrings}
|
||||
* @see {@link ErrorStrings} for definition of error messages
|
||||
* @see {@link FieldOptions.errorMessages | FieldOptions.errorMessages} for field-specific error messages
|
||||
* @see {@link FieldOptions.required | FieldOptions.required} for defining a field as required
|
||||
* @see {@link FieldOptions.minLength | FieldOptions.minLength} for defining a minimum length for the field
|
||||
* @see {@link FieldOptions.maxLength | FieldOptions.maxLength} for defining a maximum length for the field
|
||||
* @see {@link FieldOptions.validate | FieldOptions.validate} for defining a custom validation function
|
||||
*/
|
||||
errorMessages?: Partial<ErrorStrings>
|
||||
|
||||
@@ -29,6 +36,8 @@ export interface UseFormOptions<T> {
|
||||
* `onBlur` - Show validations after the user has blurred the input.
|
||||
*
|
||||
* `never` - Show validations after the user has submitted the form only, or on manual trigger.
|
||||
*
|
||||
* @see {@link UseFormReturn.validate} for manual validation trigger
|
||||
*/
|
||||
autoValidateBehavior?: 'immediate' | 'onChange' | 'onBlur' | 'never'
|
||||
}
|
||||
@@ -37,6 +46,13 @@ export interface UseFormReturn<T> {
|
||||
/**
|
||||
* Register a field input named `key` to the form. This will return props that should be injected into the input.
|
||||
* See each of the options for more information.
|
||||
*
|
||||
* @typeParam K - The key of the field in the form state.
|
||||
* @typeParam E - The type of the input element.
|
||||
* @param key The name of the field.
|
||||
* @param options Options for the field.
|
||||
* @returns Props that should be injected into the input.
|
||||
* @see {@link FieldOptions}
|
||||
*/
|
||||
field: <K extends keyof T, E>(key: K, options?: FieldOptions<T, K>) => FieldReturn<E>
|
||||
|
||||
@@ -45,6 +61,8 @@ export interface UseFormReturn<T> {
|
||||
* Each property is the name of the field and the value is the error message, if any.
|
||||
*
|
||||
* If there is no error, the value will be `undefined`.
|
||||
*
|
||||
* @see {@link ErrorMessage}
|
||||
*/
|
||||
errors: Partial<Record<keyof T, ErrorMessage>>
|
||||
|
||||
@@ -66,55 +84,86 @@ export interface UseFormReturn<T> {
|
||||
/**
|
||||
* A callback that will submit the form. This will also call `onSubmit` if it was provided.
|
||||
* You should inject this into the form's `onSubmit` prop.
|
||||
*
|
||||
* @param e The form submit event.
|
||||
*/
|
||||
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void
|
||||
|
||||
/**
|
||||
* Set multiple fields at once. This will cause the form to re-render.
|
||||
*
|
||||
* @param values The values to set, as an object of `{ field: value }`.
|
||||
* @see {@link UseFormReturn.setValue | UseFormReturn.setValue} for setting a single value
|
||||
*/
|
||||
setValues: (values: Partial<T>) => void
|
||||
|
||||
/**
|
||||
* Set a single field. This will cause the form to re-render.
|
||||
*
|
||||
* @param key The name of the field.
|
||||
* @param value The value to set.
|
||||
* @see {@link UseFormReturn.setValues | UseFormReturn.setValues} for setting multiple values at once
|
||||
*/
|
||||
setValue: <K extends keyof T>(key: K, value: T[K]) => void
|
||||
|
||||
/**
|
||||
* Perform validation on all fields, and return whether the form is valid.
|
||||
*
|
||||
* @see {@link FieldOptions.validate | FieldOptions.validate} for custom validation on a field.
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for custom error messages.
|
||||
* @returns Whether the form is valid.
|
||||
*/
|
||||
validate(): boolean
|
||||
}
|
||||
|
||||
export interface FieldOptions<T, K extends keyof T = keyof T> {
|
||||
/**
|
||||
* If `true`, the field will be required.
|
||||
*
|
||||
* This will cause the form to be invalid if the field is empty
|
||||
* and an error message will be provided in `errors`.
|
||||
* This will cause the form to be invalid if the field is empty and an error message will be provided in
|
||||
* {@link UseFormReturn.errors | UseFormReturn.errors}.
|
||||
*
|
||||
* To provide a custom error message, use {@link FieldOptions.errorMessages}.
|
||||
*/
|
||||
required?: boolean
|
||||
|
||||
/**
|
||||
* Minimum length (in characters) for the field.
|
||||
*
|
||||
* This will cause the form to be invalid if the field is shorter than the given length,
|
||||
* and an error message will be provided in `errors`.
|
||||
* This will cause the form to be invalid if the field is shorter than the given length, and an error message will
|
||||
* be provided in {@link UseFormReturn.errors | UseFormReturn.errors}.
|
||||
*
|
||||
* To provide a custom error message, use {@link FieldOptions.errorMessages}.
|
||||
*/
|
||||
minLength?: number
|
||||
|
||||
/**
|
||||
* Maximum length (in characters) for the field.
|
||||
*
|
||||
* This will cause the form to be invalid if the field is longer than the given length,
|
||||
* and an error message will be provided in `errors`.
|
||||
* This will cause the form to be invalid if the field is longer than the given length, and an error message will
|
||||
* be provided in {@link UseFormReturn.errors | UseFormReturn.errors}.
|
||||
*
|
||||
* To provide a custom error message, use {@link FieldOptions.errorMessages}.
|
||||
*/
|
||||
maxLength?: number
|
||||
|
||||
/**
|
||||
* A regular expression that the field must match.
|
||||
*
|
||||
* This will cause the input to not update if the value does not match the given pattern.
|
||||
* To cause a validation error for a pattern, use `validate` instead.
|
||||
* This will cause the input to not update if the value does not match the given pattern. To cause a validation
|
||||
* error for a pattern, use {@link FieldOptions.validate} instead.
|
||||
*/
|
||||
pattern?: string | RegExp
|
||||
|
||||
/**
|
||||
* Map of custom error messages for the default validation methods.
|
||||
*
|
||||
* @see {@link ErrorStrings} for definition of error messages
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for global error messages (for the entire form)
|
||||
* @see {@link FieldOptions.required} for defining a field as required
|
||||
* @see {@link FieldOptions.minLength} for defining a minimum length for the field
|
||||
* @see {@link FieldOptions.maxLength} for defining a maximum length for the field
|
||||
* @see {@link FieldOptions.validate} for defining a custom validation function
|
||||
*/
|
||||
errorMessages?: Partial<ErrorStrings>
|
||||
|
||||
@@ -124,14 +173,24 @@ export interface FieldOptions<T, K extends keyof T = keyof T> {
|
||||
* If it returns an empty string, `null` or `undefined`, the field is valid.
|
||||
*
|
||||
* Otherwise, the field is invalid and the returned string will be the error message.
|
||||
*
|
||||
* @param value The value of the field.
|
||||
* @returns The error message, if any, or undefined/null if the field is valid.
|
||||
* @see {@link ErrorStrings} for definition of error messages
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for global error messages for the default validation methods
|
||||
* @see {@link FieldOptions.errorMessages} for field-specific error messages for the default validation methods
|
||||
*/
|
||||
validate?: (value: T[K]) => string | undefined | null
|
||||
|
||||
/**
|
||||
* A custom parser for the field. This will be called when the field is updated, and will cause the `state` object
|
||||
* to be updated with the result of this function.
|
||||
* A custom parser for the field. This will be called when the field is updated, and will cause the
|
||||
* {@link UseFormReturn.state} object to be updated with the result of this function.
|
||||
*
|
||||
* `rawState` will always contain the raw value of the field as it was placed here.
|
||||
* {@link UseFormReturn.rawState | UseFormReturn.rawState} will always contain the raw value of the field as it was
|
||||
* placed here.
|
||||
*
|
||||
* @param value The value of the field.
|
||||
* @returns The parsed value of the field.
|
||||
*/
|
||||
parse?: (value: string) => T[K]
|
||||
|
||||
@@ -139,24 +198,56 @@ export interface FieldOptions<T, K extends keyof T = keyof T> {
|
||||
* A callback for changing the input, which also contains the parsed value.
|
||||
*
|
||||
* Never use `onChange` on the input field directly, or it will not work to update the form state.
|
||||
*
|
||||
* Use this callback instead, which acts right after the input's `onChange` callback.
|
||||
*
|
||||
* @param event The input change event.
|
||||
* @param value The parsed value of the field.
|
||||
*/
|
||||
onChange?: (event: ChangeEvent, value: T[K]) => void
|
||||
|
||||
/**
|
||||
* A callback for leaving focus from the input, which also contains the parsed value.
|
||||
*
|
||||
* Never use `onBlur` on the input field directly, or it will break validation for `onBlur`.
|
||||
*
|
||||
* Use this callback instead, which acts right after the input's `onBlur` callback.
|
||||
*
|
||||
* @param event The input change event.
|
||||
* @param value The parsed value of the field.
|
||||
*/
|
||||
onBlur?: (event: ChangeEvent, value: T[K]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* A single error message. If an error exists on a field, this will be in the {@link UseFormReturn.errors} object
|
||||
* under the field's key. Otherwise, it is `undefined`.
|
||||
*
|
||||
* @see {@link ErrorStrings} for definition of error messages
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for global error messages (for the entire form)
|
||||
* @see {@link FieldOptions.required | FieldOptions.required} for defining a field as required
|
||||
* @see {@link FieldOptions.minLength | FieldOptions.minLength} for defining a minimum length for the field
|
||||
* @see {@link FieldOptions.maxLength | FieldOptions.maxLength} for defining a maximum length for the field
|
||||
* @see {@link FieldOptions.validate | FieldOptions.validate} for defining a custom validation function
|
||||
*/
|
||||
export interface ErrorMessage {
|
||||
/**
|
||||
* The type of validation error on the field, such as `required`, `minLength`, `maxLength`, and `pattern`, or
|
||||
* `validate` for custom validations
|
||||
*
|
||||
* @see {@link ErrorStrings} for definition of error messages
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for global error messages for the default validation methods
|
||||
* @see {@link FieldOptions.errorMessages | FieldOptions.errorMessages} for field-specific error messages for the default validation methods
|
||||
*/
|
||||
error: keyof ErrorStrings | 'validate'
|
||||
|
||||
/**
|
||||
* The error message for the field.
|
||||
*/
|
||||
message: string
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
/** @hidden */
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export type FieldReturn<E> = {
|
||||
/**
|
||||
@@ -164,38 +255,47 @@ export type FieldReturn<E> = {
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value: any
|
||||
|
||||
/**
|
||||
* Whether field is required.
|
||||
*/
|
||||
required?: boolean
|
||||
|
||||
/**
|
||||
* Change event callback
|
||||
*/
|
||||
onChange: (event: ChangeEvent) => void
|
||||
|
||||
/**
|
||||
* Blur event callback
|
||||
*/
|
||||
onBlur: (event: ChangeEvent) => void
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
/** @hidden */
|
||||
export type ChangeEvent = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
target: any
|
||||
}
|
||||
/** @internal */
|
||||
/** @hidden */
|
||||
export type BlurEvent = ChangeEvent
|
||||
|
||||
/**
|
||||
* Map of custom error messages for the default validation methods.
|
||||
*
|
||||
* @see {@link UseFormOptions.errorMessages | UseFormOptions.errorMessages} for global error messages for the default validation methods
|
||||
* @see {@link FieldOptions.errorMessages | FieldOptions.errorMessages} for field-specific error messages for the default validation methods
|
||||
*/
|
||||
export interface ErrorStrings {
|
||||
/**
|
||||
* Error message for when the field is required but missing.
|
||||
*
|
||||
* Default: `"Required"`
|
||||
*
|
||||
* @see {@link FieldOptions.required | FieldOptions.required} for defining a field as required
|
||||
*/
|
||||
required: string
|
||||
|
||||
/**
|
||||
* Error message for when the field length is too short.
|
||||
*
|
||||
@@ -205,8 +305,11 @@ export interface ErrorStrings {
|
||||
* ```ts
|
||||
* (n) => `Must be at least ${n} characters long`
|
||||
* ```
|
||||
*
|
||||
* @see {@link FieldOptions.minLength | FieldOptions.minLength} for defining a minimum length for the field
|
||||
*/
|
||||
minLength: string | ((length: number) => string)
|
||||
|
||||
/**
|
||||
* Error message for when the field length is too long.
|
||||
*
|
||||
@@ -216,6 +319,8 @@ export interface ErrorStrings {
|
||||
* ```ts
|
||||
* (n) => `Must be no more than ${n} characters long`
|
||||
* ```
|
||||
*
|
||||
* @see {@link FieldOptions.maxLength | FieldOptions.maxLength} for defining a maximum length for the field
|
||||
*/
|
||||
maxLength: string | ((length: number) => string)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,15 @@ import {
|
||||
UseFormReturn,
|
||||
} from './types'
|
||||
|
||||
/** The main hook for using forms. See each option and return property for more information. */
|
||||
/**
|
||||
* The main hook for using forms. See each option and return property for more information
|
||||
*
|
||||
* @typeParam T - The type of the form state.
|
||||
* @param options Form options
|
||||
* @returns Object containing the form state, errors, and field registration.
|
||||
* @see {@link UseFormOptions}
|
||||
* @see {@link UseFormReturn}
|
||||
*/
|
||||
export function useForm<T>({
|
||||
initialState = {},
|
||||
errorMessages = {},
|
||||
@@ -23,6 +31,9 @@ export function useForm<T>({
|
||||
maxLength: (n) => `Must be no more than ${n} characters long`,
|
||||
...errorMessages,
|
||||
}
|
||||
const fields = React.useRef<Record<keyof T, FieldOptions<keyof T>>>(
|
||||
{} as unknown as Record<keyof T, FieldOptions<keyof T>>,
|
||||
)
|
||||
const [state, setState] = React.useState<Partial<T>>(initialState ?? {})
|
||||
const [rawState, setRawState] = React.useState<
|
||||
Partial<Record<keyof T, string | string[] | number>>
|
||||
@@ -99,6 +110,8 @@ export function useForm<T>({
|
||||
}
|
||||
|
||||
function fieldProps<K extends keyof T, E>(key: K, options?: FieldOptions<T, K>): FieldReturn<E> {
|
||||
fields.current = { ...fields.current, [key]: options }
|
||||
|
||||
if (autoValidateBehavior === 'immediate') {
|
||||
setErrorsFromRaw<K>(key, rawState[key], options)
|
||||
}
|
||||
@@ -174,6 +187,23 @@ export function useForm<T>({
|
||||
}
|
||||
}
|
||||
|
||||
function validateAll(): boolean {
|
||||
const errors = Object.entries(rawState).reduce((acc, [key, value]) => {
|
||||
const error = validate(
|
||||
key as keyof T,
|
||||
value as T[keyof T],
|
||||
(fields.current[key as keyof T] ?? {}) as FieldOptions<T, keyof T>,
|
||||
)
|
||||
if (error) {
|
||||
return { ...acc, [key]: error }
|
||||
}
|
||||
return acc
|
||||
}, {} as Partial<Record<keyof T, ErrorMessage>>)
|
||||
|
||||
setErrors(errors)
|
||||
return Object.values(errors).length === 0
|
||||
}
|
||||
|
||||
return {
|
||||
field: fieldProps,
|
||||
errors,
|
||||
@@ -183,5 +213,6 @@ export function useForm<T>({
|
||||
setValue,
|
||||
setValues: setValues,
|
||||
handleSubmit,
|
||||
validate: validateAll,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user