[input ]: Add formatting api

pull/22404/head
yang 2023-02-22 19:57:54 +08:00
parent 2a49142965
commit 907e548cb0
4 changed files with 432 additions and 338 deletions

View File

@ -673,7 +673,29 @@ export default {
</script> </script>
``` ```
::: :::
### 格式化
:::demo `formatter``parser` 可配合实现格式化能力(仅`type`为text生效
```html
<template>
<el-input
v-model="input"
placeholder="Please input"
:formatter="(value) => `$ ${value}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
:parser="(value) => value.replace(/\$\s?|(,*)/g, '')"
/>
</template>
<script>
export default {
data() {
return {
input: '',
}
}
}
</script>
```
:::
### Input Attributes ### Input Attributes
| 参数 | 说明 | 类型 | 可选值 | 默认值 | | 参数 | 说明 | 类型 | 可选值 | 默认值 |
@ -703,8 +725,10 @@ export default {
| autofocus | 原生属性,自动获取焦点 | boolean | true, false | false | | autofocus | 原生属性,自动获取焦点 | boolean | true, false | false |
| form | 原生属性 | string | — | — | | form | 原生属性 | string | — | — |
| label | 输入框关联的label文字 | string | — | — | | label | 输入框关联的label文字 | string | — | — |
| tabindex | 输入框的tabindex | string | - | - | | tabindex | 输入框的tabindex | string | — | — |
| validate-event | 输入时是否触发表单的校验 | boolean | - | true | | validate-event | 输入时是否触发表单的校验 | boolean | — | true |
| formatter | 指定输入值的格式。(仅当 type 是"text"时才能工作) | Function | — | — |
| parser | 指定从格式化器输入中提取的值。(仅当 type 是"text"时才起作用) | Function | — | — |
### Input Slots ### Input Slots
| name | 说明 | | name | 说明 |

View File

@ -1,16 +1,18 @@
<template> <template>
<div :class="[ <div
type === 'textarea' ? 'el-textarea' : 'el-input', :class="[
inputSize ? 'el-input--' + inputSize : '', type === 'textarea' ? 'el-textarea' : 'el-input',
{ inputSize ? 'el-input--' + inputSize : '',
'is-disabled': inputDisabled, {
'is-exceed': inputExceed, 'is-disabled': inputDisabled,
'el-input-group': $slots.prepend || $slots.append, 'is-exceed': inputExceed,
'el-input-group--append': $slots.append, 'el-input-group': $slots.prepend || $slots.append,
'el-input-group--prepend': $slots.prepend, 'el-input-group--append': $slots.append,
'el-input--prefix': $slots.prefix || prefixIcon, 'el-input-group--prepend': $slots.prepend,
'el-input--suffix': $slots.suffix || suffixIcon || clearable || showPassword 'el-input--prefix': $slots.prefix || prefixIcon,
} 'el-input--suffix':
$slots.suffix || suffixIcon || clearable || showPassword,
},
]" ]"
@mouseenter="hovering = true" @mouseenter="hovering = true"
@mouseleave="hovering = false" @mouseleave="hovering = false"
@ -25,7 +27,7 @@
v-if="type !== 'textarea'" v-if="type !== 'textarea'"
class="el-input__inner" class="el-input__inner"
v-bind="$attrs" v-bind="$attrs"
:type="showPassword ? (passwordVisible ? 'text': 'password') : type" :type="showPassword ? (passwordVisible ? 'text' : 'password') : type"
:disabled="inputDisabled" :disabled="inputDisabled"
:readonly="readonly" :readonly="readonly"
:autocomplete="autoComplete || autocomplete" :autocomplete="autoComplete || autocomplete"
@ -38,33 +40,28 @@
@blur="handleBlur" @blur="handleBlur"
@change="handleChange" @change="handleChange"
:aria-label="label" :aria-label="label"
> />
<!-- 前置内容 --> <!-- 前置内容 -->
<span class="el-input__prefix" v-if="$slots.prefix || prefixIcon"> <span class="el-input__prefix" v-if="$slots.prefix || prefixIcon">
<slot name="prefix"></slot> <slot name="prefix"></slot>
<i class="el-input__icon" <i class="el-input__icon" v-if="prefixIcon" :class="prefixIcon"> </i>
v-if="prefixIcon"
:class="prefixIcon">
</i>
</span> </span>
<!-- 后置内容 --> <!-- 后置内容 -->
<span <span class="el-input__suffix" v-if="getSuffixVisible()">
class="el-input__suffix"
v-if="getSuffixVisible()">
<span class="el-input__suffix-inner"> <span class="el-input__suffix-inner">
<template v-if="!showClear || !showPwdVisible || !isWordLimitVisible"> <template v-if="!showClear || !showPwdVisible || !isWordLimitVisible">
<slot name="suffix"></slot> <slot name="suffix"></slot>
<i class="el-input__icon" <i class="el-input__icon" v-if="suffixIcon" :class="suffixIcon">
v-if="suffixIcon"
:class="suffixIcon">
</i> </i>
</template> </template>
<i v-if="showClear" <i
v-if="showClear"
class="el-input__icon el-icon-circle-close el-input__clear" class="el-input__icon el-icon-circle-close el-input__clear"
@mousedown.prevent @mousedown.prevent
@click="clear" @click="clear"
></i> ></i>
<i v-if="showPwdVisible" <i
v-if="showPwdVisible"
class="el-input__icon el-icon-view el-input__clear" class="el-input__icon el-icon-view el-input__clear"
@click="handlePasswordVisible" @click="handlePasswordVisible"
></i> ></i>
@ -74,9 +71,11 @@
</span> </span>
</span> </span>
</span> </span>
<i class="el-input__icon" <i
class="el-input__icon"
v-if="validateState" v-if="validateState"
:class="['el-input__validateIcon', validateIcon]"> :class="['el-input__validateIcon', validateIcon]"
>
</i> </i>
</span> </span>
<!-- 后置元素 --> <!-- 后置元素 -->
@ -104,337 +103,372 @@
:aria-label="label" :aria-label="label"
> >
</textarea> </textarea>
<span v-if="isWordLimitVisible && type === 'textarea'" class="el-input__count">{{ textLength }}/{{ upperLimit }}</span> <span
v-if="isWordLimitVisible && type === 'textarea'"
class="el-input__count"
>{{ textLength }}/{{ upperLimit }}</span
>
</div> </div>
</template> </template>
<script> <script>
import emitter from 'element-ui/src/mixins/emitter'; import emitter from 'element-ui/src/mixins/emitter';
import Migrating from 'element-ui/src/mixins/migrating'; import Migrating from 'element-ui/src/mixins/migrating';
import calcTextareaHeight from './calcTextareaHeight'; import merge from 'element-ui/src/utils/merge';
import merge from 'element-ui/src/utils/merge'; import { isKorean } from 'element-ui/src/utils/shared';
import {isKorean} from 'element-ui/src/utils/shared'; import calcTextareaHeight from './calcTextareaHeight';
export default { export default {
name: 'ElInput', name: 'ElInput',
componentName: 'ElInput', componentName: 'ElInput',
mixins: [emitter, Migrating], mixins: [emitter, Migrating],
inheritAttrs: false, inheritAttrs: false,
inject: { inject: {
elForm: { elForm: {
default: '' default: ''
}, },
elFormItem: { elFormItem: {
default: '' default: ''
}
},
data() {
return {
textareaCalcStyle: {},
hovering: false,
focused: false,
isComposing: false,
passwordVisible: false
};
},
props: {
value: [String, Number],
size: String,
resize: String,
form: String,
disabled: Boolean,
readonly: Boolean,
type: {
type: String,
default: 'text'
},
autosize: {
type: [Boolean, Object],
default: false
},
autocomplete: {
type: String,
default: 'off'
},
/** @Deprecated in next major version */
autoComplete: {
type: String,
validator(val) {
process.env.NODE_ENV !== 'production' &&
console.warn(
"[Element Warn][Input]'auto-complete' property will be deprecated in next major version. please use 'autocomplete' instead."
);
return true;
} }
}, },
validateEvent: {
type: Boolean,
default: true
},
suffixIcon: String,
prefixIcon: String,
label: String,
clearable: {
type: Boolean,
default: false
},
showPassword: {
type: Boolean,
default: false
},
showWordLimit: {
type: Boolean,
default: false
},
tabindex: String,
formatter: {
type: Function,
default: null
},
parser: {
type: Function,
default: null
}
},
data() { computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
validateState() {
return this.elFormItem ? this.elFormItem.validateState : '';
},
needStatusIcon() {
return this.elForm ? this.elForm.statusIcon : false;
},
validateIcon() {
return { return {
textareaCalcStyle: {}, validating: 'el-icon-loading',
hovering: false, success: 'el-icon-circle-check',
focused: false, error: 'el-icon-circle-close'
isComposing: false, }[this.validateState];
passwordVisible: false },
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
nativeInputValue() {
return this.value === null || this.value === undefined
? ''
: String(this.value);
},
showClear() {
return (
this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.nativeInputValue &&
(this.focused || this.hovering)
);
},
showPwdVisible() {
return (
this.showPassword &&
!this.inputDisabled &&
!this.readonly &&
(!!this.nativeInputValue || this.focused)
);
},
isWordLimitVisible() {
return (
this.showWordLimit &&
this.$attrs.maxlength &&
(this.type === 'text' || this.type === 'textarea') &&
!this.inputDisabled &&
!this.readonly &&
!this.showPassword
);
},
upperLimit() {
return this.$attrs.maxlength;
},
textLength() {
if (typeof this.value === 'number') {
return String(this.value).length;
}
return (this.value || '').length;
},
inputExceed() {
// show exceed style if length of initial value greater then maxlength
return this.isWordLimitVisible && this.textLength > this.upperLimit;
}
},
watch: {
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
},
// native input value is set explicitly
// do not use v-model / :value in template
// see: https://github.com/ElemeFE/element/issues/14521
nativeInputValue() {
this.setNativeInputValue();
},
// when change between <input> and <textarea>,
// update DOM dependent value and styles
// https://github.com/ElemeFE/element/issues/14857
type() {
this.$nextTick(() => {
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
});
}
},
methods: {
focus() {
this.getInput().focus();
},
blur() {
this.getInput().blur();
},
getMigratingConfig() {
return {
props: {
icon: 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
click: 'click is removed.'
}
}; };
}, },
handleBlur(event) {
props: { this.focused = false;
value: [String, Number], this.$emit('blur', event);
size: String, if (this.validateEvent) {
resize: String, this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
form: String,
disabled: Boolean,
readonly: Boolean,
type: {
type: String,
default: 'text'
},
autosize: {
type: [Boolean, Object],
default: false
},
autocomplete: {
type: String,
default: 'off'
},
/** @Deprecated in next major version */
autoComplete: {
type: String,
validator(val) {
process.env.NODE_ENV !== 'production' &&
console.warn('[Element Warn][Input]\'auto-complete\' property will be deprecated in next major version. please use \'autocomplete\' instead.');
return true;
}
},
validateEvent: {
type: Boolean,
default: true
},
suffixIcon: String,
prefixIcon: String,
label: String,
clearable: {
type: Boolean,
default: false
},
showPassword: {
type: Boolean,
default: false
},
showWordLimit: {
type: Boolean,
default: false
},
tabindex: String
},
computed: {
_elFormItemSize() {
return (this.elFormItem || {}).elFormItemSize;
},
validateState() {
return this.elFormItem ? this.elFormItem.validateState : '';
},
needStatusIcon() {
return this.elForm ? this.elForm.statusIcon : false;
},
validateIcon() {
return {
validating: 'el-icon-loading',
success: 'el-icon-circle-check',
error: 'el-icon-circle-close'
}[this.validateState];
},
textareaStyle() {
return merge({}, this.textareaCalcStyle, { resize: this.resize });
},
inputSize() {
return this.size || this._elFormItemSize || (this.$ELEMENT || {}).size;
},
inputDisabled() {
return this.disabled || (this.elForm || {}).disabled;
},
nativeInputValue() {
return this.value === null || this.value === undefined ? '' : String(this.value);
},
showClear() {
return this.clearable &&
!this.inputDisabled &&
!this.readonly &&
this.nativeInputValue &&
(this.focused || this.hovering);
},
showPwdVisible() {
return this.showPassword &&
!this.inputDisabled &&
!this.readonly &&
(!!this.nativeInputValue || this.focused);
},
isWordLimitVisible() {
return this.showWordLimit &&
this.$attrs.maxlength &&
(this.type === 'text' || this.type === 'textarea') &&
!this.inputDisabled &&
!this.readonly &&
!this.showPassword;
},
upperLimit() {
return this.$attrs.maxlength;
},
textLength() {
if (typeof this.value === 'number') {
return String(this.value).length;
}
return (this.value || '').length;
},
inputExceed() {
// show exceed style if length of initial value greater then maxlength
return this.isWordLimitVisible &&
(this.textLength > this.upperLimit);
} }
}, },
select() {
watch: { this.getInput().select();
value(val) {
this.$nextTick(this.resizeTextarea);
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.change', [val]);
}
},
// native input value is set explicitly
// do not use v-model / :value in template
// see: https://github.com/ElemeFE/element/issues/14521
nativeInputValue() {
this.setNativeInputValue();
},
// when change between <input> and <textarea>,
// update DOM dependent value and styles
// https://github.com/ElemeFE/element/issues/14857
type() {
this.$nextTick(() => {
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
});
}
}, },
resizeTextarea() {
methods: { if (this.$isServer) return;
focus() { const { autosize, type } = this;
this.getInput().focus(); if (type !== 'textarea') return;
}, if (!autosize) {
blur() { this.textareaCalcStyle = {
this.getInput().blur(); minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
},
getMigratingConfig() {
return {
props: {
'icon': 'icon is removed, use suffix-icon / prefix-icon instead.',
'on-icon-click': 'on-icon-click is removed.'
},
events: {
'click': 'click is removed.'
}
}; };
}, return;
handleBlur(event) { }
this.focused = false; const minRows = autosize.minRows;
this.$emit('blur', event); const maxRows = autosize.maxRows;
if (this.validateEvent) {
this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
}
},
select() {
this.getInput().select();
},
resizeTextarea() {
if (this.$isServer) return;
const { autosize, type } = this;
if (type !== 'textarea') return;
if (!autosize) {
this.textareaCalcStyle = {
minHeight: calcTextareaHeight(this.$refs.textarea).minHeight
};
return;
}
const minRows = autosize.minRows;
const maxRows = autosize.maxRows;
this.textareaCalcStyle = calcTextareaHeight(this.$refs.textarea, minRows, maxRows); this.textareaCalcStyle = calcTextareaHeight(
}, this.$refs.textarea,
setNativeInputValue() { minRows,
const input = this.getInput(); maxRows
if (!input) return; );
if (input.value === this.nativeInputValue) return; },
input.value = this.nativeInputValue; setNativeInputValue() {
}, const input = this.getInput();
handleFocus(event) { if (!input) return;
this.focused = true; if (input.value === this.nativeInputValue) return;
this.$emit('focus', event); input.value = this.nativeInputValue;
}, },
handleCompositionStart(event) { handleFocus(event) {
this.$emit('compositionstart', event); this.focused = true;
this.isComposing = true; this.$emit('focus', event);
}, },
handleCompositionUpdate(event) { handleCompositionStart(event) {
this.$emit('compositionupdate', event); this.$emit('compositionstart', event);
const text = event.target.value; this.isComposing = true;
const lastCharacter = text[text.length - 1] || ''; },
this.isComposing = !isKorean(lastCharacter); handleCompositionUpdate(event) {
}, this.$emit('compositionupdate', event);
handleCompositionEnd(event) { const text = event.target.value;
this.$emit('compositionend', event); const lastCharacter = text[text.length - 1] || '';
if (this.isComposing) { this.isComposing = !isKorean(lastCharacter);
this.isComposing = false; },
this.handleInput(event); handleCompositionEnd(event) {
} this.$emit('compositionend', event);
}, if (this.isComposing) {
handleInput(event) { this.isComposing = false;
// should not emit input during composition this.handleInput(event);
// see: https://github.com/ElemeFE/element/issues/10516
if (this.isComposing) return;
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (event.target.value === this.nativeInputValue) return;
this.$emit('input', event.target.value);
// ensure native input value is controlled
// see: https://github.com/ElemeFE/element/issues/12850
this.$nextTick(this.setNativeInputValue);
},
handleChange(event) {
this.$emit('change', event.target.value);
},
calcIconOffset(place) {
let elList = [].slice.call(this.$el.querySelectorAll(`.el-input__${place}`) || []);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === this.$el) {
el = elList[i];
break;
}
}
if (!el) return;
const pendantMap = {
suffix: 'append',
prefix: 'prepend'
};
const pendant = pendantMap[place];
if (this.$slots[pendant]) {
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth}px)`;
} else {
el.removeAttribute('style');
}
},
updateIconOffset() {
this.calcIconOffset('prefix');
this.calcIconOffset('suffix');
},
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.$nextTick(() => {
this.focus();
});
},
getInput() {
return this.$refs.input || this.$refs.textarea;
},
getSuffixVisible() {
return this.$slots.suffix ||
this.suffixIcon ||
this.showClear ||
this.showPassword ||
this.isWordLimitVisible ||
(this.validateState && this.needStatusIcon);
} }
}, },
handleInput(event) {
let { value, type } = event.target;
let { formatter, parser } = this;
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
if (this.isComposing) return;
// hack for https://github.com/ElemeFE/element/issues/8548
// should remove the following line when we don't support IE
if (value === this.nativeInputValue) return;
if (formatter && type === 'text') {
value = parser ? parser(value) : value;
value = formatter(value);
}
this.$emit('input', value);
created() { // ensure native input value is controlled
this.$on('inputSelect', this.select); // see: https://github.com/ElemeFE/element/issues/12850
this.$nextTick(this.setNativeInputValue);
}, },
handleChange(event) {
mounted() { this.$emit('change', event.target.value);
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
}, },
calcIconOffset(place) {
let elList = [].slice.call(
this.$el.querySelectorAll(`.el-input__${place}`) || []
);
if (!elList.length) return;
let el = null;
for (let i = 0; i < elList.length; i++) {
if (elList[i].parentNode === this.$el) {
el = elList[i];
break;
}
}
if (!el) return;
const pendantMap = {
suffix: 'append',
prefix: 'prepend'
};
updated() { const pendant = pendantMap[place];
this.$nextTick(this.updateIconOffset); if (this.$slots[pendant]) {
el.style.transform = `translateX(${place === 'suffix' ? '-' : ''}${
this.$el.querySelector(`.el-input-group__${pendant}`).offsetWidth
}px)`;
} else {
el.removeAttribute('style');
}
},
updateIconOffset() {
this.calcIconOffset('prefix');
this.calcIconOffset('suffix');
},
clear() {
this.$emit('input', '');
this.$emit('change', '');
this.$emit('clear');
},
handlePasswordVisible() {
this.passwordVisible = !this.passwordVisible;
this.$nextTick(() => {
this.focus();
});
},
getInput() {
return this.$refs.input || this.$refs.textarea;
},
getSuffixVisible() {
return (
this.$slots.suffix ||
this.suffixIcon ||
this.showClear ||
this.showPassword ||
this.isWordLimitVisible ||
(this.validateState && this.needStatusIcon)
);
} }
}; },
created() {
this.$on('inputSelect', this.select);
},
mounted() {
this.setNativeInputValue();
this.resizeTextarea();
this.updateIconOffset();
},
updated() {
this.$nextTick(this.updateIconOffset);
}
};
</script> </script>

View File

@ -480,4 +480,30 @@ describe('Input', () => {
await waitImmediate(); await waitImmediate();
expect(inputElm4.classList.contains('is-exceed')).to.false; expect(inputElm4.classList.contains('is-exceed')).to.false;
}); });
it('formatter Processing test', async() => {
vm = createVue({
template: `
<el-input
v-model="input"
placeholder="请输入内容"
:formatter="(value) => value + 'input'"
:parser="(value) => value.replace(/input/g, '')"
></el-input>
`,
data() {
return {
input: ''
};
}
}, true);
let inputElm = vm.$el.querySelector('input');
expect(inputElm.getAttribute('placeholder')).to.equal('请输入内容');
let evt = document.createEvent('HTMLEvents');
evt = new Event('input', {'bubbles': true, 'cancelable': true});
inputElm.value = 'text';
inputElm.dispatchEvent(evt);
await waitImmediate();
expect(inputElm.value).to.equal('textinput');
});
}); });

12
types/input.d.ts vendored
View File

@ -12,7 +12,12 @@ export interface AutoSize {
/** Maximum rows to show */ /** Maximum rows to show */
maxRows: number maxRows: number
} }
export interface FormatterHandler {
/**
* @param value Current value of the text input
*/
(value: string | number): void
}
/** Input Component */ /** Input Component */
export declare class ElInput extends ElementUIComponent { export declare class ElInput extends ElementUIComponent {
/** Type of input */ /** Type of input */
@ -90,6 +95,11 @@ export declare class ElInput extends ElementUIComponent {
/** Whether to show wordCount when setting maxLength */ /** Whether to show wordCount when setting maxLength */
showWordLimit: boolean showWordLimit: boolean
/**Format value program */
formatter:FormatterHandler
/**Extract value program */
parser:FormatterHandler
/** /**
* Focus the Input component * Focus the Input component
*/ */