From 69fda4e5267e8c66e3f3f232a10d160cc0ced338 Mon Sep 17 00:00:00 2001 From: Andy Joslin Date: Thu, 13 Feb 2014 12:14:45 -0500 Subject: [PATCH] fix(tabs): broadcast tab.shown/tab.hidden to only child scopes Addresses #588 --- js/ext/angular/src/directive/ionicTabBar.js | 178 +++---- .../test/directive/ionicTabBar.unit.js | 450 ++++++++++-------- 2 files changed, 356 insertions(+), 272 deletions(-) diff --git a/js/ext/angular/src/directive/ionicTabBar.js b/js/ext/angular/src/directive/ionicTabBar.js index 494d07a193b..8c16f667a90 100644 --- a/js/ext/angular/src/directive/ionicTabBar.js +++ b/js/ext/angular/src/directive/ionicTabBar.js @@ -13,89 +13,95 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ionic.ui.bindHtml']) $ionicViewService.disableRegisterByTagName('tabs'); }]) -.directive('tabs', ['$ionicViewService', function($ionicViewService) { - return { - restrict: 'E', - replace: true, - scope: true, - transclude: true, - controller: ['$scope', '$element', function($scope, $element) { - var _this = this; - - $scope.tabCount = 0; - $scope.selectedIndex = -1; - $scope.$enableViewRegister = false; - - angular.extend(this, ionic.controllers.TabBarController.prototype); - - - ionic.controllers.TabBarController.call(this, { - controllerChanged: function(oldC, oldI, newC, newI) { - $scope.controllerChanged && $scope.controllerChanged({ - oldController: oldC, - oldIndex: oldI, - newController: newC, - newIndex: newI - }); - }, - tabBar: { - tryTabSelect: function() {}, - setSelectedItem: function(index) {}, - addItem: function(item) {} - } +.controller('$ionicTabs', ['$scope', '$ionicViewService', function($scope, $ionicViewService) { + var _this = this; + + $scope.tabCount = 0; + $scope.selectedIndex = -1; + $scope.$enableViewRegister = false; + + angular.extend(this, ionic.controllers.TabBarController.prototype); + + ionic.controllers.TabBarController.call(this, { + controllerChanged: function(oldC, oldI, newC, newI) { + $scope.controllerChanged && $scope.controllerChanged({ + oldController: oldC, + oldIndex: oldI, + newController: newC, + newIndex: newI }); + }, + tabBar: { + tryTabSelect: function() {}, + setSelectedItem: function(index) {}, + addItem: function(item) {} + } + }); - this.add = function(tabScope) { - tabScope.tabIndex = $scope.tabCount; - this.addController(tabScope); - if(tabScope.tabIndex === 0) { - this.select(0); - } - $scope.tabCount++; - }; + this.add = function(tabScope) { + tabScope.tabIndex = $scope.tabCount; + this.addController(tabScope); + if(tabScope.tabIndex === 0) { + this.select(0); + } + $scope.tabCount++; + }; - this.select = function(tabIndex, emitChange) { - if(tabIndex !== $scope.selectedIndex) { - - $scope.selectedIndex = tabIndex; - $scope.activeAnimation = $scope.animation; - _this.selectController(tabIndex); - - var viewData = { - type: 'tab', - typeIndex: tabIndex - }; - - for(var x=0; x', + $scope.tabsController = this; + +}]) +.directive('tabs', ['$ionicViewService', function($ionicViewService) { + return { + restrict: 'E', + replace: true, + scope: true, + transclude: true, + controller: '$ionicTabs', + template: '
', compile: function(element, attr, transclude, tabsCtrl) { return function link($scope, $element, $attr) { @@ -173,9 +179,9 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ionic.ui.bindHtml']) $scope.$watch(badgeGet, function(value) { $scope.badge = value; }); - var badgeStyleGet = $interpolate(attr.badgeStyle || ''); - $scope.$watch(badgeStyleGet, function(val) { - $scope.badgeStyle = val; + + $attr.$observe('badgeStyle', function(value) { + $scope.badgeStyle = value; }); var leftButtonsGet = $parse($attr.leftButtons); @@ -193,17 +199,20 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ionic.ui.bindHtml']) tabsCtrl.add($scope); - $scope.$watch('isVisible', function(value) { + function cleanupChild() { if(childElement) { childElement.remove(); childElement = null; - $rootScope.$broadcast('tab.hidden'); } if(childScope) { childScope.$destroy(); childScope = null; } - if(value) { + } + + $scope.$watch('isVisible', function(value) { + if (value) { + cleanupChild(); childScope = $scope.$new(); transclude(childScope, function(clone) { clone.addClass('pane'); @@ -211,7 +220,10 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ionic.ui.bindHtml']) childElement = clone; $element.parent().append(childElement); }); - $rootScope.$broadcast('tab.shown'); + $scope.$broadcast('tab.shown'); + } else if (childScope) { + $scope.$broadcast('tab.hidden'); + cleanupChild(); } }); @@ -229,13 +241,15 @@ angular.module('ionic.ui.tabs', ['ionic.service.view', 'ionic.ui.bindHtml']) } }); - $rootScope.$on('$stateChangeSuccess', function(value){ + var unregister = $rootScope.$on('$stateChangeSuccess', function(value){ if( $ionicViewService.isCurrentStateNavView($scope.navViewName) && $scope.tabIndex !== tabsCtrl.selectedIndex) { tabsCtrl.select($scope.tabIndex); } }); + $scope.$on('$destroy', unregister); + }; } }; diff --git a/js/ext/angular/test/directive/ionicTabBar.unit.js b/js/ext/angular/test/directive/ionicTabBar.unit.js index 7a341bccdcf..0d4e2b458db 100644 --- a/js/ext/angular/test/directive/ionicTabBar.unit.js +++ b/js/ext/angular/test/directive/ionicTabBar.unit.js @@ -1,236 +1,306 @@ -describe('Tab Bar Controller', function() { - var compile, element, scope, ctrl; - +describe('tabs', function() { beforeEach(module('ionic.ui.tabs')); - beforeEach(inject(function($compile, $rootScope, $controller) { - compile = $compile; - scope = $rootScope; - var e = compile('')(scope); - ctrl = e.scope().tabsController; - })); + describe('$ionicTabs controller', function() { - it('Select item in controller works', function() { - // Verify no items selected - expect(ctrl.getSelectedControllerIndex()).toEqual(undefined); + var ctrl, scope; + beforeEach(inject(function($rootScope, $controller) { + scope = $rootScope.$new(); + ctrl = $controller('$ionicTabs', { + $scope: scope + }); + })); - // Try selecting beyond the bounds - ctrl.selectController(1); - expect(ctrl.getSelectedControllerIndex()).toEqual(undefined); + it('select should change getSelectedControllerIndex', function() { + // Verify no items selected + expect(ctrl.getSelectedControllerIndex()).toBeUndefined(); + expect(scope.selectedIndex).toBe(-1); - // Add a controller - ctrl.add({ - title: 'Cats', - icon: 'icon-kitty-kat' - }); + // Try selecting beyond the bounds + ctrl.selectController(1); + expect(ctrl.getSelectedControllerIndex()).toBeUndefined(); + expect(scope.selectedIndex).toBe(-1); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); + // Add a controller + ctrl.add({ + title: 'Cats', + icon: 'icon-kitty-kat' + }); - ctrl.add({ - title: 'Cats', - icon: 'icon-kitty-kat' - }); + expect(ctrl.getSelectedControllerIndex()).toEqual(0); + expect(scope.selectedIndex).toBe(0); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); + ctrl.add({ + title: 'Cats', + icon: 'icon-kitty-kat' + }); - ctrl.select(1); + expect(ctrl.getSelectedControllerIndex()).toEqual(0); + expect(scope.selectedIndex).toBe(0); - expect(ctrl.getSelectedControllerIndex()).toEqual(1); - }); + ctrl.select(1); - it('Calls change callback', function() { - scope.onControllerChanged = function(oldC, oldI, newC, newI) { - }; - - // Add a controller - ctrl.add({ - title: 'Cats', - icon: 'icon-kitty-kat' - }); - ctrl.add({ - title: 'Dogs', - icon: 'icon-rufus' + expect(ctrl.getSelectedControllerIndex()).toEqual(1); + expect(scope.selectedIndex).toBe(1); }); - spyOn(ctrl, 'controllerChanged'); + it('select should emit viewData if emit is passed in', function() { + ctrl.add({ title: 'foo', icon: 'icon' }); + ctrl.add({ title: 'bar', icon: 'icon2' }); - expect(ctrl.getSelectedControllerIndex()).toEqual(0); - ctrl.select(1); + var viewData; + spyOn(scope, '$emit').andCallFake(function(e, data) { + viewData = data; + }); - expect(ctrl.controllerChanged).toHaveBeenCalled(); - }); -}); + ctrl.select(0); + expect(scope.$emit).not.toHaveBeenCalled(); -describe('Tabs directive', function() { - var compile, element, scope; + ctrl.select(1, true); + expect(scope.$emit).toHaveBeenCalledWith('viewState.changeHistory', jasmine.any(Object)); + expect(viewData).toBeTruthy(); + expect(viewData.type).toBe('tab'); + expect(viewData.typeIndex).toBe(1); + expect(viewData.title).toBe('bar'); + }); + it('select should go to root if emit is true and selecting same tab index', inject(function($ionicViewService) { + ctrl.add({ title: 'foo', icon: 'icon' }); - beforeEach(module('ionic.ui.tabs')); + spyOn($ionicViewService, 'goToHistoryRoot'); + spyOn($ionicViewService, 'getCurrentView').andCallFake(function() { + return { historyId:'001' }; + }); - beforeEach(inject(function($compile, $rootScope) { - compile = $compile; - scope = $rootScope; - })); + expect(scope.selectedIndex).toBe(0); + //Emit != true + ctrl.select(0); + expect($ionicViewService.goToHistoryRoot).not.toHaveBeenCalled(); - it('Has tab class', function() { - element = compile('')(scope); - scope.$digest(); - expect(element.find('.tabs').hasClass('tabs')).toBe(true); - }); + ctrl.select(0, true); + expect($ionicViewService.goToHistoryRoot).toHaveBeenCalledWith('001'); + })); + it('select should call change callback', function() { + scope.onControllerChanged = function(oldC, oldI, newC, newI) { + }; - it('Has tab children', function() { - element = compile('')(scope); - scope = element.scope(); - scope.controllers = [ - { title: 'Home', icon: 'icon-home' }, - { title: 'Fun', icon: 'icon-fun' }, - { title: 'Beer', icon: 'icon-beer' }, - ]; - scope.$digest(); - expect(element.find('a').length).toBe(3); - }); + // Add a controller + ctrl.add({ title: 'Cats', icon: 'icon-kitty-kat' }); + ctrl.add({ title: 'Dogs', icon: 'icon-rufus' }); - it('Has compiled children', function() { - element = compile('' + - '' + - '' + - '')(scope); - scope.$digest(); - expect(element.find('a').length).toBe(2); - }); + spyOn(ctrl, 'controllerChanged'); - it('Sets style on child tabs', function() { - element = compile('' + - '' + - '' + - '')(scope); - scope.$digest(); - var tabs = element[0].querySelector('.tabs'); - expect(angular.element(tabs).hasClass('tabs-positive')).toEqual(true); - expect(angular.element(tabs).hasClass('tabs-icon-bottom')).toEqual(true); - }); + expect(ctrl.getSelectedControllerIndex()).toEqual(0); + ctrl.select(1); + + expect(ctrl.controllerChanged).toHaveBeenCalled(); + }); + it('select should change activeAnimation=animation', function() { + // Add a controller + ctrl.add({ title: 'Cats', icon: 'icon-kitty-kat' }); + ctrl.add({ title: 'Dogs', icon: 'icon-rufus' }); + + expect(scope.activeAnimation).toBeUndefined(); + scope.animation = 'superfast'; + ctrl.select(1); + expect(scope.activeAnimation).toBe('superfast'); + + scope.animation = 'woah'; + ctrl.select(0); + expect(scope.activeAnimation).toBe('woah'); + }); - it('Has nav-view', function() { - element = compile('' + - '' + - 'content2' + - '')(scope); - scope = element.scope(); - scope.$digest(); - expect(scope.tabCount).toEqual(2); - expect(scope.selectedIndex).toEqual(0); - expect(scope.controllers.length).toEqual(2); - expect(scope.controllers[0].hasNavView).toEqual(true); - expect(scope.controllers[0].navViewName).toEqual('name1'); - expect(scope.controllers[0].url).toEqual('/page1'); - expect(scope.controllers[1].hasNavView).toEqual(false); - expect(scope.controllers[1].url).toEqual('/page2'); }); -}); -describe('Tab Item directive', function() { - var compile, element, scope, ctrl; + describe('tabs directive', function() { + var compile, scope, element; + beforeEach(inject(function($compile, $rootScope) { + compile = $compile; + scope = $rootScope; + })); + + it('Has tab class', function() { + var element = compile('')(scope); + scope.$digest(); + expect(element.find('.tabs').hasClass('tabs')).toBe(true); + }); - beforeEach(module('ionic.ui.tabs')); + it('Has tab children', function() { + element = compile('')(scope); + scope = element.scope(); + scope.controllers = [ + { title: 'Home', icon: 'icon-home' }, + { title: 'Fun', icon: 'icon-fun' }, + { title: 'Beer', icon: 'icon-beer' }, + ]; + scope.$digest(); + expect(element.find('a').length).toBe(3); + }); - beforeEach(inject(function($compile, $rootScope, $document, $controller) { - compile = $compile; - scope = $rootScope; + it('Has compiled children', function() { + element = compile('' + + '' + + '' + + '')(scope); + scope.$digest(); + expect(element.find('a').length).toBe(2); + }); - scope.badgeValue = 3; - scope.badgeStyle = 'badge-assertive'; - element = compile('' + - '' + + it('Sets style on child tabs', function() { + element = compile('' + + '' + + '' + '')(scope); - scope.$digest(); - $document[0].body.appendChild(element[0]); - })); - - it('Title works', function() { - //The badge's text gets in the way of just doing .text() on the element itself, so exclude it - var notBadge = angular.element(element[0].querySelectorAll('a >:not(.badge)')); - expect(notBadge.text().trim()).toEqual('Item'); - }); + scope.$digest(); + var tabs = element[0].querySelector('.tabs'); + expect(angular.element(tabs).hasClass('tabs-positive')).toEqual(true); + expect(angular.element(tabs).hasClass('tabs-icon-bottom')).toEqual(true); + }); - it('Default icon works', function() { - scope.$digest(); - var i = element[0].querySelectorAll('i')[1]; - expect(angular.element(i).hasClass('icon-default')).toEqual(true); - }); + it('Has nav-view', function() { + element = compile('' + + '' + + 'content2' + + '')(scope); + scope = element.scope(); + scope.$digest(); + expect(scope.tabCount).toEqual(2); + expect(scope.selectedIndex).toEqual(0); + expect(scope.controllers.length).toEqual(2); + expect(scope.controllers[0].hasNavView).toEqual(true); + expect(scope.controllers[0].navViewName).toEqual('name1'); + expect(scope.controllers[0].url).toEqual('/page1'); + expect(scope.controllers[1].hasNavView).toEqual(false); + expect(scope.controllers[1].url).toEqual('/page2'); + }); - it('Badge works', function() { - scope.$digest(); - var i = element[0].querySelector('.badge'); - expect(i.innerHTML).toEqual('3'); - expect(i.className).toMatch('badge-assertive'); }); - it('Badge updates', function() { - scope.badgeValue = 10; - scope.$digest(); - var i = element[0].querySelectorAll('i')[0]; - expect(i.innerHTML).toEqual('10'); - }); + describe('tab-item Directive', function() { + + var compile, element, scope, ctrl; + beforeEach(inject(function($compile, $rootScope, $document, $controller) { + compile = $compile; + scope = $rootScope.$new(); + + scope.badgeValue = 3; + scope.badgeStyleValue = 'badge-assertive'; + element = compile('' + + '' + + '')(scope); + scope.$digest(); + $document[0].body.appendChild(element[0]); + })); + + it('Title works', function() { + //The badge's text gets in the way of just doing .text() on the element itself, so exclude it + var notBadge = angular.element(element[0].querySelectorAll('a >:not(.badge)')); + expect(notBadge.text().trim()).toEqual('Item'); + }); - it('Click sets correct tab index', function() { - var a = element.find('a:eq(0)'); - var itemScope = a.isolateScope(); - //spyOn(a, 'click'); - spyOn(itemScope, 'selectTab'); - a.click(); - expect(itemScope.selectTab).toHaveBeenCalled(); - }); -}); + it('Default icon works', function() { + scope.$digest(); + var i = element[0].querySelectorAll('i')[1]; + expect(angular.element(i).hasClass('icon-default')).toEqual(true); + }); -describe('Tab Controller Item directive', function() { - var compile, element, scope, ctrl; + it('Badge works', function() { + scope.$digest(); + var i = element[0].querySelector('.badge'); + expect(i.innerHTML).toEqual('3'); + expect(i.className).toMatch('badge-assertive'); + scope.$apply("badgeStyleValue = 'badge-danger'"); + expect(i.className).toMatch('badge-danger'); + }); - beforeEach(module('ionic.ui.tabs')); + it('Badge updates', function() { + scope.badgeValue = 10; + scope.$digest(); + var i = element[0].querySelectorAll('i')[0]; + expect(i.innerHTML).toEqual('10'); + }); - beforeEach(inject(function($compile, $rootScope, $document, $controller) { - compile = $compile; - scope = $rootScope; - - scope.badgeValue = 3; - scope.isActive = false; - element = compile('' + - '' + - '')(scope); - scope.$digest(); - $document[0].body.appendChild(element[0]); - })); - - it('Icon title works as html', function() { - expect(element.find('a').find('span').html()).toEqual('Icon title'); + it('Click sets correct tab index', function() { + var a = element.find('a:eq(0)'); + var itemScope = a.isolateScope(); + //spyOn(a, 'click'); + spyOn(itemScope, 'selectTab'); + a.click(); + expect(itemScope.selectTab).toHaveBeenCalled(); + }); }); - it('Icon classes works', function() { - var title = ''; - var elements = element[0].querySelectorAll('.icon-class'); - expect(elements.length).toEqual(1); - var elements = element[0].querySelectorAll('.icon-off-class'); - expect(elements.length).toEqual(1); + describe('tab directive', function() { + var scope, tab; + beforeEach(inject(function($compile, $rootScope, $controller) { + var tabsScope = $rootScope.$new(); + //Setup a fake tabs controller for our tab to use so we dont have to have a parent tabs directive (isolated test) + var ctrl = $controller('$ionicTabs', { + $scope: tabsScope + }); + + //Create an outer div that has a tabsController on it so tab thinks it's in a + var element = angular.element('
'); + element.data('$tabsController', ctrl); + $compile(element)(tabsScope) + tabsScope.$apply(); + + tab = element.find('tab'); + scope = tab.scope(); + })); }); - it('Active switch works', function() { - var elements = element[0].querySelectorAll('.icon-on-class'); - expect(elements.length).toEqual(0); + describe('tab-controller-item Directive', function() { - scope.isActive = true; - scope.$digest(); + var compile, element, scope, ctrl; + beforeEach(inject(function($compile, $rootScope, $document, $controller) { + compile = $compile; + scope = $rootScope; - var elements = element[0].querySelectorAll('.icon-on-class'); - expect(elements.length).toEqual(1); - }); + scope.badgeValue = 3; + scope.isActive = false; + element = compile('' + + '' + + '')(scope); + scope.$digest(); + $document[0].body.appendChild(element[0]); + })); - it('Badge updates', function() { - scope.badgeValue = 10; - scope.badgeStyle = 'badge-assertive'; - scope.$digest(); - var i = element[0].querySelector('.badge'); - expect(i.innerHTML).toEqual('10'); - expect(i.className).toMatch('badge-assertive'); - scope.$apply('badgeStyle = "badge-super"'); - expect(i.className).toMatch('badge-super'); - }); + it('Icon title works as html', function() { + expect(element.find('a').find('span').html()).toEqual('Icon title'); + }); + + it('Icon classes works', function() { + var title = ''; + var elements = element[0].querySelectorAll('.icon-class'); + expect(elements.length).toEqual(1); + var elements = element[0].querySelectorAll('.icon-off-class'); + expect(elements.length).toEqual(1); + }); + + it('Active switch works', function() { + var elements = element[0].querySelectorAll('.icon-on-class'); + expect(elements.length).toEqual(0); + + scope.isActive = true; + scope.$digest(); + + var elements = element[0].querySelectorAll('.icon-on-class'); + expect(elements.length).toEqual(1); + }); + it('Badge updates', function() { + scope.badgeValue = 10; + scope.badgeStyle = 'badge-assertive'; + scope.$digest(); + var i = element[0].querySelector('.badge'); + expect(i.innerHTML).toEqual('10'); + expect(i.className).toMatch('badge-assertive'); + scope.$apply('badgeStyle = "badge-super"'); + expect(i.className).toMatch('badge-super'); + }); + + + }); }); + +