如何利用v-model写好vue组件
一、正确认识v-model
v-model
其实是个语法糖,可以把它拆解成两部分,首先是通过:value="someValue"
将数据通过props传入子组件,然后通过监听子组件的input
事件更新父组件的数据@input="val => someValue = val"
<child-component v-model="someValue"/>
<!--等效于-->
<child-component :value="someValue" @input="val => someValue = val"/>
<!--通常用一个method来处理input事件-->
<child-component :value="someValue" @input="handleInput"/>
handleInput (val) {
this.someValue = val
}
二、组件的设计目的
v-model一般是交由子组件处理某些数据,并且跟父组件进行数据同步,不管是父组件数据的更新还是子组件数据的操作,经由v-model绑定的数据都应该是响应的
三、设计思路
child-component 必须 要有个存储 当前数据 的地方,我们一般用做currentValue
,它的初始值是props传入的value
data () {
return {
currentValue: this.value
}
},
props: {
value: String
}
几个要点注意:
-
value是由父组件传入的,并且是响应式的,父组件的值发生改变后子组件的value也会跟着变化
-
如果value是值类型(String,Number等)而非引用类型(Array,Object等),currentValue只在子组件创建的时候得到value的值,之后value的改变跟currentValue没有半毛钱关系
清楚了以上的点,当在子组件处理数据的时候,就必须手动更改currentValue的值
为了要实时获取父组件value的变化,我们在子组件watch value
watch: {
value (value) {
if (value === this.currentValue) return
// 更新currentValue
this.currentValue = value
// do something
}
}
子组件的数据更新后要提交到父组件
someChangeHandle (value) {
// 之前提到的手动更新currentValue
this.currentValue = value
// 通过input事件告知父组件更新value
this.$emit('input', value)
}
有的时候我们要对value进行某些处理,不方便直接赋值,这时可以写个setCurrentValue方法
data () {
return {
val1: undefined,
val2: undefined,
currentValue: null
}
},
props: {
value: Array
},
methods: {
setCurrentValue (value) {
// value我们预期的是数组,如果不是,默认为空数组
value = value instanceof Array && value || []
// 如果父组件的value跟子组件的currentValue一样,则不做处理
if (JSON.stringify(value) === this.currentValue) return
this.currentValue = JSON.stringify(value)
this.val1 = [value][0]
this.val2 = [value][1]
}
},
watch: {
value: 'setCurrentValue'
},
created () {
this.setCurrentValue(this.value)
}
清楚了以上的设计思路,接下来的难点就可以顺利展开了
currentValue都是大同小异,而如何 emit 则要根据不同业务不同处理
四、实例
这里引用个checkbox的例子,checkbox的选项都是由接口获取的
前端UI组件用的 iview ,Ajax方法略过
data
属性 | 说明 |
---|---|
currentValue | 存储当前的值 |
data | 接口获取到的数组 |
props
属性 | 类型 | 说明 |
---|---|---|
url | String | 接口的地址,这个不用多说,既然是异步,肯定必传 |
params | Object | 接口的参数 |
trackBy | String | 追踪数组元素唯一性的key |
filterMethod | Function | 数据过滤的方法,对checkbox的每项数据进行过滤 |
format | Function | 对emit的array每个元素进行格式化 |
value | Array | v-model绑定的值,这里是个数组 |
computed
属性 | 说明 |
---|---|
ids | 根据value计算出追踪到的id的数组 |
paramsString | 接口参数转字符串,后续的watch用来触发拉取接口。为什么要转成字符串?因为引用类型的watch或计算属性会触发一些意料之外的响应式计算 |
filteredData | 过滤规则后的checkbox选项,模板里用来渲染的 |
emitData | emit到父组件的计算后的数据,关键 |
methods
setCheck
每次拉完数据并且成功后会将根据ids打上_checked标记,注意通过$set把_checked属性标记为响应式的
全部代码如下,enjoy
<template>
<div style="position: relative;">
<Spin fix v-show="loading"></Spin>
<Button
v-if="success === false"
size="small"
icon="refresh"
@click="fetchData({cacheClear: 1})"
:disabled="loading">重载</Button>
<template v-if="filteredData.length">
<div style="margin-bottom: 5px;">
<Checkbox :indeterminate="indeterminate" v-model="checkAll">全选</Checkbox>
</div>
<Checkbox
v-for="row of filteredData"
v-model="row._checked"
:label="row[trackBy]"
:key="row[trackBy]">
<slot :row="row">{{row[trackBy]}}</slot>
</Checkbox>
</template>
</div>
</template>
export default {
name: 'RemoteCheckbox',
data () {
return {
loading: false,
success: undefined,
data: [],
currentValue: null
}
},
props: {
url: {
type: String,
required: true
},
params: Object,
trackBy: {
type: String,
default: 'id'
},
// 数据过滤方法
filterMethod: Function,
// 格式化方法
format: {
type: Function,
default (row) {
return row[this.trackBy]
}
},
value: {
type: Array,
default () {
return []
}
}
},
computed: {
indeterminate () {
return this.emitData.length > 0 && this.emitData.length !== this.filteredData.length
},
checkAll: {
get () {
return this.filteredData.length === this.emitData.length
},
set (bool) {
this.setCheck({force: bool})
}
},
ids () {
const ids = []
for (let row of this.value) {
if (typeof row === 'object') {
if (typeof row[this.trackBy] === 'string') {
ids.push(row[this.trackBy])
} else {
return []
}
} else if (typeof row === 'string') {
return this.value
}
}
return ids
},
paramsString () {
return JSON.stringify(this.params)
},
filteredData () {
const data = []
for (let row of this.data) {
if (typeof this.filterMethod === 'function' && !this.filterMethod(row)) continue
data.push(row)
}
return data
},
emitData () {
const data = []
for (let row of this.filteredData) {
if (row._checked) data.push(this.format(row))
}
return data
}
},
methods: {
fetchData (params = {}) {
this.loading = true
this.$Ajax.get(this.url, {...this.params, ...params})
.then(rs => {
this.success = [1, -1].includes(rs.code)
this.data = (rs.data instanceof Array && rs.data) || []
if (this.success) {
this.setCheck()
}
})
.finally(() => {
this.loading = false
})
},
// 给data添加checked属性
setCheck ({force} = {}) {
if (!this.ids.length && !force) return
for (let row of this.data) {
this.$set(row, '_checked', typeof force !== 'undefined' ? force : this.ids.includes(row[this.trackBy]))
}
},
setCurrentValue (value) {
if (JSON.stringify(value) === this.currentValue) return
this.currentValue = JSON.stringify(value)
this.setCheck()
}
},
watch: {
value: 'setCurrentValue',
// 注意这里的url和paramsString的参数是有值的并且值是String,不能直接指向fetchData
url () {
this.fetchData()
},
paramsString () {
this.fetchData()
},
emitData (value) {
if (this.success) {
this.$emit('input', value)
this.currentValue = JSON.stringify(value)
this.$parent.$emit('on-form-change')
}
}
},
mounted () {
this.fetchData()
this.setCurrentValue(this.value)
}
}