Binding a UI element with a Vuex store - javascript

I'm trying to bind a UI element (a single-line textbox or 'input' element) with a Vuex store. This fiddle has the code.
When the SearchResult component is visible, it auto-updates -- see the GIF below, where Lisp or Prolog is typed. That's not what I'd like to happen. What I'd really like to do is decouple the UI state (i.e. the value of the textbox) from the model's state, so that if I type Lisp and press Search, the SearchResult component updates itself.
Ideally I'd like to bind the textbox with a variable that's not in the store, but also add some code to observe changes to the store, so that any changes to the store are reflected in the UI.
I read the forms handling documentation for Vuex but wasn't very clear about the best way to get this done. Please could anyone help? I'm new to SPAs so I'm sure there's a better way of getting this done.

I think the approach you have used is the general approach if you want to use a store variable in input. Given that you want to decouple the UI variable with the model's state(Why?), you can do following:
Have a local variable in that vue instace
use that local variable with v-model
put a watch on state variable, if state variable changes, change local variable.
set state variable on button press, or some other way like onblur event
Here are relevant JS changes:
const app = new Vue({
router,
el: '#app',
data: {
localQuery: ''
},
computed: {
query: {
get () { return store.state.query },
set (v) { store.commit('setquery', v) }
}
},
methods: {
s1: function () {
console.log('app.s1 this.query: ' + this.query);
this.query = this.localQuery
router.push({ name: 'qpath', params: { query: this.query }});
}
},
watch:{
query: function (newVal) {
this.localQuery = newVal
}
}
})
see updated fiddle here.

Related

Vue not updating global state

I have some problems with updating the global state. I'm trying to update that state by listening WebSocket, but it's not updating as expected.
Here is how did I define the global state.
state: {
userData: null
},
getters: {
userData: state => {
return state.userData
},
mutations: {
GET_USER({payload}) {
commit('GET_USER', payload)
},
And I'm updating it in App.vue like so:
mounted() {
window.Echo.channel("user." + this.userData.id).listen(".user-updated", (user) => {
this.$store.commit('GET_USER', JSON.stringify(user.user))
});
Ofcourse I'm closing that websocket. I tried with localStorage, which I think is not a bad idea, but still I'm doing it with global state, and with localstorage would look like:
localStorage.setItem('userData', JSON.stringify(user.user))
So when I want to show that data in some component, for example, Home.vue, the Only way that I can see what is happening, is by defining {{ this.$store.getters.userData }} in the template of that file, but If I try to define it in scripts data, like so:
data() {
return {
data: this.$store.getters.userData,
}
},
It's not updating real time, but only if I go to another page and return here, or update the component.
Any ideas on how to fix it?
I had success by accessing the state. As far as I understand it it should be reactive as well this.$store.state.userData and reflect the current state of the store just as well.
Keep in mind accessing object properties might not be reactive the way you think they are: https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
So you probably want to define a getter where you access the user's name, credentials, whatever userData holds.
The more explicit approach is defining every property of the userData object in the store and write a mutation for every property individually (loops are possible to do that at once, of course, but you'll have to create the mutators still).
{
state: {
userData: {
id: null,
name: '',
email: '',
// etc.
}
},
// ...
}
Doing it this way may also work successfully with the getter you defined.
For my work I would rather not trust handling whole objects at once. This is a bit of extra code to write but explicitly defining which properties the expected object should have is valuable to you and others.

Passing data to Vue child component without re-mounting it

I have a page that includes some audio visualization that's being done within a component. The component is only for displaying stuff, the audio logic is done in the parent.
I'll keep this part short, since my problem is more general, but basically I'm passing the AnalyserNode to the child to poll the realtime frequency analysis from within my Three.js render-loop in the child (WorkletModule).
So in my parent I have something like this:
<template>
...
<WorkletModule
:analyser="analyser"
:key="componentKey"
/>
...
</template>
...
data() {
return {
componentKey: 0,
analyser: null,
};
methods: {
startRecording() {
this.componentKey++
..
this.analyser = audioContext.createAnalyser();
..
}
}
while my WorkletModule.vue looks like this:
props: {
analyser: {
type: AnalyserNode
},
},
mounted() {
//initialize graphics, etc..
}
//in the render loop I need to do something like this:
this.analyser.getByteFrequencyData(soundData);
Now if I use it like above, it does actually work, however, every call to startRecording() causes the WorkletModule to re-mount, creating a hick-up and emptying the canvas of the WorkletModule.
If I remove the ":key="componentKey" it doesn't get updated, which means that the reference to the analyzer in the WorkletModule/prop always stays null, which is obviously not what I want.
Is there a way to pass the analyzer object in another form than a prop?
Basically I need the analyzer value in every frame of the render-loop. I could not pass it as a prop but rather emit an event from the render loop of the WorkletModule that in turn gets the parent to send the current values back to the child via another event. That doesn't seem like a very elegant thing though, so I guess there's a better way.
I'm really puzzled by this:
If I remove the ":key="componentKey" it doesn't get updated, which means that the reference to the analyser in the WorkletModule/prop always stays null
This is not supposed to happen since props are updated without re-rendering by default. Of course, if you use a key, the component is fully destroyed and recreated on every key update.
You could listen to your props update by using a watcher.
In the example below, I pass a prop to a children component, from null to a number. The component is only rendered once and the prop is updated as expected. I only added a watcher to track every update of the said prop and give me the possibility to add any logic from that. You could try to do that in order to understand if the prop is really never updated or maybe your implementation lacks something (and you didn't show us the malicious bit of code that drives you into thinking it's not updated).
const test = Vue.component("test", {
props: ["prop1"],
methods: {
getProp() {
console.log(this.prop1);
}
},
created() {
console.log("created"); // only called once
setInterval(this.getProp, 2000);
},
watch: {
prop1(newVal, oldVal) {
console.log("watcher:", newVal, oldVal);
// do something
}
},
template: "<div>test</div>"
});
new Vue({
el: "#app",
components: {
test
},
data: {
toProp: null
},
methods: {
setProp() {
this.toProp = new Date().valueOf();
}
},
created() {
setInterval(this.setProp, 6000);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<test :prop1="toProp" />
</div>

How to make data from localStorage reactive in Vue js

I am using localStorage as a data source in a Vue js project. I can read and write but cannot find a way to use it reactively. I need to refresh to see any changes I've made.
I'm using the data as props for multiple components, and when I write to localStorage from the components I trigger a forceUpdate on the main App.vue file using the updateData method.
Force update is not working here. Any ideas to accomplish this without a page refresh?
...............
data: function () {
return {
dataHasLoaded: false,
myData: '',
}
},
mounted() {
const localData = JSON.parse(localStorage.getItem('myData'));
const dataLength = Object.keys(localData).length > 0;
this.dataHasLoaded = dataLength;
this.myData = localData;
},
methods: {
updateData(checkData) {
this.$forceUpdate();
console.log('forceUpdate on App.vue')
},
},
...............
Here's how I solved this. Local storage just isn't reactive, but it is great for persisting state across refreshes.
What is great at being reactive are regular old data values, which can be initialized with localStorage values. Use a combination of a data values and local storage.
Let's say I was trying to see updates to a token I was keeping in localStorage as they happened, it could look like this:
const thing = new Vue({
data(){
return {
tokenValue: localStorage.getItem('id_token') || '',
userValue: JSON.parse(localStorage.getItem('user')) || {},
};
},
computed: {
token: {
get: function() {
return this.tokenValue;
},
set: function(id_token) {
this.tokenValue = id_token;
localStorage.setItem('id_token', id_token)
}
},
user: {
get: function() {
return this.userValue;
},
set: function(user) {
this.userValue = user;
localStorage.setItem('user', JSON.stringify(user))
}
}
}
});
The problem initially is I was trying to use localStorage.getItem() from my computed getters, but Vue just doesn't know about what's going on in local storage, and it's silly to focus on making it reactive when there's other options. The trick is to initially get from local storage, and continually update your local storage values as changes happen, but maintain a reactive value that Vue knows about.
For anyone facing the same dilemma, I wasn't able to solve it the way that I wanted but I found a way around it.
I originally loaded the data in localStorage to a value in the Parent's Data called myData.
Then I used myData in props to populate the data in components via props.
When I wanted to add new or edit data,
I pulled up a fresh copy of the localStorage,
added to it and saved it again,
at the same time I emit the updated copy of localStorage to myData in the parent,
which in turn updated all the data in the child components via the props.
This works well, making all the data update in real time from the one data source.
As items in localstorage may be updated by something else than the currently visible vue template, I wanted that updating function to emit a change, which vue can react to.
My localstorage.set there does this after updating the database:
window.dispatchEvent(new CustomEvent('storage-changed', {
detail: {
action: 'set',
key: key,
content: content
}
}));
and in mounted() I have a listener which updates forceRedraw, which - wait for it - force a redraw.
window.addEventListener('storage-changed', (data) => {
this.forceRedraw++;
...

Binding Vue component data to external state

I have a Vue component that needs to be embedded into a large legacy (not Vue) system in several places.
The component makes some AJAX requests and displays information based on an array of database record IDs, which it usually receives at page load time. The array is embedded directly into the page template by the server, and passed as props to the component, like so:
let config = {
records: {{ template_code_outputting_array }},
...otherConfigOptions
}
let app = new Vue({
el: '#app',
store,
render: h => h(ComponentName, {
props: config
})
})
My problem is that on one of the screens it needs to be embedded into, the database records can be selected dynamically by the user with a search-and-add type AJAX interface (rather than being pre-set at page load), so I need to keep the component in sync with the user's selection so it can be re-rendered as necessary.
I have a working solution but it's far from ideal; I'm currently directly calling a method on the component every time the user makes some change to their selection, passing the new selection so that the component can update its own internal copy:
app.$children[0].recordsChanged(newSelection)
But obviously this isn't reactive, and it feels kind of hacky. I feel like I shouldn't be directly addressing a component, and the function I'm calling is there solely for external use; nothing inside the component uses it.
I've tried binding values in data to global variables (which again isn't ideal but I don't have much choice) then adding a watch method to trigger a re-render on change. Obviously scalar values won't work as they'll be by value and not reference, but wrapping them in an object doesn't seem to work either. They bind fine and take the initial values provided, but updating the object (either directly mutating its values or replacing the object with a new one) doesn't seem to trigger the watch method or any reactivity.
Is there any best practice for doing this or is my current approach the best I can hope for?
An event bus (as suggested by AfikDeri in the comments) is a good solution. It provides a straightforward interface.
Reactive-izing your data is also possible, with some caveats. An Object used to initialize a data item will itself be made reactive. If the things that change your array do so in the approved ways, you should see the changes inside your Vue.
let stuff = [2, 3, 5];
const v = new Vue({
el: '#app',
data: {
reactiveStuff: stuff
},
components: {
myComponent: {
props: ['arr'],
template: '<div><div v-for="item in arr">{{item}}</div></div>'
}
}
});
setTimeout(() => {
stuff.push(7);
}, 1200);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<my-component id="app" :arr="reactiveStuff">
</my-component>

How to update Vue component property when Vuex store state changes?

I'm building a simple presentation tool where I can create presentations, name them and add/remove slides with Vue js and Vuex to handle the app state. All is going great but now I'm trying to implement a feature that detects changes in the presentation (title changed or slide added/removed) and couldn't not yet find the right solution for it. I'll give the example only concerning the title change for the sake of simplicity. Right now in my Vuex store I have:
const state = {
presentations: handover.presentations, //array of objects that comes from the DB
currentPresentation: handover.presentations[0]
}
In my Presentation component I have:
export default {
template: '#presentation',
props: ['presentation'],
data: () => {
return {
shadowPresentation: ''
}
},
computed: {
isSelected () {
if (this.getSelectedPresentation !== null) {
return this.presentation === this.getSelectedPresentation
}
return false
},
hasChanged () {
if (this.shadowPresentation.title !== this.presentation.title) {
return true
}
return false
},
...mapGetters(['getSelectedPresentation'])
},
methods: mapActions({
selectPresentation: 'selectPresentation'
}),
created () {
const self = this
self.shadowPresentation = {
title: self.presentation.title,
slides: []
}
self.presentation.slides.forEach(item => {
self.shadowPresentation.slides.push(item)
})
}
}
What I've done so far is to create a shadow copy of my presentation when the component is created and then by the way of a computed property compare the properties that I'm interested in (in this case the title) and return true if anything is different. This works for detecting the changes but what I want to do is to be able to update the shadow presentation when the presentation is saved and so far I've failed to do it. Since the savePresentation action triggered in another component and I don't really know how pick the 'save' event inside presentation component I fail to update my shadow presentation. Any thoughts on how I could implement such feature? Any help would be very appreciated! Thanks in advance!
I ended up solving this problem in a different way than what I asked in the question but it may be of interest for some. So here it goes:
First I abdicated from having my vue store communicating an event to a component since when you use vuex you should have all your app state managed by the vuex store. What I did was to change the presentation object structure from
{
title: 'title',
slides: []
}
to something a little more complex, like this
{
states: [{
hash: md5(JSON.stringify(presentation)),
content: presentation
}],
statesAhead: [],
lastSaved: md5(JSON.stringify(presentation))
}
where presentation is the simple presentation object that I had at first. Now my new presentation object has a prop states where I will put all my presentation states and each of this states has an hash generated by the stringified simple presentation object and the actual simple presentation object. Like this I will for every change in the presention generate a new state with a different hash and then I can compare my current state hash with the last one that was saved. Whenever I save the presentation I update the lastSaved prop to the current state hash. With this structure I could simple implement undo/redo features just by unshifting/shifting states from states to statesAhead and vice-versa and that's even more than what I intended at first and in the end I kept all my state managed by the vuex store instead of fragmenting my state management and polluting components.
I hope it wasn't too much confusing and that someone finds this helpful.
Cheers
I had this issue when trying to add new properties to my user state so I ended up with this and it works well.
Action in Vuex store
updateUser (state, newObj) {
if (!state.user) {
state.user = {}
}
for (var propertyName in newObj) {
if (newObj.hasOwnProperty(propertyName)) {
//updates store state
Vue.set(state.user, propertyName, newObj[propertyName])
}
}
}
Implementation
Call your store action above from the Vue component
this.updateUser({emailVerified: true})
Object
{"user":{"emailVerified":true},"version":"1.0.0"}

Categories

Resources