diff --git a/README.md b/README.md index 477a88b..8d0635d 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Let's start by creating a field: import { createField } from 'roqueform'; const field = createField(); -// ⮕ Field +// ⮕ Field ``` A value can be set to and retrieved from the field: @@ -85,7 +85,7 @@ Provide the initial value for a field: ```ts const ageField = createField(42); -// ⮕ Field +// ⮕ Field ageField.value; // ⮕ 42 @@ -103,7 +103,7 @@ interface Universe { } const universeField = createField(); -// ⮕ Field +// ⮕ Field universeField.value; // ⮕ undefined @@ -113,7 +113,7 @@ Retrieve a child field by its key: ```ts const planetsField = universeField.at('planets'); -// ⮕ Field +// ⮕ Field ``` `planetsField` is a child field, and it is linked to its parent `universeField`. @@ -126,7 +126,7 @@ planetsField.parent; // ⮕ universeField ``` -Fields returned by the [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#at) +Fields returned by the [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at) method have a stable identity. This means that you can invoke `at` with the same key multiple times and the same field instance is returned: @@ -142,7 +142,7 @@ The child field has all the same functionality as its parent, so you can access ```ts planetsField.at(0).at('name'); -// ⮕ Field +// ⮕ Field ``` When a value is set to a child field, a parent field value is also updated. If parent field doesn't have a value yet, @@ -186,14 +186,14 @@ const unsubscribe = planetsField.on('change:value', event => { // ⮕ () => void ``` -The [`Field.on`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#on) method +The [`Field.on`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on) method associates the event subscriber with an event type. All events that are dispatched onto fields have the share [`Event`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Event.html). Without plugins, fields can dispatch events with -[`change:value`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#on.on-2) type. This +[`change:value`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#on.on-2) type. This event is dispatched when the field value is changed via -[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue). +[`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue). Plugins may dispatch their own events. Here's an example of the [`change:errors`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.ErrorsPlugin.html#on.on-1) event @@ -234,7 +234,7 @@ planetsField.on('*', event => { # Transient updates -When you call [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue) +When you call [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue) on a field then subscribers of its ancestors and its updated child fields are triggered. To manually control the update propagation to fields ancestors, you can use transient updates. @@ -261,7 +261,7 @@ avatarField.at('eyeColor').isTransient; ``` To propagate the transient value contained by the child field to its parent, use the -[`Field.propagate`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#propagate) +[`Field.propagate`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#propagate) method: ```ts @@ -271,7 +271,7 @@ avatarField.value; // ⮕ { eyeColor: 'green' } ``` -[`Field.setTransientValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setTransientValue) +[`Field.setTransientValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setTransientValue) can be called multiple times, but only the most recent update is propagated to the parent field after the `propagate` call. @@ -308,12 +308,12 @@ planetsField.at(1).value; updates field values. - When the child field is accessed via - [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#at) method for the + [`Field.at`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#at) method for the first time, its value is read from the value of the parent field using the [`ValueAccessor.get`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#get) method. - When a field value is updated via - [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.FieldController.html#setValue), then + [`Field.setValue`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.BareField.html#setValue), then the parent field value is updated with the value returned from the [`ValueAccessor.set`](https://smikhalevski.github.io/roqueform/interfaces/roqueform.Accessor.html#set) method. If the updated field has child fields, their values are updated with values returned from the @@ -401,7 +401,7 @@ const planetField = createField( { name: 'Mars' }, injectElementPlugin ); -// ⮕ Field<{ element: Element | null }, { name: string }> +// ⮕ Field<{ name: string }, { element: Element | null }> planetField.element; // ⮕ null @@ -475,7 +475,7 @@ To combine multiple plugins into a single function, use the import { createField, composePlugins } from 'roqueform'; createField(['Mars'], composePlugins(plugin1, plugin2)); -// ⮕ Field<…, string[]> +// ⮕ Field ``` # Errors plugin diff --git a/packages/annotations-plugin/src/main/annotationsPlugin.ts b/packages/annotations-plugin/src/main/annotationsPlugin.ts index d845542..4c5c3dd 100644 --- a/packages/annotations-plugin/src/main/annotationsPlugin.ts +++ b/packages/annotations-plugin/src/main/annotationsPlugin.ts @@ -46,7 +46,7 @@ export interface AnnotationsPlugin { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: 'change:annotations', subscriber: Subscriber, Annotations>): Unsubscribe; + on(eventType: 'change:annotations', subscriber: Subscriber>): Unsubscribe; } /** @@ -97,7 +97,7 @@ function applyChanges(annotations: ReadonlyDict, patch: ReadonlyDict): ReadonlyD } function annotate( - field: Field>, + field: Field>, patch: ReadonlyDict | ((annotations: ReadonlyDict) => ReadonlyDict), applyPatch: (annotations: ReadonlyDict, patch: ReadonlyDict) => ReadonlyDict, options: AnnotateOptions | undefined, diff --git a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts index 27b4ec8..5b7cc50 100644 --- a/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts +++ b/packages/constraint-validation-plugin/src/main/constraintValidationPlugin.ts @@ -38,7 +38,7 @@ export interface ConstraintValidationPlugin { /** * Returns all invalid fields. */ - getInvalidFields(): Field>[]; + getInvalidFields(): Field>[]; /** * Subscribes to {@link validity the validity} changes of this field or any of its descendants. @@ -48,7 +48,7 @@ export interface ConstraintValidationPlugin { * @returns The callback to unsubscribe the subscriber. * @see {@link isInvalid} */ - on(eventType: 'change:validity', subscriber: Subscriber, ValidityState | null>): Unsubscribe; + on(eventType: 'change:validity', subscriber: Subscriber>): Unsubscribe; } /** @@ -118,7 +118,7 @@ export function constraintValidationPlugin(): PluginInjector): void { +function applyValidity(field: Field): void { const prevValidity = field.validity; const nextValidity = field.validatedElement !== null ? cloneValidity(field.validatedElement.validity) : null; @@ -129,7 +129,7 @@ function applyValidity(field: Field): void { } } -function reportValidity(field: Field): boolean { +function reportValidity(field: Field): boolean { if (field.children !== null) { for (const child of field.children) { if (!reportValidity(child)) { @@ -141,9 +141,9 @@ function reportValidity(field: Field): boolean { } function getInvalidFields( - field: Field, - batch: Field[] -): Field[] { + field: Field, + batch: Field[] +): Field[] { if (field.isInvalid) { batch.push(field); } diff --git a/packages/doubter-plugin/src/main/doubterPlugin.ts b/packages/doubter-plugin/src/main/doubterPlugin.ts index 467b080..5a90bf4 100644 --- a/packages/doubter-plugin/src/main/doubterPlugin.ts +++ b/packages/doubter-plugin/src/main/doubterPlugin.ts @@ -3,7 +3,7 @@ import { composePlugins, errorsPlugin, ErrorsPlugin, - FieldController, + Field, PluginInjector, Validation, ValidationPlugin, @@ -11,9 +11,9 @@ import { Validator, } from 'roqueform'; -export interface ValueShapePlugin { +export interface DoubterShapePlugin { /** - * The shape that Doubter uses to validate {@link FieldController.value the field value}, or `null` if there's no + * The shape that Doubter uses to validate {@link roqueform!BareField.value the field value}, or `null` if there's no * shape for this field. */ valueShape: Shape | null; @@ -31,7 +31,7 @@ export interface ValueShapePlugin { /** * The plugin added to fields by the {@link doubterPlugin}. */ -export type DoubterPlugin = ValidationPlugin & ErrorsPlugin & ValueShapePlugin; +export type DoubterPlugin = ValidationPlugin & ErrorsPlugin & DoubterShapePlugin; /** * Enhances fields with validation methods powered by [Doubter](https://github.com/smikhalevski/doubter#readme). @@ -40,10 +40,10 @@ export type DoubterPlugin = ValidationPlugin & ErrorsPlugin * @template Value The root field value. */ export function doubterPlugin(shape: Shape): PluginInjector { - return validationPlugin(composePlugins(errorsPlugin(concatErrors), valueShapePlugin(shape)), validator); + return validationPlugin(composePlugins(errorsPlugin(concatErrors), doubterShapePlugin(shape)), validator); } -function valueShapePlugin(rootShape: Shape): PluginInjector { +function doubterShapePlugin(rootShape: Shape): PluginInjector { return field => { field.valueShape = field.parentField === null ? rootShape : field.parentField.valueShape?.at(field.key) || null; @@ -92,7 +92,7 @@ function concatErrors(errors: readonly Issue[], error: Issue): readonly Issue[] return errors.concat(error); } -function prependPath(field: FieldController, issue: Issue): Issue { +function prependPath(field: Field, issue: Issue): Issue { for (; field.parentField !== null; field = field.parentField) { (issue.path ||= []).unshift(field.key); } diff --git a/packages/doubter-plugin/src/main/index.ts b/packages/doubter-plugin/src/main/index.ts index 4d826d6..2e7b31b 100644 --- a/packages/doubter-plugin/src/main/index.ts +++ b/packages/doubter-plugin/src/main/index.ts @@ -9,4 +9,4 @@ */ export { doubterPlugin } from './doubterPlugin'; -export type { DoubterPlugin, ValueShapePlugin } from './doubterPlugin'; +export type { DoubterPlugin, DoubterShapePlugin } from './doubterPlugin'; diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts index 2fd237f..d4e9ffa 100644 --- a/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts +++ b/packages/doubter-plugin/src/test/doubterPlugin.test-d.ts @@ -1,8 +1,13 @@ +import { annotationsPlugin, AnnotationsPlugin } from '@roqueform/annotations-plugin'; +import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin'; import * as d from 'doubter'; +import { composePlugins, createField, Field } from 'roqueform'; import { expectType } from 'tsd'; -import { createField, Field } from 'roqueform'; -import { DoubterPlugin, doubterPlugin } from '@roqueform/doubter-plugin'; const shape = d.object({ aaa: d.object({ bbb: d.string() }) }); -expectType>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape))); +expectType>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape))); + +expectType & DoubterPlugin>>( + createField({ aaa: { bbb: 'aaa' } }, composePlugins(doubterPlugin(shape), annotationsPlugin({ xxx: 'yyy' }))) +); diff --git a/packages/doubter-plugin/src/test/doubterPlugin.test.tsx b/packages/doubter-plugin/src/test/doubterPlugin.test.ts similarity index 100% rename from packages/doubter-plugin/src/test/doubterPlugin.test.tsx rename to packages/doubter-plugin/src/test/doubterPlugin.test.ts diff --git a/packages/react/src/main/FieldRenderer.ts b/packages/react/src/main/FieldRenderer.ts index 4e51e8d..934255e 100644 --- a/packages/react/src/main/FieldRenderer.ts +++ b/packages/react/src/main/FieldRenderer.ts @@ -8,23 +8,23 @@ import { useReducer, useRef, } from 'react'; -import { callOrGet, FieldController, ValueOf } from 'roqueform'; +import { callOrGet, Field, ValueOf } from 'roqueform'; /** * Properties of the {@link FieldRenderer} component. * * @template Field The rendered field. */ -export interface FieldRendererProps> { +export interface FieldRendererProps { /** * The field that triggers re-renders. */ - field: Field; + field: F; /** * The render function that receive a rendered field as an argument. */ - children: (field: Field) => ReactNode; + children: (field: F) => ReactNode; /** * If set to `true` then {@link FieldRenderer} is re-rendered whenever the {@link field} itself, its parent fields or @@ -40,7 +40,7 @@ export interface FieldRendererProps> { * * @param value The new field value. */ - onChange?: (value: ValueOf) => void; + onChange?: (value: ValueOf) => void; } /** @@ -49,11 +49,11 @@ export interface FieldRendererProps> { * * @template Field The rendered field. */ -export function FieldRenderer>(props: FieldRendererProps): ReactElement { +export function FieldRenderer(props: FieldRendererProps): ReactElement { const { field, eagerlyUpdated } = props; const [, rerender] = useReducer(reduceCount, 0); - const handleChangeRef = useRef['onChange']>(); + const handleChangeRef = useRef['onChange']>(); handleChangeRef.current = props.onChange; diff --git a/packages/react/src/main/useField.ts b/packages/react/src/main/useField.ts index 61e7367..2b39416 100644 --- a/packages/react/src/main/useField.ts +++ b/packages/react/src/main/useField.ts @@ -3,7 +3,7 @@ import { callOrGet, createField, Field, PluginInjector } from 'roqueform'; import { ValueAccessorContext } from './ValueAccessorContext'; // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types -type NoInfer = T extends infer T ? T : never; +type NoInfer = [T][T extends any ? 0 : never]; /** * Creates the new field. @@ -22,6 +22,20 @@ export function useField(): Field; */ export function useField(initialValue: Value | (() => Value)): Field; +/** + * Creates the new field enhanced by a plugin. + * + * @param initialValue The initial value assigned to the field. + * @param plugin The plugin injector that enhances the field. + * @returns The {@link Field} instance. + * @template Value The root field value. + * @template Plugin The plugin injected into the field. + */ +export function useField( + initialValue: Value | (() => Value), + plugin: PluginInjector +): Field; + /** * Creates the new field enhanced by a plugin. * @@ -34,7 +48,7 @@ export function useField(initialValue: Value | (() => Value)): Field( initialValue: Value | (() => Value), plugin: PluginInjector> -): Field; +): Field; export function useField(initialValue?: unknown, plugin?: PluginInjector) { const accessor = useContext(ValueAccessorContext); diff --git a/packages/reset-plugin/src/main/resetPlugin.ts b/packages/reset-plugin/src/main/resetPlugin.ts index 21a0bf6..eb19ba8 100644 --- a/packages/reset-plugin/src/main/resetPlugin.ts +++ b/packages/reset-plugin/src/main/resetPlugin.ts @@ -1,3 +1,4 @@ +import isDeepEqual from 'fast-deep-equal'; import { dispatchEvents, Event, @@ -9,7 +10,6 @@ import { Unsubscribe, ValueOf, } from 'roqueform'; -import isDeepEqual from 'fast-deep-equal'; /** * The plugin added to fields by the {@link resetPlugin}. @@ -33,21 +33,21 @@ export interface ResetPlugin { reset(): void; /** - * Returns all fields that have {@link roqueform!FieldController.value a value} that is different from - * {@link roqueform!FieldController.initialValue an initial value}. + * Returns all fields that have {@link roqueform!BareField.value a value} that is different from + * {@link roqueform!BareField.initialValue an initial value}. * * @see {@link isDirty} */ - getDirtyFields(): Field>[]; + getDirtyFields(): Field>[]; /** - * Subscribes to changes of {@link roqueform!FieldController.initialValue the initial value}. + * Subscribes to changes of {@link roqueform!BareField.initialValue the initial value}. * * @param eventType The type of the event. * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: 'change:initialValue', subscriber: Subscriber>): Unsubscribe; + on(eventType: 'change:initialValue', subscriber: Subscriber>): Unsubscribe; } /** @@ -77,7 +77,7 @@ export function resetPlugin( }; } -function setInitialValue(field: Field, initialValue: unknown): void { +function setInitialValue(field: Field, initialValue: unknown): void { if (isEqual(field.initialValue, initialValue)) { return; } @@ -93,8 +93,8 @@ function setInitialValue(field: Field, initialValue: unknown): void } function propagateInitialValue( - originField: Field, - targetField: Field, + originField: Field, + targetField: Field, initialValue: unknown, events: Event[] ): Event[] { @@ -114,7 +114,10 @@ function propagateInitialValue( return events; } -function getDirtyFields(field: Field, batch: Field[]): Field[] { +function getDirtyFields( + field: Field, + batch: Field[] +): Field[] { if (field.isDirty) { batch.push(field); } diff --git a/packages/roqueform/src/main/composePlugins.ts b/packages/roqueform/src/main/composePlugins.ts index 0bf738c..956d6b4 100644 --- a/packages/roqueform/src/main/composePlugins.ts +++ b/packages/roqueform/src/main/composePlugins.ts @@ -3,55 +3,58 @@ import type { PluginInjector } from './types'; /** * @internal */ -export function composePlugins(a: PluginInjector): PluginInjector; +export function composePlugins(a: PluginInjector): PluginInjector; /** * @internal */ -export function composePlugins(a: PluginInjector, b: PluginInjector): PluginInjector; +export function composePlugins( + a: PluginInjector, + b: PluginInjector +): PluginInjector; /** * @internal */ -export function composePlugins( - a: PluginInjector, - b: PluginInjector, - c: PluginInjector -): PluginInjector; +export function composePlugins( + a: PluginInjector, + b: PluginInjector, + c: PluginInjector +): PluginInjector; /** * @internal */ -export function composePlugins( - a: PluginInjector, - b: PluginInjector, - c: PluginInjector, - d: PluginInjector -): PluginInjector; +export function composePlugins( + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector +): PluginInjector; /** * @internal */ -export function composePlugins( - a: PluginInjector, - b: PluginInjector, - c: PluginInjector, - d: PluginInjector, - e: PluginInjector -): PluginInjector; +export function composePlugins( + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector, + e: PluginInjector +): PluginInjector; /** * @internal */ -export function composePlugins( - a: PluginInjector, - b: PluginInjector, - c: PluginInjector, - d: PluginInjector, - e: PluginInjector, - f: PluginInjector, - ...other: PluginInjector[] -): PluginInjector; +export function composePlugins( + a: PluginInjector, + b: PluginInjector, + c: PluginInjector, + d: PluginInjector, + e: PluginInjector, + f: PluginInjector, + ...other: PluginInjector[] +): PluginInjector; /** * Composes multiple plugin callbacks into a single callback. diff --git a/packages/roqueform/src/main/createField.ts b/packages/roqueform/src/main/createField.ts index 5cd357a..e3a5444 100644 --- a/packages/roqueform/src/main/createField.ts +++ b/packages/roqueform/src/main/createField.ts @@ -1,13 +1,13 @@ +import { naturalValueAccessor } from './naturalValueAccessor'; import type { __PLUGIN__, Event, Field, NoInfer, PluginInjector, ValueAccessor } from './types'; import { callOrGet, dispatchEvents, isEqual } from './utils'; -import { naturalValueAccessor } from './naturalValueAccessor'; /** * Creates the new field instance. * * @template Value The root field value. */ -export function createField(): Field; +export function createField(): Field; /** * Creates the new field instance. @@ -16,7 +16,22 @@ export function createField(): Field; * @param accessor Resolves values for child fields. * @template Value The root field value. */ -export function createField(initialValue: Value, accessor?: ValueAccessor): Field; +export function createField(initialValue: Value, accessor?: ValueAccessor): Field; + +/** + * Creates the new field instance. + * + * @param initialValue The initial value assigned to the field. + * @param plugin The plugin injector that enhances the field. + * @param accessor Resolves values for child fields. + * @template Value The root field initial value. + * @template Plugin The plugin injected into the field. + */ +export function createField( + initialValue: Value, + plugin: PluginInjector, + accessor?: ValueAccessor +): Field; /** * Creates the new field instance. @@ -31,7 +46,7 @@ export function createField( initialValue: Value, plugin: PluginInjector>, accessor?: ValueAccessor -): Field; +): Field; export function createField(initialValue?: unknown, plugin?: PluginInjector | ValueAccessor, accessor?: ValueAccessor) { if (typeof plugin !== 'function') { diff --git a/packages/roqueform/src/main/errorsPlugin.ts b/packages/roqueform/src/main/errorsPlugin.ts index 21cf24a..3ffcfea 100644 --- a/packages/roqueform/src/main/errorsPlugin.ts +++ b/packages/roqueform/src/main/errorsPlugin.ts @@ -53,7 +53,7 @@ export interface ErrorsPlugin { /** * Returns all fields that have associated errors. */ - getInvalidFields(): Field>[]; + getInvalidFields(): Field>[]; /** * Subscribes to {@link errors an associated error} changes. @@ -64,7 +64,7 @@ export interface ErrorsPlugin { * @see {@link errors} * @see {@link isInvalid} */ - on(eventType: 'change:errors', subscriber: Subscriber, Error[]>): Unsubscribe; + on(eventType: 'change:errors', subscriber: Subscriber>): Unsubscribe; } /** @@ -135,7 +135,11 @@ function concatUniqueErrors(errors: readonly T[], error: T): readonly T[] { return errors; } -function clearErrors(field: Field, options: ClearErrorsOptions | undefined, events: Event[]): Event[] { +function clearErrors( + field: Field, + options: ClearErrorsOptions | undefined, + events: Event[] +): Event[] { const prevErrors = field.errors; if (prevErrors.length !== 0) { @@ -150,7 +154,10 @@ function clearErrors(field: Field, options: ClearErrorsOptions | u return events; } -function getInvalidFields(field: Field, batch: Field[]): Field[] { +function getInvalidFields( + field: Field, + batch: Field[] +): Field[] { if (field.isInvalid) { batch.push(field); } diff --git a/packages/roqueform/src/main/index.ts b/packages/roqueform/src/main/index.ts index 3dee3c4..383b598 100644 --- a/packages/roqueform/src/main/index.ts +++ b/packages/roqueform/src/main/index.ts @@ -16,13 +16,13 @@ export { validationPlugin } from './validationPlugin'; export type { ClearErrorsOptions, ErrorsPlugin } from './errorsPlugin'; export type { Validator, Validation, ValidationPlugin } from './validationPlugin'; export type { - Field, + BareField, Event, + Field, + PluginInjector, + PluginOf, Subscriber, Unsubscribe, - PluginOf, - ValueOf, - FieldController, - PluginInjector, ValueAccessor, + ValueOf, } from './types'; diff --git a/packages/roqueform/src/main/types.ts b/packages/roqueform/src/main/types.ts index 359b347..3d156fd 100644 --- a/packages/roqueform/src/main/types.ts +++ b/packages/roqueform/src/main/types.ts @@ -1,83 +1,19 @@ /** - * The field that manages a value and associated data. Fields can be {@link PluginInjector enhanced by plugins} that + * The field that manages a value and an associated data. Fields can be {@link PluginInjector enhanced by plugins} that * provide integration with rendering frameworks, validation libraries, and other tools. * - * @template Plugin The plugin injected into the field. * @template Value The field value. - */ -export type Field = FieldController & Plugin; - -/** - * The event dispatched to subscribers of {@link Field a field}. - * - * @template Plugin The plugin injected into the field. - * @template Data The additional data related to the event. - */ -export interface Event { - /** - * The type of the event. - */ - type: string; - - /** - * The field onto which this event was dispatched. - */ - targetField: Field; - - /** - * The field that caused this event to be dispatched onto the {@link targetField}. - * - * For example, if a child field value is changed and causes the change event to be dispatched for the parent field - * as well, then the origin field is the child field for both change events. - */ - originField: Field; - - /** - * The {@link type type-specific} data related to the {@link targetField}. - */ - data: Data; -} - -/** - * The callback that receives events dispatched by a {@link Field}. - * - * @param event The dispatched event. * @template Plugin The plugin injected into the field. - * @template Data The additional data related to the event. - */ -export type Subscriber = (event: Event) => void; - -/** - * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. - */ -export type Unsubscribe = () => void; - -/** - * Infers plugins that were injected into a field - * - * Use `PluginOf` in plugin interfaces to infer all plugin interfaces that were intersected with the field - * controller. - * - * @template T The field to infer plugin of. */ -export type PluginOf = __PLUGIN__ extends keyof T ? T[__PLUGIN__] : unknown; +export type Field = BareField> & PreferUnknown; /** - * Infers the value of the field. - * - * Use `ValueOf` in plugin interfaces to infer the value of the current field. - * - * @template T The field to infer value of. - */ -export type ValueOf = 'value' extends keyof T ? T['value'] : unknown; - -/** - * The field controller provides the core field functionality. + * The bare field provides the core field functionality. * - * @template Plugin The plugin injected into the field. * @template Value The field value. + * @template Plugin The plugin injected into the field. */ -export interface FieldController { +export interface BareField { /** * Holds the plugin type for inference. * @@ -113,22 +49,22 @@ export interface FieldController { /** * The root field. */ - rootField: Field; + rootField: Field; /** * The parent field, or `null` if this is the root field. */ - parentField: Field | null; + parentField: Field | null; /** * The array of child fields that were {@link at previously accessed}, or `null` if there are no children. */ - children: Field[] | null; + children: Field[] | null; /** * The map from an event type to an array of associated subscribers. */ - subscribers: { [eventType: string]: Subscriber[] }; + subscribers: { [eventType: string]: Subscriber[] }; /** * The accessor that reads values of child fields from {@link Field.value the value of this field}, and updates the @@ -170,7 +106,7 @@ export interface FieldController { * @returns The child field instance. * @template Key The key in the value of this field. */ - at>(key: Key): Field>; + at>(key: Key): Field, Plugin>; /** * Subscribes to all events. @@ -179,7 +115,7 @@ export interface FieldController { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: '*', subscriber: Subscriber): Unsubscribe; + on(eventType: '*', subscriber: Subscriber): Unsubscribe; /** * Subscribes to {@link value the field value} changes. {@link Event.data} contains the previous field value. @@ -188,9 +124,72 @@ export interface FieldController { * @param subscriber The subscriber that would be triggered. * @returns The callback to unsubscribe the subscriber. */ - on(eventType: 'change:value', subscriber: Subscriber): Unsubscribe; + on(eventType: 'change:value', subscriber: Subscriber): Unsubscribe; } +/** + * The event dispatched to subscribers of {@link Field a field}. + * + * @template Data The additional data related to the event. + * @template Plugin The plugin injected into the field. + */ +export interface Event { + /** + * The type of the event. + */ + type: string; + + /** + * The field onto which this event was dispatched. + */ + targetField: Field; + + /** + * The field that caused this event to be dispatched onto the {@link targetField}. + * + * For example, if a child field value is changed and causes the change event to be dispatched for the parent field + * as well, then the origin field is the child field for both change events. + */ + originField: Field; + + /** + * The {@link type type-specific} data related to the {@link targetField}. + */ + data: Data; +} + +/** + * The callback that receives events dispatched by a {@link Field}. + * + * @param event The dispatched event. + * @template Data The additional data related to the event. + * @template Plugin The plugin injected into the field. + */ +export type Subscriber = (event: Event) => void; + +/** + * Unsubscribes the subscriber. No-op if subscriber was already unsubscribed. + */ +export type Unsubscribe = () => void; + +/** + * Infers plugins that were injected into a field + * + * Use `PluginOf` in plugin interfaces to infer all plugin interfaces that were intersected with the bare field. + * + * @template T The field to infer plugin of. + */ +export type PluginOf = __PLUGIN__ extends keyof T ? T[__PLUGIN__] : any; + +/** + * Infers the value of the field. + * + * Use `ValueOf` in plugin interfaces to infer the value of the current field. + * + * @template T The field to infer value of. + */ +export type ValueOf = 'value' extends keyof T ? T['value'] : any; + /** * The callback that enhances the field with a plugin. Injector should _mutate_ the passed field instance. * @@ -198,7 +197,7 @@ export interface FieldController { * @template Plugin The plugin injected into the field. * @template Value The root field value. */ -export type PluginInjector = (field: Field) => void; +export type PluginInjector = (field: Field, Plugin>) => void; /** * The abstraction used by the {@link Field} to read and write object properties. @@ -224,10 +223,7 @@ export interface ValueAccessor { set(obj: any, key: any, value: any): any; } -// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types -export type NoInfer = T extends infer T ? T : never; - -export declare const __PLUGIN__: unique symbol; +declare const __PLUGIN__: unique symbol; export type __PLUGIN__ = typeof __PLUGIN__; @@ -248,25 +244,47 @@ type Primitive = | undefined | null; +type OpaqueObject = WeakSet; + /** * The union of all keys of `T`, or `never` if keys cannot be extracted. */ // prettier-ignore -type KeyOf = +export type KeyOf = T extends Primitive ? never : T extends { set(key: any, value: any): any, get(key: infer K): any } ? K : T extends { add(value: any): any, [Symbol.iterator]: Function } ? number : T extends readonly any[] ? number : + T extends OpaqueObject ? never : T extends object ? keyof T : never /** - * The value that corresponds to a `Key` in an object `T`, or `never` if there's no such key. + * The value that corresponds to a `Key` in an object `T`, or `undefined` if there's no such key. */ // prettier-ignore -type ValueAt = +export type ValueAt = + T extends Primitive ? undefined : T extends { set(key: any, value: any): any, get(key: infer K): infer V } ? Key extends K ? V : undefined : T extends { add(value: infer V): any, [Symbol.iterator]: Function } ? Key extends number ? V | undefined : undefined : T extends readonly any[] ? Key extends number ? T[Key] : undefined : + T extends OpaqueObject ? undefined : Key extends keyof T ? T[Key] : undefined + +/** + * Replaces `any` with `unknown`. + */ +export type PreferUnknown = 0 extends 1 & T ? unknown : T; + +/** + * Replaces `unknown` with `any`. + */ +export type PreferAny = unknown extends T ? any : T; + +/** + * Poor Man's NoInfer polyfill. + */ +// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types +// https://devblogs.microsoft.com/typescript/announcing-typescript-5-4-beta/#the-noinfer-utility-type +export type NoInfer = [T][T extends any ? 0 : never]; diff --git a/packages/roqueform/src/main/utils.ts b/packages/roqueform/src/main/utils.ts index 92b97e0..43ecefc 100644 --- a/packages/roqueform/src/main/utils.ts +++ b/packages/roqueform/src/main/utils.ts @@ -1,4 +1,4 @@ -import type { Event, FieldController } from './types'; +import type { Event, Field } from './types'; /** * [SameValueZero](https://262.ecma-international.org/7.0/#sec-samevaluezero) comparison operation. @@ -46,7 +46,7 @@ export function dispatchEvents(events: readonly Event[]): void { continue; } - for (let ancestor: FieldController | null = event.targetField; ancestor !== null; ancestor = ancestor.parentField) { + for (let ancestor: Field | null = event.targetField; ancestor !== null; ancestor = ancestor.parentField) { const subscribers1 = ancestor.subscribers[event.type]; const subscribers2 = ancestor.subscribers['*']; diff --git a/packages/roqueform/src/main/validationPlugin.ts b/packages/roqueform/src/main/validationPlugin.ts index 5873044..2582372 100644 --- a/packages/roqueform/src/main/validationPlugin.ts +++ b/packages/roqueform/src/main/validationPlugin.ts @@ -70,7 +70,7 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:start', subscriber: Subscriber, Validation>>): Unsubscribe; + on(eventType: 'validation:start', subscriber: Subscriber>, PluginOf>): Unsubscribe; /** * Subscribes to the end of the validation. Check {@link isInvalid} to detect the actual validity status. @@ -82,7 +82,7 @@ export interface ValidationPlugin { * @see {@link validation} * @see {@link isValidating} */ - on(eventType: 'validation:end', subscriber: Subscriber, Validation>>): Unsubscribe; + on(eventType: 'validation:end', subscriber: Subscriber>, PluginOf>): Unsubscribe; } /** @@ -94,7 +94,7 @@ export interface Validation { /** * The field where the validation was triggered. */ - rootField: Field; + rootField: Field; /** * The abort controller associated with the pending {@link Validator.validateAsync async validation}, or `null` if @@ -118,7 +118,7 @@ export interface Validator { * @param field The field where {@link ValidationPlugin.validate} was called. * @param options The options passed to the {@link ValidationPlugin.validate} method. */ - validate?(field: Field, options: Options | undefined): void; + validate?(field: Field, options: Options | undefined): void; /** * Applies validation rules to a field. If this callback is omitted, then {@link Validator.validate} would be called @@ -130,7 +130,7 @@ export interface Validator { * @param field The field where {@link ValidationPlugin.validateAsync} was called. * @param options The options passed to the {@link ValidationPlugin.validateAsync} method. */ - validateAsync?(field: Field, options: Options | undefined): Promise; + validateAsync?(field: Field, options: Options | undefined): Promise; } /** @@ -159,10 +159,10 @@ export function validationPlugin( * @template Plugin The plugin that is available inside a validator. * @template Options Options passed to the validator. */ -export function validationPlugin( - plugin: PluginInjector, +export function validationPlugin( + plugin: PluginInjector, validator: Validator & NoInfer> -): PluginInjector & Plugin>; +): PluginInjector & Plugin, Value>; export function validationPlugin( plugin: Validator | PluginInjector | undefined, @@ -194,7 +194,7 @@ export function validationPlugin( }; } -function containsInvalid(field: Field): boolean { +function containsInvalid(field: Field): boolean { if (field.isInvalid) { return true; } @@ -208,7 +208,7 @@ function containsInvalid(field: Field): boolean { return false; } -function startValidation(field: Field, validation: Validation, events: Event[]): Event[] { +function startValidation(field: Field, validation: Validation, events: Event[]): Event[] { field.validation = validation; events.push({ type: 'validation:start', targetField: field, originField: validation.rootField, data: validation }); @@ -223,7 +223,7 @@ function startValidation(field: Field, validation: Validation, return events; } -function endValidation(field: Field, validation: Validation, events: Event[]): Event[] { +function endValidation(field: Field, validation: Validation, events: Event[]): Event[] { if (field.validation !== validation) { return events; } @@ -240,7 +240,7 @@ function endValidation(field: Field, validation: Validation, e return events; } -function abortValidation(field: Field, events: Event[]): Event[] { +function abortValidation(field: Field, events: Event[]): Event[] { const { validation } = field; if (validation !== null) { @@ -250,7 +250,7 @@ function abortValidation(field: Field, events: Event[]): Event return events; } -function validate(field: Field, options: unknown): boolean { +function validate(field: Field, options: unknown): boolean { const { validate } = field.validator; if (validate === undefined) { @@ -290,7 +290,7 @@ function validate(field: Field, options: unknown): boolean { return !containsInvalid(field); } -function validateAsync(field: Field, options: unknown): Promise { +function validateAsync(field: Field, options: unknown): Promise { return new Promise((resolve, reject) => { const validateAsync = field.validator.validateAsync || field.validator.validate; diff --git a/packages/roqueform/src/test/composePlugins.test-d.ts b/packages/roqueform/src/test/composePlugins.test-d.ts index 9753e5a..3e82809 100644 --- a/packages/roqueform/src/test/composePlugins.test-d.ts +++ b/packages/roqueform/src/test/composePlugins.test-d.ts @@ -1,13 +1,48 @@ -import { expectType } from 'tsd'; import { composePlugins, createField, PluginInjector } from 'roqueform'; +import { expectNotAssignable, expectType } from 'tsd'; -declare const plugin1: PluginInjector<{ xxx: number }>; -declare const plugin2: PluginInjector<{ yyy: boolean }>; +interface Aaa { + aaa: string; +} -expectType>(composePlugins(plugin1, plugin2)); +interface Bbb { + bbb: number; +} -const field = createField({ aaa: 111 }, composePlugins(plugin1, plugin2)); +interface Ccc { + ccc: number; +} -expectType<{ aaa: number }>(field.value); -expectType(field.xxx); -expectType(field.yyy); +declare const plugin1: PluginInjector; +declare const plugin2: PluginInjector; +declare const plugin3: PluginInjector; + +expectNotAssignable({} as PluginInjector); + +expectNotAssignable({} as PluginInjector); + +expectNotAssignable({} as PluginInjector); + +expectType(composePlugins(plugin1)); + +expectType(composePlugins(plugin1, plugin1)); + +expectType>(composePlugins(plugin2, plugin2, plugin2, plugin2, plugin2, plugin2, plugin2, plugin2)); + +expectType>(composePlugins(plugin1, plugin2)); + +expectType>(composePlugins(plugin1, plugin3)); + +expectType>(composePlugins(plugin2, plugin3)); + +expectType>(composePlugins(plugin3, plugin2)); + +expectNotAssignable>(composePlugins(plugin2, plugin3)); + +expectNotAssignable>(composePlugins(plugin2, plugin3)); + +const field = createField({ ccc: 111 }, composePlugins(plugin2, plugin3)); + +expectType(field.value); +expectType(field.aaa); +expectType(field.bbb); diff --git a/packages/roqueform/src/test/errorsPlugin.test.tsx b/packages/roqueform/src/test/errorsPlugin.test.ts similarity index 100% rename from packages/roqueform/src/test/errorsPlugin.test.tsx rename to packages/roqueform/src/test/errorsPlugin.test.ts diff --git a/packages/roqueform/src/test/types.test-d.ts b/packages/roqueform/src/test/types.test-d.ts new file mode 100644 index 0000000..d54b547 --- /dev/null +++ b/packages/roqueform/src/test/types.test-d.ts @@ -0,0 +1,155 @@ +import type { Event, Field, Subscriber } from 'roqueform'; +import { expectAssignable, expectNotAssignable, expectNotType, expectType } from 'tsd'; +import type { KeyOf, PreferUnknown, ValueAt } from '../main/types'; + +interface Aaa { + aaa: string; +} + +interface Bbb { + bbb: number; +} + +// Field + +expectNotType({} as Field); + +expectNotType({} as Field); + +expectAssignable>({} as Field); + +expectAssignable({} as Field); + +expectAssignable({} as Field); + +expectAssignable>({} as Field); + +expectAssignable>({} as Field); + +expectNotAssignable>({} as Field); + +expectNotAssignable>({} as Field); + +expectNotAssignable>({} as Field); + +// Event + +expectAssignable>({} as Event); + +expectAssignable({} as Event); + +expectAssignable({} as Event); + +expectAssignable>({} as Event); + +expectAssignable>({} as Event); + +// why? +expectAssignable>({} as Event); + +// why? +expectAssignable>({} as Event); + +expectNotAssignable>({} as Event); + +// Subscriber + +expectAssignable>({} as Subscriber); + +expectAssignable({} as Subscriber); + +expectAssignable({} as Subscriber); + +expectAssignable>({} as Subscriber); + +expectAssignable>({} as Subscriber); + +// why? +expectAssignable>({} as Subscriber); + +// why? +expectAssignable>({} as Subscriber); + +expectNotAssignable>({} as Subscriber); + +// KeyOf + +expectType({} as KeyOf<'aaa'>); + +expectType({} as KeyOf<111>); + +expectType({} as KeyOf); + +expectType({} as KeyOf); + +expectType({} as KeyOf>); + +expectType({} as KeyOf>); + +expectType({} as KeyOf); + +expectType<'aaa' | 'bbb'>({} as KeyOf<{ aaa: string; bbb: number }>); + +expectType<'aaa' | 'bbb'>({} as KeyOf); + +expectType({} as KeyOf>); + +expectType<111 | 222>({} as KeyOf>); + +expectType({} as KeyOf<{ set(key: any, value: any): any; get(key: string): any }>); + +expectType({} as KeyOf<{ add(value: any): any; [Symbol.iterator]: Function }>); + +// ValueAt + +expectType(undefined as ValueAt<'aaa', 'toString'>); + +expectType(undefined as ValueAt<111, 'toString'>); + +expectType(undefined as ValueAt); + +expectType(undefined as ValueAt); + +expectType(undefined as ValueAt, Aaa>); + +expectType(undefined as ValueAt, 'add'>); + +expectType({} as ValueAt, Aaa>); + +expectType(undefined as ValueAt, Bbb>); + +expectType({} as ValueAt); + +expectType(undefined as ValueAt); + +expectType({} as ValueAt<{ aaa: string; bbb: number }, 'aaa'>); + +expectType({} as ValueAt<{ aaa: string } & { bbb: number }, 'bbb'>); + +expectType({} as ValueAt<{ aaa: number } | { aaa: boolean; bbb: string }, 'aaa'>); + +expectType(undefined as ValueAt<{ aaa: number } | { aaa: boolean; bbb: string }, 'bbb'>); + +expectType(undefined as ValueAt<{ aaa: string; bbb: number }, 'xxx'>); + +expectType({} as ValueAt, 111>); + +expectType(undefined as ValueAt, 'aaa'>); + +expectType({} as ValueAt, 111>); + +expectType(undefined as ValueAt, 333>); + +expectType({} as ValueAt<{ set(key: any, value: any): any; get(key: string): Aaa }, 'aaa'>); + +expectType({} as ValueAt<{ add(value: Aaa): any; [Symbol.iterator]: Function }, 111>); + +expectType(undefined as ValueAt<{ add(value: Aaa): any; [Symbol.iterator]: Function }, 'aaa'>); + +// PreferUnknown + +expectType({} as PreferUnknown); + +expectType({} as PreferUnknown); + +expectType({} as PreferUnknown); diff --git a/packages/roqueform/src/test/validationPlugin.test.tsx b/packages/roqueform/src/test/validationPlugin.test.ts similarity index 100% rename from packages/roqueform/src/test/validationPlugin.test.tsx rename to packages/roqueform/src/test/validationPlugin.test.ts diff --git a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts index 3ea3307..263a8b3 100644 --- a/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts +++ b/packages/scroll-to-error-plugin/src/main/scrollToErrorPlugin.ts @@ -40,7 +40,7 @@ export interface ScrollToErrorPlugin { * ancestor. * @returns The field which is scrolled to, or `null` if there's no scroll happening. */ - scrollToError(index?: number, alignToTop?: boolean): Field> | null; + scrollToError(index?: number, alignToTop?: boolean): Field> | null; /** * Scroll to the element that is referenced by an invalid field. Scrolls the field element's ancestor containers such @@ -54,7 +54,7 @@ export interface ScrollToErrorPlugin { * @param options The [scroll options](https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView#sect1). * @returns The field which is scrolled to, or `null` if there's no scroll happening. */ - scrollToError(index?: number, options?: ScrollToErrorOptions): Field> | null; + scrollToError(index?: number, options?: ScrollToErrorOptions): Field> | null; } /** @@ -94,9 +94,9 @@ export function scrollToErrorPlugin(): PluginInjector { } function getTargetFields( - field: Field, - batch: Field[] -): Field[] { + field: Field, + batch: Field[] +): Field[] { if (field.isInvalid && field.element !== null && field.element.isConnected) { const rect = field.element.getBoundingClientRect(); @@ -112,7 +112,10 @@ function getTargetFields( return batch; } -function sortByBoundingRect(fields: Field[], ltr: boolean): Field[] { +function sortByBoundingRect( + fields: Field[], + ltr: boolean +): Field[] { if (fields.length === 0) { return fields; } diff --git a/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx b/packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.ts similarity index 100% rename from packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.tsx rename to packages/scroll-to-error-plugin/src/test/scrollToErrorPlugin.test.ts diff --git a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts index da4c1f0..ef3e1e0 100644 --- a/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts +++ b/packages/uncontrolled-plugin/src/main/uncontrolledPlugin.ts @@ -104,7 +104,7 @@ export function uncontrolledPlugin(accessor = elementsValueAccessor): PluginInje } function swapElements( - field: Field, + field: Field, changeListener: EventListener, prevElement: Element | null, nextElement: Element | null diff --git a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts index 9555ba7..bc9c211 100644 --- a/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts +++ b/packages/uncontrolled-plugin/src/test/uncontrolledPlugin.test.ts @@ -1,5 +1,5 @@ -import { ElementsValueAccessor, uncontrolledPlugin } from '../main'; -import { composePlugins, createField } from 'roqueform'; +import { ElementsValueAccessor, type UncontrolledPlugin, uncontrolledPlugin } from '../main'; +import { composePlugins, createField, type PluginInjector } from 'roqueform'; import { fireEvent } from '@testing-library/dom'; describe('uncontrolledPlugin', () => { @@ -15,7 +15,7 @@ describe('uncontrolledPlugin', () => { test('invokes ref from the preceding plugin', () => { const refMock = jest.fn(); - const pluginMock = jest.fn(field => { + const pluginMock: PluginInjector<{ ref: UncontrolledPlugin['ref'] }> = jest.fn(field => { field.ref = refMock; }); diff --git a/packages/zod-plugin/src/main/index.ts b/packages/zod-plugin/src/main/index.ts index d7ca15a..58a4913 100644 --- a/packages/zod-plugin/src/main/index.ts +++ b/packages/zod-plugin/src/main/index.ts @@ -9,4 +9,4 @@ */ export { zodPlugin } from './zodPlugin'; -export type { ZodPlugin, ValueTypePlugin } from './zodPlugin'; +export type { ZodPlugin, ZodTypePlugin } from './zodPlugin'; diff --git a/packages/zod-plugin/src/main/zodPlugin.ts b/packages/zod-plugin/src/main/zodPlugin.ts index f751589..b72af09 100644 --- a/packages/zod-plugin/src/main/zodPlugin.ts +++ b/packages/zod-plugin/src/main/zodPlugin.ts @@ -3,7 +3,6 @@ import { errorsPlugin, ErrorsPlugin, Field, - FieldController, PluginInjector, Validation, validationPlugin, @@ -12,7 +11,7 @@ import { } from 'roqueform'; import { ParseParams, SafeParseReturnType, ZodIssue, ZodIssueCode, ZodType, ZodTypeAny } from 'zod'; -export interface ValueTypePlugin { +export interface ZodTypePlugin { /** * The Zod validation type of the root value. */ @@ -31,7 +30,7 @@ export interface ValueTypePlugin { /** * The plugin added to fields by the {@link zodPlugin}. */ -export type ZodPlugin = ValidationPlugin> & ErrorsPlugin & ValueTypePlugin; +export type ZodPlugin = ValidationPlugin> & ErrorsPlugin & ZodTypePlugin; /** * Enhances fields with validation methods powered by [Zod](https://zod.dev/). @@ -41,10 +40,10 @@ export type ZodPlugin = ValidationPlugin> & ErrorsPlugin(type: ZodType): PluginInjector { - return validationPlugin(composePlugins(errorsPlugin(concatErrors), valueTypePlugin(type)), validator); + return validationPlugin(composePlugins(errorsPlugin(concatErrors), zodTypePlugin(type)), validator); } -function valueTypePlugin(rootType: ZodType): PluginInjector { +function zodTypePlugin(rootType: ZodType): PluginInjector { return field => { field.valueType = field.parentField?.valueType || rootType; @@ -99,7 +98,7 @@ function getValue(field: Field): unknown { return value; } -function getPath(field: FieldController): any[] { +function getPath(field: Field): any[] { const path = []; while (field.parentField !== null) { diff --git a/packages/zod-plugin/src/test/zodPlugin.test-d.ts b/packages/zod-plugin/src/test/zodPlugin.test-d.ts index 10d6c99..029f239 100644 --- a/packages/zod-plugin/src/test/zodPlugin.test-d.ts +++ b/packages/zod-plugin/src/test/zodPlugin.test-d.ts @@ -3,6 +3,6 @@ import { expectType } from 'tsd'; import { createField, Field } from 'roqueform'; import { ZodPlugin, zodPlugin } from '@roqueform/zod-plugin'; -const shape = z.object({ aaa: z.object({ bbb: z.string() }) }); +const type = z.object({ aaa: z.object({ bbb: z.string() }) }); -expectType>(createField({ aaa: { bbb: 'aaa' } }, zodPlugin(shape))); +expectType>(createField({ aaa: { bbb: 'aaa' } }, zodPlugin(type))); diff --git a/packages/zod-plugin/src/test/zodPlugin.test.tsx b/packages/zod-plugin/src/test/zodPlugin.test.ts similarity index 100% rename from packages/zod-plugin/src/test/zodPlugin.test.tsx rename to packages/zod-plugin/src/test/zodPlugin.test.ts