Skip to content

Instantly share code, notes, and snippets.

@evoactivity
Last active February 16, 2024 12:23
Show Gist options
  • Save evoactivity/39fd9061a1a560b744112b6812294031 to your computer and use it in GitHub Desktop.
Save evoactivity/39fd9061a1a560b744112b6812294031 to your computer and use it in GitHub Desktop.
Eager loading urls for Ember & trailing slash
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;
}
}
/* 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