6
votes

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).

5
You can't modify a prop in anyway, but you can setup a mirror data inside the component, and use that data to update UI while there're no v-model passed to it. - wxsm
@wxsm how should I mirror it? The problem I'm having is parent changes selection > child detects prop change and updates data > child emits event because data changed > parent detects event and updates selection and the cycle just repeats infinitely. - Flash
Read up on the sync modifier: vuejs.org/v2/guide/components.html#sync-Modifier - user320487
@btl I'm not really sure how that helps me, because in that example the state seems to live on the parent only. - Flash

5 Answers

8
votes

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.

3
votes

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.

2
votes

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

2
votes

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.

2
votes

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.