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.
@@ -9,7 +9,7 @@ jobs:
9
9
 
10
10
  strategy:
11
11
  matrix:
12
- node-version: [16.x]
12
+ node-version: [18.x]
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v1
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
- handleSubmit: (handler: (e: BaseSyntheticEvent) => (val: {
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
- }) => void | Promise<void>) => (e: BaseSyntheticEvent) => void;
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
- void eventHandled({ success: false, error: result.error.issues });
103
- store.actions.handleIssues(result.error.issues);
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
  },
@@ -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: handleSubmitTester })));
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.7",
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.4"
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.0.0",
50
- "@testing-library/user-event": "^14.4.3",
51
- "@types/node": "^17.0.23",
52
- "@types/react": "^18.2.7",
53
- "@types/use-sync-external-store": "^0.0.3",
54
- "@typescript-eslint/eslint-plugin": "^5.59.8",
55
- "@typescript-eslint/parser": "^5.59.8",
56
- "eslint": "^8.41.0",
57
- "eslint-plugin-import": "^2.27.5",
58
- "eslint-plugin-testing-library": "^5.11.0",
59
- "jsdom": "^22.1.0",
60
- "prettier": "^2.8.8",
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.24.7",
64
- "typescript": "^5.0.4",
68
+ "typedoc": "^0.25.8",
69
+ "typescript": "^5.3.3",
65
70
  "use-sync-external-store": "^1.2.0",
66
- "vitest": "^0.31.1",
67
- "zod": "^3.20.2"
71
+ "vitest": "^1.3.0",
72
+ "zod": "^3.22.4"
68
73
  }
69
74
  }
@@ -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 data-testid="submit-button" onClick={handleSubmitTester} />
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((buf, k) => {
124
- buf[k as StateKeys] = null;
125
- return buf;
126
- }, {} as any as Err),
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((buf, iss) => {
141
- // TODO: should support nested value?
142
- const shallowPath = iss.path[0];
143
- if (shallowPath === undefined) {
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
- buf[shallowPath as any as StateKeys] = iss.message;
147
- return buf;
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((buf, iss) => {
169
- const shallowPath = iss.path[0];
170
- if (shallowPath === undefined) {
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
- buf[shallowPath as any as StateKeys] = iss.message;
174
- return buf;
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
- handleSubmit: (
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
- val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>},
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
- void eventHandled({success: false, error: result.error.issues});
287
- store.actions.handleIssues(result.error.issues);
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
  },
package/tsconfig.json CHANGED
@@ -26,8 +26,6 @@
26
26
  "noErrorTruncation": true,
27
27
 
28
28
  "lib": ["dom", "esnext"],
29
- // TODO: node is for vitest... I should isolate it probably?
30
- "types": ["react/next"],
31
29
 
32
30
  "rootDir": "./src",
33
31
  "outDir": "./dist"