v-model是 Vue 最常用的指令之一,用于表单元素的双向绑定。自定义组件也可以实现v-model,让组件使用更加直观。
对于原生表单元素,v-model 是语法糖:
<input v-model="value">
<!-- 等同于 -->
<input :value="value" @input="value = $event.target.value">
对于自定义组件,v-model 同样是语法糖:
<my-component v-model="value"></my-component>
<!-- 等同于 -->
<my-component :value="value" @input="value = $event"></my-component>
默认情况下,v-model 使用 value prop 和 input 事件:
Vue.component('my-input', {
props: ['value'],
template: `
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
`
})
<my-input v-model="message"></my-input>
<p>{{ message }}</p>
Vue.component('custom-input', {
props: {
value: [String, Number],
placeholder: String,
disabled: Boolean
},
template: `
<div class="custom-input">
<input
:value="value"
:placeholder="placeholder"
:disabled="disabled"
@input="handleInput"
@focus="$emit('focus', $event)"
@blur="$emit('blur', $event)"
>
</div>
`,
methods: {
handleInput(event) {
this.$emit('input', event.target.value)
}
}
})
使用 model 选项自定义 prop 和 event:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
disabled: Boolean
},
template: `
<input
type="checkbox"
:checked="checked"
:disabled="disabled"
@change="$emit('change', $event.target.checked)"
>
`
})
<my-checkbox v-model="isChecked"></my-checkbox>
<!-- 等同于 -->
<my-checkbox :checked="isChecked" @change="isChecked = $event"></my-checkbox>
某些情况下,value prop 和 input 事件不合适:
checked 和 changechecked 和 changevalue 和 changeVue.component('switch-button', {
model: {
prop: 'on',
event: 'toggle'
},
props: {
on: {
type: Boolean,
default: false
},
disabled: Boolean
},
template: `
<button
class="switch"
:class="{ on: on }"
:disabled="disabled"
@click="toggle"
>
<span class="switch-label">{{ on ? '开' : '关' }}</span>
</button>
`,
methods: {
toggle() {
if (!this.disabled) {
this.$emit('toggle', !this.on)
}
}
}
})
<switch-button v-model="isOn"></switch-button>
Vue.component('star-rating', {
model: {
prop: 'rating',
event: 'rate'
},
props: {
rating: {
type: Number,
default: 0
},
max: {
type: Number,
default: 5
},
readonly: Boolean
},
template: `
<div class="star-rating">
<span
v-for="i in max"
:key="i"
class="star"
:class="{ active: i <= rating }"
@click="rate(i)"
@mouseenter="hoverRating = i"
@mouseleave="hoverRating = 0"
>★</span>
</div>
`,
data() {
return {
hoverRating: 0
}
},
methods: {
rate(value) {
if (!this.readonly) {
this.$emit('rate', value)
}
}
}
})
<star-rating v-model="rating" :max="10"></star-rating>
Vue.component('color-picker', {
model: {
prop: 'color',
event: 'change'
},
props: {
color: {
type: String,
default: '#000000'
}
},
data() {
return {
colors: [
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
'#ffff00', '#ff00ff', '#00ffff', '#ff8800', '#8800ff'
]
}
},
template: `
<div class="color-picker">
<div class="color-preview" :style="{ backgroundColor: color }"></div>
<div class="color-list">
<span
v-for="c in colors"
:key="c"
class="color-item"
:style="{ backgroundColor: c }"
:class="{ active: c === color }"
@click="$emit('change', c)"
></span>
</div>
<input
type="color"
:value="color"
@input="$emit('change', $event.target.value)"
>
</div>
`
})
<color-picker v-model="selectedColor"></color-picker>
Vue.component('range-slider', {
model: {
prop: 'value',
event: 'input'
},
props: {
value: {
type: Number,
default: 0
},
min: {
type: Number,
default: 0
},
max: {
type: Number,
default: 100
},
step: {
type: Number,
default: 1
}
},
computed: {
percentage() {
return ((this.value - this.min) / (this.max - this.min)) * 100
}
},
template: `
<div class="range-slider">
<input
type="range"
:value="value"
:min="min"
:max="max"
:step="step"
@input="$emit('input', Number($event.target.value))"
>
<div class="slider-track">
<div class="slider-fill" :style="{ width: percentage + '%' }"></div>
</div>
<span class="slider-value">{{ value }}</span>
</div>
`
})
<range-slider v-model="volume" :min="0" :max="100"></range-slider>
Vue.component('multi-select', {
model: {
prop: 'selected',
event: 'change'
},
props: {
options: {
type: Array,
required: true
},
selected: {
type: Array,
default: () => []
}
},
methods: {
toggle(option) {
const index = this.selected.indexOf(option.value)
if (index > -1) {
this.$emit('change', this.selected.filter(v => v !== option.value))
} else {
this.$emit('change', [...this.selected, option.value])
}
},
isSelected(option) {
return this.selected.includes(option.value)
}
},
template: `
<div class="multi-select">
<div
v-for="option in options"
:key="option.value"
class="option"
:class="{ selected: isSelected(option) }"
@click="toggle(option)"
>
{{ option.label }}
</div>
</div>
`
})
<multi-select
v-model="selectedTags"
:options="tagOptions"
></multi-select>
Vue.component('number-input', {
model: {
prop: 'value',
event: 'input'
},
props: {
value: {
type: Number,
default: 0
},
min: Number,
max: Number,
step: {
type: Number,
default: 1
}
},
methods: {
increase() {
const newValue = this.value + this.step
if (this.max === undefined || newValue <= this.max) {
this.$emit('input', newValue)
}
},
decrease() {
const newValue = this.value - this.step
if (this.min === undefined || newValue >= this.min) {
this.$emit('input', newValue)
}
}
},
template: `
<div class="number-input">
<button @click="decrease" :disabled="min !== undefined && value <= min">-</button>
<input
type="number"
:value="value"
:min="min"
:max="max"
:step="step"
@input="$emit('input', Number($event.target.value))"
>
<button @click="increase" :disabled="max !== undefined && value >= max">+</button>
</div>
`
})
<number-input v-model="quantity" :min="1" :max="99"></number-input>
prop 和 event 必须匹配
model 选项中的 prop 和 event 必须与组件中的 props 和 $emit 匹配:
// ✅ 正确
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean // 匹配 prop
},
methods: {
toggle() {
this.$emit('change', newValue) // 匹配 event
}
}
// ❌ 错误:不匹配
model: {
prop: 'checked',
event: 'change'
},
props: {
value: Boolean // 不匹配
}
保留原有 prop
即使使用 model 自定义了 prop,原有的 prop 仍然存在:
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean,
value: String // 仍然可以接收 value
}
})
自定义 v-model 要点:
value prop 和 input 事件model 选项自定义 prop 和 event