2
0

form-model-v2.ts 15 KB

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