@tendaui/components 1.0.0
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/LICENSE +21 -0
- package/README.md +176 -0
- package/alert/Alert.tsx +147 -0
- package/alert/defaultProps.ts +3 -0
- package/alert/index.ts +9 -0
- package/alert/style/css.js +1 -0
- package/alert/style/index.js +1 -0
- package/alert/type.ts +44 -0
- package/badge/Badge.tsx +85 -0
- package/badge/defaultProps.ts +10 -0
- package/badge/index.ts +9 -0
- package/badge/style/css.js +1 -0
- package/badge/style/index.js +1 -0
- package/badge/type.ts +51 -0
- package/button/Button.tsx +95 -0
- package/button/defaultProps.ts +13 -0
- package/button/index.ts +7 -0
- package/button/style/css.js +1 -0
- package/button/style/index.js +1 -0
- package/button/type.ts +82 -0
- package/checkbox/Checkbox.tsx +19 -0
- package/checkbox/CheckboxGroup.tsx +207 -0
- package/checkbox/defaultProps.ts +14 -0
- package/checkbox/index.ts +10 -0
- package/checkbox/style/css.js +1 -0
- package/checkbox/style/index.js +1 -0
- package/checkbox/type.ts +117 -0
- package/common/Check.tsx +131 -0
- package/common/FakeArrow.tsx +36 -0
- package/common/PluginContainer.tsx +21 -0
- package/common/Portal.tsx +67 -0
- package/common.ts +76 -0
- package/config-provider/ConfigContext.tsx +21 -0
- package/config-provider/ConfigProvider.tsx +53 -0
- package/config-provider/index.ts +9 -0
- package/config-provider/type.ts +1062 -0
- package/dialog/Dialog.tsx +254 -0
- package/dialog/DialogCard.tsx +152 -0
- package/dialog/defaultProps.ts +25 -0
- package/dialog/hooks/useDialogDrag.ts +50 -0
- package/dialog/hooks/useDialogEsc.ts +31 -0
- package/dialog/hooks/useDialogPosition.ts +36 -0
- package/dialog/hooks/useLockStyle.ts +54 -0
- package/dialog/index.ts +13 -0
- package/dialog/plugin.tsx +78 -0
- package/dialog/style/css.js +1 -0
- package/dialog/style/index.js +1 -0
- package/dialog/type.ts +241 -0
- package/dialog/utils.ts +4 -0
- package/form/Form.tsx +136 -0
- package/form/FormContext.tsx +64 -0
- package/form/FormItem.tsx +554 -0
- package/form/FormList.tsx +303 -0
- package/form/const.ts +6 -0
- package/form/defaultProps.ts +26 -0
- package/form/formModel.ts +117 -0
- package/form/hooks/interface.ts +20 -0
- package/form/hooks/useForm.ts +122 -0
- package/form/hooks/useFormItemInitialData.ts +95 -0
- package/form/hooks/useFormItemStyle.tsx +122 -0
- package/form/hooks/useInstance.tsx +275 -0
- package/form/hooks/useWatch.ts +42 -0
- package/form/index.ts +11 -0
- package/form/style/css.js +1 -0
- package/form/style/index.js +1 -0
- package/form/type.ts +519 -0
- package/form/utils/index.ts +69 -0
- package/hooks/useAttach.ts +24 -0
- package/hooks/useCommonClassName.ts +45 -0
- package/hooks/useConfig.ts +3 -0
- package/hooks/useControlled.ts +39 -0
- package/hooks/useDefaultProps.ts +16 -0
- package/hooks/useDomCallback.ts +13 -0
- package/hooks/useDomRefCallback.ts +12 -0
- package/hooks/useDragSorter.tsx +151 -0
- package/hooks/useEventCallback.ts +47 -0
- package/hooks/useGlobalConfig.ts +14 -0
- package/hooks/useGlobalIcon.ts +14 -0
- package/hooks/useLastest.ts +13 -0
- package/hooks/useLayoutEffect.ts +7 -0
- package/hooks/useMouseEvent.ts +142 -0
- package/hooks/useMutationObserver.ts +56 -0
- package/hooks/usePopper.ts +189 -0
- package/hooks/useRipple.ts +0 -0
- package/hooks/useSetState.ts +25 -0
- package/hooks/useVirtualScroll.ts +246 -0
- package/hooks/useWindowSize.ts +31 -0
- package/index.ts +70 -0
- package/input/Input.tsx +383 -0
- package/input/InputGroup.tsx +29 -0
- package/input/defaultProps.ts +22 -0
- package/input/index.ts +11 -0
- package/input/style/css.js +1 -0
- package/input/style/index.js +1 -0
- package/input/type.ts +219 -0
- package/loading/Gradient.tsx +36 -0
- package/loading/Loading.tsx +169 -0
- package/loading/circleAdapter.ts +44 -0
- package/loading/defaultProps.ts +12 -0
- package/loading/index.ts +13 -0
- package/loading/style/css.js +1 -0
- package/loading/style/index.js +1 -0
- package/loading/type.ts +71 -0
- package/loading/utils/setStyle.ts +13 -0
- package/myform/index.ts +0 -0
- package/notification/Notify.ts +24 -0
- package/notification/NotifyContainer.tsx +90 -0
- package/notification/NotifyContext.tsx +173 -0
- package/notification/NotifyItem.tsx +121 -0
- package/notification/index.ts +3 -0
- package/notification/style/css.js +1 -0
- package/notification/style/index.js +1 -0
- package/notification/type.ts +23 -0
- package/package.json +52 -0
- package/popup/Popup.tsx +264 -0
- package/popup/defaultProps.ts +13 -0
- package/popup/hooks/useTrigger.ts +276 -0
- package/popup/index.ts +6 -0
- package/popup/style/css.js +1 -0
- package/popup/style/index.js +1 -0
- package/popup/type.ts +130 -0
- package/portal/Portal.tsx +63 -0
- package/portal/index.ts +1 -0
- package/select/Option.tsx +162 -0
- package/select/OptionGroup.tsx +30 -0
- package/select/PopupContent.tsx +271 -0
- package/select/Select.tsx +586 -0
- package/select/defaultProps.ts +27 -0
- package/select/hooks/useOptions.ts +120 -0
- package/select/hooks/usePanelVirtualScroll.ts +111 -0
- package/select/index.ts +9 -0
- package/select/style/css.js +1 -0
- package/select/style/index.js +2 -0
- package/select/type.ts +382 -0
- package/select/utils/helper.ts +256 -0
- package/select-input/SelectInput.tsx +98 -0
- package/select-input/defaultProps.ts +15 -0
- package/select-input/hook/useMultiple.tsx +100 -0
- package/select-input/hook/useOverlayInnerStyle.ts +84 -0
- package/select-input/hook/useSingle.tsx +112 -0
- package/select-input/index.ts +6 -0
- package/select-input/interface.ts +18 -0
- package/select-input/style/css.js +1 -0
- package/select-input/style/index.js +1 -0
- package/select-input/type.ts +280 -0
- package/space/defaultProps.ts +0 -0
- package/space/index.ts +0 -0
- package/space/type.ts +0 -0
- package/style/index.js +2 -0
- package/styles/_global.scss +39 -0
- package/styles/_vars.scss +386 -0
- package/styles/components/alert/_index.scss +175 -0
- package/styles/components/alert/_vars.scss +39 -0
- package/styles/components/badge/_index.scss +70 -0
- package/styles/components/badge/_vars.scss +25 -0
- package/styles/components/button/_index.scss +511 -0
- package/styles/components/button/_mixins.scss +39 -0
- package/styles/components/button/_vars.scss +122 -0
- package/styles/components/checkbox/_index.scss +158 -0
- package/styles/components/checkbox/_mixin.scss +0 -0
- package/styles/components/checkbox/_var.scss +60 -0
- package/styles/components/dialog/_animate.scss +135 -0
- package/styles/components/dialog/_index.scss +311 -0
- package/styles/components/dialog/_mixins.scss +0 -0
- package/styles/components/dialog/_vars.scss +59 -0
- package/styles/components/form/_index.scss +174 -0
- package/styles/components/form/_mixins.scss +76 -0
- package/styles/components/form/_vars.scss +100 -0
- package/styles/components/input/_index.scss +349 -0
- package/styles/components/input/_map.scss +0 -0
- package/styles/components/input/_mixins.scss +116 -0
- package/styles/components/input/_vars.scss +134 -0
- package/styles/components/loading/_index.scss +112 -0
- package/styles/components/loading/_vars.scss +39 -0
- package/styles/components/notification/_index.scss +160 -0
- package/styles/components/notification/_mixins.scss +12 -0
- package/styles/components/notification/_vars.scss +59 -0
- package/styles/components/popup/_index.scss +82 -0
- package/styles/components/popup/_mixin.scss +149 -0
- package/styles/components/popup/_var.scss +31 -0
- package/styles/components/select/_index.scss +290 -0
- package/styles/components/select/_var.scss +65 -0
- package/styles/components/select-input/_index.scss +5 -0
- package/styles/components/select-input/_var.scss +3 -0
- package/styles/components/switch/_index.scss +279 -0
- package/styles/components/switch/_mixins.scss +0 -0
- package/styles/components/switch/_vars.scss +61 -0
- package/styles/components/tag/_index.scss +316 -0
- package/styles/components/tag/_var.scss +85 -0
- package/styles/components/tag-input/_index.scss +163 -0
- package/styles/components/tag-input/_vars.scss +16 -0
- package/styles/globals.css +250 -0
- package/styles/mixins/_focus.scss +7 -0
- package/styles/mixins/_layout.scss +32 -0
- package/styles/mixins/_reset.scss +10 -0
- package/styles/mixins/_scrollbar.scss +31 -0
- package/styles/mixins/_text.scss +48 -0
- package/styles/rillple.css +16 -0
- package/styles/scrollbar.css +42 -0
- package/styles/themes/_dark.scss +191 -0
- package/styles/themes/_font.scss +79 -0
- package/styles/themes/_index.scss +5 -0
- package/styles/themes/_light.scss +190 -0
- package/styles/themes/_radius.scss +9 -0
- package/styles/themes/_size.scss +68 -0
- package/styles/themes.css +66 -0
- package/styles/utilities/_animation.scss +57 -0
- package/styles/utilities/_tips.scss +9 -0
- package/switch/Switch.tsx +120 -0
- package/switch/defaultProps.ts +3 -0
- package/switch/index.ts +7 -0
- package/switch/style/css.js +1 -0
- package/switch/style/index.js +1 -0
- package/switch/type.ts +46 -0
- package/tag/Tag.tsx +149 -0
- package/tag/defaultProps.ts +19 -0
- package/tag/index.ts +8 -0
- package/tag/style/css.js +1 -0
- package/tag/style/index.js +1 -0
- package/tag/type.ts +170 -0
- package/tag-input/TagInput.tsx +215 -0
- package/tag-input/defaultProps.ts +15 -0
- package/tag-input/hooks/useHover.ts +28 -0
- package/tag-input/hooks/useTagList.tsx +131 -0
- package/tag-input/hooks/useTagScroll.ts +105 -0
- package/tag-input/index.ts +9 -0
- package/tag-input/style/css.js +1 -0
- package/tag-input/style/index.js +1 -0
- package/tag-input/type.ts +224 -0
- package/tag-input/useTagList.tsx +131 -0
- package/utils/composeRefs.ts +14 -0
- package/utils/dom.ts +29 -0
- package/utils/forwardRefWithStatics.ts +12 -0
- package/utils/getScrollbarWidth.ts +11 -0
- package/utils/helper.ts +161 -0
- package/utils/isFragment.ts +22 -0
- package/utils/listener.ts +37 -0
- package/utils/noop.ts +3 -0
- package/utils/parentTNode.ts +38 -0
- package/utils/parseTNode.ts +38 -0
- package/utils/react-render.ts +108 -0
- package/utils/ref.ts +6 -0
- package/utils/refs.ts +81 -0
- package/utils/style.ts +60 -0
- package/utils/transition.ts +28 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import React, { useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
2
|
+
import { flattenDeep, get, merge, set, unset } from "lodash-es";
|
|
3
|
+
// import log from '@tdesign/common-js/log/index';
|
|
4
|
+
import { FormListContext, useFormContext } from "./FormContext";
|
|
5
|
+
import type { FormItemInstance } from "./FormItem";
|
|
6
|
+
import { HOOK_MARK } from "./hooks/useForm";
|
|
7
|
+
import type { FormListField, FormListFieldOperation, TdFormListProps } from "./type";
|
|
8
|
+
import { calcFieldValue } from "./utils";
|
|
9
|
+
|
|
10
|
+
let key = 0;
|
|
11
|
+
|
|
12
|
+
const FormList: React.FC<TdFormListProps> = (props) => {
|
|
13
|
+
const {
|
|
14
|
+
formMapRef,
|
|
15
|
+
form,
|
|
16
|
+
onFormItemValueChange,
|
|
17
|
+
initialData: initialDataFromForm,
|
|
18
|
+
resetType: resetTypeFromContext
|
|
19
|
+
} = useFormContext();
|
|
20
|
+
const { name, rules, children } = props;
|
|
21
|
+
|
|
22
|
+
const initialData = props.initialData || get(initialDataFromForm, name) || [];
|
|
23
|
+
|
|
24
|
+
const [formListValue, setFormListValue] = useState(initialData);
|
|
25
|
+
const [fields, setFields] = useState<Array<FormListField>>(() =>
|
|
26
|
+
initialData.map((data, index) => ({
|
|
27
|
+
data: { ...data },
|
|
28
|
+
key: (key += 1),
|
|
29
|
+
name: index,
|
|
30
|
+
isListField: true
|
|
31
|
+
}))
|
|
32
|
+
);
|
|
33
|
+
const formListMapRef = useRef(new Map()); // 收集 formItem 实例
|
|
34
|
+
const formListRef = useRef<FormItemInstance>(null); // 当前 formList 实例
|
|
35
|
+
const fieldsTaskQueueRef = useRef([]); // 记录更改 fields 数据后 callback 队列
|
|
36
|
+
const snakeName = []
|
|
37
|
+
.concat(name)
|
|
38
|
+
.filter((item) => item !== undefined)
|
|
39
|
+
.toString(); // 转化 name
|
|
40
|
+
|
|
41
|
+
const isMounted = useRef(false);
|
|
42
|
+
|
|
43
|
+
useEffect(
|
|
44
|
+
() => () => {
|
|
45
|
+
isMounted.current = false;
|
|
46
|
+
},
|
|
47
|
+
[]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const operation: FormListFieldOperation = {
|
|
51
|
+
add(defaultValue?: unknown, insertIndex?: number) {
|
|
52
|
+
const cloneFields = [...fields];
|
|
53
|
+
const index = insertIndex ?? cloneFields.length;
|
|
54
|
+
cloneFields.splice(index, 0, {
|
|
55
|
+
key: (key += 1),
|
|
56
|
+
name: index,
|
|
57
|
+
isListField: true
|
|
58
|
+
});
|
|
59
|
+
cloneFields.forEach((field, index) => Object.assign(field, { name: index }));
|
|
60
|
+
setFields(cloneFields);
|
|
61
|
+
|
|
62
|
+
const nextFormListValue = [...formListValue];
|
|
63
|
+
if (typeof defaultValue !== "undefined") {
|
|
64
|
+
nextFormListValue[index] = defaultValue;
|
|
65
|
+
setFormListValue(nextFormListValue);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
set(form?.store, flattenDeep([name, index]), nextFormListValue);
|
|
69
|
+
|
|
70
|
+
const fieldValue = calcFieldValue(name, nextFormListValue);
|
|
71
|
+
requestAnimationFrame(() => {
|
|
72
|
+
onFormItemValueChange?.({ ...fieldValue });
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
remove(index: number | number[]) {
|
|
76
|
+
const nextFields = fields
|
|
77
|
+
.filter((item) => {
|
|
78
|
+
if (Array.isArray(index)) return !index.includes(item.name);
|
|
79
|
+
return item.name !== index;
|
|
80
|
+
})
|
|
81
|
+
.map((field, i) => ({ ...field, name: i }));
|
|
82
|
+
setFields(nextFields);
|
|
83
|
+
|
|
84
|
+
const nextFormListValue = formListValue.filter((_, idx) => idx !== index);
|
|
85
|
+
setFormListValue(nextFormListValue);
|
|
86
|
+
|
|
87
|
+
unset(form?.store, flattenDeep([name, index]));
|
|
88
|
+
|
|
89
|
+
const fieldValue = calcFieldValue(name, nextFormListValue);
|
|
90
|
+
requestAnimationFrame(() => {
|
|
91
|
+
onFormItemValueChange?.({ ...fieldValue });
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
move(from: number, to: number) {
|
|
95
|
+
const cloneFields = [...fields];
|
|
96
|
+
const fromItem = { ...cloneFields[from] };
|
|
97
|
+
const toItem = { ...cloneFields[to] };
|
|
98
|
+
cloneFields[to] = fromItem;
|
|
99
|
+
cloneFields[from] = toItem;
|
|
100
|
+
set(form?.store, name, []);
|
|
101
|
+
setFields(cloneFields);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// 外部设置 fields 优先级最高,可以更改渲染的节点
|
|
106
|
+
function setListFields(fieldData: unknown[], callback: (...args: unknown[]) => unknown, originData: unknown) {
|
|
107
|
+
setFields(
|
|
108
|
+
fieldData.map((_, index) => ({
|
|
109
|
+
key: (key += 1),
|
|
110
|
+
name: index,
|
|
111
|
+
isListField: true
|
|
112
|
+
}))
|
|
113
|
+
);
|
|
114
|
+
// 添加至队列中 等待下次渲染完成执行对应逻辑
|
|
115
|
+
fieldsTaskQueueRef.current.push({ callback, fieldData, originData });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (!name || !formMapRef) return;
|
|
120
|
+
formMapRef.current.set(name, formListRef);
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
124
|
+
formMapRef.current.delete(name);
|
|
125
|
+
};
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
}, [snakeName]);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
131
|
+
if (!formItemRef.current) return;
|
|
132
|
+
const { name, isUpdated } = formItemRef.current;
|
|
133
|
+
if (isUpdated) return; // 内部更新过值则跳过
|
|
134
|
+
|
|
135
|
+
const data = get(formListValue, name);
|
|
136
|
+
formItemRef.current.setField({ value: data, status: "not" });
|
|
137
|
+
});
|
|
138
|
+
}, [formListValue]);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!isMounted.current) {
|
|
142
|
+
isMounted.current = true;
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// fields 变化通知 watch 事件
|
|
146
|
+
form?.getInternalHooks?.(HOOK_MARK)?.notifyWatch?.(name);
|
|
147
|
+
|
|
148
|
+
// 等待子节点渲染完毕
|
|
149
|
+
Promise.resolve().then(() => {
|
|
150
|
+
if (!fieldsTaskQueueRef.current.length) return;
|
|
151
|
+
|
|
152
|
+
// fix multiple formlist stuck
|
|
153
|
+
const currentQueue = fieldsTaskQueueRef.current.pop();
|
|
154
|
+
const { fieldData, callback, originData } = currentQueue;
|
|
155
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
156
|
+
if (!formItemRef.current) return;
|
|
157
|
+
|
|
158
|
+
const { name: itemName } = formItemRef.current;
|
|
159
|
+
const data = get(fieldData, itemName);
|
|
160
|
+
callback(formItemRef, data);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// formList 嵌套 formList
|
|
164
|
+
if (!formMapRef || !formMapRef.current) return;
|
|
165
|
+
[...formMapRef.current.values()].forEach((formItemRef) => {
|
|
166
|
+
if (!formItemRef.current) return;
|
|
167
|
+
|
|
168
|
+
const { name: itemName, isFormList } = formItemRef.current;
|
|
169
|
+
if (String(itemName) === String(name) || !isFormList) return;
|
|
170
|
+
const data = get(originData, itemName);
|
|
171
|
+
if (data) callback(formItemRef, data);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
175
|
+
}, [form, snakeName, fields, formMapRef]);
|
|
176
|
+
|
|
177
|
+
useImperativeHandle(
|
|
178
|
+
formListRef,
|
|
179
|
+
(): FormItemInstance => ({
|
|
180
|
+
name,
|
|
181
|
+
isFormList: true,
|
|
182
|
+
getValue() {
|
|
183
|
+
const formListValue = [];
|
|
184
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
185
|
+
if (!formItemRef.current) return;
|
|
186
|
+
|
|
187
|
+
const { name, getValue } = formItemRef.current;
|
|
188
|
+
const fieldValue = calcFieldValue(name, getValue());
|
|
189
|
+
merge(formListValue, fieldValue);
|
|
190
|
+
});
|
|
191
|
+
return formListValue;
|
|
192
|
+
},
|
|
193
|
+
validate: (trigger = "all") => {
|
|
194
|
+
const resultList = [];
|
|
195
|
+
const validates = [...formListMapRef.current.values()].map((formItemRef) =>
|
|
196
|
+
formItemRef?.current?.validate?.(trigger)
|
|
197
|
+
);
|
|
198
|
+
return new Promise((resolve) => {
|
|
199
|
+
Promise.all(validates).then((validateResult) => {
|
|
200
|
+
validateResult.forEach((result) => {
|
|
201
|
+
const errorValue = Object.values(result)[0];
|
|
202
|
+
merge(resultList, errorValue);
|
|
203
|
+
});
|
|
204
|
+
const errorItems = validateResult.filter((item) => Object.values(item)[0] !== true);
|
|
205
|
+
if (errorItems.length) {
|
|
206
|
+
resolve({ [snakeName]: resultList });
|
|
207
|
+
} else {
|
|
208
|
+
resolve({ [snakeName]: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
// TODO 支持局部更新数据
|
|
214
|
+
setValue: (fieldData: unknown[], originData) => {
|
|
215
|
+
setListFields(
|
|
216
|
+
fieldData,
|
|
217
|
+
(formItemRef, data) => {
|
|
218
|
+
(formItemRef as React.RefObject<FormItemInstance>)?.current?.setValue?.(data);
|
|
219
|
+
},
|
|
220
|
+
originData
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
setField: (fieldData: { value?: unknown[]; status?: string }, originData) => {
|
|
224
|
+
const { value, status } = fieldData;
|
|
225
|
+
setListFields(
|
|
226
|
+
value,
|
|
227
|
+
(formItemRef, data) => {
|
|
228
|
+
(formItemRef as React.RefObject<FormItemInstance>)?.current?.setField?.({ value: data, status });
|
|
229
|
+
},
|
|
230
|
+
originData
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
resetField: (type: string) => {
|
|
234
|
+
const resetType = type || resetTypeFromContext;
|
|
235
|
+
|
|
236
|
+
if (resetType === "initial") {
|
|
237
|
+
setFormListValue(initialData);
|
|
238
|
+
|
|
239
|
+
const newFields = initialData.map((data, index) => ({
|
|
240
|
+
data: { ...data },
|
|
241
|
+
key: (key += 1),
|
|
242
|
+
name: index,
|
|
243
|
+
isListField: true
|
|
244
|
+
}));
|
|
245
|
+
setFields(newFields);
|
|
246
|
+
set(form?.store, flattenDeep([name]), initialData);
|
|
247
|
+
|
|
248
|
+
requestAnimationFrame(() => {
|
|
249
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
250
|
+
if (!formItemRef.current) return;
|
|
251
|
+
const { name: itemName } = formItemRef.current;
|
|
252
|
+
const itemValue = get(initialData, itemName);
|
|
253
|
+
if (itemValue !== undefined) {
|
|
254
|
+
formItemRef.current.setField({ value: itemValue, status: "not" });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
// 重置为空
|
|
260
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
261
|
+
formItemRef?.current?.resetField?.();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
fieldsTaskQueueRef.current = [];
|
|
265
|
+
|
|
266
|
+
setFormListValue([]);
|
|
267
|
+
setFields([]);
|
|
268
|
+
unset(form?.store, flattenDeep([name]));
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
setValidateMessage: (fieldData) => {
|
|
272
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
273
|
+
if (!formItemRef.current) return;
|
|
274
|
+
|
|
275
|
+
const { name } = formItemRef.current;
|
|
276
|
+
const data = get(fieldData, name);
|
|
277
|
+
|
|
278
|
+
formItemRef?.current?.setValidateMessage?.(data);
|
|
279
|
+
});
|
|
280
|
+
},
|
|
281
|
+
resetValidate: () => {
|
|
282
|
+
[...formListMapRef.current.values()].forEach((formItemRef) => {
|
|
283
|
+
formItemRef?.current?.resetValidate?.();
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (typeof children !== "function") {
|
|
290
|
+
console.error("Form", `FormList's children must be a function!`);
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<FormListContext.Provider value={{ name, rules, formListMapRef, initialData, form }}>
|
|
296
|
+
{children(fields, operation)}
|
|
297
|
+
</FormListContext.Provider>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
FormList.displayName = "FormList";
|
|
302
|
+
|
|
303
|
+
export default FormList;
|
package/form/const.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TdFormProps, TdFormItemProps } from "./type";
|
|
2
|
+
|
|
3
|
+
export const formDefaultProps: TdFormProps = {
|
|
4
|
+
colon: false,
|
|
5
|
+
disabled: undefined,
|
|
6
|
+
id: undefined,
|
|
7
|
+
labelAlign: "right",
|
|
8
|
+
labelWidth: "100px",
|
|
9
|
+
layout: "vertical",
|
|
10
|
+
preventSubmitDefault: true,
|
|
11
|
+
requiredMark: undefined,
|
|
12
|
+
resetType: "empty",
|
|
13
|
+
showErrorMessage: true,
|
|
14
|
+
statusIcon: undefined,
|
|
15
|
+
submitWithWarningMessage: false,
|
|
16
|
+
supportNumberKey: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const formItemDefaultProps: TdFormItemProps = {
|
|
20
|
+
label: "",
|
|
21
|
+
requiredMark: undefined,
|
|
22
|
+
shouldUpdate: false,
|
|
23
|
+
showErrorMessage: undefined,
|
|
24
|
+
statusIcon: undefined,
|
|
25
|
+
successBorder: false
|
|
26
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// https://github.com/validatorjs/validator.js
|
|
2
|
+
|
|
3
|
+
import isDate from "validator/lib/isDate";
|
|
4
|
+
import isEmail from "validator/lib/isEmail";
|
|
5
|
+
import { isEmpty, isNumber } from "lodash-es";
|
|
6
|
+
import isURL from "validator/lib/isURL";
|
|
7
|
+
import { getCharacterLength } from "../utils/helper";
|
|
8
|
+
import {
|
|
9
|
+
CustomValidator,
|
|
10
|
+
FormRule,
|
|
11
|
+
ValueType,
|
|
12
|
+
AllValidateResult,
|
|
13
|
+
ValidateResultType,
|
|
14
|
+
CustomValidateResolveType
|
|
15
|
+
} from "./type";
|
|
16
|
+
|
|
17
|
+
// `{} / [] / '' / undefined / null` 等内容被认为是空; 0 和 false 被认为是正常数据,部分数据的值就是 0 或者 false
|
|
18
|
+
export function isValueEmpty(val: ValueType): boolean {
|
|
19
|
+
const type: string = Object.prototype.toString.call(val);
|
|
20
|
+
const typeMap: Record<string, string> = {
|
|
21
|
+
Date: "[object Date]"
|
|
22
|
+
};
|
|
23
|
+
if (type === typeMap.Date) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return typeof val === "object" ? isEmpty(val) : ["", undefined, null].includes(val);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 比较值大小
|
|
30
|
+
const compareValue: (val: ValueType, num: number, isMax: boolean) => boolean = (val, num, isMax) => {
|
|
31
|
+
const compare: (a: number, b: number) => boolean = (a, b) => (isMax ? a <= b : a >= b);
|
|
32
|
+
if (isNumber(val)) return compare(val, num);
|
|
33
|
+
if (Array.isArray(val)) return compare(val.length, num);
|
|
34
|
+
const charLength = getCharacterLength(val);
|
|
35
|
+
return compare(typeof charLength === "number" ? charLength : charLength.length, num);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const VALIDATE_MAP = {
|
|
39
|
+
date: isDate,
|
|
40
|
+
url: isURL,
|
|
41
|
+
email: isEmail,
|
|
42
|
+
required: (val: ValueType): boolean => !isValueEmpty(val),
|
|
43
|
+
whitespace: (val: ValueType): boolean => !(/^\s+$/.test(val) || val === ""),
|
|
44
|
+
boolean: (val: ValueType): boolean => typeof val === "boolean",
|
|
45
|
+
max: (val: ValueType, num: number): boolean => compareValue(val, num, true),
|
|
46
|
+
min: (val: ValueType, num: number): boolean => compareValue(val, num, false),
|
|
47
|
+
len: (val: ValueType, num: number): boolean => getCharacterLength(val) === num,
|
|
48
|
+
number: (val: ValueType): boolean => isNumber(val),
|
|
49
|
+
enum: (val: ValueType, strs: Array<string>): boolean => strs.includes(val),
|
|
50
|
+
idcard: (val: ValueType): boolean => /^(\d{18,18}|\d{15,15}|\d{17,17}x)$/i.test(val),
|
|
51
|
+
telnumber: (val: ValueType): boolean => /^1[3-9]\d{9}$/.test(val),
|
|
52
|
+
pattern: (val: ValueType, regexp: RegExp): boolean => regexp.test(val),
|
|
53
|
+
// 自定义校验规则,可能是异步校验
|
|
54
|
+
validator: (val: ValueType, validate: CustomValidator): ReturnType<CustomValidator> => validate(val)
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type ValidateFuncType = (typeof VALIDATE_MAP)[keyof typeof VALIDATE_MAP];
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 校验某一条数据的某一条规则,一种校验规则不满足则不再进行校验。
|
|
61
|
+
* @param value 值
|
|
62
|
+
* @param rule 校验规则
|
|
63
|
+
* @returns 两种校验结果,一种是内置校验规则的校验结果,二种是自定义校验规则(validator)的校验结果
|
|
64
|
+
*/
|
|
65
|
+
export async function validateOneRule(value: ValueType, rule: FormRule): Promise<AllValidateResult> {
|
|
66
|
+
let validateResult: CustomValidateResolveType | ValidateResultType = {
|
|
67
|
+
result: true
|
|
68
|
+
};
|
|
69
|
+
const keys = Object.keys(rule);
|
|
70
|
+
let vOptions;
|
|
71
|
+
let vValidateFun: ValidateFuncType;
|
|
72
|
+
for (let i = 0; i < keys.length; i++) {
|
|
73
|
+
const key = keys[i];
|
|
74
|
+
// 非必填选项,值为空,非自定义规则:无需校验,直接返回 true
|
|
75
|
+
if (!rule.required && isValueEmpty(value) && !rule.validator) {
|
|
76
|
+
return validateResult;
|
|
77
|
+
}
|
|
78
|
+
const validateRule: ValidateFuncType = VALIDATE_MAP[key];
|
|
79
|
+
// 找到一个校验规则,则无需再找,因为参数只允许对一个规则进行校验
|
|
80
|
+
if (validateRule && ![undefined, null].includes(rule[key])) {
|
|
81
|
+
// rule 值为 true 则表示没有校验参数,只是对值进行默认规则校验
|
|
82
|
+
vOptions = rule[key] === true ? undefined : rule[key];
|
|
83
|
+
vValidateFun = validateRule;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (vValidateFun) {
|
|
88
|
+
validateResult = await vValidateFun(value, vOptions);
|
|
89
|
+
// 如果校验不通过,则返回校验不通过的规则
|
|
90
|
+
if (typeof validateResult === "boolean") {
|
|
91
|
+
return { ...rule, result: validateResult };
|
|
92
|
+
}
|
|
93
|
+
// 校验结果为 CustomValidateObj,只有自定义校验规则会存在这种情况
|
|
94
|
+
if (typeof validateResult === "object") {
|
|
95
|
+
return validateResult;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return validateResult;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 单个数据进行全规则校验,校验成功也可能会有 message
|
|
102
|
+
export async function validate(value: ValueType, rules: Array<FormRule>): Promise<AllValidateResult[]> {
|
|
103
|
+
const all = rules.map((rule) => validateOneRule(value, rule));
|
|
104
|
+
const r = await Promise.all(all);
|
|
105
|
+
return r;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Replace with template.
|
|
110
|
+
* `${name} is wrong` + { name: 'password' } = password is wrong
|
|
111
|
+
*/
|
|
112
|
+
export function parseMessage(template: string, options: Record<string, string>): string {
|
|
113
|
+
return template.replace(/\$\{\w+\}/g, (str: string) => {
|
|
114
|
+
const key = str.slice(2, -1);
|
|
115
|
+
return options[key];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NamePath, FormInstanceFunctions } from "../type";
|
|
2
|
+
|
|
3
|
+
export type Store = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
export type WatchCallBack = (values: Store, namePathList: NamePath) => void;
|
|
6
|
+
|
|
7
|
+
export interface InternalHooks {
|
|
8
|
+
notifyWatch: (name: NamePath) => void;
|
|
9
|
+
registerWatch: (callback: WatchCallBack) => () => void;
|
|
10
|
+
getPrevStore: () => Store;
|
|
11
|
+
setPrevStore: (store: Store) => void;
|
|
12
|
+
flashQueue: () => void;
|
|
13
|
+
setForm: (form: FormInstanceFunctions) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InternalFormInstance extends FormInstanceFunctions {
|
|
17
|
+
_init?: boolean;
|
|
18
|
+
store?: Store;
|
|
19
|
+
getInternalHooks?: (secret: string) => InternalHooks | null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { NamePath } from "../type";
|
|
3
|
+
import type { WatchCallBack, InternalHooks, InternalFormInstance, Store } from "./interface";
|
|
4
|
+
|
|
5
|
+
export const HOOK_MARK = "TD_FORM_INTERNAL_HOOKS";
|
|
6
|
+
|
|
7
|
+
class FormStore {
|
|
8
|
+
private prevStore: Store = {};
|
|
9
|
+
|
|
10
|
+
private store: Store = {};
|
|
11
|
+
|
|
12
|
+
private forceRootUpdate: () => void;
|
|
13
|
+
|
|
14
|
+
constructor(forceReRender) {
|
|
15
|
+
this.forceRootUpdate = forceReRender;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public taskQueue: { name: string; args: unknown[] }[] = [];
|
|
19
|
+
|
|
20
|
+
public flashQueue = () => {
|
|
21
|
+
this.taskQueue.forEach((task) => {
|
|
22
|
+
this[task.name].apply(this, [...task.args]);
|
|
23
|
+
});
|
|
24
|
+
this.taskQueue = [];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
public getForm = (): InternalFormInstance => ({
|
|
28
|
+
submit: (...args) => {
|
|
29
|
+
this.taskQueue.push({ args, name: "submit" });
|
|
30
|
+
},
|
|
31
|
+
reset: (...args) => {
|
|
32
|
+
this.taskQueue.push({ args, name: "reset" });
|
|
33
|
+
},
|
|
34
|
+
validate: null,
|
|
35
|
+
validateOnly: null,
|
|
36
|
+
clearValidate: (...args) => {
|
|
37
|
+
this.taskQueue.push({ args, name: "clearValidate" });
|
|
38
|
+
},
|
|
39
|
+
setFields: (...args) => {
|
|
40
|
+
this.taskQueue.push({ args, name: "setFields" });
|
|
41
|
+
},
|
|
42
|
+
setFieldsValue: (...args) => {
|
|
43
|
+
this.taskQueue.push({ args, name: "setFieldsValue" });
|
|
44
|
+
},
|
|
45
|
+
setValidateMessage: (...args) => {
|
|
46
|
+
this.taskQueue.push({ args, name: "setValidateMessage" });
|
|
47
|
+
},
|
|
48
|
+
getValidateMessage: (...args) => {
|
|
49
|
+
this.taskQueue.push({ args, name: "getValidateMessage" });
|
|
50
|
+
},
|
|
51
|
+
getFieldValue: null,
|
|
52
|
+
getFieldsValue: null,
|
|
53
|
+
_init: true,
|
|
54
|
+
store: this.store,
|
|
55
|
+
getInternalHooks: this.getInternalHooks
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
private getInternalHooks = (key: string): InternalHooks | null => {
|
|
59
|
+
if (key === HOOK_MARK) {
|
|
60
|
+
return {
|
|
61
|
+
setForm: (formInstance) => {
|
|
62
|
+
Object.keys(formInstance).forEach((key) => {
|
|
63
|
+
this[key] = formInstance[key];
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
flashQueue: this.flashQueue,
|
|
67
|
+
notifyWatch: this.notifyWatch,
|
|
68
|
+
registerWatch: this.registerWatch,
|
|
69
|
+
getPrevStore: () => this.prevStore,
|
|
70
|
+
setPrevStore: (store: Store) => {
|
|
71
|
+
this.prevStore = store;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.warn("Form", "`getInternalHooks` is internal usage. Should not call directly.");
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
private watchList: WatchCallBack[] = [];
|
|
81
|
+
|
|
82
|
+
private registerWatch: InternalHooks["registerWatch"] = (callback) => {
|
|
83
|
+
this.watchList.push(callback);
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
this.watchList = this.watchList.filter((fn) => fn !== callback);
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
private notifyWatch = (namePath: NamePath = []) => {
|
|
91
|
+
// No need to cost perf when nothing need to watch
|
|
92
|
+
if (this.watchList.length) {
|
|
93
|
+
// @ts-expect-error Internal API access
|
|
94
|
+
const values = this.getFieldsValue?.([namePath]);
|
|
95
|
+
|
|
96
|
+
this.watchList.forEach((callback) => {
|
|
97
|
+
callback(values, namePath);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default function useForm(form?: InternalFormInstance) {
|
|
104
|
+
const [formInstance, setFormInstance] = useState<InternalFormInstance>(() => {
|
|
105
|
+
if (form) {
|
|
106
|
+
return form;
|
|
107
|
+
}
|
|
108
|
+
// Create a new FormStore if not provided
|
|
109
|
+
const forceReRender = () => {
|
|
110
|
+
// This will trigger a re-render
|
|
111
|
+
};
|
|
112
|
+
const formStore: FormStore = new FormStore(forceReRender);
|
|
113
|
+
return formStore.getForm();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Update form instance when form prop changes
|
|
117
|
+
if (form && formInstance !== form) {
|
|
118
|
+
setFormInstance(form);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [formInstance];
|
|
122
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { get, unset, isEmpty } from "lodash-es";
|
|
3
|
+
|
|
4
|
+
// 兼容特殊数据结构和受控 key
|
|
5
|
+
// import Tree from '../../tree/Tree';
|
|
6
|
+
// import Upload from '../../upload/upload';
|
|
7
|
+
// import CheckTag from '../../tag/CheckTag';
|
|
8
|
+
import Checkbox from "../../checkbox/Checkbox";
|
|
9
|
+
import TagInput from "../../tag-input/TagInput";
|
|
10
|
+
// import RangeInput from '../../range-input/RangeInput';
|
|
11
|
+
// import Transfer from '../../transfer/Transfer';
|
|
12
|
+
import CheckboxGroup from "../../checkbox/CheckboxGroup";
|
|
13
|
+
// import DateRangePicker from '../../date-picker/DateRangePicker';
|
|
14
|
+
// import TimeRangePicker from '../../time-picker/TimeRangePicker';
|
|
15
|
+
|
|
16
|
+
import { useFormContext, useFormListContext } from "../FormContext";
|
|
17
|
+
import { FormItemProps } from "../FormItem";
|
|
18
|
+
|
|
19
|
+
// FormItem 子组件受控 key
|
|
20
|
+
export const ctrlKeyMap = new Map();
|
|
21
|
+
ctrlKeyMap.set(Checkbox, "checked");
|
|
22
|
+
// ctrlKeyMap.set(CheckTag, 'checked');
|
|
23
|
+
// ctrlKeyMap.set(Upload, 'files');
|
|
24
|
+
|
|
25
|
+
// FormItem 默认数据类型
|
|
26
|
+
export const initialDataMap = new Map();
|
|
27
|
+
[
|
|
28
|
+
// Tree,
|
|
29
|
+
// Upload,
|
|
30
|
+
// Transfer,
|
|
31
|
+
TagInput,
|
|
32
|
+
// RangeInput,
|
|
33
|
+
CheckboxGroup
|
|
34
|
+
// DateRangePicker,
|
|
35
|
+
// TimeRangePicker,
|
|
36
|
+
].forEach((component) => {
|
|
37
|
+
initialDataMap.set(component, []);
|
|
38
|
+
});
|
|
39
|
+
[Checkbox].forEach((component) => {
|
|
40
|
+
initialDataMap.set(component, false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export default function useFormItemInitialData(name: FormItemProps["name"]) {
|
|
44
|
+
const { floatingFormDataRef, initialData: formContextInitialData } = useFormContext();
|
|
45
|
+
|
|
46
|
+
const { name: formListName, initialData: formListInitialData } = useFormListContext();
|
|
47
|
+
|
|
48
|
+
// 整理初始值 优先级:Form.initialData < FormList.initialData < FormItem.initialData < floatFormData
|
|
49
|
+
function getDefaultInitialData({
|
|
50
|
+
children,
|
|
51
|
+
initialData
|
|
52
|
+
}: {
|
|
53
|
+
children: FormItemProps["children"];
|
|
54
|
+
initialData: FormItemProps["initialData"];
|
|
55
|
+
}) {
|
|
56
|
+
if (name && floatingFormDataRef?.current && !isEmpty(floatingFormDataRef.current)) {
|
|
57
|
+
const nameList = formListName ? [formListName, name].flat() : name;
|
|
58
|
+
const defaultInitialData = get(floatingFormDataRef.current, nameList);
|
|
59
|
+
if (typeof defaultInitialData !== "undefined") {
|
|
60
|
+
// 使用 setTimeout 来延迟清理,避免在渲染期间修改 ref
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
unset(floatingFormDataRef.current, nameList);
|
|
63
|
+
}, 0);
|
|
64
|
+
return defaultInitialData;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof initialData !== "undefined") {
|
|
69
|
+
return initialData;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (name && formListInitialData.length) {
|
|
73
|
+
const defaultInitialData = get(formListInitialData, name);
|
|
74
|
+
if (typeof defaultInitialData !== "undefined") return defaultInitialData;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (name && formContextInitialData) {
|
|
78
|
+
const defaultInitialData = get(formContextInitialData, name);
|
|
79
|
+
if (typeof defaultInitialData !== "undefined") return defaultInitialData;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof children !== "function") {
|
|
83
|
+
const childList = React.Children.toArray(children);
|
|
84
|
+
const lastChild = childList[childList.length - 1];
|
|
85
|
+
if (lastChild && React.isValidElement(lastChild)) {
|
|
86
|
+
const isMultiple = lastChild?.props?.multiple;
|
|
87
|
+
return isMultiple ? [] : initialDataMap.get(lastChild.type);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
getDefaultInitialData
|
|
94
|
+
};
|
|
95
|
+
}
|