@hookform/lenses
Version:
Type-safe lenses for React Hook Form that enable precise control over nested form state. Build reusable form components with composable operations, array handling, and full TypeScript support.
409 lines (399 loc) • 15 kB
text/typescript
import { FieldValues, Control, Path, PathValue, FieldArrayPath, FieldArrayWithId, FieldArray } from 'react-hook-form';
import { DependencyList } from 'react';
type LensesStorageComplexKey = (...args: any[]) => any;
interface LensesStorageValue<T extends FieldValues> {
plain?: LensCore<T>;
complex: WeakMap<LensesStorageComplexKey, LensCore<T>>;
}
type LensCache<T extends FieldValues> = Map<string, LensesStorageValue<T>>;
/**
* Cache storage for lenses.
*/
declare class LensesStorage<T extends FieldValues> {
private cache;
constructor(control: Control<T>);
get(path: string, complexKey?: LensesStorageComplexKey): LensCore<T> | undefined;
set(lens: LensCore<T>, path: string, complexKey?: LensesStorageComplexKey): void;
has(path: string, complexKey?: LensesStorageComplexKey): boolean;
delete(path: string): void;
clear(): void;
}
/**
* This is a trick to allow `control` to have typed `Control<T>` type.
* Because `Lens` doesn't have prop path in its types you can't use it with `Control` as is.
*
* For example this code is not valid:
*
* ```tsx
* function Test({ control }: { control: Control<string> }) {}
* ```
* To provide type checking we can simulate that `T` is an object with one field and then immediately use it:
*
* ```tsx
* function Test({ control }: { control: Control<{ __DO_NOT_USE_NON_OBJECT_FIELD_SHIM__: number }> }) {
* const { field } = useController({ control, name: '__DO_NOT_USE_NON_OBJECT_FIELD_SHIM__' });
* console.log(field.value); // field.value is number
* }
* ```
*
* This trick is needed for type checking. When you use `lens.interop()` it returns correct `name` in runtime.
*
* ```tsx
* function Test({ lens }: { lens: Lens<number> }) {
* const { field } = useController(lens.interop());
* console.log(field.value); // field.value is number
* }
* ```
*/
interface HookFormControlShim<T> {
__DO_NOT_USE_HOOK_FORM_CONTROL_SHIM__: T;
}
type ShimKeyName = keyof HookFormControlShim<unknown>;
interface LensInteropBinding<T extends FieldValues, Name> {
control: Control<T>;
name: Name;
}
interface LensInteropFunction<T extends FieldValues, Name, R> {
(control: LensInteropBinding<T, Name>['control'], name: LensInteropBinding<T, Name>['name']): R;
}
interface LensInterop<T> {
/**
* This method returns `name` and `control` properties from react-hook-form.
*
* @example
* ```tsx
* function App() {
* const { control, handleSubmit } = useForm<{
* firstName: string;
* }>();
*
* const lens = useLens({ control });
* const interop = lens.focus('firstName').interop();
*
* return (
* <form onSubmit={handleSubmit(console.log)}>
* <input {...interop.control.register(interop.name)} />
* <input type="submit" />
* </form>
* );
* }
* ```
*/
interop(): LensInteropBinding<HookFormControlShim<T>, ShimKeyName>;
/**
* This method allows you to use `control` and `name` properties from react-hook-form in a callback.
*
* @example
* ```tsx
* function App() {
* const { control, handleSubmit } = useForm<{
* firstName: string;
* }>();
*
* const lens = useLens({ control });
*
* return (
* <form onSubmit={handleSubmit(console.log)}>
* <input {...lens.focus('firstName').interop((ctrl, name) => ctrl.register(name))} />
* <input type="submit" />
* </form>
* );
* }
* ```
*/
interop<R>(callback: LensInteropFunction<HookFormControlShim<T>, ShimKeyName, R>): R;
}
interface ObjectLensGetter<T, R> {
/**
* An object lens restructures function.
*
* @param dictionary - It is a proxy which focuses a lens on property read.
* @param lens - Current object lens.
*/
(dictionary: LensesDictionary<T>, lens: Lens<T>): LensesGetter<R>;
}
interface ObjectLens<T> {
/**
* This method allows you to create a new lens that focuses on a specific field in the form.
*
* @param path - The path to the field in the form.
*
* @example
* ```tsx
* function App() {
* const { control, handleSubmit } = useForm<{
* firstName: string;
* some: { nested: { field: string } };
* arr: { value: string }[];
* }>();
*
* const lens = useLens({ control });
*
* return (
* <form onSubmit={handleSubmit(console.log)}>
* <StringInput lens={lens.focus('firstName')} />
* <StringInput lens={lens.focus('some.nested.field')} />
* <StringInput lens={lens.focus('arr.0.value')} />
* <input type="submit" />
* </form>
* );
* }
*
* function StringInput({ lens }: { lens: Lens<string> }) {
* return <input {...lens.interop((ctrl, name) => ctrl.register(name))} />;
* }
* ```
*/
focus<P extends Path<T>>(path: P): Lens<PathValue<T, P>>;
/**
* This method allows you to create a new lens with different shape.
*
* @param getter - A function that returns an object where each field is a lens.
*
* @example
* ```tsx
* function Component({ lens }: { lens: Lens<{ firstName: string; lastName: string; age: number }> }) {
* return (
* <SharedComponent
* lens={lens.reflect(({ firstName, lastName, ...rest }) => ({
* ...rest,
* name: firstName,
* surname: lastName,
* }))}
* />
* );
* }
* function SharedComponent({ lens }: { lens: Lens<{ name: string; surname: string; age: number }> }) {
* return (
* <div>
* <StringInput lens={lens.focus('name')} />
* <StringInput lens={lens.focus('surname')} />
* <NumberInput lens={lens.focus('age')} />
* </div>
* );
* }
* ```
*/
reflect<R>(getter: ObjectLensGetter<T, R>): Lens<UnwrapLens<R>>;
}
interface PrimitiveLensGetter<T, R> {
/**
* A primitive lens restructures function.
*
* @param dictionary - Since primitive values has no key-value pairs, the dictionary is always `never`.
* @param lens - Current primitive lens.
*/
(dictionary: never, lens: Lens<T>): LensesGetter<R>;
}
interface PrimitiveLens<T> {
/**
* This method allows you to create a new lens with different shape.
*
* @param getter - A function that returns an object where each field is a lens.
*
* @example
* ```tsx
* function Component({ lens }: { lens: Lens<string> }) {
* return <Inside lens={lens.reflect((_, l) => ({ data: l }))} />;
* }
*
* function Inside({ lens }: { lens: Lens<{ data: string }> }) {
* return <StringInput label="Data" lens={lens.focus('data')} />;
* }
* ```
*/
reflect<R>(getter: PrimitiveLensGetter<T, R>): Lens<UnwrapLens<R>>;
}
type LensSelector<T> = T extends any[] ? ArrayLens<T> : T extends FieldValues ? ObjectLens<T> : T extends boolean | true | false ? PrimitiveLens<boolean> : T extends null | undefined ? never : PrimitiveLens<T>;
/**
* This is a type that allows you to hold the type of a form element.
*
* ```ts
* type LensWithArray = Lens<string[]>;
* type LensWithObject = Lens<{ name: string; age: number }>;
* type LensWithPrimitive = Lens<string>;
* ```
*
* In runtime it has `control` and `name` to use latter in react-hook-form.
* Each time you do `lens.focus('propPath')` it creates a lens that keeps nesting of paths.
*/
type Lens<T> = LensInterop<Exclude<T, null | undefined>> & LensSelector<T>;
type LensesDictionary<T> = {
[P in keyof T]: Lens<T[P]>;
};
type LensesGetter<T> = LensesDictionary<T> | Lens<T>;
type UnwrapLens<T> = T extends HookFormControlShim<any> ? unknown : T extends (infer U)[] ? UnwrapLens<U>[] : T extends Lens<infer U> ? UnwrapLens<U> : T extends object ? {
[P in keyof T]: UnwrapLens<T[P]>;
} : T;
interface ArrayLensGetter<T, R> {
(dictionary: LensesDictionary<T>, lens: Lens<T>): [LensesGetter<R>];
}
interface ArrayLensMapper<T, L, R> {
(value: T, lens: Lens<L>, index: number, array: T[], origin: this): R;
}
/**
* Array item transformers for `useFieldArray` from @hookform/lenses.
*/
interface LensInteropTransformerBinding<TFieldValues extends FieldValues = FieldValues, TFieldArrayName extends FieldArrayPath<TFieldValues> = FieldArrayPath<TFieldValues>, TKeyName extends string = 'id'> extends LensInteropBinding<TFieldValues, ShimKeyName> {
getTransformer?: <R extends TFieldValues, N extends FieldArrayPath<R>>(value: FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>) => FieldArrayWithId<R, N, TKeyName>;
setTransformer?: <R extends FieldValues, N extends FieldArrayPath<R>>(value: FieldArray<R, N>) => FieldArrayWithId<TFieldValues, TFieldArrayName, TKeyName>;
}
interface ArrayLens<T extends any[]> {
/**
* This method allows you to create a new lens with specific path starting from the array item.
*
* @param path - The path to the field in the form.
*
* @example
* ```tsx
* function Component({ lens }: { lens: Lens<{ name: string }[]> }) {
* const firstName = lens.focus('0.name');
* const secondItem = lens.focus('1');
* // ...
* }
* ```
*/
focus<P extends Path<T>>(path: P): Lens<PathValue<T, P>>;
/** This method allows you to create a new lens that focuses on a specific array item.
*
* @param index - Array index to focus on.
*
* @example
* ```tsx
* function Component({ lens }: { lens: Lens<{ name: string }[]> }) {
* const thirdItem = lens.focus(2);
* /// ...
* }
* ```
*/
focus<P extends number>(index: P): Lens<T[P]>;
/**
* This method allows you to restructure the array item.
* Pay attention that this function must return an array with one item.
*
* @param getter - A function that returns an array with one object where each field is a lens.
*
* @example
* ```tsx
* function Component({
* lens,
* }: {
* lens: Lens<{
* items: {
* value: { inside: string };
* }[];
* }>;
* }) {
* return <Items lens={lens.focus('items').reflect(({ value }) => [{ data: value.focus('inside') }])} />;
* }
*
* function Items({ lens }: { lens: Lens<{ data: string }[]> }) {
* const { fields } = useFieldArray(lens.interop());
*
* return (
* <div>
* {lens.map(fields, (value, l) => (
* <div key={value.id}>
* <StringInput label="Value" lens={l.focus('data')} />
* </div>
* ))}
* </div>
* );
* }
* ```
*/
reflect<R>(getter: ArrayLensGetter<T[number], R>): Lens<UnwrapLens<R>[]>;
/**
* This method allows you to map an array lens.
* It requires the `fields` property from `useFieldArray`.
*
* @param fields - The `fields` property from `useFieldArray`.
* @param mapper - A function that will be called on each `fields` item.
*
* @example
* ```tsx
* function Component({ lens }: { lens: Lens<{ data: string }[]> }) {
* const { fields } = useFieldArray(lens.interop());
*
* return (
* <div>
* {lens.map(fields, (value, l) => (
* <div key={value.id}>
* <StringInput label="Value" lens={l.focus('data')} />
* </div>
* ))}
* </div>
* );
* }
* ```
*/
map<F extends T, R>(fields: F, mapper: ArrayLensMapper<F[number], T[number], R>): R[];
/**
* This method returns `name` and `control` properties from react-hook-form.
* The returned object must be passed to `useFieldArray` hook from `@hookform/lenses`.
*
* @example
* ```tsx
* import { useFieldArray } from '@hookform/lenses/rhf';
*
* function Component({ lens }: { lens: Lens<{ data: string }[]> }) {
* const { fields } = useFieldArray(lens.interop());
*
* return (
* <div>
* {lens.map(fields, (value, l) => (
* <div key={value.id}>
* <StringInput label="Value" lens={l.focus('data')} />
* </div>
* ))}
* </div>
* );
* }
* ```
*/
interop(): LensInteropTransformerBinding<HookFormControlShim<Exclude<T, null | undefined>>, ShimKeyName extends FieldArrayPath<HookFormControlShim<Exclude<T, null | undefined>>> ? ShimKeyName : never>;
}
interface LensCoreInteropBinding<T extends FieldValues> {
control: Control<T>;
name: string | undefined;
getTransformer?: (value: unknown) => unknown;
setTransformer?: (value: unknown) => unknown;
}
/**
* Runtime lens implementation.
*/
declare class LensCore<T extends FieldValues> {
control: Control<T>;
path: string;
cache?: LensesStorage<T> | undefined;
private isArrayItemReflection?;
private override?;
private interopCache?;
constructor(control: Control<T>, path: string, cache?: LensesStorage<T> | undefined);
static create<TFieldValues extends FieldValues = FieldValues>(control: Control<TFieldValues>, cache?: LensesStorage<TFieldValues>): Lens<TFieldValues>;
focus(prop: string | number): LensCore<T>;
reflect(getter: (dictionary: ProxyHandler<Record<string, LensCore<T>>>, lens: LensCore<T>) => Record<string, LensCore<T>> | [Record<string, LensCore<T>>]): LensCore<T>;
map<R>(fields: Record<string, any>[], mapper: (value: unknown, item: LensCore<T>, index: number, array: unknown[], lens: this) => R): R[];
interop(cb?: (control: Control<T>, name: string | undefined) => any): LensCoreInteropBinding<T> | undefined;
private getTransformer;
private setTransformer;
}
interface UseLensProps<TFieldValues extends FieldValues = FieldValues> {
control: Control<TFieldValues>;
}
/**
* Creates lens from react-hook-form control.
*
* @example
* ```tsx
* function App() {
* const { control } = useForm<{
* firstName: string;
* }>();
*
* const lens = useLens({ control });
* }
* ```
*/
declare function useLens<TFieldValues extends FieldValues = FieldValues>(props: UseLensProps<TFieldValues>, deps?: DependencyList): Lens<TFieldValues>;
export { type ArrayLens, type ArrayLensGetter, type ArrayLensMapper, type HookFormControlShim, type Lens, type LensCache, LensCore, type LensCoreInteropBinding, type LensInterop, type LensInteropBinding, type LensInteropFunction, type LensInteropTransformerBinding, type LensSelector, type LensesDictionary, type LensesGetter, LensesStorage, type LensesStorageComplexKey, type LensesStorageValue, type ObjectLens, type ObjectLensGetter, type PrimitiveLens, type PrimitiveLensGetter, type ShimKeyName, type UnwrapLens, type UseLensProps, useLens };