@xbeeant/form-engine-react 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -0
- package/dist/cjs/components/FormController.cjs +1 -0
- package/dist/cjs/components/NexusField.cjs +1 -0
- package/dist/cjs/components/NexusForm.cjs +1 -0
- package/dist/cjs/components/NexusFormProvider.cjs +1 -0
- package/dist/cjs/components/NexusLayout.cjs +1 -0
- package/dist/cjs/components/NexusObject.cjs +1 -0
- package/dist/cjs/contexts/FieldInheritContext.cjs +1 -0
- package/dist/cjs/contexts/GridContext.cjs +1 -0
- package/dist/cjs/contexts/LayoutConfigContext.cjs +1 -0
- package/dist/cjs/contexts/NexusContext.cjs +1 -0
- package/dist/cjs/hooks/index.cjs +1 -0
- package/dist/cjs/hooks/useEngine.cjs +1 -0
- package/dist/cjs/hooks/useFieldState.cjs +1 -0
- package/dist/cjs/hooks/useFieldValidator.cjs +1 -0
- package/dist/cjs/hooks/useFieldValue.cjs +1 -0
- package/dist/cjs/hooks/useForm.cjs +1 -0
- package/dist/cjs/hooks/useFormConfig.cjs +1 -0
- package/dist/cjs/hooks/useFormData.cjs +1 -0
- package/dist/cjs/hooks/useNexusContext.cjs +1 -0
- package/dist/cjs/hooks/useWatch.cjs +1 -0
- package/dist/cjs/hooks/useWatchAll.cjs +1 -0
- package/dist/cjs/hooks/useWatchMultiple.cjs +1 -0
- package/dist/cjs/hooks/useWatchState.cjs +1 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/utils/renderTreeNode.cjs +1 -0
- package/dist/cjs/utils/resolveColSpan.cjs +1 -0
- package/dist/es/components/FormController.d.ts +71 -0
- package/dist/es/components/FormController.js +153 -0
- package/dist/es/components/NexusField.d.ts +9 -0
- package/dist/es/components/NexusField.js +98 -0
- package/dist/es/components/NexusForm.d.ts +77 -0
- package/dist/es/components/NexusForm.js +120 -0
- package/dist/es/components/NexusFormProvider.d.ts +12 -0
- package/dist/es/components/NexusFormProvider.js +18 -0
- package/dist/es/components/NexusLayout.d.ts +9 -0
- package/dist/es/components/NexusLayout.js +48 -0
- package/dist/es/components/NexusObject.d.ts +16 -0
- package/dist/es/components/NexusObject.js +64 -0
- package/dist/es/contexts/FieldInheritContext.d.ts +18 -0
- package/dist/es/contexts/FieldInheritContext.js +5 -0
- package/dist/es/contexts/GridContext.d.ts +5 -0
- package/dist/es/contexts/GridContext.js +5 -0
- package/dist/es/contexts/LayoutConfigContext.d.ts +4 -0
- package/dist/es/contexts/LayoutConfigContext.js +5 -0
- package/dist/es/contexts/NexusContext.d.ts +27 -0
- package/dist/es/contexts/NexusContext.js +12 -0
- package/dist/es/hooks/index.d.ts +11 -0
- package/dist/es/hooks/index.js +24 -0
- package/dist/es/hooks/useEngine.d.ts +2 -0
- package/dist/es/hooks/useEngine.js +8 -0
- package/dist/es/hooks/useFieldState.d.ts +7 -0
- package/dist/es/hooks/useFieldState.js +13 -0
- package/dist/es/hooks/useFieldValidator.d.ts +40 -0
- package/dist/es/hooks/useFieldValidator.js +35 -0
- package/dist/es/hooks/useFieldValue.d.ts +6 -0
- package/dist/es/hooks/useFieldValue.js +13 -0
- package/dist/es/hooks/useForm.d.ts +6 -0
- package/dist/es/hooks/useForm.js +9 -0
- package/dist/es/hooks/useFormConfig.d.ts +5 -0
- package/dist/es/hooks/useFormConfig.js +8 -0
- package/dist/es/hooks/useFormData.d.ts +6 -0
- package/dist/es/hooks/useFormData.js +13 -0
- package/dist/es/hooks/useNexusContext.d.ts +11 -0
- package/dist/es/hooks/useNexusContext.js +12 -0
- package/dist/es/hooks/useWatch.d.ts +25 -0
- package/dist/es/hooks/useWatch.js +20 -0
- package/dist/es/hooks/useWatchAll.d.ts +19 -0
- package/dist/es/hooks/useWatchAll.js +29 -0
- package/dist/es/hooks/useWatchMultiple.d.ts +20 -0
- package/dist/es/hooks/useWatchMultiple.js +29 -0
- package/dist/es/hooks/useWatchState.d.ts +18 -0
- package/dist/es/hooks/useWatchState.js +19 -0
- package/dist/es/index.d.ts +24 -0
- package/dist/es/index.js +42 -0
- package/dist/es/utils/renderTreeNode.d.ts +6 -0
- package/dist/es/utils/renderTreeNode.js +17 -0
- package/dist/es/utils/resolveColSpan.d.ts +5 -0
- package/dist/es/utils/resolveColSpan.js +9 -0
- package/dist/umd/index.umd.cjs +1 -0
- package/package.json +75 -0
- package/src/components/FormController.ts +271 -0
- package/src/components/NexusField.tsx +182 -0
- package/src/components/NexusForm.tsx +290 -0
- package/src/components/NexusFormProvider.tsx +28 -0
- package/src/components/NexusLayout.tsx +81 -0
- package/src/components/NexusObject.tsx +93 -0
- package/src/contexts/FieldInheritContext.ts +21 -0
- package/src/contexts/GridContext.ts +8 -0
- package/src/contexts/LayoutConfigContext.ts +6 -0
- package/src/contexts/NexusContext.ts +41 -0
- package/src/hooks/index.ts +11 -0
- package/src/hooks/useEngine.ts +8 -0
- package/src/hooks/useFieldState.ts +21 -0
- package/src/hooks/useFieldValidator.ts +123 -0
- package/src/hooks/useFieldValue.ts +20 -0
- package/src/hooks/useForm.ts +15 -0
- package/src/hooks/useFormConfig.ts +10 -0
- package/src/hooks/useFormData.ts +20 -0
- package/src/hooks/useNexusContext.ts +24 -0
- package/src/hooks/useWatch.ts +83 -0
- package/src/hooks/useWatchAll.ts +77 -0
- package/src/hooks/useWatchMultiple.ts +86 -0
- package/src/hooks/useWatchState.ts +54 -0
- package/src/index.ts +30 -0
- package/src/utils/renderTreeNode.tsx +30 -0
- package/src/utils/resolveColSpan.ts +18 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FieldState,
|
|
3
|
+
NexusFormInstance,
|
|
4
|
+
NexusSchema,
|
|
5
|
+
} from '@xbeeant/form-engine';
|
|
6
|
+
import { AsyncValidatorPlugin, NexusEngine } from '@xbeeant/form-engine';
|
|
7
|
+
import type { RefObject } from 'react';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* FormController — 包裹 Engine,暴露 Form 实例 API
|
|
11
|
+
*/
|
|
12
|
+
export class FormController implements NexusFormInstance {
|
|
13
|
+
private engine: NexusEngine;
|
|
14
|
+
private formElementRef: RefObject<HTMLFormElement | null>;
|
|
15
|
+
/** 稳定的 getter:每次 submit 执行时从外部 ref 读取最新的 onFinish/onFinishFailed 回调 */
|
|
16
|
+
private getOnFinish: () => (
|
|
17
|
+
data: Record<string, unknown>,
|
|
18
|
+
) => void | Promise<void>;
|
|
19
|
+
private getOnFinishFailed: () => (errors: Map<string, string[]>) => void;
|
|
20
|
+
private removeHiddenData: boolean = true;
|
|
21
|
+
private watchers: Map<
|
|
22
|
+
string,
|
|
23
|
+
(value: unknown, allValues: Record<string, unknown>) => void
|
|
24
|
+
> = new Map();
|
|
25
|
+
private globalWatcher:
|
|
26
|
+
| ((
|
|
27
|
+
value: Record<string, unknown>,
|
|
28
|
+
allValues: Record<string, unknown>,
|
|
29
|
+
changedPath?: string,
|
|
30
|
+
) => void)
|
|
31
|
+
| null = null;
|
|
32
|
+
|
|
33
|
+
constructor(engine?: NexusEngine) {
|
|
34
|
+
this.engine = engine ?? new NexusEngine();
|
|
35
|
+
// 默认注入异步校验器插件:useFieldValidator / registerFieldValidator
|
|
36
|
+
// 注册的异步校验器即可在字段值变化时被触发(防抖调度,与默认 'change' trigger 对齐)。
|
|
37
|
+
// 外部已注入同名插件时不重复注入(hasPlugin 幂等)。
|
|
38
|
+
if (!this.engine.hasPlugin('async-validator')) {
|
|
39
|
+
this.engine.use(new AsyncValidatorPlugin(this.engine));
|
|
40
|
+
}
|
|
41
|
+
this.formElementRef = {
|
|
42
|
+
current: null,
|
|
43
|
+
} as RefObject<HTMLFormElement | null>;
|
|
44
|
+
this.getOnFinish = () => () => {};
|
|
45
|
+
this.getOnFinishFailed = () => () => {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 内部:首次绑定表单 DOM + 回调 getter(由 NexusForm 在挂载时调用一次) */
|
|
49
|
+
_bind(
|
|
50
|
+
formEl: HTMLFormElement | null,
|
|
51
|
+
getOnFinish: () => (data: Record<string, unknown>) => void | Promise<void>,
|
|
52
|
+
getOnFinishFailed: () => (errors: Map<string, string[]>) => void,
|
|
53
|
+
): void {
|
|
54
|
+
(this.formElementRef as { current: HTMLFormElement | null }).current =
|
|
55
|
+
formEl;
|
|
56
|
+
this.getOnFinish = getOnFinish;
|
|
57
|
+
this.getOnFinishFailed = getOnFinishFailed;
|
|
58
|
+
// 注册值变更回调到 Engine,只需一次(回调闭包引用了稳定的实例)
|
|
59
|
+
this.engine.registerOnFieldValueChange((path, value) =>
|
|
60
|
+
this._onFieldValueChange(path, value),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 内部:同步 watch / removeHiddenData 配置(由 NexusForm 在它们变化时调用) */
|
|
65
|
+
_syncConfig(config: {
|
|
66
|
+
removeHiddenData?: boolean;
|
|
67
|
+
watch?: {
|
|
68
|
+
[path: string]: (
|
|
69
|
+
value: unknown,
|
|
70
|
+
allValues: Record<string, unknown>,
|
|
71
|
+
changedPath?: string,
|
|
72
|
+
) => void;
|
|
73
|
+
};
|
|
74
|
+
}): void {
|
|
75
|
+
if (config.removeHiddenData !== undefined) {
|
|
76
|
+
this.removeHiddenData = config.removeHiddenData;
|
|
77
|
+
}
|
|
78
|
+
if (config.watch) {
|
|
79
|
+
this.watchers.clear();
|
|
80
|
+
this.globalWatcher = null;
|
|
81
|
+
for (const [path, fn] of Object.entries(config.watch)) {
|
|
82
|
+
if (path === '#') {
|
|
83
|
+
this.globalWatcher = fn;
|
|
84
|
+
} else {
|
|
85
|
+
this.watchers.set(path, fn);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 内部:值变更时调用(由 Engine 通知) */
|
|
92
|
+
_onFieldValueChange(path: string, value: unknown): void {
|
|
93
|
+
const allValues = this.engine.getFormData();
|
|
94
|
+
const globalData = this.removeHiddenData
|
|
95
|
+
? allValues
|
|
96
|
+
: this.engine.getAllFormData();
|
|
97
|
+
|
|
98
|
+
// 全局 watcher(# 监听所有字段变化,value 即为全部表单值;
|
|
99
|
+
// 第三参携带本次实际变更的字段路径,供清空值场景区分「未赋值默认值」与「用户主动清空」)
|
|
100
|
+
if (this.globalWatcher) {
|
|
101
|
+
this.globalWatcher(globalData, globalData, path);
|
|
102
|
+
}
|
|
103
|
+
// 路径匹配的 watcher
|
|
104
|
+
const fn = this.watchers.get(path);
|
|
105
|
+
if (fn) {
|
|
106
|
+
fn(value, globalData);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 内部:获取 Engine 实例 */
|
|
111
|
+
_getEngine(): NexusEngine {
|
|
112
|
+
return this.engine;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async submit(): Promise<void> {
|
|
116
|
+
const errors = await this.engine.validate();
|
|
117
|
+
if (errors.size > 0) {
|
|
118
|
+
this.focusFirstError(errors);
|
|
119
|
+
this.getOnFinishFailed()?.(errors);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const formData = this.removeHiddenData
|
|
123
|
+
? this.engine.getFormData()
|
|
124
|
+
: this.engine.getAllFormData();
|
|
125
|
+
await this.getOnFinish()?.(formData);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 定位到第一个校验失败的字段:按 DOM 渲染顺序查找(保证视觉上的"第一个"),
|
|
130
|
+
* 滚动入视并聚焦其内部可交互控件。
|
|
131
|
+
*/
|
|
132
|
+
private focusFirstError(errors: Map<string, string[]>): void {
|
|
133
|
+
const formEl = this.formElementRef.current;
|
|
134
|
+
if (!formEl || errors.size === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const errorPaths = new Set(errors.keys());
|
|
139
|
+
|
|
140
|
+
const focus = () => {
|
|
141
|
+
const fieldEls =
|
|
142
|
+
formEl.querySelectorAll<HTMLElement>('[data-nexus-field]');
|
|
143
|
+
for (const el of Array.from(fieldEls)) {
|
|
144
|
+
const path = el.getAttribute('data-nexus-field');
|
|
145
|
+
if (!path || !errorPaths.has(path)) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
150
|
+
const focusable = el.querySelector<HTMLElement>(
|
|
151
|
+
'input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
152
|
+
);
|
|
153
|
+
focusable?.focus();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// 等待 React 将错误态刷到 DOM 后再定位,保证错误提示已可见
|
|
159
|
+
requestAnimationFrame(focus);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
resetFields(): void {
|
|
163
|
+
this.engine.reset();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
setErrorFields(errors: Array<{ path: string; errors: string[] }>): void {
|
|
167
|
+
this.engine.setErrorFields(errors);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setValues(values: Record<string, unknown>): void {
|
|
171
|
+
this.engine.setFieldValues(values);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
setValueByPath(path: string, value: unknown): void {
|
|
175
|
+
this.engine.setFieldValue(path, value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
setSchemaByPath(path: string, patch: Record<string, unknown>): void {
|
|
179
|
+
this.engine.setSchemaByPath(path, patch);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
setSchema(schema: NexusSchema): void {
|
|
183
|
+
this.engine.setSchema(schema);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getValues(paths?: string[]): Record<string, unknown> {
|
|
187
|
+
return this.engine.getFormData(paths);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
getHiddenValues(): Record<string, unknown> {
|
|
191
|
+
return this.engine.getHiddenValues();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 获取所有字段值(含 hidden) */
|
|
195
|
+
getAllValues(): Record<string, unknown> {
|
|
196
|
+
return this.engine.getAllFormData();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getValueByPath(path: string): unknown {
|
|
200
|
+
return this.engine.getFieldValue(path);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 注册字段校验逻辑
|
|
205
|
+
* @param path 字段路径(如 'username')
|
|
206
|
+
* @param validator 校验函数,返回错误消息数组(空数组表示通过)
|
|
207
|
+
*/
|
|
208
|
+
registerValidator(
|
|
209
|
+
path: string,
|
|
210
|
+
validator: (
|
|
211
|
+
value: unknown,
|
|
212
|
+
formData: Record<string, unknown>,
|
|
213
|
+
) => string[] | Promise<string[]>,
|
|
214
|
+
): void {
|
|
215
|
+
// 注册到 Engine(validate 与 实时校验 统一由 Engine 执行)
|
|
216
|
+
this.engine.registerFieldValidator(path, validator);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 注销字段校验逻辑(按函数引用移除)
|
|
221
|
+
* 与 registerValidator 配对;widget 组件卸载时清理,避免校验器累积
|
|
222
|
+
*/
|
|
223
|
+
unregisterValidator(
|
|
224
|
+
path: string,
|
|
225
|
+
validator: (
|
|
226
|
+
value: unknown,
|
|
227
|
+
formData: Record<string, unknown>,
|
|
228
|
+
) => string[] | Promise<string[]>,
|
|
229
|
+
): void {
|
|
230
|
+
this.engine.unregisterFieldValidator(path, validator);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 实时重校验指定字段(同步)
|
|
235
|
+
* 供 widget 组件内部状态变化(非字段值变化)时主动刷新错误态
|
|
236
|
+
*/
|
|
237
|
+
revalidateField(path: string): void {
|
|
238
|
+
this.engine.validateField(path, { trigger: 'change' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getSchema(): NexusSchema | null {
|
|
242
|
+
return this.engine.getSchema();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
removeErrorField(path: string): void {
|
|
246
|
+
this.engine.removeErrorField(path);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
scrollToPath(path: string): void {
|
|
250
|
+
const el = this.formElementRef.current?.querySelector(
|
|
251
|
+
`[data-nexus-field="${path}"]`,
|
|
252
|
+
);
|
|
253
|
+
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
getFieldError(path: string): string[] {
|
|
257
|
+
return this.engine.getFieldError(path);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
getFieldsError(): Map<string, string[]> {
|
|
261
|
+
return this.engine.getFieldsError();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
validateFields(paths?: string[]): Promise<Map<string, string[]>> {
|
|
265
|
+
return this.engine.validate(paths);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
getFieldState(path: string): FieldState | undefined {
|
|
269
|
+
return this.engine.getFieldState(path);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { CSSProperties, FocusEvent } from 'react';
|
|
2
|
+
import { useCallback, useContext, useMemo, useSyncExternalStore } from 'react';
|
|
3
|
+
import { FieldInheritContext } from '../contexts/FieldInheritContext';
|
|
4
|
+
import { GridContext } from '../contexts/GridContext';
|
|
5
|
+
import { LayoutConfigContext } from '../contexts/LayoutConfigContext';
|
|
6
|
+
import { useNexusContext } from '../contexts/NexusContext';
|
|
7
|
+
import { resolveColSpan } from '../utils/resolveColSpan';
|
|
8
|
+
|
|
9
|
+
interface NexusFieldProps {
|
|
10
|
+
dataPath: string;
|
|
11
|
+
layoutKey: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* NexusField — 单个字段渲染器
|
|
16
|
+
*/
|
|
17
|
+
export function NexusField({ dataPath, layoutKey }: NexusFieldProps) {
|
|
18
|
+
const { engine, config, form } = useNexusContext();
|
|
19
|
+
// 按路径精准订阅:仅该字段版本变化时重渲染(reaction 影响其他字段不会触发本组件)
|
|
20
|
+
// 第三个参数 getServerSnapshot 与 getSnapshot 一致(引擎状态同步,SSR 必需)
|
|
21
|
+
useSyncExternalStore(
|
|
22
|
+
(onStoreChange) => engine.subscribeField(dataPath, onStoreChange),
|
|
23
|
+
() => engine.getFieldVersion(dataPath),
|
|
24
|
+
() => engine.getFieldVersion(dataPath),
|
|
25
|
+
);
|
|
26
|
+
const state = engine.getFieldState(dataPath);
|
|
27
|
+
// GridContext 必须在所有 early return 之前调用,否则会破坏 Hooks 调用顺序
|
|
28
|
+
const gridCtx = useContext(GridContext);
|
|
29
|
+
const layoutConfig = useContext(LayoutConfigContext);
|
|
30
|
+
// 祖先对象容器(NexusObject)下发的继承属性:visible=false 时子树整体隐藏
|
|
31
|
+
const inherit = useContext(FieldInheritContext);
|
|
32
|
+
|
|
33
|
+
const handleChange = useCallback(
|
|
34
|
+
(value: unknown) => {
|
|
35
|
+
engine.setFieldValue(dataPath, value);
|
|
36
|
+
},
|
|
37
|
+
[engine, dataPath],
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// 失焦触发 blur 规则校验(trigger: 'blur'):
|
|
41
|
+
// React onBlur 冒泡(focusout 语义),包裹层统一处理内部控件失焦;
|
|
42
|
+
// 焦点仍在字段内部(如 dateRange 双输入框间切换)时跳过。
|
|
43
|
+
const handleBlur = useCallback(
|
|
44
|
+
(e: FocusEvent<HTMLDivElement>) => {
|
|
45
|
+
if (e.currentTarget.contains(e.relatedTarget as Node)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
engine.validateField(dataPath, { trigger: 'blur' });
|
|
49
|
+
},
|
|
50
|
+
[engine, dataPath],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// 从 enum + enumNames 构建选项(x-render 对齐)。
|
|
54
|
+
// meta/props 引用在状态更新时保持稳定,useMemo 避免每次渲染重建数组(破坏子组件 memo)。
|
|
55
|
+
// 必须在所有 early return 之前调用(Hooks 顺序规则),state 未定义时安全降级
|
|
56
|
+
const options = useMemo(
|
|
57
|
+
() =>
|
|
58
|
+
state?.meta.enum
|
|
59
|
+
? state.meta.enum.map((value: any, index: number) => ({
|
|
60
|
+
value,
|
|
61
|
+
label: state.meta.enumNames?.[index] ?? String(value),
|
|
62
|
+
}))
|
|
63
|
+
: (state?.props.options as
|
|
64
|
+
| Array<{ label: string; value: unknown } | string | number>
|
|
65
|
+
| undefined),
|
|
66
|
+
[state?.meta.enum, state?.meta.enumNames, state?.props.options],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// 从 reactions 依赖构建 dependValues,供 widget 获取关联字段值。
|
|
70
|
+
// reactions 引用稳定,值在 memo 执行时读取;避免每次渲染新建对象
|
|
71
|
+
const dependValues = useMemo(() => {
|
|
72
|
+
const values: Record<string, unknown> = {};
|
|
73
|
+
if (state?.reactions) {
|
|
74
|
+
for (const reaction of state.reactions) {
|
|
75
|
+
if (reaction.dependencies) {
|
|
76
|
+
for (const dep of reaction.dependencies) {
|
|
77
|
+
values[dep] = engine.getFieldValue(dep);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return values;
|
|
83
|
+
}, [state?.reactions, engine]);
|
|
84
|
+
|
|
85
|
+
if (!state) {
|
|
86
|
+
// 仅当引擎已初始化(version > 0)但字段仍未找到时才发出警告
|
|
87
|
+
// 初始化过程中的短暂空状态不应报警
|
|
88
|
+
if (engine.getSnapshot() > 0) {
|
|
89
|
+
console.warn(`[NexusField] Field not found: ${dataPath}`);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 祖先对象容器隐藏 → 子树整体不可见(与字段自身 visible 合并判断)
|
|
95
|
+
if (inherit.visible === false || !state.visible) {
|
|
96
|
+
// 如果父布局节点配置了 removeHidden,则不渲染占位符(移除以防止栅格塌陷)
|
|
97
|
+
if (layoutConfig.removeHidden === true) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
// 默认行为:渲染 display:none 占位符以保持布局
|
|
101
|
+
return <div className='hidden' data-nexus-hidden={dataPath} />;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 从 enum + enumNames 构建选项(x-render 对齐)
|
|
105
|
+
const readOnly =
|
|
106
|
+
config.readOnly || inherit.readOnly === true || state.readOnly;
|
|
107
|
+
// 对象容器继承 disabled(父级激活时优先),与字段级 disabled 合并
|
|
108
|
+
const disabled = inherit.disabled === true || state.disabled;
|
|
109
|
+
|
|
110
|
+
// readOnlyWidget:指定 readOnly 生效时切换使用的渲染 widget(x-render readOnlyWidget 对齐)。
|
|
111
|
+
// - 配置了 readOnlyWidget 且字段为只读时,切换渲染该 widget(readOnly 一并透传,
|
|
112
|
+
// widget 按自身逻辑决定只读展示形态,如 treeSelect 的 readOnly 回显);
|
|
113
|
+
// - 未配置时 readOnly 原样透传给 widget,由 widget 自身决定只读形态
|
|
114
|
+
// (antd 原生 readOnly,或内置 widget 的 ReadOnlyDisplay 文本回退)。
|
|
115
|
+
const wantReadOnlyWidget = readOnly && !!state.meta.readOnlyWidget;
|
|
116
|
+
const widgetName = wantReadOnlyWidget
|
|
117
|
+
? state.meta.readOnlyWidget!
|
|
118
|
+
: state.meta.widget;
|
|
119
|
+
/** 获取UI组件库 进行渲染 **/
|
|
120
|
+
let Widget = engine.getWidget(widgetName);
|
|
121
|
+
// readOnlyWidget 未注册时优雅降级:退回原 widget,沿用现有只读渲染方式
|
|
122
|
+
if (!Widget && wantReadOnlyWidget) {
|
|
123
|
+
Widget = engine.getWidget(state.meta.widget);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!Widget) {
|
|
127
|
+
return (
|
|
128
|
+
<div className='text-xs text-red-500' data-nexus-field={dataPath}>
|
|
129
|
+
⚠️ Widget "{state.meta.widget}" 未注册 (path: {dataPath})
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 字段级配置优先于表单级配置
|
|
135
|
+
const fieldDisplayType = state.meta.displayType ?? config.displayType;
|
|
136
|
+
const fieldLabelWidth = state.meta.labelWidth ?? config.labelWidth;
|
|
137
|
+
const fieldColumn = state.meta.column ?? config.column;
|
|
138
|
+
|
|
139
|
+
// 布局属性作用于 NexusField 包装层而非 DOM 控件
|
|
140
|
+
// - column(fieldColumn):字段内部子元素分列数(如 checkboxes/radio),传给 Widget
|
|
141
|
+
// - colSpan:在父 Grid 中横跨多少列(tailwind 风格:gridColumn: span N)
|
|
142
|
+
// - width:在父 Flex 布局中自身宽度(百分比或固定值),flexShrink:0 防压缩
|
|
143
|
+
const effectiveColSpan = resolveColSpan(state.meta.colSpan, gridCtx);
|
|
144
|
+
const wrapperStyle: CSSProperties = {
|
|
145
|
+
...(state.meta.width ? { width: state.meta.width, flexShrink: 0 } : {}),
|
|
146
|
+
...(effectiveColSpan ? { gridColumn: `span ${effectiveColSpan}` } : {}),
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
data-nexus-field={dataPath}
|
|
152
|
+
onBlur={handleBlur}
|
|
153
|
+
style={Object.keys(wrapperStyle).length > 0 ? wrapperStyle : undefined}
|
|
154
|
+
>
|
|
155
|
+
<Widget
|
|
156
|
+
key={layoutKey}
|
|
157
|
+
dataPath={dataPath}
|
|
158
|
+
path={dataPath}
|
|
159
|
+
value={state.value}
|
|
160
|
+
onChange={handleChange}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
readOnly={readOnly}
|
|
163
|
+
loading={state.loading}
|
|
164
|
+
required={state.required}
|
|
165
|
+
title={state.meta.title}
|
|
166
|
+
description={state.meta.description}
|
|
167
|
+
placeholder={state.meta.placeholder}
|
|
168
|
+
label={state.meta.label}
|
|
169
|
+
options={options}
|
|
170
|
+
errors={state.errors}
|
|
171
|
+
extra={state.meta.extra}
|
|
172
|
+
displayType={fieldDisplayType}
|
|
173
|
+
labelWidth={fieldLabelWidth}
|
|
174
|
+
column={fieldColumn}
|
|
175
|
+
form={form}
|
|
176
|
+
dependValues={dependValues}
|
|
177
|
+
items={state.meta.items}
|
|
178
|
+
{...state.props}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|