Muhi Logo Text
AboutBlogWork With Me

[Part 3] Add and Remove Table Rows with Bootstrap Vue

Learn how to add and remove multiple rows with a confirmation dialog using Bootstrap Vue.

Last updated on February 16, 2022

vue
bootstrap vue
Bootstrap Vue

In the previous part, we learned how to convert an editable Bootstrap table to a dynamic reusable component that works with any schema. Also, we demonstrated how we can enable v-model in a custom component to support a two-way bind.

In this tutorial, we’ll continue enhancing this component by showing how to add and remove multiple rows with a confirmation dialog. Here is a quick demo of the final results:

Before we start, below is the full code from the previous part (EditableTable.vue component):

<template>
  <b-table :items="tableItems" :fields="fields">
      <template v-for="(field, index) in fields" #[`cell(${field.key})`]="data">
        <b-form-datepicker v-if="field.type === 'date' && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)"></b-form-datepicker>
        <b-form-select v-else-if="field.type === 'select' && tableItems[data.index].isEdit" :key="index" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)" :options="field.options"></b-form-select>
        <b-button :key="index" v-else-if="field.type === 'edit'" @click="editRowHandler(data)">
          <span v-if="!tableItems[data.index].isEdit">Edit</span>
          <span v-else>Done</span>
        </b-button>
        <b-form-input v-else-if="field.type && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @blur="(e) => inputHandler(e.target.value, data.index, field.key)"></b-form-input>
        <span :key="index" v-else>{{data.value}}</span>
      </template>
  </b-table>
</template>

<script>
export default {
  name: "EditableTable",
  components: {},
  props: {
    value: Array,
    fields: Array
  },
  data() {
    return {
      tableItems: this.value.map(item => ({...item, isEdit: false}))
    }
  },
  methods: {
      editRowHandler(data) {
        this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      },
      inputHandler(value, index, key) {
        this.tableItems[index][key] = value;
        this.$set(this.tableItems, index, this.tableItems[index]);
        this.$emit("input", this.tableItems);
      }
    }
};
</script>

<style>
</style>

As we continue building upon the same code from previous parts in this series, it’s recommended that you go through these tutorials from the beginning to understand how the code structure works as we will not be explaining it here. You can navigate to any part using the table of contents.

Adding new rows

Adding a new row is as simple as inserting a new object to the tableItems array and making sure to apply the proper schema. Let’s take a closer look:

  1. Add a button with an event handler
  2. Use the array reducer function to generate an object with all the field names we need for an empty row. We can default values to null
  3. Use unshift to insert a new row at the very beginning of the list
  4. Make sure to emit the latest changes for the v-model to update. This was explained in-depth in the previous part.
  5. Finally, set isEdit to true to enable editing the new row
EditableTable Component
<template>
    <article>
        <b-button class="add-button" variant="success" @click="addRowHandler">Add Row</b-button>
        <b-table class="b-table" :items="tableItems" :fields="fields" fixed>
            <template v-for="(field, index) in fields" #[`cell(${field.key})`]="data">
                <b-form-datepicker v-if="field.type === 'date' && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)"></b-form-datepicker>
                <b-form-select v-else-if="field.type === 'select' && tableItems[data.index].isEdit" :key="index" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)" :options="field.options"></b-form-select>
                <b-button :key="index" v-else-if="field.type === 'edit'" @click="editRowHandler(data)">
                <span v-if="!tableItems[data.index].isEdit">Edit</span>
                <span v-else>Done</span>
                </b-button>
                <b-form-input v-else-if="field.type && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @blur="(e) => inputHandler(e.target.value, data.index, field.key)"></b-form-input>
                <span :key="index" v-else>{{data.value}}</span>
            </template>
        </b-table>
    </article>
</template>

<script>
export default {
  name: "EditableTable",
  components: {},
  props: {
    value: Array,
    fields: Array
  },
  data() {
    return {
      tableItems: this.value.map(item => ({...item, isEdit: false}))
    }
  },
  methods: {
      editRowHandler(data) {
        this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      },
     inputHandler(value, index, key) {
       this.tableItems[index][key] = value;
       this.$set(this.tableItems, index, this.tableItems[index]);
       this.$emit("input", this.tableItems);
      },
      addRowHandler() {
        const newRow = this.fields.reduce((a, c) => ({...a, [c.key]: null}) ,{})
        newRow.isEdit = true;
        this.tableItems.unshift(newRow);
        this.$emit('input', this.tableItems);
      }
    }
};
</script>

<style>
    .add-button {
        margin-bottom: 10px;
    }
</style>

In app.vue component, we still consume the EditableTable component the same way:

App Component
<template>
  <div id="app">
      <EditableTable v-model="items" :fields="fields"></EditableTable>
  </div>
</template>

<script>
import EditableTable from './components/EditableTable.vue';
export default {
  name: "App",
  components: {
    EditableTable
  },
  data() {
    return {
      fields: [
        { key: "name", label: "Name", type: "text" },
        { key: "department", label: "Department", type: "select", options: ['Development', 'Marketing', 'HR', 'Accounting'] },
        { key: "age", label: "Age", type: "number" },
        { key: "dateOfBirth", label: "Date Of Birth", type: "date" },
        { key: "edit", label: "", type: "edit" }
      ],
       items: [
          { age: 40, name: 'Dickerson', department: 'Development', dateOfBirth: '1984-05-20' },
          { age: 21, name: 'Larsen', department: 'Marketing', dateOfBirth: '1984-05-20' },
          { age: 89, name: 'Geneva', department: 'HR', dateOfBirth: '1984-05-20' },
          { age: 38, name: 'Jami', department: 'Accounting', dateOfBirth: '1984-05-20' }
        ]
    };
  }
};
</script>

<style>
#app {
  margin: 20px;
}
</style>
Demo

Removing a row

Similar to the previous step, we’re going to modify the tableItem array directly but this time by removing an element. Let’s go through the required steps:

  • Add a remove button for every row and define an event handler
  • Use the array filter function to filter out the element using the index. The index will be passed in the event handler parameter.
  • Emit latest changes for the v-model to update
EditableTable component:
<template>
    <article>
        <b-button class="add-button" variant="success" @click="addRowwHandler">Add Row</b-button>
        <b-table class="b-table" :items="tableItems" :fields="fields" fixed>
            <template v-for="(field, index) in fields" #[`cell(${field.key})`]="data">
                <b-form-datepicker v-if="field.type === 'date' && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)"></b-form-datepicker>
                <b-form-select v-else-if="field.type === 'select' && tableItems[data.index].isEdit" :key="index" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)" :options="field.options"></b-form-select>
                <div :key="index" v-else-if="field.type === 'edit'">
                    <b-button @click="editRowHandler(data)">
                        <span v-if="!tableItems[data.index].isEdit">Edit</span>
                        <span v-else>Done</span>
                    </b-button>
                    <b-button class="delete-button" variant="danger" @click="removeRowHandler(data.index)">Remove</b-button>
                </div>
                <b-form-input v-else-if="field.type && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @blur="(e) => inputHandler(e.target.value, data.index, field.key)"></b-form-input>
                <span :key="index" v-else>{{data.value}}</span>
            </template>
        </b-table>
    </article>
</template>

<script>
export default {
  name: "EditableTable",
  components: {},
  props: {
    value: Array,
    fields: Array
  },
  data() {
    return {
      tableItems: this.value.map(item => ({...item, isEdit: false}))
    }
  },
  methods: {
      editRowHandler(data) {
        this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      },
      inputHandler(value, index, key) {
          this.tableItems[index][key] = value;
          this.$set(this.tableItems, index, this.tableItems[index]);
          this.$emit("input", this.tableItems);
        },
      addRowwHandler() {
        const newRow = this.fields.reduce((a, c) => ({...a, [c.key]: null}) ,{})
        newRow.isEdit = true;
        this.tableItems.unshift(newRow);
        this.$emit('input', this.tableItems);
      },
      removeRowHandler(index) {
        this.tableItems = this.tableItems.filter((item, i) => i !== index);
        this.$emit('input', this.tableItems);
      }
    }
};
</script>

<style>
    .add-button {
        margin-bottom: 10px;
    }
    .delete-button {
        margin-left: 5px;
    }
</style>
Demo:

Removing multiple rows

Removing multiple rows at once can be very useful, especially if we are triggering an API call. Let’s go through the required steps:

In App.vue component, we need a new column in the fields array with a key (we’ll name it selectRow but it can be any value). Notice that we are not adding type for this column because it will always be a checkbox that determines which rows have been selected:

fields: [
   { key: "selectRow", label: "" },
   ...
]

In EditableTable.vue component:

  • Add a new condition for selectRow that returns a checkbox element. It will have a @change event handler with a :checked value determined by a new isSelected property:
<b-checkbox  v-else-if="field.key === 'selectRow'" :checked="tableItems[data.index].isSelected" @change="selectRowHandler(data)"></b-checkbox>
  • Toggle isSelected value (true/false) when the checkbox is checked:
selectRowHandler(data) {
  this.tableItems[data.index].isSelected = !this.tableItems[data.index].isSelected;
}
  • Add a new “Remove Rows” button next to the add button with a @click event:
<div class="action-container">
    <b-button class="add-button" variant="success" @click="addRowHandler">Add Row</b-button>
    <b-button variant="danger" @click="removeRowsHandler">Remove Rows</b-button>
</div>
  • Finally, filter out selected rows when the “Remove Rows” button is clicked:
removeRowsHandler() {
  this.tableItems = this.tableItems.filter(item => !item.isSelected);
  this.$emit('input', this.tableItems);
}
EditableTable component:
<template>
    <article>
        <div class="action-container">
          <b-button class="add-button" variant="success" @click="addRowHandler">Add Row</b-button>
          <b-button variant="danger" @click="removeRowsHandler">Remove Rows</b-button>
        </div>
        <b-table class="b-table" :items="tableItems" :fields="fields" fixed>
            <template v-for="(field, index) in fields" #[`cell(${field.key})`]="data">
                <b-form-datepicker v-if="field.type === 'date' && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)"></b-form-datepicker>
                <b-form-select v-else-if="field.type === 'select' && tableItems[data.index].isEdit" :key="index" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)" :options="field.options"></b-form-select>
                <b-checkbox  v-else-if="field.key === 'selectRow'" :checked="tableItems[data.index].isSelected" :key="index" @change="selectRowHandler(data)"></b-checkbox>
                <div :key="index" v-else-if="field.type === 'edit'">
                    <b-button @click="editRowHandler(data)">
                        <span v-if="!tableItems[data.index].isEdit">Edit</span>
                        <span v-else>Done</span>
                    </b-button>
                    <b-button class="delete-button" variant="danger" @click="removeRowHandler(data.index)">Remove</b-button>
                </div>
                <b-form-input v-else-if="field.type && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @blur="(e) => inputHandler(e.target.value, data.index, field.key)"></b-form-input>
                <span :key="index" v-else>{{data.value}}</span>
            </template>
        </b-table>
    </article>
</template>

<script>
export default {
  name: "EditableTable",
  components: {},
  props: {
    value: Array,
    fields: Array
  },
  data() {
    return {
      tableItems: this.value.map(item => ({...item, isEdit: false, isSelected: false}))
    }
  },
  methods: {
      editRowHandler(data) {
        this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      },
      inputHandler(value, index, key) {
          this.tableItems[index][key] = value;
          this.$set(this.tableItems, index, this.tableItems[index]);
          this.$emit("input", this.tableItems);
        },
      addRowHandler() {
        const newRow = this.fields.reduce((a, c) => ({...a, [c.key]: null}) ,{})
        newRow.isEdit = true;
        this.tableItems.unshift(newRow);
        this.$emit('input', this.tableItems);
      },
      removeRowHandler(index) {
        this.tableItems = this.tableItems.filter((item, i) => i !== index);
        this.$emit('input', this.tableItems);
      },
      removeRowsHandler() {
        this.tableItems = this.tableItems.filter(item => !item.isSelected);
        this.$emit('input', this.tableItems);
      },
      selectRowHandler(data) {
        this.tableItems[data.index].isSelected = !this.tableItems[data.index].isSelected;
      }
    }
};
</script>

<style>
    .action-container {
        margin-bottom: 10px;
    }
    .action-container button {
        margin-right: 5px;
    }
    .delete-button {
        margin-left: 5px;
    }
</style>
App component:
<template>
  <div id="app">
      <EditableTable v-model="items" :fields="fields"></EditableTable>
  </div>
</template>

<script>
import EditableTable from './components/EditableTable.vue';
export default {
  name: "App",
  components: {
    EditableTable
  },
  data() {
    return {
      fields: [
        { key: "selectRow", label: "" },
        { key: "name", label: "Name", type: "text" },
        { key: "department", label: "Department", type: "select", options: ['Development', 'Marketing', 'HR', 'Accounting'] },
        { key: "age", label: "Age", type: "number" },
        { key: "dateOfBirth", label: "Date Of Birth", type: "date" },
        { key: "edit", label: "", type: "edit" }
      ],
       items: [
          { age: 40, name: 'Dickerson', department: 'Development', dateOfBirth: '1984-05-20' },
          { age: 21, name: 'Larsen', department: 'Marketing', dateOfBirth: '1984-05-20' },
          { age: 89, name: 'Geneva', department: 'HR', dateOfBirth: '1984-05-20' },
          { age: 38, name: 'Jami', department: 'Accounting', dateOfBirth: '1984-05-20' }
        ]
    };
  }
};
</script>

<style>
#app {
  margin: 20px;
}
</style>
Demo:

Confirmation dialog

If we are triggering an API call that deletes data from the database, it’s a good idea to add a confirmation dialog before proceeding. This requires few easy steps:

  • Add a new b-modal component with a title and body content
  • Create a new openDialog flag to open and close the dialog. We can use it in the v-model prop available in the Modal component
  • We’ll change the remove button event handler to open the dialog and the removeRowsHandler event handler will now be triggered on the Modal submit event instead
EditableTable component full code:
<template>
    <article>
       <b-modal id="modal-1" title="Confirm" v-model="openDialog" ok-title="Remove" @ok="removeRowsHandler">
        <p class="my-4">Are you sure you want to remove the selected rows?</p>
      </b-modal>
        <div class="action-container">
          <b-button class="add-button" variant="success" @click="addRowHandler">Add Row</b-button>
          <b-button variant="danger" @click="openDialog = true">Delete Rows</b-button>
        </div>
        <b-table class="b-table" :items="tableItems" :fields="fields" fixed>
            <template v-for="(field, index) in fields" #[`cell(${field.key})`]="data">
                <b-form-datepicker v-if="field.type === 'date' && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)"></b-form-datepicker>
                <b-form-select v-else-if="field.type === 'select' && tableItems[data.index].isEdit" :key="index" :value="tableItems[data.index][field.key]" @input="(value) => inputHandler(value, data.index, field.key)" :options="field.options"></b-form-select>
                <b-checkbox  v-else-if="field.key === 'selectRow'" :checked="tableItems[data.index].isSelected" :key="index" @change="selectRowHandler(data)"></b-checkbox>
                <div :key="index" v-else-if="field.type === 'edit'">
                    <b-button @click="editRowHandler(data)">
                        <span v-if="!tableItems[data.index].isEdit">Edit</span>
                        <span v-else>Done</span>
                    </b-button>
                    <b-button class="delete-button" variant="danger" @click="removeRowHandler(data.index)">Remove</b-button>
                </div>
                <b-form-input v-else-if="field.type && tableItems[data.index].isEdit" :key="index" :type="field.type" :value="tableItems[data.index][field.key]" @blur="(e) => inputHandler(e.target.value, data.index, field.key)"></b-form-input>
                <span :key="index" v-else>{{data.value}}</span>
            </template>
        </b-table>
    </article>
</template>

<script>
export default {
  name: "EditableTable",
  components: {},
  props: {
    value: Array,
    fields: Array
  },
  data() {
    return {
      tableItems: this.value.map(item => ({...item, isEdit: false, isSelected: false})),
      openDialog: false
    }
  },
  methods: {
      editRowHandler(data) {
        this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      },
      inputHandler(value, index, key) {
          this.tableItems[index][key] = value;
          this.$set(this.tableItems, index, this.tableItems[index]);
          this.$emit("input", this.tableItems);
        },
      addRowHandler() {
        const newRow = this.fields.reduce((a, c) => ({...a, [c.key]: null}) ,{})
        newRow.isEdit = true;
        this.tableItems.unshift(newRow);
        this.$emit('input', this.tableItems);
      },
      removeRowHandler(index) {
        this.tableItems = this.tableItems.filter((item, i) => i !== index);
        this.$emit('input', this.tableItems);
      },
      removeRowsHandler() {
        this.tableItems = this.tableItems.filter(item => !item.isSelected);
        this.$emit('input', this.tableItems);
      },
      selectRowHandler(data) {
        this.tableItems[data.index].isSelected = !this.tableItems[data.index].isSelected;
      }
    }
};
</script>

<style>
    .action-container {
        margin-bottom: 10px;
    }
    .action-container button {
        margin-right: 5px;
    }
    .delete-button {
        margin-left: 5px;
    }
</style>
Demo:

Summary

If you made it to this section, pat yourself on the back! You just learned how to:

  • Add and remove table rows
  • Create a multi-select feature that removes multiple rows
  • Confirmation dialog before removing table rows

You can access the complete repository here.

Bye for now 👋

If you enjoyed this post, I regularly share similar content on Twitter. Follow me @muhimasri to stay up to date, or feel free to message me on Twitter if you have any questions or comments. I'm always open to discussing the topics I write about!

Recommended Reading

Learn how to validate table rows and input fields with BootstrapVue and HTML form validation.

vue
bootstrap vue

Discussion

Upskill Your Frontend Development Techniques 🌟

Subscribe to stay up-to-date and receive quality front-end development tutorials straight to your inbox!

No spam, sales, or ads. Unsubscribe anytime you wish.

© 2024, Muhi Masri