diff --git a/src/field/README.zh-CN.md b/src/field/README.zh-CN.md index c68440b29..9c3fc36c5 100644 --- a/src/field/README.zh-CN.md +++ b/src/field/README.zh-CN.md @@ -279,7 +279,7 @@ export default { | format-trigger | 格式化函数触发的时机,可选值为 `onBlur` | _string_ | `onChange` | | arrow-direction | 箭头方向,可选值为 `left` `up` `down` | _string_ | `right` | | label-class | 左侧文本额外类名 | _string \| Array \| object_ | - | -| label-width | 左侧文本宽度,默认单位为`px` | _number \| string_ | `6.2em` | +| label-width | 左侧文本宽度,默认单位为 `px` | _number \| string_ | `6.2em` | | label-align | 左侧文本对齐方式,可选值为 `center` `right` | _string_ | `left` | | input-align | 输入框对齐方式,可选值为 `center` `right` | _string_ | `left` | | error-message-align | 错误提示文案对齐方式,可选值为 `center` `right` | _string_ | `left` | @@ -298,7 +298,7 @@ export default { | focus | 输入框获得焦点时触发 | _event: Event_ | | blur | 输入框失去焦点时触发 | _event: Event_ | | clear | 点击清除按钮时触发 | _event: MouseEvent_ | -| click | 点击 Field 时触发 | _event: MouseEvent_ | +| click | 点击组件时触发 | _event: MouseEvent_ | | click-input | 点击输入区域时触发 | _event: MouseEvent_ | | click-left-icon | 点击左侧图标时触发 | _event: MouseEvent_ | | click-right-icon | 点击右侧图标时触发 | _event: MouseEvent_ | @@ -316,7 +316,7 @@ export default { | 名称 | 说明 | | ---------- | ---------------------------------------------------------- | -| label | 自定义输入框 label 标签 | +| label | 自定义输入框左侧文本 | | input | 自定义输入框,使用此插槽后,与输入框相关的属性和事件将失效 | | left-icon | 自定义输入框头部图标 | | right-icon | 自定义输入框尾部图标 | diff --git a/src/field/index.js b/src/field/index.tsx similarity index 77% rename from src/field/index.js rename to src/field/index.tsx index 0b27b22c0..44333c26a 100644 --- a/src/field/index.js +++ b/src/field/index.tsx @@ -5,7 +5,9 @@ import { computed, nextTick, reactive, + PropType, onMounted, + HTMLAttributes, } from 'vue'; // Utils @@ -32,6 +34,17 @@ import { FORM_KEY, FIELD_KEY } from '../composables/use-link-field'; import Icon from '../icon'; import Cell, { cellProps } from '../cell'; +// Types +import type { + FieldRule, + FieldType, + FieldTextAlign, + FieldClearTrigger, + FieldFormatTrigger, + FieldAutosizeConfig, + FieldValidateTrigger, +} from './types'; + const [createComponent, bem] = createNamespace('field'); export default createComponent({ @@ -39,24 +52,24 @@ export default createComponent({ ...cellProps, rows: [Number, String], name: String, - rules: Array, - autosize: [Boolean, Object], + rules: Array as PropType, + autosize: [Boolean, Object] as PropType, leftIcon: String, rightIcon: String, clearable: Boolean, - formatter: Function, + formatter: Function as PropType<(value: string) => string>, maxlength: [Number, String], labelWidth: [Number, String], - labelClass: null, - labelAlign: String, - inputAlign: String, + labelClass: null as any, + labelAlign: String as PropType, + inputAlign: String as PropType, placeholder: String, autocomplete: String, errorMessage: String, - errorMessageAlign: String, + errorMessageAlign: String as PropType, showWordLimit: Boolean, type: { - type: String, + type: String as PropType, default: 'text', }, error: { @@ -80,11 +93,11 @@ export default createComponent({ default: '', }, clearTrigger: { - type: String, + type: String as PropType, default: 'focus', }, formatTrigger: { - type: String, + type: String as PropType, default: 'onChange', }, }, @@ -107,12 +120,14 @@ export default createComponent({ validateMessage: '', }); - const inputRef = ref(); - const childFieldValue = ref(); + const inputRef = ref(); + const childFieldValue = ref<() => unknown>(); - const { parent: form } = useParent(FORM_KEY); + const { parent: form } = useParent(FORM_KEY); - const getProp = (key) => { + const getModelValue = () => String(props.modelValue ?? ''); + + const getProp = (key: keyof typeof props) => { if (isDef(props[key])) { return props[key]; } @@ -125,7 +140,7 @@ export default createComponent({ const readonly = getProp('readonly'); if (props.clearable && !readonly) { - const hasValue = isDef(props.modelValue) && props.modelValue !== ''; + const hasValue = getModelValue() !== ''; const trigger = props.clearTrigger === 'always' || (props.clearTrigger === 'focus' && state.focused); @@ -141,9 +156,9 @@ export default createComponent({ return props.modelValue; }); - const runValidator = (value, rule) => + const runValidator = (value: unknown, rule: FieldRule) => new Promise((resolve) => { - const returnVal = rule.validator(value, rule); + const returnVal = rule.validator!(value, rule); if (isPromise(returnVal)) { return returnVal.then(resolve); @@ -152,16 +167,16 @@ export default createComponent({ resolve(returnVal); }); - const getRuleMessage = (value, rule) => { + const getRuleMessage = (value: unknown, rule: FieldRule) => { const { message } = rule; if (isFunction(message)) { return message(value, rule); } - return message; + return message || ''; }; - const runRules = (rules) => + const runRules = (rules: FieldRule[]) => rules.reduce( (promise, rule) => promise.then(() => { @@ -204,25 +219,25 @@ export default createComponent({ }; const validate = (rules = props.rules) => - new Promise((resolve) => { - if (!rules) { + new Promise<{ name?: string; message: string } | void>((resolve) => { + resetValidation(); + if (rules) { + runRules(rules).then(() => { + if (state.validateFailed) { + resolve({ + name: props.name, + message: state.validateMessage, + }); + } else { + resolve(); + } + }); + } else { resolve(); } - - resetValidation(); - runRules(rules).then(() => { - if (state.validateFailed) { - resolve({ - name: props.name, - message: state.validateMessage, - }); - } else { - resolve(); - } - }); }); - const validateWithTrigger = (trigger) => { + const validateWithTrigger = (trigger: FieldValidateTrigger) => { if (form && props.rules) { const defaultTrigger = form.props.validateTrigger === trigger; 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 - // see: https://github.com/youzan/vant/issues/5033 - const { maxlength, modelValue } = props; + // native maxlength have incorrect line-break counting + // see: https://github.com/youzan/vant/issues/5033 + const limitValueLength = (value: string) => { + const { maxlength } = props; if (isDef(maxlength) && value.length > maxlength) { + const modelValue = getModelValue(); if (modelValue && modelValue.length === +maxlength) { - value = modelValue; - } else { - value = value.slice(0, maxlength); + return modelValue; } + return value.slice(0, +maxlength); } + return value; + }; + + const updateValue = ( + value: string, + trigger: FieldFormatTrigger = 'onChange' + ) => { + value = limitValueLength(value); if (props.type === 'number' || props.type === 'digit') { const isNumber = props.type === 'number'; @@ -269,10 +290,10 @@ export default createComponent({ } }; - const onInput = (event) => { + const onInput = (event: Event) => { // skip update value when composing - if (!event.target.composing) { - updateValue(event.target.value); + if (!event.target!.composing) { + updateValue((event.target as HTMLInputElement).value); } }; @@ -288,7 +309,7 @@ export default createComponent({ } }; - const onFocus = (event) => { + const onFocus = (event: Event) => { state.focused = true; emit('focus', event); @@ -299,27 +320,27 @@ export default createComponent({ } }; - const onBlur = (event) => { + const onBlur = (event: Event) => { state.focused = false; - updateValue(props.modelValue, 'onBlur'); + updateValue(getModelValue(), 'onBlur'); emit('blur', event); validateWithTrigger('onBlur'); resetScroll(); }; - const onClickInput = (event) => { + const onClickInput = (event: MouseEvent) => { emit('click-input', event); }; - const onClickLeftIcon = (event) => { + const onClickLeftIcon = (event: MouseEvent) => { emit('click-left-icon', event); }; - const onClickRightIcon = (event) => { + const onClickRightIcon = (event: MouseEvent) => { emit('click-right-icon', event); }; - const onClear = (event) => { + const onClear = (event: MouseEvent) => { preventDefault(event); emit('update:modelValue', ''); emit('clear', event); @@ -341,11 +362,11 @@ export default createComponent({ } }); - const onKeypress = (event) => { + const onKeypress = (event: KeyboardEvent) => { const ENTER_CODE = 13; if (event.keyCode === ENTER_CODE) { - const submitOnEnter = getProp('submitOnEnter'); + const submitOnEnter = form && form.props.submitOnEnter; if (!submitOnEnter && props.type !== 'textarea') { preventDefault(event); } @@ -359,15 +380,15 @@ export default createComponent({ emit('keypress', event); }; - const onCompositionStart = (event) => { - event.target.composing = true; + const onCompositionStart = (event: Event) => { + event.target!.composing = true; }; - const onCompositionEnd = (event) => { + const onCompositionEnd = (event: Event) => { const { target } = event; - if (target.composing) { - target.composing = false; - trigger(target, 'input'); + if (target!.composing) { + target!.composing = false; + trigger(target as Element, 'input'); } }; @@ -383,16 +404,16 @@ export default createComponent({ let height = input.scrollHeight; if (isObject(props.autosize)) { const { maxHeight, minHeight } = props.autosize; - if (maxHeight) { + if (maxHeight !== undefined) { height = Math.min(height, maxHeight); } - if (minHeight) { + if (minHeight !== undefined) { height = Math.max(height, minHeight); } } if (height) { - input.style.height = height + 'px'; + input.style.height = `${height}px`; } }; @@ -415,7 +436,7 @@ export default createComponent({ const inputProps = { ref: inputRef, name: props.name, - rows: props.rows, + rows: props.rows !== undefined ? +props.rows : undefined, class: bem('control', inputAlign), value: props.modelValue, disabled, @@ -439,7 +460,7 @@ export default createComponent({ } let inputType = type; - let inputMode; + let inputMode: HTMLAttributes['inputmode']; // type="number" is weired in iOS, and can't prevent dot in Android // so use inputmode to set keyboard in mordern browers @@ -490,7 +511,7 @@ export default createComponent({ const renderWordLimit = () => { if (props.showWordLimit && props.maxlength) { - const count = (props.modelValue || '').length; + const count = getModelValue().length; return (
{count}/{props.maxlength} @@ -541,8 +562,8 @@ export default createComponent({ watch( () => props.modelValue, - (value) => { - updateValue(value); + () => { + updateValue(getModelValue()); resetValidation(); validateWithTrigger('onChange'); nextTick(adjustSize); @@ -550,7 +571,7 @@ export default createComponent({ ); onMounted(() => { - updateValue(props.modelValue, props.formatTrigger); + updateValue(getModelValue(), props.formatTrigger); nextTick(adjustSize); }); diff --git a/src/field/types.ts b/src/field/types.ts new file mode 100644 index 000000000..c901ff1e9 --- /dev/null +++ b/src/field/types.ts @@ -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; + formatter?: (value: any, rule: FieldRule) => string; +}; + +declare global { + interface EventTarget { + composing?: boolean; + } +} diff --git a/src/field/utils.ts b/src/field/utils.ts index 5796e1f90..6aeccd415 100644 --- a/src/field/utils.ts +++ b/src/field/utils.ts @@ -1,13 +1,4 @@ -export type FieldValidateTrigger = 'onSubmit' | 'onChange' | 'onBlur'; - -export type FieldRule = { - pattern?: RegExp; - trigger?: FieldValidateTrigger; - message?: string | ((value: unknown, rule: FieldRule) => string); - required?: boolean; - validator?: (value: unknown, rule: FieldRule) => boolean | Promise; - formatter?: (value: unknown, rule: FieldRule) => unknown; -}; +import type { FieldRule } from './types'; function isEmptyValue(value: unknown) { if (Array.isArray(value)) { diff --git a/src/vue-tsx-shim.d.ts b/src/vue-tsx-shim.d.ts index 5ca4491d6..29138c8a2 100644 --- a/src/vue-tsx-shim.d.ts +++ b/src/vue-tsx-shim.d.ts @@ -21,5 +21,6 @@ declare module 'vue' { onToggle?: EventHandler; onConfirm?: EventHandler; onClickStep?: EventHandler; + onTouchstart?: EventHandler; } }