strange behavior when pointing to localStorage with vue getter/setter - vue.js

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.

Related

How to get updated value from Vuex getter inside component template?

How can I detect changes of the getter value inside the template?
I have the following component:
computed: {
...mapGetters({
processingStep: 'products/processingStep',
}),
<div class="col" v-if="processingStep !=='last'">
...
...
</div>
So when I click the button in the Vuex state value for processingStep is getting changed during the time. The thing is that inside Vue dev tools I see updated getter value but my componnet template does not track it. How can this be fixed and how can above div be aware about processingStep value change?
You should be able to subscribe to your store's mutation. Subscribing is essentially like creating an event listener for any time a mutation is called on your store. For example, if your processingStep value is changed by a mutation called setProcessingStep:
export default {
data() {
return {
processingStep: null
}
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'setProcessingStep') {
this.processingStep = state.processingStep
}
})
}
}
This is a basic example on how this should work. I tried to mock your setup.
Below is a working example of a getter being reactive to change the vue v-if in the DOM.
Maybe this example helps you spot an error you might have made in your code.
I will not add the example using a watcher, due to this being the correct way to use vuex with getters. Using a watcher would be avoiding the real problem and be considered bad practice.
I also suspect you might have broken the vue reactivity in your app.
If you look at the vuex docs: Mutations Follow Vue's Reactivity Rules and Vue Change Detection Caveats
This essentially means that Vue cannot detect changes applied for objects and arrays done in a specific way.
For objects: Typically when you add foreign keys that was not there on initialization
For Arrays: When you directly set an item with the index or change the length
Vue.use(Vuex);
const store = new Vuex.Store({
modules: {
products: {
strict: true,
namespaced: true,
state: {
step: 'first'
},
getters: {
processingStep(state) {
return state.step;
}
},
mutations: {
CHANGE_STEP(state) {
state.step = 'last'
}
}
}
}
});
const demo = new Vue({
el: '#demo',
store: store,
computed: {
...Vuex.mapGetters({
processingStep: 'products/processingStep'
})
},
methods: {
changeStep() {
this.$store.commit('products/CHANGE_STEP')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.3/vue.js"></script>
<div id="demo">
<p>processingStep: {{ processingStep }}</p>
<button #click="changeStep">change Step</button>
<div class="col" v-if="processingStep !=='last'">
First step! :D
</div>
<p v-else-if="processingStep !== 'first'">
This is the last..
</p>
</div>

How to make Arrays reactive in VueJS

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)
},
},

Vue - Passing component data to view

Hi I'm looking at Vue and building a website with a Facebook login. I have a Facebook login component, which works, although I'm having difficulty making my acquired fbid, fbname, whatever available to my Vues outside the component. Acknowledged this most likely this is a 101 issue, help would be appreciated.
I've tried the global "prototype" variable and didn't manage to get that working. How's this done?
Code below:
main.js
new Vue({
router,
render: h => h(App),
vuetify: new Vuetify()
}).$mount('#app')
App.vue
...
import FacebookComp from './components/FacebookComp.vue'
export default {
name: 'app',
components: {
FacebookComp
},
...
}
Component - FacebookComp.vue
<template>
<div class="facebookcomp">
<facebook-login class="button"
appId="###"
#login="checkLoginState"
#logout="onLogout"
#get-initial-status="checkLoginState">
</facebook-login>
</div>
</template>
<script>
//import facebookLogin from 'facebook-login-vuejs';
//import FB from 'fb';
export default {
name: 'facebookcomp',
data: {
fbuserid: "string"
},
methods: {
checkLoginState() {
FB.getLoginStatus(function(response) {
fbLoginState=response.status;
});
if(fbLoginState === 'connected' && !fbToken){
fbToken = FB.getAuthResponse()['accessToken'];
FB.api('/me', 'get', { access_token: fbToken, fields: 'id,name' }, function(response) {
fbuserid=response.id;
});
}
},
...
}
VIEW - view.vue
...
export default {
name: 'view',
data: function() {
return {
someData: '',
},
mounted() {
alert(this.fbloginId); //my facebook ID here
},
...
}
If you would like to have all these props from FB login available throughout your whole app, I strongly suggest using vuex. If you are not familiar with state management in nowadays SPAs, you basically have global container - state. You can change it only via functions called mutations (synchronous). However, you cannot call them directly from your component. That's what functions called actions (asynchronous) are for. The whole flow is: actions => mutations => state. Once you have saved your desired data in state, you can access it in any of your vue component via functions called getters.
Another option, in case you just need the data visible in your parent only, is to simply emit the data from FacebookComp.vue and listen for event in View.vue. Note that to make this work, FacebookComp.vue has to be a child of View.vue. For more info about implementation of second approach, please look at docs.

V-model with datepicker input

Trying to build a component that works with daepicker and using v-model to bind the input value. But the input event does not appear to be firing and I can’t seem to figure out why. Here’s my component:
<div id="app">
<datepicker v-model="date"></datepicker>
</div>
Vue.component('datepicker', {
template: '<input type="text" class="form-control pull-right" placeholder="dd/mm/aaaa" autocomplete="off">',
mounted: function() {
$(this.$el).datepicker({
autoclose: true,
startView: 'years',
}).on('changeDate', function(e) {
this.$emit('input', e.format('dd/mm/yyyy'));
});
},
destroyed: function () {
$(this.$el).datepicker('destroy');
}
});
var app = new Vue({
el: '#app',
data: {
date: '2018-03-01'
}
})
In addition, the following error appears in the console:
Uncaught TypeError: this.$emit is not a function
If you're mixing jQuery and Vue (just a guess from the code fragment), you're mixing up your contexts. One (of many) ways to fix:
mounted: function() {
const self = this;
$(this.$el).datepicker({
autoclose: true,
startView: 'years',
}).on('changeDate', function(e) {
self.$emit('input', e.format('dd/mm/yyyy'));
});
},
I failed with jacky's answer, but thanks to https://github.com/Xelia, problem sovled (even in Vue 1.0, using ready life cycle instead of mounted)
Manually update vue data in datepicker changeDate event listener, like this
var app = new Vue({
el: '#app',
data: {
startDate: '',
},
mounted() {
$("#startDate").datepicker().on(
"changeDate", () => {this.startDate = $('#startDate').val()}
);
},
})
https://jsfiddle.net/3a2055ub/
And by the way, if you are working on legacy company project using ES5 function instead of ES6 fat arrow function. Need to bind this, which is vue instance, into the function. For example:
mounted() {
var self = this; // the vue instance
$("#startDate").datepicker().on(
"changeDate", function() {self.startDate = $('#startDate').val()}
);
},
Of course there are other ways to reach this goal, as this blog written by Jason Arnold
shows.
Reference: https://github.com/vuejs/vue/issues/4231
Probable related question: v-model not working with bootstrap datepicker

Why is it possible to have reactive elements added at mount?

The documentation for Reactivity in Depth explains why adding new root-level reactive properties to an already created instance is not possible (and how to actually add them via this.$set()).
In that case, why an initially empty object can be updated (and reactive) at mount time, after the instance was intialized? Or does the initialization part includes the mount? (though it is possible to mount an instance manually after the initalization)
new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.myobject = {
"x": 1
}
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
Direct further modifications after the mount are not taken into account, in line with the documentation (this.myobject.y = 2 for instance would not work, while this.$set(this.myobject, "y", 2) will be fine)
The code in your sample does not fall into the change detection caveat because you are not adding a property to myobject you are setting myobject to an entirely new object. Vue has no problem detecting object reference changes.
What Vue cannot detect is adding a property to an object that did not already exist. For example if you did this:
mounted() {
setTimeout(() => {
this.myobject.someNewProperty = "some value"
}, 2000)
}
Vue would not detect the change. Here is your example updated to demonstrate that the DOM never changes after the object is changed.
new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.myobject.someNewProperty = "some value"
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
What the documentation means when it says
Vue does not allow dynamically adding new root-level reactive
properties to an already created instance.
Is that you cannot add another property to the data object after the Vue instance is created. For example this code:
var vm = new Vue({
el: "#app",
data: {
myobject: {}
},
mounted() {
setTimeout(() => {
this.$set(vm.$data, 'newRootLevelProperty', "some value")
}, 2000)
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.js"></script>
<div id="app">
{{myobject}}
</div>
Results in the warning
[Vue warn]: Avoid adding reactive properties to a Vue instance or its
root $data at runtime - declare it upfront in the data option.
But you can add properties to nested objects (such as myobject) as long as you use $set.