formango
Version:
```bash pnpm i formango ```
575 lines (574 loc) • 19.9 kB
text/typescript
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 };