Vue js computed properties evaluation order - vue.js

I have a component which uses 'data1' props.
<template>
<div>
<component1 :data='data1'><component1>
</div>
<template>
This data1 is a computed property which needs another computed data for calculating one of its values:
computed: {
componentInfo: function() {
return this.$store.state.componentData;
}
data1: function() {
return {value1: this.componentInfo.value1, ... other values}
}
}
My problem is that the component tries to evaluate data1 value before getting the componentInfo from the store (which causes an error since this.componentInfo is still undefined)
How should such a scenario be treated?

It is very easy to handle. Just add an extra if to computed property:
data1 () {
if (this.componentInfo) { // check if it exists
return { value1: this.componentInfo.value1, ... other values }
} else {
return {} // some default value
}
}

Related

How mutate data in method computed?

I'm on Vue 3. I have an onclick method which is supposed to modify the value of my props which is a boolean, I have tried several ways, I manage to enter the computed method, but the value of my props does not change
I register my data
data() {
return {
showConsommationWindow: false
}
}
then I tried 3 ways to change the value but none of them worked.
The first :
<submit-button v-on:click="showConsommationWindow = true" />
the 2nd : (alert is executed but the data value don't change)
<submit-button v-on:click="showConsommation(true)"/>
methods: {
showConsommation(boolValue){
alert('false')
this.showConsommationWindow = boolValue;
}
}
The last :
<submit-button v-on:click="showConsommation"/>
methods: {
showConsommation(){
if (!this.showConsommationWindow) {
alert('false')
this.showConsommationWindow = true;
return
}
this.showConsommationWindow = false;
}
},
I really don't understand why my data can't mutate, thanks for your help.
If value comes from a props, it means the parent distributes a boolean to the component. So if you want to change the boolean value, you should probably do:
// in parent
<Component :booleanValue="myBoolean" #changeBooleanValueEvent="changeMyBoolean" />
...
data() {
return {
myBoolean: true
}
}
methods: {
changeMyBoolean(value) {
this.myBoolean = value
}
}
// in component
props: {
booleanValue: {
...
}
}
methods: {
showConsommation() {
this.$emit('changeBooleanValueEvent', false)
}
}

Move Vue form input validation in component into a method

I have a Vue componenet for my input field. I have added some validation that makes sure only numbers are added. I added this on the oninput.
I'd like to move this to a method so I can add more checks (eg. if Type !== number)
This works well, but with the validation inline:
<input
v-bind="$attrs"
v-on="{
...$listeners,
input: event => $emit('input', event.target.value)
}"
oninput="this.value = Math.abs(this.value)"
/>
This is how I would like it (but current the validation is not working):
<input
v-bind="$attrs"
v-on="{
...$listeners,
input: event => handleInput(event.target.value)
}"
/>
methods: {
handleInput(value) {
console.log(value);
// 1st emit
this.$emit("input", value);
// 2nd Validate -- Not working...
this.value = Math.abs(this.value);
}
}
Any ideas on how I get this.value = Math.abs(this.value); to feed back into the input?
UPDATE
Thanks to a helpful comment I made some progress. The below code works for the first character but not for ongoing characters.
If numbers are typed, then validation passes true and input emitted.
If 1 character (eg. a) is typed then we emit the number 0. If a second character is inputted then the char is emitted (eg. press b and now the input is 0b)
I can see the this.$emit("input", 0) is triggered, so not sure why char emitted.
methods: {
validateInput(value) {
// if it type isnt set as a number then leave
if (this.type != "number") {
return true;
}
// check if value a number
if (Math.abs(value)) {
return true;
}
return false;
},
handleInput(value) {
if (this.validateInput(value)) {
this.$emit("input", value);
} else {
this.$emit("input", 0);
}
}
}
If you want to check a value before emitting the input event, you could do it like this:
methods: {
validateInput(value) {
if (typeof value !== 'number') { return false; } // check if it's not a string
if (value !== Math.abs(value)) { return false; } // check if value is positive
return true
}
handleInput(value) {
if (this.validateInput(value)) { this.$emit("input", value); }
this.$emit("input") // if value is not a valid input, you may want to do nothing, or emit merely that the event happened.
}
}
A better way of doing a custom input would be to use the value prop of an input, and bind it to a dynamic property in your component, for example by using v-model="value". Fun fact: v-model has a modifier v-model.number which would do exactly what you need.
The only caveat is that you can't directly modify props, so you'd need to use a computed property as a way to automatically handle the 'getting and setting' of your form's value.
// CustomInput.vue
<template>
<input v-bind="$attrs" v-on="$listeners" v-model.number="localValue" />
</template>
<script>
export default {
props: {
value: {
type: Number,
required: true,
}
}
computed: {
localValue: {
get() { return this.value; }
set(newVal) { this.$emit('input', newVal); }
}
}
}
</script>
You don't need to make a custom component for this case. You could simply use v-model.number in the parent and it would work. Once your inputs get more complex, you want to modify the set method a bit to set(newVal) { if (this.validateInput(newVal)) {this.$emit('input', newVal);} }, defining your own 'validateInput' method.
If you find you're writing a lot of different validations for different use cases, look into libraries like Vuelidate and VeeValidate

Using $refs in a computed property

How do I access $refs inside computed? It's always undefined the first time the computed property is run.
Going to answer my own question here, I couldn't find a satisfactory answer anywhere else. Sometimes you just need access to a dom element to make some calculations. Hopefully this is helpful to others.
I had to trick Vue to update the computed property once the component was mounted.
Vue.component('my-component', {
data(){
return {
isMounted: false
}
},
computed:{
property(){
if(!this.isMounted)
return;
// this.$refs is available
}
},
mounted(){
this.isMounted = true;
}
})
I think it is important to quote the Vue js guide:
$refs are only populated after the component has been rendered, and they are not reactive. It is only meant as an escape hatch for direct child manipulation - you should avoid accessing $refs from within templates or computed properties.
It is therefore not something you're supposed to do, although you can always hack your way around it.
If you need the $refs after an v-if you could use the updated() hook.
<div v-if="myProp"></div>
updated() {
if (!this.myProp) return;
/// this.$refs is available
},
I just came with this same problem and realized that this is the type of situation that computed properties will not work.
According to the current documentation (https://v2.vuejs.org/v2/guide/computed.html):
"[...]Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that computed properties are cached based on their reactive dependencies. A computed property will only re-evaluate when some of its reactive dependencies have changed"
So, what (probably) happen in these situations is that finishing the mounted lifecycle of the component and setting the refs doesn't count as a reactive change on the dependencies of the computed property.
For example, in my case I have a button that need to be disabled when there is no selected row in my ref table.
So, this code will not work:
<button :disabled="!anySelected">Test</button>
computed: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
What you can do is replace the computed property to a method, and that should work properly:
<button :disabled="!anySelected()">Test</button>
methods: {
anySelected () {
if (!this.$refs.table) return false
return this.$refs.table.selected.length > 0
}
}
For others users like me that need just pass some data to prop, I used data instead of computed
Vue.component('my-component', {
data(){
return {
myProp: null
}
},
mounted(){
this.myProp= 'hello'
//$refs is available
// this.myProp is reactive, bind will work to property
}
})
Use property binding if you want. :disabled prop is reactive in this case
<button :disabled="$refs.email ? $refs.email.$v.$invalid : true">Login</button>
But to check two fields i found no other way as dummy method:
<button :disabled="$refs.password ? checkIsValid($refs.email.$v.$invalid, $refs.password.$v.$invalid) : true">
{{data.submitButton.value}}
</button>
methods: {
checkIsValid(email, password) {
return email || password;
}
}
I was in a similar situation and I fixed it with:
data: () => {
return {
foo: null,
}, // data
And then you watch the variable:
watch: {
foo: function() {
if(this.$refs)
this.myVideo = this.$refs.webcam.$el;
return null;
},
} // watch
Notice the if that evaluates the existence of this.$refs and when it changes you get your data.
What I did is to store the references into a data property. Then, I populate this data attribute in mounted event.
data() {
return {
childComps: [] // reference to child comps
}
},
methods: {
// method to populate the data array
getChildComponent() {
var listComps = [];
if (this.$refs && this.$refs.childComps) {
this.$refs.childComps.forEach(comp => {
listComps.push(comp);
});
}
return this.childComps = listComps;
}
},
mounted() {
// Populates only when it is mounted
this.getChildComponent();
},
computed: {
propBasedOnComps() {
var total = 0;
// reference not to $refs but to data childComps array
this.childComps.forEach(comp => {
total += comp.compPropOrMethod;
});
return total;
}
}
Another approach is to avoid $refs completely and just subscribe to events from the child component.
It requires an explicit setter in the child component, but it is reactive and not dependent on mount timing.
Parent component:
<script>
{
data() {
return {
childFoo: null,
}
}
}
</script>
<template>
<div>
<Child #foo="childFoo = $event" />
<!-- reacts to the child foo property -->
{{ childFoo }}
</div>
</template>
Child component:
{
data() {
const data = {
foo: null,
}
this.$emit('foo', data)
return data
},
emits: ['foo'],
methods: {
setFoo(foo) {
this.foo = foo
this.$emit('foo', foo)
}
}
}
<!-- template that calls setFoo e.g. on click -->

vuejs2 passing data between parent-child is wiping childs value

In VueJS 2 I am trying to create a component that gets and passes data back to the parent which then passes it to another component to display.
The component that gets the data has a user input field it uses to search. When I have it pass data back to the parent using $emit the value in the input keeps being wiped.
I am receiving the below mutation error but I haven't directly tried to change the userSearch field in the component so I am not sure why.
"Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "userSearch" (found in PersonField)"
Relevant html
<person-field v-on:event_child="eventChild"></person-field>
<person-search :prop="personListArray" ></person-search>
Parent app
var app = new Vue({
el: '#app',
data: {
personListArray : [],
tempArray: []
},
methods: {
eventChild: function (arr) {
this.personListArray = arr
}
}
})
Component 1, displays a user input. Uses the input to search and bring back data. Starts search when the length of the input is more then 2. As soon as you hit the 3rd character something is causing the input to clear which I don't want.
Vue.component('person-field', {
props: ['userSearch'],
template: '<input class="form-control" v-model="userSearch" >',
watch: {
userSearch: function () {
var arr = []
if (typeof this.userSearch !== 'undefined') { //added this because once i passed 3 characters in the field the userSearch variable becomes undefined
if (this.userSearch.length > 2) {
$.each(this.getUsers(this.userSearch), function (index, value) {
var obj = {
Title: value.Title,
ID: value.ID
}
arr.push(obj)
});
this.$emit('event_child', arr) //emits the array back to parent "eventChild" method
} else {
console.log('no length')
}
} else {
console.log('cant find field')
}
},
},
methods: {
getUsers: function (filter) {
//gets and returns an array using the filter as a search
return arr
},
}
});
Component 2 - based on the personListArray which is passed as a prop, displays the results as a list (this works)
Vue.component('person-search', {
props: ['prop'],
template: '<ul id="personList">' +
'<personli :ID="person.ID" v-for="person in persons">' +
'<a class="" href="#" v-on:click="fieldManagerTest(person.Title, person.ID)">{{person.Title}}</a>' +
'</personli></ul>',
computed: {
persons: function () {
return this.prop
}
},
methods: {
fieldManagerTest: function (title, ID) { //Remove item from users cart triggered via click of remove item button
//var user = ID + ';#' + title
//this.internalValue = true
//this.$emit('fieldManagerTest');
//this.$parent.$options.methods.selectManager(user)
},
},
});
Component 3, part of component 2
Vue.component('personli', {
props: ['ID'],
template: '<transition name="fade"><li class="moving-item" id="ID"><slot></slot></li></transition>'
})
;
The reason you get the warning,
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders. Instead, use a data or
computed property based on the prop's value. Prop being mutated:
"userSearch" (found in PersonField)
Is because of this line
<input class="form-control" v-model="userSearch" >
v-model will attempt to change the value of the expression you've told it to, which in this case is userSearch, which is a property.
Instead, you might copy userSearch into a local variable.
Vue.component('person-field', {
props: ['userSearch'],
data(){
return {
searchValue: this.userSearch
}
},
template: '<input class="form-control" v-model="searchValue" >',
...
})
And modify your watch to use searchValue.
Here is an example.

vue-route pass different props to different view component when router changed in vuejs2.0?

In this ticket, we can pass parameters to component when the route changes via
<router-view class="view" :propForA="AData"></router-view>
My question is If we need pass different props to different routed component,
say, for example, propForB property will have BData , propForC property will have CData
how to achieve that? Especially in vuejs2.0?
I would send one prop with different values that fits your component... For example:
<router-view class="view" :propData="propToSend"></router-view>
then something like this
export default {
data: function () {
return {
propAData: {
something: 'value',
somethingElese: ['other', 'value']
},
propBData: {
somethingOther: 123
},
propToSend: null
}
},
watch: {
'$route': function (val, oldVal) {
if (this.$route.name === 'Something') {
this.propToSend = this.propAData
} else {
this.propToSend = this.propBData
}
}
}
}
There are of-course other ways, to achieve same result...