form-model-v2.ts 15 KB

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