28 May 2010

In a bind

Yesterday I did some refactoring of the JavaScript that manages jQuery Datepickers and the event handlers attached to them. Several of the modal dialog boxes of the application use the following pattern: a text box that permits direct entry of a date (as 'MM/DD/YY'), a Datepicker tied to the text box that allows the user to point and click at a date, and 3 hidden fields for the month, day, and year portion of the date. It is this set of 3 fields that is persisted.

So we have an event handler that takes care of direct input (hdlChange) and one that takes care of the point-and-click selection (hdlDatePickerSelect). The difficulty I had to overcome was establishing an execution context for the event handlers so that they could access member variables that identified the date being processed (several of the app's modals [managed with a jQuery Dialog] collect 2 or 3 dates).

Another complicating factor was this unfortunate behavior of the Datepicker object: as we use it, the Datepicker's text boxes on each modal must have distinct IDs, even modals that have been closed and destroyed. Hence the variable overlayAffix (we call the modals "overlays" in this app) that is appended to the ID-based selector for the Datepicker.

Anyway, back to the problem of establishing context. The nut of it is passing some sort of extra information to the event handler that identifies the this that you want to use, because the event handler's this is the text box DOM element.

I experimented with jQuery's .bind() method, and that worked just fine for the change handler. (Mike West's article is helpful background.) Using .bind(), you can pass whatever you like into an event handler—say, a pointer to your execution context—and retrieve it through the event object. But, unfortunately, I did not find a way to make this technique work for the select handler. The Datepicker doesn't have a .bind() analogue: rather, the handler is specified as the option onSelect; furthermore, the signature of the handler doesn't include the event (AFAIK).

So I resorted to what I consider trickery, taking advantage of JavaScript's expando properties: I simply attached the execution context as a an additional property of the text box (date control).

Here's the completed script, lightly edited for presentation. Wording note: context in the code below refers to the jQuery DOM context, not the execution context that we've been discussing.



// Helpers for managing a datepicker, the associated text box, and hidden fields.

CLIENT.Utilities.DatePicker = function (context, overlayAffix, dateAffix) {
this.context = context;
this.overlayAffix = overlayAffix; //identifies the overlay we're on (e.g., 'GeneralJournal')
this.dateAffix = dateAffix; //identifies the date (e.g., 'invoice')
this.datePickerSelector = '#' + dateAffix + 'Date' + overlayAffix;
this.monthSelector = '#' + dateAffix + 'Month';
this.daySelector = '#' + dateAffix + 'Day';
this.yearSelector = '#' + dateAffix + 'Year';

this.setDate = function (date) {
try {
$(this.monthSelector, this.context).val(date.getMonth() + 1).change();
$(this.daySelector, this.context).val(date.getDate()).change();
$(this.yearSelector, this.context).val(date.getFullYear()).change();
} catch (e) {
CLIENT.Utilities.postWarnMessage('', 'DatePicker.js setDate()', e);
}
};

this.clearDate = function () {
try {
$(this.monthSelector, this.context).val(0).change();
$(this.daySelector, this.context).val(0).change();
$(this.yearSelector, this.context).val(0).change();
} catch (e) {
CLIENT.Utilities.postWarnMessage('', 'DatePicker.js clearDate()', e);
}
};


this.setupControls = function () {
try {
$(this.datePickerSelector, this.context).change(this.hdlChange);

$(this.datePickerSelector, this.context).datepicker({
showOn: 'button',
buttonImage: '/new_cms/images/calendar.png',
buttonImageOnly: true,
onSelect: this.hdlDatePickerSelect
});

//give the date control a reference to the 'this' DatePicker object,
//so that event handlers can access the DatePicker object
$(this.datePickerSelector, this.context)[0].theThis = this;
} catch (e) {
CLIENT.Utilities.postWarnMessage('', 'DatePicker.js setupControls()', e);
}
};

//event handlers for which 'this' is the date control, not the DatePicker object
this.hdlChange = function () {
try {
//retrieve the object reference from the date control
var myThis = this.theThis;

var dateText = $(myThis.datePickerSelector, myThis.context).val();
if ($.trim(dateText).length > 0) {
var date = CLIENT.Utilities.parseDateString(dateText, $(myThis.datePickerSelector, myThis.context));
if (date) {
myThis.setDate(date);
} else {
$(myThis.datePickerSelector, myThis.context).val('');
myThis.clearDate();
}
} else {
myThis.clearDate();
}
} catch (e) {
CLIENT.Utilities.postWarnMessage('', 'DatePicker.js hdlChange()', e);
}
};

this.hdlDatePickerSelect = function (dateText, inst) {
try {
//retrieve the object reference from the date control
var myThis = this.theThis;

CLIENT.Utilities.clearValidationMessages($(myThis.datePickerSelector, myThis.context).parent());
if ($.trim(dateText).length > 0) {
var date = new Date(dateText);
myThis.setDate(date);
} else {
myThis.clearDate();
}
} catch (e) {
CLIENT.Utilities.postWarnMessage('', 'DatePicker.js hdlDatePickerSelect()', e);
}
};
};



Example HTML:



<div id="divGeneralJournal">
* * *
<label for="invoiceDateGeneralJournal">Invoice date</label>
<input type="text" id="invoiceDateGeneralJournal" value="" class="datefield" />

<input type="hidden" id="invoiceMonth" />
<input type="hidden" id="invoiceDay" />
<input type="hidden" id="invoiceYear" />
* * *
</div>



Initialization script would go something like this:



var invoiceDatePicker = new CLIENT.Utilities.DatePicker('#divGeneralJournal', 'GeneralJournal', 'invoice');
invoiceDatePicker.setupControls();

1 comment:

David Gorsline said...

I did a little bit of follow-up reading. It's probably safer and more portable to use jQuery's .data() to attach the property to the DOM element.