mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-16 01:52:19 +08:00
feat(ion-router): adds ion-router
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,6 +19,7 @@ node_modules/
|
|||||||
tmp/
|
tmp/
|
||||||
temp/
|
temp/
|
||||||
packages/core/theme-builder/
|
packages/core/theme-builder/
|
||||||
|
packages/core/test-components/
|
||||||
$RECYCLE.BIN/
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
6
packages/core/package-lock.json
generated
6
packages/core/package-lock.json
generated
@ -14,9 +14,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@stencil/core": {
|
"@stencil/core": {
|
||||||
"version": "0.4.0-0",
|
"version": "0.4.0-1",
|
||||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-0.4.0-0.tgz",
|
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-0.4.0-1.tgz",
|
||||||
"integrity": "sha512-s5/dfezhD2PZoeHevxZRe7GNLmOL+vf/MRtngZCUOjFrdswwCAEnURnhTTQMh+CQi6r6cdKIgdEzzMI9WGI/rA==",
|
"integrity": "sha512-VgjzNx28O27Hd32LEf5dirJJnOUyq+MZdLpIWuaTZtAkj6EWGgIqw0ZOs0j0XzopIOJ25BqBk6nVS1xLaaqrzg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"chokidar": "2.0.0",
|
"chokidar": "2.0.0",
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
"snapshot": "node ./scripts/e2e --snapshot",
|
"snapshot": "node ./scripts/e2e --snapshot",
|
||||||
"test": "jest --no-cache",
|
"test": "jest --no-cache",
|
||||||
"test.watch": "jest --watch --no-cache",
|
"test.watch": "jest --watch --no-cache",
|
||||||
|
"build-test-cmp": "stencil build --dev --config scripts/test-components/stencil.config.js",
|
||||||
"theme-app-build": "stencil build --dev --config scripts/theme-builder/stencil.config.js",
|
"theme-app-build": "stencil build --dev --config scripts/theme-builder/stencil.config.js",
|
||||||
"theme-builder": "npm run theme-app-build && sd concurrent \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
|
"theme-builder": "npm run theme-app-build && sd concurrent \"stencil build --dev --watch\" \"stencil-dev-server\" \"npm run theme-server\" ",
|
||||||
"theme-server": "node scripts/theme-builder/server.js",
|
"theme-server": "node scripts/theme-builder/server.js",
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Component } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'page-one',
|
||||||
|
})
|
||||||
|
export class PageOne {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-page>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Page One</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
page one
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Component } from '@stencil/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'page-two',
|
||||||
|
})
|
||||||
|
export class PageTwo {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return [
|
||||||
|
<ion-page>
|
||||||
|
<ion-header>
|
||||||
|
<ion-toolbar>
|
||||||
|
<ion-title>Page Two</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content>
|
||||||
|
this is page two
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
5
packages/core/scripts/test-components/stencil.config.js
Normal file
5
packages/core/scripts/test-components/stencil.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
exports.config = {
|
||||||
|
generateWWW: true,
|
||||||
|
wwwDir: '../../test-components',
|
||||||
|
serviceWorker: false
|
||||||
|
};
|
22
packages/core/scripts/test-components/tsconfig.json
Normal file
22
packages/core/scripts/test-components/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"allowUnreachableCode": false,
|
||||||
|
"declaration": false,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"module": "es2015",
|
||||||
|
"target": "es2015",
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"jsxFactory": "h"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
24
packages/core/src/components.d.ts
vendored
24
packages/core/src/components.d.ts
vendored
@ -1834,6 +1834,7 @@ declare global {
|
|||||||
lazy?: boolean;
|
lazy?: boolean;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
root?: any;
|
root?: any;
|
||||||
|
swipeBackEnabled?: boolean;
|
||||||
useUrls?: boolean;
|
useUrls?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2442,30 +2443,31 @@ declare global {
|
|||||||
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RouterController as IonRouterController
|
Router as IonRouter
|
||||||
} from './components/router-controller/router-controller';
|
} from './components/router/router';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLIonRouterControllerElement extends IonRouterController, HTMLStencilElement {
|
interface HTMLIonRouterElement extends IonRouter, HTMLStencilElement {
|
||||||
}
|
}
|
||||||
var HTMLIonRouterControllerElement: {
|
var HTMLIonRouterElement: {
|
||||||
prototype: HTMLIonRouterControllerElement;
|
prototype: HTMLIonRouterElement;
|
||||||
new (): HTMLIonRouterControllerElement;
|
new (): HTMLIonRouterElement;
|
||||||
};
|
};
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
"ion-router-controller": HTMLIonRouterControllerElement;
|
"ion-router": HTMLIonRouterElement;
|
||||||
}
|
}
|
||||||
interface ElementTagNameMap {
|
interface ElementTagNameMap {
|
||||||
"ion-router-controller": HTMLIonRouterControllerElement;
|
"ion-router": HTMLIonRouterElement;
|
||||||
}
|
}
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
"ion-router-controller": JSXElements.IonRouterControllerAttributes;
|
"ion-router": JSXElements.IonRouterAttributes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
namespace JSXElements {
|
namespace JSXElements {
|
||||||
export interface IonRouterControllerAttributes extends HTMLAttributes {
|
export interface IonRouterAttributes extends HTMLAttributes {
|
||||||
|
base?: string;
|
||||||
|
useHash?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
3430
packages/core/src/components.d.ts.orig
Normal file
3430
packages/core/src/components.d.ts.orig
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,13 @@
|
|||||||
import { Transition } from './nav-interfaces';
|
import { Transition } from './nav-interfaces';
|
||||||
import { Animation, AnimationOptions, Config, Nav, RouterEntry, TransitionBuilder, ViewController } from '../../index';
|
import { Animation, AnimationOptions, Config, Nav, TransitionBuilder, ViewController } from '../../index';
|
||||||
import { isDef } from '../../utils/helpers';
|
import { isDef } from '../../utils/helpers';
|
||||||
|
|
||||||
|
export enum State {
|
||||||
|
New,
|
||||||
|
INITIALIZED,
|
||||||
|
ATTACHED,
|
||||||
|
DESTROYED,
|
||||||
|
}
|
||||||
export const STATE_NEW = 1;
|
export const STATE_NEW = 1;
|
||||||
export const STATE_INITIALIZED = 2;
|
export const STATE_INITIALIZED = 2;
|
||||||
export const STATE_ATTACHED = 3;
|
export const STATE_ATTACHED = 3;
|
||||||
@ -189,8 +195,4 @@ export function getNextNavId() {
|
|||||||
return navControllerIds++;
|
return navControllerIds++;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveRoute(nav: Nav, component: string): RouterEntry {
|
|
||||||
return nav.routes.find(r => r.id === component);
|
|
||||||
}
|
|
||||||
|
|
||||||
let navControllerIds = NAV_ID_START;
|
let navControllerIds = NAV_ID_START;
|
||||||
|
@ -7,11 +7,10 @@ import {
|
|||||||
Config,
|
Config,
|
||||||
FrameworkDelegate,
|
FrameworkDelegate,
|
||||||
NavOptions,
|
NavOptions,
|
||||||
|
NavOutlet,
|
||||||
NavResult,
|
NavResult,
|
||||||
NavState,
|
|
||||||
PublicNav,
|
PublicNav,
|
||||||
PublicViewController,
|
PublicViewController,
|
||||||
RouterEntries,
|
|
||||||
Transition,
|
Transition,
|
||||||
TransitionInstruction,
|
TransitionInstruction,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
@ -35,7 +34,6 @@ import {
|
|||||||
getPreviousImpl,
|
getPreviousImpl,
|
||||||
getViews,
|
getViews,
|
||||||
isViewController,
|
isViewController,
|
||||||
resolveRoute,
|
|
||||||
setZIndex,
|
setZIndex,
|
||||||
toggleHidden,
|
toggleHidden,
|
||||||
transitionFactory
|
transitionFactory
|
||||||
@ -63,16 +61,14 @@ const urlMap = new Map<string, TransitionInstruction>();
|
|||||||
tag: 'ion-nav',
|
tag: 'ion-nav',
|
||||||
styleUrl: 'nav.scss'
|
styleUrl: 'nav.scss'
|
||||||
})
|
})
|
||||||
export class Nav implements PublicNav {
|
export class Nav implements PublicNav, NavOutlet {
|
||||||
|
|
||||||
@Element() element: HTMLElement;
|
@Element() element: HTMLElement;
|
||||||
@Event() navInit: EventEmitter<NavEventDetail>;
|
@Event() navInit: EventEmitter<NavEventDetail>;
|
||||||
@Event() ionNavChanged: EventEmitter<NavEventDetail>;
|
@Event() ionNavChanged: EventEmitter<NavEventDetail>;
|
||||||
|
|
||||||
useRouter: boolean;
|
|
||||||
navId: number;
|
navId: number;
|
||||||
init = false;
|
init = false;
|
||||||
routes: RouterEntries = [];
|
|
||||||
parent: Nav;
|
parent: Nav;
|
||||||
views: ViewController[] = [];
|
views: ViewController[] = [];
|
||||||
transitioning?: boolean;
|
transitioning?: boolean;
|
||||||
@ -96,17 +92,12 @@ export class Nav implements PublicNav {
|
|||||||
this.navId = getNextNavId();
|
this.navId = getNextNavId();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillLoad() {
|
|
||||||
this.routes = Array.from(this.element.querySelectorAll('ion-route'))
|
|
||||||
.map(child => child.getRoute());
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidLoad() {
|
componentDidLoad() {
|
||||||
if (this.init) {
|
if (this.init) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.init = true;
|
this.init = true;
|
||||||
if (!this.useRouter || !this.lazy) {
|
if (!this.lazy) {
|
||||||
componentDidLoadImpl(this);
|
componentDidLoadImpl(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -198,15 +189,8 @@ export class Nav implements PublicNav {
|
|||||||
return getLastView(this);
|
return getLastView(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Method()
|
|
||||||
getState(): NavState {
|
|
||||||
assert(this.useRouter, 'routing is disabled');
|
|
||||||
return getState(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
setRouteId(id: string, _: any = {}): Promise<void> {
|
setRouteId(id: string, _: any = {}): Promise<void> {
|
||||||
assert(this.useRouter, 'routing is disabled');
|
|
||||||
const active = this.getActive();
|
const active = this.getActive();
|
||||||
if (active && active.component === id) {
|
if (active && active.component === id) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -215,9 +199,21 @@ export class Nav implements PublicNav {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
getRoutes(): RouterEntries {
|
getRouteId(): string | null {
|
||||||
assert(this.useRouter, 'routing is disabled');
|
const element = this.getContentElement();
|
||||||
return this.routes;
|
if (element) {
|
||||||
|
return element.tagName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Method()
|
||||||
|
getContentElement(): HTMLElement {
|
||||||
|
const active = getActiveImpl(this);
|
||||||
|
if (active) {
|
||||||
|
active.element;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
@ -333,23 +329,6 @@ export class Nav implements PublicNav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getState(nav: Nav): NavState {
|
|
||||||
const active = getActiveImpl(nav);
|
|
||||||
if (!active) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const component = active.component;
|
|
||||||
const route = resolveRoute(nav, component);
|
|
||||||
if (!route) {
|
|
||||||
console.error('cant reverse route by component', component);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
path: route.path,
|
|
||||||
focusNode: active.element
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function componentDidLoadImpl(nav: Nav) {
|
export function componentDidLoadImpl(nav: Nav) {
|
||||||
nav.navInit.emit();
|
nav.navInit.emit();
|
||||||
if (nav.root) {
|
if (nav.root) {
|
||||||
@ -1145,7 +1124,7 @@ export function convertViewsToViewControllers(pairs: ComponentDataPair[]): ViewC
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertComponentToViewController(nav: Nav, ti: TransitionInstruction): ViewController[] {
|
export function convertComponentToViewController(_: Nav, ti: TransitionInstruction): ViewController[] {
|
||||||
if (ti.insertViews) {
|
if (ti.insertViews) {
|
||||||
assert(ti.insertViews.length > 0, 'length can not be zero');
|
assert(ti.insertViews.length > 0, 'length can not be zero');
|
||||||
const viewControllers = convertViewsToViewControllers(ti.insertViews);
|
const viewControllers = convertViewsToViewControllers(ti.insertViews);
|
||||||
@ -1161,9 +1140,6 @@ export function convertComponentToViewController(nav: Nav, ti: TransitionInstruc
|
|||||||
if (viewController.state === STATE_DESTROYED) {
|
if (viewController.state === STATE_DESTROYED) {
|
||||||
throw new Error('The view has already been destroyed');
|
throw new Error('The view has already been destroyed');
|
||||||
}
|
}
|
||||||
if (nav.useRouter && !resolveRoute(nav, viewController.component)) {
|
|
||||||
throw new Error('Route not specified for ' + viewController.component);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return viewControllers;
|
return viewControllers;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,11 @@ string
|
|||||||
any
|
any
|
||||||
|
|
||||||
|
|
||||||
|
#### swipeBackEnabled
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
#### useUrls
|
#### useUrls
|
||||||
|
|
||||||
boolean
|
boolean
|
||||||
@ -54,6 +59,11 @@ string
|
|||||||
any
|
any
|
||||||
|
|
||||||
|
|
||||||
|
#### swipe-back-enabled
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
#### use-urls
|
#### use-urls
|
||||||
|
|
||||||
boolean
|
boolean
|
||||||
@ -72,9 +82,6 @@ boolean
|
|||||||
#### canGoBack()
|
#### canGoBack()
|
||||||
|
|
||||||
|
|
||||||
#### canSwipeBack()
|
|
||||||
|
|
||||||
|
|
||||||
#### clearTransitionInfoForUrl()
|
#### clearTransitionInfoForUrl()
|
||||||
|
|
||||||
|
|
||||||
@ -87,16 +94,16 @@ boolean
|
|||||||
#### getChildNavs()
|
#### getChildNavs()
|
||||||
|
|
||||||
|
|
||||||
|
#### getContentElement()
|
||||||
|
|
||||||
|
|
||||||
#### getId()
|
#### getId()
|
||||||
|
|
||||||
|
|
||||||
#### getPrevious()
|
#### getPrevious()
|
||||||
|
|
||||||
|
|
||||||
#### getRoutes()
|
#### getRouteId()
|
||||||
|
|
||||||
|
|
||||||
#### getState()
|
|
||||||
|
|
||||||
|
|
||||||
#### getTransitionInfoForUrl()
|
#### getTransitionInfoForUrl()
|
||||||
|
52
packages/core/src/components/nav/test/routing/e2e.js
Normal file
52
packages/core/src/components/nav/test/routing/e2e.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// 'use strict';
|
||||||
|
|
||||||
|
// const { register, Page, platforms } = require('../../../../../scripts/e2e');
|
||||||
|
// const { getElement, waitForTransition } = require('../../../../../scripts/e2e/utils');
|
||||||
|
|
||||||
|
// class E2ETestPage extends Page {
|
||||||
|
// constructor(driver, platform) {
|
||||||
|
// super(driver, `http://localhost:3333/src/components/nav/test/basic?ionicplatform=${platform}`);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// platforms.forEach(platform => {
|
||||||
|
// describe('nav/basic', () => {
|
||||||
|
|
||||||
|
// register('should init', driver => {
|
||||||
|
// const page = new E2ETestPage(driver, platform);
|
||||||
|
// return page.navigate();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// register('should go to page-one, page-two, page-three, then back to page-two, page-one', async (driver, testContext) => {
|
||||||
|
|
||||||
|
// testContext.timeout(10000);
|
||||||
|
// const page = new E2ETestPage(driver, platform);
|
||||||
|
|
||||||
|
// // go to page two
|
||||||
|
// const pageOneNextButtonSelector = '.first-page ion-button.next.hydrated';
|
||||||
|
// const pageOneNextButton = await getElement(driver, pageOneNextButtonSelector);
|
||||||
|
// pageOneNextButton.click();
|
||||||
|
// await waitForTransition(600);
|
||||||
|
|
||||||
|
// // go to page three
|
||||||
|
// const pageTwoNextButtonSelector = '.second-page ion-button.next.hydrated';
|
||||||
|
// const pageTwoNextButton = await getElement(driver, pageTwoNextButtonSelector);
|
||||||
|
// pageTwoNextButton.click();
|
||||||
|
// await waitForTransition(600);
|
||||||
|
|
||||||
|
// // go back to page two
|
||||||
|
// const pageThreeBackButtonSelector = '.third-page ion-button.previous.hydrated';
|
||||||
|
// const pageThreeBackButton = await getElement(driver, pageThreeBackButtonSelector);
|
||||||
|
// pageThreeBackButton.click();
|
||||||
|
// await waitForTransition(600);
|
||||||
|
|
||||||
|
// // go back to page two
|
||||||
|
// const pageTwoBackButtonSelector = '.second-page ion-button.previous.hydrated';
|
||||||
|
// const pageTwoBackButton = await getElement(driver, pageTwoBackButtonSelector);
|
||||||
|
// pageTwoBackButton.click();
|
||||||
|
// await waitForTransition(600);
|
||||||
|
|
||||||
|
// // we're back on page one now
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
46
packages/core/src/components/nav/test/routing/index.html
Normal file
46
packages/core/src/components/nav/test/routing/index.html
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Nav</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||||
|
<script src="/dist/ionic.js"></script>
|
||||||
|
<script src="/test-components/build/app.js"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<ion-app>
|
||||||
|
|
||||||
|
<ion-router>
|
||||||
|
<ion-route path="/" component="page-one">
|
||||||
|
<ion-route path="/nested" component="page-nested-one"></ion-route>
|
||||||
|
<ion-route path="/nestes2" component="page-nested-two"></ion-route>
|
||||||
|
</ion-route>
|
||||||
|
|
||||||
|
<ion-route path="/two" component="page-two"></ion-route>
|
||||||
|
</ion-router>
|
||||||
|
|
||||||
|
<ion-nav></ion-nav>
|
||||||
|
|
||||||
|
</ion-app>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
f {
|
||||||
|
display: block;
|
||||||
|
margin: 15px auto;
|
||||||
|
max-width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
background: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
f:last-of-type {
|
||||||
|
background: yellow;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
@ -39,11 +39,6 @@ string
|
|||||||
any
|
any
|
||||||
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
#### getRoute()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Component, Method, Prop } from '@stencil/core';
|
import { Component, Prop } from '@stencil/core';
|
||||||
import { RouterEntry, parseURL } from '../router-controller/router-utils';
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -11,13 +10,4 @@ export class Route {
|
|||||||
@Prop() component: string;
|
@Prop() component: string;
|
||||||
@Prop() props: any = {};
|
@Prop() props: any = {};
|
||||||
|
|
||||||
@Method()
|
|
||||||
getRoute(): RouterEntry {
|
|
||||||
return {
|
|
||||||
path: this.path,
|
|
||||||
segments: parseURL(this.path),
|
|
||||||
id: this.component,
|
|
||||||
props: this.props
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,105 +0,0 @@
|
|||||||
import { Component, Listen, Prop } from '@stencil/core';
|
|
||||||
import { RouterSegments, generateURL, parseURL, readNavState, writeNavState } from './router-utils';
|
|
||||||
import { Config, DomController } from '../../index';
|
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
tag: 'ion-router-controller'
|
|
||||||
})
|
|
||||||
export class RouterController {
|
|
||||||
|
|
||||||
private busy = false;
|
|
||||||
private enabled = false;
|
|
||||||
private basePrefix = '#';
|
|
||||||
|
|
||||||
@Prop({ context: 'config' }) config: Config;
|
|
||||||
@Prop({ context: 'dom' }) dom: DomController;
|
|
||||||
|
|
||||||
componentDidLoad() {
|
|
||||||
const enabled = this.enabled = this.config.getBoolean('useRouter', false);
|
|
||||||
if (enabled) {
|
|
||||||
const base = document.querySelector('head > base');
|
|
||||||
if (base) {
|
|
||||||
const baseURL = base.getAttribute('href');
|
|
||||||
if (baseURL && baseURL.length > 0) {
|
|
||||||
this.basePrefix = baseURL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dom.raf(() => {
|
|
||||||
console.debug('[OUT] page load -> write nav state');
|
|
||||||
this.writeNavStateRoot();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Listen('window:hashchange')
|
|
||||||
protected onURLHashChanged() {
|
|
||||||
if (!this.isBlocked()) {
|
|
||||||
console.debug('[OUT] hash changed -> write nav state');
|
|
||||||
this.writeNavStateRoot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Listen('body:ionNavChanged')
|
|
||||||
protected onNavChanged(ev: CustomEvent) {
|
|
||||||
if (this.isBlocked()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug('[IN] nav changed -> update URL');
|
|
||||||
const { stack, pivot } = this.readNavState();
|
|
||||||
if (pivot) {
|
|
||||||
// readNavState() found a pivot that is not initialized
|
|
||||||
console.debug('[IN] pivot uninitialized -> write partial nav state');
|
|
||||||
this.writeNavState(pivot, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPop = ev.detail.isPop === true;
|
|
||||||
this.setURL(generateURL(stack), isPop);
|
|
||||||
}
|
|
||||||
|
|
||||||
private setURL(url: string, isPop: boolean) {
|
|
||||||
url = this.basePrefix + url;
|
|
||||||
const history = window.history;
|
|
||||||
if (isPop) {
|
|
||||||
history.back();
|
|
||||||
history.replaceState(null, null, url);
|
|
||||||
} else {
|
|
||||||
history.pushState(null, null, url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private isBlocked(): boolean {
|
|
||||||
return this.busy || !this.enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
private writeNavStateRoot(): Promise<any> {
|
|
||||||
const node = document.querySelector('ion-app') as HTMLElement;
|
|
||||||
return this.writeNavState(node, this.readURL());
|
|
||||||
}
|
|
||||||
|
|
||||||
private writeNavState(node: any, url: string[]): Promise<any> {
|
|
||||||
const segments = new RouterSegments(url);
|
|
||||||
this.busy = true; // prevents reentrance
|
|
||||||
return writeNavState(node, segments)
|
|
||||||
.catch(err => console.error(err))
|
|
||||||
.then(() => this.busy = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readNavState() {
|
|
||||||
const root = document.querySelector('ion-app') as HTMLElement;
|
|
||||||
return readNavState(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isHash() {
|
|
||||||
return this.basePrefix.length > 0 && this.basePrefix[0] === '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
private readURL(): string[] {
|
|
||||||
const url = this.isHash()
|
|
||||||
? window.location.hash.substr(1)
|
|
||||||
: window.location.pathname;
|
|
||||||
return parseURL(url);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,183 +0,0 @@
|
|||||||
|
|
||||||
export interface NavElement extends HTMLElement {
|
|
||||||
setRouteId(id: any, data?: any): Promise<void>;
|
|
||||||
getRoutes(): RouterEntries;
|
|
||||||
getState(): NavState;
|
|
||||||
|
|
||||||
componentOnReady(): Promise<HTMLElement>;
|
|
||||||
componentOnReady(done: (cmp?: HTMLElement) => void): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouterEntry {
|
|
||||||
path: string;
|
|
||||||
id: any;
|
|
||||||
segments?: string[];
|
|
||||||
props?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RouterEntries = RouterEntry[];
|
|
||||||
|
|
||||||
export interface NavState {
|
|
||||||
path: string;
|
|
||||||
focusNode: HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RouterSegments {
|
|
||||||
constructor(
|
|
||||||
private segments: string[]
|
|
||||||
) {}
|
|
||||||
|
|
||||||
next(): string {
|
|
||||||
if (this.segments.length > 0) {
|
|
||||||
return this.segments.shift() as string;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function writeNavState(root: HTMLElement, segments: RouterSegments): Promise<void> {
|
|
||||||
const node = breadthFirstSearch(root);
|
|
||||||
if (!node) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return node.componentOnReady()
|
|
||||||
.then(() => node.getRoutes())
|
|
||||||
.then(routes => mustMatchRoute(segments, routes))
|
|
||||||
.then(route => node.setRouteId(route.id))
|
|
||||||
.then(() => {
|
|
||||||
const state = node.getState();
|
|
||||||
if (!state) {
|
|
||||||
throw new Error('setRouteId failed?');
|
|
||||||
}
|
|
||||||
writeNavState(state.focusNode, segments);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readNavState(node: HTMLElement) {
|
|
||||||
const stack = [];
|
|
||||||
let pivot: NavElement | null = null;
|
|
||||||
let state: NavState;
|
|
||||||
while (true) {
|
|
||||||
pivot = breadthFirstSearch(node);
|
|
||||||
if (pivot) {
|
|
||||||
state = pivot.getState();
|
|
||||||
if (state) {
|
|
||||||
node = state.focusNode;
|
|
||||||
stack.push(state);
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
stack: stack,
|
|
||||||
pivot: pivot
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mustMatchRoute(segments: RouterSegments, routes: RouterEntries) {
|
|
||||||
const r = matchRoute(segments, routes);
|
|
||||||
if (!r) {
|
|
||||||
throw new Error('no route found');
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function matchRoute(segments: RouterSegments, routes: RouterEntries): RouterEntry | null {
|
|
||||||
if (!routes) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let index = 0;
|
|
||||||
routes = routes.map(initRoute);
|
|
||||||
let selectedRoute: RouterEntry|null = null;
|
|
||||||
let ambiguous = false;
|
|
||||||
let segment: string;
|
|
||||||
let l: number;
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
routes = routes.filter(r => r.segments.length > index);
|
|
||||||
if (routes.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
segment = segments.next();
|
|
||||||
routes = routes.filter(r => r.segments[index] === segment);
|
|
||||||
l = routes.length;
|
|
||||||
if (l === 0) {
|
|
||||||
selectedRoute = null;
|
|
||||||
ambiguous = false;
|
|
||||||
} else {
|
|
||||||
selectedRoute = routes[0];
|
|
||||||
ambiguous = l > 1;
|
|
||||||
}
|
|
||||||
index++;
|
|
||||||
}
|
|
||||||
if (ambiguous) {
|
|
||||||
throw new Error('ambiguious match');
|
|
||||||
}
|
|
||||||
return selectedRoute;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateURL(stack: NavState[]): string {
|
|
||||||
const segments: string[] = [];
|
|
||||||
for (const state of stack) {
|
|
||||||
segments.push(...parseURL(state.path));
|
|
||||||
}
|
|
||||||
const path = segments
|
|
||||||
.filter(s => s.length > 0)
|
|
||||||
.join('/');
|
|
||||||
|
|
||||||
return '/' + path;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initRoute(route: RouterEntry): RouterEntry {
|
|
||||||
if (route.segments === undefined || route.segments === null) {
|
|
||||||
route.segments = parseURL(route.path);
|
|
||||||
}
|
|
||||||
return route;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseURL(url: string): string[] {
|
|
||||||
if (url === null || url === undefined) {
|
|
||||||
return [''];
|
|
||||||
}
|
|
||||||
const segments = url.split('/')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(s => s.length > 0);
|
|
||||||
|
|
||||||
if (segments.length === 0) {
|
|
||||||
return [''];
|
|
||||||
} else {
|
|
||||||
return segments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const navs = ['ION-NAV', 'ION-TABS'];
|
|
||||||
export function breadthFirstSearch(root: HTMLElement): NavElement | null {
|
|
||||||
if (!root) {
|
|
||||||
console.error('search root is null');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// we do a Breadth-first search
|
|
||||||
// Breadth-first search (BFS) is an algorithm for traversing or searching tree
|
|
||||||
// or graph data structures.It starts at the tree root(or some arbitrary node of a graph,
|
|
||||||
// sometimes referred to as a 'search key'[1]) and explores the neighbor nodes
|
|
||||||
// first, before moving to the next level neighbours.
|
|
||||||
|
|
||||||
const queue = [root];
|
|
||||||
let node: HTMLElement | undefined;
|
|
||||||
while (node = queue.shift()) {
|
|
||||||
// visit node
|
|
||||||
if (navs.indexOf(node.tagName) >= 0) {
|
|
||||||
return node as NavElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
// queue children
|
|
||||||
const children = node.children;
|
|
||||||
for (let i = 0; i < children.length; i++) {
|
|
||||||
queue.push(children[i] as NavElement);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
@ -5,6 +5,30 @@
|
|||||||
<!-- Auto Generated Below -->
|
<!-- Auto Generated Below -->
|
||||||
|
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
#### base
|
||||||
|
|
||||||
|
string
|
||||||
|
|
||||||
|
|
||||||
|
#### useHash
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
|
## Attributes
|
||||||
|
|
||||||
|
#### base
|
||||||
|
|
||||||
|
string
|
||||||
|
|
||||||
|
|
||||||
|
#### use-hash
|
||||||
|
|
||||||
|
boolean
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
224
packages/core/src/components/router/router-utils.ts
Normal file
224
packages/core/src/components/router/router-utils.ts
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import { StencilElement } from '../../index';
|
||||||
|
|
||||||
|
export interface NavOutlet {
|
||||||
|
setRouteId(id: any, data?: any): Promise<void>;
|
||||||
|
getRouteId(): string;
|
||||||
|
getContentElement(): HTMLElement | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NavOutletElement = NavOutlet & StencilElement;
|
||||||
|
|
||||||
|
export interface RouterEntry {
|
||||||
|
id: any;
|
||||||
|
path: string[];
|
||||||
|
subroutes: RouterEntries;
|
||||||
|
props?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RouterEntries = RouterEntry[];
|
||||||
|
|
||||||
|
export class RouterSegments {
|
||||||
|
constructor(
|
||||||
|
private path: string[]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
next(): string {
|
||||||
|
if (this.path.length > 0) {
|
||||||
|
return this.path.shift() as string;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeNavState(root: HTMLElement, chain: RouterEntries, index = 0): Promise<void> {
|
||||||
|
if (index >= chain.length) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = chain[index];
|
||||||
|
const node = breadthFirstSearch(root);
|
||||||
|
if (!node) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return node.componentOnReady()
|
||||||
|
.then(() => node.setRouteId(route.id, route.props))
|
||||||
|
.then(() => {
|
||||||
|
const nextEl = node.getContentElement();
|
||||||
|
if (nextEl) {
|
||||||
|
return writeNavState(nextEl, chain, index + 1);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readNavState(node: HTMLElement) {
|
||||||
|
const stack: string[] = [];
|
||||||
|
let pivot: NavOutlet|null;
|
||||||
|
while (true) {
|
||||||
|
pivot = breadthFirstSearch(node);
|
||||||
|
if (pivot) {
|
||||||
|
const cmp = pivot.getRouteId();
|
||||||
|
if (cmp) {
|
||||||
|
node = pivot.getContentElement();
|
||||||
|
stack.push(cmp.toLowerCase());
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stack: stack,
|
||||||
|
pivot: pivot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchPath(stack: string[], routes: RouterEntries): string[] {
|
||||||
|
const path: string[] = [];
|
||||||
|
for (const id of stack) {
|
||||||
|
const route = routes.find(r => r.id === id);
|
||||||
|
if (route) {
|
||||||
|
path.push(...route.path);
|
||||||
|
routes = route.subroutes;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchRouteChain(path: string[], routes: RouterEntries): RouterEntries {
|
||||||
|
const chain = [];
|
||||||
|
const segments = new RouterSegments(path);
|
||||||
|
while (routes.length > 0) {
|
||||||
|
const route = matchRoute(segments, routes);
|
||||||
|
if (!route) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
chain.push(route);
|
||||||
|
routes = route.subroutes;
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchRoute(segments: RouterSegments, routes: RouterEntries): RouterEntry | null {
|
||||||
|
if (!routes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let index = 0;
|
||||||
|
let selectedRoute: RouterEntry|null = null;
|
||||||
|
let ambiguous = false;
|
||||||
|
let segment: string;
|
||||||
|
let l: number;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
routes = routes.filter(r => r.path.length > index);
|
||||||
|
if (routes.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
segment = segments.next();
|
||||||
|
routes = routes.filter(r => r.path[index] === segment);
|
||||||
|
l = routes.length;
|
||||||
|
if (l === 0) {
|
||||||
|
selectedRoute = null;
|
||||||
|
ambiguous = false;
|
||||||
|
} else {
|
||||||
|
selectedRoute = routes[0];
|
||||||
|
ambiguous = l > 1;
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
if (ambiguous) {
|
||||||
|
throw new Error('ambiguious match');
|
||||||
|
}
|
||||||
|
return selectedRoute;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readRoutes(root: Element): RouterEntries {
|
||||||
|
return (Array.from(root.children) as HTMLIonRouteElement[])
|
||||||
|
.filter(el => el.tagName === 'ION-ROUTE')
|
||||||
|
.map(el => ({
|
||||||
|
path: parsePath(el.path),
|
||||||
|
id: el.component,
|
||||||
|
props: el.props,
|
||||||
|
subroutes: readRoutes(el)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generatePath(segments: string[]): string {
|
||||||
|
const path = segments
|
||||||
|
.filter(s => s.length > 0)
|
||||||
|
.join('/');
|
||||||
|
|
||||||
|
return '/' + path;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePath(path: string): string[] {
|
||||||
|
if (path === null || path === undefined) {
|
||||||
|
return [''];
|
||||||
|
}
|
||||||
|
const segments = path.split('/')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(s => s.length > 0);
|
||||||
|
|
||||||
|
if (segments.length === 0) {
|
||||||
|
return [''];
|
||||||
|
} else {
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const navs = ['ION-NAV', 'ION-TABS'];
|
||||||
|
export function breadthFirstSearch(root: HTMLElement): NavOutletElement | null {
|
||||||
|
if (!root) {
|
||||||
|
console.error('search root is null');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// we do a Breadth-first search
|
||||||
|
// Breadth-first search (BFS) is an algorithm for traversing or searching tree
|
||||||
|
// or graph data structures.It starts at the tree root(or some arbitrary node of a graph,
|
||||||
|
// sometimes referred to as a 'search key'[1]) and explores the neighbor nodes
|
||||||
|
// first, before moving to the next level neighbours.
|
||||||
|
|
||||||
|
const queue = [root];
|
||||||
|
let node: HTMLElement | undefined;
|
||||||
|
while (node = queue.shift()) {
|
||||||
|
// visit node
|
||||||
|
if (navs.indexOf(node.tagName) >= 0) {
|
||||||
|
return node as NavOutletElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
// queue children
|
||||||
|
const children = node.children;
|
||||||
|
for (let i = 0; i < children.length; i++) {
|
||||||
|
queue.push(children[i] as NavOutletElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writePath(history: History, base: string, usePath: boolean, path: string[], isPop: boolean) {
|
||||||
|
path = [base, ...path];
|
||||||
|
let url = generatePath(path);
|
||||||
|
if (usePath) {
|
||||||
|
url = '#' + url;
|
||||||
|
}
|
||||||
|
if (isPop) {
|
||||||
|
history.back();
|
||||||
|
history.replaceState(null, null, url);
|
||||||
|
} else {
|
||||||
|
history.pushState(null, null, url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readPath(loc: Location, base: string, useHash: boolean): string[] | null {
|
||||||
|
const path = useHash
|
||||||
|
? loc.hash.substr(1)
|
||||||
|
: loc.pathname;
|
||||||
|
|
||||||
|
if (path.startsWith(base)) {
|
||||||
|
return parsePath(path.slice(base.length));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
91
packages/core/src/components/router/router.tsx
Normal file
91
packages/core/src/components/router/router.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Component, Element, Listen, Prop } from '@stencil/core';
|
||||||
|
import { RouterEntries, matchPath, matchRouteChain, readNavState, readPath, readRoutes, writeNavState, writePath } from './router-utils';
|
||||||
|
import { Config, DomController } from '../../index';
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
tag: 'ion-router'
|
||||||
|
})
|
||||||
|
export class Router {
|
||||||
|
|
||||||
|
private routes: RouterEntries;
|
||||||
|
private busy = false;
|
||||||
|
|
||||||
|
@Prop({ context: 'config' }) config: Config;
|
||||||
|
@Prop({ context: 'dom' }) dom: DomController;
|
||||||
|
|
||||||
|
@Prop() base = '';
|
||||||
|
@Prop() useHash = true;
|
||||||
|
|
||||||
|
@Element() el: HTMLElement;
|
||||||
|
|
||||||
|
componentDidLoad() {
|
||||||
|
// read config
|
||||||
|
this.routes = readRoutes(this.el);
|
||||||
|
|
||||||
|
// perform first write
|
||||||
|
this.dom.raf(() => {
|
||||||
|
console.debug('[OUT] page load -> write nav state');
|
||||||
|
this.writeNavStateRoot();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Listen('window:hashchange')
|
||||||
|
protected onURLHashChanged() {
|
||||||
|
if (!this.busy) {
|
||||||
|
console.debug('[OUT] hash changed -> write nav state');
|
||||||
|
this.writeNavStateRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Listen('body:ionNavChanged')
|
||||||
|
protected onNavChanged(ev: CustomEvent) {
|
||||||
|
if (this.busy) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug('[IN] nav changed -> update URL');
|
||||||
|
const { stack, pivot } = this.readNavState();
|
||||||
|
if (pivot) {
|
||||||
|
// readNavState() found a pivot that is not initialized
|
||||||
|
console.debug('[IN] pivot uninitialized -> write partial nav state');
|
||||||
|
this.writeNavState(pivot, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPop = ev.detail.isPop === true;
|
||||||
|
const segments = matchPath(stack, this.routes);
|
||||||
|
this.writePath(segments, isPop);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private writeNavStateRoot(): Promise<any> {
|
||||||
|
const node = document.querySelector('ion-app') as HTMLElement;
|
||||||
|
const currentPath = this.readPath();
|
||||||
|
if (currentPath) {
|
||||||
|
return this.writeNavState(node, currentPath);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
private writeNavState(node: any, path: string[]): Promise<any> {
|
||||||
|
const chain = matchRouteChain(path, this.routes);
|
||||||
|
|
||||||
|
this.busy = true;
|
||||||
|
return writeNavState(node, chain)
|
||||||
|
.catch(err => console.error(err))
|
||||||
|
.then(() => this.busy = false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readNavState() {
|
||||||
|
const root = document.querySelector('ion-app') as HTMLElement;
|
||||||
|
return readNavState(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private writePath(path: string[], isPop: boolean) {
|
||||||
|
writePath(window.history, this.base, this.useHash, path, isPop);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readPath(): string[] | null {
|
||||||
|
return readPath(window.location, this.base, this.useHash);
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
RouterEntries, RouterEntry, RouterSegments, breadthFirstSearch,
|
RouterEntries, RouterSegments, breadthFirstSearch,
|
||||||
generateURL, initRoute, matchRoute, parseURL
|
generatePath, matchRoute, parsePath
|
||||||
} from '../../router-controller/router-utils';
|
} from '../router-utils';
|
||||||
|
|
||||||
describe('RouterSegments', () => {
|
describe('RouterSegments', () => {
|
||||||
it ('should initialize with empty array', () => {
|
it ('should initialize with empty array', () => {
|
||||||
@ -26,74 +26,74 @@ describe('RouterSegments', () => {
|
|||||||
|
|
||||||
describe('parseURL', () => {
|
describe('parseURL', () => {
|
||||||
it('should parse empty path', () => {
|
it('should parse empty path', () => {
|
||||||
expect(parseURL('')).toEqual(['']);
|
expect(parsePath('')).toEqual(['']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse empty path (2)', () => {
|
it('should parse empty path (2)', () => {
|
||||||
expect(parseURL(' ')).toEqual(['']);
|
expect(parsePath(' ')).toEqual(['']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse null path', () => {
|
it('should parse null path', () => {
|
||||||
expect(parseURL(null)).toEqual(['']);
|
expect(parsePath(null)).toEqual(['']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse undefined path', () => {
|
it('should parse undefined path', () => {
|
||||||
expect(parseURL(undefined)).toEqual(['']);
|
expect(parsePath(undefined)).toEqual(['']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse relative path', () => {
|
it('should parse relative path', () => {
|
||||||
expect(parseURL('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
expect(parsePath('path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse absolute path', () => {
|
it('should parse absolute path', () => {
|
||||||
expect(parseURL('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
expect(parsePath('/path/to/file.js')).toEqual(['path', 'to', 'file.js']);
|
||||||
});
|
});
|
||||||
it('should parse relative path', () => {
|
it('should parse relative path', () => {
|
||||||
expect(parseURL('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
expect(parsePath('/PATH///to//file.js//')).toEqual(['PATH', 'to', 'file.js']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('initRoute', () => {
|
// describe('readRoutes', () => {
|
||||||
it('should initialize empty segments', () => {
|
// it('should read URL', () => {
|
||||||
const route: RouterEntry = {
|
// const node = (<div>
|
||||||
id: 'cmp',
|
// <ion-route path='/' component='main-page'/>
|
||||||
path: 'path/to/cmp'
|
// <ion-route path='/one-page' component='one-page'/>
|
||||||
};
|
// <ion-route path='secondpage' component='second-page'/>
|
||||||
initRoute(route);
|
// <ion-route path='/5/hola' component='4'/>
|
||||||
expect(route.segments).toEqual(['path', 'to', 'cmp']);
|
// <ion-route path='path/to/five' component='5'/>
|
||||||
});
|
// </div>) as any;
|
||||||
|
// node.children = node.vchildren;
|
||||||
|
|
||||||
it('should not initialize valid segments', () => {
|
// expect(readRoutes(node)).toEqual([
|
||||||
const route: RouterEntry = {
|
// { path: [''], id: 'hola', subroutes: [] },
|
||||||
id: 'cmp',
|
// { path: ['one-page'], id: 'one-page', subroutes: [] },
|
||||||
path: 'path/to/cmp',
|
// { path: ['secondpage'], id: 'second-page', subroutes: [] },
|
||||||
segments: ['']
|
// { path: ['5', 'hola'], id: '4', subroutes: [] },
|
||||||
};
|
// { path: ['path', 'to', 'five'], id: '5', subroutes: [] }
|
||||||
initRoute(route);
|
// ]);
|
||||||
expect(route.segments).toEqual(['']);
|
// });
|
||||||
});
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
describe('matchRoute', () => {
|
describe('matchRoute', () => {
|
||||||
it('should match simple route', () => {
|
it('should match simple route', () => {
|
||||||
const seg = new RouterSegments(['path', 'to', 'component']);
|
const seg = new RouterSegments(['path', 'to', 'component']);
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 2, path: 'to' },
|
{ id: 2, path: ['to'], subroutes: [] },
|
||||||
{ id: 1, path: 'path' },
|
{ id: 1, path: ['path'], subroutes: [] },
|
||||||
{ id: 3, path: 'segment' },
|
{ id: 3, path: ['segment'], subroutes: [] },
|
||||||
{ id: 4, path: '' },
|
{ id: 4, path: [''], subroutes: [] },
|
||||||
];
|
];
|
||||||
const match = matchRoute(seg, routes);
|
const match = matchRoute(seg, routes);
|
||||||
expect(match).toEqual({ id: 1, path: 'path', segments: ['path'] });
|
expect(match).toEqual({ id: 1, path: ['path'], subroutes: [] });
|
||||||
expect(seg.next()).toEqual('to');
|
expect(seg.next()).toEqual('to');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match default route', () => {
|
it('should match default route', () => {
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 2, path: 'to' },
|
{ id: 2, path: ['to'], subroutes: [] },
|
||||||
{ id: 1, path: 'path' },
|
{ id: 1, path: ['path'], subroutes: [] },
|
||||||
{ id: 3, path: 'segment' },
|
{ id: 3, path: ['segment'], subroutes: [] },
|
||||||
{ id: 4, path: '' },
|
{ id: 4, path: [''], subroutes: [] },
|
||||||
];
|
];
|
||||||
const seg = new RouterSegments(['hola', 'path']);
|
const seg = new RouterSegments(['hola', 'path']);
|
||||||
let match = matchRoute(seg, routes);
|
let match = matchRoute(seg, routes);
|
||||||
@ -108,13 +108,12 @@ describe('matchRoute', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should not match any route', () => {
|
it('should not match any route', () => {
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 2, path: 'to/to/to' },
|
{ id: 2, path: ['to', 'to', 'to'], subroutes: [] },
|
||||||
{ id: 1, path: 'adam/manu' },
|
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||||
{ id: 3, path: 'hola/adam' },
|
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||||
{ id: 4, path: '' },
|
{ id: 4, path: [''], subroutes: [] },
|
||||||
];
|
];
|
||||||
const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
const seg = new RouterSegments(['hola', 'manu', 'adam']);
|
||||||
const match = matchRoute(seg, routes);
|
const match = matchRoute(seg, routes);
|
||||||
@ -130,9 +129,9 @@ describe('matchRoute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not match any route (2)', () => {
|
it('should not match any route (2)', () => {
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 1, path: 'adam/manu' },
|
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||||
{ id: 3, path: 'hola/adam' },
|
{ id: 3, path: ['hola', 'adam'], subroutes: [] },
|
||||||
];
|
];
|
||||||
const seg = new RouterSegments(['adam']);
|
const seg = new RouterSegments(['adam']);
|
||||||
expect(matchRoute(seg, routes)).toBeNull();
|
expect(matchRoute(seg, routes)).toBeNull();
|
||||||
@ -141,11 +140,11 @@ describe('matchRoute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it ('should match multiple segments', () => {
|
it ('should match multiple segments', () => {
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 1, path: 'adam/manu' },
|
{ id: 1, path: ['adam', 'manu'], subroutes: [] },
|
||||||
{ id: 2, path: 'manu/hello' },
|
{ id: 2, path: ['manu', 'hello'], subroutes: [] },
|
||||||
{ id: 3, path: 'hello' },
|
{ id: 3, path: ['hello'], subroutes: [] },
|
||||||
{ id: 4, path: '' },
|
{ id: 4, path: [''], subroutes: [] },
|
||||||
];
|
];
|
||||||
const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']);
|
const seg = new RouterSegments(['adam', 'manu', 'hello', 'manu', 'hello']);
|
||||||
let match = matchRoute(seg, routes);
|
let match = matchRoute(seg, routes);
|
||||||
@ -165,10 +164,10 @@ describe('matchRoute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should match long multi segments', () => {
|
it('should match long multi segments', () => {
|
||||||
const routes = [
|
const routes: RouterEntries = [
|
||||||
{ id: 1, path: 'adam/manu/hello/menu/hello' },
|
{ id: 1, path: ['adam', 'manu', 'hello', 'menu', 'hello'], subroutes: [] },
|
||||||
{ id: 2, path: 'adam/manu/hello/menu' },
|
{ id: 2, path: ['adam', 'manu', 'hello', 'menu'], subroutes: [] },
|
||||||
{ id: 3, path: 'adam/manu' },
|
{ id: 3, path: ['adam', 'manu'], subroutes: [] },
|
||||||
];
|
];
|
||||||
const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
const seg = new RouterSegments(['adam', 'manu', 'hello', 'menu', 'hello']);
|
||||||
const match = matchRoute(seg, routes);
|
const match = matchRoute(seg, routes);
|
||||||
@ -186,26 +185,26 @@ describe('matchRoute', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('generateURL', () => {
|
describe('generatePath', () => {
|
||||||
it('should generate an empty URL', () => {
|
it('should generate an empty URL', () => {
|
||||||
expect(generateURL([])).toEqual('/');
|
expect(generatePath([])).toEqual('/');
|
||||||
expect(generateURL([{ path: '' } as any])).toEqual('/');
|
expect(generatePath([{ path: '' } as any])).toEqual('/');
|
||||||
expect(generateURL([{ path: '/' } as any])).toEqual('/');
|
expect(generatePath([{ path: '/' } as any])).toEqual('/');
|
||||||
expect(generateURL([{ path: '//' } as any])).toEqual('/');
|
expect(generatePath([{ path: '//' } as any])).toEqual('/');
|
||||||
expect(generateURL([{ path: ' ' } as any])).toEqual('/');
|
expect(generatePath([{ path: ' ' } as any])).toEqual('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should genenerate a basic url', () => {
|
it('should genenerate a basic url', () => {
|
||||||
const state = [
|
const stack = [
|
||||||
{ path: '/' },
|
'',
|
||||||
{ path: '/ ' },
|
'',
|
||||||
{ path: '' },
|
'',
|
||||||
{ path: '/path// to/' },
|
'path/to',
|
||||||
{ path: '/page ' },
|
'page',
|
||||||
{ path: 'number-TWO/' },
|
'number-TWO',
|
||||||
{ path: ' / ' }
|
''
|
||||||
];
|
];
|
||||||
expect(generateURL(state as any)).toEqual('/path/to/page/number-TWO');
|
expect(generatePath(stack)).toEqual('/path/to/page/number-TWO');
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -234,8 +233,3 @@ describe('breadthFirstSearch', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// describe('readNavState', () => {
|
|
||||||
// it('should read state', () => {
|
|
||||||
|
|
||||||
// });
|
|
||||||
// });
|
|
@ -222,9 +222,15 @@ Emitted when the tab changes.
|
|||||||
#### getByIndex()
|
#### getByIndex()
|
||||||
|
|
||||||
|
|
||||||
|
#### getContentElement()
|
||||||
|
|
||||||
|
|
||||||
#### getIndex()
|
#### getIndex()
|
||||||
|
|
||||||
|
|
||||||
|
#### getRouteId()
|
||||||
|
|
||||||
|
|
||||||
#### getSelected()
|
#### getSelected()
|
||||||
|
|
||||||
|
|
||||||
@ -234,6 +240,9 @@ Emitted when the tab changes.
|
|||||||
#### select()
|
#### select()
|
||||||
|
|
||||||
|
|
||||||
|
#### setRouteId()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core';
|
import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State } from '@stencil/core';
|
||||||
import { Config } from '../../index';
|
import { Config, NavOutlet } from '../../index';
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -9,7 +9,7 @@ import { Config } from '../../index';
|
|||||||
md: 'tabs.md.scss'
|
md: 'tabs.md.scss'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
export class Tabs {
|
export class Tabs implements NavOutlet {
|
||||||
private ids = -1;
|
private ids = -1;
|
||||||
private tabsId: number = (++tabIds);
|
private tabsId: number = (++tabIds);
|
||||||
|
|
||||||
@ -134,7 +134,7 @@ export class Tabs {
|
|||||||
*/
|
*/
|
||||||
@Method()
|
@Method()
|
||||||
getSelected(): HTMLIonTabElement | undefined {
|
getSelected(): HTMLIonTabElement | undefined {
|
||||||
return this.tabs.find((tab) => tab.selected);
|
return this.selectedTab;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
@ -147,30 +147,6 @@ export class Tabs {
|
|||||||
return this.tabs;
|
return this.tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@Method()
|
|
||||||
getState(): NavState {
|
|
||||||
const selectedTab = this.getSelected();
|
|
||||||
if (!selectedTab) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
path: selectedTab.getPath(),
|
|
||||||
focusNode: selectedTab
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Method()
|
|
||||||
getRoutes(): RouterEntries {
|
|
||||||
const a = this.tabs.map(t => {
|
|
||||||
return {
|
|
||||||
path: t.getPath(),
|
|
||||||
id: t
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return a;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Method()
|
@Method()
|
||||||
setRouteId(id: any, _: any = {}): Promise<void> {
|
setRouteId(id: any, _: any = {}): Promise<void> {
|
||||||
if (this.selectedTab === id) {
|
if (this.selectedTab === id) {
|
||||||
@ -178,7 +154,21 @@ export class Tabs {
|
|||||||
}
|
}
|
||||||
return this.select(id);
|
return this.select(id);
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
|
@Method()
|
||||||
|
getRouteId(): string|null {
|
||||||
|
if (this.selectedTab) {
|
||||||
|
return this.selectedTab.tagName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Method()
|
||||||
|
getContentElement(): HTMLElement {
|
||||||
|
return this.selectedTab;
|
||||||
|
}
|
||||||
|
|
||||||
private initTabs() {
|
private initTabs() {
|
||||||
const tabs = this.tabs = Array.from(this.el.querySelectorAll('ion-tab'));
|
const tabs = this.tabs = Array.from(this.el.querySelectorAll('ion-tab'));
|
||||||
|
7
packages/core/src/index.d.ts
vendored
7
packages/core/src/index.d.ts
vendored
@ -83,8 +83,8 @@ export { ReorderGroup } from './components/reorder-group/reorder-group';
|
|||||||
export {
|
export {
|
||||||
RouterEntry,
|
RouterEntry,
|
||||||
RouterEntries,
|
RouterEntries,
|
||||||
NavState,
|
NavOutlet
|
||||||
} from './components/router-controller/router-utils';
|
} from './components/router/router-utils';
|
||||||
export { Row } from './components/row/row';
|
export { Row } from './components/row/row';
|
||||||
export { Reorder } from './components/reorder/reorder';
|
export { Reorder } from './components/reorder/reorder';
|
||||||
export { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
|
export { Scroll, ScrollCallback, ScrollDetail } from './components/scroll/scroll';
|
||||||
@ -162,6 +162,9 @@ declare global {
|
|||||||
namespace JSXElements {
|
namespace JSXElements {
|
||||||
|
|
||||||
export interface DOMAttributes {
|
export interface DOMAttributes {
|
||||||
|
// for ion-menu and ion-split-pane
|
||||||
|
main?: boolean;
|
||||||
|
|
||||||
padding?: boolean;
|
padding?: boolean;
|
||||||
['padding-top']?: boolean;
|
['padding-top']?: boolean;
|
||||||
['padding-bottom']?: boolean;
|
['padding-bottom']?: boolean;
|
||||||
|
@ -30,7 +30,7 @@ exports.config = {
|
|||||||
{ components: ['ion-popover', 'ion-popover-controller'] },
|
{ components: ['ion-popover', 'ion-popover-controller'] },
|
||||||
{ components: ['ion-radio', 'ion-radio-group'] },
|
{ components: ['ion-radio', 'ion-radio-group'] },
|
||||||
{ components: ['ion-reorder', 'ion-reorder-group'] },
|
{ components: ['ion-reorder', 'ion-reorder-group'] },
|
||||||
{ components: ['ion-route', 'ion-router-controller'] },
|
{ components: ['ion-route', 'ion-router'] },
|
||||||
{ components: ['ion-searchbar'] },
|
{ components: ['ion-searchbar'] },
|
||||||
{ components: ['ion-segment', 'ion-segment-button'] },
|
{ components: ['ion-segment', 'ion-segment-button'] },
|
||||||
{ components: ['ion-select', 'ion-select-option', 'ion-select-popover'] },
|
{ components: ['ion-select', 'ion-select-option', 'ion-select-popover'] },
|
||||||
|
Reference in New Issue
Block a user