@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.
Files changed (107) hide show
  1. package/README.md +69 -0
  2. package/dist/cjs/components/FormController.cjs +1 -0
  3. package/dist/cjs/components/NexusField.cjs +1 -0
  4. package/dist/cjs/components/NexusForm.cjs +1 -0
  5. package/dist/cjs/components/NexusFormProvider.cjs +1 -0
  6. package/dist/cjs/components/NexusLayout.cjs +1 -0
  7. package/dist/cjs/components/NexusObject.cjs +1 -0
  8. package/dist/cjs/contexts/FieldInheritContext.cjs +1 -0
  9. package/dist/cjs/contexts/GridContext.cjs +1 -0
  10. package/dist/cjs/contexts/LayoutConfigContext.cjs +1 -0
  11. package/dist/cjs/contexts/NexusContext.cjs +1 -0
  12. package/dist/cjs/hooks/index.cjs +1 -0
  13. package/dist/cjs/hooks/useEngine.cjs +1 -0
  14. package/dist/cjs/hooks/useFieldState.cjs +1 -0
  15. package/dist/cjs/hooks/useFieldValidator.cjs +1 -0
  16. package/dist/cjs/hooks/useFieldValue.cjs +1 -0
  17. package/dist/cjs/hooks/useForm.cjs +1 -0
  18. package/dist/cjs/hooks/useFormConfig.cjs +1 -0
  19. package/dist/cjs/hooks/useFormData.cjs +1 -0
  20. package/dist/cjs/hooks/useNexusContext.cjs +1 -0
  21. package/dist/cjs/hooks/useWatch.cjs +1 -0
  22. package/dist/cjs/hooks/useWatchAll.cjs +1 -0
  23. package/dist/cjs/hooks/useWatchMultiple.cjs +1 -0
  24. package/dist/cjs/hooks/useWatchState.cjs +1 -0
  25. package/dist/cjs/index.cjs +1 -0
  26. package/dist/cjs/utils/renderTreeNode.cjs +1 -0
  27. package/dist/cjs/utils/resolveColSpan.cjs +1 -0
  28. package/dist/es/components/FormController.d.ts +71 -0
  29. package/dist/es/components/FormController.js +153 -0
  30. package/dist/es/components/NexusField.d.ts +9 -0
  31. package/dist/es/components/NexusField.js +98 -0
  32. package/dist/es/components/NexusForm.d.ts +77 -0
  33. package/dist/es/components/NexusForm.js +120 -0
  34. package/dist/es/components/NexusFormProvider.d.ts +12 -0
  35. package/dist/es/components/NexusFormProvider.js +18 -0
  36. package/dist/es/components/NexusLayout.d.ts +9 -0
  37. package/dist/es/components/NexusLayout.js +48 -0
  38. package/dist/es/components/NexusObject.d.ts +16 -0
  39. package/dist/es/components/NexusObject.js +64 -0
  40. package/dist/es/contexts/FieldInheritContext.d.ts +18 -0
  41. package/dist/es/contexts/FieldInheritContext.js +5 -0
  42. package/dist/es/contexts/GridContext.d.ts +5 -0
  43. package/dist/es/contexts/GridContext.js +5 -0
  44. package/dist/es/contexts/LayoutConfigContext.d.ts +4 -0
  45. package/dist/es/contexts/LayoutConfigContext.js +5 -0
  46. package/dist/es/contexts/NexusContext.d.ts +27 -0
  47. package/dist/es/contexts/NexusContext.js +12 -0
  48. package/dist/es/hooks/index.d.ts +11 -0
  49. package/dist/es/hooks/index.js +24 -0
  50. package/dist/es/hooks/useEngine.d.ts +2 -0
  51. package/dist/es/hooks/useEngine.js +8 -0
  52. package/dist/es/hooks/useFieldState.d.ts +7 -0
  53. package/dist/es/hooks/useFieldState.js +13 -0
  54. package/dist/es/hooks/useFieldValidator.d.ts +40 -0
  55. package/dist/es/hooks/useFieldValidator.js +35 -0
  56. package/dist/es/hooks/useFieldValue.d.ts +6 -0
  57. package/dist/es/hooks/useFieldValue.js +13 -0
  58. package/dist/es/hooks/useForm.d.ts +6 -0
  59. package/dist/es/hooks/useForm.js +9 -0
  60. package/dist/es/hooks/useFormConfig.d.ts +5 -0
  61. package/dist/es/hooks/useFormConfig.js +8 -0
  62. package/dist/es/hooks/useFormData.d.ts +6 -0
  63. package/dist/es/hooks/useFormData.js +13 -0
  64. package/dist/es/hooks/useNexusContext.d.ts +11 -0
  65. package/dist/es/hooks/useNexusContext.js +12 -0
  66. package/dist/es/hooks/useWatch.d.ts +25 -0
  67. package/dist/es/hooks/useWatch.js +20 -0
  68. package/dist/es/hooks/useWatchAll.d.ts +19 -0
  69. package/dist/es/hooks/useWatchAll.js +29 -0
  70. package/dist/es/hooks/useWatchMultiple.d.ts +20 -0
  71. package/dist/es/hooks/useWatchMultiple.js +29 -0
  72. package/dist/es/hooks/useWatchState.d.ts +18 -0
  73. package/dist/es/hooks/useWatchState.js +19 -0
  74. package/dist/es/index.d.ts +24 -0
  75. package/dist/es/index.js +42 -0
  76. package/dist/es/utils/renderTreeNode.d.ts +6 -0
  77. package/dist/es/utils/renderTreeNode.js +17 -0
  78. package/dist/es/utils/resolveColSpan.d.ts +5 -0
  79. package/dist/es/utils/resolveColSpan.js +9 -0
  80. package/dist/umd/index.umd.cjs +1 -0
  81. package/package.json +75 -0
  82. package/src/components/FormController.ts +271 -0
  83. package/src/components/NexusField.tsx +182 -0
  84. package/src/components/NexusForm.tsx +290 -0
  85. package/src/components/NexusFormProvider.tsx +28 -0
  86. package/src/components/NexusLayout.tsx +81 -0
  87. package/src/components/NexusObject.tsx +93 -0
  88. package/src/contexts/FieldInheritContext.ts +21 -0
  89. package/src/contexts/GridContext.ts +8 -0
  90. package/src/contexts/LayoutConfigContext.ts +6 -0
  91. package/src/contexts/NexusContext.ts +41 -0
  92. package/src/hooks/index.ts +11 -0
  93. package/src/hooks/useEngine.ts +8 -0
  94. package/src/hooks/useFieldState.ts +21 -0
  95. package/src/hooks/useFieldValidator.ts +123 -0
  96. package/src/hooks/useFieldValue.ts +20 -0
  97. package/src/hooks/useForm.ts +15 -0
  98. package/src/hooks/useFormConfig.ts +10 -0
  99. package/src/hooks/useFormData.ts +20 -0
  100. package/src/hooks/useNexusContext.ts +24 -0
  101. package/src/hooks/useWatch.ts +83 -0
  102. package/src/hooks/useWatchAll.ts +77 -0
  103. package/src/hooks/useWatchMultiple.ts +86 -0
  104. package/src/hooks/useWatchState.ts +54 -0
  105. package/src/index.ts +30 -0
  106. package/src/utils/renderTreeNode.tsx +30 -0
  107. package/src/utils/resolveColSpan.ts +18 -0
@@ -0,0 +1,290 @@
1
+ import type { NexusSchema } from '@xbeeant/form-engine';
2
+ import type { CSSProperties, ReactNode, SubmitEvent } from 'react';
3
+ import {
4
+ useCallback,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ useSyncExternalStore,
9
+ } from 'react';
10
+
11
+ import { renderTreeNode } from '../utils/renderTreeNode';
12
+ import type { FormController } from './FormController';
13
+ import { NexusFormProvider } from './NexusFormProvider';
14
+
15
+ // ────────────────────────────────────────────────────────────────────────────
16
+ // Form 布局配置
17
+ // ────────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface NexusFormConfig {
20
+ /** label 列配置(由 ui 层 Form.Item 消费) */
21
+ labelCol?: Record<string, unknown>;
22
+ /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */
23
+ labelWidth?: number | string;
24
+ /** 是否显示冒号 */
25
+ colon?: boolean | ReactNode;
26
+ /** 是否显示 label(默认 true) */
27
+ label?: boolean;
28
+ /** 表单布局方向 */
29
+ displayType?: 'row' | 'column' | 'inline';
30
+ /** 整个表单只读,所有字段以文本展示 */
31
+ readOnly?: boolean;
32
+ /** 表单每行显示多少列 */
33
+ column?: number;
34
+ }
35
+
36
+ export interface NexusFormProps {
37
+ /** Form 实例,由 useForm() 创建 */
38
+ form: FormController;
39
+ /** Schema 定义 */
40
+ schema?: NexusSchema;
41
+ /** 初始值 */
42
+ initialValues?: Record<string, unknown>;
43
+ /** 额外注册的 widget(与已注册的合并) */
44
+ widgets?: Record<string, (props: any) => ReactNode>;
45
+ /** 额外注册的 layout */
46
+ layouts?: Record<string, (props: any) => ReactNode>;
47
+ /** 提交成功回调 */
48
+ onFinish?: (formData: Record<string, unknown>) => void | Promise<void>;
49
+ /** 校验失败回调 */
50
+ onFinishFailed?: (errors: Map<string, string[]>) => void;
51
+ /** 是否显示默认 footer(提交/重置按钮),或自定义 footer */
52
+ footer?: boolean | ReactNode;
53
+ /** 自定义类名 */
54
+ className?: string;
55
+ /** 自定义样式 */
56
+ style?: CSSProperties;
57
+ /** 子节点 */
58
+ children?: ReactNode;
59
+ /**
60
+ * 表单值变化监听
61
+ * - key 为字段路径(如 'username'),值为回调函数
62
+ * - 使用 '#' 作为 key 可监听所有字段变化
63
+ * - 回调参数为 (value, allValues);全局('#')监听额外提供第三参 changedPath,
64
+ * 即本次实际变更的字段路径(用于区分「未触碰的默认空值」与「用户主动清空」)
65
+ */
66
+ watch?: {
67
+ [path: string]: (
68
+ value: unknown,
69
+ allValues: Record<string, unknown>,
70
+ changedPath?: string,
71
+ ) => void;
72
+ };
73
+ /**
74
+ * 提交时是否移除 hidden 字段数据,默认 true
75
+ * - true: submit/getValues 不包含 hidden 字段
76
+ * - false: submit/getValues 包含所有字段(含 hidden)
77
+ */
78
+ removeHiddenData?: boolean;
79
+
80
+ // ── 表单布局配置 ──────────────────────────────────────────────────────
81
+ /** label 列配置(由 ui 层 Form.Item 消费) */
82
+ labelCol?: Record<string, unknown>;
83
+ /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */
84
+ labelWidth?: number | string;
85
+ /** 是否显示冒号 */
86
+ colon?: boolean | ReactNode;
87
+ /** 是否显示 label(默认 true) */
88
+ label?: boolean;
89
+ /** 表单布局方向:'row' = horizontal, 'column' = vertical, 'inline' = inline */
90
+ displayType?: 'row' | 'column' | 'inline';
91
+ /** 整个表单只读,所有字段以文本展示 */
92
+ readOnly?: boolean;
93
+ /** 表单每行显示多少列 */
94
+ column?: number;
95
+ }
96
+
97
+ function noop() {}
98
+
99
+ /**
100
+ * NexusForm — 顶层表单组件
101
+ */
102
+ export function NexusForm({
103
+ form,
104
+ schema,
105
+ initialValues,
106
+ widgets,
107
+ layouts,
108
+ onFinish,
109
+ onFinishFailed,
110
+ footer = false,
111
+ className,
112
+ style,
113
+ children,
114
+ labelCol,
115
+ labelWidth,
116
+ colon,
117
+ label,
118
+ displayType,
119
+ readOnly,
120
+ column,
121
+ watch,
122
+ removeHiddenData = true,
123
+ }: NexusFormProps) {
124
+ const engine = form._getEngine();
125
+ const formElRef = useRef<HTMLFormElement | null>(null);
126
+
127
+ // Schema 顶层配置作为默认值,props 优先级更高
128
+ const finalDisplayType = displayType ?? schema?.displayType ?? 'row';
129
+ const finalLabel = label ?? schema?.label ?? true;
130
+ const finalColon = colon ?? schema?.colon;
131
+ const finalLabelWidth = labelWidth ?? schema?.labelWidth;
132
+ const finalReadOnly = readOnly ?? schema?.readOnly ?? false;
133
+ const finalColumn = column ?? schema?.column;
134
+
135
+ // 注册额外 widgets / layouts(仅首次或引用变化时)
136
+ useEffect(() => {
137
+ if (widgets) {
138
+ engine.registerWidgets(widgets);
139
+ }
140
+ if (layouts) {
141
+ engine.registerLayouts(layouts);
142
+ }
143
+ }, [engine, widgets, layouts]);
144
+
145
+ // schema 变化时重新初始化
146
+ // 注意:initialValues 仅在首次挂载时使用,避免每次渲染都 re-init 导致
147
+ // 已有的 errors / 用户输入被重置。后续需要更新值请使用 form.setValues()。
148
+ const isFirstInitRef = useRef(true);
149
+ const initialValuesRef = useRef(initialValues);
150
+ useEffect(() => {
151
+ if (!schema) {
152
+ return;
153
+ }
154
+ if (isFirstInitRef.current) {
155
+ engine.init(schema, initialValuesRef.current);
156
+ isFirstInitRef.current = false;
157
+ } else {
158
+ // schema 变化:保留当前已填数据,而非重置为 initialValues
159
+ engine.init(schema, engine.getFormData());
160
+ }
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ }, [engine, schema]);
163
+
164
+ // 绑定 form controller
165
+ // 使用 ref 持有 onFinish / onFinishFailed,避免每次 re-render 都造成绑定逻辑重复执行
166
+ const onFinishRef =
167
+ useRef<(data: Record<string, unknown>) => void | Promise<void>>(noop);
168
+ const onFinishFailedRef =
169
+ useRef<(errors: Map<string, string[]>) => void>(noop);
170
+ onFinishRef.current = onFinish ?? noop;
171
+ onFinishFailedRef.current = onFinishFailed ?? noop;
172
+
173
+ // 只在挂载时绑定一次:传入「稳定的 getter」,让 FormController 在 submit 时读取最新回调
174
+ useEffect(() => {
175
+ form._bind(
176
+ formElRef.current,
177
+ () => onFinishRef.current,
178
+ () => onFinishFailedRef.current,
179
+ );
180
+ // eslint-disable-next-line react-hooks/exhaustive-deps
181
+ }, [form]);
182
+
183
+ // watch / removeHiddenData 变化时单独同步(不重置其他绑定)
184
+ useEffect(() => {
185
+ form._syncConfig({ removeHiddenData, watch });
186
+ }, [form, removeHiddenData, watch]);
187
+
188
+ const _version = useSyncExternalStore(
189
+ engine.subscribeStore,
190
+ engine.getSnapshot,
191
+ engine.getSnapshot,
192
+ );
193
+ // 依赖 _version:engine.init() / setSchema() 会 bump version,
194
+ // 需要在此后重新读取 renderTree(首次渲染时 engine 尚未 init,renderTree 为空)
195
+ // biome-ignore lint/correctness/useExhaustiveDependencies: _version 是 renderTree 失效信号(engine 内部状态,静态分析不可见)
196
+ const renderTree = useMemo(() => engine.getRenderTree(), [engine, _version]);
197
+
198
+ const handleSubmit = useCallback(
199
+ async (e: SubmitEvent) => {
200
+ e.preventDefault();
201
+ await form.submit();
202
+ },
203
+ [form],
204
+ );
205
+
206
+ const handleReset = useCallback(() => {
207
+ form.resetFields();
208
+ }, [form]);
209
+
210
+ // footer 渲染
211
+ let footerNode: ReactNode = null;
212
+ if (footer === true) {
213
+ footerNode = (
214
+ <div className='mt-4'>
215
+ <button type='submit'>提交</button>{' '}
216
+ <button type='button' onClick={handleReset}>
217
+ 重置
218
+ </button>
219
+ </div>
220
+ );
221
+ } else if (footer) {
222
+ footerNode = footer;
223
+ }
224
+
225
+ // labelCol: 合并 labelWidth 快捷方式
226
+ const mergedLabelCol = useMemo(() => {
227
+ if (labelCol) {
228
+ return labelCol;
229
+ }
230
+ if (finalLabelWidth) {
231
+ return {
232
+ style: {
233
+ width:
234
+ typeof finalLabelWidth === 'number'
235
+ ? `${finalLabelWidth}px`
236
+ : finalLabelWidth,
237
+ },
238
+ };
239
+ }
240
+ return undefined;
241
+ }, [labelCol, finalLabelWidth]);
242
+
243
+ // NexusFormConfig 传递给 Context(由 ui 层消费,自行实现布局)
244
+ const formConfig = useMemo<NexusFormConfig>(
245
+ () => ({
246
+ labelCol: mergedLabelCol,
247
+ labelWidth: finalLabelWidth,
248
+ colon: finalColon,
249
+ label: finalLabel,
250
+ displayType: finalDisplayType,
251
+ readOnly: finalReadOnly,
252
+ column: finalColumn,
253
+ }),
254
+ [
255
+ mergedLabelCol,
256
+ finalLabelWidth,
257
+ finalColon,
258
+ finalLabel,
259
+ finalDisplayType,
260
+ finalReadOnly,
261
+ finalColumn,
262
+ ],
263
+ );
264
+
265
+ return (
266
+ <NexusFormProvider engine={engine} config={formConfig} form={form}>
267
+ <form
268
+ ref={formElRef}
269
+ onSubmit={handleSubmit}
270
+ className={className}
271
+ style={{
272
+ ...style,
273
+ // 当配置了 column 时,使用 CSS Grid 布局
274
+ ...(finalColumn && finalColumn > 1
275
+ ? {
276
+ display: 'grid',
277
+ gridTemplateColumns: `repeat(${finalColumn}, 1fr)`,
278
+ gap: '0 16px',
279
+ }
280
+ : {}),
281
+ }}
282
+ noValidate
283
+ >
284
+ {renderTree.map((node, index) => renderTreeNode(node, index))}
285
+ {!readOnly && footerNode}
286
+ {children}
287
+ </form>
288
+ </NexusFormProvider>
289
+ );
290
+ }
@@ -0,0 +1,28 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+ import type { ReactNode } from 'react';
3
+ import { useMemo } from 'react';
4
+ import { NexusContext } from '../contexts/NexusContext';
5
+ import type { FormController } from './FormController';
6
+ import type { NexusFormConfig } from './NexusForm';
7
+
8
+ interface NexusFormProviderProps {
9
+ engine: NexusEngine;
10
+ config: NexusFormConfig;
11
+ form: FormController;
12
+ children: ReactNode;
13
+ }
14
+
15
+ export function NexusFormProvider({
16
+ engine,
17
+ config,
18
+ form,
19
+ children,
20
+ }: NexusFormProviderProps) {
21
+ const value = useMemo(
22
+ () => ({ engine, config, form }),
23
+ [engine, config, form],
24
+ );
25
+ return (
26
+ <NexusContext.Provider value={value}>{children}</NexusContext.Provider>
27
+ );
28
+ }
@@ -0,0 +1,81 @@
1
+ import type { RenderLayoutNode } from '@xbeeant/form-engine';
2
+ import type { CSSProperties } from 'react';
3
+ import { useContext, useMemo } from 'react';
4
+
5
+ import { GridContext } from '../contexts/GridContext';
6
+ import {
7
+ LayoutConfigContext,
8
+ type LayoutConfigContextValue,
9
+ } from '../contexts/LayoutConfigContext';
10
+ import { useNexusContext } from '../contexts/NexusContext';
11
+ import { renderTreeNode } from '../utils/renderTreeNode';
12
+ import { resolveColSpan } from '../utils/resolveColSpan';
13
+
14
+ interface NexusLayoutProps {
15
+ node: RenderLayoutNode;
16
+ }
17
+
18
+ /**
19
+ * NexusLayout — 布局容器渲染器
20
+ */
21
+ export function NexusLayout({ node }: NexusLayoutProps) {
22
+ const { engine } = useNexusContext();
23
+
24
+ const LayoutComponent = engine.getLayout(node.type);
25
+
26
+ const children = node.children.map((child, index) =>
27
+ renderTreeNode(child, index),
28
+ );
29
+
30
+ // 布局容器在父 Grid/Flex 中的跨列/宽度(与 NexusField wrapper 一致)
31
+ const gridCtx = useContext(GridContext);
32
+ const effectiveColSpan = resolveColSpan(node.props.colSpan, gridCtx);
33
+ const wrapperStyle: CSSProperties = {
34
+ ...(effectiveColSpan ? { gridColumn: `span ${effectiveColSpan}` } : {}),
35
+ ...(node.props.width ? { width: node.props.width, flexShrink: 0 } : {}),
36
+ };
37
+
38
+ const layoutConfigValue = useMemo<LayoutConfigContextValue>(
39
+ () => ({ removeHidden: node.props.removeHidden }),
40
+ [node.props.removeHidden],
41
+ );
42
+
43
+ if (!LayoutComponent) {
44
+ return (
45
+ <LayoutConfigContext.Provider value={layoutConfigValue}>
46
+ <div
47
+ data-nexus-layout={node.type}
48
+ className='mb-4'
49
+ style={
50
+ Object.keys(wrapperStyle).length > 0 ? wrapperStyle : undefined
51
+ }
52
+ >
53
+ {node.title && <div className='mb-2 font-bold'>{node.title}</div>}
54
+ {children}
55
+ </div>
56
+ </LayoutConfigContext.Provider>
57
+ );
58
+ }
59
+
60
+ // 剥离布局配置属性(displayType / labelWidth / colSpan / width 是布局配置,
61
+ // 不应透传到布局组件的 DOM 元素,否则触发 React unknown prop 警告)
62
+ const {
63
+ displayType: _dt,
64
+ labelWidth: _lw,
65
+ colSpan: _csp,
66
+ width: _w,
67
+ ...layoutProps
68
+ } = node.props;
69
+
70
+ return (
71
+ <LayoutConfigContext.Provider value={layoutConfigValue}>
72
+ <div
73
+ style={Object.keys(wrapperStyle).length > 0 ? wrapperStyle : undefined}
74
+ >
75
+ <LayoutComponent {...layoutProps} node={node} title={node.title}>
76
+ {children}
77
+ </LayoutComponent>
78
+ </div>
79
+ </LayoutConfigContext.Provider>
80
+ );
81
+ }
@@ -0,0 +1,93 @@
1
+ import type { RenderObjectNode } from '@xbeeant/form-engine';
2
+ import type { CSSProperties } from 'react';
3
+ import { useContext, useState, useSyncExternalStore } from 'react';
4
+
5
+ import { FieldInheritContext } from '../contexts/FieldInheritContext';
6
+ import { useNexusContext } from '../contexts/NexusContext';
7
+ import { renderTreeNode } from '../utils/renderTreeNode';
8
+
9
+ interface NexusObjectProps {
10
+ node: RenderObjectNode;
11
+ }
12
+
13
+ /**
14
+ * NexusObject — 数据对象容器(可折叠,支持 disabled/hidden/readOnly 属性配置)
15
+ *
16
+ * 点击 title 折叠/展开 children。
17
+ * 折叠时仅以 display:none 隐藏 children,并不销毁(字段状态与订阅保持存活)。
18
+ *
19
+ * 属性继承:容器自身(Schema 布尔值 / 表达式 / setFieldState)配置的
20
+ * disabled / readOnly / hidden 会经 FieldInheritContext 下发给子树,
21
+ * 子树中的字段继承对应状态(disabled/readOnly 仅父级激活时生效)。
22
+ */
23
+ export function NexusObject({ node }: NexusObjectProps) {
24
+ const { engine, config } = useNexusContext();
25
+ const parentInherit = useContext(FieldInheritContext);
26
+ // 按路径精准订阅:自身 disabled/readOnly/visible 变化时重渲染并下发新上下文
27
+ useSyncExternalStore(
28
+ (onStoreChange) => engine.subscribeField(node.dataPath, onStoreChange),
29
+ () => engine.getFieldVersion(node.dataPath),
30
+ () => engine.getFieldVersion(node.dataPath),
31
+ );
32
+ const state = engine.getFieldState(node.dataPath);
33
+ const [collapsed, setCollapsed] = useState(false);
34
+ const isGrid = (config.column ?? 1) > 1;
35
+ const gridStyle: CSSProperties = isGrid
36
+ ? { gridTemplateColumns: `repeat(${config.column}, 1fr)` }
37
+ : {};
38
+
39
+ // 合并继承属性:父级已激活的状态不可被当前容器覆盖;
40
+ // disabled/readOnly 仅携带 true(父级关闭状态不压制子级显式启用)
41
+ const inherit = {
42
+ disabled:
43
+ parentInherit.disabled ?? (state?.disabled === true ? true : undefined),
44
+ readOnly:
45
+ parentInherit.readOnly ?? (state?.readOnly === true ? true : undefined),
46
+ visible:
47
+ parentInherit.visible === false || state?.visible === false
48
+ ? false
49
+ : undefined,
50
+ };
51
+ const hidden = inherit.visible === false;
52
+
53
+ const toggleCollapsed = () => setCollapsed((prev) => !prev);
54
+
55
+ return (
56
+ <FieldInheritContext.Provider value={inherit}>
57
+ <div
58
+ data-nexus-object={node.dataPath}
59
+ className={`mb-4 ${hidden ? 'hidden' : ''}`}
60
+ >
61
+ {node.title && (
62
+ <div
63
+ onClick={toggleCollapsed}
64
+ className='flex gap-1 mb-2 cursor-pointer select-none border-none bg-transparent p-0 font-bold'
65
+ >
66
+ <svg
67
+ width='12'
68
+ height='12'
69
+ viewBox='0 0 24 24'
70
+ fill='none'
71
+ stroke='currentColor'
72
+ strokeWidth='2'
73
+ strokeLinecap='round'
74
+ strokeLinejoin='round'
75
+ className={`align-middle transition-transform duration-200 ease-in-out ${collapsed ? 'rotate-0' : 'rotate-90'}`}
76
+ aria-hidden='true'
77
+ >
78
+ <polyline points='9 18 15 12 9 6' />
79
+ </svg>
80
+ {node.title}
81
+ </div>
82
+ )}
83
+ {/* 折叠时隐藏但不卸载 children,保持字段状态与校验订阅 */}
84
+ <div
85
+ className={`${isGrid ? 'grid gap-x-4' : ''} ${collapsed ? 'hidden' : ''}`}
86
+ style={Object.keys(gridStyle).length > 0 ? gridStyle : undefined}
87
+ >
88
+ {node.children.map((child, index) => renderTreeNode(child, index))}
89
+ </div>
90
+ </div>
91
+ </FieldInheritContext.Provider>
92
+ );
93
+ }
@@ -0,0 +1,21 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * 字段继承上下文 — 数据对象容器(NexusObject)向下游字段下发继承属性
5
+ *
6
+ * 语义:
7
+ * - 仅「激活」状态被下发:disabled/readOnly 只在父容器显式启用时
8
+ * 携带 true(父级未设置时上下文不存在该键),子字段自行合并
9
+ * - visible 为 false 表示祖先对象容器隐藏,子树整体不可见
10
+ * - 合并优先级:祖先(已激活) > 字段自身状态 > 默认值
11
+ */
12
+ export interface FieldInheritValue {
13
+ /** 祖先对象容器禁用(仅 true 存在) */
14
+ disabled?: boolean;
15
+ /** 祖先对象容器只读(仅 true 存在) */
16
+ readOnly?: boolean;
17
+ /** 祖先对象容器隐藏 → 子树不可见(false 存在时子树全部隐藏) */
18
+ visible?: boolean;
19
+ }
20
+
21
+ export const FieldInheritContext = createContext<FieldInheritValue>({});
@@ -0,0 +1,8 @@
1
+ import { createContext } from 'react';
2
+
3
+ // 子项未显式设置 colSpan 时使用此默认值
4
+ export interface GridContextValue {
5
+ /** 当前 grid 的列数(tailwind colSpan 基准) */
6
+ column: number;
7
+ }
8
+ export const GridContext = createContext<GridContextValue | null>(null);
@@ -0,0 +1,6 @@
1
+ import { createContext } from 'react';
2
+
3
+ export interface LayoutConfigContextValue {
4
+ removeHidden?: boolean;
5
+ }
6
+ export const LayoutConfigContext = createContext<LayoutConfigContextValue>({});
@@ -0,0 +1,41 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+ import { createContext, type ReactNode, useContext } from 'react';
3
+
4
+ import type { FormController } from '../components/FormController';
5
+
6
+ // ────────────────────────────────────────────────────────────────────────────
7
+ // Form 布局配置
8
+ // ────────────────────────────────────────────────────────────────────────────
9
+
10
+ export interface NexusFormConfig {
11
+ /** label 列配置(由 ui 层 Form.Item 消费) */
12
+ labelCol?: Record<string, unknown>;
13
+ /** label 宽度(px 或 %),快捷方式 — 映射到 labelCol.style.width */
14
+ labelWidth?: number | string;
15
+ /** 是否显示冒号 */
16
+ colon?: boolean | ReactNode;
17
+ /** 是否显示 label(默认 true) */
18
+ label?: boolean;
19
+ /** 表单布局方向 */
20
+ displayType?: 'row' | 'column' | 'inline';
21
+ /** 整个表单只读,所有字段以文本展示 */
22
+ readOnly?: boolean;
23
+ /** 表单每行显示多少列 */
24
+ column?: number;
25
+ }
26
+
27
+ interface NexusContextValue {
28
+ engine: NexusEngine;
29
+ config: NexusFormConfig;
30
+ form: FormController;
31
+ }
32
+
33
+ export const NexusContext = createContext<NexusContextValue | null>(null);
34
+
35
+ export function useNexusContext(): NexusContextValue {
36
+ const ctx = useContext(NexusContext);
37
+ if (!ctx) {
38
+ throw new Error('[NexusField] Must be used within <NexusFormProvider>');
39
+ }
40
+ return ctx;
41
+ }
@@ -0,0 +1,11 @@
1
+ export { useEngine } from './useEngine';
2
+ export { useFieldState } from './useFieldState';
3
+ export { useFieldValidator } from './useFieldValidator';
4
+ export { useFieldValue } from './useFieldValue';
5
+ export { useForm } from './useForm';
6
+ export { useFormConfig } from './useFormConfig';
7
+ export { useFormData } from './useFormData';
8
+ export { useWatch } from './useWatch';
9
+ export { useWatchAll } from './useWatchAll';
10
+ export { useWatchMultiple } from './useWatchMultiple';
11
+ export { useWatchState } from './useWatchState';
@@ -0,0 +1,8 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+
3
+ import { useNexusContext } from '../contexts/NexusContext';
4
+
5
+ export function useEngine(): NexusEngine {
6
+ const { engine } = useNexusContext();
7
+ return engine;
8
+ }
@@ -0,0 +1,21 @@
1
+ import type { FieldState } from '@xbeeant/form-engine';
2
+ import { useSyncExternalStore } from 'react';
3
+
4
+ import { useNexusContext } from '../contexts/NexusContext';
5
+
6
+ /**
7
+ * useFieldState — 精准订阅单个字段状态
8
+ *
9
+ * 按路径精准订阅:只在该字段版本变化时触发重渲染,避免全局订阅拖垮表单
10
+ */
11
+ export function useFieldState(path: string): FieldState | undefined {
12
+ const { engine } = useNexusContext();
13
+
14
+ useSyncExternalStore(
15
+ (onStoreChange) => engine.subscribeField(path, onStoreChange),
16
+ () => engine.getFieldVersion(path),
17
+ () => engine.getFieldVersion(path),
18
+ );
19
+
20
+ return engine.getFieldState(path);
21
+ }