Skip to content

Commit 030dc52

Browse files
authored
feat: 完成d-validate指令功能 (#248)
1 parent 8faf782 commit 030dc52

File tree

4 files changed

+293
-4
lines changed

4 files changed

+293
-4
lines changed
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import { VNode, DirectiveBinding, h, render, nextTick } from 'vue';
2+
import { debounce } from 'lodash-es';
3+
import { EventBus, transformCamelToDash } from '../util';
4+
import useValidate from '../use-validate';
5+
import dPopover from '../../../popover/src/popover';
6+
import {DFormValidateSubmitData, positionType} from '../form-types';
7+
import './style.scss';
8+
9+
interface BindingValueRules {
10+
[prop:string]: unknown
11+
}
12+
13+
interface BindingValue {
14+
prop: string
15+
modelName?: string
16+
rules: BindingValueRules
17+
validators?: any
18+
asyncValidators?: any
19+
errorStrategy?: 'pristine' | 'dirty'
20+
updateOn: 'change' | 'input' | 'submit'
21+
asyncDebounceTime?: number | string
22+
messageShowType?: 'popover' | 'text' | 'none'
23+
popPosition: string | string[]
24+
messageChange?: (msg, { errors, fields }) => {}
25+
[prop: string]: any
26+
}
27+
28+
const getTargetElement = (el: HTMLElement, targetTag: string) => {
29+
if (!el) return;
30+
let tempEl:HTMLElement = el;
31+
while(tempEl?.tagName && tempEl.tagName.toLocaleLowerCase() !== 'body') {
32+
if(tempEl.tagName.toLocaleLowerCase() === targetTag) {
33+
return tempEl;
34+
}
35+
tempEl = tempEl.parentElement;
36+
}
37+
}
38+
39+
export default {
40+
mounted(el: HTMLElement, binding: DirectiveBinding): void {
41+
let { prop, rules, validators, asyncValidators, errorStrategy, updateOn = 'input', asyncDebounceTime = 300, messageShowType = 'popover', messageChange, popPosition = ['right', 'bottom'] }: BindingValue = binding.value;
42+
const {instance, arg: modelName} = binding;
43+
44+
const instanceRef = instance[Object.keys(instance.$refs)[0]];
45+
if(instanceRef && instanceRef?.messageShowType) {
46+
messageShowType = instanceRef.messageShowType;
47+
}
48+
const hasModelName = !!modelName;
49+
50+
const objToStyleString = (obj: any = {}) => {
51+
let style = '';
52+
for (const key in obj) {
53+
style += `${transformCamelToDash(key)}: ${obj[key]};`
54+
}
55+
return style;
56+
}
57+
58+
const renderPopover = (msg, visible = true) => {
59+
if(messageShowType !== 'popover') return;
60+
el.style.position = 'relative';
61+
const popoverPosition = () => {
62+
return Array.isArray(popPosition) ? popPosition.join('-') : popPosition;
63+
}
64+
65+
const popover = h(dPopover, {
66+
visible: visible,
67+
controlled: updateOn !== 'change',
68+
content: msg,
69+
popType: 'error',
70+
position: popoverPosition() as positionType,
71+
});
72+
73+
// 这里使用比较hack的方法控制popover显隐,因为点击popover外部元素隐藏popover之后,再重新传入visible不起作用了,popover不会重新渲染了
74+
nextTick(() => {
75+
if(visible) {
76+
addElClass(popover.el as HTMLElement, 'devui-popover-isVisible')
77+
}else {
78+
removeElClass(popover.el as HTMLElement, 'devui-popover-isVisible')
79+
}
80+
})
81+
82+
const popoverWrapperStyle = () => {
83+
let rect = el.getBoundingClientRect();
84+
let style: any = {
85+
position: 'absolute',
86+
height: 0,
87+
top: (rect.height / 2) + 'px',
88+
right: 0,
89+
}
90+
91+
let p = popoverPosition();
92+
if(popPosition === 'bottom' || popPosition === 'top') {
93+
style.left = '50%';
94+
}
95+
if(popPosition === 'left' || popPosition === 'right') {
96+
style.top = 0;
97+
}
98+
if(p.includes('top')) {
99+
style.top = -(rect.height / 2) + 'px';
100+
}
101+
if(p.endsWith('-bottom')) {
102+
style.top = (rect.height / 2) + 'px';
103+
}
104+
if(p.includes('left')) {
105+
style.left = 0;
106+
}
107+
if(p.includes('right')) {
108+
delete style.left;
109+
style.right = 0;
110+
}
111+
112+
if(p.startsWith('bottom')) {
113+
delete style.top;
114+
style.bottom = 0;
115+
}
116+
if(p.startsWith('top')) {
117+
delete style.bottom;
118+
}
119+
120+
return objToStyleString(style);
121+
};
122+
123+
const vn = h('div', {
124+
style: popoverWrapperStyle()
125+
}, popover)
126+
render(vn, el);
127+
}
128+
129+
const tipEl = document.createElement('div');
130+
if(messageShowType === 'text') {
131+
el.parentNode.appendChild(tipEl);
132+
}
133+
134+
const renderTipEl = (msg, visible = true) => {
135+
tipEl.innerText = msg;
136+
tipEl.style.display = visible ? 'block' : 'none';
137+
tipEl.setAttribute('class', 'devui-validate-tip');
138+
}
139+
140+
const addElClass = (el: HTMLElement, className: string) => {
141+
let currentClasses = el.getAttribute('class');
142+
if(!currentClasses.includes(className)) {
143+
currentClasses = currentClasses.trim() + (currentClasses.trim() ? ' ' : '') + className;
144+
}
145+
el.setAttribute('class', currentClasses);
146+
}
147+
148+
const removeElClass = (el: HTMLElement, className: string) => {
149+
let currentClasses = el.getAttribute('class');
150+
currentClasses = currentClasses.replace(className, '');
151+
el.setAttribute('class', currentClasses);
152+
}
153+
154+
const {validate, createDevUIBuiltinValidator} = useValidate();
155+
let propRule = {} || [] as any; // 值为对象数组或单个对象
156+
157+
const isCustomValidator = validators !== undefined || asyncValidators !== undefined;
158+
if(isCustomValidator) {
159+
validators && (rules = validators);
160+
asyncValidators && (rules = asyncValidators);
161+
if(asyncValidators) {
162+
let time = Number(asyncDebounceTime);
163+
if(isNaN(time)) {
164+
console.warn('[v-d-validate] invalid asyncDebounceTime');
165+
time = 300;
166+
}
167+
rules = asyncValidators.map(item => {
168+
let res = {
169+
message: item.message,
170+
asyncValidator: (rule, value) => {
171+
return new Promise(debounce((resolve, reject) => {
172+
const res = item.asyncValidator(rule, value);
173+
if(res) {
174+
resolve('');
175+
}else {
176+
reject(rule.message);
177+
}
178+
}, time))
179+
},
180+
} as any;
181+
return res;
182+
})
183+
}
184+
}else {
185+
if(Array.isArray(rules)) {
186+
rules.map(item => {
187+
return createDevUIBuiltinValidator(item);
188+
});
189+
}else {
190+
rules = createDevUIBuiltinValidator(rules);
191+
}
192+
}
193+
194+
let descriptor: any = {
195+
[prop]: rules
196+
}
197+
const validateFn = async () => {
198+
const validateModel = {
199+
[prop]: hasModelName ? instance[modelName][prop] : instance[prop]
200+
};
201+
return validate(descriptor, validateModel).then(res => {
202+
renderPopover('', false);
203+
removeElClass(el, 'devui-error');
204+
messageShowType === 'text' && renderTipEl('', true);
205+
return res;
206+
}).catch(({ errors, fields }) => {
207+
let msg = propRule.message ?? fields[prop][0].message;
208+
renderPopover(msg);
209+
addElClass(el, 'devui-error');
210+
messageShowType === 'text' && renderTipEl(msg, true);
211+
if(messageChange && typeof messageChange === 'function') {
212+
messageChange(msg, { errors, fields });
213+
}
214+
return { errors, fields };
215+
})
216+
}
217+
218+
if(errorStrategy === 'pristine') {
219+
validateFn();
220+
}else {
221+
el.childNodes[0].addEventListener(updateOn, () => {
222+
validateFn();
223+
})
224+
if(updateOn === 'change') {
225+
el.childNodes[0].addEventListener('focus', () => {
226+
renderPopover('', false);
227+
})
228+
}
229+
}
230+
231+
// 处理表单提交校验
232+
const formTag = getTargetElement(el, 'form') as HTMLFormElement;
233+
if(formTag && updateOn === 'submit') {
234+
const formName = formTag.name;
235+
const formSubmitDataCallback: any = (val: DFormValidateSubmitData) => {
236+
validateFn().then((res: any) => {
237+
val.callback(!!!res?.errors, { errors: res?.errors, fields: res?.fields });
238+
}).catch(({errors, fields}) => {
239+
console.log('validateFn {errors, fields}', {errors, fields});
240+
});
241+
};
242+
EventBus.on(`formSubmit:${formName}`, formSubmitDataCallback);
243+
EventBus.on(`formReset:${formName}:${prop}`, () => {
244+
renderPopover('', false);
245+
removeElClass(el, 'devui-error');
246+
messageShowType === 'text' && renderTipEl('', false);
247+
});
248+
}
249+
},
250+
251+
beforeUnmount(el: HTMLElement, binding: DirectiveBinding) {
252+
const {prop} = binding.value;
253+
const formTag = getTargetElement(el, 'form') as HTMLFormElement;
254+
const formName = formTag.name;
255+
256+
EventBus.off(`formSubmit:${formName}`);
257+
EventBus.off(`formReset:${formName}:${prop}`);
258+
}
259+
}

packages/devui-vue/devui/form/src/directive/style.scss

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,15 @@
1515
}
1616
}
1717

18-
.d-validate-tip {
19-
display: flex;
20-
justify-content: center;
21-
align-items: center;
18+
.devui-validate-tip {
19+
text-align: left;
2220
font-size: 12px;
2321
color: #f66f6a;
2422
}
23+
24+
.devui-error {
25+
input, .devui-tags {
26+
border-color: var(--devui-danger-line,#f66f6a) !important;
27+
background-color: var(--devui-danger-bg,#ffeeed) !important;
28+
}
29+
}

packages/devui-vue/devui/form/src/form-types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,15 @@ export const dDefaultValidators = {
164164
'pattern': Validators.pattern, // 配置正则校验,rule中使用:{ pattern: RegExp }
165165
'whitespace': Validators.whiteSpace, // 配置输入不能全为空格限制,rule中使用:{ whitespace: true }
166166
};
167+
168+
169+
export type positionType = 'top' | 'right' | 'bottom' | 'left' | 'left-top' | 'left-bottom' | 'top-left' | 'top-right' | 'right-top' | 'right-bottom' | 'bottom-left' | 'bottom-right'
170+
171+
export interface DValidateResult {
172+
errors: any
173+
fields: any
174+
}
175+
176+
export interface DFormValidateSubmitData {
177+
callback(valid: boolean, result: DValidateResult): void
178+
}

packages/devui-vue/devui/form/src/util/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@ export function getElOffset(curEl: HTMLElement) {
3131
return {left: totalLeft, top: totalTop};
3232
}
3333

34+
// 将驼峰转化为中间连接符
35+
export function transformCamelToDash(str: string = '') {
36+
let res = '';
37+
for(let i = 0; i < str.length; i++) {
38+
if(/[A-Z]/.test(str[i])) {
39+
res += '-' + str[i].toLocaleLowerCase();
40+
}
41+
else {
42+
res += str[i];
43+
}
44+
}
45+
return res;
46+
}

0 commit comments

Comments
 (0)