fix(tabs): pop tab page to parent nav

Closes #5196
This commit is contained in:
Adam Bradley
2016-02-19 21:43:24 -06:00
parent af21503a55
commit b9eec24c88
6 changed files with 152 additions and 35 deletions

View File

@ -7,7 +7,7 @@ import {IonicApp} from '../app/app';
import {Keyboard} from '../../util/keyboard'; import {Keyboard} from '../../util/keyboard';
import {NavParams} from './nav-params'; import {NavParams} from './nav-params';
import {NavRouter} from './nav-router'; import {NavRouter} from './nav-router';
import {pascalCaseToDashCase, isTrueProperty} from '../../util/util'; import {pascalCaseToDashCase, isTrueProperty, isUndefined} from '../../util/util';
import {raf} from '../../util/dom'; import {raf} from '../../util/dom';
import {SwipeBackGesture} from './swipe-back'; import {SwipeBackGesture} from './swipe-back';
import {Transition} from '../../transitions/transition'; import {Transition} from '../../transitions/transition';
@ -119,7 +119,7 @@ export class NavController extends Ion {
/** /**
* @private * @private
*/ */
id: number; id: string;
/** /**
* @private * @private
@ -163,7 +163,7 @@ export class NavController extends Ion {
this._sbEnabled = config.getBoolean('swipeBackEnabled') || false; this._sbEnabled = config.getBoolean('swipeBackEnabled') || false;
this._sbThreshold = config.get('swipeBackThreshold') || 40; this._sbThreshold = config.get('swipeBackThreshold') || 40;
this.id = ++ctrlIds; this.id = (++ctrlIds).toString();
// build a new injector for child ViewControllers to use // build a new injector for child ViewControllers to use
this.providers = Injector.resolve([ this.providers = Injector.resolve([
@ -622,6 +622,12 @@ export class NavController extends Ion {
let activeView = this.getByState(STATE_TRANS_ENTER) || let activeView = this.getByState(STATE_TRANS_ENTER) ||
this.getByState(STATE_INIT_ENTER) || this.getByState(STATE_INIT_ENTER) ||
this.getActive(); this.getActive();
// if not set, by default climb up the nav controllers if
// there isn't a previous view in this nav controller
if (isUndefined(opts.climbNav)) {
opts.climbNav = true;
}
return this.remove(this.indexOf(activeView), 1, opts); return this.remove(this.indexOf(activeView), 1, opts);
} }
@ -717,6 +723,39 @@ export class NavController extends Ion {
if (leavingView) { if (leavingView) {
// there is a view ready to leave, meaning that a transition needs // there is a view ready to leave, meaning that a transition needs
// to happen and the previously active view is going to animate out // to happen and the previously active view is going to animate out
// get the view thats ready to enter
let enteringView = this.getByState(STATE_INIT_ENTER);
if (!enteringView) {
// oh knows! no entering view to go to!
// if there is no previous view that would enter in this nav stack
// and the option is set to climb up the nav parent looking
// for the next nav we could transition to instead
if (opts.climbNav) {
let parentNav: NavController = this.parent;
while (parentNav) {
if (!parentNav['_tabs']) {
// Tabs can be a parent, but it is not a collection of views
// only we're looking for an actual NavController w/ stack of views
leavingView.willLeave();
return parentNav.pop(opts).then((rtnVal: boolean) => {
leavingView.didLeave();
return rtnVal;
});
}
parentNav = parentNav.parent;
}
}
// there's no previous view and there's no valid parent nav
// to climb to so this shouldn't actually remove the leaving
// view because there's nothing that would enter, eww
leavingView.state = STATE_ACTIVE;
return Promise.resolve(false);
}
let resolve; let resolve;
let promise = new Promise(res => { resolve = res; }); let promise = new Promise(res => { resolve = res; });
@ -724,9 +763,6 @@ export class NavController extends Ion {
opts.animation = leavingView.getTransitionName(opts.direction); opts.animation = leavingView.getTransitionName(opts.direction);
} }
// get the view thats ready to enter
let enteringView = this.getByState(STATE_INIT_ENTER);
// start the transition, fire resolve when done... // start the transition, fire resolve when done...
this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => { this._transition(enteringView, leavingView, opts, (hasCompleted: boolean) => {
// transition has completed!! // transition has completed!!
@ -1226,6 +1262,13 @@ export class NavController extends Ion {
} }
ngOnDestroy() {
for (var i = this._views.length - 1; i >= 0; i--) {
this._views[i].destroy();
}
this._views = [];
}
/** /**
* @private * @private
*/ */
@ -1266,6 +1309,8 @@ export class NavController extends Ion {
if (!hostViewRef.destroyed && index !== -1) { if (!hostViewRef.destroyed && index !== -1) {
viewContainer.remove(index); viewContainer.remove(index);
} }
view.setInstance(null);
}); });
// a new ComponentRef has been created // a new ComponentRef has been created
@ -1575,6 +1620,7 @@ export interface NavOptions {
transitionDelay?: number; transitionDelay?: number;
postLoad?: Function; postLoad?: Function;
progressAnimation?: boolean; progressAnimation?: boolean;
climbNav?: boolean;
} }
const STATE_ACTIVE = 'active'; const STATE_ACTIVE = 'active';

View File

@ -16,7 +16,7 @@ import {ViewController} from './view-controller';
selector: 'ion-nav' selector: 'ion-nav'
}) })
export class NavRouter extends RouterOutlet { export class NavRouter extends RouterOutlet {
private _activeViewId; private _lastUrl: string;
constructor( constructor(
_elementRef: ElementRef, _elementRef: ElementRef,
@ -33,11 +33,6 @@ export class NavRouter extends RouterOutlet {
_nav.registerRouter(this); _nav.registerRouter(this);
} }
/**
* @private
* TODO
* @param {ComponentInstruction} instruction TODO
*/
activate(nextInstruction: ComponentInstruction): Promise<any> { activate(nextInstruction: ComponentInstruction): Promise<any> {
var previousInstruction = this['_currentInstruction']; var previousInstruction = this['_currentInstruction'];
this['_currentInstruction'] = nextInstruction; this['_currentInstruction'] = nextInstruction;
@ -45,11 +40,17 @@ export class NavRouter extends RouterOutlet {
var childRouter = this['_parentRouter'].childRouter(componentType); var childRouter = this['_parentRouter'].childRouter(componentType);
// prevent double navigations to the same view // prevent double navigations to the same view
var lastView = this._nav.last(); let instruction = new ResolvedInstruction(nextInstruction, null, null);
if (this._nav.isTransitioning() || lastView && lastView.componentType === componentType && lastView.data === nextInstruction.params) { let url: string;
return Promise.resolve(); if (instruction) {
url = instruction.toRootUrl();
if (url === this._lastUrl) {
return Promise.resolve();
}
} }
console.debug('NavRouter, activate:', componentType.name, 'url:', url);
// tell the NavController which componentType, and it's params, to navigate to // tell the NavController which componentType, and it's params, to navigate to
return this._nav.push(componentType, nextInstruction.params); return this._nav.push(componentType, nextInstruction.params);
} }
@ -58,19 +59,13 @@ export class NavRouter extends RouterOutlet {
return Promise.resolve(); return Promise.resolve();
} }
/**
* Called by Ionic after a transition has completed.
* @param {string} direction The direction of the state change
* @param {ViewController} viewCtrl The entering ViewController
*/
stateChange(direction: string, viewCtrl: ViewController) { stateChange(direction: string, viewCtrl: ViewController) {
// stateChange is called by Ionic's NavController // stateChange is called by Ionic's NavController
// type could be "push" or "pop" // type could be "push" or "pop"
// viewCtrl is Ionic's ViewController class, which has the properties "componentType" and "params" // viewCtrl is Ionic's ViewController class, which has the properties "componentType" and "params"
// only do an update if there's an actual view change // only do an update if there's an actual view change
if (!viewCtrl || this._activeViewId === viewCtrl.id) return; if (!viewCtrl) return;
this._activeViewId = viewCtrl.id;
// get the best PathRecognizer for this view's componentType // get the best PathRecognizer for this view's componentType
let pathRecognizer = this.getPathRecognizerByComponent(viewCtrl.componentType); let pathRecognizer = this.getPathRecognizerByComponent(viewCtrl.componentType);
@ -81,16 +76,19 @@ export class NavRouter extends RouterOutlet {
// create a ResolvedInstruction from the componentInstruction // create a ResolvedInstruction from the componentInstruction
let instruction = new ResolvedInstruction(componentInstruction, null, null); let instruction = new ResolvedInstruction(componentInstruction, null, null);
if (instruction) {
let url = instruction.toRootUrl();
if (url === this._lastUrl) return;
this['_parentRouter'].navigateByInstruction(instruction); this._lastUrl = url;
this['_parentRouter'].navigateByInstruction(instruction);
console.debug('NavRouter, stateChange, name:', viewCtrl.name, 'id:', viewCtrl.id, 'url:', url);
}
} }
} }
/**
* TODO
* @param {TODO} componentType TODO
* @returns {TODO} TODO
*/
getPathRecognizerByComponent(componentType) { getPathRecognizerByComponent(componentType) {
// given a componentType, figure out the best PathRecognizer to use // given a componentType, figure out the best PathRecognizer to use
let rules = this['_parentRouter'].registry._rules; let rules = this['_parentRouter'].registry._rules;

View File

@ -1,8 +1,26 @@
import {NavController, NavOptions, Config, ViewController} from '../../../../ionic/ionic'; import {NavController, Tabs, NavOptions, Config, ViewController} from '../../../../ionic/ionic';
export function run() { export function run() {
describe('NavController', () => { describe('NavController', () => {
describe('pop', () => {
it('should do nothing if its the first view in the stack', () => {
let view1 = new ViewController(Page1);
view1.state = STATE_ACTIVE;
nav._views = [view1];
expect(nav.length()).toBe(1);
nav.pop();
expect(nav.length()).toBe(1);
expect(nav.getByIndex(0).state).toBe(STATE_ACTIVE);
expect(nav.getByIndex(0).componentType).toBe(Page1);
});
});
describe('popToRoot', () => { describe('popToRoot', () => {
it('should go back to root', () => { it('should go back to root', () => {
@ -1022,10 +1040,15 @@ export function run() {
class Page5 {} class Page5 {}
beforeEach(() => { beforeEach(() => {
nav = mockNav();
});
function mockNav() {
let elementRef = { let elementRef = {
nativeElement: document.createElement('div') nativeElement: document.createElement('div')
}; };
nav = new NavController(null, null, config, null, elementRef, null, null, null, null, null);
let nav = new NavController(null, null, config, null, elementRef, null, null, null, null, null);
nav._keyboard = { nav._keyboard = {
isOpen: function() { isOpen: function() {
@ -1045,7 +1068,9 @@ export function run() {
setElementClass: function(){}, setElementClass: function(){},
setElementStyle: function(){} setElementStyle: function(){}
}; };
});
return nav;
}
}); });
} }

View File

@ -92,7 +92,7 @@ export class Tab extends NavController {
private _panelId: string; private _panelId: string;
private _btnId: string; private _btnId: string;
private _loaded: boolean; private _loaded: boolean;
private _loadTimer: any; private _loadTmr: number;
/** /**
* @private * @private
@ -181,7 +181,7 @@ export class Tab extends NavController {
* @private * @private
*/ */
preload(wait: number) { preload(wait: number) {
this._loadTimer = setTimeout(() => { this._loadTmr = setTimeout(() => {
if (!this._loaded) { if (!this._loaded) {
console.debug('Tabs, preload', this.id); console.debug('Tabs, preload', this.id);
this.load({ this.load({
@ -247,7 +247,8 @@ export class Tab extends NavController {
* @private * @private
*/ */
ngOnDestroy() { ngOnDestroy() {
clearTimeout(this._loadTimer); clearTimeout(this._loadTmr);
super.ngOnDestroy();
} }
} }

View File

@ -209,7 +209,7 @@ export class Tabs extends Ion {
/** /**
* @private * @private
*/ */
add(tab) { add(tab: Tab) {
tab.id = this.id + '-' + (++this._ids); tab.id = this.id + '-' + (++this._ids);
this._tabs.push(tab); this._tabs.push(tab);
} }

View File

@ -49,6 +49,14 @@ class SignIn {
}) })
class ChatPage { class ChatPage {
constructor(private viewCtrl: ViewController) {} constructor(private viewCtrl: ViewController) {}
onPageDidLoad() {
console.log('ChatPage, onPageDidLoad');
}
onPageDidUnload() {
console.log('ChatPage, onPageDidUnload');
}
} }
@ -98,6 +106,10 @@ class TabsPage {
onPageDidLeave() { onPageDidLeave() {
console.log('TabsPage, onPageDidLeave'); console.log('TabsPage, onPageDidLeave');
} }
onPageDidUnload() {
console.log('TabsPage, onPageDidUnload');
}
} }
@ -113,6 +125,7 @@ class TabsPage {
'<p><button id="goToTab1Page2" (click)="push()">Go to Tab 1, Page 2</button></p>' + '<p><button id="goToTab1Page2" (click)="push()">Go to Tab 1, Page 2</button></p>' +
'<p><button (click)="logout()">Logout</button></p>' + '<p><button (click)="logout()">Logout</button></p>' +
'<p><button (click)="favoritesTab()">Favorites Tab</button></p>' + '<p><button (click)="favoritesTab()">Favorites Tab</button></p>' +
'<p><button (click)="goBack()">Go Back</button></p>' +
'<p>UserId: {{userId}}</p>' + '<p>UserId: {{userId}}</p>' +
'<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>' + '<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>' +
'<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>' + '<f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f><f></f>' +
@ -129,6 +142,13 @@ class Tab1Page1 {
this.nav.push(Tab1Page2) this.nav.push(Tab1Page2)
} }
goBack() {
console.log('go back begin');
this.nav.pop().then((val) => {
console.log('go back completed', val);
});;
}
favoritesTab() { favoritesTab() {
this.tabs.select(1); this.tabs.select(1);
} }
@ -152,6 +172,10 @@ class Tab1Page1 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab1Page1, onPageDidLeave'); console.log('Tab1Page1, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab1Page1, onPageDidUnload');
}
} }
@ -189,6 +213,10 @@ class Tab1Page2 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab1Page2, onPageDidLeave'); console.log('Tab1Page2, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab1Page2, onPageDidUnload');
}
} }
@ -221,6 +249,10 @@ class Tab1Page3 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab1Page3, onPageDidLeave'); console.log('Tab1Page3, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab1Page3, onPageDidUnload');
}
} }
@ -261,6 +293,10 @@ class Tab2Page1 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab2Page1, onPageDidLeave'); console.log('Tab2Page1, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab2Page1, onPageDidUnload');
}
} }
@ -298,6 +334,10 @@ class Tab2Page2 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab2Page2, onPageDidLeave'); console.log('Tab2Page2, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab2Page2, onPageDidUnload');
}
} }
@ -330,6 +370,10 @@ class Tab2Page3 {
onPageDidLeave() { onPageDidLeave() {
console.log('Tab2Page3, onPageDidLeave'); console.log('Tab2Page3, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab2Page3, onPageDidUnload');
}
} }
@ -362,6 +406,9 @@ class Tab3Page1 {
console.log('Tab3Page1, onPageDidLeave'); console.log('Tab3Page1, onPageDidLeave');
} }
onPageDidUnload() {
console.log('Tab3Page1, onPageDidUnload');
}
} }