refactor(nav): queue based transitions

This commit is contained in:
Adam Bradley
2016-09-13 17:13:43 -05:00
parent bc7d328bc0
commit ed221eff1d
18 changed files with 2970 additions and 2083 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,519 +0,0 @@
import { EventEmitter } from '@angular/core';
import { Config } from '../../config/config';
import { GestureController } from '../../gestures/gesture-controller';
import { Ion } from '../ion';
import { isBlank, pascalCaseToDashCase } from '../../util/util';
import { Keyboard } from '../../util/keyboard';
import { NavOptions } from './nav-interfaces';
import { ViewController } from './view-controller';
/**
* @name NavController
* @description
*
* NavController is the base class for navigation controller components like
* [`Nav`](../Nav/) and [`Tab`](../../tabs/Tab/). You use navigation controllers
* to navigate to [pages](#view-creation) in your app. At a basic level, a
* navigation controller is an array of pages representing a particular history
* (of a Tab for example). This array can be manipulated to navigate throughout
* an app by pushing and popping pages or inserting and removing them at
* arbitrary locations in history.
*
* The current page is the last one in the array, or the top of the stack if we
* think of it that way. [Pushing](#push) a new page onto the top of the
* navigation stack causes the new page to be animated in, while [popping](#pop)
* the current page will navigate to the previous page in the stack.
*
* Unless you are using a directive like [NavPush](../NavPush/), or need a
* specific NavController, most times you will inject and use a reference to the
* nearest NavController to manipulate the navigation stack.
*
* ## Basic usage
* The simplest way to navigate through an app is to create and initialize a new
* nav controller using the `<ion-nav>` component. `ion-nav` extends the `NavController`
* class.
*
* ```typescript
* import { Component } from `@angular/core`;
* import { ionicBootstrap } from 'ionic-angular';
* import { StartPage } from './start-page';
*
* @Component(
* template: `<ion-nav [root]="rootPage"></ion-nav>`
* })
* class MyApp {
* // set the rootPage to the first page we want displayed
* private rootPage: any = StartPage;
*
* constructor(){
* }
* }
*
* ionicBootstrap(MyApp);
* ```
*
* ### Injecting NavController
* Injecting NavController will always get you an instance of the nearest
* NavController, regardless of whether it is a Tab or a Nav.
*
* Behind the scenes, when Ionic instantiates a new NavController, it creates an
* injector with NavController bound to that instance (usually either a Nav or
* Tab) and adds the injector to its own providers. For more information on
* providers and dependency injection, see [Providers and DI]().
*
* Instead, you can inject NavController and know that it is the correct
* navigation controller for most situations (for more advanced situations, see
* [Menu](../../menu/Menu/) and [Tab](../../tab/Tab/)).
*
* ```ts
* import { NavController } from 'ionic-angular';
*
* class MyComponent {
* constructor(public navCtrl: NavController) {
*
* }
* }
* ```
*
* ### Navigating from the Root component
* What if you want to control navigation from your root app component?
* You can't inject `NavController` because any components that are navigation
* controllers are _children_ of the root component so they aren't available
* to be injected.
*
* By adding a reference variable to the `ion-nav`, you can use `@ViewChild` to
* get an instance of the `Nav` component, which is a navigation controller
* (it extends `NavController`):
*
* ```typescript
*
* import { App, ViewChild } from '@angular/core';
* import { NavController } from 'ionic-angular';
*
* @App({
* template: '<ion-nav #myNav [root]="rootPage"></ion-nav>'
* })
* export class MyApp {
* @ViewChild('myNav') nav : NavController
* private rootPage = TabsPage;
*
* // Wait for the components in MyApp's template to be initialized
* // In this case, we are waiting for the Nav with id="my-nav"
* ngAfterViewInit() {
* // Let's navigate from TabsPage to Page1
* this.nav.push(Page1);
* }
* }
* ```
*
* ## View creation
* Views are created when they are added to the navigation stack. For methods
* like [push()](#push), the NavController takes any component class that is
* decorated with `@Component` as its first argument. The NavController then
* compiles that component, adds it to the app and animates it into view.
*
* By default, pages are cached and left in the DOM if they are navigated away
* from but still in the navigation stack (the exiting page on a `push()` for
* example). They are destroyed when removed from the navigation stack (on
* [pop()](#pop) or [setRoot()](#setRoot)).
*
* ## Pushing a View
* To push a new view on to the navigation stack, use the `push` method.
* If the page has an [`<ion-navbar>`](../../navbar/Navbar/),
* a back button will automatically be added to the pushed view.
*
* Data can also be passed to a view by passing an object to the `push` method.
* The pushed view can then receive the data by accessing it via the `NavParams`
* class.
*
* ```typescript
* import { Component } from '@angular/core';
* import { NavController } from 'ionic-angular';
* import { OtherPage } from './other-page';
* @Component({
* template: `
* <ion-header>
* <ion-navbar>
* <ion-title>Login</ion-title>
* </ion-navbar>
* </ion-header>
*
* <ion-content>
* <button ion-button (click)="pushPage()">
* Go to OtherPage
* </button>
* </ion-content>
* `
* })
* export class StartPage {
* constructor(public navCtrl: NavController) {
* }
*
* pushPage(){
* // push another page on to the navigation stack
* // causing the nav controller to transition to the new page
* // optional data can also be passed to the pushed page.
* this.navCtrl.push(OtherPage, {
* id: "123",
* name: "Carl"
* });
* }
* }
*
* import { NavParams } from 'ionic-angular';
*
* @Component({
* template: `
* <ion-header>
* <ion-navbar>
* <ion-title>Other Page</ion-title>
* </ion-navbar>
* </ion-header>
* <ion-content>I'm the other page!</ion-content>`
* })
* class OtherPage {
* constructor(private navParams: NavParams) {
* let id = navParams.get('id');
* let name = navParams.get('name');
* }
* }
* ```
*
* ## Removing a view
* To remove a view from the stack, use the `pop` method.
* Popping a view will transition to the previous view.
*
* ```ts
* import { Component } from '@angular/core';
* import { NavController } from 'ionic-angular';
*
* @Component({
* template: `
* <ion-header>
* <ion-navbar>
* <ion-title>Other Page</ion-title>
* </ion-navbar>
* </ion-header>
* <ion-content>I'm the other page!</ion-content>`
* })
* class OtherPage {
* constructor(private navController: NavController ){
* }
*
* popView(){
* this.navController.pop();
* }
* }
* ```
*
* ## Lifecycle events
* Lifecycle events are fired during various stages of navigation. They can be
* defined in any component type which is pushed/popped from a `NavController`.
*
* ```ts
* import { Component } from '@angular/core';
*
* @Component({
* template: 'Hello World'
* })
* class HelloWorld {
* ionViewLoaded() {
* console.log("I'm alive!");
* }
* ionViewWillLeave() {
* console.log("Looks like I'm about to leave :(");
* }
* }
* ```
*
* | Page Event | Description |
* |---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
* | `ionViewLoaded` | Runs when the page has loaded. This event only happens once per page being created and added to the DOM. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The `ionViewLoaded` event is good place to put your setup code for the page. |
* | `ionViewWillEnter` | Runs when the page is about to enter and become the active page. |
* | `ionViewDidEnter` | Runs when the page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page. |
* | `ionViewWillLeave` | Runs when the page is about to leave and no longer be the active page. |
* | `ionViewDidLeave` | Runs when the page has finished leaving and is no longer the active page. |
* | `ionViewWillUnload` | Runs when the page is about to be destroyed and have its elements removed. |
* | `ionViewDidUnload` | Runs after the page has been destroyed and its elements have been removed.
*
*
* ## Asynchronous Nav Transitions
*
* Navigation transitions are asynchronous operations. When a transition is started,
* the `push` or `pop` method will return immediately, before the transition is complete.
*
* Generally, the developer does not need to be concerned about this. In the event
* multiple transitions need to be synchronized or transition timing is critical,
* the best practice is to chain the transitions together using the return value
* from the `push` and `pop` methods.
*
* The `push` and `pop` methods return a [Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
* Promises are a way to represent and chain together multiple asynchronous
* operations in order. Navigation actions can be chained together very easily using promises.
*
* ```typescript
* let navTransitionPromise = this.navController.push(Page2);
* navTransitionPromise.then( () => {
* // the transition has completed, so I can push another page now
* return this.navController.push(Page3);
* }).then( () => {
* // the second transition has completed, so I can push yet another page
return this.navController.push(Page4);
* }).then( () => {
* console.log('The transitions are complete!');
* })
* ```
*
* ## NavOptions
*
* Some methods on `NavController` allow for customizing the current transition.
* To do this, we can pass an object with the modified properites.
*
*
* | Property | Value | Description |
* |-----------|-----------|------------------------------------------------------------------------------------------------------------|
* | animate | `boolean` | Whether or not the transition should animate. |
* | animation | `string` | What kind of animation should be used. |
* | direction | `string` | The conceptual direction the user is navigating. For example, is the user navigating `forward`, or `back`? |
* | duration | `number` | The length in milliseconds the animation should take. |
* | easing | `string` | The easing for the animation. |
*
* The property 'animation' understands the following values: `md-transition`, `ios-transition` and `wp-transition`.
*
* @see {@link /docs/v2/components#navigation Navigation Component Docs}
*/
export abstract class NavController {
/**
* Observable to be subscribed to when a component is loaded.
* @returns {Observable} Returns an observable
*/
viewDidLoad: EventEmitter<any>;
/**
* Observable to be subscribed to when a component is about to be loaded.
* @returns {Observable} Returns an observable
*/
viewWillEnter: EventEmitter<any>;
/**
* Observable to be subscribed to when a component has fully become the active component.
* @returns {Observable} Returns an observable
*/
viewDidEnter: EventEmitter<any>;
/**
* Observable to be subscribed to when a component is about to leave, and no longer active.
* @returns {Observable} Returns an observable
*/
viewWillLeave: EventEmitter<any>;
/**
* Observable to be subscribed to when a component has fully left and is no longer active.
* @returns {Observable} Returns an observable
*/
viewDidLeave: EventEmitter<any>;
/**
* Observable to be subscribed to when a component is about to be unloaded and destroyed.
* @returns {Observable} Returns an observable
*/
viewWillUnload: EventEmitter<any>;
/**
* Observable to be subscribed to when a component has fully been unloaded and destroyed.
* @returns {Observable} Returns an observable
*/
viewDidUnload: EventEmitter<any>;
/**
* @private
*/
id: string;
/**
* The parent navigation instance. If this is the root nav, then
* it'll be `null`. A `Tab` instance's parent is `Tabs`, otherwise
* the parent would be another nav, if it's not already the root nav.
*/
parent: any;
/**
* @private
*/
config: Config;
/**
* Set the root for the current navigation stack.
* @param {Page} page The name of the component you want to push on the navigation stack.
* @param {object} [params={}] Any nav-params you want to pass along to the next view.
* @param {object} [opts={}] Any options you want to use pass to transtion.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setRoot(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Set the views of the current navigation stack and navigate to the
* last view. By default animations are disabled, but they can be enabled
* by passing options to the navigation controller.You can also pass any
* navigation params to the individual pages in the array.
*
* @param {array<Page>} pages An arry of page components and their params to load in the stack.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setPages(pages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Push a new component onto the current navication stack. Pass any aditional information
* along as an object. This additional information is acessible through NavParams
*
* @param {Page} page The page component class you want to push on to the navigation stack
* @param {object} [params={}] Any nav-params you want to pass along to the next view
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract push(page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Inserts a component into the nav stack at the specified index. This is useful if
* you need to add a component at any point in your navigation stack.
*
*
* @param {number} insertIndex The index where to insert the page.
* @param {Page} page The component you want to insert into the nav stack.
* @param {object} [params={}] Any nav-params you want to pass along to the next page.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract insert(insertIndex: number, page: any, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Inserts an array of components into the nav stack at the specified index.
* The last component in the array will become instantiated as a view,
* and animate in to become the active view.
*
* @param {number} insertIndex The index where you want to insert the page.
* @param {array} insertPages An array of objects, each with a `page` and optionally `params` property.
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract insertPages(insertIndex: number, insertPages: Array<{page: any, params?: any}>, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Call to navigate back from a current component. Similar to `push()`, you
* can also pass navigation options.
*
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract pop(opts?: NavOptions, done?: Function): Promise<any>;
/**
* Navigate back to the root of the stack, no matter how far back that is.
*
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract popToRoot(opts?: NavOptions, done?: Function): Promise<any>;
/**
* @private
* Pop to a specific view in the history stack.
*
* @param {ViewController} view to pop to
* @param {object} [opts={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract popTo(view: ViewController, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Removes a page from the nav stack at the specified index.
*
* @param {number} [startIndex] The starting index to remove pages from the stack. Default is the index of the last page.
* @param {number} [removeCount] The number of pages to remove, defaults to remove `1`.
* @param {object} [opts={}] Any options you want to use pass to transtion.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract remove(startIndex: number, removeCount?: number, opts?: NavOptions, done?: Function): Promise<any>;
/**
* @param {number} index The index of the page to get.
* @returns {ViewController} Returns the view controller that matches the given index.
*/
abstract getByIndex(index: number): ViewController;
/**
* @returns {ViewController} Returns the active page's view controller.
*/
abstract getActive(): ViewController;
/**
* Returns if the given view is the active view or not.
* @param {ViewController} view
* @returns {boolean}
*/
abstract isActive(view: ViewController): boolean;
/**
* Returns the view controller which is before the given view controller.
* @param {ViewController} view
* @returns {viewController}
*/
abstract getPrevious(view: ViewController): ViewController;
/**
* Returns the first view controller in this nav controller's stack.
* @returns {ViewController}
*/
abstract first(): ViewController;
/**
* Returns the last page in this nav controller's stack.
* @returns {ViewController}
*/
abstract last(): ViewController;
/**
* Returns the index number of the given view controller.
* @param {ViewController} view
* @returns {number}
*/
abstract indexOf(view: ViewController): number;
/**
* Returns the number of views in this nav controller.
* @returns {number} The number of views in this stack, including the current view.
*/
abstract length(): number;
/**
* Returns the active child navigation.
*/
abstract getActiveChildNav(): any;
/**
* Returns if the nav controller is actively transitioning or not.
* @return {boolean}
*/
abstract isTransitioning(includeAncestors?: boolean): boolean
/**
* If it's possible to use swipe back or not. If it's not possible
* to go back, or swipe back is not enabled, then this will return `false`.
* If it is possible to go back, and swipe back is enabled, then this
* will return `true`.
* @returns {boolean}
*/
abstract canSwipeBack(): boolean;
/**
* Returns `true` if there's a valid previous page that we can pop
* back to. Otherwise returns `false`.
* @returns {boolean}
*/
abstract canGoBack(): boolean;
}

View File

@@ -1,18 +0,0 @@
export interface NavOptions {
animate?: boolean;
animation?: string;
direction?: string;
duration?: number;
easing?: string;
id?: string;
keyboardClose?: boolean;
preload?: boolean;
transitionDelay?: number;
progressAnimation?: boolean;
climbNav?: boolean;
ev?: any;
}
export const DIRECTION_BACK = 'back';
export const DIRECTION_FORWARD = 'forward';

View File

@@ -1,49 +0,0 @@
/**
* @name NavParams
* @description
* NavParams are an object that exists on a page and can contain data for that particular view.
* Similar to how data was pass to a view in V1 with `$stateParams`, NavParams offer a much more flexible
* option with a simple `get` method.
*
* @usage
* ```ts
* export class MyClass{
* constructor(public params: NavParams){
* // userParams is an object we have in our nav-parameters
* this.params.get('userParams');
* }
* }
* ```
* @demo /docs/v2/demos/nav-params/
* @see {@link /docs/v2/components#navigation Navigation Component Docs}
* @see {@link ../NavController/ NavController API Docs}
* @see {@link ../Nav/ Nav API Docs}
* @see {@link ../NavPush/ NavPush API Docs}
*/
export class NavParams {
/**
* @private
* @param {TODO} data TODO
*/
constructor(public data: any = {}) {}
/**
* Get the value of a nav-parameter for the current view
*
* ```ts
* export class MyClass{
* constructor(public params: NavParams){
* // userParams is an object we have in our nav-parameters
* this.params.get('userParams');
* }
* }
* ```
*
*
* @param {string} parameter Which param you want to look up
*/
get(param: string): any {
return this.data[param];
}
}

View File

@@ -1,6 +1,9 @@
import { Directive, HostListener, Input, Optional } from '@angular/core';
import { NavController } from './nav-controller';
import { noop } from '../../util/util';
import { AfterViewInit, Directive, HostBinding, HostListener, OnChanges, Optional } from '@angular/core';
import { DeepLinker } from '../../navigation/deep-linker';
import { NavController } from '../../navigation/nav-controller';
import { ViewController } from '../../navigation/view-controller';
/**
* @name NavPop
@@ -27,9 +30,9 @@ import { noop } from '../../util/util';
})
export class NavPop {
constructor(@Optional() private _nav: NavController) {
constructor(@Optional() public _nav: NavController) {
if (!_nav) {
console.error('nav-pop must be within a NavController');
console.error('navPop must be within a NavController');
}
}
@@ -37,7 +40,7 @@ export class NavPop {
onClick(): boolean {
// If no target, or if target is _self, prevent default browser behavior
if (this._nav) {
this._nav.pop(null, noop);
this._nav.pop(null, null);
return false;
}
@@ -45,3 +48,39 @@ export class NavPop {
}
}
/**
* @private
*/
@Directive({
selector: 'a[navPop]'
})
export class NavPopAnchor implements OnChanges, AfterViewInit {
constructor(
@Optional() public host: NavPop,
public linker: DeepLinker,
@Optional() public viewCtrl: ViewController) {}
@HostBinding() href: string;
updateHref() {
if (this.host && this.viewCtrl) {
const previousView = this.host._nav.getPrevious(this.viewCtrl);
this.href = (previousView && this.linker.createUrl(this.host._nav, this.viewCtrl.component, this.viewCtrl.data)) || '#';
} else {
this.href = '#';
}
}
ngOnChanges() {
this.updateHref();
}
ngAfterViewInit() {
this.updateHref();
}
}

View File

@@ -1,7 +1,7 @@
import { Directive, HostListener, Input, Optional } from '@angular/core';
import { AfterViewInit, Directive, Host, HostBinding, HostListener, Input, Optional, OnChanges } from '@angular/core';
import { NavController } from './nav-controller';
import { noop } from '../../util/util';
import { DeepLinker } from '../../navigation/deep-linker';
import { NavController } from '../../navigation/nav-controller';
/**
* @name NavPush
@@ -60,7 +60,7 @@ export class NavPush {
@Input() navParams: {[k: string]: any};
constructor(@Optional() private _nav: NavController) {
constructor(@Optional() public _nav: NavController) {
if (!_nav) {
console.error('navPush must be within a NavController');
}
@@ -68,13 +68,44 @@ export class NavPush {
@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);
this._nav.push(this.navPush, this.navParams, null);
return false;
}
return true;
}
}
/**
* @private
*/
@Directive({
selector: 'a[navPush]'
})
export class NavPushAnchor implements OnChanges, AfterViewInit {
constructor(
@Host() public host: NavPush,
@Optional() public linker: DeepLinker) {}
@HostBinding() href: string;
updateHref() {
if (this.host && this.linker) {
this.href = this.linker.createUrl(this.host._nav, this.host.navPush, this.host.navParams) || '#';
} else {
this.href = '#';
}
}
ngOnChanges() {
this.updateHref();
}
ngAfterViewInit() {
this.updateHref();
}
}

View File

@@ -1,12 +1,15 @@
import { AfterViewInit, Component, ComponentResolver, ElementRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { AfterViewInit, Component, ComponentFactoryResolver, ElementRef, Input, Optional, NgZone, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { Keyboard } from '../../util/keyboard';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util';
import { NavControllerBase } from './nav-controller-base';
import { ViewController } from './view-controller';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions } from '../../navigation/nav-util';
import { TransitionController } from '../../transitions/transition-controller';
import { ViewController } from '../../navigation/view-controller';
/**
* @name Nav
@@ -24,32 +27,27 @@ import { ViewController } from './view-controller';
*
* ```ts
* import { Component } from '@angular/core';
* import { ionicBootstrap } from 'ionic-angular';
* import { GettingStartedPage } from './getting-started';
*
* @Component({
* template: `<ion-nav [root]="root"></ion-nav>`
* })
* class MyApp {
* private root: any = GettingStartedPage;
* root = GettingStartedPage;
*
* constructor(){
* }
* }
*
* ionicBootstrap(MyApp);
* ```
*
*
* @demo /docs/v2/demos/navigation/
* @see {@link /docs/v2/components#navigation Navigation Component Docs}
*/
@Component({
selector: 'ion-nav',
template: `
<div #viewport nav-viewport></div>
<div class="nav-decor"></div>
`,
template:
'<div #viewport nav-viewport></div>' +
'<div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None,
})
export class Nav extends NavControllerBase implements AfterViewInit {
@@ -65,16 +63,17 @@ export class Nav extends NavControllerBase implements AfterViewInit {
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
gestureCtrl: GestureController
cfr: ComponentFactoryResolver,
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() linker: DeepLinker
) {
super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(parent, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
if (viewCtrl) {
// an ion-nav can also act as an ion-page within a parent ion-nav
// this would happen when an ion-nav nests a child ion-nav.
viewCtrl.setContent(this);
viewCtrl.setContentRef(elementRef);
viewCtrl._setContent(this);
}
if (parent) {
@@ -89,7 +88,7 @@ export class Nav extends NavControllerBase implements AfterViewInit {
} else if (app && !app.getRootNav()) {
// a root nav has not been registered yet with the app
// this is the root navcontroller for the entire app
app.setRootNav(this);
app._setRootNav(this);
}
}
@@ -101,17 +100,26 @@ export class Nav extends NavControllerBase implements AfterViewInit {
this.setViewport(val);
}
/**
* @private
*/
ngAfterViewInit() {
this._hasInit = true;
if (this._root) {
this.push(this._root);
let navSegment = this._linker.initNav(this);
if (navSegment && navSegment.component) {
// there is a segment match in the linker
this.setPages(this._linker.initViews(navSegment), null, null);
} else if (this._root) {
// no segment match, so use the root property
this.push(this._root, this.rootParams, {
isNavRoot: (<any>this._app.getRootNav() === this)
}, null);
}
}
goToRoot(opts: NavOptions) {
this.setRoot(this._root, this.rootParams, opts, null);
}
/**
* @input {Page} The Page component to load as the root page within this nav.
*/
@@ -127,6 +135,11 @@ export class Nav extends NavControllerBase implements AfterViewInit {
}
}
/**
* @input {object} Any nav-params to pass to the root page of this nav.
*/
@Input() rootParams: any;
/**
* @input {boolean} Whether it's possible to swipe-to-go-back on this nav controller or not.
*/
@@ -138,4 +151,11 @@ export class Nav extends NavControllerBase implements AfterViewInit {
this._sbEnabled = isTrueProperty(val);
}
/**
* @private
*/
destroy() {
this.destroy();
}
}

View File

@@ -1,18 +1,20 @@
import { ComponentResolver, Directive, ElementRef, forwardRef, Inject, NgZone, Optional, Renderer, ViewContainerRef } from '@angular/core';
import { ComponentFactoryResolver, Directive, ElementRef, forwardRef, Inject, NgZone, Optional, Renderer, ViewContainerRef } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { TransitionController } from '../../transitions/transition-controller';
/**
* @private
*/
@Directive({
selector: '[nav-portal]'
selector: '[overlay-portal]'
})
export class NavPortal extends NavControllerBase {
export class OverlayPortal extends NavControllerBase {
constructor(
@Inject(forwardRef(() => App)) app: App,
config: Config,
@@ -20,15 +22,16 @@ export class NavPortal extends NavControllerBase {
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
cfr: ComponentFactoryResolver,
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() linker: DeepLinker,
viewPort: ViewContainerRef
) {
super(null, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(null, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
this._isPortal = true;
this._init = true;
this.setViewport(viewPort);
app.setPortal(this);
// on every page change make sure the portal has
// dismissed any views that should be auto dismissed on page change

View File

@@ -1,55 +0,0 @@
import { assign } from '../../util/util';
import { GestureController, GestureDelegate, GesturePriority } from '../../gestures/gesture-controller';
import { MenuController } from '../menu/menu-controller';
import { NavControllerBase } from './nav-controller-base';
import { SlideData } from '../../gestures/slide-gesture';
import { SlideEdgeGesture } from '../../gestures/slide-edge-gesture';
export class SwipeBackGesture extends SlideEdgeGesture {
constructor(
element: HTMLElement,
options: any,
private _nav: NavControllerBase,
gestureCtlr: GestureController
) {
super(element, assign({
direction: 'x',
maxEdgeStart: 75,
gesture: gestureCtlr.create('goback-swipe', {
priority: GesturePriority.GoBackSwipe,
})
}, options));
}
canStart(ev: any): boolean {
// the gesture swipe angle must be mainly horizontal and the
// gesture distance would be relatively short for a swipe back
// and swipe back must be possible on this nav controller
return (
this._nav.canSwipeBack() &&
super.canStart(ev)
);
}
onSlideBeforeStart(slideData: SlideData, ev: any) {
console.debug('swipeBack, onSlideBeforeStart', ev.type);
this._nav.swipeBackStart();
}
onSlide(slide: SlideData) {
let stepValue = (slide.distance / slide.max);
console.debug('swipeBack, onSlide, distance', slide.distance, 'max', slide.max, 'stepValue', stepValue);
this._nav.swipeBackProgress(stepValue);
}
onSlideEnd(slide: SlideData, ev: any) {
let shouldComplete = (Math.abs(slide.velocity) > 0.2 || Math.abs(slide.delta) > Math.abs(slide.max) * 0.5);
let currentStepValue = (slide.distance / slide.max);
console.debug('swipeBack, onSlideEnd, shouldComplete', shouldComplete, 'currentStepValue', currentStepValue);
this._nav.swipeBackEnd(shouldComplete, currentStepValue);
}
}

View File

@@ -1,116 +0,0 @@
import { LifeCycleEvent, ViewController } from '../../../../src';
export function run() {
describe('ViewController', () => {
afterEach(() => {
if ( subscription ) {
subscription.unsubscribe();
}
});
describe('willEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willEnter.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillEnter();
}, 10000);
});
describe('didEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didEnter.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireDidEnter();
}, 10000);
});
describe('willLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willLeave.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillLeave();
}, 10000);
});
describe('didLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didLeave.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireDidLeave();
}, 10000);
});
describe('willUnload', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.willUnload.subscribe((event: LifeCycleEvent) => {
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.fireWillUnload();
}, 10000);
});
describe('destroy', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = new ViewController(FakePage);
subscription = viewController.didUnload.subscribe((event: LifeCycleEvent) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done(err);
});
// act
viewController.destroy();
}, 10000);
});
});
let subscription: any = null;
class FakePage {}
}

View File

@@ -1,628 +0,0 @@
import { ChangeDetectorRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
import { Footer, Header } from '../toolbar/toolbar';
import { isPresent, merge } from '../../util/util';
import { Navbar } from '../navbar/navbar';
import { NavController } from './nav-controller';
import { NavOptions } from './nav-interfaces';
import { NavParams } from './nav-params';
/**
* @name ViewController
* @description
* Access various features and information about the current view.
* @usage
* ```ts
* import { Component } from '@angular/core';
* import { ViewController } from 'ionic-angular';
*
* @Component({...})
* export class MyPage{
*
* constructor(public viewCtrl: ViewController) {}
*
* }
* ```
*/
export class ViewController {
private _cntDir: any;
private _cntRef: ElementRef;
private _tbRefs: ElementRef[] = [];
private _hdrDir: Header;
private _ftrDir: Footer;
private _destroyFn: Function;
private _hdAttr: string = null;
private _leavingOpts: NavOptions = null;
private _loaded: boolean = false;
private _nbDir: Navbar;
private _onDidDismiss: Function = null;
private _onWillDismiss: Function = null;
private _pgRef: ElementRef;
private _cd: ChangeDetectorRef;
protected _nav: NavController;
/**
* Observable to be subscribed to when the current component will become active
* @returns {Observable} Returns an observable
*/
willEnter: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component has become active
* @returns {Observable} Returns an observable
*/
didEnter: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component will no longer be active
* @returns {Observable} Returns an observable
*/
willLeave: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component is no long active
* @returns {Observable} Returns an observable
*/
didLeave: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component will be destroyed
* @returns {Observable} Returns an observable
*/
willUnload: EventEmitter<any>;
/**
* Observable to be subscribed to when the current component has been destroyed
* @returns {Observable} Returns an observable
*/
didUnload: EventEmitter<any>;
/**
* @internal
*/
data: any;
/**
* @private
*/
id: string;
/**
* @private
*/
instance: any = {};
/**
* @private
*/
state: number = 0;
/**
* @private
* If this is currently the active view, then set to false
* if it does not want the other views to fire their own lifecycles.
*/
fireOtherLifecycles: boolean = true;
/**
* @private
*/
isOverlay: boolean = false;
/**
* @private
*/
zIndex: number;
/**
* @private
*/
@Output() private _emitter: EventEmitter<any> = new EventEmitter();
constructor(public componentType?: any, data?: any) {
// passed in data could be NavParams, but all we care about is its data object
this.data = (data instanceof NavParams ? data.data : (isPresent(data) ? data : {}));
this.willEnter = new EventEmitter();
this.didEnter = new EventEmitter();
this.willLeave = new EventEmitter();
this.didLeave = new EventEmitter();
this.willUnload = new EventEmitter();
this.didUnload = new EventEmitter();
}
/**
* @private
*/
subscribe(generatorOrNext?: any): any {
return this._emitter.subscribe(generatorOrNext);
}
/**
* @private
*/
emit(data?: any) {
this._emitter.emit(data);
}
/**
* @private
* onDismiss(..) has been deprecated. Please use onDidDismiss(..) instead
*/
private onDismiss(callback: Function) {
// deprecated warning: added beta.11 2016-06-30
console.warn('onDismiss(..) has been deprecated. Please use onDidDismiss(..) instead');
this.onDidDismiss(callback);
}
/**
* @private
*/
onDidDismiss(callback: Function) {
this._onDidDismiss = callback;
}
/**
* @private
*/
onWillDismiss(callback: Function) {
this._onWillDismiss = callback;
}
/**
* @private
*/
dismiss(data?: any, role?: any, navOptions: NavOptions = {}) {
let options = merge({}, this._leavingOpts, navOptions);
this._onWillDismiss && this._onWillDismiss(data, role);
return this._nav.remove(this._nav.indexOf(this), 1, options).then(() => {
this._onDidDismiss && this._onDidDismiss(data, role);
return data;
});
}
/**
* @private
*/
setNav(navCtrl: NavController) {
this._nav = navCtrl;
}
/**
* @private
*/
getNav() {
return this._nav;
}
/**
* @private
*/
getTransitionName(direction: string): string {
return this._nav && this._nav.config.get('pageTransition');
}
/**
* @private
*/
getNavParams(): NavParams {
return new NavParams(this.data);
}
/**
* @private
*/
setLeavingOpts(opts: NavOptions) {
this._leavingOpts = opts;
}
/**
* Check to see if you can go back in the navigation stack.
* @param {boolean} Check whether or not you can go back from this page
* @returns {boolean} Returns if it's possible to go back from this Page.
*/
enableBack(): boolean {
// update if it's possible to go back from this nav item
if (this._nav) {
let previousItem = this._nav.getPrevious(this);
// the previous view may exist, but if it's about to be destroyed
// it shouldn't be able to go back to
return !!(previousItem);
}
return false;
}
/**
* @private
*/
setChangeDetector(cd: ChangeDetectorRef) {
this._cd = cd;
}
/**
* @private
*/
setInstance(instance: any) {
this.instance = instance;
}
/**
* @private
*/
get name(): string {
return this.componentType ? this.componentType['name'] : '';
}
/**
* Get the index of the current component in the current navigation stack.
* @returns {number} Returns the index of this page within its `NavController`.
*/
get index(): number {
return (this._nav ? this._nav.indexOf(this) : -1);
}
/**
* @returns {boolean} Returns if this Page is the first in the stack of pages within its NavController.
*/
isFirst(): boolean {
return (this._nav ? this._nav.first() === this : false);
}
/**
* @returns {boolean} Returns if this Page is the last in the stack of pages within its NavController.
*/
isLast(): boolean {
return (this._nav ? this._nav.last() === this : false);
}
/**
* @private
*/
domShow(shouldShow: boolean, renderer: Renderer) {
// using hidden element attribute to display:none and not render views
// renderAttr of '' means the hidden attribute will be added
// renderAttr of null means the hidden attribute will be removed
// doing checks to make sure we only make an update to the element when needed
if (this._pgRef &&
(shouldShow && this._hdAttr === '' ||
!shouldShow && this._hdAttr !== '')) {
this._hdAttr = (shouldShow ? null : '');
renderer.setElementAttribute(this._pgRef.nativeElement, 'hidden', this._hdAttr);
}
}
/**
* @private
*/
setZIndex(zIndex: number, renderer: Renderer) {
if (this._pgRef && zIndex !== this.zIndex) {
this.zIndex = zIndex;
renderer.setElementStyle(this._pgRef.nativeElement, 'z-index', zIndex.toString());
}
}
/**
* @private
*/
setPageRef(elementRef: ElementRef) {
this._pgRef = elementRef;
}
/**
* @private
* @returns {elementRef} Returns the Page's ElementRef
*/
pageRef(): ElementRef {
return this._pgRef;
}
/**
* @private
*/
setContentRef(elementRef: ElementRef) {
this._cntRef = elementRef;
}
/**
* @private
* @returns {elementRef} Returns the Page's Content ElementRef
*/
contentRef(): ElementRef {
return this._cntRef;
}
/**
* @private
*/
setContent(directive: any) {
this._cntDir = directive;
}
/**
* @private
*/
setToolbarRef(elementRef: ElementRef) {
this._tbRefs.push(elementRef);
}
/**
* @private
*/
toolbarRefs(): ElementRef[] {
return this._tbRefs;
}
/**
* @private
*/
setHeader(directive: Header) {
this._hdrDir = directive;
}
/**
* @private
*/
getHeader() {
return this._hdrDir;
}
/**
* @private
*/
setFooter(directive: Footer) {
this._ftrDir = directive;
}
/**
* @private
*/
getFooter() {
return this._ftrDir;
}
/**
* @private
* @returns {component} Returns the Page's Content component reference.
*/
getContent() {
return this._cntDir;
}
/**
* @private
*/
setNavbar(directive: Navbar) {
this._nbDir = directive;
}
/**
* @private
*/
getNavbar() {
return this._nbDir;
}
/**
*
* Find out if the current component has a NavBar or not. Be sure
* to wrap this in an `ionViewWillEnter` method in order to make sure
* the view has rendered fully.
* @returns {boolean} Returns a boolean if this Page has a navbar or not.
*/
hasNavbar(): boolean {
return !!this.getNavbar();
}
/**
* @private
*/
navbarRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getElementRef();
}
/**
* @private
*/
titleRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getTitleRef();
}
/**
* @private
*/
navbarItemRefs(): Array<ElementRef> {
let navbar = this.getNavbar();
return navbar && navbar.getItemRefs();
}
/**
* @private
*/
backBtnRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackButtonRef();
}
/**
* @private
*/
backBtnTextRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackButtonTextRef();
}
/**
* @private
*/
navbarBgRef(): ElementRef {
let navbar = this.getNavbar();
return navbar && navbar.getBackgroundRef();
}
/**
* Change the title of the back-button. Be sure to call this
* after `ionViewWillEnter` to make sure the DOM has been rendered.
* @param {string} backButtonText Set the back button text.
*/
setBackButtonText(val: string) {
let navbar = this.getNavbar();
if (navbar) {
navbar.setBackButtonText(val);
}
}
/**
* Set if the back button for the current view is visible or not. Be sure to call this
* after `ionViewWillEnter` to make sure the DOM has been rendered.
* @param {boolean} Set if this Page's back button should show or not.
*/
showBackButton(shouldShow: boolean) {
let navbar = this.getNavbar();
if (navbar) {
navbar.hideBackButton = !shouldShow;
}
}
/**
* @private
*/
isLoaded(): boolean {
return this._loaded;
}
/**
* The loaded method is used to load any dynamic content/components
* into the dom before proceeding with the transition. If a component
* needs dynamic component loading, extending ViewController and
* overriding this method is a good option
* @param {function} done is a callback that must be called when async
* loading/actions are completed
*/
loaded(done: (() => any)) {
done();
}
/**
* @private
* The view has loaded. This event only happens once per view being
* created. If a view leaves but is cached, then this will not
* fire again on a subsequent viewing. This method is a good place
* to put your setup code for the view; however, it is not the
* recommended method to use when a view becomes active.
*/
fireLoaded() {
this._loaded = true;
ctrlFn(this, 'Loaded');
}
/**
* @private
* The view is about to enter and become the active view.
*/
fireWillEnter() {
if (this._cd) {
// ensure this has been re-attached to the change detector
this._cd.reattach();
// detect changes before we run any user code
this._cd.detectChanges();
}
this.willEnter.emit(null);
ctrlFn(this, 'WillEnter');
}
/**
* @private
* The view has fully entered and is now the active view. This
* will fire, whether it was the first load or loaded from the cache.
*/
fireDidEnter() {
let navbar = this.getNavbar();
navbar && navbar.didEnter();
this.didEnter.emit(null);
ctrlFn(this, 'DidEnter');
}
/**
* @private
* The view has is about to leave and no longer be the active view.
*/
fireWillLeave() {
this.willLeave.emit(null);
ctrlFn(this, 'WillLeave');
}
/**
* @private
* The view has finished leaving and is no longer the active view. This
* will fire, whether it is cached or unloaded.
*/
fireDidLeave() {
this.didLeave.emit(null);
ctrlFn(this, 'DidLeave');
// when this is not the active page
// we no longer need to detect changes
this._cd && this._cd.detach();
}
/**
* @private
* The view is about to be destroyed and have its elements removed.
*/
fireWillUnload() {
this.willUnload.emit(null);
ctrlFn(this, 'WillUnload');
}
/**
* @private
*/
onDestroy(destroyFn: Function) {
this._destroyFn = destroyFn;
}
/**
* @private
*/
destroy() {
this.didUnload.emit(null);
ctrlFn(this, 'DidUnload');
this._destroyFn && this._destroyFn();
this._destroyFn = null;
}
}
export interface LifeCycleEvent {
componentType?: any;
}
function ctrlFn(viewCtrl: ViewController, fnName: string) {
if (viewCtrl.instance) {
// deprecated warning: added 2016-06-01, beta.8
if (viewCtrl.instance['onPage' + fnName]) {
try {
console.warn('onPage' + fnName + '() has been deprecated. Please rename to ionView' + fnName + '()');
viewCtrl.instance['onPage' + fnName]();
} catch (e) {
console.error(viewCtrl.name + ' onPage' + fnName + ': ' + e.message);
}
}
// fire off ionView lifecycle instance method
if (viewCtrl.instance['ionView' + fnName]) {
try {
viewCtrl.instance['ionView' + fnName]();
} catch (e) {
console.error(viewCtrl.name + ' ionView' + fnName + ': ' + e.message);
}
}
}
}

View File

@@ -1,15 +1,17 @@
import { ChangeDetectorRef, Component, ComponentResolver, ElementRef, EventEmitter, forwardRef, Input, Inject, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core';
import { ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, ElementRef, EventEmitter, Input, NgZone, Optional, Output, Renderer, ViewChild, ViewEncapsulation, ViewContainerRef } from '@angular/core';
import { App } from '../app/app';
import { Config } from '../../config/config';
import { DeepLinker } from '../../navigation/deep-linker';
import { GestureController } from '../../gestures/gesture-controller';
import { isTrueProperty } from '../../util/util';
import { Keyboard } from '../../util/keyboard';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions } from '../nav/nav-interfaces';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions } from '../../navigation/nav-util';
import { TabButton } from './tab-button';
import { Tabs } from './tabs';
import { ViewController } from '../nav/view-controller';
import { TransitionController } from '../../transitions/transition-controller';
import { ViewController } from '../../navigation/view-controller';
/**
@@ -93,14 +95,14 @@ import { ViewController } from '../nav/view-controller';
* the tabs.
*
* ```html
* <ion-tabs preloadTabs="false">
* <ion-tabs>
* <ion-tab (ionSelect)="chat()"></ion-tab>
* </ion-tabs>
* ```
*
* ```ts
* export class Tabs {
* constructor(private modalCtrl: ModalController) {
* constructor(public modalCtrl: ModalController) {
*
* }
*
@@ -120,23 +122,40 @@ import { ViewController } from '../nav/view-controller';
*/
@Component({
selector: 'ion-tab',
template:
'<div #viewport></div><div class="nav-decor"></div>',
host: {
'[class.show-tab]': 'isSelected',
'[attr.id]': '_tabId',
'[attr.aria-labelledby]': '_btnId',
'role': 'tabpanel'
},
template: '<div #viewport></div><div class="nav-decor"></div>',
encapsulation: ViewEncapsulation.None,
})
export class Tab extends NavControllerBase {
private _isInitial: boolean;
private _isEnabled: boolean = true;
private _isShown: boolean = true;
private _tabId: string;
private _btnId: string;
private _loaded: boolean;
private _loadTmr: any;
/**
* @private
*/
_isInitial: boolean;
/**
* @private
*/
_isEnabled: boolean = true;
/**
* @private
*/
_isShown: boolean = true;
/**
* @private
*/
_tabId: string;
/**
* @private
*/
_btnId: string;
/**
* @private
*/
_loaded: boolean;
/**
* @private
@@ -158,6 +177,11 @@ export class Tab extends NavControllerBase {
*/
@Input() rootParams: any;
/**
* @input {string} The URL path name to represent this tab within the URL.
*/
@Input() tabUrlPath: string;
/**
* @input {string} The title of the tab button.
*/
@@ -221,21 +245,23 @@ export class Tab extends NavControllerBase {
@Output() ionSelect: EventEmitter<Tab> = new EventEmitter<Tab>();
constructor(
@Inject(forwardRef(() => Tabs)) public parent: Tabs,
parent: Tabs,
app: App,
config: Config,
keyboard: Keyboard,
elementRef: ElementRef,
zone: NgZone,
renderer: Renderer,
compiler: ComponentResolver,
cfr: ComponentFactoryResolver,
private _cd: ChangeDetectorRef,
gestureCtrl: GestureController
gestureCtrl: GestureController,
transCtrl: TransitionController,
@Optional() private linker: DeepLinker
) {
// A Tab is a NavController for its child pages
super(parent, app, config, keyboard, elementRef, zone, renderer, compiler, gestureCtrl);
super(parent, app, config, keyboard, elementRef, zone, renderer, cfr, gestureCtrl, transCtrl, linker);
parent.add(this);
this.id = parent.add(this);
this._tabId = 'tabpanel-' + this.id;
this._btnId = 'tab-' + this.id;
@@ -261,52 +287,33 @@ export class Tab extends NavControllerBase {
*/
load(opts: NavOptions, done?: Function) {
if (!this._loaded && this.root) {
this.push(this.root, this.rootParams, opts, () => {
done(true);
});
this.push(this.root, this.rootParams, opts, done);
this._loaded = true;
} else {
done(false);
done(true);
}
}
/**
* @private
*/
preload(wait: number) {
this._loadTmr = setTimeout(() => {
if (!this._loaded) {
console.debug('Tabs, preload', this.id);
this.load({
animate: false,
preload: true
}, function(){});
}
}, wait);
}
/**
* @private
*/
loadPage(viewCtrl: ViewController, viewport: ViewContainerRef, opts: NavOptions, done: Function) {
let isTabSubPage = (this.parent.subPages && viewCtrl.index > 0);
_viewInsert(viewCtrl: ViewController, componentRef: ComponentRef<any>, viewport: ViewContainerRef) {
const isTabSubPage = (this.parent._subPages && viewCtrl.index > 0);
if (isTabSubPage) {
viewport = this.parent.portal;
}
super.loadPage(viewCtrl, viewport, opts, () => {
if (isTabSubPage) {
// add the .tab-subpage css class to tabs pages that should act like subpages
let pageEleRef = viewCtrl.pageRef();
if (pageEleRef) {
this._renderer.setElementClass(pageEleRef.nativeElement, 'tab-subpage', true);
}
super._viewInsert(viewCtrl, componentRef, viewport);
if (isTabSubPage) {
// add the .tab-subpage css class to tabs pages that should act like subpages
const pageEleRef = viewCtrl.pageRef();
if (pageEleRef) {
this._renderer.setElementClass(pageEleRef.nativeElement, 'tab-subpage', true);
}
done();
});
}
}
/**
@@ -315,6 +322,9 @@ export class Tab extends NavControllerBase {
setSelected(isSelected: boolean) {
this.isSelected = isSelected;
this.setElementClass('show-tab', isSelected);
this.setElementAttribute('aria-hidden', (!isSelected).toString());
if (isSelected) {
// this is the selected tab, detect changes
this._cd.reattach();
@@ -335,9 +345,18 @@ export class Tab extends NavControllerBase {
/**
* @private
*/
ngOnDestroy() {
clearTimeout(this._loadTmr);
super.ngOnDestroy();
updateHref(component: any, data: any) {
if (this.btn && this.linker) {
let href = this.linker.createUrl(this, component, data) || '#';
this.btn.updateHref(href);
}
}
/**
* @private
*/
destroy() {
this.destroy();
}
}

View File

@@ -1,22 +1,19 @@
import { Component, ElementRef, EventEmitter, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { NgClass, NgFor, NgIf } from '@angular/common';
import { AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, Optional, Renderer, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { App } from '../app/app';
import { Badge } from '../badge/badge';
import { Config } from '../../config/config';
import { Content } from '../content/content';
import { Icon } from '../icon/icon';
import { DeepLinker } from '../../navigation/deep-linker';
import { Ion } from '../ion';
import { isBlank, isPresent, isTrueProperty } from '../../util/util';
import { isBlank } from '../../util/util';
import { nativeRaf } from '../../util/dom';
import { NavController } from '../nav/nav-controller';
import { NavControllerBase } from '../nav/nav-controller-base';
import { NavOptions, DIRECTION_FORWARD } from '../nav/nav-interfaces';
import { NavController } from '../../navigation/nav-controller';
import { NavControllerBase } from '../../navigation/nav-controller-base';
import { NavOptions, DIRECTION_SWITCH } from '../../navigation/nav-util';
import { Platform } from '../../platform/platform';
import { Tab } from './tab';
import { TabButton } from './tab-button';
import { TabHighlight } from './tab-highlight';
import { ViewController } from '../nav/view-controller';
import { ViewController } from '../../navigation/view-controller';
/**
@@ -143,72 +140,61 @@ import { ViewController } from '../nav/view-controller';
*/
@Component({
selector: 'ion-tabs',
template: `
<ion-tabbar role="tablist" #tabbar>
<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" [class.tab-disabled]="!t.enabled" [class.tab-hidden]="!t.show" role="tab" href="#" (ionSelect)="select($event)">
<ion-icon *ngIf="t.tabIcon" [name]="t.tabIcon" [isActive]="t.isSelected" class="tab-button-icon"></ion-icon>
<span *ngIf="t.tabTitle" class="tab-button-text">{{t.tabTitle}}</span>
<ion-badge *ngIf="t.tabBadge" class="tab-badge" [ngClass]="\'badge-\' + t.tabBadgeStyle">{{t.tabBadge}}</ion-badge>
<ion-button-effect></ion-button-effect>
</a>
<tab-highlight></tab-highlight>
</ion-tabbar>
<ng-content></ng-content>
<div #portal tab-portal></div>
`,
directives: [Badge, Icon, NgClass, NgFor, NgIf, TabButton, TabHighlight],
template:
'<div class="tabbar" role="tablist" #tabbar>' +
'<a *ngFor="let t of _tabs" [tab]="t" class="tab-button" [class.tab-disabled]="!t.enabled" [class.tab-hidden]="!t.show" role="tab" href="#" (ionSelect)="select($event)">' +
'<ion-icon *ngIf="t.tabIcon" [name]="t.tabIcon" [isActive]="t.isSelected" class="tab-button-icon"></ion-icon>' +
'<span *ngIf="t.tabTitle" class="tab-button-text">{{t.tabTitle}}</span>' +
'<ion-badge *ngIf="t.tabBadge" class="tab-badge" [ngClass]="\'badge-\' + t.tabBadgeStyle">{{t.tabBadge}}</ion-badge>' +
'<div class="button-effect"></div>' +
'</a>' +
'<div class="tab-highlight"></div>' +
'</div>' +
'<ng-content></ng-content>' +
'<div #portal tab-portal></div>',
encapsulation: ViewEncapsulation.None,
})
export class Tabs extends Ion {
private _ids: number = -1;
private _tabs: Tab[] = [];
private _onReady: any = null;
private _sbPadding: boolean;
private _top: number;
private _bottom: number;
/**
* @private
*/
id: string;
/**
* @private
*/
selectHistory: string[] = [];
/**
* @private
*/
subPages: boolean;
export class Tabs extends Ion implements AfterViewInit {
/** @internal */
_color: string;
_ids: number = -1;
/** @internal */
_tabs: Tab[] = [];
/** @internal */
_sbPadding: boolean;
/** @internal */
_top: number;
/** @internal */
_bottom: number;
/** @internal */
id: string;
/** @internal */
_selectHistory: string[] = [];
/** @internal */
_subPages: boolean;
/**
* @input {string} The predefined color to use. For example: `"primary"`, `"secondary"`, `"danger"`.
*/
@Input()
get color(): string {
return this._color;
set color(value: string) {
this._setColor('tabs', value);
}
set color(value: string) {
this._updateColor(value);
/**
* @input {string} The mode to apply to this component.
*/
@Input()
set mode(val: string) {
this._setMode('tabs', val);
}
/**
* @input {number} The default selected tab index when first loaded. If a selected index isn't provided then it will use `0`, the first tab.
*/
@Input() selectedIndex: any;
@Input() selectedIndex: number;
/**
* @input {boolean} Set whether to preload all the tabs: `true`, `false`.
*/
@Input() preloadTabs: any;
/**
* @private DEPRECATED. Please use `tabsLayout` instead.
* @internal DEPRECATED. Please use `tabsLayout` instead.
*/
@Input() private tabbarLayout: string;
@@ -218,7 +204,7 @@ export class Tabs extends Ion {
@Input() tabsLayout: string;
/**
* @private DEPRECATED. Please use `tabsPlacement` instead.
* @internal DEPRECATED. Please use `tabsPlacement` instead.
*/
@Input() private tabbarPlacement: string;
@@ -238,17 +224,17 @@ export class Tabs extends Ion {
@Output() ionChange: EventEmitter<Tab> = new EventEmitter<Tab>();
/**
* @private
* @internal
*/
@ViewChild(TabHighlight) private _highlight: TabHighlight;
@ViewChild(TabHighlight) _highlight: TabHighlight;
/**
* @private
* @internal
*/
@ViewChild('tabbar') private _tabbar: ElementRef;
@ViewChild('tabbar') _tabbar: ElementRef;
/**
* @private
* @internal
*/
@ViewChild('portal', {read: ViewContainerRef}) portal: ViewContainerRef;
@@ -261,29 +247,31 @@ export class Tabs extends Ion {
@Optional() parent: NavController,
@Optional() public viewCtrl: ViewController,
private _app: App,
private _config: Config,
private _elementRef: ElementRef,
config: Config,
elementRef: ElementRef,
private _platform: Platform,
private _renderer: Renderer
renderer: Renderer,
private _linker: DeepLinker
) {
super(_elementRef);
super(config, elementRef, renderer);
this.mode = config.get('mode');
this.parent = <NavControllerBase>parent;
this.id = 't' + (++tabIds);
this._sbPadding = _config.getBoolean('statusbarPadding');
this.subPages = _config.getBoolean('tabsHideOnSubPages');
this.tabsHighlight = _config.getBoolean('tabsHighlight');
this._sbPadding = config.getBoolean('statusbarPadding');
this._subPages = config.getBoolean('tabsHideOnSubPages');
this.tabsHighlight = config.getBoolean('tabsHighlight');
// TODO deprecated 07-07-2016 beta.11
if (_config.get('tabSubPages') !== null) {
if (config.get('tabSubPages') !== null) {
console.warn('Config option "tabSubPages" has been deprecated. Please use "tabsHideOnSubPages" instead.');
this.subPages = _config.getBoolean('tabSubPages');
this._subPages = config.getBoolean('tabSubPages');
}
// TODO deprecated 07-07-2016 beta.11
if (_config.get('tabbarHighlight') !== null) {
if (config.get('tabbarHighlight') !== null) {
console.warn('Config option "tabbarHighlight" has been deprecated. Please use "tabsHighlight" instead.');
this.tabsHighlight = _config.getBoolean('tabbarHighlight');
this.tabsHighlight = config.getBoolean('tabbarHighlight');
}
if (this.parent) {
@@ -297,24 +285,20 @@ export class Tabs extends Ion {
} else if (this._app) {
// this is the root navcontroller for the entire app
this._app.setRootNav(this);
this._app._setRootNav(this);
}
// Tabs may also be an actual ViewController which was navigated to
// if Tabs is static and not navigated to within a NavController
// then skip this and don't treat it as it's own ViewController
if (viewCtrl) {
viewCtrl.setContent(this);
viewCtrl.setContentRef(_elementRef);
viewCtrl.loaded = (done) => {
this._onReady = done;
};
viewCtrl._setContent(this);
viewCtrl._setContentRef(elementRef);
}
}
/**
* @private
* @internal
*/
ngAfterViewInit() {
this._setConfig('tabsPlacement', 'bottom');
@@ -328,27 +312,27 @@ export class Tabs extends Ion {
// TODO deprecated 07-07-2016 beta.11
if (this.tabbarPlacement !== undefined) {
console.warn('Input "tabbarPlacement" has been deprecated. Please use "tabsPlacement" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsPlacement', this.tabbarPlacement);
this.setElementAttribute('tabsPlacement', this.tabbarPlacement);
this.tabsPlacement = this.tabbarPlacement;
}
// TODO deprecated 07-07-2016 beta.11
if (this._config.get('tabbarPlacement') !== null) {
console.warn('Config option "tabbarPlacement" has been deprecated. Please use "tabsPlacement" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsPlacement', this._config.get('tabbarPlacement'));
this.setElementAttribute('tabsPlacement', this._config.get('tabbarPlacement'));
}
// TODO deprecated 07-07-2016 beta.11
if (this.tabbarLayout !== undefined) {
console.warn('Input "tabbarLayout" has been deprecated. Please use "tabsLayout" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsLayout', this.tabbarLayout);
this.setElementAttribute('tabsLayout', this.tabbarLayout);
this.tabsLayout = this.tabbarLayout;
}
// TODO deprecated 07-07-2016 beta.11
if (this._config.get('tabbarLayout') !== null) {
console.warn('Config option "tabbarLayout" has been deprecated. Please use "tabsLayout" instead.');
this._renderer.setElementAttribute(this._elementRef.nativeElement, 'tabsLayout', this._config.get('tabsLayout'));
this.setElementAttribute('tabsLayout', this._config.get('tabsLayout'));
}
if (this.tabsHighlight) {
@@ -361,12 +345,19 @@ export class Tabs extends Ion {
}
/**
* @private
* @internal
*/
initTabs() {
// get the selected index from the input
// otherwise default it to use the first index
let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(this.selectedIndex, 10));
let selectedIndex = (isBlank(this.selectedIndex) ? 0 : parseInt(<any>this.selectedIndex, 10));
// now see if the deep linker can find a tab index
const tabsSegment = this._linker.initNav(this);
if (tabsSegment && isBlank(tabsSegment.component)) {
// we found a segment which probably represents which tab to select
selectedIndex = this._linker.getSelectedTabIndex(this, tabsSegment.name, selectedIndex);
}
// get the selectedIndex and ensure it isn't hidden or disabled
let selectedTab = this._tabs.find((t, i) => i === selectedIndex && t.enabled && t.show);
@@ -378,91 +369,73 @@ export class Tabs extends Ion {
if (selectedTab) {
// we found a tab to select
this.select(selectedTab);
}
// check if preloadTab is set as an input @Input
// 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);
// get the segment the deep linker says this tab should load with
let pageId: string = null;
if (tabsSegment) {
let selectedTabSegment = this._linker.initNav(selectedTab);
if (selectedTabSegment && selectedTabSegment.component) {
selectedTab.root = selectedTabSegment.component;
selectedTab.rootParams = selectedTabSegment.data;
pageId = selectedTabSegment.id;
}
}
this.select(selectedTab, {
id: pageId
});
}
// set the initial href attribute values for each tab
this._tabs.forEach(t => {
t.updateHref(t.root, t.rootParams);
});
}
/**
* @private
* @internal
*/
private _setConfig(attrKey: string, fallback: any) {
var val = (<any>this)[attrKey];
_setConfig(attrKey: string, fallback: any) {
let val = (<any>this)[attrKey];
if (isBlank(val)) {
val = this._config.get(attrKey, fallback);
}
this._renderer.setElementAttribute(this._elementRef.nativeElement, attrKey, val);
}
/**
* @internal
*/
_updateColor(newColor: string) {
this._setElementColor(this._color, false);
this._setElementColor(newColor, true);
this._color = newColor;
}
/**
* @internal
*/
_setElementColor(color: string, isAdd: boolean) {
if (color !== null && color !== '') {
this._renderer.setElementClass(this._elementRef.nativeElement, `tabs-${color}`, isAdd);
}
this.setElementAttribute(attrKey, val);
}
/**
* @private
*/
add(tab: Tab) {
tab.id = this.id + '-' + (++this._ids);
this._tabs.push(tab);
return this.id + '-' + (++this._ids);
}
/**
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
*/
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);
select(tabOrIndex: number | Tab, opts: NavOptions = {}) {
const selectedTab: Tab = (typeof tabOrIndex === 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex);
if (isBlank(selectedTab)) {
return Promise.resolve();
return;
}
let deselectedTab = this.getSelected();
const deselectedTab = this.getSelected();
if (selectedTab === deselectedTab) {
// no change
this._touchActive(selectedTab);
return Promise.resolve();
return this._touchActive(selectedTab);
}
console.debug(`Tabs, select: ${selectedTab.id}`);
let deselectedPage: ViewController;
if (deselectedTab) {
deselectedPage = deselectedTab.getActive();
deselectedPage && deselectedPage.fireWillLeave();
deselectedPage && deselectedPage._willLeave();
}
opts.animate = false;
let selectedPage = selectedTab.getActive();
selectedPage && selectedPage.fireWillEnter();
const selectedPage = selectedTab.getActive();
selectedPage && selectedPage._willEnter();
selectedTab.load(opts, (initialLoad: boolean) => {
selectedTab.load(opts, (alreadyLoaded: boolean) => {
selectedTab.ionSelect.emit(selectedTab);
this.ionChange.emit(selectedTab);
@@ -478,27 +451,26 @@ export class Tabs extends Ion {
if (this.tabsHighlight) {
this._highlight.select(selectedTab);
}
if (opts.updateUrl !== false) {
this._linker.navChange(DIRECTION_SWITCH);
}
}
selectedPage && selectedPage.fireDidEnter();
deselectedPage && deselectedPage.fireDidLeave();
if (this._onReady) {
this._onReady();
this._onReady = null;
}
selectedPage && selectedPage._didEnter();
deselectedPage && deselectedPage._didLeave();
// track the order of which tabs have been selected, by their index
// do not track if the tab index is the same as the previous
if (this.selectHistory[this.selectHistory.length - 1] !== selectedTab.id) {
this.selectHistory.push(selectedTab.id);
if (this._selectHistory[this._selectHistory.length - 1] !== selectedTab.id) {
this._selectHistory.push(selectedTab.id);
}
// if this is not the Tab's initial load then we need
// to refresh the tabbar and content dimensions to be sure
// they're lined up correctly
if (!initialLoad && selectedPage) {
var content = <Content>selectedPage.getContent();
if (alreadyLoaded && selectedPage) {
let content = <Content>selectedPage.getContent();
if (content && content instanceof Content) {
nativeRaf(() => {
content.readDimensions();
@@ -506,11 +478,7 @@ export class Tabs extends Ion {
});
}
}
done();
});
return promise;
}
/**
@@ -521,12 +489,12 @@ export class Tabs extends Ion {
previousTab(trimHistory: boolean = true): Tab {
// walk backwards through the tab selection history
// and find the first previous tab that is enabled and shown
console.debug('run previousTab', this.selectHistory);
for (var i = this.selectHistory.length - 2; i >= 0; i--) {
var tab = this._tabs.find(t => t.id === this.selectHistory[i]);
console.debug('run previousTab', this._selectHistory);
for (var i = this._selectHistory.length - 2; i >= 0; i--) {
var tab = this._tabs.find(t => t.id === this._selectHistory[i]);
if (tab && tab.enabled && tab.show) {
if (trimHistory) {
this.selectHistory.splice(i + 1);
this._selectHistory.splice(i + 1);
}
return tab;
}
@@ -540,17 +508,14 @@ export class Tabs extends Ion {
* @returns {Tab} Returns the tab who's index matches the one passed
*/
getByIndex(index: number): Tab {
if (index < this._tabs.length && index > -1) {
return this._tabs[index];
}
return null;
return this._tabs[index];
}
/**
* @return {Tab} Returns the currently selected tab
*/
getSelected(): Tab {
for (let i = 0; i < this._tabs.length; i++) {
for (var i = 0; i < this._tabs.length; i++) {
if (this._tabs[i].isSelected) {
return this._tabs[i];
}
@@ -559,68 +524,57 @@ export class Tabs extends Ion {
}
/**
* @private
* @internal
*/
getActiveChildNav() {
return this.getSelected();
}
/**
* @private
* @internal
*/
getIndex(tab: Tab): number {
return this._tabs.indexOf(tab);
}
/**
* @private
* @internal
*/
length(): number {
return this._tabs.length;
}
/**
* @private
* "Touch" the active tab, going back to the root view of the tab
* or optionally letting the tab handle the event
*/
private _touchActive(tab: Tab) {
let active = tab.getActive();
const active = tab.getActive();
if (!active) {
return Promise.resolve();
if (active) {
if (active._cmp && active._cmp.instance.ionSelected) {
// if they have a custom tab selected handler, call it
active._cmp.instance.ionSelected();
} else if (tab.length() > 1) {
// if we're a few pages deep, pop to root
tab.popToRoot(null, null);
} else if (tab.root !== active.component) {
// Otherwise, if the page we're on is not our real root, reset it to our
// default root type
tab.setRoot(tab.root);
}
}
let instance = active.instance;
// If they have a custom tab selected handler, call it
if (instance.ionSelected) {
return instance.ionSelected();
}
// If we're a few pages deep, pop to root
if (tab.length() > 1) {
// Pop to the root view
return tab.popToRoot();
}
// Otherwise, if the page we're on is not our real root, reset it to our
// default root type
if (tab.root !== active.componentType) {
return tab.setRoot(tab.root);
}
// And failing all of that, we do something safe and secure
return Promise.resolve();
}
/**
* @private
* @internal
* DOM WRITE
*/
setTabbarPosition(top: number, bottom: number) {
if (this._top !== top || this._bottom !== bottom) {
let tabbarEle = <HTMLElement>this._tabbar.nativeElement;
const tabbarEle = <HTMLElement>this._tabbar.nativeElement;
tabbarEle.style.top = (top > -1 ? top + 'px' : '');
tabbarEle.style.bottom = (bottom > -1 ? bottom + 'px' : '');
tabbarEle.classList.add('show-tabbar');