mayoiga 0.1.7 → 0.1.8
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/.github/workflows/checker.yml +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +14 -2
- package/dist/index.test.js +48 -1
- package/package.json +23 -18
- package/src/index.test.tsx +66 -1
- package/src/index.ts +51 -30
- package/tsconfig.json +0 -2
- package/yarn.lock +1281 -849
package/dist/index.d.ts
CHANGED
|
@@ -101,14 +101,18 @@ export type Controller<State extends StateRestriction> = {
|
|
|
101
101
|
};
|
|
102
102
|
type FormHook<State extends StateRestriction, Schema extends ZodType<any, any, any>> = {
|
|
103
103
|
controller: Controller<State>;
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* @param handler handling logic
|
|
106
|
+
* @returns created handler is always return Promise, because of the schema validation run it asynchronously.
|
|
107
|
+
*/
|
|
108
|
+
handleSubmit: <R>(handler: (e: BaseSyntheticEvent) => (val: {
|
|
105
109
|
success: true;
|
|
106
110
|
data: zodInfer<Schema>;
|
|
107
111
|
error: undefined;
|
|
108
112
|
} | {
|
|
109
113
|
success: false;
|
|
110
114
|
error: ReadonlyArray<ZodIssue>;
|
|
111
|
-
}) =>
|
|
115
|
+
}) => R) => (e: BaseSyntheticEvent) => Promise<R>;
|
|
112
116
|
api: Subscriber<FullFormState<State>, FormControllerBehavior<State>>;
|
|
113
117
|
} & Controller<State>;
|
|
114
118
|
/**
|
package/dist/index.js
CHANGED
|
@@ -99,8 +99,20 @@ export function createFormHook(initialState, schema) {
|
|
|
99
99
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
100
100
|
return eventHandled({ success: true, data: result.data, error: undefined });
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const handled = eventHandled({ success: false, error: result.error.issues });
|
|
103
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
104
|
+
if (typeof handled['then'] === 'function') {
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
106
|
+
return handled.then((x) => {
|
|
107
|
+
store.actions.handleIssues(result.error.issues);
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
109
|
+
return x;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
store.actions.handleIssues(result.error.issues);
|
|
114
|
+
return handled;
|
|
115
|
+
}
|
|
104
116
|
});
|
|
105
117
|
};
|
|
106
118
|
},
|
package/dist/index.test.js
CHANGED
|
@@ -53,11 +53,15 @@ test('zod parse value before submit', async () => {
|
|
|
53
53
|
else {
|
|
54
54
|
expect('should not called').toBe(result.error);
|
|
55
55
|
}
|
|
56
|
+
return Promise.resolve('returns value');
|
|
56
57
|
});
|
|
57
58
|
const Top = () => {
|
|
58
59
|
return (React.createElement("div", null,
|
|
59
60
|
React.createElement(AgeInputComponent, { controller: TestFormHook }),
|
|
60
|
-
React.createElement("button", { "data-testid": "submit-button", onClick:
|
|
61
|
+
React.createElement("button", { "data-testid": "submit-button", onClick: async (e) => {
|
|
62
|
+
const returnedValue = await handleSubmitTester(e);
|
|
63
|
+
expect(returnedValue).toBe('returns value');
|
|
64
|
+
} })));
|
|
61
65
|
};
|
|
62
66
|
render(React.createElement(Top, null));
|
|
63
67
|
const ageInput = screen.getByTestId('age-input');
|
|
@@ -76,6 +80,49 @@ test('zod parse value before submit', async () => {
|
|
|
76
80
|
});
|
|
77
81
|
});
|
|
78
82
|
});
|
|
83
|
+
test('able to handle parse error on submit', async () => {
|
|
84
|
+
const TestFormHook = createTestHook();
|
|
85
|
+
const INPUT_AGE = 'aaaa';
|
|
86
|
+
const EXPECTED_ISSUE = {
|
|
87
|
+
code: 'custom',
|
|
88
|
+
message: 'Invalid input',
|
|
89
|
+
path: ['age'],
|
|
90
|
+
};
|
|
91
|
+
const handleSubmitTester = TestFormHook.handleSubmit((e) => (result) => {
|
|
92
|
+
if (result.success) {
|
|
93
|
+
expect('should not succeed').toBe(false);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
expect(result.success).toBe(false);
|
|
97
|
+
expect(result.error).toStrictEqual([EXPECTED_ISSUE]);
|
|
98
|
+
}
|
|
99
|
+
return Promise.resolve('returns value');
|
|
100
|
+
});
|
|
101
|
+
const Top = () => {
|
|
102
|
+
return (React.createElement("div", null,
|
|
103
|
+
React.createElement(AgeInputComponent, { controller: TestFormHook }),
|
|
104
|
+
React.createElement("button", { "data-testid": "submit-button", onClick: async (e) => {
|
|
105
|
+
const returnedValue = await handleSubmitTester(e);
|
|
106
|
+
expect(returnedValue).toBe('returns value');
|
|
107
|
+
} })));
|
|
108
|
+
};
|
|
109
|
+
render(React.createElement(Top, null));
|
|
110
|
+
const ageInput = screen.getByTestId('age-input');
|
|
111
|
+
await userEvent.type(ageInput, INPUT_AGE);
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
const ageInput = screen.getByTestId('age-input');
|
|
114
|
+
expect(ageInput.value).toBe(INPUT_AGE);
|
|
115
|
+
});
|
|
116
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
117
|
+
await userEvent.click(submitButton);
|
|
118
|
+
await waitFor(() => {
|
|
119
|
+
console.log('waiting');
|
|
120
|
+
TestFormHook.api.actions.peek((s) => {
|
|
121
|
+
expect(s.value.age).toBe(INPUT_AGE);
|
|
122
|
+
expect(s.errors.age).toBe(EXPECTED_ISSUE.message);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
79
126
|
test('User is able to re-initialize the form state', async () => {
|
|
80
127
|
const TestFormHook = createTestHook();
|
|
81
128
|
const Top = () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mayoiga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Type safe hook based form library for React",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -43,27 +43,32 @@
|
|
|
43
43
|
"zod": "^3.20.2"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"nozuchi": "^0.0.
|
|
46
|
+
"nozuchi": "^0.0.6"
|
|
47
|
+
},
|
|
48
|
+
"resolutions": {
|
|
49
|
+
"react": "^18.2.0",
|
|
50
|
+
"react-dom": "^18.2.0",
|
|
51
|
+
"@types/react": "^18.2.57"
|
|
47
52
|
},
|
|
48
53
|
"devDependencies": {
|
|
49
|
-
"@testing-library/react": "^14.
|
|
50
|
-
"@testing-library/user-event": "^14.
|
|
51
|
-
"@types/node": "^
|
|
52
|
-
"@types/react": "^18.2.
|
|
53
|
-
"@types/use-sync-external-store": "^0.0.
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
55
|
-
"@typescript-eslint/parser": "^
|
|
56
|
-
"eslint": "^8.
|
|
57
|
-
"eslint-plugin-import": "^2.
|
|
58
|
-
"eslint-plugin-testing-library": "^
|
|
59
|
-
"jsdom": "^
|
|
60
|
-
"prettier": "^2.
|
|
54
|
+
"@testing-library/react": "^14.2.1",
|
|
55
|
+
"@testing-library/user-event": "^14.5.2",
|
|
56
|
+
"@types/node": "^20.11.19",
|
|
57
|
+
"@types/react": "^18.2.57",
|
|
58
|
+
"@types/use-sync-external-store": "^0.0.6",
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
|
60
|
+
"@typescript-eslint/parser": "^7.0.2",
|
|
61
|
+
"eslint": "^8.56.0",
|
|
62
|
+
"eslint-plugin-import": "^2.29.1",
|
|
63
|
+
"eslint-plugin-testing-library": "^6.2.0",
|
|
64
|
+
"jsdom": "^24.0.0",
|
|
65
|
+
"prettier": "^3.2.5",
|
|
61
66
|
"react": "^18.2.0",
|
|
62
67
|
"react-dom": "^18.2.0",
|
|
63
|
-
"typedoc": "^0.
|
|
64
|
-
"typescript": "^5.
|
|
68
|
+
"typedoc": "^0.25.8",
|
|
69
|
+
"typescript": "^5.3.3",
|
|
65
70
|
"use-sync-external-store": "^1.2.0",
|
|
66
|
-
"vitest": "^
|
|
67
|
-
"zod": "^3.
|
|
71
|
+
"vitest": "^1.3.0",
|
|
72
|
+
"zod": "^3.22.4"
|
|
68
73
|
}
|
|
69
74
|
}
|
package/src/index.test.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {render, screen, waitFor} from '@testing-library/react';
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
3
4
|
import React from 'react';
|
|
4
5
|
|
|
5
6
|
import {test, expect} from 'vitest';
|
|
@@ -89,13 +90,20 @@ test('zod parse value before submit', async () => {
|
|
|
89
90
|
} else {
|
|
90
91
|
expect('should not called').toBe(result.error);
|
|
91
92
|
}
|
|
93
|
+
return Promise.resolve('returns value');
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
const Top = () => {
|
|
95
97
|
return (
|
|
96
98
|
<div>
|
|
97
99
|
<AgeInputComponent controller={TestFormHook} />
|
|
98
|
-
<button
|
|
100
|
+
<button
|
|
101
|
+
data-testid="submit-button"
|
|
102
|
+
onClick={async (e) => {
|
|
103
|
+
const returnedValue = await handleSubmitTester(e);
|
|
104
|
+
expect(returnedValue).toBe('returns value');
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
99
107
|
</div>
|
|
100
108
|
);
|
|
101
109
|
};
|
|
@@ -122,6 +130,63 @@ test('zod parse value before submit', async () => {
|
|
|
122
130
|
});
|
|
123
131
|
});
|
|
124
132
|
|
|
133
|
+
test('able to handle parse error on submit', async () => {
|
|
134
|
+
const TestFormHook = createTestHook();
|
|
135
|
+
|
|
136
|
+
const INPUT_AGE = 'aaaa';
|
|
137
|
+
const EXPECTED_ISSUE = {
|
|
138
|
+
code: 'custom',
|
|
139
|
+
message: 'Invalid input',
|
|
140
|
+
path: ['age'],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleSubmitTester = TestFormHook.handleSubmit((e) => (result) => {
|
|
144
|
+
if (result.success) {
|
|
145
|
+
expect('should not succeed').toBe(false);
|
|
146
|
+
} else {
|
|
147
|
+
expect(result.success).toBe(false);
|
|
148
|
+
expect(result.error).toStrictEqual([EXPECTED_ISSUE]);
|
|
149
|
+
}
|
|
150
|
+
return Promise.resolve('returns value');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const Top = () => {
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
<AgeInputComponent controller={TestFormHook} />
|
|
157
|
+
<button
|
|
158
|
+
data-testid="submit-button"
|
|
159
|
+
onClick={async (e) => {
|
|
160
|
+
const returnedValue = await handleSubmitTester(e);
|
|
161
|
+
expect(returnedValue).toBe('returns value');
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
render(<Top />);
|
|
169
|
+
|
|
170
|
+
const ageInput: HTMLInputElement = screen.getByTestId('age-input');
|
|
171
|
+
await userEvent.type(ageInput, INPUT_AGE);
|
|
172
|
+
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
const ageInput: HTMLInputElement = screen.getByTestId('age-input');
|
|
175
|
+
expect(ageInput.value).toBe(INPUT_AGE);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const submitButton = screen.getByTestId('submit-button');
|
|
179
|
+
await userEvent.click(submitButton);
|
|
180
|
+
|
|
181
|
+
await waitFor(() => {
|
|
182
|
+
console.log('waiting');
|
|
183
|
+
TestFormHook.api.actions.peek((s) => {
|
|
184
|
+
expect(s.value.age).toBe(INPUT_AGE);
|
|
185
|
+
expect(s.errors.age).toBe(EXPECTED_ISSUE.message);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
125
190
|
test('User is able to re-initialize the form state', async () => {
|
|
126
191
|
const TestFormHook = createTestHook();
|
|
127
192
|
|
package/src/index.ts
CHANGED
|
@@ -120,10 +120,13 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
120
120
|
type StateKeys = keyof StateBeforeValidation;
|
|
121
121
|
|
|
122
122
|
const initialErrors = Object.freeze(
|
|
123
|
-
Object.keys(initialState).reduce(
|
|
124
|
-
buf
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
Object.keys(initialState).reduce(
|
|
124
|
+
(buf, k) => {
|
|
125
|
+
buf[k as StateKeys] = null;
|
|
126
|
+
return buf;
|
|
127
|
+
},
|
|
128
|
+
{} as any as Err,
|
|
129
|
+
),
|
|
127
130
|
);
|
|
128
131
|
|
|
129
132
|
const fullInitialState: FullState = Object.freeze({
|
|
@@ -137,15 +140,18 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
137
140
|
if (result.success) {
|
|
138
141
|
return {...prev, isDirty: true, isValid: true, value: newValue, errors: initialErrors};
|
|
139
142
|
}
|
|
140
|
-
const newErrors = result.error.issues.reduce(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
const newErrors = result.error.issues.reduce(
|
|
144
|
+
(buf, iss) => {
|
|
145
|
+
// TODO: should support nested value?
|
|
146
|
+
const shallowPath = iss.path[0];
|
|
147
|
+
if (shallowPath === undefined) {
|
|
148
|
+
return buf;
|
|
149
|
+
}
|
|
150
|
+
buf[shallowPath as any as StateKeys] = iss.message;
|
|
144
151
|
return buf;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}, {} as any as Err);
|
|
152
|
+
},
|
|
153
|
+
{} as any as Err,
|
|
154
|
+
);
|
|
149
155
|
|
|
150
156
|
return {...prev, isDirty: true, isValid: false, errors: newErrors, value: newValue};
|
|
151
157
|
};
|
|
@@ -165,14 +171,17 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
165
171
|
}),
|
|
166
172
|
handleIssues: (issues) => (prev) => {
|
|
167
173
|
// FIXME: handle duplication
|
|
168
|
-
const newErrors = issues.reduce(
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
const newErrors = issues.reduce(
|
|
175
|
+
(buf, iss) => {
|
|
176
|
+
const shallowPath = iss.path[0];
|
|
177
|
+
if (shallowPath === undefined) {
|
|
178
|
+
return buf;
|
|
179
|
+
}
|
|
180
|
+
buf[shallowPath as any as StateKeys] = iss.message;
|
|
171
181
|
return buf;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}, {} as any as Err);
|
|
182
|
+
},
|
|
183
|
+
{} as any as Err,
|
|
184
|
+
);
|
|
176
185
|
|
|
177
186
|
return {...prev, isDirty: true, isValid: false, errors: newErrors};
|
|
178
187
|
},
|
|
@@ -240,13 +249,15 @@ export type Controller<State extends StateRestriction> = {
|
|
|
240
249
|
type FormHook<State extends StateRestriction, Schema extends ZodType<any, any, any>> = {
|
|
241
250
|
controller: Controller<State>;
|
|
242
251
|
|
|
243
|
-
|
|
252
|
+
/**
|
|
253
|
+
* @param handler handling logic
|
|
254
|
+
* @returns created handler is always return Promise, because of the schema validation run it asynchronously.
|
|
255
|
+
*/
|
|
256
|
+
handleSubmit: <R>(
|
|
244
257
|
handler: (
|
|
245
258
|
e: BaseSyntheticEvent,
|
|
246
|
-
) => (
|
|
247
|
-
|
|
248
|
-
) => void | Promise<void>,
|
|
249
|
-
) => (e: BaseSyntheticEvent) => void;
|
|
259
|
+
) => (val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>}) => R,
|
|
260
|
+
) => (e: BaseSyntheticEvent) => Promise<R>;
|
|
250
261
|
|
|
251
262
|
api: Subscriber<FullFormState<State>, FormControllerBehavior<State>>;
|
|
252
263
|
} & Controller<State>;
|
|
@@ -267,12 +278,10 @@ export function createFormHook<StateBeforeValidation extends StateRestriction, S
|
|
|
267
278
|
}, []);
|
|
268
279
|
},
|
|
269
280
|
useSelector: store.useSelector,
|
|
270
|
-
handleSubmit: (
|
|
281
|
+
handleSubmit: <R>(
|
|
271
282
|
handler: (
|
|
272
283
|
e: BaseSyntheticEvent,
|
|
273
|
-
) => (
|
|
274
|
-
val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>},
|
|
275
|
-
) => void | Promise<void>,
|
|
284
|
+
) => (val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>}) => R,
|
|
276
285
|
) => {
|
|
277
286
|
return (e: BaseSyntheticEvent) => {
|
|
278
287
|
const eventHandled = handler(e);
|
|
@@ -283,8 +292,20 @@ export function createFormHook<StateBeforeValidation extends StateRestriction, S
|
|
|
283
292
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
284
293
|
return eventHandled({success: true, data: result.data, error: undefined});
|
|
285
294
|
}
|
|
286
|
-
|
|
287
|
-
|
|
295
|
+
|
|
296
|
+
const handled: any = eventHandled({success: false, error: result.error.issues});
|
|
297
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
298
|
+
if (typeof handled['then'] === 'function') {
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
|
300
|
+
return handled.then((x: any) => {
|
|
301
|
+
store.actions.handleIssues(result.error.issues);
|
|
302
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
303
|
+
return x;
|
|
304
|
+
}) as R;
|
|
305
|
+
} else {
|
|
306
|
+
store.actions.handleIssues(result.error.issues);
|
|
307
|
+
return handled as R;
|
|
308
|
+
}
|
|
288
309
|
});
|
|
289
310
|
};
|
|
290
311
|
},
|