Vue 3, Pinia - child component is not updated when the store is updated - vue.js

I have this Pinia store:
import { defineStore } from 'pinia'
import axiosClient from '#/axios'
export const useTableOrderStore = defineStore( 'tableOrders', {
id : 'tableOrders',
state: () => {
return {
tableOrders: []
}
},
actions: {
addToOrder(item, quantity)
{
const searchIndex = this.tableOrders.findIndex((order) => order.id == item.id);
if(searchIndex !== -1)
{
this.tableOrders[searchIndex].quantity += quantity
}
else
{
item.quantity = quantity
this.tableOrders.push(item)
}
}
}
})
Parent component:
<script setup>
import {ref} from "vue"
import {useTableOrderStore} from "#/store/tableOrder";
import CounterInput from "#/components/Inputs/Counter.vue"
const tableOrderStore = useTableOrderStore()
const props = defineProps(['product'])
let quantity = ref(1)
let addToOrder = (product) => {
tableOrderStore.addToOrder(product, quantity.value)
quantity.value = 1
}
</script>
<template>
<div class="input-group">
<counter-input :quantity="quantity"
#quantity-event="(n) => quantity = n"></counter-input>
<button class="btn btn-primary btn-sm ms-1" #click="addToOrder(product)">
Add <font-awesome-icon icon="fas fa-receipt" class="ms-2" />
</button>
</div>
</template>
Child component:
<script setup>
import {ref} from "vue"
let props = defineProps({
quantity: {
type: Number,
required: true
}
})
let count = ref(props.quantity)
let increase = () => {
count.value++
emit('quantityEvent', count.value)
}
let decrease = () =>
{
if(count.value === 1)
return
count.value--
emit('quantityEvent', count.value)
}
const emit = defineEmits(['quantityEvent'])
</script>
<template>
<span class="input-group-btn">
<button type="button"
class="btn btn-danger btn-number"
data-type="minus"
#click="decrease"
>
<font-awesome-icon icon="fas fa-minus-circle" />
</button>
</span>
<input type="text"
name="quantity"
:value="count"
class="form-control input-number"
min="1"
>
<span class="input-group-btn">
<button type="button"
class="btn btn-success btn-number"
data-type="plus"
#click="increase"
>
<font-awesome-icon icon="fas fa-plus-circle" />
</button>
</span>
</template>
The first time method addToOrder is fired, the product is correctly added and child product renders is.
The first issue here is that the quantity is set to 1, but it is not set in the child component.
The second problem is with the quantity - first addToOrder is ok, and quantity is shown correctly, but if new quantity is added the Pinia store is updated, but it is not reflected in the component. What am I doing wrong here?

I guess you run into an vuejs caveat.
this.tableOrders[searchIndex].quantity += quantity
Vue cannot detect the following changes to an array:
When you directly set an item with the index, e.g.
vm.items[indexOfItem] = newValue When you modify the length of the
array, e.g. vm.items.length = newLength
You directly set an item.
Instead, you could use .splice() to replace your item:
let newItem = {
...this.tableOrders[searchIndex],
quantity: this.tableOrders[searchIndex].quantity + quantity
};
//replace old item with new
this.tableOrders.splice(searchIndex, 1, newItem)
Here are the mutation methods that triggers an update:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
The first time you use addToOrders works because you used .push() witch is one of the mutations that triggers the re-render.

First issue, i dont know how props.quantity is not set in the child component. Try console props.quantity right after you defined props if it displayed the result as you want then try to console it inside watch method. If props.quantity has changed but your child component is not update then your child component somehow not updated. You can try to force update it and here's how: https://medium.com/emblatech/ways-to-force-vue-to-re-render-a-component-df866fbacf47 (Using the key changing technique)
Second issue, i think this one #quantity-event="(n) => quantity = n" should be #quantity-event="(n) => { quantity = n }"

Related

cannot remove items from cart in vue 3 with pinia

so in the console log it is saying it is being removed from the cart but i can still see the items in the cart... how can i remove them from the cart? im using pinia for state management and the cart is in the state. why it is not working for me?
the code:
shop.vue
<template>
<div class="shop">
Cart Items: <cart-badge :count="cartLength">{{ count }}</cart-badge>
<h1>shop</h1>
<div class="products" v-for="item in Products" :key="item.id">
{{ item.name }} {{ item.price }}$
<button #click="storeCounter.addToCart(item)">Add to Cart</button>
</div>
</div>
</template>
<script setup>
import { useCounterStore } from "../stores/counter";
import Products from "../db.json";
import CartBadge from "../components/CartBadge.vue";
import { computed } from "vue";
const storeCounter = useCounterStore();
const cartLength = computed(() => {
return storeCounter.cart.length;
});
</script>
store.js(pinia)
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
cart: [],
}),
actions: {
addToCart(id) {
this.cart.push(id);
console.log("test passed!");
},
removeFromCart(id) {
this.cart.splice(id);
console.log("removed from cart!");
},
},
});
cart.vue
<template>
<div class="cart">
<h1>cart</h1>
<div class="cartitems" v-for="item in storeCounter.cart" :key="item.id">{{ item.name }} {{ item.price }}$
<button #click="storeCounter.removeFromCart(item.id)">X</button>
</div>
</div>
</template>
<script setup>
import { useCounterStore } from '../stores/counter';
const storeCounter = useCounterStore()
</script>
Splice uses the index of the item to delete it.
Do this instead;
removeFromCart(id) {
let cartItemIndex = this.cart.findIndex(x => x.id === id);
if (cartItemIndex >= 0) {
this.cart.splice(cartItemIndex, 1);
console.log("Removed from cart");
}
}
or if you don't want to use splice anymore (and if performance matters);
removeFromCart(id) {
this.cart = this.cart.filter(x => x.id !== id);
console.log("Removed from cart");
}
Please use more descriptive parameter names. Rename "id" to "item" in addToCart, since you're not just adding the item id, but the entire item. This can be confusing to other developers and make your code unreadable.
addToCart(item) {
this.cart.push(item)
console.log('test passed!')
}
References: splice filter

How can I hide the other elements when one dropdown menu is click

I'm still new to Vue.js so far I'm liking it. Currently stuck on drop menus.
If one drop down menu is click is there a way to hide the other menus that are open?
<script setup>
import { ref } from 'vue';
const hideshow1= ref(false);
const hideshow2= ref(false);
const hideshow3= ref(false);
function show1() {
this.hideshow1= !this.hideshow1;
};
function show2() {
this.hideshow2= !this.hideshow2;
};
function show3() {
this.hideshow3= !this.hideshow3;
};
</script>
<template>
<button #click="show1()" type="button"> show</button>
<button #click="show2()" type="button"> show</button>
<button #click="show3()" type="button"> show</button>
<div :class="{'block':hideshow1, 'hidden': ! hideshow1}" class="sm:hidden ">show1</div>
<div :class="{'block':hideshow2, 'hidden': ! hideshow2}" class="sm:hidden ">show2</div>
<div :class="{'block':hideshow3, 'hidden': ! hideshow3}" class="sm:hidden ">show3</div>
</template>
Maybe I'm grossly over-simplifying your problem, but as I mentioned in comment, why not change your apps data state with the buttons, and then use a v-if to check state of your data, toggling visibility. The data probably should be an array of objects, perhaps something like this:
<template>
<h2>Show Hide Menus</h2>
<button v-for="item in hideShows" :key="item.text" #click="show(item)">
Button {{ item.text }}
</button>
<div v-for="item in hideShows" :key="item.text">
<span v-if="item.value">Show {{ item.text }}</span>
</div>
</template>
<script setup>
import { ref } from 'vue';
const hideShows = ref([
{
text: "1",
value: true
},
{
text: "2",
value: false
},
{
text: "3",
value: false
}
]);
function setAllFalse() {
hideShows.value.forEach(hideShow => {
hideShow.value = false;
})
}
function show(item) {
setAllFalse();
item.value = !item.value;
}
</script>

Emit data from Child to parent vue 3

I have a SearchBar.vue child page with a form in this this code :
<template>
<div>
<form class="search-bar" #submit.prevent="SearchMovies()">
<input
type="text"
placeholder="Effectuez une recherche"
v-model="search"
/>
<button
type="submit"
class="search-input"
#click="$emit('get-movies', movies)"
>
CHERCHER
</button>
</form>
</div>
</template>
And my SearchMovies() function looks like :
setup() {
const search = ref("");
const movies = ref([]);
function SearchMovies () {
if (search.value != "") {
fetch(`${process.env.VUE_APP_API_URL_CALL_TWO}${search.value}`)
.then((response) => response.json())
.then((data) => {
movies.value = data.contents;
search.value = "";
console.log(
"Movies data from SearchBar.vue when fired up: ",
movies.value
);
});
}
this.$emit('get-movies', movies)
}
This is how I have tried to add the emit line
this.$emit('get-movies', movies)
And I receive the emitted data from SearchMovies() function to my parent Home.vue page like this :
<template>
<div>
<router-link to="/" href="/"
><img class="logo-img" alt="App logo" src="../assets/logo.png"
/></router-link>
<SearchBar #get-movies="getMovies($event)" />
<MovieList :movies="movies" />
</div>
</template>
methods: {
getMovies: function (movies) {
(this.movies = movies),
console.log("Movies data from Home.vue when fired up: ",
movies);
},
},
The problem is that I am not getting the movies data and when I console.log it in the Home.vue page
Movies data from Home.vue when fired up: Proxy {}
In your search bar, the #click event is never actually invoking the SearchMovies method. Try converting
<button type="submit" class="search-input" #click="searchMovies">...</button>
You're not exporting the function in your setup, at the bottom of setup
setup (_, {emit}) {
const search = ref("")
const movies = ref([])
const SearchMovies = () => {
const value = await fetch(`${process.env.VUE_APP_API_URL_CALL_TWO}${search.value}`)
const data = await value.json()
movies.value = data.contents
search.value = ""
console.log("Movies data from SearchBar.vue when fired up: ", movies.value);
emit('get-movies', movies.value)
}
return { search, movies, SearchMovies }
}
In your fetch statement, you're going to have some async code issue, the fetch statement will run, but then it will skip the await callbacks in favor of doing this.$emit. I'd convert it to
Then, finally, I wouldn't catch the value in Home.vue with
<SearchBar #get-movies="getMovies($event)" />
Instead, just use #get-movies="getMovies" You don't actually need make it call a function, it will just do it on it's own and I find trying to use the event bus causes confusion sometimes. You only need to use it if you have specific data from the template you could pass into it, like in a v-for loop you could pass in the specific object. Let me know if you need me to clarify anything so you can better understand why it's built like this.
<template>
<div class="search-bar">
<input
type="text"
placeholder="Effectuez une recherche"
v-model="search"
/>
<button
class="search-input"
#click="SearchMovies"
>
CHERCHER
</button>
</div>
</template>

accordion only show one item at same time in vue js

//this code in vue js for accordion this is working but not show open only //one at the same time.
//there component and pass props data.
<packing-material-category v-for="(category, index) in packingMaterialCategories" :category="category" :key="index"></packing-material-category>
//there template to render data.
<template>
<div class="packing-categories">
<div :class="packingCategoryClass">
<h3 class="packing-category__title" #click="toggleAccordion(category.title)" v-text="category.title" />
<div v-show="accordionOpen===true" class="packing-category__content">
<div v-if="category.description" class="packing-category__description" v-text="category.description" />
</div>
</div>
</div>
//call method for open and close
toggleAccordion() {
this.accordionOpen = !this.accordionOpen;
}
You need to track which item is shown outside of packing-material-category component (value of openedIndex) and to pass that value down to components. When value changes in the packing-material-category component, you emit event change-accordion, and in your parent component you update value of openedIndex
Try this
In your parent component, add openedIndex: 0 to data.
If you want everything closed by default, set value to false.
Update template to pass down the props index and openedIndex, so that components know when to show/hide.
<packing-material-category
v-for="(category, index) in packingMaterialCategories"
:key="index"
:index="index"
:openedIndex="openedIndex"
:category="category"
#change-accordion="(newOpen) => openedIndex = newOpen"
>
</packing-material-category>
And the packing-material-category component could look like this
<template>
<div class="packing-categories">
<div :class="packingCategoryClass">
<h3 class="packing-category__title" #click="toggleAccordion" v-text="category.title" />
<div v-show="index === openedIndex" class="packing-category__content">
<div v-if="category.description" class="packing-category__description" v-text="category.description" />
</div>
</div>
</div>
</template>
<script>
export default {
props: [
'category',
'index', // index of this component
'openedIndex', // which item should be shown/opened
],
data() {
return {
packingCategoryClass: '',
}
},
methods: {
toggleAccordion() {
// Show current item. If already opened, hide current item
let value = this.index === this.openedIndex ? false : this.index;
// notify parent component about the change
this.$emit('change-accordion', value)
}
}
}
</script>

Vue.js - v-for indicies mixed up?

What do I have: components structure
<Games> // using v-for - iterate all games
--<Game> // using v-for - iterate all players
----<Player 1>
------<DeleteWithConfirmation>
----<Player 2>
------<DeleteWithConfirmation>
----<Player 3>
------<DeleteWithConfirmation>
----...
<DeleteWithConfirmation> implementation: two clicks are required for deleting game property.
<template>
<div>
<button #click="incrementDelete"
v-html="deleteButtonHTML"></button>
<button v-if="deleteCounter === 1" #click="stopDeleting">
<i class="undo icon"></i>
</button>
</div>
</template>
<script>
export default {
name: 'DeleteWithConfirmation',
data() {
return {
deleteCounter: 0
}
},
computed: {
deleteButtonHTML: function () {
if (this.deleteCounter === 0)
return '<i class="trash icon"></i>'
else
return 'Are you sure?'
}
},
methods: {
incrementDelete() {
this.deleteCounter++
if (this.deleteCounter === 2) {
//tell parent component that deleting is confirmed.
//Parent call AJAX than.
this.$emit('deletingConfirmed')
this.stopDeleting()
}
else if (this.deleteCounter > 2)
this.stopDeleting()
},
stopDeleting() {
Object.assign(this.$data, this.$options.data())
}
}
}
</script>
My problem: seems like indicies are mixed up:
Before deleting 4th player was on "Are you sure state" (deleteCounter === 1), but after deleting it went to initial state (deleteCounter === 0). Seems like 3rd component state haven't updated its deleteCounter, but its data (player's name was updated anyway).
After successfull deleting <Games> component data is fetched again.
You don't need a delete counter for achieving this. On the contrary, it makes it hard to understand your code. Just use a boolean like this:
<template>
<div>
<button #click="clickButton"
<template v-if="confirmation">
<i class="trash icon"></i>
</template>
<template v-else>
Are you sure?
</template>
</button>
<button v-if="confirmation" #click="confirmation = false">
<i class="undo icon"></i>
</button>
</div>
</template>
<script>
export default {
name: 'DeleteWithConfirmation',
data() {
return {
confirmation: false
}
},
methods: {
clickButton() {
if (!this.confirmation) {
this.confirmation = true;
} else {
this.$emit('deleting-confirmed');
}
}
}
</script>
The parent could then be looking e.g. like this:
<div class="row" v-if="showButton">
[...]
<delete-with-confirmation #deleting-confirmed="showButton = false">
</div>
One of the answers was deleted, I wish I could mention the initial author, but I don't remeber his username, so (changed a bit):
incrementDelete() {
if (this.deleteCounter === 1) { // 1 because there is "post-increment" at the end of the fucntion
this.deletingProgress = true
this.$emit('deletingConfirmed')
this.stopDeleting()
}
else if (this.deleteCounter > 1) // 1 because there is "post-increment" at the end of the fucntion
this.stopDeleting()
this.deleteCounter++ // "post-increment"
},
ssc-hrep3 answer is more clean (and lightweight) than mine approach, link.