Skip to content

Commit

Permalink
feat(ionRefresher): allow custom text & icons
Browse files Browse the repository at this point in the history
Closes #760

BREAKING CHANGE: on-refresh and on-refresh-opening are no longer on the
ion-content directive.  They are on the ion-refresher. In addition,
on-refresh-opening has been renamed to on-pulling.

Change your code from this:

```html
<ion-content on-refresh="onRefresh()"
  on-refresh-opening="onRefreshOpening()">
  <ion-refresher></ion-refresher>
</ion-content>
```

To this:

```html
<ion-content>
  <ion-refresher on-refresh="onRefresh()"
    on-pulling="onRefreshOpening()">
  </ion-refresher>
</ion-content>
```
  • Loading branch information
ajoslin committed Mar 11, 2014
1 parent e403233 commit 573df56
Show file tree
Hide file tree
Showing 7 changed files with 228 additions and 81 deletions.
46 changes: 27 additions & 19 deletions js/ext/angular/src/controller/ionicScrollController.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,28 +36,36 @@ angular.module('ionic.ui.scroll')
scrollView.resize();
}

this.setRefresher = function(refresherScope, refresherElement) {
var refresher = this.refresher = refresherElement;
var refresherHeight = self.refresher.clientHeight || 0;
scrollView.activatePullToRefresh(refresherHeight, function() {
refresher.classList.add('active');
refresherScope.$onRefreshOpening();
}, function() {
refresher.classList.remove('refreshing');
refresher.classList.remove('active');
}, function() {
refresher.classList.add('refreshing');
refresherScope.$onRefresh();
});
};

$timeout(function() {
scrollView.run();

self.refresher = element.querySelector('.scroll-refresher');

// Activate pull-to-refresh
if(self.refresher) {
var refresherHeight = self.refresher.clientHeight || 0;
scrollView.activatePullToRefresh(refresherHeight, function() {
self.refresher.classList.add('active');
$scope.$onRefreshOpening && $scope.$onRefreshOpening();
}, function() {
self.refresher.classList.remove('refreshing');
self.refresher.classList.remove('active');
}, function() {
self.refresher.classList.add('refreshing');
$scope.$onRefresh && $scope.$onRefresh();
$scope.$parent.$broadcast('scroll.onRefresh');
});
}
});

}]);

})();

var popups = [];
function showPopup() {
var newPopupDeferred = $q.defer();
$q.all(popups).then(showThisPopup);

popups.push(newPopupDeferred);

function showThisPopup() {
popups.splice(popups.indexOf(newPopupDeferred.promise), 1);
}
}
117 changes: 90 additions & 27 deletions js/ext/angular/src/directive/ionicContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,10 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {

$ionicBind($scope, $attr, {
//Use $ to stop onRefresh from recursively calling itself
//DEPRECATED, use <ion-infinite-scroll on-infinite-scroll="">
$onRefresh: '&onRefresh',
$onRefreshOpening: '&onRefreshOpening',
$onScroll: '&onScroll',
$onScrollComplete: '&onScrollComplete',
//DEPRECATED, use <ion-infinite-scroll on-infinite-scroll="">
$onInfiniteScroll: '&onInfiniteScroll',
refreshComplete: '=',
infiniteScrollDistance: '@',
hasBouncing: '@',
scroll: '@',
padding: '@',
Expand Down Expand Up @@ -154,8 +149,6 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
if(attr.refreshComplete) {
$scope.refreshComplete = function() {
if($scope.scrollView) {
scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active');
scrollView.finishPullToRefresh();
$scope.$parent.$broadcast('scroll.onRefreshComplete');
}
};
Expand All @@ -175,24 +168,99 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
};
}])

.directive('ionRefresher', function() {
/**
* @ngdoc directive
* @name ionRefresher
* @module ionic
* @restrict E
* @parent ionContent, ionScroll
* @description
* Allows you to add pull-to-refresh to a scrollView.
*
* Place it as the first child of your {@link ionic.directive:ionContent} or
* {@link ionic.directive:ionScroll} element.
*
* When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
* from your controller.
*
* @param {expression=} on-refresh Called when the user pulls down enough and lets go
* of the refresher.
* @param {expression=} on-pulling Called when the user starts to pull down
* on the refresher.
* @param {string=} pulling-icon The icon to display while the user is pulling down.
* Default: 'ion-arrow-down-c'.
* @param {string=} pulling-text The text to display while the user is pulling down.
* @param {string=} refreshing-icon The icon to display after user lets go of the
* refresher.
* @param {string=} refreshing-text The text to display after the user lets go of
* the refresher.
*
* @usage
* ```html
* <ion-content ng-controller="MyController">
* <ion-refresher
* pulling-text="Pull to refresh..."
* on-refresh="doRefresh()">
* </ion-refresher>
* <ion-list>
* <ion-item ng-repeat="item in items"></ion-item>
* </ion-list>
* </ion-content>
* ```
* ```js
* angular.module('testApp', ['ionic'])
* .controller('MyController', function($scope, $http) {
* $scope.items = [1,2,3];
* $scope.doRefresh = function() {
* $http.get('/new-items').success(function(newItems) {
* $scope.items = newItems;
* //Stop the ion-refresher from spinning
* $scope.$broadcast('scroll.refreshComplete');
* });
* };
* });
* ```
*/
.directive('ionRefresher', ['$ionicBind', function($ionicBind) {
return {
restrict: 'E',
replace: true,
require: ['^?ionContent', '^?ionList'],
template: '<div class="scroll-refresher"><div class="ionic-refresher-content"><i class="icon ion-arrow-down-c icon-pulling"></i><i class="icon ion-loading-d icon-refreshing"></i></div></div>',
scope: true
};
})
require: '^$ionicScroll',
template:
'<div class="scroll-refresher">' +
'<div class="ionic-refresher-content">' +
'<i class="icon {{pullingIcon}} icon-pulling"></i>' +
'<span class="icon-pulling" ng-bind-html="pullingText"></span>' +
'<i class="icon {{refreshingIcon}} icon-refreshing"></i>' +
'<span class="icon-refreshing" ng-bind-html="refreshingText"></span>' +
'</div>' +
'</div>',
compile: function($element, $attrs) {
if (angular.isUndefined($attrs.pullingIcon)) {
$attrs.$set('pullingIcon', 'ion-arrow-down-c');
}
if (angular.isUndefined($attrs.refreshingIcon)) {
$attrs.$set('refreshingIcon', 'ion-loading-d');
}
return function($scope, $element, $attrs, scrollCtrl) {
$ionicBind($scope, $attrs, {
pullingIcon: '@',
pullingText: '@',
refreshingIcon: '@',
refreshingText: '@',
$onRefresh: '&onRefresh',
$onRefreshOpening: '&onRefreshOpening'
});

.directive('ionScrollRefresher', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
template: '<div class="scroll-refresher"><div class="scroll-refresher-content" ng-transclude></div></div>'
scrollCtrl.setRefresher($scope, $element[0]);
$scope.$on('scroll.refreshComplete', function() {
$element[0].classList.remove('active');
scrollCtrl.scrollView.finishPullToRefresh();
});
};
}
};
})
}])

/**
* @ngdoc directive
Expand Down Expand Up @@ -261,10 +329,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
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%';
var dist = $attrs.distance || '1%';
return dist.indexOf('%') > -1 ?
this.scrollView.getScrollMax().top * (1 - parseInt(dist,10) / 100) :
this.scrollView.getScrollMax().top - parseInt(dist, 10);
Expand Down Expand Up @@ -292,9 +357,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
scrollView.getValues().top >= infiniteScrollCtrl.getMaxScroll()) {
$element[0].classList.add('active');
infiniteScrollCtrl.isLoading = true;

//deprecated: allow $onInfiniteScroll from parent
$scope.$apply($attrs.onInfinite || $scope.$onInfiniteScroll);
$scope.$parent.$apply($attrs.onInfinite || '');
}
}));
}
Expand Down
16 changes: 5 additions & 11 deletions js/ext/angular/test/controller/ionicScrollController.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,24 +69,15 @@ describe('$ionicScroll Controller', function() {
expect($ionicScrollDelegate.register).toHaveBeenCalledWith(scope, ctrl.$element, ctrl.scrollView);
}));

it('should not setup if no child .scroll-refresher', function() {
it('should not activatePullToRefresh if setRefresher is not called', function() {
setup();
timeout.flush();
expect(ctrl.refresher).toBeFalsy();
spyOn(ctrl.scrollView, 'activatePullToRefresh');
expect(ctrl.scrollView.activatePullToRefresh).not.toHaveBeenCalled();
});

it('should not setup ctrl.refresher until after timeout', function() {
setup({
el: angular.element('<div><div class="scroll-refresher"></div></div>')[0]
});
expect(ctrl.refresher).toBeUndefined();
timeout.flush();
expect(ctrl.refresher).toBe(ctrl.element.children[0]);
});

it('should work with .scroll-refresher child and proper refresher', function() {
it('should activatePullToRefresh and work when setRefresher', function() {
var startCb, refreshingCb, doneCb, refresherEl;
setup({
el: angular.element('<div><div class="scroll-refresher"></div></div>')[0]
Expand All @@ -96,6 +87,9 @@ describe('$ionicScroll Controller', function() {
refreshingCb = refreshing;
doneCb = done;
});
ctrl.setRefresher(scope, ctrl.element);

var scrollOnRefreshSpy = jasmine.createSpy('scroll.onRefresh');

scope.$onRefresh = jasmine.createSpy('onRefresh');
scope.$onRefreshOpening = jasmine.createSpy('onRefreshOpening');
Expand Down
20 changes: 0 additions & 20 deletions js/ext/angular/test/directive/ionicInfiniteScroll.unit.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,6 @@ describe('ionicInfiniteScroll directive', 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() {
Expand All @@ -107,16 +97,6 @@ describe('ionicInfiniteScroll directive', function() {
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 });
Expand Down
102 changes: 102 additions & 0 deletions js/ext/angular/test/directive/ionicRefresher.unit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
describe('ionRefresher directive', function() {
beforeEach(module('ionic'));
function setup(attrs, scopeProps) {
var el;
inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
var ionicScrollCtrl = {
setRefresher: jasmine.createSpy('setRefresher'),
scrollView: {
finishPullToRefresh: jasmine.createSpy('finishPullToRefresh')
}
};

angular.extend(scope, scopeProps || {});

el = angular.element('<ion-refresher '+(attrs||'')+'></ion-refresher>');
el.data('$$ionicScrollController', ionicScrollCtrl);

$compile(el)(scope);
$rootScope.$apply();
});
return el;
}

it('should error without ionicScroll', inject(function($compile, $rootScope) {
expect(function() {
$compile('<ion-refresher>')($rootScope);
}).toThrow();
}));

it('should bind $onRefresh', function() {
var refreshSpy = jasmine.createSpy('onRefresh');
var el = setup('on-refresh="refreshSpy()"', {
refreshSpy: refreshSpy
});
expect(refreshSpy).not.toHaveBeenCalled();
el.scope().$onRefresh();
expect(refreshSpy).toHaveBeenCalled();
});

it('should bind $onRefreshOpening', function() {
var spyMe = jasmine.createSpy('onRefreshOpening');
var el = setup('on-refresh-opening="spyMe()"', {
spyMe: spyMe
});
expect(spyMe).not.toHaveBeenCalled();
el.scope().$onRefreshOpening();
expect(spyMe).toHaveBeenCalled();
});

it('should setRefresher on scrollCtrl', function() {
var el = setup();
expect(el.controller('$ionicScroll').setRefresher.callCount).toBe(1);
expect(el.controller('$ionicScroll').setRefresher).toHaveBeenCalledWith(
el.scope(), el[0]
)
});

it('should listen for scroll.refreshComplete', function() {
var el = setup();
el.addClass('active');
var ctrl = el.controller('$ionicScroll');
expect(ctrl.scrollView.finishPullToRefresh).not.toHaveBeenCalled();
el.scope().$broadcast('scroll.refreshComplete');
expect(el.hasClass('active')).toBe(false);
expect(ctrl.scrollView.finishPullToRefresh).toHaveBeenCalled();
});

it('should have default pullingIcon', function() {
var el = setup();
expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeTruthy();
});
it('should allow custom pullingIcon', function() {
var el = setup('pulling-icon="super-icon"');
expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeFalsy();
expect(el[0].querySelector('.icon.icon-pulling.super-icon')).toBeTruthy();
});

it('should have default refreshingIcon', function() {
var el = setup();
expect(el[0].querySelector('.icon.icon-refreshing.ion-loading-d')).toBeTruthy();
});
it('should allow custom refreshingIcon', function() {
var el = setup('refreshing-icon="monkey-icon"');
expect(el[0].querySelector('.icon.icon-refreshing.ion-arrow-down-c')).toBeFalsy();
expect(el[0].querySelector('.icon.icon-refreshing.monkey-icon')).toBeTruthy();
});

it('should have no text by default', function() {
var el = setup();
expect(el.text().trim()).toBe('');
});
it('should allow pullingText', function() {
var el = setup('pulling-text="{{2+2}} <b>some</b> text"');
expect(el[0].querySelector('span.icon-pulling').innerHTML).toBe('4 <b>some</b> text');
});
it('should allow refreshingText', function() {
var el = setup('refreshing-text="{{3+2}} <b>text</b>"');
expect(el[0].querySelector('span.icon-refreshing').innerHTML).toBe('5 <b>text</b>');
});

});
Loading

0 comments on commit 573df56

Please sign in to comment.