AngularJS fixed header scrollable table directive
This post contains a custom AngularJS directive you can use to give your html table a fixed header and footer with a scrollable body, we developed it for a law firm marketing website recently, it uses a pure CSS approach and doesn't touch any of the html tags, leaving the html table completely intact and happily semantic :)
Creating a fixed header scrollable table using purely CSS turns out to be a surprisingly tricky thing to do, in an ideal world I thought it would just be a matter of setting the height of the table body and "overflow:hidden", but there turns out to be a bit more to it than that, especially if you have a table that contains dynamic content because the width of each column in the thead and tbody need to be set in order for it to continue looking like a table and not just a big mess.
In a nutshell the CSS changes that need to happen are:
- Set the width of each column in the thead and tbody, making sure they match up so the columns aren't wonky
- Set the thead and tbody to "display:block;"
- Set the tbody height and "overflow:auto;" to add the scrollbar
- When there's a scrollbar (when the tbody content overflows it's height), reduce the width of the final column in the tbody by the width of the scrollbar
Here's the solution I came up with:
- Install using NPM:
npm install angu-fixed-header-table
- Install using Bower:
bower install angu-fixed-header-table
- See on Plunker at http://plnkr.co/edit/YIohJ7?p=preview)
- Available on GitHub at https://github.com/cornflourblue/angu-fixed-header-table
The fixedHeader AngularJS directive
/** * AngularJS fixed header scrollable table directive * @author Jason Watmore <[email protected]> (http://jasonwatmore.com) * @version 1.2.0 */ (function () { angular .module('anguFixedHeaderTable', []) .directive('fixedHeader', fixedHeader); fixedHeader.$inject = ['$timeout']; function fixedHeader($timeout) { return { restrict: 'A', link: link }; function link($scope, $elem, $attrs, $ctrl) { var elem = $elem[0]; // wait for data to load and then transform the table $scope.$watch(tableDataLoaded, function(isTableDataLoaded) { if (isTableDataLoaded) { transformTable(); } }); function tableDataLoaded() { // first cell in the tbody exists when data is loaded but doesn't have a width // until after the table is transformed var firstCell = elem.querySelector('tbody tr:first-child td:first-child'); return firstCell && !firstCell.style.width; } function transformTable() { // reset display styles so column widths are correct when measured below angular.element(elem.querySelectorAll('thead, tbody, tfoot')).css('display', ''); // wrap in $timeout to give table a chance to finish rendering $timeout(function () { // set widths of columns angular.forEach(elem.querySelectorAll('tr:first-child th'), function (thElem, i) { var tdElems = elem.querySelector('tbody tr:first-child td:nth-child(' + (i + 1) + ')'); var tfElems = elem.querySelector('tfoot tr:first-child td:nth-child(' + (i + 1) + ')'); var columnWidth = tdElems ? tdElems.offsetWidth : thElem.offsetWidth; if (tdElems) { tdElems.style.width = columnWidth + 'px'; } if (thElem) { thElem.style.width = columnWidth + 'px'; } if (tfElems) { tfElems.style.width = columnWidth + 'px'; } }); // set css styles on thead and tbody angular.element(elem.querySelectorAll('thead, tfoot')).css('display', 'block'); angular.element(elem.querySelectorAll('tbody')).css({ 'display': 'block', 'height': $attrs.tableHeight || 'inherit', 'overflow': 'auto' }); // reduce width of last column by width of scrollbar var tbody = elem.querySelector('tbody'); var scrollBarWidth = tbody.offsetWidth - tbody.clientWidth; if (scrollBarWidth > 0) { // for some reason trimming the width by 2px lines everything up better scrollBarWidth -= 2; var lastColumn = elem.querySelector('tbody tr:first-child td:last-child'); lastColumn.style.width = (lastColumn.offsetWidth - scrollBarWidth) + 'px'; } }); } } } })();
Sample HTML that uses the fixed-header directive
<table class="table table-bordered" fixed-header> <thead> <tr> <th>Header 1</th> <th>Header 2</th> <th>Header 3</th> <th>Header 4</th> </tr> </thead> <tbody> <tr ng-repeat="item in [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]"> <td>Row {{item}} Col 1</td> <td>Row {{item}} Col 2</td> <td>Row {{item}} Col 3</td> <td>Row {{item}} Col 4</td> </tr> </tbody> <tfoot> <tr> <td>Footer 1</td> <td>Footer 2</td> <td>Footer 3</td> <td>Footer 4</td> </tr> </tfoot> </table>
The default height of the table body is 400px, to change this add a table-height attribute to the table element eg: table-height="500px".
UPDATE 08/10/2014: Added support for fixed footer (tfoot) element.