mayoiga 0.1.5 → 0.1.7
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/dist/component.d.ts +4 -3
- package/dist/index.d.ts +26 -13
- package/dist/index.js +4 -2
- package/dist/index.test.js +37 -1
- package/package.json +16 -16
- package/src/index.test.tsx +55 -1
- package/src/index.ts +23 -3
- package/yarn.lock +1044 -760
package/dist/component.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import type { SliceProps, FieldProps, StateRestriction, Controller } from './index';
|
|
2
|
-
|
|
3
|
+
type ControlledComponentProps<State extends StateRestriction, Props> = Props & {
|
|
3
4
|
controller: Controller<State>;
|
|
4
5
|
};
|
|
5
|
-
export declare function Slicer<State extends StateRestriction, R extends ReadonlyArray<unknown>>(props: ControlledComponentProps<State, SliceProps<State, R>>): JSX.Element;
|
|
6
|
-
export declare function Field<State extends StateRestriction, Name extends keyof State>(props: ControlledComponentProps<State, FieldProps<State, Name>>): JSX.Element;
|
|
6
|
+
export declare function Slicer<State extends StateRestriction, R extends ReadonlyArray<unknown>>(props: ControlledComponentProps<State, SliceProps<State, R>>): React.JSX.Element;
|
|
7
|
+
export declare function Field<State extends StateRestriction, Name extends keyof State>(props: ControlledComponentProps<State, FieldProps<State, Name>>): React.JSX.Element;
|
|
7
8
|
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { Subscriber } from 'nozuchi';
|
|
2
2
|
import type { BaseSyntheticEvent, ChangeEvent, ReactNode, ReactElement } from 'react';
|
|
3
3
|
import type { infer as zodInfer, ZodIssue, ZodType } from 'zod';
|
|
4
|
-
export
|
|
5
|
-
|
|
4
|
+
export type StateRestriction = Record<string, any>;
|
|
5
|
+
type FormStatus = {
|
|
6
6
|
isDirty: boolean;
|
|
7
7
|
isValid: boolean;
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
type FormErrors<State extends StateRestriction> = {
|
|
10
10
|
[k in keyof State]: string | null;
|
|
11
11
|
};
|
|
12
|
-
export
|
|
12
|
+
export type FullFormState<State extends StateRestriction> = FormStatus & {
|
|
13
13
|
value: State;
|
|
14
14
|
errors: FormErrors<State>;
|
|
15
15
|
};
|
|
16
|
-
export
|
|
17
|
-
export
|
|
16
|
+
export type HandleChangeAction<State extends StateRestriction, R = void> = <Name extends keyof State>(name: Name, value: State[Name]) => R;
|
|
17
|
+
export type FieldProps<State extends StateRestriction, Name extends keyof State> = {
|
|
18
18
|
name: Name;
|
|
19
19
|
children: (tool: {
|
|
20
20
|
name: Name;
|
|
@@ -28,7 +28,7 @@ export declare type FieldProps<State extends StateRestriction, Name extends keyo
|
|
|
28
28
|
*/
|
|
29
29
|
deps?: ReadonlyArray<unknown>;
|
|
30
30
|
};
|
|
31
|
-
export
|
|
31
|
+
export type SliceProps<State extends StateRestriction, Selected extends ReadonlyArray<unknown>> = {
|
|
32
32
|
selector: (s: FullFormState<State>) => Selected;
|
|
33
33
|
children: (tool: {
|
|
34
34
|
handleChange: HandleChangeAction<State>;
|
|
@@ -43,14 +43,19 @@ export declare type SliceProps<State extends StateRestriction, Selected extends
|
|
|
43
43
|
*/
|
|
44
44
|
deps?: ReadonlyArray<unknown>;
|
|
45
45
|
};
|
|
46
|
-
|
|
46
|
+
type FormControllerBehavior<StateBeforeValidation extends StateRestriction> = {
|
|
47
47
|
/** For testing */
|
|
48
48
|
peek: (eyeball: (prev: FullFormState<StateBeforeValidation>) => void) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
|
|
49
49
|
reset: () => () => FullFormState<StateBeforeValidation>;
|
|
50
50
|
/**
|
|
51
51
|
* Initialize form state without validation.
|
|
52
|
+
*
|
|
53
|
+
* @param initialVal value to initialize form state
|
|
54
|
+
* @param opts.cleanup if true, form state is become pristine (which means, `form.isDirty` is going to be false) .
|
|
52
55
|
*/
|
|
53
|
-
initializeForm: (initialVal: Partial<StateBeforeValidation
|
|
56
|
+
initializeForm: (initialVal: Partial<StateBeforeValidation>, opts?: {
|
|
57
|
+
cleanup?: true;
|
|
58
|
+
}) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
|
|
54
59
|
/**
|
|
55
60
|
* Handle issue that zod reported
|
|
56
61
|
*/
|
|
@@ -66,9 +71,17 @@ declare type FormControllerBehavior<StateBeforeValidation extends StateRestricti
|
|
|
66
71
|
reduceFormState: (reducer: (prevValue: StateBeforeValidation) => StateBeforeValidation) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
|
|
67
72
|
handleBulkChange: (setter: (prev: StateBeforeValidation) => Partial<StateBeforeValidation>) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
|
|
68
73
|
};
|
|
69
|
-
|
|
74
|
+
type ActionsCanBePublic<State extends StateRestriction> = {
|
|
70
75
|
reset: VoidFunction;
|
|
71
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Initialize form state without validation.
|
|
78
|
+
*
|
|
79
|
+
* @param initialVal value to initialize form state
|
|
80
|
+
* @param opts.cleanup if true, form state is become pristine (which means, `form.isDirty` is going to be false) .
|
|
81
|
+
*/
|
|
82
|
+
initializeForm: (initial: Partial<State>, opts?: {
|
|
83
|
+
cleanup?: true;
|
|
84
|
+
}) => void;
|
|
72
85
|
pushFormErrors: (validator: (state: State) => Partial<FormErrors<State>>) => void;
|
|
73
86
|
handleChange: HandleChangeAction<State>;
|
|
74
87
|
handleBulkChange: (setter: (prev: State) => Partial<State>) => void;
|
|
@@ -77,7 +90,7 @@ declare type ActionsCanBePublic<State extends StateRestriction> = {
|
|
|
77
90
|
* Restricted type interface of the form store
|
|
78
91
|
* TODO: more restriction
|
|
79
92
|
*/
|
|
80
|
-
export
|
|
93
|
+
export type Controller<State extends StateRestriction> = {
|
|
81
94
|
useInitialize: (initialValue: Partial<State>) => void;
|
|
82
95
|
useSelector: <R>(selector: (state: FullFormState<State>) => R, isEqual?: (prev: R, current: R) => boolean) => R;
|
|
83
96
|
actions: ActionsCanBePublic<State>;
|
|
@@ -86,7 +99,7 @@ export declare type Controller<State extends StateRestriction> = {
|
|
|
86
99
|
Slicer: <Selected extends ReadonlyArray<unknown>>(props: SliceProps<State, Selected>) => ReactElement<any, any> | null;
|
|
87
100
|
};
|
|
88
101
|
};
|
|
89
|
-
|
|
102
|
+
type FormHook<State extends StateRestriction, Schema extends ZodType<any, any, any>> = {
|
|
90
103
|
controller: Controller<State>;
|
|
91
104
|
handleSubmit: (handler: (e: BaseSyntheticEvent) => (val: {
|
|
92
105
|
success: true;
|
package/dist/index.js
CHANGED
|
@@ -34,10 +34,11 @@ function createFormStore(initialState, schema) {
|
|
|
34
34
|
return prev;
|
|
35
35
|
},
|
|
36
36
|
reset: () => () => fullInitialState,
|
|
37
|
-
initializeForm: (initialVal) => (prev) => ({
|
|
37
|
+
initializeForm: (initialVal, opts) => (prev) => ({
|
|
38
38
|
...prev,
|
|
39
39
|
value: { ...prev.value, ...initialVal },
|
|
40
40
|
errors: { ...initialErrors },
|
|
41
|
+
isDirty: opts?.cleanup === true ? false : prev.isDirty,
|
|
41
42
|
}),
|
|
42
43
|
handleIssues: (issues) => (prev) => {
|
|
43
44
|
// FIXME: handle duplication
|
|
@@ -53,7 +54,8 @@ function createFormStore(initialState, schema) {
|
|
|
53
54
|
},
|
|
54
55
|
pushFormErrors: (validator) => (prev) => {
|
|
55
56
|
const pushedErrors = validator(prev.value);
|
|
56
|
-
|
|
57
|
+
const allErrors = { ...prev.errors, ...pushedErrors };
|
|
58
|
+
return { ...prev, isValid: Object.keys(allErrors).length === 0, errors: allErrors };
|
|
57
59
|
},
|
|
58
60
|
reduceFormErrors: (reducer) => (prev) => {
|
|
59
61
|
return { ...prev, errors: reducer(prev.errors) };
|
package/dist/index.test.js
CHANGED
|
@@ -13,7 +13,8 @@ const testSchema = zod.object({
|
|
|
13
13
|
.refine((val) => (Number.isNaN(val) ? 0 : val)),
|
|
14
14
|
marked: zod.boolean(),
|
|
15
15
|
});
|
|
16
|
-
const
|
|
16
|
+
const initialState = { name: '', description: '', age: '', marked: false };
|
|
17
|
+
const createTestHook = () => createFormHook(initialState, testSchema);
|
|
17
18
|
const AgeInputComponent = (props) => {
|
|
18
19
|
const { controller } = props;
|
|
19
20
|
const [[age, errorMessage], { handleChange }] = useFormSlice(controller, (s) => [s.value.age, s.errors.age]);
|
|
@@ -75,3 +76,38 @@ test('zod parse value before submit', async () => {
|
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
});
|
|
79
|
+
test('User is able to re-initialize the form state', async () => {
|
|
80
|
+
const TestFormHook = createTestHook();
|
|
81
|
+
const Top = () => {
|
|
82
|
+
return (React.createElement("div", null,
|
|
83
|
+
React.createElement(AgeInputComponent, { controller: TestFormHook.controller })));
|
|
84
|
+
};
|
|
85
|
+
render(React.createElement(Top, null));
|
|
86
|
+
const ageInput = screen.getByTestId('age-input');
|
|
87
|
+
await userEvent.type(ageInput, '9999');
|
|
88
|
+
expect(TestFormHook.api.getState().isDirty).toBeTruthy();
|
|
89
|
+
TestFormHook.actions.initializeForm({}, { cleanup: true });
|
|
90
|
+
expect(TestFormHook.api.getState().isDirty).toBeFalsy();
|
|
91
|
+
});
|
|
92
|
+
test('User is able to push server side validation', async () => {
|
|
93
|
+
const TestFormHook = createTestHook();
|
|
94
|
+
const Top = () => {
|
|
95
|
+
return (React.createElement("div", null,
|
|
96
|
+
React.createElement(AgeInputComponent, { controller: TestFormHook.controller })));
|
|
97
|
+
};
|
|
98
|
+
render(React.createElement(Top, null));
|
|
99
|
+
const ageInput = screen.getByTestId('age-input');
|
|
100
|
+
await userEvent.type(ageInput, '9999');
|
|
101
|
+
TestFormHook.actions.pushFormErrors((_s) => ({ age: 'server side error' }));
|
|
102
|
+
const state1 = TestFormHook.api.getState();
|
|
103
|
+
expect(state1.isDirty).toBeTruthy();
|
|
104
|
+
expect(state1.isValid).toBeFalsy();
|
|
105
|
+
expect(state1.errors.age).toEqual('server side error');
|
|
106
|
+
await userEvent.type(ageInput, 'hgoehoge');
|
|
107
|
+
// confirm empty field won't overwrite any messages
|
|
108
|
+
TestFormHook.actions.pushFormErrors((_s) => ({}));
|
|
109
|
+
const state2 = TestFormHook.api.getState();
|
|
110
|
+
expect(state2.isDirty).toBeTruthy();
|
|
111
|
+
expect(state2.isValid).toBeFalsy();
|
|
112
|
+
expect(state2.errors.age).toEqual('Invalid input');
|
|
113
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mayoiga",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Type safe hook based form library for React",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -40,30 +40,30 @@
|
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://github.com/hachibeeDI/mayoiga#readme",
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"zod": "^3.
|
|
43
|
+
"zod": "^3.20.2"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"nozuchi": "^0.0.
|
|
46
|
+
"nozuchi": "^0.0.4"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@testing-library/react": "^
|
|
49
|
+
"@testing-library/react": "^14.0.0",
|
|
50
50
|
"@testing-library/user-event": "^14.4.3",
|
|
51
51
|
"@types/node": "^17.0.23",
|
|
52
|
-
"@types/react": "^18.
|
|
52
|
+
"@types/react": "^18.2.7",
|
|
53
53
|
"@types/use-sync-external-store": "^0.0.3",
|
|
54
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
55
|
-
"@typescript-eslint/parser": "^5.
|
|
56
|
-
"eslint": "^8.
|
|
57
|
-
"eslint-plugin-import": "^2.
|
|
58
|
-
"eslint-plugin-testing-library": "^5.
|
|
59
|
-
"jsdom": "^
|
|
60
|
-
"prettier": "^2.
|
|
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",
|
|
61
61
|
"react": "^18.2.0",
|
|
62
62
|
"react-dom": "^18.2.0",
|
|
63
|
-
"typedoc": "^0.
|
|
64
|
-
"typescript": "^
|
|
63
|
+
"typedoc": "^0.24.7",
|
|
64
|
+
"typescript": "^5.0.4",
|
|
65
65
|
"use-sync-external-store": "^1.2.0",
|
|
66
|
-
"vitest": "^0.
|
|
67
|
-
"zod": "^3.
|
|
66
|
+
"vitest": "^0.31.1",
|
|
67
|
+
"zod": "^3.20.2"
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/index.test.tsx
CHANGED
|
@@ -28,7 +28,8 @@ type FormStateBeforeValidation = {
|
|
|
28
28
|
marked: boolean;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
const
|
|
31
|
+
const initialState = {name: '', description: '', age: '', marked: false};
|
|
32
|
+
const createTestHook = () => createFormHook(initialState as FormStateBeforeValidation, testSchema);
|
|
32
33
|
|
|
33
34
|
const AgeInputComponent = (props: {controller: Controller<FormStateBeforeValidation>}) => {
|
|
34
35
|
const {controller} = props;
|
|
@@ -120,3 +121,56 @@ test('zod parse value before submit', async () => {
|
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
});
|
|
124
|
+
|
|
125
|
+
test('User is able to re-initialize the form state', async () => {
|
|
126
|
+
const TestFormHook = createTestHook();
|
|
127
|
+
|
|
128
|
+
const Top = () => {
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
<AgeInputComponent controller={TestFormHook.controller} />
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
render(<Top />);
|
|
137
|
+
|
|
138
|
+
const ageInput: HTMLInputElement = screen.getByTestId('age-input');
|
|
139
|
+
await userEvent.type(ageInput, '9999');
|
|
140
|
+
|
|
141
|
+
expect(TestFormHook.api.getState().isDirty).toBeTruthy();
|
|
142
|
+
TestFormHook.actions.initializeForm({}, {cleanup: true});
|
|
143
|
+
expect(TestFormHook.api.getState().isDirty).toBeFalsy();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('User is able to push server side validation', async () => {
|
|
147
|
+
const TestFormHook = createTestHook();
|
|
148
|
+
|
|
149
|
+
const Top = () => {
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
<AgeInputComponent controller={TestFormHook.controller} />
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
render(<Top />);
|
|
158
|
+
|
|
159
|
+
const ageInput: HTMLInputElement = screen.getByTestId('age-input');
|
|
160
|
+
await userEvent.type(ageInput, '9999');
|
|
161
|
+
|
|
162
|
+
TestFormHook.actions.pushFormErrors((_s) => ({age: 'server side error'}));
|
|
163
|
+
const state1 = TestFormHook.api.getState();
|
|
164
|
+
expect(state1.isDirty).toBeTruthy();
|
|
165
|
+
expect(state1.isValid).toBeFalsy();
|
|
166
|
+
expect(state1.errors.age).toEqual('server side error');
|
|
167
|
+
|
|
168
|
+
await userEvent.type(ageInput, 'hgoehoge');
|
|
169
|
+
|
|
170
|
+
// confirm empty field won't overwrite any messages
|
|
171
|
+
TestFormHook.actions.pushFormErrors((_s) => ({}));
|
|
172
|
+
const state2 = TestFormHook.api.getState();
|
|
173
|
+
expect(state2.isDirty).toBeTruthy();
|
|
174
|
+
expect(state2.isValid).toBeFalsy();
|
|
175
|
+
expect(state2.errors.age).toEqual('Invalid input');
|
|
176
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -67,9 +67,15 @@ type FormControllerBehavior<StateBeforeValidation extends StateRestriction> = {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Initialize form state without validation.
|
|
70
|
+
*
|
|
71
|
+
* @param initialVal value to initialize form state
|
|
72
|
+
* @param opts.cleanup if true, form state is become pristine (which means, `form.isDirty` is going to be false) .
|
|
70
73
|
*/
|
|
71
74
|
initializeForm: (
|
|
72
75
|
initialVal: Partial<StateBeforeValidation>,
|
|
76
|
+
opts?: {
|
|
77
|
+
cleanup?: true;
|
|
78
|
+
},
|
|
73
79
|
) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
|
|
74
80
|
|
|
75
81
|
/**
|
|
@@ -151,10 +157,11 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
151
157
|
},
|
|
152
158
|
|
|
153
159
|
reset: () => () => fullInitialState,
|
|
154
|
-
initializeForm: (initialVal) => (prev) => ({
|
|
160
|
+
initializeForm: (initialVal, opts) => (prev) => ({
|
|
155
161
|
...prev,
|
|
156
162
|
value: {...prev.value, ...initialVal},
|
|
157
163
|
errors: {...initialErrors},
|
|
164
|
+
isDirty: opts?.cleanup === true ? false : prev.isDirty,
|
|
158
165
|
}),
|
|
159
166
|
handleIssues: (issues) => (prev) => {
|
|
160
167
|
// FIXME: handle duplication
|
|
@@ -171,7 +178,8 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
171
178
|
},
|
|
172
179
|
pushFormErrors: (validator) => (prev) => {
|
|
173
180
|
const pushedErrors = validator(prev.value);
|
|
174
|
-
|
|
181
|
+
const allErrors = {...prev.errors, ...pushedErrors};
|
|
182
|
+
return {...prev, isValid: Object.keys(allErrors).length === 0, errors: allErrors};
|
|
175
183
|
},
|
|
176
184
|
reduceFormErrors: (reducer) => (prev) => {
|
|
177
185
|
return {...prev, errors: reducer(prev.errors)};
|
|
@@ -196,7 +204,19 @@ function createFormStore<StateBeforeValidation extends StateRestriction, Schema
|
|
|
196
204
|
|
|
197
205
|
type ActionsCanBePublic<State extends StateRestriction> = {
|
|
198
206
|
reset: VoidFunction;
|
|
199
|
-
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Initialize form state without validation.
|
|
210
|
+
*
|
|
211
|
+
* @param initialVal value to initialize form state
|
|
212
|
+
* @param opts.cleanup if true, form state is become pristine (which means, `form.isDirty` is going to be false) .
|
|
213
|
+
*/
|
|
214
|
+
initializeForm: (
|
|
215
|
+
initial: Partial<State>,
|
|
216
|
+
opts?: {
|
|
217
|
+
cleanup?: true;
|
|
218
|
+
},
|
|
219
|
+
) => void;
|
|
200
220
|
pushFormErrors: (validator: (state: State) => Partial<FormErrors<State>>) => void;
|
|
201
221
|
handleChange: HandleChangeAction<State>;
|
|
202
222
|
handleBulkChange: (setter: (prev: State) => Partial<State>) => void;
|