Custom DataGrid Directive

In this example below you will see how to do a Custom DataGrid Directive with some HTML / CSS and Javascript

Thumbnail
This awesome code was written by nickjohnson_dev, you can see more from this user in the personal repository.
You can find the original code on Codepen.io
Copyright nickjohnson_dev ©
  • HTML
  • CSS
  • JavaScript
    <div class="wrapper" ng-app="app" ng-controller="AppController as ctrl">
  <div class="group-by">
    <div class="my-grid-cell">
      Group By:
      <select ng-model="ctrl.groupBy">
        <option value="">None</option>
        <option ng-repeat="column in ctrl.columns" value="{{column.property}}">{{column.displayText}}</option>
      </select>
      <label for="reverse-grouping-check">
        <input type="checkbox" id="reverse-grouping-check" ng-model="ctrl.groupsReversed" /> Groups Reversed
      </label>
    </div>
  </div>
  <my-grid 
    data="ctrl.people" 
    columns="ctrl.columns"
    order-by="ctrl.orderBy"
    group-by="ctrl.groupBy"
    groups-reversed="ctrl.groupsReversed"
  ></my-grid>
  <my-pagination-bar 
    items="ctrl.people"
    on-page-change="ctrl.loadPage"
    page-size="5"
    allow-size-change="true"
  ></my-pagination-bar>
</div>

/*Downloaded from https://www.codeseek.co/nickjohnson_dev/custom-datagrid-directive-qdrKWO */
    @margin-s: 8px;
@margin-m: 16px;
@margin-l: 32px;

html, body {
  width: 100%;
  height: 100%;
}

.wrapper {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  padding: 16px;
  overflow: hidden;
}

.group-by {
  box-sizing: border-box;
  display: flex;
  align-items: center;
  flex: 1 0 auto;
  width: 100%;
  height: 40px;
  padding-left: 16px;
  padding-right: 16px;
  margin-bottom: 16px;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
  select {
    margin-right: 16px;
  }
}

.my-grid {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  width: 100%;
  height: 100%;
  margin-bottom: @margin-m;
  background-color: #fff;
  border-radius: 2px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
  overflow-y: hidden;
  .my-grid-row {
    box-sizing: border-box;
    display: block;
    flex-shrink: 0;
    width: 100%;
    height: 40px;
    &.header {
      position: relative;
      z-index: 9999;
      padding-right: 16px;
      color: #fff;
      background-color: #777;
      border-top-left-radius: 2px;
      border-top-right-radius: 2px;
      box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
      .my-grid-cell {
        cursor: pointer;
      }
    }
    &.group-header {
      background-color: #ddd;
      border-bottom: 1px solid #ccc;
      cursor: pointer;
    }
  }
  .my-grid-cell {
    box-sizing: border-box;
    display: inline-block;
    align-items: center;
    height: 40px;
    padding-left: 16px;
    padding-right: 16px;
    line-height: 40px;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow: hidden;
  }
  .my-grid-body {
    box-sizing: border-box;
    flex-grow: 1;
    overflow-y: scroll;
  }
  .my-grid-group {
    overflow: hidden;
    transition: height 0.25s ease;
  }
}

.list-panel {
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 600px;
  max-height: 400px;
  margin-bottom: @margin-m;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
  overflow: hidden;
}

.list {
  box-sizing: border-box;
  height: 100%;
  min-height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
}

ul {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

.list-item {
  display: flex;
  align-items: center;
  height: 40px;
  padding-left: @margin-s;
  padding-right: @margin-s;
}

.my-pagination-bar {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  max-width: 600px;
  height: 56px;
  background-color: white;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25);
  .my-pagination-bar-items {
    display: flex;
    flex-grow: 1;
    flex-direction: row;
    align-items: center;
    justify-content: flex-start;
    padding-left: @margin-s;
    padding-right: @margin-s;
    &.right {
      margin-left: auto;
    }
  }
  .my-pagination-bar-pager {
    position: relative;
    flex-grow: 1;
    width: 40px;
    height: 40px;
    cursor: pointer;
    border-radius: 2px;
  }
  .my-pagination-bar-previous-page, 
  .my-pagination-bar-next-page {
    &:after {
        content: '';
        width: 5px;
        height: 5px;
        position: absolute;
        top: 50%;
        left: 53%;
        background: transparent;
        border: none;
        border-right-color: transparent;
        border-top-color: transparent;
        border-left: 2px solid #444;
        border-bottom: 2px solid #444;
        transform: translate(-50%, -50%) rotate(45deg);
    }
    &.disabled {
      visibility: hidden;
    }
  }
  .my-pagination-bar-previous-page {
    margin-right: @margin-s;
    &:after {
        left: 53%;
        transform: translate(-50%, -50%) rotate(45deg);
    }
  }
  .my-pagination-bar-next-page {
    &:after {
        left: 50%;
        transform: translate(-50%, -50%) rotate(225deg);
    }
  }
  .my-pagination-bar-pages {
    display: flex;
    flex-grow: 1;
    flex-shrink: 0;
    margin-right: @margin-s;
  }
  .my-pagination-bar-page {
    display: flex;
    align-items: center;
    justify-content: center;
    user-select: none;
    &.active {
      color: white;
      background-color: #7af;
    }
  }
}


/*Downloaded from https://www.codeseek.co/nickjohnson_dev/custom-datagrid-directive-qdrKWO */
    'use strict';
angular
  .module('app', [])
  .controller('AppController', AppController)
  .directive('myGrid', MyGridDirective)
  .controller('MyGridController', MyGridController)
  .filter('groupBy', GroupByFilter)
  .directive('myPaginationBar', MyPaginationBarDirective)
  .controller('MyPaginationBarController', MyPaginationBarController);

function AppController($http) {
  let vm = this;
  const dataUri = 'http://beta.json-generator.com/api/json/get/KTndR0y';
  vm.page = {};
  vm.groupBy = '';  
  vm.groupsReversed = false;
  vm.orderBy = 'id';  
  vm.people = [
    {
      id: 1,
      firstName: 'Nick',
      lastName: 'Johnson',
      country: 'United States'
    }
  ];
  vm.columns = [
    {
      property: 'id',
      displayText: 'ID',
      width: 1
    },
    {
      property: 'firstName',
      displayText: 'First Name',
      width: 1
    },
    {
      property: 'lastName',
      displayText: 'Last Name',
      width: 1
    },
    {
      property: 'country',
      displayText: 'Country',
      width: 1
    }
  ];
  vm.loadPage = loadPage;
  
  function init() {
    $http.get(dataUri)
      .then(response => {
        vm.people = response.data;
      }, reason => {
        console.log(`Error: ${reason}`)
      });
  }
  
  function loadPage(page) {
    vm.page = page;
  }
  
  init();
}

function MyGridDirective($http, $parse) {
  // This could be improved in an actual project using partialify: https://github.com/bclinkinbeard/partialify
  var template = `` +
`<div class="my-grid">
  <div class="my-grid-row header">
    <div class="my-grid-cell"
      ng-repeat="column in gridCtrl.columns"
      style="width:{{gridCtrl.getWeightedWidth(column)}};"
      ng-click="gridCtrl.setOrderBy(column.property)">
      {{ column.displayText }}
    </div>
  </div>
  <div class="my-grid-body grouped">
    <div class="my-grid-group"
      ng-repeat="group in gridCtrl.data | groupBy:gridCtrl.groupBy:gridCtrl.groupsReversed"
      style="height:{{(group.isOpen === true ? group.height : 40) + 'px'}};">
      <div class="my-grid-row group-header" 
        ng-if="gridCtrl.groupBy !== undefined && gridCtrl.groupBy !== ''"
        ng-click="group.isOpen = !group.isOpen">
        <div class="my-grid-cell">
          {{group.name}}
        </div>
      </div>
      <div class="my-grid-row" ng-repeat="item in group.items | orderBy:gridCtrl.orderBy">
        <div class="my-grid-cell"
          ng-repeat="column in gridCtrl.columns"
          style="width:{{gridCtrl.getWeightedWidth(column)}};">
          {{ item[column.property] }}
        </div> 
      </div>
    </div>
  </div>
</div>`;
  
  return {
    restrict: 'E',
    replace: true,
    scope: {
      data: '=',
      columns: '=',
      groupBy: '=?',
      groupsReversed: '=?',
      orderBy: '=?'
    },
    bindToController: true,
    controller: 'MyGridController',
    controllerAs: 'gridCtrl',
    template,
    link
  }
  
  function link(scope, element, attributes, gridCtrl) {
    gridCtrl.init();
  }
}

function MyGridController($scope) {
  const vm = this;
  vm.setOrderBy = setOrderBy;
  vm.getWeightedWidth = getWeightedWidth;
  vm.init = init;
  
  function init() {
    if (vm.groupBy === undefined) {
      vm.groupBy = '';
    }
    if (vm.groupsReversed === undefined) {
      vm.groupsReversed = false;
    }
    if (vm.orderBy === undefined) {
      vm.orderBy = '';
    }
  }
  
  function getWeightedWidth(column) {
    return (100 / vm.columns.length) + '%';
  }
  
  function setOrderBy(prop) {
    if (vm.orderBy === prop) {
      vm.orderBy = '-' + prop;
    }
    else {
      vm.orderBy = prop;
    }
  }
}

MyGridController.$inject = ['$scope'];

function GroupByFilter($parse) {
  return memoize(function(collection, prop, reversed) {
    
    if (collection === undefined || collection === null || prop === undefined || prop === '') {
      return [{
        name: '',
        items: collection,
        height: (collection.length * 40) + 40,
        isOpen: true
      }];
    }
    
    const groupedObj = _.groupBy(collection, function(item) {
      return item[prop];
    });
    
    const groups = [];
    
    for(var key in groupedObj) {
      groups.push({ 
        name: key,
        items: groupedObj[key],
        height: (groupedObj[key].length * 40) + 40,
        isOpen: true
      });
    };
    
    if (reversed === true) {
      groups.sort(byDescendingNames);
    }
    else {
      groups.sort(byAscendingNames);
    }
    
    return groups;
    
    function byAscendingNames(a, b) {
      if (a.name < b.name) {
        return -1;
      }
      else if (a.name > b.name) {
        return 1;
      }
      else {
        return 0;
      }
    }
    
    function byDescendingNames(a, b) {
      if (a.name < b.name) {
        return 1;
      }
      else if (a.name > b.name) {
        return -1;
      }
      else {
        return 0;
      }
    }
  })
  
  function memoize(func) {
        var stringifyJson = JSON.stringify,
            cache = {};

        var cachedfun = function() {
            var hash = stringifyJson(arguments);
            return (hash in cache) ? cache[hash] : cache[hash] = func.apply(this, arguments);
        };

        cachedfun.__cache = (function() {
            cache.remove || (cache.remove = function() {
                var hash = stringifyJson(arguments);
                return (delete cache[hash]);
            });
            return cache;
        }).call(this);

        return cachedfun;
    };
}

function MyPaginationBarDirective() {
  return {
    restrict: 'E',
    replace: true,
    scope: {
      items: '=',
      page: '=',
      onPageChange: '=',
      pageSize: '=?',
      allowSizeChange: '=?'
    },
    bindToController: true,
    controller: 'MyPaginationBarController',
    controllerAs: 'paginationBarCtrl',
    template: `` +
`<div class="my-pagination-bar">
<div class="my-pagination-bar-items">
  <div class="my-pagination-bar-pager my-pagination-bar-previous-page"
    ng-class="{'disabled': paginationBarCtrl.firstPageActive}"
    ng-click="paginationBarCtrl.goToPreviousPage()"></div>
  <ul class="my-pagination-bar-pages">
    <li class="my-pagination-bar-pager my-pagination-bar-page"
      ng-repeat="page in paginationBarCtrl.visiblePages"
      ng-class="{'active': paginationBarCtrl.activeIndex === page.index}"
      ng-click="paginationBarCtrl.setPage(page)">{{page.index}}</li>
  </ul>
  <div class="my-pagination-bar-pager my-pagination-bar-next-page"
    ng-class="{'disabled': paginationBarCtrl.lastPageActive}"
    ng-click="paginationBarCtrl.goToNextPage()"></div>
</div>
</div>`,
    link
  }
  
  function link(scope, element, attributes, paginationBarCtrl) {
    paginationBarCtrl.init();
  }
}

function MyPaginationBarController($scope) {
  const vm = this;
  vm.setPage = setPage;
  vm.goToPreviousPage = goToPreviousPage;
  vm.goToNextPage = goToNextPage;
  vm.init = init;
  
  function init() {
    $scope.$watch(() => vm.items, () => {
      vm.pages = groupItems(vm.items, vm.pageSize).map((itemGroup, index) => {
        return {
          index: index + 1,
          items: itemGroup
        }
      });
      setPage(vm.pages[0]);
    });
  }
  
  function groupItems(items, size) {
    if (items.length <= size) {
      return [items];
    }
    
    const result = [];
    
    for (var i = 0; (i)*size < items.length; i++) {
      result.push(items.slice(i * size, Math.min((i+1) * size), items.length))
    }
    
    return result;
  }
  
  function setPage(page) {
    updateActiveIndex(page.index);
    updateVisiblePages(page.index);
    vm.onPageChange(page);
  }
  
  function goToPreviousPage() {
    if (!vm.firstPageActive === true) {
      setPage(vm.pages[vm.activeIndex - 2]);
    }
  }
  function goToNextPage() {
    if (!vm.lastPageActive === true){
      setPage(vm.pages[vm.activeIndex]);
    }
  }
  
  function updateActiveIndex(index) {
    vm.activeIndex = index;
    
    if (vm.activeIndex === 1) {
      vm.firstPageActive = true;
    }
    else {
      vm.firstPageActive = false;
    }
    
    if (vm.activeIndex === vm.pages.length) {
      vm.lastPageActive = true;
    }
    else {
      vm.lastPageActive = false;
    }
  }
  
  function updateVisiblePages(index) {
    if (index < 4) {
      vm.visiblePages = vm.pages.slice(0, Math.min(5, vm.pages.length));
    }
    else if (index > vm.pages.length - 3) {
      vm.visiblePages = vm.pages.slice(vm.pages.length - 5, vm.pages.length);
    }
    else {
      vm.visiblePages = vm.pages.slice(index - 3, index + 2);
    }
  }
}

MyPaginationBarController.$inject = ['$scope'];



Comments