mayoiga 0.0.22 → 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.
Files changed (64) hide show
  1. package/.eslintrc.js +111 -0
  2. package/.github/workflows/checker.yml +42 -0
  3. package/.gitignore +217 -0
  4. package/.prettierrc +5 -0
  5. package/README.md +21 -45
  6. package/component.d.ts +1 -0
  7. package/component.js +1 -0
  8. package/dist/component.d.ts +7 -0
  9. package/dist/component.js +11 -0
  10. package/dist/component.test.d.ts +1 -0
  11. package/dist/component.test.js +83 -0
  12. package/dist/index.d.ts +92 -0
  13. package/dist/index.js +150 -0
  14. package/dist/index.test.d.ts +1 -0
  15. package/dist/index.test.js +77 -0
  16. package/dist/utils.d.ts +2 -0
  17. package/dist/utils.js +6 -0
  18. package/docs/.nojekyll +0 -0
  19. package/docs/assets/css/main.css +2328 -0
  20. package/docs/assets/images/icons.png +0 -0
  21. package/docs/assets/images/icons@2x.png +0 -0
  22. package/docs/assets/images/widgets.png +0 -0
  23. package/docs/assets/images/widgets@2x.png +0 -0
  24. package/docs/assets/js/main.js +1 -0
  25. package/docs/assets/js/search.js +3 -0
  26. package/docs/globals.html +185 -0
  27. package/docs/index.html +249 -0
  28. package/docs/modules/_actions_.html +398 -0
  29. package/docs/modules/_forms_.html +362 -0
  30. package/docs/modules/_index_.html +170 -0
  31. package/docs/modules/_inputprotocol_.html +319 -0
  32. package/docs/modules/_mayoiga_.html +783 -0
  33. package/docs/modules/_reducer_.html +417 -0
  34. package/example/src/demo-app.tsx +123 -0
  35. package/example/src/index.tsx +15 -0
  36. package/package.json +42 -44
  37. package/setup/vite-global-setup.js +1 -0
  38. package/setup/vite-set-global.js +0 -0
  39. package/src/component.test.tsx +146 -0
  40. package/src/component.tsx +23 -0
  41. package/src/index.test.tsx +122 -0
  42. package/src/index.ts +311 -0
  43. package/src/utils.ts +8 -0
  44. package/tsconfig.json +38 -0
  45. package/vite.config.js +10 -0
  46. package/yarn.lock +2378 -0
  47. package/lib/actions.d.ts +0 -28
  48. package/lib/actions.js +0 -29
  49. package/lib/actions.js.map +0 -1
  50. package/lib/forms.d.ts +0 -21
  51. package/lib/forms.js +0 -82
  52. package/lib/forms.js.map +0 -1
  53. package/lib/index.d.ts +0 -3
  54. package/lib/index.js +0 -7
  55. package/lib/index.js.map +0 -1
  56. package/lib/inputProtocol.d.ts +0 -17
  57. package/lib/inputProtocol.js +0 -24
  58. package/lib/inputProtocol.js.map +0 -1
  59. package/lib/mayoiga.d.ts +0 -110
  60. package/lib/mayoiga.js +0 -131
  61. package/lib/mayoiga.js.map +0 -1
  62. package/lib/reducer.d.ts +0 -20
  63. package/lib/reducer.js +0 -53
  64. package/lib/reducer.js.map +0 -1
package/src/index.ts ADDED
@@ -0,0 +1,311 @@
1
+ import {createStore} from 'nozuchi';
2
+ import {createElement, Fragment, useEffect, useMemo} from 'react';
3
+
4
+ import {isChangeEvent} from './utils';
5
+
6
+ import type {Subscriber} from 'nozuchi';
7
+ import type {BaseSyntheticEvent, ChangeEvent, ReactNode, ReactElement} from 'react';
8
+
9
+ import type {infer as zodInfer, SafeParseReturnType, ZodIssue, ZodType} from 'zod';
10
+
11
+ export type StateRestriction = Record<string, any>;
12
+
13
+ type FormStatus = {
14
+ isDirty: boolean;
15
+ isValid: boolean;
16
+ };
17
+
18
+ type FormErrors<State extends StateRestriction> = {
19
+ [k in keyof State]: string | null;
20
+ };
21
+
22
+ export type FullFormState<State extends StateRestriction> = FormStatus & {value: State; errors: FormErrors<State>};
23
+
24
+ export type HandleChangeAction<State extends StateRestriction, R = void> = <Name extends keyof State>(name: Name, value: State[Name]) => R;
25
+
26
+ export type FieldProps<State extends StateRestriction, Name extends keyof State> = {
27
+ name: Name;
28
+ children: (
29
+ tool: {
30
+ name: Name;
31
+ value: State[Name];
32
+ onChange(this: void, name: Name, value: State[Name]): void;
33
+ onChange(this: void, e: ChangeEvent<HTMLElement>): void;
34
+ },
35
+ value: State[Name],
36
+ errorMessage: string | null,
37
+ ) => ReactNode;
38
+ };
39
+
40
+ export type SliceProps<State extends StateRestriction, Selected extends ReadonlyArray<unknown>> = {
41
+ selector: (s: FullFormState<State>) => Selected;
42
+ children: (tool: {handleChange: HandleChangeAction<State>}, ...value: Readonly<Selected>) => ReactNode;
43
+ };
44
+
45
+ type FormControllerBehavior<StateBeforeValidation> = {
46
+ /** For testing */
47
+ peek: (
48
+ eyeball: (prev: FullFormState<StateBeforeValidation>) => void,
49
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
50
+
51
+ reset: () => () => FullFormState<StateBeforeValidation>;
52
+
53
+ /**
54
+ * Initialize form state without validation.
55
+ */
56
+ initializeForm: (
57
+ initialVal: Partial<StateBeforeValidation>,
58
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
59
+
60
+ /**
61
+ * Handle issue that zod reported
62
+ */
63
+ handleIssues: (issues: ReadonlyArray<ZodIssue>) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
64
+
65
+ /**
66
+ * You can handle server side or other external error via this action.
67
+ */
68
+ pushFormErrors: (
69
+ validator: (state: StateBeforeValidation) => Partial<FormErrors<StateBeforeValidation>>,
70
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
71
+
72
+ reduceFormErrors: (
73
+ reducer: (prev: FormErrors<StateBeforeValidation>) => FormErrors<StateBeforeValidation>,
74
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
75
+
76
+ /**
77
+ */
78
+ handleChange: HandleChangeAction<
79
+ StateBeforeValidation,
80
+ (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>
81
+ >;
82
+
83
+ reduceFormState: (
84
+ reducer: (prevValue: StateBeforeValidation) => StateBeforeValidation,
85
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
86
+ handleBulkChange: (
87
+ setter: (prev: StateBeforeValidation) => Partial<StateBeforeValidation>,
88
+ ) => (prev: FullFormState<StateBeforeValidation>) => FullFormState<StateBeforeValidation>;
89
+ };
90
+
91
+ const initialFormState = Object.freeze({isDirty: false, isValid: false});
92
+
93
+ function createFormStore<StateBeforeValidation extends StateRestriction, Schema extends ZodType<any, any, any>>(
94
+ initialState: StateBeforeValidation,
95
+ schema: Schema,
96
+ ): Subscriber<FullFormState<StateBeforeValidation>, FormControllerBehavior<StateBeforeValidation>> {
97
+ type FullState = FullFormState<StateBeforeValidation>;
98
+ type Err = FormErrors<StateBeforeValidation>;
99
+ type StateKeys = keyof StateBeforeValidation;
100
+
101
+ const initialErrors = Object.freeze(
102
+ Object.keys(initialState).reduce((buf, k) => {
103
+ buf[k as StateKeys] = null;
104
+ return buf;
105
+ }, {} as any as Err),
106
+ );
107
+
108
+ const fullInitialState: FullState = Object.freeze({
109
+ ...initialFormState,
110
+ value: {...initialState},
111
+ errors: {...initialErrors},
112
+ });
113
+
114
+ const withValidation = (prev: FullState, newValue: StateBeforeValidation) => {
115
+ const result = schema.safeParse(newValue);
116
+ if (result.success) {
117
+ return {...prev, isDirty: true, isValid: true, value: newValue, errors: initialErrors};
118
+ }
119
+ const newErrors = result.error.issues.reduce((buf, iss) => {
120
+ // TODO: should support nested value?
121
+ const shallowPath = iss.path[0];
122
+ if (shallowPath === undefined) {
123
+ return buf;
124
+ }
125
+ buf[shallowPath as any as StateKeys] = iss.message;
126
+ return buf;
127
+ }, {} as any as Err);
128
+
129
+ return {...prev, isDirty: true, isValid: false, errors: newErrors, value: newValue};
130
+ };
131
+
132
+ return createStore(fullInitialState, {
133
+ peek: (eyeball) => (prev) => {
134
+ eyeball(prev);
135
+ return prev;
136
+ },
137
+
138
+ reset: () => () => fullInitialState,
139
+ initializeForm: (initialVal) => (prev) => ({
140
+ ...prev,
141
+ value: {...prev.value, ...initialVal},
142
+ errors: {...initialErrors},
143
+ }),
144
+ handleIssues: (issues) => (prev) => {
145
+ // FIXME: handle duplication
146
+ const newErrors = issues.reduce((buf, iss) => {
147
+ const shallowPath = iss.path[0];
148
+ if (shallowPath === undefined) {
149
+ return buf;
150
+ }
151
+ buf[shallowPath as any as StateKeys] = iss.message;
152
+ return buf;
153
+ }, {} as any as Err);
154
+
155
+ return {...prev, isDirty: true, isValid: false, errors: newErrors};
156
+ },
157
+ pushFormErrors: (validator) => (prev) => {
158
+ const pushedErrors = validator(prev.value);
159
+ return {...prev, errors: {...prev.errors, ...pushedErrors}};
160
+ },
161
+ reduceFormErrors: (reducer) => (prev) => {
162
+ return {...prev, errors: reducer(prev.errors)};
163
+ },
164
+ handleChange: (name, value) => (prev) => {
165
+ if (name in prev.value === false) {
166
+ return prev;
167
+ }
168
+ const newValue = {...prev.value, [name]: value};
169
+ return withValidation(prev, newValue);
170
+ },
171
+ reduceFormState: (reducer) => (prev) => {
172
+ const reducedState = reducer(prev.value);
173
+ return withValidation(prev, reducedState);
174
+ },
175
+ handleBulkChange: (setter) => (prev) => {
176
+ const mergedNewState = {...prev.value, ...setter(prev.value)};
177
+ return withValidation(prev, mergedNewState);
178
+ },
179
+ });
180
+ }
181
+
182
+ type ActionsCanBePublic<State extends StateRestriction> = {
183
+ reset: VoidFunction;
184
+ initializeForm: (initial: Partial<State>) => void;
185
+ pushFormErrors: (validator: (state: State) => Partial<FormErrors<State>>) => void;
186
+ handleChange: HandleChangeAction<State>;
187
+ handleBulkChange: (setter: (prev: State) => Partial<State>) => void;
188
+ };
189
+
190
+ /**
191
+ * Restricted type interface of the form store
192
+ * TODO: more restriction
193
+ */
194
+ export type Controller<State extends StateRestriction> = {
195
+ useInitialize: (initialValue: Partial<State>) => void;
196
+ useSelector: <R>(selector: (state: FullFormState<State>) => R, isEqual?: (prev: R, current: R) => boolean) => R;
197
+
198
+ actions: ActionsCanBePublic<State>;
199
+ components: {
200
+ Field: <Name extends keyof State>(props: FieldProps<State, Name>) => ReactElement<any, any> | null;
201
+ Slicer: <Selected extends ReadonlyArray<unknown>>(props: SliceProps<State, Selected>) => ReactElement<any, any> | null;
202
+ };
203
+ };
204
+
205
+ type FormHook<State extends StateRestriction, Schema extends ZodType<any, any, any>> = {
206
+ controller: Controller<State>;
207
+
208
+ handleSubmit: (
209
+ handler: (
210
+ e: BaseSyntheticEvent,
211
+ ) => (
212
+ val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>},
213
+ ) => void | Promise<void>,
214
+ ) => (e: BaseSyntheticEvent) => void;
215
+
216
+ api: Subscriber<FullFormState<State>, FormControllerBehavior<State>>;
217
+ } & Controller<State>;
218
+
219
+ /**
220
+ * @typeParam InitialState should be shallow
221
+ */
222
+ export function createFormHook<StateBeforeValidation, Schema extends ZodType<any, any, any>>(
223
+ initialState: StateBeforeValidation,
224
+ schema: Schema,
225
+ ): FormHook<StateBeforeValidation, Schema> {
226
+ const store = createFormStore(initialState, schema);
227
+
228
+ const formHook = {
229
+ useInitialize: (initialValue: Partial<StateBeforeValidation>) => {
230
+ useEffect(() => {
231
+ store.actions.initializeForm(initialValue);
232
+ }, []);
233
+ },
234
+ useSelector: store.useSelector,
235
+ handleSubmit: (
236
+ handler: (
237
+ e: BaseSyntheticEvent,
238
+ ) => (
239
+ val: {success: true; data: zodInfer<Schema>; error: undefined} | {success: false; error: ReadonlyArray<ZodIssue>},
240
+ ) => void | Promise<void>,
241
+ ) => {
242
+ return (e: BaseSyntheticEvent) => {
243
+ const eventHandled = handler(e);
244
+ const value = store.getState().value;
245
+ return schema.safeParseAsync(value).then((result: SafeParseReturnType<StateBeforeValidation, zodInfer<typeof schema>>) => {
246
+ // FIXME: hook formを捨ててzodのバージョンをあげればこの辺に型をつけられる
247
+ if (result.success) {
248
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
249
+ return eventHandled({success: true, data: result.data, error: undefined});
250
+ }
251
+ void eventHandled({success: false, error: result.error.issues});
252
+ store.actions.handleIssues(result.error.issues);
253
+ });
254
+ };
255
+ },
256
+
257
+ components: {
258
+ Slicer<R extends ReadonlyArray<unknown>>(props: SliceProps<StateBeforeValidation, R>) {
259
+ const {selector, children} = props;
260
+ const value = formHook.useSelector(selector);
261
+ return useMemo(() => {
262
+ const renderContent = children({handleChange: formHook.actions.handleChange}, ...value);
263
+ return createElement(Fragment, {}, renderContent);
264
+ }, value);
265
+ },
266
+ Field<Name extends keyof StateBeforeValidation>(props: FieldProps<StateBeforeValidation, Name>) {
267
+ const {name, children} = props;
268
+ const [value, errMsg] = store.useSelector((s) => [s.value[name], s.errors[name]] as const);
269
+ return useMemo(() => {
270
+ const renderContent = children(
271
+ {
272
+ name,
273
+ value,
274
+ onChange: (name_or_event: unknown, value_or_none?: unknown) => {
275
+ if (typeof name_or_event === 'string') {
276
+ // assumes name is valid if it's string
277
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
278
+ return store.actions.handleChange(name_or_event as Name, value_or_none as any);
279
+ }
280
+ if (isChangeEvent(name_or_event)) {
281
+ const target: any = name_or_event.currentTarget || name_or_event.target;
282
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
283
+ return store.actions.handleChange(target.name, target.value);
284
+ }
285
+ throw new Error('`handleChange` handles unexpected formed object.');
286
+ },
287
+ },
288
+ value,
289
+ errMsg,
290
+ );
291
+ return createElement(Fragment, {}, renderContent);
292
+ }, [value, errMsg]);
293
+ },
294
+ },
295
+ actions: store.actions,
296
+ };
297
+
298
+ return {
299
+ controller: formHook,
300
+ api: store,
301
+ ...formHook,
302
+ };
303
+ }
304
+
305
+ export function useFormSlice<State extends StateRestriction, Sliced>(
306
+ formController: Controller<State>,
307
+ selector: (s: FullFormState<State>) => Sliced,
308
+ ) {
309
+ const sliced = formController.useSelector((s) => selector(s));
310
+ return [sliced, formController.actions] as const;
311
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type {ChangeEvent} from 'react';
2
+
3
+ export function isChangeEvent<T>(x: any): x is ChangeEvent<T> {
4
+ if (typeof x !== 'object') {
5
+ return false;
6
+ }
7
+ return 'target' in x;
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "declaration": true,
6
+ "jsx": "react",
7
+ "moduleResolution": "node",
8
+ // TODO: skip while vitest is broken
9
+ "skipLibCheck": true,
10
+
11
+ "strict": true,
12
+
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": false,
15
+ "noImplicitReturns": true,
16
+ "noFallthroughCasesInSwitch": true,
17
+ "noUncheckedIndexedAccess": true,
18
+ "useUnknownInCatchVariables": true,
19
+ "exactOptionalPropertyTypes": true,
20
+
21
+ "forceConsistentCasingInFileNames": true,
22
+ "removeComments": false,
23
+
24
+ "allowSyntheticDefaultImports": true,
25
+ "esModuleInterop": true,
26
+ "noErrorTruncation": true,
27
+
28
+ "lib": ["dom", "esnext"],
29
+ // TODO: node is for vitest... I should isolate it probably?
30
+ "types": ["react/next"],
31
+
32
+ "rootDir": "./src",
33
+ "outDir": "./dist"
34
+ },
35
+ "include": [
36
+ "./src/**/*"
37
+ ],
38
+ }
package/vite.config.js ADDED
@@ -0,0 +1,10 @@
1
+ import {defineConfig} from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ // environment: 'happy-dom', DO NOT USE IT
7
+ environment: 'jsdom',
8
+ setupFiles: ['./setup/vite-global-setup.js'],
9
+ },
10
+ })