typescriptvuejs3vue-componentquasar-framework

Vue emit from child and update state in parent


I'm trying to learn Vue and want to build a parent component of shopping list (my training excercise). The parent will manage the state and it contains two child components: 1) the table with products and prices for each product 2) the form to add a new item.

I have the following ShoppingListComponent.vue:

<template>
  <table>
    <thead>
      <tr>
        <th>Product</th>
        <th>Price</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="product in products" :key="product.id">
        <td>{{ product.name }}</td>
        <td>{{ product.price }}</td>
      </tr>
    </tbody>
  </table>
</template>

<script setup lang="ts">
import type { Product } from './models';

interface Props {
  products: Product[];
}

defineProps<Props>();
</script>

The Product interface is simply:

export interface Product {
  id: number;
  name: string;
  price: number;
}

The AddProductFrom.vue:

<!-- I use quasar ->
<template>
  <div class="row q-gutter-sm">
    <q-input filled v-model="product.name" label="Name" />
    <q-input filled v-model="product.price" label="Price />
    <q-btn color="black" text-color="white" label="Add" @click="$emit('addRow', product)" />
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { type Product } from './models';

const product = ref<Product>({
  id: 1,
  name: '',
  price: 0,
});

defineEmits<{
  addRow: [item: Product];
}>();
</script>

This "works" and adds a row to the table, but there is still some sort of reference because when I add a product and then change the contents of the form the data in the table changes as well.

What I want is to be able to add a product, clear the form and add a new product (what you would expect). How can I solve this?


Solution

  • When you updated the form fields after emitting, it modified the object that was already in the parent's products array (since it was the same reference). By creating a new object on emit and resetting the form, you ensure that each added product is a distinct object, and the form starts fresh each time.

    AddProductForm.vue

    <template>
          <div class="row q-gutter-sm">
            <q-input filled v-model="product.name" label="Name" />
            <q-input filled v-model="product.price" label="Price" />
            <q-btn color="black" text-color="white" label="Add" @click="addProduct" />
          </div>
    </template>
    
    <script setup lang="ts">
          const emit = defineEmits<{
            addRow: [item: Product];
          }>();
        
          const product = ref<Product>({
            id: 0,
            name: "",
            price: 0,
          });
        
          function addProduct() {
            emit("addRow", { ...product.value });
        
            product.value = {
              id: 0,
              name: "",
              price: 0,
            };
          }
    </script>
    

    ShoppingListComponent.vue

    <template>
          <div>
            <ShoppingListComponent :products="products" />
            <AddProductForm @addRow="addProduct" />
          </div>
    </template>
    
    <script setup lang="ts">
          const products = ref<Product[]>([
            { id: 1, name: "Apple", price: 0.5 },
            { id: 2, name: "Banana", price: 0.3 },
          ]);
        
          function addProduct(newProduct: Product) {
            const id = products.value.length + 1;
            products.value.push({ ...newProduct, id });
          }
    </script>