Last active
October 29, 2019 14:16
-
-
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
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 { 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