Merge pull request #2 from chenasraf/develop

v0.1.3
This commit is contained in:
Chen Asraf
2022-11-11 00:57:32 +02:00
committed by GitHub
4 changed files with 163 additions and 32 deletions

View File

@@ -1,11 +1,18 @@
# v0.1.2
# Changelog
## v0.1.3
- feat: add `validate` method
- fix: remove unnecessary `pattern` from `ErrorString`
## v0.1.2
- fix: documentation
# v0.1.1
## v0.1.1
- chore: code cleanups
# v0.1.0
## v0.1.0
- Initial release

View File

@@ -1,6 +1,6 @@
{
"name": "formplex-react",
"version": "0.1.2",
"version": "0.1.3",
"description": "Incredibly easy & flexible React form hooks",
"keywords": [
"react",

View File

@@ -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,17 +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)
/**
* Error message for when the field value does not match the given pattern.
*
* Can either be a string, or a function resolving to a string.
*
* Default:
* ```ts
* (p) => `Must match pattern ${p}`
* ```
*/
pattern: string | ((pattern: string | RegExp) => string)
}

View File

@@ -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 = {},
@@ -21,9 +29,11 @@ export function useForm<T>({
required: 'Required',
minLength: (n) => `Must be at least ${n} characters long`,
maxLength: (n) => `Must be no more than ${n} characters long`,
pattern: (p) => `Must match pattern ${p}`,
...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>>
@@ -100,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)
}
@@ -175,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,
@@ -184,5 +213,6 @@ export function useForm<T>({
setValue,
setValues: setValues,
handleSubmit,
validate: validateAll,
}
}