March 22nd, 2011
8:11 pm
Fixing Primefaces scrolling table header width issues using jQuery

Posted under JSF
Tags , , , , , , , ,

This has been very much an ongoing chestnut with Primefaces, mainly due to the tricks you have to use in order to get fixed table column headers and a scrolling table. Primefaces does this with a pair of tables – one for the header row and another for the data. The problems which often arise are mismatching columns between the header and the data, incorrectly sized table header above the column headers, and an incorrectly sized header row which does not appear to size properly dynamically when scrollbars are present/absent.

These issues can often be fixed, but it does appear that there is no overall silver bullet which makes everything work in every use case. The best approach I have found is to have a selection of approaches to try, and to apply the appropriate ones to solve problems which arise in particular use cases. The following details the different approaches I have used in various cases to solve width and alignment problems.

The following commented code sample shows how to use jQuery to correct table header width and column header width problems for scrolling tables. These problems are typically triggered by the dynamic appearance/removal of scrollbars, which will depend on table height and row count. Note that the function described, ss_FixTableHeaderWidth, would either be called in an inline script block placed immediately after a table definition in a facelets page (for a static table) or would be triggered as part of an Ajax call (for a dynamically rendered Ajax table). The example shows the static case. Note the following points about the code:-

  • The code may be found in the repository here.
  • The jQuery selectors should be optimised for performance. In general, ID selectors are the most efficient, so  narrow down with an ID selector first if you can (as below). Element selectors are also efficient, but classes can be really slow depending on what the browser supports – jQuery may end up doing a full DOM tree scan for every class selector, which is highly undesirable!. To avoid this below, I have prefixed class selectors with an element type which then makes use of the faster element searching, e.g. div.ui-scrollable-datatable.
  • Avoid duplicating selector searches where possible – in the example below a reuse them where I can, by using previous selector results as the context for subsequent lower level selectors, for example jQuery(‘div.ui-scrollable-datatable-container’, mainDiv) makes use of the jQuery result mainDiv from a previous selector when selecting its descendant scrollable container div.
  • You need to keep track of the difference between DOM elements returned by some calls, and jQuery objects which can be returned by others. You can always wrap a DOM element with a jQuery object using jQuery(element), but this is not always necessary. Avoid the overhead of doing it when you already have a jQuery object as it is redundant. The jQuery documentation makes all this clear.
  • The jQuery online docs are available here.  You cannot directly download the docs from the jQuery site, but it does link to some unofficial PDF downloads. However, the best and most searchable option seems to be an unofficial windows help file/CHM version download linked from StackOverflow (not fully up to date but still very useful). This is available here.
  • The RH header column size is set to auto to accommodate header row size changes, the idea being that this RH column will expand to fill the row, and the browser will not mess with other column sizes. Note that the column widths are just a guide to the browser and it is free to mess with them. This can be really awkward as we rely on the header table and data table being rendered exactly the same way to line up. This auto approach seems to work.
  • Firefox (3.6 and 4) has a problem whereby when setting an element width (in this case with jQuery), the actual value set is often 1px less than the requested value (particularly a problem when setting header table width). This can easily be seen by reading back the element width immediately after setting it. The fix I use for this is to do all width setting in a function (ss_setWidth) which checks the width afterwards and calculates the value of the mismatch. If there is a mismatch, the mismatch is then added to the width to set, and the element width is set a second time. This function is safe to use for all browsers.

 

Function definition, placed in page header section

    <script type=”text/javascript”>
    //<![CDATA[

        function ss_FixTableHeaderWidth(formId, tableId) {
           
            /*
             *Fix incorrect table header width due to scrollbars
             */
           
            var fullTableId = ‘#’ + formId + ‘\\3A’ + tableId;
            var topHeader = jQuery(fullTableId + ‘ > div.ui-datatable-header’);
            var mainDiv = jQuery(fullTableId + ‘ > div.ui-scrollable-datatable’);
            var headerTable = jQuery(‘table.ui-scrollable-datatable-header’, mainDiv);
            var dataContainer = jQuery(‘div.ui-scrollable-datatable-container’, mainDiv);           
            var dataTable = jQuery(‘table.ui-scrollable-datatable-body’, dataContainer);           
            var lastColumnHeader = jQuery(‘thead > tr:eq(0) > th:last’, headerTable);

/*           
            alert (‘TableId=’+fullTableId + ‘\n’ +
                   ‘topHeader=’+topHeader.attr(‘class’) + ‘\n’ +
                   ‘mainDiv=’+mainDiv.attr(‘class’) + ‘\n’ +
                   ‘headerTable=’+headerTable.attr(‘class’) + ‘\n’ +
                   ‘dataContainer=’+dataContainer.attr(‘class’) + ‘\n’ +
                   ‘dataTable=’+dataTable.attr(‘class’) + ‘\n’ +
                   ‘lastColumnHeader=’+topHeader.attr(‘class’));
*/           
            /*
             * Set the RH header column width to auto to prevent header table width adjustments
             *  from messing up column alignments
             */
            lastColumnHeader.css(‘width’, ‘auto’);

            /*
             * For some themes (pepper-grinder, overcast, ui-lightness), the width is incorrectly set for a scroll bar
             * even when there isn’t one. Detect whether scroll bars are actually present and fix the width if required.
             */
            if (dataContainer.outerHeight() >= dataContainer.get(0).scrollHeight) {
                //alert(‘Scroll bars not present’);
                //dataContainer.width(dataTable.outerWidth());
                ss_setWidth(dataContainer, dataTable.outerWidth());
            }       

            //Now set the width of the column headers table correctly and then finally the table header to match.
            var newHeaderTableWidth = headerTable.width() + dataContainer.outerWidth() – headerTable.outerWidth();
            var newTopHeaderWidth = topHeader.width() + dataContainer.outerWidth() – topHeader.outerWidth();

            //headerTable.width(newHeaderTableWidth);
            ss_setWidth(headerTable, newHeaderTableWidth);
            //topHeader.width(newTopHeaderWidth);
            ss_setWidth(topHeader, newTopHeaderWidth);
        }
       
        function ss_setWidth(element, value) {
           /*
            * This function overcomes an issue with Firefox (3.6 and 4), whereby the actual width set for an element
            * (particularly the header table) is sometimes less (typically 1px less) than the passed width to be set.
            * We read back the width after setting it, and if it differs from what we set,
            * we adjust our original value by the mismatch and set the width again.
            */           
            element.width(value);
            mismatch = value – element.width();
            if (mismatch != 0) {
                //alert(‘mismatch=’+mismatch+’ fixed’)
                element.width(value + mismatch);
            }
        }       
    //]]>
    </script>

Function call, placed inline immediately following table

<p:dataTable …>

</p:dataTable>
<script type=”text/javascript”>ss_FixTableHeaderWidth(‘frmTest’, ‘tblTable’);</script>

Iterating columns to set the width for each one individually

The following code sample uses the jQuery .each loop to iterate the header columns and set the width of each one to its matching data column (along with setting the table/row widths correctly). This sounds ideal for solving the header problems, but in practice it did not work well. The main issue is that jQuery reported a different value for the header column widths to that reported by the browser (via Firebug). The values were out by 1px, which caused an incremental mismatch in the column alignment as you go left to right. This approach was therefore abandoned in favour of just setting the RH header column size to auto to accommodate row size changes, as explained above. However, I reproduce the code here as it is a useful example of how to iterate a returned  jQuery object collection from a selector.

Function definition, column iteration version

    <script type=”text/javascript”>
    //<![CDATA[

        function ss_FixTableHeaderWidth(formId, tableId) {

            /*
             *Fix incorrect table header width due to scrollbars
             */
            
            var fullTableId = ‘#’ + formId + ‘\\3A’ + tableId;
            var topHeader = jQuery(fullTableId + ‘ > div.ui-datatable-header’);
            var mainDiv = jQuery(fullTableId + ‘ > div.ui-scrollable-datatable’);
            var headerTable = jQuery(‘table.ui-scrollable-datatable-header’, mainDiv);
            var dataContainer = jQuery(‘div.ui-scrollable-datatable-container’, mainDiv);           
            var dataTable = jQuery(‘table.ui-scrollable-datatable-body’, dataContainer);           
           
            /*
             * For some themes (pepper-grinder, overcast, ui-lightness), the width is incorrectly set for a scroll bar
             * even when there isn’t one. Detect whether scroll bars are actually present and fix the width if required.
             */
            if (dataContainer.outerHeight() >= dataContainer.get(0).scrollHeight) {
                //alert(‘Scroll bars not present’);
                //dataContainer.width(dataTable.outerWidth());
                ss_setWidth(dataContainer, dataTable.outerWidth());
            }                       

            /*
             * Now iterate the columns with a jQuery each loop and set the header column width
             * to be the same as its corresponding data column width.
             * This is for illustration – the idea has been abandoned, as I found that Javascript/jQuery
             * reports a different value for the header widths to firebug on the final rendered page (out by 1px).
             * This causes an increasing mismatch between data columns and header columns as you go from left to right.
             * The column widths are only a suggestion and the browser may modify them anyway.
             * Therefore the final version, in SortTable.xhtml just sets the RH column width to auto
             * to take up the slack when the row width is changed, and leaves the others alone.
             * This appears to work much better.
             * However the iteration technique here is a useful working jQuery example.
             */
            var columnHeaders = jQuery(‘thead > tr:eq(0) > th’, headerTable);
            var firstRowCells = jQuery(‘tbody > tr:eq(0) > td’, dataTable);
            var columnCount = columnHeaders.size()
           
            columnHeaders.each(function(idx){
                    var columnHeader = jQuery(this);
                    if (idx < columnCount – 1) {
                       
                        var firstRowCell = jQuery(firstRowCells.get(idx));
/*
                        alert(‘idx=’+idx+’\n’+
                              ‘header width=’+columnHeader.width()+’\n’ +
                              ‘header csswidth=’+columnHeader.css(‘width’)+’\n’ +
                              ‘header innerWidth=’+columnHeader.innerWidth()+’\n’ +
                              ‘header outerWidth=’+columnHeader.outerWidth()+’\n’ +
                              ‘data width=’+firstRowCell.width()+’\n’ +
                              ‘data csswidth=’+firstRowCell.css(‘width’)+’\n’ +
                              ‘data innerWidth=’+firstRowCell.innerWidth()+’\n’ +
                              ‘data outerWidth=’+firstRowCell.outerWidth());
*/
                        var newWidth = columnHeader.width() + firstRowCell.outerWidth() – columnHeader.outerWidth();
                        //alert(‘idx=’+idx+’,width=’+columnHeader.width()+’, newWidth=’+newWidth);
                        //columnHeader.width(newWidth);
                        ss_setWidth(columnHeader, newWidth);
                    }
                    else {
                        columnHeader.css(‘width’, ‘auto’);
                    }
              });

            //Now set the width of the column headers table correctly and then finally the table header to match.
            var newHeaderTableWidth = headerTable.width() + dataContainer.outerWidth() – headerTable.outerWidth();
            var newTopHeaderWidth = topHeader.width() + dataContainer.outerWidth() – topHeader.outerWidth();

            //headerTable.width(newHeaderTableWidth);
            ss_setWidth(headerTable, newHeaderTableWidth);

            //topHeader.width(newTopHeaderWidth);
            ss_setWidth(topHeader, newTopHeaderWidth);
        }
       
        function ss_setWidth(element, value) {
            /*
             * This function overcomes an issue with Firefox (3.6 and 4), whereby the actual width set for an element
             * (particularly the header table) is sometimes less (typically 1px less) than the passed width to be set.
             * We read back the width after setting it, and if it differs from what we set,
             * we adjust our original value by the mismatch and set the width again.
             */                       
            element.width(value);
            mismatch = value – element.width();
            if (mismatch != 0) {
                //alert(‘mismatch=’+mismatch+’ fixed’)
                element.width(value + mismatch);
            }
        }       
    //]]>
    </script>

No Comments »

Trackback URI | Comments RSS

Leave a Reply

You must be logged in to post a comment.