美文网首页
vue 自定义v-model 封装地址选择组件,并实现数据绑定和

vue 自定义v-model 封装地址选择组件,并实现数据绑定和

作者: 东扯葫芦西扯瓜 | 来源:发表于2020-06-25 15:55 被阅读0次

    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>
    
    

    相关文章

      网友评论

          本文标题:vue 自定义v-model 封装地址选择组件,并实现数据绑定和

          本文链接:https://www.haomeiwen.com/subject/bgvsfktx.html