fieldwise 0.1.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/LICENSE +21 -0
- package/README.md +474 -0
- package/dist/Form.d.ts +63 -0
- package/dist/Form.d.ts.map +1 -0
- package/dist/Form.js +162 -0
- package/dist/changeHandlers.d.ts +3 -0
- package/dist/changeHandlers.d.ts.map +1 -0
- package/dist/changeHandlers.js +19 -0
- package/dist/errorHandlers.d.ts +3 -0
- package/dist/errorHandlers.d.ts.map +1 -0
- package/dist/errorHandlers.js +5 -0
- package/dist/fieldwise.d.ts +38 -0
- package/dist/fieldwise.d.ts.map +1 -0
- package/dist/fieldwise.js +86 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/logFormEvents.d.ts +3 -0
- package/dist/logFormEvents.d.ts.map +1 -0
- package/dist/logFormEvents.js +22 -0
- package/dist/test/Form.test.d.ts +2 -0
- package/dist/test/Form.test.d.ts.map +1 -0
- package/dist/test/Form.test.js +218 -0
- package/dist/test/fieldwise.test.d.ts +2 -0
- package/dist/test/fieldwise.test.d.ts.map +1 -0
- package/dist/test/fieldwise.test.js +177 -0
- package/dist/test/setup.d.ts +2 -0
- package/dist/test/setup.d.ts.map +1 -0
- package/dist/test/setup.js +9 -0
- package/dist/validateZodSchema.d.ts +4 -0
- package/dist/validateZodSchema.d.ts.map +1 -0
- package/dist/validateZodSchema.js +29 -0
- package/package.json +60 -0
package/dist/Form.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
export class Form {
|
|
2
|
+
constructor(initialValues) {
|
|
3
|
+
this.fieldSubscribers = new Map();
|
|
4
|
+
this.eventHandlers = new Map();
|
|
5
|
+
this.eventQueue = new Map();
|
|
6
|
+
this.emit = (event, ...args) => {
|
|
7
|
+
this.doEmit(event, ...args);
|
|
8
|
+
};
|
|
9
|
+
this.emitLater = (event, ...args) => {
|
|
10
|
+
setTimeout(() => {
|
|
11
|
+
this.doEmit(event, ...args);
|
|
12
|
+
}, 0);
|
|
13
|
+
};
|
|
14
|
+
this.initialValues = initialValues;
|
|
15
|
+
this.fields = this.valuesToFields(initialValues);
|
|
16
|
+
}
|
|
17
|
+
getValue(key) {
|
|
18
|
+
return this.fields[key].value;
|
|
19
|
+
}
|
|
20
|
+
getValues() {
|
|
21
|
+
return Object.entries(this.fields).reduce((acc, [key, field]) => {
|
|
22
|
+
acc[key] = field.value;
|
|
23
|
+
return acc;
|
|
24
|
+
}, {});
|
|
25
|
+
}
|
|
26
|
+
get(key) {
|
|
27
|
+
return this.fields[key];
|
|
28
|
+
}
|
|
29
|
+
setValue(key, value) {
|
|
30
|
+
if (key in this.fields && this.fields[key].value !== value) {
|
|
31
|
+
this.fields[key].value = value;
|
|
32
|
+
this.fields[key].error = null;
|
|
33
|
+
this.fields[key].isTouched = true;
|
|
34
|
+
this.notify(key, this.fields[key]);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
setValues(newValues) {
|
|
38
|
+
Object.keys(newValues).forEach((key) => {
|
|
39
|
+
this.setValue(key, newValues[key]);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
touch(key) {
|
|
43
|
+
const field = this.fields[key];
|
|
44
|
+
if (!field.isTouched) {
|
|
45
|
+
field.isTouched = true;
|
|
46
|
+
this.notify(key, field);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setError(key, error) {
|
|
50
|
+
if (this.fields[key].error !== error) {
|
|
51
|
+
this.fields[key].error = error;
|
|
52
|
+
this.notify(key, this.fields[key]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
setErrors(newErrors) {
|
|
56
|
+
Object.keys(this.fields).forEach((key) => {
|
|
57
|
+
this.setError(key, newErrors[key] || null);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
reset(snapshot) {
|
|
61
|
+
this.fields = this.valuesToFields(snapshot);
|
|
62
|
+
Object.entries(this.fields).forEach(([name, field]) => {
|
|
63
|
+
this.notify(name, field);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
getSlice(keys) {
|
|
67
|
+
return keys.reduce((acc, key) => {
|
|
68
|
+
acc[key] = this.fields[key];
|
|
69
|
+
return acc;
|
|
70
|
+
}, {});
|
|
71
|
+
}
|
|
72
|
+
subscribeField(key, callback) {
|
|
73
|
+
if (!this.fieldSubscribers.has(key)) {
|
|
74
|
+
this.fieldSubscribers.set(key, new Set());
|
|
75
|
+
}
|
|
76
|
+
const subscribers = this.fieldSubscribers.get(key);
|
|
77
|
+
subscribers.add(callback);
|
|
78
|
+
return () => {
|
|
79
|
+
subscribers.delete(callback);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
on(event, handler) {
|
|
83
|
+
if (!this.eventHandlers.has(event)) {
|
|
84
|
+
this.eventHandlers.set(event, new Set());
|
|
85
|
+
}
|
|
86
|
+
const handlers = this.eventHandlers.get(event);
|
|
87
|
+
handlers.add(handler);
|
|
88
|
+
// Process queued events when first handler is added
|
|
89
|
+
this.processQueuedEvents(event);
|
|
90
|
+
return () => {
|
|
91
|
+
handlers.delete(handler);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
once(event, handler) {
|
|
95
|
+
const wrapper = (...args) => {
|
|
96
|
+
handler(...args);
|
|
97
|
+
this.eventHandlers
|
|
98
|
+
.get(event)
|
|
99
|
+
?.delete(wrapper);
|
|
100
|
+
};
|
|
101
|
+
if (!this.eventHandlers.has(event)) {
|
|
102
|
+
this.eventHandlers.set(event, new Set());
|
|
103
|
+
}
|
|
104
|
+
this.eventHandlers
|
|
105
|
+
.get(event)
|
|
106
|
+
.add(wrapper);
|
|
107
|
+
// Process queued events when first handler is added
|
|
108
|
+
this.processQueuedEvents(event);
|
|
109
|
+
}
|
|
110
|
+
doEmit(event, ...args) {
|
|
111
|
+
const handlers = this.eventHandlers.get(event);
|
|
112
|
+
if (!handlers || handlers.size === 0) {
|
|
113
|
+
// Queue event if no handlers exist
|
|
114
|
+
if (!this.eventQueue.has(event)) {
|
|
115
|
+
this.eventQueue.set(event, []);
|
|
116
|
+
}
|
|
117
|
+
this.eventQueue.get(event).push(args);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Emit to handlers immediately
|
|
121
|
+
handlers.forEach((handler) => {
|
|
122
|
+
handler(...args);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
processQueuedEvents(event) {
|
|
127
|
+
const queue = this.eventQueue.get(event);
|
|
128
|
+
if (!queue || queue.length === 0)
|
|
129
|
+
return;
|
|
130
|
+
const handlers = this.eventHandlers.get(event);
|
|
131
|
+
// Process all queued events in FIFO order while handlers exist
|
|
132
|
+
while (queue.length > 0 && handlers && handlers.size > 0) {
|
|
133
|
+
const args = queue.shift();
|
|
134
|
+
handlers.forEach((handler) => {
|
|
135
|
+
handler(...args);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Clean up empty queue
|
|
139
|
+
if (queue.length === 0) {
|
|
140
|
+
this.eventQueue.delete(event);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
notify(key, field) {
|
|
144
|
+
const subscribers = this.fieldSubscribers.get(key);
|
|
145
|
+
if (subscribers) {
|
|
146
|
+
subscribers.forEach((callback) => {
|
|
147
|
+
callback(field);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
valuesToFields(values) {
|
|
152
|
+
return Object.keys(values).reduce((acc, key) => {
|
|
153
|
+
acc[key] = {
|
|
154
|
+
value: values[key],
|
|
155
|
+
error: null,
|
|
156
|
+
isTouched: false
|
|
157
|
+
};
|
|
158
|
+
return acc;
|
|
159
|
+
}, {});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
Form.debugMode = false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changeHandlers.d.ts","sourceRoot":"","sources":["../src/changeHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE3C,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAsBrE"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export default function changeHandlers(form) {
|
|
2
|
+
form.on('change', (name, value) => {
|
|
3
|
+
form.setValue(name, value);
|
|
4
|
+
});
|
|
5
|
+
form.on('changeSome', (newValues) => {
|
|
6
|
+
form.setValues(newValues);
|
|
7
|
+
});
|
|
8
|
+
form.on('touch', (name) => {
|
|
9
|
+
form.touch(name);
|
|
10
|
+
});
|
|
11
|
+
form.on('touchSome', (names) => {
|
|
12
|
+
names.forEach((name) => {
|
|
13
|
+
form.touch(name);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
form.on('reset', (values) => {
|
|
17
|
+
form.reset(values || form.initialValues);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errorHandlers.d.ts","sourceRoot":"","sources":["../src/errorHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE3C,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAIpE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Form } from './Form';
|
|
2
|
+
import type { Values, FieldSet, EmitFn } from './Form';
|
|
3
|
+
type EmitFnWithLater<T extends Values> = EmitFn<T> & {
|
|
4
|
+
later: EmitFn<T>;
|
|
5
|
+
};
|
|
6
|
+
type FormCommons<T extends Values> = {
|
|
7
|
+
emit: EmitFnWithLater<T>;
|
|
8
|
+
once: typeof Form.prototype.once;
|
|
9
|
+
isTouched: boolean;
|
|
10
|
+
i: <K extends keyof T>(key: K) => InputProps<K, T[K]>;
|
|
11
|
+
};
|
|
12
|
+
type InputProps<K, T> = {
|
|
13
|
+
name: K;
|
|
14
|
+
value: T;
|
|
15
|
+
onChange: (value: T) => void;
|
|
16
|
+
error: string | null;
|
|
17
|
+
};
|
|
18
|
+
type FormHooks<T extends Values> = {
|
|
19
|
+
useSlice<K extends keyof T>(keys: readonly K[]): FormCommons<T> & {
|
|
20
|
+
fields: FieldSet<Pick<T, K>>;
|
|
21
|
+
};
|
|
22
|
+
useForm(): FormCommons<T> & {
|
|
23
|
+
fields: FieldSet<T>;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type PluginFunction<T extends Values, TArgs extends unknown[] = []> = (form: Form<T>, ...args: TArgs) => void;
|
|
27
|
+
export declare class FormBuilder<T extends Values> {
|
|
28
|
+
private form;
|
|
29
|
+
constructor(initialValues: T);
|
|
30
|
+
get useSlice(): FormHooks<T>['useSlice'];
|
|
31
|
+
get useForm(): FormHooks<T>['useForm'];
|
|
32
|
+
use<TArgs extends unknown[]>(plugin: PluginFunction<T, TArgs>, ...args: TArgs): FormBuilder<T>;
|
|
33
|
+
hooks(): FormHooks<T>;
|
|
34
|
+
}
|
|
35
|
+
export declare const fieldwise: <T extends Values>(initialValues: T) => FormBuilder<T>;
|
|
36
|
+
export declare const getValues: <T extends Values>(fields: Form<T>["fields"]) => T;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=fieldwise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldwise.d.ts","sourceRoot":"","sources":["../src/fieldwise.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAK9B,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAsB,MAAM,QAAQ,CAAC;AAE3E,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG;IACnD,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;CAClB,CAAC;AAEF,KAAK,WAAW,CAAC,CAAC,SAAS,MAAM,IAAI;IACnC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;IACzB,IAAI,EAAE,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;IACjC,SAAS,EAAE,OAAO,CAAC;IACnB,CAAC,EAAE,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACvD,CAAC;AAEF,KAAK,UAAU,CAAC,CAAC,EAAE,CAAC,IAAI;IACtB,IAAI,EAAE,CAAC,CAAC;IACR,KAAK,EAAE,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC7B,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AACF,KAAK,SAAS,CAAC,CAAC,SAAS,MAAM,IAAI;IACjC,QAAQ,CAAC,CAAC,SAAS,MAAM,CAAC,EACxB,IAAI,EAAE,SAAS,CAAC,EAAE,GACjB,WAAW,CAAC,CAAC,CAAC,GAAG;QAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;KAAE,CAAC;IACrD,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,GAAG;QAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;KAAE,CAAC;CACrD,CAAC;AAEF,KAAK,cAAc,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,SAAS,OAAO,EAAE,GAAG,EAAE,IAAI,CACpE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,EACb,GAAG,IAAI,EAAE,KAAK,KACX,IAAI,CAAC;AAEV,qBAAa,WAAW,CAAC,CAAC,SAAS,MAAM;IACvC,OAAO,CAAC,IAAI,CAAU;gBAEV,aAAa,EAAE,CAAC;IAS5B,IAAI,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAwDvC;IAED,IAAI,OAAO,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAMrC;IAED,GAAG,CAAC,KAAK,SAAS,OAAO,EAAE,EACzB,MAAM,EAAE,cAAc,CAAC,CAAC,EAAE,KAAK,CAAC,EAChC,GAAG,IAAI,EAAE,KAAK,GACb,WAAW,CAAC,CAAC,CAAC;IAKjB,KAAK,IAAI,SAAS,CAAC,CAAC,CAAC;CAMtB;AAED,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EACxC,eAAe,CAAC,KACf,WAAW,CAAC,CAAC,CAEf,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,CAAC,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAG,CAKvE,CAAC"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useEffect, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { Form } from './Form';
|
|
3
|
+
import logFormEvents from './logFormEvents';
|
|
4
|
+
import changeHandlers from './changeHandlers';
|
|
5
|
+
import errorHandlers from './errorHandlers';
|
|
6
|
+
export class FormBuilder {
|
|
7
|
+
constructor(initialValues) {
|
|
8
|
+
this.form = new Form(initialValues);
|
|
9
|
+
if (Form.debugMode) {
|
|
10
|
+
logFormEvents(this.form);
|
|
11
|
+
}
|
|
12
|
+
changeHandlers(this.form);
|
|
13
|
+
errorHandlers(this.form);
|
|
14
|
+
}
|
|
15
|
+
get useSlice() {
|
|
16
|
+
return (keys) => {
|
|
17
|
+
const [fields, setFields] = useState(() => this.form.getSlice(keys));
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const unsubscribers = [];
|
|
20
|
+
let pendingUpdate = false;
|
|
21
|
+
const scheduleUpdate = () => {
|
|
22
|
+
if (!pendingUpdate) {
|
|
23
|
+
pendingUpdate = true;
|
|
24
|
+
queueMicrotask(() => {
|
|
25
|
+
pendingUpdate = false;
|
|
26
|
+
setFields(this.form.getSlice(keys));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
keys.forEach((key) => {
|
|
31
|
+
const unsubscribe = this.form.subscribeField(key, scheduleUpdate);
|
|
32
|
+
unsubscribers.push(unsubscribe);
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
|
36
|
+
};
|
|
37
|
+
}, [keys]);
|
|
38
|
+
const emit = useMemo(() => {
|
|
39
|
+
const emitFn = this.form.emit.bind(this.form);
|
|
40
|
+
return Object.assign(emitFn, {
|
|
41
|
+
later: this.form.emitLater.bind(this.form)
|
|
42
|
+
});
|
|
43
|
+
}, []);
|
|
44
|
+
const once = useMemo(() => this.form.once.bind(this.form), []);
|
|
45
|
+
const isTouched = useMemo(() => {
|
|
46
|
+
return keys.some((key) => this.form.get(key).isTouched);
|
|
47
|
+
}, [keys]);
|
|
48
|
+
const inputProps = useCallback((name) => {
|
|
49
|
+
return {
|
|
50
|
+
name,
|
|
51
|
+
value: this.form.getValue(name),
|
|
52
|
+
onChange: (value) => {
|
|
53
|
+
this.form.emit('change', name, value);
|
|
54
|
+
},
|
|
55
|
+
error: this.form.get(name).error
|
|
56
|
+
};
|
|
57
|
+
}, []);
|
|
58
|
+
return { fields, isTouched, emit, once, i: inputProps };
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
get useForm() {
|
|
62
|
+
return () => {
|
|
63
|
+
const allKeys = Object.keys(this.form['initialValues']);
|
|
64
|
+
return this.useSlice(allKeys);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
use(plugin, ...args) {
|
|
68
|
+
plugin(this.form, ...args);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
hooks() {
|
|
72
|
+
return {
|
|
73
|
+
useSlice: this.useSlice,
|
|
74
|
+
useForm: this.useForm
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export const fieldwise = (initialValues) => {
|
|
79
|
+
return new FormBuilder(initialValues);
|
|
80
|
+
};
|
|
81
|
+
export const getValues = (fields) => {
|
|
82
|
+
return Object.entries(fields).reduce((acc, [key, field]) => {
|
|
83
|
+
acc[key] = field.value;
|
|
84
|
+
return acc;
|
|
85
|
+
}, {});
|
|
86
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC;AACpC,cAAc,QAAQ,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logFormEvents.d.ts","sourceRoot":"","sources":["../src/logFormEvents.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAA8B,MAAM,QAAQ,CAAC;AAevE,MAAM,CAAC,OAAO,UAAU,aAAa,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAa3E"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Form as FormClass } from './Form';
|
|
2
|
+
const allEventTypes = [
|
|
3
|
+
'change',
|
|
4
|
+
'changeSome',
|
|
5
|
+
'reset',
|
|
6
|
+
'errors',
|
|
7
|
+
'validate',
|
|
8
|
+
'validated'
|
|
9
|
+
];
|
|
10
|
+
const isDebugModeObject = (mode) => {
|
|
11
|
+
return typeof mode === 'object' && mode !== null && Array.isArray(mode.only);
|
|
12
|
+
};
|
|
13
|
+
export default function logFormEvents(form) {
|
|
14
|
+
const eventsToLog = isDebugModeObject(FormClass.debugMode)
|
|
15
|
+
? FormClass.debugMode.only
|
|
16
|
+
: allEventTypes;
|
|
17
|
+
eventsToLog.forEach((eventType) => {
|
|
18
|
+
form.on(eventType, (...args) => {
|
|
19
|
+
console.log(`[Form Event] ${eventType}:`, ...(args.length === 0 ? ['[no payload]'] : args));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Form.test.d.ts","sourceRoot":"","sources":["../../src/test/Form.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { FormBuilder } from '../fieldwise';
|
|
3
|
+
describe('Form', () => {
|
|
4
|
+
describe('initialization', () => {
|
|
5
|
+
it('should initialize with provided values', () => {
|
|
6
|
+
const initialValues = { name: 'John', email: 'john@example.com' };
|
|
7
|
+
const builder = new FormBuilder(initialValues);
|
|
8
|
+
const form = builder.form;
|
|
9
|
+
expect(form.getValues()).toEqual(initialValues);
|
|
10
|
+
});
|
|
11
|
+
it('should set all fields as not touched initially', () => {
|
|
12
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
13
|
+
const form = builder.form;
|
|
14
|
+
expect(form.get('name').isTouched).toBe(false);
|
|
15
|
+
expect(form.get('email').isTouched).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
it('should set all field errors to null initially', () => {
|
|
18
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
19
|
+
const form = builder.form;
|
|
20
|
+
expect(form.get('name').error).toBe(null);
|
|
21
|
+
expect(form.get('email').error).toBe(null);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
describe('change event', () => {
|
|
25
|
+
it('should update field value on change', () => {
|
|
26
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
27
|
+
const form = builder.form;
|
|
28
|
+
form.emit('change', 'name', 'Jane');
|
|
29
|
+
expect(form.getValue('name')).toBe('Jane');
|
|
30
|
+
});
|
|
31
|
+
it('should mark field as touched on change', () => {
|
|
32
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
33
|
+
const form = builder.form;
|
|
34
|
+
form.emit('change', 'name', 'Jane');
|
|
35
|
+
expect(form.get('name').isTouched).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
it('should trigger change event listeners', () => {
|
|
38
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
39
|
+
const form = builder.form;
|
|
40
|
+
const listener = vi.fn();
|
|
41
|
+
form.on('change', listener);
|
|
42
|
+
form.emit('change', 'name', 'Jane');
|
|
43
|
+
expect(listener).toHaveBeenCalledWith('name', 'Jane');
|
|
44
|
+
});
|
|
45
|
+
it('should notify field subscribers', () => {
|
|
46
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
47
|
+
const form = builder.form;
|
|
48
|
+
const subscriber = vi.fn();
|
|
49
|
+
form.subscribeField('name', subscriber);
|
|
50
|
+
form.emit('change', 'name', 'Jane');
|
|
51
|
+
expect(subscriber).toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('changeSome event', () => {
|
|
55
|
+
it('should update multiple fields at once', () => {
|
|
56
|
+
const builder = new FormBuilder({ name: '', email: '', age: 0 });
|
|
57
|
+
const form = builder.form;
|
|
58
|
+
form.emit('changeSome', { name: 'Jane', email: 'jane@example.com' });
|
|
59
|
+
expect(form.getValue('name')).toBe('Jane');
|
|
60
|
+
expect(form.getValue('email')).toBe('jane@example.com');
|
|
61
|
+
expect(form.getValue('age')).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
it('should mark updated fields as touched', () => {
|
|
64
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
65
|
+
const form = builder.form;
|
|
66
|
+
form.emit('changeSome', { name: 'Jane' });
|
|
67
|
+
expect(form.get('name').isTouched).toBe(true);
|
|
68
|
+
expect(form.get('email').isTouched).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe('touch event', () => {
|
|
72
|
+
it('should mark field as touched without changing value', () => {
|
|
73
|
+
const builder = new FormBuilder({ name: 'John', email: '' });
|
|
74
|
+
const form = builder.form;
|
|
75
|
+
expect(form.get('name').isTouched).toBe(false);
|
|
76
|
+
expect(form.getValue('name')).toBe('John');
|
|
77
|
+
form.emit('touch', 'name');
|
|
78
|
+
expect(form.get('name').isTouched).toBe(true);
|
|
79
|
+
expect(form.getValue('name')).toBe('John');
|
|
80
|
+
});
|
|
81
|
+
it('should notify field subscribers', () => {
|
|
82
|
+
const builder = new FormBuilder({ name: '' });
|
|
83
|
+
const form = builder.form;
|
|
84
|
+
const subscriber = vi.fn();
|
|
85
|
+
form.subscribeField('name', subscriber);
|
|
86
|
+
form.emit('touch', 'name');
|
|
87
|
+
expect(subscriber).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
it('should not notify if already touched', () => {
|
|
90
|
+
const builder = new FormBuilder({ name: '' });
|
|
91
|
+
const form = builder.form;
|
|
92
|
+
const subscriber = vi.fn();
|
|
93
|
+
form.emit('touch', 'name');
|
|
94
|
+
form.subscribeField('name', subscriber);
|
|
95
|
+
form.emit('touch', 'name');
|
|
96
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('touchSome event', () => {
|
|
100
|
+
it('should mark multiple fields as touched', () => {
|
|
101
|
+
const builder = new FormBuilder({ name: '', email: '', age: 0 });
|
|
102
|
+
const form = builder.form;
|
|
103
|
+
form.emit('touchSome', ['name', 'email']);
|
|
104
|
+
expect(form.get('name').isTouched).toBe(true);
|
|
105
|
+
expect(form.get('email').isTouched).toBe(true);
|
|
106
|
+
expect(form.get('age').isTouched).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('should not change field values', () => {
|
|
109
|
+
const builder = new FormBuilder({
|
|
110
|
+
name: 'John',
|
|
111
|
+
email: 'john@example.com'
|
|
112
|
+
});
|
|
113
|
+
const form = builder.form;
|
|
114
|
+
form.emit('touchSome', ['name', 'email']);
|
|
115
|
+
expect(form.getValue('name')).toBe('John');
|
|
116
|
+
expect(form.getValue('email')).toBe('john@example.com');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('reset event', () => {
|
|
120
|
+
it('should reset to initial values', () => {
|
|
121
|
+
const builder = new FormBuilder({
|
|
122
|
+
name: 'John',
|
|
123
|
+
email: 'john@example.com'
|
|
124
|
+
});
|
|
125
|
+
const form = builder.form;
|
|
126
|
+
form.emit('change', 'name', 'Jane');
|
|
127
|
+
form.emit('reset');
|
|
128
|
+
expect(form.getValue('name')).toBe('John');
|
|
129
|
+
});
|
|
130
|
+
it('should reset to provided snapshot', () => {
|
|
131
|
+
const builder = new FormBuilder({
|
|
132
|
+
name: 'John',
|
|
133
|
+
email: 'john@example.com'
|
|
134
|
+
});
|
|
135
|
+
const form = builder.form;
|
|
136
|
+
form.emit('reset', { name: 'Jane', email: 'jane@example.com' });
|
|
137
|
+
expect(form.getValue('name')).toBe('Jane');
|
|
138
|
+
expect(form.getValue('email')).toBe('jane@example.com');
|
|
139
|
+
});
|
|
140
|
+
it('should clear touched state', () => {
|
|
141
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
142
|
+
const form = builder.form;
|
|
143
|
+
form.emit('change', 'name', 'Jane');
|
|
144
|
+
expect(form.get('name').isTouched).toBe(true);
|
|
145
|
+
form.emit('reset');
|
|
146
|
+
expect(form.get('name').isTouched).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it('should clear errors', () => {
|
|
149
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
150
|
+
const form = builder.form;
|
|
151
|
+
form.emit('errors', { name: 'Required' });
|
|
152
|
+
expect(form.get('name').error).toBe('Required');
|
|
153
|
+
form.emit('reset');
|
|
154
|
+
expect(form.get('name').error).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('once method', () => {
|
|
158
|
+
it('should trigger handler only once', () => {
|
|
159
|
+
const builder = new FormBuilder({ name: '' });
|
|
160
|
+
const form = builder.form;
|
|
161
|
+
const handler = vi.fn();
|
|
162
|
+
form.once('change', handler);
|
|
163
|
+
form.emit('change', 'name', 'First');
|
|
164
|
+
form.emit('change', 'name', 'Second');
|
|
165
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
166
|
+
expect(handler).toHaveBeenCalledWith('name', 'First');
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('field subscriptions', () => {
|
|
170
|
+
it('should notify subscriber when field changes', () => {
|
|
171
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
172
|
+
const form = builder.form;
|
|
173
|
+
const subscriber = vi.fn();
|
|
174
|
+
form.subscribeField('name', subscriber);
|
|
175
|
+
form.emit('change', 'name', 'Jane');
|
|
176
|
+
expect(subscriber).toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
it('should not notify subscriber for other field changes', () => {
|
|
179
|
+
const builder = new FormBuilder({ name: '', email: '' });
|
|
180
|
+
const form = builder.form;
|
|
181
|
+
const subscriber = vi.fn();
|
|
182
|
+
form.subscribeField('name', subscriber);
|
|
183
|
+
form.emit('change', 'email', 'jane@example.com');
|
|
184
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
185
|
+
});
|
|
186
|
+
it('should allow unsubscribing', () => {
|
|
187
|
+
const builder = new FormBuilder({ name: '' });
|
|
188
|
+
const form = builder.form;
|
|
189
|
+
const subscriber = vi.fn();
|
|
190
|
+
const unsubscribe = form.subscribeField('name', subscriber);
|
|
191
|
+
unsubscribe();
|
|
192
|
+
form.emit('change', 'name', 'Jane');
|
|
193
|
+
expect(subscriber).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('getSlice', () => {
|
|
197
|
+
it('should return only requested fields', () => {
|
|
198
|
+
const builder = new FormBuilder({
|
|
199
|
+
name: 'John',
|
|
200
|
+
email: 'john@example.com',
|
|
201
|
+
age: 30
|
|
202
|
+
});
|
|
203
|
+
const form = builder.form;
|
|
204
|
+
const slice = form.getSlice(['name', 'email']);
|
|
205
|
+
expect(slice).toHaveProperty('name');
|
|
206
|
+
expect(slice).toHaveProperty('email');
|
|
207
|
+
expect(slice).not.toHaveProperty('age');
|
|
208
|
+
});
|
|
209
|
+
it('should include field metadata', () => {
|
|
210
|
+
const builder = new FormBuilder({ name: 'John' });
|
|
211
|
+
const form = builder.form;
|
|
212
|
+
const slice = form.getSlice(['name']);
|
|
213
|
+
expect(slice.name).toHaveProperty('value', 'John');
|
|
214
|
+
expect(slice.name).toHaveProperty('error', null);
|
|
215
|
+
expect(slice.name).toHaveProperty('isTouched', false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fieldwise.test.d.ts","sourceRoot":"","sources":["../../src/test/fieldwise.test.tsx"],"names":[],"mappings":""}
|