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
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { renderHook, act } from '@testing-library/react';
|
|
3
|
+
import { fieldwise } from '../fieldwise';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { validateZodSchema } from '../validateZodSchema';
|
|
6
|
+
describe('fieldwise', () => {
|
|
7
|
+
describe('useForm hook', () => {
|
|
8
|
+
it('should return initial values', () => {
|
|
9
|
+
const { useForm } = fieldwise({ name: '', email: '' }).hooks();
|
|
10
|
+
const { result } = renderHook(() => useForm());
|
|
11
|
+
expect(result.current.fields.name.value).toBe('');
|
|
12
|
+
expect(result.current.fields.email.value).toBe('');
|
|
13
|
+
});
|
|
14
|
+
it('should update field value', () => {
|
|
15
|
+
const { useForm } = fieldwise({ name: '' }).hooks();
|
|
16
|
+
const { result } = renderHook(() => useForm());
|
|
17
|
+
act(() => {
|
|
18
|
+
result.current.emit('change', 'name', 'John');
|
|
19
|
+
});
|
|
20
|
+
expect(result.current.fields.name.value).toBe('John');
|
|
21
|
+
});
|
|
22
|
+
it('should mark field as touched after change', async () => {
|
|
23
|
+
const { useForm } = fieldwise({ name: '' }).hooks();
|
|
24
|
+
const { result } = renderHook(() => useForm());
|
|
25
|
+
expect(result.current.isTouched).toBe(false);
|
|
26
|
+
await act(async () => {
|
|
27
|
+
result.current.emit('change', 'name', 'John');
|
|
28
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
29
|
+
});
|
|
30
|
+
expect(result.current.isTouched).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('should reset form', async () => {
|
|
33
|
+
const { useForm } = fieldwise({ name: 'John' }).hooks();
|
|
34
|
+
const { result } = renderHook(() => useForm());
|
|
35
|
+
await act(async () => {
|
|
36
|
+
result.current.emit('change', 'name', 'Jane');
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
38
|
+
});
|
|
39
|
+
expect(result.current.fields.name.value).toBe('Jane');
|
|
40
|
+
await act(async () => {
|
|
41
|
+
result.current.emit('reset');
|
|
42
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
43
|
+
});
|
|
44
|
+
expect(result.current.fields.name.value).toBe('John');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('useSlice hook', () => {
|
|
48
|
+
it('should only include specified fields', () => {
|
|
49
|
+
const { useSlice } = fieldwise({ name: '', email: '', age: 0 }).hooks();
|
|
50
|
+
const { result } = renderHook(() => useSlice(['name', 'email']));
|
|
51
|
+
expect(result.current.fields).toHaveProperty('name');
|
|
52
|
+
expect(result.current.fields).toHaveProperty('email');
|
|
53
|
+
expect(result.current.fields).not.toHaveProperty('age');
|
|
54
|
+
});
|
|
55
|
+
it('should update when subscribed field changes', () => {
|
|
56
|
+
const { useSlice } = fieldwise({ name: '', email: '' }).hooks();
|
|
57
|
+
const { result } = renderHook(() => useSlice(['name']));
|
|
58
|
+
act(() => {
|
|
59
|
+
result.current.emit('change', 'name', 'John');
|
|
60
|
+
});
|
|
61
|
+
expect(result.current.fields.name.value).toBe('John');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe('input helper (i)', () => {
|
|
65
|
+
it('should generate input props', () => {
|
|
66
|
+
const { useForm } = fieldwise({ email: '' }).hooks();
|
|
67
|
+
const { result } = renderHook(() => useForm());
|
|
68
|
+
const props = result.current.i('email');
|
|
69
|
+
expect(props.name).toBe('email');
|
|
70
|
+
expect(props.value).toBe('');
|
|
71
|
+
expect(props.error).toBe(null);
|
|
72
|
+
expect(typeof props.onChange).toBe('function');
|
|
73
|
+
});
|
|
74
|
+
it('should update value through onChange', () => {
|
|
75
|
+
const { useForm } = fieldwise({ email: '' }).hooks();
|
|
76
|
+
const { result } = renderHook(() => useForm());
|
|
77
|
+
act(() => {
|
|
78
|
+
result.current.i('email').onChange('test@example.com');
|
|
79
|
+
});
|
|
80
|
+
expect(result.current.fields.email.value).toBe('test@example.com');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('validation', () => {
|
|
84
|
+
it('should validate with Zod schema', async () => {
|
|
85
|
+
const schema = z.object({
|
|
86
|
+
email: z.string().email('Invalid email')
|
|
87
|
+
});
|
|
88
|
+
const { useForm } = fieldwise({ email: '' })
|
|
89
|
+
.use(validateZodSchema(schema))
|
|
90
|
+
.hooks();
|
|
91
|
+
const { result } = renderHook(() => useForm());
|
|
92
|
+
await act(async () => {
|
|
93
|
+
let errors = null;
|
|
94
|
+
result.current.once('validated', ({ errors: validationErrors }) => {
|
|
95
|
+
errors = validationErrors;
|
|
96
|
+
if (validationErrors) {
|
|
97
|
+
result.current.emit('errors', validationErrors);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
result.current.emit('validate');
|
|
101
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
102
|
+
});
|
|
103
|
+
expect(result.current.fields.email.error).toBe('Invalid email');
|
|
104
|
+
});
|
|
105
|
+
it('should clear errors on valid input', async () => {
|
|
106
|
+
const schema = z.object({
|
|
107
|
+
email: z.string().email('Invalid email')
|
|
108
|
+
});
|
|
109
|
+
const { useForm } = fieldwise({ email: '' })
|
|
110
|
+
.use(validateZodSchema(schema))
|
|
111
|
+
.hooks();
|
|
112
|
+
const { result } = renderHook(() => useForm());
|
|
113
|
+
// First, validate with invalid email
|
|
114
|
+
await act(async () => {
|
|
115
|
+
result.current.once('validated', ({ errors: validationErrors }) => {
|
|
116
|
+
if (validationErrors) {
|
|
117
|
+
result.current.emit('errors', validationErrors);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
result.current.emit('validate');
|
|
121
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
122
|
+
});
|
|
123
|
+
expect(result.current.fields.email.error).toBe('Invalid email');
|
|
124
|
+
// Then, update to valid email and validate again
|
|
125
|
+
await act(async () => {
|
|
126
|
+
result.current.emit('change', 'email', 'valid@example.com');
|
|
127
|
+
result.current.once('validated', ({ errors: validationErrors }) => {
|
|
128
|
+
if (validationErrors) {
|
|
129
|
+
result.current.emit('errors', validationErrors);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
result.current.emit('validate');
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
134
|
+
});
|
|
135
|
+
expect(result.current.fields.email.error).toBe(null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('touch events', () => {
|
|
139
|
+
it('should mark field as touched', async () => {
|
|
140
|
+
const { useForm } = fieldwise({ name: 'John' }).hooks();
|
|
141
|
+
const { result } = renderHook(() => useForm());
|
|
142
|
+
expect(result.current.fields.name.isTouched).toBe(false);
|
|
143
|
+
await act(async () => {
|
|
144
|
+
result.current.emit('touch', 'name');
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
146
|
+
});
|
|
147
|
+
expect(result.current.fields.name.isTouched).toBe(true);
|
|
148
|
+
expect(result.current.fields.name.value).toBe('John');
|
|
149
|
+
});
|
|
150
|
+
it('should mark multiple fields as touched', async () => {
|
|
151
|
+
const { useForm } = fieldwise({ name: '', email: '', age: 0 }).hooks();
|
|
152
|
+
const { result } = renderHook(() => useForm());
|
|
153
|
+
await act(async () => {
|
|
154
|
+
result.current.emit('touchSome', ['name', 'email']);
|
|
155
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
156
|
+
});
|
|
157
|
+
expect(result.current.fields.name.isTouched).toBe(true);
|
|
158
|
+
expect(result.current.fields.email.isTouched).toBe(true);
|
|
159
|
+
expect(result.current.fields.age.isTouched).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('emit.later', () => {
|
|
163
|
+
it('should defer event to microtask', async () => {
|
|
164
|
+
const { useForm } = fieldwise({ name: '' }).hooks();
|
|
165
|
+
const { result } = renderHook(() => useForm());
|
|
166
|
+
let validationRan = false;
|
|
167
|
+
result.current.once('validate', () => {
|
|
168
|
+
validationRan = true;
|
|
169
|
+
});
|
|
170
|
+
result.current.emit.later('validate');
|
|
171
|
+
expect(validationRan).toBe(false);
|
|
172
|
+
// Wait for setTimeout to execute
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
174
|
+
expect(validationRan).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../src/test/setup.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { expect, afterEach } from 'vitest';
|
|
2
|
+
import { cleanup } from '@testing-library/react';
|
|
3
|
+
import * as matchers from '@testing-library/jest-dom/matchers';
|
|
4
|
+
// Extend Vitest's expect with jest-dom matchers
|
|
5
|
+
expect.extend(matchers);
|
|
6
|
+
// Cleanup after each test
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
cleanup();
|
|
9
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateZodSchema.d.ts","sourceRoot":"","sources":["../src/validateZodSchema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAEnD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IACvD,MAAM,IAAI,CAAC,CAAC,CAAC,KAAG,IAAI,CA6BtC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function validateZodSchema(schema) {
|
|
2
|
+
return function (form) {
|
|
3
|
+
form.on('validate', () => {
|
|
4
|
+
const values = form.getValues();
|
|
5
|
+
const result = schema.safeParse(values);
|
|
6
|
+
if (result.success) {
|
|
7
|
+
// Validation successful - emit with null errors
|
|
8
|
+
form.emit('validated', {
|
|
9
|
+
values,
|
|
10
|
+
errors: null
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
// Validation failed - convert Zod errors to our format
|
|
15
|
+
const errors = {};
|
|
16
|
+
result.error.issues.forEach((issue) => {
|
|
17
|
+
const path = issue.path[0];
|
|
18
|
+
if (path && typeof path === 'string' && !errors[path]) {
|
|
19
|
+
errors[path] = issue.message;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
form.emit('validated', {
|
|
23
|
+
values,
|
|
24
|
+
errors
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fieldwise",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Type-safe, reactive form management for React with fine-grained field subscriptions",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/akuzko/fieldwise.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/akuzko/fieldwise/issues"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc",
|
|
28
|
+
"test": "vitest",
|
|
29
|
+
"test:ui": "vitest --ui",
|
|
30
|
+
"test:coverage": "vitest --coverage",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"react",
|
|
35
|
+
"forms",
|
|
36
|
+
"validation",
|
|
37
|
+
"zod",
|
|
38
|
+
"typescript"
|
|
39
|
+
],
|
|
40
|
+
"author": "Artem Kuzko <a.kuzko@gmail.com>",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@testing-library/jest-dom": "^6.1.5",
|
|
48
|
+
"@testing-library/react": "^16.1.0",
|
|
49
|
+
"@testing-library/user-event": "^14.5.1",
|
|
50
|
+
"@types/react": "^18.0.0",
|
|
51
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
52
|
+
"@vitest/ui": "^4.0.16",
|
|
53
|
+
"jsdom": "^23.0.1",
|
|
54
|
+
"react": "^19.0.0",
|
|
55
|
+
"react-dom": "^19.0.0",
|
|
56
|
+
"typescript": "^5.0.0",
|
|
57
|
+
"vitest": "^4.0.16",
|
|
58
|
+
"zod": "^4.3.5"
|
|
59
|
+
}
|
|
60
|
+
}
|