1

我正在制作一个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 属性和事件?

4

1 回答 1

1

这个不容易……

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

好吧,你不能。

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

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

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

<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的类型对 a 使用不同的道具名称value并使用不同的事件) - 您可以将其留给 av-model

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

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

为了克服第一个问题,可以使用与普通道具相同的技术——而不是只传递一个值,而是传递一个对象并将该值作为其属性。现在您可以根据需要更改该属性的值(这被许多人认为是肮脏的,但我认为它非常强大,如果使用得当,可以为您节省大量样板代码)

第二个问题可以通过使用Object.defineProperty API来解决,它允许您执行类似于 Vuecomputed属性的操作 - 定义对象的属性并声明您自己的函数以在读取或写入该属性时使用。

这是一个最终的解决方案:

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

这当然不是理想的(尤其是您需要model.proxyv-modelvalue"/"input" 用于v-model自定义组件)

演示

于 2021-10-22T19:51:03.547 回答