Skip to content

Instantly share code, notes, and snippets.

@estruyf
Last active October 29, 2019 14:16
Show Gist options
  • Save estruyf/d0c118301c7ae2f001e065ad8544974f to your computer and use it in GitHub Desktop.
Save estruyf/d0c118301c7ae2f001e065ad8544974f to your computer and use it in GitHub Desktop.
Application customizer with abort controller implementation to abort API calls during navigation events
import { override } from '@microsoft/decorators';
import { BaseApplicationCustomizer } from '@microsoft/sp-application-base';
import { SPEventArgs, Guid } from '@microsoft/sp-core-library';
import { SPHttpClient } from '@microsoft/sp-http';
import { cloneDeep } from '@microsoft/sp-lodash-subset';
interface NavigationEventDetails extends Window {
isNavigatedEventSubscribed: boolean;
currentPage: string;
currentHubSiteId: string;
currentUICultureName: string;
}
declare const window: NavigationEventDetails;
/** A Custom Action which can be run during execution of a Client Side Application */
export default class NavigationEventApplicationCustomizer extends BaseApplicationCustomizer<{}> {
private static abortController: AbortController = null;
private pushState: () => any = null;
private isNewPage: boolean = false;
private crntCallId: string = null;
@override
public async onInit(): Promise<void> {
console.log("onInit called:", window.location.pathname);
// Initialize the abort controller for the fetch calls
this.initAbortController();
// Bind into the browser history
this.bindToHistory();
// Bind the navigation event
if (!window.isNavigatedEventSubscribed) {
window.isNavigatedEventSubscribed = true;
this.context.application.navigatedEvent.add(this, this.navigationEventHandler);
}
}
@override
public async onDispose(): Promise<void> {
this.context.application.navigatedEvent.remove(this, this.render);
// Abort all calls
this.abortFetchRequests();
// Unset all the values
window.isNavigatedEventSubscribed = false;
window.currentPage = '';
window.currentHubSiteId = '';
window.currentUICultureName = '';
}
private async render() {
window.currentPage = window.location.href;
// window.currentHubSiteId = HubSiteService.getHubSiteId();
window.currentUICultureName = this.context.pageContext.cultureInfo.currentUICultureName;
console.log("NavigationEventApplicationCustomizer render", window.location.pathname);
this.crntCallId = Guid.newGuid().toString();
console.log(`BEFORE THE CALL: ${this.crntCallId}`);
const data = await this.getCurrentPage(this.crntCallId);
console.log(`AFTER THE CALL: Is call ID the same? ${(this.crntCallId === data.crntCallId).toString()}
- ${data.crntCallId}`);
}
private async getCurrentPage(crntCallId: string) {
const { pageContext } = this.context;
// Fetch the new page title
const restApi = `${pageContext.web.absoluteUrl}/_api/lists(guid'${pageContext.list.id.toString()}')/Items(${pageContext.listItem.id})?$select=Title`;
return await this.performFetchCall(restApi, crntCallId);
}
private async performFetchCall(restApi, crntCallId): Promise<{data: any, crntCallId: string}> {
try {
const { spHttpClient } = this.context;
// Retrieve the metadata for the current page
const data = await spHttpClient.get(restApi, SPHttpClient.configurations.v1, {
signal: this.getAbortSignal() // Add the abort controller signal here
});
if (data && data.ok) {
const pageData = await data.json();
return {
crntCallId,
data: pageData
};
}
return null;
} catch (e) {
if (e.name === "AbortError") {
console.error("Fetch was aborted by the navigation control");
} else {
console.error("Another error occurred");
}
return null;
}
}
/**
* Initializes the abort controller
*/
private initAbortController() {
if (AbortController) {
NavigationEventApplicationCustomizer.abortController = new AbortController();
}
}
/**
* Abort the fetch requests
*/
private abortFetchRequests() {
if (NavigationEventApplicationCustomizer.abortController && NavigationEventApplicationCustomizer.abortController.abort) {
NavigationEventApplicationCustomizer.abortController.abort();
}
}
/**
* Retrieve the abort signal
*/
private getAbortSignal() {
if (NavigationEventApplicationCustomizer.abortController && NavigationEventApplicationCustomizer.abortController.signal) {
return NavigationEventApplicationCustomizer.abortController.signal;
}
}
/**
* Navigation event handler
*
* @param args
*/
private navigationEventHandler(args: SPEventArgs): void {
setTimeout(() => {
if (window.currentHubSiteId !== HubSiteService.getHubSiteId()) {
this.onDispose();
this.onInit();
return;
}
if (window.currentUICultureName !== this.context.pageContext.cultureInfo.currentUICultureName) {
// Trigger a full page refresh to be sure to have to correct language loaded
location.reload();
return;
}
if (window.currentPage !== window.location.href) {
console.log("NavigationEventHandler: Trigger render again, as page was cached");
--this.nrOfTimes;
this.abortFetchRequests();
this.initAbortController();
this.render();
}
}, 50);
this.render();
}
/**
* Bind the control to the browser history to do metadata check when page is in edit mode
*/
private bindToHistory(): void {
// Only need to bind the pushState once to the history
if (!this.pushState) {
// Binding to page mode changes
this.pushState = () => {
const defaultPushState = history.pushState;
// We need the current this context to update the component its state
const self = this;
return function (data: any, title: string, url?: string | null) {
console.log("bindToHistory:", url);
// Check if you navigated to a new page creation
if (url.indexOf("newpage.aspx") !== -1) {
// Check if the page is in edit mode
if (url.toLowerCase().indexOf('mode=edit') !== -1) {
self.isNewPage = true;
} else if (self.isNewPage) {
// Page is created
self.handleNewPageReload();
}
} else if (self.isNewPage) {
// Page is created
self.handleNewPageReload();
}
// Call the original function with the provided arguments
// This context is necessary for the context of the history change
return defaultPushState.apply(this, [data, title, url]);
};
};
history.pushState = this.pushState();
}
}
/**
* New page reload handler
*/
private handleNewPageReload(count: number = 0) {
this.isNewPage = false;
const crntHref = location.href;
console.log("handleNewPageReload:", crntHref);
if (crntHref.indexOf("newpage.aspx") !== -1 || crntHref.indexOf("mode=edit") !== -1) {
if (count < 5) {
setTimeout(() => {
this.handleNewPageReload(++count);
}, 50);
}
} else {
// The page is ready to be reloaded
location.reload();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment