Skip to content

Commit ee2b768

Browse files
committed
fix(ionicTabBar): detect if matches state in all cases
Addresses #894.
1 parent 2a2f657 commit ee2b768

File tree

2 files changed

+162
-47
lines changed

2 files changed

+162
-47
lines changed

Diff for: js/ext/angular/src/directive/ionicTabBar.js

+35-27
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ angular.module('ionic.ui.tabs', ['ionic.service.view'])
214214
controller: 'ionicTabs',
215215
compile: function(element, attr) {
216216
element.addClass('view');
217-
//We cannot transclude here because it breaks element.data() inheritance on compile
217+
//We cannot use regular transclude here because it breaks element.data()
218+
//inheritance on compile
218219
var innerElement = angular.element('<div class="tabs"></div>');
219220
innerElement.append(element.contents());
220221
element.append(innerElement);
@@ -246,9 +247,26 @@ angular.module('ionic.ui.tabs', ['ionic.service.view'])
246247
};
247248
}])
248249

249-
.controller('ionicTab', ['$scope', '$ionicViewService', '$rootScope', '$element',
250-
function($scope, $ionicViewService, $rootScope, $element) {
250+
.controller('ionicTab', ['$scope', '$ionicViewService', '$attrs', '$location', '$state',
251+
function($scope, $ionicViewService, $attrs, $location, $state) {
251252
this.$scope = $scope;
253+
254+
//All of these exposed for testing
255+
this.hrefMatchesState = function() {
256+
return $attrs.href && $location.path().indexOf(
257+
$attrs.href.replace(/^#/, '').replace(/\/$/, '')
258+
) === 0;
259+
};
260+
this.srefMatchesState = function() {
261+
return $attrs.uiSref && $state.includes( $attrs.uiSref.split('(')[0] );
262+
};
263+
this.navNameMatchesState = function() {
264+
return this.navViewName && $ionicViewService.isCurrentStateNavView(this.navViewName);
265+
};
266+
267+
this.tabMatchesState = function() {
268+
return this.hrefMatchesState() || this.srefMatchesState() || this.navNameMatchesState();
269+
};
252270
}])
253271

254272
/**
@@ -300,27 +318,20 @@ function($rootScope, $animate, $ionicBind, $compile, $ionicViewService, $state,
300318
controller: 'ionicTab',
301319
scope: true,
302320
compile: function(element, attr) {
303-
//Do we have a navView?
304-
var navView = element[0].querySelector('ion-nav-view') ||
321+
var navView = element[0].querySelector('ion-nav-view') ||
305322
element[0].querySelector('data-ion-nav-view');
306323
var navViewName = navView && navView.getAttribute('name');
307324

308-
var tabNavItem = angular.element(
309-
element[0].querySelector('ion-tab-nav') ||
310-
element[0].querySelector('data-ion-tab-nav')
311-
).remove();
312-
313325
//Remove the contents of the element so we can compile them later, if tab is selected
326+
//We don't use regular transclusion because it breaks element inheritance
314327
var tabContent = angular.element('<div class="pane">')
315328
.append( element.contents().remove() );
329+
316330
return function link($scope, $element, $attr, ctrls) {
317331
var childScope, childElement, tabNavElement;
318332
tabsCtrl = ctrls[0],
319333
tabCtrl = ctrls[1];
320334

321-
//Remove title attribute so browser-tooltip does not apear
322-
$element[0].removeAttribute('title');
323-
324335
$ionicBind($scope, $attr, {
325336
animate: '=',
326337
onSelect: '&',
@@ -337,10 +348,18 @@ function($rootScope, $animate, $ionicBind, $compile, $ionicViewService, $state,
337348
tabNavElement.remove();
338349
});
339350

351+
//Remove title attribute so browser-tooltip does not apear
352+
$element[0].removeAttribute('title');
353+
340354
if (navViewName) {
341-
$scope.navViewName = navViewName;
342-
$scope.$on('$stateChangeSuccess', selectTabIfMatchesState);
343-
selectTabIfMatchesState();
355+
tabCtrl.navViewName = navViewName;
356+
}
357+
$scope.$on('$stateChangeSuccess', selectIfMatchesState);
358+
selectIfMatchesState();
359+
function selectIfMatchesState() {
360+
if (tabCtrl.tabMatchesState()) {
361+
tabsCtrl.select($scope);
362+
}
344363
}
345364

346365
tabNavElement = angular.element(
@@ -371,17 +390,6 @@ function($rootScope, $animate, $ionicBind, $compile, $ionicViewService, $state,
371390
}
372391
});
373392

374-
function selectTabIfMatchesState() {
375-
var href = $attr.href.replace(/^#/, '');
376-
var stateName = $attr.uiSref.split('(')[0];
377-
378-
if ($location.path().indexOf(href) === 0 || $state.includes(stateName)) {
379-
// this tab's ui-view is the current one, go to it!
380-
if ($ionicViewService.isCurrentStateNavView($scope.navViewName)) {
381-
tabsCtrl.select($scope);
382-
}
383-
}
384-
}
385393
};
386394
}
387395
};

Diff for: js/ext/angular/test/directive/ionicTabBar.unit.js

+127-20
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
describe('tabs', function() {
1+
ddescribe('tabs', function() {
22

33
describe('miscellaneous', function() {
44
beforeEach(module('ionic', function($provide) {
@@ -261,12 +261,103 @@ describe('tabs', function() {
261261
});
262262
});
263263

264-
describe('ionTab directive', function() {
264+
describe('ionicTab controller', function() {
265265
beforeEach(module('ionic'));
266-
var tabsCtrl, tabsEl, scope;
266+
function setup(attrs) {
267+
var ctrl;
268+
inject(function($controller, $rootScope) {
269+
ctrl = $controller('ionicTab', {
270+
$scope: $rootScope.$new(),
271+
$attrs: attrs || {}
272+
});
273+
});
274+
return ctrl;
275+
}
276+
277+
it('.hrefMatchesState', inject(function($location) {
278+
spyOn($location, 'path').andReturn('/a/b/c');
279+
var attr = {};
280+
var ctrl = setup(attr);
281+
282+
expect(ctrl.hrefMatchesState()).toBeFalsy();
283+
284+
attr.href = 'a';
285+
expect(ctrl.hrefMatchesState()).toBe(false);
286+
287+
attr.href = '/a';
288+
expect(ctrl.hrefMatchesState()).toBe(true);
289+
290+
attr.href = '#/a';
291+
expect(ctrl.hrefMatchesState()).toBe(true);
292+
293+
attr.href = '#/a/b/c';
294+
expect(ctrl.hrefMatchesState()).toBe(true);
295+
296+
attr.href = '#/a/b/c/';
297+
expect(ctrl.hrefMatchesState()).toBe(true);
298+
299+
attr.href = '/a/b/c/';
300+
expect(ctrl.hrefMatchesState()).toBe(true);
301+
302+
attr.href = '/a/b/c/d';
303+
expect(ctrl.hrefMatchesState()).toBe(false);
304+
305+
attr.href = 'something';
306+
expect(ctrl.hrefMatchesState()).toBe(false);
307+
}));
308+
309+
it('.srefMatchesState', inject(function($state) {
310+
spyOn($state, 'includes').andReturn(111);
311+
var attr = {};
312+
var ctrl = setup(attr);
313+
314+
expect(ctrl.srefMatchesState()).toBeFalsy();
315+
expect($state.includes).not.toHaveBeenCalled();
316+
317+
//We won't unit test $state.includes, only that it was called
318+
attr.uiSref = 'abc';
319+
expect(ctrl.srefMatchesState()).toBe(111);
320+
expect($state.includes).toHaveBeenCalledWith('abc');
321+
322+
$state.includes.reset();
323+
attr.uiSref = 'def({ param: "value" })';
324+
ctrl.srefMatchesState();
325+
expect($state.includes).toHaveBeenCalledWith('def');
326+
}));
327+
328+
it('.navNameMatchesState', inject(function($ionicViewService) {
329+
spyOn($ionicViewService, 'isCurrentStateNavView').andReturn(123);
330+
331+
var ctrl = setup();
332+
expect(ctrl.navNameMatchesState()).toBeFalsy();
333+
334+
ctrl.navViewName = 'foo';
335+
expect(ctrl.navNameMatchesState()).toBe(123);
336+
expect($ionicViewService.isCurrentStateNavView).toHaveBeenCalledWith('foo');
337+
}));
338+
});
339+
340+
describe('ionTab directive', function() {
341+
var tabDoesMatch;
342+
beforeEach(module('ionic', function($controllerProvider) {
343+
$controllerProvider.register('ionicTab', function($scope) {
344+
this.$scope = $scope;
345+
this.tabMatchesState = jasmine.createSpy('tabMatchesState')
346+
.andCallFake(function() {
347+
return tabDoesMatch;
348+
});
349+
});
350+
}));
351+
352+
var tabsCtrl, tabsEl, scope, tabEl;
267353
function setup(attrs, content) {
268354
inject(function($compile, $rootScope) {
269-
tabsEl = angular.element('<ion-tabs><ion-tab '+(attrs||'')+'>'+(content||'')+'</ion-tab></ion-tabs>');
355+
tabsEl = angular.element(
356+
'<ion-tabs>' +
357+
'<ion-tab '+(attrs||'')+'>'+(content||'')+'</ion-tab>' +
358+
'</ion-tabs>'
359+
);
360+
tabEl = tabsEl.find('ion-tab');
270361

271362
$compile(tabsEl)($rootScope.$new());
272363
$rootScope.$apply();
@@ -323,32 +414,48 @@ describe('tabs', function() {
323414
expect(navItem.parent().length).toBe(0);
324415
});
325416

326-
it('should not set navViewName if no child nav-view', function() {
417+
it('should not set navViewName by default', function() {
327418
setup();
328-
expect(tabsCtrl.tabs[0].navViewName).toBeUndefined();
419+
expect(tabEl.controller('ionTab').navViewName).toBeUndefined();
329420
});
330421

331422
angular.forEach(['ion-nav-view', 'data-ion-nav-view'], function(directive) {
332-
it('should set navViewName and select when necessary if a child '+directive, inject(function($ionicViewService, $rootScope) {
333-
var isCurrent = false;
334-
spyOn($ionicViewService, 'isCurrentStateNavView').andCallFake(function(name) {
335-
return isCurrent;
336-
});
337-
423+
it('should set navViewName if a child '+directive, inject(function($ionicViewService, $rootScope) {
338424
setup('', '<' + directive + ' name="banana"></' + directive + '>');
339425
spyOn(tabsCtrl, 'select');
340426
var tab = tabsCtrl.tabs[0];
341427

342-
expect(tab.navViewName).toBe('banana');
343-
expect($ionicViewService.isCurrentStateNavView).toHaveBeenCalledWith('banana');
428+
expect(tabEl.controller('ionTab').navViewName).toBe('banana');
429+
}));
430+
});
344431

345-
$ionicViewService.isCurrentStateNavView.reset();
346-
isCurrent = true;
347-
$rootScope.$broadcast('$stateChangeSuccess');
432+
it('should call tabMatchesState on compile and if match select', function() {
433+
setup();
434+
expect(tabEl.controller('ionTab').tabMatchesState).toHaveBeenCalled();
348435

349-
expect($ionicViewService.isCurrentStateNavView).toHaveBeenCalledWith('banana');
350-
expect(tabsCtrl.select).toHaveBeenCalledWith(tab);
351-
}));
436+
tabDoesMatch = true;
437+
setup();
438+
expect(tabEl.controller('ionTab').tabMatchesState).toHaveBeenCalled();
439+
});
440+
441+
it('should call selectIfMatchesState on $stateChangeSuccess', function() {
442+
setup();
443+
var tabMatchesState = tabEl.controller('ionTab').tabMatchesState;
444+
445+
tabMatchesState.reset();
446+
spyOn(tabsCtrl, 'select');
447+
tabDoesMatch = false;
448+
449+
tabEl.scope().$broadcast('$stateChangeSuccess');
450+
expect(tabMatchesState).toHaveBeenCalled();
451+
expect(tabsCtrl.select).not.toHaveBeenCalled();
452+
453+
tabMatchesState.reset();
454+
tabDoesMatch = true;
455+
456+
tabEl.scope().$broadcast('$stateChangeSuccess');
457+
expect(tabMatchesState).toHaveBeenCalled();
458+
expect(tabsCtrl.select).toHaveBeenCalledWith(tabEl.scope());
352459
});
353460

354461
it('should transclude on $tabSelected=true', function() {

0 commit comments

Comments
 (0)