Search code examples
vue.jsvuejs2axiosvue-componentdelete-row

How to delete the row without affecting the other rows


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


Solution

  • 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>