mayoiga 0.1.2 → 0.1.3

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.
@@ -4,11 +4,14 @@ import React from 'react';
4
4
  import { test, expect } from 'vitest';
5
5
  import * as zod from 'zod';
6
6
  import { Field, Slicer } from './component';
7
- import { createFormHook, } from './index';
7
+ import { createFormHook } from './index';
8
8
  const testSchema = zod.object({
9
9
  name: zod.string(),
10
10
  description: zod.string(),
11
- age: zod.string().transform(val => Number(val)).refine(val => Number.isNaN(val) ? 0 : val),
11
+ age: zod
12
+ .string()
13
+ .transform((val) => Number(val))
14
+ .refine((val) => (Number.isNaN(val) ? 0 : val)),
12
15
  marked: zod.boolean(),
13
16
  });
14
17
  const createTestHook = () => createFormHook({ name: '', description: '', age: '', marked: false }, testSchema);
@@ -36,7 +39,7 @@ test('Slice component works fine in basic usage.', async () => {
36
39
  const TestFormHook = createTestHook();
37
40
  const Top = () => {
38
41
  return (React.createElement("div", null,
39
- React.createElement(Slicer, { controller: TestFormHook.controller, selector: s => [s.value.age] }, (tool, age) => React.createElement("input", { "data-testid": "age-input", name: "age", value: age, onChange: e => tool.handleChange('age', e.target.value) }))));
42
+ React.createElement(Slicer, { controller: TestFormHook.controller, selector: (s) => [s.value.age] }, (tool, age) => (React.createElement("input", { "data-testid": "age-input", name: "age", value: age, onChange: (e) => tool.handleChange('age', e.target.value) })))));
40
43
  };
41
44
  render(React.createElement(Top, null));
42
45
  const INPUT_AGE = '42';
@@ -51,3 +54,30 @@ test('Slice component works fine in basic usage.', async () => {
51
54
  });
52
55
  });
53
56
  });
57
+ test('A user able to create a reusable component', async () => {
58
+ const TestFormHook = createTestHook();
59
+ function NarrowTypedExpected(props) {
60
+ return (React.createElement(Field, { controller: props.controller, name: "age" }, (tool) => React.createElement("input", { "data-testid": "age-input", ...tool })));
61
+ }
62
+ function ErrorDisplayer(props) {
63
+ const { controller } = props;
64
+ return (React.createElement(Slicer, { controller: controller, selector: (s) => [s.value.age] }, (tool, age) => (React.createElement("button", { onClick: (e) => {
65
+ controller.actions.pushFormErrors((_state) => ({ age: age === '42' ? 'yes!!!' : 'noooo' }));
66
+ } }))));
67
+ }
68
+ render(React.createElement("div", null,
69
+ React.createElement(NarrowTypedExpected, { controller: TestFormHook.controller }),
70
+ React.createElement(ErrorDisplayer, { controller: TestFormHook.controller })));
71
+ const INPUT_AGE = '42';
72
+ // calling `userEvent` in here will cause race condition...
73
+ await userEvent.type(screen.getByTestId('age-input'), INPUT_AGE);
74
+ await userEvent.click(screen.getByRole('button'));
75
+ await waitFor(() => {
76
+ const ageInput = screen.getByTestId('age-input');
77
+ expect(ageInput.value).toBe(INPUT_AGE);
78
+ TestFormHook.api.actions.peek((s) => {
79
+ expect(s.value.age).toBe(INPUT_AGE);
80
+ expect(s.errors.age).toBe('yes!!!');
81
+ });
82
+ });
83
+ });
package/dist/index.d.ts CHANGED
@@ -44,7 +44,8 @@ declare type FormControllerBehavior<StateBeforeValidation> = {
44
44
  /**
45
45
  * You can handle server side or other external error via this action.
46
46
  */
47
- handleError: (reducer: (prev: FormErrors<StateBeforeValidation>) => FormErrors<StateBeforeValidation>) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
47
+ pushFormErrors: (validator: (state: StateBeforeValidation) => Partial<FormErrors<StateBeforeValidation>>) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
48
+ reduceFormErrors: (reducer: (prev: FormErrors<StateBeforeValidation>) => FormErrors<StateBeforeValidation>) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
48
49
  /**
49
50
  */
50
51
  handleChange: HandleChangeAction<StateBeforeValidation, (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>>;
@@ -54,9 +55,9 @@ declare type FormControllerBehavior<StateBeforeValidation> = {
54
55
  declare type ActionsCanBePublic<State extends StateRestriction> = {
55
56
  reset: VoidFunction;
56
57
  initializeForm: (initial: Partial<State>) => void;
58
+ pushFormErrors: (validator: (state: State) => Partial<FormErrors<State>>) => void;
57
59
  handleChange: HandleChangeAction<State>;
58
60
  handleBulkChange: (setter: (prev: State) => Partial<State>) => void;
59
- handleError: (reducer: (prev: FormErrors<State>) => FormErrors<State>) => void;
60
61
  };
61
62
  /**
62
63
  * Restricted type interface of the form store
package/dist/index.js CHANGED
@@ -51,7 +51,11 @@ function createFormStore(initialState, schema) {
51
51
  }, {});
52
52
  return { ...prev, isDirty: true, isValid: false, errors: newErrors };
53
53
  },
54
- handleError: (reducer) => (prev) => {
54
+ pushFormErrors: (validator) => (prev) => {
55
+ const pushedErrors = validator(prev.value);
56
+ return { ...prev, errors: { ...prev.errors, ...pushedErrors } };
57
+ },
58
+ reduceFormErrors: (reducer) => (prev) => {
55
59
  return { ...prev, errors: reducer(prev.errors) };
56
60
  },
57
61
  handleChange: (name, value) => (prev) => {
@@ -7,13 +7,16 @@ import { createFormHook, useFormSlice } from './index';
7
7
  const testSchema = zod.object({
8
8
  name: zod.string(),
9
9
  description: zod.string(),
10
- age: zod.string().transform(val => Number(val)).refine(val => Number.isNaN(val) ? 0 : val),
10
+ age: zod
11
+ .string()
12
+ .transform((val) => Number(val))
13
+ .refine((val) => (Number.isNaN(val) ? 0 : val)),
11
14
  marked: zod.boolean(),
12
15
  });
13
16
  const createTestHook = () => createFormHook({ name: '', description: '', age: '', marked: false }, testSchema);
14
17
  const AgeInputComponent = (props) => {
15
18
  const { controller } = props;
16
- const [[age, errorMessage], { handleChange }] = useFormSlice(controller, s => [s.value.age, s.errors.age]);
19
+ const [[age, errorMessage], { handleChange }] = useFormSlice(controller, (s) => [s.value.age, s.errors.age]);
17
20
  return (React.createElement("div", null,
18
21
  React.createElement("input", { "data-testid": "age-input", value: age, name: "age", onChange: (e) => {
19
22
  handleChange('age', e.currentTarget.value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mayoiga",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Type safe hook based form library for React",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -15,10 +15,11 @@
15
15
  "test": "vitest run",
16
16
  "prebuild": "npm run clean",
17
17
  "build": "tsc",
18
- "check:style": "prettier --check 'src/**/*.{js,ts}'",
18
+ "check:type": "tsc --noEmit",
19
+ "check:style": "prettier --check 'src/**/*.{js,ts,jsx,tsx}'",
19
20
  "check:lint": "eslint 'src/**/*.{ts,tsx}'",
20
- "check-all": "npm run check:style && npm run check:lint && npm run build --noEmit && npm run test",
21
- "fix-all": "npm run check:style --write && npm run check:lint --fix",
21
+ "check-all": "npm run check:type && npm run check:style && npm run check:lint && npm run build --noEmit && npm run test",
22
+ "fix-all": "npm run check:style -- --write && npm run check:lint -- --fix",
22
23
  "prepublish": "npm run check-all && npm run build"
23
24
  },
24
25
  "repository": {
@@ -1,4 +1,3 @@
1
-
2
1
  import {render, screen, waitFor} from '@testing-library/react';
3
2
  import userEvent from '@testing-library/user-event';
4
3
  import React from 'react';
@@ -7,14 +6,19 @@ import {test, expect} from 'vitest';
7
6
 
8
7
  import * as zod from 'zod';
9
8
 
10
- import { Field, Slicer } from './component';
9
+ import {Field, Slicer} from './component';
10
+
11
+ import {createFormHook} from './index';
11
12
 
12
- import {createFormHook, } from './index';
13
+ import type {Controller} from './index';
13
14
 
14
15
  const testSchema = zod.object({
15
16
  name: zod.string(),
16
17
  description: zod.string(),
17
- age: zod.string().transform(val => Number(val)).refine(val => Number.isNaN(val) ? 0 : val),
18
+ age: zod
19
+ .string()
20
+ .transform((val) => Number(val))
21
+ .refine((val) => (Number.isNaN(val) ? 0 : val)),
18
22
  marked: zod.boolean(),
19
23
  });
20
24
 
@@ -24,9 +28,9 @@ type FormStateBeforeValidation = {
24
28
  description: string;
25
29
  age: string;
26
30
  marked: boolean;
27
- }
31
+ };
28
32
 
29
- const createTestHook = () => createFormHook({name:'', description: '', age: '', marked: false} as FormStateBeforeValidation, testSchema);
33
+ const createTestHook = () => createFormHook({name: '', description: '', age: '', marked: false} as FormStateBeforeValidation, testSchema);
30
34
 
31
35
  // afterEach(cleanup);
32
36
 
@@ -36,11 +40,11 @@ test('Field component works fine in basic usage.', async () => {
36
40
  const Top = () => {
37
41
  return (
38
42
  <div>
39
- <Field controller={TestFormHook.controller} name="age">{
40
- (tool) => <input data-testid="age-input" {...tool} />
41
- }</Field>
43
+ <Field controller={TestFormHook.controller} name="age">
44
+ {(tool) => <input data-testid="age-input" {...tool} />}
45
+ </Field>
42
46
  </div>
43
- )
47
+ );
44
48
  };
45
49
 
46
50
  render(<Top />);
@@ -66,11 +70,13 @@ test('Slice component works fine in basic usage.', async () => {
66
70
  const Top = () => {
67
71
  return (
68
72
  <div>
69
- <Slicer controller={TestFormHook.controller} selector={s => [s.value.age] as const}>{
70
- (tool, age: string) => <input data-testid="age-input" name="age" value={age} onChange={e => tool.handleChange('age' as const, e.target.value)} />
71
- }</Slicer>
73
+ <Slicer controller={TestFormHook.controller} selector={(s) => [s.value.age] as const}>
74
+ {(tool, age: string) => (
75
+ <input data-testid="age-input" name="age" value={age} onChange={(e) => tool.handleChange('age' as const, e.target.value)} />
76
+ )}
77
+ </Slicer>
72
78
  </div>
73
- )
79
+ );
74
80
  };
75
81
 
76
82
  render(<Top />);
@@ -89,3 +95,52 @@ test('Slice component works fine in basic usage.', async () => {
89
95
  });
90
96
  });
91
97
  });
98
+
99
+ test('A user able to create a reusable component', async () => {
100
+ const TestFormHook = createTestHook();
101
+
102
+ function NarrowTypedExpected(props: {controller: Controller<{age: string}>}) {
103
+ return (
104
+ <Field controller={props.controller} name="age">
105
+ {(tool) => <input data-testid="age-input" {...tool} />}
106
+ </Field>
107
+ );
108
+ }
109
+
110
+ function ErrorDisplayer(props: {controller: Controller<{age: string}>}) {
111
+ const {controller} = props;
112
+ return (
113
+ <Slicer controller={controller} selector={(s) => [s.value.age] as const}>
114
+ {(tool, age: string) => (
115
+ <button
116
+ onClick={(e) => {
117
+ controller.actions.pushFormErrors((_state) => ({age: age === '42' ? 'yes!!!' : 'noooo'}));
118
+ }}
119
+ />
120
+ )}
121
+ </Slicer>
122
+ );
123
+ }
124
+
125
+ render(
126
+ <div>
127
+ <NarrowTypedExpected controller={TestFormHook.controller} />
128
+ <ErrorDisplayer controller={TestFormHook.controller} />
129
+ </div>,
130
+ );
131
+
132
+ const INPUT_AGE = '42';
133
+
134
+ // calling `userEvent` in here will cause race condition...
135
+ await userEvent.type(screen.getByTestId('age-input'), INPUT_AGE);
136
+ await userEvent.click(screen.getByRole('button'));
137
+
138
+ await waitFor(() => {
139
+ const ageInput: HTMLInputElement = screen.getByTestId('age-input');
140
+ expect(ageInput.value).toBe(INPUT_AGE);
141
+ TestFormHook.api.actions.peek((s) => {
142
+ expect(s.value.age).toBe(INPUT_AGE);
143
+ expect(s.errors.age).toBe('yes!!!');
144
+ });
145
+ });
146
+ });
package/src/component.tsx CHANGED
@@ -1,18 +1,22 @@
1
1
  import React from 'react';
2
2
 
3
- import type {SliceProps, FieldProps, StateRestriction, Controller, } from './index';
3
+ import type {SliceProps, FieldProps, StateRestriction, Controller} from './index';
4
4
 
5
5
  type ControlledComponentProps<State, Props> = Props & {
6
6
  controller: Controller<State>;
7
- }
7
+ };
8
8
 
9
- export function Slicer<State extends StateRestriction, R extends ReadonlyArray<unknown>>(props: ControlledComponentProps<State, SliceProps<State, R>>) {
9
+ export function Slicer<State extends StateRestriction, R extends ReadonlyArray<unknown>>(
10
+ props: ControlledComponentProps<State, SliceProps<State, R>>,
11
+ ) {
10
12
  const {controller, ...restProps} = props;
11
13
  const {Slicer: Delegate} = controller.components;
12
14
  return <Delegate {...restProps} />;
13
15
  }
14
16
 
15
- export function Field<State extends StateRestriction, Name extends keyof State>(props: ControlledComponentProps<State, FieldProps<State, Name>>) {
17
+ export function Field<State extends StateRestriction, Name extends keyof State>(
18
+ props: ControlledComponentProps<State, FieldProps<State, Name>>,
19
+ ) {
16
20
  const {controller, ...restProps} = props;
17
21
  const {Field: Delegate} = controller.components;
18
22
  return <Delegate {...restProps} />;
@@ -7,14 +7,16 @@ import {test, expect} from 'vitest';
7
7
  import * as zod from 'zod';
8
8
 
9
9
  import {createFormHook, useFormSlice} from './index';
10
-
11
- import type {Controller} from './index';
12
10
 
11
+ import type {Controller} from './index';
13
12
 
14
13
  const testSchema = zod.object({
15
14
  name: zod.string(),
16
15
  description: zod.string(),
17
- age: zod.string().transform(val => Number(val)).refine(val => Number.isNaN(val) ? 0 : val),
16
+ age: zod
17
+ .string()
18
+ .transform((val) => Number(val))
19
+ .refine((val) => (Number.isNaN(val) ? 0 : val)),
18
20
  marked: zod.boolean(),
19
21
  });
20
22
 
@@ -24,13 +26,13 @@ type FormStateBeforeValidation = {
24
26
  description: string;
25
27
  age: string;
26
28
  marked: boolean;
27
- }
29
+ };
28
30
 
29
- const createTestHook = () => createFormHook({name:'', description: '', age: '', marked: false} as FormStateBeforeValidation, testSchema);
31
+ const createTestHook = () => createFormHook({name: '', description: '', age: '', marked: false} as FormStateBeforeValidation, testSchema);
30
32
 
31
33
  const AgeInputComponent = (props: {controller: Controller<FormStateBeforeValidation>}) => {
32
34
  const {controller} = props;
33
- const [[age, errorMessage], {handleChange}] = useFormSlice(controller, s => [s.value.age, s.errors.age] as const);
35
+ const [[age, errorMessage], {handleChange}] = useFormSlice(controller, (s) => [s.value.age, s.errors.age] as const);
34
36
  return (
35
37
  <div>
36
38
  <input
@@ -56,7 +58,7 @@ test('useSelector can select the fields', async () => {
56
58
  <div>
57
59
  <AgeInputComponent controller={TestFormHook.controller} />
58
60
  </div>
59
- )
61
+ );
60
62
  };
61
63
 
62
64
  render(<Top />);
@@ -81,12 +83,12 @@ test('zod parse value before submit', async () => {
81
83
  const PARSED_RESULT = Number(INPUT_AGE);
82
84
 
83
85
  const handleSubmitTester = TestFormHook.handleSubmit((e) => (result) => {
84
- if (result.success) {
85
- expect(result.data.age).toBe(PARSED_RESULT);
86
- } else {
87
- expect('should not called').toBe(result.error);
88
- }
89
- });
86
+ if (result.success) {
87
+ expect(result.data.age).toBe(PARSED_RESULT);
88
+ } else {
89
+ expect('should not called').toBe(result.error);
90
+ }
91
+ });
90
92
 
91
93
  const Top = () => {
92
94
  return (
@@ -94,16 +96,16 @@ test('zod parse value before submit', async () => {
94
96
  <AgeInputComponent controller={TestFormHook} />
95
97
  <button data-testid="submit-button" onClick={handleSubmitTester} />
96
98
  </div>
97
- )
99
+ );
98
100
  };
99
101
 
100
102
  render(<Top />);
101
103
 
102
- const ageInput: HTMLInputElement = screen.getByTestId('age-input') ;
104
+ const ageInput: HTMLInputElement = screen.getByTestId('age-input');
103
105
  await userEvent.type(ageInput, INPUT_AGE);
104
106
 
105
107
  await waitFor(() => {
106
- const ageInput: HTMLInputElement = screen.getByTestId('age-input') ;
108
+ const ageInput: HTMLInputElement = screen.getByTestId('age-input');
107
109
  expect(ageInput.value).toBe(INPUT_AGE);
108
110
  });
109
111
 
@@ -117,4 +119,4 @@ test('zod parse value before submit', async () => {
117
119
  expect(s.errors.age).toBeNull();
118
120
  });
119
121
  });
120
- });
122
+ });
package/src/index.ts CHANGED
@@ -65,7 +65,11 @@ type FormControllerBehavior<StateBeforeValidation> = {
65
65
  /**
66
66
  * You can handle server side or other external error via this action.
67
67
  */
68
- handleError: (
68
+ pushFormErrors: (
69
+ validator: (state: StateBeforeValidation) => Partial<FormErrors<StateBeforeValidation>>,
70
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
71
+
72
+ reduceFormErrors: (
69
73
  reducer: (prev: FormErrors<StateBeforeValidation>) => FormErrors<StateBeforeValidation>,
70
74
  ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
71
75
 
@@ -150,7 +154,11 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
150
154
 
151
155
  return {...prev, isDirty: true, isValid: false, errors: newErrors};
152
156
  },
153
- handleError: (reducer) => (prev) => {
157
+ pushFormErrors: (validator) => (prev) => {
158
+ const pushedErrors = validator(prev.value);
159
+ return {...prev, errors: {...prev.errors, ...pushedErrors}};
160
+ },
161
+ reduceFormErrors: (reducer) => (prev) => {
154
162
  return {...prev, errors: reducer(prev.errors)};
155
163
  },
156
164
  handleChange: (name, value) => (prev) => {
@@ -174,9 +182,9 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
174
182
  type ActionsCanBePublic<State extends StateRestriction> = {
175
183
  reset: VoidFunction;
176
184
  initializeForm: (initial: Partial<State>) => void;
185
+ pushFormErrors: (validator: (state: State) => Partial<FormErrors<State>>) => void;
177
186
  handleChange: HandleChangeAction<State>;
178
187
  handleBulkChange: (setter: (prev: State) => Partial<State>) => void;
179
- handleError: (reducer: (prev: FormErrors<State>) => FormErrors<State>) => void;
180
188
  };
181
189
 
182
190
  /**