Categories
Quasar

Developing Vue Apps with the Quasar Library — Close Popup and Go Back

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

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

v-close-popup

The v-close-popup directive that comes with Quasar lets us close a popup when we click on an item that has the directive applied.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-btn label="Menu" color="primary">
        <q-menu>
          <q-list dense style="min-width: 100px;">
            <q-item clickable v-close-popup>
              <q-item-section>Open...</q-item-section>
            </q-item>
            <q-item clickable v-close-popup>
              <q-item-section>New</q-item-section>
            </q-item>
            <q-separator></q-separator>

            <q-item clickable v-close-popup>
              <q-item-section>Quit</q-item-section>
            </q-item>
          </q-list>
        </q-menu>
      </q-btn>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

We just add it to the item that we want to have applied to make them close the popup when we click on them.

The directive can also be applied to a dialog box.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-btn label="Open Dialog" color="primary" @click="dialog = true"></q-btn>

      <q-dialog v-model="dialog">
        <q-card>
          <q-card-section>
            <div class="text-h6">Dialog</div>
          </q-card-section>
          <q-card-section class="row items-center q-gutter-sm">
            <q-btn label="Open dialog" color="primary" @click="dialog2 = true">
            </q-btn>
            <q-btn v-close-popup label="Close" color="primary"></q-btn>

            <q-dialog v-model="dialog2">
              <q-card>
                <q-card-section>
                  <div class="text-h6">Second dialog</div>
                </q-card-section>
                <q-card-section class="row items-center q-gutter-sm">
                  <q-btn
                    v-close-popup="2"
                    label="Close both dialogs"
                    color="accent"
                  >
                  </q-btn>
                  <q-btn v-close-popup label="Close this dialog" color="accent">
                  </q-btn>
                </q-card-section>
              </q-card>
            </q-dialog>
          </q-card-section>
        </q-card>
      </q-dialog>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {
          dialog: false,
          dialog2: false
        }
      });
    </script>
  </body>
</html>

We add the v-close-popup directive to the Close both dialogs button to make it close all popups.

Go Back

We can add the v-go-back directive to handle back button actions.

For instance, we can write:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-btn v-go-back=" '/' " color="primary" label="Logout"> </q-btn>
    </div>
    <script>
      new Vue({
        el: "#q-app",
        data: {}
      });
    </script>
  </body>
</html>

to add the v-go-back directive to go to the / path when we click it.

Conclusion

Quasar comes with directives to let us close popups when we click on something.

It also has the v-go-back directive to let us navigate when clicking an element.

Categories
Vue

How to Add Keyboard Shortcuts to Your Vue.js App

Keyboard shortcuts is a very convenient feature for users. It allows them to do things without many clicks, increasing productivity…

Keyboard shortcuts is a very convenient feature for users. It allows them to do things without many clicks, increasing productivity. Keyboard shortcuts handling can easily be added to Vue.js apps with the V-Hotkey add-on, located at https://github.com/Dafrok/v-hotkey.

In this article, we will write a fitness tracker app that lets users enter their distance walked for a given day. They can use keyboard shortcuts to open the modal to add an entry and also to delete the latest entry. To start the project, we run the Vue CLI by running:

npx @vue/cli create fitness-tracker

In the Vue CLI wizard, select ‘Manually select features’ and select Babel, Vuex, Vue Router, and CSS Preprocessor.

Then we install a few libraries. We will install Axios for making HTTP requests, BootstrapVue for styling, V-Hotkey for letting us define keyboard shortcuts, Moment for formatting dates and Vue-Filter-Date-Format to format dates. To install them, we run:

npm i axios bootstrap-vue v-hotkey vee-validate vue-filter-date-format moment

Next, we start writing the app. We first create a form for adding and editing their distance walked entries. Create a file called FitnessForm.vue in the components folder and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Date (YYYY-MM-DD)">
        <ValidationProvider name="date" rules="required|date" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.date"
            required
            placeholder="Date"
            name="date"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>

      <b-form-group label="Distance Walked (km)">
        <ValidationProvider
          name="distance"
          rules="required|min_value:0|max_value:9999"
          v-slot="{ errors }"
        >
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.distance"
            required
            placeholder="Distance Walked"
            name="distance"
          ></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 { requestsMixin } from "@/mixins/requestsMixin";
import * as moment from 'moment';

export default {
  name: "FitnessForm",
  mixins: [requestsMixin],
  props: {
    edit: Boolean,
    distance: Object
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      const offDate = new Date(this.form.date);
      const correctedDate = new Date(
        offDate.getTime() + Math.abs(offDate.getTimezoneOffset() * 60000)
      );

      const params = {
        ...this.form,
        date: correctedDate
      };

      if (this.edit) {
        await this.editDistance(params);
      } else {
        await this.addDistance(params);
      }
      const { data } = await this.getDistances();
      this.$store.commit("setDistances", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  watch: {
    distance: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
        this.form.date = moment(this.form.date).format("YYYY-MM-DD");
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

In this file, we have a form to let users enter their distance walked for each date. 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. In the distance field, we enforce the minimum and maximum value with the help of Vee-Validate as we wrote in the rules.

In the onSubmit function we correct the date bu adding the time zone offset to our date. We only need this because we have a date in YYYY-MM-DD format, according to Stack Overflow https://stackoverflow.com/a/14569783/6384091. After that, we submit the data and get the latest ones and put them in our Vuex store. Then we close the modal by emitting the saved event to the Home.vue component, which we will modify later.

We have the watch block to watch the distance prop, which we will need for editing. We format the date so that it conforms to the YYYY-MM-DD format with Moment.js if the date exist. We do this so that the edit form shows the right value for the date field. We run JSON.stringify then JSON.parse to make a deep copy of the distance prop so that we don’t modify the original one until it’s saved.

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: {
    getDistances() {
      return axios.get(`${APIURL}/distances`);
    },

    addDistance(data) {
      return axios.post(`${APIURL}/distances`, data);
    },

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

    deleteDistance(id) {
      return axios.delete(`${APIURL}/distances/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to get and save our data.

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

<template>
  <div class="page" v-hotkey="keymap">
    <div class="text-center">
      <h1>Fitness Tracker</h1>
      <h2>Keyboard Shortcuts:</h2>
      <p>
        <b>Ctrl + Del:</b> Delete the latest fitness tracker entry.
        <b>Ctrl + Shift + A:</b> Open the modal to add a fitness tracker entry.
      </p>
    </div>
    <b-button-toolbar class="button-toolbar">
      <b-button @click="openAddModal()" variant="primary">Add Distance Walked</b-button>
    </b-button-toolbar>

    <b-table-simple responsive>
      <b-thead>
        <b-tr>
          <b-th sticky-column>Date</b-th>
          <b-th>Distance Walked</b-th>
          <b-th>Edit</b-th>
          <b-th>Delete</b-th>
        </b-tr>
      </b-thead>
      <b-tbody>
        <b-tr v-for="d in distances" :key="d.id">
          <b-th sticky-column>{{ new Date(d.date) | dateFormat('YYYY-MM-DD') }}</b-th>
          <b-td>{{d.distance}}</b-td>
          <b-td>
            <b-button @click="openEditModal(d)">Edit</b-button>
          </b-td>
          <b-td>
            <b-button @click="deleteOneWeight(d.id)">Delete</b-button>
          </b-td>
        </b-tr>
      </b-tbody>
    </b-table-simple>

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

    <b-modal id="edit-modal" title="Edit Distance Walked" hide-footer>
      <FitnessForm
        @saved"closeModal()"
        @cancelled="closeModal()"
        :edit="true"
        :distance="selectedDistance"
      />
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    FitnessForm
  },

mixins: [requestsMixin],
  computed: {
    distances() {
      return this.$store.state.distances.sort(
        (a, b) => +new Date(b.date) - +new Date(a.date)
      );
    },
    keymap() {
      return {
        "ctrl+del": this.deleteLatest,
        "ctrl+shift+a": this.openAddModal
      };
    }
  },
  beforeMount() {
    this.getAllDistances();
  },
  data() {
    return {
      selectedDistance: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(Distance) {
      this.$bvModal.show("edit-modal");
      this.selectedDistance = Distance;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedDistance = {};
    },
    async deleteOneDistance(id) {
      await this.deleteDistance(id);
      this.getAllDistances();
    },
    async getAllDistances() {
      const { data } = await this.getDistances();
      this.$store.commit("setDistances", data);
    },
    deleteLatest() {
      this.deleteOneDistance(this.distances[0].id);
    }
  }
};
</script>

We have a table to display the entered data with a BootstrapVue table. In each row, there’s an Edit and Delete button to open the edit modal and pass that data to the FitnessForm, and delete the entry respectively.

When the page loads, we get all the entered data with the getAllDistances function called in the beforeMount hook. In the getAllDistances function, we put everything in the Vuex store. Then in here, we get the latest state of the store by putting the this.$store.state.distancesin the computed block of the code. In there, we also sort the weight data by reverse chronological order.

In the script section, we define out shortcut key combinations with the computed keymap property. We pass the object into the v-hotkey directive to enable the keyboard shortcuts. The keys of the keymap property has the keyboard combinations, and the value has the functions to run. So if we press Ctrl and Delete together, we run this.deleteLatest to delete the latest entry. If we press Ctrl, Shift and A together we call this.openAddModal to open the modal to add a new entry.

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

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Fitness Tracker</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 "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 VueFilterDateFormat from "vue-filter-date-format";
import VueHotkey from "v-hotkey";

Vue.use(VueHotkey);
Vue.use(VueFilterDateFormat);
extend("required", required);
extend("min_value", min_value);
extend("max_value", max_value);
extend("date", {
  validate: value =>
    /([12]d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01]))/.test(value),
  message: "Date must be in YYYY-MM-DD format"
});
Vue.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 V-Hotkey library, and the Vue-Filter-Date-Format library are adding here for use in our app. The min_value and max_value rules are added for validating the weight, and we made a date rule for validating that the date is in YYYY-MM-DD format.

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

to add our distances state to the store so we can observe it in the computed block of FitnessForm and HomePage components. We have the setDistances function to update the distances state and we use it in the components by call this.$store.commit(“setDistances”, data); like we did in FitnessForm .

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>Fitness Tracker</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-hotkey-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:

{
  "`distances`": [
]
}

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

After all the hard work, we get:

Categories
Express

Error Handling with Express

Like with any other apps, we have to make Express apps ready to handle errors like unexpected inputs or file errors.

In this article, we’ll look at how to handle errors with Express.

Catching Errors

Error handling is the process of processing any errors that comes up both synchronously and asynchronously. Express comes with a default error handler so that we don’t have to write our own.

For example, if we throw errors in our route handlers as follows:

app.get('/', (req, res, next) => {
  throw new Error('error');
});

Express will catch it and proceed. We should see error instead of the app crashing.

For asynchronous errors, we have to call next to pass the error to Express as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

The code above will throw an error in the setTimeout callback and the catch block has the next call with the error passed in to call the built-in error handler.

We should see error instead of the app crashing.

Likewise, we have to catch rejected promises. We can do it as follows:

app.get('/', (req, res, next) => {
  Promise
    .reject('error')
    .catch(next)
});

Or with the async and await syntax, we can write the following:

app.get('/', async (req, res, next) => {
  try {
    await Promise.reject('error');
  }
  catch (ex) {
    next(ex);
  }
});

We should see error displayed instead of the app crashing with the stack trace.

The same logic also applies to routes with a chain of event handlers. We can call next as follows:

app.get('/', [
  (req, res, next) => {
    setTimeout(() => {
      try {
        throw new Error('error');
      }
      catch (ex) {
        next(ex);
      }
    })
  },
  (req, res) => {
    res.send('foo');
  }
]);

We should see the error displayed with the stack trace.

Default Error Handler

The default error handler catches the error when we call next and don’t handle it with a custom error handler.

The stack trace isn’t displayed in production environments.

If we want to send a different response than the default, we have to write our own error handler.

The only difference between route handlers, middleware and error handlers is that error handler has the err parameter before the request parameter that contains error data.

We can write a simple route with a custom event handler as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

app.use((err, req, res, next) => {
  res.status(500).send('Error!')
})

Note that we have the error handler below the route. The order is important. It has to below all the routes that we want to handle with it so that the error handler will get called.

We can write more than one custom error handler as follows:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

app.use((err, req, res, next) => {
  if (req.foo) {
    res.status(500).send('Fail!');
  }
  else {
    next(err);
  }
})

app.use((err, req, res, next) => {
  res.status(500).send('Error!')
})

What we have above is that if req.xhr is truthy in the first error handler, then it’ll send the Fail! response and not proceed to the second one. Otherwise, the second one will be called by calling next .

So if we add req.foo = true before the setTimeout in our route handler to have:

app.get('/', (req, res, next) => {
  req.foo = true;
  setTimeout(() => {
    try {
      throw new Error('error');
    }
    catch (ex) {
      next(ex);
    }
  })
});

Then we get Fail! . Otherwise, we get Error! .

Calling next will skip to the error handler even if there’re other route handlers in the chain.

Conclusion

To handle errors, we should call next to delegate the error handling to the default event handler if no custom event handler is defined.

We can also define our own error handler function by creating a function that has the err parameter before, req , res , and next . The err parameter has the error object passed from next .

Error handlers have to be placed after all the regular route handling code so that they’ll get run.

Also, we can have multiple error handlers. If we call next on it, then it’ll proceed to the next error handler.

Categories
JavaScript Tips

Even More JavaScript Shorthands

With the latest versions of JavaScript, the language has introduced more syntactic sugar. In this article, we’ll look at handy shortcuts that are easy to read from versions of JavaScript new and old. With them, it’ll save us time and make our code easier to read.

In this article, we’ll look at object properties assignment, finding the index of a function, replacing switch statements with objects, merging objects compacting if statements and more.

Object Properties Assignment

We can use the Object.assign method to shallow copy an object from a new object to another.

For example, if we have the following object:

const foo = {
  foo: 1
}

We can clone it as follows:

const copied = Object.assign({}, foo);

Cloning will copy the structure of its own properties and will prevent the modification of the original object.

This means modifying copied won’t modify foo .

It’s a shallow clone so nested objects aren’t cloned.

We can merge 2 objects into one and return it with Object.assign . For example, if we have the following 2 objects:

const foo = {
  foo: 1
}

const bar = {
  bar: 1
}

Then we can merge them into one and return a new object with the merged structure by running:

const merged = Object.assign(foo, bar);

Then we get:

{foo: 1, bar: 1}

which don’t reference the original objects.

IndexOf and findIndex

We can use the indexOf method to find the location of a primitive element in the array. It returns -1 if it’s not found and the first index that the value if found if it exists.

For example, if we have the following array:

const arr = [1, 2, 3, 4, 1];

Then arr.indexOf(1) will get us 0.

For arrays of objects, we can use the findIndex method to do the same thing. For instance, given the following array:

const people = [{
    name: 'Joe',
    age: 10
  },
  {
    name: 'Joe',
    age: 11
  },
  {
    name: 'Mary',
    age: 13
  },
]

We can use the findIndex method as follows:

people.findIndex(p => p.name === 'Joe')

Then we get 0 since the first entry that has 'Joe' in the name property is the first entry.

Object.entries()

With ES2017, we can get all the key-value pairs of an object by using the Object.entries method. It gets its own key-value pairs and not its prototype.

For example, we can write the following code:

const obj = {
  a: 'foo',
  b: 'bar',
  c: 'baz'
};
const arr = Object.entries(obj);

Then we get back:

["a", "foo"]
["b", "bar"]
["c", "baz"]

Then first entry is the key and the second is the value.

Object.values()

We can use the Object.values method to get the values of the object.

For example, if we have:

const obj = {
  a: 'foo',
  b: 'bar',
  c: 'baz'
};
const arr = Object.values(obj);

Then we get back:

["foo", "bar", "baz"]

, which are the values of the object properties.

Getting Characters of a String Literal

We can either use the charAt method or the bracket notation to do this.

For example, we can write:

'foo'.charAt(0);

or:

'foo'[0];

Then both get us 'f' , which is the first character of 'foo' .

Compact Alternative to Switch

We can use an object as a compact alternative to switch statements.

For example, if we have:

const apple = () => console.log('apple');
const orange = () => console.log('orange');
const grape = () => console.log('grape');
const fruit = 'apple';

switch (fruit) {
  case 'apple':
    apple();
    break;
  case 'orange':
    orange();
    break;
  case 'grape':
    grape();
    break;
  default:
    return;
}

We can replace the switch statement with a plain object:

const fruit = 'apple';

const dict = {
  apple() {
    console.log('apple');
  },
  orange() {
    console.log('orange');
  },
  grape() {
    console.log('grape');
  },
}

dict[fruit]();

Replace if’s with indexOf or includes

If we have something like:

const foo = 1;
if (foo == 1 || foo == 5 || foo == 7 || foo == 12) {
  console.log(foo);
}

We can replace with the indexOf method as follows:

const foo = 1;
if ([1, 5, 7, 12].indexOf(foo) !== -1) {
  console.log(foo);
}

-1 means that that value isn’t found in the array. Any other value indicates the index that the value is located at.

Or we can use the includes method:

const foo = 1;
if ([1, 5, 7, 12].includes(foo)) {
  console.log(foo);
}

includes returns a boolean. It’s true if the value is found in the array and false otherwise.

We can get the characters of a string literal with the charAt method or with the bracket notation.

switch statements can be replaced with objects that act as dictionaries.

With newer versions of JavaScript, we can get an object’s values and key-value pairs, with the Object.values method and Object.entries method respectively.

To get the index of the first occurrence of an array element, we can use the indexOf method for primitives and findIndex for objects.

To copy and merge objects, we can use the Object.assign method.

Categories
Quasar

Developing Vue Apps with the Quasar Library — Virtual Scrolling

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

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

Horizontal Virtual Scrolling

We can add the virtual-scroll-horizontal prop to make the virtual scrolling container horizontal:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-virtual-scroll
        style="max-height: 300px;"
        :items="heavyList"
        separator
        virtual-scroll-horizontal
      >
        <template v-slot="{ item, index }">
          <q-item :key="index" dense>
            <q-item-section>
              <q-item-label>
                #{{ index }} - {{ item.label }}
              </q-item-label>
            </q-item-section>
          </q-item>
        </template>
      </q-virtual-scroll>
    </div>
    <script>
      const maxSize = 10000;
      const heavyList = [];

      for (let i = 0; i < maxSize; i++) {
        heavyList.push({
          label: `option ${i}`
        });
      }

      new Vue({
        el: "#q-app",
        data: {
          heavyList
        }
      });
    </script>
  </body>
</html>

Customized Item Template

We can customize the item template to display items the way we want:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-virtual-scroll style="max-height: 300px;" :items="heavyList" separator>
        <template v-slot="{ item, index }">
          <q-banner
            v-if="item.banner === true"
            class="bg-black text-white q-py-xl"
            :key="index"
          >
            #{{ index }} - {{ item.label }}
          </q-banner>

          <q-item v-else :key="index" dense clickable>
            <q-item-section>
              <q-item-label>
                #{{ index }} - {{ item.label }}
              </q-item-label>
            </q-item-section>
          </q-item>
        </template>
      </q-virtual-scroll>
    </div>
    <script>
      const maxSize = 10000;
      const heavyList = [];

      for (let i = 0; i < maxSize; i++) {
        heavyList.push({
          label: `option ${i}`,
          banner: i === 0
        });
      }

      new Vue({
        el: "#q-app",
        data: {
          heavyList
        }
      });
    </script>
  </body>
</html>

We just put the item template in the default slot.

Table Style Virtual Scrolling Container

Also, we can display the items in a table style virtual scrolling container:

<!DOCTYPE html>
<html>
  <head>
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900|Material+Icons"
      rel="stylesheet"
      type="text/css"
    />
    <link
      href="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.min.css"
      rel="stylesheet"
      type="text/css"
    />
  </head>
  <body class="body--dark">
    <script src="https://cdn.jsdelivr.net/npm/vue@^2.0.0/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/quasar@1.12.13/dist/quasar.umd.min.js"></script>
    <div id="q-app">
      <q-virtual-scroll
        type="table"
        style="max-height: 70vh;"
        :virtual-scroll-item-size="48"
        :virtual-scroll-sticky-size-start="48"
        :virtual-scroll-sticky-size-end="32"
        :items="heavyList"
      >
        <template v-slot="{ item: row, index }">
          <tr :key="index">
            <td>#{{ index }}</td>
            <td v-for="col in columns" :key="index + '-' + col">
              {{ row[col] }}
            </td>
          </tr>
        </template>
      </q-virtual-scroll>
    </div>
    <script>
      const data = [
        {
          name: "Frozen Yogurt",
          calories: 159,
          fat: 6.0,
          carbs: 24
        },
        {
          name: "Ice cream sandwich",
          calories: 237,
          fat: 9.0,
          carbs: 37
        },
        {
          name: "Eclair",
          calories: 262,
          fat: 16.0,
          carbs: 23
        },
        {
          name: "Cupcake",
          calories: 305,
          fat: 3.7,
          carbs: 67
        },
        {
          name: "Gingerbread",
          calories: 356,
          fat: 16.0,
          carbs: 49
        },
        {
          name: "Jelly bean",
          calories: 375,
          fat: 0.0,
          carbs: 94
        },
        {
          name: "Lollipop",
          calories: 392,
          fat: 0.2,
          carbs: 98
        },
        {
          name: "Honeycomb",
          calories: 408,
          fat: 3.2,
          carbs: 87
        },
        {
          name: "Donut",
          calories: 452,
          fat: 25.0,
          carbs: 51
        },
        {
          name: "KitKat",
          calories: 518,
          fat: 26.0,
          carbs: 65
        }
      ];

      const columns = ["name", "calories", "fat", "carbs"];

      const heavyList = [];
      for (let i = 0; i <= 1000; i++) {
        heavyList.push(...data);
      }
      Object.freeze(heavyList);
      Object.freeze(columns);

      new Vue({
        el: "#q-app",
        data: {
          columns,
          heavyList
        }
      });
    </script>
  </body>
</html>

We render the columns in the default slot.

And we set the virtual-scroll-item-size prop to change height or width of the item in pixels, depending on if the list is vertical or horizontal respectively

The virtual-scroll-sticky-size-start prop to change the height or width of the sticky part in pixels, depending on if the list is vertical or horizontal respectively.

And the virtual-scroll-sticky-size-end prop to change the height or width of the bottom sticky part in pixels, depending on if the list is vertical or horizontal respectively.

Conclusion

We can add a virtual scrolling container with various styles with Quasar’s q-virtual-scroll component.