index.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701
  1. import baseComponent from '../helpers/baseComponent'
  2. import classNames from '../helpers/libs/classNames'
  3. import warning from '../helpers/libs/warning'
  4. import { props } from './props'
  5. import createFieldsStore, {
  6. isNullValue,
  7. getValueFromEvent,
  8. getParams,
  9. transformRules,
  10. hasRules,
  11. normalizeValidateRules,
  12. getValidateTriggers,
  13. isEmptyObject,
  14. flattenArray,
  15. } from '../helpers/hooks/useFieldForm'
  16. import {
  17. FIELD_META_PROP,
  18. FIELD_DATA_PROP,
  19. DEFAULT_TRIGGER,
  20. } from '../helpers/shared/constants'
  21. import { get, set, eq } from '../helpers/shared/util'
  22. import AsyncValidator from '../helpers/libs/async-validator'
  23. baseComponent({
  24. useExport: true,
  25. relations: {
  26. '../field/index': {
  27. type: 'descendant',
  28. observer(e, { unlinked }) {
  29. this.renderFields[e.data.name] = unlinked === false
  30. this.callDebounceFn(this.changeValue)
  31. },
  32. },
  33. },
  34. properties: props,
  35. observers: {
  36. ['layout, validateMessages, requiredMarkStyle, asteriskText, requiredText, optionalText, disabled, readOnly'](...args) {
  37. this.changeFieldElem(this.data)
  38. },
  39. },
  40. computed: {
  41. classes: ['prefixCls', function(prefixCls) {
  42. const wrap = classNames(prefixCls)
  43. const footer = `${prefixCls}__footer`
  44. return {
  45. wrap,
  46. footer,
  47. }
  48. }],
  49. },
  50. methods: {
  51. changeFieldElem(props) {
  52. const {
  53. layout,
  54. validateMessages,
  55. requiredMarkStyle,
  56. asteriskText,
  57. requiredText,
  58. optionalText,
  59. disabled,
  60. readOnly,
  61. } = props
  62. const elements = this.getRelationsByName('../field/index')
  63. if (elements.length > 0) {
  64. const lastIndex = elements.length - 1
  65. elements.forEach((fieldElem, index) => {
  66. const isLast = index === lastIndex
  67. const context = {
  68. layout,
  69. validateMessages,
  70. requiredMarkStyle,
  71. asteriskText,
  72. requiredText,
  73. optionalText,
  74. disabled,
  75. readOnly,
  76. }
  77. fieldElem.changeContext(index, isLast, context)
  78. })
  79. }
  80. },
  81. changeValue() {
  82. this.changeFieldElem(this.data)
  83. this.clearUnlinkedFields()
  84. },
  85. saveRef(name, _, component) {
  86. if (!component) {
  87. const fieldMeta = this.fieldsStore.getFieldMeta(name);
  88. if (!fieldMeta.preserve) {
  89. // after destroy, delete data
  90. this.clearedFieldMetaCache[name] = {
  91. field: this.fieldsStore.getField(name),
  92. meta: fieldMeta,
  93. };
  94. this.clearField(name);
  95. }
  96. return;
  97. }
  98. this.recoverClearedField(name);
  99. this.instances[name] = component;
  100. },
  101. recoverClearedField(name) {
  102. if (this.clearedFieldMetaCache[name]) {
  103. this.fieldsStore.setFields({
  104. [name]: this.clearedFieldMetaCache[name].field,
  105. });
  106. this.fieldsStore.setFieldMeta(
  107. name,
  108. this.clearedFieldMetaCache[name].meta,
  109. );
  110. delete this.clearedFieldMetaCache[name];
  111. }
  112. },
  113. setFieldsAsErrors(name, fieldMeta) {
  114. const fieldNames = this.fieldsStore.getValidFieldsFullName(name)
  115. if (fieldNames.includes(name) && !hasRules(fieldMeta.validate)) {
  116. const nowField = this.fieldsStore.getField(name)
  117. if (nowField.errors) {
  118. this.fieldsStore.setFields({
  119. [name]: {
  120. ...nowField,
  121. errors: undefined,
  122. },
  123. })
  124. }
  125. }
  126. },
  127. /**
  128. * 清除已卸载的元素
  129. */
  130. clearUnlinkedFields() {
  131. const fieldList = this.fieldsStore.getAllFieldsName()
  132. const removedList = fieldList.filter((field) => {
  133. const fieldMeta = this.fieldsStore.getFieldMeta(field)
  134. return !this.renderFields[field] && !fieldMeta.preserve
  135. })
  136. if (removedList.length > 0) {
  137. removedList.forEach((name) => this.clearField(name))
  138. }
  139. },
  140. /**
  141. * 清除字段
  142. */
  143. clearField(name) {
  144. this.fieldsStore.clearField(name)
  145. delete this.renderFields[name]
  146. delete this.instances[name]
  147. delete this.cachedBind[name]
  148. },
  149. /**
  150. * 设置字段
  151. */
  152. setFields(maybeNestedFields, callback) {
  153. const fields = this.fieldsStore.flattenRegisteredFields(maybeNestedFields)
  154. Object.keys(fields).forEach((name) => {
  155. this.fieldsStore.setFields({ [name]: fields[name] })
  156. const fieldMeta = this.fieldsStore.getFieldMeta(name)
  157. if (fieldMeta) {
  158. const { fieldElem, inputElem } = fieldMeta
  159. fieldElem.forceUpdate(inputElem, callback)
  160. }
  161. })
  162. // trigger onFieldsChange
  163. const changedFields = Object.keys(fields).reduce(
  164. (acc, name) => set(acc, name, this.fieldsStore.getField(name)),
  165. {},
  166. )
  167. const allFields = this.fieldsStore.getNestedAllFields()
  168. this.onFieldsChange(changedFields, allFields)
  169. },
  170. onCollectValidate(name, action, ...args) {
  171. const { field, fieldMeta } = this.onCollectCommon(name, action, args);
  172. const newField = {
  173. ...field,
  174. dirty: true,
  175. };
  176. this.fieldsStore.setFieldsAsDirty();
  177. this.validateFieldsInternal([newField], {
  178. action,
  179. options: {
  180. firstFields: !!fieldMeta.validateFirst,
  181. },
  182. });
  183. },
  184. /**
  185. * 同步子元素
  186. */
  187. onCollectCommon(name, action, args) {
  188. const field = this.fieldsStore.getField(name);
  189. const fieldMeta = this.fieldsStore.getFieldMeta(name)
  190. const { oriInputEvents, fieldElem } = fieldMeta
  191. // trigger inputElem func
  192. if (oriInputEvents && oriInputEvents[action]) {
  193. oriInputEvents[action](...args)
  194. }
  195. const value = getValueFromEvent(...args)
  196. const oldValue = this.fieldsStore.getFieldValue(name)
  197. // set field value
  198. if (value !== oldValue) {
  199. if (fieldElem.data.value !== value) {
  200. fieldElem.setData({ value })
  201. }
  202. // trigger onValuesChange
  203. const changedValues = { [name]: value }
  204. const allValues = this.fieldsStore.getAllValues()
  205. this.onValuesChange(changedValues, { ...allValues, ...changedValues })
  206. }
  207. return {
  208. name,
  209. field: {
  210. ...field,
  211. value,
  212. touched: true,
  213. },
  214. fieldMeta,
  215. }
  216. },
  217. /**
  218. * 同步子元素
  219. */
  220. onCollect(name_, action, ...args) {
  221. const { name, field, fieldMeta } = this.onCollectCommon(name_, action, args)
  222. const { validate } = fieldMeta;
  223. this.fieldsStore.setFieldsAsDirty();
  224. const newField = {
  225. ...field,
  226. dirty: hasRules(validate),
  227. };
  228. this.setFields({ [name]: newField })
  229. },
  230. /**
  231. * 设置 value
  232. */
  233. setFieldsValue(changedValues, callback) {
  234. const { fieldsMeta } = this.fieldsStore;
  235. const values = this.fieldsStore.flattenRegisteredFields(changedValues);
  236. const newFields = Object.keys(values).reduce((acc, name) => {
  237. const isRegistered = fieldsMeta[name];
  238. if (isRegistered) {
  239. const value = values[name];
  240. acc[name] = {
  241. value,
  242. };
  243. }
  244. return acc;
  245. }, {});
  246. this.setFields(newFields, callback);
  247. const allValues = this.fieldsStore.getAllValues();
  248. this.onValuesChange(changedValues, allValues)
  249. },
  250. /**
  251. * 重置字段
  252. */
  253. resetFields(ns) {
  254. const names = typeof ns === 'undefined'
  255. ? ns : Array.isArray(ns)
  256. ? ns : [ns]
  257. const newFields = this.fieldsStore.resetFields(names)
  258. if (Object.keys(newFields).length > 0) {
  259. this.setFields(newFields)
  260. }
  261. if (ns) {
  262. const names = Array.isArray(ns) ? ns : [ns];
  263. names.forEach(name => delete this.clearedFieldMetaCache[name]);
  264. } else {
  265. this.clearedFieldMetaCache = {};
  266. }
  267. },
  268. validateFieldsInternal(
  269. fields,
  270. { fieldNames, action, options = {} },
  271. callback,
  272. ) {
  273. const allRules = {};
  274. const allValues = {};
  275. const allFields = {};
  276. const alreadyErrors = {};
  277. fields.forEach(field => {
  278. const name = field.name;
  279. if (options.force !== true && field.dirty === false) {
  280. if (field.errors) {
  281. set(alreadyErrors, name, { errors: field.errors });
  282. }
  283. return;
  284. }
  285. const fieldMeta = this.fieldsStore.getFieldMeta(name);
  286. const newField = {
  287. ...field,
  288. };
  289. newField.errors = undefined;
  290. newField.validating = true;
  291. newField.dirty = true;
  292. allRules[name] = this.getRules(fieldMeta, action);
  293. allValues[name] = newField.value;
  294. allFields[name] = newField;
  295. });
  296. this.setFields(allFields);
  297. // in case normalize
  298. Object.keys(allValues).forEach(f => {
  299. allValues[f] = this.fieldsStore.getFieldValue(f);
  300. });
  301. if (callback && isEmptyObject(allFields)) {
  302. callback(
  303. isEmptyObject(alreadyErrors) ? null : alreadyErrors,
  304. this.fieldsStore.getFieldsValue(fieldNames),
  305. );
  306. return;
  307. }
  308. const validator = new AsyncValidator(allRules);
  309. const { validateMessages } = this.data
  310. if (validateMessages) {
  311. validator.messages(validateMessages);
  312. }
  313. validator.validate(allValues, options, errors => {
  314. const errorsGroup = {
  315. ...alreadyErrors,
  316. };
  317. if (errors && errors.length) {
  318. errors.forEach(e => {
  319. const errorFieldName = e.field;
  320. let fieldName = errorFieldName;
  321. // Handle using array validation rule.
  322. Object.keys(allRules).some(ruleFieldName => {
  323. const rules = allRules[ruleFieldName] || [];
  324. // Exist if match rule
  325. if (ruleFieldName === errorFieldName) {
  326. fieldName = ruleFieldName;
  327. return true;
  328. }
  329. // Skip if not match array type
  330. if (
  331. rules.every(({ type }) => type !== 'array') ||
  332. errorFieldName.indexOf(`${ruleFieldName}.`) !== 0
  333. ) {
  334. return false;
  335. }
  336. // Exist if match the field name
  337. const restPath = errorFieldName.slice(ruleFieldName.length + 1);
  338. if (/^\d+$/.test(restPath)) {
  339. fieldName = ruleFieldName;
  340. return true;
  341. }
  342. return false;
  343. });
  344. const field = get(errorsGroup, fieldName);
  345. if (typeof field !== 'object' || Array.isArray(field)) {
  346. set(errorsGroup, fieldName, { errors: [] });
  347. }
  348. const fieldErrors = get(errorsGroup, fieldName.concat('.errors'));
  349. fieldErrors.push(e);
  350. });
  351. }
  352. const expired = [];
  353. const nowAllFields = {};
  354. Object.keys(allRules).forEach(name => {
  355. const fieldErrors = get(errorsGroup, name);
  356. const nowField = this.fieldsStore.getField(name);
  357. // avoid concurrency problems
  358. if (!eq(nowField.value, allValues[name])) {
  359. expired.push({
  360. name,
  361. });
  362. } else {
  363. nowField.errors = fieldErrors && fieldErrors.errors;
  364. nowField.value = allValues[name];
  365. nowField.validating = false;
  366. nowField.dirty = false;
  367. nowAllFields[name] = nowField;
  368. }
  369. });
  370. this.setFields(nowAllFields);
  371. if (callback) {
  372. if (expired.length) {
  373. expired.forEach(({ name }) => {
  374. const fieldErrors = [
  375. {
  376. message: `${name} need to revalidate`,
  377. field: name,
  378. },
  379. ];
  380. set(errorsGroup, name, {
  381. expired: true,
  382. errors: fieldErrors,
  383. });
  384. });
  385. }
  386. callback(
  387. isEmptyObject(errorsGroup) ? null : errorsGroup,
  388. this.fieldsStore.getFieldsValue(fieldNames),
  389. );
  390. }
  391. });
  392. },
  393. validateFields(ns, opt, cb) {
  394. const pending = new Promise((resolve, reject) => {
  395. const { names, options } = getParams(ns, opt, cb);
  396. let { callback } = getParams(ns, opt, cb);
  397. if (!callback || typeof callback === 'function') {
  398. const oldCb = callback;
  399. callback = (errors, values) => {
  400. if (oldCb) {
  401. oldCb(errors, values);
  402. }
  403. if (errors) {
  404. reject({ errors, values });
  405. } else {
  406. resolve(values);
  407. }
  408. };
  409. }
  410. const fieldNames = names
  411. ? this.fieldsStore.getValidFieldsFullName(names)
  412. : this.fieldsStore.getValidFieldsName();
  413. const fields = fieldNames
  414. .filter(name => {
  415. const fieldMeta = this.fieldsStore.getFieldMeta(name);
  416. return hasRules(fieldMeta.validate);
  417. })
  418. .map(name => {
  419. const field = this.fieldsStore.getField(name);
  420. field.value = this.fieldsStore.getFieldValue(name);
  421. return field;
  422. });
  423. if (!fields.length) {
  424. callback(null, this.fieldsStore.getFieldsValue(fieldNames));
  425. return;
  426. }
  427. if (!('firstFields' in options)) {
  428. options.firstFields = fieldNames.filter(name => {
  429. const fieldMeta = this.fieldsStore.getFieldMeta(name);
  430. return !!fieldMeta.validateFirst;
  431. });
  432. }
  433. this.validateFieldsInternal(
  434. fields,
  435. {
  436. fieldNames,
  437. options,
  438. },
  439. callback,
  440. );
  441. });
  442. pending.catch(e => {
  443. // eslint-disable-next-line no-console
  444. if (console.error) {
  445. // eslint-disable-next-line no-console
  446. console.error(e);
  447. }
  448. return e;
  449. });
  450. return pending;
  451. },
  452. getRules(fieldMeta, action) {
  453. const actionRules = fieldMeta.validate
  454. .filter(item => {
  455. return !action || item.trigger.indexOf(action) >= 0;
  456. })
  457. .map(item => item.rules);
  458. return flattenArray(actionRules);
  459. },
  460. getFieldInstance(name) {
  461. return this.instances[name];
  462. },
  463. getCacheBind(name, action, fn) {
  464. if (!this.cachedBind[name]) {
  465. this.cachedBind[name] = {};
  466. }
  467. const cache = this.cachedBind[name];
  468. if (!cache[action] || cache[action].oriFn !== fn) {
  469. cache[action] = {
  470. fn: fn.bind(this, name, action),
  471. oriFn: fn,
  472. };
  473. }
  474. return cache[action].fn;
  475. },
  476. getFieldDecorator(name, fieldData, fieldElem) {
  477. const props = this.getFieldProps(name, fieldData, fieldElem)
  478. return (inputElem) => {
  479. // We should put field in record if it is rendered
  480. this.renderFields[name] = true
  481. const fieldMeta = this.fieldsStore.getFieldMeta(name)
  482. const originalProps = inputElem.data
  483. fieldMeta.inputElem = inputElem
  484. // cache inputEvents
  485. if (!originalProps.oriInputEvents) {
  486. props.oriInputEvents = { ...originalProps.inputEvents }
  487. fieldMeta.oriInputEvents = { ...originalProps.inputEvents }
  488. }
  489. const decoratedFieldProps = {
  490. ...props,
  491. ...this.fieldsStore.getFieldValuePropValue(fieldMeta),
  492. }
  493. return decoratedFieldProps
  494. }
  495. },
  496. getFieldProps(name, fieldData, fieldElem) {
  497. delete this.clearedFieldMetaCache[name]
  498. const rules = transformRules(fieldData.rules)
  499. const {
  500. initialValue,
  501. trigger = DEFAULT_TRIGGER,
  502. valuePropName,
  503. validate = [],
  504. validateTrigger = [DEFAULT_TRIGGER],
  505. preserve,
  506. // rules,
  507. validateFirst,
  508. hidden,
  509. } = fieldData
  510. const fieldOption = {
  511. name,
  512. // initialValue,
  513. trigger,
  514. valuePropName,
  515. validate,
  516. validateTrigger,
  517. preserve,
  518. rules,
  519. validateFirst,
  520. hidden,
  521. }
  522. if (!isNullValue(initialValue)) {
  523. fieldOption.initialValue = initialValue
  524. }
  525. const inputProps = {
  526. ...this.fieldsStore.getFieldValuePropValue(fieldOption),
  527. inputEvents: {},
  528. }
  529. const fieldMeta = this.fieldsStore.getFieldMeta(name);
  530. if ('initialValue' in fieldOption) {
  531. fieldMeta.initialValue = fieldOption.initialValue;
  532. }
  533. fieldMeta.fieldElem = fieldElem
  534. const validateRules = normalizeValidateRules(
  535. validate,
  536. rules,
  537. validateTrigger,
  538. );
  539. const validateTriggers = getValidateTriggers(validateRules);
  540. validateTriggers.forEach(action => {
  541. if (inputProps.inputEvents[action]) { return };
  542. inputProps.inputEvents[action] = this.getCacheBind(
  543. name,
  544. action,
  545. this.onCollectValidate,
  546. )
  547. });
  548. // make sure that the value will be collect
  549. if (validateTriggers.indexOf(trigger) === -1) {
  550. inputProps.inputEvents[trigger] = this.getCacheBind(
  551. name,
  552. trigger,
  553. this.onCollect,
  554. )
  555. }
  556. const meta = {
  557. ...fieldMeta,
  558. ...fieldOption,
  559. validate: validateRules,
  560. }
  561. this.fieldsStore.setFieldMeta(name, meta)
  562. // fix errors
  563. this.setFieldsAsErrors(name, meta)
  564. const {
  565. fieldElem: __fieldElem,
  566. inputElem: __inputElem,
  567. oriInputEvents: __oriInputEvents,
  568. ...fieldMetaProp
  569. } = meta
  570. // inject field
  571. inputProps[FIELD_META_PROP] = fieldMetaProp
  572. inputProps[FIELD_DATA_PROP] = this.fieldsStore.getField(name);
  573. return inputProps
  574. },
  575. registerField(name, fieldElem) {
  576. const action = `${name}__ref`
  577. const ref = this.getCacheBind(name, action, this.saveRef)
  578. ref(fieldElem)
  579. return () => {
  580. ref(null)
  581. }
  582. },
  583. getInternalHooks(key) {
  584. if (key === 'FORM_HOOK_MARK') {
  585. return {
  586. registerField: this.registerField.bind(this),
  587. getFieldDecorator: this.getFieldDecorator.bind(this),
  588. }
  589. }
  590. warning(
  591. false,
  592. '`getInternalHooks` is internal usage of the <form />. Should not call directly.'
  593. )
  594. return null
  595. },
  596. /**
  597. * 表单对外暴露方法
  598. */
  599. expose() {
  600. return {
  601. getFieldsValue: this.getFieldsValue,
  602. getFieldValue: this.getFieldValue,
  603. setFieldsInitialValue: this.setFieldsInitialValue,
  604. getFieldsError: this.getFieldsError,
  605. getFieldError: this.getFieldError,
  606. isFieldValidating: this.isFieldValidating,
  607. isFieldsValidating: this.isFieldsValidating,
  608. isFieldsTouched: this.isFieldsTouched,
  609. isFieldTouched: this.isFieldTouched,
  610. setFieldsValue: this.setFieldsValue.bind(this),
  611. setFields: this.setFields.bind(this),
  612. resetFields: this.resetFields.bind(this),
  613. validateFields: this.validateFields.bind(this),
  614. getFieldInstance: this.getFieldInstance.bind(this),
  615. getInternalHooks: this.getInternalHooks.bind(this),
  616. }
  617. },
  618. /**
  619. * trigger onValuesChange
  620. */
  621. onValuesChange(changedValues, allValues) {
  622. this.triggerEvent('change', { form: this.expose(), changedValues, allValues })
  623. },
  624. /**
  625. * trigger onFieldsChange
  626. */
  627. onFieldsChange(changedFields, allFields) {
  628. this.triggerEvent('fieldsChange', { form: this.expose(), changedFields, allFields })
  629. },
  630. },
  631. created() {
  632. const methods = [
  633. 'getFieldsValue',
  634. 'getFieldValue',
  635. 'setFieldsInitialValue',
  636. 'getFieldsError',
  637. 'getFieldError',
  638. 'isFieldValidating',
  639. 'isFieldsValidating',
  640. 'isFieldsTouched',
  641. 'isFieldTouched',
  642. ]
  643. this.fieldsStore = createFieldsStore()
  644. this.renderFields = {}
  645. this.cachedBind = {}
  646. this.clearedFieldMetaCache = {}
  647. this.instances = {}
  648. methods.forEach((method) => {
  649. this[method] = (...args) => {
  650. return this.fieldsStore[method](...args)
  651. }
  652. })
  653. },
  654. })