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.
- package/.eslintrc.js +111 -0
- package/.github/workflows/checker.yml +42 -0
- package/.gitignore +217 -0
- package/.prettierrc +5 -0
- package/README.md +21 -45
- package/component.d.ts +1 -0
- package/component.js +1 -0
- package/dist/component.d.ts +7 -0
- package/dist/component.js +11 -0
- package/dist/component.test.d.ts +1 -0
- package/dist/component.test.js +53 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.js +146 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +74 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +6 -0
- package/docs/.nojekyll +0 -0
- package/docs/assets/css/main.css +2328 -0
- package/docs/assets/images/icons.png +0 -0
- package/docs/assets/images/icons@2x.png +0 -0
- package/docs/assets/images/widgets.png +0 -0
- package/docs/assets/images/widgets@2x.png +0 -0
- package/docs/assets/js/main.js +1 -0
- package/docs/assets/js/search.js +3 -0
- package/docs/globals.html +185 -0
- package/docs/index.html +249 -0
- package/docs/modules/_actions_.html +398 -0
- package/docs/modules/_forms_.html +362 -0
- package/docs/modules/_index_.html +170 -0
- package/docs/modules/_inputprotocol_.html +319 -0
- package/docs/modules/_mayoiga_.html +783 -0
- package/docs/modules/_reducer_.html +417 -0
- package/example/src/demo-app.tsx +123 -0
- package/example/src/index.tsx +15 -0
- package/package.json +41 -44
- package/setup/vite-global-setup.js +1 -0
- package/setup/vite-set-global.js +0 -0
- package/src/component.test.tsx +91 -0
- package/src/component.tsx +19 -0
- package/src/index.test.tsx +120 -0
- package/src/index.ts +303 -0
- package/src/utils.ts +8 -0
- package/tsconfig.json +38 -0
- package/vite.config.js +10 -0
- package/yarn.lock +2378 -0
- package/lib/actions.d.ts +0 -28
- package/lib/actions.js +0 -29
- package/lib/actions.js.map +0 -1
- package/lib/forms.d.ts +0 -21
- package/lib/forms.js +0 -82
- package/lib/forms.js.map +0 -1
- package/lib/index.d.ts +0 -3
- package/lib/index.js +0 -7
- package/lib/index.js.map +0 -1
- package/lib/inputProtocol.d.ts +0 -17
- package/lib/inputProtocol.js +0 -24
- package/lib/inputProtocol.js.map +0 -1
- package/lib/mayoiga.d.ts +0 -110
- package/lib/mayoiga.js +0 -131
- package/lib/mayoiga.js.map +0 -1
- package/lib/reducer.d.ts +0 -20
- package/lib/reducer.js +0 -53
- 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
|
+
});
|
package/dist/utils.d.ts
ADDED
package/dist/utils.js
ADDED
package/docs/.nojekyll
ADDED
|
File without changes
|