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.
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=setup.d.ts.map
@@ -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,4 @@
1
+ import { z } from 'zod';
2
+ import type { Form, Values } from './Form';
3
+ export declare function validateZodSchema<T extends Values>(schema: z.ZodSchema<T>): (form: Form<T>) => void;
4
+ //# sourceMappingURL=validateZodSchema.d.ts.map
@@ -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
+ }