This tutorial shows how to create a simple Grails web application that displays an AJAX event calendar using the open-source DayPilot Lite for Java library. It supports drag and drop operations (event creating, moving, resizing).

Included

Requirements

  • JRE 5 or higher
  • Grails 2
  • NetBeans (optional)

1. Libraries

ajax-event-calendar-grails-netbeans.png

JavaScript libraries

  • Copy common.js and calendar.js to web-app/js/daypilot directory of your web application.

Java libraries

  • Copy daypilot-lite-1.0.458.jar to lib directory of your web application.

2. Grails View

Create a new Grails view (index.gsp) and add references to jQuery and the DayPilot JavaScript libraries to the header or to the body:

    <script src="${resource(dir: 'js/jquery', file: 'jquery-1.3.2.min.js')}" type="text/javascript"></script>
    <script src="${resource(dir: 'js/daypilot', file: 'common.js')}" type="text/javascript"></script>
    <script src="${resource(dir: 'js/daypilot', file: 'calendar.js')}" type="text/javascript"></script>

Put a placeholder for the Calendar in the <body> :

<div id="dpc"></div>

Add the jQuery plugin initialization code:

<script type="text/javascript">

$(document).ready(function() {
	
	var dpc = $("#dpc").daypilotCalendar({
		backendUrl : 'backend',
		viewType : "Week"
	});
});

</script>

It is necessary to specify the backendUrl property. That's the URL which handles the AJAX callback requests.

3. Grails Controller

Now we need to add a new action to the controller that will handle the backend callback requests.

class CalendarController {
    
    static allowedMethods = [backend:'POST']

    def index() { }
    
    def backend() {
        new Dpc().process(request, response)
    }
}

The backend action will  pass control to a new instance of a Dpc class. Dpc class is a helper class derived from org.daypilot.ui.DayPilotCalendar. It will handle the calendar events, such as eventMove, eventResize, etc.

Now override onInit() method of the DayPilotCalendar class in the Dpc class in order to handle the Init event:

@Override
def void onInit() {

  // set the database fields
  dataValueField = "event_id";
  dataTextField = "event_name";
  dataStartField = "event_start";
  dataEndField = "event_end";

  events = Db.getEvents(request, startDate.toDate(), startDate.addDays(days).toDate());

  update(UpdateType.FULL);
}

This methods maps the data source fields (dataValueField, dataTextField, dataStartField, dataEndField), loads the event set from a database and requests an update.

org.daypilot.demo.db.Db is a Java class that loads events from a database. In this tutorial, we are using an embedded HSQLDB instance.

public static Table getEvents(HttpServletRequest request, Date from, Date to) throws SQLException, JSONException, ClassNotFoundException {
  Thread.currentThread().getContextClassLoader().loadClass("org.hsqldb.jdbcDriver" );
  Connection c = DriverManager.getConnection(getConnectionString(request), "sa", "");

  PreparedStatement st = c.prepareStatement("SELECT event_id, event_name, event_start, event_end FROM EVENTS WHERE NOT ((event_end <= ?) OR (event_start >= ?));");
  st.setTimestamp(1, new Timestamp(from.getTime()), Calendar.getInstance(DateTime.UTC));
  st.setTimestamp(2, new Timestamp(to.getTime()), Calendar.getInstance(DateTime.UTC));
  ResultSet rs = st.executeQuery();
  Table table = TableLoader.load(rs);
		
  rs.close();
  st.close();
  c.close();
		
  return table; 
}

You should replace it with your own data handler.

4. Event Moving

ajax-event-calendar-grails-event-moving.png

In order to handle event move action, we need to override onEventMove() method. The event arguments are available in EventMoveArgs class (ea parameter).

@Override
def void onEventMove(EventMoveArgs ea) {
  // update the DB
  Db.moveEvent(request, ea.value, ea.newStart.toTimeStamp(), ea.newEnd.toTimeStamp());
  update();
}

It has to be enabled on the client-side as well (eventMoveHandling):

<script type="text/javascript">

$(document).ready(function() {
  var dpc = $("#dpc").daypilotCalendar({
    backendUrl : 'backend',
    viewType : "Week",
    eventMoveHandling : "CallBack"
  });
});

</script>

We have moved the event reloading logic to onFinish() method which is called during every callback after the main event handler:

@Override
def void onFinish() {

  // load the events only if update was requested
  if (updateType == UpdateType.NONE) {
    return;
  }

  // set the database fields
  dataValueField = "event_id";
  dataTextField = "event_name";
  dataStartField = "event_start";
  dataEndField = "event_end";

  events = Db.getEvents(request, startDate.toDate(), startDate.addDays(days).toDate());

}
 

5. Event Resizing

ajax-event-calendar-grails-event-resizing.png

Now we will add event resize handler in a similar way. 

Override onEventResize method:

@Override
def void onEventResize(EventResizeArgs ea) {
  Db.resizeEvent(request, ea.value, ea.newStart.toTimeStamp(), ea.newEnd.toTimeStamp());
  update();
}

And enable it on the client side using eventResizeHandling property:

<script type="text/javascript">

$(document).ready(function() {
  var dpc = $("#dpc").daypilotCalendar({
    backendUrl : 'backend',
    viewType : "Week",
    eventMoveHandling : "CallBack",
    eventResizeHandling: "CallBack"
  });
});

</script>

6. Event Creating

ajax-event-calendar-grails-new-event.png

Selecting a free time range fires timeRangeSelected event. We will handle this event using JavaScript instead of direct CallBack and ask for the event text using a simple JavaScript dialog (prompt):

<script type="text/javascript">
var dpc;
$(document).ready(function() {
  dpc = $("#dpc").daypilotCalendar({
    backendUrl : 'backend',
    viewType : "Week",
    eventMoveHandling : 'CallBack',
    eventResizeHandling : 'CallBack',
    timeRangeSelectedHandling : 'JavaScript',
    onTimeRangeSelected: function(start, end) {
      var name = prompt("New Event Name:", "New Event");
      dpc.timeRangeSelectedCallBack(start, end, null, { name: name });
      dpc.clearSelection();
    }
  });
});
</script>

Server-side handler:

@Override
def void onTimeRangeSelected(TimeRangeSelectedArgs ea) {
  Db.insertEvent(request, ea.data.getString("name"), ea.start.toDate(), ea.end.toDate());
  update();
}