Categories
Vue

How to Add Font Awesome Icons to Your Vue.js App

Font Awesome is a popular CSS library for adding icons to web apps. It is available as a raw CSS package, and there are also versions for major front end frameworks. For Vue.js, we can use vue-fontawesome to add icons in our apps.

In this article, we will make a grocery list app where users can search for dishes they want to cook with the MealDB API and add the ingredients they want from it. We will add ripple effects to buttons and list items to highlight them when the mouse pointer go over them.

To start building the app, we run the Vue CLI by running npx @vue/cli grocery-app. In the wizard, select ‘Manually select features’, then select Babel, Vuex, Vue Router, and CSS Preprocessor.

Next we install some additional packages. We need Axios for making HTTP requests, BootstrapVue for styling, Vue Font Awesome for adding icons, Vee-Validate for form validation and Vue Ripple Directive for adding the ripple effect. To install them, we run:

npm i axios @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome axios bootstrap-vue vee-validate vue-ripple-directive

With all the packages installed, we can build the app. We start by add a form for adding groceries. Create a GroceryForm.vue file in the components folder and add:

<template>
  <div>
    <ValidationObserver ref="observer" v-slot="{ invalid }">
      <b-form @submit.prevent="onSubmit" novalidate>
        <b-form-group label="Search for Dish and Add Ingredients From Result">
          <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
            <b-form-input
              type="text"
              :state="errors.length == 0"
              v-model="form.keyword"
              required
              placeholder="Search for Dish and Add Ingredients From Result"
              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 v-ripple.mouseover.500 type="submit" variant="primary">Find Dishes</b-button>
      </b-form>
    </ValidationObserver>

    <b-card v-for="(m, i) in meals" :title="m.strMeal" :key="i">
      <b-card-text>
        <b-list-group>
          <b-list-group-item
            v-for="(key, index) in Object.keys(m).filter(k => k.includes('strIngredient') && m[k])"
            :key="index"
            v-ripple.mouseover="'rgba(255, 255, 255, 0.35)'"
          >{{m[key]}}</b-list-group-item>
        </b-list-group>
      </b-card-text>
      <b-button
        v-ripple.mouseover.500
        variant="primary"
        @click="addToGroceryList(i)"
      >Add Ingredients to Grocery List</b-button>
    </b-card>

    <h4>Your Chosen Ingredients</h4>
    <b-list-group>
      <b-list-group-item v-for="(ingredient, i) of ingredients" :key="i" v-ripple.mouseover>
        {{ingredient}}
        <font-awesome-icon icon="times" class="float-right" @click="removeIngredient(i)" />
      </b-list-group-item>
    </b-list-group>

   <br />

    <b-button v-ripple.mouseover.500 type="button" variant="primary" @click="saveGroceryList()">Save</b-button>
    <b-button v-ripple.mouseover.500 type="reset" variant="danger" @click="cancel()">Cancel</b-button>
  </div>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import { faTimes } from "@fortawesome/free-solid-svg-icons";

export default {
  name: "GroceryForm",
  mixins: [requestsMixin],
  components: {
    faTimes
  },
  data() {
    return {
      form: {},
      meals: [],
      ingredients: []
    };
  },
  computed: {
    grocery() {
      return this.$store.state.grocery;
    }
  },
  methods: {
    cancel() {
      this.$emit("cancelled");
    },

    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const { data } = await this.findDishes(this.form.keyword);
      this.meals = data.meals;
    },

    addToGroceryList(index) {
      const meal = this.meals[index];
      const keys = Object.keys(meal).filter(
        k => k.includes("strIngredient") && meal[k]
      );
      const ingredients = keys.map(k => meal[k]);
      this.ingredients = Array.from(
        new Set(this.ingredients.concat(ingredients))
      );
    },

    removeIngredient(index) {
      this.ingredients.splice(index, 1);
    },

    async saveGroceryList() {
      const payload = { ingredients: this.ingredients };
      if (!this.groceryListId) {
        await this.addGrocery(payload);
      } else {
        await this.editGrocery(payload);
      }
      const { data } = await this.getGrocery();
      this.$store.commit("setGrocery", data);
      this.$emit("saved");
    }
  },
  watch: {
    grocery: {
      handler(val) {
        this.ingredients = val.ingredients || [];
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

<style lang="scss" scoped>
.delete {
  cursor: pointer;
}
</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. We make the keyword field required in the rules prop so that users will have to enter something before searching.

We have buttons in the list at the bottom of the form, which has the list of ingredients, to delete each of them. This is why we imported the faTimes icon here, which displays as an ‘x’, so that users can click on it and delete it. If that element is clicked, the removeIngredient function is called. Then the user clicks Save at the bottom of the form, then the saveGroceryList function is called which saves the list to our back end.

In this component, we also have a watch block to watch the grocery value, which is obtained from the Vuex store that we have to build. We get the latest list of ingredients as the grocery value is updated.

We have the ripple effect applied to the buttons and the list rows with Vue Ripple. v-ripple.mouseover.500 means that the ripple effect will show for 500 milliseconds when the mouse is over the element with this directive. To apply a different color to the ripple effect than the default we can also specify the color value in the parameter of the directive, like we have in v-ripple.mouseover=”’rgba(255, 255, 255, 0.35)’”. The ripple will have the color specified.

We style the element for deleting the form with cursor:pointer so the mouse icon will show up a hand instead of the arrow.

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

const APIURL = "http://localhost:3000";
const MEAL_DB_URL = "https://www.themealdb.com/api/json/v1/1/search.php?s=";

const axios = require("axios");

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

    addGrocery(data) {
      return axios.post(`${APIURL}/grocery`, data);
    },

    editGrocery(data) {
      return axios.put(`${APIURL}/grocery`, data);
    },

    findDishes(keyword) {
      return axios.get(`${MEAL_DB_URL}${keyword}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to get and save our grocery data and search the Meal DB API for dishes.

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

<template>
  <div class="page">
    <h1 class="text-center">Grocery List</h1>
    <b-button-toolbar class="button-toolbar">
      <b-button
        v-ripple.mouseover.500
        @click="openAddModal()"
        variant="primary"
      >Add Ingredients to Grocery List</b-button>
    </b-button-toolbar>

    <h4>Your Grocery List</h4>
    <b-list-group>
      <b-list-group-item
        v-for="(ingredient, i) of grocery.ingredients"
        :key="i"
        v-ripple.mouseover="'rgba(255, 255, 255, 0.35)'"
      >
        {{ingredient}}
        <font-awesome-icon icon="times" class="float-right" @click="removeIngredient(i)" />
      </b-list-group-item>
    </b-list-group>

    <b-modal id="add-modal" title="Add Ingredients to Grocery List" hide-footer>
      <GroceryForm
        @saved="closeModal()"
        @cancelled="closeModal()"
        :edit="false"
        :groceryListId="grocery.id"
      />
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    GroceryForm
  },
  mixins: [requestsMixin],
  computed: {
    grocery() {
      return this.$store.state.grocery;
    }
  },
  data() {
    return {
      ingredients: []
    };
  },
  beforeMount() {
    this.getGroceryList();
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
    },
    async getGroceryList() {
      const { data } = await this.getGrocery();
      this.$store.commit("setGrocery", data);
    },
    async removeIngredient(index) {
      this.ingredients.splice(index, 1);
      const payload = { id: this.grocery.id, ingredients: this.ingredients };
      await this.editGrocery(payload);
      const { data } = await this.getGrocery();
      this.$store.commit("setGrocery", data);
    }
  },
  watch: {
    grocery: {
      handler(val) {
        this.ingredients = val.ingredients || [];
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

};

This is the component for the home page. We display the list of ingredients chosen obtained from our back end here. Also, we have a button to open a modal with the GroceryForm that we created earlier to add ingredients to our grocery list. Getting data is done in the getGroceryList function. We put the obtained data into our Vuex store in the last line of the function.

Also, we let users remove ingredients that they saved to the list in this page with the removeIngredient function. We call splice on the this.ingredients array, which we got from the grocery state in the store, then set to the current value in the handler of the watch block of grocery .

Again, we have the ripple effect applied to the buttons and the list rows with Vue Ripple. v-ripple.mouseover.500 to show the ripple effect for 500 milliseconds for the buttons and v-ripple.mouseover=”’rgba(255, 255, 255, 0.35)’”. The ripple will have the color specified in the list items like we did in GroceryForm.

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="/">Grocery List 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>

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

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

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required, min_value, max_value } from "vee-validate/dist/rules";
import Ripple from "vue-ripple-directive";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { library } from "@fortawesome/fontawesome-svg-core";
import { faTimes } from "@fortawesome/free-solid-svg-icons";

library.add(faTimes);
Vue.component("font-awesome-icon", FontAwesomeIcon);
Vue.directive("ripple", Ripple);
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);

Vue.config.productionTip = false;

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

We added all the libraries we need here, including BootstrapVue JavaScript and CSS, Vee-Validate components along with the validation rules, the Vue-Ripple library, and the Vue Font Awesome packages here. The faTimes is added to our app with the library.add function of Vue Font Awesome so that we can use it in our app.

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
    }
  ]
});

This includes 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: {
    grocery: {}
  },
  mutations: {
    setGrocery(state, payload) {
      state.grocery = payload;
    }
  },
  actions: {}
});

This adds our grocery state to the store so we can observer it in the computed block of GroceryForm and HomePage components. We have the setGrocery function to update the grocery state and we use it in the components by call this.$store.commit(“setGrocery”, data); like we did in GroceryForm 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>Grocery List App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-ripple-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>

This changes the title of our app.

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

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:

{
  "`grocery`": {}
}

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

Categories
JavaScript Answers

How to Exclude a Route from Running an Express Middleware?

Sometimes, we want to exclude a route from running an Express middleware.

In this article, we’ll look at how to exclude a route from running an Express middleware.

Exclude a Route from Running an Express Middleware

To exclude a route from running an Express middleware, we can create our own function that accepts a route path and middleware function and returns a middleware function that checks the route path before running the middleware function.

For instance, we write:

const express = require('express')
const app = express()
const port = 3000

const unless = (path, middleware) => {
  return (req, res, next) => {
    if (path === req.path) {
        return next();
    } else {
        return middleware(req, res, next);
    }
  };
};

const middleware = (req, res, next)=>{
  console.log('middleware')
  next()
}

app.use(unless('/exclude', middleware));

app.get('/', (req, res) => {
  res.send('hello world')
});

app.get('/foo', (req, res) => {
  res.send('foo')
});

app.get('/exclude', (req, res) => {
  res.send('exclude')
});

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

We create the unless function that takes the path route path string and the middleware middleware function.

In the function, we return a function that checks if the request path which is stored in req.path is the same as path.

If it is, then we call next and stop running the function with return.

Otherwise we use return and call middleware to run the middleware function.

Now if we make a request to any route other than /exclude, we see 'middleware' logged.

Conclusion

To exclude a route from running an Express middleware, we can create our own function that accepts a route path and middleware function and returns a middleware function that checks the route path before running the middleware function.

Categories
JavaScript Answers

How to Get Duplicate Values from an Array with Lodash?

Sometimes, we want to get duplicate values from an array with Lodash.

In this article, we’ll look at how to get duplicate values from an array with Lodash.

Get Duplicate Values from an Array with Lodash

To get duplicate values from an array with Lodash, we can use the countBy method to count the values.

Then we call the JavaScript array’s reduce method to get all the items that has count more than 1 and put them in an array.

For instance, we write:

const items = [1, 1, 2, 3, 3, 3, 4, 5, 6, 7, 7];

const dup = Object.entries(_.countBy(items))
  .reduce((acc, [key, val]) => {
    return val > 1 ? acc.concat(key) : acc
  }, [])
  .map(Number)

console.log(dup)

We call the countBy method with items to return an object with the count of each item in the items array.

Then we call Object.entries to return an array of key-value pairs in the object returned by countBy.

Next, we call reduce with a callback that takes the acc array and the destructured key and val from the each array returned by Object.entries.

If val is bigger than 1, then we know there’s more than one item with the given key.

So we put the in acc with concat.

Otherwise, we return acc without appending the entry since there’s only one instance of the val value.

We pass in an empty array as the value of the 2nd argument so acc is initialized to an empty array.

Finally, we call map with Number to convert all the entries back to numbers.

Therefore dup is [1, 3, 7] according to the console log.

Conclusion

To get duplicate values from an array with Lodash, we can use the countBy method to count the values.

Then we call the JavaScript array’s reduce method to get all the items that has count more than 1 and put them in an array.

Categories
JavaScript Answers

How to Clear a Text Area on Focus with JavaScript?

Sometimes, we want to clear a text area on focus with JavaScript.

In this article, we’ll look at how to clear a text area on focus with JavaScript.

Clear a Text Area on Focus with JavaScript

To clear a text area on focus with JavaScript, we can call the jQuery focus method with a callback that sets the value of the text area to an empty string.

For instance, we write:

<textarea></textarea>

to add a text area.

Then we write:

$('textarea').focus(function() {
  $(this).val('');
});

We select the text area with $('textarea').

Then we call focus with a callback that sets the text area’s value to an empty string by writing:

$(this).val('');

Conclusion

To clear a text area on focus with JavaScript, we can call the jQuery focus method with a callback that sets the value of the text area to an empty string.

Categories
JavaScript Answers

How to Convert a JavaScript Object into a Key-Value Object Array?

Sometimes, we want to convert a JavaScript object into a key-value object array.

In this article, we’ll look at how to convert a JavaScript object into a key-value object array.

Convert a JavaScript Object into a Key-Value Object Array

To convert a JavaScript object into a key-value object array, we can use the Object.entries method to return an array with of key-value pair arrays of a given object.

Then we can use the JavaScript array map method to map the key-value pair arrays into objects.

For instance, we write:

const data = {
  firstName: 'John',
  lastName: 'Doe',
  email: 'doe@gmail.com'
}
const output = Object.entries(data).map(([key, value]) => ({
  key,
  value
}));

console.log(output);

to call Object.entries with data to return an array of key-value pair arrays in the data object.

Then we call map with a callback that destructures the key and value from the parameter, put them in an object as properties, and then return the object.

Therefore, output is:

[
  {
    "key": "firstName",
    "value": "John"
  },
  {
    "key": "lastName",
    "value": "Doe"
  },
  {
    "key": "email",
    "value": "doe@gmail.com"
  }
]

according to the console log.

Conclusion

To convert a JavaScript object into a key-value object array, we can use the Object.entries method to return an array with of key-value pair arrays of a given object.

Then we can use the JavaScript array map method to map the key-value pair arrays into objects.