mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 12:29:55 +08:00
feat(nav): component url navigation
This commit is contained in:
451
src/navigation/deep-linker.ts
Normal file
451
src/navigation/deep-linker.ts
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
import { Injectable, OpaqueToken } from '@angular/core';
|
||||||
|
import { Location, LocationStrategy, HashLocationStrategy } from '@angular/common';
|
||||||
|
|
||||||
|
import { App } from '../components/app/app';
|
||||||
|
import { convertToViews, isNav, isTab, isTabs, NavSegment, DIRECTION_BACK } from './nav-util';
|
||||||
|
import { isArray, isPresent } from '../util/util';
|
||||||
|
import { Nav } from '../components/nav/nav';
|
||||||
|
import { NavController } from './nav-controller';
|
||||||
|
import { Tab } from '../components/tabs/tab';
|
||||||
|
import { Tabs } from '../components/tabs/tabs';
|
||||||
|
import { UrlSerializer } from './url-serializer';
|
||||||
|
import { ViewController } from './view-controller';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deep Linking Scenarios:
|
||||||
|
* 1) Initialize all NavControllers using the initial browser URL
|
||||||
|
* 2) User clicks browser back button
|
||||||
|
* 3) User clicks browser forward button
|
||||||
|
* 4) User changes browser URL
|
||||||
|
* 5) User clicks link href
|
||||||
|
* 6) App uses NavController push/pop/setRoot/insert/remove
|
||||||
|
*
|
||||||
|
* Terms:
|
||||||
|
* - URL: The string value found in the browser's URL bar
|
||||||
|
* - Segment: Deep linker's data about each section between / in the URL
|
||||||
|
* - Path: Deep linker's array of segments
|
||||||
|
* - History: Deep linker's string array of internal URL history
|
||||||
|
* - Location: Angular's Location provider, which abstracts Hash/Path Location Strategies
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeepLinker {
|
||||||
|
segments: NavSegment[] = [];
|
||||||
|
history: string[] = [];
|
||||||
|
indexAliasUrl: string;
|
||||||
|
|
||||||
|
constructor(public app: App, public serializer: UrlSerializer, public location: Location) { }
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
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
|
||||||
|
appRootNav.goToRoot({
|
||||||
|
updateUrl: false,
|
||||||
|
isNavRoot: true
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal url
|
||||||
|
this.segments = this.serializer.parse(browserUrl);
|
||||||
|
this.loadNavFromPath(appRootNav);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the deep linker using the NavController's current active view.
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: any): any {
|
||||||
|
const segment = this.serializer.createSegmentFromName(componentName);
|
||||||
|
if (segment && segment.component) {
|
||||||
|
return segment.component;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
initViews(segment: NavSegment): ViewController[] {
|
||||||
|
let views: ViewController[];
|
||||||
|
|
||||||
|
if (isArray(segment.defaultHistory)) {
|
||||||
|
views = convertToViews(this, segment.defaultHistory);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
views = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new ViewController(segment.component, segment.data);
|
||||||
|
view.id = segment.id;
|
||||||
|
|
||||||
|
views.push(view);
|
||||||
|
|
||||||
|
return views;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
loadNavFromPath(nav: NavController, done?: Function) {
|
||||||
|
if (!nav) {
|
||||||
|
done && done();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.loadViewFromSegment(nav, () => {
|
||||||
|
this.loadNavFromPath(nav.getActiveChildNav(), done);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
isBackUrl(browserUrl: string) {
|
||||||
|
return (browserUrl === this.history[this.history.length - 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurrentUrl(browserUrl: string) {
|
||||||
|
return (browserUrl === this.history[this.history.length - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
historyPush(browserUrl: string) {
|
||||||
|
if (!this.isCurrentUrl(browserUrl)) {
|
||||||
|
this.history.push(browserUrl);
|
||||||
|
if (this.history.length > 30) {
|
||||||
|
this.history.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
historyPop() {
|
||||||
|
this.history.pop();
|
||||||
|
if (!this.history.length) {
|
||||||
|
this.historyPush(this.location.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function setupDeepLinker(app: App, serializer: UrlSerializer, location: Location) {
|
||||||
|
const deepLinker = new DeepLinker(app, serializer, location);
|
||||||
|
deepLinker.init();
|
||||||
|
return deepLinker;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const UserDeepLinkConfig = new OpaqueToken('USERLINKS');
|
||||||
|
|
||||||
|
|
||||||
|
export function provideDeepLinker(userDeepLinkConfig: any): any[] {
|
||||||
|
return [
|
||||||
|
UrlSerializer,
|
||||||
|
Location,
|
||||||
|
{ provide: UserDeepLinkConfig, useValue: userDeepLinkConfig },
|
||||||
|
{ provide: LocationStrategy, useClass: HashLocationStrategy },
|
||||||
|
{
|
||||||
|
provide: DeepLinker,
|
||||||
|
useFactory: setupDeepLinker,
|
||||||
|
deps: [
|
||||||
|
App,
|
||||||
|
UrlSerializer,
|
||||||
|
Location
|
||||||
|
]
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
517
src/navigation/test/deep-linker.spec.ts
Normal file
517
src/navigation/test/deep-linker.spec.ts
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
import { DeepLinker, normalizeUrl } from '../deep-linker';
|
||||||
|
import { UrlSerializer } from '../url-serializer';
|
||||||
|
import { mockApp, mockDeepLinkConfig, mockNavController, mockLocation,
|
||||||
|
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 create the ViewController for just the segment', () => {
|
||||||
|
// let segment = serializer.parse('/viewone')[0];
|
||||||
|
|
||||||
|
// let views = linker.initViews(segment);
|
||||||
|
// expect(views[0].component).toEqual(segment.component);
|
||||||
|
// expect(views[0].id).toEqual('VIEWID');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
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 component class name as a string', () => {
|
||||||
|
let segment = serializer.createSegmentFromName('MockView1');
|
||||||
|
expect(segment.component).toEqual(MockView1);
|
||||||
|
});
|
||||||
|
|
||||||
|
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(() => {
|
||||||
|
serializer = new UrlSerializer(mockDeepLinkConfig());
|
||||||
|
linker = new DeepLinker(mockApp(), serializer, mockLocation());
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
628
src/navigation/test/url-serializer.spec.ts
Normal file
628
src/navigation/test/url-serializer.spec.ts
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
import { NavLink, NavSegment } from '../nav-util';
|
||||||
|
import { UrlSerializer, isPartMatch, fillMatchedUrlParts, parseUrlParts, createMatchedData, normalizeLinks } from '../url-serializer';
|
||||||
|
import { mockDeepLinkConfig, noop, MockView1, MockView2, MockView3, MockView4, MockView5 } from '../../util/mock-providers';
|
||||||
|
|
||||||
|
|
||||||
|
describe('UrlSerializer', () => {
|
||||||
|
|
||||||
|
describe('serializeComponent', () => {
|
||||||
|
|
||||||
|
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('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);
|
||||||
|
}
|
267
src/navigation/url-serializer.ts
Normal file
267
src/navigation/url-serializer.ts
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { DeepLinkConfig, NavLink, NavSegment } from './nav-util';
|
||||||
|
import { isArray, isBlank, isPresent, pascalCaseToDashCase } from '../util/util';
|
||||||
|
import { UserDeepLinkConfig } from './deep-linker';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UrlSerializer {
|
||||||
|
links: NavLink[];
|
||||||
|
|
||||||
|
constructor(@Inject(UserDeepLinkConfig) 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.links.find((link: NavLink) => {
|
||||||
|
return (link.component === nameOrComponent) ||
|
||||||
|
(link.name === nameOrComponent) ||
|
||||||
|
(link.component.name === nameOrComponent);
|
||||||
|
});
|
||||||
|
|
||||||
|
return configLink ? {
|
||||||
|
id: configLink.name,
|
||||||
|
name: configLink.name,
|
||||||
|
component: configLink.component,
|
||||||
|
data: null
|
||||||
|
} : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = this.links.find(l => component === l.component || component.name === l.name);
|
||||||
|
if (link) {
|
||||||
|
return this.createSegment(link, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
data: data,
|
||||||
|
defaultHistory: configLink.defaultHistory
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
formatUrlPart(name: string): string {
|
||||||
|
name = pascalCaseToDashCase(name.replace(URL_REPLACE_REG, '-'));
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
data: createMatchedData(matchedUrlParts, configLink)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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.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;
|
||||||
|
|
||||||
|
} 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;
|
Reference in New Issue
Block a user