UNPKG

ivue-material-plus

Version:

A high quality UI components Library with Vue.js

1 lines 21 kB
{"version":3,"file":"form-item.mjs","sources":["../../../../../src/components/ivue-form/form-item.vue"],"sourcesContent":["<template>\r\n <div :class=\"wrapperClasses\" ref=\"formItem\">\r\n <!-- 标签 -->\r\n <form-label-wrap\r\n :is-auto-width=\"labelStyle.width === 'auto'\"\r\n :update-all=\"formContext?.labelWidth === 'auto'\"\r\n >\r\n <component\r\n :class=\"`${prefixCls}--label`\"\r\n :is=\"labelFor ? 'label' : 'div'\"\r\n :id=\"labelId\"\r\n :for=\"labelFor\"\r\n :style=\"labelStyle\"\r\n v-if=\"showLabel\"\r\n >\r\n <slot name=\"label\" :label=\"currentLabel\">{{ currentLabel }}</slot>\r\n </component>\r\n </form-label-wrap>\r\n <!-- 内容 -->\r\n <div :class=\"`${prefixCls}--content`\" :style=\"contentStyle\">\r\n <slot></slot>\r\n <!-- 错误提示 -->\r\n <transition :name=\"`${prefixCls}-zoom-in-top`\">\r\n <slot name=\"error\" :error=\"validateMessage\" v-if=\"shouldShowError\">\r\n <div :class=\"validateClasses\">{{ validateMessage }}</div>\r\n </slot>\r\n </transition>\r\n <!-- 成功 -->\r\n <transition :name=\"`${prefixCls}-zoom-in-top`\">\r\n <slot name=\"success\" v-if=\"shouldShowSuccess && validateSuccessMessage\">\r\n <div :class=\"validateClasses\">{{ validateSuccessMessage }}</div>\r\n </slot>\r\n </transition>\r\n </div>\r\n </div>\r\n</template>\r\n\r\n<script lang=\"ts\">\r\nimport {\r\n computed,\r\n defineComponent,\r\n ref,\r\n provide,\r\n reactive,\r\n toRefs,\r\n inject,\r\n onMounted,\r\n onBeforeUnmount,\r\n nextTick,\r\n watch,\r\n} from 'vue';\r\nimport { isString } from '@vue/shared';\r\nimport AsyncValidator from 'async-validator';\r\nimport { refDebounced } from '@vueuse/core';\r\nimport { clone } from 'lodash-unified';\r\n\r\nimport FormLabelWrap from './form-label-wrap';\r\nimport IvueIcon from '../ivue-icon/index.vue';\r\n\r\nimport { useId } from '../../hooks';\r\nimport { addUnit } from '../../utils/dom/style';\r\nimport { isFunction } from '../../utils/helpers';\r\nimport { ensureArray } from '../../utils/arrays';\r\nimport { getProp } from '../../utils/objects';\r\n\r\n// type\r\nimport type { CSSProperties } from 'vue';\r\nimport type { RuleItem } from 'async-validator';\r\nimport { FormContextKey, FormValidateFailure } from './types/form';\r\nimport {\r\n Props,\r\n FormItemContextKey,\r\n FormItemContext,\r\n FormItemRule,\r\n Arrayable,\r\n FormItemValidateState,\r\n} from './types/form-item';\r\n\r\nconst prefixCls = 'ivue-form-item';\r\n\r\nexport default defineComponent({\r\n name: prefixCls,\r\n props: {\r\n /**\r\n * 标签文本\r\n *\r\n * @type {String}\r\n */\r\n label: {\r\n type: String,\r\n },\r\n /**\r\n * 属性规定 label 与哪个表单元素绑定\r\n *\r\n * @type {String}\r\n */\r\n for: {\r\n type: String,\r\n },\r\n /**\r\n * 标签宽度\r\n *\r\n * @type {String | Number}\r\n */\r\n labelWidth: {\r\n type: [String, Number],\r\n default: '',\r\n },\r\n /**\r\n * model 的键名\r\n *\r\n * @type {String | String[]}\r\n */\r\n prop: {\r\n type: [String, Array],\r\n },\r\n /**\r\n * 是否为必填项,如不设置,则会根据校验规则确认\r\n *\r\n * @type {Boolean}\r\n */\r\n required: {\r\n type: Boolean,\r\n },\r\n /**\r\n * 表单验证规则\r\n *\r\n * @type {Object | Array}\r\n */\r\n rules: {\r\n type: [Object, Array],\r\n },\r\n /**\r\n * 是否显示校验错误信息\r\n *\r\n * @type {Boolean}\r\n */\r\n showMessage: {\r\n type: Boolean,\r\n default: true,\r\n },\r\n /**\r\n * 验证成功提示状态\r\n *\r\n * @type {Boolean}\r\n */\r\n showSuccessStatus: {\r\n type: Boolean,\r\n default: false,\r\n },\r\n /**\r\n * 验证成功提示信息\r\n *\r\n * @type {String}\r\n */\r\n validateSuccessMessage: {\r\n type: String,\r\n default: '',\r\n },\r\n /**\r\n * 表单域验证错误时的提示信息\r\n *\r\n * @type {String}\r\n */\r\n error: {\r\n type: String,\r\n },\r\n },\r\n setup(props: Props, { slots }) {\r\n // form inject\r\n const formContext = inject(FormContextKey, undefined);\r\n // 父级item\r\n const parentFormItemContext = inject(FormItemContextKey, undefined);\r\n\r\n // 是否是嵌套的item\r\n const isNested = !!parentFormItemContext;\r\n\r\n // label id\r\n const labelId = useId().value;\r\n\r\n // ref\r\n\r\n // 输入框id\r\n const inputIds = ref<string[]>([]);\r\n // 验证状态\r\n const validateState = ref('');\r\n // 验证状态节流\r\n const validateStateDebounced = refDebounced(validateState, 100);\r\n\r\n // 是否重置验证\r\n const isResettingField = ref<boolean>(false);\r\n // 验证提示\r\n const validateMessage = ref<string>('');\r\n // item dom\r\n const formItem = ref<HTMLDivElement>();\r\n // 内敛值\r\n const initialValue = ref(undefined);\r\n\r\n // computed\r\n\r\n // class\r\n const wrapperClasses = computed(() => {\r\n return [\r\n prefixCls,\r\n {\r\n // 错误提示\r\n ['is-error']: validateState.value === 'error',\r\n // 成功提示\r\n ['is-success']: shouldShowSuccess.value,\r\n // 必填\r\n ['is-required']: isRequired.value || props.required,\r\n // 是否隐藏必填字段标签旁边的红色星号\r\n ['is-no-asterisk']: formContext.hideRequiredAsterisk,\r\n },\r\n // 星号的位置\r\n formContext.requireAsteriskPosition === 'right'\r\n ? 'asterisk-right'\r\n : 'asterisk-left',\r\n ];\r\n });\r\n\r\n // 提示样式\r\n const validateClasses = computed(() => {\r\n return {\r\n [`${prefixCls}--error`]: shouldShowError.value,\r\n [`${prefixCls}--success`]: shouldShowSuccess.value,\r\n };\r\n });\r\n\r\n // 结果反馈图标\r\n const iconClasses = computed(() => {\r\n return [\r\n `${prefixCls}--icon`,\r\n {\r\n [`${prefixCls}--icon__${validateState.value}`]: validateState.value,\r\n },\r\n ];\r\n });\r\n\r\n // 属性规定 label 与哪个表单元素绑定\r\n const labelFor = computed(() => {\r\n return props.for || inputIds.value.length === 1\r\n ? inputIds.value[0]\r\n : undefined;\r\n });\r\n\r\n // 当前标签文本\r\n const currentLabel = computed(\r\n () => `${props.label || ''}${formContext?.labelSuffix || ''}`\r\n );\r\n\r\n // 标签样式\r\n const labelStyle = computed<CSSProperties>(() => {\r\n let obj = {};\r\n\r\n if (formContext?.labelPosition === 'top') {\r\n return obj;\r\n }\r\n\r\n // 添加单位\r\n const labelWidth = addUnit(\r\n props.labelWidth || formContext?.labelWidth || ''\r\n );\r\n\r\n if (labelWidth) {\r\n obj = {\r\n width: labelWidth,\r\n };\r\n }\r\n\r\n return obj;\r\n });\r\n\r\n // 是否显示标签\r\n const showLabel = computed<boolean>(() => {\r\n return !!(props.label || slots.label);\r\n });\r\n\r\n // 内容样式\r\n const contentStyle = computed<CSSProperties>(() => {\r\n let obj = {};\r\n\r\n if (formContext?.labelPosition === 'top') {\r\n return obj;\r\n }\r\n\r\n // 是嵌套的item\r\n if (!props.label && !props.labelWidth && isNested) {\r\n return obj;\r\n }\r\n\r\n // 添加单位\r\n const labelWidth = addUnit(\r\n props.labelWidth || formContext?.labelWidth || ''\r\n );\r\n\r\n if (!props.label && !slots.label) {\r\n obj = {\r\n marginLeft: labelWidth,\r\n };\r\n }\r\n\r\n return obj;\r\n });\r\n\r\n // 格式化验证\r\n const normalizedRules = computed(() => {\r\n const { required } = props;\r\n\r\n const rules: FormItemRule[] = [];\r\n\r\n // 当前item有设置表单验证规则\r\n if (props.rules) {\r\n rules.push(...ensureArray(props.rules));\r\n }\r\n\r\n // 获取form的验证规则\r\n const formRules = formContext?.rules;\r\n\r\n // 有验证规则 && 有 model 的键名\r\n if (formRules && props.prop) {\r\n // 获取当前item对应的规则\r\n const currentRules = getProp<Arrayable<FormItemRule> | undefined>(\r\n formRules,\r\n props.prop\r\n ).value;\r\n\r\n // 有当前item对应的规则\r\n if (currentRules) {\r\n rules.push(...ensureArray(currentRules));\r\n }\r\n }\r\n\r\n // 是否为必填项\r\n if (required) {\r\n // 必填的验证\r\n const requiredRules = rules\r\n .map((rule, index) => [rule, index] as const)\r\n .filter(([rule]) => Object.keys(rule).includes('required'));\r\n\r\n // 有必填项\r\n if (requiredRules.length > 0) {\r\n for (const [rule, index] of requiredRules) {\r\n // 必填项\r\n if (rule.required === required) {\r\n continue;\r\n }\r\n\r\n rules[index] = {\r\n ...rule,\r\n required,\r\n };\r\n }\r\n }\r\n // 没有必填项\r\n else {\r\n rules.push({\r\n required,\r\n });\r\n }\r\n }\r\n\r\n return rules;\r\n });\r\n\r\n // 是否需要验证\r\n const validateEnabled = computed(() => normalizedRules.value.length > 0);\r\n\r\n // model的键名转换为 string\r\n const propString = computed(() => {\r\n if (!props.prop) {\r\n return '';\r\n }\r\n\r\n return isString(props.prop) ? props.prop : props.prop.join('.');\r\n });\r\n\r\n // 获取该item的 model 值\r\n const fieldValue = computed(() => {\r\n // 表单数据对象\r\n const model = formContext?.model;\r\n\r\n if (!model || !props.prop) {\r\n return;\r\n }\r\n\r\n return getProp(model, props.prop).value;\r\n });\r\n\r\n // 显示错误提示\r\n const shouldShowError = computed(() => {\r\n return (\r\n // 验证状态节流\r\n validateStateDebounced.value === 'error' &&\r\n // 是否显示校验错误信息\r\n props.showMessage &&\r\n // 是否显示校验错误信息\r\n (formContext?.showMessage ?? true)\r\n );\r\n });\r\n\r\n // 显示成功提示\r\n const shouldShowSuccess = computed(() => {\r\n return (\r\n // 验证状态节流\r\n validateStateDebounced.value === 'success' &&\r\n // 没有错误信息\r\n !shouldShowError.value &&\r\n // item验证成功提示状态\r\n (props.showSuccessStatus || formContext?.showSuccessStatus)\r\n );\r\n });\r\n\r\n // 是必填\r\n const isRequired = computed(() => {\r\n return normalizedRules.value.some((rule) => rule.required);\r\n });\r\n\r\n // 有标签\r\n const hasLabel = computed(() => {\r\n return !!(props.label || slots.label);\r\n });\r\n\r\n // methods\r\n\r\n // 添加输入框id\r\n const addInputId = (id: string) => {\r\n if (!inputIds.value.includes(id)) {\r\n inputIds.value.push(id);\r\n }\r\n };\r\n\r\n // 删除输入框id\r\n const removeInputId = (id: string) => {\r\n inputIds.value = inputIds.value.filter((listId) => listId !== id);\r\n };\r\n\r\n // 验证字段\r\n const validate: FormItemContext['validate'] = async (\r\n // 触发方式\r\n trigger,\r\n // 回调函数\r\n callback\r\n ) => {\r\n // 如果重置则跳过验证\r\n if (isResettingField.value || !props.prop) {\r\n return false;\r\n }\r\n\r\n // 是否有回调函数\r\n const hasCallback = isFunction(callback);\r\n\r\n // 不需要验证\r\n if (!validateEnabled.value) {\r\n callback?.(false);\r\n\r\n return false;\r\n }\r\n\r\n // 获取过滤规则\r\n const rules = getFilteredRule(trigger);\r\n if (rules.length === 0) {\r\n callback?.(true);\r\n return true;\r\n }\r\n\r\n // 验证中\r\n setValidationState('validating');\r\n\r\n // 验证规则\r\n return useValidate(rules)\r\n .then(() => {\r\n callback?.(true);\r\n\r\n return true as const;\r\n })\r\n .catch((err: FormValidateFailure) => {\r\n const { fields } = err;\r\n\r\n callback?.(false, fields);\r\n\r\n // 有回调函数\r\n return hasCallback ? false : Promise.reject(fields);\r\n });\r\n };\r\n\r\n // 设置验证状态\r\n const setValidationState = (state: FormItemValidateState) => {\r\n validateState.value = state;\r\n };\r\n\r\n // 设置验证状态为成功\r\n const setValidationSucceeded = () => {\r\n setValidationState('success');\r\n\r\n // 任一表单项被校验后触发\r\n formContext?.emit('on-validate', props.prop!, true, '');\r\n };\r\n\r\n // 设置验证状态为失败\r\n const setValidationFailed = (error: FormValidateFailure) => {\r\n const { errors, fields } = error;\r\n\r\n if (!errors || !fields) {\r\n // eslint-disable-next-line no-console\r\n console.error(error);\r\n }\r\n\r\n // 设置验证状态为失败\r\n setValidationState('error');\r\n\r\n // 验证提示\r\n validateMessage.value = errors\r\n ? errors?.[0]?.message ?? `${props.prop} is required`\r\n : '';\r\n\r\n // 任一表单项被校验后触发\r\n formContext?.emit(\r\n 'on-validate',\r\n props.prop!,\r\n false,\r\n validateMessage.value\r\n );\r\n };\r\n\r\n // 验证规则\r\n const useValidate = async (rules: RuleItem[]): Promise<true> => {\r\n // model的键名\r\n const modelName = propString.value;\r\n\r\n // 验证规则\r\n const validator = new AsyncValidator({\r\n [modelName]: rules,\r\n });\r\n\r\n return validator\r\n .validate(\r\n // 要验证的对象\r\n {\r\n [modelName]: fieldValue.value,\r\n },\r\n {\r\n // 当指定字段的第一条校验规则产生错误时调用,\r\n // 不再处理同字段的校验规则。\r\n // true表示所有字段\r\n firstFields: true,\r\n }\r\n )\r\n .then(() => {\r\n // 设置验证状态为成功\r\n setValidationSucceeded();\r\n\r\n return true as const;\r\n })\r\n .catch((err: FormValidateFailure) => {\r\n // 设置验证状态为失败\r\n setValidationFailed(err as FormValidateFailure);\r\n\r\n return Promise.reject(err);\r\n });\r\n };\r\n\r\n // 获取过滤规则\r\n const getFilteredRule = (trigger: string) => {\r\n const rules = normalizedRules.value;\r\n\r\n return (\r\n rules\r\n .filter((rule) => {\r\n // 验证逻辑的触发方式\r\n if (!rule.trigger || !trigger) {\r\n return true;\r\n }\r\n\r\n // 验证逻辑的触发方式\r\n if (Array.isArray(rule.trigger)) {\r\n return rule.trigger.includes(trigger);\r\n } else {\r\n return rule.trigger === trigger;\r\n }\r\n })\r\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\r\n .map(({ trigger, ...rule }): RuleItem => rule)\r\n );\r\n };\r\n\r\n // 对该表单项进行重置\r\n const resetField: FormItemContext['resetField'] = async () => {\r\n const model = formContext?.model;\r\n\r\n if (!model || !props.prop) {\r\n return;\r\n }\r\n\r\n // 获取当前item对应的规则\r\n const computedValue = getProp(model, props.prop);\r\n\r\n // 防止验证被触发\r\n isResettingField.value = true;\r\n\r\n // 重置的时候重新赋值给props\r\n computedValue.value = clone(initialValue.value);\r\n\r\n // nextTick\r\n await nextTick();\r\n\r\n // 清除验证\r\n clearValidate();\r\n\r\n // 是否重置验证\r\n isResettingField.value = false;\r\n };\r\n\r\n // 清除验证\r\n const clearValidate: FormItemContext['clearValidate'] = () => {\r\n // 清除验证\r\n setValidationState('');\r\n\r\n // 验证提示\r\n validateMessage.value = '';\r\n\r\n // 是否重置验证\r\n isResettingField.value = false;\r\n };\r\n\r\n // provide\r\n const context: FormItemContext = reactive({\r\n ...toRefs(props),\r\n $el: formItem,\r\n inputIds,\r\n addInputId,\r\n removeInputId,\r\n validate,\r\n resetField,\r\n clearValidate,\r\n hasLabel,\r\n });\r\n\r\n // provide\r\n provide(FormItemContextKey, context);\r\n\r\n // onMounted\r\n onMounted(() => {\r\n if (props.prop) {\r\n // 添加验证字段\r\n formContext?.addField(context);\r\n // 保存当前 item 的值\r\n initialValue.value = clone(fieldValue.value);\r\n }\r\n });\r\n\r\n // onBeforeUnmount\r\n onBeforeUnmount(() => {\r\n formContext?.removeField(context);\r\n });\r\n\r\n // watch\r\n\r\n // 监听表单域验证错误时的提示信息\r\n watch(\r\n () => props.error,\r\n (value) => {\r\n // 验证提示\r\n validateMessage.value = value || '';\r\n\r\n // 设置验证状态\r\n setValidationState(value ? 'error' : '');\r\n },\r\n {\r\n immediate: true,\r\n }\r\n );\r\n\r\n return {\r\n prefixCls,\r\n\r\n // inject\r\n formContext,\r\n\r\n // dom\r\n formItem,\r\n\r\n // data\r\n validateMessage,\r\n validateState,\r\n labelId,\r\n\r\n // computed\r\n wrapperClasses,\r\n validateClasses,\r\n iconClasses,\r\n labelStyle,\r\n contentStyle,\r\n currentLabel,\r\n labelFor,\r\n showLabel,\r\n shouldShowError,\r\n shouldShowSuccess,\r\n hasLabel,\r\n\r\n // methods\r\n addInputId,\r\n removeInputId,\r\n resetField,\r\n clearValidate,\r\n validate,\r\n };\r\n },\r\n components: {\r\n FormLabelWrap,\r\n IvueIcon,\r\n },\r\n});\r\n</script>\r\n"],"names":["_resolveComponent","_openBlock","_createElementBlock","_normalizeClass","_createCommentVNode","_createVNode","_withCtx","_resolveDynamicComponent","_normalizeStyle","_createElementVNode","_renderSlot","_Transition"],"mappings":";;;;;;qCACEA,iBAiCM,iBAAA,CAAA,CAAA;EAjC2B,OAAAC,SAAA,EAAU,EAAAC,kBAAA,CAAA,KAAA,EAAA;AAAA,IAAA,KAAA,EAAAC,cAAA,CAAA,IAAA,CAAA,cAAA,CAAA;AAAA,IACzC,GAAA,EAAA,UAAA;AAAA,GACA,EAAA;AAAA,IACkBC,mBAAA,gBAAA,CAAA;AAAA,IAAAC,YACf,0BAAyB,EAAA;AAAA,MAAA,eAAA,EAAA,IAAA,CAAA,UAAA,CAAA,KAAA,KAAA,MAAA;AAAA,MAWd,YAAA,EAAA,CAAA,CAAA,EAAA,GAAA,IAAA,CAAA,WAAA,KAAA,IAAA,GAAA,KAAA,CAAA,GAAA,EAAA,CAAA,UAAA,MAAA,MAAA;AAAA,KAAA,EAAA;AAAA,MATZ,OAAA,EAAAC,QAAA,MAAA;AAAA,QAAA,IAAA,CAAA,SAAA,IAAAL,SAAA,gBACaM,wBAAS,IAAA,CAAA,QAAA,GAAA,OAAA,GAAA,KAAA,CAAA,EAAA;AAAA,UAEnB,GAAI,EAAA,CAAA;AAAA,UACJ,KAAK,EAAAJ,cAAA,CAAQ,CAAA,EAAA,IAAA,CAAA,SAAA,CAAA,OAAA,CAAA,CAAA;AAAA,UACb,IAAK,IAAA,CAAA,OAAA;AAAA,UAAA,KAAA,IAAA,CAAA,QAAA;AAAA,UAG4D,KAAA,EAAAK,cAAA,CAAA,IAAA,CAAA,UAAA,CAAA;AAAA,SAAA,EAAA;AAAA;;;;;;;;MAGtE,CAAA,EAAA,CAAA;AAAA,KAeM,EAAA,CAAA,EAAA,CAAA,eAAA,EAAA,YAAA,CAAA,CAAA;AAAA,IAdKJ,mBAAA,gBAAA,CAAA;AAAA,IAAAK,mBAAiC,KAAE,EAAA;AAAA,MAAA,KAAA,EAAAN,cAAA,CAAA,CAAA,EAAA,IAAA,CAAA,SAAA,CAAA,SAAA,CAAA,CAAA;AAAA,MAC5C,KAAA,EAAaK,cAAA,CAAA,IAAA,CAAA,YAAA,CAAA;AAAA,KACb,EAAA;AAAA,MACAE,UAAA,CAIa,IAAA,CAAA,MAAA,EAAA,SAAA,CAAA;AAAA,MAAAN,mBAJS,4BAAS,CAAA;AAAA,MAAAC,YAAAM,UAAA,EAAA;AAAA;;iBAC7BL,QAEO,MAAA;AAAA,UAAA,IAAA,CAAA,eAAA,GAAAI,UAFoB,CAAA,IAAA,CAAe,QAAA,OAAA,EAAA;AAAA,YAEnC,GAAA,EAAA,CAAA;AAAA,YADL,OAAA,IAAA,CAAA,eAAA;AAAA,aAAW,MAAA;AAAA,YAAsBD,mBAAA,KAAA,EAAA;AAAA,cAAA,KAAA,EAAAN,cAAA,CAAA,IAAA,CAAA,eAAA,CAAA;AAAA;;;QAGrC,CAAA,EAAA,CAAA;AAAA,OAKa,EAAA,CAAA,EAAA,CAAA,MAAA,CAAA,CAAA;AAAA,MAAAC,mBAJS,gBAAS,CAAA;AAAA,MAAAC,YAAAM,UAAA,EAAA;AAAA;;iBAC7BL,QAEO,MAAA;AAAA,UADL,IAAA,CAAA,iBAAA,IAAgE,IAArD,CAAA,sBAAA,GAAAI,UAAA,CAAA,IAAA,CAAA,MAAA,EAAA,SAAA,EAAE,EAAe,GAAA,EAAA,CAAA,EAAA,EAAA,MAAA;AAAA,YAAKD,mBAAA,KAAA,EAAA;AAAA,cAAA,KAAA,EAAAN,cAAA,CAAA,IAAA,CAAA,eAAA,CAAA;AAAA;;;;;;;;;;;;"}