表單數(shù)據(jù)處理是一個(gè)項(xiàng)目中的必不可少的內(nèi)容,編寫(xiě)表單組件是完成復(fù)雜項(xiàng)目基礎(chǔ)。本文深入整理了Vue制作表單組件的各個(gè)方面,為后續(xù)制作復(fù)雜表單組件打好基礎(chǔ)。
處理簡(jiǎn)單數(shù)據(jù)類型
基本方法
Vue中用prop從父組件向子組件傳遞數(shù)據(jù)。
下面是一個(gè)簡(jiǎn)單的輸入組件MyInput.vue,定義了屬性message,并且通過(guò)v-model指令綁定到了html的input上。
<template>
<div class="my-input">
<input v-model="message" />
</div>
</template>
<script>
export default {
name: 'MyInput',
props: ['message'],
watch: {
message: function() {
console.log(this.message)
}
}
}
</script>
我們?cè)诟附M件HelloModel.vue中調(diào)用這個(gè)組件。
<template>
<div class="hello">
<my-input :message="message" />
<div>message: {{ message }}</div>
</div>
</template>
<script>
import MyInput from './MyInput'
export default {
name: 'HelloModel',
components: { MyInput },
data() {
return { message: 'hello' }
}
}
</script>
程序是可以運(yùn)行的,但是當(dāng)修改數(shù)據(jù)時(shí)(屬性message的值已經(jīng)發(fā)生變化),Vue會(huì)發(fā)出warn警告。
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "message"
而且父組件中的message并不會(huì)被修改,這是因?yàn)閂ue的屬性是單向綁定。
所有的 prop 都使得其父子 prop 之間形成了一個(gè)單向下行綁定:父級(jí) prop 的更新會(huì)向下流動(dòng)到子組件中,但是反過(guò)來(lái)則不行。
警告的問(wèn)題很好解決,把prop的賦值給組件的數(shù)據(jù),并且將input綁定到這個(gè)數(shù)據(jù)上(這里在input上用v-model只是想說(shuō)明v-model不能直接用于簡(jiǎn)單類型的屬性)。
<template>
<div class="my-input">
<input v-model="innerMessage" />
</div>
</template>
<script>
export default {
name: 'MyInput',
props: ['message'],
data() {
return {
innerMessage: this.message // 用prop賦值
}
},
watch: {
innerMessage: function() {
console.log(this.innerMessage)
}
}
}
</script>
如何將修改傳遞回父組件?捕獲input事件,發(fā)送給父組件,讓父組件自己去去修改。
MyInput.vue添加向父組件發(fā)送事件的代碼。
<template>
<div class="my-input">
<input v-model="innerMessage" @input="input" />
</div>
</template>
<script>
export default {
name: 'MyInput',
props: ['message'],
data() {
return {
innerMessage: this.message // 用prop賦值
}
},
watch: {
message: function() {
console.log('myinput.message', this.message)
},
innerMessage: function() {
console.log('myinput.innerMessage', this.innerMessage)
}
},
methods: {
input: function(event) {
// 接收事件并轉(zhuǎn)發(fā)給父組件,注意參數(shù)是html的原始對(duì)象
let newVal = event.target.value
console.log('myinput.input', newVal)
this.$emit('input', newVal)
}
}
}
</script>
HelloModel.vue添加接收子組件事件的代碼。
<template>
<div class="hello">
<my-input :message="message" @input="input" />
<div>message: {{ message }}</div>
</div>
</template>
<script>
import MyInput from './MyInput'
export default {
name: 'HelloModel',
components: { MyInput },
data() {
return { message: 'hello' }
},
methods: {
input: function(newVal) {
// 接收子組件傳遞的事件,注意參數(shù)不是event
this.message = newVal
console.log('parent.input', this.message)
}
}
}
</script>
用v-model簡(jiǎn)化代碼
既然原生的input可以用v-model實(shí)現(xiàn)雙向綁定,那么MyInput組件是否也可以支持呢?父組件中使用的時(shí)候簡(jiǎn)化為:
<my-input v-model="message" />
其實(shí)是可以的,但是MyInput要做改造,生成新的Myinput2.vue文件。
<template>
<div class="my-input">
<!-- 注意:這里不是v-model,改成了單向綁定 -->
<input :value="value" @input="input" />
</div>
</template>
<script>
export default {
name: 'MyInput',
props: ['value'], // 需要指定名稱為value的屬性
methods: {
input: function(event) {
// 接收事件并轉(zhuǎn)發(fā)給父組件,注意參數(shù)是原始event對(duì)象
let newVal = event.target.value
console.log('myinput.input', newVal, event)
this.$emit('input', newVal)
}
}
}
</script>
生成新的HelloModel2.vue文件。
<template>
<div class="hello">
<my-input v-model="message" />
<div>message: {{ message }}</div>
</div>
</template>
<script>
import MyInput from './MyInput2'
export default {
name: 'HelloModel',
components: { MyInput },
data() {
return { message: 'hello' }
}
}
</script>
參考:
用.sync修飾符簡(jiǎn)化
不進(jìn)行解釋了,直接看代碼MyInput3.vue和HelloModel.vue。
<template>
<div class="my-input">
<input :value="value" @input="input" />
</div>
</template>
<script>
export default {
name: 'MyInput',
props: ['value'], // 需要指定名稱為value的屬性
methods: {
input: function(event) {
let newVal = event.target.value
console.log('myinput.input', newVal)
// 和.sync修飾符匹配
this.$emit('update:value', newVal)
}
}
}
</script>
<template>
<div class="hello">
<!-- 使用.sync修飾符 -->
<my-input :value.sync="message" />
<div>message: {{ message }}</div>
</div>
</template>
<script>
import MyInput from './MyInput3'
export default {
name: 'HelloModel',
components: { MyInput },
data() {
return { message: 'hello' }
}
}
</script>
參考:https://cn.vuejs.org/v2/guide/components-custom-events.html#sync-修飾符
補(bǔ)充說(shuō)明
v-model和.sync能簡(jiǎn)化表單組件的代碼,它們有什么區(qū)別嗎?
v-model寫(xiě)起來(lái)更簡(jiǎn)潔,應(yīng)為父組件不需要知道表單組件中的屬性名(默認(rèn)使用value)。但是,如果表單組件中有多個(gè)input,那么v-model就無(wú)法表示綁定關(guān)系,因?yàn)?code>.sync要指定表單控件的屬性名,就可以解決1對(duì)多的綁定問(wèn)題。
使用v-model時(shí)還會(huì)碰到一個(gè)問(wèn)題,組件上的 v-model 默認(rèn)會(huì)利用名為 value 的 prop 和名為 input 的事件,但是像單選框、復(fù)選框等類型的輸入控件可能會(huì)將 value attribute 用于不同的目的。定義組件時(shí)可以用model 選項(xiàng)避免這樣的沖突。
{
model: { // 定義model
prop: 'checked', // 綁定prop傳遞的值
event: 'change' // 定義觸發(fā)事件名稱
},
props: {
checked: Boolean // 接受父組件傳遞的值
}
}
參考:https://cn.vuejs.org/v2/guide/components-custom-events.html#自定義組件的-v-model
處理對(duì)象和數(shù)組
前面表單控件的屬性都是簡(jiǎn)單類型,如果直接傳遞對(duì)象或數(shù)組會(huì)有什么不一樣?先看看Vue官網(wǎng)文檔里的一句話,然后看例子。
注意在 JavaScript 中對(duì)象和數(shù)組是通過(guò)引用傳入的,所以對(duì)于一個(gè)數(shù)組或?qū)ο箢愋偷?prop 來(lái)說(shuō),在子組件中改變這個(gè)對(duì)象或數(shù)組本身將會(huì)影響到父組件的狀態(tài)。
傳遞引用
MyForm.vue實(shí)現(xiàn)一個(gè)簡(jiǎn)單的表單組件,輸入姓名和年齡。
<template>
<div class="my-form">
<div>
<label>姓名:
<input v-model="person.name" />
</label>
</div>
<div>
<label>年齡:
<input v-model="person.age" />
</label>
</div>
</div>
</template>
<script>
export default {
name: 'MyForm',
//model: { prop: 'person' },
props: { person: Object },
watch: {
person: {
handler: function() {
console.log('myform.person', this.person)
},
deep: true
}
}
}
</script>
HelloModel.vue調(diào)用表單組件,傳遞對(duì)象。
<template>
<div class="hello">
<div>
<my-form :person="person" />
</div>
<div>
<div>姓名:{{person.name}}</div>
<div>年齡:{{person.age}}</div>
</div>
</div>
</template>
<script>
import MyForm from './MyForm'
export default {
name: 'HelloModel',
components: { MyForm },
data() {
return { person: { name: 'hello', age: 20 } }
}
}
</script>
我們?cè)诟缸咏M件中都添加了watch,子組件中并沒(méi)有拋出修改數(shù)據(jù)事件,當(dāng)input中的數(shù)據(jù)發(fā)生變化時(shí),父子組件中都可以監(jiān)控到(父組件在前)。
這里有個(gè)問(wèn)題:是否可以用v-model傳遞對(duì)象呢?我認(rèn)為是可以的,但是其實(shí)沒(méi)有意義,因?yàn)闆](méi)有拋出修改事件數(shù)據(jù)就已經(jīng)被修改了,v-model要解決問(wèn)題已經(jīng)不存在了。
不直接修改父組件數(shù)據(jù)
如果不希望直接修改父組件的數(shù)據(jù)怎么辦?例如:需要對(duì)用戶輸入的數(shù)據(jù)進(jìn)行檢查,檢查通過(guò)后再更改父組件的數(shù)據(jù)。解決辦法是在組件中的控件不直接綁定傳入的屬性對(duì)象,而是創(chuàng)建一個(gè)組件內(nèi)的數(shù)據(jù)進(jìn)行綁定,檢查通過(guò)后再將修改合并到父組件的對(duì)象中。
子組件MyForm2.vue
<template>
<div class="my-form">
<div>
<label>姓名:
<input v-model="innerPerson.name" />
</label>
</div>
<div>
<label>年齡:
<input v-model="innerPerson.age" />
</label>
</div>
</div>
</template>
<script>
export default {
name: 'MyForm',
props: { person: Object },
data() {
return {
// 復(fù)制傳入的屬性
innerPerson: JSON.parse(JSON.stringify(this.person))
}
},
methods: {
result() {
return this.innerPerson
}
}
}
</script>
父組件HelloModel5.vue。
<template>
<div class="hello">
<div>
<my-form ref="myForm" :person="person" />
</div>
<div>
<div>姓名:{{person.name}}</div>
<div>年齡:{{person.age}}</div>
</div>
<div>
<button @click="merge">合并數(shù)據(jù)</button>
</div>
</div>
</template>
<script>
import MyForm from './MyForm2'
export default {
name: 'HelloModel',
components: { MyForm },
data() {
return { person: { name: 'hello', age: 20 } }
},
methods: {
merge() {
Object.assign(this.person, this.$refs.myForm.result())
}
}
}
</script>
渲染函數(shù)(render)
當(dāng)業(yè)務(wù)邏輯比較復(fù)雜時(shí),我們用渲染函數(shù)render編寫(xiě)代碼可能更有效,所以下面看看如何用render實(shí)現(xiàn)上面的這些功能。
簡(jiǎn)單類型
<script>
export default {
name: 'MyInput',
props: ['value'], // 需要指定名稱為value的屬性
methods: {
input: function(event) {
let newVal = event.target.value
// 同時(shí)支持v-model和sync
if (this.$listeners.input) this.$emit('input', newVal)
if (this.$listeners['update:value']) this.$emit('update:value', newVal)
}
},
render(createElement) {
const vnodeInput = createElement('input', {
domProps: { value: this.value }, //這里是domProps,不是props
on: { input: this.input }
})
return createElement('div', { class: 'my-input' }, [vnodeInput])
}
}
</script>
父組件不需要有任何變化,就不貼在這里了。參照之前的代碼,就更容易理解render函數(shù),其實(shí)它就是將template變成了javascript代碼,Vue的build也是這么干的。這里稍微增加了一點(diǎn)邏輯,就是讓這個(gè)組件同時(shí)支持v-model和sync兩種方式。
參考:https://cn.vuejs.org/v2/api/#vm-listeners
對(duì)象和數(shù)組
<script>
export default {
name: 'MyForm',
props: { person: Object },
data() {
return {
innerPerson: JSON.parse(JSON.stringify(this.person))
}
},
methods: {
result() {
return this.innerPerson
}
},
render(h) {
const vnodes = [
['姓名:', 'name'],
['年齡:', 'age']
].map(([label, key]) => {
return h('div', [
h('label', [
label,
h('input', {
domProps: { value: this.innerPerson[key] },
on: { input: event => (this.innerPerson[key] = event.target.value) }
})
])
])
})
return h('div', { class: 'my-form' }, vnodes)
}
}
</script>
從渲染函數(shù)的角度看,處理對(duì)象屬性并沒(méi)有什么特殊的地方。這里雖然只有兩個(gè)屬性,但是特意寫(xiě)成了循環(huán)的方式,是要示例一下v-for和v-if在render中的寫(xiě)法。
參考:https://cn.vuejs.org/v2/guide/render-function.html#v-if-和-v-for
總結(jié)
通過(guò)上面的例子把Vue中和表單處理相關(guān)的知識(shí)點(diǎn)都整理了一遍,解決一般性的問(wèn)題應(yīng)該是夠用了。但是,表單處理是一個(gè)可以無(wú)限復(fù)雜的事情,會(huì)碰到各種各樣的情況,細(xì)節(jié)非常多,還需不斷嘗試與研究。
本系列其他文章:
Vue項(xiàng)目總結(jié)(1)-基本概念+Nodejs+VUE+VSCode
Vue項(xiàng)目總結(jié)(2)-前端獨(dú)立測(cè)試+VUE