@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,123 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // useFieldValidator — 在 widget 组件内命令式注册字段校验规则
3
+ //
4
+ // 对齐 x-render 子表单内部校验思路:校验逻辑写在 UI 组件内部,
5
+ // 而非全部下沉到 Schema。组件通过自身 dataPath + form 实例注册校验器,
6
+ // 校验器可访问组件闭包内状态(实现与组件 state 联动)。
7
+ //
8
+ // 使用:
9
+ // ```tsx
10
+ // export const confirmPasswordWidget = withFormItem((props) => {
11
+ // const { dataPath, form, value } = props;
12
+ // const [strict, setStrict] = useState(false);
13
+ // useFieldValidator(form, dataPath, (val, formData) => {
14
+ // if (strict && val && val !== formData.password) {
15
+ // return ['两次输入的密码不一致'];
16
+ // }
17
+ // return [];
18
+ // }, { dependsOn: ['password'], deps: [strict] });
19
+ // const { dependValues: _dv, path: _p, ...rest } = props;
20
+ // return <Input.Password value={value} {...rest} />;
21
+ // });
22
+ // ```
23
+ // ────────────────────────────────────────────────────────────────────────────
24
+
25
+ import type { NexusEngine, NexusFormInstance } from '@xbeeant/form-engine';
26
+ import { useEffect, useRef } from 'react';
27
+ import { useNexusContext } from '../contexts/NexusContext';
28
+
29
+ export type FieldValidator = (
30
+ value: unknown,
31
+ formData: Record<string, unknown>,
32
+ ) => string[] | Promise<string[]>;
33
+
34
+ export interface UseFieldValidatorOptions {
35
+ /**
36
+ * 依赖字段路径列表。这些字段变化时自动对目标字段重校验,
37
+ * 实现「组件内注册的跨字段校验」与依赖字段的实时联动
38
+ * (等价于 schema 中 validate 表达式的依赖图联动)。
39
+ */
40
+ dependsOn?: string[];
41
+ /**
42
+ * 校验器闭包依赖的组件 state(useEffect 语义)。
43
+ * 任意依赖值变化时,校验器会重新注册,新闭包捕获最新的组件 state,
44
+ * 保证校验逻辑读到的是当前渲染的 state,而不是注册时刻的陈旧值。
45
+ *
46
+ * 用法:
47
+ * ```tsx
48
+ * const [strict, setStrict] = useState(false);
49
+ * useFieldValidator(form, dataPath, (val, formData) => {
50
+ * if (strict && val && val !== formData.password) { ... }
51
+ * }, { deps: [strict] });
52
+ * ```
53
+ */
54
+ deps?: ReadonlyArray<unknown>;
55
+ }
56
+
57
+ /**
58
+ * 在 widget 组件内注册字段校验规则
59
+ *
60
+ * - 挂载时调用 form.registerValidator(path, validator)
61
+ * - 卸载时调用 form.unregisterValidator 清理,避免校验器累积
62
+ * - validator 通过 ref 持有最新闭包;deps 变化时 effect 重跑,
63
+ * 重新注册捕获最新组件 state 的新闭包(useEffect 语义),
64
+ * 校验逻辑总能读到当前渲染的组件 state
65
+ * - 传入 dependsOn 时,订阅依赖字段变化并实时重校验目标字段
66
+ *
67
+ * @param form - 表单实例(widget 的 props.form)
68
+ * @param path - 字段路径(widget 的 props.dataPath)
69
+ * @param validator - 校验函数,返回错误消息数组(空数组 = 通过)
70
+ * @param options - 配置(dependsOn 依赖字段联动 / deps 闭包依赖的组件 state)
71
+ */
72
+ export function useFieldValidator(
73
+ form: NexusFormInstance | undefined,
74
+ path: string | undefined,
75
+ validator: FieldValidator,
76
+ options?: UseFieldValidatorOptions,
77
+ ): void {
78
+ const { engine } = useNexusContext();
79
+
80
+ // 用 ref 保存最新 validator/options,避免外部 inline 函数导致每次渲染重注册
81
+ const validatorRef = useRef(validator);
82
+ validatorRef.current = validator;
83
+ const dependsRef = useRef(options?.dependsOn);
84
+ dependsRef.current = options?.dependsOn;
85
+ const deps = options?.deps;
86
+
87
+ // 注册 / 注销校验器
88
+ // deps 变化(useEffect 语义)时 effect 重跑:unregister 旧闭包 → register 新闭包,
89
+ // 新闭包捕获 deps 中声明的最新组件 state。
90
+ useEffect(() => {
91
+ if (!form || !path) {
92
+ return;
93
+ }
94
+ const current = validatorRef.current;
95
+ form.registerValidator(path, current);
96
+ return () => {
97
+ form.unregisterValidator(path, current);
98
+ };
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps
100
+ }, [form, path, ...(deps ?? [])]);
101
+
102
+ // 订阅依赖字段变化 → 实时重校验目标字段(跨字段联动)
103
+ useEffect(() => {
104
+ if (!path || !dependsRef.current || dependsRef.current.length === 0) {
105
+ return;
106
+ }
107
+ const engineSafe = engine as NexusEngine | undefined;
108
+ if (!engineSafe) {
109
+ return;
110
+ }
111
+ const target = path;
112
+ const unsubscribers = dependsRef.current.map((dep) =>
113
+ engineSafe.subscribeField(dep, () => {
114
+ form?.revalidateField(target);
115
+ }),
116
+ );
117
+ return () => {
118
+ for (const unsub of unsubscribers) {
119
+ unsub();
120
+ }
121
+ };
122
+ }, [engine, form, path]);
123
+ }
@@ -0,0 +1,20 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ import { useEngine } from './useEngine';
4
+
5
+ /**
6
+ * useFieldValue — 精准订阅单个字段值
7
+ *
8
+ * 按路径精准订阅:仅该字段版本变化时重渲染
9
+ */
10
+ export function useFieldValue<T = unknown>(path: string): T | undefined {
11
+ const engine = useEngine();
12
+
13
+ useSyncExternalStore(
14
+ (onStoreChange) => engine.subscribeField(path, onStoreChange),
15
+ () => engine.getFieldVersion(path),
16
+ () => engine.getFieldVersion(path),
17
+ );
18
+
19
+ return engine.getFieldValue(path) as T | undefined;
20
+ }
@@ -0,0 +1,15 @@
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
+ }
@@ -0,0 +1,10 @@
1
+ import type { NexusFormConfig } from '../components/NexusForm';
2
+ import { useNexusContext } from '../contexts/NexusContext';
3
+
4
+ /**
5
+ * useFormConfig — 获取表单布局配置
6
+ */
7
+ export function useFormConfig(): NexusFormConfig {
8
+ const { config } = useNexusContext();
9
+ return config;
10
+ }
@@ -0,0 +1,20 @@
1
+ import { useMemo, useSyncExternalStore } from 'react';
2
+
3
+ import { useEngine } from './useEngine';
4
+
5
+ /**
6
+ * useFormData — 订阅整个表单数据
7
+ *
8
+ * 用 version 作为快照依赖,避免 getFormData() 每次返回新对象引用导致的无谓重渲染
9
+ */
10
+ export function useFormData(): Record<string, unknown> {
11
+ const engine = useEngine();
12
+
13
+ const _version = useSyncExternalStore(
14
+ engine.subscribeStore,
15
+ engine.getSnapshot,
16
+ engine.getSnapshot,
17
+ );
18
+ // biome-ignore lint/correctness/useExhaustiveDependencies: _version 是 formData 失效信号(engine 内部状态,静态分析不可见)
19
+ return useMemo(() => engine.getFormData(), [engine, _version]);
20
+ }
@@ -0,0 +1,24 @@
1
+ // ────────────────────────────────────────────────────────────────────────────
2
+ // Context
3
+ // ────────────────────────────────────────────────────────────────────────────
4
+
5
+ import type { NexusEngine } from '@xbeeant/form-engine';
6
+ import { createContext, useContext } from 'react';
7
+ import type { FormController } from '../components/FormController';
8
+ import type { NexusFormConfig } from '../components/NexusForm';
9
+
10
+ interface NexusContextValue {
11
+ engine: NexusEngine;
12
+ config: NexusFormConfig;
13
+ form: FormController;
14
+ }
15
+
16
+ export const NexusContext = createContext<NexusContextValue | null>(null);
17
+
18
+ export function useNexusContext(): NexusContextValue {
19
+ const ctx = useContext(NexusContext);
20
+ if (!ctx) {
21
+ throw new Error('[NexusField] Must be used within <NexusFormProvider>');
22
+ }
23
+ return ctx;
24
+ }
@@ -0,0 +1,83 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ /**
5
+ * useWatch Hook - 监听单个字段值变化
6
+ *
7
+ * @param engine - NexusEngine实例
8
+ * @param path - 字段路径
9
+ * @param callback - 变化回调函数
10
+ * @param deep - 是否深度比较值(默认false,仅浅比较)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // 监听单个字段
15
+ * const name = useWatch(engine, 'profile.name', (value) => {
16
+ * console.log('Name changed:', value);
17
+ * });
18
+ *
19
+ * // 深度比较对象
20
+ * const profile = useWatch(engine, 'profile', (value) => {
21
+ * console.log('Profile changed:', value);
22
+ * }, { deep: true });
23
+ * ```
24
+ */
25
+ export function useWatch(
26
+ engine: NexusEngine | null,
27
+ path: string,
28
+ callback: (value: unknown) => void,
29
+ options?: {
30
+ deep?: boolean;
31
+ },
32
+ ): unknown {
33
+ const { deep = false } = options || {};
34
+ const lastValueRef = useRef<unknown>(undefined);
35
+
36
+ // callback 通过 ref 持有最新引用:外部 inline 回调不会导致每次渲染重新订阅
37
+ const callbackRef = useRef(callback);
38
+ callbackRef.current = callback;
39
+
40
+ useEffect(() => {
41
+ if (!engine) {
42
+ return;
43
+ }
44
+
45
+ const value = engine.getFieldValue(path);
46
+
47
+ // 初始化时立即执行一次
48
+ if (lastValueRef.current === undefined) {
49
+ lastValueRef.current = value;
50
+ }
51
+
52
+ // 比较值是否变化
53
+ const shouldCall = deep
54
+ ? JSON.stringify(value) !== JSON.stringify(lastValueRef.current)
55
+ : value !== lastValueRef.current;
56
+
57
+ if (shouldCall) {
58
+ lastValueRef.current = value;
59
+ callbackRef.current(value);
60
+ }
61
+
62
+ // 订阅字段变化
63
+ const unsubscribe = engine.subscribe(path, (state) => {
64
+ const newValue = state.value;
65
+
66
+ // 比较值是否变化
67
+ const shouldUpdate = deep
68
+ ? JSON.stringify(newValue) !== JSON.stringify(lastValueRef.current)
69
+ : newValue !== lastValueRef.current;
70
+
71
+ if (shouldUpdate) {
72
+ lastValueRef.current = newValue;
73
+ callbackRef.current(newValue);
74
+ }
75
+ });
76
+
77
+ return () => {
78
+ unsubscribe();
79
+ };
80
+ }, [engine, path, deep]);
81
+
82
+ return lastValueRef.current;
83
+ }
@@ -0,0 +1,77 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ /**
5
+ * useWatchAll Hook - 监听整个表单数据变化
6
+ *
7
+ * @param engine - NexusEngine实例
8
+ * @param callback - 变化回调函数
9
+ * @param deep - 是否深度比较值(默认false,仅浅比较)
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // 监听整个表单
14
+ * useWatchAll(engine, (formData) => {
15
+ * console.log('Form data changed:', formData);
16
+ * }, { deep: true });
17
+ * ```
18
+ */
19
+ export function useWatchAll(
20
+ engine: NexusEngine | null,
21
+ callback: (formData: Record<string, unknown>) => void,
22
+ options?: {
23
+ deep?: boolean;
24
+ },
25
+ ): Record<string, unknown> {
26
+ const { deep = false } = options || {};
27
+ const lastFormDataRef = useRef<Record<string, unknown>>({});
28
+ const formDataRef = useRef<Record<string, unknown>>({});
29
+
30
+ // callback 通过 ref 持有最新引用:外部 inline 回调不会导致每次渲染重新订阅
31
+ const callbackRef = useRef(callback);
32
+ callbackRef.current = callback;
33
+
34
+ useEffect(() => {
35
+ if (!engine) {
36
+ return;
37
+ }
38
+
39
+ const formData = engine.getFormData();
40
+ lastFormDataRef.current = formData;
41
+ formDataRef.current = formData;
42
+ callbackRef.current(formData);
43
+
44
+ const unsubscribe = engine.subscribeAll((newFormData) => {
45
+ if (deep) {
46
+ const changed =
47
+ JSON.stringify(newFormData) !==
48
+ JSON.stringify(lastFormDataRef.current);
49
+ if (changed) {
50
+ lastFormDataRef.current = newFormData;
51
+ formDataRef.current = newFormData;
52
+ callbackRef.current(newFormData);
53
+ }
54
+ } else {
55
+ // 简单浅比较
56
+ let changed = false;
57
+ for (const key in newFormData) {
58
+ if (newFormData[key] !== lastFormDataRef.current[key]) {
59
+ changed = true;
60
+ break;
61
+ }
62
+ }
63
+ if (changed) {
64
+ lastFormDataRef.current = newFormData;
65
+ formDataRef.current = newFormData;
66
+ callbackRef.current(newFormData);
67
+ }
68
+ }
69
+ });
70
+
71
+ return () => {
72
+ unsubscribe();
73
+ };
74
+ }, [engine, deep]);
75
+
76
+ return formDataRef.current;
77
+ }
@@ -0,0 +1,86 @@
1
+ import type { NexusEngine } from '@xbeeant/form-engine';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ /**
5
+ * useWatchMultiple Hook - 监听多个字段值变化
6
+ *
7
+ * @param engine - NexusEngine实例
8
+ * @param paths - 字段路径数组
9
+ * @param callback - 变化回调函数
10
+ * @param deep - 是否深度比较值(默认false,仅浅比较)
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * // 监听多个字段
15
+ * const values = useWatchMultiple(engine, ['name', 'email', 'age'], (values) => {
16
+ * console.log('Values changed:', values);
17
+ * }, { deep: true });
18
+ * ```
19
+ */
20
+ export function useWatchMultiple(
21
+ engine: NexusEngine | null,
22
+ paths: string[],
23
+ callback: (values: Record<string, unknown>) => void,
24
+ options?: {
25
+ deep?: boolean;
26
+ },
27
+ ): Record<string, unknown> {
28
+ const { deep = false } = options || {};
29
+ const lastValuesRef = useRef<Record<string, unknown>>({});
30
+ const valuesRef = useRef<Record<string, unknown>>({});
31
+
32
+ // callback / paths 通过 ref 持有最新引用:
33
+ // - inline 回调不会导致每次渲染重新订阅
34
+ // - 数组字面量(每次渲染新建引用)不会导致重复订阅
35
+ const callbackRef = useRef(callback);
36
+ callbackRef.current = callback;
37
+ const pathsRef = useRef(paths);
38
+ pathsRef.current = paths;
39
+ // 仅当路径列表内容变化时才重新订阅(顺序无关,排序后比较)
40
+ const pathsKey = [...paths].sort().join(',');
41
+
42
+ // pathsKey:路径列表内容变化的稳定信号(数组引用本身不稳定,不参与依赖比较)
43
+ // biome-ignore lint/correctness/useExhaustiveDependencies: paths 引用不稳定,以内容键 pathsKey 为准
44
+ useEffect(() => {
45
+ if (!engine) {
46
+ return;
47
+ }
48
+
49
+ // 初始化
50
+ const watchedPaths = pathsRef.current;
51
+ const initialValues: Record<string, unknown> = {};
52
+ for (const path of watchedPaths) {
53
+ initialValues[path] = engine.getFieldValue(path);
54
+ }
55
+ lastValuesRef.current = initialValues;
56
+ valuesRef.current = initialValues;
57
+
58
+ callbackRef.current(initialValues);
59
+
60
+ // 按路径精准订阅(依赖图 O(k) 通知),避免 subscribeAll 全表单扫描
61
+ const unsubscribe = watchedPaths.map((path) =>
62
+ engine.subscribe(path, (state) => {
63
+ const newValue = state.value;
64
+ const lastValue = lastValuesRef.current[path];
65
+
66
+ const changed = deep
67
+ ? JSON.stringify(newValue) !== JSON.stringify(lastValue)
68
+ : newValue !== lastValue;
69
+
70
+ if (changed) {
71
+ lastValuesRef.current[path] = newValue;
72
+ valuesRef.current = { [path]: newValue };
73
+ callbackRef.current(valuesRef.current);
74
+ }
75
+ }),
76
+ );
77
+
78
+ return () => {
79
+ for (const unsub of unsubscribe) {
80
+ unsub();
81
+ }
82
+ };
83
+ }, [engine, deep, pathsKey]);
84
+
85
+ return valuesRef.current;
86
+ }
@@ -0,0 +1,54 @@
1
+ import type { FieldState, NexusEngine } from '@xbeeant/form-engine';
2
+ import { useEffect, useRef } from 'react';
3
+
4
+ /**
5
+ * useWatchState Hook - 监听单个字段状态变化
6
+ *
7
+ * @param engine - NexusEngine实例
8
+ * @param path - 字段路径
9
+ * @param callback - 状态变化回调函数
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // 监听字段状态(visible/disabled/loading等)
14
+ * const isDisabled = useWatchState(engine, 'email', (state) => {
15
+ * console.log('Disabled:', state.disabled);
16
+ * console.log('Loading:', state.loading);
17
+ * });
18
+ * ```
19
+ */
20
+ export function useWatchState(
21
+ engine: NexusEngine | null,
22
+ path: string,
23
+ callback: (state: FieldState) => void,
24
+ ): FieldState {
25
+ const stateRef = useRef<FieldState | undefined>(undefined);
26
+
27
+ // callback 通过 ref 持有最新引用:外部 inline 回调不会导致每次渲染重新订阅
28
+ const callbackRef = useRef(callback);
29
+ callbackRef.current = callback;
30
+
31
+ useEffect(() => {
32
+ if (!engine) {
33
+ return;
34
+ }
35
+
36
+ const state = engine.getFieldState(path);
37
+ stateRef.current = state;
38
+
39
+ if (state) {
40
+ callbackRef.current(state);
41
+ }
42
+
43
+ const unsubscribe = engine.subscribe(path, (newState) => {
44
+ stateRef.current = newState;
45
+ callbackRef.current(newState);
46
+ });
47
+
48
+ return () => {
49
+ unsubscribe();
50
+ };
51
+ }, [engine, path]);
52
+
53
+ return stateRef.current || ({} as FieldState);
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ // ============================================================================
2
+ // React包入口文件
3
+ // 导出所有React组件、Hooks和类型定义,供上层应用使用
4
+ // ============================================================================
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';
19
+
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';
@@ -0,0 +1,30 @@
1
+ import type { RenderTreeNode } from '@xbeeant/form-engine';
2
+ import type { ReactElement } from 'react';
3
+
4
+ import { NexusField } from '../components/NexusField';
5
+ import { NexusLayout } from '../components/NexusLayout';
6
+ import { NexusObject } from '../components/NexusObject';
7
+
8
+ /**
9
+ * renderTreeNode — 递归渲染
10
+ */
11
+ export function renderTreeNode(
12
+ node: RenderTreeNode,
13
+ index: number,
14
+ ): ReactElement {
15
+ if (node.type === 'field') {
16
+ return (
17
+ <NexusField
18
+ key={node.layoutKey || node.dataPath}
19
+ dataPath={node.dataPath}
20
+ layoutKey={node.layoutKey}
21
+ />
22
+ );
23
+ }
24
+ if (node.type === 'object') {
25
+ return (
26
+ <NexusObject key={`object-${node.layoutKey}-${index}`} node={node} />
27
+ );
28
+ }
29
+ return <NexusLayout key={`layout-${node.type}-${index}`} node={node} />;
30
+ }
@@ -0,0 +1,18 @@
1
+ import type { GridContextValue } from '../contexts/GridContext';
2
+
3
+ /**
4
+ * resolveColSpan — 统一解析子项在父 Grid 中的跨列数
5
+ */
6
+ export function resolveColSpan(
7
+ colSpan: number | undefined,
8
+ gridCtx: GridContextValue | null,
9
+ ): number | undefined {
10
+ if (colSpan !== undefined) {
11
+ return colSpan;
12
+ }
13
+
14
+ // 在 grid 容器内:把 24 栅格语义缩放到当前 column
15
+ if (gridCtx && gridCtx.column > 0) {
16
+ return Math.max(1, Math.round(gridCtx.column / 24));
17
+ }
18
+ }