Categories
Vue

How to Add Input Mask to Enforce Input Format

Spread the love

Input validation is always a chore to set up. An input mask is a way to enforce the format of the user’s input in a simple way.

Input validation is always a chore to set up. An input mask is a way to enforce the format of the user’s input in a simple way. When an input mask is applied to an input element, only input in a set format can be entered.

For example, if an input has an input mask of for phone may have 3 digits for the area code, followed by a dash, then 3 digits for the prefix, followed by another dash, and then followed by the remaining 4 digits.

There are many JavaScript libraries for adding an input task to input fields. If we are writing a Vue.js app, we can use Vue-InputMask, located at https://github.com/scleriot/vue-inputmask.

In this article, we will build weight tracker that lets users enter date in YYYY-MM-DD format and the user’s weight. Then the output will be shown in a table sorted by reverse chronological order. We will also let user edit and delete the entries.

To start building the project, we will use the Vue CLI. We run npx @vue/cli create weight-tracker to start the wizard. Then we select ‘Manually select features’ and pick Babel, Vuex, and Vuex Router from the list.

Next we install Axios for making HTTP requests, Bootstrap Vue for styling, Vee-Validate for form validation, Vue-Filter-Date-Format for displaying dates, and Vue-Inputmask for the input mask. To install them, we run npm i axios bootstrap-vue vee-validate vue-filter-date-format vue-inputmask .

Once that’s done, we move on to building the form for adding and edit the weight data. To do this, we create a WeightForm.vue file in the components folder and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Date">
        <ValidationProvider name="date" rules="required|date" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.date"
            required
            placeholder="Date"
            name="date"
            v-mask="'9999-99-99'"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Weight">
        <ValidationProvider
          name="weight"
          rules="required|min_value:0|max_value:9999"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.weight"
            required
            placeholder="Weight"
            name="weight"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-button type="submit" variant="primary" style="margin-right: 10px">Submit</b-button>
      <b-button type="reset" variant="danger" @click="cancel()">Cancel</b-button>
    </b-form>
  </ValidationObserver>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "WeightForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    weight: Object
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const offDate = new Date(this.form.date);
      const correctedDate = new Date(
        offDate.getTime() + Math.abs(offDate.getTimezoneOffset() * 60000)
      );

      const params = {
        ...this.form,
        date: correctedDate
      };

      if (this.edit) {
        await this.editWeight(params);
      } else {
        await this.addWeight(params);
      }
      const { data } = await this.getWeights();
      this.$store.commit("setWeights", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  watch: {
    weight: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

In this file, we have a form to let users enter their weights. We use Vee-Validate to validate our inputs. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider , we have our BootstrapVue input for the text input fields. In the b-form-input components, we add the input mask to the date input with Vue-InputMask. It’s very simple. All we have to do is use the v-mask directive provided by Vue-InputMask like we did in the code. We also add Vee-Validate validation to make sure that users have filled out the date before submitting. In the weight field, we enforce the minimum and maximum value with the help of Vee-Validate as we wrote in the rules.

In the onSubmit function we correct the date bu adding the time zone offset to our date. We only need this because we have a date in YYYY-MM-DD format, according to Stack Overflow https://stackoverflow.com/a/14569783/6384091. After that, we submit the data and get the latest ones and put them in our Vuex store. Then we close the modal by emitting the saved event to the Home.vue component, which we will modify later.

We have the watch block to watch the weight prop, which we will need for editing.

Next we create a mixins folder and add requestsMixin.js into the mixins folder. In the file, we add:

const APIURL = "http://localhost:3000";
const axios = require("axios");

export const requestsMixin = {
  methods: {
    getWeights() {
      return axios.get(`${APIURL}/weights`);
    },

    addWeight(data) {
      return axios.post(`${APIURL}/weights`, data);
    },

    editWeight(data) {
      return axios.put(`${APIURL}/weights/${data.id}`, data);
    },

    deleteWeight(id) {
      return axios.delete(`${APIURL}/weights/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to get and save our data.

Next in Home.vue , replace the existing code with:

<template>
  <div class="page">
    <h1 class="text-center">Weights</h1>
    <b-button-toolbar class="button-toolbar">
      <b-button @click="openAddModal()" variant="primary">Add Weight</b-button>
    </b-button-toolbar>

    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th sticky-column>Date</b-th>
          <b-th>Weight</b-th>
          <b-th>Edit</b-th>
          <b-th>Delete</b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="w in weights" :key="w.id">
          <b-th sticky-column>{{ new Date(w.date) | dateFormat('YYYY-MM-DD') }}</b-th>
          <b-td>{{w.weight}}</b-td>
          <b-td>
            <b-button @click="openEditModal(w)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOneWeight(w.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>

    <b-modal id="add-modal" title="Add Weight" hide-footer>
      <WeightForm @saved="closeModal()" @cancelled="closeModal()" :edit="false" />
    </b-modal>

    <b-modal id="edit-modal" title="Edit Weight" hide-footer>
      <WeightForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :weight="selectedWeight"
      />
    </b-modal>
  </div>
</template>

<script>
// @ is an alias to /src
import WeightForm from "@/components/WeightForm.vue";
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "home",
  components: {
    WeightForm
  },
  mixins: [requestsMixin],
  computed: {
    weights() {
      return this.$store.state.weights.sort(
        (a, b) => +new Date(b.date) - +new Date(a.date)
      );
    }
  },
  beforeMount() {
    this.getAllWeights();
  },
  data() {
    return {
      selectedWeight: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(weight) {
      this.$bvModal.show("edit-modal");
      this.selectedWeight = weight;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedWeight = {};
    },
    async deleteOneWeight(id) {
      await this.deleteWeight(id);
      this.getAllWeights();
    },
    async getAllWeights() {
      const { data } = await this.getWeights();
      this.$store.commit("setWeights", data);
    }
  }
};
</script>

<style scoped>
</style>

We have a table to display the entered data with a BootstrapVue table. In each row, there’s an Edit and Delete button to open the edit modal and pass that data to the WeightForm, and delete the entry respectively.

When the page loads, we get all the entered data with the getAllWeights function called in the beforeMount hook. In the getAllWeights function, we put everything in the Vuex store. Then in here, we get the latest state of the store by putting the this.$store.state.weights in the computed block of the code. In there, we also sort the weight data by reverse chronological order.

Next in App.vue , we replace the existing code with:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Weight Tracker</b-navbar-brand>

      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>

      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

<style lang="scss">
.page {
  padding: 20px;
}

button,
.btn.btn-primary {
  margin-right: 10px !important;
}

.button-toolbar {
  margin-bottom: 10px;
}
</style>

to add a Bootstrap navigation bar to the top of our pages, and a router-view to display the routes we define. This style section isn’t scoped so the styles will apply globally. In the .page selector, we add some padding to our pages. We add some padding to the buttons in the remaining style code.

Then in main.js , replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, min_value, max_value } from "vee-validate/dist/rules";
import VueFilterDateFormat from "vue-filter-date-format";
const VueInputMask = require("vue-inputmask").default;

Vue.use(VueInputMask);
Vue.use(VueFilterDateFormat);
extend("required", required);
extend("min_value", min_value);
extend("max_value", max_value);
extend("date", {
  validate: value =>
    /([12]d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01]))/.test(value),
  message: "Date must be in YYYY-MM-DD format"
});
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);

Vue.config.productionTip = false;

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

We added all the libraries we need here, including BootstrapVue JavaScript and CSS, Vee-Validate components along with the validation rules, the Vue-InputMask library, and the Vue-Filter-Date-Format library are adding here for use in our app. The min_value and max_value rules are added for validating the weight, and we made a date rule for validating that the date is in YYYY-MM-DD format.

In router.js we replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

to include the home page in our routes so users can see the page.

And in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    weights: []
  },
  mutations: {
    setWeights(state, payload) {
      state.weights = payload;
    }
  },
  actions: {}
});

to add our weights state to the store so we can observer it in the computed block of WeightForm and HomePage components. We have the setWeights function to update the passwords state and we use it in the components by call this.$store.commit(“setWeights”, data); like we did in WeightForm .

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Weight Tracker</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-input-mask-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title of our app.

After all the hard work, we can start our app by running npm run serve.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "`weights`": [
]
}

So we have the weights endpoints defined in the requests.js available.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *