taro-form-react 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +233 -0
- package/dist/index.js +21 -0
- package/package.json +56 -0
- package/rollup.config.mjs +53 -0
- package/src/components/Item.tsx +209 -0
- package/src/components/Keep.tsx +28 -0
- package/src/components/Label.tsx +96 -0
- package/src/components/Provider.tsx +15 -0
- package/src/context/FormContext.tsx +441 -0
- package/src/hooks/useFormConfiguration.ts +37 -0
- package/src/hooks/useFormItem.ts +75 -0
- package/src/hooks/useMap.ts +36 -0
- package/src/index.tsx +99 -0
- package/src/styles/index.scss +87 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/rules.ts +55 -0
- package/src/utils/tools.ts +31 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
|
|
3
|
+
import { View } from "@tarojs/components";
|
|
4
|
+
import classNames from "classnames";
|
|
5
|
+
|
|
6
|
+
import useFormConfiguration from "../hooks/useFormConfiguration";
|
|
7
|
+
import useFormItem from "../hooks/useFormItem";
|
|
8
|
+
|
|
9
|
+
import { validateRules } from "../utils/rules";
|
|
10
|
+
|
|
11
|
+
import type { FieldRefActions, FormProviderConfiguration } from "../context/FormContext";
|
|
12
|
+
import type { NamePath, Rule } from "../types";
|
|
13
|
+
|
|
14
|
+
import FormLabel from "./Label";
|
|
15
|
+
|
|
16
|
+
export type FormItemProps =
|
|
17
|
+
& FormProviderConfiguration
|
|
18
|
+
& {
|
|
19
|
+
name: NamePath;
|
|
20
|
+
dependencies?: NamePath[];
|
|
21
|
+
noStyle?: boolean;
|
|
22
|
+
required?: boolean;
|
|
23
|
+
trigger?: string;
|
|
24
|
+
valuePropName?: string;
|
|
25
|
+
getValueFromEvent?: (...args: any[]) => any;
|
|
26
|
+
label?: React.ReactNode;
|
|
27
|
+
initialValue?: any;
|
|
28
|
+
rules?: Rule[];
|
|
29
|
+
validateTrigger?: string[];
|
|
30
|
+
className?: classNames.Argument;
|
|
31
|
+
innerClassName?: classNames.Argument;
|
|
32
|
+
transform?: (value: any) => any;
|
|
33
|
+
}
|
|
34
|
+
& (
|
|
35
|
+
| {
|
|
36
|
+
hidden?: false;
|
|
37
|
+
children: JSX.Element;
|
|
38
|
+
}
|
|
39
|
+
| {
|
|
40
|
+
hidden: true;
|
|
41
|
+
children?: any;
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const FormItem: React.FC<FormItemProps> = ({
|
|
46
|
+
hidden,
|
|
47
|
+
name,
|
|
48
|
+
required,
|
|
49
|
+
trigger = "onChange",
|
|
50
|
+
valuePropName = "value",
|
|
51
|
+
getValueFromEvent = (...args: any[]) => args[0],
|
|
52
|
+
dependencies,
|
|
53
|
+
label,
|
|
54
|
+
noStyle,
|
|
55
|
+
initialValue,
|
|
56
|
+
rules,
|
|
57
|
+
validateTrigger: pValidateTrigger,
|
|
58
|
+
className,
|
|
59
|
+
innerClassName,
|
|
60
|
+
transform,
|
|
61
|
+
children,
|
|
62
|
+
// configuration props within FormProvider
|
|
63
|
+
...props
|
|
64
|
+
}) => {
|
|
65
|
+
const ref = useRef<FieldRefActions>({
|
|
66
|
+
validate: () => Promise.resolve(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const { value, errors, isTouched, dependencyValues, onChange, onErrorsChange } = useFormItem({
|
|
70
|
+
ref,
|
|
71
|
+
name,
|
|
72
|
+
initialValue,
|
|
73
|
+
dependencies,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const {
|
|
77
|
+
colon,
|
|
78
|
+
labelProps,
|
|
79
|
+
layout,
|
|
80
|
+
validateFirst,
|
|
81
|
+
showErrors,
|
|
82
|
+
passthroughErrors,
|
|
83
|
+
transformBehavior,
|
|
84
|
+
getRequiredMessage,
|
|
85
|
+
} = useFormConfiguration(props);
|
|
86
|
+
|
|
87
|
+
const allRules = useMemo(() => {
|
|
88
|
+
if (required && !rules?.some(rule => "required" in rule)) {
|
|
89
|
+
return [
|
|
90
|
+
{ required: true, message: getRequiredMessage(String(label)) },
|
|
91
|
+
...rules ?? [],
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
return rules;
|
|
95
|
+
}, [getRequiredMessage, label, required, rules]);
|
|
96
|
+
|
|
97
|
+
const handleValidate = useCallback(async (params: { value?: any } = {}) => {
|
|
98
|
+
const errors = await validateRules(
|
|
99
|
+
"value" in params ? params.value : value,
|
|
100
|
+
allRules ?? [],
|
|
101
|
+
validateFirst,
|
|
102
|
+
);
|
|
103
|
+
onErrorsChange(errors ?? []);
|
|
104
|
+
|
|
105
|
+
return errors;
|
|
106
|
+
}, [value, allRules, validateFirst, onErrorsChange]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const nextRef: FieldRefActions = {
|
|
110
|
+
validate: handleValidate,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (transform) {
|
|
114
|
+
nextRef.transform = async () => {
|
|
115
|
+
const transformed = await transform(value);
|
|
116
|
+
return {
|
|
117
|
+
__form_internals_should_merge: transformBehavior === "merge",
|
|
118
|
+
value: transformed,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ref.current = nextRef;
|
|
124
|
+
}, [handleValidate, transform, transformBehavior, value]);
|
|
125
|
+
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
isTouched && handleValidate();
|
|
128
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
129
|
+
}, dependencyValues);
|
|
130
|
+
|
|
131
|
+
const validateTrigger = useMemo(() => pValidateTrigger ?? [trigger], [pValidateTrigger, trigger]);
|
|
132
|
+
|
|
133
|
+
const handleChange = useCallback((...args: any[]) => {
|
|
134
|
+
const newValue = getValueFromEvent(...args);
|
|
135
|
+
onChange(newValue);
|
|
136
|
+
validateTrigger.includes(trigger) && handleValidate({ value: newValue });
|
|
137
|
+
children?.props?.[trigger]?.(...args);
|
|
138
|
+
}, [children?.props, getValueFromEvent, handleValidate, onChange, trigger, validateTrigger]);
|
|
139
|
+
|
|
140
|
+
const withValidate = (fn?: (...args: any[]) => void) => {
|
|
141
|
+
return (...args: any[]) => {
|
|
142
|
+
handleValidate();
|
|
143
|
+
fn?.(...args);
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (hidden) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const hasError = errors.length > 0;
|
|
152
|
+
|
|
153
|
+
const InputComponent = React.cloneElement(children, {
|
|
154
|
+
[trigger]: handleChange,
|
|
155
|
+
[valuePropName]: value,
|
|
156
|
+
...validateTrigger.filter(cur => cur !== trigger).reduce((acc, cur) => {
|
|
157
|
+
return {
|
|
158
|
+
...acc,
|
|
159
|
+
[cur]: withValidate((children.props as any)[cur]),
|
|
160
|
+
};
|
|
161
|
+
}, {}),
|
|
162
|
+
className: classNames(
|
|
163
|
+
children.props.className,
|
|
164
|
+
),
|
|
165
|
+
...(passthroughErrors && {
|
|
166
|
+
hasError,
|
|
167
|
+
errors,
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return noStyle
|
|
172
|
+
? InputComponent
|
|
173
|
+
: (
|
|
174
|
+
<View
|
|
175
|
+
className={classNames(
|
|
176
|
+
"tfr-form-item-container",
|
|
177
|
+
layout === "horizontal" && "tfr-form-item-container-horizontal",
|
|
178
|
+
className,
|
|
179
|
+
)}
|
|
180
|
+
>
|
|
181
|
+
<View
|
|
182
|
+
className={classNames(
|
|
183
|
+
"tfr-form-item-inner",
|
|
184
|
+
layout === "horizontal" && "tfr-form-item-inner-horizontal",
|
|
185
|
+
layout === "vertical" && "tfr-form-item-inner-vertical",
|
|
186
|
+
innerClassName,
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
<FormLabel
|
|
190
|
+
label={label}
|
|
191
|
+
required={required}
|
|
192
|
+
colon={colon}
|
|
193
|
+
{...labelProps}
|
|
194
|
+
/>
|
|
195
|
+
{InputComponent}
|
|
196
|
+
</View>
|
|
197
|
+
{hasError && showErrors && errors.map((error, index) => (
|
|
198
|
+
<View
|
|
199
|
+
key={index}
|
|
200
|
+
className="tfr-form-item-error-text"
|
|
201
|
+
>
|
|
202
|
+
{error}
|
|
203
|
+
</View>
|
|
204
|
+
))}
|
|
205
|
+
</View>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default FormItem;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import type { NamePath } from "../types";
|
|
4
|
+
|
|
5
|
+
import { namePathToString } from "../utils";
|
|
6
|
+
import FormItem from "./Item";
|
|
7
|
+
|
|
8
|
+
export type FormKeepProps = {
|
|
9
|
+
fields: NamePath[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const FormKeep: React.FC<FormKeepProps> = ({ fields }) => {
|
|
13
|
+
const dedupedFields = useMemo(() => {
|
|
14
|
+
const result = new Map<string, NamePath>();
|
|
15
|
+
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
result.set(namePathToString(field), field);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return Array.from(result.values());
|
|
21
|
+
}, [fields]);
|
|
22
|
+
|
|
23
|
+
return dedupedFields.length > 0
|
|
24
|
+
? dedupedFields.map(field => <FormItem key={namePathToString(field)} name={field} hidden />)
|
|
25
|
+
: null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default FormKeep;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { Text, View } from "@tarojs/components";
|
|
4
|
+
import classNames from "classnames/dedupe";
|
|
5
|
+
|
|
6
|
+
export type FormLabelProps = {
|
|
7
|
+
required?: boolean;
|
|
8
|
+
size?: "normal" | "small";
|
|
9
|
+
label?: React.ReactNode;
|
|
10
|
+
labelClassName?: classNames.Argument;
|
|
11
|
+
addonAfter?: React.ReactNode;
|
|
12
|
+
className?: classNames.Argument;
|
|
13
|
+
containerClassName?: classNames.Argument;
|
|
14
|
+
colon?: boolean;
|
|
15
|
+
colonClassName?: classNames.Argument;
|
|
16
|
+
asteriskTookSpace?: boolean;
|
|
17
|
+
children?: React.ReactNode;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Label = ({
|
|
21
|
+
size,
|
|
22
|
+
required,
|
|
23
|
+
labelClassName,
|
|
24
|
+
label,
|
|
25
|
+
}: Pick<FormLabelProps, "size" | "label" | "labelClassName" | "required">) => {
|
|
26
|
+
const asteriskHideClassName =
|
|
27
|
+
size === "small"
|
|
28
|
+
? "tfr-small tfr-hide-asterisk"
|
|
29
|
+
: "tfr-normal tfr-hide-asterisk";
|
|
30
|
+
const asteriskShowClassName =
|
|
31
|
+
size === "small"
|
|
32
|
+
? "tfr-small show-asterisk"
|
|
33
|
+
: "tfr-normal show-asterisk";
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Text
|
|
37
|
+
className={classNames(
|
|
38
|
+
"tfr-form-label",
|
|
39
|
+
required ? asteriskShowClassName : asteriskHideClassName,
|
|
40
|
+
labelClassName,
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
{label}
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const Colon: React.FC<Pick<FormLabelProps, "colon" | "colonClassName">> = ({
|
|
49
|
+
colon = true,
|
|
50
|
+
colonClassName,
|
|
51
|
+
}) => {
|
|
52
|
+
return (
|
|
53
|
+
colon && (
|
|
54
|
+
<Text
|
|
55
|
+
className={classNames("tfr-form-label-colon", colonClassName)}
|
|
56
|
+
>
|
|
57
|
+
:
|
|
58
|
+
</Text>
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const FormLabel: React.FC<FormLabelProps> = ({
|
|
64
|
+
label,
|
|
65
|
+
required,
|
|
66
|
+
size = "normal",
|
|
67
|
+
colon = true,
|
|
68
|
+
colonClassName,
|
|
69
|
+
labelClassName,
|
|
70
|
+
className,
|
|
71
|
+
addonAfter,
|
|
72
|
+
asteriskTookSpace = true,
|
|
73
|
+
}) => {
|
|
74
|
+
return (
|
|
75
|
+
<View
|
|
76
|
+
className={classNames(
|
|
77
|
+
"tfr-form-label",
|
|
78
|
+
!asteriskTookSpace && size === "normal" && "tfr-form-label-no-asterisk-space",
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{label && (
|
|
83
|
+
<Label
|
|
84
|
+
size={size}
|
|
85
|
+
label={label}
|
|
86
|
+
required={required}
|
|
87
|
+
labelClassName={labelClassName}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
<Colon colon={colon} colonClassName={colonClassName} />
|
|
91
|
+
{addonAfter}
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default FormLabel;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { useFormContext } from "../context/FormContext";
|
|
4
|
+
|
|
5
|
+
export type FormProviderProps = {
|
|
6
|
+
children: (context: ReturnType<typeof useFormContext>) => React.ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const FormProvider: React.FC<FormProviderProps> = ({ children }) => {
|
|
10
|
+
const context = useFormContext();
|
|
11
|
+
|
|
12
|
+
return <>{children(context)}</>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default FormProvider;
|