From 3715bfcea783cf433077ec34b650f4967d856499 Mon Sep 17 00:00:00 2001 From: flmu Date: Thu, 27 Feb 2014 17:12:07 +0100 Subject: [PATCH] refactor(ionInfiniteScroll): make it primarily use own attrs, add tests --- js/ext/angular/src/directive/ionicContent.js | 206 ++++++++++-------- .../directive/ionicInfiniteScroll.unit.js | 128 +++++++++++ 2 files changed, 241 insertions(+), 93 deletions(-) create mode 100644 js/ext/angular/test/directive/ionicInfiniteScroll.unit.js diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js index 2aa8914295..fa2f244070 100644 --- a/js/ext/angular/src/directive/ionicContent.js +++ b/js/ext/angular/src/directive/ionicContent.js @@ -77,16 +77,14 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { var clone, sc, scrollView, scrollCtrl, scrollContent = angular.element($element[0].querySelector('.scroll')); - transclude($scope, function(clone) { - scrollContent.append(clone); - }); - $ionicBind($scope, $attr, { //Use $ to stop onRefresh from recursively calling itself + //DEPRECATED, use $onRefresh: '&onRefresh', $onRefreshOpening: '&onRefreshOpening', $onScroll: '&onScroll', $onScrollComplete: '&onScrollComplete', + //DEPRECATED, use $onInfiniteScroll: '&onInfiniteScroll', refreshComplete: '=', infiniteScrollDistance: '@', @@ -102,64 +100,69 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { scrollEventInterval: '@' }); - if($scope.scroll === "false") { - // No scrolling - return; - } - - if(attr.overflowScroll === "true") { + if ($scope.scroll === "false") { + //do nothing + } else if(attr.overflowScroll === "true") { $element.addClass('overflow-scroll'); - return; - } + } else { - scrollCtrl = $controller('$ionicScroll', { - $scope: $scope, - scrollViewOptions: { - el: $element[0], - bouncing: $scope.$eval($scope.hasBouncing), - startX: $scope.$eval($scope.startX) || 0, - startY: $scope.$eval($scope.startY) || 0, - scrollbarX: $scope.$eval($scope.scrollbarX) !== false, - scrollbarY: $scope.$eval($scope.scrollbarY) !== false, - scrollingX: $scope.$eval($scope.hasScrollX) === true, - scrollingY: $scope.$eval($scope.hasScrollY) !== false, - scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 20, - scrollingComplete: function() { - $scope.$onScrollComplete({ - scrollTop: this.__scrollTop, - scrollLeft: this.__scrollLeft - }); + scrollCtrl = $controller('$ionicScroll', { + $scope: $scope, + scrollViewOptions: { + el: $element[0], + bouncing: $scope.$eval($scope.hasBouncing), + startX: $scope.$eval($scope.startX) || 0, + startY: $scope.$eval($scope.startY) || 0, + scrollbarX: $scope.$eval($scope.scrollbarX) !== false, + scrollbarY: $scope.$eval($scope.scrollbarY) !== false, + scrollingX: $scope.$eval($scope.hasScrollX) === true, + scrollingY: $scope.$eval($scope.hasScrollY) !== false, + scrollEventInterval: parseInt($scope.scrollEventInterval, 10) || 20, + scrollingComplete: function() { + $scope.$onScrollComplete({ + scrollTop: this.__scrollTop, + scrollLeft: this.__scrollLeft + }); + } } - } - }); - - //Publish scrollView to parent so children can access it - scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView; - - $scope.$on('$viewContentLoaded', function(e, viewHistoryData) { - viewHistoryData || (viewHistoryData = {}); - var scroll = viewHistoryData.scrollValues; - if (scroll) { - $timeout(function() { - scrollView.scrollTo(+scroll.left || null, +scroll.top || null); - }, 0); - } - - //Save scroll onto viewHistoryData when scope is destroyed - $scope.$on('$destroy', function() { - viewHistoryData.scrollValues = scrollView.getValues(); }); + //Publish scrollView to parent so children can access it + scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView; + + $scope.$on('$viewContentLoaded', function(e, viewHistoryData) { + viewHistoryData || (viewHistoryData = {}); + var scroll = viewHistoryData.scrollValues; + if (scroll) { + $timeout(function() { + scrollView.scrollTo(+scroll.left || null, +scroll.top || null); + }, 0); + } + + //Save scroll onto viewHistoryData when scope is destroyed + $scope.$on('$destroy', function() { + viewHistoryData.scrollValues = scrollView.getValues(); + }); + }); + + if(attr.refreshComplete) { + $scope.refreshComplete = function() { + if($scope.scrollView) { + scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active'); + scrollView.finishPullToRefresh(); + $scope.$parent.$broadcast('scroll.onRefreshComplete'); + } + }; + } + + } + + transclude($scope, function(clone) { + if (scrollCtrl) { + clone.data('$$ionicScrollController', scrollCtrl); + } + scrollContent.append(clone); }); - if(attr.refreshComplete) { - $scope.refreshComplete = function() { - if($scope.scrollView) { - scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active'); - scrollView.finishPullToRefresh(); - $scope.$parent.$broadcast('scroll.onRefreshComplete'); - } - }; - } } } }; @@ -184,48 +187,65 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) { }; }) -.directive('ionInfiniteScroll', ['$ionicBind', function($ionicBind) { +/** + * @ngdoc directive + * @name ionInfiniteScroll + * @module ionic + * @restrict E + * + * @description + * TODO make complete. Mention 'scroll.infiniteScrollComplete' event + * Mention using ng-if if you wish to stop loading more. + * + * @param {expression} on-scroll What to call when the scroller reaches the bottom. + * @param {string=} distance The distance from the bottom that the scroll must reach to trigger the on-scroll expression. Default 1%. + */ +.directive('ionInfiniteScroll', ['$timeout', function($timeout) { return { restrict: 'E', - require: '^?$ionicScroll', + require: ['^$ionicScroll', 'ionInfiniteScroll'], template: - '
' + - '
' + - '' + - '
' + - '
', - link: function($scope, $element, $attrs, scrollCtrl) { - setTimeout(function() { - var scrollCtrl = $element.controller('$ionicScroll'); - var scrollView = scrollCtrl.scrollView; + '
' + + '
' + + '' + + '
' + + '
', + controller: ['$scope', '$attrs', function($scope, $attrs) { + this.isLoading = false; + this.scrollView = null; //given by link function + this.getMaxScroll = function() { + var dist = $attrs.distance || + //deprecated: allow infiniteScrollDistance from ionContent + $scope.infiniteScrollDistance || + '1%'; + return dist.indexOf('%') > -1 ? + this.scrollView.getScrollMax().top * (1 - parseInt(dist,10) / 100) : + this.scrollView.getScrollMax().top - parseInt(dist, 10); + }; + }], + link: function($scope, $element, $attrs, ctrls) { + var scrollCtrl = ctrls[0]; + var infiniteScrollCtrl = ctrls[1]; + var scrollView = infiniteScrollCtrl.scrollView = scrollCtrl.scrollView; - $ionicBind($scope, $attrs, { - distance: '@infiniteScrollDistance' - }); - function maxScroll() { - var dist = $scope.distance || '1%'; - return dist.indexOf('%') > -1 ? - scrollView.getScrollMax().top * (1 - parseInt(dist,10) / 100) : - scrollView.getScrollMax().top - parseInt(dist, 10); - } - - var infiniteScrolling = false; - $scope.$on('scroll.infiniteScrollComplete', function() { - $element[0].classList.remove('active'); - setTimeout(function() { - scrollView.resize(); - }); - infiniteScrolling = false; - }); - - scrollCtrl.$element.on('scroll', ionic.animationFrameThrottle(function() { - if (!infiniteScrolling && scrollView.getValues().top >= maxScroll()) { - $element[0].classList.add('active'); - infiniteScrolling = true; - $scope.$apply(angular.bind($scope, $scope.$onInfiniteScroll)); - } - })); + $scope.$on('scroll.infiniteScrollComplete', function() { + $element[0].classList.remove('active'); + $timeout(function() { + scrollView.resize(); + }, 0, false); + infiniteScrollCtrl.isLoading = false; }); + + scrollCtrl.$element.on('scroll', ionic.animationFrameThrottle(function() { + if (!infiniteScrollCtrl.isLoading && + scrollView.getValues().top >= infiniteScrollCtrl.getMaxScroll()) { + $element[0].classList.add('active'); + infiniteScrollCtrl.isLoading = true; + + //deprecated: allow $onInfiniteScroll from parent + $scope.$apply($attrs.onInfinite || $scope.$onInfiniteScroll); + } + })); } }; }]); diff --git a/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js b/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js new file mode 100644 index 0000000000..471646dccb --- /dev/null +++ b/js/ext/angular/test/directive/ionicInfiniteScroll.unit.js @@ -0,0 +1,128 @@ +describe('ionicInfiniteScroll directive', function() { + beforeEach(module('ionic')); + + var scrollTopValue; + var scrollMaxValue; + var ctrl; + function setup(attrs, scopeProps, noCtrl) { + var element; + scrollTopValue = 50; + scrollMaxValue = 101; + inject(function($rootScope, $compile) { + var scope = $rootScope.$new(); + angular.extend(scope, scopeProps || {}); + element = angular.element(''); + ionic.animationFrameThrottle = function(cb) { return function() { cb(); }; }; + if (!noCtrl) { + element.data('$$ionicScrollController', { + scrollView: { + getValues: jasmine.createSpy('getValues').andCallFake(function() { + return { top: scrollTopValue }; + }), + getScrollMax: jasmine.createSpy('getScrollMax').andCallFake(function() { + return { top: scrollMaxValue }; + }), + resize: jasmine.createSpy('resize') + }, + resize: jasmine.createSpy('resize'), + $element: angular.element('
') + }); + } + $compile(element)(scope); + ctrl = element.controller('ionInfiniteScroll'); + scope.$apply(); + }); + return element; + } + + it('should error if no ionicScroll parent', function() { + expect(function() { + inject(function($compile, $rootScope) { + $compile('')($rootScope.$new()); + }); + }).toThrow(); + }); + + it('should not have class or be loading by default', function() { + var el = setup(); + expect(el.hasClass('active')).toBe(false); + expect(ctrl.isLoading).toBe(false); + }); + + describe('getMaxScroll', function() { + it('getMaxScroll should default to 1%', function() { + var el = setup(); + expect(ctrl.getMaxScroll()).toBe(101 * 0.99); + }); + + it('getMaxScroll should use attr.distance as number', function() { + var el = setup('distance=3'); + expect(ctrl.getMaxScroll()).toBe(98); + }); + + it('getMaxScroll should use attr.distance as percent', function() { + var el = setup('distance=5%'); + expect(ctrl.getMaxScroll()).toBe(101 * 0.95); + }); + + it('getMaxScroll should use scope.infiniteScrolDistance as number', function() { + var el = setup('', { infiniteScrollDistance: '11' }); + expect(ctrl.getMaxScroll()).toBe(90); + }); + + it('getMaxScroll should use scope.infiniteScrolDistance as percent', function() { + var el = setup('', { infiniteScrollDistance: '50%' }); + expect(ctrl.getMaxScroll()).toBe(101 * 0.5); + }); + }); + + describe('scroll event', function() { + + it('should add active and call attr.onInfinite if past top', function() { + var el = setup('on-infinite="foo=1"'); + scrollTopValue = scrollMaxValue; + el.controller('$ionicScroll').$element.triggerHandler('scroll'); + + expect(el.hasClass('active')).toBe(true); + expect(ctrl.isLoading).toBe(true); + expect(el.scope().foo).toBe(1); + }); + it('should add active and call $scope.$onInfiniteScroll if past top', function() { + var onScrollSpy = jasmine.createSpy('onInfiniteScroll'); + var el = setup('', { $onInfiniteScroll: onScrollSpy }); + scrollTopValue = scrollMaxValue; + el.controller('$ionicScroll').$element.triggerHandler('scroll'); + + expect(el.hasClass('active')).toBe(true); + expect(ctrl.isLoading).toBe(true); + expect(onScrollSpy).toHaveBeenCalled(); + }); + it('should not run the event twice if isLoading is true', function() { + var onScrollSpy = jasmine.createSpy('onInfiniteScroll'); + var el = setup('', { $onInfiniteScroll: onScrollSpy }); + scrollTopValue = scrollMaxValue; + el.controller('$ionicScroll').$element.triggerHandler('scroll'); + + expect(el.hasClass('active')).toBe(true); + el.removeClass('active'); + expect(el.hasClass('active')).toBe(false); + + el.controller('$ionicScroll').$element.triggerHandler('scroll'); + expect(el.hasClass('active')).toBe(false); + }); + + }); + + it('scroll.infiniteScrollComplete should work', inject(function($timeout) { + var el = setup(); + ctrl.isLoading = true; + el.addClass('active'); + el.scope().$broadcast('scroll.infiniteScrollComplete'); + expect(ctrl.isLoading).toBe(false); + expect(el.hasClass('active')).toBe(false); + expect(el.controller('$ionicScroll').scrollView.resize).not.toHaveBeenCalled(); + $timeout.flush(); + expect(el.controller('$ionicScroll').scrollView.resize).toHaveBeenCalled(); + })); + +});