Can't get v-model work with Composition API and Vuex - vue.js

I've read several posts on stackoverflow and other websites, but still can't figure out what's going wrong in my case.
I'm building an app following composition api approach and using a variable called modelStartDate (which I initiate at Jan 3, 2022). This is how my store looks:
import { createStore } from 'vuex'
export default createStore({
state: {
modelStartDate: new Date(2022, 0, 3)
},
mutations: {
modelStartDateMutation(state, newDate) {
state.modelStartDate = newDate
}
},
actions: {
},
getters: {
},
modules: {
}
})
In the relevant Vue file, I have the following code snippet:
<template>
<nav class="left-bar">
<div class="block" id="modelStartDate">
<label>Model start date</label>
<input type="date" v-model="modelStartDateProxy" />
</div>
<p>{{ modelStartDate }}</p>
</nav>
</template>
<script>
import { ref } from '#vue/reactivity'
import { useStore } from 'vuex'
import { computed } from '#vue/runtime-core'
export default {
setup() {
const store = useStore()
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
const modelStartDate = store.state.modelStartDate
return { modelStartDateProxy, modelStartDate }
}
}
</script>
When I run the page, the paragraph tag prints the right date, however the input tag, where the user can change the date, is empty (I was expecting Jan 3, 2022 to be pre-selected). When the date is changed, nothing seems to change in the app. I'm getting no errors. Any idea what I'm doing incorrectly?
Also, can I access store's modelStartDate state without having to define it separately (redundantly?) in the vue setup() section?

First, I don't know which tutorial you read. But to me, the problem is here:
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
const modelStartDate = store.state.modelStartDate
The snippet
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit("modelStartDateMutation", newDate)
})
is weird to me.
Duplicate of store.state.modelStartDate. DRY.
<p>{{ modelStartDate }}</p> render data from const modelStartDate = store.state.modelStartDate. But the data was only assign once. So the new value was not render on input was changed.
Solution:
const modelStartDate = computed(() => store.state.modelStartDate);
You can take a look at this playground.

The html element input returns a string: "YYYY-MM-DD". Therefore you need the syntax new Date(value)
Take a look at this playground
<template>
<label>Model start date</label>
<input type="date" v-model="modelStartDateProxy" />
<p>{{ modelStartDateProxy }}</p>
</template>
<script>
import { store } from './store.js' //mock-up store
import { ref, computed } from 'vue'
export default {
setup() {
const modelStartDateProxy = computed({
get: () => store.state.modelStartDate,
set: (newDate) => store.commit(newDate) // Use real Vuex syntax
})
return { modelStartDateProxy }
}
}
</script>
//Mock-up Store (not real vuex)
import {reactive} from 'vue'
export const store = reactive({
state: {
modelStartDate: new Date(2022, 0, 3)
},
commit: (value) => store.state.modelStartDate = new Date(value) // new Date(value)
})

Related

How to destructure object props in Vue the way like you would do in React inside a setup?

I am wondering how to destructure an object prop without having to type data.title, data.keywords, data.image etc. I've tried spreading the object directly, but inside the template it is undefined if I do that.
Would like to return directly {{ title }}, {{ textarea }} etc.
My code:
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
<script lang="ts">
import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const articleQuery = `*[_type == "article"][0] {
title,
textarea,
}`;
const options = {
listen: true,
clientOnly: true,
};
const res = useSanityFetcher<any | object>(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
Considering that useSanityFetcher is asynchronous, and res is reactive, it's incorrect to access res.data directly in setup because this disables the reactivity. Everything should happen in computed, watch, etc callback functions.
title, etc properties need to be explicitly listed in order to map reactive object to separate refs with respective names - can probably be combined with articleQuery definition or instantly available as res.data keys
E.g.:
const dataRefs = Object.fromEntries(['title', ...].map(key => [key, ref(null)]))
const res = ...
watchEffect(() => {
if (!res.data) return;
for (const key in dataRefs)
dataRefs[key] = res.data[key];
});
return { ...dataRefs };
Destructuring the object is not the problem, see Vue SFC Playground
<script lang="ts">
//import { useSanityFetcher } from "vue-sanity";
import { defineComponent, reactive, toRefs } from "vue";
export default defineComponent({
name: "App",
setup: () => {
const res = {
data: {
title: 'Hi there'
}
}
const data = reactive(res.data);
return toRefs(data);
},
});
</script>
<template>
<div>
<h1>{{ title }}</h1>
</div>
</template>
It may simply be the space between the filter and the projection in the GROQ expression
const articleQuery = `*[_type == "article"][0]{ title, textarea }`;
See A description of the GROQ syntax
A typical GROQ query has this form:
*[ <filter> ]{ <projection> }
The Vue docs actually recommend not destructing props because of the way reactivity works but if you really want to something like this should work:
const res = useSanityFetcher<any | object(articleQuery, options);
const data = reactive(res.data);
return toRefs(data);
Don't forget to import reactive and toRefs.

Is there a way to share reactive data between random components in Vue 3 Composition API?

Having some reactive const in "Component A," which may update after some user action, how could this data be imported into another component?
For example:
const MyComponent = {
import { computed, ref } from "vue";
setup() {
name: "Component A",
setup() {
const foo = ref(null);
const updateFoo = computed(() => foo.value = "bar");
return { foo }
}
}
}
Could the updated value of 'foo' be used in another Component without using provide/inject?
I am pretty new in the Vue ecosystem; kind apologies if this is something obvious that I am missing here.
One of the best things about composition API is that we can create reusable logic and use that all across the App. You create a composable functions in which you can create the logic and then import that into the components where you want to use it. Not only does this make your component much cleaner but also your APP much more maintainable. Below is a simple example of counter to show how they can be used. You can find working demo here:
Create a composable function for counter:
import { ref, computed } from "vue";
const counter = ref(0);
export const getCounter = () => {
const incrementCounter = () => counter.value++;
const decrementCounter = () => counter.value--;
const counterPositiveOrNegitive = computed(() =>
counter.value >= 0 ? " Positive" : "Negitive"
);
return {
counter,
incrementCounter,
decrementCounter,
counterPositiveOrNegitive
};
};
Then you can import this function into your components and get the function or you want to use. Component to increment counter.
<template>
<div class="hello">
<h1>Component To Increment Counter</h1>
<button #click="incrementCounter">Increment</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "IncrementCounter",
setup() {
const { incrementCounter } = getCounter();
return { incrementCounter };
},
};
</script>
Component to decrement counter:
<template>
<div class="hello">
<h1>Component To Decrement Counter</h1>
<button #click="decrementCounter">Decrement</button>
</div>
</template>
<script>
import { getCounter } from "../composables/counterExample";
export default {
name: "DecrementCounter",
setup() {
const { decrementCounter } = getCounter();
return { decrementCounter };
},
};
</script>
Then in the main component, you can show the counter value.
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<div class="counters">
<IncrementCounter />
<DecrementCounter />
</div>
<h3>Main Component </h3>
<p>Counter: {{ counter }}</p>
<p>{{ counterPositiveOrNegitive }}</p>
</template>
<script>
import IncrementCounter from "./components/IncrementCounter.vue";
import DecrementCounter from "./components/DecrementCounter.vue";
import { getCounter } from "./composables/counterExample";
export default {
name: "App",
components: {
IncrementCounter: IncrementCounter,
DecrementCounter: DecrementCounter,
},
setup() {
const { counter, counterPositiveOrNegitive } = getCounter();
return { counter, counterPositiveOrNegitive };
},
};
Hope this was somewhat helpful. You can find a working example here:
https://codesandbox.io/s/vue3-composition-api-blfpj

How to use v-model in Vue with Vuex and composition API?

<template>
<div>
<h1>{{ counter }}</h1>
<input type="text" v-model="counter" />
</div>
</template>
<script>
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
const counter = computed(() => store.state.counter)
return { counter }
},
}
</script>
How to change value of counter in the store when input value changes
I am getting this error in the console:
[ operation failed: computed value is readonly ]
Try this:
...
const counter = computed({
get: () => store.state.counter,
set: (val) => store.commit('COUNTER_MUTATION', val)
})
...
https://v3.vuejs.org/api/computed-watch-api.html#computed
Try this:
<input v-model="counter">
computed: {
counter: {
get () {
return this.$store.state.a
},
set (value) {
this.$store.commit('updateA', value)
}
}
}
With composition API
When creating computed properties we can have two types, the readonly and a writable one.
To allow v-model to update a variable value we need a writable computed ref.
Example of a readonly computed ref:
const
n = ref(0),
count = computed(() => n.value);
console.log(count.value) // 0
count.value = 2 // error
Example of a writable computed ref:
const n = ref(0)
const count = computed({
get: () => n.value,
set: (val) => n.value = val
})
count.value = 2
console.log(count.value) // 2
So.. in summary, to use v-model with Vuex we need to use a writable computed ref. With composition API it would look like below:
note: I changed the counter variable with about so that the code makes more sense
<script setup>
import {computed} from 'vue'
import {useStore} from 'vuex'
const store = useStore()
const about = computed({
get: () => store.state.about,
set: (text) => store.dispatch('setAbout', text)
})
</script>
<template>
<div>
<h1>{{ about }}</h1>
<input type="text" v-model="about" />
</div>
</template>

vuejs treeselect - delay loading does not work via vuex action

Using Vue TreeSelect Plugin to load a nested list of nodes from firebase backend. It's doc page says,
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId }).
loadOptions (in my App.vue) dispatch vuex action_FolderNodesList, fetches (from firebase) formats (as required by vue-treeselect), and mutates the state folder_NodesList, then tries to update options this.options = this.get_FolderNodesList but this does not seems to work.
Here is the loadOptions method (in app.vue)
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
Vue errors out with Invalid prop: type check failed for prop "options". Expected Array, got String with value ""
I am not sure what am I doing wrong, why that does not work. A working Codesandbox demo
Source
App.vue
<template>
<div class="section">
<div class="columns">
<div class="column is-7">
<div class="field">
<Treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
:auto-load-root-options="false"
placeholder="Select your favourite(s)..."
v-model="value" />
<pre>{{ get_FolderNodesList }}</pre>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import Treeselect from "#riophae/vue-treeselect";
import "#riophae/vue-treeselect/dist/vue-treeselect.css";
export default {
data() {
return {
value: null,
options: null,
called: false
};
},
components: {
Treeselect
},
computed: mapGetters(["get_FolderNodesList"]),
methods: {
loadOptions() {
let getFolderListPromise = this.$store.dispatch("action_FolderNodesList");
getFolderListPromise.then(_ => {
this.options = this.get_FolderNodesList;
});
}
}
};
</script>
Store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
folder_NodesList: ""
},
getters: {
get_FolderNodesList(state) {
return state.folder_NodesList;
}
},
mutations: {
mutate_FolderNodesList(state, payload) {
state.folder_NodesList = payload;
}
},
actions: {
action_FolderNodesList({ commit }) {
fmRef.once("value", snap => {
var testObj = snap.val();
var result = Object.keys(testObj).reduce((acc, cur) => {
acc.push({
id: cur,
label: cur,
children: recurseList(testObj[cur])
});
return acc;
}, []);
commit("mutate_FolderNodesList", result);
});
}
}
});
Any help is appreciated.
Thanks
It seems you are calling this.options which would update the entire element while only the current expanding option should be updated.
It seems loadOptions() is called with some arguments that you can use to update only the current childnode. The first argument seems to contain all the required assets so I wrote my loadTreeOptions function like this:
loadTreeOptions(node) {
// On initial load, I set the 'children' to NULL for nodes to contain children
// but inserted an 'action' string with an URL to retrieve the children
axios.get(node.parentNode.action).then(response => {
// Update current node's children
node.parentNode.children = response.data.children;
// notify tree to update structure
node.callback();
}).catch(
errors => this.onFail(errors.response.data)
);
},
Then I set :load-options="loadTreeOptions" on the <vue-treeselect> element on the page. Maybe you were only missing the callback() call which updates the structure. My installation seems simpler than yours but it works properly now.

How to set state from an external API in Vuex store

In my store.js I init an object
let exchangeRates = {
GBPtoUSD: 0, // American dollar
GBPtoEUR: 0, // Euro
GBPtoCAD: 0, // Canadian dollar
GBPtoINR: 0, // Indian rupee
GBPtoCHF: 0 //Swiss franc
}
Then I am setting its value with
function getExchangeRates() {
return axios.get('https://api.exchangeratesapi.io/latest?base=GBP&symbols=USD,EUR,CAD,CHF,INR')
.then((response)=> {
exchangeRates.GBPtoUSD = response.data.rates.USD;
exchangeRates.GBPtoEUR = response.data.rates.EUR;
exchangeRates.GBPtoCAD = response.data.rates.CAD;
exchangeRates.GBPtoINR = response.data.rates.INR;
exchangeRates.GBPtoCHF = response.data.rates.CHF;
})
.catch((error)=> {console.log(error)})
}
My state gets values assigned like this
state: {
GBPtoUSD: exchangeRates.GBPtoUSD, // American dollar
GBPtoEUR: exchangeRates.GBPtoEUR, // Euro
GBPtoCAD: exchangeRates.GBPtoCAD, // Canadian dollar
GBPtoINR: exchangeRates.GBPtoINR, // Indian rupee
GBPtoCHF: exchangeRates.GBPtoCHF //Swiss franc
}
Then I set up getters, such as
getGBPtoUSD: state => {
return state.GBPtoUSD;
},
The problem is, when later on I am using calling that getter in my component like this
...mapGetters( [ "getGBPtoUSD" ] )
<li class="list-group-item py-3 ">
{{ precise(userInputPrice) }}£ in USD is
{{ precise(userInputPrice * getGBPtoUSD()) }} US dollars
</li>
I am getting getGBPtoUSD as 0 - the initial state.
How do I fix this so that state's value actually get assigned from outside? My approach to doing this clearly does not work.
EDIT1
Ok so doing
exchangeRates.GBPtoUSD = response.data.rates.USD;
console.log(exchangeRates.GBPtoUSD)
logs the correct value of around 1.27. So where am I screwing up?
I've made this example on how to set the state from external service using axios
// main.js
import Vue from "vue";
import App from "./App.vue";
import Vuex from "vuex";
import axios from "axios";
Vue.config.productionTip = false;
Vue.use(Vuex);
const store = new Vuex.Store({
// create the state to store the data
state: {
todo: {}
},
// make an asynchronous call to get the data
actions: {
getTodoItem(state, index) {
return axios.get(`https://jsonplaceholder.typicode.com/todos/${index}`);
}
},
// set data in state
mutations: {
setTodo(state, value) {
Vue.set(state, "todo", value);
}
}
});
new Vue({
store, // loads the store
render: h => h(App)
}).$mount("#app");
<!-- App.vue -->
<script>
import { mapState, mapActions, mapMutations } from "vuex";
export default {
computed: {
...mapState(["todo"]),
hasTodo() {
return Object.keys(this.todo).length;
}
},
methods: {
...mapActions(["getTodoItem"]),
...mapMutations(["setTodo"]),
async getTodo() {
// random number between 1 and 200
const index = Math.floor(Math.random() * 200) + 1;
const { data } = await this.getTodoItem(index);
this.setTodo(data);
}
},
mounted() {
this.getTodo();
}
};
</script>
<template>
<div id="app">
<div v-if="hasTodo">Todo: {{ todo.title }}</div>
<b v-else>Loading..</b>
</div>
</template>
https://codesandbox.io/s/vue-template-y69be