Skip to content

Commit 573df56

Browse files
committed
feat(ionRefresher): allow custom text & icons
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> ```
1 parent e403233 commit 573df56

File tree

7 files changed

+228
-81
lines changed

7 files changed

+228
-81
lines changed

Diff for: js/ext/angular/src/controller/ionicScrollController.js

+27-19
Original file line numberDiff line numberDiff line change
@@ -36,28 +36,36 @@ angular.module('ionic.ui.scroll')
3636
scrollView.resize();
3737
}
3838

39+
this.setRefresher = function(refresherScope, refresherElement) {
40+
var refresher = this.refresher = refresherElement;
41+
var refresherHeight = self.refresher.clientHeight || 0;
42+
scrollView.activatePullToRefresh(refresherHeight, function() {
43+
refresher.classList.add('active');
44+
refresherScope.$onRefreshOpening();
45+
}, function() {
46+
refresher.classList.remove('refreshing');
47+
refresher.classList.remove('active');
48+
}, function() {
49+
refresher.classList.add('refreshing');
50+
refresherScope.$onRefresh();
51+
});
52+
};
53+
3954
$timeout(function() {
4055
scrollView.run();
41-
42-
self.refresher = element.querySelector('.scroll-refresher');
43-
44-
// Activate pull-to-refresh
45-
if(self.refresher) {
46-
var refresherHeight = self.refresher.clientHeight || 0;
47-
scrollView.activatePullToRefresh(refresherHeight, function() {
48-
self.refresher.classList.add('active');
49-
$scope.$onRefreshOpening && $scope.$onRefreshOpening();
50-
}, function() {
51-
self.refresher.classList.remove('refreshing');
52-
self.refresher.classList.remove('active');
53-
}, function() {
54-
self.refresher.classList.add('refreshing');
55-
$scope.$onRefresh && $scope.$onRefresh();
56-
$scope.$parent.$broadcast('scroll.onRefresh');
57-
});
58-
}
5956
});
60-
6157
}]);
6258

6359
})();
60+
61+
var popups = [];
62+
function showPopup() {
63+
var newPopupDeferred = $q.defer();
64+
$q.all(popups).then(showThisPopup);
65+
66+
popups.push(newPopupDeferred);
67+
68+
function showThisPopup() {
69+
popups.splice(popups.indexOf(newPopupDeferred.promise), 1);
70+
}
71+
}

Diff for: js/ext/angular/src/directive/ionicContent.js

+90-27
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,10 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
8686

8787
$ionicBind($scope, $attr, {
8888
//Use $ to stop onRefresh from recursively calling itself
89-
//DEPRECATED, use <ion-infinite-scroll on-infinite-scroll="">
9089
$onRefresh: '&onRefresh',
9190
$onRefreshOpening: '&onRefreshOpening',
9291
$onScroll: '&onScroll',
9392
$onScrollComplete: '&onScrollComplete',
94-
//DEPRECATED, use <ion-infinite-scroll on-infinite-scroll="">
95-
$onInfiniteScroll: '&onInfiniteScroll',
96-
refreshComplete: '=',
97-
infiniteScrollDistance: '@',
9893
hasBouncing: '@',
9994
scroll: '@',
10095
padding: '@',
@@ -154,8 +149,6 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
154149
if(attr.refreshComplete) {
155150
$scope.refreshComplete = function() {
156151
if($scope.scrollView) {
157-
scrollCtrl.refresher && scrollCtrl.refresher.classList.remove('active');
158-
scrollView.finishPullToRefresh();
159152
$scope.$parent.$broadcast('scroll.onRefreshComplete');
160153
}
161154
};
@@ -175,24 +168,99 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
175168
};
176169
}])
177170

178-
.directive('ionRefresher', function() {
171+
/**
172+
* @ngdoc directive
173+
* @name ionRefresher
174+
* @module ionic
175+
* @restrict E
176+
* @parent ionContent, ionScroll
177+
* @description
178+
* Allows you to add pull-to-refresh to a scrollView.
179+
*
180+
* Place it as the first child of your {@link ionic.directive:ionContent} or
181+
* {@link ionic.directive:ionScroll} element.
182+
*
183+
* When refreshing is complete, $broadcast the 'scroll.refreshComplete' event
184+
* from your controller.
185+
*
186+
* @param {expression=} on-refresh Called when the user pulls down enough and lets go
187+
* of the refresher.
188+
* @param {expression=} on-pulling Called when the user starts to pull down
189+
* on the refresher.
190+
* @param {string=} pulling-icon The icon to display while the user is pulling down.
191+
* Default: 'ion-arrow-down-c'.
192+
* @param {string=} pulling-text The text to display while the user is pulling down.
193+
* @param {string=} refreshing-icon The icon to display after user lets go of the
194+
* refresher.
195+
* @param {string=} refreshing-text The text to display after the user lets go of
196+
* the refresher.
197+
*
198+
* @usage
199+
* ```html
200+
* <ion-content ng-controller="MyController">
201+
* <ion-refresher
202+
* pulling-text="Pull to refresh..."
203+
* on-refresh="doRefresh()">
204+
* </ion-refresher>
205+
* <ion-list>
206+
* <ion-item ng-repeat="item in items"></ion-item>
207+
* </ion-list>
208+
* </ion-content>
209+
* ```
210+
* ```js
211+
* angular.module('testApp', ['ionic'])
212+
* .controller('MyController', function($scope, $http) {
213+
* $scope.items = [1,2,3];
214+
* $scope.doRefresh = function() {
215+
* $http.get('/new-items').success(function(newItems) {
216+
* $scope.items = newItems;
217+
* //Stop the ion-refresher from spinning
218+
* $scope.$broadcast('scroll.refreshComplete');
219+
* });
220+
* };
221+
* });
222+
* ```
223+
*/
224+
.directive('ionRefresher', ['$ionicBind', function($ionicBind) {
179225
return {
180226
restrict: 'E',
181227
replace: true,
182-
require: ['^?ionContent', '^?ionList'],
183-
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>',
184-
scope: true
185-
};
186-
})
228+
require: '^$ionicScroll',
229+
template:
230+
'<div class="scroll-refresher">' +
231+
'<div class="ionic-refresher-content">' +
232+
'<i class="icon {{pullingIcon}} icon-pulling"></i>' +
233+
'<span class="icon-pulling" ng-bind-html="pullingText"></span>' +
234+
'<i class="icon {{refreshingIcon}} icon-refreshing"></i>' +
235+
'<span class="icon-refreshing" ng-bind-html="refreshingText"></span>' +
236+
'</div>' +
237+
'</div>',
238+
compile: function($element, $attrs) {
239+
if (angular.isUndefined($attrs.pullingIcon)) {
240+
$attrs.$set('pullingIcon', 'ion-arrow-down-c');
241+
}
242+
if (angular.isUndefined($attrs.refreshingIcon)) {
243+
$attrs.$set('refreshingIcon', 'ion-loading-d');
244+
}
245+
return function($scope, $element, $attrs, scrollCtrl) {
246+
$ionicBind($scope, $attrs, {
247+
pullingIcon: '@',
248+
pullingText: '@',
249+
refreshingIcon: '@',
250+
refreshingText: '@',
251+
$onRefresh: '&onRefresh',
252+
$onRefreshOpening: '&onRefreshOpening'
253+
});
187254

188-
.directive('ionScrollRefresher', function() {
189-
return {
190-
restrict: 'E',
191-
replace: true,
192-
transclude: true,
193-
template: '<div class="scroll-refresher"><div class="scroll-refresher-content" ng-transclude></div></div>'
255+
scrollCtrl.setRefresher($scope, $element[0]);
256+
$scope.$on('scroll.refreshComplete', function() {
257+
$element[0].classList.remove('active');
258+
scrollCtrl.scrollView.finishPullToRefresh();
259+
});
260+
};
261+
}
194262
};
195-
})
263+
}])
196264

197265
/**
198266
* @ngdoc directive
@@ -261,10 +329,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
261329
this.isLoading = false;
262330
this.scrollView = null; //given by link function
263331
this.getMaxScroll = function() {
264-
var dist = $attrs.distance ||
265-
//deprecated: allow infiniteScrollDistance from ionContent
266-
$scope.infiniteScrollDistance ||
267-
'1%';
332+
var dist = $attrs.distance || '1%';
268333
return dist.indexOf('%') > -1 ?
269334
this.scrollView.getScrollMax().top * (1 - parseInt(dist,10) / 100) :
270335
this.scrollView.getScrollMax().top - parseInt(dist, 10);
@@ -292,9 +357,7 @@ function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
292357
scrollView.getValues().top >= infiniteScrollCtrl.getMaxScroll()) {
293358
$element[0].classList.add('active');
294359
infiniteScrollCtrl.isLoading = true;
295-
296-
//deprecated: allow $onInfiniteScroll from parent
297-
$scope.$apply($attrs.onInfinite || $scope.$onInfiniteScroll);
360+
$scope.$parent.$apply($attrs.onInfinite || '');
298361
}
299362
}));
300363
}

Diff for: js/ext/angular/test/controller/ionicScrollController.unit.js

+5-11
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,15 @@ describe('$ionicScroll Controller', function() {
6969
expect($ionicScrollDelegate.register).toHaveBeenCalledWith(scope, ctrl.$element, ctrl.scrollView);
7070
}));
7171

72-
it('should not setup if no child .scroll-refresher', function() {
72+
it('should not activatePullToRefresh if setRefresher is not called', function() {
7373
setup();
7474
timeout.flush();
7575
expect(ctrl.refresher).toBeFalsy();
7676
spyOn(ctrl.scrollView, 'activatePullToRefresh');
7777
expect(ctrl.scrollView.activatePullToRefresh).not.toHaveBeenCalled();
7878
});
7979

80-
it('should not setup ctrl.refresher until after timeout', function() {
81-
setup({
82-
el: angular.element('<div><div class="scroll-refresher"></div></div>')[0]
83-
});
84-
expect(ctrl.refresher).toBeUndefined();
85-
timeout.flush();
86-
expect(ctrl.refresher).toBe(ctrl.element.children[0]);
87-
});
88-
89-
it('should work with .scroll-refresher child and proper refresher', function() {
80+
it('should activatePullToRefresh and work when setRefresher', function() {
9081
var startCb, refreshingCb, doneCb, refresherEl;
9182
setup({
9283
el: angular.element('<div><div class="scroll-refresher"></div></div>')[0]
@@ -96,6 +87,9 @@ describe('$ionicScroll Controller', function() {
9687
refreshingCb = refreshing;
9788
doneCb = done;
9889
});
90+
ctrl.setRefresher(scope, ctrl.element);
91+
92+
var scrollOnRefreshSpy = jasmine.createSpy('scroll.onRefresh');
9993

10094
scope.$onRefresh = jasmine.createSpy('onRefresh');
10195
scope.$onRefreshOpening = jasmine.createSpy('onRefreshOpening');

Diff for: js/ext/angular/test/directive/ionicInfiniteScroll.unit.js

-20
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,6 @@ describe('ionicInfiniteScroll directive', function() {
8484
var el = setup('distance=5%');
8585
expect(ctrl.getMaxScroll()).toBe(101 * 0.95);
8686
});
87-
88-
it('getMaxScroll should use scope.infiniteScrolDistance as number', function() {
89-
var el = setup('', { infiniteScrollDistance: '11' });
90-
expect(ctrl.getMaxScroll()).toBe(90);
91-
});
92-
93-
it('getMaxScroll should use scope.infiniteScrolDistance as percent', function() {
94-
var el = setup('', { infiniteScrollDistance: '50%' });
95-
expect(ctrl.getMaxScroll()).toBe(101 * 0.5);
96-
});
9787
});
9888

9989
describe('scroll event', function() {
@@ -107,16 +97,6 @@ describe('ionicInfiniteScroll directive', function() {
10797
expect(ctrl.isLoading).toBe(true);
10898
expect(el.scope().foo).toBe(1);
10999
});
110-
it('should add active and call $scope.$onInfiniteScroll if past top', function() {
111-
var onScrollSpy = jasmine.createSpy('onInfiniteScroll');
112-
var el = setup('', { $onInfiniteScroll: onScrollSpy });
113-
scrollTopValue = scrollMaxValue;
114-
el.controller('$ionicScroll').$element.triggerHandler('scroll');
115-
116-
expect(el.hasClass('active')).toBe(true);
117-
expect(ctrl.isLoading).toBe(true);
118-
expect(onScrollSpy).toHaveBeenCalled();
119-
});
120100
it('should not run the event twice if isLoading is true', function() {
121101
var onScrollSpy = jasmine.createSpy('onInfiniteScroll');
122102
var el = setup('', { $onInfiniteScroll: onScrollSpy });

Diff for: js/ext/angular/test/directive/ionicRefresher.unit.js

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
describe('ionRefresher directive', function() {
2+
beforeEach(module('ionic'));
3+
function setup(attrs, scopeProps) {
4+
var el;
5+
inject(function($compile, $rootScope) {
6+
var scope = $rootScope.$new();
7+
var ionicScrollCtrl = {
8+
setRefresher: jasmine.createSpy('setRefresher'),
9+
scrollView: {
10+
finishPullToRefresh: jasmine.createSpy('finishPullToRefresh')
11+
}
12+
};
13+
14+
angular.extend(scope, scopeProps || {});
15+
16+
el = angular.element('<ion-refresher '+(attrs||'')+'></ion-refresher>');
17+
el.data('$$ionicScrollController', ionicScrollCtrl);
18+
19+
$compile(el)(scope);
20+
$rootScope.$apply();
21+
});
22+
return el;
23+
}
24+
25+
it('should error without ionicScroll', inject(function($compile, $rootScope) {
26+
expect(function() {
27+
$compile('<ion-refresher>')($rootScope);
28+
}).toThrow();
29+
}));
30+
31+
it('should bind $onRefresh', function() {
32+
var refreshSpy = jasmine.createSpy('onRefresh');
33+
var el = setup('on-refresh="refreshSpy()"', {
34+
refreshSpy: refreshSpy
35+
});
36+
expect(refreshSpy).not.toHaveBeenCalled();
37+
el.scope().$onRefresh();
38+
expect(refreshSpy).toHaveBeenCalled();
39+
});
40+
41+
it('should bind $onRefreshOpening', function() {
42+
var spyMe = jasmine.createSpy('onRefreshOpening');
43+
var el = setup('on-refresh-opening="spyMe()"', {
44+
spyMe: spyMe
45+
});
46+
expect(spyMe).not.toHaveBeenCalled();
47+
el.scope().$onRefreshOpening();
48+
expect(spyMe).toHaveBeenCalled();
49+
});
50+
51+
it('should setRefresher on scrollCtrl', function() {
52+
var el = setup();
53+
expect(el.controller('$ionicScroll').setRefresher.callCount).toBe(1);
54+
expect(el.controller('$ionicScroll').setRefresher).toHaveBeenCalledWith(
55+
el.scope(), el[0]
56+
)
57+
});
58+
59+
it('should listen for scroll.refreshComplete', function() {
60+
var el = setup();
61+
el.addClass('active');
62+
var ctrl = el.controller('$ionicScroll');
63+
expect(ctrl.scrollView.finishPullToRefresh).not.toHaveBeenCalled();
64+
el.scope().$broadcast('scroll.refreshComplete');
65+
expect(el.hasClass('active')).toBe(false);
66+
expect(ctrl.scrollView.finishPullToRefresh).toHaveBeenCalled();
67+
});
68+
69+
it('should have default pullingIcon', function() {
70+
var el = setup();
71+
expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeTruthy();
72+
});
73+
it('should allow custom pullingIcon', function() {
74+
var el = setup('pulling-icon="super-icon"');
75+
expect(el[0].querySelector('.icon.icon-pulling.ion-arrow-down-c')).toBeFalsy();
76+
expect(el[0].querySelector('.icon.icon-pulling.super-icon')).toBeTruthy();
77+
});
78+
79+
it('should have default refreshingIcon', function() {
80+
var el = setup();
81+
expect(el[0].querySelector('.icon.icon-refreshing.ion-loading-d')).toBeTruthy();
82+
});
83+
it('should allow custom refreshingIcon', function() {
84+
var el = setup('refreshing-icon="monkey-icon"');
85+
expect(el[0].querySelector('.icon.icon-refreshing.ion-arrow-down-c')).toBeFalsy();
86+
expect(el[0].querySelector('.icon.icon-refreshing.monkey-icon')).toBeTruthy();
87+
});
88+
89+
it('should have no text by default', function() {
90+
var el = setup();
91+
expect(el.text().trim()).toBe('');
92+
});
93+
it('should allow pullingText', function() {
94+
var el = setup('pulling-text="{{2+2}} <b>some</b> text"');
95+
expect(el[0].querySelector('span.icon-pulling').innerHTML).toBe('4 <b>some</b> text');
96+
});
97+
it('should allow refreshingText', function() {
98+
var el = setup('refreshing-text="{{3+2}} <b>text</b>"');
99+
expect(el[0].querySelector('span.icon-refreshing').innerHTML).toBe('5 <b>text</b>');
100+
});
101+
102+
});

0 commit comments

Comments
 (0)