diff --git a/ionic/components/app/app.ts b/ionic/components/app/app.ts index 38e96b5e8c..158516c6fe 100644 --- a/ionic/components/app/app.ts +++ b/ionic/components/app/app.ts @@ -26,7 +26,11 @@ export class IonicApp { load(appRef) { this.ref(appRef); - this.zone(this.injector().get(NgZone)); + this._zone = this.injector().get(NgZone); + } + + title(val) { + document.title = val; } ref(val) { @@ -40,11 +44,8 @@ export class IonicApp { return this._ref.injector; } - zone(val) { - if (arguments.length) { - this._zone = val; - } - return this._zone; + zoneRun(fn) { + this._zone.run(fn); } stateChange(type, activeView) { @@ -74,10 +75,10 @@ export class IonicApp { * Create and append the given component into the root * element of the app. * - * @param Component the cls to create and insert + * @param Component the component to create and insert * @return Promise that resolves with the ContainerRef created */ - appendComponent(cls: Type, context=null) { + appendComponent(component: Type, context=null) { return new Promise((resolve, reject) => { let injector = this.injector(); let compiler = injector.get(Compiler); @@ -85,7 +86,7 @@ export class IonicApp { let rootComponentRef = this._ref._hostComponent; let viewContainerLocation = rootComponentRef.location; - compiler.compileInHost(cls).then(protoViewRef => { + compiler.compileInHost(component).then(protoViewRef => { let atIndex = 0; let hostViewRef = viewMngr.createViewInContainer( @@ -158,8 +159,8 @@ function initApp(window, document, config) { return app; } -export function ionicBootstrap(cls, config, router) { - return new Promise((resolve, reject) => { +export function ionicBootstrap(component, config, router) { + return new Promise(resolve => { try { // get the user config, or create one if wasn't passed in if (typeof config !== IonicConfig) { @@ -202,7 +203,7 @@ export function ionicBootstrap(cls, config, router) { bind(Modal).toValue(modal) ]; - bootstrap(cls, injectableBindings).then(appRef => { + bootstrap(component, injectableBindings).then(appRef => { app.load(appRef); router.load(window, app, config).then(() => { @@ -212,12 +213,10 @@ export function ionicBootstrap(cls, config, router) { }).catch(err => { console.error('ionicBootstrap', err); - reject(err); }); } catch (err) { - console.error('ionicBootstrap', err); - reject(err); + console.error(err); } }); } diff --git a/ionic/components/nav-bar/nav-bar.ts b/ionic/components/nav-bar/nav-bar.ts index 39e280f225..4867b9ebc1 100644 --- a/ionic/components/nav-bar/nav-bar.ts +++ b/ionic/components/nav-bar/nav-bar.ts @@ -4,6 +4,7 @@ import {ProtoViewRef} from 'angular2/src/core/compiler/view_ref'; import {Ion} from '../ion'; import {IonicConfig} from '../../config/config'; import {IonicComponent} from '../../config/annotations'; +import {IonicApp} from '../app/app'; import {ViewItem} from '../view/view-item'; import * as dom from '../../util/dom'; @@ -42,9 +43,10 @@ import * as dom from '../../util/dom'; ] }) export class Navbar extends Ion { - constructor(item: ViewItem, elementRef: ElementRef, ionicConfig: IonicConfig) { + constructor(item: ViewItem, elementRef: ElementRef, ionicConfig: IonicConfig, app: IonicApp) { super(elementRef, ionicConfig); + this.app = app; this.eleRef = elementRef; this.itemEles = []; item.navbarView(this); @@ -130,8 +132,10 @@ export class Navbar extends Ion { } didEnter() { + const titleEle = this._ttEle || (this._ttEle = this.eleRef.nativeElement.querySelector('ion-title')); + this.app.title(titleEle.textContent); + setTimeout(() => { - const titleEle = this._ttEle || (this._ttEle = this.eleRef.nativeElement.querySelector('ion-title')); //this.titleText((titleEle && titleEle.textContent) || ''); }, 32); } diff --git a/ionic/components/nav/test/basic/index.ts b/ionic/components/nav/test/basic/index.ts index 82b99b298e..04338e3f3a 100644 --- a/ionic/components/nav/test/basic/index.ts +++ b/ionic/components/nav/test/basic/index.ts @@ -1,21 +1,25 @@ import {App} from 'ionic/ionic'; +import {FirstPage} from './pages/first-page'; +import {SecondPage} from './pages/second-page'; +import {ThirdPage} from './pages/third-page'; + @App({ - routes: { - 'FirstPage': { - 'path': '/firstpage', - 'module': 'dist/examples/nav/basic/pages/first-page', - 'root': true + routes: [ + { + path: '/firstpage', + component: FirstPage, + root: true }, - 'SecondPage': { - 'path': '/secondpage', - 'module': 'dist/examples/nav/basic/pages/second-page', + { + path: '/secondpage', + component: SecondPage, }, - 'ThirdPage': { - 'path': '/thirdpage', - 'module': 'dist/examples/nav/basic/pages/third-page', - }, - } + { + path: '/thirdpage', + component: ThirdPage, + } + ] }) class MyApp {} diff --git a/ionic/components/nav/test/basic/pages/first-page.ts b/ionic/components/nav/test/basic/pages/first-page.ts index 068f453fdb..a80e2ef0cb 100644 --- a/ionic/components/nav/test/basic/pages/first-page.ts +++ b/ionic/components/nav/test/basic/pages/first-page.ts @@ -1,5 +1,5 @@ import {IonicView, IonicConfig, IonicApp} from 'ionic/ionic'; -import {NavParams, Routable, NavController} from 'ionic/ionic'; +import {NavParams, NavController} from 'ionic/ionic'; import {SecondPage} from './second-page'; import {ThirdPage} from './third-page'; @@ -52,39 +52,35 @@ export class FirstPage { this.nav.setItems(items); } - viewLoaded() { - console.log('viewLoaded first page'); - } + // viewLoaded() { + // console.log('viewLoaded first page'); + // } - viewWillEnter() { - console.log('viewWillEnter first page'); - } + // viewWillEnter() { + // console.log('viewWillEnter first page'); + // } - viewDidEnter() { - console.log('viewDidEnter first page'); - } + // viewDidEnter() { + // console.log('viewDidEnter first page'); + // } - viewWillLeave() { - console.log('viewWillLeave first page'); - } + // viewWillLeave() { + // console.log('viewWillLeave first page'); + // } - viewDidLeave() { - console.log('viewDidLeave first page'); - } + // viewDidLeave() { + // console.log('viewDidLeave first page'); + // } - viewWillUnload() { - console.log('viewWillUnload first page'); - } + // viewWillUnload() { + // console.log('viewWillUnload first page'); + // } - viewDidUnload() { - console.log('viewDidUnload first page'); - } + // viewDidUnload() { + // console.log('viewDidUnload first page'); + // } push() { this.nav.push(SecondPage, { id: 8675309, myData: [1,2,3,4] }, { animation: 'ios' }); } } - -new Routable(FirstPage, { - path: '/firstpage' -}); diff --git a/ionic/components/nav/test/basic/pages/second-page.ts b/ionic/components/nav/test/basic/pages/second-page.ts index 5ba9a9b791..6a13d39e7b 100644 --- a/ionic/components/nav/test/basic/pages/second-page.ts +++ b/ionic/components/nav/test/basic/pages/second-page.ts @@ -1,4 +1,4 @@ -import {IonicView, Routable, NavController, NavParams} from 'ionic/ionic'; +import {IonicView, NavController, NavParams} from 'ionic/ionic'; import {ThirdPage} from './third-page'; import {FirstPage} from './first-page'; @@ -45,36 +45,32 @@ export class SecondPage { this.nav.push(ThirdPage); } - viewLoaded() { - console.log('viewLoaded second page'); - } + // viewLoaded() { + // console.log('viewLoaded second page'); + // } - viewWillEnter() { - console.log('viewWillEnter second page'); - } + // viewWillEnter() { + // console.log('viewWillEnter second page'); + // } - viewDidEnter() { - console.log('viewDidEnter second page'); - } + // viewDidEnter() { + // console.log('viewDidEnter second page'); + // } - viewWillLeave() { - console.log('viewWillLeave second page'); - } + // viewWillLeave() { + // console.log('viewWillLeave second page'); + // } - viewDidLeave() { - console.log('viewDidLeave second page'); - } + // viewDidLeave() { + // console.log('viewDidLeave second page'); + // } - viewWillUnload() { - console.log('viewWillUnload second page'); - } + // viewWillUnload() { + // console.log('viewWillUnload second page'); + // } - viewDidUnload() { - console.log('viewDidUnload second page'); - } + // viewDidUnload() { + // console.log('viewDidUnload second page'); + // } } - -new Routable(SecondPage, { - path: '/secondpage' -}); diff --git a/ionic/components/nav/test/basic/pages/third-page.ts b/ionic/components/nav/test/basic/pages/third-page.ts index 8941a26f5b..e989e2ae5a 100644 --- a/ionic/components/nav/test/basic/pages/third-page.ts +++ b/ionic/components/nav/test/basic/pages/third-page.ts @@ -1,4 +1,4 @@ -import {IonicView, Routable, NavController} from 'ionic/ionic'; +import {IonicView, NavController} from 'ionic/ionic'; @IonicView({ @@ -23,36 +23,32 @@ export class ThirdPage { this.nav.pop() } - viewLoaded() { - console.log('viewLoaded third page'); - } + // viewLoaded() { + // console.log('viewLoaded third page'); + // } - viewWillEnter() { - console.log('viewWillEnter third page'); - } + // viewWillEnter() { + // console.log('viewWillEnter third page'); + // } - viewDidEnter() { - console.log('viewDidEnter third page'); - } + // viewDidEnter() { + // console.log('viewDidEnter third page'); + // } - viewWillLeave() { - console.log('viewWillLeave third page'); - } + // viewWillLeave() { + // console.log('viewWillLeave third page'); + // } - viewDidLeave() { - console.log('viewDidLeave third page'); - } + // viewDidLeave() { + // console.log('viewDidLeave third page'); + // } - viewWillUnload() { - console.log('viewWillUnload third page'); - } + // viewWillUnload() { + // console.log('viewWillUnload third page'); + // } - viewDidUnload() { - console.log('viewDidUnload third page'); - } + // viewDidUnload() { + // console.log('viewDidUnload third page'); + // } } - -new Routable(ThirdPage, { - path: '/thirdpage' -}); diff --git a/ionic/components/view/view-controller.ts b/ionic/components/view/view-controller.ts index a643f831f4..609f91e724 100644 --- a/ionic/components/view/view-controller.ts +++ b/ionic/components/view/view-controller.ts @@ -49,8 +49,8 @@ export class ViewController extends Ion { ]); } - push(ComponentType, params = {}, opts = {}) { - if (!ComponentType || this.isTransitioning()) { + push(component, params = {}, opts = {}) { + if (!component || this.isTransitioning()) { return Promise.reject(); } @@ -74,7 +74,7 @@ export class ViewController extends Ion { } // create a new ViewItem - let enteringItem = new ViewItem(this, ComponentType, params); + let enteringItem = new ViewItem(this, component, params); // add the item to the stack this.add(enteringItem); @@ -136,6 +136,10 @@ export class ViewController extends Ion { * Set the item stack to reflect the given component classes. */ setItems(components, opts = {}) { + if (!components || !components.length) { + return Promise.resolve(); + } + // if animate has not been set then default to false opts.animate = opts.animate || false; @@ -162,13 +166,15 @@ export class ViewController extends Ion { let newBeforeItems = components.slice(0, components.length - 1); for (let j = 0; j < newBeforeItems.length; j++) { component = newBeforeItems[j]; - viewItem = new ViewItem(this, component.component || component, component.params); - viewItem.state = CACHED_STATE; - viewItem.shouldDestroy = false; - viewItem.shouldCache = false; + if (component) { + viewItem = new ViewItem(this, component.component || component, component.params); + viewItem.state = CACHED_STATE; + viewItem.shouldDestroy = false; + viewItem.shouldCache = false; - // add the item to the stack - this.add(viewItem); + // add the item to the stack + this.add(viewItem); + } } } @@ -177,13 +183,13 @@ export class ViewController extends Ion { component = components[ components.length - 1 ]; // transition the leaving and entering - return this.push(component.component || component, component.params, opts); + return this.push((component && component.component) || component, (component && component.params), opts); } - setRoot(ComponentType, params = {}, opts = {}) { + setRoot(component, params = {}, opts = {}) { return this.setItems([{ - component: ComponentType, - params: params + component, + params }], opts); } @@ -485,7 +491,7 @@ export class ViewController extends Ion { } add(item) { - item.id = this.id + '' + (++this._ids); + item.id = this.id + '-' + (++this._ids); this.items.push(item); } diff --git a/ionic/components/view/view-item.ts b/ionic/components/view/view-item.ts index ab0148135b..2ba25173a8 100644 --- a/ionic/components/view/view-item.ts +++ b/ionic/components/view/view-item.ts @@ -6,9 +6,9 @@ import {NavParams} from '../nav/nav-controller'; export class ViewItem { - constructor(viewCtrl, cls, params = {}) { + constructor(viewCtrl, component, params = {}) { this.viewCtrl = viewCtrl; - this.cls = cls; + this.component = component; this.params = new NavParams(params); this.instance = null; this.state = 0; @@ -37,7 +37,7 @@ export class ViewItem { 'class': 'nav-item' } }); - let ionViewComponent = DirectiveBinding.createFromType(this.cls, annotation); + let ionViewComponent = DirectiveBinding.createFromType(this.component, annotation); // compile the Component viewCtrl.compiler.compileInHost(ionViewComponent).then(componentProtoViewRef => { diff --git a/ionic/ionic.ts b/ionic/ionic.ts index b771d66d28..12595b6f9d 100644 --- a/ionic/ionic.ts +++ b/ionic/ionic.ts @@ -12,7 +12,7 @@ export * from 'ionic/platform/platform' export * from 'ionic/platform/registry' export * from 'ionic/routing/router' -export * from 'ionic/routing/hash-url-state' +export * from 'ionic/routing/url-state' export * from 'ionic/util/click-block' export * from 'ionic/util/focus' diff --git a/ionic/routing/hash-url-state.ts b/ionic/routing/hash-url-state.ts deleted file mode 100644 index 9b730d7fc4..0000000000 --- a/ionic/routing/hash-url-state.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {IonicRouter} from './router'; -import * as util from '../util/util'; - - -class HashUrlStateManager { - - constructor(window, router) { - this.location = window.location; - this.history = window.history; - this.router = router; - - window.addEventListener('popstate', ev => { - this.onPopState(ev); - }); - } - - stateChange(path, type, activeView) { - 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 (this.isDifferentPath(path)) { - this.history.back(); - } - - } else { - // push state change - let enteringState = { - path: path, - backPath: this.router.lastPath(), - forwardPath: null - }; - - if (this._hasInit) { - // update the leaving state to know what it's forward state will be - let leavingState = util.extend(this.history.state, { - forwardPath: enteringState.path - }); - if (leavingState.path !== enteringState.path) { - this.history.replaceState(leavingState, '', '#' + leavingState.path); - } - - if (this.isDifferentPath(path)) { - // push the new state to the history stack since the path - // isn't already in the location hash - this.history.pushState(enteringState, '', '#' + enteringState.path); - } - - } else { - // replace the very first load with the correct entering state info - this.history.replaceState(enteringState, '', '#' + enteringState.path); - this._hasInit = true; - } - } - } - - onPopState(ev) { - let newState = ev.state || {}; - let newStatePath = newState.path; - let newStateBackPath = newState.backPath; - let newStateForwardPath = newState.forwardPath; - let lastLoadedStatePath = this.router.lastPath(); - - if (newStatePath === lastLoadedStatePath) { - // do nothing if the last path is the same - // as the "new" current state - return; - } - - let activeViewCtrl = this.router.activeViewController(); - if (activeViewCtrl) { - - if (newStateForwardPath === lastLoadedStatePath) { - // if the last loaded state path is the same as the new - // state's forward path then the user is moving back - activeViewCtrl.pop(); - - } else if (newStateBackPath === lastLoadedStatePath) { - // if the last loaded state path is the new state's - // back path, then the user is moving forward - this.router.loadByPath(newStatePath); - } - - } - } - - getCurrentPath() { - // Grab the path without the leading hash - return new Promise(resolve => { - resolve({ - path: this.location.hash.slice(1), - priority: 0 - }) - }); - } - - isDifferentPath(path) { - // check if the given path is different than the current location - return (this.location.hash !== ('#' + path)); - } - -} - - -IonicRouter.registerStateManager('hashurl', HashUrlStateManager); diff --git a/ionic/routing/path-recognizer.ts b/ionic/routing/path-recognizer.ts index 1749a76810..c978401160 100644 --- a/ionic/routing/path-recognizer.ts +++ b/ionic/routing/path-recognizer.ts @@ -30,8 +30,8 @@ class StaticSegment { this.regex = escapeRegex(string); } - generate(params) { - return this.string; + generate() { + return this.regex; } } @@ -49,7 +49,6 @@ class DynamicSegment { } } - class StarSegment { constructor(name) { this.regex = "(.+)"; @@ -124,8 +123,6 @@ export class PathRecognizer { constructor(path) { this.segments = []; - // TODO: use destructuring assignment - // see https://github.com/angular/ts2dart/issues/158 var parsed = parsePathString(path); var specificity = parsed['specificity']; var segments = parsed['segments']; diff --git a/ionic/routing/router.ts b/ionic/routing/router.ts index 8a728e5219..644c86a980 100644 --- a/ionic/routing/router.ts +++ b/ionic/routing/router.ts @@ -6,165 +6,189 @@ import {PathRecognizer} from './path-recognizer'; export class IonicRouter { constructor(config) { - this._routes = {}; + this._routes = []; this._viewCtrls = []; this.config(config); } app(app) { - this._app = app; + this.app = app; } config(config) { if (config) { - for (let routeName in config) { - this.addRoute(routeName, config[routeName]); + for (let i = 0; i < config.length; i++) { + this.addRoute(config[i]); } } } - addRoute(routeName, routeConfig) { - if (routeName && routeConfig && routeConfig.path) { - this._routes[routeName] = new Route(routeName, routeConfig); + addRoute(routeConfig) { + if (routeConfig && routeConfig.path && routeConfig.component) { + let route = new Route(routeConfig); if (routeConfig.root) { - this.otherwise(routeName); + this.otherwise(route); } + this._routes.push(route); } } - load(window, ionicApp, ionicConfig) { - // create each of the state manager classes - for (let name in stateManagerClasses) { - stateManagers[name] = new stateManagerClasses[name](window, this, ionicApp, ionicConfig); - } - stateManagerClasses = {}; - - return new Promise(resolve => { - this.getCurrentPath().then(path => { - this.loadByPath(path, this.otherwise()).then(resolve); - }); - }); - } - - loadByPath(path, fallbackRoute) { - return new Promise(resolve => { - let self = this; - let activeViewCtrl = self.activeViewController(); - let matchedRoute = self.match(path) || fallbackRoute; - - function zoneLoad() { - self._app.zone().run(() => { - activeViewCtrl.push(matchedRoute.cls); - self.lastPath(matchedRoute.path); - resolve(); - }, err => { - console.error(err); - }); - } - - if (activeViewCtrl && matchedRoute) { - - if (matchedRoute.cls) { - zoneLoad(); - - } else if (matchedRoute.module) { - System.import(matchedRoute.module).then(m => { - if (m) { - matchedRoute.cls = m[matchedRoute.name]; - zoneLoad(); - } - }, err => { - console.error(err); - }); - } - } - }); - } - - getCurrentPath() { - // check each of the state managers and the one with the - // highest priority wins of knowing what path we are currently at - return new Promise(resolve => { - - let promises = []; - for (let name in stateManagers) { - promises.push(stateManagers[name].getCurrentPath()); - } - - // when all the promises have resolved then see which one wins - Promise.all(promises).then(results => { - let rtnPath = null; - let highestPriority = -1; - let state = null; - - for (let i = 0; i < results.length; i++) { - state = results[i]; - if (state.path && state.priority > highestPriority) { - rtnPath = state.path; - } - } - - resolve(rtnPath); - }); - }); - } - stateChange(type, activeView) { - if (activeView && activeView.cls) { - - let routeConfig = activeView.cls.route; - if (routeConfig) { - let matchedRoute = this.match(routeConfig.path); - - if (matchedRoute) { + // 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(matchedRoute.path, type, activeView); + stateManagers[name].stateChange(path, type, activeView); } + } + } - this.lastPath(matchedRoute.path); + } + } + + 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; } - lastPath(val) { - if (arguments.length) { - this._lastPath = val; - } - return this._lastPath; - } - - match(path) { + 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; - let highestSpecifity = 0; - for (let routeName in this._routes) { - routeMatch = this._routes[routeName].match(path); + for (let i = 0; i < this._routes.length; i++) { + route = this._routes[i]; + routeMatch = route.match(path); - if (routeMatch.match && (!matchedRoute || routeMatch.specificity > highestSpecifity)) { - matchedRoute = this._routes[routeName]; - highestSpecifity = routeMatch.specificity; + 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; - - } else if (this._otherwise) { - return this._routes[this._otherwise]; } + return this._otherwise } addViewController(viewCtrl) { this._viewCtrls.push(viewCtrl); } - activeViewController() { + viewController() { if (this._viewCtrls.length) { return this._viewCtrls[ this._viewCtrls.length - 1 ]; } @@ -185,35 +209,21 @@ let stateManagerClasses = {}; let stateManagers = {}; -export class Routable { - constructor(cls, routeConfig) { - cls.route = routeConfig; - } -} - - class Route { - constructor(name, routeConfig) { - this.name = name; - this.cls = null; + constructor(routeConfig) { util.extend(this, routeConfig); this.recognizer = new PathRecognizer(this.path); + this.specificity = this.recognizer.specificity; + + this.component.route = this; } - match(matchPath) { - let routeMatch = new RouteMatch(this, matchPath); - if (routeMatch) { - return routeMatch; - } - return false; + match(path) { + return RegExpWrapper.firstMatch(this.recognizer.regex, path); + } + + generate(params) { + return this.recognizer.generate(params); } } - -class RouteMatch { - constructor(route, matchPath) { - this.route = route; - this.specificity = route.recognizer.specificity; - this.match = RegExpWrapper.firstMatch(route.recognizer.regex, matchPath); - } -} diff --git a/ionic/routing/url-state.ts b/ionic/routing/url-state.ts new file mode 100644 index 0000000000..99572d7b70 --- /dev/null +++ b/ionic/routing/url-state.ts @@ -0,0 +1,173 @@ +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; + } + + 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);