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:
- Add a button with an event handler
- Use the array
reducer
function to generate an object with all the field names we need for an empty row. We can default values tonull
- Use
unshift
to insert a new row at the very beginning of the list - Make sure to emit the latest changes for the
v-model
to update. This was explained in-depth in the previous part. - 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 newisSelected
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 thev-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 👋