diff --git a/packages/devui-vue/devui/form/index.ts b/packages/devui-vue/devui/form/index.ts index bdbb61bbf6..9a258ca540 100644 --- a/packages/devui-vue/devui/form/index.ts +++ b/packages/devui-vue/devui/form/index.ts @@ -4,11 +4,11 @@ import FormLabel from './src/form-label/form-label'; import FormItem from './src/form-item/form-item'; import FormControl from './src/form-control/form-control'; import FormOperation from './src/form-operation/form-operation'; -import dValidateRules from './src/directive/d-validate-rules'; +import dValidate from './src/directive/d-validate'; Form.install = function(app: App) { app.component(Form.name, Form); - app.directive('d-validate-rules', dValidateRules); + app.directive('d-validate', dValidate); } FormLabel.install = function(app: App) { diff --git a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts b/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts deleted file mode 100644 index f8f08980f0..0000000000 --- a/packages/devui-vue/devui/form/src/directive/d-validate-rules.ts +++ /dev/null @@ -1,445 +0,0 @@ -import AsyncValidator, { RuleItem } from 'async-validator'; -import { VNode, DirectiveBinding } from 'vue'; -import { debounce } from 'lodash-es'; -import { EventBus, isObject, hasKey } from '../util'; -import './style.scss'; - -interface ValidateFnParam { - validator: AsyncValidator - modelValue: Record - el: HTMLElement - tipEl: HTMLElement - isFormTag: boolean - message: string - messageShowType: MessageShowType - dfcUID: string - popPosition: PopPosition | Array - updateOn?: UpdateOn -} - -interface CustomValidatorRuleObject { - message: string - validator: (rule, value) => boolean - asyncValidator: (rule, value) => Promise -} - -interface DirectiveValidateRuleOptions { - updateOn?: UpdateOn - errorStrategy?: ErrorStrategy - asyncDebounceTime?: number - popPosition?: PopPosition | Array -} - -interface DirectiveBindingValue { - rules: Partial[] - options: DirectiveValidateRuleOptions - messageShowType: MessageShowType - errorStrategy: ErrorStrategy -} - -interface DirectiveCustomRuleItem extends RuleItem { - validators: CustomValidatorRuleObject[] - asyncValidators: CustomValidatorRuleObject[] -} - -export interface ShowPopoverErrorMessageEventData { - showPopover?: boolean - message?: string - uid?: string, - popPosition?: PopPosition - [prop : string]: any -} - -type MessageShowType = 'popover' | 'text' | 'none' | 'toast'; -type UpdateOn = 'input' | 'focus' | 'change' | 'blur' | 'submit'; -type ErrorStrategy = 'dirty' | 'pristine'; -type BasePopPosition = 'left' | 'right' | 'top' | 'bottom'; -type PopPosition = BasePopPosition | 'left-top' | 'left-bottom' | 'top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right'; - -enum ErrorStrategyEnum { - dirty = 'dirty', - pristine = 'pristine' -} - -enum UpdateOnEnum { - input = 'input', - focus = 'focus', - change = 'change', - blur = 'blur', - submit = 'submit', -} - -enum MessageShowTypeEnum { - popover = 'popover', - text = 'text', - none = 'none', - toast = 'toast' -} - -// 获取async-validator可用的规则名 -function getAvaliableRuleObj(ruleName: string, value: any) { - if(!ruleName) { - console.error("[v-d-validate] validator's key is invalid"); - return null; - } - switch(ruleName) { - case 'maxlength': - return { - type: 'string', - max: value, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val.length > value) { - reject('最大长度为' + value); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'minlength': - return { - type: 'string', - min: value, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val.length < value) { - reject('最小长度为' + value); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'min': - return { - type: 'number', - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val < value) { - reject('最小值为' + value); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'max': - return { - type: 'number', - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(val > value) { - reject('最大值为' + value); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'required': - return { - reqiured: true, - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(!val) { - reject('必填项'); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'requiredTrue': - return { - asyncValidator: (rule, val) => { - return new Promise((resolve, reject) => { - if(!val) { - reject('必须为true值'); - }else { - resolve('校验通过'); - } - }) - } - }; - case 'email': - return { - type: 'email', - message: '邮箱格式不正确' - }; - case 'pattern': - return { - type: 'regexp', - pattern: value, - message: '只能包含数字与大小写字符', - validator: (rule, val) => value.test(val), - }; - case 'whitespace': - return { - message: '输入不能全部为空格或空字符', - validator: (rule, val) => !!val.trim() - }; - default: - return { - [ruleName]: value, - }; - } -} - -function getKeyValueOfObjectList(obj): {key: string; value: any;}[] { - const kvArr = []; - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - kvArr.push({ - key, - value: obj[key] - }) - } - } - return kvArr; -} - -function handleErrorStrategy(el: HTMLElement): void { - const classList: Array = [...el.classList]; - classList.push('d-validate-rules-error-pristine'); - el.setAttribute('class', classList.join(' ')); -} - -function handleErrorStrategyPass(el: HTMLElement): void { - const classList: Array = [...el.classList]; - const index = classList.indexOf('d-validate-rules-error-pristine'); - index !== -1 && classList.splice(index, 1); - el.setAttribute('class', classList.join(' ')); -} - -function getFormControlUID(el: HTMLElement): string { - if(el.tagName.toLocaleLowerCase() === "body") return ""; - let uid = '' - if(el.parentElement.id.startsWith('dfc-')) { - return el.parentElement.id; - }else { - uid = getFormControlUID(el.parentElement); - } -} - -function handleValidateError({el, tipEl, message = "", isFormTag, messageShowType, dfcUID, popPosition = 'right-bottom', updateOn}: Partial): void { - // 如果该指令用在form标签上,这里做特殊处理 - if(isFormTag && messageShowType === MessageShowTypeEnum.toast) { - // todo:待替换为toast - alert(message); - return; - } - - if(!dfcUID) { - dfcUID = getFormControlUID(el); - } - - // messageShowType为popover时,设置popover - if(MessageShowTypeEnum.popover === messageShowType) { - EventBus.emit("showPopoverErrorMessage", {showPopover: true, message, uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData); - return; - } - - tipEl.innerText = '' + message; - tipEl.style.display = 'inline-flex'; - tipEl.setAttribute('class', 'd-validate-tip'); - handleErrorStrategy(el); -} - -function handleValidatePass(el: HTMLElement, tipEl: HTMLElement): void { - tipEl.style.display = 'none'; - handleErrorStrategyPass(el); -} - -// 获取ref的name -function getRefName(binding: DirectiveBinding): string { - const _refs = binding.instance.$refs; - const refName = Object.keys(_refs)[0]; - return refName; -} - -// 获取表单name -function getFormName(binding: DirectiveBinding): string { - const _refs = binding.instance.$refs; - const key = Object.keys(_refs)[0]; - return _refs[key]['name']; -} - -// 校验处理函数 -function validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, dfcUID, popPosition, updateOn}: Partial) { - validator.validate({modelName: modelValue}).then(() => { - handleValidatePass(el, tipEl); - }).catch((err) => { - const { errors } = err; - if(!errors || errors.length === 0) return; - let msg = ''; - - // todo: 待支持国际化 - if(typeof errors[0].message === 'object') { - msg = errors[0].message.default; - }else { - msg = errors[0].message; - } - - handleValidateError({el, tipEl, message: msg, isFormTag, messageShowType, dfcUID, popPosition, updateOn}); - }) -} - -// 检测popover的position是否是正确值 -function checkValidPopsition(positionStr: string): boolean { - const validPosition = ['left', 'right', 'top', 'bottom', 'left-top', 'left-bottom', 'top-left', 'top-right', 'right-top', 'right-bottom', 'bottom-left', 'bottom-right']; - const isValid = validPosition.includes(positionStr); - !isValid && console.warn(`invalid popPosition value '${positionStr}'.`); - return isValid -} - -export default { - mounted(el: HTMLElement, binding: DirectiveBinding, vnode: VNode): void { - const isFormTag = el.tagName === 'FORM'; - const dfcUID = el.parentNode.parentNode.parentElement.dataset.uid; - const refName = getRefName(binding); - - const hasOptions = isObject(binding.value) && hasKey(binding.value, 'options'); - - // 获取指令绑定的值 - let { - rules: bindingRules, - options = {}, - messageShowType = MessageShowTypeEnum.popover - }: DirectiveBindingValue = binding.value; - let { errorStrategy }: DirectiveBindingValue = binding.value; - - if(refName) { - // 判断d-form是否传递了messageShowType属性 - messageShowType = binding.instance[refName]["messageShowType"] ?? "popover"; - } - - // errorStrategy可配置在options对象中 - let { - updateOn = UpdateOnEnum.change, - errorStrategy: ErrorStrategy = ErrorStrategyEnum.dirty, - asyncDebounceTime = 300, - popPosition = ['right', 'bottom'] - }: DirectiveValidateRuleOptions = options; - - // 设置popover的位置 - if(messageShowType === MessageShowTypeEnum.popover) { - if(Array.isArray(popPosition)) { - popPosition = (popPosition.length > 1 ? popPosition.join('-') : popPosition[0]) as PopPosition; - if(!checkValidPopsition(popPosition)) { - popPosition = 'right-bottom'; - } - }else if(!checkValidPopsition(popPosition)) { - popPosition = 'right-bottom'; - } - } - - if(!errorStrategy) { - errorStrategy = ErrorStrategy; - } - - // 判断是否有options,有就取binding.value对象中的rules对象,再判断有没有rules对象,没有就取binding.value - let customRule: Partial | DirectiveBindingValue = {}; - if(hasOptions) { - customRule = bindingRules ?? binding.value - }else { - customRule = binding.value as DirectiveBindingValue - } - - const isCustomValidator = customRule && isObject(customRule) && (hasKey(customRule, 'validators') || hasKey(customRule, 'asyncValidators')); - - const rules = Array.isArray(customRule) ? customRule : [customRule]; - const tipEl = document.createElement('span'); - - // messageShowType控制是否显示文字提示 - if(messageShowType !== MessageShowTypeEnum.none) { - el.parentNode.append(tipEl); - } - - const descriptor = { - modelName: [] - }; - - rules.forEach((rule) => { - const kvObjList = !Array.isArray(rule) && getKeyValueOfObjectList(rule); - let ruleObj: Partial = {}; - let avaliableRuleObj = {}; - kvObjList.forEach(item => { - avaliableRuleObj = getAvaliableRuleObj(item.key, item.value); - ruleObj = {...ruleObj, ...avaliableRuleObj}; - }); - descriptor.modelName.push(ruleObj); - }); - - // 使用自定义的验证器 - if(isCustomValidator) { - // descriptor.modelName = []; - const {validators, asyncValidators} = customRule as DirectiveCustomRuleItem; - - // 校验器 - validators && validators.forEach(item => { - const ruleObj: Partial = { - message: item?.message || '', - validator: (rule, value) => item.validator(rule, value), - } - descriptor.modelName.push(ruleObj); - }); - - // 异步校验器 - asyncValidators && asyncValidators.forEach(item => { - const ruleObj: Partial = { - message: item?.message || '', - asyncValidator: (rule, value) => { - return new Promise(debounce((resolve, reject) => { - const res = item.asyncValidator(rule, value); - if(res) { - resolve(''); - }else { - reject(rule.message); - } - }, asyncDebounceTime)) - }, - } - descriptor.modelName.push(ruleObj); - }); - } - - // 校验器对象 - const validator = new AsyncValidator(descriptor); - - const htmlEventValidateHandler = (e) => { - const modelValue = e.target.value; - if(messageShowType === MessageShowTypeEnum.popover) { - EventBus.emit("showPopoverErrorMessage", {showPopover: false, message: "", uid: dfcUID, popPosition, updateOn} as ShowPopoverErrorMessageEventData); - } - validateFn({validator, modelValue, el, tipEl, isFormTag: false, messageShowType, dfcUID, popPosition, updateOn}); - } - - // 监听事件验证 - vnode.children[0].el.addEventListener(updateOn, htmlEventValidateHandler); - - // 如果校验时机为change,则在focus时关闭popover - if(messageShowType === MessageShowTypeEnum.popover && updateOn === UpdateOnEnum.change) { - vnode.children[0].el.addEventListener('focus', () => { - EventBus.emit("showPopoverErrorMessage", {showPopover: false, uid: dfcUID, updateOn} as ShowPopoverErrorMessageEventData); - }); - } - - // 设置errorStrategy - if(errorStrategy === ErrorStrategyEnum.pristine) { - handleErrorStrategy(el); - // pristine为初始化验证,初始化时需改变下原始值才能出发验证 - vnode.children[0].props.value = '' + vnode.children[0].props.value; - } - - const formName = getFormName(binding); - // 处理表单提交验证 - formName && EventBus.on(`formSubmit:${formName}`, () => { - const modelValue = isFormTag ? '' : vnode.children[0].el.value; - - // 进行提交验证 - validateFn({validator, modelValue, el, tipEl, isFormTag, messageShowType, updateOn: 'submit'}); - }); - - } -} diff --git a/packages/devui-vue/devui/form/src/directive/d-validate.ts b/packages/devui-vue/devui/form/src/directive/d-validate.ts new file mode 100644 index 0000000000..86e8ceb2a6 --- /dev/null +++ b/packages/devui-vue/devui/form/src/directive/d-validate.ts @@ -0,0 +1,261 @@ +import { VNode, DirectiveBinding, h, render, nextTick } from 'vue'; +import { debounce } from 'lodash-es'; +import { EventBus, transformCamelToDash } from '../util'; +import useValidate from '../use-validate'; +import dPopover from '../../../popover/src/popover'; +import {DFormValidateSubmitData} from '../form-types'; +import './style.scss'; + +interface BindingValueRules { + [prop:string]: unknown +} + +interface BindingValue { + prop: string + modelName?: string + rules: BindingValueRules + validators?: any + asyncValidators?: any + errorStrategy?: 'pristine' | 'dirty' + updateOn: 'change' | 'input' | 'submit' + asyncDebounceTime?: number | string + messageShowType?: 'popover' | 'text' | 'none' + popPosition: string | string[] + messageChange?: (msg, { errors, fields }) => {} + [prop: string]: any +} + +export type positionType = 'top' | 'right' | 'bottom' | 'left' | 'left-top' | 'left-bottom' | 'top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right' + +const getTargetElement = (el: HTMLElement, targetTag: string) => { + if (!el) return; + let tempEl:HTMLElement = el; + while(tempEl?.tagName && tempEl.tagName.toLocaleLowerCase() !== 'body') { + if(tempEl.tagName.toLocaleLowerCase() === targetTag) { + return tempEl; + } + tempEl = tempEl.parentElement; + } +} + +export default { + mounted(el: HTMLElement, binding: DirectiveBinding): void { + let { prop, rules, validators, asyncValidators, errorStrategy, updateOn = 'input', asyncDebounceTime = 300, messageShowType = 'popover', messageChange, popPosition = ['right', 'bottom'] }: BindingValue = binding.value; + const {instance, arg: modelName} = binding; + + const instanceRef = instance[Object.keys(instance.$refs)[0]]; + if(instanceRef && instanceRef?.messageShowType) { + messageShowType = instanceRef.messageShowType; + } + const hasModelName = !!modelName; + + const objToStyleString = (obj: any = {}) => { + let style = ''; + for (const key in obj) { + style += `${transformCamelToDash(key)}: ${obj[key]};` + } + return style; + } + + const renderPopover = (msg, visible = true) => { + if(messageShowType !== 'popover') return; + el.style.position = 'relative'; + const popoverPosition = () => { + return Array.isArray(popPosition) ? popPosition.join('-') : popPosition; + } + + const popover = h(dPopover, { + visible: visible, + controlled: updateOn !== 'change', + content: msg, + popType: 'error', + position: popoverPosition() as positionType, + }); + + // 这里使用比较hack的方法控制popover显隐,因为点击popover外部元素隐藏popover之后,再重新传入visible不起作用了,popover不会重新渲染了 + nextTick(() => { + if(visible) { + addElClass(popover.el as HTMLElement, 'devui-popover-isVisible') + }else { + removeElClass(popover.el as HTMLElement, 'devui-popover-isVisible') + } + }) + + const popoverWrapperStyle = () => { + let rect = el.getBoundingClientRect(); + let style: any = { + position: 'absolute', + height: 0, + top: (rect.height / 2) + 'px', + right: 0, + } + + let p = popoverPosition(); + if(popPosition === 'bottom' || popPosition === 'top') { + style.left = '50%'; + } + if(popPosition === 'left' || popPosition === 'right') { + style.top = 0; + } + if(p.includes('top')) { + style.top = -(rect.height / 2) + 'px'; + } + if(p.endsWith('-bottom')) { + style.top = (rect.height / 2) + 'px'; + } + if(p.includes('left')) { + style.left = 0; + } + if(p.includes('right')) { + delete style.left; + style.right = 0; + } + + if(p.startsWith('bottom')) { + delete style.top; + style.bottom = 0; + } + if(p.startsWith('top')) { + delete style.bottom; + } + + return objToStyleString(style); + }; + + const vn = h('div', { + style: popoverWrapperStyle() + }, popover) + render(vn, el); + } + + const tipEl = document.createElement('div'); + if(messageShowType === 'text') { + el.parentNode.appendChild(tipEl); + } + + const renderTipEl = (msg, visible = true) => { + tipEl.innerText = msg; + tipEl.style.display = visible ? 'block' : 'none'; + tipEl.setAttribute('class', 'devui-validate-tip'); + } + + const addElClass = (el: HTMLElement, className: string) => { + let currentClasses = el.getAttribute('class'); + if(!currentClasses.includes(className)) { + currentClasses = currentClasses.trim() + (currentClasses.trim() ? ' ' : '') + className; + } + el.setAttribute('class', currentClasses); + } + + const removeElClass = (el: HTMLElement, className: string) => { + let currentClasses = el.getAttribute('class'); + currentClasses = currentClasses.replace(className, ''); + el.setAttribute('class', currentClasses); + } + + const {validate, createDevUIBuiltinValidator} = useValidate(); + let propRule = {} || [] as any; // 值为对象数组或单个对象 + + const isCustomValidator = validators !== undefined || asyncValidators !== undefined; + if(isCustomValidator) { + validators && (rules = validators); + asyncValidators && (rules = asyncValidators); + if(asyncValidators) { + let time = Number(asyncDebounceTime); + if(isNaN(time)) { + console.warn('[v-d-validate] invalid asyncDebounceTime'); + time = 300; + } + rules = asyncValidators.map(item => { + let res = { + message: item.message, + asyncValidator: (rule, value) => { + return new Promise(debounce((resolve, reject) => { + const res = item.asyncValidator(rule, value); + if(res) { + resolve(''); + }else { + reject(rule.message); + } + }, time)) + }, + } as any; + return res; + }) + } + }else { + if(Array.isArray(rules)) { + rules.map(item => { + return createDevUIBuiltinValidator(item); + }); + }else { + rules = createDevUIBuiltinValidator(rules); + } + } + + let descriptor: any = { + [prop]: rules + } + const validateFn = async () => { + const validateModel = { + [prop]: hasModelName ? instance[modelName][prop] : instance[prop] + }; + return validate(descriptor, validateModel).then(res => { + renderPopover('', false); + removeElClass(el, 'devui-error'); + messageShowType === 'text' && renderTipEl('', true); + return res; + }).catch(({ errors, fields }) => { + let msg = propRule.message ?? fields[prop][0].message; + renderPopover(msg); + addElClass(el, 'devui-error'); + messageShowType === 'text' && renderTipEl(msg, true); + if(messageChange && typeof messageChange === 'function') { + messageChange(msg, { errors, fields }); + } + return { errors, fields }; + }) + } + + if(errorStrategy === 'pristine') { + validateFn(); + }else { + el.childNodes[0].addEventListener(updateOn, () => { + validateFn(); + }) + if(updateOn === 'change') { + el.childNodes[0].addEventListener('focus', () => { + renderPopover('', false); + }) + } + } + + // 处理表单提交校验 + const formTag = getTargetElement(el, 'form') as HTMLFormElement; + if(formTag && updateOn === 'submit') { + const formName = formTag.name; + const formSubmitDataCallback: any = (val: DFormValidateSubmitData) => { + validateFn().then((res: any) => { + val.callback(!!!res?.errors, { errors: res?.errors, fields: res?.fields }); + }).catch(({errors, fields}) => { + console.log('validateFn {errors, fields}', {errors, fields}); + }); + }; + EventBus.on(`formSubmit:${formName}`, formSubmitDataCallback); + EventBus.on(`formReset:${formName}:${prop}`, () => { + renderPopover('', false); + removeElClass(el, 'devui-error'); + messageShowType === 'text' && renderTipEl('', false); + }); + } + }, + + beforeUnmount(el: HTMLElement, binding: DirectiveBinding) { + const {prop} = binding.value; + const formTag = getTargetElement(el, 'form') as HTMLFormElement; + const formName = formTag.name; + + EventBus.off(`formSubmit:${formName}`); + EventBus.off(`formReset:${formName}:${prop}`); + } +} diff --git a/packages/devui-vue/devui/form/src/directive/style.scss b/packages/devui-vue/devui/form/src/directive/style.scss index 56345b9d66..c0d52b348e 100644 --- a/packages/devui-vue/devui/form/src/directive/style.scss +++ b/packages/devui-vue/devui/form/src/directive/style.scss @@ -1,5 +1,5 @@ -.d-validate-rules-error-pristine { +.devui-validate-rules-error-pristine { // background-color: #ffeeed; input { background-color: #ffeeed; @@ -15,10 +15,15 @@ } } -.d-validate-tip { - display: flex; - justify-content: center; - align-items: center; +.devui-validate-tip { + text-align: left; font-size: 12px; color: #f66f6a; } + +.devui-error { + input, .devui-tags { + border-color: var(--devui-danger-line,#f66f6a) !important; + background-color: var(--devui-danger-bg,#ffeeed) !important; + } +} \ No newline at end of file diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.scss b/packages/devui-vue/devui/form/src/form-control/form-control.scss index 1c608a95e5..3bae006aea 100644 --- a/packages/devui-vue/devui/form/src/form-control/form-control.scss +++ b/packages/devui-vue/devui/form/src/form-control/form-control.scss @@ -1,14 +1,15 @@ -.form-control { +.devui-form-control { position: relative; + width: 100%; - .star { + .devui-star { color: red; } .devui-form-control-container { position: relative; - .feedback-status { + .devui-feedback-status { position: absolute; top: 50%; right: 0; @@ -31,6 +32,7 @@ .devui-form-control-container-horizontal { display: flex; + width: 100%; .devui-radio { &:not(:last-child) { @@ -50,43 +52,17 @@ } } - input, - .devui-tags-host { - width: 200px; - } - - .d-validate-tip { - margin: 0 10px; + .devui-validate-tip { + margin: 0; } } .devui-control-content-wrapper { - .devui-popover-wrapper { - position: absolute; - width: 100%; - height: 100%; - left: 0; - top: 0; - z-index: 9; - - & > div { - width: inherit; - height: 100%; - } - } + width: 100%; } - .with-popover { - position: relative; - & > div { - z-index: 10; - } - } - - - - .has-feedback { + .devui-has-feedback { display: flex; align-items: center; @@ -95,7 +71,7 @@ } } - .feedback-error { + .devui-feedback-error { border: 1px solid #f66f6a; border-radius: 2px; @@ -124,4 +100,12 @@ line-height: 1.5; text-align: justify; } + + .devui-error-form-control { + input, .devui-tags { + border-color: var(--devui-danger-line,#f66f6a) !important; + background-color: var(--devui-danger-bg,#ffeeed) !important; + } + } + } diff --git a/packages/devui-vue/devui/form/src/form-control/form-control.tsx b/packages/devui-vue/devui/form/src/form-control/form-control.tsx index c9907d3bd7..187baeeaff 100644 --- a/packages/devui-vue/devui/form/src/form-control/form-control.tsx +++ b/packages/devui-vue/devui/form/src/form-control/form-control.tsx @@ -1,15 +1,12 @@ -import { defineComponent, inject, ref, computed, reactive, onMounted, Teleport } from 'vue'; +import { defineComponent, inject, ref, computed, reactive, onMounted, watch } from 'vue'; import { uniqueId } from 'lodash-es'; -import { IForm, formControlProps, formInjectionKey } from '../form-types'; -import { ShowPopoverErrorMessageEventData } from '../directive/d-validate-rules' +import { IForm, IFormItem, formControlProps, formInjectionKey, formItemInjectionKey } from '../form-types'; import clickoutsideDirective from '../../../shared/devui-directive/clickoutside' -import { EventBus, getElOffset } from '../util'; +import { transformCamelToDash } from '../util'; import Icon from '../../../icon/src/icon'; import Popover from '../../../popover/src/popover'; import './form-control.scss'; -type positionType = 'top' | 'right' | 'bottom' | 'left'; - export default defineComponent({ name: 'DFormControl', directives: { @@ -19,38 +16,74 @@ export default defineComponent({ setup(props, ctx) { const formControl = ref(); const dForm = reactive(inject(formInjectionKey, {} as IForm)); + const dFormItem = reactive(inject(formItemInjectionKey, {} as IFormItem)); const labelData = reactive(dForm.labelData); const isHorizontal = labelData.layout === 'horizontal'; const uid = uniqueId("dfc-"); const showPopover = ref(false); const updateOn = ref('change'); - const tipMessage = ref(""); - const popPosition = ref("bottom"); - let rectInfo: Partial = { - width: 0, - height: 0 - }; - let elOffset = { - left: 0, - top: 0 + const popPosition = ref(props.popPosition); + const messageShowTypeData = ref(props.messageShowType); + const showMessage = ref(dFormItem.showMessage); + if(!messageShowTypeData.value) { + messageShowTypeData.value = dForm.messageShowType as any; } - let popoverLeftPosition = 0 ; - let popoverTopPosition = 0 ; + const objToStyleString = (obj: any = {}) => { + let style = ''; + for (const key in obj) { + style += `${transformCamelToDash(key)}: ${obj[key]};` + } + return style; + } + + let popoverWrapperStyle = () => ''; + const popoverPosition = () => { + return Array.isArray(props.popPosition) ? props.popPosition.join('-') : props.popPosition; + } onMounted(() => { const el = document.getElementById(uid); - elOffset = getElOffset(el); - EventBus.on("showPopoverErrorMessage", (data: ShowPopoverErrorMessageEventData) => { - if (uid === data.uid) { - rectInfo = el.getBoundingClientRect(); - showPopover.value = data.showPopover; - tipMessage.value = data.message; - popPosition.value = data.popPosition as any; // todo: 待popover组件positionType完善类型之后再替换类型 - popoverLeftPosition = popPosition.value === "top" || popPosition.value === "bottom" ? rectInfo.right - (rectInfo.width / 2) : rectInfo.right; - popoverTopPosition = popPosition.value === "top" ? elOffset.top + (rectInfo.height / 2) - rectInfo.height : elOffset.top + (rectInfo.height / 2); - updateOn.value = data.updateOn ?? 'change'; - } - }); + if(messageShowTypeData.value === "popover") { + popoverWrapperStyle = () => { + let rect = el.getBoundingClientRect(); + let style: any = { + position: 'absolute', + height: 0, + top: (rect.height / 2) + 'px', + right: 0, + } + let p = popoverPosition(); + if(popPosition.value === 'bottom' || popPosition.value === 'top') { + style.left = '50%'; + } + if(popPosition.value === 'left' || popPosition.value === 'right') { + style.top = 0; + } + if(p.includes('top')) { + style.top = -(rect.height / 2) + 'px'; + } + if(p.endsWith('-bottom')) { + style.top = (rect.height / 2) + 'px'; + } + if(p.includes('left')) { + style.left = 0; + } + if(p.includes('right')) { + delete style.left; + style.right = 0; + } + + if(p.startsWith('bottom')) { + delete style.top; + style.bottom = -(rect.height / 2) + 'px'; + } + if(p.startsWith('top')) { + delete style.bottom; + } + + return objToStyleString(style); + }; + } }); const iconData = computed(() => { @@ -72,37 +105,38 @@ export default defineComponent({ } } + watch(() => dFormItem.showMessage, (newVal) => { + showMessage.value = newVal; + }, { + deep: true, + }) + return () => { const { feedbackStatus, - extraInfo, + extraInfo } = props; - return
- { showPopover.value && - -
- -
-
- } -
-
+ return
+
+
+ { messageShowTypeData.value === "popover" && +
+
+ +
+
+ } {ctx.slots.default?.()}
{ (feedbackStatus || ctx.slots.suffixTemplate?.()) && -
{extraInfo &&
{extraInfo}
} + {showMessage.value && messageShowTypeData.value === 'text' &&
{dFormItem.tipMessage}
}
} } diff --git a/packages/devui-vue/devui/form/src/form-item/form-item.scss b/packages/devui-vue/devui/form/src/form-item/form-item.scss index 41c1a73969..1e7f8c068a 100644 --- a/packages/devui-vue/devui/form/src/form-item/form-item.scss +++ b/packages/devui-vue/devui/form/src/form-item/form-item.scss @@ -1,36 +1,31 @@ -.form-item { +.devui-form-item { display: flex; // align-items: center; margin-bottom: 20px; } -.form-item-vertical { +.devui-form-item-vertical { flex-direction: column; } -.form-item-columns { +.devui-form-item-columns { flex-direction: column; display: inline-block !important; } // .u-1-3 { // width: 33.3%; // } -.column-item { +.devui-column-item { margin-bottom: 20px; } -.column-item .form-control { - width: 60% !important; -} -.d-validate-tip { - display: flex; - justify-content: center; - align-items: center; +.devui-validate-tip { + text-align: left; font-size: 12px; color: #f66f6a; } -.d-validate-tip-horizontal { +.devui-validate-tip-horizontal { margin-left: 10px; } diff --git a/packages/devui-vue/devui/form/src/form-item/form-item.tsx b/packages/devui-vue/devui/form/src/form-item/form-item.tsx index 71fe459dbe..da246e1522 100644 --- a/packages/devui-vue/devui/form/src/form-item/form-item.tsx +++ b/packages/devui-vue/devui/form/src/form-item/form-item.tsx @@ -1,33 +1,52 @@ -import { defineComponent, reactive, inject, onMounted, onBeforeUnmount, provide, ref} from 'vue'; -import AsyncValidator, { Rules } from 'async-validator'; +import { defineComponent, reactive, inject, onMounted, onBeforeUnmount, provide, ref, watch} from 'vue'; +import { Rules } from 'async-validator'; import mitt from 'mitt'; -import { dFormEvents, dFormItemEvents, IForm, formItemProps, formInjectionKey, formItemInjectionKey } from '../form-types'; +import {cloneDeep} from 'lodash-es'; +import { dFormEvents, IForm, formItemProps, formInjectionKey, formItemInjectionKey } from '../form-types'; +import useValidate from '../use-validate'; +import clickoutsideDirective from '../../../shared/devui-directive/clickoutside' import './form-item.scss'; - export default defineComponent({ name: 'DFormItem', + directives: { + clickoutside: clickoutsideDirective + }, props: formItemProps, setup(props, ctx) { const formItemMitt = mitt(); - const dForm = reactive(inject(formInjectionKey, {} as IForm)); + let dForm = reactive(inject(formInjectionKey, {} as IForm)); const formData = reactive(dForm.formData); const columnsClass = ref(dForm.columnsClass); - const initFormItemData = formData[props.prop]; + const initedFormItemData = cloneDeep(formData[props.prop]); const labelData = reactive(dForm.labelData); const rules = reactive(dForm.rules); - - const resetField = () => { - if(Array.isArray(initFormItemData)) { - formData[props.prop] = [...initFormItemData]; - }else { - formData[props.prop] = initFormItemData; + let updateOn = 'input'; + const ruleItem = rules[props.prop]; + const getValidateUpdateOn = () => { + if(rules && ruleItem) { + if(Array.isArray(ruleItem)) { + ruleItem.map(item => { + item['updateOn'] && (updateOn = item['updateOn']); + }) + }else { + ruleItem['updateOn'] && (updateOn = ruleItem['updateOn']); + } } } + const resetField = () => { + formData[props.prop] = cloneDeep(initedFormItemData); + formItem.showMessage = false; + formItem.tipMessage = ''; + } + const showMessage = ref(false); + const tipMessage = ref(''); const formItem = reactive({ dHasFeedback: props.dHasFeedback, prop: props.prop, + showMessage: showMessage.value, + tipMessage: tipMessage.value, formItemMitt, resetField }) @@ -37,72 +56,80 @@ export default defineComponent({ const isVertical = labelData.layout === 'vertical'; const isColumns = labelData.layout === 'columns'; - const showMessage = ref(false); - const tipMessage = ref(''); - - const validate = (trigger: string) => { - // console.log('trigger', trigger); - + const validate = () => { + const {validate: validateFn, createDevUIBuiltinValidator} = useValidate(); const ruleKey = props.prop; - const ruleItem = rules[ruleKey]; + let ruleItem = rules[ruleKey]; + if(!ruleItem) return; + ruleItem = ruleItem.map(item => { + return createDevUIBuiltinValidator(item); + }); const descriptor: Rules = {}; descriptor[ruleKey] = ruleItem; - - const validator = new AsyncValidator(descriptor); - validator.validate({[ruleKey]: formData[ruleKey]}).then(() => { + validateFn(descriptor, {[ruleKey]: formData[ruleKey]}).then(() => { showMessage.value = false; tipMessage.value = ''; - }).catch(({ errors }) => { + dForm.validateResult = { + prop: props.prop, + valid: true, + message: '', + errors: null, + fields: null, + } + }).catch(({ errors, fields }) => { // console.log('validator errors', errors); showMessage.value = true; tipMessage.value = errors[0].message; - }); - } - const validateEvents = []; - - const addValidateEvents = () => { - if(rules && rules[props.prop]) { - const ruleItem = rules[props.prop]; - let eventName = ruleItem['trigger']; - - if(Array.isArray(ruleItem)) { - ruleItem.forEach((item) => { - eventName = item['trigger']; - const cb = () => validate(eventName); - validateEvents.push({eventName: cb}); - formItem.formItemMitt.on(dFormItemEvents[eventName], cb); - }); - }else { - const cb = () => validate(eventName); - validateEvents.push({eventName: cb}); - ruleItem && formItem.formItemMitt.on(dFormItemEvents[eventName], cb); + dForm.validateResult = { + prop: props.prop, + valid: false, + message: errors[0].message, + errors, + fields, } - } - } - - const removeValidateEvents = () => { - if(rules && rules[props.prop] && validateEvents.length > 0) { - validateEvents.forEach(item => { - formItem.formItemMitt.off(item.eventName, item.cb); - }); - } + }).finally(() => { + formItem.showMessage = showMessage.value; + formItem.tipMessage = tipMessage.value; + dForm.formMitt.emit(`formItem:messageChange`, dForm.validateResult); + }); } onMounted(() => { dForm.formMitt.emit(dFormEvents.addField, formItem); - addValidateEvents(); + getValidateUpdateOn(); }); onBeforeUnmount(() => { dForm.formMitt.emit(dFormEvents.removeField, formItem); - removeValidateEvents(); }); + + // 标志表单域的change + let hasChange = ref(false); + + // 通过watch表单的数据变化进行校验,有2个好处: + // 1. 这样可以不用侵入其他组件去写一些表单相关的代码 + // 2. 可以不使用EventBus即可监听到表单域的数据变化,减少EventBus的使用 + watch(() => formData[props.prop], (newVal, oldVal) => { + updateOn === 'input' && validate(); + hasChange.value = newVal !== oldVal; + }, { + deep: true + }) + + // 通过ClickOutside模拟输入框change事件 + const handleClickOutside = () => { + if(updateOn === 'change' && hasChange.value) { + validate(); + } + hasChange.value = false; + } + return () => { return ( -
+
{ctx.slots.default?.()} -
{showMessage.value && tipMessage.value}
) } diff --git a/packages/devui-vue/devui/form/src/form-label/form-label.scss b/packages/devui-vue/devui/form/src/form-label/form-label.scss index d761f33beb..87d693b7d5 100644 --- a/packages/devui-vue/devui/form/src/form-label/form-label.scss +++ b/packages/devui-vue/devui/form/src/form-label/form-label.scss @@ -1,4 +1,4 @@ -.form-label { +.devui-form-label { // flex: 1 1 auto; -moz-box-flex: 1; text-align: left; @@ -21,30 +21,30 @@ } } -.form-label_sm { +.devui-form-label_sm { width: 80px; min-width: 80px; } -.form-label_sd { +.devui-form-label_sd { width: 100px; min-width: 100px; } -.form-label_lg { +.devui-form-label_lg { width: 150px; min-width: 150px; } -.form-label_center { +.devui-form-label_center { text-align: center; } -.form-label_end { +.devui-form-label_end { text-align: end; } -.form-label-help { +.devui-form-label-help { border-radius: 50%; display: inline-flex; justify-content: center; diff --git a/packages/devui-vue/devui/form/src/form-label/form-label.tsx b/packages/devui-vue/devui/form/src/form-label/form-label.tsx index 51715fdc9a..cbbd85bc22 100644 --- a/packages/devui-vue/devui/form/src/form-label/form-label.tsx +++ b/packages/devui-vue/devui/form/src/form-label/form-label.tsx @@ -17,7 +17,7 @@ export default defineComponent({ const isCenter = computed(() => labelData.labelAlign === 'center').value; const isEnd = computed(() => labelData.labelAlign === 'end').value; - const wrapperCls = `form-label${isHorizontal ? (isSm ? ' form-label_sm' : (isLg ? ' form-label_lg' : ' form-label_sd')) : ''}${isCenter ? ' form-label_center' : (isEnd ? ' form-label_end' : '')}`; + const wrapperCls = `devui-form-label${isHorizontal ? (isSm ? ' devui-form-label_sm' : (isLg ? ' devui-form-label_lg' : ' devui-form-label_sd')) : ''}${isCenter ? ' devui-form-label_center' : (isEnd ? ' devui-form-label_end' : '')}`; const className = `${props.required ? ' devui-required' : ''}`; const style = {display: isHorizontal ? 'inline' : 'inline-block'}; @@ -27,9 +27,9 @@ export default defineComponent({ {ctx.slots.default?.()} { props.hasHelp && props.helpTips && ( - ( - + ) diff --git a/packages/devui-vue/devui/form/src/form-operation/form-operation.scss b/packages/devui-vue/devui/form/src/form-operation/form-operation.scss index 3e4154caac..69db173432 100644 --- a/packages/devui-vue/devui/form/src/form-operation/form-operation.scss +++ b/packages/devui-vue/devui/form/src/form-operation/form-operation.scss @@ -1,4 +1,4 @@ -.form-operation { +.devui-form-operation { .star { color: red; } diff --git a/packages/devui-vue/devui/form/src/form-operation/form-operation.tsx b/packages/devui-vue/devui/form/src/form-operation/form-operation.tsx index 5a589a9a63..f9da081ffa 100644 --- a/packages/devui-vue/devui/form/src/form-operation/form-operation.tsx +++ b/packages/devui-vue/devui/form/src/form-operation/form-operation.tsx @@ -8,7 +8,7 @@ export default defineComponent({ }, setup(props, ctx) { return () => { - return
+ return
{ctx.slots.default?.()}
} diff --git a/packages/devui-vue/devui/form/src/form-types.ts b/packages/devui-vue/devui/form/src/form-types.ts index 5c0d6489be..b62965ba2c 100644 --- a/packages/devui-vue/devui/form/src/form-types.ts +++ b/packages/devui-vue/devui/form/src/form-types.ts @@ -31,7 +31,7 @@ export const formProps = { default: '', }, messageShowType: { - type: String as PropType<'popover' | 'text' | 'toast' | 'none'>, + type: String as PropType<'popover' | 'text' | 'none'>, default: 'popover', }, } as const @@ -63,6 +63,14 @@ export const formLabelProps = { } as const export const formControlProps = { + messageShowType: { + type: String as PropType<'popover' | 'text' | 'none' | ''>, + default: '' // 这里不给默认值,在form-control组件中初始化时会判断是否有初始值,没有初始值则使用d-form中的messageShowType进行初始化 + }, + popPosition: { + type: String as PropType<'top' | 'right' | 'bottom' | 'left' | 'left-top' | 'left-bottom' | 'top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right'>, + default: 'right-bottom' + }, feedbackStatus: { type: String as PropType<'success' | 'error' | 'pending' | ''>, default: '' @@ -95,12 +103,20 @@ export const dFormItemEvents = { export interface IForm { + name: string formData: any labelData: IFormLabel formMitt: Emitter rules: any columnsClass: string messageShowType: string + validateResult: { + prop: string, + valid: boolean, + message: string | MessageType, + errors: any, + fields: any, + } } export interface IFormLabel { @@ -112,6 +128,8 @@ export interface IFormLabel { export interface IFormItem { dHasFeedback: boolean prop: string + showMessage: boolean + tipMessage: string formItemMitt: Emitter resetField(): void } @@ -128,6 +146,12 @@ export type FormItemProps = ExtractPropTypes export type FormLabelProps = ExtractPropTypes export type FormControlProps = ExtractPropTypes +export type MessageType = { + 'zh-cn': string + 'en-us': string + 'default': string +} + export interface IValidators { required: boolean @@ -164,3 +188,11 @@ export const dDefaultValidators = { 'pattern': Validators.pattern, // 配置正则校验,rule中使用:{ pattern: RegExp } 'whitespace': Validators.whiteSpace, // 配置输入不能全为空格限制,rule中使用:{ whitespace: true } }; + +export interface DValidateResult { + errors: any + fields: any +} +export interface DFormValidateSubmitData { + callback(valid: boolean, result: DValidateResult): void +} \ No newline at end of file diff --git a/packages/devui-vue/devui/form/src/form.scss b/packages/devui-vue/devui/form/src/form.scss index f2170c54cc..9942bd035f 100644 --- a/packages/devui-vue/devui/form/src/form.scss +++ b/packages/devui-vue/devui/form/src/form.scss @@ -1,3 +1,3 @@ -.d-form { +.devui-form { position: relative; } diff --git a/packages/devui-vue/devui/form/src/form.tsx b/packages/devui-vue/devui/form/src/form.tsx index f2d48877f8..b9dc778135 100644 --- a/packages/devui-vue/devui/form/src/form.tsx +++ b/packages/devui-vue/devui/form/src/form.tsx @@ -1,21 +1,21 @@ import { defineComponent, provide } from 'vue' import mitt from 'mitt' -import { formProps, FormProps, IFormItem, dFormEvents, formInjectionKey, IForm } from './form-types' +import { formProps, FormProps, IFormItem, dFormEvents, formInjectionKey, DFormValidateSubmitData } from './form-types' import { EventBus } from './util' import './form.scss' - export default defineComponent({ name: 'DForm', props: formProps, - emits: ['submit'], + emits: ['submit', 'messageChange'], setup(props: FormProps, ctx) { const formMitt = mitt(); const fields: IFormItem[] = []; const resetFormFields = () => { fields.forEach((field: IFormItem) => { field.resetField(); + EventBus.emit(`formReset:${props.name}:${field.prop}`); }) } @@ -30,8 +30,17 @@ export default defineComponent({ fields.splice(fields.indexOf(field), 1); } }) + + let resultSet = {}; + formMitt.on('formItem:messageChange', (data: any) => { + if(!data?.prop) return; + resultSet[data.prop] = data; + delete resultSet[data.prop].prop; + ctx.emit('messageChange', resultSet); + }); provide(formInjectionKey, { + name: props.name, formData: props.formData, formMitt, labelData: { @@ -41,13 +50,27 @@ export default defineComponent({ }, rules: props.rules, columnsClass: props.columnsClass, - messageShowType: "popover" + messageShowType: props.messageShowType, + validateResult: undefined }); const onSubmit = (e) => { e.preventDefault(); - ctx.emit('submit', e); - EventBus.emit(`formSubmit:${props.name}`); + let isValid = true, resultList = []; + const formSubmitData: DFormValidateSubmitData = { + callback: (valid, result) => { + // 收集校验回调结果(微任务,校验函数是Promise) + if(!valid) { + isValid = false; + } + resultList.push(result); + } + }; + // 通过宏任务,将之前微任务执行后的结果,统一emit出去 + setTimeout(() => { + ctx.emit('submit', e, isValid, resultList); + }) + EventBus.emit(`formSubmit:${props.name}`, formSubmitData); } return { @@ -58,9 +81,9 @@ export default defineComponent({ } }, render() { - const {onSubmit} = this; + const {onSubmit, name} = this; return ( -
+ {this.$slots.default?.()}
); diff --git a/packages/devui-vue/devui/form/src/use-validate.ts b/packages/devui-vue/devui/form/src/use-validate.ts new file mode 100644 index 0000000000..cd9bf18f96 --- /dev/null +++ b/packages/devui-vue/devui/form/src/use-validate.ts @@ -0,0 +1,120 @@ +import AsyncValidator from 'async-validator'; + +export default function useValidate() { + + // 校验函数 + const validate = (descriptor, validateObject) => { + const validator = new AsyncValidator(descriptor); + return validator.validate(validateObject); + } + + // 创建内置校验器 + const createDevUIBuiltinValidator = (rule) => { + // debugger; + let res = {...rule}; + if(res.min !== undefined) { + res = { + ...res, + message: res.message ?? `最小值为${res.min}`, + asyncValidator: (r, val) => { + return new Promise((resolve, reject) => { + if(val < res.min) { + reject('最小值为' + res.min); + }else { + resolve('校验通过'); + } + }) + } + } + } + + if(res.max !== undefined) { + res = { + ...res, + message: res.message ?? `最大值为${res.max}`, + asyncValidator: (r, val) => { + return new Promise((resolve, reject) => { + if(val > res.max) { + reject('最大值为' + res.max); + }else { + resolve('校验通过'); + } + }) + } + } + } + + if(res.maxlength !== undefined) { + res = { + ...res, + max: res.maxlength, + message: res.message ?? `最大长度为${res.maxlength}` + } + delete res.maxlength; + delete res.asyncValidator; + } + + if(res.minlength !== undefined) { + res = { + ...res, + min: res.minlength, + message: res.message ?? `最小长度为${res.minlength}` + } + delete res.minlength; + delete res.asyncValidator; + } + + + if(res.requiredTrue !== undefined) { + res = { + ...res, + message: res.message ?? `必须为true值`, + asyncValidator: (r, val) => { + return new Promise((resolve, reject) => { + if(!val) { + reject('必须为true值'); + }else { + resolve('校验通过'); + } + }) + } + } + } + if(res.email !== undefined){ + res = { + ...res, + type: 'email', + message: res.message ?? '邮箱格式不正确' + } + delete res.asyncValidator; + } + if(res.pattern !== undefined){ + res = { + ...res, + type: 'pattern', + message: res.message ?? '正则不匹配' + } + delete res.asyncValidator; + } + if(res.whitespace === true){ + res = { + ...res, + type: 'string', + message: res.message ?? '不能全为空格', + asyncValidator: (r, val) => { + return new Promise((resolve, reject) => { + if(val.trim() === '') { + reject('不能全为空格'); + }else { + resolve('校验通过'); + } + }) + } + } + } + + return res; + } + + return {validate, createDevUIBuiltinValidator}; +} diff --git a/packages/devui-vue/devui/form/src/util/index.ts b/packages/devui-vue/devui/form/src/util/index.ts index 8130e097d2..f85d9e09b6 100644 --- a/packages/devui-vue/devui/form/src/util/index.ts +++ b/packages/devui-vue/devui/form/src/util/index.ts @@ -31,3 +31,16 @@ export function getElOffset(curEl: HTMLElement) { return {left: totalLeft, top: totalTop}; } +// 将驼峰转化为中间连接符 +export function transformCamelToDash(str: string = '') { + let res = ''; + for(let i = 0; i < str.length; i++) { + if(/[A-Z]/.test(str[i])) { + res += '-' + str[i].toLocaleLowerCase(); + } + else { + res += str[i]; + } + } + return res; +} \ No newline at end of file diff --git a/packages/devui-vue/devui/style/core/_form.scss b/packages/devui-vue/devui/style/core/_form.scss index 1be1406bad..ccd0134c7a 100755 --- a/packages/devui-vue/devui/style/core/_form.scss +++ b/packages/devui-vue/devui/style/core/_form.scss @@ -203,11 +203,11 @@ $border-change-function: cubic-bezier(0.645, 0.045, 0.355, 1); .devui-form-control { color: $devui-text; background-color: $devui-base-bg; - padding: 5px 10px; - border: 1px solid $devui-form-control-line; + // padding: 5px 10px; + // border: 1px solid $devui-form-control-line; display: block; - width: 100%; - height: 28px; + // width: 100%; + // height: 28px; border-radius: $devui-border-radius; outline: 0; @include border-transition; diff --git a/packages/devui-vue/docs/components/form/index.md b/packages/devui-vue/docs/components/form/index.md index 11eae36d49..259f5a2626 100644 --- a/packages/devui-vue/docs/components/form/index.md +++ b/packages/devui-vue/docs/components/form/index.md @@ -6,18 +6,14 @@ 需要进行数据收集、数据校验、数据提交功能时。 - - ### 基础用法 基本用法当中,Label是在数据框的上面。 - :::demo - ```vue - - ``` - ::: - ### 横向排列 Label左右布局方式。 - :::demo - ```vue - - ``` - ::: - ### 弹框表单 > todo
@@ -269,9 +260,7 @@ export default defineComponent({ 弹框表单,弹框建议是400px,550px,700px,900px,建议宽高比是16: 9、3: 2。 - :::demo - ```vue - ``` - ::: - - ### 模板驱动表单验证 -在`d-form`、`d-input`等表单类组件上使用`v-d-validate-rules`指令,配置校验规则。 - +在`d-input`等表单类组件上使用`v-d-validate`指令,配置校验规则。 -#### 验证单个元素,使用内置校验器,配置error message +#### 验证单个元素,使用内置校验器 当前DevUI支持的内置校验器有:`required`、`minlength`、`maxlength`、`min`、`max`、`requiredTrue`、`email`、`pattern`、`whitespace`。 - 若需限制用户输入不能全为空格,可使用`whitespace`内置校验器 -- 若需限制用户输入长度,将最大限制设置为实际校验值`+1`是一个好的办法。 - -- 除`pattern`外,其他内置校验器我们也提供了内置的错误提示信息,在你未自定义提示消息时,我们将使用默认的提示信息。 +- 内置校验器我们提供了内置的错误提示信息,在你未自定义提示消息时,我们将使用默认的提示信息。 - message配置支持string与object两种形式(支持国际化词条配置,如`'zh-cn'`,默认将取`'default'`)。 :::demo - ```vue - - ``` - ::: #### 验证单个元素,自定义校验器 -自定义校验器,可传入`validators`字段配置校验规则,你可以简单返回`true | false `来标识当前校验是否通过,来标识当前是否错误并返回错误消息,适用于动态错误提示。如果是异步校验器,可传入`asyncValidators`字段配置校验规则。 +自定义校验器,可传入`validators`字段和配置校验规则,你可以简单返回`true | false `来标识当前校验是否通过,来标识当前是否错误并返回错误消息,适用于动态错误提示。如果是异步校验器,可传入`asyncValidators`字段配置校验规则。更多规则参考[async-validator](https://www.npmjs.com/package/async-validator) :::demo - ```vue - - ``` - ::: - #### 验证单个元素,配置错误更新策略errorStrategy、校验时机updateOn - 设置`errorStrategy`属性初始化时是否进行校验 @@ -713,65 +759,47 @@ export default defineComponent({ - 若需要在初始化时将错误抛出,可配置为`pristine` - 设置`updateOn`,指定校验的时机 - - 校验器`updateOn`基于你绑定的模型的`updateOn`设置, 你可以通过`options`来指定, 默认为`change` - - 可选值还有`blur` 、`input`、`submit` + - 校验器`updateOn`基于你绑定的模型的`updateOn`设置,默认为`change` + - 可选值还有`change` 、`input`、`submit` - 设置为`submit`,则当元素所在表单进行提交时将触发校验 :::demo - ```vue - ``` - ::: - #### 验证单个元素,自定义管理消息提示 配置`messageShowType`可选择消息自动提示的方式,默认为`popover`。 @@ -843,48 +873,50 @@ export default defineComponent({ - 设置为`none`错误信息将不会自动呈现到视图, 可在模板中获取`message`或通过监听`messageChange`事件获取错误`message`, 或在模板中直接通过引用获取。 -- 在 `options`中配置 `popPosition`可在消息提示方式为`popover`时,自定义`popover`内容弹出方向, 默认为`['right', 'bottom']`。更多取值参考popover组件。 +- 配置 `popPosition`可在消息提示方式为`popover`时,自定义`popover`内容弹出方向, 默认为`['right', 'bottom']`。更多取值参考popover组件。 :::demo - ```vue - - ``` - ::: - - #### 验证单个元素,自定义asyncDebounceTime - -对于异步校验器,提供默认300ms debounce time。在options中设置`asyncDebounceTime`显示设置(单位ms)。 - +对于异步校验器,提供默认300ms debounce time。设置`asyncDebounceTime`显示设置(单位ms)。 :::demo - ```vue - - ``` - ::: - - #### Form验证与提交 -点击提交按钮时进行验证,需指定name属性,并同时绑定d-form标签的submit事件才能生效。 +点击提交按钮时进行验证,需结合d-form组件使用,并指定d-form的name属性,同时绑定submit事件才能生效。 -:::demo +对于自动错误提示的方式,在form中, 建议在dForm层统一设置`messageShowType`,需同时设置ref属性才能生效。 +:::demo ```vue - - ``` - ::: +### 响应式表单验证 -#### Form验证与提交,用户注册场景 - -对于自动错误提示的方式,在form中, 建议在dForm层统一设置`messageShowType`,需同时设置ref属性才能生效。 - +在`d-form`标签中指定校验规则rules,同时在`d-form-item`中指定`prop`的值为校验字段名。 :::demo - ```vue - - - - - - -``` - -::: - -### 响应式表单验证 - -在`d-form`标签中指定校验规则rules,同时在`d-form-item`中指定`prop`的值为校验字段名。 - - -:::demo - -```vue - - - ``` - ::: - ### 指定表单Feedback状态 你可通过对d-form-control设置feedbackStatus手动指定反馈状态。当前已支持状态:`success`、`error`、`pending`。 - :::demo - ```vue