如何使用 v-bind 和 v-on 绑定 Vue 中的自定义命名模型?

How can I use v-bind and v-on to bind on custom named model in Vue?

我正在制作一个 InputWrapper 组件,用于装饰一些 BootstrapVue 输入组件。重点是围绕给定输入自动处理验证状态、消息、样式等(未在下面的示例中显示)。

我想动态“转发”v-model。当包装组件使用自定义模型属性和更新事件进行双向绑定时,问题就来了。

主要思路如下。

InputWrapper.Vue

    <template>
      <div>
        <slot v-bind="wrappedBindings"></slot>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        value: {required: true}
      },

      methods: {
        onInput($event){
          this.$emit('input', $event);
        }
      },
      
      computed: {
        wrappedBindings(){
          return {
            attrs: {
              value: this.value
            },
            on: {
              input: $event => this.onInput($event),
              'update:value': $event => this.onInput($event)
            }
          }
        }
      }
    }
    </script>

用法

    <div>
      <input-wrapper v-model="selectModel" v-slot="{attrs, on}">
        <!-- v-model of b-form-select is :value and @input so this works -->
        <b-form-select v-bind="attrs" v-on="on" ...other stuff...></b-form-select>
      </input-wrapper>

      <input-wrapper v-model="inputModel" v-slot="{attrs, on}">
        <!-- v-model of b-form-input is :value and @update (not @update:value or @input) so this does not work -->
        <b-form-input v-bind="attrs" v-on="on" ...other stuff...></b-form-input>
      </input-wrapper>

      <input-wrapper v-model="checkModel" v-slot="{attrs, on}">
        <!-- v-model of b-form-checkbox is :checked (not :value) and @input so this does not work -->
        <b-form-checkbox v-bind="attrs" v-on="on" ...other stuff...></b-form-checkbox>
      </input-wrapper>
    </div>

我目前不满意的方案

    <div>
      <input-wrapper v-model="inputModel" v-slot="{attrs, on}">
        <b-form-input v-bind="attrs" v-on="on" @update="on.input" 
          ...other stuff...></b-form-input>
      </input-wrapper>

      <input-wrapper v-model="checkModel" v-slot="{attrs, on}">
        <b-form-checkbox v-bind="attrs" v-on="on" :checked="attrs.value"
         ...other stuff...></b-form-checkbox>
      </input-wrapper>
    </div>

这个解决方案让我可以做我想做的事,但它的实施时间更长,而且您总是需要附近的 BootstrapVue 文档。

另一种解决方案是为每个 BsVue 输入制作一个自定义组件,但我还需要将每个属性和事件转发到自定义组件。不这样做的原因有很多,但主要是它会更难维护。

我的问题如下:如何在事先不知道的情况下使用 v-bind="attrs" 和 v-on="on" 动态绑定任何自定义 v-model 属性和事件?

这个不容易...

How can I use v-bind="attrs" and v-on="on" to dynamically bind any custom v-model attribute and event without knowing them beforehand?

你不能。

我的第一个想法是以某种方式达到 Vue 2 中使用的 model option 来自定义此组件上 v-model 的道具名称和事件名称。但不幸的是,这在 $scopedSlots.default() 上无法访问(并且以这种方式使用它无论如何都是非常无效的)

恕我直言,最好的选择是在开槽组件上使用 v-model,让 Vue 为您完成繁重的工作...但是

..通常当您创建 input(或一些自定义输入)包装器时,最简单的做法是使用 computed 来“连接”(或转发)内部组件的 v-model 到包装器的 v-model

<template>
  <input :type="type" v-model="model" />
</template>
<script>
export default {
  props: ['value', 'type'],
  computed:{
    model: {
      get() { return this.value }
      set(newValue) { this.$emit('input', newValue) } 
    }
  }
}
</script>

为什么?出于完全相同的原因,你问你的问题。您不需要选择使用哪个道具和事件(因为不同的 input 类型对 value 使用不同的道具名称并使用不同的事件) - 您可以将它留给 v-model

但是在开槽组件上做同样的事情有点棘手。您不能将 v-model 直接放在 <slot> 上。它必须放置在开槽组件上(在父模板中)。所以唯一的方法是将像上面计算的“东西”传递到插槽道具中。那也是问题。

  1. slot props 不能被 slot 内容改变,就像普通 props 不应该从组件内部改变一样
  2. 不可能 v-bind 作为一个整体 计算到插槽中。如果你尝试这样做,开槽组件只接收从 getter 读取的当前值,而 setter 被留在后面......

为了克服第一个问题,可以使用与普通道具相同的技术 - 而不是只传递一个值,而是传递一个对象并将该值设为 属性。现在你可以随心所欲地改变 属性 的值(许多人认为这很脏,但我认为它非常强大,如果使用得当,可以节省大量样板代码)

第二个问题可以通过使用 Object.defineProperty API 来解决,它允许你做一些类似于 Vue computed 属性的事情——定义一个对象的 属性 并声明你自己的函数是当读取或写入 属性 时使用..

这是最终的解决方案:

// InputWrapper.vue
<template>
  <div>
    <slot v-bind="wrappedBindings"></slot>
  </div>
</template>

<script>
export default {
  props: {
    value: { required: true },
  },

  computed: {
    wrappedBindings() {
      const self = this;
      const bindings = {
        attrs: {
        },
        on: {
        },
        model: {},
      };

      // property MUST be on 2nd level otherwise it wont work thanks to how Vue 2 is implemented
      // https://github.com/vuejs/vue/blob/0948d999f2fddf9f90991956493f976273c5da1f/src/core/instance/render-helpers/render-slot.js#L24
      // https://github.com/vuejs/vue/blob/0948d999f2fddf9f90991956493f976273c5da1f/src/shared/util.js#L205
      Object.defineProperty(bindings.model, "proxy", {
        get() {
          return self.value;
        },
        set(newValue) {
          self.$emit("input", newValue);
        },
        enumerable: true,
        configurable: false,
      });

      return bindings;
    },
  },
};
</script>

以及用法:

<input-wrapper v-model="inputModel" v-slot="{ attrs, on, model }">
  <b-form-input
    v-bind="attrs"
    v-on="on"
    v-model="model.proxy"
  ></b-form-input>
</input-wrapper>

这当然不理想(尤其是您需要在 v-model 中使用 model.proxy 的事实)但现在我看不到任何其他方式如何实现这种“通用”包装器(当然除了选择一个组件库,该组件库坚持自定义组件上的 v-model 的“值”/“输入”)

Demo