@xbeeant/form-engine-react 0.0.3 → 0.0.4

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 (142) hide show
  1. package/dist/cjs/components/FormController.cjs +1 -1
  2. package/dist/cjs/components/NexusField.cjs +1 -1
  3. package/dist/cjs/components/NexusForm.cjs +1 -1
  4. package/dist/cjs/components/form-controller.cjs +1 -0
  5. package/dist/cjs/components/nexus-field.cjs +1 -0
  6. package/dist/cjs/components/nexus-form-provider.cjs +1 -0
  7. package/dist/cjs/components/nexus-form.cjs +1 -0
  8. package/dist/cjs/components/nexus-layout.cjs +1 -0
  9. package/dist/cjs/components/nexus-object.cjs +1 -0
  10. package/dist/cjs/contexts/field-inherit-context.cjs +1 -0
  11. package/dist/cjs/contexts/grid-context.cjs +1 -0
  12. package/dist/cjs/contexts/layout-config-context.cjs +1 -0
  13. package/dist/cjs/contexts/nexus-context.cjs +1 -0
  14. package/dist/cjs/hooks/index.cjs +1 -1
  15. package/dist/cjs/hooks/use-engine.cjs +1 -0
  16. package/dist/cjs/hooks/use-field-state.cjs +1 -0
  17. package/dist/cjs/hooks/use-field-validator.cjs +1 -0
  18. package/dist/cjs/hooks/use-field-value.cjs +1 -0
  19. package/dist/cjs/hooks/use-form-config.cjs +1 -0
  20. package/dist/cjs/hooks/use-form-data.cjs +1 -0
  21. package/dist/cjs/hooks/use-form-submitting.cjs +1 -0
  22. package/dist/cjs/hooks/use-form.cjs +1 -0
  23. package/dist/cjs/hooks/use-nexus-context.cjs +1 -0
  24. package/dist/cjs/hooks/use-watch-all.cjs +1 -0
  25. package/dist/cjs/hooks/use-watch-multiple.cjs +1 -0
  26. package/dist/cjs/hooks/use-watch-state.cjs +1 -0
  27. package/dist/cjs/hooks/use-watch.cjs +1 -0
  28. package/dist/cjs/hooks/useFormData.cjs +1 -1
  29. package/dist/cjs/hooks/useFormSubmitting.cjs +1 -0
  30. package/dist/cjs/index.cjs +1 -1
  31. package/dist/cjs/styles.css +1 -1
  32. package/dist/cjs/utils/omit-nil.cjs +1 -0
  33. package/dist/cjs/utils/omitNil.cjs +1 -0
  34. package/dist/cjs/utils/persist.cjs +1 -0
  35. package/dist/cjs/utils/render-tree-node.cjs +1 -0
  36. package/dist/cjs/utils/resolve-col-span.cjs +1 -0
  37. package/dist/es/components/FormController.d.ts +73 -19
  38. package/dist/es/components/FormController.js +222 -71
  39. package/dist/es/components/NexusField.js +25 -21
  40. package/dist/es/components/NexusForm.d.ts +33 -3
  41. package/dist/es/components/NexusForm.js +143 -96
  42. package/dist/es/components/form-controller.d.ts +129 -0
  43. package/dist/es/components/form-controller.js +310 -0
  44. package/dist/es/components/nexus-field.d.ts +9 -0
  45. package/dist/es/components/nexus-field.js +131 -0
  46. package/dist/es/components/nexus-form-provider.d.ts +12 -0
  47. package/dist/es/components/nexus-form-provider.js +18 -0
  48. package/dist/es/components/nexus-form.d.ts +114 -0
  49. package/dist/es/components/nexus-form.js +174 -0
  50. package/dist/es/components/nexus-layout.d.ts +9 -0
  51. package/dist/es/components/nexus-layout.js +48 -0
  52. package/dist/es/components/nexus-object.d.ts +16 -0
  53. package/dist/es/components/nexus-object.js +64 -0
  54. package/dist/es/contexts/NexusContext.d.ts +2 -0
  55. package/dist/es/contexts/field-inherit-context.d.ts +18 -0
  56. package/dist/es/contexts/field-inherit-context.js +5 -0
  57. package/dist/es/contexts/grid-context.d.ts +5 -0
  58. package/dist/es/contexts/grid-context.js +5 -0
  59. package/dist/es/contexts/layout-config-context.d.ts +4 -0
  60. package/dist/es/contexts/layout-config-context.js +5 -0
  61. package/dist/es/contexts/nexus-context.d.ts +29 -0
  62. package/dist/es/contexts/nexus-context.js +12 -0
  63. package/dist/es/hooks/index.d.ts +12 -11
  64. package/dist/es/hooks/index.js +20 -18
  65. package/dist/es/hooks/use-engine.d.ts +2 -0
  66. package/dist/es/hooks/use-engine.js +8 -0
  67. package/dist/es/hooks/use-field-state.d.ts +7 -0
  68. package/dist/es/hooks/use-field-state.js +13 -0
  69. package/dist/es/hooks/use-field-validator.d.ts +40 -0
  70. package/dist/es/hooks/use-field-validator.js +35 -0
  71. package/dist/es/hooks/use-field-value.d.ts +6 -0
  72. package/dist/es/hooks/use-field-value.js +13 -0
  73. package/dist/es/hooks/use-form-config.d.ts +5 -0
  74. package/dist/es/hooks/use-form-config.js +8 -0
  75. package/dist/es/hooks/use-form-data.d.ts +6 -0
  76. package/dist/es/hooks/use-form-data.js +13 -0
  77. package/dist/es/hooks/use-form-submitting.d.ts +7 -0
  78. package/dist/es/hooks/use-form-submitting.js +11 -0
  79. package/dist/es/hooks/use-form.d.ts +22 -0
  80. package/dist/es/hooks/use-form.js +12 -0
  81. package/dist/es/hooks/use-nexus-context.d.ts +11 -0
  82. package/dist/es/hooks/use-nexus-context.js +12 -0
  83. package/dist/es/hooks/use-watch-all.d.ts +19 -0
  84. package/dist/es/hooks/use-watch-all.js +29 -0
  85. package/dist/es/hooks/use-watch-multiple.d.ts +20 -0
  86. package/dist/es/hooks/use-watch-multiple.js +29 -0
  87. package/dist/es/hooks/use-watch-state.d.ts +18 -0
  88. package/dist/es/hooks/use-watch-state.js +19 -0
  89. package/dist/es/hooks/use-watch.d.ts +25 -0
  90. package/dist/es/hooks/use-watch.js +20 -0
  91. package/dist/es/hooks/useForm.d.ts +11 -4
  92. package/dist/es/hooks/useFormData.js +9 -9
  93. package/dist/es/hooks/useFormSubmitting.d.ts +7 -0
  94. package/dist/es/hooks/useFormSubmitting.js +11 -0
  95. package/dist/es/index.d.ts +26 -24
  96. package/dist/es/index.js +36 -32
  97. package/dist/es/styles.css +1 -1
  98. package/dist/es/utils/omit-nil.d.ts +9 -0
  99. package/dist/es/utils/omit-nil.js +14 -0
  100. package/dist/es/utils/omitNil.d.ts +9 -0
  101. package/dist/es/utils/omitNil.js +14 -0
  102. package/dist/es/utils/persist.d.ts +24 -0
  103. package/dist/es/utils/persist.js +43 -0
  104. package/dist/es/utils/render-tree-node.d.ts +6 -0
  105. package/dist/es/utils/render-tree-node.js +17 -0
  106. package/dist/es/utils/resolve-col-span.d.ts +5 -0
  107. package/dist/es/utils/resolve-col-span.js +9 -0
  108. package/dist/umd/index.css +1 -1
  109. package/dist/umd/index.umd.cjs +1 -1
  110. package/package.json +5 -4
  111. package/src/components/form-controller.ts +584 -0
  112. package/src/components/{NexusField.tsx → nexus-field.tsx} +57 -6
  113. package/src/components/{NexusFormProvider.tsx → nexus-form-provider.tsx} +3 -3
  114. package/src/components/{NexusForm.tsx → nexus-form.tsx} +135 -18
  115. package/src/components/{NexusLayout.tsx → nexus-layout.tsx} +5 -5
  116. package/src/components/{NexusObject.tsx → nexus-object.tsx} +3 -3
  117. package/src/contexts/{NexusContext.ts → nexus-context.ts} +3 -1
  118. package/src/hooks/index.ts +12 -11
  119. package/src/hooks/{useEngine.ts → use-engine.ts} +1 -1
  120. package/src/hooks/{useFieldState.ts → use-field-state.ts} +1 -1
  121. package/src/hooks/{useFieldValidator.ts → use-field-validator.ts} +1 -1
  122. package/src/hooks/{useFieldValue.ts → use-field-value.ts} +1 -1
  123. package/src/hooks/{useFormConfig.ts → use-form-config.ts} +2 -2
  124. package/src/hooks/{useFormData.ts → use-form-data.ts} +4 -4
  125. package/src/hooks/use-form-submitting.ts +16 -0
  126. package/src/hooks/{useForm.ts → use-form.ts} +12 -5
  127. package/src/hooks/{useNexusContext.ts → use-nexus-context.ts} +2 -2
  128. package/src/index.ts +26 -24
  129. package/src/utils/omit-nil.ts +24 -0
  130. package/src/utils/persist.ts +88 -0
  131. package/src/utils/{renderTreeNode.tsx → render-tree-node.tsx} +3 -3
  132. package/src/utils/{resolveColSpan.ts → resolve-col-span.ts} +1 -1
  133. package/dist/cjs/tailwind.css +0 -1
  134. package/dist/es/tailwind.css +0 -1
  135. package/src/components/FormController.ts +0 -278
  136. /package/src/contexts/{FieldInheritContext.ts → field-inherit-context.ts} +0 -0
  137. /package/src/contexts/{GridContext.ts → grid-context.ts} +0 -0
  138. /package/src/contexts/{LayoutConfigContext.ts → layout-config-context.ts} +0 -0
  139. /package/src/hooks/{useWatchAll.ts → use-watch-all.ts} +0 -0
  140. /package/src/hooks/{useWatchMultiple.ts → use-watch-multiple.ts} +0 -0
  141. /package/src/hooks/{useWatchState.ts → use-watch-state.ts} +0 -0
  142. /package/src/hooks/{useWatch.ts → use-watch.ts} +0 -0
@@ -1,16 +1,23 @@
1
- import type { NexusSchema } from '@xbeeant/form-engine';
1
+ import type { NexusComponent, NexusSchema } from '@xbeeant/form-engine';
2
2
  import type { CSSProperties, ReactNode, SubmitEvent } from 'react';
3
3
  import {
4
4
  useCallback,
5
5
  useEffect,
6
+ useId,
6
7
  useMemo,
7
8
  useRef,
8
9
  useSyncExternalStore,
9
10
  } from 'react';
10
-
11
- import { renderTreeNode } from '../utils/renderTreeNode';
12
- import type { FormController } from './FormController';
13
- import { NexusFormProvider } from './NexusFormProvider';
11
+ import { useFormSubmitting } from '../hooks';
12
+ import {
13
+ clearPersisted,
14
+ loadPersisted,
15
+ type PersistOptions,
16
+ savePersisted,
17
+ } from '../utils/persist';
18
+ import { renderTreeNode } from '../utils/render-tree-node';
19
+ import type { FormController } from './form-controller';
20
+ import { NexusFormProvider } from './nexus-form-provider';
14
21
 
15
22
  // ────────────────────────────────────────────────────────────────────────────
16
23
  // Form 布局配置
@@ -31,10 +38,16 @@ export interface NexusFormConfig {
31
38
  readOnly?: boolean;
32
39
  /** 表单每行显示多少列 */
33
40
  column?: number;
41
+ /** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
42
+ locale?: string;
34
43
  }
35
44
 
36
45
  export interface NexusFormProps {
37
- /** Form 实例,由 useForm() 创建 */
46
+ /**
47
+ * Form 实例,由 useForm() 创建。同一 form 引用 = 同一引擎宿主(组件/插件注册共享);
48
+ * 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
49
+ * form 的 API(getValues/setValues/submit...)聚合作用于全部实例。
50
+ */
38
51
  form: FormController;
39
52
  /** Schema 定义 */
40
53
  schema?: NexusSchema;
@@ -44,6 +57,8 @@ export interface NexusFormProps {
44
57
  widgets?: Record<string, (props: any) => ReactNode>;
45
58
  /** 额外注册的 layout */
46
59
  layouts?: Record<string, (props: any) => ReactNode>;
60
+ /** 字段包裹组件(UI 无关,Renderer 层注入;渲染层默认包裹所有 widget,label=false 时跳过) */
61
+ fieldWrapper?: NexusComponent;
47
62
  /** 提交成功回调 */
48
63
  onFinish?: (formData: Record<string, unknown>) => void | Promise<void>;
49
64
  /** 校验失败回调 */
@@ -77,12 +92,37 @@ export interface NexusFormProps {
77
92
  changedPath?: string,
78
93
  ) => void;
79
94
  };
95
+ /**
96
+ * 表单值变化回调(x-render onValuesChange 对齐)
97
+ * 任意字段值变化时触发,参数为 (changedValue, allValues, changedPath)
98
+ * 与 watch: { '#': fn } 语义等价,但作为标准回调 API 提供
99
+ */
100
+ onValuesChange?: (
101
+ changedValue: unknown,
102
+ allValues: Record<string, unknown>,
103
+ changedPath: string,
104
+ ) => void;
80
105
  /**
81
106
  * 提交时是否移除 hidden 字段数据,默认 true
82
107
  * - true: submit/getValues 不包含 hidden 字段
83
108
  * - false: submit/getValues 包含所有字段(含 hidden)
84
109
  */
85
110
  removeHiddenData?: boolean;
111
+ /**
112
+ * 提交时递归移除空值(undefined/null/'',ProForm omitNil 对齐)
113
+ * 与 form.submit({ omitNil: true }) / form.getValues(paths, { omitNil: true }) 等价
114
+ */
115
+ omitNil?: boolean;
116
+ /** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
117
+ locale?: string;
118
+ /**
119
+ * 表单草稿持久化:值变化时自动保存到 Web Storage,下次挂载自动恢复
120
+ * @example
121
+ * ```tsx
122
+ * <NexusForm persist={{ key: 'apply-form', storage: 'localStorage' }} />
123
+ * ```
124
+ */
125
+ persist?: PersistOptions;
86
126
 
87
127
  // ── 表单布局配置 ──────────────────────────────────────────────────────
88
128
  /** label 列配置(由 ui 层 Form.Item 消费) */
@@ -112,6 +152,7 @@ export function NexusForm({
112
152
  initialValues,
113
153
  widgets,
114
154
  layouts,
155
+ fieldWrapper,
115
156
  onFinish,
116
157
  onFinishFailed,
117
158
  onMount,
@@ -127,10 +168,24 @@ export function NexusForm({
127
168
  readOnly,
128
169
  column,
129
170
  watch,
171
+ onValuesChange,
130
172
  removeHiddenData = true,
173
+ omitNil = false,
174
+ locale,
175
+ persist,
131
176
  }: NexusFormProps) {
132
- const engine = form._getEngine();
177
+ // 实例视图引擎:同一 form 挂载的每个 NexusForm 自动获得独立实例
178
+ // (schema/值/订阅互不影响);实例标识由 useId 内部生成,用户不感知 instanceId。
179
+ // 同一 form = 同一引擎宿主(组件/插件注册共享),不同 schema = 不同实例状态。
180
+ const instanceKey = useId();
181
+ const engine = useMemo(
182
+ () => form._useInstance(instanceKey),
183
+ [form, instanceKey],
184
+ );
133
185
  const formElRef = useRef<HTMLFormElement | null>(null);
186
+ // persist 配置经 ref 持有:保存回调不随配置对象变化重建订阅
187
+ const persistRef = useRef<PersistOptions | undefined>(persist);
188
+ persistRef.current = persist;
134
189
 
135
190
  // Schema 顶层配置作为默认值,props 优先级更高
136
191
  const finalDisplayType = displayType ?? schema?.displayType ?? 'row';
@@ -139,16 +194,20 @@ export function NexusForm({
139
194
  const finalLabelWidth = labelWidth ?? schema?.labelWidth;
140
195
  const finalReadOnly = readOnly ?? schema?.readOnly ?? false;
141
196
  const finalColumn = column ?? schema?.column;
197
+ const finalLocale = locale ?? schema?.locale ?? engine.getLocale();
142
198
 
143
199
  // 注册额外 widgets / layouts(仅首次或引用变化时)
144
200
  useEffect(() => {
145
201
  if (widgets) {
146
202
  engine.registerWidgets(widgets);
147
203
  }
204
+ if (fieldWrapper) {
205
+ engine.registerFieldWrapper(fieldWrapper);
206
+ }
148
207
  if (layouts) {
149
208
  engine.registerLayouts(layouts);
150
209
  }
151
- }, [engine, widgets, layouts]);
210
+ }, [engine, widgets, layouts, fieldWrapper]);
152
211
 
153
212
  // schema 变化时重新初始化
154
213
  // 注意:initialValues 仅在首次挂载时使用,避免每次渲染都 re-init 导致
@@ -160,7 +219,11 @@ export function NexusForm({
160
219
  return;
161
220
  }
162
221
  if (isFirstInitRef.current) {
163
- engine.init(schema, initialValuesRef.current);
222
+ // 草稿持久化:恢复已保存草稿(优先于 initialValues)
223
+ const persisted = persistRef.current
224
+ ? loadPersisted(persistRef.current)
225
+ : undefined;
226
+ engine.init(schema, persisted ?? initialValuesRef.current);
164
227
  isFirstInitRef.current = false;
165
228
  } else {
166
229
  // schema 变化:保留当前已填数据,而非重置为 initialValues
@@ -169,6 +232,35 @@ export function NexusForm({
169
232
  // eslint-disable-next-line react-hooks/exhaustive-deps
170
233
  }, [engine, schema]);
171
234
 
235
+ // 草稿持久化:store 版本变化(任意数据变更)时防抖保存
236
+ // 用持久化身份(storage:key)驱动订阅:开关切换 / 换 key 时重建订阅,
237
+ // 而 persist 对象本身每次渲染重建不影响订阅稳定性
238
+ const persistIdentity = persist
239
+ ? `${persist.storage ?? 'localStorage'}:${persist.key}`
240
+ : null;
241
+ useEffect(() => {
242
+ const options = persistIdentity ? persistRef.current : undefined;
243
+ if (!options) {
244
+ return;
245
+ }
246
+ let timer: ReturnType<typeof setTimeout> | null = null;
247
+ const debounce = options.debounce ?? 300;
248
+ const unsub = engine.subscribeStore(() => {
249
+ if (timer) {
250
+ clearTimeout(timer);
251
+ }
252
+ timer = setTimeout(() => {
253
+ savePersisted(options, engine.getFormData());
254
+ }, debounce);
255
+ });
256
+ return () => {
257
+ if (timer) {
258
+ clearTimeout(timer);
259
+ }
260
+ unsub();
261
+ };
262
+ }, [engine, persistIdentity]);
263
+
172
264
  // onMount:首次传入「非空」schema 并完成渲染后执行一次
173
265
  // undefined / null / {}(无任何键)均视为空 schema,不触发;
174
266
  // schema 由空变为非空时,于首个非空渲染提交(useEffect)后触发。
@@ -191,28 +283,48 @@ export function NexusForm({
191
283
  useRef<(data: Record<string, unknown>) => void | Promise<void>>(noop);
192
284
  const onFinishFailedRef =
193
285
  useRef<(errors: Map<string, string[]>) => void>(noop);
194
- onFinishRef.current = onFinish ?? noop;
286
+ onFinishRef.current = (data) => {
287
+ // persist 未配置时不参与提交流程;配置且 clearOnSubmit !== false 时清除草稿
288
+ const options = persistRef.current;
289
+ if (options && options.clearOnSubmit !== false) {
290
+ clearPersisted(options);
291
+ }
292
+ return onFinish?.(data);
293
+ };
195
294
  onFinishFailedRef.current = onFinishFailed ?? noop;
196
295
 
197
296
  // 只在挂载时绑定一次:传入「稳定的 getter」,让 FormController 在 submit 时读取最新回调
198
297
  useEffect(() => {
199
298
  form._bind(
299
+ instanceKey,
200
300
  formElRef.current,
201
301
  () => onFinishRef.current,
202
302
  () => onFinishFailedRef.current,
203
303
  );
204
304
  // eslint-disable-next-line react-hooks/exhaustive-deps
205
- }, [form]);
305
+ }, [form, instanceKey]);
206
306
 
207
- // watch / removeHiddenData 变化时单独同步(不重置其他绑定)
307
+ // watch / removeHiddenData / onValuesChange 变化时单独同步(不重置其他绑定)
308
+ // onValuesChange 经 ref 持有:回调内容变化不重建订阅,仅内部引用更新
309
+ const onValuesChangeRef = useRef(onValuesChange);
310
+ onValuesChangeRef.current = onValuesChange;
208
311
  useEffect(() => {
209
- form._syncConfig({ removeHiddenData, watch });
210
- }, [form, removeHiddenData, watch]);
312
+ form._syncConfig(instanceKey, {
313
+ removeHiddenData,
314
+ omitNil,
315
+ watch,
316
+ onValuesChange: (changedValue, allValues, changedPath) =>
317
+ onValuesChangeRef.current?.(changedValue, allValues, changedPath),
318
+ });
319
+ }, [form, instanceKey, removeHiddenData, omitNil, watch]);
211
320
 
321
+ // 渲染树版本订阅:仅 Schema 结构变化(init/setSchema/reset)时重算 renderTree。
322
+ // 字段值/错误等数据变化只 bump store 版本(useFormData 消费),
323
+ // 不会触发 NexusForm 重渲染——各字段经字段级版本订阅精准重渲染。
212
324
  const _version = useSyncExternalStore(
213
- engine.subscribeStore,
214
- engine.getSnapshot,
215
- engine.getSnapshot,
325
+ (cb) => engine.subscribeRender(cb),
326
+ () => engine.getRenderSnapshot(),
327
+ () => engine.getRenderSnapshot(),
216
328
  );
217
329
  // 依赖 _version:engine.init() / setSchema() 会 bump version,
218
330
  // 需要在此后重新读取 renderTree(首次渲染时 engine 尚未 init,renderTree 为空)
@@ -232,11 +344,14 @@ export function NexusForm({
232
344
  }, [form]);
233
345
 
234
346
  // footer 渲染
347
+ const submitting = useFormSubmitting(form);
235
348
  let footerNode: ReactNode = null;
236
349
  if (footer === true) {
237
350
  footerNode = (
238
351
  <div className='mt-4'>
239
- <button type='submit'>提交</button>{' '}
352
+ <button type='submit' disabled={submitting}>
353
+ {submitting ? '提交中...' : '提交'}
354
+ </button>{' '}
240
355
  <button type='button' onClick={handleReset}>
241
356
  重置
242
357
  </button>
@@ -274,6 +389,7 @@ export function NexusForm({
274
389
  displayType: finalDisplayType,
275
390
  readOnly: finalReadOnly,
276
391
  column: finalColumn,
392
+ locale: finalLocale,
277
393
  }),
278
394
  [
279
395
  mergedLabelCol,
@@ -283,6 +399,7 @@ export function NexusForm({
283
399
  finalDisplayType,
284
400
  finalReadOnly,
285
401
  finalColumn,
402
+ finalLocale,
286
403
  ],
287
404
  );
288
405
 
@@ -2,14 +2,14 @@ import type { RenderLayoutNode } from '@xbeeant/form-engine';
2
2
  import type { CSSProperties } from 'react';
3
3
  import { useContext, useMemo } from 'react';
4
4
 
5
- import { GridContext } from '../contexts/GridContext';
5
+ import { GridContext } from '../contexts/grid-context';
6
6
  import {
7
7
  LayoutConfigContext,
8
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';
9
+ } from '../contexts/layout-config-context';
10
+ import { useNexusContext } from '../contexts/nexus-context';
11
+ import { renderTreeNode } from '../utils/render-tree-node';
12
+ import { resolveColSpan } from '../utils/resolve-col-span';
13
13
 
14
14
  interface NexusLayoutProps {
15
15
  node: RenderLayoutNode;
@@ -2,9 +2,9 @@ import type { RenderObjectNode } from '@xbeeant/form-engine';
2
2
  import type { CSSProperties } from 'react';
3
3
  import { useContext, useState, useSyncExternalStore } from 'react';
4
4
 
5
- import { FieldInheritContext } from '../contexts/FieldInheritContext';
6
- import { useNexusContext } from '../contexts/NexusContext';
7
- import { renderTreeNode } from '../utils/renderTreeNode';
5
+ import { FieldInheritContext } from '../contexts/field-inherit-context';
6
+ import { useNexusContext } from '../contexts/nexus-context';
7
+ import { renderTreeNode } from '../utils/render-tree-node';
8
8
 
9
9
  interface NexusObjectProps {
10
10
  node: RenderObjectNode;
@@ -1,7 +1,7 @@
1
1
  import type { NexusEngine } from '@xbeeant/form-engine';
2
2
  import { createContext, type ReactNode, useContext } from 'react';
3
3
 
4
- import type { FormController } from '../components/FormController';
4
+ import type { FormController } from '../components/form-controller';
5
5
 
6
6
  // ────────────────────────────────────────────────────────────────────────────
7
7
  // Form 布局配置
@@ -22,6 +22,8 @@ export interface NexusFormConfig {
22
22
  readOnly?: boolean;
23
23
  /** 表单每行显示多少列 */
24
24
  column?: number;
25
+ /** 表单语言标识(如 'zh-CN' / 'en-US',ui 层消费:antd locale + 内置文案) */
26
+ locale?: string;
25
27
  }
26
28
 
27
29
  interface NexusContextValue {
@@ -1,11 +1,12 @@
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';
1
+ export { useEngine } from './use-engine';
2
+ export { useFieldState } from './use-field-state';
3
+ export { useFieldValidator } from './use-field-validator';
4
+ export { useFieldValue } from './use-field-value';
5
+ export { useForm } from './use-form';
6
+ export { useFormConfig } from './use-form-config';
7
+ export { useFormData } from './use-form-data';
8
+ export { useFormSubmitting } from './use-form-submitting';
9
+ export { useWatch } from './use-watch';
10
+ export { useWatchAll } from './use-watch-all';
11
+ export { useWatchMultiple } from './use-watch-multiple';
12
+ export { useWatchState } from './use-watch-state';
@@ -1,6 +1,6 @@
1
1
  import type { NexusEngine } from '@xbeeant/form-engine';
2
2
 
3
- import { useNexusContext } from '../contexts/NexusContext';
3
+ import { useNexusContext } from '../contexts/nexus-context';
4
4
 
5
5
  export function useEngine(): NexusEngine {
6
6
  const { engine } = useNexusContext();
@@ -1,7 +1,7 @@
1
1
  import type { FieldState } from '@xbeeant/form-engine';
2
2
  import { useSyncExternalStore } from 'react';
3
3
 
4
- import { useNexusContext } from '../contexts/NexusContext';
4
+ import { useNexusContext } from '../contexts/nexus-context';
5
5
 
6
6
  /**
7
7
  * useFieldState — 精准订阅单个字段状态
@@ -24,7 +24,7 @@
24
24
 
25
25
  import type { NexusEngine, NexusFormInstance } from '@xbeeant/form-engine';
26
26
  import { useEffect, useRef } from 'react';
27
- import { useNexusContext } from '../contexts/NexusContext';
27
+ import { useNexusContext } from '../contexts/nexus-context';
28
28
 
29
29
  export type FieldValidator = (
30
30
  value: unknown,
@@ -1,6 +1,6 @@
1
1
  import { useSyncExternalStore } from 'react';
2
2
 
3
- import { useEngine } from './useEngine';
3
+ import { useEngine } from './use-engine';
4
4
 
5
5
  /**
6
6
  * useFieldValue — 精准订阅单个字段值
@@ -1,5 +1,5 @@
1
- import type { NexusFormConfig } from '../components/NexusForm';
2
- import { useNexusContext } from '../contexts/NexusContext';
1
+ import type { NexusFormConfig } from '../components/nexus-form';
2
+ import { useNexusContext } from '../contexts/nexus-context';
3
3
 
4
4
  /**
5
5
  * useFormConfig — 获取表单布局配置
@@ -1,6 +1,6 @@
1
1
  import { useMemo, useSyncExternalStore } from 'react';
2
2
 
3
- import { useEngine } from './useEngine';
3
+ import { useEngine } from './use-engine';
4
4
 
5
5
  /**
6
6
  * useFormData — 订阅整个表单数据
@@ -11,9 +11,9 @@ export function useFormData(): Record<string, unknown> {
11
11
  const engine = useEngine();
12
12
 
13
13
  const _version = useSyncExternalStore(
14
- engine.subscribeStore,
15
- engine.getSnapshot,
16
- engine.getSnapshot,
14
+ (cb) => engine.subscribeStore(cb),
15
+ () => engine.getSnapshot(),
16
+ () => engine.getSnapshot(),
17
17
  );
18
18
  // biome-ignore lint/correctness/useExhaustiveDependencies: _version 是 formData 失效信号(engine 内部状态,静态分析不可见)
19
19
  return useMemo(() => engine.getFormData(), [engine, _version]);
@@ -0,0 +1,16 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ import type { FormController } from '../components/form-controller';
4
+
5
+ /**
6
+ * useFormSubmitting — 订阅表单提交中状态(formily submitting 对齐)
7
+ *
8
+ * submit() 全流程(校验 + onFinish)期间返回 true,供提交按钮展示 loading。
9
+ */
10
+ export function useFormSubmitting(form: FormController): boolean {
11
+ return useSyncExternalStore(
12
+ (cb) => form.onSubmittingChange(cb),
13
+ () => form.getSubmitting(),
14
+ () => form.getSubmitting(),
15
+ );
16
+ }
@@ -1,19 +1,26 @@
1
1
  import { NexusEngine } from '@xbeeant/form-engine';
2
2
  import { useRef } from 'react';
3
3
 
4
- import { FormController } from '../components/FormController';
4
+ import { FormController } from '../components/form-controller';
5
5
 
6
6
  /**
7
7
  * useForm — 创建 Form 实例
8
8
  *
9
- * 支持一个页面挂载多个表单实例:
10
- * - `useForm()`:独立实例(未注册,不参与跨表单联动)
9
+ * 一个 Form 绑定一个引擎宿主(引擎为独立实体):
10
+ * - 同一 form 引用传给多个 NexusForm → 共享引擎宿主(组件/插件注册共享),
11
+ * 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
12
+ * form 的 API 聚合作用于全部实例
13
+ * - 需要完全独立的表单:各自 useForm()(独立引擎宿主)
14
+ *
15
+ * 支持:
16
+ * - `useForm()`:创建独立实例(引擎宿主内部创建,未注册,不参与跨表单联动)
11
17
  * - `useForm(formId)`:创建带 formId 的实例,自动注册到默认表单注册表,
12
18
  * 可通过 schema `crossForm` reaction / `engine.linkForm` 与其他表单联动
13
- * - `useForm(formId, engine)`:复用外部创建的引擎(如 `new NexusEngine({ formId })`)
19
+ * - `useForm(formId, engine)`:复用外部创建的引擎(如 `new NexusEngine({ formId })`),
20
+ * 实现多个 form 共享同一引擎宿主
14
21
  *
15
22
  * @param formId - 可选,表单实例唯一标识(跨表单联动寻址)
16
- * @param engine - 可选,外部引擎实例(缺省内部创建)
23
+ * @param engine - 可选,外部引擎宿主(缺省内部创建)
17
24
  */
18
25
  export function useForm(
19
26
  formId?: string,
@@ -4,8 +4,8 @@
4
4
 
5
5
  import type { NexusEngine } from '@xbeeant/form-engine';
6
6
  import { createContext, useContext } from 'react';
7
- import type { FormController } from '../components/FormController';
8
- import type { NexusFormConfig } from '../components/NexusForm';
7
+ import type { FormController } from '../components/form-controller';
8
+ import type { NexusFormConfig } from '../components/nexus-form';
9
9
 
10
10
  interface NexusContextValue {
11
11
  engine: NexusEngine;
package/src/index.ts CHANGED
@@ -5,28 +5,30 @@
5
5
 
6
6
  import './styles.css';
7
7
 
8
- export { FormController } from './components/FormController';
9
- export { NexusField } from './components/NexusField';
10
- export type { NexusFormConfig } from './components/NexusForm';
11
- export { NexusForm } from './components/NexusForm';
12
- export { NexusFormProvider } from './components/NexusFormProvider';
13
- export { NexusLayout } from './components/NexusLayout';
14
- export { NexusObject } from './components/NexusObject';
15
- export type { FieldInheritValue } from './contexts/FieldInheritContext';
16
- export { FieldInheritContext } from './contexts/FieldInheritContext';
17
- export type { GridContextValue } from './contexts/GridContext';
18
- export { GridContext } from './contexts/GridContext';
19
- export type { LayoutConfigContextValue } from './contexts/LayoutConfigContext';
20
- export { LayoutConfigContext } from './contexts/LayoutConfigContext';
8
+ export { FormController } from './components/form-controller';
9
+ export { NexusField } from './components/nexus-field';
10
+ export type { NexusFormConfig, NexusFormProps } from './components/nexus-form';
11
+ export { NexusForm } from './components/nexus-form';
12
+ export { NexusFormProvider } from './components/nexus-form-provider';
13
+ export { NexusLayout } from './components/nexus-layout';
14
+ export { NexusObject } from './components/nexus-object';
15
+ export type { FieldInheritValue } from './contexts/field-inherit-context';
16
+ export { FieldInheritContext } from './contexts/field-inherit-context';
17
+ export type { GridContextValue } from './contexts/grid-context';
18
+ export { GridContext } from './contexts/grid-context';
19
+ export type { LayoutConfigContextValue } from './contexts/layout-config-context';
20
+ export { LayoutConfigContext } from './contexts/layout-config-context';
21
+ export { NexusContext } from './contexts/nexus-context';
21
22
 
22
- export { useEngine } from './hooks/useEngine';
23
- export { useFieldState } from './hooks/useFieldState';
24
- export { useFieldValidator } from './hooks/useFieldValidator';
25
- export { useFieldValue } from './hooks/useFieldValue';
26
- export { useForm } from './hooks/useForm';
27
- export { useFormConfig } from './hooks/useFormConfig';
28
- export { useFormData } from './hooks/useFormData';
29
- export { useWatch } from './hooks/useWatch';
30
- export { useWatchAll } from './hooks/useWatchAll';
31
- export { useWatchMultiple } from './hooks/useWatchMultiple';
32
- export { useWatchState } from './hooks/useWatchState';
23
+ export { useEngine } from './hooks/use-engine';
24
+ export { useFieldState } from './hooks/use-field-state';
25
+ export { useFieldValidator } from './hooks/use-field-validator';
26
+ export { useFieldValue } from './hooks/use-field-value';
27
+ export { useForm } from './hooks/use-form';
28
+ export { useFormConfig } from './hooks/use-form-config';
29
+ export { useFormData } from './hooks/use-form-data';
30
+ export { useFormSubmitting } from './hooks/use-form-submitting';
31
+ export { useWatch } from './hooks/use-watch';
32
+ export { useWatchAll } from './hooks/use-watch-all';
33
+ export { useWatchMultiple } from './hooks/use-watch-multiple';
34
+ export { useWatchState } from './hooks/use-watch-state';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * omitNilDeep — 递归移除空值(undefined / null / '')
3
+ *
4
+ * ProForm omitNil 对齐:提交/取值时过滤掉未填写的空值字段。
5
+ * - 对象:移除值为空的键(递归处理嵌套对象)
6
+ * - 数组:逐项递归处理(保留数组结构与长度)
7
+ * - 0 / false / NaN 等非空值保留
8
+ */
9
+ export function omitNilDeep(value: unknown): unknown {
10
+ if (Array.isArray(value)) {
11
+ return value.map((item) => omitNilDeep(item));
12
+ }
13
+ if (value !== null && typeof value === 'object') {
14
+ const result: Record<string, unknown> = {};
15
+ for (const [key, item] of Object.entries(value)) {
16
+ if (item === undefined || item === null || item === '') {
17
+ continue;
18
+ }
19
+ result[key] = omitNilDeep(item);
20
+ }
21
+ return result;
22
+ }
23
+ return value;
24
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * 表单草稿持久化(NexusForm persist 选项实现)
3
+ *
4
+ * 将表单数据自动保存到 Web Storage(localStorage / sessionStorage),
5
+ * 下次挂载时自动恢复为初始值(草稿续填)。
6
+ *
7
+ * 约束:
8
+ * - 所有存储访问均 try/catch 包裹(SSR 无 window / Safari 隐私模式抛错)
9
+ * - 存储键为完整路径前缀,避免多表单实例冲突
10
+ * - 提交成功后默认清除草稿(clearOnSubmit: false 可关闭)
11
+ */
12
+
13
+ export interface PersistOptions {
14
+ /** 存储键(必填,多表单实例需唯一) */
15
+ key: string;
16
+ /** 存储介质,默认 localStorage */
17
+ storage?: 'localStorage' | 'sessionStorage';
18
+ /** 保存防抖毫秒数,默认 300 */
19
+ debounce?: number;
20
+ /** 提交成功后清除草稿,默认 true */
21
+ clearOnSubmit?: boolean;
22
+ }
23
+
24
+ function getStorage(
25
+ storage?: 'localStorage' | 'sessionStorage',
26
+ ): Storage | undefined {
27
+ if (typeof window === 'undefined') {
28
+ return undefined;
29
+ }
30
+ try {
31
+ const target =
32
+ storage === 'sessionStorage'
33
+ ? window.sessionStorage
34
+ : window.localStorage;
35
+ // 隐私模式下访问可能抛错,统一降级为不持久化
36
+ target.setItem('__nexus_persist_probe__', '1');
37
+ target.removeItem('__nexus_persist_probe__');
38
+ return target;
39
+ } catch {
40
+ return undefined;
41
+ }
42
+ }
43
+
44
+ export function loadPersisted(
45
+ options: PersistOptions,
46
+ ): Record<string, unknown> | undefined {
47
+ const storage = getStorage(options.storage);
48
+ if (!storage) {
49
+ return undefined;
50
+ }
51
+ try {
52
+ const raw = storage.getItem(options.key);
53
+ if (!raw) {
54
+ return undefined;
55
+ }
56
+ const parsed = JSON.parse(raw) as Record<string, unknown>;
57
+ return typeof parsed === 'object' && parsed !== null ? parsed : undefined;
58
+ } catch {
59
+ return undefined;
60
+ }
61
+ }
62
+
63
+ export function savePersisted(
64
+ options: PersistOptions,
65
+ data: Record<string, unknown>,
66
+ ): void {
67
+ const storage = getStorage(options.storage);
68
+ if (!storage) {
69
+ return;
70
+ }
71
+ try {
72
+ storage.setItem(options.key, JSON.stringify(data));
73
+ } catch {
74
+ // 存储配额满 / 隐私模式:静默失败
75
+ }
76
+ }
77
+
78
+ export function clearPersisted(options: PersistOptions): void {
79
+ const storage = getStorage(options.storage);
80
+ if (!storage) {
81
+ return;
82
+ }
83
+ try {
84
+ storage.removeItem(options.key);
85
+ } catch {
86
+ // 静默失败
87
+ }
88
+ }