From f2f8c4e8d32286e7504c670a4a57643193c75cb0 Mon Sep 17 00:00:00 2001 From: Wesley Cho Date: Fri, 27 Nov 2015 04:10:28 -0800 Subject: [PATCH] feat(paging): factor out common controller code - Create paging factory for creating common controller for the pagination and pager components Closes #4803 Closes #4968 --- src/pager/pager.js | 15 +- src/pagination/pagination.js | 266 ++++++++++++--------------------- src/paging/paging.js | 85 +++++++++++ src/paging/test/paging.spec.js | 256 +++++++++++++++++++++++++++++++ 4 files changed, 447 insertions(+), 175 deletions(-) create mode 100644 src/paging/paging.js create mode 100644 src/paging/test/paging.spec.js diff --git a/src/pager/pager.js b/src/pager/pager.js index 3fdccb4d1d..5d871e1ddd 100644 --- a/src/pager/pager.js +++ b/src/pager/pager.js @@ -1,4 +1,10 @@ -angular.module('ui.bootstrap.pager', ['ui.bootstrap.pagination']) +angular.module('ui.bootstrap.pager', ['ui.bootstrap.paging']) + +.controller('UibPagerController', ['$scope', '$attrs', 'uibPaging', 'uibPagerConfig', function($scope, $attrs, uibPaging, uibPagerConfig) { + $scope.align = angular.isDefined($attrs.align) ? $scope.$parent.$eval($attrs.align) : uibPagerConfig.align; + + uibPaging.create(this, $scope, $attrs); +}]) .constant('uibPagerConfig', { itemsPerPage: 10, @@ -7,7 +13,7 @@ angular.module('ui.bootstrap.pager', ['ui.bootstrap.pagination']) align: true }) -.directive('uibPager', ['uibPagerConfig', function(pagerConfig) { +.directive('uibPager', ['uibPagerConfig', function(uibPagerConfig) { return { scope: { totalItems: '=', @@ -16,7 +22,7 @@ angular.module('ui.bootstrap.pager', ['ui.bootstrap.pagination']) ngDisabled: '=' }, require: ['uibPager', '?ngModel'], - controller: 'UibPaginationController', + controller: 'UibPagerController', controllerAs: 'pager', templateUrl: function(element, attrs) { return attrs.templateUrl || 'uib/template/pager/pager.html'; @@ -29,8 +35,7 @@ angular.module('ui.bootstrap.pager', ['ui.bootstrap.pagination']) return; // do nothing if no ng-model } - scope.align = angular.isDefined(attrs.align) ? scope.$parent.$eval(attrs.align) : pagerConfig.align; - paginationCtrl.init(ngModelCtrl, pagerConfig); + paginationCtrl.init(ngModelCtrl, uibPagerConfig); } }; }]); diff --git a/src/pagination/pagination.js b/src/pagination/pagination.js index d483c9a564..c67e370429 100644 --- a/src/pagination/pagination.js +++ b/src/pagination/pagination.js @@ -1,80 +1,110 @@ -angular.module('ui.bootstrap.pagination', []) -.controller('UibPaginationController', ['$scope', '$attrs', '$parse', function($scope, $attrs, $parse) { - var self = this, - ngModelCtrl = { $setViewValue: angular.noop }, // nullModelCtrl - setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; - - this.init = function(ngModelCtrl_, config) { - ngModelCtrl = ngModelCtrl_; - this.config = config; - - ngModelCtrl.$render = function() { - self.render(); +angular.module('ui.bootstrap.pagination', ['ui.bootstrap.paging']) +.controller('UibPaginationController', ['$scope', '$attrs', '$parse', 'uibPaging', 'uibPaginationConfig', function($scope, $attrs, $parse, uibPaging, uibPaginationConfig) { + var ctrl = this; + // Setup configuration parameters + var maxSize = angular.isDefined($attrs.maxSize) ? $scope.$parent.$eval($attrs.maxSize) : uibPaginationConfig.maxSize, + rotate = angular.isDefined($attrs.rotate) ? $scope.$parent.$eval($attrs.rotate) : uibPaginationConfig.rotate, + forceEllipses = angular.isDefined($attrs.forceEllipses) ? $scope.$parent.$eval($attrs.forceEllipses) : uibPaginationConfig.forceEllipses, + boundaryLinkNumbers = angular.isDefined($attrs.boundaryLinkNumbers) ? $scope.$parent.$eval($attrs.boundaryLinkNumbers) : uibPaginationConfig.boundaryLinkNumbers; + $scope.boundaryLinks = angular.isDefined($attrs.boundaryLinks) ? $scope.$parent.$eval($attrs.boundaryLinks) : uibPaginationConfig.boundaryLinks; + $scope.directionLinks = angular.isDefined($attrs.directionLinks) ? $scope.$parent.$eval($attrs.directionLinks) : uibPaginationConfig.directionLinks; + + uibPaging.create(this, $scope, $attrs); + + if ($attrs.maxSize) { + $scope.$parent.$watch($parse($attrs.maxSize), function(value) { + maxSize = parseInt(value, 10); + ctrl.render(); + }); + } + + // Create page object used in template + function makePage(number, text, isActive) { + return { + number: number, + text: text, + active: isActive }; + } - if ($attrs.itemsPerPage) { - $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { - self.itemsPerPage = parseInt(value, 10); - $scope.totalPages = self.calculateTotalPages(); - updatePage(); - }); - } else { - this.itemsPerPage = config.itemsPerPage; - } + function getPages(currentPage, totalPages) { + var pages = []; - $scope.$watch('totalItems', function(newTotal, oldTotal) { - if (angular.isDefined(newTotal) || newTotal !== oldTotal) { - $scope.totalPages = self.calculateTotalPages(); - updatePage(); - } - }); - }; - - this.calculateTotalPages = function() { - var totalPages = this.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / this.itemsPerPage); - return Math.max(totalPages || 0, 1); - }; + // Default page limits + var startPage = 1, endPage = totalPages; + var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; - this.render = function() { - $scope.page = parseInt(ngModelCtrl.$viewValue, 10) || 1; - }; + // recompute if maxSize + if (isMaxSized) { + if (rotate) { + // Current page is displayed in the middle of the visible ones + startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); + endPage = startPage + maxSize - 1; - $scope.selectPage = function(page, evt) { - if (evt) { - evt.preventDefault(); - } + // Adjust if limit is exceeded + if (endPage > totalPages) { + endPage = totalPages; + startPage = endPage - maxSize + 1; + } + } else { + // Visible pages are paginated with maxSize + startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; - var clickAllowed = !$scope.ngDisabled || !evt; - if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { - if (evt && evt.target) { - evt.target.blur(); + // Adjust last page if limit is exceeded + endPage = Math.min(startPage + maxSize - 1, totalPages); } - ngModelCtrl.$setViewValue(page); - ngModelCtrl.$render(); } - }; - - $scope.getText = function(key) { - return $scope[key + 'Text'] || self.config[key + 'Text']; - }; - $scope.noPrevious = function() { - return $scope.page === 1; - }; - - $scope.noNext = function() { - return $scope.page === $scope.totalPages; - }; + // Add page number links + for (var number = startPage; number <= endPage; number++) { + var page = makePage(number, number, number === currentPage); + pages.push(page); + } - function updatePage() { - setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + // Add links to move between page sets + if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { + if (startPage > 1) { + if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning + var previousPageSet = makePage(startPage - 1, '...', false); + pages.unshift(previousPageSet); + } + if (boundaryLinkNumbers) { + if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential + var secondPageLink = makePage(2, '2', false); + pages.unshift(secondPageLink); + } + //add the first page + var firstPageLink = makePage(1, '1', false); + pages.unshift(firstPageLink); + } + } - if ($scope.page > $scope.totalPages) { - $scope.selectPage($scope.totalPages); - } else { - ngModelCtrl.$render(); + if (endPage < totalPages) { + if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end + var nextPageSet = makePage(endPage + 1, '...', false); + pages.push(nextPageSet); + } + if (boundaryLinkNumbers) { + if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential + var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); + pages.push(secondToLastPageLink); + } + //add the last page + var lastPageLink = makePage(totalPages, totalPages, false); + pages.push(lastPageLink); + } + } } + return pages; } + + var originalRender = this.render; + this.render = function() { + originalRender(); + if ($scope.page > 0 && $scope.page <= $scope.totalPages) { + $scope.pages = getPages($scope.page, $scope.totalPages); + } + }; }]) .constant('uibPaginationConfig', { @@ -90,9 +120,8 @@ angular.module('ui.bootstrap.pagination', []) forceEllipses: false }) -.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, paginationConfig) { +.directive('uibPagination', ['$parse', 'uibPaginationConfig', function($parse, uibPaginationConfig) { return { - restrict: 'EA', scope: { totalItems: '=', firstText: '@', @@ -115,110 +144,7 @@ angular.module('ui.bootstrap.pagination', []) return; // do nothing if no ng-model } - // Setup configuration parameters - var maxSize = angular.isDefined(attrs.maxSize) ? scope.$parent.$eval(attrs.maxSize) : paginationConfig.maxSize, - rotate = angular.isDefined(attrs.rotate) ? scope.$parent.$eval(attrs.rotate) : paginationConfig.rotate, - forceEllipses = angular.isDefined(attrs.forceEllipses) ? scope.$parent.$eval(attrs.forceEllipses) : paginationConfig.forceEllipses, - boundaryLinkNumbers = angular.isDefined(attrs.boundaryLinkNumbers) ? scope.$parent.$eval(attrs.boundaryLinkNumbers) : paginationConfig.boundaryLinkNumbers; - scope.boundaryLinks = angular.isDefined(attrs.boundaryLinks) ? scope.$parent.$eval(attrs.boundaryLinks) : paginationConfig.boundaryLinks; - scope.directionLinks = angular.isDefined(attrs.directionLinks) ? scope.$parent.$eval(attrs.directionLinks) : paginationConfig.directionLinks; - - paginationCtrl.init(ngModelCtrl, paginationConfig); - - if (attrs.maxSize) { - scope.$parent.$watch($parse(attrs.maxSize), function(value) { - maxSize = parseInt(value, 10); - paginationCtrl.render(); - }); - } - - // Create page object used in template - function makePage(number, text, isActive) { - return { - number: number, - text: text, - active: isActive - }; - } - - function getPages(currentPage, totalPages) { - var pages = []; - - // Default page limits - var startPage = 1, endPage = totalPages; - var isMaxSized = angular.isDefined(maxSize) && maxSize < totalPages; - - // recompute if maxSize - if (isMaxSized) { - if (rotate) { - // Current page is displayed in the middle of the visible ones - startPage = Math.max(currentPage - Math.floor(maxSize / 2), 1); - endPage = startPage + maxSize - 1; - - // Adjust if limit is exceeded - if (endPage > totalPages) { - endPage = totalPages; - startPage = endPage - maxSize + 1; - } - } else { - // Visible pages are paginated with maxSize - startPage = (Math.ceil(currentPage / maxSize) - 1) * maxSize + 1; - - // Adjust last page if limit is exceeded - endPage = Math.min(startPage + maxSize - 1, totalPages); - } - } - - // Add page number links - for (var number = startPage; number <= endPage; number++) { - var page = makePage(number, number, number === currentPage); - pages.push(page); - } - - // Add links to move between page sets - if (isMaxSized && maxSize > 0 && (!rotate || forceEllipses || boundaryLinkNumbers)) { - if (startPage > 1) { - if (!boundaryLinkNumbers || startPage > 3) { //need ellipsis for all options unless range is too close to beginning - var previousPageSet = makePage(startPage - 1, '...', false); - pages.unshift(previousPageSet); - } - if (boundaryLinkNumbers) { - if (startPage === 3) { //need to replace ellipsis when the buttons would be sequential - var secondPageLink = makePage(2, '2', false); - pages.unshift(secondPageLink); - } - //add the first page - var firstPageLink = makePage(1, '1', false); - pages.unshift(firstPageLink); - } - } - - if (endPage < totalPages) { - if (!boundaryLinkNumbers || endPage < totalPages - 2) { //need ellipsis for all options unless range is too close to end - var nextPageSet = makePage(endPage + 1, '...', false); - pages.push(nextPageSet); - } - if (boundaryLinkNumbers) { - if (endPage === totalPages - 2) { //need to replace ellipsis when the buttons would be sequential - var secondToLastPageLink = makePage(totalPages - 1, totalPages - 1, false); - pages.push(secondToLastPageLink); - } - //add the last page - var lastPageLink = makePage(totalPages, totalPages, false); - pages.push(lastPageLink); - } - } - } - return pages; - } - - var originalRender = paginationCtrl.render; - paginationCtrl.render = function() { - originalRender(); - if (scope.page > 0 && scope.page <= scope.totalPages) { - scope.pages = getPages(scope.page, scope.totalPages); - } - }; + paginationCtrl.init(ngModelCtrl, uibPaginationConfig); } }; }]); diff --git a/src/paging/paging.js b/src/paging/paging.js new file mode 100644 index 0000000000..7adc5ddb73 --- /dev/null +++ b/src/paging/paging.js @@ -0,0 +1,85 @@ +angular.module('ui.bootstrap.paging', []) +/** + * Helper internal service for generating common controller code between the + * pager and pagination components + */ +.factory('uibPaging', ['$parse', function($parse) { + return { + create: function(ctrl, $scope, $attrs) { + ctrl.setNumPages = $attrs.numPages ? $parse($attrs.numPages).assign : angular.noop; + ctrl.ngModelCtrl = { $setViewValue: angular.noop }; // nullModelCtrl + + ctrl.init = function(ngModelCtrl, config) { + ctrl.ngModelCtrl = ngModelCtrl; + ctrl.config = config; + + ngModelCtrl.$render = function() { + ctrl.render(); + }; + + if ($attrs.itemsPerPage) { + $scope.$parent.$watch($parse($attrs.itemsPerPage), function(value) { + ctrl.itemsPerPage = parseInt(value, 10); + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + }); + } else { + ctrl.itemsPerPage = config.itemsPerPage; + } + + $scope.$watch('totalItems', function(newTotal, oldTotal) { + if (angular.isDefined(newTotal) || newTotal !== oldTotal) { + $scope.totalPages = ctrl.calculateTotalPages(); + ctrl.updatePage(); + } + }); + }; + + ctrl.calculateTotalPages = function() { + var totalPages = ctrl.itemsPerPage < 1 ? 1 : Math.ceil($scope.totalItems / ctrl.itemsPerPage); + return Math.max(totalPages || 0, 1); + }; + + ctrl.render = function() { + $scope.page = parseInt(ctrl.ngModelCtrl.$viewValue, 10) || 1; + }; + + $scope.selectPage = function(page, evt) { + if (evt) { + evt.preventDefault(); + } + + var clickAllowed = !$scope.ngDisabled || !evt; + if (clickAllowed && $scope.page !== page && page > 0 && page <= $scope.totalPages) { + if (evt && evt.target) { + evt.target.blur(); + } + ctrl.ngModelCtrl.$setViewValue(page); + ctrl.ngModelCtrl.$render(); + } + }; + + $scope.getText = function(key) { + return $scope[key + 'Text'] || ctrl.config[key + 'Text']; + }; + + $scope.noPrevious = function() { + return $scope.page === 1; + }; + + $scope.noNext = function() { + return $scope.page === $scope.totalPages; + }; + + ctrl.updatePage = function() { + ctrl.setNumPages($scope.$parent, $scope.totalPages); // Readonly variable + + if ($scope.page > $scope.totalPages) { + $scope.selectPage($scope.totalPages); + } else { + ctrl.ngModelCtrl.$render(); + } + }; + } + }; +}]); diff --git a/src/paging/test/paging.spec.js b/src/paging/test/paging.spec.js new file mode 100644 index 0000000000..652cd02b7e --- /dev/null +++ b/src/paging/test/paging.spec.js @@ -0,0 +1,256 @@ +describe('paging factory', function() { + var $rootScope, $scope, ctrl, attrs; + + beforeEach(module('ui.bootstrap.paging')); + beforeEach(inject(function(_$rootScope_, uibPaging) { + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + ctrl = {}; + attrs = {}; + + uibPaging.create(ctrl, $scope, attrs); + })); + + describe('init', function() { + var ngModelCtrl, config; + + beforeEach(function() { + ngModelCtrl = {}; + config = { + foo: 'bar', + itemsPerPage: 12 + }; + }); + + describe('without itemsPerPage', function() { + beforeEach(function() { + ctrl.init(ngModelCtrl, config); + }); + + it('should set the ngModel and config', function() { + expect(ctrl.ngModelCtrl).toBe(ngModelCtrl); + expect(ctrl.config).toBe(config); + }); + + it('should properly render the model', function() { + spyOn(ctrl, 'render'); + + ngModelCtrl.$render(); + + expect(ctrl.render).toHaveBeenCalled(); + }); + + it('should set to default itemsPerPage', function() { + expect(ctrl.itemsPerPage).toBe(12); + }); + + it('should update the page when total items changes', function() { + spyOn(ctrl, 'calculateTotalPages').and.returnValue(5); + spyOn(ctrl, 'updatePage'); + $rootScope.$digest(); + + expect(ctrl.calculateTotalPages.calls.count()).toBe(0); + expect(ctrl.updatePage.calls.count()).toBe(0); + + $scope.totalItems = 10; + $rootScope.$digest(); + + expect(ctrl.calculateTotalPages.calls.count()).toBe(1); + expect(ctrl.updatePage.calls.count()).toBe(1); + expect($scope.totalPages).toBe(5); + + $scope.totalItems = undefined; + $scope.totalPages = 2; + $rootScope.$digest(); + + expect(ctrl.calculateTotalPages.calls.count()).toBe(2); + expect(ctrl.updatePage.calls.count()).toBe(2); + expect($scope.totalPages).toBe(5); + }); + }); + + describe('with itemsPerPage', function() { + beforeEach(function() { + attrs.itemsPerPage = 'abc'; + $rootScope.abc = 10; + + ctrl.init(ngModelCtrl, config); + }); + + it('should update the page when itemsPerPage changes', function() { + spyOn(ctrl, 'calculateTotalPages').and.returnValue(5); + spyOn(ctrl, 'updatePage'); + $rootScope.$digest(); + + expect(ctrl.itemsPerPage).toBe(10); + expect($scope.totalPages).toBe(5); + expect(ctrl.updatePage).toHaveBeenCalled(); + }); + }); + }); + + describe('calculate totalPages', function() { + it('when itemsPerPage is less than 1', function() { + ctrl.itemsPerPage = 0; + $scope.totalItems = 101; + expect(ctrl.calculateTotalPages()).toBe(1); + }); + + it('when itemsPerPage is greater than 1', function() { + ctrl.itemsPerPage = 10; + $scope.totalItems = 101; + expect(ctrl.calculateTotalPages()).toBe(11); + }); + }); + + describe('render', function() { + it('should set page to 1 when invalid', function() { + ctrl.ngModelCtrl.$viewValue = 'abcd'; + $scope.page = 10; + + ctrl.render(); + + expect($scope.page).toBe(1); + }); + + it('should set page to view value when valid', function() { + ctrl.ngModelCtrl.$viewValue = '3'; + $scope.page = 10; + + ctrl.render(); + + expect($scope.page).toBe(3); + }); + }); + + describe('select page', function() { + beforeEach(function() { + spyOn(ctrl.ngModelCtrl, '$setViewValue'); + ctrl.ngModelCtrl.$render = jasmine.createSpy('ctrl.ngModelCtrl.$render'); + $scope.page = 5; + $scope.totalPages = 20; + }); + + it('should change the page', function() { + $scope.selectPage(12); + + expect(ctrl.ngModelCtrl.$setViewValue).toHaveBeenCalledWith(12); + expect(ctrl.ngModelCtrl.$render).toHaveBeenCalled(); + }); + + it('should not change the page to one out of range', function() { + $scope.selectPage(-1); + + expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled(); + expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled(); + + $scope.selectPage(21); + + expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled(); + expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled(); + }); + + describe('on click', function() { + var evt; + + beforeEach(function() { + evt = { + preventDefault: jasmine.createSpy('evt.preventDefault'), + target: { + blur: jasmine.createSpy('evt.target.blur') + } + }; + }); + + it('should prevent default behavior', function() { + $scope.selectPage(12, evt); + + expect(evt.preventDefault).toHaveBeenCalled(); + }); + + it('should not change the page if disabled and from an event', function() { + $scope.ngDisabled = true; + + $scope.selectPage(12, evt); + + expect(ctrl.ngModelCtrl.$setViewValue).not.toHaveBeenCalled(); + expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled(); + }); + + it('should blur the element clicked', function() { + $scope.selectPage(12, evt); + + expect(evt.target.blur).toHaveBeenCalled(); + }); + }); + }); + + it('should get the text', function() { + $scope.fooText = 'bar'; + + expect($scope.getText('foo')).toBe('bar'); + }); + + it('should get the default text', function() { + ctrl.config = { + fooText: 'bar' + }; + + expect($scope.getText('foo')).toBe('bar'); + }); + + it('should disable previous button', function() { + $scope.page = 1; + + expect($scope.noPrevious()).toBe(true); + }); + + it('should enable previous button', function() { + $scope.page = 2; + + expect($scope.noPrevious()).toBe(false); + }); + + it('should disable next button', function() { + $scope.page = 10; + $scope.totalPages = 10; + + expect($scope.noNext()).toBe(true); + }); + + it('should enable next button', function() { + $scope.page = 9; + $scope.totalPages = 10; + + expect($scope.noNext()).toBe(false); + }); + + describe('update page', function() { + beforeEach(function() { + spyOn($scope, 'selectPage'); + ctrl.ngModelCtrl.$render = jasmine.createSpy('ctrl.ngModelCtrl.$render'); + ctrl.setNumPages = jasmine.createSpy('ctrl.setNumPages'); + $scope.totalPages = 10; + }); + + it('should select the last page if page is above total', function() { + $scope.page = 12; + + ctrl.updatePage(); + + expect(ctrl.setNumPages).toHaveBeenCalledWith($rootScope, 10); + expect($scope.selectPage).toHaveBeenCalledWith(10); + expect(ctrl.ngModelCtrl.$render).not.toHaveBeenCalled(); + }); + + it('should execute render if page is within range', function() { + $scope.page = 5; + + ctrl.updatePage(); + + expect(ctrl.setNumPages).toHaveBeenCalledWith($rootScope, 10); + expect($scope.selectPage).not.toHaveBeenCalled(); + expect(ctrl.ngModelCtrl.$render).toHaveBeenCalled(); + }); + }); +});