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.