eozzy
eozzy

Reputation: 68720

v-model inside a renderless component

CodeSandbox: https://codesandbox.io/s/61my3w7xrw?fontsize=14

I have this renderless component that uses a scoped slot:

name: "BlockElement",
  props: {
    element: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      inputValue: this.element.value
    };
  },
  render() {
    return this.$scopedSlots.default({
      inputName: this.inputName,
      inputValue: this.inputValue
    });
  }

Using it like so:

<block-element :element="element" v-slot="{ inputName, inputValue }">
  <div>
    <input type="text" :name="inputName" v-model="inputValue">
    <p>inputValue: {{ inputValue }}</p>
  </div>
</block-element>

... so the value is not updated on change. What am I doing wrong?

Upvotes: 0

Views: 502

Answers (1)

Decade Moon
Decade Moon

Reputation: 34306

In the following part of the template

<input type="text" :name="inputName" v-model="inputValue">

inputValue is the variable obtained from the v-slot and not the inputValue computed property on the <block-element> component; so if you assign to it (which is what v-model does) it won't be calling the setter, it's just setting the value of a local variable in the template code.

You could "fix" it like this:

<block-element :element="element" v-slot="{ inputName }" ref="block">
  <div>
    <input type="text" :name="inputName" v-model="$refs.block.inputValue">
    <p>inputValue: {{ $refs.block.inputValue }}</p>
  </div>
</block-element>

but this is just messy and breaks the abstraction you tried to create.

Another way is to have a inputValue setter property on the scope object that will correctly delegate the update to the component:

render() {
  const self = this;
  return this.$scopedSlots.default({
    inputName: this.inputName,
    get inputValue() { return self.inputValue },
    set inputValue(value) { self.inputValue = value; },
  });
}
<block-element :element="element" v-slot="scope">
  <div>
    <input type="text" :name="scope.inputName" v-model="scope.inputValue">
    <p>inputValue: {{ scope.inputValue }}</p>
  </div>
</block-element>

but this isn't ideal either because the scope object isn't typically writable, and this particular implementation detail would need to be documented.

In a situation like this where you want a scoped slot to pass data back to the parent component, you would implement this by passing a callback function to the slot. You can provide a function for setting inputValue but then you can't use v-model:

render() {
  return this.$scopedSlots.default({
    inputName: this.inputName,
    inputValue: this.inputValue,
    setInputValue: value => this.inputValue = value,
  });
}
<block-element :element="element" v-slot="{ inputName, inputValue, setInputValue }">
  <div>
    <input type="text" :name="inputName" :value="inputValue" @input="setInputValue($event.target.value)">
    <p>inputValue: {{ inputValue }}</p>
  </div>
</block-element>

Now there's no confusion about what to do.

Upvotes: 1

Related Questions