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/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,3 @@
1
+ import type { Values, Form } from './Form';
2
+ export default function changeHandlers<T extends Values>(form: Form<T>): void;
3
+ //# sourceMappingURL=changeHandlers.d.ts.map
@@ -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,3 @@
1
+ import type { Values, Form } from './Form';
2
+ export default function errorHandlers<T extends Values>(form: Form<T>): void;
3
+ //# sourceMappingURL=errorHandlers.d.ts.map
@@ -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,5 @@
1
+ export default function errorHandlers(form) {
2
+ form.on('errors', (newErrors) => {
3
+ form.setErrors(newErrors);
4
+ });
5
+ }
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ export * from './fieldwise';
2
+ export * from './validateZodSchema';
3
+ export * from './Form';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from './fieldwise';
2
+ export * from './validateZodSchema';
3
+ export * from './Form';
@@ -0,0 +1,3 @@
1
+ import type { Values, Form } from './Form';
2
+ export default function logFormEvents<T extends Values>(form: Form<T>): void;
3
+ //# sourceMappingURL=logFormEvents.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Form.test.d.ts.map
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=fieldwise.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldwise.test.d.ts","sourceRoot":"","sources":["../../src/test/fieldwise.test.tsx"],"names":[],"mappings":""}