refactor(nav): create NavControllerBase and public abstract class

Use NavController as the public API, and NavControllerBase as the
internal API. Refactored all app/nav/tabs unit tests and created
centralized mocking functions.
This commit is contained in:
Adam Bradley
2016-07-15 15:54:56 -05:00
parent 5909fa4ba5
commit 0a7d865975
17 changed files with 3384 additions and 3307 deletions

View File

@ -4,6 +4,7 @@ import { Title } from '@angular/platform-browser';
import { ClickBlock } from '../../util/click-block'; import { ClickBlock } from '../../util/click-block';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { NavController } from '../nav/nav-controller'; import { NavController } from '../nav/nav-controller';
import { isTabs, isNav } from '../nav/nav-controller-base';
import { NavOptions } from '../nav/nav-interfaces'; import { NavOptions } from '../nav/nav-interfaces';
import { NavPortal } from '../nav/nav-portal'; import { NavPortal } from '../nav/nav-portal';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
@ -195,13 +196,7 @@ export class App {
// function used to climb up all parent nav controllers // function used to climb up all parent nav controllers
function navPop(nav: any): Promise<any> { function navPop(nav: any): Promise<any> {
if (nav) { if (nav) {
if (nav.length && nav.length() > 1) { if (isTabs(nav)) {
// 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 // FYI, using "nav instanceof Tabs" throws a Promise runtime error for whatever reason, idk
// this is a Tabs container // this is a Tabs container
// see if there is a valid previous tab to go to // see if there is a valid previous tab to go to
@ -211,6 +206,12 @@ export class App {
nav.select(prevTab); nav.select(prevTab);
return Promise.resolve(); return Promise.resolve();
} }
} else if (isNav(nav) && 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();
} }
// try again using the parent nav (if there is one) // try again using the parent nav (if there is one)
@ -244,10 +245,9 @@ export class App {
console.debug('app, goBack exitApp'); console.debug('app, goBack exitApp');
this._platform.exitApp(); this._platform.exitApp();
} }
} else {
return navPromise;
} }
return navPromise;
} }
return Promise.resolve(); return Promise.resolve();

View File

@ -1,16 +1,16 @@
import {Component} from '@angular/core'; import { Component } from '@angular/core';
import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src';
import { mockNavController, mockTab, mockTabs } from '../../../../src/util/mock-providers';
export function run() { export function run() {
describe('App', () => { describe('App', () => {
describe('navPop', () => { describe('navPop', () => {
it('should select the previous tab', () => { it('should select the previous tab', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -40,8 +40,8 @@ describe('App', () => {
}); });
it('should pop from the active tab, when tabs is nested is the root nav', () => { it('should pop from the active tab, when tabs is nested is the root nav', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -91,9 +91,9 @@ describe('App', () => {
}); });
it('should pop the root nav when nested nav has less than 2 views', () => { it('should pop the root nav when nested nav has less than 2 views', () => {
let rootNav = mockNav(); let rootNav = mockNavController();
let nestedNav = mockNav(); let nestedNav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
rootNav.registerChildNav(nestedNav); rootNav.registerChildNav(nestedNav);
nestedNav.parent = rootNav; nestedNav.parent = rootNav;
@ -120,9 +120,9 @@ describe('App', () => {
}); });
it('should pop a view from the nested nav that has more than 1 view', () => { it('should pop a view from the nested nav that has more than 1 view', () => {
let rootNav = mockNav(); let rootNav = mockNavController();
let nestedNav = mockNav(); let nestedNav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(rootNav); app.setRootNav(rootNav);
rootNav.registerChildNav(nestedNav); rootNav.registerChildNav(nestedNav);
@ -149,8 +149,8 @@ describe('App', () => {
}); });
it('should pop the overlay in the portal of the root nav', () => { it('should pop the overlay in the portal of the root nav', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -173,8 +173,8 @@ describe('App', () => {
}); });
it('should pop the second view in the root nav', () => { it('should pop the second view in the root nav', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -194,8 +194,8 @@ describe('App', () => {
}); });
it('should exit app when only one view in the root nav', () => { it('should exit app when only one view in the root nav', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -217,8 +217,8 @@ describe('App', () => {
}); });
it('should not exit app when only one view in the root nav, but navExitApp config set', () => { it('should not exit app when only one view in the root nav, but navExitApp config set', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -242,8 +242,8 @@ describe('App', () => {
}); });
it('should not go back if app is not enabled', () => { it('should not go back if app is not enabled', () => {
let nav = mockNav(); let nav = mockNavController();
let portal = mockNav(); let portal = mockNavController();
app.setPortal(portal); app.setPortal(portal);
app.setRootNav(nav); app.setRootNav(nav);
@ -276,7 +276,7 @@ describe('App', () => {
describe('getActiveNav', () => { describe('getActiveNav', () => {
it('should get active NavController when using tabs with nested nav', () => { it('should get active NavController when using tabs with nested nav', () => {
let nav = mockNav(); let nav = mockNavController();
app.setRootNav(nav); app.setRootNav(nav);
let tabs = mockTabs(); let tabs = mockTabs();
@ -285,9 +285,9 @@ describe('App', () => {
nav.registerChildNav(tabs); nav.registerChildNav(tabs);
tab2.setSelected(true); tab2.setSelected(true);
let nav2 = mockNav(); let nav2 = mockNavController();
let nav3 = mockNav(); let nav3 = mockNavController();
let nav4 = mockNav(); let nav4 = mockNavController();
tab1.registerChildNav(nav4); tab1.registerChildNav(nav4);
tab2.registerChildNav(nav2); tab2.registerChildNav(nav2);
tab2.registerChildNav(nav3); tab2.registerChildNav(nav3);
@ -296,7 +296,7 @@ describe('App', () => {
}); });
it('should get active NavController when using tabs, nested in a root nav', () => { it('should get active NavController when using tabs, nested in a root nav', () => {
let nav = mockNav(); let nav = mockNavController();
app.setRootNav(nav); app.setRootNav(nav);
let tabs = mockTabs(); let tabs = mockTabs();
@ -331,9 +331,9 @@ describe('App', () => {
}); });
it('should get active NavController when nested 3 deep', () => { it('should get active NavController when nested 3 deep', () => {
let nav1 = mockNav(); let nav1 = mockNavController();
let nav2 = mockNav(); let nav2 = mockNavController();
let nav3 = mockNav(); let nav3 = mockNavController();
app.setRootNav(nav1); app.setRootNav(nav1);
nav1.registerChildNav(nav2); nav1.registerChildNav(nav2);
@ -343,8 +343,8 @@ describe('App', () => {
}); });
it('should get active NavController when nested 2 deep', () => { it('should get active NavController when nested 2 deep', () => {
let nav1 = mockNav(); let nav1 = mockNavController();
let nav2 = mockNav(); let nav2 = mockNavController();
app.setRootNav(nav1); app.setRootNav(nav1);
nav1.registerChildNav(nav2); nav1.registerChildNav(nav2);
@ -352,13 +352,13 @@ describe('App', () => {
}); });
it('should get active NavController when only one nav controller', () => { it('should get active NavController when only one nav controller', () => {
let nav = mockNav(); let nav = mockNavController();
app.setRootNav(nav); app.setRootNav(nav);
expect(app.getActiveNav()).toBe(nav); expect(app.getActiveNav()).toBe(nav);
}); });
it('should set/get the root nav controller', () => { it('should set/get the root nav controller', () => {
let nav = mockNav(); let nav = mockNavController();
app.setRootNav(nav); app.setRootNav(nav);
expect(app.getRootNav()).toBe(nav); expect(app.getRootNav()).toBe(nav);
}); });
@ -443,40 +443,13 @@ describe('App', () => {
var app: App; var app: App;
var config: Config; var config: Config;
var platform: Platform; var platform: Platform;
var _cd: any;
function mockNav(): Nav {
return new Nav(null, null, null, config, null, null, null, null, null, null);
}
function mockTabs(): Tabs {
return new Tabs(null, null, null, config, null, null, null);
}
function mockTab(parentTabs: Tabs): Tab {
var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd, null);
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();
app = new App(config, platform); app = new App(config, platform);
_cd = {
reattach: function(){},
detach: function(){}
};
}); });
}); });
} }

View File

@ -10,6 +10,7 @@ import { isTrueProperty } from '../../util/util';
import { Item } from '../item/item'; import { Item } from '../item/item';
import { NativeInput, NextInput } from './native-input'; import { NativeInput, NextInput } from './native-input';
import { NavController } from '../nav/nav-controller'; import { NavController } from '../nav/nav-controller';
import { NavControllerBase } from '../nav/nav-controller-base';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
@ -27,6 +28,7 @@ export class InputBase {
protected _autoFocusAssist: string; protected _autoFocusAssist: string;
protected _autoComplete: string; protected _autoComplete: string;
protected _autoCorrect: string; protected _autoCorrect: string;
protected _nav: NavControllerBase;
inputControl: NgControl; inputControl: NgControl;
@ -44,9 +46,10 @@ export class InputBase {
protected _platform: Platform, protected _platform: Platform,
protected _elementRef: ElementRef, protected _elementRef: ElementRef,
protected _scrollView: Content, protected _scrollView: Content,
protected _nav: NavController, nav: NavController,
ngControl: NgControl ngControl: NgControl
) { ) {
this._nav = <NavControllerBase>nav;
this._useAssist = config.getBoolean('scrollAssist', false); this._useAssist = config.getBoolean('scrollAssist', false);
this._usePadding = config.getBoolean('scrollPadding', this._useAssist); this._usePadding = config.getBoolean('scrollPadding', this._useAssist);
this._keyboardHeight = config.getNumber('keyboardHeight'); this._keyboardHeight = config.getNumber('keyboardHeight');

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,3 +13,6 @@ export interface NavOptions {
climbNav?: boolean; climbNav?: boolean;
ev?: any; ev?: any;
} }
export const DIRECTION_BACK = 'back';
export const DIRECTION_FORWARD = 'forward';

View File

@ -1,11 +1,12 @@
import { Directive, Optional } from '@angular/core'; import { Directive, HostListener, Input, Optional } from '@angular/core';
import { NavController } from './nav-controller'; import { NavController } from './nav-controller';
import { noop } from '../../util/util';
/** /**
* @name NavPop * @name NavPop
* @description * @description
* Directive for declaratively pop the current page off from the navigation stack. * Directive to declaratively pop the current page off from the
* navigation stack.
* *
* @usage * @usage
* ```html * ```html
@ -22,11 +23,7 @@ import { NavController } from './nav-controller';
* @see {@link ../NavPush NavPush API Docs} * @see {@link ../NavPush NavPush API Docs}
*/ */
@Directive({ @Directive({
selector: '[nav-pop]', selector: '[navPop]'
host: {
'(click)': 'onClick()',
'role': 'link'
}
}) })
export class NavPop { export class NavPop {
@ -36,10 +33,15 @@ export class NavPop {
} }
} }
/** @HostListener('click')
* @private onClick(): boolean {
*/ // If no target, or if target is _self, prevent default browser behavior
onClick() { if (this._nav) {
this._nav && this._nav.pop(); this._nav.pop(null, noop);
return false;
}
return true;
} }
} }

View File

@ -4,7 +4,7 @@ import { App } from '../app/app';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { GestureController } from '../../gestures/gesture-controller'; import { GestureController } from '../../gestures/gesture-controller';
import { Keyboard } from '../../util/keyboard'; import { Keyboard } from '../../util/keyboard';
import { NavController } from '../nav/nav-controller'; import { NavControllerBase } from '../nav/nav-controller-base';
/** /**
* @private * @private
@ -12,7 +12,7 @@ import { NavController } from '../nav/nav-controller';
@Directive({ @Directive({
selector: '[nav-portal]' selector: '[nav-portal]'
}) })
export class NavPortal extends NavController { export class NavPortal extends NavControllerBase {
constructor( constructor(
@Inject(forwardRef(() => App)) app: App, @Inject(forwardRef(() => App)) app: App,
config: Config, config: Config,
@ -25,7 +25,7 @@ export class NavPortal extends NavController {
viewPort: ViewContainerRef viewPort: ViewContainerRef
) { ) {
super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl); super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
this.isPortal = true; this._isPortal = true;
this.setViewport(viewPort); this.setViewport(viewPort);
app.setPortal(this); app.setPortal(this);

View File

@ -1,28 +1,35 @@
import { Directive, Input, Optional } from '@angular/core'; import { Directive, HostListener, Input, Optional } from '@angular/core';
import { NavController } from './nav-controller'; import { NavController } from './nav-controller';
import { noop } from '../../util/util';
/** /**
* @name NavPush * @name NavPush
* @description * @description
* Directive for declaratively linking to a new page instead of using * Directive to declaratively push a new page to the current nav
* {@link ../NavController/#push NavController.push}. Similar to ui-router's `ui-sref`. * stack.
* *
* @usage * @usage
* ```html * ```html
* <button [navPush]="pushPage"></button> * <button [navPush]="pushPage"></button>
* ``` * ```
* To specify parameters you can use array syntax or the `nav-params` property: *
* To specify parameters you can use array syntax or the `navParams`
* property:
*
* ```html * ```html
* <button [navPush]="pushPage" [navParams]="params"></button> * <button [navPush]="pushPage" [navParams]="params">Go</button>
* ``` * ```
* Where `pushPage` and `params` are specified in your component, and `pushPage` *
* contains a reference to a [@Page component](../../../config/Page/): * Where `pushPage` and `params` are specified in your component,
* and `pushPage` contains a reference to a
* [@Page component](../../../config/Page/):
* *
* ```ts * ```ts
* import {LoginPage} from 'login'; * import { LoginPage } from './login';
*
* @Component({ * @Component({
* template: `<button [navPush]="pushPage" [navParams]="params"></button>` * template: `<button [navPush]="pushPage" [navParams]="params">Go</button>`
* }) * })
* class MyPage { * class MyPage {
* constructor(){ * constructor(){
@ -32,61 +39,42 @@ import { NavController } from './nav-controller';
* } * }
* ``` * ```
* *
* ### Alternate syntax
* You can also use syntax similar to Angular2's router, passing an array to
* NavPush:
* ```html
* <button [navPush]="[pushPage, params]"></button>
* ```
* @demo /docs/v2/demos/navigation/ * @demo /docs/v2/demos/navigation/
* @see {@link /docs/v2/components#navigation Navigation Component Docs} * @see {@link /docs/v2/components#navigation Navigation Component Docs}
* @see {@link ../NavPop NavPop API Docs} * @see {@link ../NavPop NavPop API Docs}
*
*/ */
@Directive({ @Directive({
selector: '[navPush]', selector: '[navPush]'
host: {
'(click)': 'onClick()',
'role': 'link'
}
}) })
export class NavPush { export class NavPush {
/** /**
* @input {Page} the page you want to push * @input {Page} The Page to push onto the Nav.
*/
@Input() navPush: any;
/**
* @input {any} Any parameters you want to pass along
*/
@Input() navParams: any;
constructor(
@Optional() private _nav: NavController
) {
if (!_nav) {
console.error('nav-push must be within a NavController');
}
}
/**
* @private
*/ */
onClick() { @Input() navPush: any[]|string;
let destination: any, params: any;
if (this.navPush instanceof Array) { /**
if (this.navPush.length > 2) { * @input {any} Parameters to pass to the page.
throw 'Too many [navPush] arguments, expects [View, { params }]'; */
} @Input() navParams: {[k: string]: any};
destination = this.navPush[0];
params = this.navPush[1] || this.navParams;
} else {
destination = this.navPush; constructor(@Optional() private _nav: NavController) {
params = this.navParams; if (!_nav) {
console.error('navPush must be within a NavController');
}
}
@HostListener('click')
onClick(): boolean {
// If no target, or if target is _self, prevent default browser behavior
if (this._nav) {
this._nav.push(this.navPush, this.navParams, noop);
return false;
} }
this._nav && this._nav.push(destination, params); return true;
} }
} }

View File

@ -5,7 +5,7 @@ import { Config } from '../../config/config';
import { Keyboard } from '../../util/keyboard'; import { Keyboard } from '../../util/keyboard';
import { GestureController } from '../../gestures/gesture-controller'; import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util'; import { isTrueProperty } from '../../util/util';
import { NavController } from './nav-controller'; import { NavControllerBase } from './nav-controller-base';
import { ViewController } from './view-controller'; import { ViewController } from './view-controller';
/** /**
@ -114,13 +114,13 @@ import { ViewController } from './view-controller';
`, `,
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Nav extends NavController implements AfterViewInit { export class Nav extends NavControllerBase implements AfterViewInit {
private _root: any; private _root: any;
private _hasInit: boolean = false; private _hasInit: boolean = false;
constructor( constructor(
@Optional() viewCtrl: ViewController, @Optional() viewCtrl: ViewController,
@Optional() parent: NavController, @Optional() parent: NavControllerBase,
app: App, app: App,
config: Config, config: Config,
keyboard: Keyboard, keyboard: Keyboard,
@ -164,9 +164,6 @@ export class Nav extends NavController implements AfterViewInit {
this._hasInit = true; this._hasInit = true;
if (this._root) { if (this._root) {
if (typeof this._root !== 'function') {
throw 'The [root] property in <ion-nav> must be given a reference to a component class from within the constructor.';
}
this.push(this._root); this.push(this._root);
} }
} }

View File

@ -1,7 +1,7 @@
import { assign } from '../../util/util'; import { assign } from '../../util/util';
import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller'; import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller';
import { MenuController } from '../menu/menu-controller'; import { MenuController } from '../menu/menu-controller';
import { NavController } from './nav-controller'; import { NavControllerBase } from './nav-controller-base';
import { SlideData } from '../../gestures/slide-gesture'; import { SlideData } from '../../gestures/slide-gesture';
import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture'; import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture';
@ -11,7 +11,7 @@ export class SwipeBackGesture extends SlideEdgeGesture {
constructor( constructor(
element: HTMLElement, element: HTMLElement,
options: any, options: any,
private _nav: NavController, private _nav: NavControllerBase,
gestureCtlr: GestureController gestureCtlr: GestureController
) { ) {
super(element, assign({ super(element, assign({

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core'; import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core';
import { App } from '../app/app'; import { App } from '../app/app';
import { Config } from '../../config/config'; import { Config } from '../../config/config';
import { GestureController } from '../../gestures/gesture-controller'; import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty} from '../../util/util'; import { isTrueProperty} from '../../util/util';
import { Keyboard} from '../../util/keyboard'; import { Keyboard} from '../../util/keyboard';
import { NavController } from '../nav/nav-controller'; import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions} from '../nav/nav-interfaces'; import { NavOptions} from '../nav/nav-interfaces';
import { TabButton} from './tab-button'; import { TabButton} from './tab-button';
import { Tabs} from './tabs'; import { Tabs} from './tabs';
@ -128,7 +128,7 @@ import { ViewController} from '../nav/view-controller';
template: '<div #viewport></div><div class="nav-decor"></div>', template: '<div #viewport></div><div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None, encapsulation: ViewEncapsulation.None,
}) })
export class Tab extends NavController { export class Tab extends NavControllerBase {
private _isInitial: boolean; private _isInitial: boolean;
private _isEnabled: boolean = true; private _isEnabled: boolean = true;
private _isShown: boolean = true; private _isShown: boolean = true;
@ -236,10 +236,6 @@ export class Tab extends NavController {
parent.add(this); parent.add(this);
if (parent.rootNav) {
this._sbEnabled = parent.rootNav.isSwipeBackEnabled();
}
this._tabId = 'tabpanel-' + this.id; this._tabId = 'tabpanel-' + this.id;
this._btnId = 'tab-' + this.id; this._btnId = 'tab-' + this.id;
} }
@ -264,7 +260,7 @@ export class Tab extends NavController {
*/ */
load(opts: NavOptions, done?: Function) { load(opts: NavOptions, done?: Function) {
if (!this._loaded && this.root) { if (!this._loaded && this.root) {
this.push(this.root, this.rootParams, opts).then(() => { this.push(this.root, this.rootParams, opts, () => {
done(true); done(true);
}); });
this._loaded = true; this._loaded = true;

View File

@ -7,9 +7,11 @@ import { Config } from '../../config/config';
import { Content } from '../content/content'; import { Content } from '../content/content';
import { Icon } from '../icon/icon'; import { Icon } from '../icon/icon';
import { Ion } from '../ion'; import { Ion } from '../ion';
import { isBlank, isTrueProperty } from '../../util/util'; import { isBlank, isPresent, isTrueProperty } from '../../util/util';
import { nativeRaf } from '../../util/dom'; import { nativeRaf } from '../../util/dom';
import { NavController, DIRECTION_FORWARD } from '../nav/nav-controller'; import { NavController } from '../nav/nav-controller';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions, DIRECTION_FORWARD } from '../nav/nav-interfaces';
import { Platform } from '../../platform/platform'; import { Platform } from '../../platform/platform';
import { Tab } from './tab'; import { Tab } from './tab';
import { TabButton } from './tab-button'; import { TabButton } from './tab-button';
@ -164,7 +166,7 @@ export class Tabs extends Ion {
/** /**
* @private * @private
*/ */
id: number; id: string;
/** /**
* @private * @private
@ -219,7 +221,7 @@ export class Tabs extends Ion {
/** /**
* @private * @private
*/ */
parent: NavController; parent: NavControllerBase;
constructor( constructor(
@Optional() parent: NavController, @Optional() parent: NavController,
@ -232,8 +234,8 @@ export class Tabs extends Ion {
) { ) {
super(_elementRef); super(_elementRef);
this.parent = parent; this.parent = <NavControllerBase>parent;
this.id = ++tabIds; this.id = 't' + (++tabIds);
this._sbPadding = _config.getBoolean('statusbarPadding'); this._sbPadding = _config.getBoolean('statusbarPadding');
this._useHighlight = _config.getBoolean('tabsHighlight'); this._useHighlight = _config.getBoolean('tabsHighlight');
@ -248,9 +250,9 @@ export class Tabs extends Ion {
this._useHighlight = _config.getBoolean('tabbarHighlight'); this._useHighlight = _config.getBoolean('tabbarHighlight');
} }
if (parent) { if (this.parent) {
// this Tabs has a parent Nav // this Tabs has a parent Nav
parent.registerChildNav(this); this.parent.registerChildNav(this);
} else if (this._app) { } else if (this._app) {
// this is the root navcontroller for the entire app // this is the root navcontroller for the entire app
@ -320,41 +322,32 @@ export class Tabs extends Ion {
* @private * @private
*/ */
initTabs() { initTabs() {
// first check if preloadTab is set as an input @Input, then check the config // get the selected index from the input
let preloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs)); // otherwise default it to use the first index
let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10));
// get the selected index // get the selectedIndex and ensure it isn't hidden or disabled
let selectedIndex = this.selectedIndex ? parseInt(this.selectedIndex, 10) : 0; let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show);
if (!selectedTab) {
// ensure the selectedIndex isn't a hidden or disabled tab // wasn't able to select the tab they wanted
// also find the first available index incase we need it later // try to find the first tab that's available
let availableIndex = -1; selectedTab = this._tabs.find(t => t.enabled && t.show);
this._tabs.forEach((tab, index) => {
if (tab.enabled && tab.show && availableIndex < 0) {
// we know this tab index is safe to show
availableIndex = index;
}
if (index === selectedIndex && (!tab.enabled || !tab.show)) {
// the selectedIndex is not safe to show
selectedIndex = -1;
}
});
if (selectedIndex < 0) {
// the selected index wasn't safe to show
// instead use an available index found to be safe to show
selectedIndex = availableIndex;
} }
this._tabs.forEach((tab, index) => { if (selectedTab) {
if (index === selectedIndex) { // we found a tab to select
this.select(tab); this.select(selectedTab);
}
} else if (preloadTabs) { // check if preloadTab is set as an input @Input
tab.preload(1000 * index); // otherwise check the preloadTabs config
} let shouldPreloadTabs = (isBlank(this.preloadTabs) ? this._config.getBoolean('preloadTabs') : isTrueProperty(this.preloadTabs));
}); if (shouldPreloadTabs) {
// preload all the tabs which isn't the selected tab
this._tabs.filter((t) => t !== selectedTab).forEach((tab, index) => {
tab.preload(this._config.getNumber('tabsPreloadDelay', 1000) * index);
});
}
} }
/** /**
@ -379,31 +372,33 @@ export class Tabs extends Ion {
/** /**
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select. * @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
*/ */
select(tabOrIndex: number | Tab) { select(tabOrIndex: number | Tab, opts: NavOptions = {}, done?: Function): Promise<any> {
let promise: Promise<any>;
if (!done) {
promise = new Promise(res => { done = res; });
}
let selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex); let selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
if (isBlank(selectedTab)) { if (isBlank(selectedTab)) {
return; return Promise.resolve();
} }
let deselectedTab = this.getSelected(); let deselectedTab = this.getSelected();
if (selectedTab === deselectedTab) { if (selectedTab === deselectedTab) {
// no change // no change
return this._touchActive(selectedTab); this._touchActive(selectedTab);
return Promise.resolve();
} }
console.debug(`Tabs, select: ${selectedTab.id}`); console.debug(`Tabs, select: ${selectedTab.id}`);
let opts = {
animate: false
};
let deselectedPage: ViewController; let deselectedPage: ViewController;
if (deselectedTab) { if (deselectedTab) {
deselectedPage = deselectedTab.getActive(); deselectedPage = deselectedTab.getActive();
deselectedPage && deselectedPage.fireWillLeave(); deselectedPage && deselectedPage.fireWillLeave();
} }
opts.animate = false;
let selectedPage = selectedTab.getActive(); let selectedPage = selectedTab.getActive();
selectedPage && selectedPage.fireWillEnter(); selectedPage && selectedPage.fireWillEnter();
@ -451,7 +446,11 @@ export class Tabs extends Ion {
}); });
} }
} }
done();
}); });
return promise;
} }
/** /**
@ -513,6 +512,13 @@ export class Tabs extends Ion {
return this._tabs.indexOf(tab); return this._tabs.indexOf(tab);
} }
/**
* @private
*/
length(): number {
return this._tabs.length;
}
/** /**
* @private * @private
* "Touch" the active tab, going back to the root view of the tab * "Touch" the active tab, going back to the root view of the tab
@ -548,20 +554,6 @@ export class Tabs extends Ion {
return Promise.resolve(); return Promise.resolve();
} }
/**
* @private
* Returns the root NavController. Returns `null` if Tabs is not
* within a NavController.
* @returns {NavController}
*/
get rootNav(): NavController {
let nav = this.parent;
while (nav && nav.parent) {
nav = nav.parent;
}
return nav;
}
/** /**
* @private * @private
* DOM WRITE * DOM WRITE

View File

@ -1,10 +1,89 @@
import {Component} from '@angular/core'; import { Component } from '@angular/core';
import {App, Nav, Tabs, Tab, NavOptions, Config, ViewController, Platform} from '../../../../src'; import { App, Config, Nav, NavOptions, Platform, Tab, Tabs, ViewController } from '../../../../src';
import { mockTab, mockTabs } from '../../../../src/util/mock-providers';
export function run() { export function run() {
describe('Tabs', () => { describe('Tabs', () => {
describe('initTabs', () => {
it('should preload all tabs', () => {
var tabs = mockTabs();
var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs);
tab0.root = SomePage;
tab1.root = SomePage;
tab0.preload = () => {};
tab1.preload = () => {};
spyOn(tab0, 'preload');
spyOn(tab1, 'preload');
tabs.preloadTabs = true;
tabs.initTabs();
expect(tab0.isSelected).toEqual(true);
expect(tab1.isSelected).toEqual(false);
expect(tab0.preload).not.toHaveBeenCalled();
expect(tab1.preload).toHaveBeenCalled();
});
it('should not select a hidden or disabled tab', () => {
var tabs = mockTabs();
var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs);
tab0.root = SomePage;
tab1.root = SomePage;
tab1.enabled = false;
tab1.show = false;
tabs.selectedIndex = '1';
tabs.initTabs();
expect(tab0.isSelected).toEqual(true);
expect(tab1.isSelected).toEqual(false);
});
it('should select the second tab from selectedIndex input', () => {
var tabs = mockTabs();
var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs);
tab0.root = SomePage;
tab1.root = SomePage;
tabs.selectedIndex = '1';
tabs.initTabs();
expect(tab0.isSelected).toEqual(false);
expect(tab1.isSelected).toEqual(true);
});
it('should select the first tab by default', () => {
var tabs = mockTabs();
var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs);
tab0.root = SomePage;
tab1.root = SomePage;
spyOn(tab0, 'preload');
spyOn(tab1, 'preload');
tabs.initTabs();
expect(tab0.isSelected).toEqual(true);
expect(tab1.isSelected).toEqual(false);
expect(tab0.preload).not.toHaveBeenCalled();
expect(tab1.preload).not.toHaveBeenCalled();
});
});
describe('previousTab', () => { describe('previousTab', () => {
it('should find the previous tab when there has been 3 selections', () => { it('should find the previous tab when there has been 3 selections', () => {
@ -12,9 +91,6 @@ describe('Tabs', () => {
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
var tab2 = mockTab(tabs); var tab2 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tabs.add(tab2);
tab0.root = SomePage; tab0.root = SomePage;
tab1.root = SomePage; tab1.root = SomePage;
tab2.root = SomePage; tab2.root = SomePage;
@ -36,8 +112,6 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tab0.root = SomePage; tab0.root = SomePage;
tab1.root = SomePage; tab1.root = SomePage;
@ -56,8 +130,6 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tab0.root = SomePage; tab0.root = SomePage;
tab1.root = SomePage; tab1.root = SomePage;
@ -87,8 +159,6 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tab0.root = SomePage; tab0.root = SomePage;
tab1.root = SomePage; tab1.root = SomePage;
@ -103,12 +173,11 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tab0.root = SomePage; tab0.root = SomePage;
tab1.root = SomePage; tab1.root = SomePage;
expect(tabs.length()).toEqual(2);
expect(tab0.isSelected).toBeUndefined(); expect(tab0.isSelected).toBeUndefined();
expect(tab1.isSelected).toBeUndefined(); expect(tab1.isSelected).toBeUndefined();
@ -118,16 +187,6 @@ describe('Tabs', () => {
expect(tab1.isSelected).toEqual(false); expect(tab1.isSelected).toEqual(false);
}); });
it('should not select an invalid tab index', () => {
var tabs = mockTabs();
var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
expect(tabs.select(22)).toBeUndefined();
});
}); });
describe('getByIndex', () => { describe('getByIndex', () => {
@ -137,8 +196,6 @@ describe('Tabs', () => {
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
tab0.setRoot(<any>{}); tab0.setRoot(<any>{});
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
expect(tabs.getIndex(tab0)).toEqual(0); expect(tabs.getIndex(tab0)).toEqual(0);
expect(tabs.getIndex(tab1)).toEqual(1); expect(tabs.getIndex(tab1)).toEqual(1);
@ -152,8 +209,6 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
tab1.setSelected(true); tab1.setSelected(true);
@ -164,48 +219,15 @@ describe('Tabs', () => {
var tabs = mockTabs(); var tabs = mockTabs();
var tab0 = mockTab(tabs); var tab0 = mockTab(tabs);
var tab1 = mockTab(tabs); var tab1 = mockTab(tabs);
tabs.add(tab0);
tabs.add(tab1);
expect(tabs.getSelected()).toEqual(null); expect(tabs.getSelected()).toEqual(null);
}); });
}); });
var app: App;
var config: Config;
var platform: Platform;
var _cd: any;
function mockNav(): Nav {
return new Nav(null, null, null, config, null, null, null, null, null);
}
function mockTabs(): Tabs {
return new Tabs(null, null, null, config, null, null, null);
}
function mockTab(parentTabs: Tabs): Tab {
var tab = new Tab(parentTabs, app, config, null, null, null, null, null, _cd);
tab.load = function(opts: any, cb: Function) {
cb();
};
return tab;
}
@Component({}) @Component({})
class SomePage {} class SomePage {}
beforeEach(() => {
config = new Config();
platform = new Platform();
app = new App(config, platform);
_cd = {
reattach: function(){},
detach: function(){}
};
});
}); });

171
src/util/mock-providers.ts Normal file
View File

@ -0,0 +1,171 @@
import { ChangeDetectorRef, ElementRef, NgZone, Renderer } from '@angular/core';
import { Location } from '@angular/common';
import { App, Config, Form, GestureController, Keyboard, MenuController, NavOptions, Platform, Tab, Tabs, Transition, ViewController } from '../../src';
import { NavControllerBase } from '../../src/components/nav/nav-controller-base';
export const mockConfig = function(config?: any) {
return new Config(config);
};
export const mockPlatform = function(platforms?: string[]) {
return new Platform(platforms);
};
export const mockApp = function(config?: Config, platform?: Platform) {
config = config || mockConfig();
platform = platform || mockPlatform();
return new App(config, platform);
};
export const mockZone = function(): NgZone {
let zone: any = {
run: function(cb: any) {
cb();
},
runOutsideAngular: function(cb: any) {
cb();
}
};
return zone;
};
export const mockChangeDetectorRef = function(): ChangeDetectorRef {
let cd: any = {
reattach: () => {},
detach: () => {}
};
return cd;
};
export const mockElementRef = function(): ElementRef {
return {
nativeElement: document.createElement('div')
};
};
export const mockRenderer = function(): Renderer {
let renderer: any = {
setElementAttribute: () => {},
setElementClass: () => {},
setElementStyle: () => {}
};
return renderer;
};
export const mockLocation = function(): Location {
let location: any = {
path: () => { return ''; },
subscribe: () => {},
go: () => {},
back: () => {}
};
return location;
};
export const mockTransition = function(playCallback: Function, duration: number) {
return function _createTrans(enteringView: ViewController, leavingView: ViewController, transitionOpts: any): Transition {
let transition: any = {
play: () => {
playCallback();
},
getDuration: () => { return duration; },
onFinish: () => {}
};
return transition;
};
};
export const mockNavController = function(): NavControllerBase {
let platform = mockPlatform();
let config = mockConfig();
config.setPlatform(platform);
let app = mockApp(config, platform);
let form = new Form();
let zone = mockZone();
let keyboard = new Keyboard(config, form, zone);
let elementRef = mockElementRef();
let renderer = mockRenderer();
let compiler: any = null;
let gestureCtrl = new GestureController(app);
let location = mockLocation();
return new NavControllerBase(
null,
app,
config,
keyboard,
elementRef,
zone,
renderer,
compiler,
gestureCtrl
);
};
export const mockTab = function(parentTabs: Tabs): Tab {
let platform = mockPlatform();
let config = mockConfig();
config.setPlatform(platform);
let app = (<any>parentTabs)._app || mockApp(config, platform);
let form = new Form();
let zone = mockZone();
let keyboard = new Keyboard(config, form, zone);
let elementRef = mockElementRef();
let renderer = mockRenderer();
let changeDetectorRef = mockChangeDetectorRef();
let compiler: any = null;
let gestureCtrl = new GestureController(app);
let location = mockLocation();
let tab = new Tab(
parentTabs,
app,
config,
keyboard,
elementRef,
zone,
renderer,
compiler,
changeDetectorRef,
gestureCtrl
);
tab.load = (opts: any, cb: Function) => {
cb();
};
return tab;
};
export const mockTabs = function(app?: App): Tabs {
let config = mockConfig();
let platform = mockPlatform();
app = app || mockApp(config, platform);
let elementRef = mockElementRef();
let renderer = mockRenderer();
return new Tabs(null, null, app, config, elementRef, platform, renderer);
};

View File

@ -1,4 +1,6 @@
export function noop() {}
/** /**
* Given a min and max, restrict the given number * Given a min and max, restrict the given number
* to the range. * to the range.