Vue Calendar Component with Laravel API

It’s possible we need to handle and display events in our app. We can find some plain JS solutions or complex libraries for generating a calendar, but mostly we don’t use all the features what these 3rd party packages offer. Also, if we want to connect it to our back-end API, we need to hack something most of the time and the result is messy. Let’s see, how can we bake our own component for this with moment.

Composing the data structure

If we want a monthly view, the best way to organize the data is to group them by day. We will generate the grid for the calendar and every cell will represent a date. This way we can easily check if the date has any events or not. Let’s see an example of the data structure:

let events = {
   "2017-10-30":[
       {"id":26,"name":"Icie Williamson","starts_at":"2017-10-30 15:21:28"}
   ],
   "2017-10-27":[
       {"id":1,"name":"Olaf Hintz","starts_at":"2017-10-27 15:21:28"},
       {"id":10,"name":"Mr. Sanford Kassulke","starts_at":"2017-10-27 15:21:28"}
   ],
   "2017-10-14":[
       {"id":2,"name":"Juana Schmitt","starts_at":"2017-10-14 15:21:28"},
       {"id":6,"name":"Vito Simonis","starts_at":"2017-10-14 15:21:28"},
       {"id":14,"name":"Lisette McLaughlin","starts_at":"2017-10-14 15:21:28"}
   ]
};

The logic behind the component

Here we have a lot of jobs, to do. Like we need to calculate the start week and the end week of the month. Then we need to find the day what represents today in the calendar and we need to give an extra class for it. Also, we need to list the events for every they if it has any.

Now let’s see the code and then the explanation:

<script>
export default {
    data() {
        return {
    	    events: events,
            date: moment()
        };
    },
    computed: {
        startWeek() {
            return this.date.month() === 0 ? 0 : this.date.clone().startOf('month').week();
        },
        endWeek() {
            return this.date.clone().endOf('month').week();
        },
        month() {
            const month = [];

            for (let week = this.startWeek; week <= this.endWeek; week++) { 
                month.push({ 
                    week: week, 
                    days: [,,,,,,,].fill(0).map((n, i) => {
                        return this.date
                            .clone()
                            .week(week)
                            .startOf('week')
                            .add(i, 'day');
                    });
                });
            }

            return month;
        }
    },

    methods: {
        eventsOf(day) {
            return this.events[day.format('YYYY-MM-DD')];
        },
        isToday(day) {
            return moment().format('YYYY-MM-DD') === day.format('YYYY-MM-DD');
        }
    }
}
</script>

Data:

So our data is no more but the events and a moment instance what we will use later.

Computed:

At the startWeek, we calculate the number of the first week in the month. Also, we have to check if a week is the first week of the year or not.

The endWeek property is the last week of the month. Note the clone() method on the moment instance. We need it to prevent any manipulation on the original moment instance, so we perform the actions on the cloned version.

At the month property we run a loop between the start and the end week, and populate the weeks with the days. Every day will be a moment instance.

Methods:

With the eventsOf method will return the events of the given day.

The isToday method will determine if the given day is today.

So this is the basic logic behind our component. This is a very basic one, we don’t even change the dates or we have nothing like axios to handle the AJAX calls for the API end.

The template and the stlye

At the template part we have nothing complex, but some loops and class bindings. Let’s see it:

<template>
<div class="event-calendar__grid">
    <div class="days-of-week">
        <div>Mo</div>
        <div>Tu</div>
        <div>We</div>
        <div>Th</div>
        <div>Fr</div>
        <div>Sa</div>
        <div>Su</div>
     </div>
     <div class="week" v-for="week in month" :key="week.week">
         <div class="day" :class="{ 'is-today': isToday(day) }" v-for="(day, index) in week.days" :key="index">
             <div class="day__header">{{ day.format('DD') }}</div>
             <div class="day__events">
                 <div class="event" v-for="event in eventsOf(day)" :key="event.id">{{ event.name }}</div>
             </div>
         </div>
     </div>
</div>
</template>

We have some fixed parts, like the name of the days, and the others are dynamic based on the date and the events. Note that, we are using the :key attribute. This is an essential thing to when we are working with loops. Read more about the style guide for more information.

Also, we have a very basic style for the component. To make it flexible, we will use flexbox.

<style scoped>
.days-of-week {
    display: flex;
    text-align: center;
}
.days-of-week > div {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50px;
    border-bottom: 1px solid rgba(0,0,0,0.1);
}
.event-calendar__grid {
    border: 1px solid rgba(0, 0, 0, 0.1);
}
.week {
    display: flex;
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.week:last-child {
    border-bottom: none;
}
.week > .day {
    flex: 1;
    height: 130px;
    padding: 5px;
    box-sizing: border-box;
    border-right: 1px solid rgba(0, 0, 0, 0.1);
}
.week > .day:last-child {
    border-right: none;
}
.day.is-today {
    background-color: #ddd;
}
</style>

This is really just a very basic styling for the component, the main point is the flexbox here.

The template part is quite understandable, we would like to move on. All our component parts are ready with static data. We won’t integrate axios for AJAX request and Promise handling, but from here it’s a very easy job, not too hard to bake it out by yourself. Now let’s move on to the server-side a bit.

Obtaining the data from the API

The point of this section, to show how can we generate and group the eloquent results to the structure we need. Let’s say we can make a request from the component, with all the parameters we need, so we will take care of the controller only.

Since we are using an API here, we need to handle the authentication with API tokens. We don’t cover this now, but you can read more about this topic here: Laravel API Auth with Tokens

We will filter the events here by year and month and convert them to the structure we need, to make it compatible with the Vue component.

// app/Http/Controllers/EventsController.php

public function index(Request $request)
{
    $events = Event::whereYear('starts_at', $request->year)
        ->whereMonth('starts_at', $request->month)
        ->orderBy('starts_at', 'desc')
        ->get()
        ->groupBy(function ($event) {
            return $event->starts_at->format('Y-m-d');
        });

    return response()->json($events);
}

Note, we have starts_at attribute in our Event models. We filter and arrange the records by this field. You may not be familiar with the whereYear and the whereMonth where clauses. Since we have a monthly view, we need the year and the month to retrieve the proper events, these two methods are just perfect for us. We can retrieve the events we want very easily. You can find the docs about them here.

Since we use $request->month and $request->year. That means, we have two query parameters, something like this: ?year=2017&month=10. We need to pass these two parameters on every request.

Next, we need to group the events by day. It’s important to understand, we don’t do it on the query builder, we group the events on the collection instance after the query was fired and the results were fetched into an Eloquent Collection.

At the end, we return with a JSON response what our Vue component can handle.

Summary

This post is just a very basic way to create a calendar component. We did not cover how to use axios, how to setup watchers, how to change the date and so on. But still, it can be a very good starting point to bake your own ideas in.

You can find the code and a working example here: https://jsfiddle.net/ohawxv73/1/