Skip to content

Commit

Permalink
feat(ionContent): use child scope instead of isolate scope
Browse files Browse the repository at this point in the history
Adds new '$ionicBindParent' service, which takes an object containing
binding definitions (similar to angular directive isolate scope
definition).  Allows binding of any parent scope & directive attributes
to a child scope, letting us do normal parent -> child scope binding
without having to create isolate scopes.

Closes #555
  • Loading branch information
ajoslin committed Feb 24, 2014
1 parent 7977d53 commit 9185817
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 34 deletions.
57 changes: 34 additions & 23 deletions js/ext/angular/src/directive/ionicContent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<div class="scroll-content"><div class="scroll" ng-transclude></div></div>',
template: '<div class="scroll-content"><div class="scroll"></div></div>',
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'); }
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions js/ext/angular/src/ionicAngular.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* modules.
*/
angular.module('ionic.service', [
'ionic.service.bind',
'ionic.service.platform',
'ionic.service.actionSheet',
'ionic.service.gesture',
Expand Down
53 changes: 53 additions & 0 deletions js/ext/angular/src/service/ionicBindFromParent.js
Original file line number Diff line number Diff line change
@@ -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;
}
});
};
}]);
37 changes: 27 additions & 10 deletions js/ext/angular/test/directive/ionicContent.unit.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
describe('Ionic Content directive', function() {
var compile, element, scope;
var compile, scope;

beforeEach(module('ionic'));

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

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

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

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

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

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

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

it('Disables bouncing by default on Android', function() {
ionic.Platform.setPlatform('Android');
element = compile('<ion-content has-header="true"></ion-content>')(scope);
var element = compile('<ion-content has-header="true"></ion-content>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
Expand All @@ -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('<ion-content has-header="true" has-bouncing="true"></ion-content>')(scope);
var element = compile('<ion-content has-header="true" has-bouncing="true"></ion-content>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
Expand All @@ -72,7 +72,7 @@ describe('Ionic Content directive', function() {


it('Should set start x and y', function() {
element = compile('<ion-content start-x="100" start-y="300" has-header="true"></ion-content>')(scope);
var element = compile('<ion-content start-x="100" start-y="300" has-header="true"></ion-content>')(scope);
scope.$apply();
var newScope = element.isolateScope();
var scrollView = scope.scrollView;
Expand Down Expand Up @@ -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('<ion-content ng-controller="ContentTestCtrl">' +
'<form name="myForm"></form>' +
'</ion-content>')($rootScope.$new());
var contentScope = element.scope();
var ctrl = element.data('$ngControllerController');
expect(contentScope.myForm).toBeTruthy();
expect(ctrl.$scope.myForm).toBeTruthy();
}));
});
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Ionic ScrollDelegate Service', function() {
it('scroll event', function() {
var scope = rootScope.$new();
var el = compile('<ion-content></ion-content>')(scope);
scope = el.isolateScope();
scope = el.scope();
scope.$apply();
var top, left;
scope.onScroll = jasmine.createSpy('scroll').andCallFake(function(data) {
Expand Down
144 changes: 144 additions & 0 deletions js/ext/angular/test/service/ionicBindFromParent.unit.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});

0 comments on commit 9185817

Please sign in to comment.