mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 11:17:19 +08:00
ionic router update
This commit is contained in:
@ -24,6 +24,7 @@ export * from 'ionic/components/modal/modal'
|
||||
export * from 'ionic/components/nav/nav'
|
||||
export * from 'ionic/components/nav/nav-controller'
|
||||
export * from 'ionic/components/nav/nav-push'
|
||||
export * from 'ionic/components/nav/nav-router'
|
||||
export * from 'ionic/components/nav-bar/nav-bar'
|
||||
export * from 'ionic/components/popup/popup'
|
||||
export * from 'ionic/components/slides/slides'
|
||||
@ -36,3 +37,4 @@ export * from 'ionic/components/switch/switch'
|
||||
export * from 'ionic/components/tabs/tabs'
|
||||
export * from 'ionic/components/tabs/tab'
|
||||
export * from 'ionic/components/toolbar/toolbar'
|
||||
export * from 'ionic/components/view/view-item'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Component, View, bootstrap, ElementRef, NgZone, bind, DynamicComponentLoader, Injector} from 'angular2/angular2';
|
||||
import {routerInjectables, HashLocationStrategy, LocationStrategy, Router} from 'angular2/router';
|
||||
|
||||
import {IonicRouter} from '../../routing/router';
|
||||
import {IonicConfig} from '../../config/config';
|
||||
import {Platform} from '../../platform/platform';
|
||||
import * as util from '../../util/util';
|
||||
@ -19,8 +19,6 @@ export class IonicApp {
|
||||
|
||||
// Our component registry map
|
||||
this.components = {};
|
||||
|
||||
this._activeViewId = null;
|
||||
}
|
||||
|
||||
load(appRef) {
|
||||
@ -54,17 +52,6 @@ export class IonicApp {
|
||||
this._zone.run(fn);
|
||||
}
|
||||
|
||||
stateChange(type, activeView) {
|
||||
if (this._activeViewId !== activeView.id) {
|
||||
this.router.stateChange(type, activeView);
|
||||
this._activeViewId = activeView.id;
|
||||
}
|
||||
}
|
||||
|
||||
stateClear() {
|
||||
this.router.stateClear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a known component with a key, for easy lookups later.
|
||||
*/
|
||||
@ -175,7 +162,7 @@ class RootAnchor {
|
||||
}
|
||||
}
|
||||
|
||||
export function ionicBootstrap(rootComponentType, config, router) {
|
||||
export function ionicBootstrap(rootComponentType, config) {
|
||||
return new Promise(resolve => {
|
||||
try {
|
||||
// get the user config, or create one if wasn't passed in
|
||||
@ -197,15 +184,6 @@ export function ionicBootstrap(rootComponentType, config, router) {
|
||||
// prepare the ready promise to fire....when ready
|
||||
Platform.prepareReady(config);
|
||||
|
||||
// setup router
|
||||
if (typeof router !== IonicRouter) {
|
||||
router = new IonicRouter(router);
|
||||
}
|
||||
router.app(app);
|
||||
|
||||
// TODO: don't wire these together
|
||||
app.router = router;
|
||||
|
||||
// TODO: probs need a better way to inject global injectables
|
||||
let actionMenu = new ActionMenu(app, config);
|
||||
let modal = new Modal(app, config);
|
||||
@ -215,10 +193,11 @@ export function ionicBootstrap(rootComponentType, config, router) {
|
||||
let appBindings = Injector.resolve([
|
||||
bind(IonicApp).toValue(app),
|
||||
bind(IonicConfig).toValue(config),
|
||||
bind(IonicRouter).toValue(router),
|
||||
bind(ActionMenu).toValue(actionMenu),
|
||||
bind(Modal).toValue(modal),
|
||||
bind(Popup).toValue(popup)
|
||||
bind(Popup).toValue(popup),
|
||||
routerInjectables,
|
||||
bind(LocationStrategy).toClass(HashLocationStrategy)
|
||||
]);
|
||||
|
||||
bootstrap(rootComponentType, appBindings).then(appRef => {
|
||||
@ -240,10 +219,7 @@ export function ionicBootstrap(rootComponentType, config, router) {
|
||||
console.error(err)
|
||||
});
|
||||
|
||||
router.load(window, app, config).then(() => {
|
||||
// resolve that the app has loaded
|
||||
resolve(app);
|
||||
});
|
||||
resolve(app);
|
||||
|
||||
}).catch(err => {
|
||||
console.error('ionicBootstrap', err);
|
||||
|
@ -1,10 +1,6 @@
|
||||
|
||||
it('should check apple via checkbox element click', function() {
|
||||
it('should check apple, enable/check grape, submit form', function() {
|
||||
element(by.css('#e2eAppleCheckbox')).click();
|
||||
});
|
||||
|
||||
|
||||
it('should enable/check grape via buttons and submit form', function() {
|
||||
element(by.css('#e2eGrapeDisabled')).click();
|
||||
element(by.css('#e2eGrapeChecked')).click();
|
||||
element(by.css('#e2eSubmit')).click();
|
||||
|
@ -37,7 +37,10 @@ export class NavController {
|
||||
}
|
||||
|
||||
export class NavParams {
|
||||
constructor(params) {
|
||||
extend(this, params);
|
||||
constructor(data) {
|
||||
this.data = data || {};
|
||||
}
|
||||
get(param) {
|
||||
return this.data[param];
|
||||
}
|
||||
}
|
||||
|
81
ionic/components/nav/nav-router.ts
Normal file
81
ionic/components/nav/nav-router.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import {Directive, ElementRef, DynamicComponentLoader, Attribute} from 'angular2/angular2';
|
||||
import {
|
||||
RouterOutlet,
|
||||
Router,
|
||||
ComponentInstruction,
|
||||
Instruction,
|
||||
Location} from 'angular2/router';
|
||||
|
||||
import {Nav} from './nav';
|
||||
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-nav'
|
||||
})
|
||||
export class NavRouter extends RouterOutlet {
|
||||
|
||||
constructor(_elementRef: ElementRef, _loader: DynamicComponentLoader,
|
||||
_parentRouter: Router, @Attribute('name') nameAttr: string,
|
||||
nav: Nav) {
|
||||
super(_elementRef, _loader, _parentRouter, nameAttr);
|
||||
|
||||
// Nav is Ionic's ViewController, which we injected into this class
|
||||
this.nav = nav;
|
||||
|
||||
// register this router with Ionic's ViewController
|
||||
// Ionic's ViewController will call this NavRouter's "stateChange"
|
||||
// method when the ViewController has...changed its state
|
||||
nav.registerRouter(this);
|
||||
}
|
||||
|
||||
_activate(instruction: ComponentInstruction): Promise<any> {
|
||||
var previousInstruction = this._currentInstruction;
|
||||
this._currentInstruction = instruction;
|
||||
var componentType = instruction.componentType;
|
||||
this.childRouter = this._parentRouter.childRouter(componentType);
|
||||
|
||||
// tell the ViewController which componentType, and it's params, to navigate to
|
||||
this.nav.push(componentType, instruction.params);
|
||||
}
|
||||
|
||||
stateChange(type, viewItem) {
|
||||
// stateChange is called by Ionic's ViewController
|
||||
// type could be "push" or "pop"
|
||||
// viewItem is Ionic's ViewItem class, which has the properties "componentType" and "params"
|
||||
|
||||
// only do an update if there's an actual view change
|
||||
if (!viewItem || this._activeViewId === viewItem.id) return;
|
||||
this._activeViewId = viewItem.id;
|
||||
|
||||
// get the best PathRecognizer for this view's componentType
|
||||
let pathRecognizer = this.getPathRecognizerByComponent(viewItem.componentType);
|
||||
if (pathRecognizer) {
|
||||
|
||||
// generate a componentInstruction from the view's PathRecognizer and params
|
||||
let componentInstruction = pathRecognizer.generate(viewItem.params.data);
|
||||
|
||||
// create an Instruction from the componentInstruction
|
||||
let instruction = new Instruction(componentInstruction, null);
|
||||
|
||||
// update the browser's URL
|
||||
this._parentRouter.navigateInstruction(instruction);
|
||||
}
|
||||
}
|
||||
|
||||
getPathRecognizerByComponent(componentType) {
|
||||
// given a componentType, figure out the best PathRecognizer to use
|
||||
let rules = this._parentRouter.registry._rules;
|
||||
|
||||
let pathRecognizer = null;
|
||||
rules.forEach((rule) => {
|
||||
|
||||
pathRecognizer = rule.matchers.find((matcherPathRecognizer) => {
|
||||
return (matcherPathRecognizer.handler.componentType === componentType);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return pathRecognizer;
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +1,12 @@
|
||||
import {App} from 'ionic/ionic';
|
||||
|
||||
import {App, NavController} from 'ionic/ionic';
|
||||
import {FirstPage} from './pages/first-page';
|
||||
import {SecondPage} from './pages/second-page';
|
||||
import {ThirdPage} from './pages/third-page';
|
||||
|
||||
|
||||
@App({
|
||||
routes: [
|
||||
{
|
||||
path: '/firstpage',
|
||||
component: FirstPage,
|
||||
root: true
|
||||
},
|
||||
{
|
||||
path: '/secondpage',
|
||||
component: SecondPage,
|
||||
},
|
||||
{
|
||||
path: '/thirdpage',
|
||||
component: ThirdPage,
|
||||
}
|
||||
]
|
||||
template: '<ion-nav [root]="root"></ion-nav>'
|
||||
})
|
||||
class MyApp {}
|
||||
class E2EApp {
|
||||
constructor() {
|
||||
this.root = FirstPage;
|
||||
}
|
||||
}
|
||||
|
56
ionic/components/nav/test/routing/index.ts
Normal file
56
ionic/components/nav/test/routing/index.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import {RouteConfig, Location} from 'angular2/router';
|
||||
|
||||
import {App, IonicView, NavParams, ViewItem} from 'ionic/ionic';
|
||||
|
||||
|
||||
@IonicView({templateUrl: 'view1.html'})
|
||||
class View1Cmp {
|
||||
constructor(location: Location, viewItem: ViewItem) {
|
||||
this.path = location.path();
|
||||
this.viewItem = viewItem;
|
||||
console.log(`View1Cmp, path: ${this.path}`);
|
||||
}
|
||||
viewDidEnter() {
|
||||
this.windowHash = window.location.hash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IonicView({templateUrl: 'view2.html'})
|
||||
class View2Cmp {
|
||||
constructor(location: Location, viewItem: ViewItem) {
|
||||
this.path = location.path();
|
||||
this.viewItem = viewItem;
|
||||
console.log(`View2Cmp, path: ${this.path}`);
|
||||
}
|
||||
viewDidEnter() {
|
||||
this.windowHash = window.location.hash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IonicView({templateUrl: 'view3.html'})
|
||||
class View3Cmp {
|
||||
constructor(params: NavParams, location: Location, viewItem: ViewItem) {
|
||||
this.id = params.get('id');
|
||||
this.path = location.path();
|
||||
this.viewItem = viewItem;
|
||||
console.log(`View3Cmp, path: ${this.path}, param id: ${this.id}`);
|
||||
}
|
||||
viewDidEnter() {
|
||||
this.windowHash = window.location.hash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@App()
|
||||
@RouteConfig([
|
||||
{ path: '/', component: View1Cmp, as: 'first' },
|
||||
{ path: '/second', component: View2Cmp, as: 'second' },
|
||||
{ path: '/third/:id', component: View3Cmp, as: 'third' }
|
||||
])
|
||||
class InboxApp {
|
||||
constructor(location: Location) {
|
||||
this.location = location;
|
||||
}
|
||||
}
|
22
ionic/components/nav/test/routing/view1.html
Normal file
22
ionic/components/nav/test/routing/view1.html
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
<ion-navbar *navbar primary>
|
||||
<ion-title>First View</ion-title>
|
||||
</ion-navbar>
|
||||
|
||||
<ion-content class="padding">
|
||||
<h4>
|
||||
Window Hash: {{windowHash}}
|
||||
</h4>
|
||||
<h4>
|
||||
Location Path: {{path}}
|
||||
</h4>
|
||||
<h4>
|
||||
ViewItem Id: {{viewItem.id}}
|
||||
</h4>
|
||||
<p>
|
||||
<a href="#/second">Second View via href</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#/third/3">Third View via href, 3 as id param</a>
|
||||
</p>
|
||||
</ion-content>
|
22
ionic/components/nav/test/routing/view2.html
Normal file
22
ionic/components/nav/test/routing/view2.html
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
<ion-navbar *navbar primary>
|
||||
<ion-title>Second View</ion-title>
|
||||
</ion-navbar>
|
||||
|
||||
<ion-content class="padding">
|
||||
<h4>
|
||||
Window Hash: {{windowHash}}
|
||||
</h4>
|
||||
<h4>
|
||||
Location Path: {{path}}
|
||||
</h4>
|
||||
<h4>
|
||||
ViewItem Id: {{viewItem.id}}
|
||||
</h4>
|
||||
<p>
|
||||
<a href="#/third/12">Third View via href, 12 as id param</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#/">First View via href</a>
|
||||
</p>
|
||||
</ion-content>
|
25
ionic/components/nav/test/routing/view3.html
Normal file
25
ionic/components/nav/test/routing/view3.html
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
<ion-navbar *navbar primary>
|
||||
<ion-title>Third View</ion-title>
|
||||
</ion-navbar>
|
||||
|
||||
<ion-content class="padding">
|
||||
<h4>
|
||||
Window Hash: {{windowHash}}
|
||||
</h4>
|
||||
<h4>
|
||||
Location Path: {{path}}
|
||||
</h4>
|
||||
<h4>
|
||||
NavParams, id: {{id}}
|
||||
</h4>
|
||||
<h4>
|
||||
ViewItem Id: {{viewItem.id}}
|
||||
</h4>
|
||||
<p>
|
||||
<a href="#/second">Second View via href</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="#/">First View via href</a>
|
||||
</p>
|
||||
</ion-content>
|
@ -1,4 +1,4 @@
|
||||
import {Component} from 'angular2/angular2';
|
||||
import {RouteConfig, Location} from 'angular2/router';
|
||||
|
||||
import {App, IonicView, NavController} from 'ionic/ionic';
|
||||
|
||||
@ -24,10 +24,10 @@ class SignIn {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IonicView({
|
||||
templateUrl: './tabs.html'
|
||||
})
|
||||
|
||||
class TabsPage {
|
||||
constructor(nav: NavController) {
|
||||
this.tab1Root = Tab1Page1
|
||||
@ -184,13 +184,9 @@ class Tab2Page3 {
|
||||
class Tab3Page1 {}
|
||||
|
||||
|
||||
@App({
|
||||
routes: [
|
||||
{
|
||||
path: '/signin',
|
||||
component: SignIn,
|
||||
root: true
|
||||
}
|
||||
]
|
||||
})
|
||||
@App()
|
||||
@RouteConfig([
|
||||
{ path: '/', component: SignIn, as: 'signin' },
|
||||
{ path: '/tabs', component: TabsPage, as: 'tabs' },
|
||||
])
|
||||
class E2EApp {}
|
||||
|
@ -5,7 +5,6 @@ import {AppViewManager} from 'angular2/src/core/compiler/view_manager';
|
||||
import {Ion} from '../ion';
|
||||
import {IonicConfig} from '../../config/config';
|
||||
import {IonicApp} from '../app/app';
|
||||
import {IonicRouter} from '../../routing/router';
|
||||
import {ViewItem} from './view-item';
|
||||
import {NavController} from '../nav/nav-controller';
|
||||
import {PaneController} from '../nav/pane';
|
||||
@ -30,13 +29,10 @@ export class ViewController extends Ion {
|
||||
this.compiler = injector.get(Compiler);
|
||||
this.loader = injector.get(DynamicComponentLoader);
|
||||
this.viewMngr = injector.get(AppViewManager);
|
||||
this.router = injector.get(IonicRouter);
|
||||
this.app = injector.get(IonicApp);
|
||||
this.config = config;
|
||||
this.zone = zone;
|
||||
|
||||
this.router.addViewController(this);
|
||||
|
||||
this.items = [];
|
||||
this.panes = new PaneController(this);
|
||||
|
||||
@ -54,8 +50,8 @@ export class ViewController extends Ion {
|
||||
]);
|
||||
}
|
||||
|
||||
push(component, params = {}, opts = {}) {
|
||||
if (!component || this.isTransitioning()) {
|
||||
push(componentType, params = {}, opts = {}) {
|
||||
if (!componentType || this.isTransitioning()) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
@ -79,13 +75,15 @@ export class ViewController extends Ion {
|
||||
}
|
||||
|
||||
// create a new ViewItem
|
||||
let enteringItem = new ViewItem(this, component, params);
|
||||
let enteringItem = new ViewItem(this, componentType, params);
|
||||
|
||||
// add the item to the stack
|
||||
this.add(enteringItem);
|
||||
|
||||
// notify app of the state change
|
||||
this.app.stateChange('push', enteringItem);
|
||||
if (this.router) {
|
||||
// notify router of the state change
|
||||
this.router.stateChange('push', enteringItem, params);
|
||||
}
|
||||
|
||||
// start the transition
|
||||
this.transition(enteringItem, leavingItem, opts, () => {
|
||||
@ -120,8 +118,10 @@ export class ViewController extends Ion {
|
||||
// only item on the history stack.
|
||||
let enteringItem = this.getPrevious(leavingItem);
|
||||
if (enteringItem) {
|
||||
// notify app of the state change
|
||||
this.app.stateChange('pop', enteringItem);
|
||||
if (this.router) {
|
||||
// notify router of the state change
|
||||
this.router.stateChange('pop', enteringItem);
|
||||
}
|
||||
|
||||
// start the transition
|
||||
this.transition(enteringItem, leavingItem, opts, () => {
|
||||
@ -145,8 +145,6 @@ export class ViewController extends Ion {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
this.app.stateClear();
|
||||
|
||||
// if animate has not been set then default to false
|
||||
opts.animate = opts.animate || false;
|
||||
|
||||
@ -193,9 +191,9 @@ export class ViewController extends Ion {
|
||||
return this.push((component && component.component) || component, (component && component.params), opts);
|
||||
}
|
||||
|
||||
setRoot(component, params = {}, opts = {}) {
|
||||
setRoot(componentType, params = {}, opts = {}) {
|
||||
return this.setItems([{
|
||||
component,
|
||||
componentType,
|
||||
params
|
||||
}], opts);
|
||||
}
|
||||
@ -321,8 +319,10 @@ export class ViewController extends Ion {
|
||||
enteringItem.didEnter();
|
||||
leavingItem.didLeave();
|
||||
|
||||
// notify app of the state change
|
||||
this.app.stateChange('pop', enteringItem);
|
||||
if (this.router) {
|
||||
// notify router of the state change
|
||||
this.router.stateChange('pop', enteringItem);
|
||||
}
|
||||
|
||||
} else {
|
||||
// cancelled the swipe back, return items to original state
|
||||
@ -550,6 +550,10 @@ export class ViewController extends Ion {
|
||||
return (item && item.state === STAGED_ENTERING_STATE);
|
||||
}
|
||||
|
||||
registerRouter(router) {
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ACTIVE_STATE = 1;
|
||||
|
@ -2,7 +2,6 @@ import {CORE_DIRECTIVES, FORM_DIRECTIVES, Component, Directive, View, forwardRef
|
||||
|
||||
import * as util from 'ionic/util';
|
||||
import {IonicConfig} from './config';
|
||||
import {IonicRouter} from '../routing/router';
|
||||
import {ionicBootstrap} from '../components/app/app';
|
||||
import {
|
||||
Aside, Button, Content, Scroll, Refresher,
|
||||
@ -16,7 +15,7 @@ import {
|
||||
TextInput, TextInputElement, Label,
|
||||
Segment, SegmentButton, SegmentControlValueAccessor,
|
||||
RadioGroup, RadioButton, SearchBar,
|
||||
Nav, NavbarTemplate, Navbar, NavPush, NavPop,
|
||||
Nav, NavbarTemplate, Navbar, NavPush, NavPop, NavRouter,
|
||||
TapClick, TapDisabled,
|
||||
Register,
|
||||
ShowWhen, HideWhen,
|
||||
@ -78,6 +77,7 @@ export const IonicDirectives = [
|
||||
forwardRef(() => Navbar),
|
||||
forwardRef(() => NavPush),
|
||||
forwardRef(() => NavPop),
|
||||
forwardRef(() => NavRouter),
|
||||
forwardRef(() => Register),
|
||||
|
||||
forwardRef(() => ShowWhen),
|
||||
@ -172,7 +172,7 @@ export function App(args={}) {
|
||||
// redefine with added annotations
|
||||
Reflect.defineMetadata('annotations', annotations, cls);
|
||||
|
||||
ionicBootstrap(cls, args.config, args.routes);
|
||||
ionicBootstrap(cls, args.config);
|
||||
|
||||
return cls;
|
||||
}
|
||||
|
@ -9,9 +9,6 @@ export * from './components'
|
||||
export * from './platform/platform'
|
||||
export * from './platform/registry'
|
||||
|
||||
export * from './routing/router'
|
||||
export * from './routing/url-state'
|
||||
|
||||
export * from './util/click-block'
|
||||
export * from './util/focus'
|
||||
|
||||
|
@ -1,178 +0,0 @@
|
||||
import {
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
RegExpMatcherWrapper,
|
||||
StringWrapper,
|
||||
isPresent,
|
||||
isBlank,
|
||||
BaseException,
|
||||
normalizeBlank
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
Map,
|
||||
MapWrapper,
|
||||
StringMap,
|
||||
StringMapWrapper,
|
||||
List,
|
||||
ListWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
|
||||
|
||||
class ContinuationSegment {
|
||||
generate(params) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
class StaticSegment {
|
||||
constructor(string) {
|
||||
this.name = '';
|
||||
this.regex = escapeRegex(string);
|
||||
}
|
||||
|
||||
generate() {
|
||||
return this.regex;
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicSegment {
|
||||
constructor(name) {
|
||||
this.regex = "([^/]+)";
|
||||
}
|
||||
|
||||
generate(params) {
|
||||
if (!StringMapWrapper.contains(params, this.name)) {
|
||||
throw new BaseException(
|
||||
`Route generator for '${this.name}' was not included in parameters passed.`)
|
||||
}
|
||||
return normalizeBlank(StringMapWrapper.get(params, this.name));
|
||||
}
|
||||
}
|
||||
|
||||
class StarSegment {
|
||||
constructor(name) {
|
||||
this.regex = "(.+)";
|
||||
}
|
||||
|
||||
generate(params) {
|
||||
return normalizeBlank(StringMapWrapper.get(params, this.name));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var paramMatcher = RegExpWrapper.create("^:([^\/]+)$");
|
||||
var wildcardMatcher = RegExpWrapper.create("^\\*([^\/]+)$");
|
||||
|
||||
function parsePathString(route: string) {
|
||||
// normalize route as not starting with a "/". Recognition will
|
||||
// also normalize.
|
||||
if (StringWrapper.startsWith(route, "/")) {
|
||||
route = StringWrapper.substring(route, 1);
|
||||
}
|
||||
|
||||
var segments = splitBySlash(route);
|
||||
var results = [];
|
||||
var specificity = 0;
|
||||
|
||||
// The "specificity" of a path is used to determine which route is used when multiple routes match
|
||||
// a URL.
|
||||
// Static segments (like "/foo") are the most specific, followed by dynamic segments (like
|
||||
// "/:id"). Star segments
|
||||
// add no specificity. Segments at the start of the path are more specific than proceeding ones.
|
||||
// The code below uses place values to combine the different types of segments into a single
|
||||
// integer that we can
|
||||
// sort later. Each static segment is worth hundreds of points of specificity (10000, 9900, ...,
|
||||
// 200), and each
|
||||
// dynamic segment is worth single points of specificity (100, 99, ... 2).
|
||||
if (segments.length > 98) {
|
||||
throw new BaseException(`'${route}' has more than the maximum supported number of segments.`);
|
||||
}
|
||||
|
||||
var limit = segments.length - 1;
|
||||
for (var i = 0; i <= limit; i++) {
|
||||
var segment = segments[i], match;
|
||||
|
||||
if (isPresent(match = RegExpWrapper.firstMatch(paramMatcher, segment))) {
|
||||
results.push(new DynamicSegment(match[1]));
|
||||
specificity += (100 - i);
|
||||
} else if (isPresent(match = RegExpWrapper.firstMatch(wildcardMatcher, segment))) {
|
||||
results.push(new StarSegment(match[1]));
|
||||
} else if (segment == '...') {
|
||||
if (i < limit) {
|
||||
// TODO (matsko): setup a proper error here `
|
||||
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
|
||||
}
|
||||
results.push(new ContinuationSegment());
|
||||
} else if (segment.length > 0) {
|
||||
results.push(new StaticSegment(segment));
|
||||
specificity += 100 * (100 - i);
|
||||
}
|
||||
}
|
||||
|
||||
return {segments: results, specificity};
|
||||
}
|
||||
|
||||
function splitBySlash(url: string): List<string> {
|
||||
return url.split('/');
|
||||
}
|
||||
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
export class PathRecognizer {
|
||||
|
||||
constructor(path) {
|
||||
this.segments = [];
|
||||
|
||||
var parsed = parsePathString(path);
|
||||
var specificity = parsed['specificity'];
|
||||
var segments = parsed['segments'];
|
||||
var regexString = '^';
|
||||
|
||||
ListWrapper.forEach(segments, (segment) => {
|
||||
if (segment instanceof ContinuationSegment) {
|
||||
this.terminal = false;
|
||||
} else {
|
||||
regexString += '/' + segment.regex;
|
||||
}
|
||||
});
|
||||
|
||||
if (this.terminal) {
|
||||
regexString += '$';
|
||||
}
|
||||
|
||||
this.regex = RegExpWrapper.create(regexString);
|
||||
this.segments = segments;
|
||||
this.specificity = specificity;
|
||||
}
|
||||
|
||||
parseParams(url) {
|
||||
var params = StringMapWrapper.create();
|
||||
var urlPart = url;
|
||||
for (var i = 0; i < this.segments.length; i++) {
|
||||
var segment = this.segments[i];
|
||||
if (segment instanceof ContinuationSegment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
|
||||
urlPart = StringWrapper.substring(urlPart, match[0].length);
|
||||
if (segment.name.length > 0) {
|
||||
StringMapWrapper.set(params, segment.name, match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
generate(params) {
|
||||
return ListWrapper.join(
|
||||
ListWrapper.map(this.segments, (segment) => '/' + segment.generate(params)), '');
|
||||
}
|
||||
}
|
||||
|
||||
var specialCharacters = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
|
||||
var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g');
|
||||
|
||||
function escapeRegex(string): string {
|
||||
return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return "\\" + match; });
|
||||
}
|
@ -1,235 +0,0 @@
|
||||
import {RegExpWrapper} from 'angular2/src/facade/lang';
|
||||
|
||||
import * as util from '../util/util';
|
||||
import {PathRecognizer} from './path-recognizer';
|
||||
|
||||
|
||||
export class IonicRouter {
|
||||
constructor(config) {
|
||||
this._routes = [];
|
||||
this._viewCtrls = [];
|
||||
this.config(config);
|
||||
}
|
||||
|
||||
app(app) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
config(config) {
|
||||
if (config) {
|
||||
for (let i = 0; i < config.length; i++) {
|
||||
this.addRoute(config[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addRoute(routeConfig) {
|
||||
if (routeConfig && routeConfig.path && routeConfig.component) {
|
||||
let route = new Route(routeConfig);
|
||||
if (routeConfig.root) {
|
||||
this.otherwise(route);
|
||||
}
|
||||
this._routes.push(route);
|
||||
}
|
||||
}
|
||||
|
||||
stateChange(type, activeView) {
|
||||
// this fires when the app's state has changed. `stateChange` will
|
||||
// tell each of the state managers that the state has changed, and
|
||||
// each state manager will decide what to do with this info
|
||||
// (the url state manager updates the url bar if a route was setup)
|
||||
if (activeView && activeView.component) {
|
||||
|
||||
let componentRoute = activeView.component.route;
|
||||
if (componentRoute) {
|
||||
let path = componentRoute.generate(activeView.params);
|
||||
if (path) {
|
||||
for (let name in stateManagers) {
|
||||
stateManagers[name].stateChange(path, type, activeView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
stateClear() {
|
||||
for (let name in stateManagers) {
|
||||
stateManagers[name].stateClear();
|
||||
}
|
||||
}
|
||||
|
||||
matchPaths(paths) {
|
||||
// load each of paths to a component
|
||||
let components = [];
|
||||
let route;
|
||||
|
||||
if (paths) {
|
||||
for (let i = 0; i < paths.length; i++) {
|
||||
route = this.matchPath(paths[i]);
|
||||
if (route && route.component) {
|
||||
components.push(route.component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return components;
|
||||
}
|
||||
|
||||
matchPath(path) {
|
||||
// takes a string path and loops through each of the setup
|
||||
// routes to see if the path matches any of the routes
|
||||
// the matched path with the highest specifity wins
|
||||
let matchedRoute = null;
|
||||
let route = null;
|
||||
let routeMatch = null;
|
||||
|
||||
for (let i = 0; i < this._routes.length; i++) {
|
||||
route = this._routes[i];
|
||||
routeMatch = route.match(path);
|
||||
|
||||
if (routeMatch && (!matchedRoute || route.specificity > matchedRoute.specificity)) {
|
||||
matchedRoute = route;
|
||||
}
|
||||
}
|
||||
return matchedRoute;
|
||||
}
|
||||
|
||||
load(window, ionicApp, ionicConfig) {
|
||||
// load is called when the app has finished loading each state
|
||||
// manager gets a chance to say what path the app should be at
|
||||
let viewCtrl = this.viewController();
|
||||
if (!viewCtrl || !this._routes.length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let resolve;
|
||||
let promise = new Promise(res => { resolve = res; });
|
||||
|
||||
// get the initial load paths from the state manager with the highest priorty
|
||||
this.getManagerPaths(window, ionicApp, ionicConfig).then(paths => {
|
||||
|
||||
// load all of the paths the highest priority state manager has given
|
||||
let components = this.matchPaths(paths);
|
||||
|
||||
if (!components.length && this.otherwise()) {
|
||||
// the state manager did not find and loaded components
|
||||
// use the "otherwise" path
|
||||
components = [this.otherwise().component];
|
||||
}
|
||||
|
||||
this.app.zoneRun(() => {
|
||||
viewCtrl.setItems(components).then(resolve);
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
getManagerPaths(window, ionicApp, ionicConfig) {
|
||||
// loop through all of the state managers and load their paths
|
||||
// the state manager with valid paths and highest priority wins
|
||||
let resolve;
|
||||
let promise = new Promise(res => { resolve = res; });
|
||||
|
||||
// load each of the state managers
|
||||
let stateManagerPromises = [];
|
||||
for (let name in stateManagerClasses) {
|
||||
stateManagers[name] = new stateManagerClasses[name](window, this, ionicApp, ionicConfig);
|
||||
stateManagerPromises.push( stateManagers[name].load() );
|
||||
}
|
||||
|
||||
// when all the state manager loads have resolved then see which one wins
|
||||
Promise.all(stateManagerPromises).then(stateManagerLoadResults => {
|
||||
|
||||
// now that all the state managers are loaded
|
||||
// get the highest priority state manager's paths
|
||||
let stateLoadResult = null;
|
||||
let paths = null;
|
||||
let highestPriority = -1;
|
||||
|
||||
for (let i = 0; i < stateManagerLoadResults.length; i++) {
|
||||
stateLoadResult = stateManagerLoadResults[i];
|
||||
if (stateLoadResult && stateLoadResult.paths.length && stateLoadResult.priority > highestPriority) {
|
||||
paths = stateLoadResult.paths;
|
||||
highestPriority = stateLoadResult.priority;
|
||||
}
|
||||
}
|
||||
|
||||
resolve(paths);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
push(path) {
|
||||
let viewCtrl = this.viewController();
|
||||
if (viewCtrl) {
|
||||
let matchedRoute = this.matchPath(path);
|
||||
if (matchedRoute && matchedRoute.component) {
|
||||
this.app.zoneRun(() => {
|
||||
viewCtrl.push(matchedRoute.component, matchedRoute.params, {});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pop() {
|
||||
let viewCtrl = this.viewController();
|
||||
if (viewCtrl) {
|
||||
this.app.zoneRun(() => {
|
||||
viewCtrl.pop();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
otherwise(val) {
|
||||
if (arguments.length) {
|
||||
this._otherwise = val;
|
||||
}
|
||||
return this._otherwise
|
||||
}
|
||||
|
||||
addViewController(viewCtrl) {
|
||||
this._viewCtrls.push(viewCtrl);
|
||||
}
|
||||
|
||||
viewController() {
|
||||
if (this._viewCtrls.length) {
|
||||
return this._viewCtrls[ this._viewCtrls.length - 1 ];
|
||||
}
|
||||
}
|
||||
|
||||
static registerStateManager(name, StateManagerClass) {
|
||||
stateManagerClasses[name] = StateManagerClass;
|
||||
}
|
||||
|
||||
static deregisterStateManager(name) {
|
||||
delete stateManagerClasses[name];
|
||||
delete stateManagers[name];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let stateManagerClasses = {};
|
||||
let stateManagers = {};
|
||||
|
||||
|
||||
class Route {
|
||||
constructor(routeConfig) {
|
||||
util.extend(this, routeConfig);
|
||||
this.recognizer = new PathRecognizer(this.path);
|
||||
this.specificity = this.recognizer.specificity;
|
||||
|
||||
this.component.route = this;
|
||||
}
|
||||
|
||||
match(path) {
|
||||
return RegExpWrapper.firstMatch(this.recognizer.regex, path);
|
||||
}
|
||||
|
||||
generate(params) {
|
||||
return this.recognizer.generate(params);
|
||||
}
|
||||
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
import {IonicRouter} from './router';
|
||||
import * as util from '../util/util';
|
||||
|
||||
|
||||
class UrlStateManager {
|
||||
|
||||
constructor(window, router) {
|
||||
this.location = window.location;
|
||||
this.history = window.history;
|
||||
this.ls = window.localStorage;
|
||||
this.router = router;
|
||||
|
||||
// overkill for location change listeners, but ensures we
|
||||
// know when the location has changed. Only 1 of the listeners
|
||||
// will actually do the work, the other will be skipped.
|
||||
window.addEventListener('popstate', () => {
|
||||
this.onLocationChange();
|
||||
});
|
||||
window.addEventListener('hashchange', () => {
|
||||
this.onLocationChange();
|
||||
});
|
||||
}
|
||||
|
||||
load() {
|
||||
let paths = [this.getCurrentPath()];
|
||||
let savedPaths = this.paths();
|
||||
|
||||
if (savedPaths[savedPaths.length - 1] == paths[0]) {
|
||||
// the last path in the saved paths is the same as the
|
||||
// current path, so use the saved paths to rebuild the history
|
||||
paths = savedPaths;
|
||||
|
||||
} else {
|
||||
// the current path is not the same as the last path in the
|
||||
// saved history, so the saved history is no good, erase it
|
||||
this.paths([]);
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
paths: paths,
|
||||
priority: 0
|
||||
});
|
||||
}
|
||||
|
||||
stateChange(path, type, activeView) {
|
||||
let savedPaths = this.paths();
|
||||
|
||||
// check if the given path is different than the current location
|
||||
let isDifferentPath = (this.getCurrentPath() !== path);
|
||||
|
||||
if (type == 'pop') {
|
||||
// if the popstate came from the browser's back button (and not Ionic)
|
||||
// then we shouldn't force another browser history.back()
|
||||
// only do a history.back() if the URL hasn't been updated yet
|
||||
if (isDifferentPath) {
|
||||
this.history.back();
|
||||
}
|
||||
|
||||
if (savedPaths.length && savedPaths[savedPaths.length - 1] != path) {
|
||||
// only if the last item in the saved paths
|
||||
// equals this path then it can be removed
|
||||
savedPaths.pop();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if (this._hasInit) {
|
||||
if (isDifferentPath) {
|
||||
// push the new state to the history stack since the path
|
||||
// isn't already in the location hash
|
||||
this.history.pushState(path, '', '#' + path);
|
||||
}
|
||||
|
||||
} else {
|
||||
// replace the very first load with the correct entering state info
|
||||
this.history.replaceState(path, '', '#' + path);
|
||||
this._hasInit = true;
|
||||
}
|
||||
|
||||
if (savedPaths[savedPaths.length - 1] != path) {
|
||||
// only if the last item in the saved paths does
|
||||
// not equal this path then it can be added
|
||||
savedPaths.push(path);
|
||||
|
||||
// don't allow the history to grow too large
|
||||
if (savedPaths.length > MAX_PATH_STORE) {
|
||||
savedPaths = savedPaths.slice( savedPaths.length - MAX_PATH_STORE );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// save the new path data
|
||||
this.paths(savedPaths);
|
||||
|
||||
// ensure this resets
|
||||
this._currentPath = null;
|
||||
}
|
||||
|
||||
stateClear() {
|
||||
this.paths([]);
|
||||
}
|
||||
|
||||
onLocationChange() {
|
||||
let currentPath = this.getCurrentPath();
|
||||
|
||||
if (currentPath == this._currentPath) {
|
||||
// absolutely no change since last onLocationChange
|
||||
return;
|
||||
}
|
||||
|
||||
// keep in-memory the current path to quickly tell if things have changed
|
||||
this._currentPath = currentPath;
|
||||
|
||||
// load up the saved paths
|
||||
let savedPaths = this.paths();
|
||||
|
||||
if (currentPath === savedPaths[savedPaths.length - 1]) {
|
||||
// do nothing if the last saved path is
|
||||
// the same as the current path
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentPath === savedPaths[savedPaths.length - 2]) {
|
||||
// the user is moving back
|
||||
this.router.pop();
|
||||
|
||||
} else {
|
||||
// the user is moving forward
|
||||
this.router.push(currentPath);
|
||||
}
|
||||
}
|
||||
|
||||
paths(val) {
|
||||
if (arguments.length) {
|
||||
// set in-memory data
|
||||
this._paths = val;
|
||||
|
||||
// set localStorage data
|
||||
try {
|
||||
this.ls.setItem(PATH_STORE_KEY, JSON.stringify(val));
|
||||
} catch(e) {}
|
||||
|
||||
} else {
|
||||
|
||||
if (!this._paths) {
|
||||
// we don't already have data in-memory
|
||||
|
||||
// see if we have data in localStorage
|
||||
try {
|
||||
let strData = this.ls.getItem(PATH_STORE_KEY);
|
||||
if (strData) {
|
||||
this._paths = JSON.parse(strData);
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// if not in localStorage yet then create new path data
|
||||
if (!this._paths) {
|
||||
this._paths = [];
|
||||
}
|
||||
}
|
||||
|
||||
// return the in-memory data
|
||||
return this._paths;
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentPath() {
|
||||
// remove leading # to get the path
|
||||
return this.location.hash.slice(1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const PATH_STORE_KEY = 'ionic:history';
|
||||
const MAX_PATH_STORE = 20;
|
||||
|
||||
IonicRouter.registerStateManager('url', UrlStateManager);
|
@ -16,6 +16,7 @@ module.exports = {
|
||||
'node_modules/systemjs/node_modules/es6-module-loader/dist/es6-module-loader.js',
|
||||
'node_modules/systemjs/dist/system.js',
|
||||
'node_modules/angular2/bundles/angular2.dev.js',
|
||||
'node_modules/angular2/bundles/router.dev.js',
|
||||
'dist/js/ionic.js',
|
||||
'node_modules/web-animations-js/web-animations.min.js'
|
||||
],
|
||||
|
Reference in New Issue
Block a user