Muhi Logo Text
AboutBlogWork With Me

[Part 5] Validate Table Rows and Fields with Bootstrap Vue

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

Last updated on February 16, 2022

vue
bootstrap vue
Bootstrap Vue

In this part of the BootstrapVue Table series, we will implement table validation for input fields.

We will start by adding a basic validation in EditableTable.vue, then work our way up to make it defined and customized by the consumer. Remember, we built this component to support dynamic schemas so developers can re-use it in different features or projects.

If you’d like to skip all the details, below is the final interactive demo on Condsandbox. In this example, we’re validating the name and email, but the email is customized with regex only to accept strings ending with “bootstrapvue.com”:

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.

For your reference, below is the complete code from the last 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.mapItems(this.value),
      openDialog: false,
    };
  },
  watch: {
    value(newVal) {
      this.tableItems = this.mapItems(newVal);
    },
  },
  methods: {
    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;
    },
    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);
      this.$emit("remove", this.tableItems[index]);
    },
    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);
    },
    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>
App component
<template>
  <div id="app">
    <EditableTable
      v-model="users"
      :fields="fields"
      @submit="handleUpdateUser($event)"
      @remove="handleRemoveUser($event)"
    ></EditableTable>
  </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", required: true },
        { key: "email", label: "Email", type: "email", required: true, pattern: ".+@bootstrapvue\\.com" },
        { 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) {
      if (user.length > 0) {
        await user.map(async (item) => {
          await deleteUser(item.id);  
        })
      } else {
        await deleteUser(user.id);
      }
    }
  },
};
</script>

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

Add Table Row Validation

To validate a specific field in a table row, we need to flag that row so that other rows are not validated.

Following the same approach we did in previous parts, we’ll add a new validity property within every row in the tableItems data:

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,
        validity: this.tableItems[index] ? this.tableItems[index].validity : {},
    }));
}

Similar to isEdit and isSelected properties, each row will have its validity object that will contain the fields that are being validated. For example, if the name and email fields of the first row are required, and the user leaves them empty, then the data array will look as follows:

[
  {
    name: '',
    email: '',
    phone: '',
    isEdit: true,
    validity: {
      name: false,
      email: false
    },
    ...
  }
]

Now, in the template, let’s add a bootstrap state property that will automatically change the style of the input field based on a Boolean value:

image 2 1024x176

<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, data.index, field)"
    :state="tableItems[data.index].validity[field.key]"
  ></b-form-input>

Since the template is iterating through rows and fields, we can quickly get the validity value using the index and field key (true, false, or null value).

Add Input Field Validation

There are multiple ways to validate the HTML input field. This tutorial will leverage the built-in HTML form validation by using the required and pattern attributes.

Let’s start with a basic empty field validation:

  <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, data.index, field)"
    :required="true"
    :state="tableItems[data.index].validity[field.key]"
  ></b-form-input>

Using the inputHandler event, we can get the validity value from the target object to set the required field of the current row.

inputHandler(e, index, field) {
    // If the input is not valid, assign the validity of the specific field to false
    if (!e.target.validity.valid) {
      this.tableItems[index].validity[field.key] = false;
      this.$set(this.tableItems, index, this.tableItems[index]);
    } else { // Otherwise make sure to assign the validity back to true or null
      this.tableItems[index].validity[field.key] = true;
      this.tableItems[index][field.key] = e.target.value;
      this.$set(this.tableItems, index, this.tableItems[index]);
      this.$emit("input", this.tableItems);
    }
}

Now, let’s try the pattern attribute for custom validation. For example, the regex pattern below only allows an email value that ends with “bootstrapvue.com”:

  <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, data.index, field)"
    :required="true"
    :pattern=".+@bootstrapvue\.com"
    :state="tableItems[data.index].validity[field.key]"
  ></b-form-input>

Lastly, we can disable the “Done” button if the fields are invalid using the disabled attribute.

Let’s write a function that checks if any of the validity values are false, then the button gets disabled:

disableButton(data) {
  return Object.values(data.item.validity).some(valid => !valid);
}
<b-button @click="editRowHandler(data, field)" :disabled="disableButton(data)">
   <span v-if="!tableItems[data.index].isEdit">Edit</span>
   <span v-else>Done</span>
</b-button>

image 3 1024x119

Enable Dynamic Validation

So far, we were able to add a required and custom validation to the input field. But since the code above is hard coded, every input field will be validated.

Since this is a dynamic table and column types are defined by the consumer in the main app.vue component, we can enable passing the validation attributes as well so only required columns are validated:

fields: [
    { key: "selectRow", label: "" },
    { key: "name", label: "Name", type: "text", required: true },
    { key: "email", label: "Email", type: "email", required: true, pattern: ".+@bootstrapvue\\.com" },
    { key: "phone", label: "Phone", type: "text" },
    { key: "edit", label: "", type: "edit" },
]

Then, in EditableTable.vue, the required and pattern attributes will read from the field object instead of hard coding a value.

<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, data.index, field)"
    :required="field.required"
    :pattern="field.pattern"
    :state="tableItems[data.index].validity[field.key]"
  ></b-form-input>

This way, if needed, each column can have its’ own custom validation.

Below is the complete code:

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)"
        ></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)"
          :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, field)" :disabled="disableButton(data)">
            <span v-if="!tableItems[data.index].isEdit">Edit</span>
            <span v-else>Done</span>
          </b-button>
          <b-button
            v-if="!tableItems[data.index].isEdit"
            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, data.index, field)"
          :required="field.required"
          :pattern="field.pattern"
          :state="tableItems[data.index].validity[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) {
      if (this.tableItems[data.index].isEdit) {
        this.$emit("submit", this.tableItems[data.index]);
      }
      this.tableItems[data.index].isEdit = !this.tableItems[data.index].isEdit;
      this.tableItems[data.index].validity = {};
      this.$set(this.tableItems, data.index, this.tableItems[data.index]);
    },
    inputHandler(e, index, field) {
      if (!e.target.validity.valid) {
        this.tableItems[index].validity[field.key] = false;
        this.$set(this.tableItems, index, this.tableItems[index]);
      } else {
        if (field.required || field.patter) {
          this.tableItems[index].validity[field.key] = true;
        }
        this.tableItems[index][field.key] = e.target.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);
      this.$emit("remove", this.tableItems[index]);
    },
    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);
    },
    selectRowHandler(data) {
      this.tableItems[data.index].isSelected = !this.tableItems[data.index]
        .isSelected;
    },
    disableButton(data) {
      return Object.values(data.item.validity).some(valid => !valid);
    },
    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,
        validity: this.tableItems[index] ? this.tableItems[index].validity : {},
      }));
    },
  },
};
</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="users"
      :fields="fields"
      @submit="handleUpdateUser($event)"
      @remove="handleRemoveUser($event)"
    ></EditableTable>
  </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", required: true },
        { key: "email", label: "Email", type: "email", required: true, pattern: ".+@bootstrapvue\\.com" },
        { 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) {
      if (user.length > 0) {
        await user.map(async (item) => {
          await deleteUser(item.id);  
        })
      } else {
        await deleteUser(user.id);
      }
    }
  },
};
</script>

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

Summary

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

  • Update EditableTable.vue to support input field validation
  • Leverage built-in HTML form validations for adding custom validations
  • Enable the consumer to dynamically add validations

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 load, add, update and delete table rows with API services using Bootstrap Vue.

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