I am having a hard time trying to update my component state based off a reference Id that I am using to display a result that lives in my store/vuex. Basically, what I want to happen is for when I update a reference ID, my getters/computed passes down the updates value. I have provided a simple version of what I am trying to do with no luck. Any ideas?
What am I doing wrong?
Parent Component file
<template>
<div class="parent"
<Car :car="car" />
<div class="car-btn" #click="switchCarId('11')" >Change Car</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import Car from './Car.vue';
export default {
name: 'Car',
components: { Car },
computed: {
...mapGetters(['car'])
},
methods: {
switchCarId(val) {
this.$store.commit('updateCarReferenceId', val);
}
}
};
</script>
Child Component file
<template>
<div class="car">{{ car.name }} </div>
</template>
<script>
export default {
name: 'Car',
props: { 'car': Object }
};
</script>
Store file
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
selectedCarId: '10',
cars: {
'10': {
name: 'Red Car'
},
'11': {
name: 'Blue Car'
}
}
},
mutations: {
updateCarReferenceId(state, val) {
state.selectedCarId = val;
}
},
getters: {
car: (state, getters) => {
return getters.cars[getters.getSelectedCarId];
},
cars: (state) => {
return state.cars;
},
getSelectedCarId: (state) => {
return state.selectedCarId;
}
}
});
I solved this issue. Turns out deep down in a component I was change changing the value based on a input. So no matter what the state read, it only read it initially. Thanks to Anoatoly for helping me run through it and making sure I dotted my t's and crossing my i's.
Related
I have a pinia data store similar to the following code snippet that stores user information and a list of individual orders he is placing:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
})
Also I am using the v-for directive to render the components that should display the individual orders
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in orders" :key="order.id">
<OrderComp />
</div>
</div>
</template>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
How can I access the store data for the individual orders in the child component OrderComp
Basically I want something like this:
OrderComp.vue
<div>
<p>{{ orders.id }}</p>
<input v-model="orders.item" />
<input v-model="orders.price" />
</div>
<script>
import { storeToRefs } from 'pinia'
import { useOrderStore } from "#/store/order";
setup() {
const { orders } = storeToRefs(useOrderStore())
return { orders };
},
</script>
and still keep its reactive state? How does the child component know which order of the orders array to modify? Can/Should I combine the pinia data store with props that pass the data from parent to child? (Though this seems somewhat wrong for me, as pinia is probably able to replace all data passing between components) And furthermore as item and price are bound to input fields, they should of course dynamically change based on a user input.
Based on Estus Flasks comments I got it working by emitting events from the child OrderComp to the parent OrdersComp and on each change it invoked a function that modified my orders array at the correct index in the datastore.
So following the example above I did something like this:
order.js
import { defineStore } from 'pinia'
import { reactive } from 'vue'
export const useOrderStore = defineStore('order', {
state: () => ({
username: '',
orders: reactive([
{
id: '',
item: '',
price: ''
}
])
}),
actions: {
modifyOrder (id, order) {
var foundIndex = this.orders.findIndex(elem => elem.id == order.id)
this.orders[foundIndex] = order
}
}
})
OrdersComp.vue
<template>
<div class="orders_container">
<div v-for="(order, index) in order_store.orders" :key="order.id">
<OrderComp #change="order_store.modifyOrder(index, order)"
v-model:itemProp="order.item"
v-model:priceProp="order.price"
/>
</div>
</div>
</template>
<script>
import { useOrderStore } from "#/store/order";
export default {
setup() {
const order_store = useOrderStore()
return { order_store };
},
}
</script>
Note: I use a wrapper function here to emit the inputs, however you can of course emit it directly e.g. via #input/#change
OrderComp.vue
<div>
<input v-model="item" />
<input v-model="price" />
</div>
<script>
import { useModelWrapper } from "#/modelWrapper";
export default {
name: "OrderComp",´
props: {
itemProp: { type: String, default: "" },
priceProp: { type: String, default: "" },
},
emits: [
"update:itemProp",
"update:priceProp",
],
setup(props, { emit }) {
return {
item: useModelWrapper(props, emit, "itemProp"),
price: useModelWrapper(props, emit, "priceProp"),
};
},
}
modelWrapper.js
import { computed } from "vue";
export function useModelWrapper(props, emit, name = "modelValue") {
return computed({
get: () => props[name],
set: (value) => emit(`update:${name}`, value),
});
}
I have an action in Vuex actions which commit a mutation that it take a payload from the component, that is a number of the index for returning an object, it works fine on Vuex js file meaning that shows the selected item on the console, as I said it gets index from the payload,
but on the component, it gives me Promise <Pending>, why that's happening? for now, I do not use any API for my Nuxt/Vue app, but I will, and for now, I just want to know why this is happening and what is the best solution for solving this
Here my Vuex codes:
export const state = () => ({
articles: [
{
uid: 0,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/1.jpg',
link: '/articles/1',
},
{
uid: 1,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/2.jpg',
link: '/articles/2',
},
],
})
export const getters = {
getArticles(state) {
return state.articles
},
}
export const mutations = {
getSpeceficArticle(state, payload) {
return state.articles[payload]
},
}
export const actions = {
getSpeceficArticle({ commit }, payload) {
commit('getSpeceficArticle', payload)
},
}
and here my component codes:
<template>
<div class="article">
{{ getSpeceficArticle() }}
<div class="article__banner">
<img src="" alt="" />
</div>
<div class="article__text">
<p></p>
</div>
</div>
</template>
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
// computed: {},
methods: {
async getSpeceficArticle() {
return await this.$store.dispatch('articles/getSpeceficArticle', 0)
},
},
}
</script>
actions are used to update the state they are like mutations but the main difference between them is that actions can include some asynchronous tasks, if you want to get a specific article at given index you should use a getter named getArticleByIndex :
export const getters = {
getArticles(state) {
return state.articles
},
getArticleByIndex:: (state) => (index) => {
return state.articles[index]
}
}
then define a computed property called articleByIndex :
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.$store.getters.articles.getArticleByIndex(0)
}
},
methods: {
},
}
</script>
#Mohammad if you find yourself using a lot of getters/actions etc from Vuex and they're starting to get a little wordy, you can bring in mapGetters from Vuex and rename your calls to something a little more convenient. So your script would become,
<script>
import { mapGetters } from 'vuex'
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.getArticleByIndex(0)
}
},
methods: {
...mapGetters({
getArticleByIndex: 'articles/getArticleByIndex',
})
},
}
</script>
You can add ...mapGetters, ...mapActions to your computed section also.
since there is no web service call in vuex action, try to remove async and await keywords from the component.
Later when you add a webservice call than you can wrap action body in new Promise with resolve and reject and then you can use async and await in component. let me know if this works for you.
I've mapped chartData to a state property using vuex. What I'd like to do is update the chart when a dataset is updated. I have heard that it can be done with mixins or watchers but I don't know how to implement it. I understand that mixins creates a watcher but I don't know how it is used within vuex.
Chartline.vue:
<script>
import { Line } from 'vue-chartjs'
import { mapState } from 'vuex'
export default {
name: 'ChartLine',
extends: Line,
computed:{
...mapState(['charData','options'])
},
methods:{
regraph: function(){
this.renderChart(this.charData,this.options);
}
},
mounted () {
this.regraph();
},
watch: {
}
}
</script>
Pension.vue:
<template>
<div id='pension' class="navbarPar">
<ChartLine/>
</div>
</template>
<script>
import ChartLine from '../components/ChartLine.vue';
import { mapState } from 'vuex'
//import { Line } from 'vue-chartjs'
export default {
name: 'Pension',
components: {
ChartLine,
},
data(){
return{
form: {
...
},
var:{
...
},
}
},
methods: {
calculate: function(indice){
...
//modify data of mapState
//after here, I want to rerender chart
}
},
computed:{
...mapState(['charData','options']),
},
}
</script>
Using a watcher like this should be enough:
<script>
import { Line } from "vue-chartjs";
import { mapState } from "vuex";
export default {
name: "ChartLine",
extends: Line,
computed: {
...mapState(["chartData", "options"])
},
methods: {
regraph() {
this.renderChart(this.chartData, this.options);
}
},
mounted() {
this.regraph();
},
watch: {
chartData: {
handler: this.regraph,
deep: true
}
}
};
</script>
Also having the explicit vuex state map inside the ChartLine component seems a bit wasteful - passing the vuex data through props would render the component more generic:
<template>
<div id='pension' class="navbarPar">
<ChartLine :options="options" :chart-data="chartData"/>
</div>
</template>
<script>...
Chartline.vue:
<script>
import { Line } from "vue-chartjs";
export default {
name: "ChartLine",
extends: Line,
props: {
options: {
type: Object,
default: () => ({})
},
chartData: {
type: Object /*is it?*/,
default: () => ({})
}
},
methods: {
regraph() {
this.renderChart(this.chartData, this.options);
}
},
mounted() {
this.regraph();
},
watch: {
chartData: {
handler: this.regraph,
deep: true
}
}
};
</script>
If you are using vue-chartjs, the library has its own way to handle reactive data in charts:
// ChartLine.js
import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: Line,
mixins: [reactiveProp],
props: ['options'], // passed from the parent
mounted () {
// this.chartData is created in the mixin (pass it as any prop with :chart-data="data").
this.renderChart(this.chartData, this.options)
}
}
Now the Pension.vue file
// Pension.vue
<template>
<div id='pension' class="navbarPar">
<ChartLine :chart-data="charData" :options="options" />
</div>
</template>
<script>
import ChartLine from '../components/ChartLine';
import { mapState } from 'vuex'
export default {
name: 'Pension',
components: {
ChartLine,
},
data(){
return{
form: {
...
},
var:{
...
},
}
},
methods: {
calculate: function(indice){
...
//modify data of mapState
//after here, I want to rerender chart
}
},
computed:{
...mapState(['charData','options']),
},
}
</script>
You can read more about it here: https://vue-chartjs.org/guide/#updating-charts,
there are some caveats
I am trying to get the changed value of vuetify v-select by using $emit but it doesn't work.
I divided components by applying atomic design pattern (atoms(child component and not to connect with the store), organisms(parent component)) and vuex stores.
I think $emit data is OK but anything doesn't work after the process.
This is for a new application for management page with
using vue, vuex, vuetify, atomic design connecting to API server.
Components
child component - in atoms folder
<template>
<v-select
:items="list"
:label="label"
v-model="selected"
item-value="id"
item-text="name"
return-object
#change="changeSelected"
></v-select>
</template>
<script>
export default {
props: ["list", "label", "defaultSelected"],
data() {
return {
selected: this.defaultSelected
};
},
methods: {
changeSelected(newValue) {
console.log(newValue); // display changed new data
this.$emit("changeSelected", newValue);
}
}
};
</script>
parent component - in organisms folder
<template>
<v-select-child
:select-label="label"
:select-list="list"
:default-selected="selected"
#change-selected="changeSelected" // problem issue?
>
</v-select-child>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
...
},
computed: {
...mapState({
list: state => state.list
})
},
methods: {
changeSelected() {
console.log("changeSelected"); // doesn't work
this.$store.dispatch("setSelected", { payload: this.selected });
}
}
};
</script>
vuex stores
index.js
export default new Vuex.Store({
modules: {
xxx
},
state: {
list: [
{
name: "aaaaa",
id: "001"
},
{
name: "bbbbb",
id: "002"
}
]
},
getters: {},
mutations: {},
actions: {}
});
xxx.js
export default {
selected: { id: "001" }
},
getters: {
//
},
mutations: {
updateSelected(state, payload) {
console.log("payload"); // doesn't work
console.log(payload);
state.selected = payload;
console.log(state.selected);
}
},
actions: {
setSelected({ commit }, payload) {
console.log("Action"); // doesn't work
commit("updateSelected", payload);
}
}
};
It does not print any console log after changeSelected function.
In document
Unlike components and props, event names don’t provide any automatic
case transformation. Instead, the name of an emitted event must
exactly match the name used to listen to that event.
That means if you emit event like $emit('changeSelected'), then you need to use #changeSelected. #change-selected will not work.
<v-select-child
:select-label="label"
:select-list="list"
:default-selected="selected"
#changeSelected="changeSelected"
>
</v-select-child>
I found a solution below:
child component
<template>
<v-select
:label="label"
:items="list"
v-model="selected"
item-value="id"
item-text="name"
return-object
></v-select>
</template>
<script>
export default {
props: ["list", "label", "defaultSelected"],
computed: {
selected: {
get() {
return this.defaultSelected;
},
set(newVal) {
if (this.selected !== newVal) {
this.$emit("changeSelected", newVal);
}
}
}
}
};
</script>
parent component
<template>
<v-select-child
:label="label"
:list="list"
:defaultSelected="selected"
#changeSelected="changeSelected" // fix the property using camelCase
></v-select-child>
</template>
<script>
import { mapState } from "vuex";
export default {
data() {
...
},
computed: {
...mapState({
list: state => state.list
})
},
methods: {
changeSelected(val) { // val: changed object value
this.$store.dispatch("setSelected", { id:val.id });
}
}
};
</script>
You can also use watch;
<v-select
:label="label"
:items="list"
v-model="selected"
item-value="id"
item-text="name"
></v-select>
</template>
...
watch:{
selected(){
this.$emit('changeValue', this.selected.id');
}
}
...
and from parent;
<child #changeValue="id = $event" .. />
I have a component (modal) which relies on a store. The store has the state of the modal component - whether it is active or not.
I need to be able to call this modal to open from other components or even just on a standard link. It opens by adding an .active class.
How can I change the state of the store - either by calling the stores action or calling the modal components method (which is mapped to the store).
Modal Store:
class ModalModule {
constructor() {
return {
namespaced: true,
state: {
active: false,
},
mutations: {
toggleActive: (state) => {
return state.active = ! state.active;
},
},
actions: {
toggleActive({ commit }) {
commit('toggleActive');
},
},
getters: {
active: state => {
return state.active;
}
}
};
}
}
export default ModalModule;
Vue Component:
<template>
<div class="modal" v-bind:class="{ active: active }">
<div class="modal-inner">
<h1>modal content here</h1>
</div>
<div class="modal-close" v-on:click="this.toggleActive">
X
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
computed: {
...mapGetters('Modal', [
'active',
])
},
methods: {
...mapActions('Modal', [
'toggleActive',
]),
}
}
</script>
And somewhere else I want to be able to have something like:
<button v-on:click="how to change the state??">OPEN MODAL</button>
Edit:
Here's the store:
import Vuex from 'vuex';
import ModalModule from './ModalModule';
class Store extends Vuex.Store {
constructor() {
Vue.use(Vuex);
super({
modules: {
Modal: new ModalModule(),
}
});
};
}
You do not need an action for your particular usecase . You just just define a mutation as you are just changing the boolean value of a property in a state. Actions are for async functionality. You usecase is just synchronous change of Boolean value
So you can do
<button v-on:click="$store.commit('toggleActive')">OPEN MODAL</button>
EDIT:
Just export a plain object
const ModalModule = {
namespaced: true,
state: {
active: false,
},
mutations: {
toggleActive: (state) => {
return state.active = ! state.active;
},
},
actions: {
toggleActive({ commit }) {
commit('toggleActive');
},
},
getters: {
active: state => {
return state.active;
}
}
}
export default ModalModule;// export the module
Even remove the class based definition of the store
import Vue from 'vue'
import Vuex from 'vuex';
import ModalModule from './ModalModule';
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: {
ModalModule
}
});
And change it like this in you component for mapping of the mutation (<MODULE_NAME>/<MUTATION_NAME>)
...mapMutations([
'ModalModule/toggleActive'
])
You can access the store from your components via this.$store. There you can call your actions and mutations. So
<button v-on:click="$store.commit('your mutation name', true)">OPEN MODAL</button>