Vue .sync only works with v-model, but gives mutation error

Viewed 1171

// NOTE: Issue was due to VueFormulate's FormulaInput (custom input).

Check the code sandbox for 3 working examples of .sync



Usecase

My app is injecting multiple dynamic components into a view which then binds multiple inputs within each component to data in the parent.

Since v-model only works on a single value, I've found that .sync (added again after Vue 2.3) is the only way to two-way bind multiple inputs in each child component to my parent's data.

The Problem

I've followed the exact syntax in the Vue docs and many tutorials, but when I use :value="value in my child component it inputs undefined in my data with no errors in console.

If I use v-model, it works as expected, however produces a no-mutate-props error in the console for every single keystroke I press.

Expected Result

I expect two-way binding to work without producing any errors of no-mutate-props in the console.

I think I need some kind of watcher to check a value that references my prop, but that seems a bit messy, and I'd have to implement it for like 30 components... I'd would prefer something cleaner if possible.

Code Sandbox Example of issue

In Child

// input1
<input
  type="text"
  :value="value" <----- this will work if I make it a v-model, but produces mutation error in console
  @input="$emit('update:value', value)"
/>

// input2
<input
  type="text"
  :value2="value2" <----- again, will work with v-model only
  @input="$emit('update:value2', value2)"
/>

props: {
  value: {
    type: String
  },
  value2: {
    type: String
  }
}

In Parent

<component
  :is="step.component"
  :value.sync="step.value"
  :value2.sync="step.value2"
  :value3.sync="step.value3"
/>
2 Answers

The reason value didn't work is only because you are emitting the same unchanged value which is passed down. Without v-model, nothing changes value, so there was nothing new to emit back up.

Change that input to:

<input
  :value="value"
  @input="$emit('update:value', $event.target.value)"
  type="text"
  step="1"
  placeholder="Child Input1 (value)"
/>

This way, when the input event happens, you emit a new value from the input box.

For completeness, I'd like to add to Dan's answer a generic alternative: a Vue pattern that allows using v-model with anything that can't be directly mutated: computed getter + setter.

Proof of concept:

Vue.component('child', {
  template: `
  <input v-model="local" type="text" />
  `,
  props: ['value'],
  computed: {
    local: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit('update:value', value);
      }
    }
  }
})

new Vue({
  el: '#app',
  data: () => ({
    foo: {
      bar: 'baz'
    }
  })
})
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>

<div id="app">
  <div>
    <child :value.sync="foo.bar" />
  </div>
  <pre v-html="foo" />
</div>

I purposefully used a nested property, which is not normally reactive.

While using it in this particular example is really just a little more verbose (so it's probably less useful than Dan's proposed syntax) it can come in handy when used with Vuex state properties (get the store value in the getter and commit the mutation in the setter - especially since you can name the local computed the same as the state property).

Notably, it does not need an extra listener (which is a negligible performance increase) (e.g: @input, @change, @keydown, etc... - for completeness, in production code you might want to add in paste event listener and there might be other edge cases - autocomplete!? - although most cases are covered by @input, though).

The code in the setter gets run once every time there is a change in value for the v-model property, as you'd expect. In short, it's a proper two-way binding.

Related