diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js
index f69f187d69..ea04c436f8 100644
--- a/js/ext/angular/src/directive/ionicContent.js
+++ b/js/ext/angular/src/directive/ionicContent.js
@@ -18,33 +18,23 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
// The content directive is a core scrollable content area
// that is part of many View hierarchies
-.directive('ionContent', ['$parse', '$timeout', '$ionicScrollDelegate', '$controller', function($parse, $timeout, $ionicScrollDelegate, $controller) {
+.directive('ionContent', [
+ '$parse',
+ '$timeout',
+ '$ionicScrollDelegate',
+ '$controller',
+ '$ionicBind',
+function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBind) {
return {
restrict: 'E',
replace: true,
- template: '
',
transclude: true,
require: '^?ionNavView',
- scope: {
- onRefresh: '&',
- onRefreshOpening: '&',
- onScroll: '&',
- onScrollComplete: '&',
- refreshComplete: '=',
- onInfiniteScroll: '=',
- infiniteScrollDistance: '@',
- hasBouncing: '@',
- scroll: '@',
- padding: '@',
- hasScrollX: '@',
- hasScrollY: '@',
- scrollbarX: '@',
- scrollbarY: '@',
- startX: '@',
- startY: '@',
- scrollEventInterval: '@'
- },
-
+ scope: true,
+ template:
+ '',
compile: function(element, attr, transclude) {
if(attr.hasHeader == "true") { element.addClass('has-header'); }
if(attr.hasSubheader == "true") { element.addClass('has-subheader'); }
@@ -60,7 +50,31 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
function prelink($scope, $element, $attr, navViewCtrl) {
var clone, sc, scrollView, scrollCtrl,
- c = angular.element($element.children()[0]);
+ scrollContent = angular.element($element[0].querySelector('.scroll'));
+
+ transclude($scope, function(clone) {
+ scrollContent.append(clone);
+ });
+
+ $ionicBind($scope, $attr, {
+ onRefresh: '&',
+ onRefreshOpening: '&',
+ onScroll: '&',
+ onScrollComplete: '&',
+ refreshComplete: '=',
+ onInfiniteScroll: '&',
+ infiniteScrollDistance: '@',
+ hasBouncing: '@',
+ scroll: '@',
+ padding: '@',
+ hasScrollX: '@',
+ hasScrollY: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ startX: '@',
+ startY: '@',
+ scrollEventInterval: '@'
+ });
if($scope.scroll === "false") {
// No scrolling
@@ -92,6 +106,7 @@ angular.module('ionic.ui.content', ['ionic.ui.service', 'ionic.ui.scroll'])
}
}
});
+
//Publish scrollView to parent so children can access it
scrollView = $scope.$parent.scrollView = scrollCtrl.scrollView;
diff --git a/js/ext/angular/src/ionicAngular.js b/js/ext/angular/src/ionicAngular.js
index e0a14257c2..5e0fbf9283 100644
--- a/js/ext/angular/src/ionicAngular.js
+++ b/js/ext/angular/src/ionicAngular.js
@@ -3,6 +3,7 @@
* modules.
*/
angular.module('ionic.service', [
+ 'ionic.service.bind',
'ionic.service.platform',
'ionic.service.actionSheet',
'ionic.service.gesture',
diff --git a/js/ext/angular/src/service/ionicBind.js b/js/ext/angular/src/service/ionicBind.js
new file mode 100644
index 0000000000..a06ce69abd
--- /dev/null
+++ b/js/ext/angular/src/service/ionicBind.js
@@ -0,0 +1,53 @@
+angular.module('ionic.service.bind', [])
+.factory('$ionicBind', ['$parse', '$interpolate', function($parse, $interpolate) {
+ var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
+ return function(scope, attrs, bindDefinition) {
+ angular.forEach(bindDefinition || {}, function (definition, scopeName) {
+ //Adapted from angular.js $compile
+ var match = definition.match(LOCAL_REGEXP) || [],
+ attrName = match[3] || scopeName,
+ mode = match[1], // @, =, or &
+ parentGet,
+ unwatch;
+
+ switch(mode) {
+ case '@':
+ if (!attrs[attrName]) {
+ return;
+ }
+ attrs.$observe(attrName, function(value) {
+ scope[scopeName] = value;
+ });
+ // we trigger an interpolation to ensure
+ // the value is there for use immediately
+ if (attrs[attrName]) {
+ scope[scopeName] = $interpolate(attrs[attrName])(scope);
+ }
+ break;
+
+ case '=':
+ if (!attrs[attrName]) {
+ return;
+ }
+ unwatch = scope.$watch(attrs[attrName], function(value) {
+ scope[scopeName] = value;
+ });
+ //Destroy parent scope watcher when this scope is destroyed
+ scope.$on('$destroy', unwatch);
+ break;
+
+ case '&':
+ /* jshint -W044 */
+ if (attrs[attrName] && attrs[attrName].match(RegExp(scopeName + '\(.*?\)'))) {
+ throw new Error('& expression binding "' + scopeName + '" looks like it will recursively call "' +
+ attrs[attrName] + '" and cause a stack overflow! Please choose a different scopeName.');
+ }
+ parentGet = $parse(attrs[attrName]);
+ scope[scopeName] = function(locals) {
+ return parentGet(scope, locals);
+ };
+ break;
+ }
+ });
+ };
+}]);
diff --git a/js/ext/angular/test/directive/ionicContent.unit.js b/js/ext/angular/test/directive/ionicContent.unit.js
index e9e4ec1ebf..41f18159db 100644
--- a/js/ext/angular/test/directive/ionicContent.unit.js
+++ b/js/ext/angular/test/directive/ionicContent.unit.js
@@ -1,5 +1,5 @@
describe('Ionic Content directive', function() {
- var compile, element, scope;
+ var compile, scope;
beforeEach(module('ionic'));
@@ -12,22 +12,22 @@ describe('Ionic Content directive', function() {
}));
it('Has $ionicScroll controller', function() {
- element = compile('')(scope);
+ var element = compile('')(scope);
expect(element.controller('$ionicScroll').element).toBe(element[0]);
});
it('Has content class', function() {
- element = compile('')(scope);
+ var element = compile('')(scope);
expect(element.hasClass('scroll-content')).toBe(true);
});
it('Has header', function() {
- element = compile('')(scope);
+ var element = compile('')(scope);
expect(element.hasClass('has-header')).toEqual(true);
});
it('should add padding classname', function() {
- element = compile('')(scope);
+ var element = compile('')(scope);
expect(element.hasClass('scroll-content')).toEqual(true);
expect(element.hasClass('padding')).toEqual(false);
var scrollElement = element.find('.scroll');
@@ -36,7 +36,7 @@ describe('Ionic Content directive', function() {
// it('Enables bouncing by default', function() {
// ionic.Platform.setPlatform('iPhone');
- // element = compile('')(scope);
+ // var element = compile('')(scope);
// scope.$apply();
// var newScope = element.isolateScope();
// var scrollView = scope.scrollView;
@@ -45,7 +45,7 @@ describe('Ionic Content directive', function() {
it('Disables bouncing when has-bouncing = false', function() {
ionic.Platform.setPlatform('iPhone');
- element = compile('')(scope);
+ var element = compile('')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
@@ -54,7 +54,7 @@ describe('Ionic Content directive', function() {
it('Disables bouncing by default on Android', function() {
ionic.Platform.setPlatform('Android');
- element = compile('')(scope);
+ var element = compile('')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
@@ -63,7 +63,7 @@ describe('Ionic Content directive', function() {
it('Disables bouncing by default on Android unless has-bouncing = true', function() {
ionic.Platform.setPlatform('Android');
- element = compile('')(scope);
+ var element = compile('')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
@@ -72,7 +72,7 @@ describe('Ionic Content directive', function() {
it('Should set start x and y', function() {
- element = compile('')(scope);
+ var element = compile('')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
@@ -139,3 +139,20 @@ describe('Ionic Content directive', function() {
});
});
});
+/* Tests #555 */
+describe('Ionic Content Directive scoping', function() {
+ beforeEach(module('ionic', function($controllerProvider) {
+ $controllerProvider.register('ContentTestCtrl', function($scope){
+ this.$scope = $scope;
+ });
+ }));
+ it('should have same scope as content', inject(function($compile, $rootScope) {
+ var element = $compile('' +
+ '' +
+ '')($rootScope.$new());
+ var contentScope = element.scope();
+ var ctrl = element.data('$ngControllerController');
+ expect(contentScope.myForm).toBeTruthy();
+ expect(ctrl.$scope.myForm).toBeTruthy();
+ }));
+});
diff --git a/js/ext/angular/test/list-fit.html b/js/ext/angular/test/list-fit.html
index 62b9333249..70715a8814 100644
--- a/js/ext/angular/test/list-fit.html
+++ b/js/ext/angular/test/list-fit.html
@@ -14,7 +14,7 @@
-
+
@@ -45,7 +45,11 @@
24
25
26
+ more {{$index}}
+
+
+
@@ -59,6 +63,12 @@ function ContentCtrl($scope, $timeout) {
$scope.$broadcast('scroll.refreshComplete');
}, 1000);
};
+ $scope.more = [];
+ $scope.addMore = function() {
+ for (var i=0; i<15; i++) {
+ $scope.more.push(i);
+ }
+ };
}
diff --git a/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js b/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js
index 063feab16b..08256d7553 100644
--- a/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js
+++ b/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js
@@ -34,7 +34,7 @@ describe('Ionic ScrollDelegate Service', function() {
it('scroll event', function() {
var scope = rootScope.$new();
var el = compile('')(scope);
- scope = el.isolateScope();
+ scope = el.scope();
scope.$apply();
var top, left;
scope.onScroll = jasmine.createSpy('scroll').andCallFake(function(data) {
diff --git a/js/ext/angular/test/service/ionicBind.unit.js b/js/ext/angular/test/service/ionicBind.unit.js
new file mode 100644
index 0000000000..32523305f2
--- /dev/null
+++ b/js/ext/angular/test/service/ionicBind.unit.js
@@ -0,0 +1,137 @@
+describe('$ionicBind', function() {
+ beforeEach(module('ionic.service.bind'));
+
+ var $bind, scope, attr, $observeFn;
+ beforeEach(inject(function($ionicBind, $rootScope, $interpolate) {
+ $bind = $ionicBind;
+ scope = $rootScope.$new();
+ attr = {
+ $observe: jasmine.createSpy('observe').andCallFake(function(name, fn) {
+ $observeFn = fn;
+ })
+ };
+ }));
+
+ describe('= bind', function() {
+
+ it('should bind expression to scope', function() {
+ scope.$parent.coffee = 2;
+ attr.eq = 'coffee';
+ $bind(scope, attr, {
+ eq: '='
+ });
+ scope.$apply();
+ expect(scope.eq).toEqual(2);
+ scope.$parent.$apply('coffee = 100');
+ expect(scope.eq).toEqual(100);
+ });
+
+ it('should allow binding a different name to scope', function() {
+ scope.$parent.coffee = 2;
+ attr.eq = 'coffee';
+ $bind(scope, attr, {
+ coolVar: '=eq'
+ });
+ scope.$apply();
+ expect(scope.coolVar).toEqual(scope.$parent.coffee);
+ scope.$parent.$apply('coffee = 100');
+ expect(scope.coolVar).toEqual(100);
+ });
+
+ it('should work as expected if bind name is same', function() {
+ scope.$parent.foo = 2;
+ attr.espresso = 'foo';
+ $bind(scope, attr, {
+ foo: '=espresso'
+ });
+ scope.$apply();
+ expect(scope.$parent.foo).toBe(2);
+ scope.$parent.$apply('foo = 4');
+ expect(scope.$parent.foo).toBe(4);
+ });
+
+ it('should unwatch on $destroy', function() {
+ var watchUnregister = jasmine.createSpy('watchUnreg');
+ spyOn(scope.$parent, '$watch').andCallFake(function() {
+ return watchUnregister;
+ });
+ attr.binding = 'something';
+ $bind(scope, attr, {
+ binding: '='
+ });
+ scope.$destroy();
+ expect(watchUnregister).toHaveBeenCalled();
+ });
+ });
+
+ describe ('@ bind', function() {
+
+ it('should bind expression to scope', function() {
+ scope.$parent.coffee = 'cool';
+ attr.special = '{{coffee}}';
+ $bind(scope, attr, {
+ special: '@'
+ });
+ expect(attr.$observe).toHaveBeenCalledWith('special', $observeFn);
+ expect(scope.special).toBe('cool');
+ scope.$parent.coffee = 'espresso';
+ $observeFn(scope.$parent.coffee);
+ expect(scope.special).toBe('espresso');
+ });
+
+ it('should allow binding a different name to scope', function() {
+ scope.$parent.coffee = 'cool';
+ attr.special = '{{coffee}}';
+ $bind(scope, attr, {
+ scopeName: '@special'
+ });
+ expect(scope.scopeName).toBe('cool');
+ scope.$parent.coffee = 'espresso';
+ $observeFn(scope.$parent.coffee);
+ expect(scope.scopeName).toBe('espresso');
+ });
+
+ it('should allow binding a different name to scope', function() {
+ scope.$parent.coffee = 'cool';
+ attr.special = '{{coffee}}';
+ $bind(scope, attr, {
+ coffee: '@special'
+ });
+ expect(scope.coffee).toBe('cool');
+ scope.$parent.coffee = 'espresso';
+ $observeFn(scope.$parent.coffee);
+ expect(scope.coffee).toBe('espresso');
+ });
+
+ });
+
+ describe('& bind', function() {
+
+ it('should bind expression to scope', function() {
+ attr.math = '1+1';
+ $bind(scope, attr, {
+ two: '&math'
+ });
+ expect(scope.two()).toBe(2);
+ });
+
+ it('should bind expression with different name to scope', function() {
+ attr.doIt = 'fun()';
+ scope.$parent.fun = function() {
+ return 'this is cool!';
+ };
+ $bind(scope, attr, {
+ party: '&doIt'
+ });
+ expect(scope.party()).toBe('this is cool!');
+ });
+
+ it('should error for similar scopeName and expression', function() {
+ scope.$parent.foo = function(){};
+ attr.bar = 'foo()';
+ expect(function() {
+ $bind(scope, attr, { foo: '&bar' });
+ }).toThrow();
+ });
+ });
+});