types(Field): use tsx (#8151)

This commit is contained in:
neverland 2021-02-14 11:31:43 +08:00 committed by GitHub
parent fa8b494f0e
commit 393b2a256f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 84 deletions

View File

@ -298,7 +298,7 @@ export default {
| focus | 输入框获得焦点时触发 | _event: Event_ | | focus | 输入框获得焦点时触发 | _event: Event_ |
| blur | 输入框失去焦点时触发 | _event: Event_ | | blur | 输入框失去焦点时触发 | _event: Event_ |
| clear | 点击清除按钮时触发 | _event: MouseEvent_ | | clear | 点击清除按钮时触发 | _event: MouseEvent_ |
| click | 点击 Field 时触发 | _event: MouseEvent_ | | click | 点击组件时触发 | _event: MouseEvent_ |
| click-input | 点击输入区域时触发 | _event: MouseEvent_ | | click-input | 点击输入区域时触发 | _event: MouseEvent_ |
| click-left-icon | 点击左侧图标时触发 | _event: MouseEvent_ | | click-left-icon | 点击左侧图标时触发 | _event: MouseEvent_ |
| click-right-icon | 点击右侧图标时触发 | _event: MouseEvent_ | | click-right-icon | 点击右侧图标时触发 | _event: MouseEvent_ |
@ -316,7 +316,7 @@ export default {
| 名称 | 说明 | | 名称 | 说明 |
| ---------- | ---------------------------------------------------------- | | ---------- | ---------------------------------------------------------- |
| label | 自定义输入框 label 标签 | | label | 自定义输入框左侧文本 |
| input | 自定义输入框,使用此插槽后,与输入框相关的属性和事件将失效 | | input | 自定义输入框,使用此插槽后,与输入框相关的属性和事件将失效 |
| left-icon | 自定义输入框头部图标 | | left-icon | 自定义输入框头部图标 |
| right-icon | 自定义输入框尾部图标 | | right-icon | 自定义输入框尾部图标 |

View File

@ -5,7 +5,9 @@ import {
computed, computed,
nextTick, nextTick,
reactive, reactive,
PropType,
onMounted, onMounted,
HTMLAttributes,
} from 'vue'; } from 'vue';
// Utils // Utils
@ -32,6 +34,17 @@ import { FORM_KEY, FIELD_KEY } from '../composables/use-link-field';
import Icon from '../icon'; import Icon from '../icon';
import Cell, { cellProps } from '../cell'; import Cell, { cellProps } from '../cell';
// Types
import type {
FieldRule,
FieldType,
FieldTextAlign,
FieldClearTrigger,
FieldFormatTrigger,
FieldAutosizeConfig,
FieldValidateTrigger,
} from './types';
const [createComponent, bem] = createNamespace('field'); const [createComponent, bem] = createNamespace('field');
export default createComponent({ export default createComponent({
@ -39,24 +52,24 @@ export default createComponent({
...cellProps, ...cellProps,
rows: [Number, String], rows: [Number, String],
name: String, name: String,
rules: Array, rules: Array as PropType<FieldRule[]>,
autosize: [Boolean, Object], autosize: [Boolean, Object] as PropType<boolean | FieldAutosizeConfig>,
leftIcon: String, leftIcon: String,
rightIcon: String, rightIcon: String,
clearable: Boolean, clearable: Boolean,
formatter: Function, formatter: Function as PropType<(value: string) => string>,
maxlength: [Number, String], maxlength: [Number, String],
labelWidth: [Number, String], labelWidth: [Number, String],
labelClass: null, labelClass: null as any,
labelAlign: String, labelAlign: String as PropType<FieldTextAlign>,
inputAlign: String, inputAlign: String as PropType<FieldTextAlign>,
placeholder: String, placeholder: String,
autocomplete: String, autocomplete: String,
errorMessage: String, errorMessage: String,
errorMessageAlign: String, errorMessageAlign: String as PropType<FieldTextAlign>,
showWordLimit: Boolean, showWordLimit: Boolean,
type: { type: {
type: String, type: String as PropType<FieldType>,
default: 'text', default: 'text',
}, },
error: { error: {
@ -80,11 +93,11 @@ export default createComponent({
default: '', default: '',
}, },
clearTrigger: { clearTrigger: {
type: String, type: String as PropType<FieldClearTrigger>,
default: 'focus', default: 'focus',
}, },
formatTrigger: { formatTrigger: {
type: String, type: String as PropType<FieldFormatTrigger>,
default: 'onChange', default: 'onChange',
}, },
}, },
@ -107,12 +120,14 @@ export default createComponent({
validateMessage: '', validateMessage: '',
}); });
const inputRef = ref(); const inputRef = ref<HTMLInputElement>();
const childFieldValue = ref(); const childFieldValue = ref<() => unknown>();
const { parent: form } = useParent(FORM_KEY); const { parent: form } = useParent<any>(FORM_KEY);
const getProp = (key) => { const getModelValue = () => String(props.modelValue ?? '');
const getProp = (key: keyof typeof props) => {
if (isDef(props[key])) { if (isDef(props[key])) {
return props[key]; return props[key];
} }
@ -125,7 +140,7 @@ export default createComponent({
const readonly = getProp('readonly'); const readonly = getProp('readonly');
if (props.clearable && !readonly) { if (props.clearable && !readonly) {
const hasValue = isDef(props.modelValue) && props.modelValue !== ''; const hasValue = getModelValue() !== '';
const trigger = const trigger =
props.clearTrigger === 'always' || props.clearTrigger === 'always' ||
(props.clearTrigger === 'focus' && state.focused); (props.clearTrigger === 'focus' && state.focused);
@ -141,9 +156,9 @@ export default createComponent({
return props.modelValue; return props.modelValue;
}); });
const runValidator = (value, rule) => const runValidator = (value: unknown, rule: FieldRule) =>
new Promise((resolve) => { new Promise((resolve) => {
const returnVal = rule.validator(value, rule); const returnVal = rule.validator!(value, rule);
if (isPromise(returnVal)) { if (isPromise(returnVal)) {
return returnVal.then(resolve); return returnVal.then(resolve);
@ -152,16 +167,16 @@ export default createComponent({
resolve(returnVal); resolve(returnVal);
}); });
const getRuleMessage = (value, rule) => { const getRuleMessage = (value: unknown, rule: FieldRule) => {
const { message } = rule; const { message } = rule;
if (isFunction(message)) { if (isFunction(message)) {
return message(value, rule); return message(value, rule);
} }
return message; return message || '';
}; };
const runRules = (rules) => const runRules = (rules: FieldRule[]) =>
rules.reduce( rules.reduce(
(promise, rule) => (promise, rule) =>
promise.then(() => { promise.then(() => {
@ -204,12 +219,9 @@ export default createComponent({
}; };
const validate = (rules = props.rules) => const validate = (rules = props.rules) =>
new Promise((resolve) => { new Promise<{ name?: string; message: string } | void>((resolve) => {
if (!rules) {
resolve();
}
resetValidation(); resetValidation();
if (rules) {
runRules(rules).then(() => { runRules(rules).then(() => {
if (state.validateFailed) { if (state.validateFailed) {
resolve({ resolve({
@ -220,9 +232,12 @@ export default createComponent({
resolve(); resolve();
} }
}); });
} else {
resolve();
}
}); });
const validateWithTrigger = (trigger) => { const validateWithTrigger = (trigger: FieldValidateTrigger) => {
if (form && props.rules) { if (form && props.rules) {
const defaultTrigger = form.props.validateTrigger === trigger; const defaultTrigger = form.props.validateTrigger === trigger;
const rules = props.rules.filter((rule) => { const rules = props.rules.filter((rule) => {
@ -237,19 +252,25 @@ export default createComponent({
} }
}; };
const updateValue = (value, trigger = 'onChange') => {
value = isDef(value) ? String(value) : '';
// native maxlength have incorrect line-break counting // native maxlength have incorrect line-break counting
// see: https://github.com/youzan/vant/issues/5033 // see: https://github.com/youzan/vant/issues/5033
const { maxlength, modelValue } = props; const limitValueLength = (value: string) => {
const { maxlength } = props;
if (isDef(maxlength) && value.length > maxlength) { if (isDef(maxlength) && value.length > maxlength) {
const modelValue = getModelValue();
if (modelValue && modelValue.length === +maxlength) { if (modelValue && modelValue.length === +maxlength) {
value = modelValue; return modelValue;
} else {
value = value.slice(0, maxlength);
} }
return value.slice(0, +maxlength);
} }
return value;
};
const updateValue = (
value: string,
trigger: FieldFormatTrigger = 'onChange'
) => {
value = limitValueLength(value);
if (props.type === 'number' || props.type === 'digit') { if (props.type === 'number' || props.type === 'digit') {
const isNumber = props.type === 'number'; const isNumber = props.type === 'number';
@ -269,10 +290,10 @@ export default createComponent({
} }
}; };
const onInput = (event) => { const onInput = (event: Event) => {
// skip update value when composing // skip update value when composing
if (!event.target.composing) { if (!event.target!.composing) {
updateValue(event.target.value); updateValue((event.target as HTMLInputElement).value);
} }
}; };
@ -288,7 +309,7 @@ export default createComponent({
} }
}; };
const onFocus = (event) => { const onFocus = (event: Event) => {
state.focused = true; state.focused = true;
emit('focus', event); emit('focus', event);
@ -299,27 +320,27 @@ export default createComponent({
} }
}; };
const onBlur = (event) => { const onBlur = (event: Event) => {
state.focused = false; state.focused = false;
updateValue(props.modelValue, 'onBlur'); updateValue(getModelValue(), 'onBlur');
emit('blur', event); emit('blur', event);
validateWithTrigger('onBlur'); validateWithTrigger('onBlur');
resetScroll(); resetScroll();
}; };
const onClickInput = (event) => { const onClickInput = (event: MouseEvent) => {
emit('click-input', event); emit('click-input', event);
}; };
const onClickLeftIcon = (event) => { const onClickLeftIcon = (event: MouseEvent) => {
emit('click-left-icon', event); emit('click-left-icon', event);
}; };
const onClickRightIcon = (event) => { const onClickRightIcon = (event: MouseEvent) => {
emit('click-right-icon', event); emit('click-right-icon', event);
}; };
const onClear = (event) => { const onClear = (event: MouseEvent) => {
preventDefault(event); preventDefault(event);
emit('update:modelValue', ''); emit('update:modelValue', '');
emit('clear', event); emit('clear', event);
@ -341,11 +362,11 @@ export default createComponent({
} }
}); });
const onKeypress = (event) => { const onKeypress = (event: KeyboardEvent) => {
const ENTER_CODE = 13; const ENTER_CODE = 13;
if (event.keyCode === ENTER_CODE) { if (event.keyCode === ENTER_CODE) {
const submitOnEnter = getProp('submitOnEnter'); const submitOnEnter = form && form.props.submitOnEnter;
if (!submitOnEnter && props.type !== 'textarea') { if (!submitOnEnter && props.type !== 'textarea') {
preventDefault(event); preventDefault(event);
} }
@ -359,15 +380,15 @@ export default createComponent({
emit('keypress', event); emit('keypress', event);
}; };
const onCompositionStart = (event) => { const onCompositionStart = (event: Event) => {
event.target.composing = true; event.target!.composing = true;
}; };
const onCompositionEnd = (event) => { const onCompositionEnd = (event: Event) => {
const { target } = event; const { target } = event;
if (target.composing) { if (target!.composing) {
target.composing = false; target!.composing = false;
trigger(target, 'input'); trigger(target as Element, 'input');
} }
}; };
@ -383,16 +404,16 @@ export default createComponent({
let height = input.scrollHeight; let height = input.scrollHeight;
if (isObject(props.autosize)) { if (isObject(props.autosize)) {
const { maxHeight, minHeight } = props.autosize; const { maxHeight, minHeight } = props.autosize;
if (maxHeight) { if (maxHeight !== undefined) {
height = Math.min(height, maxHeight); height = Math.min(height, maxHeight);
} }
if (minHeight) { if (minHeight !== undefined) {
height = Math.max(height, minHeight); height = Math.max(height, minHeight);
} }
} }
if (height) { if (height) {
input.style.height = height + 'px'; input.style.height = `${height}px`;
} }
}; };
@ -415,7 +436,7 @@ export default createComponent({
const inputProps = { const inputProps = {
ref: inputRef, ref: inputRef,
name: props.name, name: props.name,
rows: props.rows, rows: props.rows !== undefined ? +props.rows : undefined,
class: bem('control', inputAlign), class: bem('control', inputAlign),
value: props.modelValue, value: props.modelValue,
disabled, disabled,
@ -439,7 +460,7 @@ export default createComponent({
} }
let inputType = type; let inputType = type;
let inputMode; let inputMode: HTMLAttributes['inputmode'];
// type="number" is weired in iOS, and can't prevent dot in Android // type="number" is weired in iOS, and can't prevent dot in Android
// so use inputmode to set keyboard in mordern browers // so use inputmode to set keyboard in mordern browers
@ -490,7 +511,7 @@ export default createComponent({
const renderWordLimit = () => { const renderWordLimit = () => {
if (props.showWordLimit && props.maxlength) { if (props.showWordLimit && props.maxlength) {
const count = (props.modelValue || '').length; const count = getModelValue().length;
return ( return (
<div class={bem('word-limit')}> <div class={bem('word-limit')}>
<span class={bem('word-num')}>{count}</span>/{props.maxlength} <span class={bem('word-num')}>{count}</span>/{props.maxlength}
@ -541,8 +562,8 @@ export default createComponent({
watch( watch(
() => props.modelValue, () => props.modelValue,
(value) => { () => {
updateValue(value); updateValue(getModelValue());
resetValidation(); resetValidation();
validateWithTrigger('onChange'); validateWithTrigger('onChange');
nextTick(adjustSize); nextTick(adjustSize);
@ -550,7 +571,7 @@ export default createComponent({
); );
onMounted(() => { onMounted(() => {
updateValue(props.modelValue, props.formatTrigger); updateValue(getModelValue(), props.formatTrigger);
nextTick(adjustSize); nextTick(adjustSize);
}); });

39
src/field/types.ts Normal file
View File

@ -0,0 +1,39 @@
export type FieldType =
| 'tel'
| 'text'
| 'digit'
| 'number'
| 'search'
| 'password'
| 'textarea';
export type FieldTextAlign = 'left' | 'center' | 'right';
export type FieldClearTrigger = 'always' | 'focus';
export type FieldFormatTrigger = 'onBlur' | 'onChange';
export type FieldValidateTrigger = 'onBlur' | 'onChange' | 'onSubmit';
export type FieldAutosizeConfig = {
maxHeight?: number;
minHeight?: number;
};
export type FieldRule = {
pattern?: RegExp;
trigger?: FieldValidateTrigger;
message?: string | ((value: any, rule: FieldRule) => string);
required?: boolean;
validator?: (
value: any,
rule: FieldRule
) => boolean | string | Promise<boolean | string>;
formatter?: (value: any, rule: FieldRule) => string;
};
declare global {
interface EventTarget {
composing?: boolean;
}
}

View File

@ -1,13 +1,4 @@
export type FieldValidateTrigger = 'onSubmit' | 'onChange' | 'onBlur'; import type { FieldRule } from './types';
export type FieldRule = {
pattern?: RegExp;
trigger?: FieldValidateTrigger;
message?: string | ((value: unknown, rule: FieldRule) => string);
required?: boolean;
validator?: (value: unknown, rule: FieldRule) => boolean | Promise<boolean>;
formatter?: (value: unknown, rule: FieldRule) => unknown;
};
function isEmptyValue(value: unknown) { function isEmptyValue(value: unknown) {
if (Array.isArray(value)) { if (Array.isArray(value)) {

View File

@ -21,5 +21,6 @@ declare module 'vue' {
onToggle?: EventHandler; onToggle?: EventHandler;
onConfirm?: EventHandler; onConfirm?: EventHandler;
onClickStep?: EventHandler; onClickStep?: EventHandler;
onTouchstart?: EventHandler;
} }
} }