6
votes

Below is my current structure (which doesn't work).

Parent component:

<template>
<field-input ref="title" :field.sync="title" />
</template>

<script>
import Field from './input/Field'
export default {
  components: {
    'field-input': Field
  },
  data() {
    return {
      title: {
        value: '',
        warn: false
      }
    }
  }
}
</script>

Child component:

<template>
<div>
  <input type="text" v-model="field.value">
  <p v-bind:class="{ 'is-invisible' : !field.warn }">Some text</p>
</div>
</template>

<script>
export default {
  props: ['field']
}
</script>

The requirements are:

  • If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).
  • If the child's <input> is updated (field.value), then the parent's title.value should be updated.

What's the cleanest working solution to achieve this?

2
All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent’s state, which can make your app’s data flow harder to understand. [...] This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console. vuejs.org/v2/guide/components.html#One-Way-Data-Flow - connexo
For your first use case just pass title.warn as a prop to the child. For your second use case, pass a reference to a handler down to the child. On the child, emit the event that is triggering this handler. - connexo
You are basically home-brewing your own v-model and it is outlined how to do so in the docs. Bind prop and emit events. - Slava Knyazev
You code should meet the requirements, see codesandbox.io/s/421m2611p4 , but you say it does not work? maybe it's something else wrong. please checkout vue docs about sync, because you do not use it right vuejs.org/v2/guide/components.html#sync-Modifier - Paul Tsai

2 Answers

12
votes

Don't bind the child component's <input> to the parent's title.value (like <input type="text" v-model="field.value">). This is a known bad practice, capable of making your app's data flow much harder to understand.

The requirements are:

  • If parent's data title.warn value changes in parent, the child's class bind should be updated (field.warn).

This is simple, just create a warn prop and pass it from parent to child.

Parent (passing the prop to the child):

<field-input ref="title" :warn="title.warn" />

Child/template (using the prop -- reading, only):

<p v-bind:class="{ 'is-invisible' : !warn }">Some text</p>

Child/JavaScript (declaring the prop and its expected type):

export default {
  props: {warn: Boolean}
}

Notice that in the template it is !warn, not !title.warn. Also, you should declare warn as a Boolean prop because if you don't the parent may use a string (e.g. <field-input warn="false" />) which would yield unexpected results (!"false" is actually false, not true).

  • If the child's <input> is updated (field.value), then the parent's title.value should be updated.

You have a couple of possible options here (like using .sync in a prop), but I'd argue the cleanest solution in this case is to create a value prop and use v-model on the parent.

Parent (binding the prop using v-model):

<field-input ref="title" v-model="title.value" />

Child/template (using the prop as initial value and emitting input events when it changes):

<input type="text" :value="value" @input="$emit('input', $event.target.value)">

Child/JavaScript (declaring the prop and its expected type):

export default {
  props: {value: String}
}

Click here for a working DEMO of those two solutions together.

6
votes

There are several ways of achieving two-way data binding:

  1. Use props on components
  2. Use v-model attribute
  3. Use the sync modifier
  4. Use Vuex

For two-way bindings keep in mind that it can cause a chain of mutations that are difficult to maintain, quoted from the docs:

Unfortunately, true two-way binding can create maintenance issues, because child components can mutate the parent without the source of that mutation being obvious in both the parent and the child.

Here are some details to the methods that are available:

1.) Use props on components

Using props for two-way binding is not usually advised but possible, by passing an object or array you can change a property of that object and it will be observed in both child and parent without Vue printing a warning in the console.

Every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component

Props are easy to use and are the ideal way to solve most common problems.
Because of how Vue observes changes all properties need to be available on an object or they will not be reactive. If any properties are added after Vue has finished making them observable 'set' will have to be used.

 //Normal usage
 Vue.set(aVariable, 'aNewProp', 42);
 //This is how to use it in Nuxt
 this.$set(this.historyEntry, 'date', new Date());

The object will be reactive for both component and the parent:

I you pass an object/array as a prop, it's two-way syncing automatically - change data in the child, it is changed in the parent.

If you pass simple values (strings, numbers) via props, you have to explicitly use the .sync modifier

As quoted from --> https://stackoverflow.com/a/35723888/1087372

2.) Use v-model attribute

The v-model attribute is syntactic sugar that enables easy two-way binding between parent and child. It does the same thing as the sync modifier does only it uses a specific prop and a specific event for the binding

This:

 <input v-model="searchText">

is the same as this:

 <input
   v-bind:value="searchText"
   v-on:input="searchText = $event.target.value"
 >

Where the prop must be value and the event must be input

3.) Use the sync modifier

The sync modifier is also syntactic sugar and does the same as v-model, just that the prop and event names are set by whatever is being used.

In the parent it can be used as follows:

 <text-document v-bind:title.sync="doc.title"></text-document>

From the child an event can be emitted to notify the parent of any changes:

 this.$emit('update:title', newTitle)

4.) Use Vuex

Vuex is a data store that is accessible from every component. Changes can be subscribed to.

By using the Vuex store it is easier to see the flow of data mutations and they are explicitly defined. By using the vue developer tools it is easy to debug and rollback changes that were made.

This approach needs a bit more boilerplate, but if used throughout a project it becomes a much cleaner way to define how changes are made and from where.

See the getting started guide