Muhi Logo Text
AboutBlogWork With Me

[Part 4] Load, Add, Update and Delete Table Rows using API Services

Learn how to load, add, update and delete table rows with API services using Bootstrap Vue.

Last updated on February 16, 2022

vue
bootstrap vue
Bootstrap Vue

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:

  1. Create a new service folder with a user.js file, this will take care of all the API calls. We’ll add the first GET call using fetch in an async function:
export const getUsers = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/users');
    const users = await response.json();
    return users;
}
  1. In App.vue, update the fields 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" },
   ]
  1. In App.vue, call the getUsers 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

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:

  1. In EditableTable.vue emit an event when the “done” button is clicked, this can be added to the editRowHandler 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;
}
  1. 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 👋

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