cc icon indicating copy to clipboard operation
cc copied to clipboard

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

Open ccforward opened this issue 6 years ago • 4 comments

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 官方以提供了很多优秀的第三方组件库,自定义组件的实现原理其实也大同小异。

ccforward avatar Aug 13 '17 10:08 ccforward

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))

otizis avatar Aug 15 '17 02:08 otizis

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

plh97 avatar Apr 30 '18 08:04 plh97

自定义 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>

plh97 avatar Apr 30 '18 14:04 plh97

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,而是等于输入框中输入的值。这点我也好奇为啥会这样...

sfsoul avatar Feb 14 '19 07:02 sfsoul