vue 自定义v-model 封装地址选择组件,并实现数据绑定和表单验证
vue是双向数据绑定的,v-model可以自动搜集数据,这在我们使用过程中可以说是非常方便。但是,在开发中,如果想把代码写的更精简,提供更多的复用。那么我们就免不了想自己封装一个用有v-model属性的组件。(本人工作中,就迫切有这种需求,因为表单页面太大,如果不做封装精简,就算用了element-ui这种已经封装过的框架,页面依然会很庞大!)
关于v-model
要实现自己的v-model,首先要了解到,v-model实际上是由两部分组成的,即value和input事件,例如下面两行代码,是等价的
<input v-model="name">
<input :value="name" @input="name=$event.target.value">
知道了原理,我们就可以开干了。
下面以分装一个三联动地址选择的小组件为例,使用的select基于element-ui
新建vue组件choose-address-form-item.vue
这里封装一个表单中的地址选择组件,所以默认认为他的父组件由<el-form>标签
封装组件
先上html部分代码,代码使用flex布局,样式相关的类名可忽略。这里说明下 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标签,为了方便处理,并且能实现表单验证,所以统一用address来接收数据
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:[],//地址数组数据
streetInputWidth:'auto',//地址输入框宽度
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: {
// 获取省市区信息 code 父级code 000000 省份 type类型 (province省份 city城市 district区域县 )
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)
},
// 点击切换区县
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)
},
//获取经纬度
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 {}
}
},//默认地址
考虑到编辑状态,会从后台获取数据显示默认数据,用defaultAddress接收,
对应的需要在watch里面做监听。并把值赋给address
watch:{
defaultAddress:{
deep:true,
handler:async function (value) {
console.log(value)
if(!value){
//没有数据时,清空
this.address={}
this.cityList=[]
this.districtList=[]
}
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}`
}
}
},
}
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)
}
这里的重点在于,当下拉框发生改变,输入框发生改变时,要及时把数据返给父级组件:
在changeProvince函数中,changeCity函数中,streetInput中,均需要执行 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",这里是数据绑定;
第二是addressProp=“address,指定子组件prop属性,用于表单验证,
第三,表单验证:下面仔细说下表单验证
由于要验证的是一个对象,并且还有存在直辖市等特殊情况,不能依靠element-ui本身的基础验证,需要自定义,在表单验证数据rules中
rules: {
address:[{validator:(rule, value, callback)=>validAddressInfo(rule, value, callback),trigger:['blur', 'change']}],
},
element-ui提供了自定义验证方式validator函数,参数有rule,value,callback,这里单独去定义一个验证函数
src下面新建utils文件夹,utils文件夹下面新建validateMethods.js
在validateMethods.js里面定义地址验证方法
validateMethods.js
//检查地址是否完善——地址封装组件
export const validAddressInfo=(rule, value, callback,msg='请完善地址信息')=>{
//如果值不是对象,肯定不通过,调用 callback(new Error(msg))函数
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))
}
}
那么现在在form.vue中引入地址验证函数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>
到此,组件封装和使用讲完。不仅简化了代码,而且数据绑定,表单验证都没少。
后面附上组件全部代码
由于本组件地址联动数据通过服务器请求获取的,请自动忽略,你只需要找到相关数据对上即可
<!--选择地址-->
<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:[],//地址数组数据
streetInputWidth:'auto',//地址输入框宽度
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: {
// 获取省市区信息 code 父级code 000000 省份 type类型 (province省份 city城市 district区域县 )
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)
},
// 点击切换区县
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)
},
//获取经纬度
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){
//没有数据时,清空
this.address={}
this.cityList=[]
this.districtList=[]
}
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>
<style scoped lang="scss">
.addressFormItemBox{
.addressFormItem{
margin-right:10px;
}
.addressFormInput{
/*flex:1;*/
}
}
</style>