mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-21 01:02:59 +08:00
feat(components): [input] add input formatter (#6876)
Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
parent
8be7123c75
commit
9b23b1c9ec
@ -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 count,only 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 count,only 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
|
||||
|
||||
|
13
docs/examples/input/formatter.vue
Normal file
13
docs/examples/input/formatter.vue
Normal 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>
|
@ -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')
|
||||
|
@ -39,6 +39,12 @@ export const inputProps = buildProps({
|
||||
type: String,
|
||||
default: 'off',
|
||||
},
|
||||
formatter: {
|
||||
type: Function,
|
||||
},
|
||||
parser: {
|
||||
type: Function,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
},
|
||||
|
@ -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()
|
||||
|
21
packages/hooks/__tests__/use-cursor.vitest.tsx
Normal file
21
packages/hooks/__tests__/use-cursor.vitest.tsx
Normal 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)
|
||||
})
|
||||
})
|
@ -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'
|
||||
|
66
packages/hooks/use-cursor/index.ts
Normal file
66
packages/hooks/use-cursor/index.ts
Normal 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]
|
||||
}
|
Loading…
Reference in New Issue
Block a user