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,212 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * ============================================================================
4
+ * XForm / XForm.Item —— 对标 antd Form(RN 自实现版)
5
+ * ============================================================================
6
+ *
7
+ * 用法(和 antd 一致):
8
+ * ```tsx
9
+ * const [form] = XForm.useForm();
10
+ *
11
+ * <XForm
12
+ * form={form}
13
+ * initialValues={{name: '张三'}}
14
+ * onFinish={values => console.log('提交', values)}
15
+ * onFinishFailed={({errorFields}) => console.log('失败', errorFields)}
16
+ * >
17
+ * <XForm.Item label='姓名' name='name' rules={[{required: true, message: '请输入姓名'}]}>
18
+ * <TextInput placeholder='请输入姓名' />
19
+ * </XForm.Item>
20
+ *
21
+ * <XForm.Item label='性别' name='sex' rules={[{required: true, message: '请选择性别'}]}>
22
+ * <XRadio.Group options={[{label: '男', value: 1}, {label: '女', value: 0}]} />
23
+ * </XForm.Item>
24
+ *
25
+ * <XButton type='primary' onPress={() => form.submit()}>提交</XButton>
26
+ * </XForm>
27
+ * ```
28
+ *
29
+ * Form.Item 支持的 antd API:
30
+ * - name / label / rules / initialValue
31
+ * - validateTrigger: 'onChange' | 'onBlur' | 两者数组(默认 onChange)
32
+ * - valuePropName: 默认 'value'(Checkbox 这类受控属性叫 checked 时改成 'checked')
33
+ * - trigger: 值变化回调的 prop 名(默认 'onChange')
34
+ * - getValueFromEvent: 自定义从回调参数里取值
35
+ * - dependencies: 这些字段的值变化会触发本字段重新校验(如二次输入密码)
36
+ * - help / validateStatus: 手动控制提示文案与状态色(设置后优先生效,help 默认按 error 色显示)
37
+ * - required: 只控制 * 号显示(校验语义请写在 rules 里,同 antd)
38
+ * - noStyle: 不渲染 label/错误文案/外层间距,只注入 value/onChange
39
+ * - shouldUpdate + 函数 children: 渲染函数拿到 form 实例,做联动 UI
40
+ *
41
+ * 与 antd 的差异:
42
+ * - 无 Form.List / Form.Provider(动态列表后续有需要再加)
43
+ * - store 任何变化会通知所有 Item 重渲染(antd 是精确调度;表单规模下性能足够)
44
+ * - label 布局提供 vertical(默认)/ horizontal 两种
45
+ */
46
+ import React, { createContext, useContext, useEffect, useMemo, useReducer, useRef } from 'react';
47
+ import { StyleSheet, View, Text } from 'react-native';
48
+ import { useXTheme, xTheme } from '../theme';
49
+ import { useForm } from './useForm';
50
+ import { defaultGetValueFromEvent } from './utils';
51
+ import { hasRequiredRule } from './validate';
52
+ export { useForm } from './useForm';
53
+ const XFormContext = createContext(null);
54
+ function XForm(props) {
55
+ const { form, initialValues, onFinish, onFinishFailed, onValuesChange, layout = 'vertical', labelWidth, labelStyle, requiredMark = true, style, children, testID, } = props;
56
+ const [internalForm] = useForm();
57
+ const formInstance = form ?? internalForm;
58
+ const store = formInstance.__getStore();
59
+ // 每轮渲染都刷新 callbacks 闭包,保证 onFinish 等拿到父组件最新的 state
60
+ useEffect(() => {
61
+ store.setCallbacks({ onFinish, onFinishFailed, onValuesChange });
62
+ });
63
+ // initialValues 只在 Form 首次挂载时应用一次(antd 同款约定)
64
+ const initialized = useRef(false);
65
+ useEffect(() => {
66
+ if (initialized.current)
67
+ return;
68
+ initialized.current = true;
69
+ store.initValues(initialValues);
70
+ }, [store, initialValues]);
71
+ const ctx = useMemo(() => ({ store, formInstance, layout, labelWidth, labelStyle, requiredMark }), [store, formInstance, layout, labelWidth, labelStyle, requiredMark]);
72
+ return (_jsx(XFormContext.Provider, { value: ctx, children: _jsx(View, { testID: testID, style: [styles.form, style], children: children }) }));
73
+ }
74
+ function XFormItem(props) {
75
+ const t = useXTheme();
76
+ const ctx = useContext(XFormContext);
77
+ // 脱离 <XForm> 使用:按纯布局渲染(hooks 必须无条件调用,所以早退分支放 Inner 外)
78
+ if (!ctx) {
79
+ return (_jsxs(View, { testID: props.testID, style: [styles.item, props.style], children: [props.label != null && _jsx(Text, { style: [styles.label, { color: t.colorText }], children: props.label }), typeof props.children === 'function' ? null : props.children] }));
80
+ }
81
+ return _jsx(XFormItemInner, { ...props, ctx: ctx });
82
+ }
83
+ /**
84
+ * 真正的实现拆到 Inner:上面的"无 ctx 早退分支"return 之后不能再有 hooks。
85
+ */
86
+ function XFormItemInner(props) {
87
+ const { name, label, rules, initialValue, validateTrigger = 'onChange', valuePropName = 'value', trigger = 'onChange', getValueFromEvent, dependencies, help, validateStatus, required, noStyle, labelWidth, labelStyle, style, children, testID, ctx, } = props;
88
+ const { store, formInstance, layout, labelWidth: formLabelWidth, labelStyle: formLabelStyle, requiredMark } = ctx;
89
+ const t = useXTheme();
90
+ // 订阅 store:任何值/错误变化 bump 一次,本 Item 读自己的最新值
91
+ const [, forceUpdate] = useReducer(c => c + 1, 0);
92
+ const nameDefined = name !== undefined && name !== null;
93
+ const nameKey = nameDefined ? (Array.isArray(name) ? name.map(String).join('.') : String(name)) : '';
94
+ /**
95
+ * 字段实体只创建一次;rules/validateTrigger 等每轮渲染原地更新。
96
+ * 不能按 rules 依赖重建实体 —— 调用方常写内联 rules 数组(每轮新引用),
97
+ * 重建会导致 register/unregister 反复横跳,错误信息被 unregister 清掉。
98
+ */
99
+ const entityRef = useRef(null);
100
+ if (!entityRef.current) {
101
+ entityRef.current = { name: name, rules, validateTrigger, dependencies, initialValue, reRender: forceUpdate };
102
+ }
103
+ else {
104
+ entityRef.current.name = name;
105
+ entityRef.current.rules = rules;
106
+ entityRef.current.validateTrigger = validateTrigger;
107
+ entityRef.current.dependencies = dependencies;
108
+ entityRef.current.initialValue = initialValue;
109
+ }
110
+ // 注册/注销只跟随 name 变化(name 变了视为换了字段)
111
+ useEffect(() => {
112
+ if (!nameDefined)
113
+ return;
114
+ store.registerField(entityRef.current);
115
+ return () => store.unregisterField(entityRef.current);
116
+ }, [store, nameKey, nameDefined]);
117
+ // 无 name 的 Item(shouldUpdate + 函数式 children):走通用订阅,store 变化即重渲染
118
+ useEffect(() => {
119
+ if (nameDefined || typeof children !== 'function')
120
+ return;
121
+ return store.subscribe(forceUpdate);
122
+ }, [store, nameDefined, typeof children === 'function']);
123
+ const value = nameDefined ? store.getFieldValue(name) : undefined;
124
+ const autoErrors = nameDefined ? store.getFieldError(name) : [];
125
+ // ---- help / validateStatus 手动态优先(antd:设置 help 且未设状态时按 error 展示) ----
126
+ const manualActive = help !== undefined || validateStatus !== undefined;
127
+ const effectiveStatus = validateStatus ?? (help !== undefined ? 'error' : undefined);
128
+ const helpText = manualActive ? help : autoErrors.length ? autoErrors.join(',') : null;
129
+ const helpColorStyle = manualActive
130
+ ? effectiveStatus === 'warning'
131
+ ? { color: t.colorWarning }
132
+ : effectiveStatus === 'error' || effectiveStatus === undefined
133
+ ? { color: t.colorError }
134
+ : { color: t.colorTextTertiary }
135
+ : { color: t.colorError };
136
+ // ---- children 处理:函数式 or 注入 value/onChange ----
137
+ let childNode = typeof children === 'function' ? null : children;
138
+ if (typeof children === 'function') {
139
+ // shouldUpdate + 函数 children:把 form 实例交给业务自己渲染(联动场景)
140
+ childNode = children(formInstance);
141
+ }
142
+ else if (nameDefined && React.isValidElement(children)) {
143
+ const child = children;
144
+ const childProps = {
145
+ [valuePropName]: value,
146
+ };
147
+ // 合并子组件自己的 trigger 回调(不能吞掉别人的 onChange)
148
+ const childTrigger = child.props?.[trigger];
149
+ childProps[trigger] = (...args) => {
150
+ childTrigger?.(...args);
151
+ const nextValue = getValueFromEvent ? getValueFromEvent(...args) : defaultGetValueFromEvent(valuePropName, args[0]);
152
+ store.updateValue(name, nextValue, 'onChange');
153
+ };
154
+ // validateTrigger 含 onBlur 时注入 onBlur 校验(同样合并子组件自己的 onBlur)
155
+ const triggers = Array.isArray(validateTrigger) ? validateTrigger : [validateTrigger];
156
+ if (triggers.includes('onBlur')) {
157
+ const childBlur = child.props?.onBlur;
158
+ childProps.onBlur = (...args) => {
159
+ childBlur?.(...args);
160
+ store.validateOnBlur(name);
161
+ };
162
+ }
163
+ childNode = React.cloneElement(child, childProps);
164
+ }
165
+ // ---- 渲染 ----
166
+ if (noStyle) {
167
+ return _jsx(_Fragment, { children: childNode });
168
+ }
169
+ const showRequiredMark = requiredMark && (required || hasRequiredRule(rules));
170
+ const isHorizontal = layout === 'horizontal';
171
+ const effectiveLabelWidth = labelWidth ?? formLabelWidth;
172
+ return (_jsxs(View, { testID: testID, style: [styles.item, isHorizontal && styles.itemHorizontal, style], children: [label != null && (_jsxs(View, { style: [styles.labelRow, isHorizontal && { width: effectiveLabelWidth, marginBottom: 0, marginRight: 12 }], children: [showRequiredMark && _jsx(Text, { style: [styles.requiredStar, { color: t.colorError }], children: "*" }), _jsx(Text, { style: [styles.label, { color: t.colorText }, formLabelStyle, labelStyle], allowFontScaling: false, children: label })] })), _jsxs(View, { style: isHorizontal ? styles.controlHorizontal : styles.control, children: [childNode, helpText != null && _jsx(Text, { style: [styles.help, { color: t.colorTextTertiary }, helpColorStyle], children: helpText })] })] }));
173
+ }
174
+ const XFormExported = XForm;
175
+ XFormExported.Item = XFormItem;
176
+ XFormExported.useForm = useForm;
177
+ const styles = StyleSheet.create({
178
+ form: {
179
+ width: '100%',
180
+ },
181
+ item: {
182
+ marginBottom: 18,
183
+ },
184
+ itemHorizontal: {
185
+ flexDirection: 'row',
186
+ alignItems: 'flex-start',
187
+ },
188
+ labelRow: {
189
+ flexDirection: 'row',
190
+ alignItems: 'center',
191
+ marginBottom: 8,
192
+ },
193
+ label: {
194
+ fontSize: xTheme.fontSize,
195
+ },
196
+ requiredStar: {
197
+ marginRight: 4,
198
+ fontSize: xTheme.fontSize,
199
+ },
200
+ control: {
201
+ width: '100%',
202
+ },
203
+ controlHorizontal: {
204
+ flex: 1,
205
+ },
206
+ help: {
207
+ fontSize: xTheme.fontSizeSM,
208
+ marginTop: 6,
209
+ lineHeight: 18,
210
+ },
211
+ });
212
+ export default XFormExported;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * XForm 类型定义 —— 对标 antd Form / rc-field-form 的类型子集
3
+ */
4
+ import type React from 'react';
5
+ /** 字段路径:'a' | ['a', 'b'] | ['a', 0, 'b'](数组下标场景) */
6
+ export type XNamePath = string | number | Array<string | number>;
7
+ /** 校验触发时机 */
8
+ export type XValidateTrigger = 'onChange' | 'onBlur';
9
+ /**
10
+ * 单条校验规则(对标 antd Rule,实现 async-validator 常用子集)
11
+ */
12
+ export interface XRuleObject {
13
+ /** 必填 */
14
+ required?: boolean;
15
+ /** 报错文案;未填时按规则类型给中文默认文案 */
16
+ message?: React.ReactNode;
17
+ /** 值类型校验:string | number | boolean | array | email | url */
18
+ type?: 'string' | 'number' | 'boolean' | 'array' | 'email' | 'url';
19
+ /** string:长度下限;number:数值下限;array:长度下限 */
20
+ min?: number;
21
+ /** 同上,上限 */
22
+ max?: number;
23
+ /** 长度/数值必须等于 */
24
+ len?: number;
25
+ /** 正则校验(对 string 值) */
26
+ pattern?: RegExp;
27
+ /** 值必须是枚举之一 */
28
+ enum?: Array<string | number>;
29
+ /** 空格字符串视为空(配合 required) */
30
+ whitespace?: boolean;
31
+ /**
32
+ * 自定义校验器(对标 antd validator):
33
+ * - 返回 Promise,reject / throw 即失败;reject 的值(或 error.message)作为文案
34
+ * - resolve(包括 reject() 空值 + rule.message 兜底)即通过
35
+ */
36
+ validator?: (rule: XRuleObject, value: any) => Promise<void | any>;
37
+ /** 先转换再校验(不影响 store 里的值,只影响校验输入) */
38
+ transform?: (value: any) => any;
39
+ }
40
+ export type XRule = XRuleObject;
41
+ /** validateFields 失败时 reject 的对象(对标 antd ValidateErrorEntity) */
42
+ export interface XValidateErrorEntity<Values = any> {
43
+ values: Values;
44
+ errorFields: XErrorEntity[];
45
+ outOfDate: boolean;
46
+ }
47
+ /** 单个字段的错误信息 */
48
+ export interface XErrorEntity {
49
+ name: XNamePath;
50
+ errors: string[];
51
+ }
52
+ /** setFields 的入参(对标 antd) */
53
+ export interface XFieldData {
54
+ name: XNamePath;
55
+ value?: any;
56
+ /** 传 [] 清除错误;传 ['xxx'] 设置错误 */
57
+ errors?: string[];
58
+ touched?: boolean;
59
+ }
60
+ /**
61
+ * 表单实例(对标 antd FormInstance)
62
+ * 所有方法与 antd 同名同义;__getStore 为内部实现细节,业务代码不要调用
63
+ */
64
+ export interface XFormInstance<Values = any> {
65
+ getFieldValue: (name: XNamePath) => any;
66
+ /** nameList 传 true 返回全部(含未注册字段);传数组只取指定字段;不传返回整个 values */
67
+ getFieldsValue: (nameList?: XNamePath[] | true) => Values;
68
+ setFieldValue: (name: XNamePath, value: any) => void;
69
+ setFieldsValue: (values: Partial<Values>) => void;
70
+ setFields: (fields: XFieldData[]) => void;
71
+ resetFields: (nameList?: XNamePath[]) => void;
72
+ getFieldError: (name: XNamePath) => string[];
73
+ getFieldsError: (nameList?: XNamePath[]) => XErrorEntity[];
74
+ /** 校验(不传 name 校验全部);失败 reject {values, errorFields, outOfDate} */
75
+ validateFields: (nameList?: XNamePath[]) => Promise<Values>;
76
+ /** 触发 onFinish / onFinishFailed(对标 antd form.submit()) */
77
+ submit: () => void;
78
+ isFieldTouched: (name: XNamePath) => boolean;
79
+ isFieldsTouched: (nameList?: XNamePath[], allFieldsTouched?: boolean) => boolean;
80
+ /** 内部:拿到 FormStore(XForm 组件接线用) */
81
+ __getStore: () => unknown;
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { XFormInstance } from './types';
2
+ export declare function useForm<Values extends Record<string, any> = any>(): [XFormInstance<Values>];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * XForm.useForm —— 对标 antd Form.useForm()
3
+ *
4
+ * const [form] = XForm.useForm();
5
+ * <XForm form={form}>...</XForm>
6
+ *
7
+ * 实例对象与 FormStore 绑定(__getStore),XForm 组件拿 form prop 反查 store;
8
+ * 不传 form 时 XForm 内部自建,一样可用(只是外部拿不到实例方法)。
9
+ */
10
+ import { useRef } from 'react';
11
+ import { FormStore } from './FormStore';
12
+ export function useForm() {
13
+ const ref = useRef(undefined);
14
+ if (!ref.current) {
15
+ const store = new FormStore();
16
+ ref.current = store.getForm();
17
+ }
18
+ return [ref.current];
19
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * XForm 工具函数:路径读写 / 事件取值 / 空值判断
3
+ */
4
+ import type { XNamePath } from './types';
5
+ /** namePath 序列化成 map key(['a', 0] -> 'a.0';'a' -> 'a') */
6
+ export declare function namePathKey(name: XNamePath): string;
7
+ /** 判断两个 namePath 是否同一个字段 */
8
+ export declare function isSameNamePath(a: XNamePath | undefined, b: XNamePath | undefined): boolean;
9
+ /** 判断 namePath 是否在依赖列表里(依赖支持单路径或路径数组) */
10
+ export declare function matchDependency(dependencies: XNamePath[] | undefined, name: XNamePath): boolean;
11
+ /** 空值:null/undefined/空串/纯空格串/空数组 */
12
+ export declare function isEmptyValue(value: any): boolean;
13
+ /**
14
+ * 按路径读值(只读,不创建中间对象)
15
+ * get({a: {b: 1}}, ['a','b']) -> 1
16
+ */
17
+ export declare function getValueByPath(store: any, name: XNamePath): any;
18
+ /**
19
+ * 按路径写值(浅拷贝路径上的对象,不 mutate 原 store —— 保证 React 数据流可预测)
20
+ * set({}, ['a','b'], 1) -> {a: {b: 1}}
21
+ */
22
+ export declare function setValueByPath<T>(store: T, name: XNamePath, value: any): T;
23
+ /**
24
+ * 深拷贝(Form values 里可能有 Date 等复杂对象,用 JSON 会丢内容,沿用 lodash cloneDeep 的场景)
25
+ *
26
+ * 环安全:一旦值被污染成循环引用(比如事件对象被误存进 store),
27
+ * 在 revisit 处返回原引用打断递归 —— 宁可数据带环也不能让 App 爆栈崩溃。
28
+ */
29
+ export declare function deepClone<T>(value: T): T;
30
+ /**
31
+ * 从事件参数里提取表单值(对标 rc-field-form 的 defaultGetValueFromEvent,做 RN 适配)
32
+ *
33
+ * 覆盖四种调用形态:
34
+ * 1. 直传值:TextInput onChangeText(text) —— text 直接是字符串
35
+ * 2. RN 原生事件对象:onChange({nativeEvent: {text}}) —— 必须先判这种,
36
+ * 否则事件对象会被整个存进 store:事件内部(_targetInst.viewConfig 等)全是
37
+ * 循环引用,后续 deepClone 直接 Maximum call stack size exceeded
38
+ * 3. 合成事件:XRadio/XCheckbox 的 onChange({value, checked, target})
39
+ * 4. 兜底:看起来像事件对象(有 nativeEvent/_targetInst/stopPropagation)的一律
40
+ * 返回 undefined,绝不把事件本身当值存
41
+ */
42
+ export declare function defaultGetValueFromEvent(valuePropName: string, arg: any): any;
@@ -0,0 +1,131 @@
1
+ /** namePath 序列化成 map key(['a', 0] -> 'a.0';'a' -> 'a') */
2
+ export function namePathKey(name) {
3
+ if (typeof name === 'string' || typeof name === 'number')
4
+ return String(name);
5
+ return name.map(String).join('.');
6
+ }
7
+ /** 判断两个 namePath 是否同一个字段 */
8
+ export function isSameNamePath(a, b) {
9
+ if (a === b)
10
+ return true;
11
+ if (!a || !b)
12
+ return false;
13
+ return namePathKey(a) === namePathKey(b);
14
+ }
15
+ /** 判断 namePath 是否在依赖列表里(依赖支持单路径或路径数组) */
16
+ export function matchDependency(dependencies, name) {
17
+ if (!dependencies?.length)
18
+ return false;
19
+ const key = namePathKey(name);
20
+ return dependencies.some(dep => namePathKey(dep) === key);
21
+ }
22
+ /** 空值:null/undefined/空串/纯空格串/空数组 */
23
+ export function isEmptyValue(value) {
24
+ if (value === null || value === undefined)
25
+ return true;
26
+ if (typeof value === 'string')
27
+ return value.trim() === '';
28
+ if (Array.isArray(value))
29
+ return value.length === 0;
30
+ return false;
31
+ }
32
+ /**
33
+ * 按路径读值(只读,不创建中间对象)
34
+ * get({a: {b: 1}}, ['a','b']) -> 1
35
+ */
36
+ export function getValueByPath(store, name) {
37
+ const path = Array.isArray(name) ? name : [name];
38
+ let current = store;
39
+ for (const key of path) {
40
+ if (current === null || current === undefined)
41
+ return undefined;
42
+ current = current[key];
43
+ }
44
+ return current;
45
+ }
46
+ /**
47
+ * 按路径写值(浅拷贝路径上的对象,不 mutate 原 store —— 保证 React 数据流可预测)
48
+ * set({}, ['a','b'], 1) -> {a: {b: 1}}
49
+ */
50
+ export function setValueByPath(store, name, value) {
51
+ const path = Array.isArray(name) ? name : [name];
52
+ if (path.length === 0)
53
+ return value;
54
+ function cloneLevel(node, level) {
55
+ const key = path[level];
56
+ if (level === path.length - 1) {
57
+ // 叶子:数组下标保持数组类型
58
+ if (Array.isArray(node)) {
59
+ const copy = [...node];
60
+ copy[key] = value;
61
+ return copy;
62
+ }
63
+ return { ...node, [key]: value };
64
+ }
65
+ const nextNode = node?.[key] ?? (typeof path[level + 1] === 'number' ? [] : {});
66
+ const clonedNext = cloneLevel(nextNode, level + 1);
67
+ if (Array.isArray(node)) {
68
+ const copy = [...node];
69
+ copy[key] = clonedNext;
70
+ return copy;
71
+ }
72
+ return { ...node, [key]: clonedNext };
73
+ }
74
+ return cloneLevel(store, 0);
75
+ }
76
+ /**
77
+ * 深拷贝(Form values 里可能有 Date 等复杂对象,用 JSON 会丢内容,沿用 lodash cloneDeep 的场景)
78
+ *
79
+ * 环安全:一旦值被污染成循环引用(比如事件对象被误存进 store),
80
+ * 在 revisit 处返回原引用打断递归 —— 宁可数据带环也不能让 App 爆栈崩溃。
81
+ */
82
+ export function deepClone(value) {
83
+ return cloneWithCycleGuard(value, new WeakSet());
84
+ }
85
+ function cloneWithCycleGuard(value, seen) {
86
+ if (value === null || typeof value !== 'object')
87
+ return value;
88
+ if (seen.has(value))
89
+ return value; // 循环引用:返回原引用打断
90
+ seen.add(value);
91
+ if (Array.isArray(value))
92
+ return value.map(item => cloneWithCycleGuard(item, seen));
93
+ if (value instanceof Date)
94
+ return new Date(value.getTime());
95
+ const out = {};
96
+ for (const key of Object.keys(value)) {
97
+ out[key] = cloneWithCycleGuard(value[key], seen);
98
+ }
99
+ return out;
100
+ }
101
+ /**
102
+ * 从事件参数里提取表单值(对标 rc-field-form 的 defaultGetValueFromEvent,做 RN 适配)
103
+ *
104
+ * 覆盖四种调用形态:
105
+ * 1. 直传值:TextInput onChangeText(text) —— text 直接是字符串
106
+ * 2. RN 原生事件对象:onChange({nativeEvent: {text}}) —— 必须先判这种,
107
+ * 否则事件对象会被整个存进 store:事件内部(_targetInst.viewConfig 等)全是
108
+ * 循环引用,后续 deepClone 直接 Maximum call stack size exceeded
109
+ * 3. 合成事件:XRadio/XCheckbox 的 onChange({value, checked, target})
110
+ * 4. 兜底:看起来像事件对象(有 nativeEvent/_targetInst/stopPropagation)的一律
111
+ * 返回 undefined,绝不把事件本身当值存
112
+ */
113
+ export function defaultGetValueFromEvent(valuePropName, arg) {
114
+ // RN 原生事件(TextInput 的 onChange/onBlur 等):值在 nativeEvent.text
115
+ if (arg && typeof arg === 'object' && arg.nativeEvent != null) {
116
+ return typeof arg.nativeEvent.text === 'string' ? arg.nativeEvent.text : undefined;
117
+ }
118
+ // 自定义合成事件对象(有对象形态的 target):优先按 valuePropName 取,其次 value
119
+ if (arg && typeof arg === 'object' && arg.target && typeof arg.target === 'object') {
120
+ if (valuePropName in arg.target)
121
+ return arg.target[valuePropName];
122
+ if ('value' in arg.target)
123
+ return arg.target.value;
124
+ return arg.value ?? arg;
125
+ }
126
+ // 防御:事件样子的对象(React 合成事件带 _targetInst/stopPropagation)不当值
127
+ if (arg && typeof arg === 'object' && (arg._targetInst !== undefined || typeof arg.stopPropagation === 'function')) {
128
+ return undefined;
129
+ }
130
+ return arg;
131
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ============================================================================
3
+ * XForm 校验引擎 —— 对标 async-validator 的常用子集
4
+ * ============================================================================
5
+ *
6
+ * 支持的规则:required / type / min / max / len / pattern / enum / whitespace /
7
+ * validator(自定义)/ transform
8
+ *
9
+ * 与 async-validator 的差异(有意为之):
10
+ * - type='number' 放宽:字符串 '12' 也按数字校验(RN TextInput 只产字符串,
11
+ * 实际项目里输入框数字校验基本都是字符串形态,严格类型反而难用)
12
+ * - 未实现的规则字段直接忽略,不报错
13
+ */
14
+ import type { XRule, XNamePath } from './types';
15
+ /** 规则里声明了 required 吗(Form.Item 的 * 号判断也用它) */
16
+ export declare function hasRequiredRule(rules?: XRule[]): boolean;
17
+ /**
18
+ * 校验一个字段:按规则顺序执行,收集全部错误(antd 展示所有错误文案,这里保持一致)
19
+ */
20
+ export declare function validateValue(value: any, rules: XRule[] | undefined, name: XNamePath, label?: string): Promise<string[]>;
@@ -0,0 +1,118 @@
1
+ import { isEmptyValue } from './utils';
2
+ /** 文案模板里的 ${label} 占位符替换 */
3
+ function fillTemplate(template, label) {
4
+ return template.replace(/\$\{label\}/g, label);
5
+ }
6
+ /** 规则里声明了 required 吗(Form.Item 的 * 号判断也用它) */
7
+ export function hasRequiredRule(rules) {
8
+ return !!rules?.some(rule => rule.required);
9
+ }
10
+ function typeOfValue(value) {
11
+ if (Array.isArray(value))
12
+ return 'array';
13
+ if (value === null)
14
+ return 'null';
15
+ if (value instanceof Date)
16
+ return 'date';
17
+ return typeof value;
18
+ }
19
+ /** 单条规则单值校验:返回错误文案数组(空数组 = 通过) */
20
+ async function validateRule(rule, rawValue, ctx) {
21
+ const { label } = ctx;
22
+ const message = rule.message !== undefined ? String(rule.message) : undefined;
23
+ const value = rule.transform ? rule.transform(rawValue) : rawValue;
24
+ const empty = isEmptyValue(value);
25
+ const errors = [];
26
+ // ---- required:空值直接失败(whitespace=true 时纯空格串也算空,isEmptyValue 已覆盖)----
27
+ if (rule.required && empty) {
28
+ return [message ?? fillTemplate('${label}是必填字段', label)];
29
+ }
30
+ // 非必填且值为空:跳过其余检查(async-validator 同款行为)
31
+ if (empty)
32
+ return [];
33
+ // ---- 自定义 validator:reject/throw 即失败 ----
34
+ if (rule.validator) {
35
+ try {
36
+ await rule.validator(rule, value);
37
+ }
38
+ catch (err) {
39
+ // reject() 空值 → 用 rule.message;reject('文案') / throw Error → 用其内容
40
+ const text = err?.message ?? (typeof err === 'string' ? err : undefined);
41
+ return [text || message || fillTemplate('${label}校验失败', label)];
42
+ }
43
+ return [];
44
+ }
45
+ // ---- type ----
46
+ if (rule.type) {
47
+ const actual = typeOfValue(value);
48
+ let typeOk = true;
49
+ switch (rule.type) {
50
+ case 'string':
51
+ typeOk = actual === 'string';
52
+ break;
53
+ case 'number':
54
+ // 放宽:数字字符串也算 number(RN 适配,见文件头注释)
55
+ typeOk = actual === 'number' || (actual === 'string' && value.trim() !== '' && !isNaN(Number(value)));
56
+ break;
57
+ case 'boolean':
58
+ typeOk = actual === 'boolean';
59
+ break;
60
+ case 'array':
61
+ typeOk = actual === 'array';
62
+ break;
63
+ case 'email':
64
+ typeOk = actual === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
65
+ break;
66
+ case 'url':
67
+ typeOk = actual === 'string' && /^https?:\/\/\S+$/.test(value);
68
+ break;
69
+ }
70
+ if (!typeOk) {
71
+ return [message ?? `${label}的类型应为 ${rule.type}`];
72
+ }
73
+ }
74
+ // ---- min / max / len:按实际值类型分派 ----
75
+ const actual = typeOfValue(value);
76
+ const asNumber = actual === 'number' ? value : actual === 'string' && !isNaN(Number(value)) ? Number(value) : null;
77
+ if (rule.len != null) {
78
+ const lenOk = actual === 'string' || actual === 'array' ? value.length === rule.len : asNumber != null ? asNumber === rule.len : true;
79
+ if (!lenOk)
80
+ return [message ?? `${label}应为 ${rule.len} 个字符/项`];
81
+ }
82
+ if (rule.min != null) {
83
+ const minOk = actual === 'string' || actual === 'array' ? value.length >= rule.min : asNumber != null ? asNumber >= rule.min : true;
84
+ if (!minOk)
85
+ return [message ?? `${label}不能少于 ${rule.min}${actual === 'string' ? ' 个字符' : actual === 'array' ? ' 项' : ''}`];
86
+ }
87
+ if (rule.max != null) {
88
+ const maxOk = actual === 'string' || actual === 'array' ? value.length <= rule.max : asNumber != null ? asNumber <= rule.max : true;
89
+ if (!maxOk)
90
+ return [message ?? `${label}不能超过 ${rule.max}${actual === 'string' ? ' 个字符' : actual === 'array' ? ' 项' : ''}`];
91
+ }
92
+ // ---- pattern ----
93
+ if (rule.pattern && !(actual === 'string' && rule.pattern.test(value))) {
94
+ return [message ?? fillTemplate('${label}格式不正确', label)];
95
+ }
96
+ // ---- enum ----
97
+ if (rule.enum && !rule.enum.includes(value)) {
98
+ return [message ?? `${label}必须是 [${rule.enum.join(' / ')}] 中的一个`];
99
+ }
100
+ return errors;
101
+ }
102
+ /**
103
+ * 校验一个字段:按规则顺序执行,收集全部错误(antd 展示所有错误文案,这里保持一致)
104
+ */
105
+ export async function validateValue(value, rules, name, label) {
106
+ if (!rules?.length)
107
+ return [];
108
+ const displayName = label ?? (typeof name === 'string' || typeof name === 'number' ? String(name) : namePathLabel(name));
109
+ const allErrors = [];
110
+ for (const rule of rules) {
111
+ const errs = await validateRule(rule, value, { label: displayName, required: !!rule.required });
112
+ allErrors.push(...errs);
113
+ }
114
+ return allErrors;
115
+ }
116
+ function namePathLabel(name) {
117
+ return Array.isArray(name) ? name.map(String).join('.') : String(name);
118
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * XFormPro 内部字段触发器:select / datePicker 这类"弹层控件"的触发区。
3
+ *
4
+ * 与 XInput 同视觉(浅灰底、发丝边框、右侧 ›),点击由外部接管打开弹层。
5
+ */
6
+ import React from 'react';
7
+ export interface FieldTriggerProps {
8
+ /** 展示文本(已选值/已选 label;空串或 undefined 显示 placeholder) */
9
+ text?: string;
10
+ placeholder?: string;
11
+ onPress: () => void;
12
+ disabled?: boolean;
13
+ }
14
+ export declare function FieldTrigger({ text, placeholder, onPress, disabled }: FieldTriggerProps): React.JSX.Element;