vue 自定義v-model 封裝地址選擇組件跃须,并實現(xiàn)數(shù)據(jù)綁定和表單驗證
vue是雙向數(shù)據(jù)綁定的,v-model可以自動搜集數(shù)據(jù)娃兽,這在我們使用過程中可以說是非常方便菇民。但是,在開發(fā)中投储,如果想把代碼寫的更精簡第练,提供更多的復用。那么我們就免不了想自己封裝一個用有v-model屬性的組件轻要。(本人工作中复旬,就迫切有這種需求,因為表單頁面太大冲泥,如果不做封裝精簡驹碍,就算用了element-ui這種已經(jīng)封裝過的框架,頁面依然會很龐大7不小)
關(guān)于v-model
要實現(xiàn)自己的v-model志秃,首先要了解到,v-model實際上是由兩部分組成的嚼酝,即value和input事件浮还,例如下面兩行代碼,是等價的
<input v-model="name">
<input :value="name" @input="name=$event.target.value">
知道了原理闽巩,我們就可以開干了钧舌。
下面以分裝一個三聯(lián)動地址選擇的小組件為例担汤,使用的select基于element-ui
新建vue組件choose-address-form-item.vue
這里封裝一個表單中的地址選擇組件,所以默認認為他的父組件由<el-form>標簽
封裝組件
先上html部分代碼洼冻,代碼使用flex布局崭歧,樣式相關(guān)的類名可忽略。這里說明下 rowStart樣式:
.rowStart {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
<el-row>
<el-form-item :label="title" label-position="top" class="addressFormItemBox" :required="required" :prop="addressProp">
<div v-if="edit" class="rowStart">
<!--@change="changeProvince"-->
<el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince" placeholder="請選擇">
<el-option
v-for="item in provinceList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-select class="addressFormItem" :size="size" :value="address.cityId" @change="changeCity" placeholder="請選擇">
<el-option
v-for="item in cityList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-select class="addressFormItem" v-if="isNotTwoLevels" :size="size" :value="address.districtId" @change="changeDistrict" placeholder="請選擇">
<el-option
v-for="item in districtList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-input class="addressFormInput" v-if="showStreet" :style="{width:streetInputWidth}" :size="size" placeholder="請輸入" :value="address.street" @blur="streetBlur" @input="streetInput"></el-input>
</div>
<span v-else>{{fullAddress}}</span>
</el-form-item>
</el-row>
這里重點說下el-select和el-input的拆分撞牢。el-select本來的v-model被重新寫為 :value="address.provinceId" @input="changeProvince"
<el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince" placeholder="請選擇">
<el-option
v-for="item in provinceList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
由于地址里面有三個input標簽率碾,為了方便處理,并且能實現(xiàn)表單驗證屋彪,所以統(tǒng)一用address來接收數(shù)據(jù)
address.provinceId是省所宰,address.cityId是城市,address.districtId是縣
el-form-item上畜挥,:prop="addressProp"仔粥,指定prop用于設置prop屬性,便于表單驗證砰嘁。
isNotTwoLevels 用于判斷是否是直轄市件炉,直轄市只有兩級勘究,隱藏縣級矮湘。
下面看看js部分
<script>
import {mapState} from 'vuex'
import {reqRegionInfo} from "../api/commonApi"
import {isNumber} from "../utils/validate";
export default {
name: 'addressFormItem',
props:{
required:{
type:Boolean,
default:true
},//是否必須
title:{
type:String,
default:'選擇地址'
},//標題
showStreet:{
type:Boolean,
default:true
},//是否顯示輸入詳情地址
defaultAddress:{
type:Object,
default:()=>{
return {}
}
},//默認地址
edit:{
type:Boolean,
default:true
},//是否可編輯
size:{
type:String,
default:'mini'
},//尺寸
inputWidth:{
type:[Number,String],
default:'auto'
},
addressProp:{
type:String,
default:'address'
}
},
data() {
return {
address:{},//地址
isNotTwoLevels:false,//是否直轄市
cityList:[],//市
districtList:[],//縣
fullAddress:'',//地址全部信息
projectAddress:[],//地址數(shù)組數(shù)據(jù)
streetInputWidth:'auto',//地址輸入框?qū)挾? getDefault:false,//是否獲取了默認值
}
},
computed: {
...mapState(['provinceList'])
},
async mounted() {
let {inputWidth}=this
this.streetInputWidth=(typeof inputWidth==='number' || isNumber(inputWidth)) ? (inputWidth+'px') : inputWidth
this.address=this.defaultAddress
},
methods: {
// 獲取省市區(qū)信息 code 父級code 000000 省份 type類型 (province省份 city城市 district區(qū)域縣 )
async getRegionInfo(code,type=0){
let result=await reqRegionInfo(code)
// //console.log(result)
this[['provinceList','cityList','districtList'][type]]=result
;(type===2) && (this.isNotTwoLevels=!!result.length)
// //console.log(this.isNotTwoLevels)
},
// 點擊切換省份
changeProvince(val){
this.$set(this.address,'provinceId',val)
this.$set(this.address,'cityId','')
this.$set(this.address,'districtId','')
this.$set(this.address,'street','')
this.districtList = []
this.cityList = []
this.getRegionInfo(val,1)
let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
// //console.log(thisProvince)
this.projectAddress[1]=this.projectAddress[2]=''
this.projectAddress[0]=[thisProvince[0].regionName]
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
// 點擊切換城市
changeCity(val){
this.districtList = []
this.getRegionInfo(val,2)
this.$set(this.address,'cityId',val)
this.$set(this.address,'districtId','')
// //console.log(this.cityList)
let thisCity=this.cityList.filter(item=>item.regionCode===val)
this.projectAddress[2]=''
this.projectAddress[1]=thisCity[0].regionName
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
// 點擊切換區(qū)縣
async changeDistrict(val){
let {districtList}=this
this.$set(this.address,'districtId',val)
// //console.log(this.address)
let thisdistrictList=districtList.filter(item=>item.regionCode===val)
this.projectAddress[2]=thisdistrictList[0].regionName
this.projectAddress[3] && this.getLonLat(this.projectAddress.join(''))
this.address.projectAddress= this.projectAddress
this.address.showStreet= this.showStreet
this.$emit('input',this.address)
},
//獲取經(jīng)緯度
async getLonLat(data){
let lngLatArr = await this.$globalMethods.getLngLat(AMap,data)
// //console.log(lngLatArr)
let {projectAddress,isNotTwoLevels,showStreet}=this
this.address={...this.address,projectAddress,isNotTwoLevels,showStreet}
this.$emit('getLngLatInfo',{
longitude:lngLatArr[0].lng,
latitude:lngLatArr[0].lat,
})
// //console.log(this.address)
this.$emit('input',this.address)
},
//詳細地址改變
streetBlur(e){
this.projectAddress[3]=e.target.value
// //console.log(this.projectAddress.join(''))
;((this.isNotTwoLevels && this.projectAddress[1]) || this.projectAddress[2]) && this.getLonLat(this.projectAddress.join(''))
this.address.projectAddress= this.projectAddress
let {isNotTwoLevels,showStreet}=this
this.address={...this.address,projectAddress:this.projectAddress,isNotTwoLevels,showStreet}
this.$emit('input',this.address)
},
//
streetInput(value){
this.$set(this.address,'street',value)
this.$emit('input', this.address)
}
},
watch:{
defaultAddress:{
deep:true,
handler:async function (value) {
console.log(value)
let {cityId,provinceId,districtId ,street }=value
if(this.getDefault) return
if(provinceId && cityId){
let cityList = await reqRegionInfo(provinceId)
this.cityList=cityList
let districtList = await reqRegionInfo(cityId)
this.districtList=districtList
//console.log(districtList)
this.isNotTwoLevels=!!districtList.length
this.address={...value}
this.getDefault=true
let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
this.fullAddress=`${province} ${city} ${district} ${street}`
}
}
},
}
}
</script>
首先說下props部分
重點的:
defaultAddress:{
type:Object,
default:()=>{
return {}
}
},//默認地址
考慮到編輯狀態(tài),會從后臺獲取數(shù)據(jù)顯示默認數(shù)據(jù)口糕,用defaultAddress接收缅阳,
對應的需要在watch里面做監(jiān)聽。并把值賦給address
watch:{
defaultAddress:{
deep:true,
handler:async function (value) {
console.log(value)
if(!value){
//沒有數(shù)據(jù)時景描,清空
this.address={}
this.cityList=[]
this.districtList=[]
}
let {cityId,provinceId,districtId ,street }=value
//有數(shù)據(jù)時只允許更新一次
if(this.getDefault) return
if(provinceId && cityId){
let cityList = await reqRegionInfo(provinceId)
this.cityList=cityList
let districtList = await reqRegionInfo(cityId)
this.districtList=districtList
//console.log(districtList)
this.isNotTwoLevels=!!districtList.length
this.address={...value}
this.getDefault=true
let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
this.fullAddress=`${province} ${city} ${district} ${street}`
}
}
},
}
methods部分
看重點:
// 點擊切換省份
changeProvince(val){
this.$set(this.address,'provinceId',val)
this.$set(this.address,'cityId','')
this.$set(this.address,'districtId','')
this.$set(this.address,'street','')
this.districtList = []
this.cityList = []
this.getRegionInfo(val,1)
let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
// //console.log(thisProvince)
this.projectAddress[1]=this.projectAddress[2]=''
this.projectAddress[0]=[thisProvince[0].regionName]
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
// 點擊切換城市
changeCity(val){
this.districtList = []
this.getRegionInfo(val,2)
this.$set(this.address,'cityId',val)
this.$set(this.address,'districtId','')
// //console.log(this.cityList)
let thisCity=this.cityList.filter(item=>item.regionCode===val)
this.projectAddress[2]=''
this.projectAddress[1]=thisCity[0].regionName
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
//詳細地址輸入
streetInput(value){
this.$set(this.address,'street',value)
this.$emit('input', this.address)
}
這里的重點在于十办,當下拉框發(fā)生改變,輸入框發(fā)生改變時超棺,要及時把數(shù)據(jù)返給父級組件:
在changeProvince函數(shù)中向族,changeCity函數(shù)中,streetInput中棠绘,均需要執(zhí)行 this.$emit('input',this.address)
組件使用和表單驗證
封裝完了件相,開始使用
在views中新建form.vue,并且引用ChooseAddressFormItem組件:
import ChooseAddressFormItem from ../components/ChooseAddressFormItem.vue
在form.vue template中使用:
<ChooseAddressFormItem title="項目地址:" @getLngLatInfo="getLngLatInfo" size="larger" input-width="400px" v-model="projCardForm.address" addressProp=“address"/>
這里重點有三:
第一個是v-model="projCardForm.address"氧苍,這里是數(shù)據(jù)綁定夜矗;
第二是addressProp=“address,指定子組件prop屬性让虐,用于表單驗證紊撕,
第三,表單驗證:下面仔細說下表單驗證
由于要驗證的是一個對象赡突,并且還有存在直轄市等特殊情況对扶,不能依靠element-ui本身的基礎驗證区赵,需要自定義,在表單驗證數(shù)據(jù)rules中
rules: {
address:[{validator:(rule, value, callback)=>validAddressInfo(rule, value, callback),trigger:['blur', 'change']}],
},
element-ui提供了自定義驗證方式validator函數(shù)浪南,參數(shù)有rule,value,callback惧笛,這里單獨去定義一個驗證函數(shù)
src下面新建utils文件夾,utils文件夾下面新建validateMethods.js
在validateMethods.js里面定義地址驗證方法
validateMethods.js
//檢查地址是否完善——地址封裝組件
export const validAddressInfo=(rule, value, callback,msg='請完善地址信息')=>{
//如果值不是對象逞泄,肯定不通過患整,調(diào)用 callback(new Error(msg))函數(shù)
if(!value || !(value instanceof Object)){
callback(new Error(msg))
return
}
let {districtId,isNotTwoLevels,showStreet,street}=value
//顯示地址輸入框的時候,如果地址輸入框沒有值喷众,肯定不通過
if(showStreet){
if(!street){
callback(new Error(msg))
return
}
}
最后的情況各谚,非直轄市情況下,沒有縣id到千,肯定不通過
if(!districtId && isNotTwoLevels){
callback(new Error(msg))
}
}
那么現(xiàn)在在form.vue中引入地址驗證函數(shù)validAddressInfo昌渤,然后賦值給rule中的validator就行
<script>
import {validAddressInfo} from '../utils/validateMethods.js'
export default{
data(){
return{
rules: {
address:[{validator:(rule, value, callback)=>validAddressInfo(rule, value, callback),trigger:['blur', 'change']}],
},
}
}
}
</script>
到此,組件封裝和使用講完憔四。不僅簡化了代碼膀息,而且數(shù)據(jù)綁定,表單驗證都沒少了赵。
后面附上組件全部代碼
由于本組件地址聯(lián)動數(shù)據(jù)通過服務器請求獲取的潜支,請自動忽略,你只需要找到相關(guān)數(shù)據(jù)對上即可
<!--選擇地址-->
<template>
<el-row>
<el-form-item :label="title" label-position="top" class="addressFormItemBox" :required="required" :prop="addressProp">
<div v-if="edit" class="rowStart">
<!--@change="changeProvince"-->
<el-select class="addressFormItem" :size="size" :value="address.provinceId" @input="changeProvince" placeholder="請選擇">
<el-option
v-for="item in provinceList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-select class="addressFormItem" :size="size" :value="address.cityId" @change="changeCity" placeholder="請選擇">
<el-option
v-for="item in cityList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-select class="addressFormItem" v-if="isNotTwoLevels" :size="size" :value="address.districtId" @change="changeDistrict" placeholder="請選擇">
<el-option
v-for="item in districtList"
:key="item.regionCode"
:label="item.regionName"
:value="item.regionCode">
</el-option>
</el-select>
<el-input class="addressFormInput" v-if="showStreet" :style="{width:streetInputWidth}" :size="size" placeholder="請輸入" :value="address.street" @blur="streetBlur" @input="streetInput"></el-input>
</div>
<span v-else>{{fullAddress}}</span>
</el-form-item>
</el-row>
</template>
<script>
import {mapState} from 'vuex'
import {reqRegionInfo} from "../api/commonApi"
import {isNumber} from "../utils/validate";
export default {
name: 'addressFormItem',
props:{
required:{
type:Boolean,
default:true
},//是否必須
title:{
type:String,
default:'選擇地址'
},//標題
showStreet:{
type:Boolean,
default:true
},//是否顯示輸入詳情地址
defaultAddress:{
type:Object,
default:()=>{
return {}
}
},//默認地址
edit:{
type:Boolean,
default:true
},//是否可編輯
size:{
type:String,
default:'mini'
},//尺寸
inputWidth:{
type:[Number,String],
default:'auto'
},
addressProp:{
type:String,
default:'address'
}
},
data() {
return {
address:{},//地址
isNotTwoLevels:false,//是否直轄市
cityList:[],//市
districtList:[],//縣
fullAddress:'',//地址全部信息
projectAddress:[],//地址數(shù)組數(shù)據(jù)
streetInputWidth:'auto',//地址輸入框?qū)挾? getDefault:false,//是否獲取了默認值
}
},
computed: {
...mapState(['provinceList'])
},
async mounted() {
let {inputWidth}=this
this.streetInputWidth=(typeof inputWidth==='number' || isNumber(inputWidth)) ? (inputWidth+'px') : inputWidth
this.address=this.defaultAddress
},
methods: {
// 獲取省市區(qū)信息 code 父級code 000000 省份 type類型 (province省份 city城市 district區(qū)域縣 )
async getRegionInfo(code,type=0){
let result=await reqRegionInfo(code)
// //console.log(result)
this[['provinceList','cityList','districtList'][type]]=result
;(type===2) && (this.isNotTwoLevels=!!result.length)
// //console.log(this.isNotTwoLevels)
},
// 點擊切換省份
changeProvince(val){
this.$set(this.address,'provinceId',val)
this.$set(this.address,'cityId','')
this.$set(this.address,'districtId','')
this.$set(this.address,'street','')
this.districtList = []
this.cityList = []
this.getRegionInfo(val,1)
let thisProvince=this.provinceList.filter((item,index)=>item.regionCode===val)
// //console.log(thisProvince)
this.projectAddress[1]=this.projectAddress[2]=''
this.projectAddress[0]=[thisProvince[0].regionName]
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
// 點擊切換城市
changeCity(val){
this.districtList = []
this.getRegionInfo(val,2)
this.$set(this.address,'cityId',val)
this.$set(this.address,'districtId','')
// //console.log(this.cityList)
let thisCity=this.cityList.filter(item=>item.regionCode===val)
this.projectAddress[2]=''
this.projectAddress[1]=thisCity[0].regionName
this.address.projectAddress= this.projectAddress
this.$emit('input',this.address)
},
// 點擊切換區(qū)縣
async changeDistrict(val){
let {districtList}=this
this.$set(this.address,'districtId',val)
// //console.log(this.address)
let thisdistrictList=districtList.filter(item=>item.regionCode===val)
this.projectAddress[2]=thisdistrictList[0].regionName
this.projectAddress[3] && this.getLonLat(this.projectAddress.join(''))
this.address.projectAddress= this.projectAddress
this.address.showStreet= this.showStreet
this.$emit('input',this.address)
},
//獲取經(jīng)緯度
async getLonLat(data){
let lngLatArr = await this.$globalMethods.getLngLat(AMap,data)
// //console.log(lngLatArr)
let {projectAddress,isNotTwoLevels,showStreet}=this
this.address={...this.address,projectAddress,isNotTwoLevels,showStreet}
this.$emit('getLngLatInfo',{
longitude:lngLatArr[0].lng,
latitude:lngLatArr[0].lat,
})
// //console.log(this.address)
this.$emit('input',this.address)
},
//詳細地址改變
streetBlur(e){
this.projectAddress[3]=e.target.value
// //console.log(this.projectAddress.join(''))
;((this.isNotTwoLevels && this.projectAddress[1]) || this.projectAddress[2]) && this.getLonLat(this.projectAddress.join(''))
this.address.projectAddress= this.projectAddress
let {isNotTwoLevels,showStreet}=this
this.address={...this.address,projectAddress:this.projectAddress,isNotTwoLevels,showStreet}
this.$emit('input',this.address)
},
//
streetInput(value){
this.$set(this.address,'street',value)
this.$emit('input', this.address)
}
},
watch:{
defaultAddress:{
deep:true,
handler:async function (value) {
console.log(value)
if(!value){
//沒有數(shù)據(jù)時柿汛,清空
this.address={}
this.cityList=[]
this.districtList=[]
}
let {cityId,provinceId,districtId ,street }=value
//有數(shù)據(jù)時只允許更新一次
if(this.getDefault) return
if(provinceId && cityId){
let cityList = await reqRegionInfo(provinceId)
this.cityList=cityList
let districtList = await reqRegionInfo(cityId)
this.districtList=districtList
//console.log(districtList)
this.isNotTwoLevels=!!districtList.length
this.address={...value}
this.getDefault=true
let province=provinceId ? this.provinceList.filter(item=>item.regionCode===provinceId)[0].regionName : ''
let city=cityId ? cityList.filter(item=>item.regionCode===cityId)[0].regionName : ''
let district=districtId ? districtList.filter(item=>item.regionCode===districtId)[0].regionName : ''
this.fullAddress=`${province} ${city} ${district} ${street}`
}
}
},
}
}
</script>
<style scoped lang="scss">
.addressFormItemBox{
.addressFormItem{
margin-right:10px;
}
.addressFormInput{
/*flex:1;*/
}
}
</style>