Skip to content

Commit

Permalink
feat(form): blur trigger support (#1051)
Browse files Browse the repository at this point in the history
* fix(form): number validate

* feat(form): blur trigger

* fix(form): error message template

Co-authored-by: yuyang <uyarnchen@gmail.com>
  • Loading branch information
k1nz and uyarn committed Jul 1, 2022
1 parent f2de4d4 commit e3f5508
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 30 deletions.
6 changes: 5 additions & 1 deletion examples/form/demos/error-message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
@submit="onSubmit"
scrollToFirstError="smooth"
>
<t-form-item label="用户名" help="这是用户名字段帮助说明" name="account">
<!-- !!!注意:当 FormItem 的 label 属性为 Function 时,errorMessage 模板中的 ${name} 会被替换为 FormItem.name 属性值 -->
<t-form-item :label="renderAccountLabel" help="这是用户名字段帮助说明" name="account">
<t-input v-model="formData.account"></t-input>
</t-form-item>
<t-form-item label="个人简介" help="一句话介绍自己" name="description">
Expand Down Expand Up @@ -149,6 +150,9 @@ export default {
},
methods: {
renderAccountLabel() {
return '用户名';
},
onReset() {
this.$message.success('重置成功');
},
Expand Down
21 changes: 19 additions & 2 deletions examples/form/demos/validator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,27 @@ export default {
// FormItem.rules 优先级大于 Form.rules
rules: {
account: [
{
required: true,
message: '姓名必填',
type: 'error',
trigger: 'blur',
},
// trigger 默认为 'change'
{ required: true, message: '姓名必填', type: 'error' },
{ whitespace: true, message: '姓名不能为空' },
{ min: 2, message: '至少需要两个字符,一个中文等于两个字符', type: 'warning' },
{ max: 10, message: '姓名字符长度超出', type: 'warning' },
{
min: 2,
message: '至少需要两个字符,一个中文等于两个字符',
type: 'warning',
trigger: 'blur',
},
{
max: 10,
message: '姓名字符长度超出',
type: 'warning',
trigger: 'blur',
},
],
description: [
{
Expand Down
24 changes: 24 additions & 0 deletions src/form/const.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { prefix } from '../config';
import { AllValidateResult, FormRule, ValidateResultType } from '../form/type';

export const FORM_ITEM_CLASS_PREFIX = `${prefix}-form-item__`;

Expand Down Expand Up @@ -45,3 +46,26 @@ export const FORM_CONTROL_COMPONENTS = [
'TTransfer',
'TSlider',
];

export type ErrorListType =
| {
result: false;
message: string;
type: 'error' | 'warning';
}
| ValidateResultType;
export type SuccessListType =
| {
result: true;
message: string;
type: 'success';
}
| ValidateResultType;

export interface AnalysisValidateResult {
successList?: SuccessListType[];
errorList?: ErrorListType[];
rules: FormRule[];
resultList: AllValidateResult[];
allowSetValue: boolean;
}
53 changes: 36 additions & 17 deletions src/form/form-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
FormItemValidateMessage,
} from './type';
import props from './form-item-props';
import { CLASS_NAMES, FORM_ITEM_CLASS_PREFIX } from './const';
import {
AnalysisValidateResult, CLASS_NAMES, ErrorListType, FORM_ITEM_CLASS_PREFIX, SuccessListType,
} from './const';
import Form from './form';
import { ClassName, TNodeReturnValue, Styles } from '../common';
import mixins from '../utils/mixins';
Expand Down Expand Up @@ -55,6 +57,12 @@ export default mixins(getConfigReceiverMixins<FormItemConstructor, FormConfig>('
form: { default: undefined },
},

provide(): { tFormItem: any } {
return {
tFormItem: this,
};
},

data() {
return {
// 校验不通过信息列表
Expand Down Expand Up @@ -243,11 +251,13 @@ export default mixins(getConfigReceiverMixins<FormItemConstructor, FormConfig>('
this.freeShowErrorMessage = source === 'submit-function' ? showErrorMessage : undefined;
this.resetValidating = true;
const {
errorList, resultList, successList, rules,
errorList, resultList, successList, rules, allowSetValue,
} = await this.analysisValidateResult(trigger);
this.errorList = errorList;
// 仅有自定义校验方法才会存在 successList
this.successList = successList;
if (allowSetValue) {
this.errorList = errorList;
// 仅有自定义校验方法才会存在 successList
this.successList = successList;
}
// 根据校验结果设置校验状态
if (rules.length) {
this.verifyStatus = errorList.length ? VALIDATE_STATUS.FAIL : VALIDATE_STATUS.SUCCESS;
Expand All @@ -272,33 +282,42 @@ export default mixins(getConfigReceiverMixins<FormItemConstructor, FormConfig>('
},

async analysisValidateResult(trigger: ValidateTriggerType) {
const result: AnalysisValidateResult = {
successList: [],
errorList: [],
rules: [],
resultList: [],
allowSetValue: false,
};
// 过滤不需要校验的规则
const rules = trigger === 'all' ? this.innerRules : this.innerRules.filter((item) => (item.trigger || 'change') === trigger);
result.rules = trigger === 'all' ? this.innerRules : this.innerRules.filter((item) => (item.trigger || 'change') === trigger);
if (this.innerRules.length && !result.rules.length) {
return result;
}
// 校验结果,包含正确的校验信息
const resultList = await validate(this.value, rules);
const errorList = resultList
result.allowSetValue = true;
result.resultList = await validate(this.value, result.rules);
result.errorList = result.resultList
.filter((item) => item.result !== true)
.map((item) => {
.map((item: ErrorListType) => {
Object.keys(item).forEach((key) => {
if (typeof item.message === 'undefined' && this.errorMessages[key]) {
const compiled = lodashTemplate(this.errorMessages[key]);
const name = typeof this.label === 'string' ? this.label : this.name;
// eslint-disable-next-line no-param-reassign
item.message = compiled({
name: this.label,
name,
validate: item[key],
});
}
});
return item;
});
// 仅有自定义校验方法才会存在 successList
const successList = resultList.filter((item) => item.result === true && item.message && item.type === 'success');
return {
resultList,
errorList,
successList,
rules,
};
result.successList = result.resultList.filter(
(item) => item.result === true && item.message && item.type === 'success',
) as SuccessListType[];
return result;
},

getLabelContent(): TNodeReturnValue {
Expand Down
23 changes: 15 additions & 8 deletions src/form/form-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import isURL from 'validator/lib/isURL';
import isNumber from 'lodash/isNumber';
import { getCharacterLength } from '../utils/helper';
import {
CustomValidator, FormRule, ValueType, AllValidateResult,
CustomValidator,
FormRule,
ValueType,
AllValidateResult,
CustomValidateResolveType,
ValidateResultType,
} from './type';

// `{} / [] / '' / undefined / null` 等内容被认为是空; 0 和 false 被认为是正常数据,部分数据的值就是 0 或者 false
Expand Down Expand Up @@ -40,7 +45,7 @@ const VALIDATE_MAP = {
max: (val: ValueType, num: number): boolean => compareValue(val, num, true),
min: (val: ValueType, num: number): boolean => compareValue(val, num, false),
len: (val: ValueType, num: number): boolean => getCharacterLength(val) === num,
number: (val: ValueType): boolean => !Number.isNaN(val),
number: (val: ValueType): boolean => isNumber(val),
enum: (val: ValueType, strs: Array<string>): boolean => strs.includes(val),
idcard: (val: ValueType): boolean => /^(\d{18,18}|\d{15,15}|\d{17,17}x)$/i.test(val),
telnumber: (val: ValueType): boolean => /^1[3-9]\d{9}$/.test(val),
Expand All @@ -49,28 +54,30 @@ const VALIDATE_MAP = {
validator: (val: ValueType, validate: CustomValidator): ReturnType<CustomValidator> => validate(val),
};

export type ValidateFuncType = typeof VALIDATE_MAP[keyof typeof VALIDATE_MAP];

/**
* 校验某一条数据的某一条规则,一种校验规则不满足则不再进行校验。
* @param value 值
* @param rule 校验规则
* @returns 两种校验结果,一种是内置校验规则的校验结果哦,二种是自定义校验规则(validator)的校验结果
*/
export async function validateOneRule(value: ValueType, rule: FormRule): Promise<AllValidateResult> {
let validateResult: AllValidateResult = { result: true };
let validateResult: CustomValidateResolveType | ValidateResultType = { result: true };
const keys = Object.keys(rule);
let vOptions = {};
let vValidateFun;
let vOptions;
let vValidateFun: ValidateFuncType;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
// 非必填选项,值为空,非自定义规则:无需校验,直接返回 true
if (!rule.required && isValueEmpty(value) && !rule.validator) {
return validateResult;
}
const validateRule = VALIDATE_MAP[key];
const validateRule: ValidateFuncType = VALIDATE_MAP[key];
// 找到一个校验规则,则无需再找,因为参数只允许对一个规则进行校验
if (validateRule && rule[key]) {
// rule 值为 true 则表示没有校验参数,只是对值进行默认规则校验
vOptions = rule[key] === true ? {} : rule[key];
vOptions = rule[key] === true ? undefined : rule[key];
vValidateFun = validateRule;
break;
}
Expand All @@ -81,7 +88,7 @@ export async function validateOneRule(value: ValueType, rule: FormRule): Promise
if (typeof validateResult === 'boolean') {
return { ...rule, result: validateResult };
}
// 校验结果为 CustomValidateResolveType,只有自定义校验规则会存在这种情况
// 校验结果为 CustomValidateObj,只有自定义校验规则会存在这种情况
if (typeof validateResult === 'object') {
return validateResult;
}
Expand Down
3 changes: 3 additions & 0 deletions src/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { emitEvent } from '../utils/event';
import { prefix } from '../config';
import props from './props';
import { renderTNodeJSX } from '../utils/render-tnode';
import FormItem from '../form/form-item';

const name = `${prefix}-input`;
const INPUT_WRAP_CLASS = `${prefix}-input__wrap`;
Expand All @@ -29,6 +30,7 @@ function getValidAttrs(obj: object): object {

interface InputInstance extends Vue {
composing: boolean;
tFormItem: InstanceType<typeof FormItem>;
}

export default mixins(getConfigReceiverMixins<InputInstance, InputConfig>('input')).extend({
Expand Down Expand Up @@ -233,6 +235,7 @@ export default mixins(getConfigReceiverMixins<InputInstance, InputConfig>('input
this.inputValue = this.format(this.value);
}
this.focused = false;
this.tFormItem?.validate('blur');
emitEvent<Parameters<TdInputProps['onBlur']>>(this, 'blur', this.value, { e });
},
compositionstartHandler(e: CompositionEvent) {
Expand Down
14 changes: 12 additions & 2 deletions src/textarea/textarea.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Vue from 'vue';
import Vue, { VueConstructor } from 'vue';
import isFunction from 'lodash/isFunction';
import { prefix } from '../config';
import CLASSNAMES from '../utils/classnames';
Expand All @@ -22,11 +22,20 @@ function getValidAttrs(obj: object): object {
});
return newObj;
}
export default Vue.extend({
export interface Textarea extends Vue {
tFormItem: {
validate(trigger: string): Promise<any>
};
}

export default (Vue as VueConstructor<Textarea>).extend({
name: 'TTextarea',
props: {
...props,
},
inject: {
tFormItem: { default: undefined },
},
data() {
return {
formDisabled: undefined,
Expand Down Expand Up @@ -151,6 +160,7 @@ export default Vue.extend({
},
emitBlur(e: FocusEvent) {
this.focused = false;
this.tFormItem?.validate('blur');
this.emitEvent('blur', this.value, { e });
},
},
Expand Down

0 comments on commit e3f5508

Please sign in to comment.