diff --git a/js/ext/angular/src/directive/ionicContent.js b/js/ext/angular/src/directive/ionicContent.js
index f69f187d696..f092133845a 100644
--- a/js/ext/angular/src/directive/ionicContent.js
+++ b/js/ext/angular/src/directive/ionicContent.js
@@ -18,33 +18,20 @@ 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',
+ '$ionicBindFromParent',
+function($parse, $timeout, $ionicScrollDelegate, $controller, $ionicBindFromParent) {
return {
restrict: 'E',
replace: true,
- template: '
',
+ 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,
compile: function(element, attr, transclude) {
if(attr.hasHeader == "true") { element.addClass('has-header'); }
if(attr.hasSubheader == "true") { element.addClass('has-subheader'); }
@@ -60,7 +47,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'));
+
+ $ionicBindFromParent($scope, $attr, {
+ onRefresh: '&',
+ onRefreshOpening: '&',
+ onScroll: '&',
+ onScrollComplete: '&',
+ refreshComplete: '=',
+ onInfiniteScroll: '=',
+ infiniteScrollDistance: '@',
+ hasBouncing: '@',
+ scroll: '@',
+ padding: '@',
+ hasScrollX: '@',
+ hasScrollY: '@',
+ scrollbarX: '@',
+ scrollbarY: '@',
+ startX: '@',
+ startY: '@',
+ scrollEventInterval: '@'
+ });
+
+ transclude($scope, function(clone) {
+ scrollContent.append(clone);
+ });
if($scope.scroll === "false") {
// No scrolling
diff --git a/js/ext/angular/src/ionicAngular.js b/js/ext/angular/src/ionicAngular.js
index e0a14257c2b..5e0fbf92833 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/ionicBindFromParent.js b/js/ext/angular/src/service/ionicBindFromParent.js
new file mode 100644
index 00000000000..c135e1465e8
--- /dev/null
+++ b/js/ext/angular/src/service/ionicBindFromParent.js
@@ -0,0 +1,53 @@
+angular.module('ionic.service.bind', [])
+.factory('$ionicBindFromParent', ['$parse', '$interpolate', function($parse, $interpolate) {
+ var LOCAL_REGEXP = /^\s*([@=&])(\??)\s*(\w*)\s*$/;
+ return function(scope, attrs, bindings) {
+ var parentScope = scope.$parent;
+ if (!parentScope) {
+ throw new Error('Cannot bind to $rootScope!');
+ }
+
+ angular.forEach(bindings || {}, 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])(parentScope);
+ }
+ break;
+
+ case '=':
+ if (!attrs[attrName]) {
+ return;
+ }
+ unwatch = parentScope.$watch(attrs[attrName], function(value) {
+ scope[scopeName] = value;
+ });
+ //Destroy parent scope watcher when this scope is destroyed
+ scope.$on('$destroy', unwatch);
+ break;
+
+ case '&':
+ parentGet = $parse(attrs[attrName]);
+ scope[scopeName] = function(locals) {
+ return parentGet(parentScope, locals);
+ };
+ break;
+ }
+ });
+ };
+}]);
diff --git a/js/ext/angular/test/directive/ionicContent.unit.js b/js/ext/angular/test/directive/ionicContent.unit.js
index e9e4ec1ebff..41f18159dbb 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/service/delegates/ionicScrollDelegate.unit.js b/js/ext/angular/test/service/delegates/ionicScrollDelegate.unit.js
index 063feab16b5..08256d75532 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/ionicBindFromParent.unit.js b/js/ext/angular/test/service/ionicBindFromParent.unit.js
new file mode 100644
index 00000000000..3785d54f534
--- /dev/null
+++ b/js/ext/angular/test/service/ionicBindFromParent.unit.js
@@ -0,0 +1,144 @@
+describe('$ionicBindFromParent', function() {
+ beforeEach(module('ionic.service.bind'));
+
+ var $bind, scope, attr, $observeFn;
+ beforeEach(inject(function($ionicBindFromParent, $rootScope, $interpolate) {
+ $bind = $ionicBindFromParent;
+ scope = $rootScope.$new();
+ attr = {
+ $observe: jasmine.createSpy('observe').andCallFake(function(name, fn) {
+ $observeFn = fn;
+ })
+ };
+ }));
+
+ it('should error if rootScope', inject(function($rootScope) {
+ expect(function() {
+ $bind($rootScope, {}, {});
+ }).toThrow();
+ }));
+
+ 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, {
+ espresso: '='
+ });
+ scope.$apply();
+ expect(scope.foo).toBe(2);
+ scope.$parent.$apply('foo = 4');
+ expect(scope.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 work as expected if scopeNames are the same', function() {
+ scope.$parent.fn = function() { return 1; };
+ attr.bad = 'fn()';
+ $bind(scope, attr, {
+ fn: '&bad'
+ });
+ expect(scope.fn()).toBe(1);
+ });
+ });
+});