diff --git a/src/components/app/app.ts b/src/components/app/app.ts
index b3df0d51d27..a0f4753b017 100644
--- a/src/components/app/app.ts
+++ b/src/components/app/app.ts
@@ -1,9 +1,11 @@
import {Injectable, Injector} from '@angular/core';
import {Title} from '@angular/platform-browser';
-import {Config} from '../../config/config';
import {ClickBlock} from '../../util/click-block';
+import {Config} from '../../config/config';
+import {NavController} from '../nav/nav-controller';
import {Platform} from '../../platform/platform';
+import {Tabs} from '../tabs/tabs';
/**
@@ -15,24 +17,17 @@ export class App {
private _scrollTime: number = 0;
private _title: string = '';
private _titleSrv: Title = new Title();
- private _rootNav: any = null;
+ private _rootNav: NavController = null;
private _appInjector: Injector;
constructor(
private _config: Config,
private _clickBlock: ClickBlock,
- platform: Platform
+ private _platform: Platform
) {
- platform.backButton.subscribe(() => {
- let activeNav = this.getActiveNav();
- if (activeNav) {
- if (activeNav.length() === 1) {
- platform.exitApp();
- } else {
- activeNav.pop();
- }
- }
- });
+ // listen for hardware back button events
+ // register this back button action with a default priority
+ _platform.registerBackButtonAction(this.navPop.bind(this));
}
/**
@@ -100,7 +95,7 @@ export class App {
/**
* @private
*/
- getActiveNav(): any {
+ getActiveNav(): NavController {
var nav = this._rootNav || null;
var activeChildNav: any;
@@ -118,7 +113,7 @@ export class App {
/**
* @private
*/
- getRootNav(): any {
+ getRootNav(): NavController {
return this._rootNav;
}
@@ -129,6 +124,72 @@ export class App {
this._rootNav = nav;
}
+ /**
+ * @private
+ */
+ navPop(): Promise {
+ // function used to climb up all parent nav controllers
+ function navPop(nav: any): Promise {
+ 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
*/
diff --git a/src/components/app/test/app.spec.ts b/src/components/app/test/app.spec.ts
index 44b63a56bf5..9ec924992b9 100644
--- a/src/components/app/test/app.spec.ts
+++ b/src/components/app/test/app.spec.ts
@@ -1,9 +1,277 @@
+import {Component} from '@angular/core';
import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src';
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', () => {
@@ -27,7 +295,7 @@ describe('IonicApp', () => {
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();
app.setRootNav(nav);
@@ -46,6 +314,22 @@ describe('IonicApp', () => {
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', () => {
let nav1 = mockNav();
let nav2 = mockNav();
@@ -170,9 +454,18 @@ describe('IonicApp', () => {
}
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(() => {
config = new Config();
platform = new Platform();
diff --git a/src/components/modal/modal.ts b/src/components/modal/modal.ts
index 7bc4f353cef..2b415962a6f 100644
--- a/src/components/modal/modal.ts
+++ b/src/components/modal/modal.ts
@@ -118,6 +118,7 @@ export class Modal extends ViewController {
this.modalViewType = componentType.name;
this.viewType = 'modal';
this.isOverlay = true;
+ this.usePortal = true;
}
/**
diff --git a/src/components/nav/nav-controller.ts b/src/components/nav/nav-controller.ts
index 61b391bfca8..5edd378dbd4 100644
--- a/src/components/nav/nav-controller.ts
+++ b/src/components/nav/nav-controller.ts
@@ -6,8 +6,8 @@ import {Config} from '../../config/config';
import {Ion} from '../ion';
import {isBlank, pascalCaseToDashCase} from '../../util/util';
import {Keyboard} from '../../util/keyboard';
-import {NavParams} from './nav-params';
import {MenuController} from '../menu/menu-controller';
+import {NavParams} from './nav-params';
import {NavPortal} from './nav-portal';
import {SwipeBackGesture} from './swipe-back';
import {Transition} from '../../transitions/transition';
@@ -245,6 +245,13 @@ export class NavController extends Ion {
this.viewDidUnload = new EventEmitter();
}
+ /**
+ * @private
+ */
+ getPortal(): NavController {
+ return this._portal;
+ }
+
/**
* @private
*/
diff --git a/src/components/nav/test/nested/index.ts b/src/components/nav/test/nested/index.ts
index 31868369531..663d120ac4a 100644
--- a/src/components/nav/test/nested/index.ts
+++ b/src/components/nav/test/nested/index.ts
@@ -1,6 +1,6 @@
import {Component, ViewChild} from '@angular/core';
-import {ionicBootstrap, NavParams, NavController, ViewController, MenuController} from '../../../../../src';
-import {Config, Nav} from '../../../../../src';
+import {ionicBootstrap, NavController, MenuController} from '../../../../../src';
+import {Config, Nav, App} from '../../../../../src';
@Component({
@@ -9,16 +9,21 @@ import {Config, Nav} from '../../../../../src';
Login
-
+
+
`
})
export class Login {
- constructor(private nav: NavController) {}
+ constructor(private nav: NavController, private app: App) {}
goToAccount() {
this.nav.push(Account);
}
+
+ goBack() {
+ this.app.navPop();
+ }
}
@@ -39,21 +44,22 @@ export class Login {
+
-
+
`
})
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() {
this.accountNav.setRoot(Profile).then(() => {
@@ -68,7 +74,13 @@ export class Account {
}
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 {
+
`
})
export class Dashboard {
- constructor(private nav: NavController) {}
+ constructor(private nav: NavController, private app: App) {}
goToProfile() {
this.nav.push(Profile);
}
+
logOut() {
this.nav.parent.setRoot(Login, null, {
animate: true,
direction: 'back'
});
}
+
+ goBack() {
+ this.app.navPop();
+ }
}
@@ -113,11 +131,12 @@ export class Dashboard {
+
`
})
export class Profile {
- constructor(private nav: NavController) {}
+ constructor(private nav: NavController, private app: App) {}
goToDashboard() {
this.nav.push(Dashboard);
@@ -129,6 +148,10 @@ export class Profile {
direction: 'back'
});
}
+
+ goBack() {
+ this.app.navPop();
+ }
}
diff --git a/src/components/tabs/test/basic/index.ts b/src/components/tabs/test/basic/index.ts
index 2351767a4e3..a9fd393064c 100644
--- a/src/components/tabs/test/basic/index.ts
+++ b/src/components/tabs/test/basic/index.ts
@@ -1,5 +1,5 @@
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
@@ -34,6 +34,9 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../..
+
`
@@ -41,7 +44,7 @@ import {ionicBootstrap, NavController, Alert, Modal, ViewController} from '../..
class MyModal {
items: any[] = [];
- constructor(private viewCtrl: ViewController) {
+ constructor(private viewCtrl: ViewController, private app: App) {
for (var i = 1; i <= 10; i++) {
this.items.push(i);
}
@@ -52,6 +55,10 @@ class MyModal {
// can "dismiss" itself and pass back data
this.viewCtrl.dismiss();
}
+
+ appNavPop() {
+ this.app.navPop();
+ }
}
//
@@ -69,17 +76,31 @@ class MyModal {
Item {{i}} {{i}} {{i}} {{i}}
+
+
+
+
+
+
`
})
export class Tab1 {
items: any[] = [];
- constructor() {
+ constructor(private tabs: Tabs, private app: App) {
for (var i = 1; i <= 250; i++) {
this.items.push(i);
}
}
+
+ selectPrevious() {
+ this.tabs.select(this.tabs.previousTab());
+ }
+
+ appNavPop() {
+ this.app.navPop();
+ }
}
//
@@ -103,13 +124,19 @@ export class Tab1 {
+
+
+
+
+
+
`
})
export class Tab2 {
sessions: any[] = [];
- constructor() {
+ constructor(private tabs: Tabs, private app: App) {
for (var i = 1; i <= 250; i++) {
this.sessions.push({
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 {
+
+
+
+
+
+
`
})
export class Tab3 {
- constructor(private nav: NavController) {}
+ constructor(private nav: NavController, private tabs: Tabs, private app: App) {}
presentAlert() {
let alert = Alert.create({
@@ -154,6 +195,14 @@ export class Tab3 {
let modal = Modal.create(MyModal);
this.nav.present(modal);
}
+
+ selectPrevious() {
+ this.tabs.select(this.tabs.previousTab());
+ }
+
+ appNavPop() {
+ this.app.navPop();
+ }
}
@@ -184,11 +233,11 @@ export class TabsPage {
root2 = Tab2;
root3 = Tab3;
- onChange(ev) {
+ onChange(ev: Tab) {
console.log("Changed tab", ev);
}
- onSelect(ev) {
+ onSelect(ev: Tab) {
console.log("Selected tab", ev);
}
}