FormItem.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
  2. import _extends from "@babel/runtime/helpers/esm/extends";
  3. import { resolveDirective as _resolveDirective, Fragment as _Fragment, createVNode as _createVNode } from "vue";
  4. import { onMounted, reactive, watch, defineComponent, computed, nextTick, shallowRef, watchEffect, onBeforeUnmount, toRaw } from 'vue';
  5. import LoadingOutlined from "@ant-design/icons-vue/es/icons/LoadingOutlined";
  6. import CloseCircleFilled from "@ant-design/icons-vue/es/icons/CloseCircleFilled";
  7. import CheckCircleFilled from "@ant-design/icons-vue/es/icons/CheckCircleFilled";
  8. import ExclamationCircleFilled from "@ant-design/icons-vue/es/icons/ExclamationCircleFilled";
  9. import cloneDeep from 'lodash-es/cloneDeep';
  10. import PropTypes from '../_util/vue-types';
  11. import Row from '../grid/Row';
  12. import { filterEmpty } from '../_util/props-util';
  13. import { validateRules as validateRulesUtil } from './utils/validateUtil';
  14. import { getNamePath } from './utils/valueUtil';
  15. import { toArray } from './utils/typeUtil';
  16. import { warning } from '../vc-util/warning';
  17. import find from 'lodash-es/find';
  18. import { tuple } from '../_util/type';
  19. import useConfigInject from '../config-provider/hooks/useConfigInject';
  20. import { useInjectForm } from './context';
  21. import FormItemLabel from './FormItemLabel';
  22. import FormItemInput from './FormItemInput';
  23. import { FormItemInputContext, useProvideFormItemContext } from './FormItemContext';
  24. import useDebounce from './utils/useDebounce';
  25. import classNames from '../_util/classNames';
  26. import useStyle from './style';
  27. const ValidateStatuses = tuple('success', 'warning', 'error', 'validating', '');
  28. const iconMap = {
  29. success: CheckCircleFilled,
  30. warning: ExclamationCircleFilled,
  31. error: CloseCircleFilled,
  32. validating: LoadingOutlined
  33. };
  34. function getPropByPath(obj, namePathList, strict) {
  35. let tempObj = obj;
  36. const keyArr = namePathList;
  37. let i = 0;
  38. try {
  39. for (let len = keyArr.length; i < len - 1; ++i) {
  40. if (!tempObj && !strict) break;
  41. const key = keyArr[i];
  42. if (key in tempObj) {
  43. tempObj = tempObj[key];
  44. } else {
  45. if (strict) {
  46. throw Error('please transfer a valid name path to form item!');
  47. }
  48. break;
  49. }
  50. }
  51. if (strict && !tempObj) {
  52. throw Error('please transfer a valid name path to form item!');
  53. }
  54. } catch (error) {
  55. console.error('please transfer a valid name path to form item!');
  56. }
  57. return {
  58. o: tempObj,
  59. k: keyArr[i],
  60. v: tempObj ? tempObj[keyArr[i]] : undefined
  61. };
  62. }
  63. export const formItemProps = () => ({
  64. htmlFor: String,
  65. prefixCls: String,
  66. label: PropTypes.any,
  67. help: PropTypes.any,
  68. extra: PropTypes.any,
  69. labelCol: {
  70. type: Object
  71. },
  72. wrapperCol: {
  73. type: Object
  74. },
  75. hasFeedback: {
  76. type: Boolean,
  77. default: false
  78. },
  79. colon: {
  80. type: Boolean,
  81. default: undefined
  82. },
  83. labelAlign: String,
  84. prop: {
  85. type: [String, Number, Array]
  86. },
  87. name: {
  88. type: [String, Number, Array]
  89. },
  90. rules: [Array, Object],
  91. autoLink: {
  92. type: Boolean,
  93. default: true
  94. },
  95. required: {
  96. type: Boolean,
  97. default: undefined
  98. },
  99. validateFirst: {
  100. type: Boolean,
  101. default: undefined
  102. },
  103. validateStatus: PropTypes.oneOf(tuple('', 'success', 'warning', 'error', 'validating')),
  104. validateTrigger: {
  105. type: [String, Array]
  106. },
  107. messageVariables: {
  108. type: Object
  109. },
  110. hidden: Boolean,
  111. noStyle: Boolean,
  112. tooltip: String
  113. });
  114. let indexGuid = 0;
  115. // default form item id prefix.
  116. const defaultItemNamePrefixCls = 'form_item';
  117. export default defineComponent({
  118. compatConfig: {
  119. MODE: 3
  120. },
  121. name: 'AFormItem',
  122. inheritAttrs: false,
  123. __ANT_NEW_FORM_ITEM: true,
  124. props: formItemProps(),
  125. slots: Object,
  126. setup(props, _ref) {
  127. let {
  128. slots,
  129. attrs,
  130. expose
  131. } = _ref;
  132. warning(props.prop === undefined, `\`prop\` is deprecated. Please use \`name\` instead.`);
  133. const eventKey = `form-item-${++indexGuid}`;
  134. const {
  135. prefixCls
  136. } = useConfigInject('form', props);
  137. const [wrapSSR, hashId] = useStyle(prefixCls);
  138. const itemRef = shallowRef();
  139. const formContext = useInjectForm();
  140. const fieldName = computed(() => props.name || props.prop);
  141. const errors = shallowRef([]);
  142. const validateDisabled = shallowRef(false);
  143. const inputRef = shallowRef();
  144. const namePath = computed(() => {
  145. const val = fieldName.value;
  146. return getNamePath(val);
  147. });
  148. const fieldId = computed(() => {
  149. if (!namePath.value.length) {
  150. return undefined;
  151. } else {
  152. const formName = formContext.name.value;
  153. const mergedId = namePath.value.join('_');
  154. return formName ? `${formName}_${mergedId}` : `${defaultItemNamePrefixCls}_${mergedId}`;
  155. }
  156. });
  157. const getNewFieldValue = () => {
  158. const model = formContext.model.value;
  159. if (!model || !fieldName.value) {
  160. return;
  161. } else {
  162. return getPropByPath(model, namePath.value, true).v;
  163. }
  164. };
  165. const fieldValue = computed(() => getNewFieldValue());
  166. const initialValue = shallowRef(cloneDeep(fieldValue.value));
  167. const mergedValidateTrigger = computed(() => {
  168. let validateTrigger = props.validateTrigger !== undefined ? props.validateTrigger : formContext.validateTrigger.value;
  169. validateTrigger = validateTrigger === undefined ? 'change' : validateTrigger;
  170. return toArray(validateTrigger);
  171. });
  172. const rulesRef = computed(() => {
  173. let formRules = formContext.rules.value;
  174. const selfRules = props.rules;
  175. const requiredRule = props.required !== undefined ? {
  176. required: !!props.required,
  177. trigger: mergedValidateTrigger.value
  178. } : [];
  179. const prop = getPropByPath(formRules, namePath.value);
  180. formRules = formRules ? prop.o[prop.k] || prop.v : [];
  181. const rules = [].concat(selfRules || formRules || []);
  182. if (find(rules, rule => rule.required)) {
  183. return rules;
  184. } else {
  185. return rules.concat(requiredRule);
  186. }
  187. });
  188. const isRequired = computed(() => {
  189. const rules = rulesRef.value;
  190. let isRequired = false;
  191. if (rules && rules.length) {
  192. rules.every(rule => {
  193. if (rule.required) {
  194. isRequired = true;
  195. return false;
  196. }
  197. return true;
  198. });
  199. }
  200. return isRequired || props.required;
  201. });
  202. const validateState = shallowRef();
  203. watchEffect(() => {
  204. validateState.value = props.validateStatus;
  205. });
  206. const messageVariables = computed(() => {
  207. let variables = {};
  208. if (typeof props.label === 'string') {
  209. variables.label = props.label;
  210. } else if (props.name) {
  211. variables.label = String(props.name);
  212. }
  213. if (props.messageVariables) {
  214. variables = _extends(_extends({}, variables), props.messageVariables);
  215. }
  216. return variables;
  217. });
  218. const validateRules = options => {
  219. // no name, no value, so the validate result is incorrect
  220. if (namePath.value.length === 0) {
  221. return;
  222. }
  223. const {
  224. validateFirst = false
  225. } = props;
  226. const {
  227. triggerName
  228. } = options || {};
  229. let filteredRules = rulesRef.value;
  230. if (triggerName) {
  231. filteredRules = filteredRules.filter(rule => {
  232. const {
  233. trigger
  234. } = rule;
  235. if (!trigger && !mergedValidateTrigger.value.length) {
  236. return true;
  237. }
  238. const triggerList = toArray(trigger || mergedValidateTrigger.value);
  239. return triggerList.includes(triggerName);
  240. });
  241. }
  242. if (!filteredRules.length) {
  243. return Promise.resolve();
  244. }
  245. const promise = validateRulesUtil(namePath.value, fieldValue.value, filteredRules, _extends({
  246. validateMessages: formContext.validateMessages.value
  247. }, options), validateFirst, messageVariables.value);
  248. validateState.value = 'validating';
  249. errors.value = [];
  250. promise.catch(e => e).then(function () {
  251. let results = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
  252. if (validateState.value === 'validating') {
  253. const res = results.filter(result => result && result.errors.length);
  254. validateState.value = res.length ? 'error' : 'success';
  255. errors.value = res.map(r => r.errors);
  256. formContext.onValidate(fieldName.value, !errors.value.length, errors.value.length ? toRaw(errors.value[0]) : null);
  257. }
  258. });
  259. return promise;
  260. };
  261. const onFieldBlur = () => {
  262. validateRules({
  263. triggerName: 'blur'
  264. });
  265. };
  266. const onFieldChange = () => {
  267. if (validateDisabled.value) {
  268. validateDisabled.value = false;
  269. return;
  270. }
  271. validateRules({
  272. triggerName: 'change'
  273. });
  274. };
  275. const clearValidate = () => {
  276. validateState.value = props.validateStatus;
  277. validateDisabled.value = false;
  278. errors.value = [];
  279. };
  280. const resetField = () => {
  281. var _a;
  282. validateState.value = props.validateStatus;
  283. validateDisabled.value = true;
  284. errors.value = [];
  285. const model = formContext.model.value || {};
  286. const value = fieldValue.value;
  287. const prop = getPropByPath(model, namePath.value, true);
  288. if (Array.isArray(value)) {
  289. prop.o[prop.k] = [].concat((_a = initialValue.value) !== null && _a !== void 0 ? _a : []);
  290. } else {
  291. prop.o[prop.k] = initialValue.value;
  292. }
  293. // reset validateDisabled after onFieldChange triggered
  294. nextTick(() => {
  295. validateDisabled.value = false;
  296. });
  297. };
  298. const htmlFor = computed(() => {
  299. return props.htmlFor === undefined ? fieldId.value : props.htmlFor;
  300. });
  301. const onLabelClick = () => {
  302. const id = htmlFor.value;
  303. if (!id || !inputRef.value) {
  304. return;
  305. }
  306. const control = inputRef.value.$el.querySelector(`[id="${id}"]`);
  307. if (control && control.focus) {
  308. control.focus();
  309. }
  310. };
  311. expose({
  312. onFieldBlur,
  313. onFieldChange,
  314. clearValidate,
  315. resetField
  316. });
  317. useProvideFormItemContext({
  318. id: fieldId,
  319. onFieldBlur: () => {
  320. if (props.autoLink) {
  321. onFieldBlur();
  322. }
  323. },
  324. onFieldChange: () => {
  325. if (props.autoLink) {
  326. onFieldChange();
  327. }
  328. },
  329. clearValidate
  330. }, computed(() => {
  331. return !!(props.autoLink && formContext.model.value && fieldName.value);
  332. }));
  333. let registered = false;
  334. watch(fieldName, val => {
  335. if (val) {
  336. if (!registered) {
  337. registered = true;
  338. formContext.addField(eventKey, {
  339. fieldValue,
  340. fieldId,
  341. fieldName,
  342. resetField,
  343. clearValidate,
  344. namePath,
  345. validateRules,
  346. rules: rulesRef
  347. });
  348. }
  349. } else {
  350. registered = false;
  351. formContext.removeField(eventKey);
  352. }
  353. }, {
  354. immediate: true
  355. });
  356. onBeforeUnmount(() => {
  357. formContext.removeField(eventKey);
  358. });
  359. const debounceErrors = useDebounce(errors);
  360. const mergedValidateStatus = computed(() => {
  361. if (props.validateStatus !== undefined) {
  362. return props.validateStatus;
  363. } else if (debounceErrors.value.length) {
  364. return 'error';
  365. }
  366. return validateState.value;
  367. });
  368. const itemClassName = computed(() => ({
  369. [`${prefixCls.value}-item`]: true,
  370. [hashId.value]: true,
  371. // Status
  372. [`${prefixCls.value}-item-has-feedback`]: mergedValidateStatus.value && props.hasFeedback,
  373. [`${prefixCls.value}-item-has-success`]: mergedValidateStatus.value === 'success',
  374. [`${prefixCls.value}-item-has-warning`]: mergedValidateStatus.value === 'warning',
  375. [`${prefixCls.value}-item-has-error`]: mergedValidateStatus.value === 'error',
  376. [`${prefixCls.value}-item-is-validating`]: mergedValidateStatus.value === 'validating',
  377. [`${prefixCls.value}-item-hidden`]: props.hidden
  378. }));
  379. const formItemInputContext = reactive({});
  380. FormItemInputContext.useProvide(formItemInputContext);
  381. watchEffect(() => {
  382. let feedbackIcon;
  383. if (props.hasFeedback) {
  384. const IconNode = mergedValidateStatus.value && iconMap[mergedValidateStatus.value];
  385. feedbackIcon = IconNode ? _createVNode("span", {
  386. "class": classNames(`${prefixCls.value}-item-feedback-icon`, `${prefixCls.value}-item-feedback-icon-${mergedValidateStatus.value}`)
  387. }, [_createVNode(IconNode, null, null)]) : null;
  388. }
  389. _extends(formItemInputContext, {
  390. status: mergedValidateStatus.value,
  391. hasFeedback: props.hasFeedback,
  392. feedbackIcon,
  393. isFormItemInput: true
  394. });
  395. });
  396. const marginBottom = shallowRef(null);
  397. const showMarginOffset = shallowRef(false);
  398. const updateMarginBottom = () => {
  399. if (itemRef.value) {
  400. const itemStyle = getComputedStyle(itemRef.value);
  401. marginBottom.value = parseInt(itemStyle.marginBottom, 10);
  402. }
  403. };
  404. onMounted(() => {
  405. watch(showMarginOffset, () => {
  406. if (showMarginOffset.value) {
  407. updateMarginBottom();
  408. }
  409. }, {
  410. flush: 'post',
  411. immediate: true
  412. });
  413. });
  414. const onErrorVisibleChanged = nextVisible => {
  415. if (!nextVisible) {
  416. marginBottom.value = null;
  417. }
  418. };
  419. return () => {
  420. var _a, _b;
  421. if (props.noStyle) return (_a = slots.default) === null || _a === void 0 ? void 0 : _a.call(slots);
  422. const help = (_b = props.help) !== null && _b !== void 0 ? _b : slots.help ? filterEmpty(slots.help()) : null;
  423. const withHelp = !!(help !== undefined && help !== null && Array.isArray(help) && help.length || debounceErrors.value.length);
  424. showMarginOffset.value = withHelp;
  425. return wrapSSR(_createVNode("div", {
  426. "class": [itemClassName.value, withHelp ? `${prefixCls.value}-item-with-help` : '', attrs.class],
  427. "ref": itemRef
  428. }, [_createVNode(Row, _objectSpread(_objectSpread({}, attrs), {}, {
  429. "class": `${prefixCls.value}-item-row`,
  430. "key": "row"
  431. }), {
  432. default: () => {
  433. var _a, _b;
  434. return _createVNode(_Fragment, null, [_createVNode(FormItemLabel, _objectSpread(_objectSpread({}, props), {}, {
  435. "htmlFor": htmlFor.value,
  436. "required": isRequired.value,
  437. "requiredMark": formContext.requiredMark.value,
  438. "prefixCls": prefixCls.value,
  439. "onClick": onLabelClick,
  440. "label": props.label
  441. }), {
  442. label: slots.label,
  443. tooltip: slots.tooltip
  444. }), _createVNode(FormItemInput, _objectSpread(_objectSpread({}, props), {}, {
  445. "errors": help !== undefined && help !== null ? toArray(help) : debounceErrors.value,
  446. "marginBottom": marginBottom.value,
  447. "prefixCls": prefixCls.value,
  448. "status": mergedValidateStatus.value,
  449. "ref": inputRef,
  450. "help": help,
  451. "extra": (_a = props.extra) !== null && _a !== void 0 ? _a : (_b = slots.extra) === null || _b === void 0 ? void 0 : _b.call(slots),
  452. "onErrorVisibleChanged": onErrorVisibleChanged
  453. }), {
  454. default: slots.default
  455. })]);
  456. }
  457. }), !!marginBottom.value && _createVNode("div", {
  458. "class": `${prefixCls.value}-margin-offset`,
  459. "style": {
  460. marginBottom: `-${marginBottom.value}px`
  461. }
  462. }, null)]));
  463. };
  464. }
  465. });