1
0

foundation.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  1. /* eslint-disable prefer-const, max-len */
  2. import BaseFoundation from '../base/foundation';
  3. import * as ObjectUtil from '../utils/object';
  4. import isPromise from '../utils/isPromise';
  5. import { isValid } from './utils';
  6. import { isUndefined, isFunction, toPath } from 'lodash';
  7. import scrollIntoView, { Options as scrollIntoViewOptions } from 'scroll-into-view-if-needed';
  8. import { BaseFormAdapter, FormState, CallOpts, FieldState, FieldStaff, ComponentProps, setValuesConfig, ArrayFieldStaff } from './interface';
  9. export { BaseFormAdapter };
  10. export default class FormFoundation extends BaseFoundation<BaseFormAdapter> {
  11. data: FormState;
  12. fields: Map<string, FieldStaff>;
  13. registered: Record<string, boolean>;
  14. registeredArrayField: Map<string, ArrayFieldStaff>;
  15. constructor(adapter: BaseFormAdapter) {
  16. super({ ...adapter });
  17. /*
  18. Also need to read initValue here, because the Form level can set the initial value,
  19. and the Field level can also set the initial value.
  20. The field set in the Form does not necessarily have a Field entity,
  21. so you cannot completely rely on the register moment to set the initial value
  22. 这里也需要读一次initValue,因为Form级别可设置初始值,Field级别也可设置初始值.
  23. Form中设置的字段,不一定会存在Field实体,所以不能完全依赖register时刻来设置初始值
  24. */
  25. let { initValues } = this._adapter.getProps();
  26. initValues = this._adapter.cloneDeep(initValues);
  27. this.data = {
  28. values: initValues ? initValues : {},
  29. errors: {},
  30. touched: {},
  31. // invalid: false,
  32. // dirty: false,
  33. };
  34. // Map store all fields
  35. // key: fieldName
  36. // value: { field, fieldApi, keepState, initValue}
  37. this.fields = new Map();
  38. // Record all registered fields
  39. this.registered = {};
  40. // Record all registered ArrayField
  41. this.registeredArrayField = new Map();
  42. this.register = this.register.bind(this);
  43. this.unRegister = this.unRegister.bind(this);
  44. this.registerArrayField = this.registerArrayField.bind(this);
  45. this.unRegisterArrayField = this.unRegisterArrayField.bind(this);
  46. this.getArrayField = this.getArrayField.bind(this);
  47. this.updateArrayField = this.updateArrayField.bind(this);
  48. this.getField = this.getField.bind(this);
  49. this.setValues = this.setValues.bind(this);
  50. this.updateStateValue = this.updateStateValue.bind(this);
  51. this.updateStateError = this.updateStateError.bind(this);
  52. this.updateStateTouched = this.updateStateTouched.bind(this);
  53. this.getFormState = this.getFormState.bind(this);
  54. this.getValue = this.getValue.bind(this);
  55. this.getError = this.getError.bind(this);
  56. this.getTouched = this.getTouched.bind(this);
  57. this.getInitValues = this.getInitValues.bind(this);
  58. this.getInitValue = this.getInitValue.bind(this);
  59. this.getFormProps = this.getFormProps.bind(this);
  60. this.getFieldExist = this.getFieldExist.bind(this);
  61. this.scrollToField = this.scrollToField.bind(this);
  62. }
  63. getField(field: string): FieldStaff | undefined {
  64. const targetField = this.fields.get(field);
  65. return targetField;
  66. }
  67. register(field: string, fieldState: FieldState, fieldStuff: FieldStaff): void {
  68. // determine if this field has been register before
  69. const registered = this.registered[field];
  70. this.registered[field] = true;
  71. this.fields.set(field, fieldStuff);
  72. if (fieldStuff.keepState) {
  73. // TODO support keepState
  74. } else {
  75. const allowEmpty = fieldStuff.allowEmpty || false;
  76. const opts = {
  77. notNotify: true,
  78. notUpdate: false,
  79. allowEmpty,
  80. };
  81. let fieldValue = fieldState.value;
  82. // When allowEmpty is false, 'is equivalent to undefined, and the key of the field does not need to be reflected on values
  83. if (!allowEmpty && fieldValue === '') {
  84. fieldValue = undefined;
  85. }
  86. this.updateStateValue(field, fieldValue, opts);
  87. if (fieldState.error) {
  88. this.updateStateError(field, fieldState.error, opts);
  89. }
  90. }
  91. // this.log(this.fields);
  92. }
  93. unRegister(field: string): void {
  94. const targetField = this.fields.get(field);
  95. // delete data
  96. try {
  97. if (!targetField.keepState) {
  98. ObjectUtil.remove(this.data.values, field);
  99. ObjectUtil.remove(this.data.errors, field);
  100. ObjectUtil.remove(this.data.touched, field);
  101. }
  102. } catch (error) {
  103. console.error(`some thing wrong when unregister field:${field}`);
  104. }
  105. // delete field
  106. this.fields.delete(field);
  107. this._adapter.notifyChange(this.data);
  108. this._adapter.forceUpdate();
  109. }
  110. // in order to slove byted-issue-289
  111. registerArrayField(arrayFieldPath: string, val: any): void {
  112. this.updateArrayField(arrayFieldPath, {
  113. updateKey: new Date().valueOf(),
  114. initValue: val
  115. });
  116. }
  117. unRegisterArrayField(arrayField: string): void {
  118. this.registeredArrayField.delete(arrayField);
  119. }
  120. getArrayField(arrayField: string): ArrayFieldStaff {
  121. return this.registeredArrayField.get(arrayField);
  122. }
  123. updateArrayField(arrayField: string, updateValue: any): void {
  124. const mergeVal = { ...this.registeredArrayField.get(arrayField), ...updateValue };
  125. this.registeredArrayField.set(arrayField, mergeVal);
  126. }
  127. validate(fieldPaths?: Array<string>): Promise<unknown> {
  128. const { validateFields } = this.getProps();
  129. if (validateFields && isFunction(validateFields)) {
  130. return this._formValidate();
  131. } else {
  132. return this._fieldsValidate(fieldPaths);
  133. }
  134. }
  135. // form level validate
  136. _formValidate(): Promise<unknown> {
  137. const { values } = this.data;
  138. const { validateFields } = this.getProps();
  139. return new Promise((resolve, reject) => {
  140. let maybePromisedErrors;
  141. try {
  142. maybePromisedErrors = validateFields(values);
  143. } catch (errors) {
  144. // error throw by sync validate directly
  145. maybePromisedErrors = errors;
  146. }
  147. if (!maybePromisedErrors) {
  148. resolve(values);
  149. this.injectErrorToField({});
  150. } else if (isPromise(maybePromisedErrors)) {
  151. maybePromisedErrors.then(
  152. (result: any) => {
  153. // validate success,clear error
  154. if (!result) {
  155. resolve(values);
  156. this.injectErrorToField({});
  157. } else {
  158. this.data.errors = result;
  159. this._adapter.notifyChange(this.data);
  160. this.injectErrorToField(result);
  161. this._adapter.forceUpdate();
  162. this._autoScroll(100);
  163. reject(result);
  164. }
  165. },
  166. (errors: any) => {
  167. // validate failed
  168. // this._adapter.notifyChange(this.data);
  169. this._autoScroll(100);
  170. reject(errors);
  171. }
  172. );
  173. } else {
  174. // TODO: current design, returning an empty object will be considered a checksum failure and will be rejected. Only returning an empty string will be considered a success, consider resetting it in 1.0?
  175. this.data.errors = maybePromisedErrors;
  176. this.injectErrorToField(maybePromisedErrors);
  177. this._adapter.notifyChange(this.data);
  178. this._adapter.forceUpdate();
  179. this._autoScroll(100);
  180. reject(maybePromisedErrors);
  181. }
  182. });
  183. }
  184. // field level validate
  185. _fieldsValidate(fieldPaths: Array<string>): Promise<unknown> {
  186. const { values } = this.data;
  187. // When there is no custom validation function at Form level, perform validation of each Field
  188. return new Promise((resolve, reject) => {
  189. let promiseSet: Promise<any>[] = [];
  190. const targetFields = this._getOperateFieldMap(fieldPaths);
  191. targetFields.forEach((field, fieldPath) => {
  192. // Call each fieldApi for verification
  193. const fieldValue = this.getValue(fieldPath);
  194. // When centralized verification, no need to trigger forceUpdate and notify
  195. const opts = {
  196. notNotify: true,
  197. notUpdate: true,
  198. };
  199. const validateResult = field.fieldApi.validate(fieldValue, opts);
  200. promiseSet.push(validateResult);
  201. field.fieldApi.setTouched(true, opts);
  202. });
  203. Promise.all(promiseSet).then(() => {
  204. // After the centralized verification is completed, trigger notify and forceUpdate once.
  205. this._adapter.notifyChange(this.data);
  206. this._adapter.forceUpdate();
  207. const errors = this.getError();
  208. if (this._isValid(targetFields)) {
  209. resolve(values);
  210. } else {
  211. this._autoScroll();
  212. reject(errors);
  213. }
  214. });
  215. });
  216. }
  217. submit(): void {
  218. const { values } = this.data;
  219. // validate form
  220. this.validate()
  221. .then((resolveValues: any) => {
  222. // if valid do submit
  223. const _values = this._adapter.cloneDeep(resolveValues);
  224. this._adapter.notifySubmit(_values);
  225. })
  226. .catch(errors => {
  227. const _errors = this._adapter.cloneDeep(errors);
  228. const _values = this._adapter.cloneDeep(values);
  229. this._adapter.notifySubmitFail(_errors, _values);
  230. });
  231. }
  232. /**
  233. * Case A:
  234. * All fields: a[0]、a[1]、b.type、b.name[2]、b.name[0]
  235. * input => output:
  236. * a => a[0]、a[1]
  237. * b => b.type、b.name[0]、b.name[2]
  238. *
  239. * Case B:
  240. * All fields: activity.a[0]、activity.a[1]、activity.c、activity.d、other
  241. * input => output:
  242. * activity.a => activity.a[0]、activity.a[1]
  243. *
  244. */
  245. _getNestedField(path: string): Map<string, FieldStaff> {
  246. const allRegisterField = this.fields;
  247. const allFieldPath = [...allRegisterField].map(item => item[0]);
  248. let nestedFieldPath = new Map();
  249. allFieldPath.forEach(item => {
  250. let itemPath = toPath(item);
  251. let targetPath = toPath(path);
  252. if (targetPath.every((path, i) => (targetPath[i] === itemPath[i]))) {
  253. const realField = allRegisterField.get(item);
  254. nestedFieldPath.set(item, realField);
  255. }
  256. });
  257. return nestedFieldPath;
  258. }
  259. // get all operate fields, called by validate() / reset()
  260. _getOperateFieldMap(fieldPaths?: Array<string>): Map<string, FieldStaff> {
  261. let targetFields = new Map();
  262. if (!isUndefined(fieldPaths)) {
  263. // reset or validate spcific fields
  264. fieldPaths.forEach(path => {
  265. const field = this.fields.get(path);
  266. // may be undefined, if exists two fields like 'a[0]'、'a[1]', but user directly call reset(['a']) / validate(['a'])
  267. if (isUndefined(field)) {
  268. const nestedFields = this._getNestedField(path);
  269. targetFields = new Map([...targetFields, ...nestedFields]);
  270. } else {
  271. targetFields.set(path, field);
  272. }
  273. });
  274. } else {
  275. // reset or validate all fields
  276. targetFields = this.fields;
  277. }
  278. return targetFields;
  279. }
  280. // Reset the entire form, reset all fields and remove validation results
  281. reset(fieldPaths?: Array<string>): void {
  282. const targetFields = this._getOperateFieldMap(fieldPaths);
  283. targetFields.forEach(field => {
  284. field.fieldApi.reset();
  285. });
  286. if (this.registeredArrayField.size) {
  287. this._resetArrayField();
  288. }
  289. this._adapter.notifyChange(this.data);
  290. this._adapter.forceUpdate();
  291. this._adapter.notifyReset();
  292. }
  293. _resetArrayField(): void {
  294. /*
  295. When Reset, arrayField needs to be processed separately. Restore the key/value of arrayField in formState according to the initial value
  296. Update the key inside the arrayField to make it actively renderer
  297. Reset时,arrayField需要单独处理, 根据初始值还原 arrayField在formState中的key/value, 更新 arrayField内部的key,使其主动rerender
  298. */
  299. const arrayFieldPaths = [...this.registeredArrayField.keys()];
  300. arrayFieldPaths.forEach(path => {
  301. const arrayFieldState = this.registeredArrayField.get(path);
  302. const arrayFieldInitValue = arrayFieldState.initValue;
  303. this.updateStateValue(path, arrayFieldInitValue, { notNotify: true, notUpdate: true });
  304. this.updateArrayField(path, { updateKey: new Date().valueOf() });
  305. });
  306. }
  307. // After calling the form's custom validateFields function, reject the returned error to the corresponding field
  308. // 调用了Form的自定义validateFields函数后,将返回的错误展示到对应的field中
  309. injectErrorToField(errors: any): void {
  310. this.fields.forEach(field => {
  311. const fieldError = ObjectUtil.get(errors, field.field);
  312. const opts = {
  313. notNotify: true,
  314. notUpdate: true,
  315. };
  316. field.fieldApi.setError(fieldError, opts);
  317. });
  318. }
  319. getValue(field: string | undefined, opts?: CallOpts): any {
  320. const isAllField = typeof field === 'undefined';
  321. const needClone = opts && opts.needClone;
  322. let result, fieldValue;
  323. switch (true) {
  324. case !isAllField && !needClone:
  325. result = ObjectUtil.get(this.data.values, field);
  326. break;
  327. case !isAllField && needClone:
  328. fieldValue = ObjectUtil.get(this.data.values, field);
  329. result = this._adapter.cloneDeep(fieldValue);
  330. break;
  331. case isAllField && !needClone:
  332. result = { ...this.data.values };
  333. break;
  334. case isAllField && needClone:
  335. result = this._adapter.cloneDeep(this.data.values);
  336. break;
  337. default:
  338. break;
  339. }
  340. return result;
  341. }
  342. setValues(values: any, { isOverride = false }): void {
  343. const _values = this._adapter.cloneDeep(values);
  344. this.fields.forEach(field => {
  345. const value = ObjectUtil.get(_values, field.field);
  346. // When calling setValues to override the values, only need to trigger onValueChange and onChange once, so setNotNotify of setValue to true
  347. // 调用setValues进行值的覆盖时,只需要回调一次onValueChange、onChange即可,所以此处将setValue的notNotify置为true
  348. const opts = {
  349. notNotify: true,
  350. notUpdate: true,
  351. };
  352. field.fieldApi.setValue(value, opts);
  353. });
  354. // if there exists any arrayField component in this form
  355. if (this.registeredArrayField.size) {
  356. const arrayFieldPaths = [...this.registeredArrayField.keys()];
  357. arrayFieldPaths.forEach(path => {
  358. this.updateArrayField(path, { updateKey: new Date().valueOf() });
  359. });
  360. }
  361. // When isOverrid is true, there may be a non-existent field in the values passed in, directly synchronized to formState.values
  362. // 当isOverrid为true,传入的values中可能存在不存在的field时,直接将其同步到formState.values中
  363. if (isOverride) {
  364. this.data.values = _values;
  365. }
  366. // After completing the assignment, the unified callback can be done once.
  367. // 在完成赋值后,统一回调一次即可
  368. this._adapter.notifyChange(this.data);
  369. this._adapter.notifyValueChange(this.data.values, { ...values });
  370. this._adapter.forceUpdate();
  371. }
  372. // update formState value
  373. updateStateValue(field: string, value: any, opts: CallOpts): void {
  374. const notNotify = opts && opts.notNotify;
  375. const notUpdate = opts && opts.notUpdate;
  376. const fieldAllowEmpty = opts && opts.fieldAllowEmpty;
  377. /**
  378. * 当Form.allowEmpty为true时,所有的field,key都会在formState.values中出现,如果值为空那么就是undefined
  379. * 当Form.allowEmpty为false时,只有有值的field,key才会在formState.values中出现
  380. * When F orm.allow Empty is true, all fields and keys will appear in the formS tate.values. If the value is empty, it is undefined
  381. * When F orm.allow Empty is false, only fields with values will key appear in the formS tate.values
  382. */
  383. const formAllowEmpty = this.getProp('allowEmpty');
  384. // priority at Field level
  385. const allowEmpty = fieldAllowEmpty ? fieldAllowEmpty : formAllowEmpty;
  386. ObjectUtil.set(this.data.values, field, value, allowEmpty);
  387. /**
  388. * When registering, setValue called when Field initValue is synchronized to FormState should not trigger notify
  389. * but need to trigger forceUpdate, otherwise useFormState, useFieldState initial rendering will have problems
  390. *
  391. * register时,Field中同步initValue到FormState时调用的setValue不应该触发notify
  392. * 但需要触发forceUpdate,否则useFormState、useFieldState初始渲染会有问题
  393. */
  394. if (!notNotify) {
  395. this._adapter.notifyChange(this.data);
  396. this._adapter.notifyValueChange(this.data.values, { [field]: value });
  397. }
  398. if (!notUpdate) {
  399. this._adapter.forceUpdate();
  400. }
  401. }
  402. // get touched from formState
  403. getTouched(field?: string): boolean | Record<string, any> | undefined {
  404. if (typeof field === 'undefined') {
  405. return this.data.touched;
  406. }
  407. return ObjectUtil.get(this.data.touched, field);
  408. }
  409. // update formState touched
  410. updateStateTouched(field: string, isTouched: boolean, opts?: CallOpts): void {
  411. const notNotify = opts && opts.notNotify;
  412. const notUpdate = opts && opts.notUpdate;
  413. ObjectUtil.set(this.data.touched, field, isTouched);
  414. if (!notNotify) {
  415. this._adapter.notifyChange(this.data);
  416. }
  417. if (!notUpdate) {
  418. this._adapter.forceUpdate();
  419. }
  420. }
  421. // get error from formState
  422. getError(field?: string): any {
  423. if (typeof field === 'undefined') {
  424. return this.data.errors;
  425. }
  426. return ObjectUtil.get(this.data.errors, field);
  427. }
  428. // update formState error
  429. updateStateError(field: string, error: any, opts: CallOpts): void {
  430. const notNotify = opts && opts.notNotify;
  431. const notUpdate = opts && opts.notUpdate;
  432. ObjectUtil.set(this.data.errors, field, error);
  433. // The setError caused by centralized validation does not need to trigger notify, otherwise it will be called too frequently, as many times as there are fields
  434. // 集中validate时,引起的setError不需要触发notify,否则会过于频繁调用,有多少个field就调用了多少次
  435. if (!notNotify) {
  436. this._adapter.notifyChange(this.data);
  437. }
  438. if (!notUpdate) {
  439. this._adapter.forceUpdate();
  440. }
  441. }
  442. // For internal use in the FormApi Operating Field
  443. getFieldSetterApi() {
  444. const setValue = (field: string, value: any, opts: CallOpts) => {
  445. const fieldApi = this.fields.get(field) ? this.fields.get(field).fieldApi : undefined;
  446. // DeepClone the value entered from the outside to avoid unexpected errors caused by not isolating the scope to the greatest extent. This setValue will be called in eg: ArrayField
  447. const newValue = this._adapter.cloneDeep(value);
  448. if (fieldApi) {
  449. // If there is a corresponding Field entity, call FieldApi to update the value
  450. fieldApi.setValue(newValue, opts);
  451. } else {
  452. // If you reset an entire array, such as Array Field, the array as a whole may actually have no Field entities (but each array element corresponds to a Field)
  453. // At this time, first modify formState directly, then find out the subordinate fields and drive them to update
  454. // Eg: peoples: [0, 2, 3]. Each value of the peoples array corresponds to an Input Field
  455. // When the user directly calls formA pi.set Value ('peoples', [2,3])
  456. this.updateStateValue(field, newValue, opts);
  457. let nestedFields = this._getNestedField(field);
  458. if (nestedFields.size) {
  459. nestedFields.forEach(fieldStaff => {
  460. let fieldPath = fieldStaff.field;
  461. let newFieldVal = ObjectUtil.get(this.data.values, fieldPath);
  462. let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
  463. fieldStaff.fieldApi.setValue(newFieldVal, nestedBatchUpdateOpts);
  464. });
  465. }
  466. // If the reset happens to be, then update the updateKey corresponding to ArrayField to render it again
  467. if (this.getArrayField(field)) {
  468. this.updateArrayField(field, { updateKey: new Date().valueOf() });
  469. }
  470. }
  471. };
  472. const setError = (field: string, error: any, opts: CallOpts) => {
  473. const fieldApi = this.fields.get(field) ? this.fields.get(field).fieldApi : undefined;
  474. const newError = this._adapter.cloneDeep(error);
  475. if (fieldApi) {
  476. fieldApi.setError(newError, opts);
  477. } else {
  478. this.updateStateError(field, newError, opts);
  479. let nestedFields = this._getNestedField(field);
  480. if (nestedFields.size) {
  481. nestedFields.forEach(fieldStaff => {
  482. let fieldPath = fieldStaff.field;
  483. let newFieldError = ObjectUtil.get(this.data.errors, fieldPath);
  484. let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
  485. fieldStaff.fieldApi.setError(newFieldError, nestedBatchUpdateOpts);
  486. });
  487. }
  488. if (this.getArrayField(field)) {
  489. this.updateArrayField(field, { updateKey: new Date().valueOf() });
  490. }
  491. }
  492. };
  493. const setTouched = (field: string, isTouched: boolean, opts: CallOpts) => {
  494. const fieldApi = this.fields.get(field) ? this.fields.get(field).fieldApi : undefined;
  495. // touched is boolean variable, no need to execu deepClone like setValue
  496. if (fieldApi) {
  497. fieldApi.setTouched(isTouched, opts);
  498. } else {
  499. this.updateStateTouched(field, isTouched, opts);
  500. let nestedFields = this._getNestedField(field);
  501. if (nestedFields.size) {
  502. nestedFields.forEach(fieldStaff => {
  503. let fieldPath = fieldStaff.field;
  504. let newFieldTouch = ObjectUtil.get(this.data.touched, fieldPath);
  505. let nestedBatchUpdateOpts = { notNotify: true, notUpdate: true };
  506. fieldStaff.fieldApi.setTouched(newFieldTouch, nestedBatchUpdateOpts);
  507. });
  508. }
  509. if (this.getArrayField(field)) {
  510. this.updateArrayField(field, { updateKey: new Date().valueOf() });
  511. }
  512. }
  513. };
  514. return {
  515. setValue,
  516. setError,
  517. setTouched,
  518. };
  519. }
  520. // For Field and ArrayField to read and modify FormState
  521. getModifyFormStateApi() {
  522. return {
  523. register: this.register,
  524. unRegister: this.unRegister,
  525. updateStateValue: this.updateStateValue,
  526. updateStateError: this.updateStateError,
  527. updateStateTouched: this.updateStateTouched,
  528. getValue: this.getValue,
  529. getError: this.getError,
  530. getTouched: this.getTouched,
  531. getInitValues: this.getInitValues,
  532. getInitValue: this.getInitValue,
  533. getFormProps: this.getFormProps,
  534. getField: this.getField,
  535. registerArrayField: this.registerArrayField,
  536. unRegisterArrayField: this.unRegisterArrayField,
  537. getArrayField: this.getArrayField,
  538. updateArrayField: this.updateArrayField,
  539. };
  540. }
  541. // Form APIs for external use, exposed to the user
  542. getFormApi() {
  543. const fieldSetterApi = this.getFieldSetterApi();
  544. return {
  545. ...fieldSetterApi,
  546. reset: (fields?: Array<string>) => this.reset(fields),
  547. validate: (fields?: Array<string>) => this.validate(fields),
  548. getValue: (field?: string) => this.getValue(field, { needClone: true }),
  549. getValues: () => this.getValue(undefined, { needClone: true }),
  550. getFormState: () => this.getFormState(true),
  551. getInitValue: (field: string) => this.getInitValue(field),
  552. getInitValues: () => this.getInitValues(),
  553. getTouched: (field?: string) => this.getTouched(field),
  554. getError: (field?: string) => this.getError(field),
  555. setValues: (values: any, opts?: setValuesConfig) => this.setValues(values, opts || { isOverride: false }),
  556. submitForm: () => this.submit(),
  557. getFieldExist: (field: string) => this.getFieldExist(field),
  558. scrollToField: (field: string, scrollOpts?: ScrollIntoViewOptions) => this.scrollToField(field, scrollOpts),
  559. };
  560. }
  561. getFormState(needClone = false): FormState {
  562. // NOTES:这里如果直接返回this.data,forceUpdae触发Formrerender时,通过context传下去的formState会被认为是同一个对象【应该是浅对比的原因】
  563. // 使用了useFormState相关的component都不会触发重新渲染。所以使用...复制一次
  564. /*
  565. The reason for distinguishing deepClone: When semi is calling getFormState for internal consumption,
  566. the value of formState will not be modified, so deep cloning is not necessary, which can reduce performance loss
  567. But if the users use formApi.getFormState(), the behavior is unpredictable, and invasive modifications may be performed,
  568. so deep clones are used for isolation.
  569. 对deepClone进行区分的原因:semi调用getFormState内部消费时,不会对formState的值进行修改,所以无需深克隆,节约开销
  570. 但如果是业务方用formApi调用getFormState时,行为无法预料,有可能会进行侵入式修改,所以用深克隆进行隔离
  571. */
  572. if (!needClone) {
  573. return {
  574. ...this.data,
  575. };
  576. } else {
  577. return this._adapter.cloneDeep(this.data);
  578. }
  579. }
  580. _isValid(targetFields: Map<string, FieldStaff>): boolean {
  581. let valid = true;
  582. if (!targetFields) {
  583. valid = Boolean(ObjectUtil.empty(this.data.errors));
  584. } else {
  585. // when trigger partial validate
  586. const targetFieldStr = [...targetFields.keys()];
  587. targetFieldStr.forEach(fieldStr => {
  588. const fieldError = ObjectUtil.get(this.data.errors, fieldStr);
  589. if (!isValid(fieldError)) {
  590. valid = false;
  591. }
  592. });
  593. }
  594. return valid;
  595. }
  596. // get form.props.initValues
  597. getInitValues(): any {
  598. return this._adapter.getInitValues();
  599. }
  600. getInitValue(field?: string): any {
  601. if (typeof field === 'undefined') {
  602. return this._adapter.getInitValues();
  603. }
  604. return ObjectUtil.get(this._adapter.getInitValues(), field);
  605. }
  606. getFormProps(keys?: Array<string>): ComponentProps {
  607. return this._adapter.getFormProps(keys);
  608. }
  609. getFieldExist(field: string): boolean {
  610. return Boolean(this.fields.has(field));
  611. }
  612. _autoScroll(timeout?: boolean | number): void {
  613. const { autoScrollToError } = this.getFormProps();
  614. if (!autoScrollToError) {
  615. return;
  616. }
  617. let scrollOpts = { behavior: 'smooth' as const, block: 'start' as const };
  618. typeof autoScrollToError === 'object' ? (scrollOpts = autoScrollToError) : null;
  619. if (timeout) {
  620. setTimeout(() => this._getErrorFieldAndScroll(scrollOpts), 100);
  621. } else {
  622. this._getErrorFieldAndScroll(scrollOpts);
  623. }
  624. }
  625. _getErrorFieldAndScroll(scrollOpts?: scrollIntoViewOptions | boolean): void {
  626. const errorDOM = this._adapter.getAllErrorDOM();
  627. if (errorDOM && errorDOM.length) {
  628. try {
  629. const fieldDom = errorDOM[0].parentNode.parentNode;
  630. scrollIntoView(fieldDom as Element, scrollOpts);
  631. } catch (error) {}
  632. }
  633. }
  634. scrollToField(field: string, scrollOpts = { behavior: 'smooth', block: 'start' } as scrollIntoViewOptions): void {
  635. if (this.getFieldExist(field)) {
  636. const fieldDOM = this._adapter.getFieldDOM(field);
  637. scrollIntoView(fieldDOM as Element, scrollOpts);
  638. }
  639. }
  640. }