Categories
Vue

Check if an Input Element is Focused with Vue-Focus

To make an app’s user experience better, often you have to do something when an input element is focused. For example, you might want to highlight the label for the input when the input is focused so that users know which field they’re filling in. With Vue.js, the easiest way is the use the Vue-Focus library to do this. It provides a directive and mixin that lets you handle focused and blur events and bind it to a data field of a component.

In this article, we will make a website bookmark manager app that lets users bookmark their favorite URLs. The labels will be highlighted when the input is in focus. To start building the project, we run the Vue CLI by running:

npx @vue/cli create bookmark-app

When the wizard runs, we select ‘Manually select features’, and select Babel, CSS preprocessor, Vuex, and Vue Router.

Next we install some packages. We need Axios to make HTTP requests to our back end, Bootstrap-Vue for styling, Vee-Validate for form validation, and Vue-Focus for handling the focus state of the inputs. To install the packages, we run npm i axios bootstrap-vue vee-validate vue-focus. After installing the packages we can start building our bookmark app.

First, we create our form for letting users add and edit their bills. In the components folder, create a file called BookmarkForm.vue and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group>
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <label :class="{'highlight': nameFocused}">Name</label>
          <b-form-input
            type="text"
            v-model="form.name"
            placeholder="Name"
            name="name"
            [@focus](http://twitter.com/focus "Twitter profile for @focus")="nameFocused = true"
            [@blur](http://twitter.com/blur "Twitter profile for @blur")="nameFocused = false"
            v-focus="nameFocused"
            :state="errors.length == 0"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group>
        <ValidationProvider name="url" rules="required|url" v-slot="{ errors }">
          <label :class="{'highlight': urlFocused}">URL</label>
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.url"
            required
            placeholder="URL"
            name="url"
            [@focus](http://twitter.com/focus "Twitter profile for @focus")="urlFocused = true"
            [@blur](http://twitter.com/blur "Twitter profile for @blur")="urlFocused = false"
            v-focus="urlFocused"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

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

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

export default {
  name: "BookmarkForm",
  mixins: [requestsMixin],
  directives: { focus },
  props: {
    bookmark: Object,
    edit: Boolean
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }

if (this.edit) {
        await this.editBookmark(this.form);
      } else {
        await this.addBookmark(this.form);
      }
      const { data } = await this.getBookmarks();
      this.$store.commit("setBookmarks", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {},
      nameFocused: false,
      urlFocused: false
    };
  },
  watch: {
    bookmark: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.highlight {
  color: #42b983;
}
</style>

This form lets users search for dishes with the given keyword, then return a list of ingredients for the dishes and then the user can add them to a list with the duplicates removed. We use Vee-Validate to validate our inputs. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider , we have our BootstrapVue input for the text input fields. In the b-form-input components.

We also add Vee-Validate validation to make sure that users have filled out the date before submitting it. We make the name and url fields required in the rules prop so that users will have to enter all of them to save the bill. Also, we made a custom url Vee-Validate validation rule checks if the URL is valid.

In the inputs, we used the v-focus directive provided by Vue-Focus to set the focus state of the inputs. We bind the focus state of the inputs to the nameFocused and urlFocused variables respectively. Once users put the cursor in the input, the label for the input will be highlighted since we set the highlight class to the label depending on the state of the focus of the input.

We validate the values in the onSubmit function by running this.$refs.observer.validate() . If that resolves to true , then we run the code to save the data by calling the functions in the if block, then we call getNotes to get the notes. These functions are from the requestsMixin that we will add. The obtained data are stored in our Vuex store by calling this.$store.commit .

In this component, we also have a watch block to watch the bill value, which is obtained from the Vuex store that we have to build. We get the latest list of ingredients as the billvalue is updated so that the latest can be edited by the user as we copy the values to this.form .

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

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

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

    addBookmark(data) {
      return axios.post(`${APIURL}/bookmarks`, data);
    },

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

    deleteBookmark(id) {
      return axios.delete(`${APIURL}/bookmarks/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to our back end to save the bookmarks.

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

<template>
  <div class="page">
    <h1 class="text-center">Bookmark App</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Bookmark</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>Link</b-th>
          <b-th></b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="b in bookmarks" :key="b.id">
          <b-td>{{b.name}}</b-td>
          <b-td>
            <a :href="b.url">Link</a>
          </b-td>
          <b-td>
            <b-button @click="openEditModal(b)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOnebookmark(b.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>

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

    <b-modal id="edit-modal" title="Edit Bookmark" hide-footer>
      <BookmarkForm
        @saved"closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :bookmark="selectedBookmark"
      ></BookmarkForm>
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    BookmarkForm
  },
  mixins: [requestsMixin],
  computed: {
    bookmarks() {
      return this.$store.state.bookmarks;
    }
  },
  beforeMount() {
    this.getAllBookmarks();
  },
  data() {
    return {
      selectedBookmark: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },

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

    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedBookmark = {};
      this.getAllBookmarks();
    },

    async deleteOnebookmark(id) {
      await this.deleteBookmark(id);
      this.getAllBookmarks();
    },

    async getAllBookmarks () {
      const { data } = await this.getBookmarks();
      this.$store.commit("setBookmarks", data);
    }
  }
};
</script>

This is where we display the bills in a BootstrapVue table. The columns are the name, the amount, and the due date, along with the Edit button to open the edit modal, and Delete button to delete an entry when it’s clicked.

We also added an ‘Add Bill’ button to open the modal to let users add a bill. The notes are obtained from the back end by running the this.getAllBookmarks function in the beforeMount hook which stores the data in our Vuex store.

The openAddModal, openEditModal, closeModal open the open and close modals, and close the modal respectively. When openEditModal is called, we set the this.selectedBookmark variable so that we can pass it to our BookmarkForm .

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="/">Bookmark 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>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

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

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

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

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

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

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { 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";

extend("url", {
  validate: value => {
    return /(https?://(?:www.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^s]{2,}|www.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9].[^s]{2,}|https?://(?:www.|(?!www))[a-zA-Z0-9]+.[^s]{2,}|www.[a-zA-Z0-9]+.[^s]{2,})/.test(
      value
    );
  },
  message: "Invalid URL."
});
Vue.use(BootstrapVue);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
extend("required", required);

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 and Vee-Validate components along with the required validation rule and a url rule for validating that the URL entered is valid by checking against the given regex.

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: {
    bookmarks: []
  },
  mutations: {
    setBookmarks(state, payload) {
      state.bookmarks = payload;
    }
  },
  actions: {}
});

to add our bookmarks state to the store so we can observer it in the computed block of BookmarkForm and HomePage components. We have the setBookmarks function to update the notes state and we use it in the components by call this.$store.commit(“setBookmarks”, data); like we did in BookmarkForm 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>Bookmark App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-focus-tutorial-app doesn't work properly without
        JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title of our app.

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

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

json-server --watch db.json

In db.json, change the text to:

{
  "bookmarks": []
}

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

After all the hard work, we get:

Categories
Vue

Add Popups and Menus Easily with V-Click-Outside

Popups and menus are common features in web apps. It’s often used for giving a place for exposing users to add functionality in an app. The frequency of its use led to developers developing libraries for us to use to add popups and menus to apps. UI libraries like Bootstrap have menus built-in. For customized menus, we can add one by creating a div with a button that toggles the opening and closing of the menu.

When users click outside the menu and the button, then the menu closes. For Vue.js apps, we have the V-Click-Outside library for handling clicks outside an element. We can use it easily to add popups and menus to our apps.

In this article, we will write a note-taking app that lets users take notes. There will be a table to display the notes and a popup menu in each row that lets users click a button to edit or delete a note. To start building the project, we run the Vue CLI by running:

npx @vue/cli create bookmark-app

When the wizard runs, we select ‘Manually select features’, and select Babel, CSS preprocessor, Vuex, and Vue Router.

Next, we install some packages. We need Axios to make HTTP requests to our back end, Bootstrap-Vue for styling, Vee-Validate for form validation, and V-Click-Outside for handling the focus state of the inputs. To install the packages, we run npm i axios bootstrap-vue vee-validate v-click-outside. After installing the packages we can start building our note-taking app.

First, we create our form for letting users add and edit their bills. In the components folder, create a file called NoteForm.vue and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group>
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <label>Name</label>
          <b-form-input
            type="text"
            v-model="form.name"
            placeholder="Name"
            name="name"
            :state="errors.length == 0"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">Name is requied.</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group>
        <ValidationProvider name="note" rules="required" v-slot="{ errors }">
          <label>Note</label>
          <b-form-textarea
            type="text"
            :state="errors.length == 0"
            v-model="form.note"
            required
            placeholder="Note"
            name="note"
            rows="5"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

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

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

export default {
  name: "NoteForm",
  mixins: [requestsMixin],
  props: {
    note: Object,
    edit: Boolean
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }

      if (this.edit) {
        await this.editNote(this.form);
      } else {
        await this.addNote(this.form);
      }
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    note: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

This form lets users search for dishes with the given keyword, then return a list of ingredients for the dishes and then the user can add them to a list with the duplicates removed. We use Vee-Validate to validate our inputs. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider , we have our BootstrapVue input for the text input fields. In the b-form-input components. We also add Vee-Validate validation to make sure that users have filled out the date before submitting. We make the name and note fields required in the rules prop so that users will have to enter all of them to save the note.

We validate the values in the onSubmit function by running this.$refs.observer.validate() . If that resolves to true , then we run the code to save the data by calling the functions in the if block, then we call getNotes to get the notes. These functions are from the requestsMixin that we will add. The obtained data are stored in our Vuex store by calling this.$store.commit .

In this component, we also have a watch block to watch the note value, which is obtained from the Vuex store that we have to build. We get the latest list of ingredients as the note value is updated so that the latest can be edited by the user as we copy the values to this.form .

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

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

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

    addNote(data) {
      return axios.post(`${APIURL}/notes`, data);
    },

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

    deleteNote(id) {
      return axios.delete(`${APIURL}/notes/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to our back end to save the bookmarks.

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

<template>
  <div class="page">
    <h1 class="text-center">Note Taking App</h1>
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Note</b-button>
    </b-button-toolbar>
    <br />
    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th>Name</b-th>
          <b-th>Note</b-th>
          <b-th></b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="n in notes" :key="n.id">
          <b-td>{{n.name}}</b-td>
          <b-td>{{n.note}}</b-td>
          <b-td>
            <b-button @click="toggleMenu(n.id)" class="menu-button">Menu</b-button>
            <div class="dropdown" v-show="openMenu[n.id]" v-click-outside="onClickOutside">
              <b-list-group>
                <b-list-group-item @click="openEditModal(n)">Edit</b-list-group-item>
                <b-list-group-item @click="deleteOneNote(n.id)">Delete</b-list-group-item>
              </b-list-group>
            </div>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>

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

    <b-modal id="edit-modal" title="Edit Note" hide-footer>
      <NoteForm @saved="closeModal()" @cancelled="closeModal()" :edit="true" :note="selectedNote"></NoteForm>
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    NoteForm
  },
  mixins: [requestsMixin],
  computed: {
    notes() {
      return this.$store.state.notes;
    }
  },
  beforeMount() {
    this.getAllNotes();
  },
  data() {
    return {
      selectedNote: {},
      openMenu: {}
    };
  },
  methods: {
    toggleMenu(id) {
      this.$set(this.openMenu, id, !this.openMenu[id]);
    },
    onClickOutside(event, el) {
      if (!event.target.className.includes("menu-button")) {
        this.openMenu = {};
      }
    },
    openAddModal() {
      this.$bvModal.show("add-modal");
    },

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

    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedNote = {};
      this.getAllNotes();
    },

    async deleteOneNote(id) {
      await this.deleteNote(id);
      this.getAllNotes();
    },

    async getAllNotes() {
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
    }
  }
};
</script>

<style lang="scss" scoped>
.dropdown {
  position: absolute;
  max-width: 100px;
}

.list-group-item {
  cursor: pointer;
}
</style>

This is where we display the bills in a BootstrapVue table. The columns are the name, the amount, and the due date, along with the Edit button to open the edit modal, and Delete button to delete an entry when it’s clicked. We also added an ‘Add Bill’ button to open the modal to let users add a bill. The notes are obtained from the back end by running the this.getAllNotes function in the beforeMount hook which stores the data in our Vuex store.

In the table, we have the notes displayed in the table rows. On the rightmost column, we have the menu that we built from scratch. We added a Menu button to each row and a div below it to serve as the container of the list group, which contains our Edit and Delete items for users to click on to Edit and Delete the note respectively. We toggle the menu with the toggleMenu function when the user clicks the Menu button. Notice that we need to call the this.$set function to force Vue.js to refresh since we’re modifying an entry in an object. Vue cannot detect changes within an object automatically. For more details about this function, see https://vuejs.org/v2/api/#Vue-set.

In the styles section, we style out menu popup by setting the dropdown class with absolute position and set its max-width to 100px. The absolute position will make it stack on top of our table, right below the button for each row.

The openAddModal, openEditModal, closeModal open the open and close modals, and close the modal respectively. When openEditModal is called, we set the this.selectedNote variable so that we can pass it to our NoteForm .

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="/">Note Taking 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>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>

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

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

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

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

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

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import { 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 vClickOutside from "v-click-outside";

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

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 , and the Vee-Validate components along with the required validation rule. We also include our V-Click-Outside library here so we can use it in any component.

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: {
    notes: []
  },
  mutations: {
    setNotes(state, payload) {
      state.notes = payload;
    }
  },
  actions: {}
});

to add our notes state to the store so we can observe it in the computed block of NoteForm and HomePage components. We have the setNotes function to update the notes state and we use it in the components by call this.$store.commit(“setNotes”, data); like we did in NoteForm 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>Note Taking App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but v-click-outside-tutorial-app doesn't work properly
        without JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title of our app.

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

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

json-server --watch db.json

In db.json, change the text to:

{
  "`notes`": []
}

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

After all the hard work, we get:

Categories
TypeScript

TypeScript Advanced Types: Union and Intersection

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There’re multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at intersection and union types.

Intersection Types

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s denoted by an & sign. All members of all the types are required in the object of an intersection type.

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

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
}

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20,
  employeeCode: '123'
}

As we can see from the code above, each type is separated by an & sign. Also, the employee object has all the properties of Animal , Person , and Employee . Each property has a type that’s defined in each interface. If the structure doesn’t match exactly, then we’ll get error messages like the following from the TypeScript compiler:

Type '{ kind: string; firstName: string; lastName: string; age: number; }' is not assignable to type 'Animal & Person & Employee'.

Property 'employeeCode' is missing in type '{ kind: string; firstName: string; lastName: string; age: number; }' but required in type 'Employee'.(2322)

input.ts(12, 3): 'employeeCode' is declared here.

The error above occurs if we have the following code:

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

TypeScript looks for the employeeCode property since the employeeCode property is in the Employee interface.

If 2 types have the same member name but different type, then it’s automatically assigned the never type, when they’re joined together as an intersection type. This means that we can’t assign anything to it. For example, if we have:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal & Person & Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

Then we get the following error from the TypeScript compiler:

Type 'number' is not assignable to type 'never'.(2322)

input.ts(8, 3): The expected type comes from property 'age' which is declared here on type 'Animal & Person & Employee'

If we omit the property then the compiler will also raise an error about the age property being missing. Therefore, we should never have types that have the same member name if we want to create intersection types from them.

Union Types

Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol.

For example, we can define an object that has a union type like in the following code:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20
}

The code above has an employee object of the Animal | Person | Employee type which means that it can have some of the properties of the Animal, Person, or Employee interfaces. Not all of them have to be included, but if they’re included, then the type has to match the ones in the interface.

With union types, we can have 2 types that have the same member name but with different types. For example, if we have the following code:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: '20'
}

Then we can assign both a number or a string to the age property. This fits with the dynamic nature of JavaScript while letting us assign data types to objects. This is different from traditional object-oriented code where we may abstract common members into a parent class and then derive sub-classes from it which has specialized members.

The pipe symbol means the object of a union type can take on none, some, or all properties of each type.

Accessing members in an object of a union type is different from how members are accessed from an intersection type. Objects that have intersection types have to have all the properties listed in the members of each type, so logically, we can access all the properties that are defined in the object. However, this isn’t the case with union types since some members may be available to only some of the types that make up the union type.

If we have a union type, then we can only access members that are available in all the types that form the union type. For example, if we have:

interface Animal {
  kind: string;
}

interface Person {
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  employeeCode: string;
  age: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: '20'
}

console.log(employee)

Then we can’t access any properties of the employee object since none of the members are available in all the types. If we try to access a property like the kind property, we’ll get the following error:

Property 'kind' does not exist on type 'Animal | Person | Employee'.

Property 'kind' does not exist on type 'Person'.(2339)

If we want to make some property accessible, we can write something like the following code to let us access a property:

interface Animal {
  kind: string;
}

interface Person {
  kind: string;
  firstName: string;
  lastName: string;
  age: number;
}

interface Employee {
  kind: string;
  employeeCode: string;
}

let employee: Animal | Person | Employee = {
  kind: 'human',
  firstName: 'Jane',
  lastName: 'Smith',
  age: 20,
  employeeCode: '123'
}

console.log(employee.kind)

In all 3 interfaces above, we have the kind member. Since they’re in all 3 interfaces, which we used in the union type, we can access the employee.kind property. Then we would get the text ‘human’ in the console.log statement.

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s formed by joining multiple types by an & sign. Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol. It lets us create a new type that has some of the structure of each type that forms the union type.

Categories
JavaScript

Form Validation with HTML5 and JavaScript

Form validation is part of browser-side HTML and JavaScript. We can use it to validate form inputs before sending the data to the server. However, we should trust the content that’s being sent, so the final validation should still be on the server.

With HTML5, form validation is a built-in feature. There’re various validation attributes that come with HTML5. When form input is valid, the :valid CSS pseudoclass is applied to the element. If it’s invalid, then the :invalid CSS pseudoclass is applied to the element.

When the form has invalid inputs, then the browser will block the form from being submitted and display an error message.

Form Validation Attributes

Pattern

The pattern attribute is available for input elements with type text , search , url , tel , email , and password . It lets us set a regular expression as the value and the browser will validate it against it.

Min

This attribute applies to range , number , date , month , week , datetime , datetime-local , and time inputs. When it’s applied to a range or number input, it’ll check if the inputted number is the value of the min attribute or higher.

When it’s applied to the date , month , or week element, then it’ll check if the date is greater than or equal to the one given in the value of this attribute.

For the datetime, datetime-local, time type inputs, both the date and time are checked to see if it’s greater than or equal to the one given in the value of the min attribute.

Max

The max attribute is the opposite of the min attribute. It checks if something entered is less than or equal to what’s given in the value of this attribute.

When it’s applied to a range or number input, it’ll check if the inputted number is the value of the min attribute or lower.

When it’s applied to the date , month , or week element, then it’ll check if the date is less than or equal to the one given in the value of this attribute.

For the datetime, datetime-local, time type inputs, both the date and time are checked to see if it’s less than or equal to the one given in the value of the min attribute.

Required

The required attribute checks if the input value is entered by the user.

It’s available to text, search, url, tel, email, password, date, datetime, datetime-local, month, week, time, number, checkbox, radio, file, and also on the <select> and <textarea> elements.

Step

The step attribute checks if the input is an integer.

If it’s applied to an input element with the type date , then it checks for an integer number of days.

If it’s applied to an input element with the type month, then it checks for an integer number of months.

If it’s applied to an input element with the type week, then it checks for an integer number of weeks.

If it’s applied to an input element with the type datetime, datetime-local, time, then it checks for an integer number of seconds.

If it’s applied to an input element with the type range, number, then it checks for an integer.

Minlength

The minlength attribute is available to the input with types text, search, url, tel, email, password . It’s also available on the textarea element.

It checks for a greater than or equal to the number of characters of text that the user entered.

Maxlength

The maxlength attribute is available to the input with types text, search, url, tel, email, password . It’s also available on the textarea element.

It checks for a less than or equal to the number of characters of text that the user entered.

Using Form Validation Attributes

We can use form validation attributes by adding them to the element. For example, we can write a form that accepts an email address as input.

First, we write the HTML as follows:

<form id='form'>
  <label for="email">What's your email address?</label>
  <input id="email" name="email" required pattern="[^@]+@\[^\.]+\..+">
  <button type='submit'>Submit</button>
</form>

In the code above, we have the input element with the required attribute, which sets the input as required.

We also added the pattern attribute with the regular expression for an email address as the value. It checks for characters before and after the at sign and periods.

Next, we add the CSS for changing the border of the input element when the input is valid or invalid as follows:

input:invalid {
  border: 1px solid red
}

input:vvalid {
  border: 1px solid black
}

We used the pseudoclasses that we mentioned at the beginning of the article to do this.

Finally, we add JavaScript code to prevent form submission for this example by calling preventDefault:

const form = document.querySelector('#form');
form.onsubmit = (e) => {
  e.preventDefault();
}

Another example would be to check the length of our inputs and range. For example, we can write the following HTML to get the name and age of the user:

<form id='form'>
  <label for="name">What's your name?</label>
  <input id="name" name="name" required minlength='5' maxlength='20'>
  <br>
  <span id='name-too-short' hidden>Name is too short</span>
  <span id='name-too-long' hidden>Name is too long</span>

  <br>

  <label for="age">What's your age?</label>
  <input id="age" name="age" type='number' required min='0' max='150'>
  <br>
  <span id='age-too-high' hidden>Age is too high</span>
  <span id='age-too-low' hidden>Age is too low</span>

  <br>

  <button type='submit'>Submit</button>
</form>

We have the input elements with the length and range attributes for the name and age respectively. Also, we have the input messages and we can see them when the inputs are invalid in a way that’s described in the messages.

Again, we have the same CSS as in the first example for changing the border of the input box when the inputs are valid or invalid:

input:invalid {
  border: 1px solid red
}

input:vvalid {
  border: 1px solid black
}

Finally, we have the JavaScript for displaying the validation messages when the input is invalid:

const form = document.querySelector('#form');
const name = document.querySelector('#name');
const age = document.querySelector('#age');
const nameTooShort = document.querySelector('#name-too-short');
const nameTooLong = document.querySelector('#name-too-long');
const ageTooLow = document.querySelector('#age-too-low');
const ageTooHigh = document.querySelector('#age-too-high');
form.onsubmit = (e) => {
  e.preventDefault();
}

name.oninput = (e) => {
  nameTooShort.hidden = true;
  nameTooLong.hidden = true;
  if (e.srcElement.validity.tooShort) {
    nameTooShort.hidden = false;
  }

if (e.srcElement.validity.tooLong) {
    nameTooLong.hidden = false;
  }
}

age.oninput = (e) => {
  ageTooLow.hidden = true;
  ageTooHigh.hidden = true;
  if (e.srcElement.validity.rangeOverflow) {
    ageTooHigh.hidden = false;
  }

if (e.srcElement.validity.rangeUnderflow) {
    ageTooLow.hidden = false;
  }
}

In the code above, we set the oninput event handler to an event handler function to check for input validity whenever something is being entered.

Inside each function, we first hide all the messages so that we don’t see the outdated ones and then check for tooShort or tooLong for the name input since we specified the min and max lengths. If any of those errors occurred, we unhide the corresponding message element in the HTML.

Likewise, we check for rangeOverflow or rangeUnderflow for the age input since we specified the min and max lengths. If any of those errors occurred, we unhide the corresponding message element in the HTML.

With HTML5 and JavaScript, we can check for all kinds of input values for validity. We don’t have to use any libraries to do this. Some things we can check for including length, range, any pattern with regular expressions and so on. However, we should also check on server side before saving since users can still hack the browser side app to skip validation.

Categories
Functional Javascript

Functional Programming Concepts in JavaScript

Functional programming is a programming paradigm which states that we create computation as the evaluation of functions and avoid changing state and use mutable data.

In JavaScript, we can apply these principles to make our programs more robust and create fewer bugs.

In this article, we’ll look at how to apple some functional programming principles in our JavaScript programs, including pure functions and creating immutable objects.

Pure Functions

Pure functions are functions that always return the same output given the same set of inputs.

We should use pure functions as much as possible because they’re easier to test and the output is always predictable given a set of inputs.

Also, pure functions don’t create any side effects, which means that it does change anything outside the function.

An example of a pure function would be the following add function:

const add = (a, b) => a + b;

This function is a pure function because if we pass in 2 numbers, we’ll get the 2 numbers added together returned.

It doesn’t do anything outside the function, and it doesn’t have values that change the returned value inside the function.

Example of functions that aren’t pure functions include:

let x;
const setX = val => x = val;

setX isn’t a pure function because it sets the value of x which is outside the function. This is called a side effect.

A side effect is state change that’s observed outside the called function other than its returned value. setX sets a state outside the function, so it commits a side effect.

Another example would be the following:

const getCurrentDatePlusMs = (milliseconds) => +new Date() + milliseconds;

getCurrentDatePlusMs returns a value that depends on +new Date() which always changes, so we can check the value of it with tests easily. It’s also hard to predict the return value given the input since it always changes.

getCurrentDatePlusMs isn’t a pure function and it’s also a pain to maintain and test because of its ever-changing return value.

To make it pure, we should put the Date object outside as follows:

const getCurrentDatePlusMs = (date, milliseconds) => +date + milliseconds;

That way, given the same date and milliseconds, we’ll always get the same results.

Immutability

Immutability means that a piece of data can’t be changed once it’s defined.

In JavaScript, primitive values are immutable, this includes numbers, booleans, strings, etc.

However, there’s no way to define an immutable object in JavaScript. This means that we have to be careful with them.

We have the const keyword, which should create a constant by judging from the name of it, but it doesn’t. We can’t reassign new values to it, and we can’t redefine a constant with the same name.

However, the object assigned to const is still mutable. We can change its properties’ values, and we can remove existing properties. Array values can be added or removed.

This means that we need some other way to make objects immutable.

Fortunately, JavaScript has the Object.freeze method to make objects immutable. It prevents property descriptors of an object from being modified. Also, it prevents properties from being deleted with the delete keyword.

Once an object is frozen, it can’t be undone. To make it mutable again, we have to copy the object to another variable.

It applies these changes to the top level of an object. So if our object has nesting, then it won’t be applied to the nested objects.

We can define a simple recursive function to do freeze level of an object:

const deepFreeze = (obj) => {
  for (let prop of Object.keys(obj)) {
    if (typeof obj[prop] === 'object') {
      Object.freeze(obj[prop]);
      deepFreeze(obj[prop]);
    }
  }
}

The function above goes through every level of an object and calls Object.freeze on it. This means it makes the whole nested object immutable.

We can also make a copy of an object before manipulating it. To do this, we can use the spread operator, which works with objects since ES2018.

For example, we can write the following function to make a deep copy of an object:

const deepCopy = (obj, copiedObj) => {
  if (!copiedObj) {
    copiedObj = {};
  }

  for (let prop of Object.keys(obj)) {
    copiedObj = {
      ...copiedObj,
      ...obj
    };
    if (typeof obj[prop] === 'object' && !copiedObj[prop]) {
      copiedObj = {
        ...copiedObj,
        ...obj[prop]
      };
      deepCopy(obj[prop], copiedObj);
    }
  }
  return copiedObj;
}

In the code above, we create a new copiedObj object to hold the properties of the copied object if it doesn’t exist yet, which shouldn’t in the first call.

Then we loop through each property of the obj object and then apply the spread operator to copiedObj and obj to make a copy of the values at a given level. Then we do this recursively at every level until we went through every level, then we return the final copiedObj .

We only apply the spread operator if the property doesn’t exist at that level yet.

How do we know that this works? First, we can check if properties of each value are the same, which it is if we run console.log on it. Then we can also check with the === if the copied object has the same reference as the original object.

For example, if we have:

const obj = {
  foo: {
    bar: {
      bar: 1
    },
    a: 2
  }
};

Then we can check by writing:

const result = deepCopy(obj);
console.log(result);

To check the content of result .

We should get:

{
  "foo": {
    "bar": {
      "bar": 1
    },
    "a": 2
  }
}

This is the same as obj . If we run:

console.log(result === obj);

we get false , which means the 2 aren’t referencing the same object. This means that they’re a copy of each other.

Pure functions and immutability are important parts of functional programming. These concepts are popular for a reason. They make outputs predictable and make accidental state changes harder.

Pure functions are predictable since they return the same output for the same set of inputs.

Immutable objects are good because it prevents accidental changes. Primitive values in JavaScript are immutable, but objects are not. We can freeze it with the Object.freeze method to prevent changes to it and we can also make a copy of it recursively with the spread operator.