Dual loading issue in Javascript / jQuery / Microsoft-ajax hybrid
I have a weird problem with some JavaScript code (again, I hate debug JS code). I am working on a normal table that I populate from a JSON call and have added in support of some paging (like 2 pages, I think you could call it), sorting and row selection. Everything works nicely - BUT when the row is REFUSED (and canceled only), my add_navigate event is fired twice, resulting in a little data overload that is not needed - and a load indication that is no longer needed.
First, here's my JS code:
var customerType;
var selYear;
var selMonth;
var sdir;
var sort;
var page;
var noteId;
var hasDoneCall;
var customerId;
var customerIdChanged = false;
function initValues() {
customerType = "Publisher";
selYear = new Date().getFullYear();
selMonth = new Date().getMonth()+1;
sdir = false;
sort = "CustomerName";
page = 1;
noteId = false;
customerId = 0;
hasDoneCall = location.href.indexOf('#') > 0;
}
function flash(elm, color, duration) {
var current = elm.css('backgroundColor');
elm.animate({ backgroundColor: 'rgb(' + color + ')' }, duration / 2).animate({ backgroundColor: current }, duration / 2);
}
function createNotes(elm) {
var btn = jQuery(elm);
btn.attr('disabled', 'disabled');
bulkCreditOption('true', '', function(changeSet) {
var i = 0;
while (i < changeSet.length) {
var selector = "input[type=checkbox][value=" + changeSet[i] + "].check:checked";
var row = jQuery(selector).parent().parent();
var cell = row.find("td:nth-child(2)");
cell.html("<a href=\"javascript:showNotes('" + changeSet[i] + "')\">" + cell.html() + "</a>");
flash(row, '60, 130, 200', 500);
i++;
}
btn.removeAttr('disabled');
});
}
function deleteNotes(elm) {
var btn = jQuery(elm);
btn.attr('disabled', 'disabled');
bulkCreditOption('', 'true', function(changeSet) {
var i = 0;
while (i < changeSet.length) {
var selector = "input[type=checkbox][value=" + changeSet[i] + "].check:checked";
var row = jQuery(selector).parent().parent();
var cell = row.find("td:nth-child(2)");
cell.html(cell.text());
flash(row, '60, 130, 200', 500);
i++;
}
btn.removeAttr('disabled');
});
}
function bulkCreditOption(createNotes, deleteNotes, callback) {
var path = "/BulkCredit";
var data = "";
var checked = jQuery("input[type=checkbox].check:checked");
checked.each(function(chk) {
data += "&ids=" + urlencode(jQuery(this).val());
});
jQuery.ajax({
type: 'POST',
url: path,
dataType: 'json',
data: "createNotes=" + urlencode(createNotes) + data + "&deleteNotes=" + urlencode(deleteNotes),
success: function(msg) {
callback(msg);
}
});
}
initValues();
Sys.Application.add_init(function() {
Sys.Application.add_navigate(function(sender, e) {
var reinstate = e.get_state();
if (typeof (reinstate) != 'undefined' && typeof (reinstate.customerType) != 'undefined') {
customerType = reinstate.customerType;
selYear = reinstate.selYear;
selMonth = reinstate.selMonth;
sdir = reinstate.sdir;
sort = reinstate.sort;
page = reinstate.page;
noteId = reinstate.noteId;
customerId = reinstate.customerId;
} else {
initValues();
}
if (!customerIdChanged) {
jQuery("#customerTypeChanger").val(customerType);
jQuery("#customerFilter").val(customerId);
jQuery("#monthPicker").empty();
makeMonthPicker();
if (noteId != false && noteId != 'false') {
doShowNotes();
} else {
jQuery("#notesContent").hide();
jQuery("#tableContent").show();
doAjaxCall();
}
} else {
//logic to fetch customer specific stuff here, TODO
customerIdChanged = false;
}
});
Sys.Application.set_enableHistory(true);
jQuery(document).ready(function() {
origColor = jQuery("#dataTable > thead > tr > th").css('backgroundColor');
makeMonthPicker();
jQuery("#customerTypeChanger").val(customerType);
jQuery("#customerTypeChanger").change(function() {
customerType = jQuery(this).val();
iqSetHistory();
});
jQuery("#customerFilter").change(function() {
customerId = jQuery(this).val();
var tableBody = jQuery("#dataTable > tbody");
tableBody.find("tr").removeClass("selected");
tableBody.find("tr[rel=" + customerId + "]").addClass("selected");
customerIdChanged = true;
iqSetHistory();
});
jQuery(".checkAll").click(function() {
var elm = jQuery(this);
if (elm.is(':checked')) {
jQuery(".check").attr('checked', 'checked');
} else {
jQuery(".check").removeAttr('checked');
}
});
if (!hasDoneCall) {
if (noteId == false) {
doAjaxCall();
} else {
doShowNotes();
}
}
});
});
function makeMonthPicker() {
var selDate = new Date();
selDate.setFullYear(selYear);
selDate.setMonth(selMonth-1);
jQuery("#monthPicker").monthPicker(function(year, month) {
selYear = year;
selMonth = month;
iqSetHistory();
}, selDate);
}
var origColor;
var notesPath = "/ShowNotes";
function fadeOut(elm) {
elm.animate({ backgroundColor: 'rgb(180, 180, 180)' }, 250);
}
function fadeIn(elm) {
elm.animate({ backgroundColor: origColor }, 250);
}
function iqSetHistory() {
var state = { 'customerType': customerType, 'selYear': selYear, 'selMonth': selMonth, 'sdir': sdir, 'sort': sort, 'page': page, 'noteId': noteId, 'customerId':customerId };
Sys.Application.addHistoryPoint(state);
}
var ajaxPath = "/GetCreditListMonth";
function doAjaxCall() {
fadeOut(jQuery("#dataTable > thead > tr > th"));
jQuery.ajax({
type: "POST",
url: ajaxPath,
dataType: "json",
data: "month=" + selMonth + "&year=" + selYear + "&custType=" + customerType + "&sort=" + sort + "&sdir=" + sdir + "&page=" + page + "&asCsv=false",
success: function(msg) {
var table = jQuery("#dataTable");
var tableBody = table.find("tbody");
tableBody.empty();
var i = 0;
while (i < msg.Rows.length) {
var data = msg.Rows[i];
var row = jQuery("<tr rel=\"" + data.CustomerId + "\"></tr>");
if (data.CustomerId == customerId) {
row.addClass("selected");
}
if (i % 2 == 1) {
row.addClass("alternatetablerow");
}
var custName = data.CustomerName;
if (data.PaymentCreated) {
custName = "<a href=\"javascript:showNotes('" + getCreditId(data.CustomerId) + "')\">" + custName + "</a>";
}
row.append("<td><input type=\"checkbox\" class=\"check\" name=\"ids\" value=\"" + getCreditId(data.CustomerId) + "\" /></td>");
row.append("<td>" + custName + "</td>");
row.append("<td>" + data.AmountExcludingTaxes + "</td>");
row.append("<td>" + data.BonusAmount + "</td>");
row.append("<td>" + data.Amount + "</td>");
row.appendTo(tableBody);
i++;
}
tableBody.find("input, a").click(function(event){ //Stop clicks from falling through to the table row event
event.stopPropagation();
return true;
});
tableBody.find("tr").click(function(event){
var row = jQuery(this);
if (row.hasClass("selected")) { //Deselect
jQuery("#customerFilter").val(0);
} else {
jQuery("#customerFilter").val(jQuery(this).attr('rel'));
}
jQuery("#customerFilter").triggerHandler("change");
});
createPager(msg.Pages, jQuery("#pager"));
jQuery(".checkAll").triggerHandler('click');
fadeIn(table.find('thead > tr > th'));
}
});
}
function downloadListAsCsv() {
window.location.href = ajaxPath + "?month=" + selMonth + "&year=" + selYear + "&custType=" + customerType + "&sort=" + sort + "&sdir=" + sdir + "&page=0&asCsv=true";
}
function doShowNotes(){
jQuery.ajax({
type: "GET",
url: notesPath + "/" + noteId,
success: function(msg) {
jQuery("#tableContent").hide();
jQuery("#notesContent").html(msg).show();
}
});
}
function showNotes(id) {
noteId = id;
iqSetHistory();
}
function showTable() {
noteId = false;
iqSetHistory();
}
function getCreditId(custId) {
return selYear + "-" + selMonth + "-" + custId;
}
function sortDataTable(col) {
if (col == sort) {
sdir = !sdir;
} else {
sdir = false;
}
page = 1
sort = col;
iqSetHistory();
}
function createPager(totalPages, elm) {
elm.empty();
if (totalPages > 1)
{
var builder = "";
var numDirections = 2;
if (page > 1)
{
if (page - numDirections - 1 > 0)
{
builder += CreatePageLinkStatic(1, "«");
builder += " ";
}
builder += CreatePageLinkStatic(page - 1, "<");
builder += " ";
}
var n = page - numDirections;
while (n < page)
{
if (n > 0)
{
builder += CreatePageLinkStatic(n, n);
builder += " ";
}
n++;
}
builder += page;
builder += " ";
n = page + 1;
while (n <= page + numDirections && n <= totalPages)
{
builder += CreatePageLinkStatic(n, n);
builder +=" ";
n++;
}
if (page < totalPages)
{
builder += CreatePageLinkStatic(page + 1, ">");
builder += " ";
if (page + numDirections < totalPages)
{
builder += CreatePageLinkStatic(totalPages, "»");
}
}
builder;
elm.append(builder);
}
}
function CreatePageLinkStatic(page, str){
return "<a href=\"javascript:pageDataTable(" + page + ")\">" + str + "</a>";
}
function pageDataTable(newPage){
page = newPage;
iqSetHistory();
}
And the markup:
<div id="tableContent">
<select id="customerTypeChanger">
<option selected="selected" value="Publisher">Publisher</option>
<option value="Advertiser">Advertiser</option>
</select>
<select id="customerFilter"><option value="0">Choose Customer</option><option value="1">Customer 1</option><option value="1">Customer 2</option>...</select>
<div id="monthPicker"></div>
<div><a href="javascript:downloadListAsCsv()">DownloadAsCSV</a></div>
<table id="dataTable" class="grid">
<thead>
<tr>
<th style="text-align: left"><input type="checkbox" name="toggleCheckBox" class="checkAll" value="dummy" /></th>
<th><a href="javascript:sortDataTable('CustomerName')">Customer name</a></th>
<th><a href="javascript:sortDataTable('AmountExcludingTaxes')">Amount</th>
<th><a href="javascript:sortDataTable('BonusAmount')">Bonus amount</a></th>
<th><a href="javascript:sortDataTable('Amount')">Amount including VAT</a></th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="pagination" id="pager"></div>
<div>With the selected rows</div>
<input id="createNotes" type="button" value="Create notes" onclick="javascript:createNotes(this)" /> <input id="deleteNotes" value="Delete notes" type="submit" onclick="javascript:deleteNotes(this)" />
</div>
<div id="notesContent"></div>
If needed, here's the code I did for monthpicker (it's a very simple datepicker function that just lets you navigate between months and gives a result like
<April 2009 May 2009 June 2009>
(where bold are links allowing you to see only that month period and italic is already selected, obviously the actual html markup is different)
It uses datepicker from jQuery UI to get localized month names
(function($) {
var selDate;
$.fn.monthPicker = function(callback, selectedDate) {
selDate = selectedDate;
var elm = this;
this.html("<span class=\"prevMonthButton\"><a href=\"\"><</a></span><span class=\"prevMonth\"><a href=\"\"></a></span><span class=\"curMonth\"></span><span class=\"nextMonth\"><a href=\"\"></a></span><span class=\"nextMonthButton\"><a href=\"\">></a></span>");
populateDates(this);
var prevMonthFunc = function() {
var month = selDate.getMonth() - 1;
if (month < 0) {
month = 11;
selDate.setFullYear(selDate.getFullYear() - 1);
}
selDate.setMonth(month);
populateDates(elm);
callback(selDate.getFullYear(), selDate.getMonth() + 1);
return false;
}
var nextMonthFunc = function() {
var month = selDate.getMonth() + 1;
if (month > 11) {
month = 0;
selDate.setFullYear(selDate.getFullYear() + 1);
}
selDate.setMonth(month);
populateDates(elm);
callback(selDate.getFullYear(), selDate.getMonth() + 1);
return false;
};
this.find(".prevMonth > a").click(prevMonthFunc);
this.find(".prevMonthButton > a").click(prevMonthFunc);
this.find(".nextMonth > a").click(nextMonthFunc);
this.find(".nextMonthButton > a").click(nextMonthFunc);
}
function populateDates(elm) {
var months = jQuery.datepicker._defaults.monthNames;
var selYear = selDate.getFullYear();
var selMonth = selDate.getMonth();
elm.find(".curMonth").text(months[selMonth] + " " + selYear);
var prevMonth = selMonth - 1;
var prevYear = selYear;
if (prevMonth < 0) {
prevMonth = 11;
prevYear = prevYear - 1;
}
elm.find(".prevMonth > a").text(months[prevMonth] + " " + prevYear);
var nextMonth = selMonth + 1;
var nextYear = selYear;
if (nextMonth > 11) {
nextMonth = 0;
nextYear = nextYear + 1;
}
elm.find(".nextMonth > a").text(months[nextMonth] + " " + nextYear);
}
})(jQuery);
I know most of this JavaScript sucks, but for the main part it seems to work pretty well, but as I said, click on a row to select it, then click on it to deselect and execute an arrow double click add_navigate results in an extra call to my JSON service and visual flicker on the client side - and I can't figure out why this is happening (and even weirder why it happens when it's canceled and not on the selected one).
a source to share
I think the problem is the double binding of events to the same element unconsciously.
This is usually a scenario,
(function($){
var MyDocument = new Object({
prepareBody : function(){
//addClick Event
$('div#updatedElement').click(MyDocument.ajaxCall());
//adjusting the height of an updatedElement to an-otherElement
$('div#updatedElement').css('height', $('div#otherElement').height());
},
ajaxCall : function(){
//do your ajax Call
$.getJSON('index.php',{param:1, param:2},function(response){
//do something with your response
$('div#updatedElement').html(response)
//say if after your call you decide to update the body again
MyDocument.prepareBody();
//what that does is you will double bind click to the updateElement div.
//The next time that it is click, the AjaxCall function will run twice
//The next time it is clicked the MyDocument.ajaxCall function will be run four times
//8 - 16 - 32 and by now, firefox would have crashed!
},'json');
}
});
$(document).ready(function(){
MyDocument.prepareBody()
});
})(jQuery);
As KClough advises, cancel events before binding them so that they only fire once! Sad jQuery doesn't overwrite them like other frameworks!
Hope this helps someone else.
a source to share
Oh, sorry I completely forgot about this question.
I changed some of my code a bit after that and the problem went away. I'm not entirely sure what caused this, seeing as it still surprised me as I only ever seemingly got double the load twice.
Anyhoow, the problem is solved, only I do not know how and why, which bothers me a little, but on the other - it works.
a source to share