@xbeeant/form-engine-react 0.0.2 → 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 (145) 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/useForm.cjs +1 -1
  29. package/dist/cjs/hooks/useFormData.cjs +1 -1
  30. package/dist/cjs/hooks/useFormSubmitting.cjs +1 -0
  31. package/dist/cjs/index.cjs +1 -1
  32. package/dist/cjs/styles.css +1 -0
  33. package/dist/cjs/utils/omit-nil.cjs +1 -0
  34. package/dist/cjs/utils/omitNil.cjs +1 -0
  35. package/dist/cjs/utils/persist.cjs +1 -0
  36. package/dist/cjs/utils/render-tree-node.cjs +1 -0
  37. package/dist/cjs/utils/resolve-col-span.cjs +1 -0
  38. package/dist/es/components/FormController.d.ts +76 -18
  39. package/dist/es/components/FormController.js +227 -70
  40. package/dist/es/components/NexusField.js +63 -60
  41. package/dist/es/components/NexusForm.d.ts +40 -3
  42. package/dist/es/components/NexusForm.js +143 -89
  43. package/dist/es/components/form-controller.d.ts +129 -0
  44. package/dist/es/components/form-controller.js +310 -0
  45. package/dist/es/components/nexus-field.d.ts +9 -0
  46. package/dist/es/components/nexus-field.js +131 -0
  47. package/dist/es/components/nexus-form-provider.d.ts +12 -0
  48. package/dist/es/components/nexus-form-provider.js +18 -0
  49. package/dist/es/components/nexus-form.d.ts +114 -0
  50. package/dist/es/components/nexus-form.js +174 -0
  51. package/dist/es/components/nexus-layout.d.ts +9 -0
  52. package/dist/es/components/nexus-layout.js +48 -0
  53. package/dist/es/components/nexus-object.d.ts +16 -0
  54. package/dist/es/components/nexus-object.js +64 -0
  55. package/dist/es/contexts/NexusContext.d.ts +2 -0
  56. package/dist/es/contexts/field-inherit-context.d.ts +18 -0
  57. package/dist/es/contexts/field-inherit-context.js +5 -0
  58. package/dist/es/contexts/grid-context.d.ts +5 -0
  59. package/dist/es/contexts/grid-context.js +5 -0
  60. package/dist/es/contexts/layout-config-context.d.ts +4 -0
  61. package/dist/es/contexts/layout-config-context.js +5 -0
  62. package/dist/es/contexts/nexus-context.d.ts +29 -0
  63. package/dist/es/contexts/nexus-context.js +12 -0
  64. package/dist/es/hooks/index.d.ts +12 -11
  65. package/dist/es/hooks/index.js +20 -18
  66. package/dist/es/hooks/use-engine.d.ts +2 -0
  67. package/dist/es/hooks/use-engine.js +8 -0
  68. package/dist/es/hooks/use-field-state.d.ts +7 -0
  69. package/dist/es/hooks/use-field-state.js +13 -0
  70. package/dist/es/hooks/use-field-validator.d.ts +40 -0
  71. package/dist/es/hooks/use-field-validator.js +35 -0
  72. package/dist/es/hooks/use-field-value.d.ts +6 -0
  73. package/dist/es/hooks/use-field-value.js +13 -0
  74. package/dist/es/hooks/use-form-config.d.ts +5 -0
  75. package/dist/es/hooks/use-form-config.js +8 -0
  76. package/dist/es/hooks/use-form-data.d.ts +6 -0
  77. package/dist/es/hooks/use-form-data.js +13 -0
  78. package/dist/es/hooks/use-form-submitting.d.ts +7 -0
  79. package/dist/es/hooks/use-form-submitting.js +11 -0
  80. package/dist/es/hooks/use-form.d.ts +22 -0
  81. package/dist/es/hooks/use-form.js +12 -0
  82. package/dist/es/hooks/use-nexus-context.d.ts +11 -0
  83. package/dist/es/hooks/use-nexus-context.js +12 -0
  84. package/dist/es/hooks/use-watch-all.d.ts +19 -0
  85. package/dist/es/hooks/use-watch-all.js +29 -0
  86. package/dist/es/hooks/use-watch-multiple.d.ts +20 -0
  87. package/dist/es/hooks/use-watch-multiple.js +29 -0
  88. package/dist/es/hooks/use-watch-state.d.ts +18 -0
  89. package/dist/es/hooks/use-watch-state.js +19 -0
  90. package/dist/es/hooks/use-watch.d.ts +25 -0
  91. package/dist/es/hooks/use-watch.js +20 -0
  92. package/dist/es/hooks/useForm.d.ts +17 -1
  93. package/dist/es/hooks/useForm.js +9 -6
  94. package/dist/es/hooks/useFormData.js +9 -9
  95. package/dist/es/hooks/useFormSubmitting.d.ts +7 -0
  96. package/dist/es/hooks/useFormSubmitting.js +11 -0
  97. package/dist/es/index.d.ts +26 -24
  98. package/dist/es/index.js +43 -38
  99. package/dist/es/styles.css +1 -0
  100. package/dist/es/utils/omit-nil.d.ts +9 -0
  101. package/dist/es/utils/omit-nil.js +14 -0
  102. package/dist/es/utils/omitNil.d.ts +9 -0
  103. package/dist/es/utils/omitNil.js +14 -0
  104. package/dist/es/utils/persist.d.ts +24 -0
  105. package/dist/es/utils/persist.js +43 -0
  106. package/dist/es/utils/render-tree-node.d.ts +6 -0
  107. package/dist/es/utils/render-tree-node.js +17 -0
  108. package/dist/es/utils/resolve-col-span.d.ts +5 -0
  109. package/dist/es/utils/resolve-col-span.js +9 -0
  110. package/dist/umd/index.css +1 -0
  111. package/dist/umd/index.umd.cjs +1 -1
  112. package/package.json +5 -4
  113. package/src/components/form-controller.ts +584 -0
  114. package/src/components/{NexusField.tsx → nexus-field.tsx} +108 -31
  115. package/src/components/{NexusFormProvider.tsx → nexus-form-provider.tsx} +3 -3
  116. package/src/components/{NexusForm.tsx → nexus-form.tsx} +159 -18
  117. package/src/components/{NexusLayout.tsx → nexus-layout.tsx} +5 -5
  118. package/src/components/{NexusObject.tsx → nexus-object.tsx} +3 -3
  119. package/src/contexts/{NexusContext.ts → nexus-context.ts} +3 -1
  120. package/src/hooks/index.ts +12 -11
  121. package/src/hooks/{useEngine.ts → use-engine.ts} +1 -1
  122. package/src/hooks/{useFieldState.ts → use-field-state.ts} +1 -1
  123. package/src/hooks/{useFieldValidator.ts → use-field-validator.ts} +1 -1
  124. package/src/hooks/{useFieldValue.ts → use-field-value.ts} +1 -1
  125. package/src/hooks/{useFormConfig.ts → use-form-config.ts} +2 -2
  126. package/src/hooks/{useFormData.ts → use-form-data.ts} +4 -4
  127. package/src/hooks/use-form-submitting.ts +16 -0
  128. package/src/hooks/use-form.ts +36 -0
  129. package/src/hooks/{useNexusContext.ts → use-nexus-context.ts} +2 -2
  130. package/src/index.ts +28 -24
  131. package/src/styles.css +6 -0
  132. package/src/utils/omit-nil.ts +24 -0
  133. package/src/utils/persist.ts +88 -0
  134. package/src/utils/{renderTreeNode.tsx → render-tree-node.tsx} +3 -3
  135. package/src/utils/{resolveColSpan.ts → resolve-col-span.ts} +1 -1
  136. package/src/vite-env.d.ts +1 -0
  137. package/src/components/FormController.ts +0 -271
  138. package/src/hooks/useForm.ts +0 -15
  139. /package/src/contexts/{FieldInheritContext.ts → field-inherit-context.ts} +0 -0
  140. /package/src/contexts/{GridContext.ts → grid-context.ts} +0 -0
  141. /package/src/contexts/{LayoutConfigContext.ts → layout-config-context.ts} +0 -0
  142. /package/src/hooks/{useWatchAll.ts → use-watch-all.ts} +0 -0
  143. /package/src/hooks/{useWatchMultiple.ts → use-watch-multiple.ts} +0 -0
  144. /package/src/hooks/{useWatchState.ts → use-watch-state.ts} +0 -0
  145. /package/src/hooks/{useWatch.ts → use-watch.ts} +0 -0
@@ -0,0 +1,36 @@
1
+ import { NexusEngine } from '@xbeeant/form-engine';
2
+ import { useRef } from 'react';
3
+
4
+ import { FormController } from '../components/form-controller';
5
+
6
+ /**
7
+ * useForm — 创建 Form 实例
8
+ *
9
+ * 一个 Form 绑定一个引擎宿主(引擎为独立实体):
10
+ * - 同一 form 引用传给多个 NexusForm → 共享引擎宿主(组件/插件注册共享),
11
+ * 每个 NexusForm 挂载自动获得独立实例(schema/值/订阅互不影响),
12
+ * form 的 API 聚合作用于全部实例
13
+ * - 需要完全独立的表单:各自 useForm()(独立引擎宿主)
14
+ *
15
+ * 支持:
16
+ * - `useForm()`:创建独立实例(引擎宿主内部创建,未注册,不参与跨表单联动)
17
+ * - `useForm(formId)`:创建带 formId 的实例,自动注册到默认表单注册表,
18
+ * 可通过 schema `crossForm` reaction / `engine.linkForm` 与其他表单联动
19
+ * - `useForm(formId, engine)`:复用外部创建的引擎(如 `new NexusEngine({ formId })`),
20
+ * 实现多个 form 共享同一引擎宿主
21
+ *
22
+ * @param formId - 可选,表单实例唯一标识(跨表单联动寻址)
23
+ * @param engine - 可选,外部引擎宿主(缺省内部创建)
24
+ */
25
+ export function useForm(
26
+ formId?: string,
27
+ engine?: NexusEngine,
28
+ ): [FormController] {
29
+ const formRef = useRef<FormController | null>(null);
30
+ if (!formRef.current) {
31
+ formRef.current = new FormController(
32
+ engine ?? (formId ? new NexusEngine({ formId }) : undefined),
33
+ );
34
+ }
35
+ return [formRef.current];
36
+ }
@@ -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
@@ -3,28 +3,32 @@
3
3
  // 导出所有React组件、Hooks和类型定义,供上层应用使用
4
4
  // ============================================================================
5
5
 
6
- export { FormController } from './components/FormController';
7
- export { NexusField } from './components/NexusField';
8
- export type { NexusFormConfig } from './components/NexusForm';
9
- export { NexusForm } from './components/NexusForm';
10
- export { NexusFormProvider } from './components/NexusFormProvider';
11
- export { NexusLayout } from './components/NexusLayout';
12
- export { NexusObject } from './components/NexusObject';
13
- export type { FieldInheritValue } from './contexts/FieldInheritContext';
14
- export { FieldInheritContext } from './contexts/FieldInheritContext';
15
- export type { GridContextValue } from './contexts/GridContext';
16
- export { GridContext } from './contexts/GridContext';
17
- export type { LayoutConfigContextValue } from './contexts/LayoutConfigContext';
18
- export { LayoutConfigContext } from './contexts/LayoutConfigContext';
6
+ import './styles.css';
19
7
 
20
- export { useEngine } from './hooks/useEngine';
21
- export { useFieldState } from './hooks/useFieldState';
22
- export { useFieldValidator } from './hooks/useFieldValidator';
23
- export { useFieldValue } from './hooks/useFieldValue';
24
- export { useForm } from './hooks/useForm';
25
- export { useFormConfig } from './hooks/useFormConfig';
26
- export { useFormData } from './hooks/useFormData';
27
- export { useWatch } from './hooks/useWatch';
28
- export { useWatchAll } from './hooks/useWatchAll';
29
- export { useWatchMultiple } from './hooks/useWatchMultiple';
30
- export { useWatchState } from './hooks/useWatchState';
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';
22
+
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';
package/src/styles.css ADDED
@@ -0,0 +1,6 @@
1
+ /* Renderer 样式入口(Tailwind CSS v4)
2
+ * 仅引入 theme + utilities(不引入 preflight),随构建产物输出为 index.css,
3
+ * 供未接入 Tailwind 的宿主应用使用;已接入 Tailwind 的应用可忽略该文件
4
+ */
5
+ @import "tailwindcss/theme";
6
+ @import "tailwindcss/utilities";
@@ -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
+ }
@@ -1,9 +1,9 @@
1
1
  import type { RenderTreeNode } from '@xbeeant/form-engine';
2
2
  import type { ReactElement } from 'react';
3
3
 
4
- import { NexusField } from '../components/NexusField';
5
- import { NexusLayout } from '../components/NexusLayout';
6
- import { NexusObject } from '../components/NexusObject';
4
+ import { NexusField } from '../components/nexus-field';
5
+ import { NexusLayout } from '../components/nexus-layout';
6
+ import { NexusObject } from '../components/nexus-object';
7
7
 
8
8
  /**
9
9
  * renderTreeNode — 递归渲染
@@ -1,4 +1,4 @@
1
- import type { GridContextValue } from '../contexts/GridContext';
1
+ import type { GridContextValue } from '../contexts/grid-context';
2
2
 
3
3
  /**
4
4
  * resolveColSpan — 统一解析子项在父 Grid 中的跨列数
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -1,271 +0,0 @@
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
- }
@@ -1,15 +0,0 @@
1
- import type { NexusEngine } from '@xbeeant/form-engine';
2
- import { useRef } from 'react';
3
-
4
- import { FormController } from '../components/FormController';
5
-
6
- /**
7
- * useForm — 创建 Form 实例
8
- */
9
- export function useForm(engine?: NexusEngine): [FormController] {
10
- const formRef = useRef<FormController | null>(null);
11
- if (!formRef.current) {
12
- formRef.current = new FormController(engine);
13
- }
14
- return [formRef.current];
15
- }
File without changes
File without changes