UNPKG

formango

Version:
575 lines (574 loc) 19.9 kB
import { ComputedRef, MaybeRefOrGetter, Ref } from "vue"; import { ZodFormattedError } from "zod"; //#region src/types/utils.type.d.ts declare const $NestedValue: unique symbol; type NestedValue<TValue extends object = object> = { [$NestedValue]: never; } & TValue; interface File extends Blob { readonly name: string; readonly lastModified: number; } interface FileList { [index: number]: File; item: (index: number) => File | null; readonly length: number; } type Primitive = bigint | boolean | number | string | symbol | null | undefined; type BrowserNativeObject = Date | File | FileList; type DeepPartial<T> = T extends BrowserNativeObject | NestedValue ? T : { [K in keyof T]?: DeepPartial<T[K]> }; /** * Checks whether the type is any * See {@link https://stackoverflow.com/a/49928360/3406963} * @typeParam T - type which may be any * ``` * IsAny<any> = true * IsAny<string> = false * ``` */ type IsAny<T> = 0 extends 1 & T ? true : false; /** * Checks whether T1 can be exactly (mutually) assigned to T2 * @typeParam T1 - type to check * @typeParam T2 - type to check against * ``` * IsEqual<string, string> = true * IsEqual<'foo', 'foo'> = true * IsEqual<string, number> = false * IsEqual<string, number> = false * IsEqual<string, 'foo'> = false * IsEqual<'foo', string> = false * IsEqual<'foo' | 'bar', 'foo'> = boolean // 'foo' is assignable, but 'bar' is not (true | false) -> boolean * ``` */ type IsEqual<T1, T2> = T1 extends T2 ? (<G>() => G extends T1 ? 1 : 2) extends (<G>() => G extends T2 ? 1 : 2) ? true : false : false; type NestedNullableKeys<T> = { [K in keyof T]: T[K] extends object ? NestedNullableKeys<T[K]> | null : T[K] | null }; //#endregion //#region src/types/common.type.d.ts /** * Type to query whether an array type T is a tuple type. * @typeParam T - type which may be an array or tuple * @example * ``` * IsTuple<[number]> = true * IsTuple<number[]> = false * ``` */ type IsTuple<T extends readonly any[]> = number extends T['length'] ? false : true; /** * Type which can be used to index an array or tuple type. */ type ArrayKey = number; /** * Type which given a tuple type returns its own keys, i.e. only its indices. * @typeParam T - tuple type * @example * ``` * TupleKeys<[number, string]> = '0' | '1' * ``` */ type TupleKeys<T extends readonly any[]> = Exclude<keyof T, keyof any[]>; //#endregion //#region src/types/eager.type.d.ts type FieldValues = Record<string, any>; /** * Helper function to break apart T1 and check if any are equal to T2 * * See {@link IsEqual} */ type AnyIsEqual<T1, T2> = T1 extends T2 ? IsEqual<T1, T2> extends true ? true : never : never; /** * Helper type for recursively constructing paths through a type. * This actually constructs the strings and recurses into nested * object types. * * See {@link Path} */ type PathImpl<K$1 extends number | string, V$1, TraversedTypes> = V$1 extends BrowserNativeObject | Primitive ? `${K$1}` : true extends AnyIsEqual<TraversedTypes, V$1> ? `${K$1}` : `${K$1}.${PathInternal<V$1, TraversedTypes | V$1>}` | `${K$1}`; /** * Helper type for recursively constructing paths through a type. * This obsucres the internal type param TraversedTypes from exported contract. * * See {@link Path} */ type PathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? { [K in TupleKeys<T>]-?: PathImpl<K & string, T[K], TraversedTypes> }[TupleKeys<T>] : PathImpl<ArrayKey, V, TraversedTypes> : { [K in keyof T]-?: PathImpl<K & string, T[K], TraversedTypes> }[keyof T]; /** * Type which eagerly collects all paths through a type * @typeParam T - type which should be introspected * @example * ``` * Path<{foo: {bar: string}}> = 'foo' | 'foo.bar' * ``` */ type Path<T> = T extends any ? PathInternal<T> : never; /** * See {@link Path} */ type FieldPath<TFieldValues> = Path<TFieldValues>; /** * Helper type for recursively constructing paths through a type. * This actually constructs the strings and recurses into nested * object types. * * See {@link ArrayPath} */ type ArrayPathImpl<K$1 extends number | string, V$1, TraversedTypes> = V$1 extends BrowserNativeObject | Primitive ? IsAny<V$1> extends true ? string : never : V$1 extends ReadonlyArray<infer U> ? U extends BrowserNativeObject | Primitive ? IsAny<V$1> extends true ? string : never : true extends AnyIsEqual<TraversedTypes, V$1> ? never : `${K$1}.${ArrayPathInternal<V$1, TraversedTypes | V$1>}` | `${K$1}` : true extends AnyIsEqual<TraversedTypes, V$1> ? never : `${K$1}.${ArrayPathInternal<V$1, TraversedTypes | V$1>}`; /** * Helper type for recursively constructing paths through a type. * This obsucres the internal type param TraversedTypes from exported contract. * * See {@link ArrayPath} */ type ArrayPathInternal<T, TraversedTypes = T> = T extends ReadonlyArray<infer V> ? IsTuple<T> extends true ? { [K in TupleKeys<T>]-?: ArrayPathImpl<K & string, T[K], TraversedTypes> }[TupleKeys<T>] : ArrayPathImpl<ArrayKey, V, TraversedTypes> : { [K in keyof T]-?: ArrayPathImpl<K & string, T[K], TraversedTypes> }[keyof T]; /** * Type which eagerly collects all paths through a type which point to an array * type. * @typeParam T - type which should be introspected. * @example * ``` * Path<{foo: {bar: string[], baz: number[]}}> = 'foo.bar' | 'foo.baz' * ``` */ type ArrayPath<T> = T extends any ? ArrayPathInternal<T> : never; /** * Type to evaluate the type which the given path points to. * @typeParam T - deeply nested type which is indexed by the path * @typeParam P - path into the deeply nested type * @example * ``` * PathValue<{foo: {bar: string}}, 'foo.bar'> = string * PathValue<[number, string], '1'> = string * ``` */ type PathValue<T, P extends ArrayPath<T> | Path<T>> = T extends any ? P extends `${infer K}.${infer R}` ? K extends keyof T ? R extends Path<T[K]> ? PathValue<T[K], R> : never : K extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? PathValue<V, R & Path<V>> : never : never : P extends keyof T ? T[P] : P extends `${ArrayKey}` ? T extends ReadonlyArray<infer V> ? V : never : never : never; /** * See {@link PathValue} */ type FieldPathValue<TFieldValues, TFieldPath extends FieldPath<TFieldValues>> = PathValue<TFieldValues, TFieldPath>; //#endregion //#region src/types/standardSpec.type.d.ts /** * The Standard Schema interface. */ interface StandardSchemaV1<Input = unknown, Output = Input> { /** * The Standard Schema properties. */ readonly '~standard': StandardSchemaV1.Props<Input, Output>; } declare namespace StandardSchemaV1 { /** * The Standard Schema properties interface. */ export interface Props<Input = unknown, Output = Input> { /** * Inferred types associated with the schema. */ readonly types?: Types<Input, Output> | undefined; /** * Validates unknown input values. */ readonly validate: (value: unknown) => Promise<Result<Output>> | Result<Output>; /** * The vendor name of the schema library. */ readonly vendor: string; /** * The version number of the standard. */ readonly version: 1; } /** * The result interface of the validate function. */ export type Result<Output> = FailureResult | SuccessResult<Output>; /** * The result interface if validation succeeds. */ export interface SuccessResult<Output> { /** * The non-existent issues. */ readonly issues?: undefined; /** * The typed output value. */ readonly value: Output; } /** * The result interface if validation fails. */ export interface FailureResult { /** * The issues of failed validation. */ readonly issues: ReadonlyArray<Issue>; } /** * The issue interface of the failure output. */ export interface Issue { /** * The error message of the issue. */ readonly message: string; /** * The path of the issue, if any. */ readonly path?: ReadonlyArray<PathSegment | PropertyKey> | undefined; } /** * The path segment interface of the issue. */ export interface PathSegment { /** * The key representing a path segment. */ readonly key: PropertyKey; } /** * The Standard Schema types interface. */ export interface Types<Input = unknown, Output = Input> { /** * The input type of the schema. */ readonly input: Input; /** * The output type of the schema. */ readonly output: Output; } /** * Infers the input type of a Standard Schema. */ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input']; /** * Infers the output type of a Standard Schema. */ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output']; export {}; } //#endregion //#region src/types/form.type.d.ts /** * Represents a form field. * * @typeparam TValue The type of the field value. * @typeparam TDefaultValue The type of the field default value. */ interface Field<TValue, TDefaultValue = undefined> { /** * The unique id of the field. */ '_id': string; /** * Indicates whether the field has been changed. * This flag will remain `true` even if the field value is set back to its initial value. */ 'isChanged': Ref<boolean>; /** * Indicates whether the field value is different from its initial value. */ 'isDirty': ComputedRef<boolean>; /** * Indicates whether the field has been touched (blurred). */ 'isTouched': ComputedRef<boolean>; /** * Indicates whether the field has any errors. */ 'isValid': ComputedRef<boolean>; /** * Internal flag to track if the field has been touched (blurred). */ '_isTouched': Ref<boolean>; /** * The current path of the field. This can change if fields are unregistered. */ '_path': ComputedRef<string | null>; /** * Blur the field and all it's children. */ 'blurAll': () => void; /** * The errors associated with the field and its children. */ 'errors': ComputedRef<FormattedError<TValue>[]>; /** * The current value of the field. */ 'modelValue': ComputedRef<TDefaultValue extends undefined ? TValue | null : TValue>; /** * The raw errors associated with the field and its children. */ 'rawErrors': ComputedRef<StandardSchemaV1.Issue[]>; 'register': <TValueAsFieldValues extends (TValue extends FieldValues ? TValue : never), TChildPath extends FieldPath<TValueAsFieldValues>, TChildDefaultValue extends FieldPathValue<TValueAsFieldValues, TChildPath> | undefined>(path: TChildPath, defaultValue?: TChildDefaultValue) => Field<FieldPathValue<TValueAsFieldValues, TChildPath>, TChildDefaultValue>; 'registerArray': <TPath extends (TValue extends FieldValues ? FieldPath<TValue> : never), TChildDefaultValue extends (TValue extends FieldValues ? FieldPathValue<TValue, TPath> | undefined : never)>(path: TPath, defaultValue?: TChildDefaultValue) => FieldArray<FieldPathValue<TValue, TPath> extends Array<any> ? FieldPathValue<TValue, TPath>[number] : never>; /** * Sets the current value of the field. * * This is an alias of `onUpdate:modelValue`. * * @param value The new value of the field. */ 'setValue': (value: TValue | null) => void; /** * The current value of the field. * * This is an alias of `attrs.modelValue`. */ 'value': ComputedRef<TDefaultValue extends undefined ? TValue | null : TValue>; /** * Called when the field input is blurred. */ 'onBlur': () => void; /** * Called when the field input value is changed. */ 'onChange': () => void; /** * Updates the current value of the field. * * @param value The new value of the field. */ 'onUpdate:modelValue': (value: TValue | null) => void; } /** * Represents a form field array. * * @typeparam T The type of the form schema. */ interface FieldArray<TValue> { /** * The unique id of the field. */ _id: string; /** * Indicates whether the field value is different from its initial value. */ isDirty: ComputedRef<boolean>; /** * Indicates whether the field has been touched (blurred). */ isTouched: ComputedRef<boolean>; /** * Indicates whether the field has any errors. */ isValid: ComputedRef<boolean>; /** * The current path of the field. This can change if fields are unregistered. */ _path: ComputedRef<string | null>; /** * Add a new field at the end of the array. */ append: (value?: TValue) => void; blurAll: () => void; /** * Empty the array. */ empty: () => void; /** * The errors associated with the field and its children. */ errors: ComputedRef<FormattedError<TValue[]>[]>; /** * Blur the field and all it's children. */ /** * Array of unique ids of the fields. */ fields: Ref<string[]>; /** * Insert a new field at the given index. * @param index The index of the field to insert. */ insert: (index: number, value?: TValue) => void; /** * The current value of the field. */ modelValue: ComputedRef<TValue[]>; /** * Move a field from one index to another. */ move: (from: number, to: number) => void; /** * Remove the last field of the array. */ pop: () => void; /** * Add a new field at the beginning of the array. */ prepend: (value?: TValue) => void; /** * The raw errors associated with the field and its children. */ rawErrors: ComputedRef<StandardSchemaV1.Issue[]>; register: <TChildPath extends (TValue[] extends FieldValues ? FieldPath<TValue[]> : never), TChildDefaultValue extends (TValue[] extends FieldValues ? FieldPathValue<TValue[], TChildPath> | undefined : never)>(path: TChildPath, defaultValue?: TChildDefaultValue) => TValue[] extends FieldValues ? Field<FieldPathValue<TValue[], TChildPath>, any> : never; registerArray: <TPath extends (TValue[] extends FieldValues ? FieldPath<TValue[]> : never), TArrayValue extends FieldPathValue<TValue[], TPath>, TChildDefaultValue extends (TValue[] extends FieldValues ? FieldPathValue<TValue[], TPath> | undefined : never), TSingleValue extends (TArrayValue extends Array<any> ? TArrayValue[number] : never)>(path: TPath, defaultValue?: TChildDefaultValue) => FieldArray<TSingleValue>; /** * Remove a field at the given index. * @param index The index of the field to remove. */ remove: (index: number) => void; /** * Set the current value of the field. */ setValue: (value: TValue[]) => void; /** * Remove the first field of the array. */ shift: () => void; /** * The current value of the field. * * This is an alias of `attrs.modelValue`. */ value: ComputedRef<TValue[]>; } type Register<TSchema> = <TPath extends FieldPath<TSchema>, TValue extends FieldPathValue<TSchema, TPath>, TDefaultValue extends FieldPathValue<TSchema, TPath> | undefined>(field: TPath, defaultValue?: TDefaultValue) => Field<TValue, TDefaultValue>; type RegisterArray<TSchema extends StandardSchemaV1> = <TPath extends FieldPath<StandardSchemaV1.InferOutput<TSchema>>, TValue extends FieldPathValue<StandardSchemaV1.InferOutput<TSchema>, TPath>, TSingleValue extends (TValue extends Array<any> ? TValue[number] : never), TDefaultValue extends FieldPathValue<StandardSchemaV1.InferOutput<TSchema>, TPath> | undefined>(field: TPath, defaultValue?: TDefaultValue) => FieldArray<TSingleValue>; type Unregister<T extends StandardSchemaV1> = <P extends FieldPath<StandardSchemaV1.InferOutput<T>>>(field: P) => void; interface Form<TSchema extends StandardSchemaV1> { /** * Internal id of the form, to track it in the devtools. */ _id: string; /** * Indicates whether the form has been attempted to submit. */ hasAttemptedToSubmit: ComputedRef<boolean>; /** * Indicates whether the form is dirty or not. * * A form is considered dirty if any of its fields have been changed. */ isDirty: ComputedRef<boolean>; /** * Indicates whether the form is currently submitting or not. */ isSubmitting: ComputedRef<boolean>; /** * Indicates whether the form is currently valid or not. * * A form is considered valid if all of its fields are valid. */ isValid: ComputedRef<boolean>; /** * Sets errors in the form. * * @param errors The new errors for the form fields. */ addErrors: (errors: FormattedError<StandardSchemaV1.InferOutput<TSchema>>[]) => void; /** * Blurs all inputs in the form. */ blurAll: () => void; /** * The collection of all registered fields' errors. */ errors: ComputedRef<FormattedError<StandardSchemaV1.InferOutput<TSchema>>[]>; /** * The raw errors associated with the field and its children. */ rawErrors: ComputedRef<StandardSchemaV1.Issue[]>; /** * Registers a new form field. * * @returns A `Field` instance that can be used to interact with the field. */ register: Register<StandardSchemaV1.InferOutput<TSchema>>; /** * Registers a new form field array. * * @returns A `FieldArray` instance that can be used to interact with the field array. */ registerArray: RegisterArray<TSchema>; /** * Resets the form to the initial state. */ reset: () => void; /** * Sets values in the form. * * @param values The new values for the form fields. */ setValues: (values: DeepPartial<StandardSchemaV1.InferOutput<TSchema>>) => void; /** * The current state of the form. */ state: ComputedRef<Readonly<DeepPartial<StandardSchemaV1.InferOutput<TSchema>>>>; /** * Submits the form. * * @returns A promise that resolves once the form has been successfully submitted. */ submit: () => Promise<void>; /** * Unregisters a previously registered field. * * @param path The path of the field to unregister. */ unregister: Unregister<TSchema>; } /** * Represents a form instance. * * @typeparam T The type of the form schema. */ interface UseForm<T extends StandardSchemaV1> { /** * The form instance itself. */ form: Form<T>; /** * Called when the form is valid and submitted. * @param cb - Callback invoked with the current form data when the form is valid and submitted. */ onSubmitForm: (cb: (data: StandardSchemaV1.InferOutput<T>) => void) => void; } interface FormattedError<TType> { message: string; path: FieldPath<TType extends FieldValues ? TType : never>; } //#endregion //#region src/lib/formatErrors.d.ts type SomeIssues<TType> = FormattedError<TType>[] | readonly StandardSchemaV1.Issue[]; declare function formatErrorsToZodFormattedError<TType>(issues: SomeIssues<TType>): ZodFormattedError<TType>; //#endregion //#region src/lib/useForm.d.ts interface UseFormOptions<TSchema extends StandardSchemaV1> { /** * The initial state of the form */ initialState?: MaybeRefOrGetter<NestedNullableKeys<StandardSchemaV1.InferOutput<TSchema>> | null>; /** * The zod schema of the form. */ schema: TSchema; /** * Called when the form is valid and submitted. * @param data The current form data. */ onSubmit: (data: StandardSchemaV1.InferOutput<TSchema>) => void; /** * Called when the form is attempted to be submitted, but is invalid. * Only called for client-side validation. */ onSubmitError?: ({ data, errors }: { data: DeepPartial<NestedNullableKeys<StandardSchemaV1.InferOutput<TSchema>>>; errors: FormattedError<StandardSchemaV1.InferOutput<TSchema>>[]; }) => void; } declare function useForm<TSchema extends StandardSchemaV1>({ initialState, schema, onSubmit, onSubmitError }: UseFormOptions<TSchema>): Form<TSchema>; //#endregion export { type Field, type FieldArray, type Form, type FormattedError, type UseForm, formatErrorsToZodFormattedError, useForm };