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.

Categories
Vue

Blur Web Page Elements Easily with V-Blur for Vue.js

Blurring elements is useful when you want something hidden. For example, it’s handy to blur something for paywalls. With CSS, blurring content is easy with the blur property. However, if you want to change the blurring dynamically, then the blur settings has to be changed by JavaScript. For Vue.js apps, there’s the V-Blur library to help us achieve the dynamic blur effect. This makes changing the blur setting as easy as adding a few lines of code.

In this article, we will make a news reader app which lets users blur and unnlur headline contents. There will be a home page where you can get headlines by country, and a search page for searching headlines by keyword. We will get our content from the News API, located at https://newsapi.org/docs. To start, we will run the Vue CLI by running:

npx @vue/cli create news-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, the Country-List for getting a list of country names and codes, V-Blur for adjusting the blur effects, and Vee-Validate for form validation. To install them, we run:

npm i axios bootstrap-vue country-list v-blur vee-validate

With all the libraries install, we can start writing our news app. First we create an .env file in the project’s root folder and add our API key there. The key of the entry should be VUE_APP_APIKEY and the value should be the API key you got from the News API website.

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

const APIURL = "https://newsapi.org/v2";
const axios = require("axios");

export const requestsMixin = {
  methods: {
    getHeadlines(country) {
      return axios.get(
        `${APIURL}/top-headlines?country=${country}&apiKey=${process.env.VUE_APP_APIKEY}`
      );
    },

    getEverything(keyword) {
      return axios.get(
        `${APIURL}/everything?q=${keyword}&apiKey=${process.env.VUE_APP_APIKEY}`
      );
    }
  }
};

This file has the code to get the headlines by country and keyword from the News API.

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

<template>
  <div class="page">
    <h1 class="text-center">Headlines</h1>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="getHeadlinesByCountry" novalidate>
        <b-form-group>
          <ValidationProvider name="country" rules="required" v-slot="{ errors }">
            <b-form-select v-model="country" @change="getHeadlinesByCountry">
              <option :value="c.code" v-for="c of countries" :key="c.code">{{c.name}}</option>
            </b-form-select>
            <b-form-invalid-feedback :state="errors.length == 0">Country is requied.</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
      </b-form>
    </ValidationObserver>

    <b-card
      v-for="(h, i) in headlines"
      :title="h.title"
      :img-src="h.urlToImage"
      img-bottom
      :key="i"
    >
      <b-card-text v-blur="blurConfigs[i]">{{h.content}}</b-card-text>
      <b-button
        variant="primary"
        @click="blurConfigs[i].isBlurred = !blurConfigs[i].isBlurred"
      >Toggle Summary</b-button>
      <b-button :href="h.url" target="_blank" variant="primary">Read</b-button>
    </b-card>
  </div>
</template>

<script>
// @ is an alias to /src
const { getName, getData } = require("country-list");
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "home",
  mixins: [requestsMixin],
  data() {
    return {
      countries: getData(),
      country: "US",
      headlines: [],
      blurConfigs: []
    };
  },
  beforeMount() {
    this.getHeadlinesByCountry();
  },
  methods: {
    async getHeadlinesByCountry() {
      const { data } = await this.getHeadlines(this.country);
      this.headlines = data.articles;
      this.blurConfigs = this.headlines.map(h => ({
        isBlurred: true,
        opacity: 0.3,
        filter: "blur(1.2px)",
        transition: "all .3s linear"
      }));
    }
  }
};
</script>

We have the password form in this component. The form includes name, URL, username, and password fields. All of them are required. 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 each 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 data in the drop down is populated with the country-list package we installed.

In the beforeMount hook, we run the getHeadlinesByCountry to get the headlines by running the this.getHeadlines from the requestsMixin, with the initial value for country , which is set to the "US”. Once we get the data, we map them to the default blur config so that we can toggle it in the template. In the template, we have the cards to display the headlines. They are blurred by default. Below the headline, we have a toggle button to toggle the blurring of the news summary for each entry.

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 @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Keyword">
          <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
            <b-form-input
              type="text"
              v-model="form.name"
              placeholder="Keyword"
              name="keyword"
              :state="errors.length == 0"
            ></b-form-input>
            <b-form-invalid-feedback :state="errors.length == 0">Keyword is requied.</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>
        <b-button type="submit" variant="primary">Search</b-button>
      </b-form>
    </ValidationObserver>

    <b-card
      v-for="(h, i) in headlines"
      :title="h.title"
      :img-src="h.urlToImage"
      img-bottom
      :key="i"
    >
      <b-card-text v-blur="blurConfigs[i]">{{h.content}}</b-card-text>
      <b-button
        variant="primary"
        @click="blurConfigs[i].isBlurred = !blurConfigs[i].isBlurred"
      >Toggle Summary</b-button>
      <b-button :href="h.url" target="_blank" variant="primary">Read</b-button>
    </b-card>
  </div>
</template>

<script>
// @ is an alias to /src
const { getName, getData } = require("country-list");
import { requestsMixin } from "@/mixins/requestsMixin";

export default {
  name: "search",
  mixins: [requestsMixin],
  data() {
    return {
      form: {},
      headlines: [],
      blurConfigs: []
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const { data } = await this.getEverything(this.form.keyword);
      this.headlines = data.articles;
      this.blurConfigs = this.headlines.map(h => ({
        isBlurred: true,
        opacity: 0.3,
        filter: "blur(1.2px)",
        transition: "all .3s linear"
      }));
    }
  }
};
</script>

In this file, we added a form to let users search for news headlines by keyword. We call onSubmit when the user click Search on the form. We get the validation state of the form by using this.$refs.observer.validate(); . The ref refers to the ref of the ValidationObserver . If it resolves to true , then we call the this.getEverything function from the requestsMixin to get the headlines by keyword. Once we get the headlines, we map them to the default blur config so that we can toggle it in the template. In the template, we have the cards to display the headlines. They are blurred by default. Below the headline, we have a toggle button to toggle the blurring of the news summary for each entry.

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="/">News 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="/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 BootstrapVue from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import vBlur from "v-blur";

Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
extend("required", required);
Vue.use(vBlur);
Vue.use(BootstrapVue);

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 V-Blur library here so we can use it in all our components. We 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 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: "/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>News App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but v-blur-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

Formatting Relative Time with JavaScript’s RelativeTimeFormat Constructor

With the Intl.RelativeTimeFormat constructor, we can format relative time in a locale sensitive manner with ease. We can style in different ways and format it the way we want. This lets us format relative time strings without much hassle that comes from manipulating strings. The constructor takes 2 arguments. The first argument is for a locale string or an array of such strings. The second is an object which takes variety of arguments for adjusting the relative time string to the way we want. The instance of this constructor has a few methods to return the formatted string, the formatted string as an array of substrings, and a method to return the options that we set for formatting the string.

The first argument for the Intl.RelativeTimeFormat constructor, is the locale, which should be a BCP 47 language tag, or an array of such locale strings. This is an optional argument. It takes a BCP 47 language tag for the locale. An abridged list of BCP 47 language tags include:

  • ar — Arabic
  • bg — Bulgarian
  • ca — Catalan
  • zh-Hans — Chinese, Han (Simplified variant)
  • cs — Czech
  • da — Danish
  • de — German
  • el — Modern Greek (1453 and later)
  • en — English
  • es — Spanish
  • fi — Finnish
  • fr — French
  • he — Hebrew
  • hu — Hungarian
  • is — Icelandic
  • it — Italian
  • ja — Japanese
  • ko — Korean
  • nl — Dutch
  • no — Norwegian
  • pl — Polish
  • pt — Portuguese
  • rm — Romansh
  • ro — Romanian
  • ru — Russian
  • hr — Croatian
  • sk — Slovak
  • sq — Albanian
  • sv — Swedish
  • th — Thai
  • tr — Turkish
  • ur — Urdu
  • id — Indonesian
  • uk — Ukrainian
  • be — Belarusian
  • sl — Slovenian
  • et — Estonian
  • lv — Latvian
  • lt — Lithuanian
  • tg — Tajik
  • fa — Persian
  • vi — Vietnamese
  • hy — Armenian
  • az — Azerbaijani
  • eu — Basque
  • hsb — Upper Sorbian
  • mk — Macedonian
  • tn — Tswana
  • xh — Xhosa
  • zu — Zulu
  • af — Afrikaans
  • ka — Georgian
  • fo — Faroese
  • hi — Hindi
  • mt — Maltese
  • se — Northern Sami
  • ga — Irish
  • ms — Malay (macrolanguage)
  • kk — Kazakh
  • ky — Kirghiz
  • sw — Swahili (macrolanguage)
  • tk — Turkmen
  • uz — Uzbek
  • tt — Tatar
  • bn — Bengali
  • pa — Panjabi
  • gu — Gujarati
  • or — Oriya
  • ta — Tamil
  • te — Telugu
  • kn — Kannada
  • ml — Malayalam
  • as — Assamese
  • mr — Marathi
  • sa — Sanskrit
  • mn — Mongolian
  • bo — Tibetan
  • cy — Welsh
  • km — Central Khmer
  • lo — Lao
  • gl — Galician
  • kok — Konkani (macrolanguage)
  • syr — Syriac
  • si — Sinhala
  • iu — Inuktitut
  • am — Amharic
  • tzm — Central Atlas Tamazight
  • ne — Nepali
  • fy — Western Frisian
  • ps — Pushto
  • fil — Filipino
  • dv — Dhivehi
  • ha — Hausa
  • yo — Yoruba
  • quz — Cusco Quechua
  • nso — Pedi
  • ba — Bashkir
  • lb — Luxembourgish
  • kl — Kalaallisut
  • ig — Igbo
  • ii — Sichuan Yi
  • arn — Mapudungun
  • moh — Mohawk
  • br — Breton
  • ug — Uighur
  • mi — Maori
  • oc — Occitan (post 1500)
  • co — Corsican
  • gsw — Swiss German
  • sah — Yakut
  • qut — Guatemala
  • rw — Kinyarwanda
  • wo — Wolof
  • prs — Dari
  • gd — Scottish Gaelic

The second argument accepts an object with a few properties — localeMatcher , numeric , and style .

The localeMatcher option specifies the locale matching algorithm to use. The possible values are lookup and best fit . The lookup algorithm search for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited that the lookup algorithm.

The numeric option lets us set the option for how the formatted string’s message is outputted. The possible values are always which is like ‘2 days ago’, or auto , for example, like ‘yesterday’. The auto allows us to not always use numeric values for output. The style option lets us change the length of the internationalized message. The possible values are long , short , or narrow . long would output something like ‘in 2 months’, short would be something like ‘in 2 mo.’, and narrow would be something like in ‘in 2 mo.’. It could be similar to the short style in some locales.

Instances of the Intl.RelativeTimeFormat constructor has a few methods. It has the format method to get the formatted relative time string with the value and the unit according to the locale and the formatting option that’s given in the constructor. The formatToParts method is similar to the format method except that the formatted string is returned as an array instead of a string. The resolvedOptions method returns an object with the options that we set in the constructor for formatting the string and the locale that were set.

The format method takes 2 arguments. The first is the value for quantity of the relative date and the second is the time unit in string form. For example, we can format relative dates with the format method like in the following code:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

console.log(rtf.format(-1, "day"));

The code above would log ‘1 day ago’ since we specified the value of the relative date to be -1, which means 1 day before today, and the time unit is day . We can also put in other units, for example, if we want minutes, then we get:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

console.log(rtf.format(-10, "minute"));

Then we get ’10 minutes ago’ from the console.log statement. We can also change the style and the length. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "short",
});
console.log(rtf.format(10, "minute"));

Then we get ‘in 10 min.’ from the console.log statement since we have positive 10 instead of negative 10 which is 10 minutes from the current time. Also we had the short style. which abbreviates the unit.

We can also change the locale for different locales. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});
console.log(rtf.format(1, "minute"));

to get the relative date time string in Chinese Traditional characters instead of English. If we run the console.log statement in the code above, we get ‘1 分鐘後’, which means 1 minute later.

We can get the formatted string in an array of string parts with the formatToParts() method. It returns an array of substrings of the formatted strings. For example, we can call it like in the following code:

const rtf = new Intl.RelativeTimeFormat("en", {
  localeMatcher: "best fit",
  numeric: "always",
  style: "long",
});

const parts = rtf.formatToParts(-1, "day");
console.log(parts);

The code above would get us:

[
  {
    "type": "integer",
    "value": "1",
    "unit": "day"
  },
  {
    "type": "literal",
    "value": " day ago"
  }
]

with the console.log statement in the code above.

The method works equally well with non-English locales. For example, we can write:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});

const parts = rtf.formatToParts(1, "minute")
console.log(parts);

Then we get:

[
  {
    "type": "integer",
    "value": "1",
    "unit": "minute"
  },
  {
    "type": "literal",
    "value": " 分鐘後"
  }
]

with the console.log statement in the code above.

The resolvedOptions() method gets us an object with the options that we set in the constructor for formatting the string and the locale that were set. We can use it like in the following code:

const rtf = new Intl.RelativeTimeFormat("zh-hant", {
  localeMatcher: "best fit",
  numeric: "auto",
  style: "long",
});
console.log(rtf.`resolvedOptions`());

With the code above, we get the following from the console.log statement:

{
  "locale": "zh-Hant",
  "style": "long",
  "numeric": "auto",
  "numberingSystem": "latn"
}

The Intl.RelativeDateFormat constructor also has a supportedLocalesOf method to get us the supported locales for formatting date and time. It take an array of BCP 47 locale strings as an argument. Unicode extension keys will be returned along with the locale code even though it has no relevance for date formatting if provided. It takes the localeMatcher option to specify the locale matching algorithm to use. The possible values are lookup and best fit . The lookup algorithm search for the locale until it finds the one that fits the character set of the strings that are being compared. best fit finds the locale that is at least but possibly more suited that the lookup algorithm.

For example, we can use it like in the following code:

const locales = ['en-ca', 'id-u-co-pinyin', 'ban'];
const options = {
  localeMatcher: 'lookup'
};

console.log(Intl.RelativeTimeFormat.supportedLocalesOf(locales, options));

Then we get [“en-CA”, “id-u-co-pinyin”] . This is because Balinese is similar enough to Indonesian to be considered the same for the lookup algorithm. Note that the Unicode extensions that are in the input array are returned along with the output even though it has no relevance in this context.

The JavaScript Intl.RelativeTimeFormat constructor let us format relative time in a locale sensitive manner with ease. We can style in different ways and format it the way we want. This lets us format relative time strings without much hassle that comes from manipulating strings. The constructor takes 2 arguments. The first argument is for a locale string or an array of such strings. The second is an object which takes variety of arguments for adjusting the relative time string to the way we want. The instance of this constructor has a few methods to return the formatted string, the formatted string as an array of substrings, and a method to return the options that we set for formatting the string. We can also check the locales that supported with the static supportedLocalesOf method.

Categories
Quasar

Developing Vue Apps with the Quasar Library — Intersection Observer

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.

Intersection Directive

We can watch for element visibility with Quasar’s wrapper on the Intersection Observer API.

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>
      .state {
        background: #ccc;
        font-size: 20px;
        color: gray;
        padding: 10px;
        opacity: 0.8;
      }

      .observed {
        width: 100%;
        font-size: 20px;
        color: #ccc;
        background: #282a37;
        padding: 10px;
      }

      .area {
        height: 300px;
      }

      .filler {
        height: 500px;
      }
    </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="relative-position">
        <div class="area q-pa-lg scroll">
          <div class="filler"></div>

          <div
            v-intersection="onIntersection"
            class="observed text-center rounded-borders"
          >
            Observed Element
          </div>

          <div class="filler"></div>
        </div>

        <div
          class="state rounded-borders text-center absolute-top q-mt-md q-ml-md q-mr-lg text-white"
          :class="visibleClass"
        >
          {{ visible === true ? 'Visible' : 'Hidden' }}
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          visible: false
        },
        computed: {
          visibleClass() {
            return `bg-${this.visible ? "positive" : "negative"}`;
          }
        },
        methods: {
          onIntersection(entry) {
            this.visible = entry.isIntersecting;
          }
        }
      });
    </script>
  </body>
</html>

We add the v-intersection directive to run the onIntersection method went the intersection status changes.

We get the intersection status with the entry.isIntersecting property.

The handler will run when the div with the directive applied intersections the edge of the scroll container.

We add the visibleClass computed property to return the class to apply when the visible reactive property changes.

We can make the onIntersection method trigger only once with the once modifier.

Also, we can watch for the intersection percentage by referencing the entry.isIntersection property:

<!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>
      .state {
        background: #ccc;
        font-size: 20px;
        color: gray;
        padding: 10px;
        opacity: 0.8;
      }

      .observed {
        width: 100%;
        font-size: 20px;
        color: #ccc;
        background: #282a37;
        padding: 10px;
      }

      .area {
        height: 300px;
      }

      .filler {
        height: 500px;
      }
    </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="relative-position">
        <div class="area q-pa-lg scroll">
          <div class="filler"></div>

<div
            v-intersection="onIntersection"
            class="observed text-center rounded-borders"
          >
            Observed Element
          </div>

          <div class="filler"></div>
        </div>

        <div
          class="state rounded-borders text-center absolute-top q-mt-md q-ml-md q-mr-lg text-white"
          :class="visibleClass"
        >
          Percent: {{ percent }}%
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          visible: false,
          percent: 0
        },
        computed: {
          visibleClass() {
            return `bg-${this.visible ? "positive" : "negative"}`;
          }
        },
        methods: {
          onIntersection(entry) {
            this.visible = entry.isIntersecting;
            const percent = (entry.intersectionRatio * 100).toFixed(0);
            if (this.percent !== percent) {
              this.percent = percent;
            }
          }
        }
      });
    </script>
  </body>
</html>

Also, we can use it to render items that are displayed on the screen:

<!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>
      .item {
        height: 200px;
        width: 200px;
      }
    </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 class="row justify-center q-gutter-sm">
          <div
            v-for="index in inView.length"
            :key="index"
            :data-id="index - 1"
            class="item q-pa-sm flex flex-center relative-position"
            v-intersection="onIntersection"
          >
            <transition name="q-transition--scale">
              <q-card v-if="inView[index - 1]">
                <img src="https://cdn.quasar.dev/img/mountains.jpg" />

                <q-card-section>
                  <div class="text-h6">Card #{{ index }}</div>
                  <div class="text-subtitle2">by John Doe</div>
                </q-card-section>
              </q-card>
            </transition>
          </div>
        </div>
      </div>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          inView: Array(50)
            .fill()
            .map(() => false)
        },
        methods: {
          onIntersection(entry) {
            const index = +entry.target.dataset.id;
            setTimeout(() => {
              this.inView.splice(index, 1, entry.isIntersecting);
            }, 50);
          }
        }
      });
    </script>
  </body>
</html>

All we have to do is call splice to set the items that are displayed on the screen.

Conclusion

We can watch for intersections with Quasar’s wrapper for the Intersection Observer API.