How to make Arrays reactive in VueJS - vue.js

I am trying to add a simple hover effect on images with Vue, so that on hover the second item in the array of images in shown. I cannot work out the best way to constantly update the templates data if the data has changed. I tried Computed, didn't work either for me.
Everything updates fine in console.log() for the methods. But not for watchers or computed.
I see that watch works only on Mount, but not on mouseenter.
<div class="cursor-pointer" v-for="(image, index) in images" :key="index"
#mouseenter="hoverEnter(index)"
#mouseleave="hoverLeave(index)"
>
// MOUSE OVER IS NOT UPDATING
<div><img :src="mouseOver[index] ? image[1].src : image[0].src"></div>
</div>
data() {
return {
id: this.$route.params.id,
images: [],
mouseOver: []
};
},
mounted() {
this.returnImages
this.mouseOver = this.currentCollection.products.map((res) => {
return false
})
},
methods: {
hoverEnter(index) {
this.mouseOver[index] = true
},
hoverLeave(index) {
this.mouseOver[index] = false
},
},
watch: {
mouseOver: function (newValue) {
console.log(newValue) // THIS IS NOT UPDATING
}
},

I was able to make this reactive using the this.$set from Vue and remove the watch. Edit: Got it, it's splice in nice form. I made my data non reactive. Hope this helps someone.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely.
they say.https://vuejs.org/2016/02/06/common-gotchas/
arr.splice(index, 1, value)
Vue Set
methods: {
hoverEnter(index) {
this.$set(this.mouseOver, index, true)
console.log(this.mouseOver)
},
hoverLeave(index) {
this.$set(this.mouseOver, index, false)
console.log(this.mouseOver)
},
},
Splice
methods: {
hoverEnter(index) {
this.mouseOver.splice(index, 1, true)
console.log(this.mouseOver)
},
hoverLeave(index) {
this.mouseOver.splice(index, 1, false)
console.log(this.mouseOver)
},
},

Related

Why my Vue Child Component have already assign prop to $data at first, but still accidentally mutate the Parent $data?

Please see this minimum example
App.vue
<template>
<EditPerseon :previousPerson="persons[0]" />
</template>
<script>
import EditPerseon from "./EditPerseon.vue";
export default {
data() {
return {
persons: [
{ name: "Bob", age: 18 },
{ name: "Amy", age: 20 },
],
};
},
watch: {
$data: {
handler() {
console.log("My $data have been mutated!");
},
deep: true,
},
},
components: {
EditPerseon,
},
};
</script>
EditPerseon.vue
<template>
<input v-model="currentPerson.name" />
</template>
<script>
export default {
props: {
previousPerson: Object,
},
data() {
return {
currentPerson: this.previousPerson,
};
},
};
</script>
Also on CodeSandbox:
https://codesandbox.io/s/why-my-vue-child-component-have-already-assign-prop-to-data-at-first-but-still-accidentaly-mutate-the-parent-data-fueuw?fontsize=14&hidenavigation=1&theme=dark
EditPerseon.vue is my child component, but it still accidentally mutate my parent's $data.
Why is this happening?
How can I prevent this?
When you define an object (or an array) assigning it to another object in javaScript, you end up with two references to the same object. I think you believe that your line:
currentPerson: this.previousPerson,
is somehow creating a new object currentPerson that copies the values from this.previousPerson. However, what it's happening is that now you have two different variables that both are linked with the original object: so changes to currentPerson will actually modify the parent's person[0].
One thing you can do to mitigate this is to forge a new object in the child component:
data() {
return {
currentPerson: {...this.previousPerson},
};
},
or:
data() {
return {
currentPerson: {
name: this.previousPerson.name
},
};
},
The first option will fail if some fields of the object are objects themselves. The second one is safer but more verbose. You may try some packages like cloneDeep to safely clone objects.
A good general resource to get more info about the deal with object references and copies: https://javascript.info/object-copy.
Apart from this, I believe watcher handlers always trigger at created, so the handler will fire even if no change is detected (but I'm not 100% sure about this atm).
Because objects passes by reference. When you change to current person actually you change the original one in the memory.Because of that vue gives warning for that. You need to copy to prevent this.
data() {
return {
currentPerson: {...this.previousPerson},
};
},

strange behavior when pointing to localStorage with vue getter/setter

I am getting strange behavior from this code. I want to use getter and setter to point to local storage.
The first time the app is rendered, the items are properly retrieved.
Afterwards, when adding a new item to the list, it is not rendered. Also, when adding an item, it will just swap the value at the index of the previous item, after adding the very first item.
<input type="text" v-model="term" #keyup.enter="submit()" />
<ul>
<li v-for="(item, i) in history" :key="i">{{item}}</li>
</ul>
const app = new Vue({
el: '#app',
data: {
term: '',
},
computed: {
history: {
get() {
return JSON.parse(localStorage.getItem('history')) || [];
},
set(value) {
localStorage.setItem('history', JSON.stringify(value));
},
},
},
methods: {
submit() {
this.history = [...this.history, this.term];
this.term = '';
},
},
});
You can check the code here because SO is not allowing to acces localStorage. Remember to refresh the page once you have added the first item and also to investigate what is happening inside the localStorage.
https://codepen.io/bluebrown/pen/dyMMRKj?editors=1010
This code is a port of some alpinejs project. And there it worked.
That said, I can make it work when I write it like below. However, I got curious now, why above example behaves like that.
const app = new Vue({
el: '#app',
data: {
term: '',
history: [],
},
created() {
this.history = JSON.parse(localStorage.getItem('history')) || [];
},
watch: {
history: {
deep: true,
handler(value) {
localStorage.setItem('history', JSON.stringify(value));
},
},
},
methods: {
submit() {
this.history = [...this.history, this.term];
this.term = '';
},
},
});
The reason why it didn't work is because Vue can only observe plain objects. Native objects like localStorage cannot be observed by Vue. If you put something into local storage, Vue won't know about it and your view will not automatically update to display those changes.
Vue mentions this limitation in the docs for the data object:
The data object for the Vue instance. Vue will recursively convert its properties into getter/setters to make it "reactive". The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior.
These limitations apply to Vue's entire reactivity system, including computed properties.

How do I access programmatically created refs in vue.js?

I would like to access refs in a vue.js component, where the ref itself is created dynamically like so:
<style>
</style>
<template>
<div>
<lmap class="map" v-for="m in [1, 2, 3]" :ref="'map' + m"></lmap>
</div>
</template>
<script>
module.exports = {
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
console.log('all refs', this.$refs);
// prints an object with 3 keys: map1, map2, map3
console.log('all ref keys', Object.keys(this.$refs));
// would expect ["map1", "map2", "map3"], prints an empty array instead
Vue.nextTick().then(() => {
console.log('map1', this.$refs["map1"]);
// would expect a DOM element, instead prints undefined
})
},
destroyed: function(){
},
methods: {
},
}
</script>
However this seems not to work (see above in the comments), and I can't figure why.
I think the problem is that you are importing the component asynchronously, with httpVueLoader, which then downloads and imports the component only when the component is rendered from the dom, therefore, the component has not yet been imported into the nextTick callback.
I suggest you put a loaded event in the map.vue component, maybe in mounted lifecycle , which will be listened to in the father, example #loaded = "showRefs"
surely when the showRefs(){ } method is invoked, you will have your refs populated ;)
Try using a template string e.g
`map${m}`
You have to wait until components have been rendered / updated. This works:
module.exports = {
data: function () {
return {
};
},
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
},
destroyed: function(){
},
updated: function(){
Vue.nextTick().then(() => {
console.log('all ref keys', Object.keys(this.$refs));
console.log('map1', this.$refs['map1'][0].$el);
})
},
methods: {
},
}

watch for the whole array as well as one of the properties of it

Let's say I have a prop such as
[{id:1, name:"first"}, {id:2, name:"second"}]
I have a watcher for this called Points. as soon as parent component changes this array, watcher function gets called in child.
Now, I also want to watch if name field in any of this array's object got changed. Workaround I know is to set deep as true in watcher, but this means this is gonna make the watcher for each property of the object. As I have a very huge array of huge objects, I don't want to make this many watcher.
So is there a way to make watcher for the whole array and also one of the properties of the array's object?
You can mark the non-reactive properties as non-configurable, and Vue will not detect changes in their values.
let arr = [{id:1, name:"first"}, {id:2, name:"second"}];
arr.forEach(item => Object.defineProperty(item, "id", {configurable: false}));
Alternatively, you could use a shallow watcher and require code that modifies the reactive properties to use Vue.set() instead of simple assignment.
Vue.set(arr[0], "name", "new name"); // don't use arr[0].name = "new name";
You can create a child component, where you bind objects to the array and place the watcher inside the child component as below:
Parent.vue
<template>
<child-component v-for="point in points" :point="point" ></child-component>
</template>
data: {
return {
points: [{id:1, name:"first"}, {id:2, name:"second"}]
}
}
Child.vue:
props: ['point']
...
watch: {
name: function(newVal){
// watch name field
}
}
One way would be to create a computed property that just touches the bits you care about and then watch that instead.
new Vue({
el: '#app',
data () {
return {
points: [{id: 1, name: 'first'}, {id: 2, name: 'second'}]
}
},
computed: {
pointStuffICareAbout () {
return this.points.map(point => point.name)
}
},
methods: {
updateId () {
this.points[0].id = Math.round(Math.random() * 1000)
},
updateName () {
this.points[0].name = Math.random().toString(36).slice(2, 7)
},
addItem () {
this.points.push({id: 3, name: 'third'})
}
},
watch: {
pointStuffICareAbout () {
console.log('watcher triggered')
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button #click="updateId">Update id</button>
<button #click="updateName">Update name</button>
<button #click="addItem">Add item</button>
<p>
{{ points }}
</p>
</div>

Dynamic Vue components with sync and events

I'm using <component v-for="..."> tags in Vue.js 2.3 to dynamically render a list of components.
The template looks like this:
<some-component v-for="{name, props}, index in modules" :key="index">
<component :is="name" v-bind="props"></component>
</some-component>
The modules array is in my component data() here:
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
},
{
name: 'some-thing',
props: {
color: '#f3f',
text: 'some other text',
},
},
],
I'm using the v-bind={...} object syntax to dynamically bind props and this works perfectly. I also want to bind event listeners with v-on (and use .sync'd props) with this approach, but I don't know if it's possible without creating custom directives.
I tried adding to my props objects like this, but it didn't work:
props: {
color: '#f3f',
text: 'some other text',
'v-on:loaded': 'handleLoaded', // no luck
'volume.sync': 'someValue', // no luck
},
My goal is to let users re-order widgets in a sidebar with vuedraggable, and persist their layout preference to a database, but some of the widgets have #events and .synced props. Is this possible? I welcome any suggestions!
I don't know of a way you could accomplish this using a dynamic component. You could, however, do it with a render function.
Consider this data structure, which is a modification of yours.
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync:{
"volume": "volume"
},
on:{
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on:{
clicked: "onClicked"
}
},
],
Here I am defining two other properties: sync and on. The sync property is an object that contains a list of all the properties you would want to sync. For example, above the sync property for one of the components contains volume: "volume". That represents a property you would want to typically add as :volume.sync="volume". There's no way (that I know of) that you can add that to your dynamic component dynamically, but in a render function, you could break it down into it's de-sugared parts and add a property and a handler for updated:volume.
Similarly with the on property, in a render function we can add a handler for an event identified by the key that calls a method identified in the value. Here is a possible implementation for that render function.
render(h){
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {on:{}, props:{}}
// add props
if (template.props){
def.props = template.props
}
// add sync props
if (template.sync){
for (let sync of Object.keys(template.sync)){
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on){
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)){
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
}
Basically, the render method looks through all the properties in your template in modules to decide how to render the component. In the case of properties, it just passes them along. For sync properties it breaks it down into the property and event handler, and for on handlers it adds the appropriate event handler.
Here is an example of this working.
console.clear()
Vue.component("some-thing", {
props: ["volume","text","color"],
template: `
<div>
<span :style="{color}">{{text}}</span>
<input :value="volume" #input="$emit('update:volume', $event.target.value)" />
<button #click="$emit('loaded')">Click me</button>
</div>
`
})
Vue.component("other-thing", {
template: `
<div>
<button #click="$emit('clicked')">Click me</button>
</div>
`
})
new Vue({
el: "#app",
data: {
modules: [{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync: {
"volume": "volume"
},
on: {
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on: {
clicked: "onClicked"
}
},
],
volume: "stuff"
},
methods: {
handleLoaded() {
alert('loaded')
},
onClicked() {
alert("clicked")
}
},
render(h) {
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {
on: {},
props: {}
}
// add props
if (template.props) {
def.props = template.props
}
// add sync props
if (template.sync) {
for (let sync of Object.keys(template.sync)) {
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on) {
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)) {
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
},
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app"></div>