feat(navPop): add nav pop method on the app instance

This commit is contained in:
Adam Bradley
2016-06-08 10:42:13 -05:00
parent 84f37cf4d5
commit 9f293e8549
6 changed files with 473 additions and 39 deletions

View File

@ -1,9 +1,11 @@
import {Injectable, Injector} from '@angular/core'; import {Injectable, Injector} from '@angular/core';
import {Title} from '@angular/platform-browser'; import {Title} from '@angular/platform-browser';
import {Config} from '../../config/config';
import {ClickBlock} from '../../util/click-block'; import {ClickBlock} from '../../util/click-block';
import {Config} from '../../config/config';
import {NavController} from '../nav/nav-controller';
import {Platform} from '../../platform/platform'; import {Platform} from '../../platform/platform';
import {Tabs} from '../tabs/tabs';
/** /**
@ -15,24 +17,17 @@ export class App {
private _scrollTime: number = 0; private _scrollTime: number = 0;
private _title: string = ''; private _title: string = '';
private _titleSrv: Title = new Title(); private _titleSrv: Title = new Title();
private _rootNav: any = null; private _rootNav: NavController = null;
private _appInjector: Injector; private _appInjector: Injector;
constructor( constructor(
private _config: Config, private _config: Config,
private _clickBlock: ClickBlock, private _clickBlock: ClickBlock,
platform: Platform private _platform: Platform
) { ) {
platform.backButton.subscribe(() => { // listen for hardware back button events
let activeNav = this.getActiveNav(); // register this back button action with a default priority
if (activeNav) { _platform.registerBackButtonAction(this.navPop.bind(this));
if (activeNav.length() === 1) {
platform.exitApp();
} else {
activeNav.pop();
}
}
});
} }
/** /**
@ -100,7 +95,7 @@ export class App {
/** /**
* @private * @private
*/ */
getActiveNav(): any { getActiveNav(): NavController {
var nav = this._rootNav || null; var nav = this._rootNav || null;
var activeChildNav: any; var activeChildNav: any;
@ -118,7 +113,7 @@ export class App {
/** /**
* @private * @private
*/ */
getRootNav(): any { getRootNav(): NavController {
return this._rootNav; return this._rootNav;
} }
@ -129,6 +124,72 @@ export class App {
this._rootNav = nav; this._rootNav = nav;
} }
/**
* @private
*/
navPop(): Promise<any> {
// function used to climb up all parent nav controllers
function navPop(nav: any): Promise<any> {
if (nav) {
if (nav.length && nav.length() > 1) {
// this nav controller has more than one view
// pop the current view on this nav and we're done here
console.debug('app, goBack pop nav');
return nav.pop();
} else if (nav.previousTab) {
// FYI, using "nav instanceof Tabs" throws a Promise runtime error for whatever reason, idk
// this is a Tabs container
// see if there is a valid previous tab to go to
let prevTab = nav.previousTab(true);
if (prevTab) {
console.debug('app, goBack previous tab');
nav.select(prevTab);
return Promise.resolve();
}
}
// try again using the parent nav (if there is one)
return navPop(nav.parent);
}
// nerp, never found nav that could pop off a view
return null;
}
// app must be enabled and there must be a
// root nav controller for go back to work
if (this._rootNav && this.isEnabled()) {
// first check if the root navigation has any overlays
// opened in it's portal, like alert/actionsheet/popup
let portal = this._rootNav.getPortal && this._rootNav.getPortal();
if (portal && portal.length() > 0) {
// there is an overlay view in the portal
// let's pop this one off to go back
console.debug('app, goBack pop overlay');
return portal.pop();
}
// next get the active nav, check itself and climb up all
// of its parent navs until it finds a nav that can pop
let navPromise = navPop(this.getActiveNav());
if (navPromise === null) {
// no views to go back to
// let's exit the app
if (this._config.getBoolean('navExitApp', true)) {
console.debug('app, goBack exitApp');
this._platform.exitApp();
}
} else {
return navPromise;
}
}
return Promise.resolve();
}
/** /**
* @private * @private
*/ */

View File

@ -1,9 +1,277 @@
import {Component} from '@angular/core';
import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src';
export function run() { export function run() {
describe('IonicApp', () => { describe('App', () => {
describe('navPop', () => {
it('should select the previous tab', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
nav.registerChildNav(tabs);
tabs.select(tab1);
tabs.select(tab2);
expect(tabs.selectHistory).toEqual([tab1.id, tab2.id]);
spyOn(platform, 'exitApp');
spyOn(tabs, 'select');
spyOn(tab1, 'pop');
spyOn(tab2, 'pop');
spyOn(portal, 'pop');
app.navPop();
expect(tabs.select).toHaveBeenCalledWith(tab1);
expect(tab1.pop).not.toHaveBeenCalled();
expect(tab2.pop).not.toHaveBeenCalled();
expect(portal.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop from the active tab, when tabs is nested is the root nav', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
let tab3 = mockTab(tabs);
nav.registerChildNav(tabs);
tab2.setSelected(true);
spyOn(platform, 'exitApp');
spyOn(tab2, 'pop');
spyOn(portal, 'pop');
let view1 = new ViewController();
let view2 = new ViewController();
tab2._views = [view1, view2];
app.navPop();
expect(tab2.pop).toHaveBeenCalled();
expect(portal.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop from the active tab, when tabs is the root', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
let tab3 = mockTab(tabs);
app.setRootNav(tabs);
tab2.setSelected(true);
spyOn(platform, 'exitApp');
spyOn(tab2, 'pop');
let view1 = new ViewController();
let view2 = new ViewController();
tab2._views = [view1, view2];
app.navPop();
expect(tab2.pop).toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop the root nav when nested nav has less than 2 views', () => {
let rootNav = mockNav();
let nestedNav = mockNav();
let portal = mockNav();
rootNav.setPortal(portal);
rootNav.registerChildNav(nestedNav);
nestedNav.parent = rootNav;
app.setRootNav(rootNav);
spyOn(platform, 'exitApp');
spyOn(rootNav, 'pop');
spyOn(nestedNav, 'pop');
spyOn(portal, 'pop');
let rootView1 = new ViewController();
let rootView2 = new ViewController();
rootNav._views = [rootView1, rootView2];
let nestedView1 = new ViewController();
nestedNav._views = [nestedView1];
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(rootNav.pop).toHaveBeenCalled();
expect(nestedNav.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop a view from the nested nav that has more than 1 view', () => {
let rootNav = mockNav();
let nestedNav = mockNav();
let portal = mockNav();
rootNav.setPortal(portal);
app.setRootNav(rootNav);
rootNav.registerChildNav(nestedNav);
spyOn(platform, 'exitApp');
spyOn(rootNav, 'pop');
spyOn(nestedNav, 'pop');
spyOn(portal, 'pop');
let rootView1 = new ViewController();
let rootView2 = new ViewController();
rootNav._views = [rootView1, rootView2];
let nestedView1 = new ViewController();
let nestedView2 = new ViewController();
nestedNav._views = [nestedView1, nestedView2];
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(rootNav.pop).not.toHaveBeenCalled();
expect(nestedNav.pop).toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop the overlay in the portal of the root nav', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
spyOn(platform, 'exitApp');
spyOn(nav, 'pop');
spyOn(portal, 'pop');
let view1 = new ViewController();
let view2 = new ViewController();
nav._views = [view1, view2];
let overlay = new ViewController();
portal._views = [overlay];
app.navPop();
expect(portal.pop).toHaveBeenCalled();
expect(nav.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should pop the second view in the root nav', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
spyOn(platform, 'exitApp');
spyOn(nav, 'pop');
spyOn(portal, 'pop');
let view1 = new ViewController();
let view2 = new ViewController();
nav._views = [view1, view2];
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(nav.pop).toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should exit app when only one view in the root nav', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
spyOn(platform, 'exitApp');
spyOn(nav, 'pop');
spyOn(portal, 'pop');
let view1 = new ViewController();
nav._views = [view1];
expect(app.getActiveNav()).toBe(nav);
expect(nav.first()).toBe(view1);
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(nav.pop).not.toHaveBeenCalled();
expect(platform.exitApp).toHaveBeenCalled();
});
it('should not exit app when only one view in the root nav, but navExitApp config set', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
spyOn(platform, 'exitApp');
spyOn(nav, 'pop');
spyOn(portal, 'pop');
config.set('navExitApp', false);
let view1 = new ViewController();
nav._views = [view1];
expect(app.getActiveNav()).toBe(nav);
expect(nav.first()).toBe(view1);
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(nav.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should not go back if app is not enabled', () => {
let nav = mockNav();
let portal = mockNav();
nav.setPortal(portal);
app.setRootNav(nav);
spyOn(platform, 'exitApp');
spyOn(nav, 'pop');
spyOn(portal, 'pop');
let view1 = new ViewController();
nav._views = [view1];
app.setEnabled(false, 10000);
app.navPop();
expect(portal.pop).not.toHaveBeenCalled();
expect(nav.pop).not.toHaveBeenCalled();
expect(platform.exitApp).not.toHaveBeenCalled();
});
it('should not go back if there is no root nav', () => {
spyOn(platform, 'exitApp');
app.navPop();
expect(platform.exitApp).not.toHaveBeenCalled();
});
});
describe('getActiveNav', () => { describe('getActiveNav', () => {
@ -27,7 +295,7 @@ describe('IonicApp', () => {
expect(app.getActiveNav()).toBe(nav3); expect(app.getActiveNav()).toBe(nav3);
}); });
it('should get active NavController when using tabs', () => { it('should get active NavController when using tabs, nested in a root nav', () => {
let nav = mockNav(); let nav = mockNav();
app.setRootNav(nav); app.setRootNav(nav);
@ -46,6 +314,22 @@ describe('IonicApp', () => {
expect(app.getActiveNav()).toBe(tab3); expect(app.getActiveNav()).toBe(tab3);
}); });
it('should get active tab NavController when using tabs, and tabs is the root', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
let tab3 = mockTab(tabs);
app.setRootNav(tabs);
tab2.setSelected(true);
expect(app.getActiveNav()).toBe(tab2);
tab2.setSelected(false);
tab3.setSelected(true);
expect(app.getActiveNav()).toBe(tab3);
});
it('should get active NavController when nested 3 deep', () => { it('should get active NavController when nested 3 deep', () => {
let nav1 = mockNav(); let nav1 = mockNav();
let nav2 = mockNav(); let nav2 = mockNav();
@ -170,9 +454,18 @@ describe('IonicApp', () => {
} }
function mockTab(parentTabs: Tabs): Tab { function mockTab(parentTabs: Tabs): Tab {
return new Tab(parentTabs, app, config, null, null, null, null, null, _cd); var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd);
parentTabs.add(tab);
tab.root = SomePage;
tab.load = function(opts: any, cb: Function) {
cb();
};
return tab;
} }
@Component({})
class SomePage {}
beforeEach(() => { beforeEach(() => {
config = new Config(); config = new Config();
platform = new Platform(); platform = new Platform();

View File

@ -118,6 +118,7 @@ export class Modal extends ViewController {
this.modalViewType = componentType.name; this.modalViewType = componentType.name;
this.viewType = 'modal'; this.viewType = 'modal';
this.isOverlay = true; this.isOverlay = true;
this.usePortal = true;
} }
/** /**

View File

@ -6,8 +6,8 @@ import {Config} from '../../config/config';
import {Ion} from '../ion'; import {Ion} from '../ion';
import {isBlank, pascalCaseToDashCase} from '../../util/util'; import {isBlank, pascalCaseToDashCase} from '../../util/util';
import {Keyboard} from '../../util/keyboard'; import {Keyboard} from '../../util/keyboard';
import {NavParams} from './nav-params';
import {MenuController} from '../menu/menu-controller'; import {MenuController} from '../menu/menu-controller';
import {NavParams} from './nav-params';
import {NavPortal} from './nav-portal'; import {NavPortal} from './nav-portal';
import {SwipeBackGesture} from './swipe-back'; import {SwipeBackGesture} from './swipe-back';
import {Transition} from '../../transitions/transition'; import {Transition} from '../../transitions/transition';
@ -245,6 +245,13 @@ export class NavController extends Ion {
this.viewDidUnload = new EventEmitter(); this.viewDidUnload = new EventEmitter();
} }
/**
* @private
*/
getPortal(): NavController {
return this._portal;
}
/** /**
* @private * @private
*/ */

View File

@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core'; import {Component, ViewChild} from '@angular/core';
import {ionicBootstrap, NavParams, NavController, ViewController, MenuController} from '../../../../../src'; import {ionicBootstrap, NavController, MenuController} from '../../../../../src';
import {Config, Nav} from '../../../../../src'; import {Config, Nav, App} from '../../../../../src';
@Component({ @Component({
@ -9,16 +9,21 @@ import {Config, Nav} from '../../../../../src';
<ion-title>Login</ion-title> <ion-title>Login</ion-title>
</ion-navbar> </ion-navbar>
<ion-content style="text-align:center;" padding> <ion-content style="text-align:center;" padding>
<button (click)="goToAccount()">Login</button> <p><button (click)="goToAccount()">Login</button></p>
<p><button (click)="goBack()">App goBack()</button></p>
</ion-content> </ion-content>
` `
}) })
export class Login { export class Login {
constructor(private nav: NavController) {} constructor(private nav: NavController, private app: App) {}
goToAccount() { goToAccount() {
this.nav.push(Account); this.nav.push(Account);
} }
goBack() {
this.app.navPop();
}
} }
@ -39,21 +44,22 @@ export class Login {
<button ion-item detail-none (click)="logOut()"> <button ion-item detail-none (click)="logOut()">
Logout Logout
</button> </button>
<button ion-item detail-none (click)="goBack()">
App Go Back
</button>
</ion-list> </ion-list>
</ion-content> </ion-content>
</ion-menu> </ion-menu>
<ion-nav id="account-nav" [root]="rootPage" #content swipeBackEnabled="false"></ion-nav> <ion-nav #accountNav #content [root]="root" swipeBackEnabled="false"></ion-nav>
` `
}) })
export class Account { export class Account {
@ViewChild('account-nav') accountNav: Nav; @ViewChild('accountNav') accountNav: Nav;
rootPage = Dashboard; root = Dashboard;
constructor(private menu: MenuController, private nav: NavController) { constructor(private menu: MenuController, private app: App) {}
}
goToProfile() { goToProfile() {
this.accountNav.setRoot(Profile).then(() => { this.accountNav.setRoot(Profile).then(() => {
@ -68,7 +74,13 @@ export class Account {
} }
logOut() { logOut() {
this.nav.parent.setRoot(Login, null, { animate: true }); this.accountNav.setRoot(Login, null, { animate: true }).then(() => {
this.menu.close();
});
}
goBack() {
this.app.navPop();
} }
} }
@ -84,21 +96,27 @@ export class Account {
<ion-content padding> <ion-content padding>
<p><button (click)="goToProfile()">Profile</button></p> <p><button (click)="goToProfile()">Profile</button></p>
<p><button (click)="logOut()">Logout</button></p> <p><button (click)="logOut()">Logout</button></p>
<p><button (click)="goBack()">App goBack()</button></p>
</ion-content> </ion-content>
` `
}) })
export class Dashboard { export class Dashboard {
constructor(private nav: NavController) {} constructor(private nav: NavController, private app: App) {}
goToProfile() { goToProfile() {
this.nav.push(Profile); this.nav.push(Profile);
} }
logOut() { logOut() {
this.nav.parent.setRoot(Login, null, { this.nav.parent.setRoot(Login, null, {
animate: true, animate: true,
direction: 'back' direction: 'back'
}); });
} }
goBack() {
this.app.navPop();
}
} }
@ -113,11 +131,12 @@ export class Dashboard {
<ion-content padding> <ion-content padding>
<p><button (click)="goToDashboard()">Dashboard</button></p> <p><button (click)="goToDashboard()">Dashboard</button></p>
<p><button (click)="logOut()">Logout</button></p> <p><button (click)="logOut()">Logout</button></p>
<p><button (click)="goBack()">App goBack()</button></p>
</ion-content> </ion-content>
` `
}) })
export class Profile { export class Profile {
constructor(private nav: NavController) {} constructor(private nav: NavController, private app: App) {}
goToDashboard() { goToDashboard() {
this.nav.push(Dashboard); this.nav.push(Dashboard);
@ -129,6 +148,10 @@ export class Profile {
direction: 'back' direction: 'back'
}); });
} }
goBack() {
this.app.navPop();
}
} }

View File

@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../../../../../src'; import {ionicBootstrap, NavController, App, Alert, Modal, ViewController, Tab, Tabs} from '../../../../../src';
// //
// Modal // Modal
@ -34,6 +34,9 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../..
<button ion-item danger detail-none> <button ion-item danger detail-none>
Reset All Filters Reset All Filters
</button> </button>
<button ion-item danger detail-none (click)="appNavPop()">
App Nav Pop
</button>
</ion-list> </ion-list>
</ion-content> </ion-content>
` `
@ -41,7 +44,7 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../..
class MyModal { class MyModal {
items: any[] = []; items: any[] = [];
constructor(private viewCtrl: ViewController) { constructor(private viewCtrl: ViewController, private app: App) {
for (var i = 1; i <= 10; i++) { for (var i = 1; i <= 10; i++) {
this.items.push(i); this.items.push(i);
} }
@ -52,6 +55,10 @@ class MyModal {
// can "dismiss" itself and pass back data // can "dismiss" itself and pass back data
this.viewCtrl.dismiss(); this.viewCtrl.dismiss();
} }
appNavPop() {
this.app.navPop();
}
} }
// //
@ -69,17 +76,31 @@ class MyModal {
</ion-list-header> </ion-list-header>
<ion-item *ngFor="let i of items">Item {{i}} {{i}} {{i}} {{i}}</ion-item> <ion-item *ngFor="let i of items">Item {{i}} {{i}} {{i}} {{i}}</ion-item>
</ion-list> </ion-list>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content> </ion-content>
` `
}) })
export class Tab1 { export class Tab1 {
items: any[] = []; items: any[] = [];
constructor() { constructor(private tabs: Tabs, private app: App) {
for (var i = 1; i <= 250; i++) { for (var i = 1; i <= 250; i++) {
this.items.push(i); this.items.push(i);
} }
} }
selectPrevious() {
this.tabs.select(this.tabs.previousTab());
}
appNavPop() {
this.app.navPop();
}
} }
// //
@ -103,13 +124,19 @@ export class Tab1 {
</ion-item-options> </ion-item-options>
</ion-item-sliding> </ion-item-sliding>
</ion-list> </ion-list>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content> </ion-content>
` `
}) })
export class Tab2 { export class Tab2 {
sessions: any[] = []; sessions: any[] = [];
constructor() { constructor(private tabs: Tabs, private app: App) {
for (var i = 1; i <= 250; i++) { for (var i = 1; i <= 250; i++) {
this.sessions.push({ this.sessions.push({
name: 'Name ' + i, name: 'Name ' + i,
@ -117,6 +144,14 @@ export class Tab2 {
}); });
} }
} }
selectPrevious() {
this.tabs.select(this.tabs.previousTab());
}
appNavPop() {
this.app.navPop();
}
} }
// //
@ -136,11 +171,17 @@ export class Tab2 {
<button (click)="presentAlert()">Present Alert</button> <button (click)="presentAlert()">Present Alert</button>
<button (click)="presentModal()">Present Modal</button> <button (click)="presentModal()">Present Modal</button>
</p> </p>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content> </ion-content>
` `
}) })
export class Tab3 { export class Tab3 {
constructor(private nav: NavController) {} constructor(private nav: NavController, private tabs: Tabs, private app: App) {}
presentAlert() { presentAlert() {
let alert = Alert.create({ let alert = Alert.create({
@ -154,6 +195,14 @@ export class Tab3 {
let modal = Modal.create(MyModal); let modal = Modal.create(MyModal);
this.nav.present(modal); this.nav.present(modal);
} }
selectPrevious() {
this.tabs.select(this.tabs.previousTab());
}
appNavPop() {
this.app.navPop();
}
} }
@ -184,11 +233,11 @@ export class TabsPage {
root2 = Tab2; root2 = Tab2;
root3 = Tab3; root3 = Tab3;
onChange(ev) { onChange(ev: Tab) {
console.log("Changed tab", ev); console.log("Changed tab", ev);
} }
onSelect(ev) { onSelect(ev: Tab) {
console.log("Selected tab", ev); console.log("Selected tab", ev);
} }
} }