Why does vue emit propagate to parent? - vue.js

I'm new to Vue. I have a child component like below. I added a validator via the 'emits' property and I assumed that if the validator fails then the parent event handler doesn't get called but it does. I also don't see a way to validate the input in the parent or in the child. I know I could add a method in the child component that I assign to the emit property and check it in the child and then only call $emit' if it returns true but it seems backwards. I'm sure I'm missing the point here, someone please clarify because it seems to me that the 'emits' property validator is only for debugging purposes and doesn't actually modify the app behavior, just throws a console warning. To me that is a bug waiting to happen. Is there some vue config setting that I need to change to enable the behavior I was expecting? What am I missing? (btw, when I run npm show vue version I get 3.2.45 if that would matter.
The console showing that the parent handler was called.
Child Component:
<script>
export default {
data() {
return {
username: "",
age: null,
};
},
emits: {
"new-user": function (obj) {
return obj && typeof obj.username == 'string' && typeof obj.age == 'number';
},
},
methods: {
newUser() {
const output = { username: this.username, age: this.age };
this.$emit("new-user", output);
},
},
};
</script>
The parent component:
<template>
<h2>Hello</h2>
<user-data #new-user="newUser"></user-data>
</template>
<script>
export default {
data() {
return {
};
},
methods: {
newUser(val) {
console.log('emitted newUser',val)
},
},
};
</script>

Passing a function to emits will not affect whether the event is emitted, it is only meant for validation. See docs here
though I see that it's not very clear from the docs:
export default {
emits: {
submit(payload) {
// return `true` or `false` to indicate
// validation pass / fail
}
}
}
To make the emit conditional you could use
export default {
data() {
return {
username: "",
age: null,
};
},
emits: ["new-user"],
methods: {
newUser() {
if (typeof this.username == 'string' && typeof this.age == 'number'){
const output = { username: this.username, age: this.age };
this.$emit("new-user", output);
}
},
},
};

You're spot on.
Emit validation only shows a console warning. It is not a mechanism to not emit something.
Similar to prop validation, its primary use is for development. Very useful for large teams or if you're making a component library.

Related

Vue3-My provide is not updated after changing the data using mounted

I tried to provide my data from a parent element like below.
data(){
return{
allData:null,
ingCollection:null,
selectedDish:[]
}
},
mounted(){
Promise.all([
d3.json('data.json'),
d3.json('ingredientsonly.json')
]).then((data)=>{
this.allData=data[0];
this.ingCollection=data[1];
})
},
components:{sidePanel,centerPiece},
methods: {
// receiveIngredients(selected){
// let selections = this.allData.filter(d=>{
// d.ingredients.includes(selected)
// });
// }
},
provide() {
return{
allData:this.allData,
ingCollection:this.ingCollection,
selectedDish:this.selectedDish,
receiveIngredients:this.receiveIngredients
}
}
However, after mounted lifecycle hook was run,
data is updated while provide elements were not updated.
Why is it?
Thank you
provide() is only called once at initialization, and not when there's a change to the references within.
Instead, you can provide an object (e.g., named root), and then update a property of that object in mounted():
export default {
provide() {
return {
root: {
allData: null,
ingCollection: null,
}
}
},
mounted() {
Promise.all([
d3.json('data.json'),
d3.json('ingredientsonly.json')
]).then((data)=>{
this.root.allData = data[0];
this.root.ingCollection = data[1];
})
}
}

Setting intial local data from Vuex store giving "do not mutate" error

I thought I understood the correct way to load inital state data from Vuex into the local data of a component, but why is this giving me “[vuex] do not mutate vuex store state outside mutation handlers.” errors! I am using a mutation handler!
I want my component data to start empty, unless coming back from a certain page (then it should pull some values from Vuex).
The component is using v-model=“selected” on a bunch of checkboxes. Then I have the following:
// Template
<grid-leaders
v-if="selected.regions.length"
v-model="selected"
/>
// Script
export default {
data() {
return {
selectedProxy: {
regions: [],
parties: [],
},
}
},
computed: {
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
this.selectedProxy = newVal
// If I remove this next line, it works fine.
this.$store.commit("SET_LEADER_REGIONS", newVal)
},
},
},
mounted() {
// Restore saved selections if coming back from a specific page
if (this.$store.state.referrer.name == "leaders-detail") {
this.selectedProxy = {...this.$store.state.leaderRegions }
}
}
}
// Store mutation
SET_LEADER_REGIONS(state, object) {
state.leaderRegions = object
}
OK I figured it out! The checkbox component (which I didn't write) was doing this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
if (index == -1) {
this.value.regions.push(region)
} else {
this.value.regions.splice(index, 1)
}
this.$emit("input", this.value)
},
The line this.value.regions.push(region) is the problem. You can't edit the this.value prop directly. I made it this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
let regions = [...this.value.regions]
if (index == -1) {
regions.push(region)
} else {
regions.splice(index, 1)
}
this.$emit("input", {
...this.value,
regions,
})
},
And then I needed this for my computed selected:
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
// Important to spread here to avoid Vuex mutation errors
this.selectedProxy = { ...newVal }
this.$store.commit("SET_LEADER_REGIONS", { ...newVal })
},
},
And it works great now!
I think the issue is that you can't edit a v-model value directly, and also you also have to be aware of passing references to objects, and so the object spread operator is a real help.

local storage data not being bound to data

i have a component, which calls a footer component. inside the footer component, i have a modal that shows on click. i want the title of that modal to be a data variable, and i want to set that data variable with data from local storage. for some reason, its not appearing.
I know that the data is in local storage. Any idea's?
export default {
name: "FooterComponent",
data: () => {
return {
privacy_modal: false,
privacy_title: '',
privacy_content: ''
};
},
methods: {
beforeMount() {
this.privacy_title = localStorage.getItem("privacy_title");
}
}
};
beforeMount is a lifecycle hook, not a method. It must be defined directly within options, not inside methods:
export default {
name: "FooterComponent",
data() {
return {
privacy_modal: false,
privacy_title: '',
privacy_content: ''
};
},
beforeMount() {
this.privacy_title = localStorage.getItem("privacy_title"); // || "some default value"
},
methods: {
// custom methods
}
};
Also, don't forget to update local storage, as demanded by your app logic.

How to compute a property based on an object with fallback

I have a component that receives an object as prop, like this:
props: ['propObject']
Then, there's a default object defined (I use VueX, so it's actually defined as a $store getter, but to make it simpler, let's say it's defined in the data method) in the data:
data() {
return {
dataObject: {defaultValueA: 1, defaultValueB: 2}
}
}
And I'd like to have a computed property that would behavior like this:
computed: {
computedObject() {
return Object.values(this.propObject).length > 0 ? this.propObject : this.dataObject;
}
}
However, I know this is not possible because Vue watchers don't watch for changes in the key/value pairs of an object.
I have tried to go with a watched property, like this:
props: ['propObject'],
data() {
return {
object: {},
defaultObject: {}
}
},
watch: {
propObject: {
handler: function() {
this.setComputedObject();
},
deep: true
}
},
methods: {
setComputedObject() {
this.object = Object.values(this.propObject).length > 0 ? this.propObject : this.defaultObject;
}
},
mounted() {
this.setComputedObject();
}
However, the watcher handler is not being called at all when the propObject changes, but if I call it directly via console, it works. Is there any way that I can make the computedObject become reactive?
you need to use Vue.set/vm.$set where you change the props (in source component)
for example
changeProp(){
this.$set(propObject,'newprop','newval');
}
and then just you regualr compouted in the target component (the component which receive the prop)
source : https://v2.vuejs.org/v2/guide/list.html#Object-Change-Detection-Caveats

How to set the whole data property of component to a new object keeping reactivity in Vue?

This must have been asked before, but I failed to find such question, so let me apologize in advance if there is one.
In my Vue component (written using vue-property-decorator in a class-based manner) I have a data property which is an object:
const emptyFilters = {
name: ''
};
export default class MyComponent extends Vue {
filtersFields = emptyFilters;
...
}
At some point I'd like to clean the filters. As for now there's only one filter, so I can do this:
this.filtersFields.name = '';
but to make this work with any number of filters, I'd like to do something like this:
this.filtersFields = emptyFilters;
The method above fails which is presumably because I set the property that is an observable to a new object hence the subscription of the model keeps observing the old object instead the new one. As far as I understand, Vue.set is what I need to use to set the whole this.filtersFields to a new object (emptyFilters). I tried:
this.$set(this, 'filtersFields', emptyFilters);
but this fails too (an input with v-model="filtersFields.name" is not cleared). What am I doing wrong? How to use Vue.set correctly? Is it only used only for adding a new property which was not set before? Do I need to use some other method?
PS As a solution based on answers below, I decided not to recreate the object from itself, but use a "factory" getter:
// instead of using a const emptyFilters we use a getter because otherwise
// emptyFilters becomes an observable and should be recreated each time anyway
const getEmptyFilters = (): examinationsFilters => ({
name: ''
});
...
// (initializing)
filtersFields: examinationsFilters = getEmptyFilters();
...
// (in method)
this.filtersFields = getEmptyFilters();
As Jesus Galvan correctly points out, this is very likely caused by your initialization code. Let's say you have the following template:
<template>
<div>
<input v-model="filtersFields.firstName" type="text" />
<input v-model="filtersFields.lastName" type="text" />
<button #click="onReset">reset</button>
</div>
</template>
Here's an example implementation that will not work:
const EMPTY_FILTERS = { firstName: '', lastName: '' };
export default {
data() {
return { filtersFields: EMPTY_FILTERS };
},
methods: {
onReset() {
this.filtersFields = EMPTY_FILTERS;
},
},
};
This fails because this.filtersFields = EMPTY_FILTERS actually does nothing. Object assignment in JS is done by reference, so filtersFields already points to EMPTY_FILTERS. Here's an example that does work:
const EMPTY_FILTERS = { firstName: '', lastName: '' };
export default {
data() {
return { filtersFields: { ...EMPTY_FILTERS } };
},
methods: {
onReset() {
this.filtersFields = { ...EMPTY_FILTERS };
},
},
};
This time we are always assigning a copy of EMPTY_FILTERS, which will correctly be observed by Vue.
I had a similar issue like this once. It is because (depending on how you are assigning the initial state) you are probably still observing the same object.
Try recreating the object that you want to assign on each assignment.
const emptyFilters = {
name: ''
};
export default class MyComponent extends Vue {
methods: {
setFilters () {
let filters = JSON.parse(JSON.stringify(emptyFilters));
this.$set(this, 'filtersFields', filters);
},
},
};