Skip to content

Commit ff3c690

Browse files
authored
feat(Form): 完成基本的校验功能 (DevCloudFE#534)
1 parent f71d7b9 commit ff3c690

File tree

17 files changed

+437
-239
lines changed

17 files changed

+437
-239
lines changed

packages/devui-vue/devui/form/src/components/form-control/form-control.scss

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import '../../../../styles-var/devui-var.scss';
2+
13
.devui-form__control {
24
flex: 1 1 auto;
35
position: relative;
@@ -73,11 +75,20 @@
7375
}
7476
}
7577

76-
.devui-form__control-extra {
77-
font-size: 12px;
78-
color: #8a8e99;
79-
min-height: 20px;
80-
line-height: 1.5;
81-
text-align: justify;
78+
.devui-form__control-info {
79+
.error-message {
80+
display: inline-block;
81+
min-height: 20px;
82+
font-size: $devui-font-size;
83+
color: $devui-danger;
84+
}
85+
86+
.devui-form__control-extra {
87+
font-size: 12px;
88+
color: #8a8e99;
89+
min-height: 20px;
90+
line-height: 1.5;
91+
text-align: justify;
92+
}
8293
}
8394
}

packages/devui-vue/devui/form/src/components/form-control/form-control.tsx

Lines changed: 8 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,32 @@
1-
import { defineComponent, ref, computed, onMounted, Teleport } from 'vue';
1+
import { defineComponent, ref } from 'vue';
22
import type { SetupContext } from 'vue';
33
import { uniqueId } from 'lodash';
44
import { formControlProps, FormControlProps } from './form-control-types';
5-
import { ShowPopoverErrorMessageEventData } from '../../directives/d-validate-rules';
6-
import clickoutsideDirective from '../../../../shared/devui-directive/clickoutside';
7-
import { EventBus, getElOffset } from '../../utils';
8-
import Icon from '../../../../icon/src/icon';
9-
import Popover from '../../../../popover/src/popover';
105
import { useNamespace } from '../../../../shared/hooks/use-namespace';
6+
import { useFormControl, useFormControlValidate } from './use-form-control';
117
import './form-control.scss';
12-
import { useFormControl } from './use-form-control';
13-
14-
type positionType = 'top' | 'right' | 'bottom' | 'left';
158

169
export default defineComponent({
1710
name: 'DFormControl',
18-
directives: {
19-
clickoutside: clickoutsideDirective,
20-
},
2111
props: formControlProps,
2212
setup(props: FormControlProps, ctx: SetupContext) {
2313
const formControl = ref();
2414
const uid = uniqueId('dfc-');
25-
const showPopover = ref(false);
26-
const updateOn = ref('change');
27-
const tipMessage = ref('');
28-
const popPosition = ref<positionType>('bottom');
2915
const ns = useNamespace('form');
3016
const { controlClasses, controlContainerClasses } = useFormControl(props);
31-
let rectInfo: Partial<DOMRect> = {
32-
width: 0,
33-
height: 0,
34-
};
35-
let elOffset = {
36-
left: 0,
37-
top: 0,
38-
};
39-
let popoverLeftPosition = 0;
40-
let popoverTopPosition = 0;
41-
42-
onMounted(() => {
43-
const el = document.getElementById(uid);
44-
elOffset = getElOffset(el);
45-
EventBus.on('showPopoverErrorMessage', (data: ShowPopoverErrorMessageEventData) => {
46-
if (uid === data.uid) {
47-
rectInfo = el.getBoundingClientRect();
48-
showPopover.value = data.showPopover;
49-
tipMessage.value = data.message;
50-
popPosition.value = data.popPosition as any; // todo: 待popover组件positionType完善类型之后再替换类型
51-
popoverLeftPosition =
52-
popPosition.value === 'top' || popPosition.value === 'bottom' ? rectInfo.right - rectInfo.width / 2 : rectInfo.right;
53-
popoverTopPosition =
54-
popPosition.value === 'top' ? elOffset.top + rectInfo.height / 2 - rectInfo.height : elOffset.top + rectInfo.height / 2;
55-
updateOn.value = data.updateOn ?? 'change';
56-
}
57-
});
58-
});
59-
60-
const iconData = computed(() => {
61-
switch (props.feedbackStatus) {
62-
case 'pending':
63-
return { name: 'priority', color: '#e9edfa' };
64-
case 'success':
65-
return { name: 'right-o', color: 'rgb(61, 204, 166)' };
66-
case 'error':
67-
return { name: 'error-o', color: 'rgb(249, 95, 91)' };
68-
default:
69-
return { name: '', color: '' };
70-
}
71-
});
72-
73-
const handleClickOutside = () => {
74-
if (updateOn.value !== 'change') {
75-
showPopover.value = false;
76-
}
77-
};
17+
const { errorMessage } = useFormControlValidate();
7818

7919
return () => (
80-
<div class={controlClasses.value} ref={formControl} data-uid={uid} v-clickoutside={handleClickOutside}>
81-
{showPopover.value && (
82-
<Teleport to="body">
83-
<div
84-
style={{
85-
position: 'absolute',
86-
left: popoverLeftPosition + 'px',
87-
top: popoverTopPosition + 'px',
88-
width: rectInfo.width + 'px',
89-
height: rectInfo.height + 'px',
90-
}}>
91-
<Popover
92-
controlled={updateOn.value !== 'change'}
93-
visible={showPopover.value}
94-
content={tipMessage.value}
95-
popType={'error'}
96-
position={popPosition.value}
97-
/>
98-
</div>
99-
</Teleport>
100-
)}
20+
<div class={controlClasses.value} ref={formControl} data-uid={uid}>
10121
<div class={controlContainerClasses.value}>
10222
<div class={ns.e('control-content')} id={uid}>
10323
{ctx.slots.default?.()}
10424
</div>
105-
{(props.feedbackStatus || ctx.slots.suffixTemplate?.()) && (
106-
<span class={ns.e('control-feedback')}>
107-
{ctx.slots.suffixTemplate?.() ? (
108-
ctx.slots.suffixTemplate?.()
109-
) : (
110-
<Icon name={iconData.value.name} color={iconData.value.color}></Icon>
111-
)}
112-
</span>
113-
)}
11425
</div>
115-
{props.extraInfo && <div class={ns.e('control-extra')}>{props.extraInfo}</div>}
26+
<div class={ns.e('control-info')}>
27+
{errorMessage.value && <div class="error-message">{errorMessage.value}</div>}
28+
{props.extraInfo && <div class={ns.e('control-extra')}>{props.extraInfo}</div>}
29+
</div>
11630
</div>
11731
);
11832
},

packages/devui-vue/devui/form/src/components/form-control/use-form-control.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { computed, reactive, inject, toRefs } from 'vue';
2-
import { FORM_TOKEN, IForm } from '../../form-types';
2+
import { FORM_TOKEN, FormContext } from '../../form-types';
33
import { FormControlProps, UseFormControl } from './form-control-types';
4+
import { FormItemContext, FORM_ITEM_TOKEN } from '../form-item/form-item-types';
45
import { useNamespace } from '../../../../shared/hooks/use-namespace';
56

67
export function useFormControl(props: FormControlProps): UseFormControl {
7-
const Form = reactive(inject(FORM_TOKEN) as IForm);
8-
const labelData = reactive(Form.labelData);
8+
const formContext = inject(FORM_TOKEN) as FormContext;
9+
const labelData = reactive(formContext.labelData);
910
const ns = useNamespace('form');
1011
const { feedbackStatus } = toRefs(props);
1112

@@ -23,3 +24,10 @@ export function useFormControl(props: FormControlProps): UseFormControl {
2324

2425
return { controlClasses, controlContainerClasses };
2526
}
27+
28+
export function useFormControlValidate() {
29+
const formItemContext = inject(FORM_ITEM_TOKEN) as FormItemContext;
30+
const errorMessage = computed(() => formItemContext.validateMessage);
31+
32+
return { errorMessage };
33+
}

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

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { ComputedRef, ExtractPropTypes } from 'vue';
1+
import type { RuleItem, ValidateFieldsError } from 'async-validator';
2+
import type { ComputedRef, ExtractPropTypes, PropType, InjectionKey, Ref } from 'vue';
3+
4+
export type FormItemValidateState = '' | 'error' | 'pending' | 'success';
5+
6+
export interface FormRuleItem extends RuleItem {
7+
trigger?: Array<string>;
8+
}
29

310
export const formItemProps = {
411
field: {
@@ -9,10 +16,34 @@ export const formItemProps = {
916
type: Boolean,
1017
default: false,
1118
},
19+
required: {
20+
type: Boolean,
21+
default: false,
22+
},
23+
rules: {
24+
type: [Object, Array] as PropType<[FormRuleItem, Array<FormRuleItem>]>,
25+
},
1226
};
1327

1428
export type FormItemProps = ExtractPropTypes<typeof formItemProps>;
1529

30+
export type FormValidateCallback = (isValid: boolean, invalidFields?: ValidateFieldsError) => void;
31+
export type FormValidateResult = Promise<boolean>;
32+
33+
export interface FormItemContext extends FormItemProps {
34+
validateState: FormItemValidateState;
35+
validateMessage: string;
36+
validate: (trigger: string, callback?: FormValidateCallback) => FormValidateResult;
37+
}
38+
1639
export interface UseFormItem {
1740
itemClasses: ComputedRef<Record<string, boolean>>;
1841
}
42+
43+
export interface UseFormItemValidate {
44+
validateState: Ref<FormItemValidateState>;
45+
validateMessage: Ref<string>;
46+
validate: (trigger: string, callback?: FormValidateCallback) => FormValidateResult;
47+
}
48+
49+
export const FORM_ITEM_TOKEN: InjectionKey<FormItemContext> = Symbol('dFormItem');
Lines changed: 18 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,37 @@
1-
import { defineComponent, reactive, inject, onMounted, onBeforeUnmount, provide, ref } from 'vue';
1+
import { defineComponent, onMounted, inject, reactive, toRefs, onBeforeUnmount, provide, toRef } from 'vue';
22
import type { SetupContext } from 'vue';
3-
import AsyncValidator, { Rules } from 'async-validator';
4-
import mitt from 'mitt';
5-
import { dFormEvents, dFormItemEvents, IForm, FORM_TOKEN, FORM_ITEM_TOKEN } from '../../form-types';
6-
import { formItemProps, FormItemProps } from './form-item-types';
7-
import { useFormItem } from './use-form-item';
3+
import { FORM_TOKEN } from '../../form-types';
4+
import { FormItemContext, formItemProps, FormItemProps, FORM_ITEM_TOKEN } from './form-item-types';
5+
import { useFormItem, useFormItemRule, useFormItemValidate } from './use-form-item';
86
import './form-item.scss';
97

108
export default defineComponent({
119
name: 'DFormItem',
1210
props: formItemProps,
1311
setup(props: FormItemProps, ctx: SetupContext) {
14-
const formItemMitt = mitt();
15-
const dForm = reactive(inject(FORM_TOKEN, {} as IForm));
16-
const formData = reactive(dForm.formData);
17-
const initFormItemData = formData[props.field];
18-
const rules = reactive(dForm.rules);
12+
const formContext = inject(FORM_TOKEN);
1913
const { itemClasses } = useFormItem();
20-
21-
const resetField = () => {
22-
if (Array.isArray(initFormItemData)) {
23-
formData[props.field] = [...initFormItemData];
24-
} else {
25-
formData[props.field] = initFormItemData;
26-
}
27-
};
28-
29-
const formItem = reactive({
30-
dHasFeedback: props.dHasFeedback,
31-
field: props.field,
32-
formItemMitt,
33-
resetField,
14+
const { _rules } = useFormItemRule(props);
15+
const { validateState, validateMessage, validate } = useFormItemValidate(props, _rules);
16+
const context: FormItemContext = reactive({
17+
...toRefs(props),
18+
validateState,
19+
validateMessage,
20+
validate,
3421
});
35-
provide(FORM_ITEM_TOKEN, formItem);
36-
37-
const showMessage = ref(false);
38-
const tipMessage = ref('');
39-
40-
const validate = (trigger: string) => {
41-
const ruleKey = props.field;
42-
const ruleItem = rules[ruleKey];
43-
const descriptor: Rules = {};
44-
descriptor[ruleKey] = ruleItem;
45-
46-
const validator = new AsyncValidator(descriptor);
47-
48-
validator
49-
.validate({ [ruleKey]: formData[ruleKey] })
50-
.then(() => {
51-
showMessage.value = false;
52-
tipMessage.value = '';
53-
})
54-
.catch(({ errors }) => {
55-
showMessage.value = true;
56-
tipMessage.value = errors[0].message;
57-
});
58-
};
59-
const validateEvents = [];
6022

61-
const addValidateEvents = () => {
62-
if (rules && rules[props.field]) {
63-
const ruleItem = rules[props.field];
64-
let eventName = ruleItem['trigger'];
65-
66-
if (Array.isArray(ruleItem)) {
67-
ruleItem.forEach((item) => {
68-
eventName = item['trigger'];
69-
const cb = () => validate(eventName);
70-
validateEvents.push({ eventName: cb });
71-
formItem.formItemMitt.on(dFormItemEvents[eventName], cb);
72-
});
73-
} else {
74-
const cb = () => validate(eventName);
75-
validateEvents.push({ eventName: cb });
76-
ruleItem && formItem.formItemMitt.on(dFormItemEvents[eventName], cb);
77-
}
78-
}
79-
};
80-
81-
const removeValidateEvents = () => {
82-
if (rules && rules[props.field] && validateEvents.length > 0) {
83-
validateEvents.forEach((item) => {
84-
formItem.formItemMitt.off(item.eventName, item.cb);
85-
});
86-
}
87-
};
23+
provide(FORM_ITEM_TOKEN, context);
8824

8925
onMounted(() => {
90-
dForm.formMitt.emit(dFormEvents.addField, formItem);
91-
addValidateEvents();
26+
if (props.field) {
27+
formContext?.addItemContext(context);
28+
}
9229
});
9330

9431
onBeforeUnmount(() => {
95-
dForm.formMitt.emit(dFormEvents.removeField, formItem);
96-
removeValidateEvents();
32+
formContext?.removeItemContext(context);
9733
});
34+
9835
return () => <div class={itemClasses.value}>{ctx.slots.default?.()}</div>;
9936
},
10037
});

0 commit comments

Comments
 (0)