VueJs 3 emit event from grand child to his grand parent component - vue.js

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

Related

Vue.js - How to emit to parent component on change in data object?

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

Vue how to get value from sub component

Here is my use case:
My main page have several sub-components that collect different input from user, finally I want to submit the whole page with all inputs collected. Therefore I want to retrieve the data from sub-component
One option is to use store, but my sub-components are super simple, just some forms, store seems too heavy...
Another option is that I can modify prop, although I know this is bad practice, but this approach looks just perfect....
Is it ok to modify prop if my logic is simple?(just collect inputs from user)Or I have to go for Vuex and store
Expanding on excellent answers from Ifaruki and Andres Foronda, another, related option is the use of the sync modifier on the child component's prop.
Suppose the child component has a prop named name. In the parent component, you can use the sync modifier like this:
<Child :name.sync="childName"></Child>
Then, in the child component, when the value of the name prop should be updated, don't update it directly. Instead, emit an event that follows the naming convention for sync-able props, which is update:nameOfProp. So in our example, the child component would do this:
this.$emit('update:name', newName);
The benefit of the sync modifier is that we don't have to write an event handler function in the parent component--Vue does that for us and updates the variable that is bound to the prop automatically.
You can read more details about the sync modifier in the official docs.
Retreiving data from sub component works with $emit here an exapmle:
//parent copmonent
<template>
<div>
<child #someEvent="someMethod"></child>
</div>
</template>
import child from "path/"
<script>
export default {
components: {
child
},
methods: {
someMethod(data){
console.log(data);
}
}
}
</script>
Child component
<template>
<div>
<button #click="sendEvent">send</button>
</div>
</template>
<script>
export default {
methods: {
sendEvent(){
this.$emit("someEvent", "working");
}
}
}
</script>
$emit takes 2 arguments. The first is the event name and the second one is the data that you send.
The parent just needs to listen with # for that event that being fired.
you can listen an event from child an update the parent data property
//parent component
<div>
<input-name #updateName="eventToUpdateName" /> <!--child component-->
</div>
...
data: () => ({ nameFromChild: '' )},
methods: {
eventToUpdateName(value) {
this.nameFromChild = value; // Update from child value emitted
}
}
...
And in the child component
// Child component
<input v-model="name" />
...
data: () => ({ name: '' }),
// watch for changes in the name property and emit an event, and pass the value to the parent
watch: { name() { this.$emit('updateName', this.name } }
...
Also, You can use a v-model directive and emit 'input' event from child.
//parent component
<div>
<input-name v-model="nameFromChild" /> <!--child component-->
</div>
...
data: () => ({ nameFromChild: '' )}
...
Now in the child component you can have
// Child component
<div>
<input v-model="name" />
</div>
data: () => ({ name: '' }),
props: { value: { type: String, default: '' },
created() { this.name = this.value }, // You can receive a default value
watch: { name() { this.$emit('input', this.name } }
...

Vuejs editing prop object property in child component

I have been using Vue for a while but still do not have a clear understanding of the benefits related to this (probably controversial) question. I know it has been asked in different ways many times, but I can't find a clear answer.
As "Example 1", let's say I have a parent component that contains an address object, and it passes that address to a child AddressForm so it can be edited by the user, like this:
// Parent.vue
<template>
<AddressForm :address="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
Then, the child component looks like this, which directly manipulates the properties of the parent address object.
Here I am only showing an input for the address.name to keep things concise, but you can imagine there is a similar text input for any other properties of the address.
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="address.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
address: {
type: Object,
required: true,
}
},
methods: {
this.$emit('submit');
}
}
</script>
The issues here is that I am directly editing the address prop in the child. Of course I do not get any warnings from Vue about it because I am only editing a property of the address object, not actually mutating it by reassigning.
There are countless places where I can read about why this is not best practice, it's an anti-pattern, and it's a bad idea. You shouldn't alter props in child components.
Ok, but why? Can someone provide any type of real world use-case where the code above can lead to issues down the road?
When I read about the "proper" alternative ways to do things, I am even more confused because I don't see any real difference. Here's what I mean:
Let's call this "Example 2", where I now use v-model to enforce "proper" 2-way binding rather than editing the prop directly like I was in Example 1:
// Parent.vue
<template>
<AddressForm v-model="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="localValue.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true,
}
},
computed: {
localValue: {
get: function() {
return this.value;
}
set: function(value) {
this.$emit('input', value);
}
}
},
methods: {
this.$emit('submit');
}
}
</script>
What is the difference between Example 1 and Example 2? Aren't they doing, literally, exactly the same thing?
In Example 2, the computed property says that localValue is always equal to value. Meaning, they are the same exact object, just like they were in Example 1.
Further, as you type in the input, if you watch the events being fired in the Vue debugger, AddressForm never even emits the input events, presumably because localValue isn't actually being set, since it's just an object property that's being changed.
This again shows that Example 1 and Example 2 are doing the exact same thing. The object property is still being directly mutated within the child even though v-model is being used.
So, again, my question is, why is Example 1 considered bad practice? And how is Example 2 any different?
Referring to the docs, manipulating a prop directly causes several issues:
Makes app's data flow harder to understand
Every time a parent component is updated, all props in the child component will be refreshed

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

2-way binding in Vue 2.3 component

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>