<!DOCTYPE html>
<html>

<head>
  <script data-require="angular.js@1.5.6" data-semver="1.5.6" src="https://code.angularjs.org/1.5.6/angular.min.js"></script>
  <link rel="stylesheet" href="style.css" />
  <script src="app.module.js"></script>
  <script src="app.controller.js"></script>
  <script src="fixedColumnTable.directive.js"></script>
</head>

<body ng-app="app" ng-controller="AppCtrl as vm">
  <h1>Fixed Header / Columns Directive</h1>

  <div class="container" fixed-column-table fixed-columns="3">
    <table>
      <thead>
        <tr>
          <th>Id</th>
          <th>Make</th>
          <th>Model</th>
          <th>Engine</th>
          <th>Power</th>
          <th>Transmission</th>
          <th>CO2</th>
          <th>RRP</th>
          <th>OTR</th>
        </tr>
      </thead>
      <tbody>
        <tr ng-repeat="car in vm.cars">
          <td>{{car.id}}</td>
          <td>{{car.make}}</td>
          <td>{{car.model}}</td>
          <td>{{car.engine}}</td>
          <td>{{car.power}}</td>
          <td>{{car.transmission}}</td>
          <td>{{car.co2}}</td>
          <td>{{car.rrp}}</td>
          <td>{{car.otr}}</td>
        </tr>
      </tbody>
    </table>
  </div>
  
  <p>
    This AngularJS directive was created from Vanilla JS and CSS found in the article  
    <a href="http://shuheikagawa.com/blog/2016/01/11/freeze-panes-with-css-and-a-bit-of-javascript" target="_new">Freeze panes with CSS and a bit of JavaScript</a>
  </p>
</body>
</html>
// Code goes here

body {
  background: #ccc;
}

.container {
  width: 500px;
  height: 200px;
  border: 2px solid #444444;
  overflow: scroll;
  position: relative;
}

table {
  table-layout: fixed;
  border-collapse: separate;
  border-spacing: 0;
  position: absolute;
  width: 100%;
}

th,
td {
  border-right: 1px solid #ccc;
  border-bottom: 1px solid #ccc;
  padding: 5px 10px;
  width: 100px;
  box-sizing: border-box;
  margin: 0;
}

td {
  background: #fff;
}

th,
.fixed-cell {
  background: #eee;
}

.cross {
  position: relative;
  z-index: 1;
}
# Fixed headers and columns in AngularJS (v1.x)

There are challenges involved in presenting large tabular datasets through a user interface.  The human brain can only absorb a certain amount of information and keep track of the context of the data presented.  Fixing column headers and rows in tables of data is an important technique employed here at cap hpi, allowing users to retain column and row headers on screen whilst being able to scroll through the data.  This enables the user to more easily relate to individual data items within large and complex tables of data presented to them through the UI. 

Shuhei Kagawa's blog post [Freeze panes with CSS and a bit of JavaScript](http://shuheikagawa.com/blog/2016/01/11/freeze-panes-with-css-and-a-bit-of-javascript) gives a neat and concise example of the concept.  The original code has been translated into an AngularJS directive.  Here's a [Plunker](https://plnkr.co/edit/CqjOy3AQ5HrlH1PVTvbs?p=preview) to demonstrate the solution.

## How it works

The table is placed inside of a container div.  The number of fixed columns is set using the ```fixed-columns``` attribute :

```
<div class="container" fixed-column-table fixed-columns="3">
    <table>
        <! -- table content removed for brevity -->
    </table>
</div>
```

## Issues encountered

Once the code had been converted into a directive, the fixed columns were not functioning as expected.

After a small amount of trial and error, it was found that it was necessary to place a ```$timeout``` into the code in order to trigger a digest cycle:

```
$timeout(function () {
    activate();
}, 0);

```

## Differences from original version

In order to respond to new items being added to a table, the directive listens for a broadcast message of ``` 'refreshFixedColumns' ```.  This allows the directive to re-apply the necessary CSS classes and event listener for the scroll event:

```
scope.$on('refreshFixedColumns', function() {
    $timeout(function () {
        activate();
        container.scrollLeft = 0;
    }, 0);
```

So, to add a new item to an array that's bound to the table:

```
vm.items.push({
    id: 1
});

// Inform directive of need to refresh
$rootScope.$broadcast('refreshFixedColumns');
```

And that's it.  A very simple directive that delivers great functionality. Credit and thanks goes to the original author (Shuhei Kagawa)! 
(function () {
    'use strict';

    angular
         .module('app')
         .directive('fixedColumnTable', fixedColumnTable);

    fixedColumnTable.$inject = ['$timeout'];
    function fixedColumnTable($timeout) {
        return {
            restrict: 'A',
            scope: {
                fixedColumns: "@"
            },
            link: function (scope, element) {
                var container = element[0];

                function activate() {
                    applyClasses('thead tr', 'cross', 'th');
                    applyClasses('tbody tr', 'fixed-cell', 'td');
                    
                    var leftHeaders = [].concat.apply([], container.querySelectorAll('tbody td.fixed-cell'));
                    var topHeaders = [].concat.apply([], container.querySelectorAll('thead th'));
                    var crossHeaders = [].concat.apply([], container.querySelectorAll('thead th.cross'));

                    console.log('line before setting up event handler');
                    
                    container.addEventListener('scroll', function () {
                        console.log('scroll event handler hit');
                        var x = container.scrollLeft;
                        var y = container.scrollTop;

                        //Update the left header positions when the container is scrolled
                        leftHeaders.forEach(function (leftHeader) {
                            leftHeader.style.transform = translate(x, 0);
                        });

                        //Update the top header positions when the container is scrolled
                        topHeaders.forEach(function (topHeader) {
                          topHeader.style.transform = translate(0, y);
                        });

                        //Update headers that are part of the header and the left column
                        crossHeaders.forEach(function (crossHeader) {
                            crossHeader.style.transform = translate(x, y);
                        });

                    });

                    function translate(x, y) {
                        return 'translate(' + x + 'px, ' + y + 'px)';
                    }

                    function applyClasses(selector, newClass, cell) {
                        var arrayItems = [].concat.apply([], container.querySelectorAll(selector));
                        var currentElement;
                        var colspan;
                        
                        arrayItems.forEach(function (row, i) {
                            var numFixedColumns = scope.fixedColumns;
                            for (var j = 0; j < numFixedColumns; j++) {
                                currentElement = angular.element(row).find(cell)[j];
                                currentElement.classList.add(newClass);

                                if (currentElement.hasAttribute('colspan')) {
                                    colspan = currentElement.getAttribute('colspan');
                                    numFixedColumns -= (parseInt(colspan) - 1);
                                }
                            }
                        });
                    }
                }

                $timeout(function () {
                    activate();
                }, 0);
                
                scope.$on('refreshFixedColumns', function() {
                    $timeout(function () {
                        activate();
                        container.scrollLeft = 0;
                    }, 0);
                });
            }
        };
    }
})();
(function() {
    'use strict';

    angular.module('app', []);
})();
(function() {
  'use strict';

  angular
    .module('app')
    .controller('AppCtrl', AppCtrl);

  function AppCtrl() {
    var vm = this;
    
    vm.cars = [{
      id: 1,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.2 TFSI',
      power: '110PS',
      transmission: '6-speed manual',
      co2: 114,
      rrp: 18180.00,
      otr: 18865
    }, {
      id: 2,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.2 TFSI',
      power: '110PS',
      transmission: '7-speed S tronic',
      co2: 110,
      rrp: 19660.00,
      otr: 20345
    }, {
      id: 3,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.4 TFSI',
      power: '125PS',
      transmission: '6-speed manual',
      co2: 117,
      rrp: 19480.00,
      otr: 20165
    }, {
      id: 4,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.4 TFSI',
      power: '125PS',
      transmission: '7-speed S tronic',
      co2: 110,
      rrp: 20960.00,
      otr: 21645
    }, {
      id: 5,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.4 TFSI',
      power: '150PS',
      transmission: '6-speed manual',
      co2: 105,
      rrp: 20330.00,
      otr: 21015
    }, {
      id: 6,
     make: 'Audi',
      model: 'A3 SE',
      engine: '1.4 TFSI',
      power: '150PS',
      transmission: '7-speed S tronic',
      co2: 104,
      rrp: 21810.00,
      otr: 22495
    }, {
      id: 7,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.6 TDI',
      power: '110PS',
      transmission: '6-speed manual',
      co2: 99,
      rrp: 20430.00,
      otr: 21115
    }, {
      id: 8,
      make: 'Audi',
      model: 'A3 SE',
      engine: '1.6 TDI',
      power: '110PS',
      transmission: '7-speed S tronic',
      co2: 99,
      rrp: 21910.00,
      otr: 22595
    }, {
      id: 9,
      make: 'Audi',
      model: 'A3 SE',
      engine: '2.0 TDI',
      power: '150PS',
      transmission: '6-speed manual',
      co2: 108,
      rrp: 21780.00,
      otr: 22465
    }, {
      id: 10,
      make: 'Audi',
      model: 'A3 SE',
      engine: '2.0 TDI',
      power: '150PS',
      transmission: '6-speed S tronic',
      co2: 119,
      rrp: 23260.00,
      otr: 23945
    }];
  }
})();