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:
<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>
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 👋