How to use pinia store with v-for directive whilst keeping it reactive?

Viewed 1158

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.

1 Answers

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

Related