12

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?

jones1008
  • 157
  • 1
  • 15
Diolor
  • 13,181
  • 30
  • 111
  • 179
  • *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.* https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow – connexo Feb 25 '18 at 23:46
  • 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 Feb 25 '18 at 23:50
  • 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 Feb 26 '18 at 00:57
  • You code should meet the requirements, see https://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 https://vuejs.org/v2/guide/components.html#sync-Modifier – Paul Tsai Feb 26 '18 at 02:18

2 Answers2

17

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.

tony19
  • 125,647
  • 18
  • 229
  • 307
acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • Thanks. Regarding the first: so vue doesn't update props which have nested objects? Is this why you inline the property? Regarding second: why not use v-model in the child? Because we need the `$emit` event to the parent which v-model would not perform by default? – Diolor Feb 26 '18 at 14:41
  • Nested prop updates also work. Okay bottom line, my logic was also okay. Had few extra logic error. – Diolor Feb 26 '18 at 20:20
  • Hello, sorry for the delay (saw the comment on the phone, couldn't answer right away). So, yes, updating the values of *nested* properties of `prop` objects will work, but that is frowned upon, it's seen as a bad practice. The docs go on it here: https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow (bottom): *Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child will affect parent state.* It is believed this makes your application logic flow harder to understand in the long run. – acdcjunior Feb 26 '18 at 20:38
  • *why not use v-model in the child?* Because `v-model="value"` in the child would be the same as `:value="value" @input="value = $event.target.value">` which attempts to mutate the `value` `prop` -- if you try it, it show a warning *[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"*. – acdcjunior Feb 26 '18 at 20:47
  • We are `$emit`ting the event because we want to "forward" the child's ``'s `input` event to the parent element. When such event hits the parent, it updates the paren'ts `value`. But notice this updating is made in the parent (locally), not in the child. Which is a big difference. – acdcjunior Feb 26 '18 at 20:50
  • Two way binding of properties between parent and child components is somewhat clunky. I would suggest you use a shared state, this makes data delegation much easier. I forked your example and added a simple VUEX implementation. https://codesandbox.io/embed/7zk575o2yj – joronimo May 04 '19 at 12:02
16

There are several ways of doing it, and some are mentioned in other answers:

  1. Use props on components

  2. Use v-model attribute

  3. Use the sync modifier (for Vue 2.0)

  4. Use v-model arguments (for Vue 3.0)

  5. Use Pinia

Here are some details to the methods that are available:

1.) Use props on components

Props should ideally only be used to pass data down into a component and events should pass data back up. This is the way the system was intended. (Use either v-model or sync modifier as "shorthands")

Props and events are easy to use and are the ideal way to solve most common problems.

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.

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 (for Vue 2.0)

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 v-model arguments (for Vue 3.0)

In Vue 3.x the sync modifier was removed.

Instead you can use v-model arguments which solve the same problem

 <ChildComponent v-model:title="pageTitle" />

<!-- would be shorthand for: -->

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />

5.) Use Pinia (or Vuex)

As of now Pinia is the official recommended state manager/data store

Pinia is a store library for Vue, it allows you to share a state across components/pages.

By using the Pinia 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.

Take a look at their getting started section


**In case of legacy projects** :

If your project already uses Vuex, you can keep on using it.

Vuex 3 and 4 will still be maintained. However, it's unlikely to add new functionalities to it. Vuex and Pinia can be installed in the same project. If you're migrating existing Vuex app to Pinia, it might be a suitable option. However, if you're planning to start a new project, we highly recommend using Pinia instead.

SanBen
  • 2,581
  • 26
  • 35