@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.
Files changed (245) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/alert/Alert.tsx +147 -0
  4. package/alert/defaultProps.ts +3 -0
  5. package/alert/index.ts +9 -0
  6. package/alert/style/css.js +1 -0
  7. package/alert/style/index.js +1 -0
  8. package/alert/type.ts +44 -0
  9. package/badge/Badge.tsx +85 -0
  10. package/badge/defaultProps.ts +10 -0
  11. package/badge/index.ts +9 -0
  12. package/badge/style/css.js +1 -0
  13. package/badge/style/index.js +1 -0
  14. package/badge/type.ts +51 -0
  15. package/button/Button.tsx +95 -0
  16. package/button/defaultProps.ts +13 -0
  17. package/button/index.ts +7 -0
  18. package/button/style/css.js +1 -0
  19. package/button/style/index.js +1 -0
  20. package/button/type.ts +82 -0
  21. package/checkbox/Checkbox.tsx +19 -0
  22. package/checkbox/CheckboxGroup.tsx +207 -0
  23. package/checkbox/defaultProps.ts +14 -0
  24. package/checkbox/index.ts +10 -0
  25. package/checkbox/style/css.js +1 -0
  26. package/checkbox/style/index.js +1 -0
  27. package/checkbox/type.ts +117 -0
  28. package/common/Check.tsx +131 -0
  29. package/common/FakeArrow.tsx +36 -0
  30. package/common/PluginContainer.tsx +21 -0
  31. package/common/Portal.tsx +67 -0
  32. package/common.ts +76 -0
  33. package/config-provider/ConfigContext.tsx +21 -0
  34. package/config-provider/ConfigProvider.tsx +53 -0
  35. package/config-provider/index.ts +9 -0
  36. package/config-provider/type.ts +1062 -0
  37. package/dialog/Dialog.tsx +254 -0
  38. package/dialog/DialogCard.tsx +152 -0
  39. package/dialog/defaultProps.ts +25 -0
  40. package/dialog/hooks/useDialogDrag.ts +50 -0
  41. package/dialog/hooks/useDialogEsc.ts +31 -0
  42. package/dialog/hooks/useDialogPosition.ts +36 -0
  43. package/dialog/hooks/useLockStyle.ts +54 -0
  44. package/dialog/index.ts +13 -0
  45. package/dialog/plugin.tsx +78 -0
  46. package/dialog/style/css.js +1 -0
  47. package/dialog/style/index.js +1 -0
  48. package/dialog/type.ts +241 -0
  49. package/dialog/utils.ts +4 -0
  50. package/form/Form.tsx +136 -0
  51. package/form/FormContext.tsx +64 -0
  52. package/form/FormItem.tsx +554 -0
  53. package/form/FormList.tsx +303 -0
  54. package/form/const.ts +6 -0
  55. package/form/defaultProps.ts +26 -0
  56. package/form/formModel.ts +117 -0
  57. package/form/hooks/interface.ts +20 -0
  58. package/form/hooks/useForm.ts +122 -0
  59. package/form/hooks/useFormItemInitialData.ts +95 -0
  60. package/form/hooks/useFormItemStyle.tsx +122 -0
  61. package/form/hooks/useInstance.tsx +275 -0
  62. package/form/hooks/useWatch.ts +42 -0
  63. package/form/index.ts +11 -0
  64. package/form/style/css.js +1 -0
  65. package/form/style/index.js +1 -0
  66. package/form/type.ts +519 -0
  67. package/form/utils/index.ts +69 -0
  68. package/hooks/useAttach.ts +24 -0
  69. package/hooks/useCommonClassName.ts +45 -0
  70. package/hooks/useConfig.ts +3 -0
  71. package/hooks/useControlled.ts +39 -0
  72. package/hooks/useDefaultProps.ts +16 -0
  73. package/hooks/useDomCallback.ts +13 -0
  74. package/hooks/useDomRefCallback.ts +12 -0
  75. package/hooks/useDragSorter.tsx +151 -0
  76. package/hooks/useEventCallback.ts +47 -0
  77. package/hooks/useGlobalConfig.ts +14 -0
  78. package/hooks/useGlobalIcon.ts +14 -0
  79. package/hooks/useLastest.ts +13 -0
  80. package/hooks/useLayoutEffect.ts +7 -0
  81. package/hooks/useMouseEvent.ts +142 -0
  82. package/hooks/useMutationObserver.ts +56 -0
  83. package/hooks/usePopper.ts +189 -0
  84. package/hooks/useRipple.ts +0 -0
  85. package/hooks/useSetState.ts +25 -0
  86. package/hooks/useVirtualScroll.ts +246 -0
  87. package/hooks/useWindowSize.ts +31 -0
  88. package/index.ts +70 -0
  89. package/input/Input.tsx +383 -0
  90. package/input/InputGroup.tsx +29 -0
  91. package/input/defaultProps.ts +22 -0
  92. package/input/index.ts +11 -0
  93. package/input/style/css.js +1 -0
  94. package/input/style/index.js +1 -0
  95. package/input/type.ts +219 -0
  96. package/loading/Gradient.tsx +36 -0
  97. package/loading/Loading.tsx +169 -0
  98. package/loading/circleAdapter.ts +44 -0
  99. package/loading/defaultProps.ts +12 -0
  100. package/loading/index.ts +13 -0
  101. package/loading/style/css.js +1 -0
  102. package/loading/style/index.js +1 -0
  103. package/loading/type.ts +71 -0
  104. package/loading/utils/setStyle.ts +13 -0
  105. package/myform/index.ts +0 -0
  106. package/notification/Notify.ts +24 -0
  107. package/notification/NotifyContainer.tsx +90 -0
  108. package/notification/NotifyContext.tsx +173 -0
  109. package/notification/NotifyItem.tsx +121 -0
  110. package/notification/index.ts +3 -0
  111. package/notification/style/css.js +1 -0
  112. package/notification/style/index.js +1 -0
  113. package/notification/type.ts +23 -0
  114. package/package.json +52 -0
  115. package/popup/Popup.tsx +264 -0
  116. package/popup/defaultProps.ts +13 -0
  117. package/popup/hooks/useTrigger.ts +276 -0
  118. package/popup/index.ts +6 -0
  119. package/popup/style/css.js +1 -0
  120. package/popup/style/index.js +1 -0
  121. package/popup/type.ts +130 -0
  122. package/portal/Portal.tsx +63 -0
  123. package/portal/index.ts +1 -0
  124. package/select/Option.tsx +162 -0
  125. package/select/OptionGroup.tsx +30 -0
  126. package/select/PopupContent.tsx +271 -0
  127. package/select/Select.tsx +586 -0
  128. package/select/defaultProps.ts +27 -0
  129. package/select/hooks/useOptions.ts +120 -0
  130. package/select/hooks/usePanelVirtualScroll.ts +111 -0
  131. package/select/index.ts +9 -0
  132. package/select/style/css.js +1 -0
  133. package/select/style/index.js +2 -0
  134. package/select/type.ts +382 -0
  135. package/select/utils/helper.ts +256 -0
  136. package/select-input/SelectInput.tsx +98 -0
  137. package/select-input/defaultProps.ts +15 -0
  138. package/select-input/hook/useMultiple.tsx +100 -0
  139. package/select-input/hook/useOverlayInnerStyle.ts +84 -0
  140. package/select-input/hook/useSingle.tsx +112 -0
  141. package/select-input/index.ts +6 -0
  142. package/select-input/interface.ts +18 -0
  143. package/select-input/style/css.js +1 -0
  144. package/select-input/style/index.js +1 -0
  145. package/select-input/type.ts +280 -0
  146. package/space/defaultProps.ts +0 -0
  147. package/space/index.ts +0 -0
  148. package/space/type.ts +0 -0
  149. package/style/index.js +2 -0
  150. package/styles/_global.scss +39 -0
  151. package/styles/_vars.scss +386 -0
  152. package/styles/components/alert/_index.scss +175 -0
  153. package/styles/components/alert/_vars.scss +39 -0
  154. package/styles/components/badge/_index.scss +70 -0
  155. package/styles/components/badge/_vars.scss +25 -0
  156. package/styles/components/button/_index.scss +511 -0
  157. package/styles/components/button/_mixins.scss +39 -0
  158. package/styles/components/button/_vars.scss +122 -0
  159. package/styles/components/checkbox/_index.scss +158 -0
  160. package/styles/components/checkbox/_mixin.scss +0 -0
  161. package/styles/components/checkbox/_var.scss +60 -0
  162. package/styles/components/dialog/_animate.scss +135 -0
  163. package/styles/components/dialog/_index.scss +311 -0
  164. package/styles/components/dialog/_mixins.scss +0 -0
  165. package/styles/components/dialog/_vars.scss +59 -0
  166. package/styles/components/form/_index.scss +174 -0
  167. package/styles/components/form/_mixins.scss +76 -0
  168. package/styles/components/form/_vars.scss +100 -0
  169. package/styles/components/input/_index.scss +349 -0
  170. package/styles/components/input/_map.scss +0 -0
  171. package/styles/components/input/_mixins.scss +116 -0
  172. package/styles/components/input/_vars.scss +134 -0
  173. package/styles/components/loading/_index.scss +112 -0
  174. package/styles/components/loading/_vars.scss +39 -0
  175. package/styles/components/notification/_index.scss +160 -0
  176. package/styles/components/notification/_mixins.scss +12 -0
  177. package/styles/components/notification/_vars.scss +59 -0
  178. package/styles/components/popup/_index.scss +82 -0
  179. package/styles/components/popup/_mixin.scss +149 -0
  180. package/styles/components/popup/_var.scss +31 -0
  181. package/styles/components/select/_index.scss +290 -0
  182. package/styles/components/select/_var.scss +65 -0
  183. package/styles/components/select-input/_index.scss +5 -0
  184. package/styles/components/select-input/_var.scss +3 -0
  185. package/styles/components/switch/_index.scss +279 -0
  186. package/styles/components/switch/_mixins.scss +0 -0
  187. package/styles/components/switch/_vars.scss +61 -0
  188. package/styles/components/tag/_index.scss +316 -0
  189. package/styles/components/tag/_var.scss +85 -0
  190. package/styles/components/tag-input/_index.scss +163 -0
  191. package/styles/components/tag-input/_vars.scss +16 -0
  192. package/styles/globals.css +250 -0
  193. package/styles/mixins/_focus.scss +7 -0
  194. package/styles/mixins/_layout.scss +32 -0
  195. package/styles/mixins/_reset.scss +10 -0
  196. package/styles/mixins/_scrollbar.scss +31 -0
  197. package/styles/mixins/_text.scss +48 -0
  198. package/styles/rillple.css +16 -0
  199. package/styles/scrollbar.css +42 -0
  200. package/styles/themes/_dark.scss +191 -0
  201. package/styles/themes/_font.scss +79 -0
  202. package/styles/themes/_index.scss +5 -0
  203. package/styles/themes/_light.scss +190 -0
  204. package/styles/themes/_radius.scss +9 -0
  205. package/styles/themes/_size.scss +68 -0
  206. package/styles/themes.css +66 -0
  207. package/styles/utilities/_animation.scss +57 -0
  208. package/styles/utilities/_tips.scss +9 -0
  209. package/switch/Switch.tsx +120 -0
  210. package/switch/defaultProps.ts +3 -0
  211. package/switch/index.ts +7 -0
  212. package/switch/style/css.js +1 -0
  213. package/switch/style/index.js +1 -0
  214. package/switch/type.ts +46 -0
  215. package/tag/Tag.tsx +149 -0
  216. package/tag/defaultProps.ts +19 -0
  217. package/tag/index.ts +8 -0
  218. package/tag/style/css.js +1 -0
  219. package/tag/style/index.js +1 -0
  220. package/tag/type.ts +170 -0
  221. package/tag-input/TagInput.tsx +215 -0
  222. package/tag-input/defaultProps.ts +15 -0
  223. package/tag-input/hooks/useHover.ts +28 -0
  224. package/tag-input/hooks/useTagList.tsx +131 -0
  225. package/tag-input/hooks/useTagScroll.ts +105 -0
  226. package/tag-input/index.ts +9 -0
  227. package/tag-input/style/css.js +1 -0
  228. package/tag-input/style/index.js +1 -0
  229. package/tag-input/type.ts +224 -0
  230. package/tag-input/useTagList.tsx +131 -0
  231. package/utils/composeRefs.ts +14 -0
  232. package/utils/dom.ts +29 -0
  233. package/utils/forwardRefWithStatics.ts +12 -0
  234. package/utils/getScrollbarWidth.ts +11 -0
  235. package/utils/helper.ts +161 -0
  236. package/utils/isFragment.ts +22 -0
  237. package/utils/listener.ts +37 -0
  238. package/utils/noop.ts +3 -0
  239. package/utils/parentTNode.ts +38 -0
  240. package/utils/parseTNode.ts +38 -0
  241. package/utils/react-render.ts +108 -0
  242. package/utils/ref.ts +6 -0
  243. package/utils/refs.ts +81 -0
  244. package/utils/style.ts +60 -0
  245. 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,6 @@
1
+ export const enum ValidateStatus {
2
+ SUCCESS = "success",
3
+ WARNING = "warning",
4
+ ERROR = "error",
5
+ VALIDATING = "validating"
6
+ }
@@ -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
+ }