Categories
Vue

How to Make a Calendar App with Vue

Spread the love

For many applications, recording dates is an important feature. Having a calendar is often a handy feature to have. Fortunately, many developers have made calendar components that other developers can easily add to their apps.

Vue.js has many calendar widgets that we can add to our apps. One of them is Vue.js Full Calendar. It has a lot of features. It has a month, week, and day calendar. Also, you can navigate easily to today or any other days with back and next buttons. You can also drag over a date range in the calendar to select the date range. With that, you can do any manipulation you want with the dates.

In this article, we will make a simple calendar app where users can drag over a date range and add a calendar entry. Users can also click on an existing calendar entry and edit the entry. Existing entries can also be deleted. The form for adding and editing the calendar entry will have date and time pickers to select the date and time.

We will save the data on the back end in a JSON file.

We will use Vue.js to build our app. To start, we run:

npx @vue/cli create calendar-app

Next we select ‘Manually select features’ and select Babel, CSS Preprocessor, Vue Router and Vuex.

After the app is created, we have to install some packages that we need. We need Axios for making HTTP requests to our back end, BootstrapVue for styling, jQuery and Moment are dependencies for the Vue-Full-Calendar package which we will use to display a calendar. Vee-Validate for form validation, Vue-Ctk-Date-Time-Picker to let users pick the date and time for the calendar events andVue-Full-Calendar is used for the calendar widget.

We run:

npm i axios bootstrap-vue jquery moment vee-validate vue-ctk-date-time-picker vue-full-calendar

to install all the packages.

With all the packages installed, we can start writing the app. First we start with the form for entering the calendar entries.

Create a file called CalendarForm.vue in the components folder and add:

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

        <b-form-group label="Start" label-for="start">
          <ValidationProvider name="start" rules="required" v-slot="{ errors }">
            <VueCtkDateTimePicker
              input-class="form-control"
              :state="errors.length == 0"
              v-model="form.start"
              name="start"
            ></VueCtkDateTimePicker>
            <b-form-invalid-feedback :state="errors.length == 0">Start is required</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>

        <b-form-group label="End" label-for="end">
          <ValidationProvider name="end" rules="required" v-slot="{ errors }">
            <VueCtkDateTimePicker
              input-class="form-control"
              :state="errors.length == 0"
              v-model="form.end"
              name="end"
            ></VueCtkDateTimePicker>
            <b-form-invalid-feedback :state="errors.length == 0">End is required</b-form-invalid-feedback>
          </ValidationProvider>
        </b-form-group>

        <b-button type="submit" variant="primary">Save</b-button>
        <b-button type="button" variant="primary" @click="deleteEvent(form.id)">Delete</b-button>
      </b-form>
    </ValidationObserver>
  </div>
</template>

<script>
import { requestsMixin } from "../mixins/requestsMixin";
import * as moment from "moment";

export default {
  name: "CalendarForm",
  props: {
    edit: Boolean,
    calendarEvent: Object
  },
  mixins: [requestsMixin],
  data() {
    return {
      form: {}
    };
  },
  watch: {
    calendarEvent: {
      immediate: true,
      deep: true,
      handler(val, oldVal) {
        this.form = val || {};
      }
    }
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      this.form.start = moment(this.form.start).format("YYYY-MM-DD HH:mm:ss");
      this.form.end = moment(this.form.end).format("YYYY-MM-DD HH:mm:ss");

      if (this.edit) {
        await this.editCalendar(this.form);
      } else {
        await this.addCalendar(this.form);
      }
      const response = await this.getCalendar();
      this.$store.commit("setEvents", response.data);
      this.$emit("eventSaved");
    },

    async deleteEvent(id) {
      await this.deleteCalendar(id);
      const response = await this.getCalendar();
      this.$store.commit("setEvents", response.data);
      this.$emit("eventSaved");
    }
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
button {
  margin-right: 10px;
}
</style>

In this file, we use the BootstrapVue form component to build our form. We use the VueCtkDateTimePicker to add the date and time picker for our form to let users pick the time and date.

We wrap each input with the ValidationProvider component to let us validate each field. Each field is required so we set the rules prop to required .

We set the :state binding to errors.length == 0 to display errors only when the errors array has length bigger than 0. This also applies to b-form-invalid-feedback component.

The form has a Save button to to run onSubmit when the button is clicked. We check the form’s validity by calling this.$refs.observer.validate() . We have this object because we wrapped the form with theValidationObserver component with ref set to observer .

In the function, we format the start and end dates so that we save the correct date and time.

If the edit prop is set to true, then we call the this.editCalendar function in requestsMixin . Otherwise we call this.addCalendar in the same mixin.

Once that succeeds, we call this.$store.commit(“setEvents”, response.data); after calling this.getCalendar to put the latest calendar events into our Vuex store.

After that’s done, we emit the eventSaved event so that we can close the modals in located in the home page.

Next we create the mixins folder and the requestsMixin.js file inside it. In there, we add:

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

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

    addCalendar(data) {
      return axios.post(`${APIURL}/calendar`, data);
    },

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

    deleteCalendar(id) {
      return axios.delete(`${APIURL}/calendar/${id}`);
    }
  }
};

These are the functions for making HTTP requests to the back end.

Next we modify Home.vue , by replacing the existing code with:

<template>
  <div class="page">
    <div class="buttons">
      <b-button v-b-modal.add-modal>Add Calendar Event</b-button>
    </div>
    <full-calendar :events="events" @event-selected="openEditModal" defaultView="month" />

    <b-modal id="add-modal" title="Add Calendar Event" hide-footer ref="add-modal">
      <CalendarForm :edit="false" @eventSaved="closeModal()" />
    </b-modal>

    <b-modal id="edit-modal" title="Edit Calendar Event" hide-footer ref="edit-modal">
      <CalendarForm :edit="true" :calendarEvent="calendarEvent" @eventSaved="closeModal()" />
    </b-modal>
  </div>
</template>

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

export default {
  name: "home",
  components: {
    CalendarForm
  },
  mixins: [requestsMixin],
  computed: {
    events() {
      return this.$store.state.events;
    }
  },
  data() {
    return {
      calendarEvent: {}
    };
  },
  async beforeMount() {
    await this.getEvents();
  },
  methods: {
    async getEvents() {
      const response = await this.getCalendar();
      this.$store.commit("setEvents", response.data);
    },
    closeModal() {
      this.$refs["add-modal"].hide();
      this.$refs["edit-modal"].hide();
      this.calendarEvent = {};
    },
    openEditModal(event) {
      let { id, start, end, title } = event;
      this.calendarEvent = { id, start, end, title };
      this.$refs["edit-modal"].show();
    }
  }
};
</script>

<style lang="scss" scoped>
.buttons {
  margin-bottom: 10px;
}
</style>

In this file, we include the full-calendar component from the Vue Full Calendar package, and an add and edit calendar event modals. We use CalendarForm for both.

Notice that we handle the eventSaved event here, which is emitted by CalendarForm . We call closeModal when the event is emitted, so that the modals will close.

We also pass in the calendarEvent and edit prop set to true when we open the edit modal.

The ref for the modal is set so we can show and hide the modal by their ref .

We get the latest state of the events in the Vuex store by watching this.$store.state.events .

Next we replace the code in App.vue with:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Calendar 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;
}
</style>

We add the BootstrapVue b-navbar here and watch the route as it changes so that we can set the active prop to the link of the page the user is currently in.

Next we change the code in main.js to:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import FullCalendar from "vue-full-calendar";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import 'vue-ctk-date-time-picker/dist/vue-ctk-date-time-picker.css';
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
import VueCtkDateTimePicker from 'vue-ctk-date-time-picker';

extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(FullCalendar);
Vue.use(BootstrapVue);
Vue.component('VueCtkDateTimePicker', VueCtkDateTimePicker);

Vue.config.productionTip = false;

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

We import all the app-wide packages we use here, like BootstrapVue, Vee-Validate and the calendar and date-time picker widgets.

The styles are also imported here so we can see them throughout the app.

Next in router.js , replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
import 'fullcalendar/dist/fullcalendar.css'

Vue.use(Router);

export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

to set the routes for our app, so that when users enter the given URL or click on a link with it, they can see our page.

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

We added an events state for the calendar events, and a setEvents function that we dispatched with this.$store.commit so that we can set the events in the store and access it in all our components.

Finally, we replace the code in index.html 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>Calendar App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-calendar-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 app’s title.

Now all the hard work is done. All we have to do is use JSON Server NPM package located at https://github.com/typicode/json-server for our back end.

Install it by running:

npm i -g json-server

Then run it by running:

json-server --watch db.json

In db.json , replace the existing content with:

{
  "calendar": []
}

Next we run our app by running npm run serve in our app’s project folder to run our app.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *