Site icon Shine Technologies

Building a shared calendar with Backbone.js and FullCalendar: A step-by-step tutorial

In a prior post, I explained how Backbone.js can be used to implement cascading select boxes. However, this was pretty much just a read-only affair, and these were relatively simple HTML elements. How does Backbone fare creating, updating and deleting data via a much more complex UI component?

I recently had the chance to build a shared calendar tool with Backbone and FullCalendar, a jQuery plugin for a full-sized calendar with drag-and-drop capabilities. I was quickly able to get them playing nicely with each other, and in this entry I’ll cover step-by-step what it took to get there.

Introducing FullCalendar

FullCalendar is a great plugin. The documentation is complete and useful, and it’s not that hard to get it going – considering everything that it is capable of. Here’s a picture of it in action:

So how do we get a screen like this up and running? Let’s assume that we’ve got a RESTful service such that if we do a GET to /events, we are returned a JSON array of objects representing events, where each event will contain properties recognized by FullCalendar, specifically:

We’ll assume that our server has been configured with some test data.

Now we can write the following HTML:

[sourcecode language=”html”]
<html>
<head>
<link rel=’stylesheet’ type=’text/css’ href=’stylesheets/fullcalendar.css’/>
<link rel=’stylesheet’ type=’text/css’ href=’stylesheets/application.css’/>

<script type=’text/javascript’ src=’javascripts/jquery-1.5.1.min.js’></script>
<script type=’text/javascript’ src=’javascripts/fullcalendar.min.js’></script>
<script type=’text/javascript’ src=’javascripts/underscore.js’></script>
<script type=’text/javascript’ src=’javascripts/backbone.js’></script>
<script type=’text/javascript’ src=’javascripts/application.js’></script>
</head>
<body>
<div id=’calendar’></div>
</body>
</html>
[/sourcecode]

And then the following application.js file:

[sourcecode language=”javascript”]
$(function(){
$(‘#calendar’).fullCalendar({
header: {
left: ‘prev,next today’,
center: ‘title’,
right: ‘month,basicWeek,basicDay’,
ignoreTimezone: false
},
selectable: true,
selectHelper: true,
editable: true,
events: ‘events’
});
});
[/sourcecode]

Running this will give us a calendar complete with events, which FullCalendar can load itself from the back-end. Even though we included the Backbone.js files, they’re not actually being used yet.

Note the setting of ignoreTimezone to false. If we don’t do this, FullCalendar will ignore any timezone information we’ve embedded in our ISO8601 dates.

Bringing in Backbone

Instead of having FullCalendar load the events for us, let’s have Backbone do it and then pass them to FullCalendar to render. This is going to require us to introduce a Backbone model, collection and view:

[sourcecode language=”javascript”]
$(function(){
var Event = Backbone.Model.extend();

var Events = Backbone.Collection.extend({
model: Event,
url: ‘events’
});

var EventsView = Backbone.View.extend({
initialize: function(){
_.bindAll(this);

this.collection.bind(‘reset’, this.addAll);
},
render: function() {
this.el.fullCalendar({
header: {
left: ‘prev,next today’,
center: ‘title’,
right: ‘month,basicWeek,basicDay’,
ignoreTimezone: false
},
selectable: true,
selectHelper: true,
editable: true
});
},
addAll: function(){
this.el.fullCalendar(‘addEventSource’, this.collection.toJSON());
}
});

var events = new Events();
new EventsView({el: $("#calendar"), collection: events}).render();
events.fetch();
});
[/sourcecode]

All of the action happens at the end. We create an events collection, and then a view that’s going to mediate between the collection and the calendar element in the DOM. This view registers itself to receive ‘reset’ events from the collection.

Next, we render the view immediately, which pops the calendar on the page. At this stage however, the calendar will have nothing in it.

Finally, we call events.fetch(), which causes Backbone to fetch the events from our back-end service and populate the collection with them. The collection then triggers a ‘reset’ event, which is detected by the view. The view then adds the events to the calendar as an event source, which will automatically cause the events to be displayed on the calendar. Note that the Events collection is not what’s passed into the calendar – instead we convert it to a plain array of JSON objects. Things like the id, title etc will only be available on each Event model via the get() function, whereas FullCalendar expects to be able to access them directly as properties. Fortunately, Backbone provides us with the toJSON method to do this transformation for us.

Let’s start a dialog

FullCalendar lets us detect when a period of time has been selected on the calendar. Let’s detect that event, and utilize jQuery UI to pop-up a dialog box for filling in the event details. First we have to add to the HTML. I’ve highlighted the new lines:

[sourcecode language=”html” highlight=”3,8,17,18,19,20,21,22,23,24,25,26,27,28″]
<html>
<head>
<link rel=’stylesheet’ type=’text/css’ href=’stylesheets/jquery-ui-1.8.13.custom.css’/>
<link rel=’stylesheet’ type=’text/css’ href=’stylesheets/fullcalendar.css’/>
<link rel=’stylesheet’ type=’text/css’ href=’stylesheets/application.css’/>

<script type=’text/javascript’ src=’javascripts/jquery-1.5.1.min.js’></script>
<script type=’text/javascript’ src=’javascripts/jquery-ui-1.8.13.custom.min.js’></script>
<script type=’text/javascript’ src=’javascripts/fullcalendar.min.js’></script>
<script type=’text/javascript’ src=’javascripts/underscore.js’></script>
<script type=’text/javascript’ src=’javascripts/backbone.js’></script>
<script type=’text/javascript’ src=’javascripts/application.js’></script>
</head>
<body>
<div id=’calendar’></div>

<div id=’eventDialog’ class=’dialog ui-helper-hidden’>
<form>
<div>
<label>Title:</label>
<input id=’title’ class="field" type="text"></input>
</div>
<div>
<label>Color:</label>
<input id=’color’ class="field" type="text"></input>
</div>
</form>
</div>
</body>
</html>
[/sourcecode]

Note that the dialog is initially hidden. We’ll get the Javascript to render it when the user selects some date range on the calendar. We’ve already done this sort of thing with the FullCalendar component, so doing it with a jQuery UI dialog should be pretty straightforward. In the interests of brevity, I’m not going to include all of the code again, only those lines that have changed, plus some context:

[sourcecode language=”javascript” light=”true” highlight=”15,19,20,21,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40″]

var EventsView = Backbone.View.extend({

render: function() {
this.el.fullCalendar({
header: {
left: ‘prev,next today’,
center: ‘title’,
right: ‘month,basicWeek,basicDay’,
ignoreTimezone: false
},
selectable: true,
selectHelper: true,
editable: true,
select: this.select
});
},

select: function(startDate, endDate) {
new EventView().render();
},
});

var EventView = Backbone.View.extend({
el: $(‘#eventDialog’),
initialize: function() {
_.bindAll(this);
},
render: function() {
this.el.dialog({
modal: true,
title: ‘New Event’,
buttons: {‘Cancel’: this.close}
});

return this;
},
close: function() {
this.el.dialog(‘close’);
}
});

[/sourcecode]

Now when we select a date range, we see this:

I agree that having to enter a color as text is kind of poxy, but that’s not why we’re here. I’ll leave it as an exercise to you, dear reader, to improve that in the final version of the source code if you want.

Oh, and there’s one other thing: this code doesn’t actually save anything yet.

Creating Events

Getting to this point wasn’t too hard, but it’s certainly more code than we’d probably have if we didn’t use Backbone. So the question arises: This is better…how?

The advantage of Backbone comes in when we start putting some logic behind this UI. Let’s start with creating an event. Firstly, let’s assume that if we do a POST to /events with JSON representing an event, it’ll save that event and return it to us – including it’s newly-set id. With this service in place, we can now do the following:

[sourcecode language=”javascript” light=”true” highlight=”5,6,7,8,18,23,24,25,26″]

var EventsView = Backbone.View.extend({

select: function(startDate, endDate) {
var eventView = new EventView();
eventView.collection = this.collection;
eventView.model = new Event({start: startDate, end: endDate});
eventView.render();
}
});

var EventView = Backbone.View.extend({

render: function() {
this.el.dialog({
modal: true,
title: ‘New Event’,
buttons: {‘Ok’: this.save, ‘Cancel’: this.close}
});

return this;
},
save: function() {
this.model.set({‘title’: this.$(‘#title’).val(), ‘color’: this.$(‘#color’).val()});
this.collection.create(this.model, {success: this.close});
},

});

});
[/sourcecode]

So now when a user selects a date range, we create a new Event model object and set the start and end date on it, before passing it to dialog, along with the collection that we’re expecting to add the new model to. We’ve also added a ‘Ok’ button to the dialog. It’ll now look like this when we put some data in it:

When the user clicks the ‘Ok’ button, it populates the remaining fields of the model, saves it to the back-end, then adds it to the collection.

But there’s a problem here. The new event doesn’t appear on the screen. If we refresh the page it’ll show up, but we don’t want to have to do that. Instead, let’s have our EventsView register to receive notifications when a new model is added to the collection. When it detects this notification, it’ll render the new event into the calendar:

[sourcecode language=”javascript” light=”true” highlight=”7,10,11,12″]

var EventsView = Backbone.View.extend({
initialize: function(){
_.bindAll(this);

this.collection.bind(‘reset’, this.addAll);
this.collection.bind(‘add’, this.addOne);
},

addOne: function(event) {
this.el.fullCalendar(‘renderEvent’, event.toJSON());
}
});

[/sourcecode]

So now, as soon as we click ‘OK’ and the booking is successfully saved to the back-end, it’ll appear in the calendar:

Awesome! We’ve got two views that are interacting, but only via a collection. Something else could add an event to the same collection, and the calendar would pick it up just the same. This is one of the strengths of Backbone.js.

Updating Events

Now let’s add some support for updating events. Let’s assume we can do a PUT request to /events/[id] (where [id] is the ID of the event that we want to update) where the payload is JSON containing the updated details for the event.

Now we can then extend the existing dialog to support both creation and editing:

[sourcecode language=”javascript” light=”true” highlight=”7,11,12,13,14,22,24,29,30,31,32,36,38,39,40″]

var EventsView = Backbone.View.extend({
render: function() {
this.el.fullCalendar({

select: this.select,
eventClick: this.eventClick
});
},

eventClick: function(fcEvent) {
this.eventView.model = this.collection.get(fcEvent.id);
this.eventView.render();
}
});

var EventView = Backbone.View.extend({

render: function() {
this.el.dialog({
modal: true,
title: (this.model.isNew() ? ‘New’ : ‘Edit’) + ‘ Event’,
buttons: {‘Ok’: this.save, ‘Cancel’: this.close},
open: this.open
});

return this;
},
open: function() {
this.$(‘#title’).val(this.model.get(‘title’));
this.$(‘#color’).val(this.model.get(‘color’));
},
save: function() {
this.model.set({‘title’: this.$(‘#title’).val(), ‘color’: this.$(‘#color’).val()});

if (this.model.isNew()) {
this.collection.create(this.model, {success: this.close});
} else {
this.model.save({}, {success: this.close});
}
},

});
[/sourcecode]

Note how:

Now when we click on an event, a dialog will pop-up and we can edit the details of that event:

However, when we click ‘OK’, we’ll have the same problem we had earlier: the calendar won’t redisplay the new details. To fix this, we need to detect the change and update our UI. Fortunately, the collection will emit a ‘change’ event if one of its models gets changed. We can listen for this event and take action:

[sourcecode language=”javascript” light=”true” highlight=”8,13,14,15,16,17,18″]

var EventsView = Backbone.View.extend({
initialize: function(){
_.bindAll(this);

this.collection.bind(‘reset’, this.addAll);
this.collection.bind(‘add’, this.addOne);
this.collection.bind(‘change’, this.change);

this.eventView = new EventView();
},

change: function(event) {
var fcEvent = this.el.fullCalendar(‘clientEvents’, event.get(‘id’))[0];
fcEvent.title = event.get(‘title’);
fcEvent.color = event.get(‘color’);
this.el.fullCalendar(‘updateEvent’, fcEvent);
}
});

[/sourcecode]

Note that we have to lookup the underlying event object in the calendar in order to update it. Fortunately, FullCalendar makes this easy for us by providing the clientEvents method to look up events by ID.

Now that we’ve done this, our event will be updated in the calendar immediately:

It even updated the color for us! How good is that?

Moving and Resizing Events

Updating an event’s name and color is OK, but what’d be really cool is if we could move and resize events on the calendar. Turn’s out it’s surprisingly easy:

[sourcecode language=”javascript” light=”true” highlight=”8,9,13,14,15″]

var EventsView = Backbone.View.extend({

render: function() {
this.el.fullCalendar({

eventClick: this.eventClick,
eventDrop: this.eventDropOrResize,
eventResize: this.eventDropOrResize
});
},

eventDropOrResize: function(fcEvent) {
this.collection.get(fcEvent.id).save({start: fcEvent.start, end: fcEvent.end});
}
});

[/sourcecode]

Once again, we look the element up in the collection and update the appropriate details. It’s kind of hard to demonstrate with screenshots (I’d encourage you to get the source code if you’d like to take it for a spin yourself), but I can now, for example, move our updated booking back a week and change it to a four-day event:

What’s more, if I reload the page, the booking retains in its new location and length – the updates really have been saved to the back-end! Impressive, eh?

Deleting Events

Whilst we’re here, we might as well allow users to delete events. Let’s assume we can do a DELETE request to /events/[id], where [id] is the ID of the event that we want to delete. We can then add a ‘Delete’ button to the dialog box for editing an event. Sure, it’s not the prettiest UI in the world, but we’re not being paid for our good looks here, are we?

[sourcecode language=”javascript” light=”true” highlight=”5,9,10,11,17,18,19,20,21,26,33,34,35″]

var EventsView = Backbone.View.extend({
initialize: function(){

this.collection.bind(‘destroy’, this.destroy);

},

destroy: function(event) {
this.el.fullCalendar(‘removeEvents’, event.id);
}
});

var EventView = Backbone.View.extend({

render: function() {
var buttons = {‘Ok’: this.save};
if (!this.model.isNew()) {
_.extend(buttons, {‘Delete’: this.destroy});
}
_.extend(buttons, {‘Cancel’: this.close});

this.el.dialog({
modal: true,
title: (this.model.isNew() ? ‘New’ : ‘Edit’) + ‘ Event’,
buttons: buttons,
open: this.open
});

return this;
},

destroy: function() {
this.model.destroy({success: this.close});
}
});

[/sourcecode]

So now, when we click on an event, we see a ‘Delete’ button:

When we click the ‘Delete’ button, the event model is deleted on the back-end. It’s collection detects this and emits a ‘destroy’ event, which is picked up by the EventsView. It then removes the event from the calendar, which puts us back to where we started this whole post:

So now we’ve come full circle: creating, editing and – lastly – deleting an event.

Summing Up

In this post I’ve shown how Backbone.js can work quite nicely with sophisticated jQuery plugins like FullCalendar. We’ve also worked through the full lifecycle of creating, reading, updating and deleting model objects – a task that Backbone makes easy. Finally, we’ve seen how Backbone’s event-based approach can decouple models from views, which reduces spaghetti code.

In my opinion, frameworks like Backbone.js will have a strong role to play in the future of web development by proving that client-side Javascript development doesn’t have to be a structureless free-for-all.

The final version of the source code in this blog entry can be found here

Exit mobile version