Categories
Quasar

Developing Vue Apps with the Quasar Library — Touch and Click Events

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Touch Hold Directive

We can add the v-touch-hold directive to run code when we click and hold the mouse button on an element.

For instance, we can write:

<!DOCTYPE html>  
<html>  
  <head>  
    <link  
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"  
      rel="stylesheet"  
      type="text/css"  
    />  
    <link  
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"  
      rel="stylesheet"  
      type="text/css"  
    />  
  </head>  
  <body class="body--dark">  
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>  
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>  
    <div id="q-app">  
      <div class="q-pa-md">  
        <q-card  
          v-touch-hold.mouse="handleHold"  
          class="custom-area cursor-pointer bg-primary text-white shadow-2 relative-position row flex-center"  
        >  
          <div v-if="info" class="custom-info">  
            <pre>{{ info }}</pre>  
          </div>  
          <div v-else class="text-center">  
            Click me  
          </div>  
        </q-card>  
      </div>  
    </div>  
    <script>  
      new Vue({  
        el: "#q-app",  
        data: {  
          info: null  
        },  
        methods: {  
          handleHold({ evt, ...info }) {  
            this.info = info;  
          }  
        }  
      });  
    </script>  
  </body>  
</html>

We get the data from the click event listener, which includes whether a tap or click is done.

Also, we get the position of the mouse click or tap and the duration.

The listener method runs 600ms after a click or tap is done by default.

We can change it with the argument:

<!DOCTYPE html>  
<html>  
  <head>  
    <link  
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"  
      rel="stylesheet"  
      type="text/css"  
    />  
    <link  
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"  
      rel="stylesheet"  
      type="text/css"  
    />  
  </head>  
  <body class="body--dark">  
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>  
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>  
    <div id="q-app">  
      <div class="q-pa-md">  
        <q-card  
          v-touch-hold:2000.mouse="handleHold"  
          class="custom-area cursor-pointer bg-primary text-white shadow-2 relative-position row flex-center"  
        >  
          <div v-if="info" class="custom-info">  
            <pre>{{ info }}</pre>  
          </div>  
          <div v-else class="text-center">  
            Click me  
          </div>  
        </q-card>  
      </div>  
    </div>  
    <script>  
      new Vue({  
        el: "#q-app",  
        data: {  
          info: null  
        },  
        methods: {  
          handleHold({ evt, ...info }) {  
            this.info = info;  
          }  
        }  
      });  
    </script>  
  </body>  
</html>

We changed the listen to run 2000ms after a click or tap.

Also, we can change the sensitivity with some arguments:

<!DOCTYPE html>  
<html>  
  <head>  
    <link  
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"  
      rel="stylesheet"  
      type="text/css"  
    />  
    <link  
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"  
      rel="stylesheet"  
      type="text/css"  
    />  
  </head>  
  <body class="body--dark">  
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>  
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>  
    <div id="q-app">  
      <div class="q-pa-md">  
        <q-card  
          v-touch-hold:2000:12:15.mouse="handleHold"  
          class="custom-area cursor-pointer bg-primary text-white shadow-2 relative-position row flex-center"  
        >  
          <div v-if="info" class="custom-info">  
            <pre>{{ info }}</pre>  
          </div>  
          <div v-else class="text-center">  
            Click me  
          </div>  
        </q-card>  
      </div>  
    </div>  
    <script>  
      new Vue({  
        el: "#q-app",  
        data: {  
          info: null  
        },  
        methods: {  
          handleHold({ evt, ...info }) {  
            this.info = info;  
          }  
        }  
      });  
    </script>  
  </body>  
</html>

We change the sensitivity to 12px for touch events and 15px for clicks.

Conclusion

We can add listeners for various mouse and touch events with the directives provided by Quasar.

Categories
Quasar

Developing Vue Apps with the Quasar Library — Effect Directives

Quasar is a popular Vue UI library for developing good looking Vue apps.

In this article, we’ll take a look at how to create Vue apps with the Quasar UI library.

Ripple Effect

We can use the v-ripple directive to add ripple effects to elements or components that don’t have the effect built-in.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <div class="q-pa-md row justify-center">
        <div v-ripple class="relative-position container flex flex-center">
          Click me
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

We just add it to the element directly to add the effect.

We can change the color with an argument:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <div class="q-pa-md row justify-center">
        <div
          v-ripple:purple
          class="relative-position container flex flex-center"
        >
          Click me
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

Also, we can change the position by writing:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <style>
      .container {
        border-radius: 50%;
        cursor: pointer;
        width: 150px;
        height: 150px;
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <div class="q-pa-md row justify-center">
        <div
          v-ripple.center
          class="relative-position container bg-grey-3 text-black inline flex flex-center"
        >
          Click me
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

Watch Scrolling

We can watch scrolling of an element with the v-scroll directive.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <div class="q-pa-md">
        <div v-scroll="scrolled">
          <p v-for="n of 1000" :key="n">{{n}}</p>
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {
          scrolled(position) {
            console.log(position);
          }
        }
      });
    </script>
  </body>
</html>

We pass in the scrolled event listener as the value of v-scroll .

Then we can get the position in pixels with the position parameter.

Trigger Scroll Effect Once

We can add the v-scroll-fire directive to trigger an effect once when the element is in view.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <style>
      .animate-bounce {
        animation: q-bounce 2s infinite;
      }

      @keyframes q-bounce {
        0%,
        20%,
        50%,
        80%,
        100% {
          transform: translateY(0);
        }
        40% {
          transform: translateY(-30px);
        }
        60% {
          transform: translateY(-15px);
        }
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <div class="q-pa-md">
        <div>
          <p v-for="n of 1000" :key="n">{{n}}</p>
          <img
            v-scroll-fire="bounceImage"
            src="https://cdn.quasar.dev/logo/svg/quasar-logo.svg"
          />
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {},
        methods: {
          bounceImage(el) {
            el.classList.add("animate-bounce");
            setTimeout(() => {
              if (document.body.contains(el)) {
                el.classList.remove("animate-bounce");
              }
            }, 2000);
          }
        }
      });
    </script>
  </body>
</html>

The bounceImage method is set as the value of the directive.

And it run once when the img element is in view to add the animate-bounce class and remove it after 2 seconds.

Conclusion

We can add various effects with the directives that are provided by Quasar.

Categories
Vue

Add a Gauge Chart to Your Vue.js App with VGauge

Gauge charts are handy for showing if something is close to the maximum or minimum amount. It’s handy for visualizing any quantities that have maximum or minimum values. Making our own would be a real pain since we have to draw shapes, lines and text on the canvas to create the gauge. Fortunately, developers have created premade solutions for us to use.

For Vue.js apps, we can use the VGauge library, located at https://github.com/amroessam/vgauge, to put gauges on our own apps. In this article, we will create a piggy bank app that lets users set their goals for saving and add their amount of money saved for a given date. We will display displaying the gauge to let users know if they reached their savings goal or not. To start, we will run the Vue CLI by running:

npx @vue/cli create piggy-bank

In the wizard, we select the ‘Manually select features’ and select Vue Router, Vuex, and Babel.

Next we install some packages. We will use Axios for making HTTP requests, BootstrapVue for styling, VGauge for displaying the gauge for showing their savings relative to their goal, VueFilterDateFormat for formatting dates in templates, and Vee-Validate for form validation. To install them, we run:

npm i axios bootstrap-vue vgauge vee-validate vue-filter-date-format

Now we can being building our app. Create a file called GoalForm.vue in the components folder and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Savings Amount Goal">
        <ValidationProvider name="amount" rules="required|min_value:0" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.amount"
            required
            placeholder="Savings Amount Goal"
            name="amount"
          ></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">Save</b-button>
    </b-form>
  </ValidationObserver>
</template>

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

export default {
  name: "SavingForm",
  mixins: [requestsMixin],
  data() {
    return {
      form: {}
    };
  },
  computed: {
    goal() {
      return this.$store.state.goal;
    }
  },
  beforeMount() {
    this.getSavingsGoal();
  },
  methods: {
    cancel() {
      this.$emit("cancelled");
    },

async getSavingsGoal() {
      const { data } = await this.getGoal();
      this.$store.commit("setGoal", data);
    },

async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      await this.setGoal(this.form);

const { data } = await this.getSavings();
      this.$store.commit("setGoal", data);
      this.$emit("saved");
    }
  },
  watch: {
    goal: {
      handler(val) {
        this.form = val || {};
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

The file is the form for letting users entering their savings goal. We have a getSavingsGoal function for getting the savings of the user from back end. It’s called in the beforeMount hook and at the end of the onSubmit function to get the latest values.

In the onSubmit function, we validate our form with Vee-Validate by calling this.$refs.observer.validate(); to make sure amount is entered and that it’s 0 or higher before saving it. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of the amount field. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component. This page only has the countries drop down.

Then create a SavingForm.vue file in the same 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"
          ></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="Amount Saved">
        <ValidationProvider name="amount" rules="required|min_value:0" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.amount"
            required
            placeholder="Amount Saved"
            name="amount"
          ></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">Save</b-button>
    </b-form>
  </ValidationObserver>
</template>

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

export default {
  name: "SavingForm",
  props: {
    saving: Object,
    edit: Boolean
  },
  mixins: [requestsMixin],
  data() {
    return {
      form: {}
    };
  },
  computed: {
    savings() {
      return this.$store.state.savings;
    }
  },
  methods: {
    cancel() {
      this.$emit("cancelled");
    },

    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.addSaving(params);
      } else {
        await this.editSaving(params);
      }
      const { data } = await this.getSavings();
      this.$store.commit("setSavings", data);
      this.$emit("saved");
    }
  },
  watch: {
    saving: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
        if (this.form.date) {
          this.form.date = moment(this.form.date).format("YYYY-MM-DD");
        }
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

The file is the form for letting users entering their savings data. We have a form with the date and amount fields to let users save their savings amount for the date they entered.

In the onSubmit function, we validate our form with Vee-Validate by calling this.$refs.observer.validate(); to make sure and date and amount are entered and that date is in YYYY-MM-DD format and amount is 0 or higher before saving it. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of the form fields. date is a custom made validation rule that we will add. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component. This page only has the countries drop down.

Next we create amixins folder in the src folder and create a file called requestsMixin.js file. In there, we add:

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

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

    addSaving(data) {
      return axios.post(`${APIURL}/savings`, data);
    },

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

    deleteSaving(id) {
      return axios.delete(`${APIURL}/savings/${id}`);
    },

    getGoal() {
      return axios.get(`${APIURL}/goal`);
    },

    setGoal(data) {
      return axios.post(`${APIURL}/goal`, data);
    }
  }
};

These are the functions to get and save our savings and savings goal data to back end.

Next in the views folder, we replace the code in the Home.vue file with:

<template>
  <div class="page">
    <h1 class="text-center">Piggy Bank App</h1>

    <b-button-toolbar class="button-toolbar">
      <b-button @click="openAddModal()" variant="primary">Add Money Saved</b-button>
      <b-button @click="openGoalModal()" variant="primary">Set Saving Goal</b-button>
    </b-button-toolbar>

    <div class="text-center" v-if="goal && totalSavings">
      <v-gauge :maxValue="+goal" :minValue="0" :value="+totalSavings" unit="USD" top />
      <p>Savings goal is {{+goal}} USD.</p>
    </div>

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

    <b-modal id="edit-modal" title="Edit Money Saved" hide-footer>
      <SavingForm
       @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :saving="selectedSaving"
      />
    </b-modal>

    <b-modal id="goal-modal" title="Set Saving Goal" hide-footer>
      <GoalForm@saved="closeModal()" @cancelled="closeModal()" />
    </b-modal>

    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th sticky-column>Date</b-th>
          <b-th>Money Saved</b-th>
          <b-th>Edit</b-th>
          <b-th>Delete</b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="s in savings" :key="s.id">
          <b-th sticky-column>{{ new Date(s.date) | dateFormat('YYYY-MM-DD') }}</b-th>
          <b-td>{{s.amount}}</b-td>
          <b-td>
            <b-button @click="openEditModal(s)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOneSaving(s.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    SavingForm,
    GoalForm,
    VGauge
  },
  mixins: [requestsMixin],
  data() {
    return {
      selectedSaving: {},
      goal: 0,
      totalSavings: 0
    };
  },
  computed: {
    savings() {
      return this.$store.state.savings.sort(
        (a, b) => +new Date(b.date) - +new Date(a.date)
      );
    }
  },
  beforeMount() {
    this.getAllSavings();
    this.getSavingsGoal();
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },

    openEditModal(saving) {
      this.$bvModal.show("edit-modal");
      this.selectedSaving = saving;
    },

    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.$bvModal.hide("goal-modal");
      this.getSavingsGoal();
      this.getAllSavings();
    },

    openGoalModal() {
      this.$bvModal.show("goal-modal");
    },

    async getAllSavings() {
      this.totalSavings = 0;
      const { data } = await this.getSavings();
      this.$store.commit("setSavings", data);
      this.totalSavings = +this.$store.state.savings
        .map(s => s.amount)
        .reduce((a, b) => +a + +b, 0);
    },

    async deleteOneSaving(id) {
      await this.deleteSaving(id);
      this.getAllSavings();
    },

    async getSavingsGoal() {
      this.goal = 0;
      const { data } = await this.getGoal();
      this.goal = +data.amount;
    }
  }
};
</script>

We have buttons to add saving data as well as edit and delete them in each row. The Add Money Saved and Set Saving Goal will open modals with the SavingForm and GoalForm respectively. We use the SavingForm for both adding and editing saving entries.

The savings data is obtained from back end and saved in our Vuex store. Once it’s there, we get the data in the computed property and then display the data from the computed property.

We have a table to display the saving data entered. We sort it by date descending date to display them in reverse chronological order. In the scripts section, we have the beforeMount hook to get all the password entries during page load with the getAllSavings function we wrote in our mixin. When the Edit button is clicked, the selectedSavingvariable is set, and we pass it to the SavingForm for editing.

To delete a recipe, we call deleteSavingin our mixin to make the request to the back end via the deleteOneSaving function.

The VGauge component is used here. The v-gauge element in the template will display the gauge graph. The maxValue prop is set to our saving goal, which is stored in the goal variable, and the value prop is set to the totalSavings variable. The units is set to USD and it’s displayed at the end of the amount, and the top prop makes the value display on the top of the gauge. The v-if=”goal && totalSavings” is very important. It allows us to refresh the graph by setting goal and totalSavings to 0 then setting the values obtained from the store or back end again. We did this in the getAllSavings and getSavingsGoal functions.

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="/">Piggy Bank App</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>
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  mixins: [requestsMixin],
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  },
  beforeMount() {
    this.getSavingsGoal();
  },
  methods: {
    async getSavingsGoal() {
      const { data } = await this.getGoal();
      this.$store.commit("setGoal", data);
    }
  }
};
</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 also add margins to our app’s buttons here.

In the script section, we get the savings goal by calling this.getSavingsGoal and put them in our Vuex store.

Next in main.js , we 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 } from "vee-validate/dist/rules";
import VueFilterDateFormat from "vue-filter-date-format";

extend("required", required);
extend("min_value", min_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.use(VueFilterDateFormat);
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. We created the date rule which verifies that the inputs that used the rule will be in YYYY-MM-DD format. Also, we added the VueFilterDateFormat package to format the dates in the Date column of our table in Home.vue .

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: {
    savings: [],
    goal: {}
  },
  mutations: {
    setSavings(state, payload) {
      state.savings = payload;
    },

    setGoal(state, payload) {
      state.goal = payload;
    }
  },
  actions: {}
});

to add our recipes state to the store so we can observer it in the computed block of GoalForm,SavingForm, and HomePage components. We have the setSavings function to update the savings state and we use it in the components by call this.$store.commit(“setSavings”, data); like we did in SavingForm , and the setGoal to set the savings goal data for the GoalForm and HomePage .

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>Piggy Bank App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vgauge-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.

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:

{
  "savings": [],
  "goal": {}
}

So we have the savings and goal endpoints defined in the requests.js available.

After all the hard work, we get:

Categories
Vue

Add a Progress Bar to Your Vue.js App with Vue-ProgressBar

A progress bar is a line that shows how close to completion something is in a GUI app. It’s provides a good user experience for users because they can know when something is complete and how close to completion it is, making users’ minds more comfortable.

Vue.js has many progress bar libraries built for it. One of them is Vue-ProgressBar, located at https://github.com/hilongjw/vue-progressbar. It is easy too incorporate to any Vue.js app and it’s very flexible, with lots of options you can change.

In this article, we will build an app that display Chuck Norris jokes from the Chuck Norris Jokes API, located at https://api.chucknorris.io/. The app will have a home page for displaying a random joke, a page that lets users look for a random joke by category, and a search page to search for jokes. To start, we will run the Vue CLI by running:

npx @vue/cli create chuck-norris-app

In the wizard, we select the ‘Manually select features’ and select Vue Router and Babel.

Next we install some packages. We will use Axios for making HTTP requests, BootstrapVue for styling, Vue-ProgressBar for adding our progress bar, and Vee-Validate for form validation. To install them, we run:

npm i axios bootstrap-vue vue-progressbar vee-validate

Next we create amixins folder in the src folder and create a file called requestsMixin.js file. In there, we add:

const APIURL = "https://api.chucknorris.io/jokes";
const axios = require("axios");

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

    getJokeByCategory(category) {
      return axios.get(`${APIURL}/random?category=${category}`);
    },

    getCategories() {
      return axios.get(`${APIURL}/categories`);
    },

    searchJokes(query) {
      return axios.get(`${APIURL}/search?query=${query}`);
    }
  }
};

This file has the code to call all the endpoints of the Chuck Norris Jokes API to get the jokes and categories, and also search for jokes by keyword.

Next in the views folder, we replace the code in the Home.vue file with:

<template>
  <div class="page">
    <h1 class="text-center">Random Joke</h1>
    <p>{{joke.value}}</p>
  </div>
</template>

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

export default {
  name: "home",
  mixins: [requestsMixin],
  data() {
    return {
      joke: {}
    };
  },
  beforeMount() {
    this.$Progress.start();
    this.getJoke();
  },
  methods: {
    async getJoke() {
      const { data } = await this.getRandomJoke();
      this.joke = data;
      this.$Progress.finish();
    }
  }
};
</script>

With the Vue-ProgressBar libary, we have the this.$Progress object available in all our components since we will add it to main.js . We call the this.$Progress.start(); to display the progress bar right before the HTTP request is made by calling the this.getRandomJoke function from requestsMixin . Then once the response is successfully retrieved, then we call this.$Progress.finish(); to make the progress bar disappear. In the template, we display the joke.

Next create a file calledJokeByCategory.vue in the views folder and add:

<template>
  <div class="page">
    <h1 class="text-center">Joke by Category</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form novalidate>
        <b-form-group label="Category">
          <ValidationProvider name="category" rules="required" v-slot="{ errors }">
            <b-form-select v-model="category" :options="categories" @change="getJoke()"></b-form-select>
            <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
      </b-form>
    </ValidationObserver>

<p>{{joke.value}}</p>
  </div>
</template>

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

export default {
  mixins: [requestsMixin],
  data() {
    return {
      category: "",
      categories: [],
      joke: {}
    };
  },
  beforeMount() {
    this.getJokeCategories();
  },

  methods: {
    async getJokeCategories() {
      this.$Progress.start();
      const { data } = await this.getCategories();
      this.categories = data.map(d => ({
        value: d,
        text: d
      }));
      this.$Progress.finish();
    },

    async getJoke() {
      this.$Progress.start();
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        this.$Progress.finish();
        return;
      }
      const { data } = await this.getJokeByCategory(this.category);
      this.joke = data;
      this.$Progress.finish();
    }
  }
};
</script>

In the beforeMount hook, we run the getJokeCategories , which call this.getCategories from the requestsMixin to get the categories when the page loads.

This page works the same as Home.vue . We display the progress bar when requests are started and remove it when the request is finished. This file makes 2 requests, one to get the categories from the API withn the this.categories function from the requestsMixin and the this.getJokesByCategory function from the same file. In the getJoke function, we validate our form with Vee-Validate by calling this.$refs.observer.validate(); to make sure category is selected before getting the joke. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of the category field. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component. This page only has the countries drop down.

Next, we add a Search.vue file in the views folder and add:

<template>
  <div class="page">
    <h1 class="text-center">Search</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form novalidate @submit.prevent="onSubmit">
        <b-form-group label="Keyword">
          <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
            <b-form-input
              type="text"
              :state="errors.length == 0"
              v-model="keyword"
              required
              placeholder="Search "
              name="keyword"
            ></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">Search</b-button>
      </b-form>
    </ValidationObserver>

<p v-for="(j, i) of jokes" :key="i">{{j.value}}</p>
  </div>
</template>

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

export default {
  mixins: [requestsMixin],
  data() {
    return {
      keyword: "",
      jokes: []
    };
  },
  methods: {
    async onSubmit() {
      this.$Progress.start();
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        this.$Progress.finish();
        return;
      }
      const {
        data: { result }
      } = await this.searchJokes(this.keyword);
      this.jokes = result;
      this.$Progress.finish();
    }
  }
};
</script>

We let users search for jokes from the API.

In the onSubmit function, we validate our form with Vee-Validate by calling this.$refs.observer.validate(); to make sure category is selected before getting the joke. We use Vee-Validate to validate the form fields. The ValidationObserver component is for validating the whole form, while the ValidationProvider component is for validating the form fields that it wraps around. The validation rule is specified by the rule prop of the category field. The state prop is for setting the validation state which shows the green when errors has length 0 and red otherwise. The error messages are shown in the b-form-invalid-feedback component. This page only has the countries drop down.

The progress bar works the same as the other components. We display the progress bar when searching for jokes by calling this.$Progress.start(); , then this.searchJokes and remove it when the request is finished. Finally this.$Progress.finish(); is called to make the progress bar disappear.

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

<template>
  <div id="app">
    <vue-progress-bar></vue-progress-bar>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Chuck Norris Jokes App</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-nav-item to="/jokebycategory" :active="path  == '/jokebycategory'">Jokes By Category</b-nav-item>
          <b-nav-item to="/search" :active="path  == '/search'">Search</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.

Next in main.js , replace the 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 } from "vee-validate/dist/rules";
import VueProgressBar from "vue-progressbar";

extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.use(VueProgressBar, {
  color: "rgb(143, 255, 199)",
  failedColor: "red",
  height: "2px"
});

Vue.config.productionTip = false;

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

so that we add the libraries we installed to our app so we can use it in our components. We call extend from Vee-Validate to add the form validation rules that we want to use. Also, we add the Vue-ProgressBar library here so we can use it in all our components. When we include it with Vue.use , we pass in the progress bar options as the second argument. In this app, we set the color to a greenish color, failed color to be red, and the height to be 2 pixels. We also imported the Bootstrap CSS in this file to get the styles.

In router.js , we replace the existing code with:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import JokeByCategory from './views/JokeByCategory.vue'
import Search from './views/Search.vue'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/jokebycategory',
      name: 'jokebycategory',
      component: JokeByCategory
    },
    {
      path: '/search',
      name: 'search',
      component: Search
    }
  ]
})

to include our home and search pages.

Finally, in index.html , 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>Chuck Norris Jokes App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-progress-bar-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.

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

Categories
JavaScript

Basic Guide to JavaScript Regular Expressions

Regular expressions let us manipulate strings with ease. They are patterns that let us match the text in pretty much in any way we imagine. Without it, we’ll have big trouble searching for texts with complex patterns. It’s also useful to check inputs against regular expressions for form validation.

In JavaScript, there’s a regular expression object. We can define it with a regular expression literal or use the RegExp constructor to define a regular expression object.

To define regular expression literals, we wrap our regular expression pattern with slashes. For example, we can write:

const re = /a/

to define a regular expression object to look for the letter ‘a’. Alternatively, we can use the regular expression constructor to define the regular expression object by passing in a string to the RegExp constructor as follows:

const re = new RegExp('a');

Using letters or digits as we have above are useful for searching for simple patterns like numbers and letters. If we want to search for more complex patterns, we have to use special characters to build a regular expression that lets us search for more complex patterns. Most of them are the same for most programming languages, but there may also be language-specific extensions.

Special Characters

Below are the special characters that we can use with JavaScript objects:

\

A backslash that precedes a non-special character indicates that the next character is special and not to be interpreted literally. For instance \b means a word boundary, while b matches the letter ‘b’.

A backslash that precedes a special character indicates the next character isn’t special and should be interpreted literally. For example, \^ will search for the ‘^’ character.

^

The ^ character matches the beginning of the input. If the multiline flag is set to true, it also matches immediately after a line break character.

For example, /^a/ matches the ‘a’ in string abc but not ‘a’ in cba .

$

The $ matches the end of the input. If the multiline flag is set to true, it also matches immediately after a line break character.

For example, /t$/ matches the ‘t’ in bat , but not the ‘t’ in tab .

*

The * matches the preceding expression 0 or more times. It’s the same as {0*} .

For instance, /o*/ matches the ‘oo’ in moo, but nothing in tab .

+

This character matches the expression preceding it 1 or more times. It’s the same as {1,} .

For example, /b+/ matches bb in cabbage but nothing in moo .

?

The question mark matches the expression preceding it 0 or 1 time. It’s the same as {0,1} .

For example, /b?a/ matches ba in bat , and a in cat .

If it’s used after any of the characters *, +, ? or {}, then it makes the ? non-greedy, which means it matches the fewest possible characters. This is the opposite of the default which is greedy, which matches as many characters as possible.

For example, [a-z]+ matches abc but [a-z]+? matches a .

.

The period matches any single character except the newline character by default.

For example .a matches ba in ba or bat .

(x)

Matches whatever pattern in the parentheses and remembers that match. The parentheses are called capturing parentheses. Then whole pattern is called a capturing group.

For example, if we have (abc)(def) \1 , then if we get a few matches for abcdef abcdef . We get abcdef abc , abc , and def The \1 is for denoting the first substring match, which is abc .

We can have more than one capturing group in one regular expression For example if we have (abc) (def) \1 \2 , then we get the matches abc def abc def , abc and def . As we can see, anything that matches the pattern in each capturing group and the combination of them is considered matches. \1 is the stands for the same thing as in the previous example, and \2 stands for the second substring, which is def .

(?:x)

This is similar to (x) but doesn’t remember the match.

For example, if we have the pattern (?:abc){1,2} , then if we have the string abc def abc def then the first abc will be matched.

x(?=y)

The pattern matches x only if x if before y . It’s called a lookahead pattern.

For example, if we have the pattern x(?=y) and the string xy , then we get the x as the match.

x(?!y)

This pattern matches x only if x isn’t followed by y . This is called a lookbehind.

For instance x(?!y) won’t match xylophone , but it’ll match the x in axe .

(?<!y)x

This pattern matches x only is x isn’t preceded by y . This is called a negated lookahead.

For example, if we have the pattern (?<=b)a and the string bat , then it’ll match the a in bat . However, the same pattern won’t match the word cat since b isn’t before a .

(?<!y)x

This pattern matches x only if x isn’t preceded by y . This is called a negated lookbehind.

For instance, if we have the pattern (?<!b)a and the string cat , then it’ll match the a in cat . However, it won’t match anything in bat .

x|y

Matches x or y . For example, if we have the pattern x|y and the word xylophone , then it’ll match the x .

{n}

Match exactly n occurrences of an expression. For example, if we have a{2} , then we match the aa in baa .

{n,}

This pattern matches at least n occurrences of the preceding expression where n is a positive integer.

For example, b{2,} matches bb ,bbb , bbbb and so on.

{a,b}

Matches an expression preceding this from a to b times. For example, if we have b{2,4} , it’ll match the strings bb ,bbb , bbbb and nothing else.

[xyz]

A pattern that matches one of the characters within the brackets, including escape sequences. For example, [xyz] will match the x in xylophone , and y in yawn .

Combining Special Characters to Form Complex Patterns

We can combine these special expressions to make a search for more complex patterns. One common example are validating email address.

A simple regular expression for email may be:

[a-z0-9.]+@[a-z0-9.]+.[a-z]

In the regular expression above, we have [a-z0–9.]+ which matches any digit or letters or a period occurring any number of times. This is followed by an @ sign, which is in every email address to separate the username from the domain name. Then we have [a-z0–9.]+ again to match any letter or digit or a period occurring any number of times. This is followed by a period. Then [a-z] to match the domain name.

In this article, we just began looking at regular expressions in JavaScript. Defining a regular expression can be done with a literal expression or the RegExp constructor. We can construct regular expressions with special characters which denote certain patterns. Then we can combine them into bigger regular expressions to search for more complex patterns like emails.