Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored typings #33

Merged
merged 6 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 16 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ Let's start by creating a field:
import { createField } from 'roqueform';

const field = createField();
// ⮕ Field<unknown, any>
// ⮕ Field<any>
```

A value can be set to and retrieved from the field:
Expand All @@ -85,7 +85,7 @@ Provide the initial value for a field:

```ts
const ageField = createField(42);
// ⮕ Field<unknown, number>
// ⮕ Field<number>

ageField.value;
// ⮕ 42
Expand All @@ -103,7 +103,7 @@ interface Universe {
}

const universeField = createField<Universe>();
// ⮕ Field<unknown, Universe | undefined>
// ⮕ Field<Universe | undefined>

universeField.value;
// ⮕ undefined
Expand All @@ -113,7 +113,7 @@ Retrieve a child field by its key:

```ts
const planetsField = universeField.at('planets');
// ⮕ Field<unknown, Planet[] | undefined>
// ⮕ Field<Planet[] | undefined>
```

`planetsField` is a child field, and it is linked to its parent `universeField`.
Expand All @@ -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:

Expand All @@ -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<unknown, string>
// ⮕ Field<string>
```

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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<string[], …>
```

# Errors plugin
Expand Down
4 changes: 2 additions & 2 deletions packages/annotations-plugin/src/main/annotationsPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export interface AnnotationsPlugin<Annotations extends object> {
* @param subscriber The subscriber that would be triggered.
* @returns The callback to unsubscribe the subscriber.
*/
on(eventType: 'change:annotations', subscriber: Subscriber<PluginOf<this>, Annotations>): Unsubscribe;
on(eventType: 'change:annotations', subscriber: Subscriber<Annotations, PluginOf<this>>): Unsubscribe;
}

/**
Expand Down Expand Up @@ -97,7 +97,7 @@ function applyChanges(annotations: ReadonlyDict, patch: ReadonlyDict): ReadonlyD
}

function annotate(
field: Field<AnnotationsPlugin<ReadonlyDict>>,
field: Field<unknown, AnnotationsPlugin<ReadonlyDict>>,
patch: ReadonlyDict | ((annotations: ReadonlyDict) => ReadonlyDict),
applyPatch: (annotations: ReadonlyDict, patch: ReadonlyDict) => ReadonlyDict,
options: AnnotateOptions | undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface ConstraintValidationPlugin {
/**
* Returns all invalid fields.
*/
getInvalidFields(): Field<PluginOf<this>>[];
getInvalidFields(): Field<any, PluginOf<this>>[];

/**
* Subscribes to {@link validity the validity} changes of this field or any of its descendants.
Expand All @@ -48,7 +48,7 @@ export interface ConstraintValidationPlugin {
* @returns The callback to unsubscribe the subscriber.
* @see {@link isInvalid}
*/
on(eventType: 'change:validity', subscriber: Subscriber<PluginOf<this>, ValidityState | null>): Unsubscribe;
on(eventType: 'change:validity', subscriber: Subscriber<ValidityState | null, PluginOf<this>>): Unsubscribe;
}

/**
Expand Down Expand Up @@ -118,7 +118,7 @@ export function constraintValidationPlugin(): PluginInjector<ConstraintValidatio
};
}

function applyValidity(field: Field<ConstraintValidationPlugin>): void {
function applyValidity(field: Field<unknown, ConstraintValidationPlugin>): void {
const prevValidity = field.validity;
const nextValidity = field.validatedElement !== null ? cloneValidity(field.validatedElement.validity) : null;

Expand All @@ -129,7 +129,7 @@ function applyValidity(field: Field<ConstraintValidationPlugin>): void {
}
}

function reportValidity(field: Field<ConstraintValidationPlugin>): boolean {
function reportValidity(field: Field<unknown, ConstraintValidationPlugin>): boolean {
if (field.children !== null) {
for (const child of field.children) {
if (!reportValidity(child)) {
Expand All @@ -141,9 +141,9 @@ function reportValidity(field: Field<ConstraintValidationPlugin>): boolean {
}

function getInvalidFields(
field: Field<ConstraintValidationPlugin>,
batch: Field<ConstraintValidationPlugin>[]
): Field<ConstraintValidationPlugin>[] {
field: Field<unknown, ConstraintValidationPlugin>,
batch: Field<unknown, ConstraintValidationPlugin>[]
): Field<unknown, ConstraintValidationPlugin>[] {
if (field.isInvalid) {
batch.push(field);
}
Expand Down
14 changes: 7 additions & 7 deletions packages/doubter-plugin/src/main/doubterPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {
composePlugins,
errorsPlugin,
ErrorsPlugin,
FieldController,
Field,
PluginInjector,
Validation,
ValidationPlugin,
validationPlugin,
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;
Expand All @@ -31,7 +31,7 @@ export interface ValueShapePlugin {
/**
* The plugin added to fields by the {@link doubterPlugin}.
*/
export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue> & ValueShapePlugin;
export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue> & DoubterShapePlugin;

/**
* Enhances fields with validation methods powered by [Doubter](https://github.com/smikhalevski/doubter#readme).
Expand All @@ -40,10 +40,10 @@ export type DoubterPlugin = ValidationPlugin<ParseOptions> & ErrorsPlugin<Issue>
* @template Value The root field value.
*/
export function doubterPlugin<Value>(shape: Shape<Value, any>): PluginInjector<DoubterPlugin, Value> {
return validationPlugin(composePlugins(errorsPlugin(concatErrors), valueShapePlugin(shape)), validator);
return validationPlugin(composePlugins(errorsPlugin(concatErrors), doubterShapePlugin(shape)), validator);
}

function valueShapePlugin(rootShape: Shape): PluginInjector<ValueShapePlugin> {
function doubterShapePlugin<Value>(rootShape: Shape<Value, any>): PluginInjector<DoubterShapePlugin, Value> {
return field => {
field.valueShape = field.parentField === null ? rootShape : field.parentField.valueShape?.at(field.key) || null;

Expand Down Expand Up @@ -92,7 +92,7 @@ function concatErrors(errors: readonly Issue[], error: Issue): readonly Issue[]
return errors.concat(error);
}

function prependPath(field: FieldController<any>, issue: Issue): Issue {
function prependPath(field: Field, issue: Issue): Issue {
for (; field.parentField !== null; field = field.parentField) {
(issue.path ||= []).unshift(field.key);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/doubter-plugin/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
*/

export { doubterPlugin } from './doubterPlugin';
export type { DoubterPlugin, ValueShapePlugin } from './doubterPlugin';
export type { DoubterPlugin, DoubterShapePlugin } from './doubterPlugin';
11 changes: 8 additions & 3 deletions packages/doubter-plugin/src/test/doubterPlugin.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<Field<DoubterPlugin, { aaa: { bbb: string } }>>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape)));
expectType<Field<{ aaa: { bbb: string } }, DoubterPlugin>>(createField({ aaa: { bbb: 'aaa' } }, doubterPlugin(shape)));

expectType<Field<{ aaa: { bbb: string } }, AnnotationsPlugin<{ xxx: string }> & DoubterPlugin>>(
createField({ aaa: { bbb: 'aaa' } }, composePlugins(doubterPlugin(shape), annotationsPlugin({ xxx: 'yyy' })))
);
14 changes: 7 additions & 7 deletions packages/react/src/main/FieldRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Field extends FieldController<any>> {
export interface FieldRendererProps<F extends Field> {
/**
* 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
Expand All @@ -40,7 +40,7 @@ export interface FieldRendererProps<Field extends FieldController<any>> {
*
* @param value The new field value.
*/
onChange?: (value: ValueOf<Field>) => void;
onChange?: (value: ValueOf<F>) => void;
}

/**
Expand All @@ -49,11 +49,11 @@ export interface FieldRendererProps<Field extends FieldController<any>> {
*
* @template Field The rendered field.
*/
export function FieldRenderer<Field extends FieldController<any>>(props: FieldRendererProps<Field>): ReactElement {
export function FieldRenderer<F extends Field>(props: FieldRendererProps<F>): ReactElement {
const { field, eagerlyUpdated } = props;

const [, rerender] = useReducer(reduceCount, 0);
const handleChangeRef = useRef<FieldRendererProps<Field>['onChange']>();
const handleChangeRef = useRef<FieldRendererProps<F>['onChange']>();

handleChangeRef.current = props.onChange;

Expand Down
18 changes: 16 additions & 2 deletions packages/react/src/main/useField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> = T extends infer T ? T : never;
type NoInfer<T> = [T][T extends any ? 0 : never];

/**
* Creates the new field.
Expand All @@ -22,6 +22,20 @@ export function useField<Value = any>(): Field<Value | undefined>;
*/
export function useField<Value>(initialValue: Value | (() => Value)): Field<Value>;

/**
* 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<Value, Plugin>(
initialValue: Value | (() => Value),
plugin: PluginInjector<Plugin>
): Field<Value, Plugin>;

/**
* Creates the new field enhanced by a plugin.
*
Expand All @@ -34,7 +48,7 @@ export function useField<Value>(initialValue: Value | (() => Value)): Field<Valu
export function useField<Value, Plugin>(
initialValue: Value | (() => Value),
plugin: PluginInjector<Plugin, NoInfer<Value>>
): Field<Plugin, Value>;
): Field<Value, Plugin>;

export function useField(initialValue?: unknown, plugin?: PluginInjector) {
const accessor = useContext(ValueAccessorContext);
Expand Down
Loading
Loading