From: Eduardo San Martin Morote Date: Fri, 28 Jun 2019 09:29:15 +0000 (+0200) Subject: feat: handle initial navigation X-Git-Tag: v4.0.0-alpha.0~333 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f9fef785fb1efb49b5b3abedd11e35d397ac1007;p=thirdparty%2Fvuejs%2Frouter.git feat: handle initial navigation --- diff --git a/src/history/html5.ts b/src/history/html5.ts index 661b2d5f..2e10e447 100644 --- a/src/history/html5.ts +++ b/src/history/html5.ts @@ -51,7 +51,7 @@ export class HTML5History extends BaseHistory { constructor() { super() - const to = buildFullPath() + const to = this.createCurrentLocation() // cs.log('created', to) this.history.replaceState(buildState(null, to, null), '', to.fullPath) this.location = to @@ -157,7 +157,7 @@ export class HTML5History extends BaseHistory { const from = this.location // we have the state from the old entry, not the current one being removed // TODO: correctly parse pathname - const to = state ? state.current : buildFullPath() + const to = state ? state.current : this.createCurrentLocation() this.location = to if ( @@ -194,14 +194,14 @@ export class HTML5History extends BaseHistory { to, } } -} -const buildFullPath = () => { - const { location } = window - return { - fullPath: location.pathname + location.search + location.hash, - path: location.pathname, - query: {}, // TODO: parseQuery - hash: location.hash, + createCurrentLocation(): HistoryLocationNormalized { + const { location } = window + return { + fullPath: location.pathname + location.search + location.hash, + path: location.pathname, + query: this.utils.parseQuery(location.search), + hash: location.hash, + } } } diff --git a/src/index.ts b/src/index.ts index 5d5afbd3..8c98689b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ const plugin: PluginFunction = Vue => { // @ts-ignore this._routerRoot = this // @ts-ignore - this._router = this.$options.router + this._router = this.$options.router as Router // this._router.init(this) // @ts-ignore this._router.app = this @@ -28,6 +28,9 @@ const plugin: PluginFunction = Vue => { // undefined, // true ) + + // @ts-ignore + this._router.doInitialNavigation() } else { // @ts-ignore this._routerRoot = (this.$parent && this.$parent._routerRoot) || this diff --git a/src/router.ts b/src/router.ts index eb789946..68877714 100644 --- a/src/router.ts +++ b/src/router.ts @@ -32,6 +32,8 @@ export interface RouterOptions { type ErrorHandler = (error: any) => any +// resolve, reject arguments of Promise constructor +type OnReadyCallback = [() => void, (reason?: any) => void] export class Router { protected history: BaseHistory private matcher: RouterMatcher @@ -42,6 +44,8 @@ export class Router { private app: any // TODO: should these be triggered before or after route.push().catch() private errorHandlers: ErrorHandler[] = [] + private ready: boolean = false + private onReadyCbs: OnReadyCallback[] = [] constructor(options: RouterOptions) { this.history = options.history @@ -217,7 +221,7 @@ export class Router { } // TODO: should we throw an error as the navigation was aborted - // TODO: needs a proper check because order could be different + // TODO: needs a proper check because order in query could be different if ( this.currentRoute !== START_LOCATION_NORMALIZED && this.currentRoute.fullPath === url.fullPath @@ -409,4 +413,82 @@ export class Router { Object.defineProperty(route, 'matched', { enumerable: false }) this.app._route = Object.freeze(route) } + + /** + * Returns a Promise that resolves once the router is ready to be used for navigation + * Eg: Calling router.push() or router.replace(). This is necessary because we have to + * wait for the Vue root instance to be created + */ + onReady(): Promise { + if (this.ready) return Promise.resolve() + return new Promise((resolve, reject) => { + this.onReadyCbs.push([resolve, reject]) + }) + } + + /** + * Mark the router as ready. This function is used internal and should not be called + * by the developper. You can optionally provide an error. + * This will trigger all onReady callbacks and empty the array + * @param {*} err + */ + protected markAsReady(err?: any): void { + if (this.ready) return + for (const [resolve, reject] of this.onReadyCbs) { + if (err) reject(err) + else resolve() + } + this.onReadyCbs = [] + this.ready = true + } + + async doInitialNavigation(): Promise { + // TODO: refactor code that was duplicated from push method + const toLocation: RouteLocationNormalized = this.resolveLocation( + this.history.location, + this.currentRoute + ) + + this.pendingLocation = toLocation + // trigger all guards, throw if navigation is rejected + try { + await this.navigate(toLocation, this.currentRoute) + } catch (error) { + if (error instanceof NavigationGuardRedirect) { + // push was called while waiting in guards + if (this.pendingLocation !== toLocation) { + // TODO: trigger onError as well + throw new NavigationCancelled(toLocation, this.currentRoute) + } + // TODO: setup redirect stack + return this.push(error.to) + } else { + // TODO: write tests + // triggerError as well + if (this.pendingLocation !== toLocation) { + // TODO: trigger onError as well + throw new NavigationCancelled(toLocation, this.currentRoute) + } + + this.triggerError(error) + } + } + + // push was called while waiting in guards + if (this.pendingLocation !== toLocation) { + throw new NavigationCancelled(toLocation, this.currentRoute) + } + + // NOTE: here we removed the pushing to history part as the history + // already contains current location + + const from = this.currentRoute + this.currentRoute = toLocation + this.updateReactiveRoute() + + // navigation is confirmed, call afterGuards + for (const guard of this.afterGuards) guard(toLocation, from) + + return this.currentRoute + } }