cc
cc copied to clipboard
65.从 Vue.js 自定义输入框深入理解 v-model
Vue.js 中使用内置的 v-model
指令通过绑定值和捕获 input
事件来模拟双向绑定。
在官方文档中也只是对 input
输入框做了自定义的组件,并没有 radio
和 checkbox
的举例。
关于 v-model
表单的处理在官方文档已经说的很细了,这里再深入一番。
input 输入框
input输入框上的 v-model
只是一个简化的指令,它的双向绑定原理很简单,如下:
<input v-model="msg" placeholder="input message">
<p>Msg: {{ msg }}</p>
<p>Msg:</p>
<p>{{ msg }}</p>
<textarea v-model="msg" placeholder="input message"></textarea>
在 input
或者 textarea
标签上使用 v-model="msg"
相当于
<input :value="msg" @input="e => msg = e.target.value">
<textarea :value="msg" @input="e => msg = e.target.value"></textarea>
radio 单选按钮
正常用法:
<input type="radio" value="msg1" v-model="msg">
<input type="radio" value="msg2" v-model="msg">
<span>data: {{ msg }}</span>
相当于
<input type="radio" value="msg1" :checked="msg == 'msg1'" @change="e => msg = e.target.value">
<input type="radio" value="msg2" :checked="msg == 'msg2'" @change="e => msg = e.target.value">
<span>data: {{ msg }}</span>
checkbox 多选按钮
checkbox
略微复杂,因为涉及到了只有一个还是多个多选框的情况。
如果只有一个 checkbox
v-model
会把它视为一个 Boolean
类型的值并且忽略 value
, 比如:
<input type="checkbox" value="check" v-model="isChecked">
等价于
<input type="checkbox" value="check" :checked="!!isChecked" @change="e => isChecked = e.target.checked">
如果获取的值不希望是 true
或 false
还可以加上 true-value
和 false-value
属性
<input type="checkbox" value="check" v-model="isChecked" true-value="1" false-value="0">
相当于
<input type="checkbox" value="check" :checked="isChecked == '1'" @change="e => isChecked = e.target.checked ? '1' : '0'">
上面的例子只是存在一个多选框的情况,如果多个 checkbox
共用同一个 model
,那这些 checkbox
将会把所有选中的值组成一个数组放进去。同时 true-value
和 false-value
属性将不会再有效。
正常写法:
<template>
<div>
<input type="checkbox" value="c1" v-model="vals">
<input type="checkbox" value="c2" v-model="vals">
<input type="checkbox" value="c3" v-model="vals">
</div>
</template>
<script>
export default {
data: () => ({
vals: ['c2']
})
}
</script>
相当于
<template>
<input type="checkbox" value="c1" :checked="checkVal('c1')" @change="update">
<input type="checkbox" value="c2" :checked="checkVal('c2')" @change="update">
<input type="checkbox" value="c3" :checked="checkVal('c3')" @change="update">
</template>
<script>
export default {
data() {
return { vals: ['c2'] }
},
methods: {
checkVal(val) {
return this.vals.includes(val)
},
update(e) {
const isChecked = e.target.checked
const val = e.target.value
if (isChecked) {
this.vals.push(val)
} else {
this.vals.splice(this.vals.indexOf(val), 1)
}
}
}
}
</script>
这里得逻辑就相对于复杂了,checkVal
用来判断是否被选中 update
方法用来更新整个被选中值的数组。
v-model
在自定义组件中的使用
自定义组件中也可以使用 v-model
<custom-component v-model="custom" />
和这种用法是一样的:
<custom-component :value="custom" @input="val => custom = val" />
在 2.2.0+ 的版本中可以使用 model
属性在自定义组件中来实现属性和事件的自定义:
export default {
name: 'custom-component',
model: {
prop: 'customVal',
event: 'customEvent'
}
}
v-model
将会查询所有的属性来替代 value
属性,并使用 prop
中来替代 input
事件的监听。
因此上面的那个 custom-component
组件被改写为
<custom-component :customVal="custom" @customEvent="val => custom = val" />
自定义 radio
按钮中使用 v-model
用 label
标签来做模拟一个简单的实现:
<template>
<label>
<input type="radio" :checked="checkVal" :value="value" @change="update">
{{ label }}
</label>
</template>
<script>
export default {
model: {
prop: 'modelVal',
event: 'change'
},
props: {
value: {
type: String,
},
modelVal: {
default: ""
},
label: {
type: String,
required: true
},
},
computed: {
checkVal() {
return this.modelVal == this.value
}
},
methods: {
update() {
this.$emit('change', this.value)
}
}
}
</script>
这里只是做模拟所以 props
中只写了这里能用到的属性
自定义 checkbox
按钮中使用 v-model
因为要支持单个 true false
类型的 checkbox
(同时支持 true-value
false-value
)和多个 checkbox
,将所有选中的值存入数组中。因此这里的代码就稍微复杂了一些。
其实只要把上面 checkbox
v-model
代码的实现再增加些判断逻辑就能实现:
<template>
<label>
<input type="checkbox" :checked="checkVal" :value="value" @change="update">
{{ label }}
</label>
</template>
<script>
export default {
model: {
prop: 'modelVal',
event: 'change'
},
props: {
value: {
type: String,
},
modelVal: {
default: false
},
label: {
type: String,
required: true
},
// 定义 true-value false-value
trueValue: {
default: true
},
falseValue: {
default: false
}
},
computed: {
checkVal() {
// 判断是一个还是多个 checkbox
if (this.modelVal instanceof Array) {
return this.modelVal.includes(this.value)
}
return this.modelVal === this.trueValue
}
},
methods: {
update(event) {
const isChecked = event.target.checked
// 这里也要判断是一个还是多个 checkbox
if (this.modelVal instanceof Array) {
const newVal = [...this.modelVal]
if (isChecked) {
newVal.push(this.value)
} else {
newVal.splice(newVal.indexOf(this.value), 1)
}
this.$emit('change', newVal)
} else {
this.$emit('change', isChecked ? this.trueValue : this.falseValue)
}
}
}
}
</script>
上面自定义的组件代码也不是很复杂,只是为了通过代码解释下 v-model
在内部是如何工作的,所以功能肯定不完整。
最后
vue.js 官方以提供了很多优秀的第三方组件库,自定义组件的实现原理其实也大同小异。
Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)"\
>\
</span>\
',
props: ['value'],
methods: {
// 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
updateValue: function (value) {
var formattedValue = value
// 删除两侧的空格符
.trim()
// 保留 2 小数位
.slice(
0,
value.indexOf('.') === -1
? value.length
: value.indexOf('.') + 3
)
// 如果值不统一,手动覆盖以保持一致
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// 通过 input 事件发出数值
this.$emit('input', Number(formattedValue))
}
}
})
官网例子中有这个例子
不是很明白这个if里面的事情,因为我理解后面$emit更新后,input的value会跟着变,是不是多此一举了?
// 如果值不统一,手动覆盖以保持一致
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// 通过 input 事件发出数值
this.$emit('input', Number(formattedValue))
大哥,这个难了点, 据说v-model是语法糖,,
自定义 checkbox 按钮中使用 v-model(我也写了一个新的)
// App.vue
<template>
<div class="wrap">
<v-input id="s1" v-model="selected"/>
<v-input id="s2" v-model="selected"/>
<v-input id="s3" v-model="selected"/>
<v-input id="s4" v-model="selected"/>
{{selected}}
</div>
</template>
<script>
import Input from './Input.vue';
export default {
components: {Input},
data:()=>({
selected: []
})
}
</script>
// Input.vue 组件文件
<template id='input'>
<label :for="id">
<input type="checkbox" :checked="checkVal()" :id='id' @input="updateValue">
<slot />
</label>
</template>
<script>
export default {
name:'v-input',
model: {
prop: 'selected',
event: 'input'
},
props: {
id: {
type: String,
required: true
// default: '需要一个独一无二的id'+ ~~(Math.random()*1000) + 999
},
selected: {},
},
methods: {
checkVal(){
return this.selected.includes(this.id)
},
updateValue(e) {
let newVal = [...this.selected]
if (e.target.checked) {
newVal.push(this.id)
} else {
newVal.splice(newVal.indexOf(this.id), 1)
}
this.$emit('input', newVal)
}
}
};
</script>
Vue.component('currency-input', { template: '\ <span>\ $\ <input\ ref="input"\ v-bind:value="value"\ v-on:input="updateValue($event.target.value)"\ >\ </span>\ ', props: ['value'], methods: { // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制 updateValue: function (value) { var formattedValue = value // 删除两侧的空格符 .trim() // 保留 2 小数位 .slice( 0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3 ) // 如果值不统一,手动覆盖以保持一致 if (formattedValue !== value) { this.$refs.input.value = formattedValue } // 通过 input 事件发出数值 this.$emit('input', Number(formattedValue)) } } })
官网例子中有这个例子
不是很明白这个if里面的事情,因为我理解后面$emit更新后,input的value会跟着变,是不是多此一举了?
// 如果值不统一,手动覆盖以保持一致 if (formattedValue !== value) { this.$refs.input.value = formattedValue } // 通过 input 事件发出数值 this.$emit('input', Number(formattedValue))
并没有多次一举啊。 将if那段代码注释掉,往input中输入内容是没有限制的,并不是只到了小数点后两位就停止了。同时input标签的value值也不全等于formattedValue,而是等于输入框中输入的值。这点我也好奇为啥会这样...