Skip to content

Commit 49e0dac

Browse files
committed
feat(ionContent): use child scope instead of isolate scope
Adds new '$ionicBind' service, which takes an object containing binding definitions (similar to angular directive isolate scope definition). Allows binding of any directive attribute & expressions from a scope, letting us do normal attribute -> scope binding without having to create isolate scopes. Closes #555. Closes #669
1 parent 9e74608 commit 49e0dac

File tree

7 files changed

+268
-35
lines changed

7 files changed

+268
-35
lines changed

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

+38-23
Original file line numberDiff line numberDiff line change
@@ -18,33 +18,23 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
1818

1919
// The content directive is a core scrollable content area
2020
// that is part of many View hierarchies
21-
.directive('ionContent', ['$parse', '$timeout', '$ionicScrollDelegate', '$controller', function($parse, $timeout, $ionicScrollDelegate, $controller) {
21+
.directive('ionContent', [
22+
'$parse',
23+
'$timeout',
24+
'$ionicScrollDelegate',
25+
'$controller',
26+
'$ionicBind',
27+
function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
2228
return {
2329
restrict: 'E',
2430
replace: true,
25-
template: '<div class="scroll-content"><div class="scroll" ng-transclude></div></div>',
2631
transclude: true,
2732
require: '^?ionNavView',
28-
scope: {
29-
onRefresh: '&',
30-
onRefreshOpening: '&',
31-
onScroll: '&',
32-
onScrollComplete: '&',
33-
refreshComplete: '=',
34-
onInfiniteScroll: '=',
35-
infiniteScrollDistance: '@',
36-
hasBouncing: '@',
37-
scroll: '@',
38-
padding: '@',
39-
hasScrollX: '@',
40-
hasScrollY: '@',
41-
scrollbarX: '@',
42-
scrollbarY: '@',
43-
startX: '@',
44-
startY: '@',
45-
scrollEventInterval: '@'
46-
},
47-
33+
scope: true,
34+
template:
35+
'<div class="scroll-content">' +
36+
'<div class="scroll"></div>' +
37+
'</div>',
4838
compile: function(element, attr, transclude) {
4939
if(attr.hasHeader == "true") { element.addClass('has-header'); }
5040
if(attr.hasSubheader == "true") { element.addClass('has-subheader'); }
@@ -60,7 +50,31 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
6050

6151
function prelink($scope, $element, $attr, navViewCtrl) {
6252
var clone, sc, scrollView, scrollCtrl,
63-
c = angular.element($element.children()[0]);
53+
scrollContent = angular.element($element[0].querySelector('.scroll'));
54+
55+
transclude($scope, function(clone) {
56+
scrollContent.append(clone);
57+
});
58+
59+
$ionicBind($scope, $attr, {
60+
onRefresh: '&',
61+
onRefreshOpening: '&',
62+
onScroll: '&',
63+
onScrollComplete: '&',
64+
refreshComplete: '=',
65+
onInfiniteScroll: '&',
66+
infiniteScrollDistance: '@',
67+
hasBouncing: '@',
68+
scroll: '@',
69+
padding: '@',
70+
hasScrollX: '@',
71+
hasScrollY: '@',
72+
scrollbarX: '@',
73+
scrollbarY: '@',
74+
startX: '@',
75+
startY: '@',
76+
scrollEventInterval: '@'
77+
});
6478

6579
if($scope.scroll === "false") {
6680
// No scrolling
@@ -92,6 +106,7 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
92106
}
93107
}
94108
});
109+
95110
//Publish scrollView to parent so children can access it
96111
scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView;
97112

Diff for: js/ext/angular/src/ionicAngular.js

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* modules.
44
*/
55
angular.module('ionic.service', [
6+
'ionic.service.bind',
67
'ionic.service.platform',
78
'ionic.service.actionSheet',
89
'ionic.service.gesture',

Diff for: js/ext/angular/src/service/ionicBind.js

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
angular.module('ionic.service.bind', [])
2+
.factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) {
3+
var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
4+
return function(scope, attrs, bindDefinition) {
5+
angular.forEach(bindDefinition || {}, function (definition, scopeName) {
6+
//Adapted from angular.js $compile
7+
var match = definition.match(LOCAL_REGEXP) || [],
8+
attrName = match[3] || scopeName,
9+
mode = match[1], // @, =, or &
10+
parentGet,
11+
unwatch;
12+
13+
switch(mode) {
14+
case '@':
15+
if (!attrs[attrName]) {
16+
return;
17+
}
18+
attrs.$observe(attrName, function(value) {
19+
scope[scopeName] = value;
20+
});
21+
// we trigger an interpolation to ensure
22+
// the value is there for use immediately
23+
if (attrs[attrName]) {
24+
scope[scopeName] = $interpolate(attrs[attrName])(scope);
25+
}
26+
break;
27+
28+
case '=':
29+
if (!attrs[attrName]) {
30+
return;
31+
}
32+
unwatch = scope.$watch(attrs[attrName], function(value) {
33+
scope[scopeName] = value;
34+
});
35+
//Destroy parent scope watcher when this scope is destroyed
36+
scope.$on('$destroy', unwatch);
37+
break;
38+
39+
case '&':
40+
/* jshint -W044 */
41+
if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
42+
throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
43+
attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
44+
}
45+
parentGet = $parse(attrs[attrName]);
46+
scope[scopeName] = function(locals) {
47+
return parentGet(scope, locals);
48+
};
49+
break;
50+
}
51+
});
52+
};
53+
}]);

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

+27-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
describe('Ionic Content directive', function() {
2-
var compile, element, scope;
2+
var compile, scope;
33

44
beforeEach(module('ionic'));
55

@@ -12,22 +12,22 @@ describe('Ionic Content directive', function() {
1212
}));
1313

1414
it('Has $ionicScroll controller', function() {
15-
element = compile('<ion-content></ion-content>')(scope);
15+
var element = compile('<ion-content></ion-content>')(scope);
1616
expect(element.controller('$ionicScroll').element).toBe(element[0]);
1717
});
1818

1919
it('Has content class', function() {
20-
element = compile('<ion-content></ion-content>')(scope);
20+
var element = compile('<ion-content></ion-content>')(scope);
2121
expect(element.hasClass('scroll-content')).toBe(true);
2222
});
2323

2424
it('Has header', function() {
25-
element = compile('<ion-content has-header="true"></ion-content>')(scope);
25+
var element = compile('<ion-content has-header="true"></ion-content>')(scope);
2626
expect(element.hasClass('has-header')).toEqual(true);
2727
});
2828

2929
it('should add padding classname', function() {
30-
element = compile('<ion-content padding="true"></ion-content>')(scope);
30+
var element = compile('<ion-content padding="true"></ion-content>')(scope);
3131
expect(element.hasClass('scroll-content')).toEqual(true);
3232
expect(element.hasClass('padding')).toEqual(false);
3333
var scrollElement = element.find('.scroll');
@@ -36,7 +36,7 @@ describe('Ionic Content directive', function() {
3636

3737
// it('Enables bouncing by default', function() {
3838
// ionic.Platform.setPlatform('iPhone');
39-
// element = compile('<ion-content has-header="true"></ion-content>')(scope);
39+
// var element = compile('<ion-content has-header="true"></ion-content>')(scope);
4040
// scope.$apply();
4141
// var newScope = element.isolateScope();
4242
// var scrollView = scope.scrollView;
@@ -45,7 +45,7 @@ describe('Ionic Content directive', function() {
4545

4646
it('Disables bouncing when has-bouncing = false', function() {
4747
ionic.Platform.setPlatform('iPhone');
48-
element = compile('<ion-content has-header="true" has-bouncing="false"></ion-content>')(scope);
48+
var element = compile('<ion-content has-header="true" has-bouncing="false"></ion-content>')(scope);
4949
scope.$apply();
5050
var newScope = element.isolateScope();
5151
var scrollView = scope.scrollView;
@@ -54,7 +54,7 @@ describe('Ionic Content directive', function() {
5454

5555
it('Disables bouncing by default on Android', function() {
5656
ionic.Platform.setPlatform('Android');
57-
element = compile('<ion-content has-header="true"></ion-content>')(scope);
57+
var element = compile('<ion-content has-header="true"></ion-content>')(scope);
5858
scope.$apply();
5959
var newScope = element.isolateScope();
6060
var scrollView = scope.scrollView;
@@ -63,7 +63,7 @@ describe('Ionic Content directive', function() {
6363

6464
it('Disables bouncing by default on Android unless has-bouncing = true', function() {
6565
ionic.Platform.setPlatform('Android');
66-
element = compile('<ion-content has-header="true" has-bouncing="true"></ion-content>')(scope);
66+
var element = compile('<ion-content has-header="true" has-bouncing="true"></ion-content>')(scope);
6767
scope.$apply();
6868
var newScope = element.isolateScope();
6969
var scrollView = scope.scrollView;
@@ -72,7 +72,7 @@ describe('Ionic Content directive', function() {
7272

7373

7474
it('Should set start x and y', function() {
75-
element = compile('<ion-content start-x="100" start-y="300" has-header="true"></ion-content>')(scope);
75+
var element = compile('<ion-content start-x="100" start-y="300" has-header="true"></ion-content>')(scope);
7676
scope.$apply();
7777
var newScope = element.isolateScope();
7878
var scrollView = scope.scrollView;
@@ -139,3 +139,20 @@ describe('Ionic Content directive', function() {
139139
});
140140
});
141141
});
142+
/* Tests #555 */
143+
describe('Ionic Content Directive scoping', function() {
144+
beforeEach(module('ionic', function($controllerProvider) {
145+
$controllerProvider.register('ContentTestCtrl', function($scope){
146+
this.$scope = $scope;
147+
});
148+
}));
149+
it('should have same scope as content', inject(function($compile, $rootScope) {
150+
var element = $compile('<ion-content ng-controller="ContentTestCtrl">' +
151+
'<form name="myForm"></form>' +
152+
'</ion-content>')($rootScope.$new());
153+
var contentScope = element.scope();
154+
var ctrl = element.data('$ngControllerController');
155+
expect(contentScope.myForm).toBeTruthy();
156+
expect(ctrl.$scope.myForm).toBeTruthy();
157+
}));
158+
});

Diff for: js/ext/angular/test/list-fit.html

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<ion-header-bar title="'Sample UL'" type="bar-positive"></ion-header-bar>
1616

17-
<ion-content has-header="true" scroll="true" ng-controller="ContentCtrl" on-refresh="onRefresh()" has-footer="true" padding="false">
17+
<ion-content on-infinite-scroll="addMore()" has-header="true" scroll="true" ng-controller="ContentCtrl" on-refresh="onRefresh()" has-footer="true" padding="false">
1818

1919
<ion-refresher></ion-refresher>
2020

@@ -45,7 +45,11 @@
4545
<li class="item">24</li>
4646
<li class="item">25</li>
4747
<li class="item">26</li>
48+
<li ng-repeat="i in more">more {{$index}}</li>
4849
</ul>
50+
51+
<ion-infinite-scroll></ion-infinite-scroll>
52+
4953
</ion-content>
5054

5155
<ion-footer-bar type="bar-assertive">
@@ -59,6 +63,12 @@ <h1 class="title">Footer!</h1>
5963
$scope.$broadcast('scroll.refreshComplete');
6064
}, 1000);
6165
};
66+
$scope.more = [];
67+
$scope.addMore = function() {
68+
for (var i=0; i<15; i++) {
69+
$scope.more.push(i);
70+
}
71+
};
6272
}
6373
</script>
6474

Diff for: js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ describe('Ionic ScrollDelegate Service', function() {
3434
it('scroll event', function() {
3535
var scope = rootScope.$new();
3636
var el = compile('<ion-content></ion-content>')(scope);
37-
scope = el.isolateScope();
37+
scope = el.scope();
3838
scope.$apply();
3939
var top, left;
4040
scope.onScroll = jasmine.createSpy('scroll').andCallFake(function(data) {

0 commit comments

Comments
 (0)