Skip to content

Commit

Permalink
Multiple errors support for validation plugins (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
smikhalevski authored Dec 15, 2023
1 parent 3c963a1 commit 4ea4c63
Show file tree
Hide file tree
Showing 46 changed files with 1,917 additions and 7,336 deletions.
5,711 changes: 339 additions & 5,372 deletions package-lock.json

Large diffs are not rendered by default.

21 changes: 10 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,22 @@
"./packages/zod-plugin"
],
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-typescript": "^11.1.3",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.5",
"@testing-library/dom": "^9.3.3",
"@testing-library/react": "^14.0.0",
"@types/jest": "^29.5.5",
"@types/react": "^18.2.21",
"@testing-library/react": "^14.1.2",
"@types/jest": "^29.5.11",
"@types/react": "^18.2.45",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"prettier": "^3.0.3",
"rimraf": "^5.0.1",
"rollup": "^3.29.2",
"rollup-plugin-dts": "^6.0.2",
"prettier": "^3.1.1",
"rimraf": "^5.0.5",
"rollup": "^4.9.0",
"ts-jest": "^29.1.1",
"tsd": "^0.29.0",
"tslib": "^2.6.2",
"typedoc": "^0.25.1",
"typedoc": "^0.25.4",
"typedoc-custom-css": "github:smikhalevski/typedoc-custom-css#master",
"typescript": "^5.2.2"
"typescript": "^5.3.3"
}
}
4 changes: 2 additions & 2 deletions packages/annotations-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export const App = () => {

const handleSubmit = () => {
// Disable interface before submit
planetField.annotateAll({ isDisabled: true });
planetField.annotate({ isDisabled: true }, { recursive: true });

doSubmit(planetField.value).then(() => {
// Enable interface after submit is completed
planetField.annotateAll({ isDisabled: false });
planetField.annotate({ isDisabled: false }, { recursive: true });
});
};

Expand Down
2 changes: 1 addition & 1 deletion packages/annotations-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"bugs": {
"url": "https://github.com/smikhalevski/roqueform/issues"
},
"homepage": "https://github.com/smikhalevski/roqueform/tree/master/packages/reset-plugin#readme",
"homepage": "https://github.com/smikhalevski/roqueform/tree/master/packages/annotations-plugin#readme",
"peerDependencies": {
"roqueform": "^4.0.0"
}
Expand Down
120 changes: 60 additions & 60 deletions packages/annotations-plugin/src/main/annotationsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,43 @@
import { callOrGet, dispatchEvents, Event, Field, PluginInjector, PluginOf, Subscriber, Unsubscribe } from 'roqueform';

interface ReadonlyDict {
readonly [key: string]: any;
}

/**
* The callback that applies patches to field annotations.
*
* @param annotations Annotations associated with this field.
* @param patch The patch that must be applied to annotations.
* @returns The new annotations object that contains original annotations that are partially overridden by the patch.
* @template Annotations Annotations associated with fields.
* Options of the {@link AnnotationsPlugin.annotate} method.
*/
export type AnnotationsPatcher<Annotations extends object = { [annotation: string]: any }> = (
annotations: Readonly<Annotations>,
patch: Partial<Readonly<Annotations>>
) => Annotations;

const naturalAnnotationsPatcher: AnnotationsPatcher = (annotations, patch) => Object.assign({}, annotations, patch);
export interface AnnotateOptions {
/**
* If `true` then patch is applied to this field and all of its descendant fields.
*
* @default false
*/
recursive?: boolean;
}

/**
* The plugin added to fields by the {@link annotationsPlugin}.
*
* @template Annotations Annotations associated with fields.
*/
export interface AnnotationsPlugin<Annotations extends object = { [annotation: string]: any }> {
export interface AnnotationsPlugin<Annotations extends object> {
/**
* Annotations associated with this field.
*/
annotations: Readonly<Annotations>;

/**
* The callback that applies patches to field annotations.
*/
annotationsPatcher: AnnotationsPatcher<Annotations>;
annotations: Annotations;

/**
* Updates annotations of this field.
*
* @param patch The patch that is applied to current annotations, or a callback that receives the current annotations
* and returns a patch that must be applied. A patch is applied using {@link annotationsPatcher}.
* and returns a patch that must be applied.
* @param options Additional options.
*/
annotate(patch: Partial<Annotations> | ((annotations: Readonly<Annotations>) => Partial<Annotations>)): void;

/**
* Updates annotations of this field and all of its child fields.
*
* @param patch The patch that is applied to current annotations, or a callback that receives the current annotations
* and returns a patch that must be applied. A patch is applied using {@link annotationsPatcher}.
*/
annotateAll(patch: Partial<Annotations> | ((annotations: Readonly<Annotations>) => Partial<Annotations>)): void;
annotate(
patch: Partial<Annotations> | ((annotations: Readonly<Annotations>) => Partial<Annotations>),
options?: AnnotateOptions
): void;

/**
* Subscribes to changes of {@link AnnotationsPlugin.annotations the field annotations}.
Expand All @@ -59,61 +51,69 @@ export interface AnnotationsPlugin<Annotations extends object = { [annotation: s

/**
* Enhances fields with methods that manage annotations.
*
* @param patcher The callback that applies patches to field annotations. By default, patches are applied using
* [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).
* @template Annotations Annotations associated with fields.
*/
export function annotationsPlugin<Annotations extends object = { [annotation: string]: any }>(
patcher?: AnnotationsPatcher<Partial<Annotations>>
): PluginInjector<AnnotationsPlugin<Partial<Annotations>>>;
export function annotationsPlugin(): PluginInjector<AnnotationsPlugin<ReadonlyDict>>;

/**
* Enhances fields with methods that manage annotations.
*
* @param annotations The initial annotations that are associated with fields.
* @param patcher The callback that applies patches to field annotations. By default, patches are applied using
* @param applyPatch The callback that applies patches to field annotations. By default, patches are applied using
* [Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign).
* @template Annotations Annotations associated with fields.
*/
export function annotationsPlugin<Annotations extends object = { [annotation: string]: any }>(
export function annotationsPlugin<Annotations extends object>(
annotations: Annotations,
patcher?: AnnotationsPatcher<Annotations>
/**
* The callback that applies patches to field annotations.
*
* @param annotations Annotations associated with this field.
* @param patch The patch that must be applied to annotations.
* @returns The new annotations object that contains original annotations that are partially overridden by the patch,
* or the original annotations object if nothing has changed.
* @template Annotations Annotations associated with fields.
*/
applyPatch?: (annotations: Readonly<Annotations>, patch: Readonly<Partial<Annotations>>) => Annotations
): PluginInjector<AnnotationsPlugin<Annotations>>;

export function annotationsPlugin(
annotations: AnnotationsPatcher | object = {},
patcher = naturalAnnotationsPatcher
): PluginInjector<AnnotationsPlugin> {
if (typeof annotations === 'function') {
patcher = annotations as AnnotationsPatcher;
annotations = {};
}

annotations = {},
applyPatch = applyChanges
): PluginInjector<AnnotationsPlugin<ReadonlyDict>> {
return field => {
field.annotations = annotations;
field.annotationsPatcher = patcher;

field.annotate = patch => dispatchEvents(annotate(field, field, patch, false, []));

field.annotateAll = patch => dispatchEvents(annotate(field, field, patch, true, []));
field.annotate = (patch, options) => dispatchEvents(annotate(field, patch, applyPatch, options, []));
};
}

function applyChanges(annotations: ReadonlyDict, patch: ReadonlyDict): ReadonlyDict {
for (const key in patch) {
if (patch[key] !== annotations[key]) {
return Object.assign(Object.create(Object.getPrototypeOf(annotations)), annotations, patch);
}
}
return annotations;
}

function annotate(
target: Field<AnnotationsPlugin>,
origin: Field<AnnotationsPlugin>,
patch: object | ((annotations: object) => object),
deep: boolean,
field: Field<AnnotationsPlugin<ReadonlyDict>>,
patch: ReadonlyDict | ((annotations: ReadonlyDict) => ReadonlyDict),
applyPatch: (annotations: ReadonlyDict, patch: ReadonlyDict) => ReadonlyDict,
options: AnnotateOptions | undefined,
events: Event[]
): Event[] {
events.push({ type: 'change:annotations', target, origin, data: target.annotations });
const prevAnnotations = field.annotations;
const nextAnnotations = applyPatch(prevAnnotations, callOrGet(patch, field.annotations));

target.annotations = target.annotationsPatcher(target.annotations, callOrGet(patch, target.annotations));
if (prevAnnotations !== nextAnnotations) {
field.annotations = nextAnnotations;
events.push({ type: 'change:annotations', targetField: field, originField: field, data: prevAnnotations });
}

if (deep && target.children !== null) {
for (const child of target.children) {
annotate(child, origin, patch, deep, events);
if (field.children !== null && options !== undefined && options.recursive) {
for (const child of field.children) {
annotate(child, patch, applyPatch, options, events);
}
}
return events;
Expand Down
3 changes: 2 additions & 1 deletion packages/annotations-plugin/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* @module annotations-plugin
*/

export { annotationsPlugin, AnnotationsPlugin, AnnotationsPatcher } from './annotationsPlugin';
export { annotationsPlugin } from './annotationsPlugin';
export type { AnnotateOptions, AnnotationsPlugin } from './annotationsPlugin';
24 changes: 12 additions & 12 deletions packages/annotations-plugin/src/test/annotationsPlugin.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createField } from 'roqueform';
import { AnnotationsPatcher, annotationsPlugin } from '../main';
import { annotationsPlugin } from '../main';

describe('annotationsPlugin', () => {
test('annotations are an empty object by default', () => {
const field = createField({ aaa: 111 }, annotationsPlugin());
const field = createField({ aaa: 111 }, annotationsPlugin({}));

expect(field.annotations).toEqual({});
expect(field.at('aaa').annotations).toEqual({});
Expand Down Expand Up @@ -35,8 +35,8 @@ describe('annotationsPlugin', () => {
expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(subscriberMock).toHaveBeenNthCalledWith(1, {
type: 'change:annotations',
target: field,
origin: field,
targetField: field,
originField: field,
data: { xxx: 222 },
});
expect(aaaSubscriberMock).not.toHaveBeenCalled();
Expand Down Expand Up @@ -75,27 +75,27 @@ describe('annotationsPlugin', () => {
expect(subscriberMock).toHaveBeenCalledTimes(1);
expect(subscriberMock).toHaveBeenNthCalledWith(1, {
type: 'change:annotations',
target: field.at('aaa'),
origin: field.at('aaa'),
targetField: field.at('aaa'),
originField: field.at('aaa'),
data: { xxx: 222 },
});
expect(aaaSubscriberMock).toHaveBeenCalledTimes(1);
expect(aaaSubscriberMock).toHaveBeenNthCalledWith(1, {
type: 'change:annotations',
target: field.at('aaa'),
origin: field.at('aaa'),
targetField: field.at('aaa'),
originField: field.at('aaa'),
data: { xxx: 222 },
});
});

test('uses patcher to apply patches', () => {
const patcherMock: AnnotationsPatcher = jest.fn((a, b) => Object.assign({}, a, b));
const applyPatchMock = jest.fn((a, b) => Object.assign({}, a, b));

const field = createField(111, annotationsPlugin(patcherMock));
const field = createField(111, annotationsPlugin({}, applyPatchMock));

field.annotate({ xxx: 222 });

expect(patcherMock).toHaveBeenCalledTimes(1);
expect(patcherMock).toHaveBeenNthCalledWith(1, {}, { xxx: 222 });
expect(applyPatchMock).toHaveBeenCalledTimes(1);
expect(applyPatchMock).toHaveBeenNthCalledWith(1, {}, { xxx: 222 });
});
});
Loading

0 comments on commit 4ea4c63

Please sign in to comment.