Flash
Flash

Reputation: 16753

Modify props.value from within child component

I am new to Vue and trying to build a "dropdown" component. I want to use it from a parent component like this:

<my-dropdown v-model="selection"></my-dropdown>

where selection is stored as data on the parent, and should be updated to reflect the user's selection. To do this I believe my dropdown component needs a value prop and it needs to emit input events when the selection changes.

However, I also want to modify the value from within the child itself, because I want to be able to use the dropdown component on its own (I need to modify it because otherwise the UI will not update to reflect the newly selected value if the component is used on its own).

Is there a way I can bind with v-model as above, but also modify the value from within the child (it seems I can't, because value is a prop and the child can't modify its own props).

Upvotes: 8

Views: 23413

Answers (6)

Peter V. M&#248;rch
Peter V. M&#248;rch

Reputation: 15967

Echoing @zero298's answer this time for Vue3, the Vue3 Guide explicity calls out it being a best practice for components not to modify (the insides of) props:

Mutating Object / Array Props​

When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.

The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn't obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.

So even though it is possible, it is not best practice. $emit events with the new value instead, and let the parent modify it's own state.

Upvotes: 1

zolamk
zolamk

Reputation: 6417

You could use a custom form input component

Form Input Components using Custom Events

Basically your custom component should accept a value prop and emit input event when value changes

Upvotes: 2

zero298
zero298

Reputation: 26920

Vue's philosophy is: "props down, events up". It even says this in the documentation: Composing Components.

Components are meant to be used together, most commonly in parent-child relationships: component A may use component B in its own template. They inevitably need to communicate to one another: the parent may need to pass data down to the child, and the child may need to inform the parent of something that happened in the child. However, it is also very important to keep the parent and the child as decoupled as possible via a clearly-defined interface. This ensures each component’s code can be written and reasoned about in relative isolation, thus making them more maintainable and potentially easier to reuse.

In Vue, the parent-child component relationship can be summarized as props down, events up. The parent passes data down to the child via props, and the child sends messages to the parent via events. Let’s see how they work next.

Don't try to modify the value from within the child component. Tell the parent component about something and, if the parent cares, it can do something about it. Having the child component change things gives too much responsibility to the child.

Upvotes: 3

Brandon Deo
Brandon Deo

Reputation: 4305

You need to have a computed property proxy for a local value that handles the input/value values.

props: {
  value: {
    required: true,
  }
},
computed: {
  mySelection: {
    get() {
      return this.value;
    },
    set(v) {
      this.$emit('input', v)
    }
  }
}

Now you can set your template to use the mySelection value for managing your data inside this component and as it changes, the data is emitted correctly and is always in sync with the v-model (selected) when you use it in the parent.

Upvotes: 10

anon
anon

Reputation:

If you want to modify a prop inside a component, I recommend passing a "default value" prop to your component. Here is how I would do that

<MyDropdownComponent
  :default="defaultValue"
  :options="options"
  v-model="defaultValue"
/>

And then there are 2 options to how I would go from there -

Option 1 - custom dropdown element

As you're using custom HTML, you won't be able to set a selected attribute. So you'll need to be creative about your methods. Inside your component, you can set the default value prop to a data attribute on component creation. You may want to do this differently, and use a watcher instead. I've added that to the example below.

export default {
  ...
  data() {
    return {
      selected: '',
    };
  },
  created() {
    this.selected = this.default;
  },
  methods: {
    // This would be fired on change of your dropdown.
    // You'll have to pass the `option` as a param here,
    // So that you can send  that data back somehow
    myChangeMethod(option) {
      this.$emit('input', option);
    },
  },
  watch: {
    default() {
      this.selected = this.default;
    },
  },
};

The $emit will pass the data back to the parent component, which won't have been modified. You won't need to do this if you're using a standard select element.

Option 2 - Standard select

<template>
  <select
    v-if="options.length > 1"
    v-model="value"
    @change="myChangeMethod"
  >
    <option
      v-for="(option, index) of options"
      :key="option.name"
      :value="option"
      :selected="option === default"
    >{{ option.name }}</option>
  </select> 
</template>

<script>
export default {
  ...
  data() {
    return {
      value: '',
    };
  },
  methods: {
    // This would be fired on change of your dropdown
    myChangeMethod() {
      this.$emit('input', this.value);
    },
  },
};
</script>

This method is definitely the easiest, and means you need to use the default select element.

Upvotes: 3

Ferrybig
Ferrybig

Reputation: 18834

You could use the following pattern:

  1. Accept 1 input prop
  2. Have another variable inside your data
  3. On creation, white the incoming prop into your data variable
  4. Using a watcher, watch the incoming prop for changes
  5. On a change inside your component, send the change away

Demo:

'use strict';
Vue.component('prop-test', {
  template: "#prop-test-template",
  props: {
    value: { // value is the default prop used by v-model
      required: true,
      type: String,
    },
  },
  data() {
    return {
      dataObject: undefined,
      // For testing purposes:
      receiveData: true,
      sendData: true,
    };
  },
  created() {
    this.dataObject = this.value;
  },
  watch: {
    value() {
      // `If` is only here for testing purposes
      if(this.receiveData)
      this.dataObject = this.value;
    },
    dataObject() {
      // `If` is only here for testing purposes
      if(this.sendData)
      this.$emit('input', this.dataObject);
    },
  },
});

var app = new Vue({
  el: '#app',
  data: {
    test: 'c',
  },
});
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script type="text/x-template" id="prop-test-template">
  <fieldset>
    <select v-model="dataObject">
      <option value="a">a</option>
      <option value="b">b</option>
      <option value="c">c</option>
      <option value="d">d</option>
      <option value="e">e</option>
      <option value="f">f</option>
      <option value="g">g</option>
      <option value="h">h</option>
      <option value="i">i</option>
      <option value="j">j</option>
    </select>
    <!-- For testing purposed only: -->
    <br>
    <label>
      <input type="checkbox" v-model="receiveData">
      Receive updates
    </label>
    <br>
    <label>
      <input type="checkbox" v-model="sendData">
      Send updates
    </label>
    <!--/ For testing purposed only: -->
  </fieldset>
</script>
<div id="app">
  <prop-test v-model="test"></prop-test>
  <prop-test v-model="test"></prop-test>
  <prop-test v-model="test"></prop-test>
</div>

Notice that this demo has a feature that you can turn off the propagation of the events per select box, so you can test if the values are properly updated locally, this is of course not needed for production.

Upvotes: 2

Related Questions