受控输入框的 Vue 业务封装
很多时候业务层更倾向直接使用封装好的组件,不在关心具体的细节配置。我们可以把键盘类型、受控组件等功能一并封装处理好。
下文以 Vue 为例。
Vue 受控组件底层封装
参考上文,先准备封装一个最底层受控组件 CtrlInput.vue
:
CtrlInput.vue
<!-- 受控输入框 -->
<template>
<input ref="input" v-bind="$attrs" :value="value" v-on="inputListeners"/>
</template>
<script>
/**
* 输入内容字符串化
*/
const Stringify = val => {
return (val === null || val === undefined) ? '' : String(val)
}
export default {
props: {
value: {
type: String
}
},
computed: {
inputListeners() {
return {
// 从父级添加所有的监听器
...this.$listeners,
// 添加自定义监听器
// 这里确保组件配合 `v-model` 的工作
input: e => {
this.$emit('input', e.target.value, e)
// 保证原生的 input value 是可控的
this.$nextTick(this.setNativeInputValue)
}
}
}
},
watch: {
// 输入框值改变时,将展示的原生的 input value 和 this 中的 input value 保持一致
value() {
this.setNativeInputValue()
}
},
methods: {
// 将展示的原生的 input value 和 this 中的 input value 保持一致
setNativeInputValue() {
const input = this.$refs.input
if (!input) return
if (input.value === Stringify(this.value)) return
input.value = Stringify(this.value)
}
}
}
</script>
<style scoped>
input {
margin: 0;
padding: 0;
width: inherit;
height: inherit;
}
</style>
我把公共方法,放到了 extend.js
中,如果封装多个组件,可以方便复用:
extend.js
/**
* extend 处理功能
* 启动初始化时,增加数据验证和格式处理
* 输入时,处理数据验证(this.formatter 回调实现)及保存 lastText 中
* 输入时,处理 focus 状态
* 增加 autoEnterBlur,键盘按回车后,键盘收起
*/
export default {
props: {
value: {type: String, default: ''},
autoEnterBlur: {type: Boolean, default: false}
},
data() {
return {
lastText: '', // 上一次输入的内容
focus: false // 当前是否获取焦点
}
},
computed: {
inputListeners() {
return {
...this.$listeners,
input: (value, e) => {
const v = this.formatter(value || '', e, {})
this.lastText = v
this.$emit('input', v)
if (this.$listeners.input) this.$listeners.input(v, e)
},
keyup: (e) => {
if (this.autoEnterBlur) {
if (e.code === 'Enter' || e.key === 'Enter' || e.keyCode === 13) {
e.target.blur()
}
}
if (this.$listeners.keyup) this.$listeners.keyup(e)
},
blur: (e) => {
e.target && e.target.scrollIntoViewIfNeeded && e.target.scrollIntoViewIfNeeded(true)
this.focus = false
if (this.$listeners.blur) this.$listeners.blur(e)
},
focus: (e) => {
this.focus = true
if (this.$listeners.focus) this.$listeners.focus(e)
}
}
}
},
created() {
// 初始化处理默认上一次输入内容
this.lastText = this.formatter(this.value || '', null, {})
// 处理不符合格式的默认值
this.$emit('input', this.lastText)
}
}
要注意的是,如果底层 extend.js
劫持了相应的事件,最后要调用上层组件的对应方法。如:
if (this.$listeners.focus) this.$listeners.focus(e)
否则上层组件无法监听到对应事件了。
还有,初始化及输入后,会调用上层组件的 this.formatter()
,来实现内容展示的格式化。
封装手机号输入组件
InputPhone.vue
<template>
<div class="e2-input-phone">
<CtrlInput
type="text"
inputmode="numeric"
:maxlength="11"
v-bind="$attrs"
:value="value"
v-on="inputListeners"/>
</div>
</template>
<script>
import CtrlInput from './CtrlInput.vue'
import extend from './extend'
const fn = {
// 过滤清理无效输入等
filter(value, e) {
let val = value
val = val.replace(/\s/g, '') // 禁止空格类字符
val = val.replace(/\D/g, '') // 禁止非数字
return val
},
// 格式化为最终需要格式
format(value, e) {
return value
}
}
export default {
components: {CtrlInput},
extends: extend,
methods: {
// 数据处理方法,不要改名(extend 中有调用)
formatter(value, e) {
let val = value
val = fn.filter(val, e, {})
val = fn.format(val, e, {})
return val
}
}
}
</script>
<style scoped>
.e2-input-phone {
display: flex;
align-items: center;
width: inherit;
height: inherit;
}
</style>
html 结构上,建议包裹一层 div
,主要考虑可以做除了输入框的其他功能,如一键清除功能等。这样未来结构不会有明显的层级变化了。
定义一个 fn
对象,实现了过滤器 filter
和格式化效果器 formatter
。这么写,主要是为了写其他组件时候拷贝修改起来方便。