Tips for updating parent data with multiple values from a child component

Check out the code snippet below.

I seem to be stuck on a crucial component. Despite going through the documentation and watching tutorials on Vue2, I can't seem to grasp this key element. Any assistance would be greatly appreciated. If my approach is completely off, please feel free to point that out as I'm open to suggestions.

Desired functionality: I have a Vue instance named order which contains line items.

Upon order.mounted(), we make an API call to fetch the order data, including any existing line items. If there are existing line items, we update the order data with this information (

this.lineitems = request.body.lineitems
or something similar). This part is working correctly, and I can calculate the order total since the line items are updated at this stage.

Each line item consists of an editable form with a quantity and a product field. Whenever the quantity or product of a line item is changed, I want the child component (line-item) to notify the parent component of the change. Subsequently, the parent component should update its line items data array with the new values and make a POST request with the current line item data to allow the server to recalculate the total (considering specials, discounts, etc). The updated line item data array returned by the server will then be passed down to re-render the line items.

Issues:

  1. The "update..." methods in the line-items components don't feel right, and I'm struggling to figure out how to update the parent's line items data array with the new data. For example:

lineitems = [
  {id: 1000, quantity: 3, product: 555, total: 30.00}, 
  {id: 1001, quantity: 2, product: 777, total: 10.00}
]

If the quantity of the second line item is changed to 1, how do I update the parent's line items data accordingly? My main challenge is understanding how the parent component can identify which line items in its data array need to be modified and how to retrieve data from the changed child. I assume the data comes in through an event emitted by the child component, but do I need to pass around the primary key everywhere for comparisons? What if it's a new line item without a primary key yet?

  1. As mentioned above, I'm using the existing line item's DB primary key as the key in v-for. How should I handle adding a "new line item" that appends a blank line item below existing ones or in cases of a new order without primary keys?

  2. Is there a recommended practice for handling props other than the "initial..." style I'm using? Should I directly use $emit on the v-on, and if so, how can I pass the relevant information through?

This task seems tailor-made for VueJS, yet I'm feeling like I'm going in circles. Any assistance would be greatly appreciated!

LineItem

Vue.component('line-item', {
    props: ["initialQuantity", "initialProduct", "total"],
    data () {
        return {
            // More properties here, but kept to a minimum for example
            quantity: initialQuantity,
            product: initialProduct,
            productOptions = [
                { id: 333, text: "Product A"},
                { id: 555, text: "Product B"},
                { id: 777, text: "Product C"},
            ]
        }
    },
    updateQuantity(event) {
        item = {
            quantity: event.target.value,
            product: this.product
        }
        this.$emit('update-item', item)
    },
    updateProduct(event) {
        item = {
            quantity: this.quantity,
            product: event.target.value
        }
        this.$emit('update-item', item)
    }
    template: `
        <input :value="quantity" type="number" @input="updateQuantity">

        <select :value="product" @input="updateProduct">
            <option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
        </select>

        Line Item Price: {{ total }}
        <hr />
    `
})

Order/App

var order = new Vue({
    el: '#app',
    data: {
        orderPK: orderPK,
        lineitems: []
    },
    mounted() {
        this.fetchLineItems()
    },
    computed: {
        total() {
            // Should calculate the sum of line items, something like (li.total for li in this.lineitems)
            return 0.0
    },
    methods: {
        updateOrder(item) {
            // First, update this.lineitems with the passed item, then
            fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems)
                .then(resp => resp.json())
                .then(data => {
                    this.lineitems = data.lineitems;
                })
        },
        fetchLineItems() {
            fetch(`domain.com/api/orders/${this.orderPK}`)
                .then(resp => resp.json())
                .then(data => {
                    this.lineitems = data.lineitems;
                })
        },
    },
    template: `
        <div>
            <h2 id="total">Order total: {{ total }}</h2>

            <line-item v-for="item in lineitems"
                @update-item="updateOrder"
                :key="item.id"
                :quantity="item.quantity"
                :product="item.product"
                :total="item.total"
                ></line-item>
        </div>
    `
})

Answer №1

Below are some issues with your attempt that could result in nothing being displayed:

  1. quantity: initialQuantity, should be written as quantity: this.initialQuantity, ... and so on for other similar data
  2. missing } for computed total
  3. the line-item template is invalid - it contains multiple "root" elements

Additionally, there are some minor problems:

  1. Use the @change handler instead of @input for the select. Running the code will show the difference.
  2. Similarly, use @change for input to prevent fetch requests for every keystroke.

Despite these issues, I have created functional code that meets your requirements - mostly for my own learning purposes, to be honest :p

// ******** some dummy data and functions to emulate fetches
const products = [
    { id: 333, text: "Product A", unitPrice: 10},
    { id: 555, text: "Product B", unitPrice: 11},
    { id: 777, text: "Product C", unitPrice: 12},
];

let dummy = [
    {id: 1, quantity:2, product: 333, total: 20},
    {id: 2, quantity:3, product: 777, total: 36},
];

const getLineItems = () => new Promise(resolve => setTimeout(resolve, 1000, JSON.stringify({lineitems: dummy})));
const update = items => {
    return new Promise(resolve => setTimeout(() => {
        dummy = JSON.parse(items);
        dummy.forEach(item => 
            item.total = parseFloat(
                (
                    item.quantity * 
                    (products.find(p => p.id === item.product) || {unitPrice: 0}).unitPrice *
                    (item.quantity > 4 ? 0.9 : 1.0)
                ).toFixed(2)
            )
        );
        let res = JSON.stringify({lineitems: dummy});
        resolve(res);
    }, 50));
}

//********* lineItem component
Vue.component('line-item', {
    props: ["value"],
    data () {
        return {
            productOptions: [
                { id: 333, text: "Product A"},
                { id: 555, text: "Product B"},
                { id: 777, text: "Product C"},
            ]
        }
    },
    methods: {
        doupdate() {
            this.$emit('update-item', this.value.product);
        }
    },
    template: `
        <p>
            <input v-model="value.quantity" type="number" @change="doupdate()"/>

            <select v-model="value.product" @change="doupdate()">
                <option v-for="option in productOptions" v-bind:value="option.id"> {{ option.text }} </option>
            </select>

            Line Item Price: {{ '$' + value.total.toFixed(2) }}
        </p>
    `
})

//********* Order/App
const orderPK = '';
var order = new Vue({
    el: '#app',
    data: {
        orderPK: orderPK,
        lineitems: []
    },
    mounted() {
        // initial load
        this.fetchLineItems();
    },
    computed: {
        carttotal() {
            return this.lineitems.reduce((a, {total}) => a + total, 0)
        }
    },
    methods: {
        updateOrder(productCode) {
            // only call update if the updated item has a product code
            if (productCode) {
                // real code would be
                // fetch(`domain.com/orders/${this.orderPK}/calculate`, this.lineitems).then(resp => resp.json())
                // dummy code is
                update(JSON.stringify(this.lineitems)).then(data => JSON.parse(data))
                
                .then(data => this.lineitems = data.lineitems);
            }
        },
        fetchLineItems() {

            // real code would be
            //fetch(`domain.com/api/orders/${this.orderPK}`).then(resp => resp.json())
            // dummy code is
            getLineItems().then(data => JSON.parse(data))
            
            .then(data => this.lineitems = data.lineitems);
            
        },
        addLine() {
            this.lineitems.push({
                id: Math.max([this.lineitems.map(({id}) => id)]) + 1, 
                quantity:0, 
                product: 0, 
                total: 0
            });
        }
    },
    template: `
        <div>
            <h2 id="total">Order: {{lineitems.length}} items, total: {{'$'+carttotal.toFixed(2)}}</h2>

            <line-item v-for="(item, index) in lineitems"
                :key="item.id"
                v-model="lineitems[index]"
                @update-item="updateOrder"
            />
            <button @click="addLine()">
                Add item
            </button>
        </div>
    `
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
        <div id="app">
        </div>

Please note: there might be some inefficient code in there, as I am relatively new to using Vue.js for just a week.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Retrieve Gridview properties using JavaScript

I need to adjust the font size of my gridview using JavaScript to make it more suitable for printing. What is the best way to change the font size specifically for a gridview using JavaScript? ...

Display the closest locations on the Google Maps interface

I am currently working on a project that involves integrating Google Maps. The project includes storing hospital addresses (in longitude and latitude) in a database. However, I need assistance in displaying the nearest hospital from my current location. I ...

Utilizing Radio Buttons for Table Selection in a React Application

Currently, I am utilizing React, MUI, and Formik in my project. My task involves implementing a table where only one radio button can be selected per row. How can I achieve this functionality? If you want to take a look at my code, it's available on ...

Passing a JavaScript function as an argument

In the process of developing a JavaScript application, I am tasked with creating a handler that can accept a function as a parameter: Here is an example function: function myFunction() { alert("hello world"); } This is the handler implementation: func ...

What is the process for activating and deactivating the scroll trigger within Material UI's useScrollTrigger module?

I'm currently working on setting up a survey page with Material UI in React. My goal is to have the survey questions appear when the user scrolls over them and disappear when they scroll out of view, similar to the behavior on this page. After some r ...

View the edited image preview instantly upon selecting the image

I have selected an image and previewed it before submitting the form. However, now I wish to be able to edit the file immediately after selecting it, preview the changes, and then submit the file. <input type ="file" accept="image/*" id="image" name="i ...

How can I implement the self-clearing feature of a timer using clearInterval in ReactJS?

I am a newcomer to the world of React and I am currently working on creating a React component with multiple features. Users can input a random number, which will then be displayed on the page. The component includes a button labeled 'start'. W ...

What are the steps for publishing a Three.js project on github pages?

Currently, I've been putting together my own personal portfolio website which involves using the Three.js library. The library has been downloaded onto my laptop and when running locally, everything works smoothly. Now, I want to move forward by deplo ...

javascript - disable screen capture of specific parts of a webpage

Recently, I've come across certain web pages that have a unique feature preventing screen capture of specific regions. When attempting to take a screenshot using Chrome, some areas that are visible on the live page appear as black frames in the captur ...

Istanbul provides me with a thorough analysis, yet it always seems to conclude with an error

Currently, I am experimenting with a basic application (found in the Mocha tutorial code available at ) to troubleshoot why Istanbul is giving me trouble. The issue is that Istanbul successfully generates a coverage summary but then throws an error for unk ...

Comparing json results from ng-repeat against an array

Currently, I am working with a JSON result that appears in an ng-repeat and I want to filter it based on separate data objects or arrays: Controller.js $scope.jsonResult = [ { "id": "a123" }, { "id": "b456" } ] HTML <span ng-repeat="r in js ...

What is the process for assigning a value to the body in a div element?

Within a div, there is a body element structured like this: <div id = "TextBox1"> <iframe id = "TextBox1_1"> #document <html> <head></head> <body></body> </html> </iframe> </div> I have attempte ...

Is it possible to create a central hub for all routes in a large AngularJS application?

Developing a large angularjs application with the intention of utilizing require.js for lazy-loading additional modules. The main question at hand is whether to create a comprehensive route.js file containing all the routes to other modules, or if each mod ...

How can a bootstrap gallery maintain a consistent size despite variations in picture dimensions?

I am currently working on creating an image gallery for our website using the latest version of Bootstrap. However, we are facing an issue with the varying sizes of our images. Each time the gallery switches to a new image, it disrupts the overall size and ...

Are HTML entities ineffective in innerHTML in javascript?

Take this example: <script type="text/javascript> function showText() { var text = document.getElementById("text-input").value; document.getElementById("display").innerHTML = text; } </script> <?php $text = "<html>some ...

Updating a specific row in a multiple row form can be achieved when the input field names match in a column

My task involves working with a report that generates a form in input mode. This form contains multiple rows of data, each row consisting of a button and an input field. The input field name remains consistent across all rows for easier processing by the C ...

Extracting information from a webpage by using Javascript to locate and interact

Seeking a way to retrieve the src attribute from an audio tag dynamically inserted into the DOM by third-party JavaScript without the ability to modify it. The goal is to back up these sounds by capturing their sources on the server side across multiple pa ...

Retrieve data from one array using information from another array. Alternatively, merging the arrays could also be a solution

Welcome, developers, hackers, and watchers! I'm facing an issue that I can't quite wrap my head around, specifically the part where I need to extract data from one array based on another. Would merging them help? Let's take a look at the ...

Issues arise with User obj's utilization of the Instagram API

After setting up my Instagram developer account, I was able to obtain the client_secret and client_id. Authentication went smoothly for my own user (myusername), and I received an access_token. However, when attempting to utilize this endpoint TOKEN] Fo ...

The Ajax Process continues to run even after the user has navigated away from the page

Currently, I have a JavaScript function that refreshes a specific section of a Rails-generated view every 2.5 seconds using a JS request to update a basic progress bar. function refreshPage(){ $.ajax({ type:"GET", dataType:"script" }) } s ...