From 26e0117a3f5b6efc75f8ff220a18466656f6b970 Mon Sep 17 00:00:00 2001 From: Adam Bradley Date: Fri, 21 Aug 2015 23:04:23 -0500 Subject: [PATCH] ionic router update --- ionic/components.ts | 2 + ionic/components/app/app.ts | 36 +-- ionic/components/checkbox/test/basic/e2e.ts | 6 +- ionic/components/nav/nav-controller.ts | 7 +- ionic/components/nav/nav-router.ts | 81 +++++++ ionic/components/nav/test/basic/index.ts | 27 +-- ionic/components/nav/test/routing/index.ts | 56 +++++ ionic/components/nav/test/routing/view1.html | 22 ++ ionic/components/nav/test/routing/view2.html | 22 ++ ionic/components/nav/test/routing/view3.html | 25 ++ ionic/components/tabs/test/advanced/index.ts | 18 +- ionic/components/view/view-controller.ts | 38 +-- ionic/config/annotations.ts | 6 +- ionic/ionic.ts | 3 - ionic/routing/path-recognizer.ts | 178 -------------- ionic/routing/router.ts | 235 ------------------- ionic/routing/url-state.ts | 177 -------------- scripts/build/config.js | 1 + 18 files changed, 259 insertions(+), 681 deletions(-) create mode 100644 ionic/components/nav/nav-router.ts create mode 100644 ionic/components/nav/test/routing/index.ts create mode 100644 ionic/components/nav/test/routing/view1.html create mode 100644 ionic/components/nav/test/routing/view2.html create mode 100644 ionic/components/nav/test/routing/view3.html delete mode 100644 ionic/routing/path-recognizer.ts delete mode 100644 ionic/routing/router.ts delete mode 100644 ionic/routing/url-state.ts diff --git a/ionic/components.ts b/ionic/components.ts index 8c85dca4cf..53cbbc7903 100644 --- a/ionic/components.ts +++ b/ionic/components.ts @@ -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' diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index 8892b2978c..48fb0c735b 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -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); diff --git a/ionic/components/checkbox/test/basic/e2e.ts b/ionic/components/checkbox/test/basic/e2e.ts index d666104663..38e0cd5cc8 100644 --- a/ionic/components/checkbox/test/basic/e2e.ts +++ b/ionic/components/checkbox/test/basic/e2e.ts @@ -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(); diff --git a/ionic/components/nav/nav-controller.ts b/ionic/components/nav/nav-controller.ts index 76509a2499..eb2eee3c3c 100644 --- a/ionic/components/nav/nav-controller.ts +++ b/ionic/components/nav/nav-controller.ts @@ -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]; } } diff --git a/ionic/components/nav/nav-router.ts b/ionic/components/nav/nav-router.ts new file mode 100644 index 0000000000..2514b830ed --- /dev/null +++ b/ionic/components/nav/nav-router.ts @@ -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 { + 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; + } + +} diff --git a/ionic/components/nav/test/basic/index.ts b/ionic/components/nav/test/basic/index.ts index 04338e3f3a..520a75dcb9 100644 --- a/ionic/components/nav/test/basic/index.ts +++ b/ionic/components/nav/test/basic/index.ts @@ -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: '' }) -class MyApp {} +class E2EApp { + constructor() { + this.root = FirstPage; + } +} diff --git a/ionic/components/nav/test/routing/index.ts b/ionic/components/nav/test/routing/index.ts new file mode 100644 index 0000000000..228e942b4d --- /dev/null +++ b/ionic/components/nav/test/routing/index.ts @@ -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; + } +} diff --git a/ionic/components/nav/test/routing/view1.html b/ionic/components/nav/test/routing/view1.html new file mode 100644 index 0000000000..449c1db122 --- /dev/null +++ b/ionic/components/nav/test/routing/view1.html @@ -0,0 +1,22 @@ + + + First View + + + +

+ Window Hash: {{windowHash}} +

+

+ Location Path: {{path}} +

+

+ ViewItem Id: {{viewItem.id}} +

+

+ Second View via href +

+

+ Third View via href, 3 as id param +

+
diff --git a/ionic/components/nav/test/routing/view2.html b/ionic/components/nav/test/routing/view2.html new file mode 100644 index 0000000000..9838895cab --- /dev/null +++ b/ionic/components/nav/test/routing/view2.html @@ -0,0 +1,22 @@ + + + Second View + + + +

+ Window Hash: {{windowHash}} +

+

+ Location Path: {{path}} +

+

+ ViewItem Id: {{viewItem.id}} +

+

+ Third View via href, 12 as id param +

+

+ First View via href +

+
diff --git a/ionic/components/nav/test/routing/view3.html b/ionic/components/nav/test/routing/view3.html new file mode 100644 index 0000000000..661a3c7bce --- /dev/null +++ b/ionic/components/nav/test/routing/view3.html @@ -0,0 +1,25 @@ + + + Third View + + + +

+ Window Hash: {{windowHash}} +

+

+ Location Path: {{path}} +

+

+ NavParams, id: {{id}} +

+

+ ViewItem Id: {{viewItem.id}} +

+

+ Second View via href +

+

+ First View via href +

+
diff --git a/ionic/components/tabs/test/advanced/index.ts b/ionic/components/tabs/test/advanced/index.ts index 43062ac2d9..af72582eba 100644 --- a/ionic/components/tabs/test/advanced/index.ts +++ b/ionic/components/tabs/test/advanced/index.ts @@ -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 {} diff --git a/ionic/components/view/view-controller.ts b/ionic/components/view/view-controller.ts index 13544adf56..fc282b19bf 100644 --- a/ionic/components/view/view-controller.ts +++ b/ionic/components/view/view-controller.ts @@ -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; diff --git a/ionic/config/annotations.ts b/ionic/config/annotations.ts index 33723f91ce..96863e72a3 100644 --- a/ionic/config/annotations.ts +++ b/ionic/config/annotations.ts @@ -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; } diff --git a/ionic/ionic.ts b/ionic/ionic.ts index a1c0747217..1ddb0984db 100644 --- a/ionic/ionic.ts +++ b/ionic/ionic.ts @@ -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' diff --git a/ionic/routing/path-recognizer.ts b/ionic/routing/path-recognizer.ts deleted file mode 100644 index c978401160..0000000000 --- a/ionic/routing/path-recognizer.ts +++ /dev/null @@ -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 { - 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; }); -} diff --git a/ionic/routing/router.ts b/ionic/routing/router.ts deleted file mode 100644 index d7a1fb93b7..0000000000 --- a/ionic/routing/router.ts +++ /dev/null @@ -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); - } - -} diff --git a/ionic/routing/url-state.ts b/ionic/routing/url-state.ts deleted file mode 100644 index 018675505a..0000000000 --- a/ionic/routing/url-state.ts +++ /dev/null @@ -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); diff --git a/scripts/build/config.js b/scripts/build/config.js index 266127401d..04ddc9b588 100644 --- a/scripts/build/config.js +++ b/scripts/build/config.js @@ -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' ],