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 {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<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
*/

View File

@ -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();

View File

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

View File

@ -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
*/

View File

@ -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';
<ion-title>Login</ion-title>
</ion-navbar>
<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>
`
})
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 {
<button ion-item detail-none (click)="logOut()">
Logout
</button>
<button ion-item detail-none (click)="goBack()">
App Go Back
</button>
</ion-list>
</ion-content>
</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 {
@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 {
<ion-content padding>
<p><button (click)="goToProfile()">Profile</button></p>
<p><button (click)="logOut()">Logout</button></p>
<p><button (click)="goBack()">App goBack()</button></p>
</ion-content>
`
})
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 {
<ion-content padding>
<p><button (click)="goToDashboard()">Dashboard</button></p>
<p><button (click)="logOut()">Logout</button></p>
<p><button (click)="goBack()">App goBack()</button></p>
</ion-content>
`
})
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();
}
}

View File

@ -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 '../..
<button ion-item danger detail-none>
Reset All Filters
</button>
<button ion-item danger detail-none (click)="appNavPop()">
App Nav Pop
</button>
</ion-list>
</ion-content>
`
@ -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 {
</ion-list-header>
<ion-item *ngFor="let i of items">Item {{i}} {{i}} {{i}} {{i}}</ion-item>
</ion-list>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content>
`
})
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 {
</ion-item-options>
</ion-item-sliding>
</ion-list>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content>
`
})
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 {
<button (click)="presentAlert()">Present Alert</button>
<button (click)="presentModal()">Present Modal</button>
</p>
<p>
<button (click)="selectPrevious()">Select Previous Tab</button>
</p>
<p>
<button (click)="appNavPop()">App Nav Pop</button>
</p>
</ion-content>
`
})
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);
}
}