form-model-v2.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  1. /**
  2. * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
  3. * SPDX-License-Identifier: MIT
  4. */
  5. import { get, groupBy, isEmpty, isNil, mapKeys, uniq } from 'lodash-es';
  6. import { Disposable, DisposableCollection, Emitter } from '@flowgram.ai/utils';
  7. import {
  8. FlowNodeFormData,
  9. FormFeedback,
  10. FormItem,
  11. FormManager,
  12. FormModel,
  13. FormModelValid,
  14. IFormItem,
  15. NodeFormContext,
  16. OnFormValuesChangePayload,
  17. } from '@flowgram.ai/form-core';
  18. import {
  19. createForm,
  20. FieldArrayModel,
  21. FieldName,
  22. FieldValue,
  23. type FormControl,
  24. FormModel as NativeFormModel,
  25. FormValidateReturn,
  26. Glob,
  27. IField,
  28. IFieldArray,
  29. toForm,
  30. } from '@flowgram.ai/form';
  31. import { FlowNodeEntity } from '@flowgram.ai/document';
  32. import { PlaygroundContext, PluginContext } from '@flowgram.ai/core';
  33. import {
  34. convertGlobPath,
  35. findMatchedInMap,
  36. formFeedbacksToNodeCoreFormFeedbacks,
  37. mergeEffectReturn,
  38. runAndDeleteEffectReturn,
  39. } from './utils';
  40. import {
  41. DataEvent,
  42. Effect,
  43. EffectOptions,
  44. EffectReturn,
  45. FormMeta,
  46. onFormValueChangeInPayload,
  47. } from './types';
  48. import { renderForm } from './form-render';
  49. import { FormPlugin } from './form-plugin';
  50. const DEFAULT = {
  51. // Different formModel should have different reference
  52. EFFECT_MAP: () => ({}),
  53. EFFECT_RETURN_MAP: () =>
  54. new Map([
  55. [DataEvent.onValueInitOrChange, {}],
  56. [DataEvent.onValueChange, {}],
  57. [DataEvent.onValueInit, {}],
  58. [DataEvent.onArrayAppend, {}],
  59. [DataEvent.onArrayDelete, {}],
  60. ]),
  61. FORM_FEEDBACKS: () => [],
  62. VALID: null,
  63. };
  64. export class FormModelV2 extends FormModel implements Disposable {
  65. protected effectMap: Record<string, EffectOptions[]> = DEFAULT.EFFECT_MAP();
  66. protected effectReturnMap: Map<DataEvent, Record<string, EffectReturn>> =
  67. DEFAULT.EFFECT_RETURN_MAP();
  68. protected plugins: FormPlugin[] = [];
  69. protected node: FlowNodeEntity;
  70. protected formFeedbacks: FormValidateReturn | undefined = DEFAULT.FORM_FEEDBACKS();
  71. protected onInitializedEmitter = new Emitter<FormModel>();
  72. protected onValidateEmitter = new Emitter<FormModel>();
  73. readonly onValidate = this.onValidateEmitter.event;
  74. readonly onInitialized = this.onInitializedEmitter.event;
  75. protected onDisposeEmitter = new Emitter<void>();
  76. readonly onDispose = this.onDisposeEmitter.event;
  77. protected toDispose = new DisposableCollection();
  78. protected onFormValuesChangeEmitter = new Emitter<OnFormValuesChangePayload>();
  79. readonly onFormValuesChange = this.onFormValuesChangeEmitter.event;
  80. protected onValidChangeEmitter = new Emitter<FormModelValid>();
  81. readonly onValidChange = this.onValidChangeEmitter.event;
  82. protected onFeedbacksChangeEmitter = new Emitter<FormFeedback[]>();
  83. readonly onFeedbacksChange = this.onFeedbacksChangeEmitter.event;
  84. constructor(node: FlowNodeEntity) {
  85. super();
  86. this.node = node;
  87. this.toDispose.pushAll([
  88. this.onInitializedEmitter,
  89. this.onValidateEmitter,
  90. this.onValidChangeEmitter,
  91. this.onFeedbacksChangeEmitter,
  92. this.onFormValuesChangeEmitter,
  93. ]);
  94. }
  95. protected _valid: FormModelValid = DEFAULT.VALID;
  96. get valid(): FormModelValid {
  97. return this._valid;
  98. }
  99. private set valid(valid: FormModelValid) {
  100. this._valid = valid;
  101. this.onValidChangeEmitter.fire(valid);
  102. }
  103. get flowNodeEntity() {
  104. return this.node;
  105. }
  106. get formManager() {
  107. return this.node.getService(FormManager);
  108. }
  109. protected _formControl?: FormControl<any>;
  110. get formControl() {
  111. return this._formControl;
  112. }
  113. protected _formMeta: FormMeta;
  114. get formMeta(): FormMeta {
  115. return this._formMeta || (this.node.getNodeRegistry().formMeta as FormMeta);
  116. }
  117. get values() {
  118. return this.nativeFormModel?.values;
  119. }
  120. protected _feedbacks: FormFeedback[] = [];
  121. get feedbacks(): FormFeedback[] {
  122. return this._feedbacks;
  123. }
  124. updateFormValues(value: any) {
  125. if (this.nativeFormModel) {
  126. const finalValue = this.formMeta.formatOnInit
  127. ? this.formMeta.formatOnInit(value, this.nodeContext)
  128. : value;
  129. this.nativeFormModel.values = finalValue;
  130. }
  131. }
  132. private set feedbacks(feedbacks: FormFeedback[]) {
  133. this._feedbacks = feedbacks;
  134. this.onFeedbacksChangeEmitter.fire(feedbacks);
  135. }
  136. get formItemPathMap(): Map<string, IFormItem> {
  137. return new Map<string, IFormItem>();
  138. }
  139. protected _initialized: boolean = false;
  140. get initialized(): boolean {
  141. return this._initialized;
  142. }
  143. get nodeContext(): NodeFormContext {
  144. return {
  145. node: this.node,
  146. playgroundContext: this.node.getService(PlaygroundContext),
  147. clientContext: this.node.getService(PluginContext),
  148. };
  149. }
  150. get nativeFormModel(): NativeFormModel | undefined {
  151. return this._formControl?._formModel;
  152. }
  153. render() {
  154. return renderForm(this);
  155. }
  156. initPlugins(plugins: FormPlugin[]) {
  157. if (!plugins.length) {
  158. return;
  159. }
  160. this.plugins = plugins;
  161. plugins.forEach((plugin) => {
  162. plugin.init(this);
  163. });
  164. }
  165. init(formMeta: FormMeta, rawInitialValues?: any) {
  166. /* 透传 onFormValuesChange 事件给 FlowNodeFormData */
  167. const formData = this.node.getData<FlowNodeFormData>(FlowNodeFormData);
  168. this.onFormValuesChange(() => {
  169. this._valid = null;
  170. formData.fireChange();
  171. });
  172. (formMeta.plugins || [])?.forEach((_plugin) => {
  173. if (_plugin.setupFormMeta) {
  174. formMeta = _plugin.setupFormMeta(formMeta, this.nodeContext);
  175. }
  176. });
  177. this._formMeta = formMeta;
  178. const { validateTrigger, validate, effect } = formMeta;
  179. if (effect) {
  180. this.effectMap = effect;
  181. }
  182. // 计算初始值: defaultValues 是默认表单值,不需要被format, 而rawInitialValues 是用户创建form 时传入的初始值,可能不同于表单数据格式,需要被format
  183. const defaultValues =
  184. typeof formMeta.defaultValues === 'function'
  185. ? formMeta.defaultValues(this.nodeContext)
  186. : formMeta.defaultValues;
  187. const initialValues = formMeta.formatOnInit
  188. ? formMeta.formatOnInit(rawInitialValues, this.nodeContext)
  189. : rawInitialValues;
  190. // 初始化底层表单
  191. const { control } = createForm({
  192. initialValues: initialValues || defaultValues,
  193. validateTrigger,
  194. context: this.nodeContext,
  195. validate: validate,
  196. disableAutoInit: true,
  197. });
  198. this._formControl = control;
  199. const nativeFormModel = control._formModel;
  200. this.toDispose.push(nativeFormModel);
  201. // forward onFormValuesChange event
  202. nativeFormModel.onFormValuesChange((props) => {
  203. this.onFormValuesChangeEmitter.fire(props);
  204. });
  205. if (formMeta.plugins) {
  206. this.initPlugins(formMeta.plugins);
  207. }
  208. // Form 数据变更时触发对应的effect
  209. nativeFormModel.onFormValuesChange(({ values, prevValues, name, options }) => {
  210. Object.keys(this.effectMap).forEach((pattern) => {
  211. // 找到匹配 pattern 的数据路径
  212. const paths = uniq([
  213. ...Glob.findMatchPaths(values, pattern),
  214. ...Glob.findMatchPaths(prevValues, pattern),
  215. ]).filter(
  216. (path) =>
  217. // trigger effect by compare if value changed
  218. get(values, path) !== get(prevValues, path)
  219. );
  220. if (Glob.isMatchOrParent(pattern, name)) {
  221. const currentName = Glob.getParentPathByPattern(pattern, name);
  222. if (!paths.includes(currentName)) {
  223. // trigger effect anyway
  224. paths.push(currentName);
  225. }
  226. }
  227. const effectOptionsArr = this.effectMap[pattern];
  228. paths.forEach((path) => {
  229. let eventList = [DataEvent.onValueChange, DataEvent.onValueInitOrChange];
  230. const isPrevNil = isNil(get(prevValues, path));
  231. if (isPrevNil) {
  232. // HACK: For array append, onFormValuesInit will auto triggered for array[index]
  233. if (options?.action === 'array-append' && Glob.isMatch(`${name}.*`, path)) {
  234. eventList = [];
  235. } else {
  236. eventList = [DataEvent.onValueInit, DataEvent.onValueInitOrChange];
  237. }
  238. }
  239. // 对触发 init 事件的 name 或他的字 path 触发 effect
  240. runAndDeleteEffectReturn(this.effectReturnMap, path, eventList);
  241. // 执行该事件配置下所有 onValueChange 事件的 effect
  242. effectOptionsArr.forEach(({ effect, event }: EffectOptions) => {
  243. if (eventList.includes(event)) {
  244. // 执行 effect
  245. const effectReturn = (effect as Effect)({
  246. name: path,
  247. value: get(values, path),
  248. prevValue: get(prevValues, path),
  249. formValues: values,
  250. form: toForm(this.nativeFormModel!),
  251. context: this.nodeContext,
  252. });
  253. // 更新 effect return
  254. if (
  255. effectReturn &&
  256. typeof effectReturn === 'function' &&
  257. this.effectReturnMap.has(event)
  258. ) {
  259. const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;
  260. eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);
  261. }
  262. }
  263. });
  264. });
  265. });
  266. });
  267. // Form 数据初始化时触发对应的 effect
  268. nativeFormModel.onFormValuesInit(({ values, name, prevValues }) => {
  269. Object.keys(this.effectMap).forEach((pattern) => {
  270. // 找到匹配 pattern 的数据路径
  271. const paths = Glob.findMatchPaths(values, pattern);
  272. // 获取配置在该 pattern上的所有effect配置
  273. const effectOptionsArr = this.effectMap[pattern];
  274. paths.forEach((path) => {
  275. if (Glob.isMatchOrParent(name, path) || name === path) {
  276. // 对触发 init 事件的 name 或他的字 path 触发 effect
  277. runAndDeleteEffectReturn(this.effectReturnMap, path, [
  278. DataEvent.onValueInit,
  279. DataEvent.onValueInitOrChange,
  280. ]);
  281. effectOptionsArr.forEach(({ event, effect }: EffectOptions) => {
  282. if (event === DataEvent.onValueInit || event === DataEvent.onValueInitOrChange) {
  283. const effectReturn = (effect as Effect)({
  284. name: path,
  285. value: get(values, path),
  286. formValues: values,
  287. prevValue: get(prevValues, path),
  288. form: toForm(this.nativeFormModel!),
  289. context: this.nodeContext,
  290. });
  291. // 更新 effect return
  292. if (
  293. effectReturn &&
  294. typeof effectReturn === 'function' &&
  295. this.effectReturnMap.has(event)
  296. ) {
  297. const eventMap = this.effectReturnMap.get(event) as Record<string, EffectReturn>;
  298. eventMap[path] = mergeEffectReturn(eventMap[path], effectReturn);
  299. }
  300. }
  301. });
  302. }
  303. });
  304. });
  305. });
  306. // 为 Field 添加 effect, 主要针对array
  307. nativeFormModel.onFieldModelCreate((field) => {
  308. // register effect
  309. const effectOptionsArr = findMatchedInMap<EffectOptions[]>(field, this.effectMap);
  310. if (effectOptionsArr?.length) {
  311. // 按事件聚合
  312. const eventMap = groupBy(effectOptionsArr, 'event');
  313. mapKeys(eventMap, (optionsArr, event) => {
  314. const combinedEffect = (props: any) => {
  315. // 该事件下执行所有effect
  316. optionsArr.forEach(({ effect }) =>
  317. effect({
  318. ...props,
  319. formValues: nativeFormModel.values,
  320. form: toForm(this.nativeFormModel!),
  321. context: this.nodeContext,
  322. })
  323. );
  324. };
  325. switch (event) {
  326. case DataEvent.onArrayAppend:
  327. if (field instanceof FieldArrayModel) {
  328. (field as FieldArrayModel).onAppend(combinedEffect);
  329. }
  330. break;
  331. case DataEvent.onArrayDelete:
  332. if (field instanceof FieldArrayModel) {
  333. (field as FieldArrayModel).onDelete(combinedEffect);
  334. }
  335. break;
  336. }
  337. });
  338. }
  339. });
  340. // 手动初始化form
  341. this._formControl.init();
  342. this._initialized = true;
  343. this.onInitializedEmitter.fire(this);
  344. this.onDispose(() => {
  345. this._initialized = false;
  346. this.effectMap = {};
  347. nativeFormModel.dispose();
  348. });
  349. }
  350. toJSON() {
  351. if (this.formMeta.formatOnSubmit) {
  352. return this.formMeta.formatOnSubmit(this.nativeFormModel?.values, this.nodeContext);
  353. }
  354. return this.nativeFormModel?.values;
  355. }
  356. clearValid() {}
  357. async validate() {
  358. this.formFeedbacks = await this.nativeFormModel?.validate();
  359. this.valid = isEmpty(this.formFeedbacks?.filter((f) => f.level === 'error'));
  360. this.onValidateEmitter.fire(this);
  361. return this.valid;
  362. }
  363. getValues<T = any>(): T | undefined {
  364. return this._formControl?._formModel.values;
  365. }
  366. getField<
  367. TValue = FieldValue,
  368. TField extends IFieldArray<TValue> | IField<TValue> = IField<TValue>
  369. >(name: FieldName): TField | undefined {
  370. let finalName = name.includes('/') ? convertGlobPath(name) : name;
  371. return this.formControl?.getField<TValue, TField>(finalName) as TField;
  372. }
  373. getValueIn<TValue>(name: FieldName): TValue | undefined {
  374. let finalName = name.includes('/') ? convertGlobPath(name) : name;
  375. return this.nativeFormModel?.getValueIn(finalName);
  376. }
  377. setValueIn(name: FieldName, value: any) {
  378. let finalName = name.includes('/') ? convertGlobPath(name) : name;
  379. this.nativeFormModel?.setValueIn(finalName, value);
  380. }
  381. /**
  382. * 监听表单某个路径下的值变化
  383. * @param name 路径
  384. * @param callback 回调函数
  385. */
  386. onFormValueChangeIn<TValue = FieldValue, TFormValue = FieldValue>(
  387. name: FieldName,
  388. callback: (payload: onFormValueChangeInPayload<TValue, TFormValue>) => void
  389. ): Disposable {
  390. if (!this._initialized) {
  391. throw new Error(
  392. `[NodeEngine] FormModel Error: onFormValueChangeIn can not be called before initialized`
  393. );
  394. }
  395. return this.formControl!._formModel.onFormValuesChange(
  396. ({ name: changedName, values, prevValues }) => {
  397. if (changedName === name) {
  398. callback({
  399. value: get(values, name),
  400. prevValue: get(prevValues, name),
  401. formValues: values,
  402. prevFormValues: prevValues,
  403. });
  404. }
  405. }
  406. );
  407. }
  408. /**
  409. * @deprecated 该方法用于兼容 V1 版本 FormModel接口,如果确定是FormModelV2 请使用 FormModel.getValueIn
  410. * @param path glob path
  411. */
  412. getFormItemValueByPath(globPath: string) {
  413. if (!globPath) {
  414. return;
  415. }
  416. if (globPath === '/') {
  417. return this._formControl?._formModel.values;
  418. }
  419. const name = convertGlobPath(globPath);
  420. return this.getValueIn(name!);
  421. }
  422. async validateWithFeedbacks(): Promise<FormFeedback[]> {
  423. await this.validate();
  424. return formFeedbacksToNodeCoreFormFeedbacks(this.formFeedbacks!);
  425. }
  426. /**
  427. * @deprecated 该方法用于兼容 V1 版本 FormModel接口,如果确定是FormModelV2, 请使用FormModel.getValueIn 和 FormModel.setValueIn
  428. * @param path glob path
  429. */
  430. getFormItemByPath(path: string): FormItem | undefined {
  431. if (!this.nativeFormModel) {
  432. return;
  433. }
  434. const that = this;
  435. if (path === '/') {
  436. return {
  437. get value() {
  438. return that.nativeFormModel!.values;
  439. },
  440. set value(v) {
  441. that.nativeFormModel!.values = v;
  442. },
  443. } as FormItem;
  444. }
  445. const name = convertGlobPath(path);
  446. const formItemValue = that.getValueIn(name!);
  447. return {
  448. get value() {
  449. return formItemValue;
  450. },
  451. set value(v) {
  452. that.setValueIn(name, v);
  453. },
  454. } as FormItem;
  455. }
  456. dispose(): void {
  457. this.onDisposeEmitter.fire();
  458. // 执行所有effect return
  459. this.effectReturnMap.forEach((eventMap) => {
  460. Object.values(eventMap).forEach((effectReturn) => {
  461. effectReturn();
  462. });
  463. });
  464. this.effectMap = DEFAULT.EFFECT_MAP();
  465. this.effectReturnMap = DEFAULT.EFFECT_RETURN_MAP();
  466. this.plugins.forEach((p) => {
  467. p.dispose();
  468. });
  469. this.plugins = [];
  470. this.formFeedbacks = DEFAULT.FORM_FEEDBACKS();
  471. this._valid = DEFAULT.VALID;
  472. this._formControl = undefined;
  473. this._initialized = false;
  474. this.toDispose.dispose();
  475. }
  476. }