Categories
JavaScript Vue

Validate Phone Number with vee-validate

To validate phone numbers with vee-validate, we can use the regex rule so that we can validate any phone number format we want.

First, we install the package by running:

npm i vee-validate

Next, we register the plugin in our Vue app by writing:

import Vue from "vue";
import App from "./App.vue";
import { ValidationProvider, extend } from "vee-validate";
import { regex } from "vee-validate/dist/rules";

extend("regex", regex);

Vue.component("ValidationProvider", ValidationProvider);

Vue.config.productionTip = false;

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

We used the ValidationProvider package which is used to add form validation capabilities to all components in our app.

Also, we import the regex rule from the package and register it with extend so we can use the regex rule.

Then in App.vue, we write:

<template>
  <div id="app">
    <ValidationProvider :rules="{ regex:/^[2-9]\d{2}[2-9]\d{2}\d{4}$/ }" v-slot="{ errors }">
      <input name="phone" type="text" v-model="phone">
      <p>{{ errors[0] }}</p>
    </ValidationProvider>
  </div>
</template>

<script>
export default {
  name: "App",

  data() {
    return {
      phone: ""
    };
  }
};
</script>

to add a phone input field.

We wrap the ValidationProvider around the input box. The validation rule is defined by passion in an object as the value of the rules prop.

Also, we have the errors object obtained from the ValidationProvider component so that we can display form validation errors.

The name attribute must be set since it’s used for by the form validation message.

The regex /^[2-9]\d{2}[2-9]\d{2}\d{4}$/ is the regex for a North American phone number.

Once we did that, when we type in anything other than a North American phone number, we’ll see an error message displayed.

Categories
JavaScript Vue

Build a Drag and Drop Grid with Vuejs

We can create a draggable and sortable grid with the vue-js-grid package.

It’s easy to use.

First, we install it by running:

npm install vue-js-grid

Next, we can use it by writing:

main.js

import Vue from "vue";
import App from "./App.vue";
import Grid from "vue-js-grid";

Vue.use(Grid);

Vue.config.productionTip = false;

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

We registered the Grid component from vue-js-grid globally with Vue.use.

App.vue

<template>
  <div id="app">
    <grid :draggable="true" :sortable="true" :items="items" :height="100" :width="100">
      <template slot="cell" scope="props">
        <div>{{props.item}}</div>
      </template>
    </grid>
  </div>
</template>

<script>
export default {
  name: "App",

  data() {
    return {
      items: Array(50)
        .fill()
        .map((_, i) => i)
    };
  }
};
</script>

We created an array with 50 minutes as the property of items.

To do that we created an array with 50 elements with Array(50).

Then we called fill and map to fill the entries with numbers.

In the template, we have the grid component with a few props.

draggable is set to true to make the grid items draggable.

sortable is also set to true so that we can sort then entries.

items is set to items. This is the prop for setting the grid items.

height sets the height of the grid in percentage.

width sets the width as a percentage.

In the grid component, we have the template to render to grid items.

Now we get a responsive grid with 50 numbers displayed as the content.

It responds to resizing so it’s responsive.

We can drag and drop them grid items as we wish.

This is one of the easiest components for creating a drag and drop grid.

Categories
JavaScript Vue

Adding Form Validation with vue-form-generator

Creating forms with validation can be a pain in a Vue.js project.

Therefore, solutions exists to let us create forms easily.

One of them if the vue-form-generator package.

To use it, we install the package by running:

npm install vue-form-generator

Then we can use it by including the library in our app:

main.js

import Vue from "vue";
import App from "./App.vue";
import VueFormGenerator from "vue-form-generator";
import "vue-form-generator/dist/vfg.css";

Vue.use(VueFormGenerator);
Vue.config.productionTip = false;

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

We include the CSS file that comes with the package.

Also, we call Vue.use with the package name to include the package.

Then we in App.vue, we write:

<template>
  <div id="app">
    <vue-form-generator :schema="schema" :model="model" :options="formOptions"></vue-form-generator>
    <p>{{model}}</p>
  </div>
</template>

<script>
import VueFormGenerator from "vue-form-generator";
VueFormGenerator.validators.email = value => {
  if (!/\S+@\S+\.\S+/.test(value)) {
    return ["invalid email"];
  } else {
    return [];
  }
};

export default {
  name: "App",
  data() {
    return {
      model: {
        id: 1,
        name: "jane smith",
        skills: ["Javascript"],
        email: "janesmith@example.com",
        status: true
      },
      schema: {
        fields: [
          {
            type: "input",
            inputType: "text",
            label: "ID ",
            model: "id",
            readonly: true,
            disabled: true
          },
          {
            type: "input",
            inputType: "text",
            label: "Name",
            model: "name",
            placeholder: "name",
            featured: true,
            required: true
          },
          {
            type: "select",
            label: "Skills",
            model: "skills",
            values: ["Javascript", "VueJS", "CSS3", "HTML5"]
          },
          {
            type: "input",
            inputType: "email",
            label: "E-mail",
            model: "email",
            placeholder: "e-mail address",
            validator: VueFormGenerator.validators.email
          },
          {
            type: "checkbox",
            label: "Status",
            model: "status",
            default: true
          }
        ]
      },
      formOptions: {
        validateAfterLoad: true,
        validateAfterChanged: true,
        validateAsync: true
      }
    };
  }
};
</script>

We import the vue-form-generator package’s VueFormGenerator component, then we add our own validator function by writing:

VueFormGenerator.validators.email = value => {
  if (!/\S+@\S+\.\S+/.test(value)) {
    return ["invalid email"];
  } else {
    return [];
  }
};

We just check if what we entered matched the email format and return an array with our form validation error message if it doesn’t.

Otherwise, we return an empty array, which indicates that what we entered is valid.

Then in our data method, we return:

{
  model: {
    id: 1,
    name: "jane smith",
    skills: ["Javascript"],
    email: "janesmith@example.com",
    status: true
  },
  schema: {
    fields: [{
        type: "input",
        inputType: "text",
        label: "ID ",
        model: "id",
        readonly: true,
        disabled: true
      },
      {
        type: "input",
        inputType: "text",
        label: "Name",
        model: "name",
        placeholder: "name",
        featured: true,
        required: true
      },
      {
        type: "select",
        label: "Skills",
        model: "skills",
        values: ["Javascript", "VueJS", "CSS3", "HTML5"]
      },
      {
        type: "input",
        inputType: "email",
        label: "E-mail",
        model: "email",
        placeholder: "e-mail address",
        validator: VueFormGenerator.validators.email
      },
      {
        type: "checkbox",
        label: "Status",
        model: "status",
        default: true
      }
    ]
  },
  formOptions: {
    validateAfterLoad: true,
    validateAfterChanged: true,
    validateAsync: true
  }
};

We use the validator function we created as the value of the validator property.

The schema property is used to define the form field behavior with various properties.

The model property will be the value that’s populated when we enter something into the form.

formOptions includes form options for changing validation behavior. We specified that we want to validate after the form is loaded or changed, and do both asynchronously

In the template, we have the vue-form-generator component, with the schema and model returned in the object returned by the data method.

In the end, we get:

https://thewebdev.info/wp-content/uploads/2020/05/form.png

It’s easy to create a form with validation with the vue-form-generator package.

All we have to do is to pass in some objects as props into the vue-form-generator component including the models and form field options and then we’re set.

Categories
JavaScript Vue

Introduction to Vue.js Testing

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue.js apps by writing a simple app and testing it.

Getting Started

To get started, we create an app that gets a joke from the Chuck Norris Jokes API.

We start by creating an empty folder, going into it, and running the Vue CLI by running:

npx vue create .

In the wizard, we select Unit Tests, then choose Jest and then proceed.

Now that we have the files generated, we can change some code. We can delete the components folder and replace the code in App.vue with:

<template>  
  <div id="app">  
    <button @click='toggleJoke()'>{{jokeHidden ? 'Show' : 'Hide'}} Joke</button>  
    <p v-if="!jokeHidden">{{data.value.joke}}</p>  
  </div>  
</template>

<script>  
export default {  
  name: "app",  
  data() {  
    return {  
      jokeHidden: false,  
      data: { value: {} }  
    };  
  },  
  beforeMount() {  
    this.getJoke();  
  },  
  methods: {  
    async getJoke() {  
      const res = await fetch("http://api.icndb.com/jokes/random");  
      this.data = await res.json();  
    }, 

    toggleJoke() {  
      this.jokeHidden = !this.jokeHidden;  
    }  
  }  
};  
</script>

<style>  
#app {  
  font-family: "Avenir", Helvetica, Arial, sans-serif;  
  -webkit-font-smoothing: antialiased;  
  -moz-osx-font-smoothing: grayscale;  
  text-align: center;  
  color: #2c3e50;  
  margin-top: 60px;  
}  
</style>

The code just gets a joke from the API and then display it. Also, it has a button to show and hide the joke.

Our app looks something like the following:

https://thewebdev.info/wp-content/uploads/2020/04/chuck.png

Creating the Tests

Now that we have something to test, we can actually write the tests.

In the tests/unit folder, we delete what we have then create app.spec.js in that folder.

Then we open the file we created and add:

import { mount } from '@vue/test-utils';  
import App from '@/App.vue'

const mockResponse = {  
  "type": "success",  
  "value": {  
    "id": 178,  
    "joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",  
    "categories": []  
  }  
}

To import the component that we’ll test, the mount function to let the Vue Test Utils build and render the component for testing, and the mockResponse object that we’ll use to set the mock data.

Then we add the skeleton for our test by writing:

describe('App.vue', () => {  
  beforeEach(() => {  
    jest.clearAllMocks()  
  })  
})

We have the string description for our test suite and a callback which we add out tests to.

Inside the callback, we have the beforeEach hook to clear all the mocks by running jest.clearAllMocks() .

We need this because we’ll mock some of the functions in our component later.

Adding our First Test

Next, we write our first test. This test will simulate getting the data from the API and then displaying the joke on the screen.

It won’t actually get the joke from the server since we want our test to run anywhere and at any time. Getting it from the server won’t let us do that.

The API returns something different every time we call it and also it might not always be available.

With that in mind, we write:

it('renders joke', async () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)  
  })

in the callback we passed into the describe function after the beforeEach call.

The test above calls mount on our App component to build and render the component and returns a Wrapper object to let us access it.

In the second argument, we pass in the options with the methods property so that we can mock the getJoke method with Jest with jest.fn(). We want to mock it so that our test doesn’t call the API.

Once we have the wrapper then we run:

wrapper.vm.data = mockResponse;

to set the mockResponse data to the data property of our component instance.

Once we did that, we check that we get the joke in our mockResponse rendered by writing:

expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)

since we put our joke in the p tag in our App component.

The expect method and toMatch are from Jest.

Writing Test that Interacts with UI Elements

Writing a test that does something to UI elements like buttons isn’t that much more work.

To test the button that we added to our app actually shows and hides the joke, we write:

it('toggles joke', () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Show Joke');  
    expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);  
  }
)

The first part:

const wrapper = mount(App, {  
  methods: {  
    getJoke: jest.fn()  
  }  
});  
wrapper.vm.data = mockResponse;

is the same as before. We mock the getJoke function with jest.fn() so that our test won’t call the API. Then set the mock data.

Next, we check the button text by writing:

expect(wrapper.find('button').text()).toMatch('Hide Joke');

and that our mocked joke is shown in the p element:

expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);

Then we click our button by running:

wrapper.find('button').trigger('click');

And then check for the text of the button and whether the p element is removed by our v-if directive:

expect(wrapper.find('button').text()).toMatch('Show Joke');  
expect(wrapper.find('p').exists()).toBe(false);

Finally, we can do the click again and check if the joke is shown again as follows:

wrapper.find('button').trigger('click');  
expect(wrapper.find('button').text()).toMatch('Hide Joke');  
expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);

Running the Tests

Together, we have the following test code in app.test.js :

import { mount } from '@vue/test-utils';  
import App from '@/App.vue'

const mockResponse = {  
  "type": "success",  
  "value": {  
    "id": 178,  
    "joke": "In an act of great philanthropy, Chuck made a generous donation to the American Cancer Society. He donated 6,000 dead bodies for scientific research.",  
    "categories": []  
  }  
}

describe('App.vue', () => {  
  beforeEach(() => {  
    jest.clearAllMocks()  
  }) 

  it('renders joke', async () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke)  
  }) 

  it('toggles joke', () => {  
    const wrapper = mount(App, {  
      methods: {  
        getJoke: jest.fn()  
      }  
    });  
    wrapper.vm.data = mockResponse;  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
      expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Show Joke');  
    expect(wrapper.find('p').exists()).toBe(false); wrapper.find('button').trigger('click');  
    expect(wrapper.find('button').text()).toMatch('Hide Joke');  
    expect(wrapper.find('p').text()).toMatch(mockResponse.value.joke);  
  })  
})

Then we run the tests by npm run test:unit .

We should get:

PASS  tests/unit/app.spec.js  
  App.vue  
    √ renders joke (19ms)  
    √ toggles joke (11ms)
Test Suites: 1 passed, 1 total  
Tests:       2 passed, 2 total  
Snapshots:   0 total  
Time:        2.102s  
Ran all test suites.

every time that we run our tests since we mocked the data.

Conclusion

Vue CLI creates a project that has unit testing built-in if we choose to include it. This saves us lots of work.

Jest is an easy test runner with lots of features like mocking and expect matchers that we can use.

To test UI components, we use the wrapper object returned by mount , which has the rendered component. Then we can use find to search the DOM for what we want to look for.

If the element exists, we can also trigger events on it by calling the trigger method with the event that we want to fire.

Finally, we have the exists method to check if the element we look for actually exists.

Categories
JavaScript Vue

Vue Router 4 — Navigation

Vue Router 4 is in beta and it’s subject to change.

To build a single page app easily, we got to add routing so that URLs will be mapped to components that are rendered.

In this article, we’ll look at how to use Vue Router 4 with Vue 3.

Navigation

We can navigate to a route with the this.$router ‘s methods.

For example, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo">Go to Foo</router-link>
        <router-link to="/bar">Go to Bar</router-link>
        <a @click="goBack" href="#">Go Back</a>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = { template: "<div>foo</div>" };
      const Bar = { template: "<div>bar</div>" };

      const routes = [
        { path: "/foo", component: Foo },
        { path: "/bar", component: Bar }
      ];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({
        methods: {
          goBack() {
            window.history.length > 1
              ? this.$router.go(-1)
              : this.$router.push("/foo");
          }
        }
      });
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

to create a goBack method and call it when we click on the Go Back link.

In the method, we check if there are anything in the browser history.

If there is, then we go back to the previous page.

Otherwise, we go to the /foo page.

Reacting to Params Changes

We can react to route params changes in a few ways.

For instance, we can write:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="https://unpkg.com/vue@next"></script>
    <script src="https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        watch: {
          $route(to, from) {
            console.log(to, from);
          }
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

We have the /foo/:id route that maps to the Foo component.

And in the Foo component, we have the $route watcher to watch the route.

to has the route to go to. from has the route that we departed from.

They’re both objects with the path, route metadata, route parameters, query parameters, and more.

Also, we can use the beforeRouteUpdate method to watch for route changes:

<!DOCTYPE html>
<html lang="en">
  <head>
    <script src="[https://unpkg.com/vue@next](https://unpkg.com/vue@next)"></script>
    <script src="[https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js](https://unpkg.com/vue-router@4.0.0-beta.7/dist/vue-router.global.js)"></script>
    <title>App</title>
  </head>
  <body>
    <div id="app">
      <p>
        <router-link to="/foo/1">Foo 1</router-link>
        <router-link to="/foo/2">Foo 2</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script>
      const Foo = {
        template: "<div>foo</div>",
        beforeRouteUpdate(to, from, next) {
          console.log(to, from);
          next();
        }
      };

      const routes = [{ path: "/foo/:id", component: Foo }];

      const router = VueRouter.createRouter({
        history: VueRouter.createWebHistory(),
        routes
      });

      const app = Vue.createApp({});
      app.use(router);
      app.mount("#app");
    </script>
  </body>
</html>

Instead of watching the $route object, we have the beforeRouteUpdate hook, which is made available with the app.use(router) call.

Now when we click on the links, we’ll see the same data as we did with the watcher.

The only difference is that we call next to move to the next route.

Conclusion

We can watch for navigation with watchers or hooks with Vue Router 4.