Demonstration of jTable with scrolling body

This was an interesting question raised on the jtable discussion forum which intrigued me. The kernel of the solution was found on jsFiddle but it didn't apply directly to jTable, which uses ajax to update the table dynamically. So here is my take on a solution. As usual with these tutorials we'll start with standard jTable demonstration. Firstly an HTML element to contain the table.

Standard demonstration jTable html
<div id="PeopleTableContainer" class="demo-jtable-div"></div>
	
Standard demonstration jTable definition PLUS recordsLoaded event handler
		    //Prepare jTable
			var PeopleJtable = {
				title: 'Demonstration of standard example',
				jqueryuiTheme: true ,
				actions: {
					listAction: 'PersonActionsACE.php?action=list',
					createAction: 'PersonActionsACE.php?action=create',
					updateAction: 'PersonActionsACE.php?action=update',
					deleteAction: 'PersonActionsACE.php?action=delete'
				},
				fields: {
					PersonId: {
						key: true,
						create: false,
						edit: false,
						list: false
					},
					Name: {
						title: 'Author&nbsp;Name',
						width: '55%'
					},
					Age: {
						title: 'Age',
						width: '10%'
					},
					RecordDate: {
						title: 'Record&nbsp;date',
						width: '30%',
						type: 'date',
						create: false,
						edit: false
					}
				},
				recordsLoaded: function(event, data) { 
					$(this).resizeColumns();
				}
			};
			$('#PeopleTableContainer').jtable(PeopleJtable).jtable('load');	

A standard jtable definition, with a one line recordsLoaded event handler. The handler just calls our new columnResize function described below, every time a set of data is loaded. Note also the use of &nbsp; (non-breaking space) entities in the column titles. This prevents word wrapping after the resizing.

Splitting the jTable head from the body

Next we need some css.

Some housekeeping and killer css
div.jtable-main-container table.jtable tbody > tr > td {
    padding: 5px 3px 5px 6px;
}

th.jtable-command-column-header {
	min-width:18px;
}

table.jtable thead tr:first-child th:last-child {
	border-right:18px solid #f0f0f0;
}

table.jtable tbody,
table.jtable thead { display: block; }

table.jtable tbody {
    height: 100px;
    overflow-y: auto;
    overflow-x: hidden;
}

.demo-jtable-div {
	max-width: 600px;
	margin-left:auto;
	margin-right:auto;
}
	

Ln 1. This is housekeeping. The standard jtable css sets the th padding at left:5 and right:5 while the td padding is left:3 and right:6 so a difference of just one pixel per column. Rather than change the heading which has the drag handles etc, we change the td padding to match. It is quite surprising what a difference this little change makes.

Ln 5. This styling sets the two command column headings the same width that the data columns will be when they have their edit and delete icons. This is a change we are going to adopt as a standard, as it prevents the column headings jumping about quite as much when the data records are loaded for the first time.

Ln. 9. This styling sets the border of the last heading column to be about the same width as the vertical scroll bar. We've tried to pick an RGB that is about the same colour as the vertical scroll, at least in Firefox, which we use.

Ln. 13. This styling destroys the normal HTML table behaviour, it breaks the link between table-head and table-body, which will have to re-implement ourselves.

Ln. 16. This styling make the table body limited in height and scrollable.

Ln. 22. This styling centres the table on the page and make it resize with the window

Now we need the script which is called from the recordsLoaded handler, that actually resizes the table body to match the table head. We've made it a jQuery function so that we can apply it to a jQuery object.

jQuery column resize functions
		// jQuery function to resize tbody widths based on width of thead
		jQuery.fn.resizeColumns = function () {
			var $bodyCells = $(this).find('thead tr:first').children();
			var colWidth = $bodyCells.map(function() {
				return $(this).width();
			}).get();

			// Set the width of thead columns
			$(this).find('tbody tr:first').children().each(function(i, v) {
				$(v).width(colWidth[i]);
			});
			return this;	// for jQuery chaining
		}

		// Bind window resize handler
		$(window).resize(function() {
			$('#PeopleTableContainer').resizeColumns();
		})
	

Ln. 2. Onwards creates a resizing function as a jQuery function.

Lns. 3-6 Finds the first row of the table heading and grabs the column widths.

Lns. 9-11 Finds the first row of the table body and sets the column widths to match the headings.

Setting the width of an element sets the width of the inside content. The actual width has the padding, border and margin added. This explains why we had to align the padding of the table head and the table body in the CSS.

Lns. 14-17 Binds an event handler to the browser window, to recompute the widths on resizing.

So now below we have an almost working demonstration.

Traditionally using a normal HTML table, which jTable generates for us, the column widths are for guidance only. The browser uses column widths and column contents to construct the table. See mozilla explantion for more information. During contruction jTable makes the DOM elements of the table and populates the table head with column titles etc, and a single full span body row. The browser only has the column width guidelines and the heading contents with which to layout the table. Normally when jTable('load') gets data from the servers and populates the table body with the data rows, the browser would re-layout the table, but by splitting the table head from the table body to make the body scrollable, we are preventing the layout algorithm from balancing the table. Therefore it is even more important than ever to get the initial jTable field width values suitable for the data.

We have our table with scrolling body, with table heading columns the same size as the table data columns, and it resizes as the window changes the size of the table container. jTable defaults columnResizable and columnSelectable options to true. This is the case in our demonstration above. Try dragging a heading column, or right click the heading bar and hide/show a column on the demonstration jTable above. It's broken.

Resizing columns after change of visibility or dragging.

Because the table is in two parts, only the table head gets rebalanced by the browser after column width dragging or column hiding/showing. Sadly there are no jTable events relating to column changes for us to handle. However we can spy on some of jTables internals, and address both these matter. Very satisfactorily in the case of column visibility, less so for column resizing.

During construction, jTable creates, among other things, two elements inside the jtable-container, which we can exploit.

  1. A <div class='jtable-column-selection-container' /> containing the non-fixed column titles with checkboxes for hiding/showing.
  2. Each th element of the thead tr is contructed thus.
    th.jtable-column-header
    div.jtable-column-header-container
    span.jtable-column-header-textColumn Title
    div.jtable-column-resize-handlerHandle to be dragged for resizing, not the handler of the event

Let's create a new instance of the same jtable.

Standard demonstration jTable html
<div id="PeopleTableContainer2" class="demo-jtable-div"></div>
	
Extended jTable - title only
		    //Prepare jTable
			var PeopleJtable2 = $.extend(true,{},PeopleJtable,{
				title: 'Scrolling table + column select & drag'
			});
			$('#PeopleTableContainer2').jtable(PeopleJtable2).jtable('load');
	

Resizing after column visibilty change

The <div class='jtable-column-selection-container' /> is dynamically populated on popup with a list of columns with visibily checkboxes. Because the checkboxes trigger a change event when any of them are changed, we simply use jQuery to bind a change event handler to the container. jTable will handle the event on the individual colum first, and show/hide that colunn (both title and data). The event will then bubble up to the container, and trigger our event handler. We are not concerned with which column has changed, only the overall layout of the table head, so we just execute our resize function.

jQuery event handler to resize columns on visibilty change
			/*
			** Make columns resize after column visibilty change
			*/
			$('#PeopleTableContainer2 .jtable-column-selection-container').change(function (event) {
				$('#PeopleTableContainer2').resizeColumns();
			});
	

Resizing after column width drag

This solution is not so elegant, and requires a good understanding of jQuery events in general, and mouseup and mousemove events in particular. Without a long explanation of the reasoning, here are the general guidelines for implementing a drag type operation without using jQuery draggables.

  1. Bind a mousedown event handler to the element that is to be dragged.
  2. Inside the mousedown event handler, apply both mousemove and mouseup event handlers. These can be computationally demanding so are only bound during individual drag operations. As the mouse can be dragged over elements, other than the one with the bound mousedown event, it is normal to bind these two handlers to an element high up the DOM. In this case jTable binds the events to the document itself
  3. Inside the mouseup event handler, the final action of the drag can be applied, and then both handlers unbound from the document.

It is also worth mentioning that using jQuery we can be sure that on each element, event handlers are executed in the order in which they are bound. Sometimes event bubbling and event delegation may make this appear not to be so, but analysis will show they are.

The crux of our problem is to get a mouseup handler bound AFTER the jTable mouseup handler. To do that, ideally we need to execute our own mousedown handler, immediately after the jTable mousedown handler. Unfortunatley for us the jTable mousedown handler calls the jQuery method event.stopPropagation() , which means that even if we bind our own mousedown handler, it will not be triggered.

We warned that the solution was not elegant. We are going to use a hack explained by Robee Shepherd to reorder the jQuery events on each of the div.jtable-column-resize-handler elements. This places our mousedown handler BEFORE the jTable one. Any mouseup event handler we apply within it will be triggered BEFORE the jTable one, that resizes the column.

Rather than bind the event handler that actually performs the resizing we bind an interim handler to the body. When the mouseup event is triggered, it will bubble up the DOM from wherever it was triggered, until it reaches the body, at which point our interim handler will execute. This will still be BEFORE the jTable mouseup handler, but at a time when the jTable mouseup handler is bound to the document, so we bind our real mouseup handler, to the document, remove our interim handler from the body, then let the mouseup event bubble up to the document. When it reaches the document the jTable mouseup handler will resize the table head, then our real mouseup handler will resize the table body.

jQuery event handler to resize columns on column drag
		/*
		** Handlers and hacks to make columns resize after column dragging
		*/			
		$('#PeopleTableContainer2 .jtable-column-resize-handler').mousedown(function () {
			// only thing to do is bind our mouseup handler
			$('body').mouseup(interimColumnResizeMouseupHandler);
		});
		// even though our mousedown handler is now in the jQuery eventList, after the jTable handler,
		// it will not execute because of stopPropogation.  So we hack the eventList to put our handler
		// infront of the jTable handler

		$('#PeopleTableContainer2 .jtable-column-resize-handler').each(function () {
			var eventList = $._data($(this)[0], "events");
			eventList.mousedown.unshift(eventList.mousedown.pop());
		});
		
		function interimColumnResizeMouseupHandler(event) {
			// by now the jTable mouseup handler is in place, so add our actual handler  behind it,
			// and renove ourself
			$(document).bind('mouseup',realColumnResizeMouseupHandler)
			$('body').unbind('mouseup',interimColumnResizeMouseupHandler);
		}
		
		function realColumnResizeMouseupHandler (event) {
			// now that jTable has dragged the column width in the thead, resize the tbody,
			// and remove ourself
			$('#PeopleTableContainer2').resizeColumns();
			$(document).unbind('mouseup',realColumnResizeMouseupHandler)
		}
	

So now we have a fully working scrollable jTable. It adpats to window resizing, column visibily changes, and column width drags. Try dragging a column width, and move the mouse outside the table somewhere.

Conclusion

It is always satisfying to solve a problem. Splitting the table to make it scroll and the actual data column resizing work well, The column visibilty solution is simple and robust. The solution for column dragging, seems to work, that is the most positve thing we'd say about it. It uses a jQuery private function to manipulate internal data, which could easily break with new releases of jQuery. The whole event handler juggling has a fragility about it, that may crumble on more complex pages. Should a paying client ever require scrolling tables, we'd firstly suggest avoiding draggable column widths. If not we'd think about extending jTable to provide a proper columnsChanged callback.

You've made it this far, we hope you've learnt something. We are available to offer local tutoring on a range of technologies, or to build you a website, standard or non-standard. The less standard the better! Contact us at tutor@swaleinternet.co.uk