react-native-x-components 0.1.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 (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/XActionSheet/index.d.ts +47 -0
  4. package/dist/XActionSheet/index.js +100 -0
  5. package/dist/XAnimatedSearchPanel/index.d.ts +33 -0
  6. package/dist/XAnimatedSearchPanel/index.js +295 -0
  7. package/dist/XAnimatedView/index.d.ts +77 -0
  8. package/dist/XAnimatedView/index.js +227 -0
  9. package/dist/XButton/index.d.ts +44 -0
  10. package/dist/XButton/index.js +126 -0
  11. package/dist/XCalendar/XCalendarPopup.d.ts +38 -0
  12. package/dist/XCalendar/XCalendarPopup.js +76 -0
  13. package/dist/XCalendar/index.d.ts +50 -0
  14. package/dist/XCalendar/index.js +241 -0
  15. package/dist/XCarousel/index.d.ts +72 -0
  16. package/dist/XCarousel/index.js +183 -0
  17. package/dist/XCascadeSelect/index.d.ts +39 -0
  18. package/dist/XCascadeSelect/index.js +147 -0
  19. package/dist/XChart/index.d.ts +78 -0
  20. package/dist/XChart/index.js +162 -0
  21. package/dist/XCheckbox/index.d.ts +61 -0
  22. package/dist/XCheckbox/index.js +128 -0
  23. package/dist/XConfirmForm/index.d.ts +69 -0
  24. package/dist/XConfirmForm/index.js +113 -0
  25. package/dist/XDivider/index.d.ts +33 -0
  26. package/dist/XDivider/index.js +61 -0
  27. package/dist/XDropdownMenu/index.d.ts +66 -0
  28. package/dist/XDropdownMenu/index.js +258 -0
  29. package/dist/XElevator/index.d.ts +52 -0
  30. package/dist/XElevator/index.js +138 -0
  31. package/dist/XForm/FormStore.d.ts +82 -0
  32. package/dist/XForm/FormStore.js +279 -0
  33. package/dist/XForm/index.d.ts +116 -0
  34. package/dist/XForm/index.js +212 -0
  35. package/dist/XForm/types.d.ts +82 -0
  36. package/dist/XForm/types.js +1 -0
  37. package/dist/XForm/useForm.d.ts +2 -0
  38. package/dist/XForm/useForm.js +19 -0
  39. package/dist/XForm/utils.d.ts +42 -0
  40. package/dist/XForm/utils.js +131 -0
  41. package/dist/XForm/validate.d.ts +20 -0
  42. package/dist/XForm/validate.js +118 -0
  43. package/dist/XFormPro/components/FieldTrigger.d.ts +14 -0
  44. package/dist/XFormPro/components/FieldTrigger.js +38 -0
  45. package/dist/XFormPro/components/XDateField.d.ts +22 -0
  46. package/dist/XFormPro/components/XDateField.js +22 -0
  47. package/dist/XFormPro/components/XSelectField.d.ts +16 -0
  48. package/dist/XFormPro/components/XSelectField.js +19 -0
  49. package/dist/XFormPro/index.d.ts +44 -0
  50. package/dist/XFormPro/index.js +111 -0
  51. package/dist/XFormPro/renderers.d.ts +24 -0
  52. package/dist/XFormPro/renderers.js +53 -0
  53. package/dist/XFormPro/types.d.ts +142 -0
  54. package/dist/XFormPro/types.js +1 -0
  55. package/dist/XImage/index.d.ts +13 -0
  56. package/dist/XImage/index.js +32 -0
  57. package/dist/XInput/index.d.ts +43 -0
  58. package/dist/XInput/index.js +136 -0
  59. package/dist/XLicensePlate/index.d.ts +40 -0
  60. package/dist/XLicensePlate/index.js +172 -0
  61. package/dist/XLocale/index.d.ts +23 -0
  62. package/dist/XLocale/index.js +174 -0
  63. package/dist/XModalForm/index.d.ts +32 -0
  64. package/dist/XModalForm/index.js +85 -0
  65. package/dist/XMultiSelect/index.d.ts +31 -0
  66. package/dist/XMultiSelect/index.js +131 -0
  67. package/dist/XNumberKeyboard/index.d.ts +57 -0
  68. package/dist/XNumberKeyboard/index.js +142 -0
  69. package/dist/XPicker/index.d.ts +35 -0
  70. package/dist/XPicker/index.js +96 -0
  71. package/dist/XPickerDate/index.d.ts +58 -0
  72. package/dist/XPickerDate/index.js +234 -0
  73. package/dist/XPopupProvider/index.d.ts +23 -0
  74. package/dist/XPopupProvider/index.js +6 -0
  75. package/dist/XProgress/index.d.ts +49 -0
  76. package/dist/XProgress/index.js +196 -0
  77. package/dist/XProviders/XImagePreview/index.d.ts +44 -0
  78. package/dist/XProviders/XImagePreview/index.js +164 -0
  79. package/dist/XProviders/XLoadingModal/index.d.ts +46 -0
  80. package/dist/XProviders/XLoadingModal/index.js +119 -0
  81. package/dist/XProviders/XToast/index.d.ts +50 -0
  82. package/dist/XProviders/XToast/index.js +183 -0
  83. package/dist/XProviders/index.d.ts +31 -0
  84. package/dist/XProviders/index.js +28 -0
  85. package/dist/XProviders/usageExample.d.ts +19 -0
  86. package/dist/XProviders/usageExample.js +83 -0
  87. package/dist/XPullView/index.d.ts +76 -0
  88. package/dist/XPullView/index.js +220 -0
  89. package/dist/XRadio/index.d.ts +83 -0
  90. package/dist/XRadio/index.js +186 -0
  91. package/dist/XRecord/index.d.ts +37 -0
  92. package/dist/XRecord/index.js +160 -0
  93. package/dist/XSignature/XSignatureSkia.d.ts +39 -0
  94. package/dist/XSignature/XSignatureSkia.js +184 -0
  95. package/dist/XSignature/core.d.ts +69 -0
  96. package/dist/XSignature/core.js +108 -0
  97. package/dist/XSignature/index.d.ts +33 -0
  98. package/dist/XSignature/index.js +88 -0
  99. package/dist/XTabs/index.d.ts +65 -0
  100. package/dist/XTabs/index.js +210 -0
  101. package/dist/XTag/index.d.ts +61 -0
  102. package/dist/XTag/index.js +153 -0
  103. package/dist/XTopView/index.d.ts +25 -0
  104. package/dist/XTopView/index.js +8 -0
  105. package/dist/XTopViewStore/index.d.ts +45 -0
  106. package/dist/XTopViewStore/index.js +49 -0
  107. package/dist/XUpload/XUploadImage.d.ts +37 -0
  108. package/dist/XUpload/XUploadImage.js +142 -0
  109. package/dist/XUpload/XUploadVideo.d.ts +24 -0
  110. package/dist/XUpload/XUploadVideo.js +83 -0
  111. package/dist/XUpload/adapters.d.ts +50 -0
  112. package/dist/XUpload/adapters.js +110 -0
  113. package/dist/XUpload/helpers.d.ts +35 -0
  114. package/dist/XUpload/helpers.js +85 -0
  115. package/dist/XUpload/index.d.ts +12 -0
  116. package/dist/XUpload/index.js +5 -0
  117. package/dist/XUpload/provider.d.ts +26 -0
  118. package/dist/XUpload/provider.js +43 -0
  119. package/dist/XUpload/types.d.ts +31 -0
  120. package/dist/XUpload/types.js +4 -0
  121. package/dist/XWheel/index.d.ts +49 -0
  122. package/dist/XWheel/index.js +263 -0
  123. package/dist/index.d.ts +113 -0
  124. package/dist/index.js +127 -0
  125. package/dist/theme.d.ts +164 -0
  126. package/dist/theme.js +161 -0
  127. package/package.json +78 -0
@@ -0,0 +1,138 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XElevator —— 电梯楼层/字母导航列表(duxui Elevator 的 RN 原生重写版)
5
+ * ============================================================================
6
+ *
7
+ * 【与 duxui Elevator 的关系】
8
+ * duxui 用 ScrollView + Layout 手动累加每组高度算 scrollTop;
9
+ * RN 端 SectionList 原生支持分组吸顶 + scrollToLocation,
10
+ * 这里直接用 SectionList 重写:代码量减半、吸顶/定位更稳。
11
+ *
12
+ * 【右侧导航条】
13
+ * - 触摸(按下/滑动)定位:容器拦截 responder 事件,用 locationY 与
14
+ * 各标签 onLayout 记录的 y 区间比对,命中即 scrollToLocation;
15
+ * - 滚动联动高亮:onViewableItemsChanged 取第一个可见项所属 section。
16
+ *
17
+ * 【API 对标 duxui】list -> sections({title, data}[])、onItemClick、
18
+ * showNav、renderTop/Header/Footer/Empty;扩展 navLabel(导航条短标签,
19
+ * 默认取 title 首字符)、keyExtractor。
20
+ * ============================================================================
21
+ */
22
+ import { useCallback, useRef, useState } from 'react';
23
+ import { SectionList, StyleSheet, Text, View, } from 'react-native';
24
+ import { useXTheme } from '../theme';
25
+ export function XElevator({ sections, renderItem, keyExtractor, onItemClick, showNav = true, renderTop, renderFooter, renderEmpty, style, }) {
26
+ const t = useXTheme();
27
+ const listRef = useRef(null);
28
+ /** 导航条每个标签的 y 中心(onLayout 记录,用于触摸命中) */
29
+ const navYRef = useRef([]);
30
+ /** 当前高亮的分组下标 */
31
+ const [activeIndex, setActiveIndex] = useState(0);
32
+ /** 滚动定位到分组 */
33
+ const scrollToSection = useCallback((index) => {
34
+ if (index < 0 || index >= sections.length)
35
+ return;
36
+ listRef.current?.scrollToLocation({ sectionIndex: index, itemIndex: 0, viewPosition: 0 });
37
+ }, [sections.length]);
38
+ /**
39
+ * 导航条触摸(按下/滑动):locationY 落在哪个标签区间 → 定位。
40
+ * onStartShouldSetResponder 抢占事件,滑动中 onResponderMove 连续命中。
41
+ */
42
+ const handleNavTouch = useCallback((e) => {
43
+ const y = e.nativeEvent.locationY;
44
+ const ys = navYRef.current;
45
+ for (let i = ys.length - 1; i >= 0; i--) {
46
+ if (ys[i] !== undefined && y >= ys[i]) {
47
+ if (activeIndex !== i)
48
+ setActiveIndex(i);
49
+ scrollToSection(i);
50
+ return;
51
+ }
52
+ }
53
+ scrollToSection(0);
54
+ }, [activeIndex, scrollToSection]);
55
+ /** 第一个可见项所属分组 → 导航高亮 */
56
+ const handleViewableItemsChanged = useRef(({ viewableItems }) => {
57
+ const first = viewableItems.find(v => v.section);
58
+ if (!first?.section)
59
+ return;
60
+ const index = sections.findIndex(s => s.title === first.section.title);
61
+ if (index >= 0)
62
+ setActiveIndex(prev => (prev === index ? prev : index));
63
+ }).current;
64
+ /** 默认行渲染 */
65
+ const defaultRenderItem = (item, section, index) => (_jsx(Text, { style: [styles.itemText, { color: t.colorText }], children: item.name }));
66
+ /** SectionList 的行:包一层按压反馈 */
67
+ const renderRow = ({ item, section, index }) => {
68
+ const Row = renderItem ?? defaultRenderItem;
69
+ return (_jsx(View, { style: [styles.item, { borderBottomColor: t.colorSplit }], onTouchEnd: () => onItemClick?.(item, section), children: Row(item, section, index) }));
70
+ };
71
+ const hasData = sections.some(s => s.data.length > 0);
72
+ return (_jsxs(View, { style: [styles.container, style], children: [_jsxs(View, { style: styles.listWrap, children: [renderTop?.(), _jsx(SectionList, { ref: listRef, sections: sections, renderItem: renderRow, renderSectionHeader: ({ section }) => (_jsx(View, { style: [styles.sectionHeader, { backgroundColor: t.colorBgLayout }], children: _jsx(Text, { style: [styles.sectionTitle, { color: t.colorTextSecondary }], children: section.title }) })), keyExtractor: keyExtractor ?? ((item, index) => String(index)), stickySectionHeadersEnabled: true, onViewableItemsChanged: handleViewableItemsChanged, viewabilityConfig: { itemVisiblePercentThreshold: 10 }, ListEmptyComponent: renderEmpty ?? _jsx(Text, { style: [styles.empty, { color: t.colorTextTertiary }], children: "\u6682\u65E0\u6570\u636E" }), ListFooterComponent: renderFooter, getItemLayout: undefined, onScrollToIndexFailed: () => {
73
+ // 分组高度未测量完成时定位可能失败,兜底到顶部分组
74
+ listRef.current?.scrollToLocation({ sectionIndex: 0, itemIndex: 0, viewPosition: 0 });
75
+ } }), renderFooter?.()] }), showNav && hasData && (_jsx(View, { style: [styles.nav, { backgroundColor: t.colorBgContainerDisabled }], onStartShouldSetResponder: () => true, onMoveShouldSetResponder: () => true, onResponderGrant: handleNavTouch, onResponderMove: handleNavTouch, children: sections.map((section, i) => (_jsx(View, { onLayout: e => {
76
+ // 记录每个标签的"区间下界",命中判断用
77
+ navYRef.current[i] = e.nativeEvent.layout.y + e.nativeEvent.layout.height / 2;
78
+ }, style: [styles.navItem, activeIndex === i && styles.navItemActive], children: _jsx(Text, { style: [
79
+ styles.navText,
80
+ { color: t.colorTextSecondary },
81
+ activeIndex === i && { color: t.colorTextLightSolid },
82
+ ], allowFontScaling: false, children: section.navLabel ?? section.title.slice(0, 1) }) }, section.title + i))) }))] }));
83
+ }
84
+ const styles = StyleSheet.create({
85
+ container: {
86
+ flex: 1,
87
+ overflow: 'hidden',
88
+ position: 'relative',
89
+ },
90
+ listWrap: {
91
+ flex: 1,
92
+ },
93
+ sectionHeader: {
94
+ paddingHorizontal: 16,
95
+ paddingVertical: 6,
96
+ },
97
+ sectionTitle: {
98
+ fontSize: 13,
99
+ },
100
+ item: {
101
+ paddingHorizontal: 16,
102
+ paddingVertical: 12,
103
+ borderBottomWidth: StyleSheet.hairlineWidth,
104
+ },
105
+ itemText: {
106
+ fontSize: 15,
107
+ },
108
+ empty: {
109
+ textAlign: 'center',
110
+ paddingVertical: 40,
111
+ fontSize: 14,
112
+ },
113
+ nav: {
114
+ position: 'absolute',
115
+ right: 4,
116
+ top: '20%',
117
+ borderRadius: 12,
118
+ paddingHorizontal: 5,
119
+ paddingVertical: 6,
120
+ alignItems: 'center',
121
+ },
122
+ navItem: {
123
+ paddingVertical: 2,
124
+ paddingHorizontal: 3,
125
+ borderRadius: 9,
126
+ minHeight: 18,
127
+ justifyContent: 'center',
128
+ },
129
+ navItemActive: {
130
+ backgroundColor: '#2080F0',
131
+ },
132
+ navText: {
133
+ fontSize: 11,
134
+ lineHeight: 14,
135
+ textAlign: 'center',
136
+ },
137
+ });
138
+ export default XElevator;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ============================================================================
3
+ * FormStore —— XForm 的数据中枢(对标 rc-field-form 的 FormStore 思路)
4
+ * ============================================================================
5
+ *
6
+ * 职责:
7
+ * 1. 集中保存所有字段值(store)与错误(errorsMap)
8
+ * 2. Form.Item 挂载时注册字段实体(rules / validateTrigger / initialValue),
9
+ * 卸载时注销(值保留,对标 antd preserve 默认行为)
10
+ * 3. 值变化 -> 通知所有字段实体重渲染 + onValuesChange 回调 + 按触发时机校验
11
+ * 本字段和 dependencies 依赖它的字段
12
+ * 4. validateFields / submit 的聚合校验
13
+ *
14
+ * 注册条目(entry)设计说明:
15
+ * Item 传进来的 entity 是"稳定对象 + 每轮渲染原地更新字段",name 变化时
16
+ * entry.key 在注册那一刻被捕获,注销时按捕获值清理 —— 避免 name 原地更新后
17
+ * 清错 key 的竞态。
18
+ *
19
+ * 不做的事(刻意保持小):
20
+ * - 不做 Form.List(antd 的动态列表字段)
21
+ * - 不做精确依赖调度:所有值变化通知全部实体,由 Item 自行读值,
22
+ * 表单规模下性能足够,换来实现简单可靠
23
+ */
24
+ import type { XErrorEntity, XFieldData, XFormInstance, XNamePath, XRule, XValidateErrorEntity, XValidateTrigger } from './types';
25
+ export interface XFieldEntity {
26
+ name: XNamePath;
27
+ rules?: XRule[];
28
+ validateTrigger?: XValidateTrigger | XValidateTrigger[];
29
+ dependencies?: XNamePath[];
30
+ initialValue?: any;
31
+ /** 触发该 Form.Item 重渲染(读取最新 value/errors) */
32
+ reRender: () => void;
33
+ }
34
+ interface XFormCallbacks {
35
+ onFinish?: (values: any) => void;
36
+ onFinishFailed?: (error: XValidateErrorEntity) => void;
37
+ onValuesChange?: (changedValues: any, allValues: any) => void;
38
+ }
39
+ export declare class FormStore {
40
+ private store;
41
+ /** initialValues(Form 级):resetFields 的还原基准 */
42
+ private initialValues;
43
+ private fieldEntries;
44
+ /** 无 name 的 Item(函数式 children / shouldUpdate 联动渲染)的通用订阅者 */
45
+ private listeners;
46
+ private errorsMap;
47
+ private touchedKeys;
48
+ private callbacks;
49
+ /** 字段级校验 token:同一字段并发校验时只保留最后一次的结果(打字快时的竞态) */
50
+ private validateTokens;
51
+ private validateSeq;
52
+ registerField: (entity: XFieldEntity) => void;
53
+ unregisterField: (entity: XFieldEntity) => void;
54
+ /**
55
+ * 通用订阅:无 name 的 Form.Item(shouldUpdate + 函数式 children)注册,
56
+ * store 任何变化时被通知。返回退订函数。
57
+ */
58
+ subscribe: (listener: () => void) => (() => void);
59
+ setCallbacks: (callbacks: XFormCallbacks) => void;
60
+ initValues: (initialValues?: Record<string, any>) => void;
61
+ getFieldValue: (name: XNamePath) => any;
62
+ getFieldsValue: (nameList?: XNamePath[] | true) => any;
63
+ setFieldValue: (name: XNamePath, value: any) => void;
64
+ setFieldsValue: (values: Record<string, any>) => void;
65
+ setFields: (fields: XFieldData[]) => void;
66
+ resetFields: (nameList?: XNamePath[]) => void;
67
+ getFieldError: (name: XNamePath) => string[];
68
+ getFieldsError: (nameList?: XNamePath[]) => XErrorEntity[];
69
+ isFieldTouched: (name: XNamePath) => boolean;
70
+ isFieldsTouched: (nameList?: XNamePath[], allFieldsTouched?: boolean) => boolean;
71
+ updateValue: (name: XNamePath, value: any, trigger?: XValidateTrigger) => void;
72
+ /** 某字段的 onBlur 触发的校验入口 */
73
+ validateOnBlur: (name: XNamePath) => void;
74
+ private emitValuesChange;
75
+ validateFields: (nameList?: XNamePath[]) => Promise<any>;
76
+ /** 校验一组注册条目:并发执行,写回 errorsMap,通知重渲染 */
77
+ private validateEntries;
78
+ submit: () => Promise<void>;
79
+ private notifyAll;
80
+ getForm: () => XFormInstance<any>;
81
+ }
82
+ export {};
@@ -0,0 +1,279 @@
1
+ import { deepClone, getValueByPath, isSameNamePath, matchDependency, namePathKey, setValueByPath } from './utils';
2
+ import { validateValue } from './validate';
3
+ export class FormStore {
4
+ store = {};
5
+ /** initialValues(Form 级):resetFields 的还原基准 */
6
+ initialValues = {};
7
+ fieldEntries = [];
8
+ /** 无 name 的 Item(函数式 children / shouldUpdate 联动渲染)的通用订阅者 */
9
+ listeners = new Set();
10
+ errorsMap = new Map();
11
+ touchedKeys = new Set();
12
+ callbacks = {};
13
+ /** 字段级校验 token:同一字段并发校验时只保留最后一次的结果(打字快时的竞态) */
14
+ validateTokens = new Map();
15
+ validateSeq = 0;
16
+ // ------------------------------------------------------------------
17
+ // 字段实体注册
18
+ // ------------------------------------------------------------------
19
+ registerField = (entity) => {
20
+ const key = namePathKey(entity.name);
21
+ // 同 key 重复注册(StrictMode 双挂载等场景):先清旧条目
22
+ this.fieldEntries = this.fieldEntries.filter(entry => entry.key !== key);
23
+ this.fieldEntries.push({ entity, key });
24
+ // Item 的 initialValue:仅当 store 里还没有值时生效(对标 antd initialValue 语义)
25
+ if (entity.initialValue !== undefined && getValueByPath(this.store, entity.name) === undefined) {
26
+ this.store = setValueByPath(this.store, entity.name, entity.initialValue);
27
+ entity.reRender();
28
+ }
29
+ };
30
+ unregisterField = (entity) => {
31
+ const entry = this.fieldEntries.find(e => e.entity === entity);
32
+ if (entry) {
33
+ this.fieldEntries = this.fieldEntries.filter(e => e !== entry);
34
+ this.errorsMap.delete(entry.key);
35
+ this.touchedKeys.delete(entry.key);
36
+ }
37
+ };
38
+ /**
39
+ * 通用订阅:无 name 的 Form.Item(shouldUpdate + 函数式 children)注册,
40
+ * store 任何变化时被通知。返回退订函数。
41
+ */
42
+ subscribe = (listener) => {
43
+ this.listeners.add(listener);
44
+ return () => this.listeners.delete(listener);
45
+ };
46
+ // ------------------------------------------------------------------
47
+ // callbacks(Form 组件每轮渲染注入最新闭包)
48
+ // ------------------------------------------------------------------
49
+ setCallbacks = (callbacks) => {
50
+ this.callbacks = callbacks;
51
+ };
52
+ initValues = (initialValues) => {
53
+ if (initialValues) {
54
+ this.initialValues = deepClone(initialValues);
55
+ this.store = { ...this.store, ...deepClone(initialValues) };
56
+ this.notifyAll();
57
+ }
58
+ };
59
+ // ------------------------------------------------------------------
60
+ // 取值
61
+ // ------------------------------------------------------------------
62
+ getFieldValue = (name) => getValueByPath(this.store, name);
63
+ getFieldsValue = (nameList) => {
64
+ if (nameList === true)
65
+ return deepClone(this.store);
66
+ if (!nameList)
67
+ return deepClone(this.store);
68
+ const out = {};
69
+ for (const name of nameList) {
70
+ const key = namePathKey(name);
71
+ out[key] = deepClone(getValueByPath(this.store, name));
72
+ }
73
+ return out;
74
+ };
75
+ // ------------------------------------------------------------------
76
+ // 设值(外部 API:setFieldValue / setFieldsValue / setFields)
77
+ // ------------------------------------------------------------------
78
+ setFieldValue = (name, value) => {
79
+ this.store = setValueByPath(this.store, name, value);
80
+ // 程序化设值只重渲染,不触发 onValuesChange(antd 同款语义:
81
+ // 外部回显/回填不属于"用户输入",避免受控回声)
82
+ this.notifyAll();
83
+ };
84
+ setFieldsValue = (values) => {
85
+ Object.keys(values).forEach(key => {
86
+ this.store = setValueByPath(this.store, key, values[key]);
87
+ });
88
+ this.notifyAll();
89
+ };
90
+ setFields = (fields) => {
91
+ for (const field of fields) {
92
+ if ('value' in field) {
93
+ this.store = setValueByPath(this.store, field.name, field.value);
94
+ }
95
+ if (field.errors) {
96
+ this.errorsMap.set(namePathKey(field.name), field.errors);
97
+ }
98
+ }
99
+ this.notifyAll();
100
+ };
101
+ // ------------------------------------------------------------------
102
+ // 重置
103
+ // ------------------------------------------------------------------
104
+ resetFields = (nameList) => {
105
+ const targets = nameList ?? this.fieldEntries.map(entry => entry.entity.name);
106
+ for (const name of targets) {
107
+ const key = namePathKey(name);
108
+ const fromInitial = getValueByPath(this.initialValues, name);
109
+ const entry = this.fieldEntries.find(e => e.key === key);
110
+ const nextValue = fromInitial !== undefined ? fromInitial : entry?.entity.initialValue !== undefined ? entry.entity.initialValue : undefined;
111
+ this.store = setValueByPath(this.store, name, nextValue);
112
+ this.errorsMap.delete(key);
113
+ this.touchedKeys.delete(key);
114
+ }
115
+ this.notifyAll();
116
+ };
117
+ // ------------------------------------------------------------------
118
+ // 错误读取
119
+ // ------------------------------------------------------------------
120
+ getFieldError = (name) => this.errorsMap.get(namePathKey(name)) ?? [];
121
+ getFieldsError = (nameList) => {
122
+ const targets = nameList ?? this.fieldEntries.map(entry => entry.entity.name);
123
+ return targets.map(name => ({ name, errors: this.getFieldError(name) }));
124
+ };
125
+ // ------------------------------------------------------------------
126
+ // touched
127
+ // ------------------------------------------------------------------
128
+ isFieldTouched = (name) => this.touchedKeys.has(namePathKey(name));
129
+ isFieldsTouched = (nameList, allFieldsTouched = false) => {
130
+ const keys = (nameList ?? [...this.touchedKeys]).map(namePathKey);
131
+ if (!keys.length)
132
+ return false;
133
+ return allFieldsTouched ? keys.every(k => this.touchedKeys.has(k)) : keys.some(k => this.touchedKeys.has(k));
134
+ };
135
+ // ------------------------------------------------------------------
136
+ // 值更新(Form.Item 注入的 onChange 走这里)
137
+ // ------------------------------------------------------------------
138
+ updateValue = (name, value, trigger = 'onChange') => {
139
+ const key = namePathKey(name);
140
+ const prev = getValueByPath(this.store, name);
141
+ const unchanged = Object.is(prev, value);
142
+ this.store = setValueByPath(this.store, name, value);
143
+ this.touchedKeys.add(key);
144
+ /**
145
+ * 同值守卫(关键):Android 受控 TextInput 在 IME 合成期间会把 React 提交的
146
+ * value 原样回抛 onChangeText;若值没变仍通知重渲染,会形成
147
+ * "commit -> 回抛 -> commit" 的同步递归,直接 Maximum call stack size exceeded。
148
+ * 值不变时只记 touched,不触发重渲染/回调/校验。
149
+ */
150
+ if (unchanged) {
151
+ return;
152
+ }
153
+ this.notifyAll();
154
+ this.emitValuesChange(name, value);
155
+ // 本字段按触发时机校验
156
+ const entry = this.fieldEntries.find(e => e.key === key);
157
+ if (entry) {
158
+ const triggers = normalizeTriggers(entry.entity.validateTrigger);
159
+ if (triggers.includes(trigger)) {
160
+ this.validateEntries([entry]);
161
+ }
162
+ // dependencies:依赖本字段的其他字段重新校验(经典场景:二次输入密码)
163
+ const dependents = this.fieldEntries.filter(e => e !== entry && matchDependency(e.entity.dependencies, name));
164
+ if (dependents.length) {
165
+ this.validateEntries(dependents);
166
+ }
167
+ }
168
+ };
169
+ /** 某字段的 onBlur 触发的校验入口 */
170
+ validateOnBlur = (name) => {
171
+ const key = namePathKey(name);
172
+ const entry = this.fieldEntries.find(e => e.key === key);
173
+ if (!entry)
174
+ return;
175
+ const triggers = normalizeTriggers(entry.entity.validateTrigger);
176
+ if (triggers.includes('onBlur')) {
177
+ this.validateEntries([entry]);
178
+ }
179
+ };
180
+ emitValuesChange = (name, value) => {
181
+ if (!this.callbacks.onValuesChange)
182
+ return;
183
+ const all = deepClone(this.store);
184
+ if (name === null) {
185
+ this.callbacks.onValuesChange(value, all);
186
+ }
187
+ else {
188
+ this.callbacks.onValuesChange(setValueByPath({}, name, value), all);
189
+ }
190
+ };
191
+ // ------------------------------------------------------------------
192
+ // 校验
193
+ // ------------------------------------------------------------------
194
+ validateFields = async (nameList) => {
195
+ const entries = nameList
196
+ ? this.fieldEntries.filter(entry => nameList.some(n => isSameNamePath(n, entry.entity.name)))
197
+ : [...this.fieldEntries];
198
+ await this.validateEntries(entries);
199
+ const failed = entries.filter(entry => (this.errorsMap.get(entry.key) ?? []).length > 0);
200
+ const values = nameList ? this.getFieldsValue(nameList) : this.getFieldsValue();
201
+ if (failed.length) {
202
+ const errorEntity = {
203
+ values,
204
+ errorFields: failed.map(entry => ({ name: entry.entity.name, errors: this.errorsMap.get(entry.key) ?? [] })),
205
+ outOfDate: false,
206
+ };
207
+ throw errorEntity;
208
+ }
209
+ return values;
210
+ };
211
+ /** 校验一组注册条目:并发执行,写回 errorsMap,通知重渲染 */
212
+ validateEntries = async (entries) => {
213
+ if (!entries.length)
214
+ return;
215
+ // 每个字段领一个新 token;await 期间同字段又触发了新一轮校验的话,本轮结果作废
216
+ const myTokens = new Map();
217
+ for (const entry of entries) {
218
+ const token = ++this.validateSeq;
219
+ this.validateTokens.set(entry.key, token);
220
+ myTokens.set(entry.key, token);
221
+ }
222
+ const results = await Promise.all(entries.map(async (entry) => {
223
+ const { name, rules } = entry.entity;
224
+ const value = getValueByPath(this.store, name);
225
+ const errors = await validateValue(value, rules, name);
226
+ return { entry, errors };
227
+ }));
228
+ for (const { entry, errors } of results) {
229
+ if (this.validateTokens.get(entry.key) !== myTokens.get(entry.key))
230
+ continue; // 过期结果,丢弃
231
+ if (errors.length) {
232
+ this.errorsMap.set(entry.key, errors);
233
+ }
234
+ else {
235
+ this.errorsMap.delete(entry.key);
236
+ }
237
+ }
238
+ this.notifyAll();
239
+ };
240
+ // ------------------------------------------------------------------
241
+ // 提交
242
+ // ------------------------------------------------------------------
243
+ submit = async () => {
244
+ try {
245
+ const values = await this.validateFields();
246
+ this.callbacks.onFinish?.(values);
247
+ }
248
+ catch (error) {
249
+ this.callbacks.onFinishFailed?.(error);
250
+ }
251
+ };
252
+ // ------------------------------------------------------------------
253
+ // 通知 & 实例
254
+ // ------------------------------------------------------------------
255
+ notifyAll = () => {
256
+ this.fieldEntries.forEach(entry => entry.entity.reRender());
257
+ this.listeners.forEach(listener => listener());
258
+ };
259
+ getForm = () => ({
260
+ getFieldValue: this.getFieldValue,
261
+ getFieldsValue: this.getFieldsValue,
262
+ setFieldValue: this.setFieldValue,
263
+ setFieldsValue: this.setFieldsValue,
264
+ setFields: this.setFields,
265
+ resetFields: this.resetFields,
266
+ getFieldError: this.getFieldError,
267
+ getFieldsError: this.getFieldsError,
268
+ validateFields: this.validateFields,
269
+ submit: this.submit,
270
+ isFieldTouched: this.isFieldTouched,
271
+ isFieldsTouched: this.isFieldsTouched,
272
+ __getStore: () => this,
273
+ });
274
+ }
275
+ function normalizeTriggers(trigger) {
276
+ if (!trigger)
277
+ return ['onChange'];
278
+ return Array.isArray(trigger) ? trigger : [trigger];
279
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * ============================================================================
3
+ * XForm / XForm.Item —— 对标 antd Form(RN 自实现版)
4
+ * ============================================================================
5
+ *
6
+ * 用法(和 antd 一致):
7
+ * ```tsx
8
+ * const [form] = XForm.useForm();
9
+ *
10
+ * <XForm
11
+ * form={form}
12
+ * initialValues={{name: '张三'}}
13
+ * onFinish={values => console.log('提交', values)}
14
+ * onFinishFailed={({errorFields}) => console.log('失败', errorFields)}
15
+ * >
16
+ * <XForm.Item label='姓名' name='name' rules={[{required: true, message: '请输入姓名'}]}>
17
+ * <TextInput placeholder='请输入姓名' />
18
+ * </XForm.Item>
19
+ *
20
+ * <XForm.Item label='性别' name='sex' rules={[{required: true, message: '请选择性别'}]}>
21
+ * <XRadio.Group options={[{label: '男', value: 1}, {label: '女', value: 0}]} />
22
+ * </XForm.Item>
23
+ *
24
+ * <XButton type='primary' onPress={() => form.submit()}>提交</XButton>
25
+ * </XForm>
26
+ * ```
27
+ *
28
+ * Form.Item 支持的 antd API:
29
+ * - name / label / rules / initialValue
30
+ * - validateTrigger: 'onChange' | 'onBlur' | 两者数组(默认 onChange)
31
+ * - valuePropName: 默认 'value'(Checkbox 这类受控属性叫 checked 时改成 'checked')
32
+ * - trigger: 值变化回调的 prop 名(默认 'onChange')
33
+ * - getValueFromEvent: 自定义从回调参数里取值
34
+ * - dependencies: 这些字段的值变化会触发本字段重新校验(如二次输入密码)
35
+ * - help / validateStatus: 手动控制提示文案与状态色(设置后优先生效,help 默认按 error 色显示)
36
+ * - required: 只控制 * 号显示(校验语义请写在 rules 里,同 antd)
37
+ * - noStyle: 不渲染 label/错误文案/外层间距,只注入 value/onChange
38
+ * - shouldUpdate + 函数 children: 渲染函数拿到 form 实例,做联动 UI
39
+ *
40
+ * 与 antd 的差异:
41
+ * - 无 Form.List / Form.Provider(动态列表后续有需要再加)
42
+ * - store 任何变化会通知所有 Item 重渲染(antd 是精确调度;表单规模下性能足够)
43
+ * - label 布局提供 vertical(默认)/ horizontal 两种
44
+ */
45
+ import React from 'react';
46
+ import { StyleProp, ViewStyle, TextStyle } from 'react-native';
47
+ import { useForm } from './useForm';
48
+ import type { XFormInstance, XNamePath, XRule, XValidateTrigger } from './types';
49
+ export type { XRule, XRuleObject, XNamePath, XFormInstance, XErrorEntity, XFieldData, XValidateErrorEntity, XValidateTrigger } from './types';
50
+ export { useForm } from './useForm';
51
+ export interface XFormProps {
52
+ /** useForm 创建的实例(不传时内部自建,外部拿不到实例方法) */
53
+ form?: XFormInstance<any>;
54
+ /** 初始值(只做初始化与 resetFields 还原基准,运行中修改不生效) */
55
+ initialValues?: Record<string, any>;
56
+ onFinish?: (values: any) => void;
57
+ onFinishFailed?: (errorInfo: {
58
+ values: any;
59
+ errorFields: {
60
+ name: XNamePath;
61
+ errors: string[];
62
+ }[];
63
+ outOfDate: boolean;
64
+ }) => void;
65
+ onValuesChange?: (changedValues: any, allValues: any) => void;
66
+ layout?: 'vertical' | 'horizontal';
67
+ /** horizontal 布局的 label 宽度 */
68
+ labelWidth?: number;
69
+ labelStyle?: StyleProp<TextStyle>;
70
+ requiredMark?: boolean;
71
+ style?: StyleProp<ViewStyle>;
72
+ children?: React.ReactNode;
73
+ testID?: string;
74
+ }
75
+ declare function XForm(props: XFormProps): React.JSX.Element;
76
+ export interface XFormItemProps {
77
+ /** 字段名(不传则纯布局:不注入 value/onChange,也不校验) */
78
+ name?: XNamePath;
79
+ label?: React.ReactNode;
80
+ rules?: XRule[];
81
+ initialValue?: any;
82
+ validateTrigger?: XValidateTrigger | XValidateTrigger[];
83
+ /** 受控值的 prop 名,默认 'value';Checkbox 场景传 'checked' */
84
+ valuePropName?: string;
85
+ /** 值变化的 prop 名,默认 'onChange' */
86
+ trigger?: string;
87
+ /** 自定义从回调参数里取值(默认自动识别:直传值 / 合成事件 / RN 原生事件) */
88
+ getValueFromEvent?: (...args: any[]) => any;
89
+ /** 这些字段的值变化会触发本字段重新校验 */
90
+ dependencies?: XNamePath[];
91
+ /** 手动提示文案(设置后优先生效,默认按 error 色显示;配 validateStatus 可换色) */
92
+ help?: React.ReactNode;
93
+ /** 手动校验状态:'error' | 'warning' | 'success' | 'validating' */
94
+ validateStatus?: 'error' | 'warning' | 'success' | 'validating';
95
+ /** 兼容 antd 的占位 prop(RN 无反馈图标,恒为无操作) */
96
+ hasFeedback?: boolean;
97
+ /** 只显示 * 号,不产生校验规则(同 antd:校验语义放 rules) */
98
+ required?: boolean;
99
+ /** 不渲染任何包裹:只注入 value/onChange */
100
+ noStyle?: boolean;
101
+ /** 函数式 children 联动渲染(配 shouldUpdate 使用;实现上 store 变化即重渲染) */
102
+ shouldUpdate?: boolean | ((prevValues: any, curValues: any) => boolean);
103
+ labelWidth?: number;
104
+ labelStyle?: StyleProp<TextStyle>;
105
+ style?: StyleProp<ViewStyle>;
106
+ /** 普通节点,或 shouldUpdate 场景下的渲染函数(拿到 form 实例) */
107
+ children?: React.ReactNode | ((form: XFormInstance<any>) => React.ReactNode);
108
+ testID?: string;
109
+ }
110
+ declare function XFormItem(props: XFormItemProps): React.JSX.Element;
111
+ type XFormWithStatics = typeof XForm & {
112
+ Item: typeof XFormItem;
113
+ useForm: typeof useForm;
114
+ };
115
+ declare const XFormExported: XFormWithStatics;
116
+ export default XFormExported;