Pure Javascript to detect SPA DOM ready and page change - javascript

I need to fire a function when the DOM ready or page view changed on single-page applications, here is my code and it works for the first time the user visits the site (DOM ready), but it didn't work when a user switches SPA pages (view changed).
I need a common method for most websites and SPA projects since this code needs to be executed on each of our client's websites(sort of like Google Analytics tracking code), and this code need executed every time when the end-users load or switch the pages. Is there any pure Javascript that can detect when the SPA changes pages?
function docReady(fn) {
if (document.readyState === "complete" || document.readyState === "interactive") {
setTimeout(fn, 1);
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
docReady(function() {
_AcodeInit();
});
function _AcodeInit()
{
...
}

Option 1: Listening to Changes in URL
As SPAs use routing, you could ideally listen to url changes.
One crude way of doing this is to keep polling using setInterval and track the changes to the url. If the url has changed you could run your handler. However this is wasteful.
Given the lack of options you can listen to all events which could cause transitions, (e.g. mouse clicks, keyboard Enter/Space key press etc.) and in the handler check if the url has changed. Use requestAnimationFrame so that we don't execute handler prematurely.
let currentUrl = location.href;
const checkPageTransition = () => {
requestAnimationFrame(() => {
if (currentUrl !== location.href) {
console.log("url changed");
}
currentUrl = location.href;
}, true);
};
document.body.addEventListener("click", checkPageTransition);
document.body.addEventListener("keyup", e => {
if (e.code === "Enter" || e.code === "Space") checkPageTransition()
});
Option 2: Listening to popstate event
If you are using modern SPAs, they would most likely be using history.pushState and history.popState to manage routing. We could then listen to the window.popstate event. But there are limitations to this event.
From: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onpopstate
Note: Calling history.pushState() or history.replaceState() won't trigger a popstate event. The popstate event is only triggered by performing a browser action, such as clicking on the back button (or calling history.back() in JavaScript), when navigating between two history entries for the same document.
However we can monkey patch the history.pushState function so that it always fires the history.onpushstate function.
(function(history) {
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function")
{
history.onpushstate({
state: state
});
}
return pushState.apply(history, arguments);
}
})(window.history);
You would need to similarly patch the replaceState function as well.

Related

In Javascript, is there a way to know the path is changed?

I am wondering, if there is a way to know(using events) that the path is changed? I looked into both onpopstate & onhashchange. Both are not what I am looking for. My current requirements are, in a single page application:
When the url is changed from one to something else, I want a function to be called.
When the page is loaded initially, the function for the current page to be called.
Are these possible with native API's in the browser?
Listen for the load event, which fires when the page has finished loading:
function code(){
console.log('page loaded')
}
window.addEventListener('load', code)
To listen for pushState changes, you can override the default pushState behavior:
function trigger() {
console.log('state change')
}
var pushState = history.pushState;
history.pushState = function(state) {
if (typeof history.onpushstate == "function") {
history.onpushstate({state: state});
}
trigger()
return pushState.apply(history, arguments);
};
history.pushState(null, null, '/endpoint')

Disable browser history stack or force reload on back()/forward()

I have a situation where I need to force a reload() on the server-side for a timer.
The issue we're encountering is the browser history stack allows users to navigate back and forth without hitting the server and we're losing the time spent on a tracked task. Kind of hacky, I know.
I thought something like this might be a start (it isn't working):
window.addEventListener('pageload', function () {
console.log('Page loaded via back/for button');
});
The caveat, is I can only reload() when the browser back/forward functions were used. The URI will change constantly via the UI, in which case it already invokes a request to the server.
EDIT | I tried this as well (capture the back/next event and force a reload or AJAX request to server to capture context change)
(function($) {
window.addEventListener('popstate', function(event) {
console.log(event);
});
// Second solution...
window.addEventListener('popstate', function () {
console.log('URI changed');
});
const pushUrl = (href) => {
history.pushState({}, '', href);
window.dispatchEvent(new Event('popstate'));
};
})(jQuery);
Also not working, what am I missing?
Any ideas?

Need something earlier than a page unload?

I have set up an unload listener which sets a flag that error handlers from an ajax request check.
jQuery(window).unload(function() {
unloadhappening = true;
});
However, the ajax request can be aborted (when the user navigates to another page) and the error handler for the ajax request invoked before the unload event is fired.
I was wondering could I get an event earlier than unload? Obviously I could put a listener on every link to move from the page but was looking for a neater way if there is one.
Thanks
You could probably use onbeforeunload or $(window).on('beforeunload') but return an empty string from the function to prevent the prompt about navigation.
window.onbeforeunload = function(e) {
unloadhappening = true;
// maybe other logic
return ''; // or maybe return null
}
I haven't tested the solution to avoid the popup box in all browsers, so your milage may vary.

Chrome extension, javascript: Why is this firing twice?

I have a very very simple bit of code in my (test) Chrome extension:
function test()
{
alert("In test!");
}
chrome.tabs.onUpdated.addListener(function(tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined) {
test();
}
});
My question is, why is test() firing twice? And more importantly, how do I make it fire just once?
Have a look at what the different states are when the event is dispatched. I presume, that it is getting dispatched once when the state is "loading" or when the state is "complete". If that is the case, then your problem would be fixed with:
function test()
{
alert("In test!");
}
chrome.tabs.onUpdated.addListener(function(tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined && changeinfo.status == "complete") {
test();
}
});
I was also confused by this and tried to find the answer. Only after some experimentation did I figure out why I was receiving multiple "complete" update events for what I thought was a single page "update".
If your page has iframes, each will trigger a "complete" event and bubble up to the parent content script. So more complex pages will trigger a slew of onUpdated events if they have iframes.
When you write the following code:
chrome.tabs.onUpdated.addListener(function(tabid, changeinfo, tab) {
var url = tab.url;
if (url !== undefined) {
test();
}
});
You're calling addListener and telling it to call test() not immediately but rather whenever the tab is updated. Tab update events are broadcast by the Chrome browser itself, which in turn, causes your test() code to run.
I know this is old but anyway... it's happening to me too and maybe this can help someone. Looking into the Tab object that the handler function is receiving, I saw that the favIconUrl property is different in both calls so I guess it has something to do with that although I have no idea about the reason behind this.
I thought it was a bug but after a second thought and some testing I discard the bug theory.
What I know so far is that if two properties are changed, the event is triggered twice with the changeInfo object containing one property each time. In other words if for instance the properties that change are status and favIconUrl, then
changeInfo = {status:"complete"};
and then in the next call
changeInfo = {favIconuUrl : ".... whatever ..."};
Although I'm not sure why our code is firing twice, I had a similar problem and this answer helped me out a lot.
https://stackoverflow.com/a/49307437/15238857
Vaibhav's answer was really smart in incorporating validation in the form of the details object of the request.
When logging the details object I noticed a trend that the original firing had a frameId of 0, and the second firing was always something else. So instead of positively validating that you're in the correct submission of the URL, I like using an if statement to return and bail out when you have the duplicate entry.
I'm using onCompleted here, but it should work for onUpdated as well.
chrome.webNavigation.onCompleted.addListener(details => {
//need to make sure that this only triggers once per navigation
console.log(details);
if (details.frameId !== 0) return;
});
I solved this problem by checking the title of the tab when updated:
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
var title = changeInfo.title;
if (title !== undefined) {
doSomething();
}
});
Twice is due to status being loading once and status being completed once.
if you are unconcerned about the status, you may also try the onCreated event. That will be fired just once!

How to hook up default beforeunload event on an aspx page?

I have a custom web part placed on one of the application page in SharePoint. This page it seems already have a function which gets executed on windows beforeunload Javascript event.
My problem is that I too need to execute some client side code (to prompt user for any unsaved changes in my web part) on windows beforeunload event.
How can I achieve this? I mean let the default event be fired as well as call my function also?
Appreciate any help.
This should be possible by checking for an existing handler assigned to the onbeforeunload event and, if one exists, saving a reference to it before replacing with your own handler.
Your web part might emit the following script output to do this:
<script type="text/javascript">
// Check for existing handler
var fnOldBeforeUnload = null;
if (typeof(window.onbeforeunload) == "function") {
fnOldBeforeUnload = window.onbeforeunload;
}
// Wire up new handler
window.onbeforeunload = myNewBeforeUnload;
// Handler
function myNewBeforeUnload() {
// Perform some test to determine if you need to prompt user
if (unsavedChanges == true) {
if (window.confirm("You have unsaved changes. Click 'OK' to stay on this page.") == true) {
return false;
}
}
// Call the original onbeforeunload handler
if (fnOldBeforeUnload != null) {
fnOldBeforeUnload();
}
}
</script>
This should allow you to inject your own logic into the page and make your own determination as to what code executes when the page unloads.

Categories