UNPKG

formularity

Version:

The last React form library you will ever need!

309 lines (308 loc) 14.3 kB
import { ChangeEvent, FocusEvent, FormEvent, ReactNode } from 'react'; import { FieldProps } from './Field'; import { SubmitButton } from './SubmitButton'; import { ResetButton } from './ResetButton'; import { DeepKeys, DeepPartial, DeepValue, EmptyObject, IntrinsicFormElements, MapNestedPrimitivesTo, NoInfer, Nullish, UnsubScribeFn } from './utilityTypes'; import { CreateFormStoreParams } from './createFormStore'; import { FieldListProps } from './FieldList'; export type FormValues = Record<PropertyKey, unknown> | null; export type FormErrors<TFormValues extends FormValues> = MapNestedPrimitivesTo<TFormValues, string> | EmptyObject; export type FormTouched<TFormValues extends FormValues> = MapNestedPrimitivesTo<TFormValues, boolean> | EmptyObject; export type DirtyFields<TFormValues extends FormValues> = Array<DeepKeys<NonNullable<TFormValues>>>; export type FieldRegistry<TFormValues extends FormValues> = Record<DeepKeys<TFormValues>, NewFieldRegistration<TFormValues> | null> | EmptyObject; export type NewFieldRegistration<TFormValues extends FormValues, TFieldName extends DeepKeys<TFormValues> = DeepKeys<TFormValues>> = { name: TFieldName; /** * single field validator that is passed to the field */ validationHandler: SingleFieldValidator<TFormValues, NoInfer<TFieldName>> | null; /** * Any ID that may be passed to the field */ fieldId?: string | number; /** * type of the field for registration purposes (parent can have access) */ type?: string; }; export type FieldRegistration<TFormValues extends FormValues = FormValues> = { /** * Function for adding a field to the registry */ registerField: <TFieldName extends DeepKeys<TFormValues>>(newFieldRegistration: NewFieldRegistration<TFormValues, TFieldName>) => void; /** * Function for removing a field from the registry */ unregisterField: <TFieldName extends DeepKeys<TFormValues>>(fieldName: TFieldName) => void; }; export type ValidationHandler<TFormValues extends FormValues> = (values: TFormValues) => Promise<DeepPartial<FormErrors<TFormValues>> | null> | DeepPartial<FormErrors<TFormValues>>; export type SingleFieldValidator<TFormValues extends FormValues, TFieldName extends DeepKeys<TFormValues>> = (value: DeepValue<TFormValues, TFieldName>) => Promise<string | Nullish> | string | Nullish; /** * @param shouldValidate * Whether to run validation after field value is updated or the field is blurred. * Setting this prop to `false` will cancel any validation set for the field. * Granular configuration of the field validation can be set with the `validationEvent` * prop if set to `true` or not set. * * @default true * ----------------------- * @param validationEvent * The field events for which validation should occur. *Only applies if * `shouldValidate` is set to `true` or not set at all.* * * Not setting this prop or setting this prop to `'all'` will run validation on every field * change or blur event. * * @default 'all' */ export type FieldValidationOptions = { shouldValidate?: true; validationEvent?: 'onChange' | 'onBlur' | 'all'; } | { shouldValidate: false; validationEvent?: never; }; export type FormStore<TFormValues extends FormValues> = { get: () => FormStoreState<TFormValues>; set: (newFormStore: Partial<FormStoreState<TFormValues>>) => void; subscribe: (callback: () => void) => UnsubScribeFn; }; export type FormStoreState<TFormValues extends FormValues> = { /** * The initial form values provided by the user at the * time of form store creation. */ initialValues: TFormValues; /** * The current state of the values of the form */ values: TFormValues; /** * The current errors in the form. *Note that only fields with validation * logic attached to them (whether through field or form-level validation) * can ever have errors populate in this object.* */ errors: FormErrors<TFormValues>; /** * The currently touched fields of the form */ touched: FormTouched<TFormValues>; /** * Current submitting status of the form */ isSubmitting: boolean; /** * This returns true if the form is currently validating (running validation). */ isValidating: boolean; /** * The number of times the form has been submitted since it * has been mounted in the DOM */ submitCount: number; /** * Whether the form is being used in an editing state. * This is a custom field that is provided by the user at the time * of initializing the form based on the logic of their application. */ isEditing: boolean; /** * Manual validation handler passed to the form store instance */ manualValidationHandler?: CreateFormStoreParams<TFormValues>['manualValidationHandler']; /** * Validation schema passed to the form store instance */ validationSchema?: CreateFormStoreParams<TFormValues>['validationSchema']; /** * Optional top-level submit handler *See ```CreateFormStoreParams``` type for full * description/usage */ onSubmit?: (formValues: TFormValues) => void | Promise<void>; }; export type FormHandlers<TFormValues extends FormValues> = { /** * Set the value of a single field */ setFieldValue: <TFieldName extends DeepKeys<TFormValues> = DeepKeys<TFormValues>, TFieldValue extends DeepValue<TFormValues, TFieldName> = DeepValue<TFormValues, TFieldName>, TFieldValidationOptions extends FieldValidationOptions = FieldValidationOptions>(fieldName: TFieldName, newValue: TFieldValue, options?: TFieldValidationOptions) => void; /** * Set the values of any number of fields simultaneously */ setValues: (newValues: DeepPartial<TFormValues>) => void; /** * Set the error message of a single field */ setFieldError: (fieldName: DeepKeys<TFormValues>, newError: string) => void; /** * Set the error messages of any number of fields simultaneously */ setErrors: (newErrors: DeepPartial<FormErrors<TFormValues>>) => void; /** * Set the touched status of a single field */ setFieldTouched: (fieldName: DeepKeys<TFormValues>, newTouched: boolean) => void; /** * Set the touched statuses of any number of fields simultaneously */ setTouched: (newTouched: FormTouched<TFormValues>) => void; /** * Helper method to handle the updating of a field's value by * taking the event emitted from onChange and setting the * field's value accordingly */ handleChange: (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>, fieldValidationOptions?: FieldValidationOptions) => void; /** * Helper method to handle the updating of a field's touched status by * taking the event emitted from onBlur and setting the * field's touched status accordingly */ handleBlur: (e: FocusEvent<HTMLInputElement | HTMLSelectElement>, fieldValidationOptions?: FieldValidationOptions) => void; /** * Helper method for submitting the form imperatively */ submitForm: () => Promise<void>; /** * Helper method for handling form submission */ handleSubmit: (e: FormEvent<HTMLFormElement>) => void | Promise<void>; /** * Reset the form imperatively and optional set all or some of the * form values to new values(s) */ resetForm: (newFormValues?: DeepPartial<TFormValues>) => void; /** * Helper method for handling form reset */ handleReset: (e: FormEvent<HTMLFormElement>) => void; /** * Helper method or explicitly calling validation on the entire form. * For validating certain fields, use `validateField` instead. * **Please note that you must `await` any calls to `validateForm` in * order for the the form to properly update, as this method can handle * asynchronous operations** * * @param options a set of options to configure the validation call */ validateForm: (options?: { /** * An option to use for extending the manual validation by also touching all fields. * This ensures that calling `validateForm` also shows all of the field errors on * the screen as the fields have also been touched. This is the equivalent to what * is done when validating during `onSubmit`. * * @default true */ shouldTouchAllFields?: boolean; }) => Promise<FormErrors<TFormValues>> | FormErrors<TFormValues>; /** * Helper method for validating a single field. **The field to be validated must have * a validator set as via the `validator` prop on the `<Field />` or pass an optional `validator` * to the options object (second argument) of `validateField`. **Please note that you must `await` * any calls to `validateField` in order for the the form to properly update, as this method can handle * asynchronous operations** * * @param fieldName the name of the field to validate * @param validator an optional validator to use for validation in the case a validator is not provided via props * @param shouldTouchField whether to touch the field or not after validation (defaults to `true`) */ validateField: <TFieldName extends DeepKeys<TFormValues>>(fieldName: TFieldName, options?: { validator?: SingleFieldValidator<TFormValues, TFieldName>; shouldTouchField?: boolean; }) => Promise<string | null> | string | null; }; export type SubmissionOrResetHelpers<TFormValues extends FormValues> = Omit<FormHandlers<TFormValues>, 'handleReset' | 'handleSubmit' | 'submitForm' | 'handleBlur' | 'handleChange' | 'validateForm' | 'validateField'>; export type OnSubmitOrReset<TFormValues extends FormValues> = (formValues: TFormValues, submitOrResetHelpers: SubmissionOrResetHelpers<TFormValues>) => void | Promise<void>; export type FormComputedProps<TFormValues extends FormValues> = { /** * Returns true if any field in the form is dirty (the field value * is not exactly the same as it was in the initialValues) */ isDirty: boolean; /** * An array of the names of all fields that are dirty */ dirtyFields: DirtyFields<TFormValues>; /** * Returns true if all fields in the form are pristine (every value in the form * matches the initialValues) */ isPristine: boolean; /** * This returns true if the form is currently in a valid state; * There are no errors present. */ isValid: boolean; /** * Returns true if any field in the form is touched */ isFormTouched: boolean; /** * Returns `true` if every field (all nested fields included) are touched */ areAllFieldsTouched: boolean; }; export type FormularityProps<TFormValues extends FormValues = FormValues> = FormStoreState<TFormValues> & FieldRegistration<TFormValues> & FormHandlers<TFormValues> & FormComputedProps<TFormValues> & FormularityComponents<TFormValues>; export type FormularityComponents<TFormValues extends FormValues> = { /** * The `<Field />` component is the main component for hooking up inputs and * sections of forms in Formularity. It drastically reduces the amount of * boilerplate code needed to manage the state of a form field by taking care of * many basic actions such as handling change, blur, and showing errors. **Must * be used underneath a `<Formularity />` component.** */ Field: FieldComponent<TFormValues>; /** * `<FieldList />` is a component that helps you * render out multiple fields in an array-like (list) fashion * to help manage values in the form that are grouped together. * @example * * ```jsx * <FieldList name='hobbies' render={ ( hobbies, { addField } ) => { return ( <> <label>Hobbies</label> { hobbies.map( ( _, idx ) => ( <Field key={ idx } name={ `hobbies[${ idx }]` } showErrors /> ) ) } <button onClick={ () => addField( '' ) } type='button' > Add Hobby </button> </> ); } } /> * ``` */ FieldList: FieldListComponent<TFormValues>; /** * This component is an abstraction of the `<button type='submit' />` pattern in forms, * as well as a simple way to set common submit disabling logic on the form. * Use this component to reduce submit button boilerplate code and ensure proper * submitting of the form. */ SubmitButton: SubmitButtonComponent; /** * This component is an abstraction of the `<button type='reset' />` pattern in forms. * Use this component to reduce reset button boilerplate code and ensure proper * resetting of the form. */ ResetButton: ResetButtonComponent; }; export type FieldComponent<TFormValues extends FormValues> = <TFieldName extends DeepKeys<TFormValues>, TComponentProps = keyof IntrinsicFormElements, TShowErrors extends boolean = false, TLabel extends string | undefined = undefined, TFieldValue extends DeepValue<TFormValues, TFieldName> = DeepValue<TFormValues, TFieldName>, TShouldValidate extends boolean = boolean>(props: FieldProps<TFormValues, TFieldName, TComponentProps, TShowErrors, TLabel, TFieldValue, TShouldValidate>) => ReactNode; export type FieldListComponent<TFormValues extends FormValues> = <TFieldName extends DeepKeys<TFormValues>, TFieldData extends DeepValue<TFormValues, TFieldName>>(props: FieldListProps<TFormValues, TFieldName, TFieldData>) => ReactNode; export type SubmitButtonComponent = typeof SubmitButton; export type ResetButtonComponent = typeof ResetButton;