reactivity disappears after the object was in the array - vue.js

Why, after passing through the reactive array, did the element's reactive property turn into a non-reactive one? Can you explain please.
<script setup lang="ts">
import { ref, Ref } from 'vue'
interface Obj {
a: number;
r: Ref<{s:string}>;
}
const obj: Obj = { a: 1, r: ref<string>({s:"one"}) }; //create an instance
const valBeforeArr=obj.r.value //the variable is displayed as it should be
//put an instance in a reactive array, and get from it
const arr = ref<Obj[]>([]);
arr.value.push(obj)
const valAfterArrRef=arr.value[0].r.value //reference value missing
const valAfterArrNonRef=arr.value[0].r //and the original value is displayed normally
</script>
<template>
<h2>valBeforeArr: {{ valBeforeArr }}</h2>
<h2>valAfterArrRef: {{ valAfterArrRef ?? 'underfined' }}</h2>
<h2>valAfterArrNonRef: {{ valAfterArrNonRef ?? 'underfined' }}</h2>
</template>
playground
As explained to me
Estus Flask, reactivity has not disappeared. You can verify this with the following code:
<template>
<h2>valBeforeArr: {{ valBeforeArr }}</h2>
<h2>valAfterArrRef: {{ valAfterArrRef ?? 'underfined' }}</h2>
<h2>valAfterArrNonRef: {{ valAfterArrNonRef ?? 'underfined' }}</h2>
<button #click="obj.r.value.s='seven'">updateBeforeArr</button>
<button #click="arr[0].r.s='five'">updateAfterArr</button>
</template>
But in one case you have to access using ".value", and in the other, without it. This is inconvenient, given that the object, in accordance with the business logic, can either pass through the array or not. How to access a property universally?

The documentation explains this:
When a ref is accessed or mutated as a property of a reactive object,
it automatically unwraps to the inner value so it behaves like a
normal property
<...>
Ref unwrapping only happens when nested inside a reactive Object.
There is no unwrapping performed when the ref is accessed from an
Array or a native collection type like Map
Since ref is deeply reactive, arr.value elements are reactive objects, and refs in obj properties are unwrapped when it's added to the array and becomes reactive.
shallowRef can be used in order to avoid unwrapping in nested refs.

Related

VueJS: Altering prop directly vs. this.[prop] in v-if

I built a vue component that get's a number value via prop from the outside laravel blade, like this:
<my-custom-template :mynumber="{{$numbervalue}}" :list:{{$alist}}></my-custom-template>
inside the template I have a v-for list and the prop:
props:{
list:Array,
mynumber: Number,
[..]
}
and
<template>
<ul>
<li v-for="item in list">{{item}}<span v-if="item.id == mynumber">active</span></li>
</ul>
</template>
Whenever the ID of the item is the same as the value mynumber, I want the "active" tag/span to be displayed.
Now in this template I also have a method that sends an axios request and on success it alters the value of the prop "mynumber", so the list should rerender:
axios.post('/api/someurl', this.obj)
.then(res => {
this.mynumber= res.data[something]; // returns a new number from the db.
})
.catch(error => { [..]
};
Issue: If I use this.mynumber in the list's v-if condition, the "active" tag is never being shown. If I use directly == mynumber then it works, but I cannot alter it with the axios response.
How should I approach this correctly?
How can I alter the initial prop, with the new value from the axios call?
First, you shouldn't be modifying props directly, as mentioned in this prior Stack Overflow post.
[Vue warn]: 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: "propRoomSelected"
Second, as seen in the Vue documentation for conditionals, you do not use this within templates, the this is inferred.
Now, to get to the meat of your question, how to look at either a prop or a new value when rendering. Here's how I'd do it.
<template>
<ul>
<li v-for="item in list">{{item}}<span v-if="isCurrent(item.id)">active</span></li>
</ul>
</template>
<script>
export default {
props: ['list', 'myNumber'],
data() {
return {
myNewNumber: undefined
}
},
methods: {
isCurrent(itemId) {
return itemId == (myNewNumber || myNumber)
}
}
}
</script>
Edit:
Note that there is a difference between
return itemId == (myNewNumber || myNumber)
and
return (itemId == myNewNumber) || (itemId == myNumber)
The first one "short circuits" a comparison against myNumber once myNewNumber becomes anything "truthy". Read more here.
Don't mutate props directly. Use this.$emit (Docs: https://v2.vuejs.org/v2/guide/components-custom-events.html) instead to change the myNumber in the parent. myNumber will then automatically update in the child component.

Vue: How to perform reactive object change detection in v-for?

I read this documentation but cannot use the proposed solution.
I have a v-for loop over objects. These objects are changed dynamically over time and I need that change to show reactively in the v-for loop.
<b-row lg="12" v-for="data in objects" :key="data.id">
<div v-if="data.loading">
loading...
{{data.loading}}
</div>
<div v-else>
loaded, done
{{data.loading}}
</div>
</b-row>
In my methods, I have a for loop that downloads data for each object and changes the object value like this:
for(var i = 0; i<response.ids.length; i++){
var newId = response.ids[i].id
this.objects.newId = {"loading":true, "id": newId}
downloadSomething(newId).then(res => {
this.objects.newId = res[0] //<-- this change needs to be shown reactively.
})
}
According to Vue documentation, Object changes are not reactive:
var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` is now reactive
vm.b = 2
// `vm.b` is NOT reactive
Vue propses some workaround like this:
Vue.set(vm.userProfile, 'age', 27)
UPDATE
But for my case, this just creates a new parameter in the object with the same ID and creates a duplicate key warning and other problems.
I also tried Vue.delete just before Vue.set but it is not actually deleting.
Is there a way to not replace the key/value pair but add more/change parameters to the first child of the root with the ID of newID
Thanks!
Solution: Replace this.objects.newId = res[0] with this.$set(this.objects, 'newId', res[0]) and it should work.
Explanation: this.$set is just an alias to Vue.set, available within any Vue instance. Bear in mind that vm (stands for view model) in the example is the same as this and equals to Vue component instance. Also due to ES5 JS restrictions, you have to set objects' properties explicitly via Vue.set/this.$set to manually trigger re-render. This problem will be resolved when Vue 3.0 is released.
Hope this helps, if you need any clarifications - feel free to ask.
Try that:
downloadSomething(newId).then(res => {
this.$set(this.objects, 'newId', res[0])
})
you need Vue.set() when you want to define a new property to an existing object (not directly to the data), and this function will assign the new property to the built-in watcher, and it will become reactive as well.
its all being explained in the docs: https://v2.vuejs.org/v2/guide/reactivity.html
in your case, it seems to be all you need to make it work. in my example:https://jsfiddle.net/efrat19/eywraw8t/484131/ an async function fetches the data and define a new property to contain te response.
new Vue({
el: "#app",
data: {
objects:{
property1:12
}
},
methods: {
fetchDataAndAddProperty(response){
fetch('https://free.currencyconverterapi.com/api/v6/countries').then(res =>
res.json()).then(data => Vue.set(this.objects,'property2',data))
}
}
})
and the template:
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<div lg="12" v-for="(val,key,index) in objects" :key="index">
{{key}}:{{val}}
</div>
<button #click="fetchDataAndAddProperty()">fetch</button>
</div>
as you can see in the fiddle, the new property becomes reactive and being displayed as well.

In Vue 2 after passing props v-for does not get updated after items are removed

If I pass an array of objects using props in Vue 2 and on this array I use the v-for directive, view does not get updated if one of the array elements get removed.
This seems to work only if the v-for elements are declared as data, but my component needs to receive props...
In the example below you can see that the elements in the services array are indeed removed, but the v-for isn't triggered.
I'm pretty sure I'm doing here something wrong...
Vue.component('location-service-list', {
props: ['services'],
template: '<div>{{ services }}<div v-for="(service, index) in services">{{ service.id }} - {{ service.name }} <a #click.prevent="remove(index)">remove</a></div></div>',
methods: {
remove(index) {
this.services.splice(index, 1);
console.log(this.services);
},
}
});
const app = window.app = new Vue({
el: '#admin-app'
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.8/vue.js"></script>
<div id="admin-app">
<location-service-list :services='[{"id":1,"name":"Test 1"},{"id":2,"name":"Test 2"}]'></location-service-list>
</div>
Try defining your servicesList inside the root component as follows:
const app = window.app = new Vue({
el: '#admin-app',
data: {
servicesList: [{"id":1,"name":"Test 1"},{"id":2,"name":"Test 2"}]
}
});
And your template as:
<div id="admin-app">
<location-service-list :services='servicesList'></location-service-list>
</div>
Now it will work alright without any issues. It was not working earlier because you passed it as a constant / immutable object (JSON string in the parent template which always evaluates to the same value whenever the parent template re-renders).
Technically you are not supposed to change objects passed via props in the child component. If you do the same to a string value that is passed via props, you will get an error message like:
[Vue warn]: Avoid mutating a prop directly...
To process this remove action from parent component, you may refer to the answer under this question: Delete a Vue child component
The jsFiddle in that answer provides a way to send an event from child component to parent component, so that the appropriate child component can be deleted.

Passing entire data object as props

Is it possible in Vue to pass the whole data object as props?
For example
Vue.component('comp', {
props: ['allData'],
template: '<div>{{allData.msg}}</div>'
})
new Vue({
el: "#test",
data: {
msg: "Hello"
}
})
In my view:
<div id="test">
<comp :allData="data"></comp>
</div>
It's possible like this:
<div id="test">
<comp :allData="$data"></comp>
</div>
However, mutating allData in the component will affect the parent's state since it's an object. See the warning from Vue 1.0 docs below:
Note that if the prop being passed down is an Object or an Array, it is passed by reference. Mutating the Object or Array itself inside the child will affect parent state, regardless of the binding type you are using.
and Vue 2.0 docs
Note that objects and arrays in JavaScript are passed by reference, so if the prop is an array or object, mutating the object or array itself inside the child will affect parent state.
You can access the whole object via $data, and pass it.
But it's usually not the best idea to mess with it.

Use global functions in vue directives

I'm trying to use lodash methods (_.isEmpty) in vue directives like this:
<div class="post" v-for="post in posts"></div>
...
<div class="comments" v-if="! _.isEmpty(post.comments)">
<div class="comment" v-for="comment in post.comments"></div>
</div>
...
</div>
but getting the following error:
Uncaught TypeError: Cannot read property 'isEmpty' of undefined
It seems vue is looking for the _.isEmpty method inside the current scope. How should I call global functions in this case?
You can only access functions of the current Vue instance/component in a template:
data
props
methods
No "third-party" code can be run.
So, you would have to create a method in the Vue component to proxy to the lodash methods:
methods: {
isEmpty: function (arr) { return _.isEmpty(arr)}
}
and use this method in the template instead:
<div class="comments" v-if="! isEmpty(post.comments)">
Why not just add _ to your Vue component:
data(){
return {
_:require('lodash') //or however you include it. maybe just window._
}
}
Then it would be accessible. Not positive if _ is a valid object key, so might just call it lo or lodash if needed.
Also, assuming that comments is an array, there would be no problem using v-if='post.comments.length'. Lo-dash is great but unnecessary if you already know it's an array.