Scheduling UI for Ignite

Alexander Todorov / Wednesday, March 19, 2014

You may have probably wondered why we still haven’t brought to you a full blown Scheduling/Calendar solution for Ignite. Well, there are a couple of reasons for this – one of them is that we’d like to make sure we focus on the best of breed feature rich LoB and DV components first – such as Grids and Charts. But more importantly – it’s because there are a number of existing jQuery plugins that can be easily extended in order to provide some good scheduling support, which will probably satisfy a lot of your scenarios.

One such widget is FullCalendar, and after looking at it I was really impressed by the features it has. In this blog post, I am going to demonstrate how you can easily extend FullCalendar’s styling and functionality in order to make it look and function like it is a part of Ignite UI. 

There are three main areas I’d like to cover – styling, data binding and additional functionality – custom dialogs, selection, deleting of events, etc. The styling part refers to adapting FullCalendar to the rest of the Ignite toolset – that is, we would like to make its look and feel the same, and we will achieve this using the jQuery UI theming support.

The Data Binding part is about different ways to bind calendar events to FullCalendar and what the data objects structure should be, including binding it to data from the $.ig.DataSource component.

The custom dialogs are about adding extra UI around FullCalendar, which will allow us to edit appointments by double clicking on them, as well as create a nice dialog UI for adding new appointments (events). This also includes creating and editing existing appointment/event objects and persisting them through $.ig.DataSource. In addition to that, I’ve implemented UI for selection and removal of existing events from the calendar views. 

Styling

Let’s start with styling. If you look at the default FullCalendar styles, you can see they are different from the styles of the new Ignite Theme, and simply putting it on a page with other Ignite components doesn’t look very nice. Example (FullCalendar on the left, Ignite Grid on the right):

Luckily, FullCalendar has great support for jQuery UI styling, and it applies classes like ui-state-default, ui-state-active, ui-widget-header, and so on – in the rendered markup. And because Ignite UI is fully jQuery UI CSS compliant, this means that we can automatically apply our Infragistics CSS, and it should take effect. Indeed, this turns out to be a piece of cake – we only need to add the Infragistics.theme.css style reference in order to make it happen:

<head>
<link rel='stylesheet' href='../css/themes/infragistics/infragistics.theme.css' />
<link href='../css/fullcalendar.css' rel='stylesheet' />
<link href='../css/fullcalendar.print.css' rel='stylesheet' media='print' />
<script src='../js/jquery.js'></script>
<script src='../js/jquery-ui.js'></script>
<script src='../js/fullcalendar.js'></script>

And we get the nice Ignite-friendly FullCalendar look and feel:

Additionally, you can change the background color of the event/appointment boxes, by editing the fc-event-* set of CSS classes.

Data Binding

FullCalendar supports several different data formats:

 – You can define events inline, for example:

events: [
	{
		title: 'All Day Event',
		start: new Date(y, m, 1)
	},
	{
		title: 'Long Event',
		start: new Date(y, m, d-5),
		end: new Date(y, m, d-2)
	}
]

-  You can provide a URL string which will be queried and need to return data in a similar format:

events: "/api/eventlist"

which returns an array of objects that have a title, start, end (optional), and url (optional) properties.

What’s really cool about FullCalendar is that you can specify multiple event sources using the eventSources option – moreover, those can have mixed types, for instance the first event source may be a JSON array, while the second may be a URL. You can also specify different colors for the events/appointments from those multiple sources. 

- You can also set events to point to a function that, once executed, should return an array of event objects. What’s nice about this approach is that it can be called multiple times, whenever FullCalendar needs events data (this also depends on the value of the lazyFetching property). The function basically accepts a start and end parameters, and calls the callback parameter whenever the events, which fall into this range, are generated/obtained from the data source. 

Additionally, when you bind FullCalendar to an URL, and lazyFetching is enabled (default), FullCalendar will only request events for the currently visible UI – for instance the current day, week or month. It will pass a start and end parameters to the URL, and it’s responsibility of the server-side logic to handle these params. 

Having said all of the above, $.ig.DataSource can come quite handy when it comes to data binding data to FullCalendar. Since it has out of the box filtering support, and also supports multiple data formats, we can make use of it in order to hide some of the complexity required to normalize and filter the data. 

Example – binding using a function (the complete sample is attached to this blog post):

var dataSource = new $.ig.DataSource({
	dataSource: data, // list of event objects
	schema: {
		fields: [
			{name: "title", type: "string"},
			{name: "start", type: "date"},
			{name: "end", type: "date"},
			{name: "allDay", type: "bool"},
			{name: "url", type: "string"}
		]
	}
}).dataBind();

$('#calendar').fullCalendar({
	theme: true,
	defaultView: "agendaDay",
	header: {
		left: 'prev,next today',
		center: 'title',
		right: 'month,agendaWeek,agendaDay'
	},
	editable: true,
	events: function (start, end, callback) {
		// we may also query a remote URL here, passing the same filtering expressions
		dataSource.filter([
			{fieldName: "start", expr: start, cond: "after"},
			{fieldName: "end", expr: end, cond: "before"}
		], "AND");
		callback(dataSource.dataView());
	}
});

Custom Dialogs and extra functionality

There are two main types of dialogs I would like to cover – creating new appointments, and editing existing ones. We can basically achieve both of these features with a single custom dialog. FullCalendar does not provide UI for adding/editing/removing events. There are a couple of basic samples with browser prompts but they are quite basic. On top of that, we would like to support some validation for our dialogs. 

In order to achieve the above, let’s start by examining the hooks FullCalendar provides – for instance, we can use a “select” event handler and open a dialog from it. The handler will be invoked any time we click on a day/month/week slot:

select: function(start, end, allDay) {

    // custom logic to open a dialog for a new event goes here

},

When it comes to editing existing events, we need to bind to the “eventRender” event that FullCalendar fires for every event, and bind a click/dblclick handler to the event element. Here, I would like a custom UI modal dialog to appear once I double click on an event:

eventRender: function (event, element) {
	element.bind('dblclick', function () {
		<logic goes here>
	});
},

Now let’s write the markup for our custom dialog first. I’d like to make use of our igDialog as well as our numeric/date and text editors, in order to save tons of time. Here is the markup for my dialog template:

<div id='eventdialog'>
	<div>
		<label class="label">Title: </label>
		<input class="title input"/>
	</div>
	<div>
		<label class="label">Start: </label>
		<div class="startdate input"></div>
	</div>
	<div>
		<label class="label">End: </label>
		<div class="enddate input"></div>
	</div>
	<div>
		<label>All Day Event</label>
		<input type="checkbox" class="allday"/>
	</div>
	<div class="buttons">
		<button class="okbutton ui-state-default">Save</button>
		<button class="cancelbutton ui-state-default">Cancel</button>
	</div>
</div>

Basically, I have labels and inputs for the Start date, End date, Title, and AllDay event values. I also have two buttons – Ok and Cancel. Validation will be done whenever Ok is clicked.

I’d like to make the title input mandatory, and the Start & End date date editors with custom format – where values can be incremented with a spin button in 30 minute intervals. This is very easy to do, here is the initialization logic for my editors:

$(".title").igEditor({
	required: true,
	validatorOptions: {
		required: true
	}
});
$(".startdate").igDateEditor({
	dateDisplayFormat: "HH:mm",
	dateInputFormat: "HH:mm",
	button: "spin",
	spinDelta: 30
});
$(".enddate").igDateEditor({
	dateDisplayFormat: "HH:mm",
	dateInputFormat: "HH:mm",
	button: "spin",
	spinDelta: 30,
	validatorOptions: {
		errorMessage: "End Date cannot be earlier than Start Date"
	},
	minValue: $(".startdate").igDateEditor("value")
});

Note that I am adding additional validation logic to the start and end dates, such that we don’t end up with an appointment which has the Start Date greater than the End Date.

After this is done, we can initialize our igDialog itself:

$("#eventdialog").igDialog({
	state: "closed",
	modal: true,
	draggable: true,
	resizable: true,
	height: 350,
	width: 400,
	headerText: "Add/Update Event"
});

Because the allDay property may be both true/false, as well as undefined, we’d like to make sure we track whether it has been enabled through the UI, or whether it’s undefined:

$(".allday").click(function (event) {
    $("#calendar").data("allDayUserSet", true);
});

Now let’s go back to our “select” and “dblclick” handlers, and fill them in, so that we open our dialog for adding and editing events:

select: function(start, end, allDay) {
	// set the initial values
	$('#calendar').removeData("originalEvent");
	$(".startdate").igDateEditor("value", start);
	$(".enddate").igDateEditor("value", end);
	$(".allday").prop("checked", allDay);
	$(".title").val("");
	$("#eventdialog").igDialog("open");
},

Since need to keep track of the event we’re editing – whether it’s an existing one or not, we want to make sure we remove this reference if we are actually adding a brand new event. Also, we are filling the editors by taking the start/end/allDay event parameters that FullCalendar passes.

And for editing existing events:

eventRender: function (event, element) {
	element.bind('dblclick', function () {
		$("#calendar").data("allDayUserSet", false);
		$(".startdate").igDateEditor("value", event.start);
		$(".enddate").igDateEditor("value", event.end);
		var isAllDay = event.allDay || event.end === null;
		$(".allday").prop("checked", isAllDay);
		$(".title").val(event.title);
		$("#calendar").data("originalEvent", event);
		$("#eventdialog").igDialog("open");
	});
	element.data("eventId", event._id);
}

Note that we consider an allDay event to be an event which either has allDay set to true (explicitly), or an event which doesn’t have an “end” date set.

Now the only thing we need to do is implement what happens when someone clicks on the OK and Cancel buttons:

Cancel is really easy, we don’t add & persist anything, just close the dialog:

$(".cancelbutton").click(function () {
	if (!$(".ui-state-error").is(":visible")) {
		$("#eventdialog").igDialog("close");
	}
});

Note that if there are errors on the page, because of failed validation, we don’t want to close the window until those errors are corrected.

Now some of the more important snippets from the OK part – the complete code is in the attached sample:

First, obtain the event data from the dialog:

var start = $(".startdate").igDateEditor("value");
var end = $(".enddate").igDateEditor("value");
var title = $(".title").val();

and so on. Then, perform validation:

$(".title").igValidator("validate");
$(".enddate").data("igDateEditor").options.minValue = start;
$(".enddate").igDateEditor("validate");

Then we need to find the existing event in our data source, in case we are editing an already added event. Remember, we are reusing the same dialog for both editing and adding new events. If we are updating an existing event, we can then just call:

$('#calendar').fullCalendar('updateEvent', originalEvent);

where originalEvent is the originalEvent with updated start/end/allDay properties. Note it’s important that we actually find this original event and don’t pass a new object with those property values. That’s because FullCalendar uses the _id internal property in order to match data. 

Remember that since we’re binding to $.ig.DataSource, we want to update it through the API as well, so that we get nice transactions recorded – they can be later used to make an AJAX call in order to persist our event data to a database:

dataSource.updateRow(i, {
	allDay: allDay,
	start: start,
	end: end,
	title: title
}, true);

In case we are adding a new event, we do:

$('#calendar').fullCalendar('renderEvent',
	{
		title: title,
		start: start,
		end: end,
		allDay: allDay
	},
	true // make the event "stick"
);

And then also add the data through the dataSource’s API:

dataSource.addRow(events.length, {
	allDay: allDay,
	start: start,
	end: end,
	title: title
}, true);

Finally, we can close the dialog, if everything is ok. Here are a couple of screenshots of this awesome functionality in action:

Editing an existing event by double click:

Validation:

Now, let’s enable selection. This is quite easy to do, actually. We just need to add the following code to our eventRender logic:

element.bind("click", function () {
	$(".fc-event .fc-event-inner").removeClass("ui-state-hover");
	element.find(".fc-event-inner").toggleClass("ui-state-hover");
});

What we get is this:

Now since we have a special style applied to the selected event (appointment), let’s add some logic to be able to delete it by pressing the DELETE keyboard key:

$("body").keydown(function (event) {
	if (event.keyCode === 46) {
		// remove event
		var confirmRemove = confirm("Do you really want to remove this event?");
		if (confirmRemove) {
			$("#calendar").fullCalendar("removeEvents", $(".fc-event .ui-state-hover").closest(".fc-event").data("eventId"));
		}
	}
});

Quite elegant and powerful. And all of this works for any view – Day, Week, Month.

We can add many more extra features to the scheduling story, and it would be great if you can share your thoughts with us, so that we can better prioritize. 

app.zip