In this part of the Bootstrap Vue Table series, we are taking all features we built in previous parts and connecting them to API services.
We won’t be building or using any custom backend solution as this is not the objective of this series. Instead, we are utilizing {JSON} Placeholder, which is a free fake API for testing and prototyping.
At the end of this tutorial, we should have the following results (all video demos will have the network panel open to show API calls):
Before we start, below is the full code from the previous part:
EditableTable Component
<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"
>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,
})),
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>
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>
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.
Load data with REST API
Since we already built a re-usable table with a dynamic schema in the second part, all we need to do is update the fields
and items
object with the new data source.
Let’s go through the required steps:
- Create a new service folder with a
user.js
file, this will take care of all the API calls. We’ll add the firstGET
call usingfetch
in anasync
function:
export const getUsers = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
return users;
}
- In
App.vue
, update thefields
object with the new schema:
fields: [
{ key: "selectRow", label: "" },
{ key: "name", label: "Name", type: "text" },
{ key: "email", label: "Email", type: "email" },
{ key: "phone", label: "Phone", type: "text" },
{ key: "edit", label: "", type: "edit" },
]
- In
App.vue
, call thegetUsers
function as soon as the component is rendered:
async mounted() {
this.users = await getUsers();
}
Here is the full code:
<template>
<div id="app">
<EditableTable v-model="users" :fields="fields"></EditableTable>
</div>
</template>
<script>
import EditableTable from "./components/EditableTable.vue";
import { getUsers } from "./services/user";
export default {
name: "App",
components: {
EditableTable,
},
data() {
return {
fields: [
{ key: "selectRow", label: "" },
{ key: "name", label: "Name", type: "text" },
{ key: "email", label: "Email", type: "email" },
{ key: "phone", label: "Phone", type: "text" },
{ key: "edit", label: "", type: "edit" },
],
users: [],
};
},
async mounted() {
this.users = await getUsers();
}
};
</script>
<style>
#app {
margin: 20px;
}
</style>
Watch and update data
After running the code in the previous step, we’ll notice that the table is empty. Recall from previous parts that we created a reusable table component that has its’ own internal copy of the data, this was necessary in order to edit and select rows. Because of that, when updating the items
object, the table will not update the internal copy of the data automatically.
This can be easily fixed by adding a watcher in EditableTable.vue
and updating tableItems
when change is detected:
watch: {
value(newVal) {
this.tableItems = this.mapItems(newVal);
},
},
mapItems()
is a shared method used to add and maintain additional properties such as isEdit
and isSelected
mapItems(data) {
return data.map((item, index) => ({
...item,
isEdit: this.tableItems[index] ? this.tableItems[index].isEdit : false,
isSelected: this.tableItems[index] ? this.tableItems[index].isSelected : false,
}))
}
Since the watcher will be triggered when the user is changing the input field, line 4 and 5 ensures that existing rows will remain editable and/or selected.
Here is the full update to the EditableTable.vue
component:
<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"
>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.mapItems(this.value),
openDialog: false
};
},
watch: {
value(newVal) {
this.tableItems = this.mapItems(newVal);
},
},
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;
},
mapItems(data) {
return data.map((item, index) => ({
...item,
isEdit: this.tableItems[index] ? this.tableItems[index].isEdit : false,
isSelected: this.tableItems[index] ? this.tableItems[index].isSelected : false,
}))
}
},
};
</script>
<style>
.action-container {
margin-bottom: 10px;
}
.action-container button {
margin-right: 5px;
}
.delete-button {
margin-left: 5px;
}
</style>
Update a row with REST API
A PATCH
API service is usually the most suitable operation for updating data. Let’s add a new updateUser
function in the user.js
service that does the following:
- Attach the user id to the service URL
- Pass the user object as a parameter
- Return updated results
export const updateUser = async (user) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${user.id}`, {
method: 'PATCH',
body: JSON.stringify(user)
});
const result = await response.json();
return result;
}
Now, to consume the function and update the edited user, we have to make two modifications:
- In
EditableTable.vue
emit an event when the “done” button is clicked, this can be added to theeditRowHandler
function. Also, we have to emit only when the row is on edit mode so that we don’t save on “cancel” or “edit” click:
editRowHandler(data) {
if (this.tableItems[data.index].isEdit) {
this.$emit("submit", this.tableItems[data.index]);
}
this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
}
- In
App.vue
add an event handler to trigger the update user service:
<template>
<div id="app">
<EditableTable
v-model="users"
:fields="fields"
@submit="handleUpdateUser($event)"
></EditableTable>
<pre>
{{ users }}
</pre>
</div>
</template>
<script>
import EditableTable from "./components/EditableTable.vue";
import { getUsers, updateUser } from "./services/user";
export default {
name: "App",
components: {
EditableTable,
},
data() {
return {
fields: [
{ key: "selectRow", label: "" },
{ key: "name", label: "Name", type: "text" },
{ key: "email", label: "Email", type: "email" },
{ key: "phone", label: "Phone", type: "text" },
{ key: "edit", label: "", type: "edit" },
],
users: [],
};
},
async mounted() {
this.users = await getUsers();
},
methods: {
async handleUpdateUser(user) {
await updateUser(user);
},
},
};
</script>
<style>
#app {
margin: 20px;
}
</style>
Demo
Add a row with REST API
Adding a new row requires a post
API service with the user object passed as a parameter. Let’s add a new addUser
function in the user.js
service:
export const addUser = async (user) => {
const response = await fetch('https://jsonplaceholder.typicode.com/users', {
method: 'POST',
body: JSON.stringify(user)
});
const result = await response.json();
return result;
}
Using the same event handler we had in the previous step, we can check if the user id is undefined
to know whether it’s a new row or not:
async handleUpdateUser(user) {
if (user.id) {
await updateUser(user);
} else {
await addUser(user);
}
}
Demo
Delete rows with REST API:
Deleting a row requires a delete
API service with only the id attached to the URL. Let’s add a new deleteUser
function in the user.js
service:
export const deleteUser = async (id) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`, {
method: 'DELETE'
});
const result = await response.json();
return result;
}
Similar to updating a user, we need to emit an event from the EditableTable.vue
component upon removing a row:
removeRowHandler(index) {
this.tableItems = this.tableItems.filter((item, i) => i !== index);
this.$emit("input", this.tableItems);
this.$emit("remove", this.tableItems[index]);
},
Then, we can create a remove event handler in App.vue
to trigger the delete API call:
<template>
<div id="app">
<EditableTable
v-model="users"
:fields="fields"
@submit="handleUpdateUser($event)"
@remove="handleRemoveUser($event)"
></EditableTable>
<pre>
{{ users }}
</pre>
</div>
</template>
<script>
import EditableTable from "./components/EditableTable.vue";
import { addUser, deleteUser, getUsers, updateUser } from "./services/user";
export default {
name: "App",
components: {
EditableTable,
},
data() {
return {
fields: [
{ key: "selectRow", label: "" },
{ key: "name", label: "Name", type: "text" },
{ key: "email", label: "Email", type: "email" },
{ key: "phone", label: "Phone", type: "text" },
{ key: "edit", label: "", type: "edit" },
],
users: [],
};
},
async mounted() {
this.users = await getUsers();
},
methods: {
async handleUpdateUser(user) {
if (user.id) {
await updateUser(user);
} else {
await addUser(user);
}
},
async handleRemoveUser(user) {
await deleteUser(user.id);
}
},
};
</script>
<style>
#app {
margin: 20px;
}
</style>
Demo:
In the previous part, we added a feature to remove multiple rows with a confirmation dialog. Let’s modify it to make it work with an API call.
Since {JSON} Placeholder doesn’t support multiple deletes (at least as far I know), we can use map
to trigger multiple API calls.
First, we need to emit the selected rows in the EditableTable.vue
component:
removeRowsHandler() {
const selectedItems = this.tableItems.filter(item => item.isSelected);
this.tableItems = this.tableItems.filter(item => !item.isSelected);
this.$emit("input", this.tableItems);
this.$emit("remove", selectedItems);
}
Then, in App.vue
using the same event handler, we can check if the user
object is a list and trigger multiple delete calls:
async handleRemoveUser(user) {
if (user.length > 0) {
await user.map(async (item) => {
await deleteUser(item.id);
})
} else {
await deleteUser(user.id);
}
}
Demo
Summary
If you made it to this section, pat yourself on the back! You just learned how to:
- Create a service layer with different API calls
- Enhance the editable table component by watching and emitting updated data
- Load, add, edit and delete rows using Bootstrap Vue table and API services
You can access the complete repository here.
Bye for now 👋