feat(components): [input] add input formatter (#6876)

Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
Serendipity96 2022-04-23 22:48:21 +08:00 committed by GitHub
parent 8be7123c75
commit 9b23b1c9ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 30 deletions

View File

@ -41,6 +41,16 @@ input/clearable
:::
## Formatter
Display value within it's situation with `formatter`, and we usually use `parser` at the same time.
:::demo
input/formatter
:::
## Password box
:::demo Make a toggle-able password Input with the `show-password` attribute.
@ -137,35 +147,37 @@ input/length-limiting
## Input Attributes
| Attribute | Description | Type | Accepted Values | Default |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| type | type of input | string | text, textarea and other [native input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types) | text |
| modelValue / v-model | binding value | string / number | — | — |
| maxlength | the max length | string / number | — | — |
| minlength | same as `minlength` in native input | number | — | — |
| show-word-limit | whether show word countonly works when `type` is 'text' or 'textarea' | boolean | — | false |
| placeholder | placeholder of Input | string | — | — |
| clearable | whether to show clear button | boolean | — | false |
| show-password | whether to show toggleable password input | boolean | — | false |
| disabled | whether Input is disabled | boolean | — | false |
| size | size of Input, works when `type` is not 'textarea' | string | large / default / small | — |
| prefix-icon | prefix icon component | string / Component | — | — |
| suffix-icon | suffix icon component | string / Component | — | — |
| rows | number of rows of textarea, only works when `type` is 'textarea' | number | — | 2 |
| autosize | whether textarea has an adaptive height, only works when `type` is 'textarea'. Can accept an object, e.g. `{ minRows: 2, maxRows: 6 }` | boolean / object | — | false |
| autocomplete | same as `autocomplete` in native input | string | — | off |
| name | same as `name` in native input | string | — | — |
| readonly | same as `readonly` in native input | boolean | — | false |
| max | same as `max` in native input | — | — | — |
| min | same as `min` in native input | — | — | — |
| step | same as `step` in native input | — | — | — |
| resize | control the resizability | string | none / both / horizontal / vertical | — |
| autofocus | same as `autofocus` in native input | boolean | — | false |
| form | same as `form` in native input | string | — | — |
| label | label text | string | — | — |
| tabindex | input tabindex | string / number | - | - |
| validate-event | whether to trigger form validation | boolean | - | true |
| input-style | the style of the input element or textarea element | object | - | {} |
| Attribute | Description | Type | Accepted Values | Default |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| type | type of input | string | text, textarea and other [native input types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types) | text |
| modelValue / v-model | binding value | string / number | — | — |
| maxlength | the max length | string / number | — | — |
| minlength | same as `minlength` in native input | number | — | — |
| show-word-limit | whether show word countonly works when `type` is 'text' or 'textarea' | boolean | — | false |
| placeholder | placeholder of Input | string | — | — |
| clearable | whether to show clear button | boolean | — | false |
| formatter | specifies the format of the value presented input.(only works when `type` is 'input') | function(value: string / number): string | — | — |
| parser | specifies the value extracted from formatter input.(only works when `type` is 'input') | function(string): string | — | — |
| show-password | whether to show toggleable password input | boolean | — | false |
| disabled | whether Input is disabled | boolean | — | false |
| size | size of Input, works when `type` is not 'textarea' | string | large / default / small | — |
| prefix-icon | prefix icon component | string / Component | — | — |
| suffix-icon | suffix icon component | string / Component | — | — |
| rows | number of rows of textarea, only works when `type` is 'textarea' | number | — | 2 |
| autosize | whether textarea has an adaptive height, only works when `type` is 'textarea'. Can accept an object, e.g. `{ minRows: 2, maxRows: 6 }` | boolean / object | — | false |
| autocomplete | same as `autocomplete` in native input | string | — | off |
| name | same as `name` in native input | string | — | — |
| readonly | same as `readonly` in native input | boolean | — | false |
| max | same as `max` in native input | — | — | — |
| min | same as `min` in native input | — | — | — |
| step | same as `step` in native input | — | — | — |
| resize | control the resizability | string | none / both / horizontal / vertical | — |
| autofocus | same as `autofocus` in native input | boolean | — | false |
| form | same as `form` in native input | string | — | — |
| label | label text | string | — | — |
| tabindex | input tabindex | string / number | - | - |
| validate-event | whether to trigger form validation | boolean | - | true |
| input-style | the style of the input element or textarea element | object | - | {} |
## Input slots

View File

@ -0,0 +1,13 @@
<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 lang="ts" setup>
import { ref } from 'vue'
const input = ref('')
</script>

View File

@ -264,6 +264,24 @@ describe('Input.vue', () => {
`)
})
test('use formatter and parser', () => {
const val = ref('10000')
const formatter = (val: string) => {
return val.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
const parser = (val: string) => {
return val.replace(/\$\s?|(,*)/g, '')
}
const wrapper = mount(() => (
<Input v-model={val.value} formatter={formatter} parser={parser} />
))
const vm = wrapper.vm
expect(vm.$el.querySelector('input').value).toEqual('10000')
expect(vm.$el.querySelector('input').value).not.toEqual('1000')
})
describe('Input Methods', () => {
test('method:select', async () => {
const testContent = ref('test')

View File

@ -39,6 +39,12 @@ export const inputProps = buildProps({
type: String,
default: 'off',
},
formatter: {
type: Function,
},
parser: {
type: Function,
},
placeholder: {
type: String,
},

View File

@ -45,6 +45,8 @@
v-bind="attrs"
:type="showPassword ? (passwordVisible ? 'text' : 'password') : type"
:disabled="inputDisabled"
:formatter="formatter"
:parser="parser"
:readonly="readonly"
:autocomplete="autocomplete"
:tabindex="tabindex"
@ -171,6 +173,7 @@ import {
} from '@element-plus/utils'
import {
useAttrs,
useCursor,
useDisabled,
useFormItem,
useNamespace,
@ -275,6 +278,8 @@ const suffixVisible = computed(
(!!validateState.value && needStatusIcon.value)
)
const [recordCursor, setCursor] = useCursor(input)
const resizeTextarea = () => {
const { type, autosize } = props
@ -325,7 +330,14 @@ const updateIconOffset = () => {
}
const handleInput = async (event: Event) => {
const { value } = event.target as TargetElement
recordCursor()
let { value } = event.target as TargetElement
if (props.formatter) {
value = props.parser ? props.parser(value) : value
value = props.formatter(value)
}
// should not emit input during composition
// see: https://github.com/ElemeFE/element/issues/10516
@ -342,6 +354,7 @@ const handleInput = async (event: Event) => {
// see: https://github.com/ElemeFE/element/issues/12850
await nextTick()
setNativeInputValue()
setCursor()
}
const handleChange = (event: Event) => {
@ -448,6 +461,12 @@ watch(
)
onMounted(async () => {
if (!props.formatter && props.parser) {
debugWarn(
'ElInput',
'If you set the parser, you also need to set the formatter.'
)
}
setNativeInputValue()
updateIconOffset()
await nextTick()

View File

@ -0,0 +1,21 @@
import { nextTick, shallowRef } from 'vue'
import { describe, expect, it } from 'vitest'
import { useCursor } from '../use-cursor'
describe('useCursor', () => {
it('record and set cursor correctly', async () => {
const inputRef = shallowRef<HTMLInputElement>()
const [recordCursor, setCursor] = useCursor(inputRef)
if (inputRef.value == undefined) return
inputRef.value.value = 'abc'
//set a cursor position
inputRef.value.setSelectionRange(1, 1)
recordCursor()
inputRef.value.value = 'a-bc'
setCursor()
await nextTick()
const { selectionStart, selectionEnd } = inputRef.value
expect(selectionStart).toEqual(2)
expect(selectionEnd).toEqual(2)
})
})

View File

@ -27,3 +27,4 @@ export * from './use-forward-ref'
export * from './use-namespace'
export * from './use-z-index'
export * from './use-floating'
export * from './use-cursor'

View File

@ -0,0 +1,66 @@
import { ref } from 'vue'
import type { ShallowRef } from 'vue'
// Keep input cursor in the correct position when we use formatter.
export function useCursor(
input: ShallowRef<HTMLInputElement | undefined>
): [() => void, () => void] {
const selectionRef = ref<{
selectionStart?: number
selectionEnd?: number
value?: string
beforeTxt?: string
afterTxt?: string
}>()
function recordCursor() {
if (input.value == undefined) return
const { selectionStart, selectionEnd, value } = input.value
if (selectionStart == null || selectionEnd == null) return
const beforeTxt = value.slice(0, Math.max(0, selectionStart))
const afterTxt = value.slice(Math.max(0, selectionEnd))
selectionRef.value = {
selectionStart,
selectionEnd,
value,
beforeTxt,
afterTxt,
}
}
function setCursor() {
if (input.value == undefined || selectionRef.value == undefined) return
const { value } = input.value
const { beforeTxt, afterTxt, selectionStart } = selectionRef.value
if (
beforeTxt == undefined ||
afterTxt == undefined ||
selectionStart == undefined
)
return
let startPos = value.length
if (value.endsWith(afterTxt)) {
startPos = value.length - afterTxt.length
} else if (value.startsWith(beforeTxt)) {
startPos = beforeTxt.length
} else {
const beforeLastChar = beforeTxt[selectionStart - 1]
const newIndex = value.indexOf(beforeLastChar, selectionStart - 1)
if (newIndex !== -1) {
startPos = newIndex + 1
}
}
input.value.setSelectionRange(startPos, startPos)
}
return [recordCursor, setCursor]
}