mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
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
This commit is contained in:
61
js/ext/angular/src/directive/ionicContent.js
vendored
61
js/ext/angular/src/directive/ionicContent.js
vendored
@@ -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: '<div class="scroll-content"><div class="scroll" ng-transclude></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,
|
||||
template:
|
||||
'<div class="scroll-content">' +
|
||||
'<div class="scroll"></div>' +
|
||||
'</div>',
|
||||
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;
|
||||
|
||||
|
||||
1
js/ext/angular/src/ionicAngular.js
vendored
1
js/ext/angular/src/ionicAngular.js
vendored
@@ -3,6 +3,7 @@
|
||||
* modules.
|
||||
*/
|
||||
angular.module('ionic.service', [
|
||||
'ionic.service.bind',
|
||||
'ionic.service.platform',
|
||||
'ionic.service.actionSheet',
|
||||
'ionic.service.gesture',
|
||||
|
||||
53
js/ext/angular/src/service/ionicBind.js
vendored
Normal file
53
js/ext/angular/src/service/ionicBind.js
vendored
Normal file
@@ -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;
|
||||
}
|
||||
});
|
||||
};
|
||||
}]);
|
||||
@@ -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('<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');
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<ion-header-bar title="'Sample UL'" type="bar-positive"></ion-header-bar>
|
||||
|
||||
<ion-content has-header="true" scroll="true" ng-controller="ContentCtrl" on-refresh="onRefresh()" has-footer="true" padding="false">
|
||||
<ion-content on-infinite-scroll="addMore()" has-header="true" scroll="true" ng-controller="ContentCtrl" on-refresh="onRefresh()" has-footer="true" padding="false">
|
||||
|
||||
<ion-refresher></ion-refresher>
|
||||
|
||||
@@ -45,7 +45,11 @@
|
||||
<li class="item">24</li>
|
||||
<li class="item">25</li>
|
||||
<li class="item">26</li>
|
||||
<li ng-repeat="i in more">more {{$index}}</li>
|
||||
</ul>
|
||||
|
||||
<ion-infinite-scroll></ion-infinite-scroll>
|
||||
|
||||
</ion-content>
|
||||
|
||||
<ion-footer-bar type="bar-assertive">
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
137
js/ext/angular/test/service/ionicBind.unit.js
Normal file
137
js/ext/angular/test/service/ionicBind.unit.js
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user