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).
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.
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.
You could use the following pattern:
Accept 1 input prop
Have another variable inside your data
On creation, white the incoming prop into your data variable
Using a watcher, watch the incoming prop for changes
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/vue#2.0.1/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.
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.
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
Related
I'm relatively new to Vue and working on my first project. I'm working on creating a form with several children and grandchildren components. I ran into an issue where I will need the ability to generate multiple copies of a form. Therefore I moved some data properties up 1 level. Currently, the form is ApplicationPage.Vue > TheApplication.Vue > PersonalInformation.Vue > BaseInput.Vue. My issue is I need to emit changes from PersonalInformation to ApplicationPage passing through TheApplication. I'm having a hard time figuring out how to handle this situation. I keep finding solutions for Vue2 but not for Vue3.
ApplicationPage.vue
template
<TheApplication :petOptions="petOptions"
:stateOptions='stateOptions'
v-model="data.primary"
applicant="Primary"/>
script
data() {
return {
data: {
primary: {
personalInformation: {
first_name: "",
middle_name: "",
last_name: "",
date_of_birth: "",
phone: null,
email: "",
pets: "",
driver_license: null,
driver_license_state: "",
number_of_pets: null,
additional_comments: ""
},
},
},
}
},
TheApplication.Vue
<personal-information :petOptions="petOptions"
:stateOptions='stateOptions'
:personalInformation="modelValue.personalInformation"
#updateField="UpdateField"
/>
methods: {
UpdateField(field, value) {
this.$emit('update:modelValue', {...this.modelValue, [field]: value})
},
PersonalInformation.vue
<base-input :value="personalInformation.first_name"
#change="onInput('personalInformation.first_name', $event.target.value)"
label="First Name*"
type="text" class=""
required/>
methods: {
onInput(field, value) {
console.log(field + " " + value)
// this.$emit('updateField', { ...this.personalInformation, [field]: value })
this.$emit('updateField', field, value)
},
}
This is how I would do it: codesandbox
Emits only take two parameters, the name of the emit and the value being emitted. If emitting more than one value you have to emit the values as a single object.
In my solution, the grandchild component emits the field name and the value as a single object
Grandchild
<input
:value="personalInformation.first_name"
#input="onInput('first_name', $event.target.value)"
...
>
onInput(field, value) {
this.$emit("update-field", { field: field, value: value });
},
Which the child object catches and re-emits, but first takes care to emit in the format expected by the parent component (it expects the entire data.primary object since that is what was set as the v-model)
Child
<grandchild
:personalInformation="modelValue.personalInformation"
#updateField="UpdateField"
/>
UpdateField({ field, value }) {
const newVal = this.modelValue;
newVal.personalInformation[field] = value;
this.$emit("update:modelValue", newVal);
}
The parent component then automatically receives and updates the v-model data.primary object.
Alternatively, I have to mention that instead of dealing with any emitting / passing down objects between multiple levels of components, you can always use Pinia, the official state management library for Vue (save some state in one component, read same state from any other component). There's of course a learning curve but it's definitely worth learning and is made to simplify exactly this type of situation.
For anyone that doesn't want to chain event emits, there is the parent object on the child which can be used to emit an event as well. Be sure to register the emits in the parent to avoid warnings in the console.
Child
Call the immediate parent's $emit here.
Child.vue
<input #input="$parent.$emit('custom-event', e.target.value) />
Or using methods:
<input #input="handleInput" />
export default {
methods: {
handleInput(e) {
this.$parent.$emit('custom-event', e.target.value)
}
}
}
Parent
Since it's the parent that does the emitting to the ancestor, declare the emits here. For <script setup>, simply use the defineEmits() method to declare emits. See the docs.
Parent.vue
<Child /> <!-- No need to listen to the event here -->
export default {
emits: ['custom-event'] // Register the emits
}
If using <script setup>
<script setup>
defineEmits(['custom-event']) // Register the emits
</script>
Grandparent
Then listen to the event in the grandparent component.
GrandParent.vue
<Parent #custom-event="doSomething()" /> <!-- The event is being listened to in the grandparent component -->
I'm trying to create a two way binding between my parent (create user form) and a child component (reusable selectbox).
The parent component
<template>
<Selectbox :selectedOption="selectedRole" :options="roles" />
<span>SelectedRole: {{ selectedRole }}</span>
</template>
<script>
import Selectbox from '#/components/formElements/Selectbox.vue';
export default {
components: {
Selectbox,
},
async created() {
await this.$store.dispatch('roles/fetchRoles');
this.selectedRole = this.roles[0].value;
},
data() {
return {
selectedRole: null,
};
},
computed: {
roles() {
return this.$store.getters['roles/roles'].map((role) => ({
value: role.id.toString(),
label: role.name,
}));
},
},
};
</script>
I'm passing down the roles as options and the selectedRole variable as selectedOption.
The child component
<template>
<select :value="selectedOption" #input="(event) => $emit('update:selectedOption', event.target.value)">
<option v-for="option in options" :value="option.value" :key="option.value">{{ option.label }}</option>
</select>
</template>
<script>
export default {
props: {
options: {
type: Array,
required: true,
},
selectedOption: {
type: String,
required: false,
},
},
};
</script>
The selectedOption is assigned to the value together. When another value is selected I want to update the passed down value in the parent component. Therefore I'm using an $emit function but that's not working right now.
I also tried to use v-model to combine the value and change attributes but without success.
<select v-model="selectedOption">
What's the correct way?
Code: Codesandbox
I guess this is the handling you want to achieve: https://codesandbox.io/s/practical-orla-i8n3t?file=/src/components/Selectbox.vue
If you use v-model on a sub-component, you have to handle it properly in the sub-component.
<custom-select v-model="value" />
<!-- IS THE SAME AS -->
<custom-select
:modelValue="value"
#update:modelValue="value = $event"
/>
So if you use v-model, a property with the name modelValue gets passed down to the sub-component. If the modelValue changes (which means another option in the select list gets selected) you have to emit a change event, indicating that the modelValue got changed: $emit('update:modelValue'). v-model automatically updates it's value if this event occurs.
Source: https://learnvue.co/2021/01/everything-you-need-to-know-about-vue-v-model/
The use of v-model creates a two-way bind between the view and data model WITHIN a component. And a view interaction can emit an event to a parent component.
It is possible, though, to have a data model change in a child component emit an event to a parent component?
Because as long as the user is the one clicking the checkbox, things are fine in both the parent and the child (the data model updates AND the event is emitted). But if I pull up the Vue dev tools and toggle the checkbox within the child component's data, the two-way bind from the v-model will make the appropriate updates within the CHILD component, but nothing ever makes it over to the parent component (I suspect because an event is not emitted).
How can I make sure the parent component "knows" about the data change in the child component? I assume this must be possible in some way... If not emitting from the child, perhaps there's some way to have the parent component "watch" the child component's data?
Thank you for any help or guidance! I'll keep reading and looking for an answer in the meantime!
child component
<template>
<div class="list-item" v-on:click="doSomething">
<input type="checkbox" v-model="checked">
<label v-bind:class="{ checked: checked }">{{ name }}</label>
</div>
...
</template>
<script>
...
data: function() {
return {
checked: false,
}
},
methods: {
doSomething() {
...
this.$emit('doSomething', this)
}
}
</script>
parent component
<template>
<ChildComponent v-on:doSomething="getItDone"></ChildComponent>
...
</template>
<script>
...
methods: {
getItDone(target) {
...
}
}
</script>
UPDATE: After playing around a bit more with #IVO GELOV's solution, the issue I'm running into now is that, when multiple Child components are involved, since the Parent's one singular value of myBooleanVar drives the whole thing, checking the box of one child component causes all child components to be checked.
So it's definitely progress in that both view and data manipulations make it over to the parent, but I'm still trying to figure out how to "isolate" the situation so that just the one Child component that was acted upon gets dragged into the party...
You can keep the data in the parent, provide it to the child as a prop, make the child watch the prop and update its internal state, and finally emit an event to the parent when the internal state of the child has been changed.
parent
<template>
<ChildComponent v-model="myBooleanVar" />
...
</template>
<script>
data()
{
return {
myBooleanVar: false,
}
},
watch:
{
myBooleanVar(newValue, oldValue)
{
if (newValue !== oldValue) this.getItDone(newValue);
}
},
methods:
{
getItDone(value)
{
...
}
}
</script>
child
<template>
<div class="list-item">
<input type="checkbox" v-model="checked">
<label :class="{ checked: checked }">{{ name }}</label>
</div>
...
</template>
<script>
props:
{
value:
{
type: Boolean,
default: false
}
}
data()
{
return {
checked: this.value,
}
},
watch:
{
value(newVal, oldVal)
{
// this check is mandatory to prevent endless cycle
if(newVal !== oldVal) this.checked = newVal;
},
checked(newVal, oldVal)
{
// this check is mandatory to prevent endless cycle
if(newVal !== oldVal) this.$emit('input', newVal);
}
},
</script>
Maybe you can directly bind the checkbox attribut to the parent attribut using v-bind="$attrs"
Take a look at this answer: https://stackoverflow.com/a/56226236/10514369
I have select displayed in v-for loop:
<div v-for="(n, key) in selectedLanguages">
<select class="input input__col"
v-model="currentLang[key]"
#change="changeLanguage(currentLang[key], key)"
id="lang_select">
<option value="pl">Polski</option>
<option value="en">Angielski</option>
<option value="es">Hiszpański</option>
</select>
</div>
To each select I'm adding changeLanguage method which is:
<script>
export default {
data() {
return {
currentLang: []
}
},
methods: {
changeLanguage(value, key) {
let data = { value, key };
this.$nuxt.$emit('change::language', data);
}
},
props: ['selectedLanguages']
}
</script>
and it is in child component. In parent I'm listening for this change::language event:
this.$nuxt.$on('change::language', res => {
console.log(res);
this.selectedLanguages[res.key] = res.value;
console.log(this.selectedLanguages);
Although it's working correctly and it's updating selectedLanguages array just fine it doesn't rerender interpolation {{ selectedLanguages }} in parent. However it's correctly rerendering interpolation {{ selectedLanguages }} in child where it's passed by props. Why?
It seems like vue doesn't "catch" that selectedLanguages array have been changed. It only see when I .push or .pop this array. Is there something like apply method in vue?
I found this link in documentation: https://v2.vuejs.org/v2/guide/list.html#Caveats and added this.$set(this.selectedLanguages, res.value, res.key); in parent below my assignment but it didn't fix.
I found solution in Vue docs:
Due to limitations in JavaScript, Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
I was doing exacly like above.
The solution is instead
this.selectedLanguages[res.key] = res.value;
use
this.$set(this.selectedLanguages, res.key, res.value);
which is basically a bit weird but it works.
I understand the .sync modifier returned in Vue 2.3, and am using it for a simple child component which implements a 'multiple-choice' question and answer. The parent component calls the child like this:
<question
:stem="What is your favourite colour?"
:options="['Blue', 'No, wait, aaaaargh!']
:answer.sync="userChoice"
>
The parent has a string data element userChoice to store the result from the child component. The child presents the question and radio buttons for the options. The essential bits of the child look like this (I'm using Quasar, hence q-radio):
<template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<label >
<q-radio v-model="option" :val="opt.val" #input="handleInput"></q-radio>
{{opt.text}}
</label>
</div>
</div>
</template>
export default {
props: {
stem: String,
options: Array,
answer: String
},
data: () => ({
option: null
}),
methods: {
handleInput () {
this.$emit('update:answer', this.option)
}
}
}
This is all working fine, apart from the fact that if the parent then changes the value of userChoice due to something else happening in the app, the child doesn't update the radio buttons. I had to include this watch in the child:
watch: {
answer () {
this.option = this.answer
}
}
But it feels a little redundant, and I was worried that emitting the event to update the parent's data would in fact cause the child 'watch' event to also fire. In this case it would have no effect other than wasting a few cycles, but if it was logging or counting anything, that would be a false positive...
Maybe that is the correct solution for true 2-way binding (i.e. dynamic Parent → Child, as well as Child → Parent). Did I miss something about how to connect the 'in' and 'out' data on both sides?
In case you're wondering, the most common case of the parent wanting to change 'userChoice' would be in response to a 'Clear Answers' button which would set userChoice back to an empty string. That should have the effect of 'unsetting' all the radio buttons.
Your construction had some oddities that didn't work, but basically answer.sync works if you propagate it down to the q-radio component where the changing happens. Changing the answer in the parent is handled properly, but to clear values, it seems you need to set it to an object rather than null (I think this is because it needs to be assignable).
Update
Your setup of options is a notable thing that didn't work.
I use answer in the q-radio to control its checked state (v-model has special behavior in a radio, which is why I use value in conjunction with v-model). From your comment, it looks like q-radio wants to have a value it can set. You ought to be able to do that with a computed based on answer, which you would use instead of your option data item: the get returns answer, and the set does the emit. I have updated my snippet to use the val prop for q-radio plus the computed I describe. The proxyAnswer emits an update event, which is what the .sync modifier wants. I also implemented q-radio using a proxy computed, but that's just to get the behavior that should already be baked-into your q-radio.
(What I describe is effectively what you're doing with a data item and a watcher, but a computed is a nicer way to encapsulate that).
new Vue({
el: '#app',
data: {
userChoice: null,
options: ['Blue', 'No, wait, aaaaargh!'].map(v => ({
value: v,
text: v
}))
},
components: {
question: {
props: {
stem: String,
options: Array,
answer: String
},
computed: {
proxyAnswer: {
get() {
return this.answer;
},
set(newValue) {
this.$emit('update:answer', newValue);
}
}
},
components: {
qRadio: {
props: ['value', 'val'],
computed: {
proxyValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue);
}
}
}
}
}
}
},
methods: {
clearSelection() {
this.userChoice = {};
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.3.3/vue.min.js"></script>
<div id="app">
<question stem="What is your favourite colour?" :options="options" :answer.sync="userChoice" inline-template>
<div>
<h5>{{stem}}</h5>
<div class="option" v-for="opt in options">
<div>Answer={{answer && answer.text}}, option={{opt.text}}</div>
<label>
<q-radio :val="opt" v-model="proxyAnswer" inline-template>
<input type="radio" :value="val" v-model="proxyValue">
</q-radio>
{{opt.text}}
</label>
</div>
</div>
</question>
<button #click="clearSelection">Clear</button>
</div>