Last active
February 16, 2024 12:23
-
-
Save evoactivity/39fd9061a1a560b744112b6812294031 to your computer and use it in GitHub Desktop.
Eager loading urls for Ember & trailing slash
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import EmberObject from '@ember/object'; | |
declare module '@ember/routing/history-location' { | |
// More info about interface over here - https://github.com/emberjs/ember.js/blob/v3.28.1/packages/%40ember/-internals/routing/lib/location/api.ts | |
export default class HistoryLocation extends EmberObject { | |
protected history: History; | |
replaceURL(url: string): void; | |
setURL(path: string): void; | |
getURL(): string; | |
formatURL(url: string): string; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable ember/no-private-routing-service */ | |
/* Based on https://github.com/raido/ember-refined-route-substate-url */ | |
import config from 'YOUR-APP/config/environment'; | |
import { debug } from '@ember/debug'; | |
import type RouteInfo from '@ember/routing/-private/route-info'; | |
import HistoryLocation from '@ember/routing/history-location'; | |
import type { RouteModel } from '@ember/routing/router-service'; | |
import type RouterService from '@ember/routing/router-service'; | |
import type Transition from '@ember/routing/transition'; | |
import { service } from '@ember/service'; | |
const SUBSTATE_ROUTE_SUFFIX = { | |
LOADING: 'loading', | |
ERROR: 'error', | |
} as const; | |
type RouterServiceWithPrivates = RouterService & { | |
_router: { | |
_routerMicrolib: { | |
updateURL(url: string): void; | |
}; | |
}; | |
}; | |
export type ShouldUpdateURLCallback = (path: string) => void; | |
const LOG_TAG = '[RouterTransitionAnalyzer]'; | |
const LOG_TRANSITIONS = config.APP.LOG_TRANSITIONS || config.APP.LOG_TRANSITIONS_INTERNAL || false; | |
export default class HistoryLocationTrailing extends HistoryLocation { | |
@service router!: RouterServiceWithPrivates; | |
constructor(...args: []) { | |
super(...args); | |
this.onShouldUpdateURL(this.shouldUpdateURL); | |
this.router.on('routeWillChange', this.routeWillChangeHandler); | |
this.router.on('routeDidChange', this.routeDidChangeHandler); | |
} | |
private get historyStateKey() { | |
return 'history_location_update_url_eagerly_to_destination_route'; | |
} | |
public onShouldUpdateURL(callbackToAdd: ShouldUpdateURLCallback): void { | |
this.shouldUpdateURLCallbacks.push(callbackToAdd); | |
} | |
public offShouldUpdateURL(callbackToRemove: ShouldUpdateURLCallback): void { | |
this.shouldUpdateURLCallbacks = this.shouldUpdateURLCallbacks.filter((cb) => { | |
return cb !== callbackToRemove; | |
}); | |
} | |
public setURL(path: string) { | |
if (this.history.state && this.history.state[this.historyStateKey] === true) { | |
super.replaceURL(path); | |
return; | |
} | |
super.setURL(path); | |
} | |
public formatURL(originalUrl: string) { | |
const url = super.formatURL(originalUrl); | |
if (url.includes('#')) { | |
return url.replace(/([^/])#(.*)/, '$1/#$2'); | |
} | |
if (url.includes('?')) { | |
return url.replace(/([^/])\?(.*)/, '$1/?$2'); | |
} | |
return url.replace(/\/?$/, '/'); | |
} | |
private shouldUpdateURL = (path: string) => { | |
if (this.getURL() === path) { | |
return; | |
} | |
this.router._router._routerMicrolib.updateURL(path); | |
}; | |
private shouldUpdateURLCallbacks: ShouldUpdateURLCallback[] = []; | |
private transitionQueue: Transition[] = []; | |
private routeWillChangeHandler = (transition: Transition) => { | |
if (!this.hasRouteInfoForAnalysis(transition)) { | |
return; | |
} | |
if (LOG_TRANSITIONS) debug(`${LOG_TAG}: routeWillChange -> ${transition.to.name}`); | |
this.transitionQueue.push(transition); | |
const newUrl = this.determineUrlForDestinationRoute(transition); | |
if (newUrl) { | |
if (LOG_TRANSITIONS) debug(`${LOG_TAG}: shouldUpdateCallback -> ${newUrl}`); | |
this.shouldUpdateURLCallbacks.forEach((cb) => { | |
cb(newUrl); | |
}); | |
} | |
}; | |
private routeDidChangeHandler = (transition: Transition) => { | |
this.transitionQueue = []; | |
if (!this.hasRouteInfoForAnalysis(transition)) { | |
return; | |
} | |
if (LOG_TRANSITIONS) debug(`${LOG_TAG}: routeDidChange -> ${transition.to.name}`); | |
}; | |
private determineUrlForDestinationRoute(transition: Transition): string | null { | |
if (!this.isIntermediateRoute(transition)) { | |
return null; | |
} | |
// These fail-safes here should never be triggered but just to be double sure that our queue is not messed up. | |
// We have those sanity checks to avoid possible TypeErrors. | |
const substateTransitionIndexInQueue = this.transitionQueue.indexOf(transition); | |
if (substateTransitionIndexInQueue <= 0) { | |
return null; | |
} | |
const routeBeforeIntermediateRoute = this.transitionQueue[substateTransitionIndexInQueue - 1]; | |
if (!routeBeforeIntermediateRoute) { | |
return null; | |
} | |
// Transition queue can include: "my.route, my.route.loading, my.route.error" transitions | |
// Since for the first "my.route.loading" route we already triggered shouldUpdateUrlCallbacks | |
// We bail out for error route ones, this avoids updating url to "my/route/loading". | |
if (this.isIntermediateRoute(routeBeforeIntermediateRoute)) { | |
return null; | |
} | |
const params = this.findAllRouteParamsForUrlLookup(routeBeforeIntermediateRoute); | |
const { queryParams } = transition.to; | |
let url = this.router.urlFor.apply(this.router, [routeBeforeIntermediateRoute.to.name, ...params, { queryParams }]); | |
url = url.replace(config.rootURL, '/'); | |
return url; | |
} | |
// We need to collect all parent routes params, like | |
// /route1/:id/route2/:id | |
// So we can call router.urlFor() to give us new destination route URL which we can push | |
private findAllRouteParamsForUrlLookup(transition: Transition) { | |
let route: RouteInfo | null = transition.to; | |
const params: RouteInfo['params'][] = []; | |
const finalParams: Array<RouteModel> = []; | |
if (route) { | |
const keys = Object.keys(route.params); | |
if (keys.length > 0) { | |
params.push(route.params); | |
} | |
route = route.parent; | |
} | |
params.map((obj) => { | |
Object.keys(obj).forEach((key: string) => { | |
if (typeof obj[key] !== 'undefined') { | |
finalParams.push(obj[key] as RouteModel); | |
} | |
}); | |
}); | |
finalParams.reverse(); | |
return finalParams; | |
} | |
private isIntermediateRoute(transition: Transition): boolean { | |
const routeName = transition.to.name; | |
return routeName.endsWith(SUBSTATE_ROUTE_SUFFIX.LOADING) || routeName.endsWith(SUBSTATE_ROUTE_SUFFIX.ERROR); | |
} | |
private hasRouteInfoForAnalysis(transition: Transition): boolean { | |
return !!transition.to; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment