@codeleap/form 7.3.0-next.0 → 7.3.1-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/index.d.ts +5 -5
- package/dist/fields/list.d.ts +4 -4
- package/dist/fields/list.d.ts.map +1 -1
- package/dist/fields/list.js +6 -5
- package/dist/fields/list.js.map +1 -1
- package/dist/lib/Form.d.ts +16 -3
- package/dist/lib/Form.d.ts.map +1 -1
- package/dist/lib/Form.js +25 -8
- package/dist/lib/Form.js.map +1 -1
- package/dist/types/field.d.ts +1 -1
- package/dist/types/field.d.ts.map +1 -1
- package/dist/types/globals.d.ts +2 -0
- package/dist/types/globals.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/fields/list.ts +18 -16
- package/src/lib/Form.ts +25 -6
- package/src/tests/Field.spec.ts +186 -0
- package/src/tests/Form.spec.ts +160 -0
- package/src/tests/fields.spec.ts +90 -0
- package/src/tests/hooks.spec.tsx +111 -0
- package/src/tests/setup.ts +25 -0
- package/src/types/field.ts +1 -1
- package/src/types/globals.ts +2 -1
package/dist/fields/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import { FileField } from "./file";
|
|
|
22
22
|
export declare const fields: {
|
|
23
23
|
text: <A extends {
|
|
24
24
|
name?: string;
|
|
25
|
-
defaultValue?: string | null | undefined;
|
|
25
|
+
defaultValue?: string | (() => string) | (() => Promise<string>) | null | undefined;
|
|
26
26
|
state?: import("nanostores").WritableAtom<string> | undefined;
|
|
27
27
|
validate?: import("./text").TextValidator<any, any> | undefined;
|
|
28
28
|
loader?: ((form: any) => Partial<Omit<import("..").FieldOptions<string, import("./text").TextValidator<any, any>>, "loader">>) | undefined;
|
|
@@ -34,7 +34,7 @@ export declare const fields: {
|
|
|
34
34
|
list: typeof listFieldFactory;
|
|
35
35
|
number: <A extends {
|
|
36
36
|
name?: string;
|
|
37
|
-
defaultValue?: number | number[] | null | undefined;
|
|
37
|
+
defaultValue?: number | number[] | (() => number | number[]) | (() => Promise<number | number[]>) | null | undefined;
|
|
38
38
|
state?: import("..").FieldState<number | number[]> | undefined;
|
|
39
39
|
validate?: import("./number").NumberValidator<any, any> | undefined;
|
|
40
40
|
loader?: ((form: any) => Partial<Omit<import("..").FieldOptions<number | number[], import("./number").NumberValidator<any, any>>, "loader">>) | undefined;
|
|
@@ -45,7 +45,7 @@ export declare const fields: {
|
|
|
45
45
|
}>(options?: A | undefined) => import("..").Field<number | number[], A["validate"], A["validate"] extends infer T ? T extends A["validate"] ? T extends import("..").Validator<number | number[], infer R, any> ? R : never : never : never, A["validate"] extends infer T_1 ? T_1 extends A["validate"] ? T_1 extends import("..").Validator<number | number[], any, infer E> ? E : never : never : never>;
|
|
46
46
|
boolean: <A extends {
|
|
47
47
|
name?: string;
|
|
48
|
-
defaultValue?: boolean | null | undefined;
|
|
48
|
+
defaultValue?: boolean | (() => boolean) | (() => Promise<boolean>) | null | undefined;
|
|
49
49
|
state?: import("nanostores").WritableAtom<boolean> | undefined;
|
|
50
50
|
validate?: import("./bool").BooleanValidator<any, any> | undefined;
|
|
51
51
|
loader?: ((form: any) => Partial<Omit<import("..").FieldOptions<boolean, import("./bool").BooleanValidator<any, any>>, "loader">>) | undefined;
|
|
@@ -53,7 +53,7 @@ export declare const fields: {
|
|
|
53
53
|
} & import("..").ExtraFieldOptions>(options?: A | undefined) => import("..").Field<boolean, A["validate"], A["validate"] extends infer T ? T extends A["validate"] ? T extends import("..").Validator<boolean, infer R, any> ? R : never : never : never, A["validate"] extends infer T_1 ? T_1 extends A["validate"] ? T_1 extends import("..").Validator<boolean, any, infer E> ? E : never : never : never>;
|
|
54
54
|
selectable: <A extends {
|
|
55
55
|
name?: string;
|
|
56
|
-
defaultValue?: (string | number) | null | undefined;
|
|
56
|
+
defaultValue?: (string | number) | (() => string | number) | (() => Promise<string | number>) | null | undefined;
|
|
57
57
|
state?: import("nanostores").WritableAtom<string | number> | undefined;
|
|
58
58
|
validate?: import("./selectable").SelectableValidator<string | number, any, any> | undefined;
|
|
59
59
|
loader?: ((form: any) => Partial<Omit<import("..").FieldOptions<string | number, import("./selectable").SelectableValidator<string | number, any, any>>, "loader">>) | undefined;
|
|
@@ -65,7 +65,7 @@ export declare const fields: {
|
|
|
65
65
|
}>(options?: A | undefined) => import("..").Field<string | number, A["validate"], A["validate"] extends infer T ? T extends A["validate"] ? T extends import("..").Validator<string | number, infer R, any> ? R : never : never : never, A["validate"] extends infer T_1 ? T_1 extends A["validate"] ? T_1 extends import("..").Validator<string | number, any, infer E> ? E : never : never : never>;
|
|
66
66
|
date: <A extends {
|
|
67
67
|
name?: string;
|
|
68
|
-
defaultValue?: Date | null | undefined;
|
|
68
|
+
defaultValue?: Date | (() => Date) | (() => Promise<Date>) | null | undefined;
|
|
69
69
|
state?: import("..").FieldState<Date> | undefined;
|
|
70
70
|
validate?: import("./date").DateValidator<any, any> | undefined;
|
|
71
71
|
loader?: ((form: any) => Partial<Omit<import("..").FieldOptions<Date, import("./date").DateValidator<any, any>>, "loader">>) | undefined;
|
package/dist/fields/list.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Field } from
|
|
2
|
-
import { FieldOptions, Validator } from
|
|
1
|
+
import { Field } from '../lib/Field';
|
|
2
|
+
import { FieldOptions, Validator } from '../types';
|
|
3
3
|
/** Validator signature for array-valued fields. `R` and `Err` are the result and error types for the whole list, not individual items. */
|
|
4
4
|
export type ListValidator<Val, R = any, Err = any> = Validator<Val[], R, Err>;
|
|
5
5
|
type ExtractFieldValue<T> = T extends Field<infer Value, any> ? Value : never;
|
|
6
|
-
type AsListValidator<T> = T extends Field<infer Value, infer Validate> ? Validator<Value[], Validate extends Validator<any, infer R, any> ? R[] :
|
|
7
|
-
export type _ListFieldOptions<T> = T extends Field<infer Value, infer Validate> ? FieldOptions<Value[], Validator<Value[], Validate extends Validator<any, infer R, any> ? R[] :
|
|
6
|
+
type AsListValidator<T> = T extends Field<infer Value, infer Validate> ? Validator<Value[], Validate extends Validator<any, infer R, any> ? R[] : unknown, Validate extends Validator<any, any, infer Err> ? Err[] : unknown> : never;
|
|
7
|
+
export type _ListFieldOptions<T> = T extends Field<infer Value, infer Validate> ? FieldOptions<Value[], Validator<Value[], Validate extends Validator<any, infer R, any> ? R[] : unknown, Validate extends Validator<any, any, infer Err> ? Err[] : unknown>> : never;
|
|
8
8
|
export type ListFieldOptions<ItemField extends Field<any, any>> = {
|
|
9
9
|
item: ItemField;
|
|
10
10
|
} & _ListFieldOptions<ItemField>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/fields/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAElD,0IAA0I;AAC1I,MAAM,MAAM,aAAa,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,CAAA;AAE7E,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,KAAK,EAAE,GAAG,CAAC,GAAG,KAAK,GAAG,KAAK,CAAA;AAE7E,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,KAAK,EAAE,MAAM,QAAQ,CAAC,GACpE,SAAS,CACP,KAAK,EAAE,EACP,QAAQ,SAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/fields/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AAElD,0IAA0I;AAC1I,MAAM,MAAM,aAAa,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,GAAG,CAAC,CAAA;AAE7E,KAAK,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,KAAK,EAAE,GAAG,CAAC,GAAG,KAAK,GAAG,KAAK,CAAA;AAE7E,KAAK,eAAe,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,KAAK,EAAE,MAAM,QAAQ,CAAC,GACpE,SAAS,CACP,KAAK,EAAE,EACP,QAAQ,SAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,OAAO,EAC7D,QAAQ,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,OAAO,CAClE,GACA,KAAK,CAAA;AAER,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,KAAK,CAAC,MAAM,KAAK,EAAE,MAAM,QAAQ,CAAC,GAAG,YAAY,CAC5F,KAAK,EAAE,EACP,SAAS,CACP,KAAK,EAAE,EACP,QAAQ,SAAS,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,GAAG,OAAO,EAC7D,QAAQ,SAAS,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,GAAG,EAAE,GAAG,OAAO,CAClE,CACF,GAAG,KAAK,CAAA;AAET,MAAM,MAAM,gBAAgB,CAAC,SAAS,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI;IAChE,IAAI,EAAE,SAAS,CAAA;CAChB,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAA;AAEhC;;;;;;;;;GASG;AACH,qBAAa,SAAS,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAE,SAAQ,KAAK,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IACzG,KAAK,SAAS;gBAEF,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC;CAiCzC;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAEtG"}
|
package/dist/fields/list.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Field } from
|
|
1
|
+
import { Field } from '../lib/Field';
|
|
2
2
|
/**
|
|
3
3
|
* Field that holds an array of values, each validated by a delegate `item`
|
|
4
4
|
* field. The built-in validator iterates every element through
|
|
@@ -10,17 +10,18 @@ import { Field } from "../lib/Field";
|
|
|
10
10
|
* does not own its own atom. Do not call `use()` on it directly.
|
|
11
11
|
*/
|
|
12
12
|
export class ListField extends Field {
|
|
13
|
-
_type =
|
|
13
|
+
_type = 'LIST';
|
|
14
14
|
constructor(options) {
|
|
15
15
|
super({
|
|
16
16
|
...options,
|
|
17
17
|
validate: ((v, form) => {
|
|
18
|
-
if (!options.item?.validate)
|
|
18
|
+
if (!options.item?.validate) {
|
|
19
19
|
return {
|
|
20
|
-
isValid: true
|
|
20
|
+
isValid: true,
|
|
21
21
|
};
|
|
22
|
+
}
|
|
22
23
|
const errors = [];
|
|
23
|
-
for (const value of v) {
|
|
24
|
+
for (const value of (v ?? [])) {
|
|
24
25
|
const validation = options.item?.validate?.(value);
|
|
25
26
|
if (!validation.isValid) {
|
|
26
27
|
errors.push(validation);
|
package/dist/fields/list.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/fields/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AA6BpC;;;;;;;;;GASG;AACH,MAAM,OAAO,SAAqC,SAAQ,KAAiD;IACzG,KAAK,GAAG,MAAM,CAAA;IAEd,YAAY,OAA4B;QACtC,KAAK,CAAC;YACJ,GAAG,OAAO;YACV,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;gBACrB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ;
|
|
1
|
+
{"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/fields/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AA6BpC;;;;;;;;;GASG;AACH,MAAM,OAAO,SAAqC,SAAQ,KAAiD;IACzG,KAAK,GAAG,MAAM,CAAA;IAEd,YAAY,OAA4B;QACtC,KAAK,CAAC;YACJ,GAAG,OAAO;YACV,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;gBACrB,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;oBAC5B,OAAO;wBACL,OAAO,EAAE,IAAI;qBACd,CAAA;gBACH,CAAC;gBAED,MAAM,MAAM,GAAG,EAAE,CAAA;gBAEjB,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;oBAC9B,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAA;oBAElD,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC;wBACxB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;oBACzB,CAAC;gBACH,CAAC;gBAED,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;oBAClB,OAAO;wBACL,OAAO,EAAE,KAAK;wBACd,KAAK,EAAE,MAAM;qBACd,CAAA;gBACH,CAAC;gBAED,OAAO;oBACL,OAAO,EAAE,IAAI;iBACd,CAAA;YACH,CAAC,CAAuB;SAC8C,CAAC,CAAA;IAC3E,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAA4B,OAA4B;IACtF,OAAO,IAAI,SAAS,CAAC,OAAO,CAAC,CAAA;AAC/B,CAAC"}
|
package/dist/lib/Form.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GlobalState } from "@codeleap/store";
|
|
2
|
+
import { DependencyList } from "react";
|
|
2
3
|
import { FieldPaths, FieldTuples, FormDef, FormValues, PropertyForKeys, ValidationResult } from "../types";
|
|
3
4
|
type FormSelector<T extends FormDef, S> = (form: Form<T>) => S;
|
|
4
5
|
/**
|
|
@@ -37,9 +38,10 @@ declare class Form<T extends FormDef> {
|
|
|
37
38
|
slice<K extends FieldPaths<T>>(field: K): import("nanostores").WritableAtom<unknown>;
|
|
38
39
|
iterFields<V>(cb: (field: FieldTuples<T>, index: number) => V): V[];
|
|
39
40
|
/**
|
|
40
|
-
* Bulk-sets field values from a partial record.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* Bulk-sets field values from a partial record. Only keys present in
|
|
42
|
+
* `values` are written — absent keys leave the corresponding field
|
|
43
|
+
* untouched. Falsy values (`''`, `0`, `false`, `null`) are written as-is.
|
|
44
|
+
* Use `resetValues()` to restore all fields to their initial values.
|
|
43
45
|
*/
|
|
44
46
|
setValues(values: Partial<FormValues<T>>): void;
|
|
45
47
|
resetValues(): void;
|
|
@@ -65,6 +67,17 @@ declare class Form<T extends FormDef> {
|
|
|
65
67
|
register(field: FieldPaths<T>): import("..").IFieldProps;
|
|
66
68
|
use<Selected>(selector: FormSelector<T, Selected>): Selected;
|
|
67
69
|
useReset(): void;
|
|
70
|
+
/**
|
|
71
|
+
* Initializes form fields from async or derived data at mount time.
|
|
72
|
+
* Calls `setValues` inside a `useEffect` whenever `values` is non-null and
|
|
73
|
+
* the deps array changes. Pass `null` or `undefined` to skip (e.g. while a
|
|
74
|
+
* query is still loading).
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const { data, isSuccess } = useQuery(...)
|
|
78
|
+
* myForm.useInitialize(isSuccess ? data : null)
|
|
79
|
+
*/
|
|
80
|
+
useInitialize(values: Partial<FormValues<T>> | null | undefined, deps?: DependencyList): void;
|
|
68
81
|
useShared<Selected>(selector: FormSelector<T, Selected>): Selected;
|
|
69
82
|
}
|
|
70
83
|
/**
|
package/dist/lib/Form.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../../src/lib/Form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,WAAW,EAAe,MAAM,iBAAiB,CAAA;
|
|
1
|
+
{"version":3,"file":"Form.d.ts","sourceRoot":"","sources":["../../src/lib/Form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,WAAW,EAAe,MAAM,iBAAiB,CAAA;AAE5E,OAAO,EAAE,cAAc,EAAsE,MAAM,OAAO,CAAA;AAC1G,OAAO,EAAE,UAAU,EAAuB,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAiB/H,KAAK,YAAY,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,KAAM,CAAC,CAAA;AAI/D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,cAAM,IAAI,CAAC,CAAC,SAAS,OAAO;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,CAAC,CAAA;IACT,KAAK,EAAE,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAA;gBAGrB,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAehC,IAAI,MAAM,8HAET;IAED,IAAI,SAAS,YAEZ;IAGD,OAAO;IAQP,IAAI,OAAO,YAIV;IAED,KAAK,CAAC,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;IAgBvC,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,CAAC;IAe7D;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAQxC,WAAW;IAMX,WAAW;IAUX;;;;OAIG;IACH,YAAY;;;;IAWZ,QAAQ,CAAC,MAAM,SAAS,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,eAAe,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IA2BxK;;;;OAIG;IACH,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC;IAO7B,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,QAAQ;IAQ5D,QAAQ;IAMR;;;;;;;;;OASG;IACH,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,cAAc;IAMtF,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,QAAQ;CAiBnE;AAGD;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,WAO9D;AAED;;;;;GAKG;AACH,wBAAgB,IAAI,CAAC,GAAG,SAAS,OAAO,EAAE,GAAG,IAAI,EAAE,qBAAqB,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,aAEzF"}
|
package/dist/lib/Form.js
CHANGED
|
@@ -45,6 +45,7 @@ class Form {
|
|
|
45
45
|
this.use = this.use.bind(this);
|
|
46
46
|
this.useShared = this.useShared.bind(this);
|
|
47
47
|
this.useReset = this.useReset.bind(this);
|
|
48
|
+
this.useInitialize = this.useInitialize.bind(this);
|
|
48
49
|
}
|
|
49
50
|
get values() {
|
|
50
51
|
return this.state.get();
|
|
@@ -82,17 +83,17 @@ class Form {
|
|
|
82
83
|
return results;
|
|
83
84
|
}
|
|
84
85
|
/**
|
|
85
|
-
* Bulk-sets field values from a partial record.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
86
|
+
* Bulk-sets field values from a partial record. Only keys present in
|
|
87
|
+
* `values` are written — absent keys leave the corresponding field
|
|
88
|
+
* untouched. Falsy values (`''`, `0`, `false`, `null`) are written as-is.
|
|
89
|
+
* Use `resetValues()` to restore all fields to their initial values.
|
|
88
90
|
*/
|
|
89
91
|
setValues(values) {
|
|
92
|
+
const source = values ?? {};
|
|
90
93
|
this.iterFields(([name, field]) => {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
else if (value)
|
|
95
|
-
field.setValue(value);
|
|
94
|
+
if (!(name in source))
|
|
95
|
+
return;
|
|
96
|
+
field.setValue(source[name]);
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
resetValues() {
|
|
@@ -158,6 +159,22 @@ class Form {
|
|
|
158
159
|
this.resetValues();
|
|
159
160
|
});
|
|
160
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Initializes form fields from async or derived data at mount time.
|
|
164
|
+
* Calls `setValues` inside a `useEffect` whenever `values` is non-null and
|
|
165
|
+
* the deps array changes. Pass `null` or `undefined` to skip (e.g. while a
|
|
166
|
+
* query is still loading).
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* const { data, isSuccess } = useQuery(...)
|
|
170
|
+
* myForm.useInitialize(isSuccess ? data : null)
|
|
171
|
+
*/
|
|
172
|
+
useInitialize(values, deps) {
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
if (!TypeGuards.isNil(values))
|
|
175
|
+
this.setValues(values);
|
|
176
|
+
}, deps ?? [values]);
|
|
177
|
+
}
|
|
161
178
|
useShared(selector) {
|
|
162
179
|
const [selected, setSelected] = useState(() => selector(this));
|
|
163
180
|
const reselect = useCallback(() => {
|
package/dist/lib/Form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.js","sourceRoot":"","sources":["../../src/lib/Form.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAe,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAC5C,OAAO,EAAkB,WAAW,EAAE,SAAS,EAAmB,OAAO,EAAU,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE1G,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAK5C,SAAS,UAAU,CAAoB,GAAM;IAC3C,MAAM,QAAQ,GAA4B,EAAE,CAAA;IAE5C,KAAI,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,KAAK,CAAA;IAC9B,CAAC;IAED,OAAO,QAAyB,CAAA;AAClC,CAAC;AAOD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,IAAI;IACR,EAAE,CAAQ;IACV,MAAM,CAAG;IACT,KAAK,CAA4B;IAGjC,YAAY,EAAU,EAAE,KAAQ;QAC9B,IAAI,CAAC,EAAE,GAAG,EAAE,CAAA;QACZ,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QAEnB,IAAI,CAAC,KAAK,GAAG,WAAW,CACtB,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CACxB,CAAA;QAED,IAAI,CAAC,WAAW,EAAE,CAAA;QAClB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC9B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1C,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"Form.js","sourceRoot":"","sources":["../../src/lib/Form.ts"],"names":[],"mappings":"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"}
|
package/dist/types/field.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface ExtraFieldOptions {
|
|
|
5
5
|
}
|
|
6
6
|
export type FieldOptions<T, Validate extends Validator<T, any, any>> = {
|
|
7
7
|
name?: string;
|
|
8
|
-
defaultValue?: T | null;
|
|
8
|
+
defaultValue?: T | null | (() => T) | (() => Promise<T>);
|
|
9
9
|
state?: FieldState<T>;
|
|
10
10
|
validate?: Validate;
|
|
11
11
|
loader?: (form: any) => Partial<Omit<FieldOptions<T, Validate>, 'loader'>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/types/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAG,aAAa,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxC,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,CAAA;AAE5C,MAAM,WAAW,iBAAiB;CAEjC;AAED,MAAM,MAAM,YAAY,CACtB,CAAC,EACD,QAAQ,SAAS,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IACrC;IACF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../src/types/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAG,aAAa,EAAE,MAAM,YAAY,CAAA;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAIxC,MAAM,MAAM,UAAU,CAAC,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC,CAAA;AAE5C,MAAM,WAAW,iBAAiB;CAEjC;AAED,MAAM,MAAM,YAAY,CACtB,CAAC,EACD,QAAQ,SAAS,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IACrC;IACF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,CAAC,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC,CAAA;IACxD,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAA;IAErB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IAEnB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,GAAG,KAAK,OAAO,CAC7B,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,QAAQ,CAAE,EAAE,QAAQ,CAAC,CAC3C,CAAA;IAED,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,IAAI,CAAA;CAEtC,GAAG,iBAAiB,CAAA;AAGrB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,CAAC,CAAC,EAAE,MAAM,CAAA;IACV,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA"}
|
package/dist/types/globals.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface IFieldRef<T> {
|
|
|
10
10
|
emit(event: string, ...args: any[]): void;
|
|
11
11
|
}
|
|
12
12
|
export interface IFieldProps {
|
|
13
|
+
name?: string;
|
|
14
|
+
field: any;
|
|
13
15
|
}
|
|
14
16
|
export type PropTransformer = (props: AnyRecord) => AnyRecord;
|
|
15
17
|
//# sourceMappingURL=globals.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../../src/types/globals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,QAAQ,IAAI,CAAC,CAAA;IACb,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;IACZ,WAAW,IAAI,IAAI,CAAA;IACnB,qBAAqB,IAAI,IAAI,CAAA;IAC7B,SAAS,IAAI,IAAI,CAAA;IACjB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;CAC1C;AAGD,MAAM,WAAW,WAAW;
|
|
1
|
+
{"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../../src/types/globals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,QAAQ,IAAI,CAAC,CAAA;IACb,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,KAAK,IAAI,IAAI,CAAA;IACb,IAAI,IAAI,IAAI,CAAA;IACZ,WAAW,IAAI,IAAI,CAAA;IACnB,qBAAqB,IAAI,IAAI,CAAA;IAC7B,SAAS,IAAI,IAAI,CAAA;IACjB,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;CAC1C;AAGD,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,GAAG,CAAA;CACX;AAED,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,SAAS,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeleap/form",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.1-next.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"directory": "packages/form"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@codeleap/config": "7.3.
|
|
26
|
-
"@codeleap/types": "7.3.
|
|
27
|
-
"@codeleap/store": "7.3.
|
|
28
|
-
"@codeleap/hooks": "7.3.
|
|
25
|
+
"@codeleap/config": "7.3.1-next.0",
|
|
26
|
+
"@codeleap/types": "7.3.1-next.0",
|
|
27
|
+
"@codeleap/store": "7.3.1-next.0",
|
|
28
|
+
"@codeleap/hooks": "7.3.1-next.0",
|
|
29
29
|
"zod": "4.4.3",
|
|
30
30
|
"ts-node-dev": "1.1.8"
|
|
31
31
|
},
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"playground": "bun src/test.ts"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@codeleap/types": "7.3.
|
|
39
|
-
"@codeleap/store": "7.3.
|
|
40
|
-
"@codeleap/logger": "7.3.
|
|
41
|
-
"@codeleap/hooks": "7.3.
|
|
38
|
+
"@codeleap/types": "7.3.1-next.0",
|
|
39
|
+
"@codeleap/store": "7.3.1-next.0",
|
|
40
|
+
"@codeleap/logger": "7.3.1-next.0",
|
|
41
|
+
"@codeleap/hooks": "7.3.1-next.0",
|
|
42
42
|
"zod": "*",
|
|
43
43
|
"react": "19.1.0",
|
|
44
44
|
"typescript": "5.5.2"
|
package/src/fields/list.ts
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import { Field } from
|
|
2
|
-
import { FieldOptions, Validator } from
|
|
1
|
+
import { Field } from '../lib/Field'
|
|
2
|
+
import { FieldOptions, Validator } from '../types'
|
|
3
3
|
|
|
4
4
|
/** Validator signature for array-valued fields. `R` and `Err` are the result and error types for the whole list, not individual items. */
|
|
5
5
|
export type ListValidator<Val, R = any, Err = any> = Validator<Val[], R, Err>
|
|
6
6
|
|
|
7
7
|
type ExtractFieldValue<T> = T extends Field<infer Value, any> ? Value : never
|
|
8
8
|
|
|
9
|
-
type AsListValidator<T> = T extends Field<infer Value, infer Validate> ?
|
|
9
|
+
type AsListValidator<T> = T extends Field<infer Value, infer Validate> ?
|
|
10
10
|
Validator<
|
|
11
|
-
Value[],
|
|
12
|
-
Validate extends Validator<any, infer R, any> ? R[] :
|
|
13
|
-
Validate extends Validator<any, any, infer Err> ? Err[] :
|
|
11
|
+
Value[],
|
|
12
|
+
Validate extends Validator<any, infer R, any> ? R[] : unknown,
|
|
13
|
+
Validate extends Validator<any, any, infer Err> ? Err[] : unknown
|
|
14
14
|
>
|
|
15
15
|
: never
|
|
16
16
|
|
|
17
17
|
export type _ListFieldOptions<T> = T extends Field<infer Value, infer Validate> ? FieldOptions<
|
|
18
18
|
Value[],
|
|
19
19
|
Validator<
|
|
20
|
-
Value[],
|
|
21
|
-
Validate extends Validator<any, infer R, any> ? R[] :
|
|
22
|
-
Validate extends Validator<any, any, infer Err> ? Err[] :
|
|
20
|
+
Value[],
|
|
21
|
+
Validate extends Validator<any, infer R, any> ? R[] : unknown,
|
|
22
|
+
Validate extends Validator<any, any, infer Err> ? Err[] : unknown
|
|
23
23
|
>
|
|
24
24
|
> : never
|
|
25
25
|
|
|
26
26
|
export type ListFieldOptions<ItemField extends Field<any, any>> = {
|
|
27
27
|
item: ItemField
|
|
28
|
-
}
|
|
28
|
+
} & _ListFieldOptions<ItemField>
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Field that holds an array of values, each validated by a delegate `item`
|
|
@@ -38,19 +38,21 @@ export type ListFieldOptions<ItemField extends Field<any, any>> = {
|
|
|
38
38
|
* does not own its own atom. Do not call `use()` on it directly.
|
|
39
39
|
*/
|
|
40
40
|
export class ListField<T extends Field<any, any>> extends Field<ExtractFieldValue<T>[], AsListValidator<T>> {
|
|
41
|
-
_type =
|
|
41
|
+
_type = 'LIST'
|
|
42
42
|
|
|
43
43
|
constructor(options: ListFieldOptions<T>) {
|
|
44
44
|
super({
|
|
45
45
|
...options,
|
|
46
46
|
validate: ((v, form) => {
|
|
47
|
-
if (!options.item?.validate)
|
|
48
|
-
|
|
47
|
+
if (!options.item?.validate) {
|
|
48
|
+
return {
|
|
49
|
+
isValid: true,
|
|
50
|
+
}
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
const errors = []
|
|
52
54
|
|
|
53
|
-
for (const value of v) {
|
|
55
|
+
for (const value of (v ?? [])) {
|
|
54
56
|
const validation = options.item?.validate?.(value)
|
|
55
57
|
|
|
56
58
|
if (!validation.isValid) {
|
|
@@ -80,6 +82,6 @@ export class ListField<T extends Field<any, any>> extends Field<ExtractFieldValu
|
|
|
80
82
|
* exists to provide a consistent `fields.list(...)` call signature alongside
|
|
81
83
|
* the other entries in the `fields` namespace.
|
|
82
84
|
*/
|
|
83
|
-
export function listFieldFactory<T extends Field<any,any>>(options: ListFieldOptions<T>): ListField<T> {
|
|
85
|
+
export function listFieldFactory<T extends Field<any, any>>(options: ListFieldOptions<T>): ListField<T> {
|
|
84
86
|
return new ListField(options)
|
|
85
|
-
}
|
|
87
|
+
}
|
package/src/lib/Form.ts
CHANGED
|
@@ -64,6 +64,7 @@ class Form<T extends FormDef> {
|
|
|
64
64
|
this.use = this.use.bind(this)
|
|
65
65
|
this.useShared = this.useShared.bind(this)
|
|
66
66
|
this.useReset = this.useReset.bind(this)
|
|
67
|
+
this.useInitialize = this.useInitialize.bind(this)
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
get values(){
|
|
@@ -121,15 +122,16 @@ class Form<T extends FormDef> {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
/**
|
|
124
|
-
* Bulk-sets field values from a partial record.
|
|
125
|
-
*
|
|
126
|
-
*
|
|
125
|
+
* Bulk-sets field values from a partial record. Only keys present in
|
|
126
|
+
* `values` are written — absent keys leave the corresponding field
|
|
127
|
+
* untouched. Falsy values (`''`, `0`, `false`, `null`) are written as-is.
|
|
128
|
+
* Use `resetValues()` to restore all fields to their initial values.
|
|
127
129
|
*/
|
|
128
130
|
setValues(values: Partial<FormValues<T>>) {
|
|
131
|
+
const source = values ?? {}
|
|
129
132
|
this.iterFields(([name, field]) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
else if (value) field.setValue(value)
|
|
133
|
+
if (!(name in source)) return
|
|
134
|
+
field.setValue(source[name] as any)
|
|
133
135
|
})
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -217,6 +219,23 @@ class Form<T extends FormDef> {
|
|
|
217
219
|
this.resetValues()
|
|
218
220
|
})
|
|
219
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Initializes form fields from async or derived data at mount time.
|
|
225
|
+
* Calls `setValues` inside a `useEffect` whenever `values` is non-null and
|
|
226
|
+
* the deps array changes. Pass `null` or `undefined` to skip (e.g. while a
|
|
227
|
+
* query is still loading).
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* const { data, isSuccess } = useQuery(...)
|
|
231
|
+
* myForm.useInitialize(isSuccess ? data : null)
|
|
232
|
+
*/
|
|
233
|
+
useInitialize(values: Partial<FormValues<T>> | null | undefined, deps?: DependencyList) {
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
if (!TypeGuards.isNil(values)) this.setValues(values)
|
|
236
|
+
}, deps ?? [values])
|
|
237
|
+
}
|
|
238
|
+
|
|
220
239
|
useShared<Selected>(selector: FormSelector<T, Selected>): Selected {
|
|
221
240
|
const [selected, setSelected] = useState(() => selector(this))
|
|
222
241
|
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'bun:test'
|
|
2
|
+
import { Field, ValidationError } from '../lib/Field'
|
|
3
|
+
import { fields } from '../fields'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { zodValidator } from '../validators'
|
|
6
|
+
import './setup'
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
Field.transformers.clear()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('Field construction', () => {
|
|
13
|
+
it('initializes with static defaultValue', () => {
|
|
14
|
+
const field = fields.text({ defaultValue: 'hello' })
|
|
15
|
+
expect(field.value).toBe('hello')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('initializes with function defaultValue', () => {
|
|
19
|
+
const field = fields.text({ defaultValue: () => 'from fn' })
|
|
20
|
+
expect(field.value).toBe('from fn')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('initializes with undefined when no defaultValue', () => {
|
|
24
|
+
const field = fields.text({})
|
|
25
|
+
expect(field.value).toBeUndefined()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('errorRevealed starts as false', () => {
|
|
29
|
+
const field = fields.text({ defaultValue: '' })
|
|
30
|
+
expect(field.isErrorRevealed).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('Field.setValue', () => {
|
|
35
|
+
it('updates the stored value', () => {
|
|
36
|
+
const field = fields.text({ defaultValue: '' })
|
|
37
|
+
field.setValue('updated')
|
|
38
|
+
expect(field.value).toBe('updated')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('calls onValueChange callback', () => {
|
|
42
|
+
const calls: string[] = []
|
|
43
|
+
const field = fields.text({
|
|
44
|
+
defaultValue: '',
|
|
45
|
+
onValueChange: (v) => calls.push(v),
|
|
46
|
+
})
|
|
47
|
+
field.setValue('hello')
|
|
48
|
+
expect(calls).toEqual(['hello'])
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Field.resetValue', () => {
|
|
53
|
+
it('restores the initial value', () => {
|
|
54
|
+
const field = fields.text({ defaultValue: 'initial' })
|
|
55
|
+
field.setValue('changed')
|
|
56
|
+
field.resetValue()
|
|
57
|
+
expect(field.value).toBe('initial')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('hides the revealed error', () => {
|
|
61
|
+
const field = fields.text({ defaultValue: '' })
|
|
62
|
+
field.revealError()
|
|
63
|
+
field.resetValue()
|
|
64
|
+
expect(field.isErrorRevealed).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('Field.changed', () => {
|
|
69
|
+
it('returns false when value equals initial', () => {
|
|
70
|
+
const field = fields.text({ defaultValue: 'init' })
|
|
71
|
+
expect(field.changed()).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('returns true after setValue with a different value', () => {
|
|
75
|
+
const field = fields.text({ defaultValue: 'init' })
|
|
76
|
+
field.setValue('different')
|
|
77
|
+
expect(field.changed()).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('returns false after resetValue', () => {
|
|
81
|
+
const field = fields.text({ defaultValue: 'init' })
|
|
82
|
+
field.setValue('different')
|
|
83
|
+
field.resetValue()
|
|
84
|
+
expect(field.changed()).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('Field.validate', () => {
|
|
89
|
+
it('returns isValid: true for a passing validator', () => {
|
|
90
|
+
const field = fields.text({
|
|
91
|
+
defaultValue: 'hello',
|
|
92
|
+
validate: zodValidator(z.string().min(1)),
|
|
93
|
+
})
|
|
94
|
+
expect(field.validate().isValid).toBe(true)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('returns isValid: false for a failing validator', () => {
|
|
98
|
+
const field = fields.text({
|
|
99
|
+
defaultValue: '',
|
|
100
|
+
validate: zodValidator(z.string().min(1, 'Required')),
|
|
101
|
+
})
|
|
102
|
+
expect(field.validate().isValid).toBe(false)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('validates the supplied value, not the stored value', () => {
|
|
106
|
+
const field = fields.text({
|
|
107
|
+
defaultValue: 'valid',
|
|
108
|
+
validate: zodValidator(z.string().min(1)),
|
|
109
|
+
})
|
|
110
|
+
expect(field.validate('').isValid).toBe(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('catches ValidationError and returns structured failure', () => {
|
|
114
|
+
const field = fields.text({
|
|
115
|
+
defaultValue: 'x',
|
|
116
|
+
validate: () => { throw new ValidationError('custom-error') },
|
|
117
|
+
})
|
|
118
|
+
const result = field.validate()
|
|
119
|
+
expect(result.isValid).toBe(false)
|
|
120
|
+
expect(result.error).toBe('custom-error')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('re-throws non-ValidationError exceptions', () => {
|
|
124
|
+
const field = fields.text({
|
|
125
|
+
defaultValue: 'x',
|
|
126
|
+
validate: () => { throw new Error('unexpected') },
|
|
127
|
+
})
|
|
128
|
+
expect(() => field.validate()).toThrow('unexpected')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('Field.isValid', () => {
|
|
133
|
+
it('is true when validator passes', () => {
|
|
134
|
+
const field = fields.text({ defaultValue: 'hello', validate: zodValidator(z.string().min(1)) })
|
|
135
|
+
expect(field.isValid).toBe(true)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('is false when validator fails', () => {
|
|
139
|
+
const field = fields.text({ defaultValue: '', validate: zodValidator(z.string().min(1)) })
|
|
140
|
+
expect(field.isValid).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('Field error visibility', () => {
|
|
145
|
+
it('revealError sets isErrorRevealed to true', () => {
|
|
146
|
+
const field = fields.text({ defaultValue: '' })
|
|
147
|
+
field.revealError()
|
|
148
|
+
expect(field.isErrorRevealed).toBe(true)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('hideError sets isErrorRevealed to false', () => {
|
|
152
|
+
const field = fields.text({ defaultValue: '' })
|
|
153
|
+
field.revealError()
|
|
154
|
+
field.hideError()
|
|
155
|
+
expect(field.isErrorRevealed).toBe(false)
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe('Field.props and transformers', () => {
|
|
160
|
+
it('props returns field name and field reference', () => {
|
|
161
|
+
const field = fields.text({ defaultValue: '', name: 'email' })
|
|
162
|
+
const props = field.props()
|
|
163
|
+
expect(props.name).toBe('email')
|
|
164
|
+
expect(props.field).toBe(field)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('attachTransformer applies transformation to props', () => {
|
|
168
|
+
Field.attachTransformer('add-flag', (p) => ({ ...p, flagged: true }))
|
|
169
|
+
const field = fields.text({ defaultValue: '', name: 'x' })
|
|
170
|
+
expect((field.props() as any).flagged).toBe(true)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('detachTransformer removes the transformation', () => {
|
|
174
|
+
Field.attachTransformer('to-remove', (p) => ({ ...p, extra: 1 }))
|
|
175
|
+
Field.detachTransformer('to-remove')
|
|
176
|
+
const field = fields.text({ defaultValue: '', name: 'x' })
|
|
177
|
+
expect((field.props() as any).extra).toBeUndefined()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('multiple transformers compose in insertion order', () => {
|
|
181
|
+
Field.attachTransformer('first', (p) => ({ ...p, order: 'first' }))
|
|
182
|
+
Field.attachTransformer('second', (p) => ({ ...p, order: 'second' }))
|
|
183
|
+
const field = fields.text({ defaultValue: '' })
|
|
184
|
+
expect((field.props() as any).order).toBe('second')
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { form } from '../lib/Form'
|
|
3
|
+
import { fields } from '../fields'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import { zodValidator } from '../validators'
|
|
6
|
+
import './setup'
|
|
7
|
+
|
|
8
|
+
const makeBasicForm = () =>
|
|
9
|
+
form('basic', {
|
|
10
|
+
name: fields.text({ defaultValue: 'Alice' }),
|
|
11
|
+
age: fields.number({ defaultValue: 25 }),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const makeValidationForm = () =>
|
|
15
|
+
form('validation', {
|
|
16
|
+
email: fields.text({ validate: zodValidator(z.string().email('Invalid email')) }),
|
|
17
|
+
bio: fields.text({ defaultValue: 'hello', validate: zodValidator(z.string().min(1)) }),
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('Form construction', () => {
|
|
21
|
+
it('sets field names from object keys', () => {
|
|
22
|
+
const f = makeBasicForm()
|
|
23
|
+
expect(f.fields.name.name).toBe('name')
|
|
24
|
+
expect(f.fields.age.name).toBe('age')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('initializes values from field defaults', () => {
|
|
28
|
+
const f = makeBasicForm()
|
|
29
|
+
expect(f.values.name).toBe('Alice')
|
|
30
|
+
expect(f.values.age).toBe(25)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
describe('Form.setValues', () => {
|
|
35
|
+
it('updates only keys present in source', () => {
|
|
36
|
+
const f = makeBasicForm()
|
|
37
|
+
f.setValues({ name: 'Bob' })
|
|
38
|
+
expect(f.values.name).toBe('Bob')
|
|
39
|
+
expect(f.values.age).toBe(25)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('writes falsy string value', () => {
|
|
43
|
+
const f = makeBasicForm()
|
|
44
|
+
f.setValues({ name: '' })
|
|
45
|
+
expect(f.values.name).toBe('')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('writes zero', () => {
|
|
49
|
+
const f = makeBasicForm()
|
|
50
|
+
f.setValues({ age: 0 })
|
|
51
|
+
expect(f.values.age).toBe(0)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('Form.resetValues', () => {
|
|
56
|
+
it('restores all fields to initial values', () => {
|
|
57
|
+
const f = makeBasicForm()
|
|
58
|
+
f.setValues({ name: 'Bob', age: 30 })
|
|
59
|
+
f.resetValues()
|
|
60
|
+
expect(f.values.name).toBe('Alice')
|
|
61
|
+
expect(f.values.age).toBe(25)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('hides revealed errors', () => {
|
|
65
|
+
const f = makeValidationForm()
|
|
66
|
+
f.fields.email.revealError()
|
|
67
|
+
f.resetValues()
|
|
68
|
+
expect(f.fields.email.isErrorRevealed).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('Form.changed / isChanged', () => {
|
|
73
|
+
it('is false when no fields have changed', () => {
|
|
74
|
+
const f = makeBasicForm()
|
|
75
|
+
expect(f.changed()).toBe(false)
|
|
76
|
+
expect(f.isChanged).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('is true when any field changes', () => {
|
|
80
|
+
const f = makeBasicForm()
|
|
81
|
+
f.fields.name.setValue('Bob')
|
|
82
|
+
expect(f.changed()).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('is false after resetValues', () => {
|
|
86
|
+
const f = makeBasicForm()
|
|
87
|
+
f.setValues({ name: 'Bob' })
|
|
88
|
+
f.resetValues()
|
|
89
|
+
expect(f.changed()).toBe(false)
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('Form.isValid', () => {
|
|
94
|
+
it('is true when all fields pass validation', () => {
|
|
95
|
+
const f = form('all-valid', {
|
|
96
|
+
name: fields.text({ defaultValue: 'Alice', validate: zodValidator(z.string().min(1)) }),
|
|
97
|
+
})
|
|
98
|
+
expect(f.isValid).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('is false when any field fails validation', () => {
|
|
102
|
+
const f = makeValidationForm()
|
|
103
|
+
// email starts as undefined → fails z.string().email()
|
|
104
|
+
expect(f.isValid).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
describe('Form.validate', () => {
|
|
109
|
+
it('returns a result map keyed by field name', () => {
|
|
110
|
+
const f = makeValidationForm()
|
|
111
|
+
const results = f.validate()
|
|
112
|
+
expect('email' in results).toBe(true)
|
|
113
|
+
expect('bio' in results).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('reveals errors on all fields when revealErrors is true', () => {
|
|
117
|
+
const f = makeValidationForm()
|
|
118
|
+
expect(f.fields.email.isErrorRevealed).toBe(false)
|
|
119
|
+
f.validate({ revealErrors: true })
|
|
120
|
+
expect(f.fields.email.isErrorRevealed).toBe(true)
|
|
121
|
+
expect(f.fields.bio.isErrorRevealed).toBe(true)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('validates only specified fields when fields option is provided', () => {
|
|
125
|
+
const f = makeValidationForm()
|
|
126
|
+
const results = f.validate({ fields: ['email'] })
|
|
127
|
+
expect('email' in results).toBe(true)
|
|
128
|
+
expect('bio' in results).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
describe('Form.firstInvalid', () => {
|
|
133
|
+
it('returns undefined when all fields are valid', () => {
|
|
134
|
+
const f = form('all-valid', {
|
|
135
|
+
name: fields.text({ defaultValue: 'Alice', validate: zodValidator(z.string().min(1)) }),
|
|
136
|
+
})
|
|
137
|
+
expect(f.firstInvalid()).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('returns the first invalid field with its validation result', () => {
|
|
141
|
+
const f = makeValidationForm()
|
|
142
|
+
const result = f.firstInvalid()
|
|
143
|
+
expect(result).toBeDefined()
|
|
144
|
+
expect(result!.validation.isValid).toBe(false)
|
|
145
|
+
expect(result!.field).toBe(f.fields.email)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('Form.register', () => {
|
|
150
|
+
it('returns props for a known field', () => {
|
|
151
|
+
const f = makeBasicForm()
|
|
152
|
+
const props = f.register('name')
|
|
153
|
+
expect(props.field).toBe(f.fields.name)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('throws for an unknown field key', () => {
|
|
157
|
+
const f = makeBasicForm()
|
|
158
|
+
expect(() => f.register('nonexistent' as any)).toThrow()
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { fields } from '../fields'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { zodValidator } from '../validators'
|
|
5
|
+
import './setup'
|
|
6
|
+
|
|
7
|
+
describe('TextField', () => {
|
|
8
|
+
it('is valid for any non-empty string by default', () => {
|
|
9
|
+
const f = fields.text({ defaultValue: 'hello' })
|
|
10
|
+
expect(f.isValid).toBe(true)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('is valid for undefined by default (z.string().optional())', () => {
|
|
14
|
+
const f = fields.text({})
|
|
15
|
+
expect(f.isValid).toBe(true)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('fails with a custom validator', () => {
|
|
19
|
+
const f = fields.text({
|
|
20
|
+
defaultValue: 'not-an-email',
|
|
21
|
+
validate: zodValidator(z.string().email()),
|
|
22
|
+
})
|
|
23
|
+
expect(f.isValid).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('has _type TEXT', () => {
|
|
27
|
+
expect(fields.text({})._type).toBe('TEXT')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('NumberField', () => {
|
|
32
|
+
it('is valid for a number within default range', () => {
|
|
33
|
+
const f = fields.number({ defaultValue: 100 })
|
|
34
|
+
expect(f.isValid).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('is invalid when value is below min', () => {
|
|
38
|
+
const f = fields.number({ defaultValue: -1, min: 0 })
|
|
39
|
+
expect(f.isValid).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('is invalid when value exceeds max', () => {
|
|
43
|
+
const f = fields.number({ defaultValue: 100, max: 50 })
|
|
44
|
+
expect(f.isValid).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('supports array (range) defaultValue', () => {
|
|
48
|
+
const f = fields.number({ defaultValue: [10, 90] })
|
|
49
|
+
expect(f.isValid).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('array range is invalid when any element is out of bounds', () => {
|
|
53
|
+
const f = fields.number({ defaultValue: [-1, 90], min: 0 })
|
|
54
|
+
expect(f.isValid).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('has _type NUMBER', () => {
|
|
58
|
+
expect(fields.number({ defaultValue: 0 })._type).toBe('NUMBER')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('ListField', () => {
|
|
63
|
+
const makeItemField = () => fields.text({ validate: zodValidator(z.string().min(1)) })
|
|
64
|
+
|
|
65
|
+
it('is valid when empty', () => {
|
|
66
|
+
const f = fields.list({ item: makeItemField(), defaultValue: [] })
|
|
67
|
+
expect(f.isValid).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('is valid when all items pass item validator', () => {
|
|
71
|
+
const f = fields.list({ item: makeItemField(), defaultValue: ['a', 'b', 'c'] })
|
|
72
|
+
expect(f.isValid).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('is invalid when any item fails item validator', () => {
|
|
76
|
+
const f = fields.list({ item: makeItemField(), defaultValue: ['a', '', 'c'] })
|
|
77
|
+
expect(f.isValid).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('is always valid when item has no validator', () => {
|
|
81
|
+
const noValidatorItem = fields.text({})
|
|
82
|
+
const f = fields.list({ item: noValidatorItem, defaultValue: ['', ''] })
|
|
83
|
+
expect(f.isValid).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('has _type LIST', () => {
|
|
87
|
+
const f = fields.list({ item: fields.text({}), defaultValue: [] })
|
|
88
|
+
expect(f._type).toBe('LIST')
|
|
89
|
+
})
|
|
90
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { renderHook, act, waitFor } from '@testing-library/react'
|
|
3
|
+
import { useForm, form } from '../lib/Form'
|
|
4
|
+
import { fields } from '../fields'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { zodValidator } from '../validators'
|
|
7
|
+
import './setup'
|
|
8
|
+
|
|
9
|
+
describe('useForm', () => {
|
|
10
|
+
it('creates a form with the given id', () => {
|
|
11
|
+
const { result } = renderHook(() =>
|
|
12
|
+
useForm('my-form', { name: fields.text({ defaultValue: 'Alice' }) })
|
|
13
|
+
)
|
|
14
|
+
expect(result.current.id).toBe('my-form')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns a stable reference across re-renders', () => {
|
|
18
|
+
const { result, rerender } = renderHook(() =>
|
|
19
|
+
useForm('stable', { name: fields.text({ defaultValue: '' }) })
|
|
20
|
+
)
|
|
21
|
+
const first = result.current
|
|
22
|
+
rerender()
|
|
23
|
+
expect(result.current).toBe(first)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('sets field names from the definition keys', () => {
|
|
27
|
+
const { result } = renderHook(() =>
|
|
28
|
+
useForm('named', {
|
|
29
|
+
email: fields.text({}),
|
|
30
|
+
password: fields.text({}),
|
|
31
|
+
})
|
|
32
|
+
)
|
|
33
|
+
expect(result.current.fields.email.name).toBe('email')
|
|
34
|
+
expect(result.current.fields.password.name).toBe('password')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('Form.useInitialize', () => {
|
|
39
|
+
it('does not change values when called with null', () => {
|
|
40
|
+
const f = form('init-null', { name: fields.text({ defaultValue: 'original' }) })
|
|
41
|
+
renderHook(() => { f.useInitialize(null) })
|
|
42
|
+
expect(f.values.name).toBe('original')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not change values when called with undefined', () => {
|
|
46
|
+
const f = form('init-undef', { name: fields.text({ defaultValue: 'original' }) })
|
|
47
|
+
renderHook(() => { f.useInitialize(undefined) })
|
|
48
|
+
expect(f.values.name).toBe('original')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('sets values when non-null data is provided', async () => {
|
|
52
|
+
const f = form('init-values', { name: fields.text({ defaultValue: '' }) })
|
|
53
|
+
const initValues = { name: 'Bob' }
|
|
54
|
+
renderHook(() => { f.useInitialize(initValues) })
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
expect(f.values.name).toBe('Bob')
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('re-applies values when deps change', async () => {
|
|
61
|
+
const f = form('init-deps', { name: fields.text({ defaultValue: '' }) })
|
|
62
|
+
let trigger = 0
|
|
63
|
+
const { rerender } = renderHook(() => {
|
|
64
|
+
f.useInitialize({ name: `value-${trigger}` }, [trigger])
|
|
65
|
+
})
|
|
66
|
+
await waitFor(() => { expect(f.values.name).toBe('value-0') })
|
|
67
|
+
|
|
68
|
+
trigger = 1
|
|
69
|
+
rerender()
|
|
70
|
+
await waitFor(() => { expect(f.values.name).toBe('value-1') })
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('Form.useShared', () => {
|
|
75
|
+
it('returns the initial selected value', () => {
|
|
76
|
+
const f = form('shared-init', { count: fields.number({ defaultValue: 5 }) })
|
|
77
|
+
const { result } = renderHook(() => f.useShared((frm) => frm.values.count))
|
|
78
|
+
expect(result.current).toBe(5)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('updates when the underlying state changes', async () => {
|
|
82
|
+
const f = form('shared-update', { count: fields.number({ defaultValue: 0 }) })
|
|
83
|
+
const { result } = renderHook(() => f.useShared((frm) => frm.values.count))
|
|
84
|
+
expect(result.current).toBe(0)
|
|
85
|
+
|
|
86
|
+
act(() => { f.fields.count.setValue(42) })
|
|
87
|
+
|
|
88
|
+
await waitFor(() => { expect(result.current).toBe(42) })
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('does not reset on unmount', () => {
|
|
92
|
+
const f = form('shared-no-reset', { name: fields.text({ defaultValue: 'init' }) })
|
|
93
|
+
f.fields.name.setValue('changed')
|
|
94
|
+
const { unmount } = renderHook(() => f.useShared((frm) => frm.values.name))
|
|
95
|
+
unmount()
|
|
96
|
+
expect(f.values.name).toBe('changed')
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('Form.use (resets on unmount)', () => {
|
|
101
|
+
it('resets all field values when the component unmounts', () => {
|
|
102
|
+
const f = form('use-reset', { name: fields.text({ defaultValue: 'initial' }) })
|
|
103
|
+
f.fields.name.setValue('changed')
|
|
104
|
+
|
|
105
|
+
const { unmount } = renderHook(() => f.use((frm) => frm.values.name))
|
|
106
|
+
expect(f.values.name).toBe('changed')
|
|
107
|
+
|
|
108
|
+
unmount()
|
|
109
|
+
expect(f.values.name).toBe('initial')
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { afterEach } from 'bun:test'
|
|
2
|
+
import { cleanup } from '@testing-library/react'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
import { zodValidator } from '../validators'
|
|
5
|
+
import { fields } from '../fields'
|
|
6
|
+
import { form } from '../lib/Form'
|
|
7
|
+
|
|
8
|
+
export const makeTextField = (defaultValue = '') =>
|
|
9
|
+
fields.text({ defaultValue })
|
|
10
|
+
|
|
11
|
+
export const makeRequiredTextField = () =>
|
|
12
|
+
fields.text({
|
|
13
|
+
defaultValue: '',
|
|
14
|
+
validate: zodValidator(z.string().min(1, 'Required')),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const makeLoginForm = () =>
|
|
18
|
+
form('login', {
|
|
19
|
+
email: fields.text({ validate: zodValidator(z.string().email('Invalid email')) }),
|
|
20
|
+
password: fields.text({ validate: zodValidator(z.string().min(8, 'Too short')) }),
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
cleanup()
|
|
25
|
+
})
|
package/src/types/field.ts
CHANGED