feat(content): automatically add/remove has-* classes to content

Also, do manual transclusion on items that would be 'deep' directives -
to fix a problem with transcluding & requiring parent elements.

Closes #619
This commit is contained in:
Andy Joslin
2014-03-19 16:31:29 -06:00
parent 4e605979ec
commit e94d400648
14 changed files with 244 additions and 153 deletions

View File

@@ -112,25 +112,47 @@ angular.module('ionic.ui.header', ['ngAnimate', 'ngSanitize'])
.directive('ionFooterBar', barDirective(false));
function barDirective(isHeader) {
var BAR_TEMPLATE = isHeader ?
'<header class="bar bar-header" ng-transclude></header>' :
'<footer class="bar bar-footer" ng-transclude></footer>';
var BAR_MODEL_DEFAULT = isHeader ?
'$ionicHeaderBarController' :
'$ionicFooterBarController';
return ['$parse', function($parse) {
return {
restrict: 'E',
replace: true,
transclude: true,
template: BAR_TEMPLATE,
link: function($scope, $element, $attr) {
var hb = new ionic.views.HeaderBar({
el: $element[0],
alignTitle: $attr.alignTitle || 'center'
});
compile: function($element, $attr) {
$element.addClass(isHeader ? 'bar bar-header' : 'bar bar-footer');
return { pre: prelink };
function prelink($scope, $element, $attr) {
var hb = new ionic.views.HeaderBar({
el: $element[0],
alignTitle: $attr.alignTitle || 'center'
});
$parse($attr.controllerBind || BAR_MODEL_DEFAULT).assign($scope, hb);
$parse($attr.controllerBind ||
(isHeader ? '$ionicHeaderBarController' : '$ionicFooterBarController')
).assign($scope, hb);
var el = $element[0];
if (isHeader) {
$scope.$watch(function() { return el.className; }, function(value) {
var isSubheader = value.indexOf('bar-subheader') !== -1;
$scope.$parent.$hasHeader = !isSubheader;
$scope.$parent.$hasSubheader = isSubheader;
});
$scope.$on('$destroy', function() {
$scope.$parent.$hasHeader = $scope.$parent.$hasSubheader = null;
});
} else {
$scope.$watch(function() { return el.className; }, function(value) {
var isSubfooter = value.indexOf('bar-subfooter') !== -1;
$scope.$parent.$hasFooter = !isSubfooter;
$scope.$parent.$hasSubfooter = isSubfooter;
});
$scope.$on('$destroy', function() {
$scope.$parent.$hasFooter = $scope.$parent.$hasSubfooter = null;
});
$scope.$watch('$hasTabs', function(val) {
$element.toggleClass('has-tabs', !!val);
});
}
}
}
};
}];

View File

@@ -44,9 +44,6 @@ angular.module('ionic.ui.content', ['ionic.ui.scroll'])
* directive, and infinite scrolling with the {@link ionic.directive:ionInfiniteScroll}
* directive.
*
* Use the classes 'has-header', 'has-subheader', 'has-footer', and 'has-tabs'
* to modify the positioning of the ion-content relative to surrounding elements.
*
* @param {string=} controller-bind The scope variable to bind this element's scrollView's
* {@link ionic.controller:ionicScroll ionicScroll controller} to.
* Default: $scope.$ionicScrollController.
@@ -68,24 +65,31 @@ angular.module('ionic.ui.content', ['ionic.ui.scroll'])
function($parse, $timeout, $controller, $ionicBind) {
return {
restrict: 'E',
replace: true,
transclude: true,
require: '^?ionNavView',
scope: true,
template:
'<div class="scroll-content">' +
'<div class="scroll"></div>' +
'</div>',
compile: function(element, attr, transclude) {
return {
//Prelink <ion-content> so it can compile before other directives compile.
//Then other directives can require ionicScrollCtrl
pre: prelink
};
compile: function(element, attr) {
element.addClass('scroll-content');
//We cannot transclude here because it breaks element.data() inheritance on compile
var innerElement = angular.element('<div class="scroll"></div>');
innerElement.append(element.contents());
element.append(innerElement);
return { pre: prelink };
function prelink($scope, $element, $attr, navViewCtrl) {
var clone, sc, scrollView, scrollCtrl,
scrollContent = angular.element($element[0].querySelector('.scroll'));
var clone, sc, scrollView, scrollCtrl;
$scope.$watch(function() {
return ($scope.$hasHeader ? ' has-header' : '') +
($scope.$hasSubheader ? ' has-subheader' : '') +
($scope.$hasFooter ? ' has-footer' : '') +
($scope.$hasSubfooter ? ' has-subfooter' : '') +
($scope.$hasTabs ? ' has-tabs' : '') +
($scope.$hasTabsTop ? ' has-tabs-top' : '');
}, function(className, oldClassName) {
$element.removeClass(oldClassName);
$element.addClass(className);
});
$ionicBind($scope, $attr, {
$onScroll: '&onScroll',
@@ -104,7 +108,7 @@ function($parse, $timeout, $controller, $ionicBind) {
if (angular.isDefined($attr.padding)) {
$scope.$watch($attr.padding, function(newVal) {
scrollContent.toggleClass('padding', !!newVal);
innerElement.toggleClass('padding', !!newVal);
});
}
@@ -139,13 +143,6 @@ function($parse, $timeout, $controller, $ionicBind) {
scrollView = scrollCtrl.scrollView;
}
transclude($scope, function(clone) {
if (scrollCtrl) {
clone.data('$$ionicScrollController', scrollCtrl);
}
scrollContent.append(clone);
});
}
}
};

View File

@@ -215,20 +215,22 @@ function($ionicViewService, $rootScope, $animate, $compile, $parse) {
return {
restrict: 'E',
replace: true,
transclude: true,
controller: '$ionicNavBar',
template:
'<header class="bar bar-header nav-bar{{navBarClass()}}">' +
'<div class="buttons left-buttons"> ' +
'</div>' +
'<h1 ng-bind-html="title" class="title"></h1>' +
'<div class="buttons right-buttons"> ' +
'</div>' +
'</header>',
compile: function(tElement, tAttrs, transclude) {
scope: true,
compile: function(tElement, tAttrs) {
//We cannot transclude here because it breaks element.data() inheritance on compile
tElement
.addClass('bar bar-header nav-bar')
.append(
'<div class="buttons left-buttons"> ' +
'</div>' +
'<h1 ng-bind-html="title" class="title"></h1>' +
'<div class="buttons right-buttons"> ' +
'</div>'
);
return function link($scope, $element, $attr, navBarCtrl) {
return { pre: prelink };
function prelink($scope, $element, $attr, navBarCtrl) {
navBarCtrl._headerBarView = new ionic.views.HeaderBar({
el: $element[0],
alignTitle: $attr.alignTitle || 'center'
@@ -237,23 +239,23 @@ function($ionicViewService, $rootScope, $animate, $compile, $parse) {
$parse($attr.controllerBind || '$ionicNavBarController')
.assign($scope, navBarCtrl);
//Put transcluded content (usually a back button) before the rest
transclude($scope, function(clone) {
$element.prepend(clone);
});
//defaults
$scope.backButtonShown = false;
$scope.shouldAnimate = true;
$scope.isReverse = false;
$scope.isInvisible = true;
$scope.$parent.$hasHeader = true;
$scope.navBarClass = function() {
$scope.$watch(function() {
return ($scope.isReverse ? ' reverse' : '') +
($scope.isInvisible ? ' invisible' : '') +
(!$scope.shouldAnimate ? ' no-animation' : '');
};
};
}, function(className, oldClassName) {
$element.removeClass(oldClassName);
$element.addClass(className);
});
}
}
};
}])
@@ -311,32 +313,30 @@ function($ionicViewService, $rootScope, $animate, $compile, $parse) {
return {
restrict: 'E',
require: '^ionNavBar',
replace: true,
transclude: true,
template:
'<button class="button back-button" ng-transclude>' +
'</button>',
link: function($scope, $element, $attr, navBarCtrl) {
$scope.$navBack = navBarCtrl.back;
if (!$attr.ngClick) {
$ionicNgClick($scope, $element, '$navBack($event)');
}
//If the current viewstate does not allow a back button,
//always hide it.
var deregisterListener = $scope.$parent.$on(
'$viewHistory.historyChange',
function(e, data) {
$scope.hasBackButton = !!data.showBack;
compile: function(tElement, tAttrs) {
tElement.addClass('button back-button');
return function($scope, $element, $attr, navBarCtrl) {
$scope.$navBack = navBarCtrl.back;
if (!$attr.ngClick) {
$ionicNgClick($scope, $element, '$navBack($event)');
}
);
$scope.$on('$destroy', deregisterListener);
//Make sure both that a backButton is allowed in the first place,
//and that it is shown by the current view.
$scope.$watch('!!(backButtonShown && hasBackButton)', function(val) {
$element.toggleClass('hide', !val);
});
//If the current viewstate does not allow a back button,
//always hide it.
var deregisterListener = $scope.$parent.$on(
'$viewHistory.historyChange',
function(e, data) {
$scope.hasBackButton = !!data.showBack;
}
);
$scope.$on('$destroy', deregisterListener);
//Make sure both that a backButton is allowed in the first place,
//and that it is shown by the current view.
$scope.$watch('!!(backButtonShown && hasBackButton)', function(val) {
$element.toggleClass('hide', !val);
});
};
}
};
}])
@@ -379,9 +379,8 @@ function($ionicViewService, $rootScope, $animate, $compile, $parse) {
.directive('ionNavButtons', ['$compile', '$animate', function($compile, $animate) {
return {
require: '^ionNavBar',
transclude: true,
restrict: 'E',
compile: function($element, $attrs, transclude) {
compile: function($element, $attrs) {
return function($scope, $element, $attrs, navBarCtrl) {
var navElement = $attrs.side === 'right' ?
navBarCtrl.rightButtonsElement :
@@ -391,13 +390,13 @@ function($ionicViewService, $rootScope, $animate, $compile, $parse) {
//so we can remove them all when this element dies -
//even if the buttons have changed through an ng-repeat or the like,
//we just remove their div parent and they are gone.
var clone = angular.element('<div>').append(transclude($scope));
$animate.enter(clone, navElement);
var buttons = angular.element('<div>').append($element.contents().remove());
$animate.enter(buttons, navElement);
//When our ion-nav-buttons container is destroyed,
//destroy everything in the navbar
$scope.$on('$destroy', function() {
$animate.leave(clone);
$animate.leave(buttons);
});
// The original element is just a completely empty <ion-nav-buttons> element.

View File

@@ -23,6 +23,18 @@ angular.module('ionic.ui.popup', [])
replace: true,
transclude: true,
scope: true,
template:
'<div class="popup">' +
'<div class="popup-head">' +
'<h3 class="popup-title" ng-bind-html="title"></h3>' +
'<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
'</div>' +
'<div class="popup-body" ng-transclude>' +
'</div>' +
'<div class="popup-buttons row">' +
'<button ng-repeat="button in buttons" ng-click="_buttonTapped(button, $event)" class="button col" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
'</div>' +
'</div>',
link: function($scope, $element, $attr) {
$ionicBind($scope, $attr, {
title: '@',
@@ -45,18 +57,7 @@ angular.module('ionic.ui.popup', [])
}
$scope.$onButtonTap({button: button, event: event});
}
},
template: '<div class="popup">' +
'<div class="popup-head">' +
'<h3 class="popup-title" ng-bind-html="title"></h3>' +
'<h5 class="popup-sub-title" ng-bind-html="subTitle" ng-if="subTitle"></h5>' +
'</div>' +
'<div class="popup-body" ng-transclude>' +
'</div>' +
'<div class="popup-buttons row">' +
'<button ng-repeat="button in buttons" ng-click="_buttonTapped(button, $event)" class="button col" ng-class="button.type || \'button-default\'" ng-bind-html="button.text"></button>' +
'</div>' +
'</div>'
}
};
}]);

View File

@@ -26,9 +26,6 @@ angular.module('ionic.ui.scroll', [])
.directive('ionScroll', ['$parse', '$timeout', '$controller', function($parse, $timeout, $controller) {
return {
restrict: 'E',
replace: true,
template: '<div class="scroll-view"><div class="scroll" ng-transclude></div></div>',
transclude: true,
scope: {
direction: '@',
paging: '@',
@@ -39,23 +36,25 @@ angular.module('ionic.ui.scroll', [])
scrollbarY: '@',
},
controller: function() {},
compile: function(element, attr, transclude) {
compile: function(element, attr) {
element.addClass('scroll-view');
return {
//Prelink <ion-scroll> so it can compile before other directives compile.
//Then other directives can require ionicScrollCtrl
pre: prelink
};
//We cannot transclude here because it breaks element.data() inheritance on compile
var innerElement = angular.element('<div class="scroll"></div>');
innerElement.append(element.contents());
element.append(innerElement);
return { pre: prelink };
function prelink($scope, $element, $attr) {
var scrollView, scrollCtrl,
sc = $element[0].children[0];
var scrollView, scrollCtrl;
if(attr.padding == "true") {
sc.classList.add('padding');
if (angular.isDefined($attr.padding)) {
$scope.$watch($attr.padding, function(newVal) {
innerElement.toggleClass('padding', !!newVal);
});
}
if($scope.$eval($scope.paging) === true) {
sc.classList.add('scroll-paging');
innerElement.addClass('scroll-paging');
}
if(!$scope.direction) { $scope.direction = 'y'; }

View File

@@ -157,8 +157,9 @@ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture', 'ionic.service.vie
restrict: 'AC',
require: '^ionSideMenus',
scope: true,
compile: function(element, attr, transclude) {
return function($scope, $element, $attr, sideMenuCtrl) {
compile: function(element, attr) {
return { pre: prelink };
function prelink($scope, $element, $attr, sideMenuCtrl) {
$element.addClass('menu-content');
@@ -248,7 +249,7 @@ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture', 'ionic.service.vie
$ionicGesture.off(releaseGesture, 'release', dragReleaseFn);
ionic.off('tap', contentTap, $element[0]);
});
};
}
}
};
}])
@@ -282,14 +283,13 @@ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture', 'ionic.service.vie
return {
restrict: 'E',
require: '^ionSideMenus',
replace: true,
transclude: true,
scope: true,
template: '<div class="menu menu-{{side}}"></div>',
compile: function(element, attr, transclude) {
compile: function(element, attr) {
angular.isUndefined(attr.isEnabled) && attr.$set('isEnabled', 'true');
angular.isUndefined(attr.width) && attr.$set('width', '275');
element.addClass('menu menu-' + attr.side);
return function($scope, $element, $attr, sideMenuCtrl) {
$scope.side = $attr.side || 'left';
@@ -308,10 +308,6 @@ angular.module('ionic.ui.sideMenu', ['ionic.service.gesture', 'ionic.service.vie
$scope.$watch($attr.isEnabled, function(val) {
sideMenu.setIsEnabled(!!val);
});
transclude($scope, function(clone) {
$element.append(clone);
});
};
}
};

View File

@@ -172,28 +172,33 @@ angular.module('ionic.ui.tabs', ['ionic.service.view'])
.directive('ionTabs', ['$ionicViewService', '$parse', function($ionicViewService, $parse) {
return {
restrict: 'E',
replace: true,
scope: true,
transclude: true,
controller: 'ionicTabs',
template:
'<div class="view">' +
'<div class="tabs">' +
'</div>' +
'</div>',
compile: function(element, attr, transclude) {
compile: function(element, attr) {
element.addClass('view');
//We cannot transclude here because it breaks element.data() inheritance on compile
var innerElement = angular.element('<div class="tabs"></div>');
innerElement.append(element.contents());
element.append(innerElement);
return function link($scope, $element, $attr, tabsCtrl) {
return { pre: prelink };
function prelink($scope, $element, $attr, tabsCtrl) {
$parse(attr.model || '$ionicTabsController').assign($scope, tabsCtrl);
tabsCtrl.$scope = $scope;
tabsCtrl.$element = $element;
tabsCtrl.$tabsElement = angular.element($element[0].querySelector('.tabs'));
transclude($scope, function(clone) {
$element.append(clone);
var el = $element[0];
$scope.$watch(function() { return el.className; }, function(value) {
var isTabsTop = value.indexOf('tabs-top') !== -1;
$scope.$hasTabs = !isTabsTop;
$scope.$hasTabsTop = isTabsTop;
});
};
$scope.$on('$destroy', function() {
$scope.$hasTabs = $scope.$hasTabsTop = null;
});
}
}
};
}])

View File

@@ -55,11 +55,11 @@ describe('bar directives', function() {
angular.forEach([{
tag: 'ion-header-bar',
element: 'header',
className: 'bar bar-header',
controllerBind: '$ionicHeaderBarController'
}, {
tag: 'ion-footer-bar',
element: 'footer',
className: 'bar bar-footer',
controllerBind: '$ionicFooterBarController'
}], function(data) {
describe(data.tag, function() {
@@ -78,9 +78,50 @@ describe('bar directives', function() {
return el;
}
it('should compile to ' + data.element, function() {
if (data.tag === 'ion-header-bar') {
it('$hasHeader $hasSubheader', function() {
var el = setup();
var scope = el.scope().$parent;
expect(scope.$hasHeader).toEqual(true);
expect(scope.$hasSubheader).toEqual(false);
el.addClass('bar-subheader');
scope.$apply();
expect(scope.$hasHeader).toEqual(false);
expect(scope.$hasSubheader).toEqual(true);
el.removeClass('bar-subheader');
scope.$apply();
expect(scope.$hasHeader).toEqual(true);
expect(scope.$hasSubheader).toEqual(false);
});
} else {
it('$hasFooter $hasSubheader', function() {
var el = setup();
var scope = el.scope().$parent;
expect(scope.$hasFooter).toEqual(true);
expect(scope.$hasSubfooter).toEqual(false);
el.addClass('bar-subfooter');
scope.$apply();
expect(scope.$hasFooter).toEqual(false);
expect(scope.$hasSubfooter).toEqual(true);
el.removeClass('bar-subfooter');
scope.$apply();
expect(scope.$hasFooter).toEqual(true);
expect(scope.$hasSubfooter).toEqual(false);
});
it('.has-tabs', function() {
var el = setup();
var scope = el.scope().$parent;
expect(el.hasClass('has-tabs')).toBe(false);
scope.$apply('$hasTabs = true');
expect(el.hasClass('has-tabs')).toBe(true);
scope.$apply('$hasTabs = false');
expect(el.hasClass('has-tabs')).toBe(false);
});
}
it('should compile to ' + data.className, function() {
var el = setup();
expect(el[0].tagName.toLowerCase()).toBe(data.element);
expect(el.hasClass(data.className)).toBe(true);
});
it('should assign views.HeaderBar to default controllerBind', function() {

View File

@@ -229,6 +229,11 @@ describe('ionNavBar', function() {
expect(el.children().eq(0).html()).toBe('<b>super</b> content 4');
});
it('should $parent.$hasHeader', function() {
var el = setup();
expect(el.scope().$parent.$hasHeader).toBe(true);
});
it('should assign $scope.navBarController by default', function() {
var el = setup();
expect(el.controller('ionNavBar')).toBeTruthy(); //sanity

View File

@@ -26,6 +26,7 @@ describe('Ionic Scroll Directive', function() {
it('should add padding classname', function() {
element = compile('<ion-scroll padding="true"></ion-scroll>')(scope);
scope.$apply();
expect(element.children().eq(0).hasClass('padding')).toEqual(true);
var scrollElement = element.find('.scroll');
expect(scrollElement.hasClass('padding')).toEqual(true);

View File

@@ -224,6 +224,21 @@ describe('tabs', function() {
return element;
}
it('should $hasTabs and $hasTabsTop', function() {
var el = setup();
var scope = el.scope();
expect(scope.$hasTabs).toBe(true);
expect(scope.$hasTabsTop).toBe(false);
el.addClass('tabs-top');
scope.$apply();
expect(scope.$hasTabs).toBe(false);
expect(scope.$hasTabsTop).toBe(true);
el.removeClass('tabs-top');
scope.$apply();
expect(scope.$hasTabs).toBe(true);
expect(scope.$hasTabsTop).toBe(false);
});
it('should bind controller to scope.$ionicTabsController by default', function() {
var el = setup();
expect(el.controller('ionTabs')).toBeTruthy(); //sanity
@@ -238,8 +253,7 @@ describe('tabs', function() {
it('should transclude content with same scope', function() {
var el = setup('', '<div class="content"></div>');
expect(el.children().eq(1).hasClass('content')).toBe(true);
expect(el.children().eq(1).scope()).toBe(el.scope());
expect(el[0].querySelector('.tabs .content')).toBeTruthy();
});
});

View File

@@ -16,7 +16,12 @@
<h1 class="title">Header!</h1>
</ion-header-bar>
<ion-content scroll="true" ng-controller="ContentCtrl" padding="false" class="has-header" ng-class="{'has-subheader': $root.isSub}">
<ion-content scroll="true" ng-controller="ContentCtrl" padding="false">
<pre>
$hasHeader: {{!!$hasHeader}}
$hasSubheader: {{!!$hasSubheader}}
</pre>
<ion-refresher on-refresh="onRefresh()" pulling-text="pull!" refreshing-text="refreshing!"></ion-refresher>

View File

@@ -33,7 +33,7 @@
<button class="button button-icon ion-navicon"></button>
</ion-nav-buttons>
<ion-content class="has-header">
<ion-content>
<div class="list">
<label class="item item-input">
<span class="input-label">Username</span>
@@ -66,7 +66,7 @@
<script id="forgot-password.html" type="text/ng-template">
<ion-view title="Forgot Password" hide-nav-bar="true">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<p>This ion-view hides the nav bar using the hideNavBar attribute.</p>
<p>
<button ng-click="hideNavBar()">Hide Nav Bar</button>
@@ -91,7 +91,7 @@
<script id="contact.html" type="text/ng-template">
<ion-view hide-back-button="true">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<p>The views title is blank on purpose.</p>
<p>The hideBackButton attribute is "true" for this view.</p>
<p>@drifty</p>
@@ -127,7 +127,7 @@
<ion-tab title="Info" icon="ion-information-circled">
<ion-view title="Information">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<h3>Information</h3>
<p>
This is a sample seed project for the Ionic Framework. Please change it to match your needs.
@@ -150,7 +150,7 @@
<script id="auto-list.html" type="text/ng-template">
<ion-view title="Auto List">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<ion-list>
<ion-item ng-repeat="auto in autos" ng-href="#/tabs/autos/{{ $index }}" ion-nav-animation="{{$index === 0 ? 'slide-in-up' : 'slide-left-right'}}">
{{ auto.year }} {{ auto.make }} {{ auto.model }}
@@ -174,7 +174,7 @@
<button class="button button-icon icon ion-refresher">
</button>
</ion-nav-buttons>
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<h2>{{ auto.year }} {{ auto.make }} {{ auto.model }}</h2>
<p ng-bind="auto.desc"></p>
@@ -212,7 +212,7 @@
<script id="add-auto.html" type="text/ng-template">
<ion-view title="Add Auto">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<div class="list">
<label class="item item-input">
@@ -242,7 +242,7 @@
<script id="about.html" type="text/ng-template">
<ion-view title="About">
<ion-content class="has-header" padding="true">
<ion-content padding="true">
<h3>About this app!</h3>
<p>
Current View: {{ $viewHistory.currentView }}<br>

View File

@@ -281,6 +281,12 @@ ion-infinite-scroll.active .scroll-infinite {
.has-subheader {
top: $bar-height * 2;
}
.has-tabs-top {
top: $bar-height + $tabs-height;
}
.has-header.has-subheader.has-tabs-top {
top: 2 * $bar-height + $tabs-height;
}
.has-footer {
bottom: $bar-height;