form-model-v2.ts 15 KB

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