Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

65.从 Vue.js 自定义输入框深入理解 v-model #70

Open
ccforward opened this issue Aug 13, 2017 · 4 comments
Open

65.从 Vue.js 自定义输入框深入理解 v-model #70

ccforward opened this issue Aug 13, 2017 · 4 comments
Assignees

Comments

@ccforward
Copy link
Owner

Vue.js 中使用内置的 v-model 指令通过绑定值和捕获 input 事件来模拟双向绑定。

官方文档中也只是对 input 输入框做了自定义的组件,并没有 radiocheckbox 的举例。

关于 v-model

表单的处理在官方文档已经说的很细了,这里再深入一番。

input 输入框

input输入框上的 v-model 只是一个简化的指令,它的双向绑定原理很简单,如下:

<input v-model="msg" placeholder="input message">
<p>Msg: {{ msg }}</p>

<p>Msg:</p>
<p>{{ msg }}</p>
<textarea v-model="msg" placeholder="input message"></textarea>

input 或者 textarea 标签上使用 v-model="msg" 相当于

<input :value="msg" @input="e => msg = e.target.value">

<textarea :value="msg" @input="e => msg = e.target.value"></textarea>

radio 单选按钮

正常用法:

<input type="radio" value="msg1" v-model="msg">
<input type="radio" value="msg2" v-model="msg">
<span>data: {{ msg }}</span>

相当于

<input type="radio" value="msg1" :checked="msg == 'msg1'" @change="e => msg = e.target.value">
<input type="radio" value="msg2" :checked="msg == 'msg2'" @change="e => msg = e.target.value">
<span>data: {{ msg }}</span>

checkbox 多选按钮

checkbox 略微复杂,因为涉及到了只有一个还是多个多选框的情况。

如果只有一个 checkbox v-model 会把它视为一个 Boolean 类型的值并且忽略 value, 比如:

<input type="checkbox" value="check" v-model="isChecked">

等价于

<input type="checkbox" value="check" :checked="!!isChecked" @change="e => isChecked = e.target.checked">

如果获取的值不希望是 truefalse 还可以加上 true-valuefalse-value 属性

<input type="checkbox" value="check" v-model="isChecked" true-value="1" false-value="0">

相当于

<input type="checkbox" value="check" :checked="isChecked == '1'" @change="e => isChecked = e.target.checked ? '1' : '0'">

上面的例子只是存在一个多选框的情况,如果多个 checkbox 共用同一个 model,那这些 checkbox 将会把所有选中的值组成一个数组放进去。同时 true-valuefalse-value 属性将不会再有效。

正常写法:

<template>
  <div>
    <input type="checkbox" value="c1" v-model="vals">
    <input type="checkbox" value="c2" v-model="vals">
    <input type="checkbox" value="c3" v-model="vals">
  </div>
</template>
<script>
  export default {
    data: () => ({
      vals: ['c2']
    })
  }
</script>

相当于

<template>
  <input type="checkbox" value="c1" :checked="checkVal('c1')" @change="update">
  <input type="checkbox" value="c2" :checked="checkVal('c2')" @change="update">
  <input type="checkbox" value="c3" :checked="checkVal('c3')" @change="update">
</template>

<script>
  export default {
    data() {
      return { vals: ['c2'] }
    },
    methods: {
      checkVal(val) {
        return this.vals.includes(val)
      },
      update(e) {
        const isChecked = e.target.checked
        const val = e.target.value
        if (isChecked) {
          this.vals.push(val)
        } else {
          this.vals.splice(this.vals.indexOf(val), 1)
        }
      }
    }
  }
</script>

这里得逻辑就相对于复杂了,checkVal 用来判断是否被选中 update 方法用来更新整个被选中值的数组。

v-model 在自定义组件中的使用

自定义组件中也可以使用 v-model

<custom-component v-model="custom" />

和这种用法是一样的:

<custom-component :value="custom" @input="val => custom = val" />

在 2.2.0+ 的版本中可以使用 model 属性在自定义组件中来实现属性和事件的自定义:

export default {
  name: 'custom-component',
  model: {
    prop: 'customVal',
    event: 'customEvent'
  }
}

v-model 将会查询所有的属性来替代 value 属性,并使用 prop 中来替代 input 事件的监听。
因此上面的那个 custom-component 组件被改写为

<custom-component :customVal="custom" @customEvent="val => custom = val" />

自定义 radio 按钮中使用 v-model

label 标签来做模拟一个简单的实现:

<template>
  <label>
    <input type="radio" :checked="checkVal" :value="value" @change="update">
    {{ label }}
  </label>
</template>
<script>
export default {
  model: {
    prop: 'modelVal',
    event: 'change'
  },
  props: {
    value: {
      type: String,
    },
    modelVal: {
      default: ""
    },
    label: {
      type: String,
      required: true
    },
  },
  computed: {
    checkVal() {
      return this.modelVal == this.value
    }
  },
  methods: {
    update() {
      this.$emit('change', this.value)
    }
  }
}
</script>

这里只是做模拟所以 props 中只写了这里能用到的属性

自定义 checkbox 按钮中使用 v-model

因为要支持单个 true false 类型的 checkbox(同时支持 true-value false-value)和多个 checkbox,将所有选中的值存入数组中。因此这里的代码就稍微复杂了一些。

其实只要把上面 checkbox v-model 代码的实现再增加些判断逻辑就能实现:

<template>
  <label>
    <input type="checkbox" :checked="checkVal" :value="value" @change="update">
    {{ label }}
  </label>
</template>
<script>
export default {
  model: {
    prop: 'modelVal',
    event: 'change'
  },
  props: {
    value: {
      type: String,
    },
    modelVal: {
      default: false
    },
    label: {
      type: String,
      required: true
    },
    // 定义 true-value  false-value
    trueValue: {
      default: true
    },
    falseValue: {
      default: false
    }
  },
  computed: {
    checkVal() {
      // 判断是一个还是多个 checkbox
      if (this.modelVal instanceof Array) {
        return this.modelVal.includes(this.value)
      }
      return this.modelVal === this.trueValue
    }
  },
  methods: {
    update(event) {
      const isChecked = event.target.checked
      // 这里也要判断是一个还是多个 checkbox
      if (this.modelVal instanceof Array) {
        const newVal = [...this.modelVal]
        if (isChecked) {
          newVal.push(this.value)
        } else {
          newVal.splice(newVal.indexOf(this.value), 1)
        }
        this.$emit('change', newVal)
      } else {
        this.$emit('change', isChecked ? this.trueValue : this.falseValue)
      }
    }
  }
}
</script>

上面自定义的组件代码也不是很复杂,只是为了通过代码解释下 v-model 在内部是如何工作的,所以功能肯定不完整。

最后

vue.js 官方以提供了很多优秀的第三方组件库,自定义组件的实现原理其实也大同小异。

@otizis
Copy link

otizis commented Aug 15, 2017

Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
      var formattedValue = value
        // 删除两侧的空格符
        .trim()
        // 保留 2 小数位
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值
      this.$emit('input', Number(formattedValue))
    }
  }
})

官网例子中有这个例子

不是很明白这个if里面的事情,因为我理解后面$emit更新后,input的value会跟着变,是不是多此一举了?

// 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值
      this.$emit('input', Number(formattedValue))

@plh97
Copy link

plh97 commented Apr 30, 2018

大哥,这个难了点,
据说v-model是语法糖,,

@plh97
Copy link

plh97 commented Apr 30, 2018

自定义 checkbox 按钮中使用 v-model(我也写了一个新的)

// App.vue
<template>
  <div class="wrap">
    <v-input id="s1" v-model="selected"/>
    <v-input id="s2" v-model="selected"/>
    <v-input id="s3" v-model="selected"/>
    <v-input id="s4" v-model="selected"/>
    {{selected}}
	</div>
</template>
<script>
import Input from './Input.vue';
export default {
  components: {Input},
  data:()=>({
    selected: []
  })
}
</script>
// Input.vue 组件文件 
<template id='input'>
  <label :for="id">
    <input type="checkbox" :checked="checkVal()" :id='id' @input="updateValue">
    <slot />
  </label>
</template>
<script>
export default {
  name:'v-input',  
  model: {
    prop: 'selected',
    event: 'input'
  },
  props: {
    id: {
      type: String,
      required: true
      // default: '需要一个独一无二的id'+ ~~(Math.random()*1000) + 999
    },
    selected: {},
  },
  methods: {
    checkVal(){
      return this.selected.includes(this.id)
    },
updateValue(e) {
      let newVal = [...this.selected]
      if (e.target.checked) {
        newVal.push(this.id)
      } else {
        newVal.splice(newVal.indexOf(this.id), 1)
      }
      this.$emit('input', newVal)
    }
  }
};
</script>

@sfsoul
Copy link

sfsoul commented Feb 14, 2019

Vue.component('currency-input', {
  template: '\
    <span>\
      $\
      <input\
        ref="input"\
        v-bind:value="value"\
        v-on:input="updateValue($event.target.value)"\
      >\
    </span>\
  ',
  props: ['value'],
  methods: {
    // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制
    updateValue: function (value) {
      var formattedValue = value
        // 删除两侧的空格符
        .trim()
        // 保留 2 小数位
        .slice(
          0,
          value.indexOf('.') === -1
            ? value.length
            : value.indexOf('.') + 3
        )
      // 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值
      this.$emit('input', Number(formattedValue))
    }
  }
})

官网例子中有这个例子

不是很明白这个if里面的事情,因为我理解后面$emit更新后,input的value会跟着变,是不是多此一举了?

// 如果值不统一,手动覆盖以保持一致
      if (formattedValue !== value) {
        this.$refs.input.value = formattedValue
      }
      // 通过 input 事件发出数值
      this.$emit('input', Number(formattedValue))

并没有多次一举啊。 将if那段代码注释掉,往input中输入内容是没有限制的,并不是只到了小数点后两位就停止了。同时input标签的value值也不全等于formattedValue,而是等于输入框中输入的值。这点我也好奇为啥会这样...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants