vue中的v-model原理,与组件自定义v-model详解

VUE中的v-model可以实现双向绑定,但是原理是什么呢?往下看看吧

根据官方文档的解释,v-model其实是一个语法糖,它会自动的在元素或者组件上面解析为 :value="" 和 @input="", 就像下面这样

 // 标准写法
 <input v-model="name">

 // 等价于
 <input :value="name" @input="name = $event.target.value">

 // 在组件上面时
 <div :value="name" @input="name = $event"></div>

1.当在input输入框输入内容时,会自动的触发input事件,更新绑定的name值。

2.当name的值通过JavaScript改变时,会更新input的value值

根据上面的原理,vue就通过v-model实现双向数据绑定

看了前面的解释,对于v-model有了一定的理解。下面我们就来实现自己组件上面的v-model吧

需求:实现一个简单的点击按钮,每次点击都自动的给绑定值price加100。 组件名为 AddPrice.vue

// AddPrice.vue
// 通过props接受绑定的value参数
<template>
 <div @click="$emit('input',value + 100 )">点击加钱<div>
</template>

<script>
 export default {
 props: ['value']
 }

</script>

// 在父组件中调用
<add-price v-model="price"></add-price>

组件中使用props接受传入的参数值value, 组件点击事件触发并 使用$emit调用父组件上的input事件,实现了自定义的双向绑定

补充知识:vue - v-model实现自定义样式の多选与单选

这两天在玩mpvue,但是下午如果对着文档大眼瞪小眼的话,肯定会睡着的。

想起昨晚的flag,我就想直接用demo上手吧,一举两得

谁想到我好不容易快做完了,v-model在小程序中不起作用!

来不及研究为什么,我先直接在原来项目上赶紧建了一个test页面,先赶紧实现我的这种设想:

使用v-model和原生表单也可以实现这么好看且达到需求的效果。

重要的是不用自己跟在用户屁股后面屁颠屁颠的监听人家到底何时用了点击事件,又把点击事件用在何处了!

效果图如下,和之前的没什么两样呢!

具体实现我想,vue官网有关于表单输入绑定的讲解和demo,事实上,我只要做到利用他的demo把我的数据和样式调整一下就万事大吉了!

没有什么比简单解决一个功能更让人开心的了!

说干就干,我直接在原来项目代码的基础上动手:

之前的选项处理就一个li孤军奋战,数据渲染、样式切换、包括点击事件都绑定在上边,

ul.qus-list

li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" @click="choosed(index)" v-bind:class="{'li-focus' : chooseNum==index}" ref="liId") {{item.Code}}、{{item.Description}}

简直忙到没朋友啊有没有!光他和ul的长度差距就说明了一切!

现在我们把他要做的事分解一下:

现在他只负责v-for循环数据渲染

ul.qus-list

li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" v-bind:class="{'li-focus' : chooseNum==index}")

内部分配给他两个小弟

input:radio/checkbox和label,这俩人一个负责点击后与数据的绑定,一个负责样式。这么一说大神就明了了,好你可以走了,把沙发腾出来。

这俩人中,Input负责数据绑定,其实也就是利用v-model。具体原理直接看https://cn.vuejs.org/v2/guide/forms.html

input( type="radio" :value="item.Code" :id="'choice1'+index" v-model="picked")

然后时label负责样式。样式也包括用户看到的选项文本的展示:

label(:for="'choice1'+index" class="choice-item") {{item.Code}}、{{item.Description}}

至于他具体怎么负责样式?这个也利用了css的选择器

主要是:checked选择器和+相邻兄弟选择器

/*普通样式*/
 .choice-item{
  display: block;
  margin: .2rem auto 0;
  padding: .3rem .3rem .34rem;
  color: $qusTxt;
  font-size: .34rem;
  text-align: center;
  @include boxStyle(1rem,.12rem,rgba(49,32,114,0.16));
 }
/*input被选中时,label的样式*/
input:checked + .choice-item{
  background: $purpleClr;
  color: #FFF;
}

于是就有了这样的样式:

这里可以看出,二者是相互成就的关系:

首先通过html那里,label的for属性和input的id属性关联,使得点击label的时候,input也就被选择上了。

然后是css样式这里,label除了自己正常的样式,还受input被选中状态的影响,当input被选中后(input:checked),作为input在li爸爸内部的唯一兄弟元素(+选择符),label的样式就被重新更新了选中态。

因为选中展示的效果被label做了,那么input也就可以归隐山林,幽香田园生活了。所以直接设置样式不可见即可。

这也就是我上一篇说的,不会巧妙的利用每一个代码的特性。

而这一篇的实现方式正是还算巧妙的利用了该用的知识点。

也就不再需要li身上绑定的哪个choose事件来监听用户点击了。代码自己给我们做了!

甚至最后连用户选了什么都不用管,直接将v-model绑定的变量传给后端即可。

强大的v-model!

最后因为本需求有多选和单选,作为单页应用,又因不需要渲染很多道题目,每次只渲染一道。

所以我们可以最后根据选项判断确定是需要多选还是单选,动态的切换这两套就行了。

这么一看是不是特别简单名了!却被我之前实现的那么麻烦。。。。。我也是佩服自己光脚登山的傻劲。

整篇源码:

<template lang='pug'>
 //- 答题 组件
 #QuestionTest
 //- 弹层
 layer(:layerItem="layerItem" @confirmsubmit= "confirmSubmit($event)" @changelayershow= "changeLayerShow($event)" @hidelayer="hideLayer($event)" v-show="showLayer")
 h3.zhanshi 您的选择是:{{picked}}
  //- 题目表单
 form.question
  div
  h3.qus-title(:data-id="state.ExamInfo.QuestionID") {{state.ExamInfo.ExamQuestionNo}}、{{state.ExamInfo.Description}}
  ul.qus-list
   li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" v-bind:class="{'li-focus' : chooseNum==index}")
   input( type="radio" :value="item.Code" :id="'choice1'+index" v-model="picked")
   label(:for="'choice1'+index" class="choice-item") {{item.Code}}、{{item.Description}}
 h3.zhanshi 您的多选选择是:{{pickedBox}}
 form.question
  div
  h3.qus-title(:data-id="state.ExamInfo.QuestionID") 15、这是多选题目?-多选
  ul.qus-list
   li(v-for="(item,index) in state.ExamInfo.QuestionAnswerCode" v-bind:class="{'li-focus' : chooseNum==index}")
   input( type="checkbox" :value="item.Code" :id="'choice2'+index" v-model="pickedBox")
   label(:for="'choice2'+index" class="choice-item") {{item.Code}}、多选{{item.Description.substring(2)}}
</template>
<script>
import $axios from '../fetch/api'
export default {
 name: 'questiontest',
 data () {
 return {
  picked: '',
  pickedBox: [],
  state: {
  dataUrl: this.$store.state.ownSet.dataUrl,
  progress: this.$store.state.init.ActiveProgressEnum,
  ExamInfo: this.$store.state.init.ExamInfo,
  PersonID: this.$store.state.init.PersonID,
  TeamID: this.$store.state.init.TeamID,
  },
  unclickable: true, // 判断是否已选择答案,不选择不能下一题,并置灰按钮
  showLayer: false, //是否显示弹层
  layerItem: {
  isQuestion: false,
  isSubmit: false, //是否是最后一道题时触发“下一题"按钮,点击了提交
  isSuccess: false,
  isLoading: false
  },
  chooseNum: null,
  isFocus: false,
  isLast: false,
  isClicked: false//是否已经点击下一题,防止二次提交
 }
 },
 created(){
 // 点击开始答题,新页面应该定位到顶头题干位置
 document.body.scrollTop = 0;
 if(this.state.progress > 100107 && this.state.progress !== 100112){
  alert('您已答题完毕!');
 }
 if(this.state.ExamInfo.QuestionID == 15){//答到14题退出的情况
  //判断切换下一题和提交按钮
  this.isLast = true;
 }
 },
 methods: {
 choosed(index){
  this.chooseNumStr = '';//初始化
  // 单选or多选
  if(this.state.ExamInfo.IsMulti){
  // 多选
  if(this.$refs.liId[index].className.length <= 0){
   // 添加类
   this.$refs.liId[index].className = 'li-focus';
  }else{
   // 选中再取消
   this.$refs.liId[index].className = '';
  }
  // 获取选中结果
  for (let i = 0; i < this.$refs.liId.length; i++) {
   if(this.$refs.liId[i].className.length > 0){
   this.chooseNumStr += this.$refs.liId[i].innerText.substring(0,1);
   }
  }
  // 置灰提交按钮与否
  if(this.chooseNumStr.length > 0){
   this.unclickable = false;
  }else{
   // 没有选东西,就置灰按钮
   this.unclickable = true;
   // 注意,再添加按钮的不可点击状态
  }
  }else{
  // 单选
  this.unclickable = false;
  this.chooseNum = index;
  //索引0-3对应答案A-B
  // 注意,这里看看最多的选项是多少个,进行下配置,当前只是配置到了F
  switch(index){
   case 0: this.chooseNumStr = 'A';
   break;
   case 1: this.chooseNumStr = 'B';
   break;
   case 2: this.chooseNumStr = 'C';
   break;
   case 3: this.chooseNumStr = 'D';
   break;
   case 4: this.chooseNumStr = 'E';
   break;
   case 5: this.chooseNumStr = 'F';
   break;
  }
  }
 },
 nextItem(){//下一题
  if(this.$store.state.ownSet.test){
  // let submitFun = false;
  var newExamInfo = {
   QuestionID: 15,
   Description: "这里是一个测试标题?-多选",
   QuestionAnswerCode: [{
   Code: "A",
   Description: "多选一"
   },{
   Code: "B",
   Description: "多选二"
   },{
   Code: "C",
   Description: "多选三"
   },{
   Code: "D",
   Description: "多选四"
   }],
   IsMulti: true,
   ExamQuestionNo: 15,
   PersonID: 1
  }
  if(!this.isClicked){
   // 按钮可以点击-如果提交过一次,不能二次提交,如果提交失败,可以二次提交
   if(this.unclickable){
   alert('您还没有选择答案哦!');
   }else{
   this.isClicked = true; // 还没提交过,可以提交
   this.ajaxFun(newExamInfo,false)
   }
  }
  }else{
  if(this.state.progress > 100107 && this.state.progress != 100112){
   alert('您已答题完毕!不能重复答题。');
  }else{
   if(!this.isClicked){
   // 按钮可以点击-如果提交过一次,不能二次提交,如果提交失败,可以二次提交
   if(this.unclickable){
    alert('您还没有选择答案哦!');
   }else{
    this.isClicked = true; // 还没提交过,可以提交
    let postData = `Type=2&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//2为下一题
    if(this.state.TeamID > 0){
    postData+= `&TeamID=${this.state.TeamID}`;
    }
    this.ajaxFun(postData,false)
    .then((response)=>{
    // console.log(this.state.ExamInfo.ExamQuestionNo)
    })
    .catch((err)=>{
    this.isClicked = false;
    console.log(err);
    });
   }
   }
  }
  }
 },
 submitItem(){//提交按钮
  if(!this.isClicked){
  if(this.unclickable){
   alert('您还没有选择答案哦!');
  }else if(!this.$store.state.ownSet.test){
   if(this.state.progress > 100107){
   alert('您已答题完毕!不能重复答题。');
   }else{
   this.showLayer = true;
   this.layerItem.isSubmit = true;
   }
  }
  if(this.$store.state.ownSet.test){
   this.showLayer = true;
   this.layerItem.isSubmit = true;
  }
  }
 },
 confirmSubmit(data){// 提交弹层 之 确定
  if(this.$store.state.ownSet.test){
  this.ajaxFun('',true)
  }else{
  if(!this.isClicked){
   this.isClicked = true;
   // 发送ajax
   let postData = `Type=3&PersonID=${this.state.PersonID}&QuestionID=${this.state.ExamInfo.QuestionID}&Result=${this.chooseNumStr}`;//3为提交
   if(this.state.TeamID > 0){
   postData+= `&TeamID=${this.state.TeamID}`;
   }
   this.ajaxFun(postData,true)
   .then((response)=>{
   // 关闭提交弹层
   })
   .catch((err)=>{
   this.isClicked = false;
   console.log(err);
   });
  }
  }
 },
 changeLayerShow(data){// 提交弹层 之 取消 + 状态重置
  this.showLayer = false;
  this.layerItem.isSubmit = false;
 },
 hideLayer(data){
  this.showLayer = false;
 },
 ajaxFun(postData,submitFun){
  let _this = this;
  if(this.$store.state.ownSet.test){
  //测试效果
  return new Promise(function(resolve,reject){
   if(submitFun){
   // 关闭提交弹层
   _this.layerItem.isSubmit = false;
   }
   // 判断返回结果-弹层
   _this.layerItem.isQuestion = true;
   _this.showLayer = true;
   setTimeout(()=>{
   if(submitFun){
    // 提交
    // 判断返回结果
    _this.layerItem.isSuccess = false;
    // 改值
    _this.$store.dispatch('setProgress',100110);
    _this.$router.replace('redpacket');
   }else{
    // 判断返回结果
    _this.layerItem.isSuccess = true;
    // 下一题
    if(_this.state.ExamInfo.QuestionID == 14){ //ExamQuestionNo
    //判断切换下一题和提交按钮
    _this.isLast = true;
    }
    // 下一题重新赋值
    _this.state.ExamInfo = postData;
    _this.$store.dispatch('setExaminfo',postData)
    // 点击下一题,新页面应该定位到顶头题干位置
    document.body.scrollTop = 0;
    // 样式清空
    for (let i = 0; i < _this.$refs.liId.length; i++) {
    _this.$refs.liId[i].className = '';
    }
   }
   _this.showLayer = false;
   _this.layerItem.isQuestion = false;
   _this.chooseNumStr = '';
   _this.chooseNum = null;
   _this.unclickable = true;
   _this.isClicked = false;
   }, 2000);
  });
  }else{
  return new Promise(function(resolve,reject){
   if(submitFun){
   // 关闭提交弹层
   _this.layerItem.isSubmit = false;
   }
   _this.layerItem.isQuestion = false;
   _this.showLayer = true;
   _this.layerItem.isLoading = true;
   $axios.get(_this.state.dataUrl+'ExamAnswer?'+postData)
   .then((response)=>{
   console.log(response);
   if(response && response.data && response.data.result === 1){
    _this.layerItem.isLoading = false;
    _this.layerItem.isQuestion = true;
    // 判断返回结果
    if(response.data.RetValue.proResult){
    _this.layerItem.isSuccess = true;
    }else{
    _this.layerItem.isSuccess = false;
    }
    resolve(response);
    setTimeout(()=>{
    if(submitFun){
     // 提交
     // resolve(response);
     _this.$store.dispatch('setUser',response.data.RetValue);
     _this.$router.replace('redpacket');
    }else{
     // 下一题
     if(_this.state.ExamInfo.QuestionID == 14){ //ExamQuestionNo
     //判断切换下一题和提交按钮
     _this.isLast = true;
     }
     // 下一题重新赋值
     _this.state.ExamInfo = response.data.RetValue;
     // 点击下一题,新页面应该定位到顶头题干位置
     document.body.scrollTop = 0;
     // 样式清空
     for (let i = 0; i < _this.$refs.liId.length; i++) {
     _this.$refs.liId[i].className = '';
     }
    }
    _this.showLayer = false;
    _this.layerItem.isQuestion = false;
    _this.chooseNumStr = '';
    _this.chooseNum = null;
    _this.unclickable = true;
    _this.isClicked = false;
    }, 2000);
   }else{
    _this.showLayer = false;
    _this.layerItem.isQuestion = false;
    _this.isClicked = false;
    reject('数据提交失败,请刷新重试!')
   }
   })
   .catch((err)=>{
   _this.showLayer = false;
   _this.layerItem.isQuestion = false;
   _this.isClicked = false;
   reject(err)
   });
  });
  }
 }
 }
}
</script>
<style scoped lang='scss'>
 @import '../assets/css/var.scss';
 body{
 position: relative;
 }
 .zhanshi{
 padding: .1rem .35rem;
 color: #fff;
 font-size: .28rem;
 }
 .question{
 position: relative;
 padding: .77rem .3rem .4rem;
 margin: .21rem .3rem 1rem;
 @include boxStyle();
 .qus-title{
  margin-bottom: .77rem;
  font-size: .38rem;
  color: $textClr;
 }
 }
 .qus-box{
 display: inline-block;
 width: .3rem;
 height: .3rem;
 margin-right: .2rem;
 }
 .qus-list li{
 input{
  display: none;
 }
 input:checked + .choice-item{
  background: $purpleClr;
  color: #FFF;
 }
 .choice-item{
  display: block;
  margin: .2rem auto 0;
  padding: .3rem .3rem .34rem;
  color: $qusTxt;
  font-size: .34rem;
  text-align: center;
  @include boxStyle(1rem,.12rem,rgba(49,32,114,0.16));
 }
 &.li-focus .choice-item{
  background: $purpleClr;
  color: #FFF;
 }
 }
</style>

以上这篇vue中的v-model原理,与组件自定义v-model详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • vue中v-model的应用及使用详解

    vue中经常使用到<input>和<textarea>这类表单元素,vue对于这些元素的数据绑定和我们以前经常用的jQuery有些区别.vue使用v-model实现这些标签数据的双向绑定,它会根据控件类型自动选取正确的方法来更新元素. v-model本质上是一个语法糖.如下代码<input v-model="test">本质上是<input :value="test" @input="test = $event.t

  • vue.js指令v-model实现方法

    V-MODEL 是VUE 的一个指令,在input 控件上使用时,可以实现双向绑定. 通过看文档,发现他不过是一个语法糖. 实际是通过下面的代码来实现的: <%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!DOCTYPE html> <html lang="en">

  • Vue自定义组件的四种方式示例详解

    四种组件定义方式都存在以下共性(血泪史) 规则: 1.组件只能有一个根标签 2.记住两个词全局和局部 3.组件名称命名中'-小写字母'相当于大写英文字母(hello-com 相当于 helloCom) 而对于在HTML中自定义组件的时候有4种写法,不过也只是殊途同归,都是用template属性对应的只有一个根标签的HTML代码. 1.全局组件 定义方式示例: Vue.component("hello-component",{ props:["message"], t

  • 自定义Vue中的v-module双向绑定的实现

    v-module 双向绑定实际上就是通过子组件中的 $emit 方法派发 input 事件,父组件监听 input 事件中传递的 value 值,并存储在父组件 data 中:然后父组件再通过 prop 的形式传递给子组件 value 值,再子组件中绑定 input 的 value 属性即可. 我们着手实现一遍: 子组件传值 首先子组件需要一个 input 标签,这个 input 标签需要绑定 input 事件,$emit 触发父组件的 input 事件,通过这种方法子组件传递值给父组件 <in

  • vue服务端渲染页面缓存和组件缓存的实例详解

    vue缓存分为页面缓存.组建缓存.接口缓存,这里我主要说到了页面缓存和组建缓存 页面缓存: 在server.js中设置 const LRU = require('lru-cache') const microCache = LRU({ max: 100, // 最大缓存的数目 maxAge: 1000 // 重要提示:条目在 1 秒后过期. }) const isCacheable = req => { //判断是否需要页面缓存 if (req.url && req.url ===

  • 基于vue中css预加载使用sass的配置方式详解

    1.安装sass的依赖包 npm install --save-dev sass-loader //sass-loader依赖于node-sass npm install --save-dev node-sass 2.在build文件夹下的webpack.base.conf.js的rules里面添加配置,如下红色部分 { test: /\.sass$/, loaders: ['style', 'css', 'sass'] } <span style="color:#454545;"

  • vue的生命周期钩子与父子组件的生命周期详解

    目录 vue的生命周期钩子的介绍 父子组件的生命周期 加载渲染过程 父组件更新过程 子组件更新过程 父子组件更新过程 销毁过程 代码示例 created和mounted的区别 vue的生命周期钩子的介绍 vue官网中提供的vue的生命周期钩子图 vue的生命周期可以分为8个阶段: 1.创建vue实例涉及的两个钩子 (1)beforeCreate:创建前,vue实例初始化之前调用. 此时的数据观察和事件配置都还没有准备好,而此时的实例中的data和el还是underfined状态,不可用的.Dom

  • 微前端之Web组件自定义元素示例详解

    目录 我们知道的 Web组件使用 名称规范 组件传参数并可以写模板包括js和css Shadow Dom 影子节点 类中的构造函数和钩子函数 getter/setter属性和属性反射 扩展原生 HTML 我们知道的 第一:我们熟知的HTML标签有 a, p, div, section, ul, li, h2, article, head, body, strong, video, audio 等等 第二:我们知道,a标签是链接,p标签是段落,div是块级,h2是字体,strong 是粗体,vid

  • vue中Axios的封装和API接口的管理示例详解

    目录 一.axios的封装 安装 引入 环境的切换 设置请求超时 post请求头的设置 请求拦截 响应的拦截 封装get方法和post方法 axios的封装基本就完成了,下面再简单说下api的统一管理. 2018.8.14更新 我们所要的说的axios的封装和api接口的统一管理,其实主要目的就是在帮助我们简化代码和利于后期的更新维护. 一.axios的封装 在vue项目中,和后台交互获取数据这块,我们通常使用的是axios库,它是基于promise的http库,可运行在浏览器端和node.js

  • iOS中的应用启动原理以及嵌套模型开发示例详解

    程序启动原理和UIApplication   一.UIApplication 1.简单介绍 (1)UIApplication对象是应用程序的象征,一个UIApplication对象就代表一个应用程序. (2)每一个应用都有自己的UIApplication对象,而且是单例的,如果试图在程序中新建一个UIApplication对象,那么将报错提示. (3)通过[UIApplicationsharedApplication]可以获得这个单例对象 (4) 一个iOS程序启动后创建的第一个对象就是UIAp

  • vue中的v-model原理,与组件自定义v-model详解

    VUE中的v-model可以实现双向绑定,但是原理是什么呢?往下看看吧 根据官方文档的解释,v-model其实是一个语法糖,它会自动的在元素或者组件上面解析为 :value="" 和 @input="", 就像下面这样 // 标准写法 <input v-model="name"> // 等价于 <input :value="name" @input="name = $event.target.val

  • Vue中的scoped实现原理及穿透方法

    何为scoped? 在vue文件中的style标签上,有一个特殊的属性:scoped.当一个style标签拥有scoped属性时,它的CSS样式就只能作用于当前的组件,也就是说,该样式只能适用于当前组件元素.通过该属性,可以使得组件之间的样式不互相污染.如果一个项目中的所有style标签全部加上了scoped,相当于实现了样式的模块化. scoped的实现原理 vue中的scoped属性的效果主要通过PostCSS转译实现,如下是转译前的vue代码: <style scoped> .examp

  • vue中的双向数据绑定原理与常见操作技巧详解

    本文实例讲述了vue中的双向数据绑定原理与常见操作技巧.分享给大家供大家参考,具体如下: 什么是双向数据绑定? vue是一个mvvm框架,即数据双向绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化.这也是算是vue的精髓之处了.值得注意的是,我们所说的数据双向绑定,一定是对于UI控件来说的,非UI控件不会涉及到数据双向绑定.单向数据绑定是使用状态管理工具的前提,如果我们使用vuex,那么数据流也是单向的,这时就会和双向数据绑定有冲突,我们可以这么解决.

  • 如何在vue中更优雅的封装第三方组件详解

    目录 一.需求场景描述 二.关键技术点介绍 1.v-bind="$attrs" 2.v-on="$listeners" 三.封装el-image的代码示例 总结 一.需求场景描述 实际开发的时候,为了减少重复造轮子,提高工作效率,节省开发时间成本, 免不了会使用ui组件库,比如在web前端很受欢迎的element-ui. 但有的时候,我们需要在原组件的基础上做些改造,比如一个image组件, 我们需要统一在图片加载失败的时候展示的特定图,每次使用组件都加一遍, 麻烦

随机推荐