Vue3受控表单组件实现

109 阅读2分钟

Vue3受控表单组件实现

表单组件是常用的组件之一。在Vue中,如果不对表单组件进行封装,在同一个组件中使用的话可以方便的使用v-model来实现数据的双向绑定。

例如:

<script setup lang="ts">
import { ref } from 'vue'
const value = ref('')
</script>

<template>
  <div>
   <input type="text" v-model="value"/>
   <div>{{ value }}</div>
  </div>
</template>

<style scoped></style>

但是,如果我们要对表单组件进行一层封装,那么我们怎么使用v-model实现数据的双向绑定呢?

我们实现一个MyInput组件,这个组件的value值完全是由父组件控制的,但是在子组件中也可以对它的值做一些操作 (双向绑定)。

在下面的代码中,我们需要让MyInput组件可以正常输入,并且在父组件中更改传递的value值以后,MyInput中的值也会发生改变。

<script setup lang="ts">
import { ref } from 'vue'
import MyInput from './components/MyInput.vue'

const value = ref('')
const clear = () => {
  value.value = ''
}
</script>

<template>
  <div>
    <MyInput v-model:value="value" />
    <div>{{ 'APP' + ':' + value }}</div>
    <button @click="clear">clear</button>
  </div>
</template>

<style scoped></style>

我们分析一下怎么实现。

首先父组件需要使用v-model双向绑定数据。而v-model的实质只是一种语法糖,vue会将其解码为绑定事件和v-bind的写法。来自vue官网的例子:

<input v-model="searchText" />
<!--等价于: -->
<input
  :value="searchText"
  @input="searchText = $event.target.value"
/>

因此,我们可以在子组件触发值的更改时做一个拦截,让其不直接修改值,而是传递一个事件,由父组件中的v-model展开后的事件处理帮我们更新这个值。

那么,我们怎么拦截子组件触发值的更改呢?

我们可以使用computed这个方法帮我们拦截到值的更改。一般来说计算属性是不能直接改的,但是``vue也提供了手动修改computed`值的一个方案。

来自官网的例子:

export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    }
  },
  computed: {
    fullName: {
      // getter
      get() {
        return this.firstName + ' ' + this.lastName
      },
      // setter
      set(newValue) {
        // 注意:我们这里使用的是解构赋值语法
        [this.firstName, this.lastName] = newValue.split(' ')
      }
    }
  }
}

可以看到,我们可以在computed中传递一个对象,为这个对象实现两个方法。一个是get,即读取计算属性时提供的值。另外一个是set,即企图修改计算属性值时做的一个拦截。说白了就是proxysetget用法。

这样看来,我们可以设置一个computed, 将其get方法返回的值设置为父组件传递过来的值。将其set方法设置为传递一个修改事件。这样的话,我们就实现了父组件和自定义子组件的数据双向绑定。

实现的MyInput组件如下:

<script lang="ts" setup>
import { computed } from 'vue'
defineOptions({
  name: 'my-input',
})
const props = withDefaults(
  defineProps<{
    value: string
  }>(),
  {
    value: '',
  }
)
const emit = defineEmits(['update:value'])
const handleInput = (e: Event) => {
  const newVal = (e.target as HTMLInputElement).value
  inputValue.value = newVal
}
const inputValue = computed({
  get() {
    return props.value
  },
  set(value: string) {
    emit('update:value', value)
  },
})
</script>

<template>
  <input type="text" :value="inputValue" @input="handleInput"/>
</template>

<style lang="scss" scoped></style>