0

Problem Statement: I am trying to work on a design that involves replicating the Parties Being Served form fields on each Add button click. On clicking the Add Button, the corresponding PartyInfo object gets added to the PartyInfoList array enter image description here

In the BaseButtonList.vue file, I use the PartyInfoList array to setup the v-for loop

<base-card
            v-for="(data, index) of partiesInfoList" :key="index"
            ref="childComponent"
            @add-parties="updatePartiesInfoList"
            @delete-party="deleteParty(index)"
            :lastElement="index ===  partiesInfoListLength - 1"
            >

However, it is important to note that initially the array has no elements. But, since I have no other way to enter the data to the array, I'm forced to load the form despite the partiesInfoList array having zero elements. I achieve this by setting the partiesInfoList with dummy data. This allows the initial form to be displayed. And when the Add Another button gets hit, this first real entry will be pushed to the 2nd index of the partiesInfoList.

enter image description here

However, this causes serious issues when deleting the entries from the array. The splice does function properly when I console log the output. But, Vue ends up deleting the wrong element from the DOM.

I have used a unique index key and also tried other possible keys, but all yield the same pernicious bug. Any help would be greatly appreciated. I think this is a really tricky design pattern, I can think of easier alternatives, but I want to get this working as a challenge. Maybe there's a better data flow I can set up or something.

Attached is the code

BaseCard.vue

<template>
//contains the template for the input form element
</template>

<script>
import { random } from '@amcharts/amcharts4/.internal/core/utils/String';
import { EventBus } from './bus.js';
export default {
    emits:['add-parties','delete-party'],
    props:['lastElement'],
    data() {
        return {
            partyInfo: 
                {
                    id: '',
                    fullName: '',
                    preAuthorize: '',
                    serviceAddress: '',
            
                },
            validation: {
                fullNameIsValid: true,
                serviceAddressIsValid: true
            },
            hideAddButton: false,
            formIsValid: true,
            addServiceButtonText: '+ Add Service Notes (Optional)',
            serviceNotes: [],
            showServiceNotes: false,
            showDeleteButton: true,
            enteredServiceNote: '', //service notes addendum
        }
    },
    computed : {
        showServiceNotex(){
            if(!this.showServiceNotes){
                return '+Add Service Notes (Optional)'
            }else{
                return '- Remove Service Notes';
            }
        }
    },
    methods: {
        
        setServiceNotes(){
            this.showServiceNotes = !this.showServiceNotes;
        },
        addAnotherParty(){
            this.validateForm();
            if(this.counter === 0){
                this.counter++;
                this.lastElement = false;
            }
            if(!this.formIsValid){
                return;
            }

            
            let emitObj = JSON.parse(JSON.stringify(this.partyInfo));
            this.$emit('add-parties', emitObj); //event
        },
        deleteParty(){
            this.$emit('delete-party');
        },

        validateForm(){
            this.formIsValid = true;

            if(this.partyInfo.fullName === ''){
                this.validation.fullNameIsValid = false;
                this.formIsValid = false;
            }
            if(this.partyInfo.serviceAddress === ''){
                this.validation.serviceAddressIsValid = false;
                this.formIsValid = false;
            }
        },
        clearValidity(input){
            this.validation[input] = true; 
        },
        clearForm(){
            this.partyInfo.fullName = '';
            this.partyInfo.serviceAddress = '';
            this.partyInfo.preAuthorize = false;
        }
    },
    created(){
        console.log('created');
       
    }
}
</script>

Attached is the <BaseCardList> which renders the form elements in a v-for loop

BaseCardList.vue

<template>
        <ul>
             
        <base-card
            v-for="(data, index) of partiesInfoList" :key="index"
            ref="childComponent"
            @add-parties="updatePartiesInfoList"
            @delete-party="deleteParty(index)"
            :lastElement="index ===  partiesInfoListLength - 1"
            >
            <!-- Wrapper for the `Parties Being Served` component-->
                <template v-slot:title>
                    
                    <slot></slot>
                    
                </template>    
         </base-card>
        </ul>
    
</template>
<script>
import BaseCard from './BaseCard.vue';
export default {
  components: { BaseCard },
   
    data() {
        return {
            selectedComponent: 'base-card',
            partiesInfoList : [
                {id: 0,
                fullName: 'dummy',
                serviceAddress: 'dummy',
                preAuthorize: ''
                }
            ],
            clearForm: false,
            counter: 1
        }
    },
    computed : {
        hasParty(){
            return this.partiesInfoList.length > 0;
        },
        partiesInfoListLength(){
            return this.partiesInfoList.length;
        }
    },

    methods: {
        updatePartiesInfoList(additionalInfo){
            // if(this.counter == 0){
            //     this.partiesInfoList.splice(0,1);
            // }
            this.partiesInfoList.push(additionalInfo);
            
            this.counter++;
            console.log(this.partiesInfoList);
            console.log('The length of list is '+this.partiesInfoList.length);
        },
        deleteParty(resId){
            // const resIndex = this.partiesInfoList.findIndex(
            //     res => res.id === resId
            // );
            // this.partiesInfoList.splice(resIndex, 1);
            if(this.counter == 1){
                return;
            }
            this.partiesInfoList.splice(resId, 1);
            console.log('Index is '+resId);
            console.log('after del');
            console.log(this.partiesInfoList);
        }
    }
}
</script>

Actual Output Bug on screen : The adjacent element gets removed from the DOM. Say,I click on delete for the 'Second' but 'Third' gets removed. But, if there's an empty form element at the end of the v-for, then this one gets deleted. enter image description here

0

2 Answers 2

2

The keys in your v-for should be an id, not an index. Vue ties each component to its key.

With keys, it will reorder elements based on the order change of keys, and elements with keys that are no longer present will always be removed/destroyed.

vue documentation

When you splice the partyInfoList item, Vue reregisters the indexes. The indexes cascade down, and since the there are now 2 items in partyInfoList instead of 3, the Vue component with an index of 2, or the last component, gets removed.

Here is an example:

// partyInfoList
[
    // index: 0
    {
        id: 0,
        fullName: 'first',
        serviceAddress: '1',
        preAuthorize: ''
    },

    // index: 1
    {
        id: 1,
        fullName: 'second',
        serviceAddress: '2',
        preAuthorize: ''
    },

    // index: 2
    {
        id: 2,
        fullName: 'third',
        serviceAddress: '3',
        preAuthorize: ''
    },
]

Splice

// resId = 1
this.partiesInfoList.splice(resId, 1);

Resulting array

// partyInfoList
[
    // index: 0
    {
        id: 0,
        fullName: 'first',
        serviceAddress: '1',
        preAuthorize: ''
    },

    // index: 1
    {
        id: 2,
        fullName: 'third',
        serviceAddress: '3',
        preAuthorize: ''
    },
]

The array looks fine, but templates on the screen do not. This is because Vue saw that the partyInfoList item with an index of 2 does not exist anymore, so the component with :key="2" and all of its data was removed the DOM.

Your solution is actually very simple. All you need to do is change :key="index" to :key="data.id". This will tie your component to the data, not a dynamic value like an index.

I also don't know how you are setting the id on each partyInfoList item, but those must be unique for the keys to work.

This is what your new code should look like:

<base-card
        v-for="(data, index) of partiesInfoList" :key="data.id"
        ref="childComponent"
        @add-parties="updatePartiesInfoList"
        @delete-party="deleteParty(index)"
        :lastElement="index ===  partiesInfoListLength - 1"
        >
Sign up to request clarification or add additional context in comments.

Comments

1

In updatePartiesInfoList just before pushing new object to partiesInfoList, check if this.partiesInfoList[0].fullName === 'dummy' returns true, then if the condition is truthy, execute this.partiesInfoList.splice(0, 1) to remove the dummy object from your array!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.