I am creating a VueJs parent component that can create rows dynamically, and this component call another component which can populate 2 dropdowns with axios. One for categories The second one for subcategories (this dropdown can depend with the first one)
This is the first component for adding rows
<template>
<div>
<ul>
<li v-for="(input, index) in inputs" :key="index">
<request-part :index="index" :input="input" :inputs="inputs">
</request-part>
<hr />
</li>
</ul>
<button
type="button"
@click="addRow"
class="btn font-montserrat-regular btn-success btn-plus bt-radius-add"
>
Onderdeel toevoegen
</button>
</div>
</template>
<script>
export default {
data() {
return {
category: null,
selectedFile: null,
subcategory: null,
current: 0,
id: 0,
inputs: [],
categories: [],
subcategories: []
}
},
mounted() {
axios.get('/api/categories').then(res => {
this.categories = res.data
})
},
created() {
this.addRow()
},
methods: {
addRow() {
this.inputs.push({
category: '',
subcategory: '',
sku: '',
description: '',
image: ''
})
},
onFileChanged(event) {
this.selectedFile = event.target.files[0]
}
}
}
</script>
This is the second component for populating the dropdowns
<template>
<div class="border-0">
<div class="row">
<div class="col-md-8">
<div class="form-group ml-2">
<label class="gray-text-color font-montserrat-regular" :for="part">
{{ $t('labels.frontend.request.part') }} *
</label>
<div class="form-group brd3">
<select
:name="'r[' + index + '][category]'"
:id="category + index"
class="form-control light-gray-background arrow-select-position request-input"
v-model="input.category"
@change="onchangeCategorie"
required
>
<option :value="null" disabled selected>
{{ $t('labels.account.create.selectCategory') }}
</option>
<option
v-for="(option, index1) in categories"
:value="index1"
:key="index1"
>
{{ option }}
</option>
</select>
</div>
<div class="form-group brd3">
<select
:name="'r[' + index + '][subcategory]'"
:id="subcategory + index"
class="form-control light-gray-background arrow-select-position request-input"
v-model="input.subcategory"
required
>
<option :value="null" disabled selected>
{{ $t('labels.frontend.request.subCategory') }}
</option>
<option
v-for="(option, index1) in subcategories"
:value="index1"
:key="option.id"
>
{{ option }}
</option>
</select>
</div>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label class="gray-text-color font-montserrat-regular" :for="sku">
{{ $t('labels.frontend.request.articleNumber') }}
</label>
<input
type="text"
:name="'r[' + index + '][sku]'"
:id="'sku' + index"
v-model="input.sku"
class="form-control light-gray-background request-input"
/>
</div>
</div>
</div>
<div class="row">
<div class="col-9">
<div class="form-group" style="margin-right:-40px">
<input
type="text"
:name="'r[' + index + '][description]'"
v-model="input.description"
class="form-control light-gray-background input-width-mobile request-input"
placeholder="Toelichting (optioneel)"
/>
</div>
</div>
<input
:id="'image' + index"
:name="'r[' + index + '][image]'"
type="file"
class="camera-button inputfile"
:change="input.image"
accept="image/*"
@change="onFileChanged"
/>
<label :for="'image' + index">
<img
src="../../../../../resources/assets/images/cameraIcon.png"
alt="Camera icon"
class="camera-button-position"
/>
</label>
<div class="pr-l-200 ft-14 mr-3">
<label>{{ $t('labels.frontend.request.image') }}</label>
</div>
<div id="preview">
<img v-if="url" :src="url" alt="no Image." />
<button
v-if="url != null"
type="button"
@click="url = null"
class="btn fa fa-trash btn-default bt-radius"
></button>
</div>
</div>
<button
type="button"
@click="deleteRow(index)"
class="btn btn-danger fa fa-trash bt-radius"
></button>
</div>
</template>
<script>
export default {
props: {
part: {
type: String,
default: null
},
sku: {
type: String,
default: null
},
description: {
type: String,
default: null
},
image: {
type: String,
default: null
},
index: {
type: Number,
default: 0
},
input: {
type: Object,
default: () => ({})
},
inputs: {
type: Array,
default: () => []
}
},
data() {
return {
test: null,
category: null,
selectedFile: null,
subcategory: null,
categories: [],
subcategories: [],
url: null
}
},
mounted() {
axios.get('/api/categories').then(res => {
this.categories = res.data
})
},
methods: {
deleteRow(index) {
console.log(index)
this.$delete(this.inputs, index)
},
onFileChanged(event) {
this.selectedFile = event.target.files[0]
this.input.image = this.selectedFile
this.url = URL.createObjectURL(this.selectedFile)
},
onchangeCategorie(e) {
axios.get('/api/categories/' + e.target.value).then(res => {
this.subcategories = res.data
})
}
}
}
</script>
<style>
#preview {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
}
#preview img {
max-width: 200px;
max-height: 200px;
border-radius: 5px;
border: 1px solid lightgray;
}
</style>
When i try to delete the first or any row from top to down, all the subcategories are gone. When i delete the row from down to up it works fine
Your problem is caused by the fact that your rows don't have a proper stable unique ID, and that you're instead using their array index as the :key
in your v-for
directive. The reason this is a problem is that when you delete an element from an array using .$delete()
, all the later elements get shifted down to a new, lower index so that the array remains contiguous.
The solution is to give your rows a unique ID. A simple global counter will do just fine:
var counter = 0; // global counter for row IDs (or anything else that needs one)
export default {
// ...
methods: {
addRow() {
this.inputs.push({
category: '',
subcategory: '',
sku: '',
description: '',
image: '',
id: ++counter // this gives each row a distinct ID number
})
},
// ...
}
Then you can use this unique ID as the :key
in your v-for
directive:
<li v-for="(input, index) in inputs" :key="input.id">
<request-part :index="index" :input="input" :inputs="inputs">
</request-part>
<hr />
</li>