mayoiga 0.0.21 → 0.1.2

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 +53 -0
  12. package/dist/index.d.ts +91 -0
  13. package/dist/index.js +146 -0
  14. package/dist/index.test.d.ts +1 -0
  15. package/dist/index.test.js +74 -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 +41 -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 +91 -0
  40. package/src/component.tsx +19 -0
  41. package/src/index.test.tsx +120 -0
  42. package/src/index.ts +303 -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/dist/index.js ADDED
@@ -0,0 +1,146 @@
1
+ import { createStore } from 'nozuchi';
2
+ import { createElement, Fragment, useEffect, useMemo } from 'react';
3
+ import { isChangeEvent } from './utils';
4
+ const initialFormState = Object.freeze({ isDirty: false, isValid: false });
5
+ function createFormStore(initialState, schema) {
6
+ const initialErrors = Object.freeze(Object.keys(initialState).reduce((buf, k) => {
7
+ buf[k] = null;
8
+ return buf;
9
+ }, {}));
10
+ const fullInitialState = Object.freeze({
11
+ ...initialFormState,
12
+ value: { ...initialState },
13
+ errors: { ...initialErrors },
14
+ });
15
+ const withValidation = (prev, newValue) => {
16
+ const result = schema.safeParse(newValue);
17
+ if (result.success) {
18
+ return { ...prev, isDirty: true, isValid: true, value: newValue, errors: initialErrors };
19
+ }
20
+ const newErrors = result.error.issues.reduce((buf, iss) => {
21
+ // TODO: should support nested value?
22
+ const shallowPath = iss.path[0];
23
+ if (shallowPath === undefined) {
24
+ return buf;
25
+ }
26
+ buf[shallowPath] = iss.message;
27
+ return buf;
28
+ }, {});
29
+ return { ...prev, isDirty: true, isValid: false, errors: newErrors, value: newValue };
30
+ };
31
+ return createStore(fullInitialState, {
32
+ peek: (eyeball) => (prev) => {
33
+ eyeball(prev);
34
+ return prev;
35
+ },
36
+ reset: () => () => fullInitialState,
37
+ initializeForm: (initialVal) => (prev) => ({
38
+ ...prev,
39
+ value: { ...prev.value, ...initialVal },
40
+ errors: { ...initialErrors },
41
+ }),
42
+ handleIssues: (issues) => (prev) => {
43
+ // FIXME: handle duplication
44
+ const newErrors = issues.reduce((buf, iss) => {
45
+ const shallowPath = iss.path[0];
46
+ if (shallowPath === undefined) {
47
+ return buf;
48
+ }
49
+ buf[shallowPath] = iss.message;
50
+ return buf;
51
+ }, {});
52
+ return { ...prev, isDirty: true, isValid: false, errors: newErrors };
53
+ },
54
+ handleError: (reducer) => (prev) => {
55
+ return { ...prev, errors: reducer(prev.errors) };
56
+ },
57
+ handleChange: (name, value) => (prev) => {
58
+ if (name in prev.value === false) {
59
+ return prev;
60
+ }
61
+ const newValue = { ...prev.value, [name]: value };
62
+ return withValidation(prev, newValue);
63
+ },
64
+ reduceFormState: (reducer) => (prev) => {
65
+ const reducedState = reducer(prev.value);
66
+ return withValidation(prev, reducedState);
67
+ },
68
+ handleBulkChange: (setter) => (prev) => {
69
+ const mergedNewState = { ...prev.value, ...setter(prev.value) };
70
+ return withValidation(prev, mergedNewState);
71
+ },
72
+ });
73
+ }
74
+ /**
75
+ * @typeParam InitialState should be shallow
76
+ */
77
+ export function createFormHook(initialState, schema) {
78
+ const store = createFormStore(initialState, schema);
79
+ const formHook = {
80
+ useInitialize: (initialValue) => {
81
+ useEffect(() => {
82
+ store.actions.initializeForm(initialValue);
83
+ }, []);
84
+ },
85
+ useSelector: store.useSelector,
86
+ handleSubmit: (handler) => {
87
+ return (e) => {
88
+ const eventHandled = handler(e);
89
+ const value = store.getState().value;
90
+ return schema.safeParseAsync(value).then((result) => {
91
+ // FIXME: hook formを捨ててzodのバージョンをあげればこの辺に型をつけられる
92
+ if (result.success) {
93
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
94
+ return eventHandled({ success: true, data: result.data, error: undefined });
95
+ }
96
+ void eventHandled({ success: false, error: result.error.issues });
97
+ store.actions.handleIssues(result.error.issues);
98
+ });
99
+ };
100
+ },
101
+ components: {
102
+ Slicer(props) {
103
+ const { selector, children } = props;
104
+ const value = formHook.useSelector(selector);
105
+ return useMemo(() => {
106
+ const renderContent = children({ handleChange: formHook.actions.handleChange }, ...value);
107
+ return createElement(Fragment, {}, renderContent);
108
+ }, value);
109
+ },
110
+ Field(props) {
111
+ const { name, children } = props;
112
+ const [value, errMsg] = store.useSelector((s) => [s.value[name], s.errors[name]]);
113
+ return useMemo(() => {
114
+ const renderContent = children({
115
+ name,
116
+ value,
117
+ onChange: (name_or_event, value_or_none) => {
118
+ if (typeof name_or_event === 'string') {
119
+ // assumes name is valid if it's string
120
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
121
+ return store.actions.handleChange(name_or_event, value_or_none);
122
+ }
123
+ if (isChangeEvent(name_or_event)) {
124
+ const target = name_or_event.currentTarget || name_or_event.target;
125
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
126
+ return store.actions.handleChange(target.name, target.value);
127
+ }
128
+ throw new Error('`handleChange` handles unexpected formed object.');
129
+ },
130
+ }, value, errMsg);
131
+ return createElement(Fragment, {}, renderContent);
132
+ }, [value, errMsg]);
133
+ },
134
+ },
135
+ actions: store.actions,
136
+ };
137
+ return {
138
+ controller: formHook,
139
+ api: store,
140
+ ...formHook,
141
+ };
142
+ }
143
+ export function useFormSlice(formController, selector) {
144
+ const sliced = formController.useSelector((s) => selector(s));
145
+ return [sliced, formController.actions];
146
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,74 @@
1
+ import { render, screen, waitFor } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import React from 'react';
4
+ import { test, expect } from 'vitest';
5
+ import * as zod from 'zod';
6
+ import { createFormHook, useFormSlice } from './index';
7
+ const testSchema = zod.object({
8
+ name: zod.string(),
9
+ description: zod.string(),
10
+ age: zod.string().transform(val => Number(val)).refine(val => Number.isNaN(val) ? 0 : val),
11
+ marked: zod.boolean(),
12
+ });
13
+ const createTestHook = () => createFormHook({ name: '', description: '', age: '', marked: false }, testSchema);
14
+ const AgeInputComponent = (props) => {
15
+ const { controller } = props;
16
+ const [[age, errorMessage], { handleChange }] = useFormSlice(controller, s => [s.value.age, s.errors.age]);
17
+ return (React.createElement("div", null,
18
+ React.createElement("input", { "data-testid": "age-input", value: age, name: "age", onChange: (e) => {
19
+ handleChange('age', e.currentTarget.value);
20
+ } }),
21
+ React.createElement("span", { "data-testid": "age-error" }, errorMessage)));
22
+ };
23
+ // afterEach(cleanup);
24
+ test('useSelector can select the fields', async () => {
25
+ const TestFormHook = createTestHook();
26
+ const Top = () => {
27
+ return (React.createElement("div", null,
28
+ React.createElement(AgeInputComponent, { controller: TestFormHook.controller })));
29
+ };
30
+ render(React.createElement(Top, null));
31
+ const INPUT_AGE = '4';
32
+ // fireEvent.change(screen.getByTestId('age-input') , {target: {value: INPUT_AGE}})
33
+ // calling `userEvent` in here will cause race condition...
34
+ await userEvent.type(screen.getByTestId('age-input'), INPUT_AGE);
35
+ // TestFormHook.actions.handleBulkChange(prev => ({...prev, name: 'test', description: 'desc'}));
36
+ await waitFor(() => {
37
+ const ageInput = screen.getByTestId('age-input');
38
+ expect(ageInput.value).toBe(INPUT_AGE);
39
+ });
40
+ });
41
+ test('zod parse value before submit', async () => {
42
+ const TestFormHook = createTestHook();
43
+ const INPUT_AGE = '9';
44
+ const PARSED_RESULT = Number(INPUT_AGE);
45
+ const handleSubmitTester = TestFormHook.handleSubmit((e) => (result) => {
46
+ if (result.success) {
47
+ expect(result.data.age).toBe(PARSED_RESULT);
48
+ }
49
+ else {
50
+ expect('should not called').toBe(result.error);
51
+ }
52
+ });
53
+ const Top = () => {
54
+ return (React.createElement("div", null,
55
+ React.createElement(AgeInputComponent, { controller: TestFormHook }),
56
+ React.createElement("button", { "data-testid": "submit-button", onClick: handleSubmitTester })));
57
+ };
58
+ render(React.createElement(Top, null));
59
+ const ageInput = screen.getByTestId('age-input');
60
+ await userEvent.type(ageInput, INPUT_AGE);
61
+ await waitFor(() => {
62
+ const ageInput = screen.getByTestId('age-input');
63
+ expect(ageInput.value).toBe(INPUT_AGE);
64
+ });
65
+ const submitButton = screen.getByTestId('submit-button');
66
+ await userEvent.click(submitButton);
67
+ await waitFor(() => {
68
+ console.log('waiting');
69
+ TestFormHook.api.actions.peek((s) => {
70
+ expect(s.value.age).toBe(INPUT_AGE);
71
+ expect(s.errors.age).toBeNull();
72
+ });
73
+ });
74
+ });
@@ -0,0 +1,2 @@
1
+ import type { ChangeEvent } from 'react';
2
+ export declare function isChangeEvent<T>(x: any): x is ChangeEvent<T>;
package/dist/utils.js ADDED
@@ -0,0 +1,6 @@
1
+ export function isChangeEvent(x) {
2
+ if (typeof x !== 'object') {
3
+ return false;
4
+ }
5
+ return 'target' in x;
6
+ }
package/docs/.nojekyll ADDED
File without changes