mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
chore(): begin adding ionic components to mono-repo.
This commit is contained in:
498
packages/ionic-angular/src/navigation/deep-linker.ts
Normal file
498
packages/ionic-angular/src/navigation/deep-linker.ts
Normal 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;
|
||||
}
|
270
packages/ionic-angular/src/navigation/ionic-page.ts
Normal file
270
packages/ionic-angular/src/navigation/ionic-page.ts
Normal 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;
|
||||
};
|
||||
}
|
1144
packages/ionic-angular/src/navigation/nav-controller-base.ts
Normal file
1144
packages/ionic-angular/src/navigation/nav-controller-base.ts
Normal file
File diff suppressed because it is too large
Load Diff
622
packages/ionic-angular/src/navigation/nav-controller.ts
Normal file
622
packages/ionic-angular/src/navigation/nav-controller.ts
Normal 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<void> | 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<void> | 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;
|
||||
}
|
34
packages/ionic-angular/src/navigation/nav-interfaces.ts
Normal file
34
packages/ionic-angular/src/navigation/nav-interfaces.ts
Normal 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;
|
||||
}
|
49
packages/ionic-angular/src/navigation/nav-params.ts
Normal file
49
packages/ionic-angular/src/navigation/nav-params.ts
Normal 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];
|
||||
}
|
||||
}
|
219
packages/ionic-angular/src/navigation/nav-util.ts
Normal file
219
packages/ionic-angular/src/navigation/nav-util.ts
Normal 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';
|
77
packages/ionic-angular/src/navigation/overlay-proxy.ts
Normal file
77
packages/ionic-angular/src/navigation/overlay-proxy.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
8
packages/ionic-angular/src/navigation/overlay.ts
Normal file
8
packages/ionic-angular/src/navigation/overlay.ts
Normal 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;
|
||||
}
|
67
packages/ionic-angular/src/navigation/swipe-back.ts
Normal file
67
packages/ionic-angular/src/navigation/swipe-back.ts
Normal 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);
|
||||
}
|
||||
}
|
526
packages/ionic-angular/src/navigation/test/deep-linker.spec.ts
Normal file
526
packages/ionic-angular/src/navigation/test/deep-linker.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
1138
packages/ionic-angular/src/navigation/test/nav-controller.spec.ts
Normal file
1138
packages/ionic-angular/src/navigation/test/nav-controller.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
184
packages/ionic-angular/src/navigation/test/nav-util.spec.ts
Normal file
184
packages/ionic-angular/src/navigation/test/nav-util.spec.ts
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
123
packages/ionic-angular/src/navigation/test/overlay-proxy.spec.ts
Normal file
123
packages/ionic-angular/src/navigation/test/overlay-proxy.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
@ -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;
|
||||
|
||||
});
|
327
packages/ionic-angular/src/navigation/url-serializer.ts
Normal file
327
packages/ionic-angular/src/navigation/url-serializer.ts
Normal 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);
|
||||
}
|
582
packages/ionic-angular/src/navigation/view-controller.ts
Normal file
582
packages/ionic-angular/src/navigation/view-controller.ts
Normal 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';
|
Reference in New Issue
Block a user