chore(): begin adding ionic components to mono-repo.

This commit is contained in:
Josh Thomas
2017-06-21 09:33:06 -05:00
parent 1181fe98fc
commit bd5b67304d
2159 changed files with 15687 additions and 147 deletions

View File

@ -0,0 +1,498 @@
import { ComponentFactory, ComponentFactoryResolver } from '@angular/core';
import { Location } from '@angular/common';
import { App } from '../components/app/app';
import { convertToViews, DIRECTION_BACK, isNav, isTab, isTabs, NavLink, NavSegment } from './nav-util';
import { ModuleLoader } from '../util/module-loader';
import { isArray, isPresent } from '../util/util';
import { Nav, Tab, Tabs } from './nav-interfaces';
import { NavController } from './nav-controller';
import { UrlSerializer } from './url-serializer';
import { ViewController } from './view-controller';
/**
* @hidden
*/
export class DeepLinker {
/** @internal */
_segments: NavSegment[] = [];
/** @internal */
_history: string[] = [];
/** @internal */
_indexAliasUrl: string;
constructor(
public _app: App,
public _serializer: UrlSerializer,
public _location: Location,
public _moduleLoader: ModuleLoader,
public _baseCfr: ComponentFactoryResolver
) {}
/**
* @internal
*/
init() {
// scenario 1: Initial load of all navs from the initial browser URL
const browserUrl = normalizeUrl(this._location.path());
console.debug(`DeepLinker, init load: ${browserUrl}`);
// update the Path from the browser URL
this._segments = this._serializer.parse(browserUrl);
// remember this URL in our internal history stack
this._historyPush(browserUrl);
// listen for browser URL changes
this._location.subscribe((locationChg: { url: string }) => {
this._urlChange(normalizeUrl(locationChg.url));
});
}
/**
* The browser's location has been updated somehow.
* @internal
*/
_urlChange(browserUrl: string) {
// do nothing if this url is the same as the current one
if (!this._isCurrentUrl(browserUrl)) {
if (this._isBackUrl(browserUrl)) {
// scenario 2: user clicked the browser back button
// scenario 4: user changed the browser URL to what was the back url was
// scenario 5: user clicked a link href that was the back url
console.debug(`DeepLinker, browser urlChange, back to: ${browserUrl}`);
this._historyPop();
} else {
// scenario 3: user click forward button
// scenario 4: user changed browser URL that wasn't the back url
// scenario 5: user clicked a link href that wasn't the back url
console.debug(`DeepLinker, browser urlChange, forward to: ${browserUrl}`);
this._historyPush(browserUrl);
}
// get the app's root nav
const appRootNav = <Nav> (this._app.getRootNav() as any);
if (appRootNav) {
if (browserUrl === '/') {
// a url change to the index url
if (isPresent(this._indexAliasUrl)) {
// we already know the indexAliasUrl
// update the url to use the know alias
browserUrl = this._indexAliasUrl;
} else {
// the url change is to the root but we don't
// already know the url used. So let's just
// reset the root nav to its root page
return appRootNav.goToRoot({
updateUrl: false,
isNavRoot: true
});
}
}
// normal url
this._segments = this._serializer.parse(browserUrl);
// this is so dirty I need a shower
this._loadNavFromPath(((appRootNav as any) as NavController));
}
}
}
/**
* Update the deep linker using the NavController's current active view.
* @internal
*/
navChange(direction: string) {
// all transitions completed
if (direction) {
// get the app's active nav, which is the lowest level one being viewed
const activeNav = this._app.getActiveNav();
if (activeNav) {
// build up the segments of all the navs from the lowest level
this._segments = this._pathFromNavs(activeNav);
// build a string URL out of the Path
const browserUrl = this._serializer.serialize(this._segments);
// update the browser's location
this._updateLocation(browserUrl, direction);
}
}
}
/**
* @internal
*/
_updateLocation(browserUrl: string, direction: string) {
if (this._indexAliasUrl === browserUrl) {
browserUrl = '/';
}
if (direction === DIRECTION_BACK && this._isBackUrl(browserUrl)) {
// this URL is exactly the same as the back URL
// it's safe to use the browser's location.back()
console.debug(`DeepLinker, location.back(), url: '${browserUrl}'`);
this._historyPop();
this._location.back();
} else if (!this._isCurrentUrl(browserUrl)) {
// probably navigating forward
console.debug(`DeepLinker, location.go('${browserUrl}')`);
this._historyPush(browserUrl);
this._location.go(browserUrl);
}
}
getComponentFromName(componentName: string): Promise<any> {
const link = this._serializer.getLinkFromName(componentName);
if (link) {
// cool, we found the right link for this component name
return this.getNavLinkComponent(link);
}
// umm, idk
return Promise.reject(`invalid link: ${componentName}`);
}
getNavLinkComponent(link: NavLink) {
if (link.component) {
// sweet, we're already got a component loaded for this link
return Promise.resolve(link.component);
}
if (link.loadChildren) {
// awesome, looks like we'll lazy load this component
// using loadChildren as the URL to request
return this._moduleLoader.load(link.loadChildren).then((response) => {
link.component = response.component;
return response.component;
});
}
return Promise.reject(`invalid link component: ${link.name}`);
}
/**
* @internal
*/
resolveComponent(component: any): ComponentFactory<any> {
let cfr = this._moduleLoader.getComponentFactoryResolver(component);
if (!cfr) {
cfr = this._baseCfr;
}
return cfr.resolveComponentFactory(component);
}
/**
* @internal
*/
createUrl(nav: any, nameOrComponent: any, data: any, prepareExternalUrl: boolean = true): string {
// create a segment out of just the passed in name
const segment = this._serializer.createSegmentFromName(nameOrComponent);
if (segment) {
const path = this._pathFromNavs(nav, segment.component, data);
// serialize the segments into a browser URL
// and prepare the URL with the location and return
const url = this._serializer.serialize(path);
return prepareExternalUrl ? this._location.prepareExternalUrl(url) : url;
}
return '';
}
/**
* Build a browser URL out of this NavController. Climbs up the tree
* of NavController's to create a string representation of all the
* NavControllers state.
*
* @internal
*/
_pathFromNavs(nav: NavController, component?: any, data?: any): NavSegment[] {
const segments: NavSegment[] = [];
let view: ViewController;
let segment: NavSegment;
let tabSelector: string;
// recursivly climb up the nav ancestors
// and set each segment's data
while (nav) {
// this could be an ion-nav, ion-tab or ion-portal
// if a component and data was already passed in then use it
// otherwise get this nav's active view controller
if (!component && isNav(nav)) {
view = nav.getActive(true);
if (view) {
component = view.component;
data = view.data;
}
}
// the ion-nav or ion-portal has an active view
// serialize the component and its data to a NavSegment
segment = this._serializer.serializeComponent(component, data);
// reset the component/data
component = data = null;
if (!segment) {
break;
}
// add the segment to the path
segments.push(segment);
if (isTab(nav)) {
// this nav is a Tab, which is a child of Tabs
// add a segment to represent which Tab is the selected one
tabSelector = this._getTabSelector(<any>nav);
segments.push({
id: tabSelector,
name: tabSelector,
component: null,
data: null
});
// a parent to Tab is a Tabs
// we should skip over any Tabs and go to the next parent
nav = nav.parent && nav.parent.parent;
} else {
// this is an ion-nav
// climb up to the next parent
nav = nav.parent;
}
}
// segments added from bottom to top, so Ti esrever dna ti pilf
return segments.reverse();
}
/**
* @internal
*/
_getTabSelector(tab: Tab): string {
if (isPresent(tab.tabUrlPath)) {
return tab.tabUrlPath;
}
if (isPresent(tab.tabTitle)) {
return this._serializer.formatUrlPart(tab.tabTitle);
}
return `tab-${tab.index}`;
}
/**
* @internal
*/
getSelectedTabIndex(tabsNav: Tabs, pathName: string, fallbackIndex: number = 0): number {
// we found a segment which probably represents which tab to select
const indexMatch = pathName.match(/tab-(\d+)/);
if (indexMatch) {
// awesome, the segment name was something "tab-0", and
// the numbe represents which tab to select
return parseInt(indexMatch[1], 10);
}
// wasn't in the "tab-0" format so maybe it's using a word
const tab = tabsNav._tabs.find(t => {
return (isPresent(t.tabUrlPath) && t.tabUrlPath === pathName) ||
(isPresent(t.tabTitle) && this._serializer.formatUrlPart(t.tabTitle) === pathName);
});
return isPresent(tab) ? tab.index : fallbackIndex;
}
/**
* Each NavController will call this method when it initializes for
* the first time. This allows each NavController to figure out
* where it lives in the path and load up the correct component.
* @internal
*/
initNav(nav: any): NavSegment {
const path = this._segments;
if (nav && path.length) {
if (!nav.parent) {
// a nav without a parent is always the first nav segment
path[0].navId = nav.id;
return path[0];
}
for (var i = 1; i < path.length; i++) {
if (path[i - 1].navId === nav.parent.id) {
// this nav's parent segment is the one before this segment's index
path[i].navId = nav.id;
return path[i];
}
}
}
return null;
}
/**
* @internal
*/
initViews(segment: NavSegment) {
const link = this._serializer.getLinkFromName(segment.name);
return this.getNavLinkComponent(link).then((component: any) => {
segment.component = component;
const view = new ViewController(component, segment.data);
view.id = segment.id;
if (isArray(segment.defaultHistory)) {
return convertToViews(this, segment.defaultHistory).then(views => {
views.push(view);
return views;
});
}
return [view];
});
}
/**
* Using the known Path of Segments, walk down all descendents
* from the root NavController and load each NavController according
* to each Segment. This is usually called after a browser URL and
* Path changes and needs to update all NavControllers to match
* the new browser URL. Because the URL is already known, it will
* not update the browser's URL when transitions have completed.
*
* @internal
*/
_loadNavFromPath(nav: NavController, done?: Function) {
if (!nav) {
done && done();
} else {
this._loadViewFromSegment(nav, () => {
this._loadNavFromPath(nav.getActiveChildNav(), done);
});
}
}
/**
* @internal
*/
_loadViewFromSegment(navInstance: any, done: Function) {
// load up which nav ids belong to its nav segment
let segment = this.initNav(navInstance);
if (!segment) {
done();
return;
}
if (isTabs(navInstance)) {
(<Tabs>navInstance).select(
this.getSelectedTabIndex((<Tabs>navInstance), segment.name),
{
updateUrl: false,
animate: false
}
);
done();
return;
}
let nav = <NavController>navInstance;
// walk backwards to see if the exact view we want to show here
// is already in the stack that we can just pop back to
let view: ViewController;
const count = nav.length() - 1;
for (var i = count; i >= 0; i--) {
view = nav.getByIndex(i);
if (view && view.id === segment.id) {
// hooray! we've already got a view loaded in the stack
// matching the view they wanted to show
if (i === count) {
// this is the last view in the stack and it's the same
// as the segment so there's no change needed
done();
} else {
// it's not the exact view as the end
// let's have this nav go back to this exact view
nav.popTo(view, {
animate: false,
updateUrl: false,
}, done);
}
return;
}
}
// ok, so they must be pushing a new view to the stack
// since we didn't find this same component already in the stack
nav.push(segment.component, segment.data, {
id: segment.id, animate: false, updateUrl: false
}, done);
}
/**
* @internal
*/
_isBackUrl(browserUrl: string) {
return (browserUrl === this._history[this._history.length - 2]);
}
/**
* @internal
*/
_isCurrentUrl(browserUrl: string) {
return (browserUrl === this._history[this._history.length - 1]);
}
/**
* @internal
*/
_historyPush(browserUrl: string) {
if (!this._isCurrentUrl(browserUrl)) {
this._history.push(browserUrl);
if (this._history.length > 30) {
this._history.shift();
}
}
}
/**
* @internal
*/
_historyPop() {
this._history.pop();
if (!this._history.length) {
this._historyPush(this._location.path());
}
}
}
export function setupDeepLinker(app: App, serializer: UrlSerializer, location: Location, moduleLoader: ModuleLoader, cfr: ComponentFactoryResolver) {
const deepLinker = new DeepLinker(app, serializer, location, moduleLoader, cfr);
deepLinker.init();
return deepLinker;
}
export function normalizeUrl(browserUrl: string): string {
browserUrl = browserUrl.trim();
if (browserUrl.charAt(0) !== '/') {
// ensure first char is a /
browserUrl = '/' + browserUrl;
}
if (browserUrl.length > 1 && browserUrl.charAt(browserUrl.length - 1) === '/') {
// ensure last char is not a /
browserUrl = browserUrl.substr(0, browserUrl.length - 1);
}
return browserUrl;
}

View File

@ -0,0 +1,270 @@
/**
* @hidden
* public link interface
*/
export interface IonicPageMetadata {
name?: string;
segment?: string;
defaultHistory?: string[];
priority?: string;
}
/**
* @name IonicPage
* @description
* The Ionic Page handles registering and displaying specific pages based on URLs. It's used
* underneath `NavController` so it will never have to be interacted with directly. When a new
* page is pushed with `NavController`, the URL is updated to match the path to this page.
*
* Unlike traditional web apps, URLs don't dictate navigation in Ionic apps.
* Instead, URLs help us link to specific pieces of content as a breadcrumb.
* The current URL gets updated as we navigate, but we use the `NavController`
* push and pop, or `NavPush` and `NavPop` to move around. This makes it much easier
* to handle complicated nested navigation.
*
* We refer to our URL system as a deep link system instead of a router to encourage
* Ionic developers to think of URLs as a breadcrumb rather than as the source of
* truth in navigation. This encourages flexible navigation design and happy apps all
* over the world.
*
*
* @usage
*
* The first step to setting up deep links is to add the page that should be
* a deep link in the `IonicPageModule.forChild` import of the page's module.
* For our examples, this will be `MyPage`:
*
* ```ts
* @NgModule({
* declarations: [
* MyPage
* ],
* imports: [
* IonicPageModule.forChild(MyPage)
* ],
* entryComponents: [
* MyPage
* ]
* })
* export class MyPageModule {}
* ```
*
* Then, add the `@IonicPage` decorator to the component. The most simple usage is adding an
* empty decorator:
*
* ```ts
* @IonicPage()
* @Component({
* templateUrl: 'main.html'
* })
* export class MyPage {}
* ```
*
* This will automatically create a link to the `MyPage` component using the same name as the class,
* `name`: `'MyPage'`. The page can now be navigated to by using this name. For example:
*
* ```ts
* @Component({
* templateUrl: 'another-page.html'
* })
* export class AnotherPage {
* constructor(public navCtrl: NavController) {}
*
* goToMyPage() {
* // go to the MyPage component
* this.navCtrl.push('MyPage');
* }
* }
* ```
*
* The `@IonicPage` decorator accepts a `DeepLinkMetadataType` object. This object accepts
* the following properties: `name`, `segment`, `defaultHistory`, and `priority`. All of them
* are optional but can be used to create complex navigation links.
*
*
* ### Changing Name
*
* As mentioned previously, the `name` property will be set to the class name if it isn't provided.
* Changing the name of the link is extremely simple. To change the name used to link to the
* component, simply pass it in the decorator like so:
*
* ```ts
* @IonicPage({
* name: 'my-page'
* })
* ```
*
* This will create a link to the `MyPage` component using the name `'my-page'`. Similar to the previous
* example, the page can be navigated to by using the name:
*
* ```ts
* goToMyPage() {
* // go to the MyPage component
* this.navCtrl.push('my-page');
* }
* ```
*
*
* ### Setting URL Path
*
* The `segment` property is used to set the URL to the page. If this property isn't provided, the
* `segment` will use the value of `name`. Since components can be loaded anywhere in the app, the
* `segment` doesn't require a full URL path. When a page becomes the active page, the `segment` is
* appended to the URL.
*
* The `segment` can be changed to anything and doesn't have to match the `name`. For example, passing
* a value for `name` and `segment`:
*
* ```ts
* @IonicPage({
* name: 'my-page',
* segment: 'some-path'
* })
* ```
*
* When navigating to this page as the first page in the app, the URL will look something like:
*
* ```
* http://localhost:8101/#/some-path
* ```
*
* However, navigating to the page will still use the `name` like the previous examples do.
*
*
* ### Dynamic Links
*
* The `segment` property is useful for creating dynamic links. Sometimes the URL isn't known ahead
* of time, so it can be passed as a variable.
*
* Since passing data around is common practice in an app, it can be reflected in the app's URL by
* using the `:param` syntax. For example, set the `segment` in the `@IonicPage` decorator:
*
* ```ts
* @IonicPage({
* name: 'detail-page',
* segment: 'detail/:id'
* })
* ```
*
* In this case, when we `push` to a new instance of `'detail-page'`, the value of `id` will
* in the `detailInfo` data being passed to `push` will replace `:id` in the URL.
*
* Important: The property needs to be something that can be converted into a string, objects
* are not supported.
*
* For example, to push the `'detail-page'` in the `ListPage` component, the following code could
* be used:
*
* ```ts
* @IonicPage({
* name: 'list'
* })
* export class ListPage {
* constructor(public navCtrl: NavController) {}
*
* pushPage(detailInfo) {
* // Push an `id` to the `'detail-page'`
* this.navCtrl.push('detail-page', {
* 'id': detailInfo.id
* })
* }
* }
* ```
*
* If the value of `detailInfo.id` is `12`, for example, the URL would end up looking like this:
*
* ```
* http://localhost:8101/#/list/detail/12
* ```
*
* Since this `id` will be used to pull in the data of the specific detail page, it's Important
* that the `id` is unique.
*
* Note: Even though the `name` is `detail-page`, the `segment` uses `detail/:id`, and the URL
* will use the `segment`.
*
*
* ### Default History
*
* Pages can be navigated to using deep links from anywhere in the app, but sometimes the app is
* launched from a URL and the page needs to have the same history as if it were navigated to from
* inside of the app.
*
* By default, the page would be navigated to as the first page in the stack with no prior history.
* A good example is the App Store on iOS. Clicking on a URL to an application in the App Store will
* load the details of the application with no back button, as if it were the first page ever viewed.
*
* The default history of any page can be set in the `defaultHistory` property. This history will only
* be used if the history doesn't already exist, meaning if you navigate to the page the history will
* be the pages that were navigated from.
*
* The `defaultHistory` property takes an array of strings. For example, setting the history of the
* detail page to the list page where the `name` is `list`:
*
* ```ts
* @IonicPage({
* name: 'detail-page',
* segment: 'detail/:id',
* defaultHistory: ['list']
* })
* ```
*
* In this example, if the app is launched at `http://localhost:8101/#/detail/my-detail` the displayed page
* will be the `'detail-page'` with an id of `my-detail` and it will show a back button that goes back to
* the `'list'` page.
*
* An example of an application with a set history stack is the Instagram application. Opening a link
* to an image on Instagram will show the details for that image with a back button to the user's profile
* page. There is no "right" way of setting the history for a page, it is up to the application.
*
* ### Priority
*
* The `priority` property is only used during preloading. By default, preloading is turned off so setting
* this property would do nothing. Preloading eagerly loads all deep links after the application boots
* instead of on demand as needed. To enable preloading, set `preloadModules` in the main application module
* config to `true`:
*
* ```ts
* @NgModule({
* declarations: [
* MyApp
* ],
* imports: [
* BrowserModule,
* IonicModule.forRoot(MyApp, {
* preloadModules: true
* })
* ],
* bootstrap: [IonicApp],
* entryComponents: [
* MyApp
* ]
* })
* export class AppModule { }
* ```
*
* If preloading is turned on, it will load the modules based on the value of `priority`. The following
* values are possible for `priority`: `"high"`, `"low"`, and `"off"`. When there is no `priority`, it
* will be set to `"low"`.
*
* All deep links with their priority set to `"high"` will be loaded first. Upon completion of loading the
* `"high"` priority modules, all deep links with a priority of `"low"` (or no priority) will be loaded. If
* the priority is set to `"off"` the link will not be preloaded. Setting the `priority` is as simple as
* passing it to the `@IonicPage` decorator:
*
* ```ts
* @IonicPage({
* name: 'my-page',
* priority: 'high'
* })
* ```
*
* We recommend setting the `priority` to `"high"` on the pages that will be viewed first when launching
* the application.
*
*/
export function IonicPage(config?: IonicPageMetadata): ClassDecorator {
return function(clazz: any) {
return clazz;
};
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,622 @@
import { EventEmitter } from '@angular/core';
import { Config } from '../config/config';
import { NavOptions } from './nav-util';
import { Page } from './nav-util';
import { ViewController } from './view-controller';
/**
* @name NavController
* @description
*
* NavController is the base class for navigation controller components like
* [`Nav`](../../components/nav/Nav/) and [`Tab`](../../components/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](../../components/nav/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 { StartPage } from './start-page';
*
* @Component(
* template: `<ion-nav [root]="rootPage"></ion-nav>`
* })
* class MyApp {
* // set the rootPage to the first page we want displayed
* public rootPage: any = StartPage;
*
* constructor(){
* }
* }
*
* ```
*
* ### 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 [Dependency Injection](https://angular.io/docs/ts/latest/guide/dependency-injection.html).
*
* 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 { Component, ViewChild } from '@angular/core';
* import { NavController } from 'ionic-angular';
*
* @Component({
* template: '<ion-nav #myNav [root]="rootPage"></ion-nav>'
* })
* export class MyApp {
* @ViewChild('myNav') nav: NavController
* public rootPage: any = TabsPage;
*
* // Wait for the components in MyApp's template to be initialized
* // In this case, we are waiting for the Nav with reference variable of "#myNav"
* ngOnInit() {
* // Let's navigate from TabsPage to Page1
* this.nav.push(Page1);
* }
* }
* ```
*
* ### Navigating from an Overlay Component
* What if you wanted to navigate from an overlay component (popover, modal, alert, etc)?
* In this example, we've displayed a popover in our app. From the popover, we'll get a
* reference of the root `NavController` in our app, using the `getRootNav()` method.
*
*
* ```typescript
* import { Component } from '@angular/core';
* import { App, ViewController } from 'ionic-angular';
*
* @Component({
* template: `
* <ion-content>
* <h1>My PopoverPage</h1>
* <ion-button (click)="pushPage()">Call pushPage</ion-button>
* </ion-content>
* `
* })
* class PopoverPage {
* constructor(
* public viewCtrl: ViewController
* public appCtrl: App
* ) {}
*
* pushPage() {
* this.viewCtrl.dismiss();
* this.appCtrl.getRootNav().push(SecondPage);
* }
* }
*```
*
*
* ## 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 onto 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>
* <ion-button (click)="pushPage()">
* Go to OtherPage
* </ion-button>
* </ion-content>
* `
* })
* export class StartPage {
* constructor(public navCtrl: NavController) {
* }
*
* pushPage(){
* // push another page onto 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(public navCtrl: NavController ){
* }
*
* popView(){
* this.navCtrl.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 {
* ionViewDidLoad() {
* console.log("I'm alive!");
* }
* ionViewWillLeave() {
* console.log("Looks like I'm about to leave :(");
* }
* }
* ```
*
* | Page Event | Returns | Description |
* |---------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
* | `ionViewDidLoad` | void | Runs when the page has loaded. This event only happens once per page being created. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The `ionViewDidLoad` event is good place to put your setup code for the page. |
* | `ionViewWillEnter` | void | Runs when the page is about to enter and become the active page. |
* | `ionViewDidEnter` | void | 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` | void | Runs when the page is about to leave and no longer be the active page. |
* | `ionViewDidLeave` | void | Runs when the page has finished leaving and is no longer the active page. |
* | `ionViewWillUnload` | void | Runs when the page is about to be destroyed and have its elements removed. |
* | `ionViewCanEnter` | boolean/Promise&lt;void&gt; | Runs before the view can enter. This can be used as a sort of "guard" in authenticated views where you need to check permissions before the view can enter |
* | `ionViewCanLeave` | boolean/Promise&lt;void&gt; | Runs before the view can leave. This can be used as a sort of "guard" in authenticated views where you need to check permissions before the view can leave |
*
*
* ## Nav Guards
*
* In some cases, a developer should be able to control views leaving and entering. To allow for this, NavController has the `ionViewCanEnter` and `ionViewCanLeave` methods.
* Similar to Angular route guards, but are more integrated with NavController. For example, if you wanted to prevent a user from leaving a view:
*
* ```ts
* export class MyClass{
* constructor(
* public navCtrl: NavController
* ){}
*
* pushPage(){
* this.navCtrl.push(DetailPage);
* }
*
* ionViewCanLeave(): boolean{
* // here we can either return true or false
* // depending on if we want to leave this view
* if(isValid(randomValue)){
* return true;
* } else {
* return false;
* }
* }
* }
* ```
*
* We need to make sure that our `navCtrl.push` has a catch in order to catch the and handle the error.
* If you need to prevent a view from entering, you can do the same thing
*
* ```ts
* export class MyClass{
* constructor(
* public navCtrl: NavController
* ){}
*
* pushPage(){
* this.navCtrl.push(DetailPage);
* }
*
* }
*
* export class DetailPage(){
* constructor(
* public navCtrl: NavController
* ){}
* ionViewCanEnter(): boolean{
* // here we can either return true or false
* // depending on if we want to leave this view
* if(isValid(randomValue)){
* return true;
* } else {
* return false;
* }
* }
* }
* ```
*
* Similar to `ionViewCanLeave` we still need a catch on the original `navCtrl.push` in order to handle it properly.
* When handling the back button in the `ion-navbar`, the catch is already taken care of for you by the framework.
*
* ## 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/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>;
/**
* @hidden
*/
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;
/**
* @hidden
*/
config: Config;
/**
* @input {boolean} If true, swipe to go back is enabled.
*/
swipeBackEnabled: boolean;
/**
* Push a new component onto the current navigation stack. Pass any aditional information
* along as an object. This additional information is accessible through NavParams
*
* @param {Page|string} page The component class or deeplink name you want to push onto the navigation stack.
* @param {object} [params={}] Any NavParams 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: Page | string, 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|string} page The component class or deeplink name you want to push onto the navigation stack.
* @param {object} [params={}] Any NavParams 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 insert(insertIndex: number, page: Page | string, 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: Page | string, 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>;
/**
* @hidden
* Pop to a specific view in the history stack. If an already created
* instance of the page is not found in the stack, then it'll `setRoot`
* to the nav stack by removing all current pages and pushing on a
* new instance of the given page. Note that any params passed to
* this method are not used when an existing page instance has already
* been found in the stack. Nav params are only used by this method
* when a new instance needs to be created.
*
* @param {Page|string|ViewController} page The component class or deeplink name you want to push onto the navigation stack.
* @param {object} [params={}] Any NavParams to be used when a new view instance is created at the root.
* @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(page: Page | string | ViewController, params?: any, opts?: NavOptions, done?: Function): Promise<any>;
/**
* @hidden
* Pop sequently all the pages in the stack.
*
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract popAll(): 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>;
/**
* Removes the specified view controller from the nav stack.
*
* @param {ViewController} [viewController] The viewcontroller to remove.
* @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 removeView(viewController: ViewController, opts?: NavOptions, done?: Function): Promise<any>;
/**
* Set the root for the current navigation stack.
* @param {Page|string|ViewController} pageOrViewCtrl The name of the component you want to push on the navigation stack.
* @param {object} [params={}] Any NavParams you want to pass along to the next view.
* @param {object} [opts={}] Any options you want to use pass to transtion.
* @param {Function} done Callback function on done.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
abstract setRoot(pageOrViewCtrl: Page | string | ViewController, 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:any, params: any}>} pages An array of objects, each with a `page` and optionally `params` property 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: ({page: Page | string, params?: any} | ViewController)[], 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(includeEntering?: boolean): 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.
* If no view controller is passed in, then it'll default to the active view.
* @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 current stack of views in this nav controller.
* @returns {Array<ViewController>} the stack of view controllers in this nav controller.
*/
abstract getViews(): Array<ViewController>;
/**
* 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;
/**
* @hidden
*/
abstract registerChildNav(nav: any): void;
/**
* @hidden
*/
abstract resize(): void;
}

View File

@ -0,0 +1,34 @@
import { NavOptions } from './nav-util';
export interface Nav {
goToRoot(opts: NavOptions): Promise<any>;
}
export interface Tabs {
_tabs: Tab[];
select(tabOrIndex: number | Tab, opts: NavOptions): void;
_top: number;
setTabbarPosition(top: number, bottom: number): void;
}
export interface Tab {
tabUrlPath: string;
tabTitle: string;
index: number;
}
export interface Content {
resize(): void;
}
export interface Footer {
}
export interface Header {
}
export interface Navbar {
setBackButtonText(backButtonText: string): void;
hideBackButton: boolean;
didEnter(): void;
}

View File

@ -0,0 +1,49 @@
/**
* @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 navParams: NavParams){
* // userParams is an object we have in our nav-parameters
* this.navParams.get('userParams');
* }
* }
* ```
* @demo /docs/demos/src/nav-params/
* @see {@link /docs/components#navigation Navigation Component Docs}
* @see {@link ../NavController/ NavController API Docs}
* @see {@link /docs/api/components/nav/Nav/ Nav API Docs}
* @see {@link /docs/api/components/nav/NavPush/ NavPush API Docs}
*/
export class NavParams {
/**
* @hidden
* @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 navParams: NavParams){
* // userParams is an object we have in our nav-parameters
* this.navParams.get('userParams');
* }
* }
* ```
*
*
* @param {string} param Which param you want to look up
*/
get(param: string): any {
return this.data[param];
}
}

View File

@ -0,0 +1,219 @@
import { Renderer, TypeDecorator } from '@angular/core';
import { DeepLinker } from './deep-linker';
import { IonicPageMetadata } from './ionic-page';
import { isArray, isPresent } from '../util/util';
import { isViewController, ViewController } from './view-controller';
import { NavControllerBase } from './nav-controller-base';
import { Transition } from '../transitions/transition';
export function getComponent(linker: DeepLinker, nameOrPageOrView: any, params?: any): Promise<ViewController> {
if (typeof nameOrPageOrView === 'function') {
return Promise.resolve(
new ViewController(nameOrPageOrView, params)
);
}
if (typeof nameOrPageOrView === 'string') {
return linker.getComponentFromName(nameOrPageOrView).then((component) => {
return new ViewController(component, params);
});
}
return Promise.resolve(null);
}
export function convertToView(linker: DeepLinker, nameOrPageOrView: any, params: any): Promise<ViewController> {
if (nameOrPageOrView) {
if (isViewController(nameOrPageOrView)) {
// is already a ViewController
return Promise.resolve(<ViewController>nameOrPageOrView);
}
return getComponent(linker, nameOrPageOrView, params);
}
return Promise.resolve(null);
}
export function convertToViews(linker: DeepLinker, pages: any[]): Promise<ViewController[]> {
const views: Promise<ViewController>[] = [];
if (isArray(pages)) {
for (var i = 0; i < pages.length; i++) {
var page = pages[i];
if (page) {
if (isViewController(page)) {
views.push(page);
} else if (page.page) {
views.push(convertToView(linker, page.page, page.params));
} else {
views.push(convertToView(linker, page, null));
}
}
}
}
return Promise.all(views);
}
let portalZindex = 9999;
export function setZIndex(nav: NavControllerBase, enteringView: ViewController, leavingView: ViewController, direction: string, renderer: Renderer) {
if (enteringView) {
if (nav._isPortal) {
if (direction === DIRECTION_FORWARD) {
enteringView._setZIndex(nav._zIndexOffset + portalZindex, renderer);
}
portalZindex++;
return;
}
leavingView = leavingView || nav.getPrevious(enteringView);
if (leavingView && isPresent(leavingView._zIndex)) {
if (direction === DIRECTION_BACK) {
enteringView._setZIndex(leavingView._zIndex - 1, renderer);
} else {
enteringView._setZIndex(leavingView._zIndex + 1, renderer);
}
} else {
enteringView._setZIndex(INIT_ZINDEX + nav._zIndexOffset, renderer);
}
}
}
export function isTabs(nav: any): boolean {
// Tabs (ion-tabs)
return !!nav && !!nav.getSelected;
}
export function isTab(nav: any): boolean {
// Tab (ion-tab)
return !!nav && isPresent(nav._tabId);
}
export function isNav(nav: any): boolean {
// Nav (ion-nav), Tab (ion-tab), Portal (ion-portal)
return !!nav && !!nav.push;
}
/**
* @hidden
*/
export class DeepLinkMetadata implements IonicPageMetadata {
component?: any;
loadChildren?: string;
name?: string;
segment?: string;
defaultHistory?: string[] | any[];
priority?: string;
}
export interface DeepLinkDecorator extends TypeDecorator {}
export interface DeepLinkMetadataFactory {
(obj: IonicPageMetadata): DeepLinkDecorator;
new (obj: IonicPageMetadata): DeepLinkMetadata;
}
/**
* @hidden
*/
export var DeepLinkMetadataFactory: DeepLinkMetadataFactory;
/**
* @hidden
*/
export interface DeepLinkConfig {
links: DeepLinkMetadata[];
}
// internal link interface, not exposed publicly
export interface NavLink {
component?: any;
loadChildren?: string;
name?: string;
segment?: string;
parts?: string[];
partsLen?: number;
staticLen?: number;
dataLen?: number;
dataKeys?: {[key: string]: boolean};
defaultHistory?: any[];
}
export interface NavResult {
hasCompleted: boolean;
requiresTransition: boolean;
enteringName?: string;
leavingName?: string;
direction?: string;
}
export interface NavSegment {
id: string;
name: string;
component?: any;
loadChildren?: string;
data: any;
navId?: string;
defaultHistory?: NavSegment[];
}
export interface NavOptions {
animate?: boolean;
animation?: string;
direction?: string;
duration?: number;
easing?: string;
id?: string;
keyboardClose?: boolean;
progressAnimation?: boolean;
disableApp?: boolean;
minClickBlockDuration?: number;
ev?: any;
updateUrl?: boolean;
isNavRoot?: boolean;
}
export interface Page extends Function {
new (...args: any[]): any;
}
export interface TransitionResolveFn {
(hasCompleted: boolean, requiresTransition: boolean, enteringName?: string, leavingName?: string, direction?: string): void;
}
export interface TransitionRejectFn {
(rejectReason: any, transition?: Transition): void;
}
export interface TransitionInstruction {
opts: NavOptions;
insertStart?: number;
insertViews?: any[];
removeView?: ViewController;
removeStart?: number;
removeCount?: number;
resolve?: (hasCompleted: boolean) => void;
reject?: (rejectReason: string) => void;
done?: Function;
leavingRequiresTransition?: boolean;
enteringRequiresTransition?: boolean;
requiresTransition?: boolean;
}
export const STATE_NEW = 1;
export const STATE_INITIALIZED = 2;
export const STATE_ATTACHED = 3;
export const STATE_DESTROYED = 4;
export const INIT_ZINDEX = 100;
export const DIRECTION_BACK = 'back';
export const DIRECTION_FORWARD = 'forward';
export const DIRECTION_SWITCH = 'switch';

View File

@ -0,0 +1,77 @@
import { App } from '../components/app/app';
import { Config } from '../config/config';
import { isString } from '../util/util';
import { DeepLinker } from './deep-linker';
import { NavOptions } from './nav-util';
import { Overlay } from './overlay';
export class OverlayProxy {
overlay: Overlay;
_onWillDismiss: Function;
_onDidDismiss: Function;
constructor(public _app: App, public _component: any, public _config: Config, public _deepLinker: DeepLinker) {
}
getImplementation(): Overlay {
throw new Error('Child class must implement "getImplementation" method');
}
/**
* Present the modal instance.
*
* @param {NavOptions} [navOptions={}] Nav options to go with this transition.
* @returns {Promise} Returns a promise which is resolved when the transition has completed.
*/
present(navOptions: NavOptions = {}) {
// check if it's a lazy loaded component, or not
const isLazyLoaded = isString(this._component);
if (isLazyLoaded) {
return this._deepLinker.getComponentFromName(this._component).then((loadedComponent: any) => {
this._component = loadedComponent;
return this.createAndPresentOverlay(navOptions);
});
} else {
return this.createAndPresentOverlay(navOptions);
}
}
dismiss(data?: any, role?: string, navOptions?: NavOptions): Promise<any> {
if (this.overlay) {
return this.overlay.dismiss();
}
}
/**
* Called when the current viewController has be successfully dismissed
*/
onDidDismiss(callback: (data: any, role: string) => void) {
this._onDidDismiss = callback;
if (this.overlay) {
this.overlay.onDidDismiss(this._onDidDismiss);
}
}
createAndPresentOverlay(navOptions: NavOptions) {
this.overlay = this.getImplementation();
this.overlay.onWillDismiss(this._onWillDismiss);
this.overlay.onDidDismiss(this._onDidDismiss);
return this.overlay.present(navOptions);
}
/**
* Called when the current viewController will be dismissed
*/
onWillDismiss(callback: Function) {
this._onWillDismiss = callback;
if (this.overlay) {
this.overlay.onWillDismiss(this._onWillDismiss);
}
}
}

View File

@ -0,0 +1,8 @@
import { NavOptions } from './nav-util';
export interface Overlay {
present(opts?: NavOptions): Promise<any>;
dismiss(data?: any, role?: string, navOptions?: NavOptions): Promise<any>;
onDidDismiss(callback: Function): void;
onWillDismiss(callback: Function): void;
}

View File

@ -0,0 +1,67 @@
import { swipeShouldReset } from '../util/util';
import { DomController } from '../platform/dom-controller';
import { GestureController, GESTURE_PRIORITY_GO_BACK_SWIPE, GESTURE_GO_BACK_SWIPE } from '../gestures/gesture-controller';
import { NavControllerBase } from './nav-controller-base';
import { Platform } from '../platform/platform';
import { SlideData } from '../gestures/slide-gesture';
import { SlideEdgeGesture } from '../gestures/slide-edge-gesture';
/**
* @hidden
*/
export class SwipeBackGesture extends SlideEdgeGesture {
constructor(
plt: Platform,
private _nav: NavControllerBase,
gestureCtlr: GestureController,
domCtrl: DomController,
) {
super(plt, plt.doc().body, {
direction: 'x',
edge: 'start',
maxEdgeStart: 75,
threshold: 5,
zone: false,
domController: domCtrl,
gesture: gestureCtlr.createGesture({
name: GESTURE_GO_BACK_SWIPE,
priority: GESTURE_PRIORITY_GO_BACK_SWIPE,
disableScroll: true
})
});
}
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(ev: any) {
this._nav.swipeBackStart();
}
onSlide(slide: SlideData, ev: any) {
ev.preventDefault();
ev.stopPropagation();
const stepValue = (slide.distance / slide.max);
this._nav.swipeBackProgress(stepValue);
}
onSlideEnd(slide: SlideData, ev: any) {
const velocity = slide.velocity;
const currentStepValue = (slide.distance / slide.max);
const isResetDirecction = velocity < 0;
const isMovingFast = Math.abs(slide.velocity) > 0.4;
const isInResetZone = Math.abs(slide.delta) < Math.abs(slide.max) * 0.5;
const shouldComplete = !swipeShouldReset(isResetDirecction, isMovingFast, isInResetZone);
this._nav.swipeBackEnd(shouldComplete, currentStepValue, velocity);
}
}

View File

@ -0,0 +1,526 @@
import { DeepLinker, normalizeUrl } from '../deep-linker';
import { UrlSerializer } from '../url-serializer';
import { mockApp, mockDeepLinkConfig, mockNavController, mockLocation,
mockModuleLoader, mockTab, mockTabs, mockViews, mockView, noop,
MockView1, MockView2, MockView3 } from '../../util/mock-providers';
describe('DeepLinker', () => {
describe('updateLocation', () => {
it('should update the browserUrl to / when the passed in url matches indexAliasUrl', () => {
linker._indexAliasUrl = '/my-special/url';
linker._updateLocation('/my-special/url', 'forward');
expect(linker._history[0]).toEqual('/');
});
it('should update location.back when back direction and previous url is the same', () => {
spyOn(linker._location, 'back');
spyOn(linker._location, 'go');
spyOn(linker, '_historyPop');
linker._history = ['first-page', 'some-page', 'current-page'];
linker._updateLocation('some-page', 'back');
expect(linker._location.back).toHaveBeenCalled();
expect(linker._location.go).not.toHaveBeenCalled();
expect(linker._historyPop).toHaveBeenCalled();
});
it('should not update location.go when same as current page', () => {
spyOn(linker._location, 'back');
spyOn(linker._location, 'go');
linker._history = ['current-page'];
linker._updateLocation('current-page', 'forward');
expect(linker._location.back).not.toHaveBeenCalled();
expect(linker._location.go).not.toHaveBeenCalled();
});
it('should update location.go when back direction but not actually the previous url', () => {
spyOn(linker._location, 'back');
spyOn(linker._location, 'go');
spyOn(linker, '_historyPush');
linker._history = ['first-page', 'some-other-page'];
linker._updateLocation('some-page', 'forward');
expect(linker._location.back).not.toHaveBeenCalled();
expect(linker._location.go).toHaveBeenCalledWith('some-page');
expect(linker._historyPush).toHaveBeenCalledWith('some-page');
});
it('should update location.go when forward direction', () => {
spyOn(linker._location, 'back');
spyOn(linker._location, 'go');
spyOn(linker, '_historyPush');
linker._updateLocation('new-url', 'forward');
expect(linker._location.back).not.toHaveBeenCalled();
expect(linker._location.go).toHaveBeenCalledWith('new-url');
expect(linker._historyPush).toHaveBeenCalledWith('new-url');
});
});
describe('loadViewFromSegment', () => {
it('should call done if the view is the same as the last one in the stack', () => {
let nav = mockNavController();
let view1 = mockView(MockView1);
view1.id = 'MockPage1';
let view2 = mockView(MockView2);
view2.id = 'MockPage2';
mockViews(nav, [view1, view2]);
linker._segments = serializer.parse('/MockPage2');
spyOn(nav, 'push');
spyOn(nav, 'popTo');
linker._loadViewFromSegment(nav, noop);
expect(nav.push).not.toHaveBeenCalled();
expect(nav.popTo).not.toHaveBeenCalled();
});
it('should popTo a view thats already in the stack', () => {
let nav = mockNavController();
let view1 = mockView(MockView1);
view1.id = 'MockPage1';
let view2 = mockView(MockView2);
view2.id = 'MockPage2';
mockViews(nav, [view1, view2]);
linker._segments = serializer.parse('/MockPage1');
spyOn(nav, 'push');
spyOn(nav, 'popTo');
linker._loadViewFromSegment(nav, noop);
expect(nav.push).not.toHaveBeenCalled();
expect(nav.popTo).toHaveBeenCalled();
});
it('should push a new page', () => {
let nav = mockNavController();
linker._segments = serializer.parse('/MockPage1');
spyOn(nav, 'push');
spyOn(nav, 'popTo');
linker._loadViewFromSegment(nav, noop);
expect(nav.push).toHaveBeenCalled();
expect(nav.popTo).not.toHaveBeenCalled();
});
it('should call select when its a Tabs nav', () => {
let tabs = mockTabs();
mockTab(tabs);
mockTab(tabs);
linker._segments = serializer.parse('/MockPage1');
spyOn(tabs, 'select');
linker._loadViewFromSegment(tabs, noop);
expect(tabs.select).toHaveBeenCalled();
});
it('should not error when no segment found', () => {
let calledDone = false;
let done = () => { calledDone = true; };
let nav = mockNavController();
linker._loadViewFromSegment(nav, done);
expect(calledDone).toEqual(true);
});
});
describe('pathFromNavs', () => {
it('should climb up through Tab and selected Tabs', () => {
let nav1 = mockNavController();
let nav1View1 = mockView(MockView1);
let nav1View2 = mockView(MockView2);
mockViews(nav1, [nav1View1, nav1View2]);
let tabs = mockTabs();
tabs.parent = nav1;
mockTab(tabs);
mockTab(tabs);
let tab3 = mockTab(tabs);
let path = linker._pathFromNavs(tab3, MockView3);
expect(path.length).toEqual(3);
expect(path[0].id).toEqual('viewtwo');
expect(path[1].id).toEqual('tab-2');
expect(path[2].id).toEqual('viewthree');
});
it('should climb up two navs to set path', () => {
let nav1 = mockNavController();
let nav1View1 = mockView(MockView1);
mockViews(nav1, [nav1View1]);
let nav2 = mockNavController();
nav2.parent = nav1;
let path = linker._pathFromNavs(nav2, MockView3);
expect(path.length).toEqual(2);
expect(path[0].id).toEqual('viewone');
expect(path[0].name).toEqual('viewone');
expect(path[1].id).toEqual('viewthree');
expect(path[1].name).toEqual('viewthree');
});
it('should get the path for view and nav', () => {
let nav = mockNavController();
let view = MockView1;
let path = linker._pathFromNavs(nav, view, null);
expect(path.length).toEqual(1);
expect(path[0].id).toEqual('viewone');
expect(path[0].name).toEqual('viewone');
expect(path[0].component).toEqual(MockView1);
expect(path[0].data).toEqual(null);
});
it('should do nothing if blank nav', () => {
let path = linker._pathFromNavs(null, null, null);
expect(path.length).toEqual(0);
});
});
describe('getTabSelector', () => {
it('should get tab url path selector', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
tab1.tabUrlPath = 'some-tab-url-path';
tab1.tabTitle = 'My Tab Title';
expect(linker._getTabSelector(tab1)).toEqual('some-tab-url-path');
});
it('should get tab title selector', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
tab1.tabTitle = 'My Tab Title';
expect(linker._getTabSelector(tab1)).toEqual('my-tab-title');
});
it('should get tab-0 selector', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
expect(linker._getTabSelector(tab1)).toEqual('tab-0');
});
});
describe('getSelectedTabIndex', () => {
it('should select index from tab title', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
let tab3 = mockTab(tabs);
tab1.tabTitle = 'My Account';
tab2.tabTitle = 'My Contact';
tab3.tabTitle = 'My Settings!!';
let selectedIndex = linker.getSelectedTabIndex(tabs, 'my-settings');
expect(selectedIndex).toEqual(2);
});
it('should select index from tab url path', () => {
let tabs = mockTabs();
let tab1 = mockTab(tabs);
let tab2 = mockTab(tabs);
let tab3 = mockTab(tabs);
tab1.tabUrlPath = 'account';
tab2.tabUrlPath = 'contact';
tab3.tabUrlPath = 'settings';
let selectedIndex = linker.getSelectedTabIndex(tabs, 'settings');
expect(selectedIndex).toEqual(2);
});
it('should select index 2 from tab-2 format', () => {
let tabs = mockTabs();
mockTab(tabs);
mockTab(tabs);
mockTab(tabs);
let selectedIndex = linker.getSelectedTabIndex(tabs, 'tab-2');
expect(selectedIndex).toEqual(2);
});
it('should select index 0 when not found', () => {
let tabs = mockTabs();
mockTab(tabs);
mockTab(tabs);
mockTab(tabs);
let selectedIndex = linker.getSelectedTabIndex(tabs, 'notfound');
expect(selectedIndex).toEqual(0);
});
});
describe('initViews', () => {
it('should return an array with one view controller when there isnt default history', (done: Function) => {
const knownSegment = {
id: 'idk',
name: 'viewone',
data: {}
};
const promise = linker.initViews(knownSegment);
promise.then((result: any[]) => {
expect(Array.isArray(result)).toBeTruthy();
expect(result.length).toEqual(1);
done();
}).catch((err: Error) => {
fail(err);
done(err);
});
});
});
describe('initNav', () => {
it('should load root view that contains tabs, and the selected tabs view', () => {
let nav1 = mockNavController();
nav1.id = 'nav1';
nav1.parent = null;
let tabs = mockTabs();
tabs.id = 'tabs';
tabs.parent = nav1;
let tab1 = mockTab(tabs);
tab1.id = 'tab1';
tab1.parent = tabs;
let tab2 = mockTab(tabs);
tab2.id = 'tab2';
tab2.parent = tabs;
linker._segments = serializer.parse('/viewone/account/viewtwo');
let navSegment = linker.initNav(nav1);
expect(navSegment.navId).toEqual('nav1');
expect(navSegment.id).toEqual('viewone');
let tabsSegment = linker.initNav(tabs);
expect(tabsSegment.navId).toEqual('tabs');
expect(tabsSegment.id).toEqual('account');
let tabSegment = linker.initNav(tab2);
expect(tabSegment.navId).toEqual('tab2');
expect(tabSegment.id).toEqual('viewtwo');
});
it('should load root and descendant nav', () => {
let nav1 = mockNavController();
nav1.parent = null;
nav1.id = 'nav1';
let nav2 = mockNavController();
nav2.parent = nav1;
nav2.id = 'nav2';
let nav3 = mockNavController();
nav3.parent = nav2;
nav3.id = 'nav3';
linker._segments = serializer.parse('/viewone/viewtwo/viewthree');
let p1 = linker.initNav(nav1);
expect(p1.navId).toEqual('nav1');
expect(p1.id).toEqual('viewone');
let p2 = linker.initNav(nav2);
expect(p2.navId).toEqual('nav2');
expect(p2.id).toEqual('viewtwo');
let p3 = linker.initNav(nav3);
expect(p3.navId).toEqual('nav3');
expect(p3.id).toEqual('viewthree');
});
it('should load root nav', () => {
let nav = mockNavController();
nav.id = 'myNavId';
linker._segments = serializer.parse('MockPage1');
let p = linker.initNav(nav);
expect(p.navId).toEqual('myNavId');
expect(p.id).toEqual('MockPage1');
});
it('should return null when no nav', () => {
linker._segments = serializer.parse('MockPage1');
expect(linker.initNav(null)).toEqual(null);
});
it('should return null when segments in path', () => {
let nav = mockNavController();
linker._segments = [];
expect(linker.initNav(nav)).toEqual(null);
});
});
describe('createSegmentFromName', () => {
it('should match by the links string name', () => {
let segment = serializer.createSegmentFromName('viewone');
expect(segment.component).toEqual(MockView1);
});
it('should get no match', () => {
let segment = serializer.createSegmentFromName('nonofindo');
expect(segment).toEqual(null);
});
});
describe('urlChange', () => {
it('should use indexAliasUrl when set and browserUrl is /', () => {
linker._loadNavFromPath = (nav: any): any => {};
linker._app.getRootNav = () => {
return mockNavController();
};
spyOn(serializer, 'parse');
linker._indexAliasUrl = '/tabs-page/recents/tab1-page1';
linker._urlChange('/');
expect(serializer.parse).toHaveBeenCalledWith('/tabs-page/recents/tab1-page1');
});
it('should use indexAliasUrl when set and browserUrl is /', () => {
linker._loadNavFromPath = (nav: any): any => {};
linker._app.getRootNav = () => {
return mockNavController();
};
spyOn(serializer, 'parse');
linker._indexAliasUrl = '/tabs-page/recents/tab1-page1';
linker._urlChange('/');
expect(serializer.parse).toHaveBeenCalledWith('/tabs-page/recents/tab1-page1');
});
it('should historyPush if new url', () => {
spyOn(linker, '_historyPop');
spyOn(linker, '_historyPush');
linker._history = ['back-url', 'current-url'];
linker._urlChange('new-url');
expect(linker._historyPop).not.toHaveBeenCalled();
expect(linker._historyPush).toHaveBeenCalled();
});
it('should historyPop if back url', () => {
spyOn(linker, '_historyPop');
spyOn(linker, '_historyPush');
linker._history = ['back-url', 'current-url'];
linker._urlChange('back-url');
expect(linker._historyPop).toHaveBeenCalled();
expect(linker._historyPush).not.toHaveBeenCalled();
});
it('should do nothing if the url is the same', () => {
spyOn(linker, '_historyPop');
spyOn(linker, '_historyPush');
linker._history = ['current-url'];
linker._urlChange('current-url');
expect(linker._historyPop).not.toHaveBeenCalled();
expect(linker._historyPush).not.toHaveBeenCalled();
});
});
describe('isBackUrl', () => {
it('should not be the back path when no history', () => {
expect(linker._isBackUrl('some-page')).toEqual(false);
});
it('should not be the back when same as last path', () => {
linker._history = ['first-page', 'some-page'];
expect(linker._isBackUrl('some-page')).toEqual(false);
});
it('should be the back when same as second to last path', () => {
linker._history = ['first-page', 'some-page', 'current-page'];
expect(linker._isBackUrl('some-page')).toEqual(true);
});
});
describe('isCurrentUrl', () => {
it('should not be the current path when no history', () => {
expect(linker._isCurrentUrl('some-page')).toEqual(false);
});
it('should be the current when same as last path', () => {
linker._history = ['first-page', 'some-page'];
expect(linker._isCurrentUrl('some-page')).toEqual(true);
});
it('should not be the current when not the last path', () => {
linker._history = ['first-page', 'some-page', 'current-page'];
expect(linker._isCurrentUrl('some-page')).toEqual(false);
});
});
describe('normalizeUrl', () => {
it('should parse multiple segment with leading and following / path', () => {
expect(normalizeUrl(' /MockPage1/MockPage2/ ')).toEqual('/MockPage1/MockPage2');
});
it('should parse following / path', () => {
expect(normalizeUrl('MockPage1/')).toEqual('/MockPage1');
});
it('should parse leading / path', () => {
expect(normalizeUrl('/MockPage1')).toEqual('/MockPage1');
});
it('should parse / path', () => {
expect(normalizeUrl('/')).toEqual('/');
});
it('should parse empty path with padding', () => {
expect(normalizeUrl(' ')).toEqual('/');
});
it('should parse empty path', () => {
expect(normalizeUrl('')).toEqual('/');
});
});
var linker: DeepLinker;
var serializer: UrlSerializer;
beforeEach(() => {
let linkConfig = mockDeepLinkConfig();
serializer = new UrlSerializer(linkConfig);
let moduleLoader = mockModuleLoader();
let baseCfr: any = null;
linker = new DeepLinker(mockApp(), serializer, mockLocation(), moduleLoader as any, baseCfr);
});
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,184 @@
import { convertToView, convertToViews, DIRECTION_BACK, DIRECTION_FORWARD, setZIndex } from '../nav-util';
import { mockDeepLinker, mockNavController, MockView, mockRenderer, mockView, mockViews } from '../../util/mock-providers';
import { ViewController } from '../view-controller';
describe('NavUtil', () => {
describe('convertToViews', () => {
it('should convert all page components', (done) => {
let linker = mockDeepLinker();
let pages = [{ page: MockView }, { page: MockView }, { page: MockView }];
convertToViews(linker, pages).then(views => {
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
done();
});
});
it('should convert all string names', (done) => {
let linker = mockDeepLinker({
links: [{ component: MockView, name: 'someName' }]
});
let pages = ['someName', 'someName', 'someName'];
convertToViews(linker, pages).then(views => {
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
done();
});
});
it('should convert all page string names', (done) => {
let linker = mockDeepLinker({
links: [{ component: MockView, name: 'someName' }]
});
let pages = [{ page: 'someName' }, { page: 'someName' }, { page: 'someName' }];
convertToViews(linker, pages).then(views => {
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
done();
});
});
it('should convert all ViewControllers', (done) => {
let pages = [mockView(MockView), mockView(MockView), mockView(MockView)];
let linker = mockDeepLinker();
convertToViews(linker, pages).then(views => {
expect(views.length).toEqual(3);
expect(views[0].component).toEqual(MockView);
expect(views[1].component).toEqual(MockView);
expect(views[2].component).toEqual(MockView);
done();
});
});
});
describe('convertToView', () => {
it('should return new ViewController instance from page component link config name', (done) => {
let linker = mockDeepLinker({
links: [{ component: MockView, name: 'someName' }]
});
convertToView(linker, 'someName', null).then(view => {
expect(view.component).toEqual(MockView);
done();
});
});
it('should return new ViewController instance from page component', (done) => {
let linker = mockDeepLinker();
convertToView(linker, MockView, null).then(view => {
expect(view.component).toEqual(MockView);
done();
});
});
it('should return existing ViewController instance', (done) => {
let linker = mockDeepLinker();
let inputView = new ViewController(MockView);
convertToView(linker, inputView, null).then(outputView => {
expect(outputView).toEqual(inputView);
done();
});
});
it('should return null for null', (done) => {
let linker = mockDeepLinker();
convertToView(linker, null, null).then(view => {
expect(view).toEqual(null);
done();
});
});
it('should return null for undefined', (done) => {
let linker = mockDeepLinker();
convertToView(linker, undefined, undefined).then(view => {
expect(view).toEqual(null);
done();
});
});
it('should return null for number', (done) => {
let linker = mockDeepLinker();
convertToView(linker, 8675309, null).then(view => {
expect(view).toEqual(null);
done();
});
});
});
describe('setZIndex', () => {
it('should set zIndex 100 when leaving view doesnt have a zIndex', () => {
let leavingView = mockView();
let enteringView = mockView();
let nav = mockNavController();
mockViews(nav, [leavingView, enteringView]);
setZIndex(nav, enteringView, leavingView, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 100 on first entering view', () => {
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, null, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 101 on second entering view', () => {
let leavingView = mockView();
leavingView._zIndex = 100;
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, leavingView, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(101);
});
it('should set zIndex 100 on entering view going back', () => {
let leavingView = mockView();
leavingView._zIndex = 101;
let enteringView = mockView();
let nav = mockNavController();
setZIndex(nav, enteringView, leavingView, DIRECTION_BACK, mockRenderer());
expect(enteringView._zIndex).toEqual(100);
});
it('should set zIndex 9999 on first entering portal view', () => {
let enteringView = mockView();
let nav = mockNavController();
nav._isPortal = true;
setZIndex(nav, enteringView, null, DIRECTION_FORWARD, mockRenderer());
expect(enteringView._zIndex).toEqual(9999);
});
});
});

View File

@ -0,0 +1,123 @@
import { OverlayProxy } from '../overlay-proxy';
import { mockApp, mockConfig, mockDeepLinker, mockOverlay } from '../../util/mock-providers';
describe('Overlay Proxy', () => {
describe('dismiss', () => {
it('should call dismiss if overlay is loaded', (done: Function) => {
const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker());
instance.overlay = mockOverlay();
spyOn(instance.overlay, instance.overlay.dismiss.name).and.returnValue(Promise.resolve());
const promise = instance.dismiss();
promise.then(() => {
expect(instance.overlay.dismiss).toHaveBeenCalled();
done();
}).catch((err: Error) => {
fail(err);
done(err);
});
});
});
describe('onWillDismiss', () => {
it('should update the handler on the overlay object', () => {
const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker());
instance.overlay = mockOverlay();
spyOn(instance.overlay, instance.overlay.onWillDismiss.name);
const handler = () => { };
instance.onWillDismiss(handler);
expect(instance.overlay.onWillDismiss).toHaveBeenCalledWith(handler);
});
});
describe('onDidDismiss', () => {
it('should update the handler on the overlay object', () => {
const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker());
instance.overlay = mockOverlay();
spyOn(instance.overlay, instance.overlay.onDidDismiss.name);
const handler = () => { };
instance.onDidDismiss(handler);
expect(instance.overlay.onDidDismiss).toHaveBeenCalledWith(handler);
});
});
describe('createAndPresentOverlay', () => {
it('should set onWillDismiss and onDidDismiss handlers', (done: Function) => {
const instance = new OverlayProxy(mockApp(), 'my-component', mockConfig(), mockDeepLinker());
const handler = () => { };
instance.onWillDismiss(handler);
instance.onDidDismiss(handler);
const knownOptions = {};
const knownOverlay = mockOverlay();
spyOn(knownOverlay, knownOverlay.present.name).and.returnValue(Promise.resolve());
spyOn(knownOverlay, knownOverlay.onDidDismiss.name);
spyOn(knownOverlay, knownOverlay.onWillDismiss.name);
spyOn(instance, 'getImplementation').and.returnValue(knownOverlay);
const promise = instance.createAndPresentOverlay(knownOptions);
promise.then(() => {
expect(knownOverlay.present).toHaveBeenCalledWith(knownOptions);
expect(knownOverlay.onDidDismiss).toHaveBeenCalledWith(handler);
expect(knownOverlay.onWillDismiss).toHaveBeenCalledWith(handler);
done();
}).catch((err: Error) => {
fail(err);
done(err);
});
});
});
describe('present', () => {
it('should use present the overlay immediately if the component is not a string', (done: Function) => {
const knownComponent = { };
const deepLinker = mockDeepLinker();
const knownOverlay = mockOverlay();
const instance = new OverlayProxy(mockApp(), knownComponent, mockConfig(), deepLinker);
const knownOptions = {};
spyOn(instance, 'getImplementation').and.returnValue(knownOverlay);
spyOn(deepLinker, 'getComponentFromName');
const promise = instance.present(knownOptions);
promise.then(() => {
expect(deepLinker.getComponentFromName).not.toHaveBeenCalled();
done();
}).catch((err: Error) => {
fail(err);
done(err);
});
});
it('should load the component if its a string before using it', (done: Function) => {
const knownComponent = { };
const deepLinker = mockDeepLinker();
const knownOverlay = mockOverlay();
const componentName = 'my-component';
const instance = new OverlayProxy(mockApp(), componentName, mockConfig(), deepLinker);
const knownOptions = {};
spyOn(instance, 'getImplementation').and.returnValue(knownOverlay);
spyOn(deepLinker, 'getComponentFromName').and.returnValue(Promise.resolve(knownComponent));
const promise = instance.present(knownOptions);
promise.then(() => {
expect(deepLinker.getComponentFromName).toHaveBeenCalledWith(componentName);
done();
}).catch((err: Error) => {
fail(err);
done(err);
});
});
});
});

View File

@ -0,0 +1,722 @@
import { NavLink, NavSegment } from '../nav-util';
import { UrlSerializer, isPartMatch, fillMatchedUrlParts, parseUrlParts, createMatchedData, normalizeLinks, findLinkByComponentData } from '../url-serializer';
import { mockDeepLinkConfig, noop, MockView1, MockView2, MockView3, MockView4, MockView5 } from '../../util/mock-providers';
describe('UrlSerializer', () => {
describe('serializeComponent', () => {
it('should create segement when config has multiple links to same component', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView1, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView1, name: 'viewthree', segment: 'view/:param1/:param2' };
serializer = mockSerializer([link1, link2, link3]);
serializer._createSegment = noop;
spyOn(serializer, '_createSegment');
serializer.serializeComponent(MockView1, null);
expect(serializer._createSegment).toHaveBeenCalledWith(link1, null);
});
it('should create segment if component found in links', () => {
serializer._createSegment = noop;
spyOn(serializer, '_createSegment');
serializer.serializeComponent(MockView1, null);
expect(serializer._createSegment).toHaveBeenCalled();
});
it('should return null if component not found in links', () => {
serializer._createSegment = noop;
spyOn(serializer, '_createSegment');
serializer.serializeComponent(NotFound, null);
expect(serializer._createSegment).not.toHaveBeenCalled();
});
it('should create tab segment if component found in deep links', () => {
serializer._createSegment = noop;
spyOn(serializer, '_createSegment');
serializer.serializeComponent(MockView1, null);
expect(serializer._createSegment).toHaveBeenCalled();
});
});
describe('_createSegment', () => {
it('should create segement path data', () => {
let link: NavLink = {
parts: ['a', ':id', ':name'],
component: MockView1
};
let data: any = {
id: 8675309,
name: 'jenny'
};
let p = serializer._createSegment(link, data);
expect(p.id).toEqual('a/8675309/jenny');
expect(p.component).toEqual(MockView1);
});
it('should create segement with encodeURIComponent data', () => {
let char = '道';
let encoded = encodeURIComponent(char);
let link: NavLink = {
parts: ['a', ':id'],
component: MockView1
};
let data: any = {
id: char
};
let p = serializer._createSegment(link, data);
expect(p.id).toEqual('a/' + encoded);
expect(p.component).toEqual(MockView1);
expect(p.data.id).toEqual(char);
});
it('should create segement with no data', () => {
let link: NavLink = {
parts: ['a'],
component: MockView1
};
let p = serializer._createSegment(link, null);
expect(p.id).toEqual('a');
expect(p.component).toEqual(MockView1);
expect(p.data).toEqual(null);
});
});
describe('parse', () => {
it('should parse mix match of component paths', () => {
serializer = mockSerializer([
{ segment: 'b/c', name: 'viewone', component: MockView1 },
{ segment: 'a/:id', name: 'viewtwo', component: MockView2 }
]);
let p = serializer.parse('a/b/c');
expect(p.length).toEqual(2);
expect(p[0].component).toEqual(null);
expect(p[0].data).toEqual(null);
expect(p[1].name).toEqual('viewone');
expect(p[1].data).toEqual(null);
});
it('should parse by higher priority with data in middle', () => {
serializer = mockSerializer([
{ segment: 'viewone/:id/viewtwo', name: 'viewone', component: MockView1 },
{ segment: 'viewone/viewtwo', name: 'viewtwo', component: MockView2 },
{ segment: 'viewtwo', name: 'viewthree', component: MockView3 }
]);
let p = serializer.parse('viewone/viewtwo/viewtwo');
expect(p.length).toEqual(1);
expect(p[0].name).toEqual('viewone');
expect(p[0].data.id).toEqual('viewtwo');
});
it('should parse by higher priority, two segments', () => {
serializer = mockSerializer([
{ segment: 'viewone/:id', name: 'viewone', component: MockView1 },
{ name: 'viewtwo', component: MockView2 }
]);
let p = serializer.parse('viewone/viewtwo');
expect(p.length).toEqual(1);
expect(p[0].name).toEqual('viewone');
expect(p[0].data.id).toEqual('viewtwo');
});
it('should parse path with one slash and data', () => {
serializer = mockSerializer([
{ segment: 'a/:id', name: 'a', component: MockView1 },
]);
let p = serializer.parse('a/b');
expect(p.length).toEqual(1);
expect(p[0].name).toEqual('a');
expect(p[0].data.id).toEqual('b');
});
it('should parse multiple url part path', () => {
serializer = mockSerializer([
{ segment: 'c/a/b/d', name: 'five', component: MockView5 },
{ segment: 'c/a/b', name: 'four', component: MockView4 },
{ segment: 'a/b/c', name: 'three', component: MockView3 },
{ segment: 'a/b', name: 'two', component: MockView2 },
{ segment: 'a', name: 'one', component: MockView1 }
]);
let p = serializer.parse('a/b');
expect(p.length).toEqual(1);
expect(p[0].name).toEqual('two');
p = serializer.parse('a');
expect(p.length).toEqual(1);
expect(p[0].name).toEqual('one');
});
it('should parse multiple segments with data', () => {
let p = serializer.parse('viewone/viewtwo');
expect(p.length).toEqual(2);
expect(p[0].name).toEqual('viewone');
expect(p[1].name).toEqual('viewtwo');
});
it('should parse one segment path', () => {
let p = serializer.parse('viewone');
expect(p.length).toEqual(1);
expect(p[0].id).toEqual('viewone');
expect(p[0].name).toEqual('viewone');
expect(p[0].data).toEqual(null);
});
describe('serialize', () => {
it('should bring together two paths that are not the index', () => {
let path: NavSegment[] = [
{ id: 'a', name: 'a', component: MockView1, data: null },
{ id: 'b', name: 'b', component: MockView1, data: null }
];
expect(serializer.serialize(path)).toEqual('/a/b');
});
it('should bring together one path, not the index', () => {
let path: NavSegment[] = [
{ id: 'a', name: 'a', component: MockView1, data: null }
];
expect(serializer.serialize(path)).toEqual('/a');
});
it('should bring together one path that is the index', () => {
let path: NavSegment[] = [
{ id: '', name: 'a', component: MockView1, data: null }
];
expect(serializer.serialize(path)).toEqual('/');
});
});
describe('createMatchedData', () => {
it('should get data from multiple parts', () => {
let matchedUrlParts = ['a', 'ellie', 'blacklab'];
let link: NavLink = {
parts: ['a', ':name', ':breed'], partsLen: 3, component: MockView1
};
let data = createMatchedData(matchedUrlParts, link);
expect(data.name).toEqual('ellie');
expect(data.breed).toEqual('blacklab');
});
it('should get data within the config link path', () => {
let char = '道';
let matchedUrlParts = ['a', 'b', encodeURIComponent(char), 'd'];
let link: NavLink = {
parts: ['a', ':id', ':name', 'd'], partsLen: 4, component: MockView1
};
let data = createMatchedData(matchedUrlParts, link);
expect(data.id).toEqual('b');
expect(data.name).toEqual(char);
});
it('should get data within the config link path', () => {
let matchedUrlParts = ['a', '8675309'];
let link: NavLink = {
parts: ['a', ':num'], partsLen: 2, component: MockView1
};
let data = createMatchedData(matchedUrlParts, link);
expect(data.num).toEqual('8675309');
});
it('should get uri decode data', () => {
let char = '道';
let matchedUrlParts = [`${encodeURIComponent(char)}`];
let link: NavLink = {
parts: [':name'], partsLen: 1, component: MockView1
};
let data = createMatchedData(matchedUrlParts, link);
expect(data.name).toEqual(char);
});
it('should get null data if nothing in the url', () => {
let matchedUrlParts = ['a'];
let link: NavLink = {
parts: ['a'], partsLen: 1, component: MockView1
};
let data = createMatchedData(matchedUrlParts, link);
expect(data).toEqual(null);
});
});
describe('parseUrlParts', () => {
it('should match with complex path', () => {
let urlParts = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
let configLinks: NavLink[] = [
{ parts: ['a', 'b', 'c', 'e'], partsLen: 4, component: MockView2 },
{ parts: ['a', ':key', ':val'], partsLen: 3, component: MockView1 },
{ parts: ['a', 'c', 'd'], partsLen: 3, component: MockView5 },
{ parts: ['d', 'e'], partsLen: 2, component: MockView4 },
{ parts: ['d', ':x'], partsLen: 2, component: MockView3 },
{ parts: ['f'], partsLen: 1, component: MockView2 },
{ parts: [':last'], partsLen: 1, component: MockView1 },
];
let segments = parseUrlParts(urlParts, configLinks);
expect(segments.length).toEqual(4);
expect(segments[0].id).toEqual('a/b/c');
expect(segments[0].data.key).toEqual('b');
expect(segments[0].data.val).toEqual('c');
expect(segments[1].id).toEqual('d/e');
expect(segments[1].data).toEqual(null);
expect(segments[2].id).toEqual('f');
expect(segments[3].id).toEqual('g');
expect(segments[3].data.last).toEqual('g');
});
it('should not get a match on already matched parts', () => {
let urlParts = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
let configLinks: NavLink[] = [
{ parts: ['a', 'b', 'c'], partsLen: 3, component: MockView1 },
{ parts: ['b', 'c', 'd'], partsLen: 3, component: MockView1 }, // no match
{ parts: ['a', 'b'], partsLen: 2, component: MockView1 }, // no match
{ parts: ['d', 'e'], partsLen: 2, component: MockView1 },
{ parts: ['e', 'f'], partsLen: 2, component: MockView1 }, // no match
{ parts: ['e'], partsLen: 1, component: MockView1 }, // no match
{ parts: ['f'], partsLen: 1, component: MockView1 },
];
let segments = parseUrlParts(urlParts, configLinks);
expect(segments.length).toEqual(4);
expect(segments[0].id).toEqual('a/b/c');
expect(segments[1].id).toEqual('d/e');
expect(segments[2].id).toEqual('f');
expect(segments[3].id).toEqual('g');
});
it('should get a one part match', () => {
let urlParts = ['a', 'b', 'c'];
let configLinks: NavLink[] = [
{ parts: ['a'], partsLen: 1, component: MockView1 },
{ parts: ['b'], partsLen: 1, component: MockView2 },
{ parts: ['c'], partsLen: 1, component: MockView3 },
];
let segments = parseUrlParts(urlParts, configLinks);
expect(segments.length).toEqual(3);
expect(segments[0].id).toEqual('a');
expect(segments[1].id).toEqual('b');
expect(segments[2].id).toEqual('c');
});
it('should not match', () => {
let urlParts = ['z'];
let configLinks: NavLink[] = [
{ parts: ['a'], partsLen: 1, component: MockView1 }
];
let segments = parseUrlParts(urlParts, configLinks);
expect(segments.length).toEqual(1);
expect(segments[0].id).toEqual('z');
expect(segments[0].name).toEqual('z');
expect(segments[0].component).toEqual(null);
expect(segments[0].data).toEqual(null);
});
});
describe('fillMatchedUrlParts', () => {
it('should match w/ many url parts and many config parts w/ : data', () => {
let urlParts = ['a', 'b', 'c', 'd', 'e', 'b', 'c'];
let configLink: NavLink = { parts: ['b', 'c', ':key'], partsLen: 3, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(segments[1].id).toEqual('b/c/d');
expect(segments[1].data.key).toEqual('d');
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual(undefined);
expect(urlParts[2]).toEqual(undefined);
expect(urlParts[3]).toEqual(undefined);
expect(urlParts[4]).toEqual('e');
expect(urlParts[5]).toEqual('b');
expect(urlParts[6]).toEqual('c');
});
it('should not match w/ many url parts and many config parts', () => {
let urlParts = ['a', 'b', 'c', 'd', 'e', 'f', 'g'];
let configLink: NavLink = { parts: ['e', 'c', 'd'], partsLen: 3, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments.filter(f => !!f).length).toEqual(0);
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual('b');
expect(urlParts[2]).toEqual('c');
expect(urlParts[3]).toEqual('d');
expect(urlParts[4]).toEqual('e');
expect(urlParts[5]).toEqual('f');
expect(urlParts[6]).toEqual('g');
});
it('should match w/ two sets of the same parts', () => {
let urlParts = ['a', 'b', 'c', 'd', 'b', 'c'];
let configLink: NavLink = { parts: ['b', 'c'], partsLen: 2, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(segments[1].id).toEqual('b/c');
expect(segments[2]).toEqual(undefined);
expect(segments[3]).toEqual(undefined);
expect(segments[4].id).toEqual('b/c');
expect(segments[5]).toEqual(undefined);
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual(undefined);
expect(urlParts[2]).toEqual(undefined);
expect(urlParts[3]).toEqual('d');
expect(urlParts[4]).toEqual(undefined);
expect(urlParts[5]).toEqual(undefined);
});
it('should match w/ many url parts and many config parts', () => {
let urlParts = ['a', 'b', 'c', 'd'];
let configLink: NavLink = { parts: ['c', 'd'], partsLen: 2, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(segments[1]).toEqual(undefined);
expect(segments[2].id).toEqual('c/d');
expect(segments[3]).toEqual(undefined);
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual('b');
expect(urlParts[2]).toEqual(undefined);
expect(urlParts[3]).toEqual(undefined);
});
it('should match the repeated url parts', () => {
let urlParts = ['a', 'a', 'a', 'a'];
let configLink: NavLink = { parts: ['a'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0].id).toEqual('a');
expect(segments[1].id).toEqual('a');
expect(segments[2].id).toEqual('a');
expect(segments[3].id).toEqual('a');
expect(urlParts[0]).toEqual(undefined);
expect(urlParts[1]).toEqual(undefined);
expect(urlParts[2]).toEqual(undefined);
expect(urlParts[3]).toEqual(undefined);
});
it('should not match w/ two url parts', () => {
let urlParts = ['a', 'b'];
let configLink: NavLink = { parts: ['c'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(segments[1]).toEqual(undefined);
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual('b');
});
it('should match data only config link part', () => {
let urlParts = ['a', 'b'];
let configLink: NavLink = { parts: [':key'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0].id).toEqual('a');
expect(segments[0].data.key).toEqual('a');
expect(segments[1].id).toEqual('b');
expect(segments[1].data.key).toEqual('b');
expect(urlParts[0]).toEqual(undefined);
expect(urlParts[1]).toEqual(undefined);
});
it('should match w/ many url parts', () => {
let urlParts = ['a', 'b', 'c', 'd'];
let configLink: NavLink = { parts: ['d'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(segments[1]).toEqual(undefined);
expect(segments[2]).toEqual(undefined);
expect(segments[3].id).toEqual('d');
expect(urlParts[0]).toEqual('a');
expect(urlParts[1]).toEqual('b');
expect(urlParts[2]).toEqual('c');
expect(urlParts[3]).toEqual(undefined);
});
it('should match single part', () => {
let urlParts = ['a'];
let configLink: NavLink = { parts: ['a'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0].id).toEqual('a');
expect(segments[0].component).toEqual(MockView1);
expect(segments[0].data).toEqual(null);
expect(urlParts[0]).toEqual(undefined);
});
it('should not match single part', () => {
let urlParts = ['a'];
let configLink: NavLink = { parts: ['b'], partsLen: 1, component: MockView1 };
let segments: NavSegment[] = new Array(urlParts.length);
fillMatchedUrlParts(segments, urlParts, configLink);
expect(segments[0]).toEqual(undefined);
expect(urlParts[0]).toEqual('a');
});
});
describe('isPartMatch', () => {
it('should match if parts are equal', () => {
expect(isPartMatch('a', 'a')).toEqual(true);
});
it('should not match if parts are not equal', () => {
expect(isPartMatch('a', 'b')).toEqual(false);
});
it('should not match if configLinkPart has a : thats not index 0', () => {
expect(isPartMatch('urlPart', 'my:id')).toEqual(false);
});
it('should match if configLinkPart starts with :', () => {
expect(isPartMatch('urlPart', ':id')).toEqual(true);
});
it('should not match an empty urlPart', () => {
expect(isPartMatch(null, 'configLinkPart')).toEqual(false);
});
it('should not match an empty configLinkPart', () => {
expect(isPartMatch('urlPart', null)).toEqual(false);
});
});
});
describe('formatUrlPart', () => {
it('should encodeURIComponent', () => {
let name = '你好';
let encoded = encodeURIComponent(name);
expect(serializer.formatUrlPart(name)).toEqual(encoded);
});
it('should not allow restricted characters', () => {
expect(serializer.formatUrlPart('!!!Restricted \'?$,.+"*^|/\#%`><;:@&[]=! Characters!!!')).toEqual('restricted-characters');
});
it('should trim and replace spaces with dashes', () => {
expect(serializer.formatUrlPart(' This is the name ')).toEqual('this-is-the-name');
});
it('should not have multiple dashes', () => {
expect(serializer.formatUrlPart('Contact Detail Page')).toEqual('contact-detail-page');
});
it('should change to pascal case for multiple words', () => {
expect(serializer.formatUrlPart('ContactDetailPage')).toEqual('contact-detail-page');
});
it('should change to pascal case for one work', () => {
expect(serializer.formatUrlPart('View1')).toEqual('view1');
});
});
describe('findLinkByComponentData', () => {
it('should get matching link by component w/ data and multiple links using same component, 2 matches', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView1, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView1, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView1, {
param1: false,
param2: 0,
param3: 0
});
expect(foundLink.name).toEqual('viewthree');
});
it('should get matching link by component w/ data and multiple links using same component, 1 match', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView1, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView1, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView1, {
param1: false,
param3: 0
});
expect(foundLink.name).toEqual('viewtwo');
});
it('should get matching link by component w/ no data and multiple links using same component', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView1, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView1, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView1, null);
expect(foundLink.name).toEqual('viewone');
});
it('should get matching link by component data and link data', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView2, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView3, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView3, {
param1: null,
param2: false,
param3: 0,
param4: 'hello'
});
expect(foundLink.name).toEqual('viewthree');
});
it('should get matching link by component without data and link without data', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView2, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView3, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView1, null);
expect(foundLink.name).toEqual('viewone');
});
it('should get no matching link by component without data, but link requires data', () => {
const link1 = { component: MockView1, name: 'viewone', segment: 'view' };
const link2 = { component: MockView2, name: 'viewtwo', segment: 'view/:param1' };
const link3 = { component: MockView3, name: 'viewthree', segment: 'view/:param1/:param2' };
let links = normalizeLinks([link1, link2, link3]);
let foundLink = findLinkByComponentData(links, MockView2, null);
expect(foundLink).toEqual(null);
});
});
describe('normalizeLinks', () => {
it('should sort with four parts, the most number of paths w/out data first', () => {
let links: NavLink[] = [
{ segment: 'a/:val/:id/:name', component: MockView1, name: 'viewone' },
{ segment: 'a/:id/:name/d', component: MockView1, name: 'viewone' },
{ segment: 'a/b/c/d', component: MockView1, name: 'viewone' },
{ segment: 'a/b/:id/d', component: MockView1, name: 'viewone' },
{ segment: 'a/b/:id/:name', component: MockView1, name: 'viewone' },
{ segment: 'a/b/c/:id', component: MockView1, name: 'viewone' },
];
let sortedLinks = normalizeLinks(links);
expect(sortedLinks[0].segment).toEqual('a/b/c/d');
expect(sortedLinks[1].segment).toEqual('a/b/c/:id');
expect(sortedLinks[2].segment).toEqual('a/b/:id/d');
expect(sortedLinks[3].segment).toEqual('a/b/:id/:name');
expect(sortedLinks[4].segment).toEqual('a/:id/:name/d');
expect(sortedLinks[5].segment).toEqual('a/:val/:id/:name');
});
it('should sort with the most number of paths w/out data first', () => {
let links: NavLink[] = [
{ segment: 'a/:id', component: MockView1, name: 'viewone' },
{ segment: 'a/b', component: MockView1, name: 'viewone' },
{ segment: 'a/:id/c', component: MockView1, name: 'viewone' },
];
let sortedLinks = normalizeLinks(links);
expect(sortedLinks[0].segment).toEqual('a/:id/c');
expect(sortedLinks[1].segment).toEqual('a/b');
expect(sortedLinks[2].segment).toEqual('a/:id');
});
it('should sort with the most number of paths first', () => {
let links: NavLink[] = [
{ segment: 'c', component: MockView1, name: 'viewone' },
{ segment: 'b', component: MockView1, name: 'viewone' },
{ segment: 'a', component: MockView1, name: 'viewone' },
{ segment: 'd/c/b/a', component: MockView1, name: 'viewone' },
{ segment: 'aaaaa/bbbb/ccccc', component: MockView1, name: 'viewone' },
{ segment: 'bbbbbbbbbbbbbbbb/c', component: MockView1, name: 'viewone' },
{ segment: 'a/b', component: MockView1, name: 'viewone' },
{ segment: 'a/b/c', component: MockView1, name: 'viewone' },
{ segment: 'aa/b/c', component: MockView1, name: 'viewone' },
];
let sortedLinks = normalizeLinks(links);
expect(sortedLinks[0].segment).toEqual('d/c/b/a');
expect(sortedLinks[1].segment).toEqual('aaaaa/bbbb/ccccc');
expect(sortedLinks[2].segment).toEqual('a/b/c');
expect(sortedLinks[3].segment).toEqual('aa/b/c');
expect(sortedLinks[4].segment).toEqual('bbbbbbbbbbbbbbbb/c');
expect(sortedLinks[5].segment).toEqual('a/b');
expect(sortedLinks[6].segment).toEqual('c');
expect(sortedLinks[7].segment).toEqual('b');
expect(sortedLinks[8].segment).toEqual('a');
});
it('should create a parts from the name', () => {
let links: NavLink[] = [
{ name: 'somename', component: ContactDetailPage },
];
expect(normalizeLinks(links)[0].parts).toEqual(['somename']);
});
it('should create path from name if path missing', () => {
let links: NavLink[] = [
{ component: ContactDetailPage, name: 'contact-detail-page' },
{ component: MockView2, name: 'view-two' },
];
expect(normalizeLinks(links)[0].segment).toEqual('contact-detail-page');
expect(normalizeLinks(links)[1].segment).toEqual('view-two');
});
});
var serializer: UrlSerializer;
beforeEach(() => {
serializer = mockSerializer();
});
});
class ContactDetailPage {}
class NotFound {}
function mockSerializer(navLinks?: NavLink[]) {
let deepLinkConfig = mockDeepLinkConfig(navLinks);
return new UrlSerializer(deepLinkConfig);
}

View File

@ -0,0 +1,154 @@
import { mockNavController, mockView, mockViews } from '../../util/mock-providers';
import { STATE_ATTACHED } from '../nav-util';
describe('ViewController', () => {
describe('willEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willEnter.subscribe((event: any) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._state = STATE_ATTACHED;
viewController._willEnter();
}, 10000);
});
describe('didEnter', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.didEnter.subscribe((event: any) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._state = STATE_ATTACHED;
viewController._didEnter();
}, 10000);
});
describe('willLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willLeave.subscribe((event: any) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._state = STATE_ATTACHED;
viewController._willLeave(false);
}, 10000);
});
describe('didLeave', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.didLeave.subscribe((event: any) => {
// assert
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._didLeave();
}, 10000);
});
describe('willUnload', () => {
it('should emit LifeCycleEvent when called with component data', (done) => {
// arrange
let viewController = mockView();
subscription = viewController.willUnload.subscribe((event: any) => {
expect(event).toEqual(null);
done();
}, (err: any) => {
done();
});
// act
viewController._willUnload();
}, 10000);
});
describe('willDismiss', () => {
it('should have data in the willDismiss', (done) => {
// arrange
let viewController = mockView();
let navControllerBase = mockNavController();
navControllerBase._isPortal = true;
mockViews(navControllerBase, [viewController]);
viewController.onWillDismiss((data: any) => {
expect(data).toEqual('willDismiss data');
done();
});
viewController.dismiss('willDismiss data');
}, 10000);
});
describe('didDismiss', () => {
it('should have data in the didDismiss', (done) => {
// arrange
let viewController = mockView();
let navControllerBase = mockNavController();
navControllerBase._isPortal = true;
mockViews(navControllerBase, [viewController]);
viewController.onDidDismiss((data: any) => {
expect(data).toEqual('didDismiss data');
done();
});
viewController.dismiss('didDismiss data');
}, 10000);
it('should not crash when calling dismiss() twice', (done) => {
// arrange
let viewController = mockView();
let navControllerBase = mockNavController();
navControllerBase._isPortal = true;
mockViews(navControllerBase, [viewController]);
viewController.onDidDismiss((data: any) => {
expect(data).toEqual('didDismiss data');
setTimeout(() => {
viewController.dismiss(); // it should not crash
done();
}, 100);
});
viewController.dismiss('didDismiss data');
}, 10000);
});
afterEach(() => {
if (subscription) {
subscription.unsubscribe();
}
});
let subscription: any = null;
});

View File

@ -0,0 +1,327 @@
import { OpaqueToken } from '@angular/core';
import { DeepLinkConfig, NavLink, NavSegment } from './nav-util';
import { isArray, isBlank, isPresent } from '../util/util';
/**
* @hidden
*/
export class UrlSerializer {
links: NavLink[];
constructor(config: DeepLinkConfig) {
if (config && isArray(config.links)) {
this.links = normalizeLinks(config.links);
} else {
this.links = [];
}
}
/**
* Parse the URL into a Path, which is made up of multiple NavSegments.
* Match which components belong to each segment.
*/
parse(browserUrl: string): NavSegment[] {
if (browserUrl.charAt(0) === '/') {
browserUrl = browserUrl.substr(1);
}
// trim off data after ? and #
browserUrl = browserUrl.split('?')[0].split('#')[0];
return parseUrlParts(browserUrl.split('/'), this.links);
}
createSegmentFromName(nameOrComponent: any): NavSegment {
const configLink = this.getLinkFromName(nameOrComponent);
return configLink ? {
id: configLink.name,
name: configLink.name,
component: configLink.component,
loadChildren: configLink.loadChildren,
data: null,
defaultHistory: configLink.defaultHistory
} : null;
}
getLinkFromName(nameOrComponent: any) {
return this.links.find(link => {
return (link.component === nameOrComponent) ||
(link.name === nameOrComponent);
});
}
/**
* Serialize a path, which is made up of multiple NavSegments,
* into a URL string. Turn each segment into a string and concat them to a URL.
*/
serialize(path: NavSegment[]): string {
return '/' + path.map(segment => segment.id).join('/');
}
/**
* Serializes a component and its data into a NavSegment.
*/
serializeComponent(component: any, data: any): NavSegment {
if (component) {
const link = findLinkByComponentData(this.links, component, data);
if (link) {
return this._createSegment(link, data);
}
}
return null;
}
/** @internal */
_createSegment(configLink: NavLink, data: any): NavSegment {
let urlParts = configLink.parts;
if (isPresent(data)) {
// create a copy of the original parts in the link config
urlParts = urlParts.slice();
// loop through all the data and convert it to a string
const keys = Object.keys(data);
const keysLength = keys.length;
if (keysLength) {
for (var i = 0; i < urlParts.length; i++) {
if (urlParts[i].charAt(0) === ':') {
for (var j = 0; j < keysLength; j++) {
if (urlParts[i] === `:${keys[j]}`) {
// this data goes into the URL part (between slashes)
urlParts[i] = encodeURIComponent(data[keys[j]]);
break;
}
}
}
}
}
}
return {
id: urlParts.join('/'),
name: configLink.name,
component: configLink.component,
loadChildren: configLink.loadChildren,
data: data,
defaultHistory: configLink.defaultHistory
};
}
formatUrlPart(name: string): string {
name = name.replace(URL_REPLACE_REG, '-');
name = name.charAt(0).toLowerCase() + name.substring(1).replace(/[A-Z]/g, match => {
return '-' + match.toLowerCase();
});
while (name.indexOf('--') > -1) {
name = name.replace('--', '-');
}
if (name.charAt(0) === '-') {
name = name.substring(1);
}
if (name.substring(name.length - 1) === '-') {
name = name.substring(0, name.length - 1);
}
return encodeURIComponent(name);
}
}
export const parseUrlParts = (urlParts: string[], configLinks: NavLink[]): NavSegment[] => {
const configLinkLen = configLinks.length;
const urlPartsLen = urlParts.length;
const segments: NavSegment[] = new Array(urlPartsLen);
for (var i = 0; i < configLinkLen; i++) {
// compare url parts to config link parts to create nav segments
var configLink = configLinks[i];
if (configLink.partsLen <= urlPartsLen) {
fillMatchedUrlParts(segments, urlParts, configLink);
}
}
// remove all the undefined segments
for (var i = urlPartsLen - 1; i >= 0; i--) {
if (segments[i] === undefined) {
if (urlParts[i] === undefined) {
// not a used part, so remove it
segments.splice(i, 1);
} else {
// create an empty part
segments[i] = {
id: urlParts[i],
name: urlParts[i],
component: null,
loadChildren: null,
data: null
};
}
}
}
return segments;
};
export const fillMatchedUrlParts = (segments: NavSegment[], urlParts: string[], configLink: NavLink) => {
for (var i = 0; i < urlParts.length; i++) {
var urlI = i;
for (var j = 0; j < configLink.partsLen; j++) {
if (isPartMatch(urlParts[urlI], configLink.parts[j])) {
urlI++;
} else {
break;
}
}
if ((urlI - i) === configLink.partsLen) {
var matchedUrlParts = urlParts.slice(i, urlI);
for (var j = i; j < urlI; j++) {
urlParts[j] = undefined;
}
segments[i] = {
id: matchedUrlParts.join('/'),
name: configLink.name,
component: configLink.component,
loadChildren: configLink.loadChildren,
data: createMatchedData(matchedUrlParts, configLink),
defaultHistory: configLink.defaultHistory
};
}
}
};
export const isPartMatch = (urlPart: string, configLinkPart: string) => {
if (isPresent(urlPart) && isPresent(configLinkPart)) {
if (configLinkPart.charAt(0) === ':') {
return true;
}
return (urlPart === configLinkPart);
}
return false;
};
export const createMatchedData = (matchedUrlParts: string[], link: NavLink): any => {
let data: any = null;
for (var i = 0; i < link.partsLen; i++) {
if (link.parts[i].charAt(0) === ':') {
data = data || {};
data[link.parts[i].substring(1)] = decodeURIComponent(matchedUrlParts[i]);
}
}
return data;
};
export const findLinkByComponentData = (links: NavLink[], component: any, instanceData: any): NavLink => {
let foundLink: NavLink = null;
let foundLinkDataMatches = -1;
for (var i = 0; i < links.length; i++) {
var link = links[i];
if (link.component === component) {
// ok, so the component matched, but multiple links can point
// to the same component, so let's make sure this is the right link
var dataMatches = 0;
if (instanceData) {
var instanceDataKeys = Object.keys(instanceData);
// this link has data
for (var j = 0; j < instanceDataKeys.length; j++) {
if (isPresent(link.dataKeys[instanceDataKeys[j]])) {
dataMatches++;
}
}
} else if (link.dataLen) {
// this component does not have data but the link does
continue;
}
if (dataMatches >= foundLinkDataMatches) {
foundLink = link;
foundLinkDataMatches = dataMatches;
}
}
}
return foundLink;
};
export const normalizeLinks = (links: NavLink[]): NavLink[] => {
for (var i = 0, ilen = links.length; i < ilen; i++) {
var link = links[i];
if (isBlank(link.segment)) {
link.segment = link.name;
}
link.dataKeys = {};
link.parts = link.segment.split('/');
link.partsLen = link.parts.length;
// used for sorting
link.staticLen = link.dataLen = 0;
var stillCountingStatic = true;
for (var j = 0; j < link.partsLen; j++) {
if (link.parts[j].charAt(0) === ':') {
link.dataLen++;
stillCountingStatic = false;
link.dataKeys[link.parts[j].substring(1)] = true;
} else if (stillCountingStatic) {
link.staticLen++;
}
}
}
// sort by the number of parts, with the links
// with the most parts first
return links.sort(sortConfigLinks);
};
function sortConfigLinks(a: NavLink, b: NavLink) {
// sort by the number of parts
if (a.partsLen > b.partsLen) {
return -1;
}
if (a.partsLen < b.partsLen) {
return 1;
}
// sort by the number of static parts in a row
if (a.staticLen > b.staticLen) {
return -1;
}
if (a.staticLen < b.staticLen) {
return 1;
}
// sort by the number of total data parts
if (a.dataLen < b.dataLen) {
return -1;
}
if (a.dataLen > b.dataLen) {
return 1;
}
return 0;
}
const URL_REPLACE_REG = /\s+|\?|\!|\$|\,|\.|\+|\"|\'|\*|\^|\||\/|\\|\[|\]|#|%|`|>|<|;|:|@|&|=/g;
/**
* @hidden
*/
export const DeepLinkConfigToken = new OpaqueToken('USERLINKS');
export function setupUrlSerializer(userDeepLinkConfig: any): UrlSerializer {
return new UrlSerializer(userDeepLinkConfig);
}

View File

@ -0,0 +1,582 @@
import { ComponentRef, ElementRef, EventEmitter, Output, Renderer } from '@angular/core';
import { isPresent, assert } from '../util/util';
import { NavController } from './nav-controller';
import { NavOptions, STATE_NEW, STATE_INITIALIZED, STATE_ATTACHED, STATE_DESTROYED } from './nav-util';
import { NavParams } from './nav-params';
import { Content, Footer, Header, Navbar } from './nav-interfaces';
/**
* @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 _ionCntDir: Content;
private _ionCntRef: ElementRef;
private _hdrDir: Header;
private _ftrDir: Footer;
private _isHidden: boolean = false;
private _leavingOpts: NavOptions;
private _nb: Navbar;
private _onDidDismiss: (data: any, role: string) => void;
private _onWillDismiss: (data: any, role: string) => void;
private _dismissData: any;
private _dismissRole: string;
private _detached: boolean;
_cmp: ComponentRef<any>;
_nav: NavController;
_zIndex: number;
_state: number = STATE_NEW;
_cssClass: string;
/**
* Observable to be subscribed to when the current component will become active
* @returns {Observable} Returns an observable
*/
willEnter: EventEmitter<any> = new EventEmitter();
/**
* Observable to be subscribed to when the current component has become active
* @returns {Observable} Returns an observable
*/
didEnter: EventEmitter<any> = new EventEmitter();
/**
* Observable to be subscribed to when the current component will no longer be active
* @returns {Observable} Returns an observable
*/
willLeave: EventEmitter<any> = new EventEmitter();
/**
* Observable to be subscribed to when the current component is no long active
* @returns {Observable} Returns an observable
*/
didLeave: EventEmitter<any> = new EventEmitter();
/**
* Observable to be subscribed to when the current component has been destroyed
* @returns {Observable} Returns an observable
*/
willUnload: EventEmitter<any> = new EventEmitter();
/**
* @hidden
*/
readReady: EventEmitter<any> = new EventEmitter<any>();
/**
* @hidden
*/
writeReady: EventEmitter<any> = new EventEmitter<any>();
/** @hidden */
data: any;
/** @hidden */
instance: any;
/** @hidden */
id: string;
/** @hidden */
isOverlay: boolean = false;
/** @hidden */
@Output() private _emitter: EventEmitter<any> = new EventEmitter();
constructor(
public component?: any,
data?: any,
rootCssClass: string = DEFAULT_CSS_CLASS
) {
// 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._cssClass = rootCssClass;
}
/**
* @hidden
*/
init(componentRef: ComponentRef<any>) {
assert(componentRef, 'componentRef can not be null');
this._cmp = componentRef;
this.instance = this.instance || componentRef.instance;
this._detached = false;
}
_setNav(navCtrl: NavController) {
this._nav = navCtrl;
}
_setInstance(instance: any) {
this.instance = instance;
}
/**
* @hidden
*/
subscribe(generatorOrNext?: any): any {
return this._emitter.subscribe(generatorOrNext);
}
/**
* @hidden
*/
emit(data?: any) {
this._emitter.emit(data);
}
/**
* Called when the current viewController has be successfully dismissed
*/
onDidDismiss(callback: (data: any, role: string) => void) {
this._onDidDismiss = callback;
}
/**
* Called when the current viewController will be dismissed
*/
onWillDismiss(callback: (data: any, role: string) => void) {
this._onWillDismiss = callback;
}
/**
* Dismiss the current viewController
* @param {any} [data] Data that you want to return when the viewController is dismissed.
* @param {any} [role ]
* @param {NavOptions} navOptions Options for the dismiss navigation.
* @returns {any} data Returns the data passed in, if any.
*/
dismiss(data?: any, role?: string, navOptions: NavOptions = {}): Promise<any> {
if (!this._nav) {
assert(this._state === STATE_DESTROYED, 'ViewController does not have a valid _nav');
return Promise.resolve(false);
}
if (this.isOverlay && !navOptions.minClickBlockDuration) {
// This is a Modal being dismissed so we need
// to add the minClickBlockDuration option
// for UIWebView
navOptions.minClickBlockDuration = 400;
}
this._dismissData = data;
this._dismissRole = role;
const options = Object.assign({}, this._leavingOpts, navOptions);
return this._nav.removeView(this, options).then(() => data);
}
/**
* @hidden
*/
getNav(): NavController {
return this._nav;
}
/**
* @hidden
*/
getTransitionName(direction: string): string {
return this._nav && this._nav.config.get('pageTransition');
}
/**
* @hidden
*/
getNavParams(): NavParams {
return new NavParams(this.data);
}
/**
* @hidden
*/
setLeavingOpts(opts: NavOptions) {
this._leavingOpts = opts;
}
/**
* Check to see if you can go back in the navigation stack.
* @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) {
return false;
}
// the previous view may exist, but if it's about to be destroyed
// it shouldn't be able to go back to
const previousItem = this._nav.getPrevious(this);
return !!(previousItem);
}
/**
* @hidden
*/
get name(): string {
return (this.component ? this.component.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);
}
/**
* @hidden
* DOM WRITE
*/
_domShow(shouldShow: boolean, renderer: Renderer) {
// using hidden element attribute to display:none and not render views
// _hidden value of '' means the hidden attribute will be added
// _hidden value of null means the hidden attribute will be removed
// doing checks to make sure we only update the DOM when actually needed
// if it should render, then the hidden attribute should not be on the element
if (this._cmp && shouldShow === this._isHidden) {
this._isHidden = !shouldShow;
let value = (shouldShow ? null : '');
// ******** DOM WRITE ****************
renderer.setElementAttribute(this.pageRef().nativeElement, 'hidden', value);
}
}
/**
* @hidden
*/
getZIndex(): number {
return this._zIndex;
}
/**
* @hidden
* DOM WRITE
*/
_setZIndex(zIndex: number, renderer: Renderer) {
if (zIndex !== this._zIndex) {
this._zIndex = zIndex;
const pageRef = this.pageRef();
if (pageRef) {
// ******** DOM WRITE ****************
renderer.setElementStyle(pageRef.nativeElement, 'z-index', (<any>zIndex));
}
}
}
/**
* @returns {ElementRef} Returns the Page's ElementRef.
*/
pageRef(): ElementRef {
return this._cmp && this._cmp.location;
}
_setContent(directive: any) {
this._cntDir = directive;
}
/**
* @returns {component} Returns the Page's Content component reference.
*/
getContent(): any {
return this._cntDir;
}
_setContentRef(elementRef: ElementRef) {
this._cntRef = elementRef;
}
/**
* @returns {ElementRef} Returns the Content's ElementRef.
*/
contentRef(): ElementRef {
return this._cntRef;
}
_setIONContent(content: Content) {
this._setContent(content);
this._ionCntDir = content;
}
/**
* @hidden
*/
getIONContent(): Content {
return this._ionCntDir;
}
_setIONContentRef(elementRef: ElementRef) {
this._setContentRef(elementRef);
this._ionCntRef = elementRef;
}
/**
* @hidden
*/
getIONContentRef(): ElementRef {
return this._ionCntRef;
}
_setHeader(directive: Header) {
this._hdrDir = directive;
}
/**
* @hidden
*/
getHeader(): Header {
return this._hdrDir;
}
_setFooter(directive: Footer) {
this._ftrDir = directive;
}
/**
* @hidden
*/
getFooter(): Footer {
return this._ftrDir;
}
_setNavbar(directive: Navbar) {
this._nb = directive;
}
/**
* @hidden
*/
getNavbar(): Navbar {
return this._nb;
}
/**
* 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._nb;
}
/**
* Change the title of the back-button. Be sure to call this
* after `ionViewWillEnter` to make sure the DOM has been rendered.
* @param {string} val Set the back button text.
*/
setBackButtonText(val: string) {
this._nb && this._nb.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) {
if (this._nb) {
this._nb.hideBackButton = !shouldShow;
}
}
_preLoad() {
assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED');
this._lifecycle('PreLoad');
}
/**
* @hidden
* The view has loaded. This event only happens once per view will be created.
* This event is fired before the component and his children have been initialized.
*/
_willLoad() {
assert(this._state === STATE_INITIALIZED, 'view state must be INITIALIZED');
this._lifecycle('WillLoad');
}
/**
* @hidden
* 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.
*/
_didLoad() {
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
this._lifecycle('DidLoad');
}
/**
* @hidden
* The view is about to enter and become the active view.
*/
_willEnter() {
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
if (this._detached && this._cmp) {
// ensure this has been re-attached to the change detector
this._cmp.changeDetectorRef.reattach();
this._detached = false;
}
this.willEnter.emit(null);
this._lifecycle('WillEnter');
}
/**
* @hidden
* 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.
*/
_didEnter() {
assert(this._state === STATE_ATTACHED, 'view state must be ATTACHED');
this._nb && this._nb.didEnter();
this.didEnter.emit(null);
this._lifecycle('DidEnter');
}
/**
* @hidden
* The view is about to leave and no longer be the active view.
*/
_willLeave(willUnload: boolean) {
this.willLeave.emit(null);
this._lifecycle('WillLeave');
if (willUnload && this._onWillDismiss) {
this._onWillDismiss(this._dismissData, this._dismissRole);
this._onWillDismiss = null;
}
}
/**
* @hidden
* The view has finished leaving and is no longer the active view. This
* will fire, whether it is cached or unloaded.
*/
_didLeave() {
this.didLeave.emit(null);
this._lifecycle('DidLeave');
// when this is not the active page
// we no longer need to detect changes
if (!this._detached && this._cmp) {
this._cmp.changeDetectorRef.detach();
this._detached = true;
}
}
/**
* @hidden
*/
_willUnload() {
this.willUnload.emit(null);
this._lifecycle('WillUnload');
this._onDidDismiss && this._onDidDismiss(this._dismissData, this._dismissRole);
this._onDidDismiss = null;
this._dismissData = null;
this._dismissRole = null;
}
/**
* @hidden
* DOM WRITE
*/
_destroy(renderer: Renderer) {
assert(this._state !== STATE_DESTROYED, 'view state must be ATTACHED');
if (this._cmp) {
if (renderer) {
// ensure the element is cleaned up for when the view pool reuses this element
// ******** DOM WRITE ****************
var cmpEle = this._cmp.location.nativeElement;
renderer.setElementAttribute(cmpEle, 'class', null);
renderer.setElementAttribute(cmpEle, 'style', null);
}
// completely destroy this component. boom.
this._cmp.destroy();
}
this._nav = this._cmp = this.instance = this._cntDir = this._cntRef = this._leavingOpts = this._hdrDir = this._ftrDir = this._nb = this._onDidDismiss = this._onWillDismiss = null;
this._state = STATE_DESTROYED;
}
/**
* @hidden
*/
_lifecycleTest(lifecycle: string): Promise<boolean> {
const instance = this.instance;
const methodName = 'ionViewCan' + lifecycle;
if (instance && instance[methodName]) {
try {
var result = instance[methodName]();
if (result instanceof Promise) {
return result;
} else {
// Any value but explitic false, should be true
return Promise.resolve(result !== false);
}
} catch (e) {
return Promise.reject(`${this.name} ${methodName} error: ${e.message}`);
}
}
return Promise.resolve(true);
}
/**
* @hidden
*/
_lifecycle(lifecycle: string) {
const instance = this.instance;
const methodName = 'ionView' + lifecycle;
if (instance && instance[methodName]) {
instance[methodName]();
}
}
}
export function isViewController(viewCtrl: any): boolean {
return !!(viewCtrl && (<ViewController>viewCtrl)._didLoad && (<ViewController>viewCtrl)._willUnload);
}
const DEFAULT_CSS_CLASS = 'ion-page';