Adding Browser History and Navigation To JavaScript DOM Page Change Function - javascript

I am using JavaScript to change the pages of my website, but I am running into the issue where I can't use browser navigation to go back or forwards through the history once I have changed pages.
I have tried adding a /#pagename to the URL for each page, and a few other things, but I can't seem to find something that works. I need to be able to make sure that the browser is saving the history of the page changes I make, so it can navigate those pages with the forward and back buttons.
// Change Page Function
function ChangeContent (page) {
var pages={"homepage":{title: "homepage"},"servicespage":{title: "servicespage"}};
//Show home page
for(var homepage in pages) {
if(page!==homepage) {
document.getElementById(homepage).style.display='none';
}
else {
document.getElementById(homepage).style.display='block';
window.scrollTo(0,0);
}
}
//Show services page
for(var servicespage in pages) {
if(page!==servicespage) {
document.getElementById(servicespage).style.display='none';
}
else {
document.getElementById(servicespage).style.display='block';
window.scrollTo(0,0);
}
}
}

You should look at window.history.pushState to add history entries and window.onpopstate to react to the user travelling "backward" through the entries you've added.
When you want to "change" pages, make a call like:
window.history.pushState( stateData, title, url );
where...
stateData is some object containing data you define
title is a string name for the new state
url is the string to which the browser's url should be changed
For example, if you want to change to "servicespage" and make the url change to "/#services":
window.history.pushState( { title: "Services" }, "servicespage", "/#services" );
Note that you still have to manipulate the DOM at this point.
Then, if you want to react to the user going backward through history:
window.onpopstate = function( event ) {
var data = event.state;
// data references the first argument of 'pushState'
// do whatever showing or hiding of <div>s here
};

Related

How to determine whether the user closes browser tab or refreshes the page

I am building a two person game app using vue.js. The app uses vuex for state management and Firestore as the backend server.
If the user leaves the app by either closing the browser tab or navigating away, the games Firestore files need to be deleted. However, if the user refreshes the page, the Firestore files need to remain so that the reload process can repopulate the game.
So I need to determine if the user has refreshed the page as opposed to closing the browser or navigating away.
As shown below, in vue's created lifecycle I setup a "beforeunload" event Listener and also start my Firestore listeners
created() {
// This window event listener fires when the user
// navigates away from or closes the browser window
window.addEventListener("beforeunload", (event) => {
const isByRefresh = getUnloadInitiator();
if (!isByRefresh) {
this.stopFirestoreListeners("beforeunload");
}
// Cancel the event. This allows the user to cancel via popup. (for debug purposes)
event.preventDefault();
event.returnValue = "";
// the absence of a returnValue property on the event
// guarantees the browser unload happens
// delete event["returnValue"];
});
this.startFirestoreListeners("created");
},
The getUnloadInitiator function is shown below. This is where I need help. Right now all this function does is console.log various performance values.
function getUnloadInitiator() {
// check for feature support before continuing
if (performance.mark === undefined) {
console.log("performance.mark NOT supported");
return false;
}
console.log("===============================");
// Yes I know that performance.navigation is depreciated.
const nav = performance.navigation;
console.log("nav=", nav);
console.log("===============================");
// Use getEntriesByType() to just get the "navigation" events
var perfEntries = performance.getEntriesByType("navigation");
for (var i = 0; i < perfEntries.length; i++) {
var p = perfEntries[i];
console.log("= Navigation entry[" + i + "]=", p);
// other properties
console.log("type = " + p.type);
}
console.log("===============================");
performance.mark("beginLoop");
const entries = performance.getEntries({
name: "beginLoop",
entryType: "mark",
});
const firstEntry = entries[0];
console.log("firstEntry.type=", firstEntry.type);
console.log("===============================");
//TODO: Determine how unload was initiated
return true;
}
Below is the output from my console.logs. They are the same for refreshing the page, closing the browser tab, or navigating away. All show "reload" as the navigation type.
===============================
nav= PerformanceNavigation {type: 1, redirectCount: 0}
===============================
= Navigation entry[0]= PerformanceNavigationTiming {unloadEventStart: 25.399999976158142, unloadEventEnd: 25.69999998807907, domInteractive: 633, domContentLoadedEventStart: 633, domContentLoadedEventEnd: 633, …}
type = reload
===============================
firstEntry.type= reload
===============================
Any help on how to differentiate between refreshing the page, closing the browser tab, or navigating away would be appreciated. There must be away, because the native cancel browser popup I'm using for debug purposes differentiates between fresh and browser tab close.
Thanks
You can use a source of authority as persistence, be it firestore, local storage, or cookies. you are able to get the browser's tab ID with tab.id and compare it to an existing one should one exist.
browser.pageAction.show(tab.id);
Source: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Working_with_the_Tabs_API

AJAX navigation + window.history.pushState: "back" browser button doesn't work

I have created a litte Javascript tool to support partial loading of contents in a Plone site via AJAX (global AjaxNav object containing all functionality). Of course I'd like the browser history (back and forward buttons) to work, so I have the following little function:
var set_base_url = function (url) { // important for local links (hash only)
var head=null,
base=$('html base').first();
if (base) {
base.attr('href', url);
log('set base[href] to '+url);
} else {
$(document).add('<base>').attr('href', url);
log('created <base> and set href to '+url);
}
var history_and_title = function (url, title, data) {
var stateObj = { // can be anything which is serializable, right?
cnt: increased_state_counter()
};
if (typeof data.uid !== 'undefined') {
stateObj['uid'] = data.uid;
}
if (title) {
if (AjaxNav.options.development_mode) {
title = '(AJAX) ' + title;
}
document.title = title;
}
if (url) {
set_base_url(url);
window.history.pushState(stateObj, '', url);
} else {
window.history.pushState(stateObj, '');
}
};
AjaxNav.history_and_title = history_and_title;
This function is called and doesn't yield any errors (none I could spot, at least); but when I try to go back by clicking the "back" browser button or hitting the backspace key, only the visible url changes, but no content is reloaded. I'd accept full-page reloads for now, but of course reloading pages from the history via AJAX would be even better.
Is there any obvious error?
The whole thing is a little bit lengthy (~ 850 lines, currently) because there often is no way to know whether the target URL specifies an object or it's view method; thus I try up to two URs per hyperlink, and then do the processing (replacing contents, setting the title, event.preventDefault() and the like), or simply return true to load the page as a whole.
Your missing the popstate event listener. While pushState pushes a new history entry to the collection, clicking the back button will pop a state from the history collection. Just like with an array. The popstate event is set to the window object and has access to the state object that you set in the pushState function.
It is useful to give the state object information on what it should do on the current page. For example, what data you have to fetch to load the current page.
Try the snippet below in your code and see what it outputs.
window.addEventListener('popstate', event => {
const { state } = event;
console.log(state);
});

URL routing not working when user clicks a link

I am trying to modify a web application we have and I'm not sure if I can do what is being requested. My boss wants to be able to click a link from an email and have our internal company web application go straight to a page identified at the end of a provided URL.
If I click on the link below the first time, it goes to the index page of our web application. If I leave the web application open and click on the link again, it goes to the correct page identified at the end of the URL.
http://mycompanyweb.com/handbook/mycompanyprocess/#/rt/softwate/9.13_UpdateSDP
I've tried adding an init(), thinking that is where the application goes first in the lifecycle and I only see this part of the URL at that point (http://mycompanyweb.com/handbook/mycompanyprocess/). This leads me to believe that the browser is stripping everything off after the # when it first opens. Is that correct? Is there something I can do to get our web application to go directly to the document the first time a user clicks on the link, without the web application open?
http://mycompanyweb.com/handbook/mycompanyprocess/ - Base URL
#/rt - Used by our javascript engine to determine which path to take
(dev or production).
/software/9.13_UpdateSDP - Logical path to a web page named 6.034_UpdateSDP.htm
Our engine that determines where to route based on the URL. I assume that the second time a link is clicked that it goes to the correct page is because the engine has been loaded (provided the browser is left open when clicked a second time).
$(document).ready(function () {
// Define the routes.
Path.map("#/:program").to(function () {
var program = this.params['program'];
if (program == "search") {
$("#mainContent").load("search.html");
}
else {
$("#mainContent").load("views/onepageprocess.html");
}
$("#subheader").html("");
$("#headerLevelTwoBreadcrumbLink").html("");
}).enter(setPageActions);
Path.map("#/:program/:swimlane").to(function () {
localStorage.removeItem("SearchString");
var swimlane = this.params['swimlane'];
var view = "views/" + swimlane + ".html";
$("#mainContent").load(view);
}).enter(setPageActions);
// Sends all links to the level three view and updates the breadcrumb.
Path.map("#/:program/:swimlane/:page").to(function () {
var page = this.params['page'];
var url = "views/levelthree/" + page.replace("", "") + ".htm";
var levelThreeTitle = "";
$.get(url)
.done(function () {
// Nothing here at this time...
}).fail(function () {
url = "views/levelthree/badurlpage.htm";
levelThreeTitle = "Page Unavailable";
}).always(function (data) {
$("#subheader").html("");
level_three_breadcrumb = "views/breadcrumbs/breadcrumb_link.html";
$("#headerLevelTwoBreadcrumbLink").load(level_three_breadcrumb);
$("#headerLevelThreeBreadcrumb").html("");
$('#headerLevelThreeBreadcrumb').append('<img src="images/Chevron.gif" />');
if (data.status != "404") {
$("#headerLevelThreeBreadcrumb").append(retrieveStorageItem("LevelThreeSubheader"));
}
$("#mainContent").load(url);
});
}).enter(setPageActions);
// Set a "root route". User will be automatically re-directed here. The definition
// below tells PathJS to load this route automatically if one isn't provided.
Path.root("#/rt");
// Start the path.js listener.
Path.listen();
});
Is there something I can do to get our web application to go directly to the document the first time a user clicks on the link, without the web application open?
If anyone runs into something like this, I found out that my company's servers were stripping anything after the # in the URL at authentication. I will be modifying my app to not use hash tags in the URL to fix it.

AJAX content and browser navigation buttons

Hi have the following jQuery event:
listItems.on('click', function(ev) {
ev.preventDefault();
reload = true;
var url = jQuery(this).attr('data-url');
history.pushState({}, '', url);
...
}
Which load dynamic content via AJAX and pushes history state to modify URL in browser from www.domain.com to www.domain.com/page-x. The only reason im loading content via AJAX is that i need to animate page transitions. If you go to www.domain.com/page-x you get normal HTML page.
The problem occurs if after clicking listItem user clicks back button on it's browser. The url changes back from www.domain.com/page-x to www.domain.com, but the page doesn't reload. Thats why i added this event:
window.onpopstate = function() {
if (reload == true) {
reload = false;
location.reload();
}
}
It reloads page after url has changed, if current page was loaded via AJAX. It's working fine, but now i have other problem:
User in frontpage clicks list item;
Browser URL changes, content is loaded via AJAX and animated;
User clicks BACK browser button;
Browser URL changes to previous and page is reloaded;
User clicks FORWARD browser button, URL changes but nothing happens;
Any ideas/solutions would be highly appreciated.
Rather than using reload=true you should rely on event.state so that when popstateoccurs you get a snapshot of what you recorded for that URL.
Manipulating the browser history (MDN)
For example:
listItems.on('click', function(ev) {
ev.preventDefault();
var url = jQuery(this).attr('data-url');
history.pushState({ action: 'list-item-focused' }, '', url);
...
}
And then:
window.onpopstate = function(e) {
/// this state object will have the action attribute 'list-item-focused'
/// when the user navigates forward to the list item. Meaning you can
/// do what you will at this point.
console.log(e.state.action);
}
You probably should avoid a full page reload when the user hits back, and instead animate your content back that used to be there, that way you aren't trashing the page. You can then tell the difference between the landing page and the list pages by checking for event.state.action and code your responses accordingly.
window.onpopstate = function(e) {
if ( e.state.action == 'list-item-focused') {
/// a list item is focused by the history item
}
else {
/// a list item isn't focused, so therefore landing page
/// obviously this only remains true whilst you don't
/// make further pushed states. If you do, you will have to
/// extend your popstate to take those new actions into account.
}
}
I'm sure you are aware of this, but pushState isn't fully cross-browser, so you should also anticipate what could happen for users if these methods aren't supported.
further enhancements
Also, as you are using jQuery, it makes it quite easy for your to store further useful information in the state object, that may help you enhance your reactions in popstate:
history.pushState({
action: 'list-item-focused',
listindex: jQuery(this).index()
});
You have to bear in mind that any data you store is serialised, meaning that it will most likely be converted to some form of non-interactive string or binary data. This means you can't store references to elements or other "live" instances; hence the fact I'm storing the list items index instead.
With the above you now know what action was occurring and on what list item, which you can retrieve at the other end by using:
listItems.eq(event.state.listindex)
what to do onload?
MDN has this to say about what you should do onload.
Reading the current state
When your page loads, it might have a non-null state object. This can happen, for example, if the page sets a state object (using pushState() or replaceState()) and then the user restarts her browser. When your page reloads, the page will receive an onload event, but no popstate event. However, if you read the history.state property, you'll get back the state object you would have gotten if a popstate had fired.
You can read the state of the current history entry without waiting for a popstate event using the history.state property like this:
Put simply they are just recommending that you should listen out for the current history.state when the page loads, and act accordingly based on what your state.action describes. This way you support both events that are triggered within a page's life-time i.e. popstate and when a user directly jumps to a history state which causes a page load.
So with the above in mind it would probably be best to structure things like so:
window.onpopstate = function(e) {
reactToState(e.state);
}
window.onload = function(){
history.state && reactToState(history.state);
}
var reactToState = function(state){
if ( state.action == 'list-item-focused') {
/// a list item is focused by the history item
}
else {
/// a list item isn't focused, so therefore landing page
/// obviously this only remains true whilst you don't
/// make further pushed states. If you do, you will have to
/// extend your popstate to take those new actions into account.
}
}
I've used inline event listeners for simplicity and because your examples do too, however it would be advised to use the addEventListener and attachEvent (IE only) methods instead... or better still, because you are using jQuery.
jQuery(window)
.on('popstate', function(e) {
reactToState(e.originalEvent.state);
}
.on('load', function(){
history.state && reactToState(history.state);
}
;
switching states
Obviously in order to move between two states, you need to know what both those states are; this means having some way to record your current state — so you can compare with the new state and act accordingly. As you only have two states this may not be imperative, but I'd urge you to always be thinking forward about the possibility of having more complexity than you currently have.
I do not know the layout of your code, so it makes it tricky recommending where you should place variables and other such items. However, no matter how you store the information, it doesn't change the fact that it will make your life and code easier if you do:
var reactToState = function(state){
var currentState = reactToState.currentState || {};
if ( !currentState.action ) { currentState.action = 'start'; }
if ( state.action == 'list-item-focused') {
if ( currentState.action == 'start' ) {
/// here we know we are shifting from the start page to list-item
}
}
else {
if ( currentState.action == 'list-item-focused' ) {
/// here we know we are shifting from the list-item to the start
}
}
/// as the state will be global for your application there is no harm
/// in storing the current state on this function as a "static" attribute.
reactToState.currentState = state;
}
Better yet, if you're not averse to switch statements, you can make the above more readable:
var reactToState = function(state){
/// current state with fallback for initial state
var currentState = reactToState.currentState || {action: 'start'};
/// current to new with fallback for empty action i.e. initial state
var a = (currentState.action||'start');
var b = (state.action||'start');
switch ( a + ' >> ' + b ) {
case 'start >> list-item-focused':
/// animate and update here
break;
case 'list-item-focused >> start':
/// animate and update here
break;
}
/// remember to store the current state again
reactToState.currentState = state;
}

Ajax with history.pushState and popstate - what do I do when popstate state property is null?

I'm trying out the HTML5 history API with ajax loading of content.
I've got a bunch of test pages connected by relative links. I have this JS, which handles clicks on those links. When a link is clicked the handler grabs its href attribute and passes it to ajaxLoadPage(), which loads content from the requested page into the content area of the current page. (My PHP pages are set up to return a full HTML page if you request them normally, but only a chunk of content if ?fragment=true is appended to the URL of the request.)
Then my click handler calls history.pushState() to display the URL in the address bar and add it to the browser history.
$(document).ready(function(){
var content = $('#content');
var ajaxLoadPage = function (url) {
console.log('Loading ' + url + ' fragment');
content.load(url + '?fragment=true');
}
// Handle click event of all links with href not starting with http, https or #
$('a').not('[href^=http], [href^=https], [href^=#]').on('click', function(e){
e.preventDefault();
var href = $(this).attr('href');
ajaxLoadPage(href);
history.pushState({page:href}, null, href);
});
// This mostly works - only problem is when popstate happens and state is null
// e.g. when we try to go back to the initial page we loaded normally
$(window).bind('popstate', function(event){
console.log('Popstate');
var state = event.originalEvent.state;
console.log(state);
if (state !== null) {
if (state.page !== undefined) {
ajaxLoadPage(state.page);
}
}
});
});
When you add URLs to the history with pushState you also need to include an event handler for the popstate event to deal with clicks on the back or forward buttons. (If you don't do this, clicking back shows the URL you pushed to history in the address bar, but the page isn't updated.) So my popstate handler grabs the URL saved in the state property of each entry I created, and passes it to ajaxLoadPage to load the appropriate content.
This works OK for pages my click handler added to the history. But what happens with pages the browser added to history when I requested them "normally"? Say I land on my first page normally and then navigate through my site with clicks that do that ajax loading - if I then try to go back through the history to that first page, the last click shows the URL for the first page, but doesn't load the page in the browser. Why is that?
I can sort of see this has something to do with the state property of that last popstate event. The state property is null for that event, because it's only entries added to the history by pushState() or replaceState() that can give it a value. But my first loading of the page was a "normal" request - how come the browser doesn't just step back and load the initial URL normally?
This is an older question but there is a much simpler answer using native javascript for this issue.
For the initial state you should not be using history.pushState but rather history.replaceState.
All arguments are the same for both methods with the only difference is that pushState creates a NEW history record and thus is the source of your problem. replaceState only replaces the state of that history record and will behave as expected, that is go back to the initial starting page.
I ran into the same issue as the original question. This line
var initialPop = !popped && location.href == initialURL;
should be changed to
var initialPop = !popped;
This is sufficient to catch the initial pop. Then you do not need to add the original page to the pushState. i.e. remove the following:
var home = 'index.html';
history.pushState({page:home}, null, home);
The final code based on AJAX tabs (and using Mootools):
if ( this.supports_history_api() ) {
var popped = ('state' in window.history && window.history.state !== null)
, changeTabBack = false;
window.addEvent('myShowTabEvent', function ( url ) {
if ( url && !changingTabBack )
setLocation(url);
else
changingTabBack = false;
//Make sure you do not add to the pushState after clicking the back button
});
window.addEventListener("popstate", function(e) {
var initialPop = !popped;
popped = true;
if ( initialPop )
return;
var tabLink = $$('a[href="' + location.pathname + '"][data-toggle*=tab]')[0];
if ( tabLink ) {
changingTabBack = true;
tabLink.tab('show');
}
});
}
I still don't understand why the back button behaves like this - I'd have thought the browser would be happy to step back to an entry that was created by a normal request. Maybe when you insert other entries with pushState the history stops behaving in the normal way. But I found a way to make my code work better. You can't always depend on the state property containing the URL you want to step back to. But stepping back through history changes the URL in the address bar as you would expect, so it may be more reliable to load your content based on window.location. Following this great example I've changed my popstate handler so it loads content based on the URL in the address bar instead of looking for a URL in the state property.
One thing you have to watch out for is that some browsers (like Chrome) fire a popstate event when you initially hit a page. When this happens you're liable to reload your initial page's content unnecessarily. So I've added some bits of code from the excellent pjax to ignore that initial pop.
$(document).ready(function(){
// Used to detect initial (useless) popstate.
// If history.state exists, pushState() has created the current entry so we can
// assume browser isn't going to fire initial popstate
var popped = ('state' in window.history && window.history.state !== null), initialURL = location.href;
var content = $('#content');
var ajaxLoadPage = function (url) {
console.log('Loading ' + url + ' fragment');
content.load(url + '?fragment=true');
}
// Handle click event of all links with href not starting with http, https or #
$('a').not('[href^=http], [href^=https], [href^=#]').on('click', function(e){
e.preventDefault();
var href = $(this).attr('href');
ajaxLoadPage(href);
history.pushState({page:href}, null, href);
});
$(window).bind('popstate', function(event){
// Ignore inital popstate that some browsers fire on page load
var initialPop = !popped && location.href == initialURL;
popped = true;
if (initialPop) return;
console.log('Popstate');
// By the time popstate has fired, location.pathname has been changed
ajaxLoadPage(location.pathname);
});
});
One improvement you could make to this JS is only to attach the click event handler if the browser supports the history API.
I actually found myself with a similar need today and found the code you provided to be very useful. I came to the same problem you did, and I believe all that you're missing is pushing your index file or home page to the history in the same manner that you are all subsequent pages.
Here is an example of what I did to resolve this (not sure if it's the RIGHT answer, but it's simple and it works!):
var home = 'index.html';
history.pushState({page:home}, null, home);
Hope this helps!
I realize this is an old question, but when trying to manage state easily like this, it might be better to take the following approach:
$(window).on('popstate',function(e){
var state = e.originalEvent.state;
if(state != null){
if(state.hasOwnProperty('window')){
//callback on window
window[state.window].call(window,state);
}
}
});
in this way, you can specify an optional callback function on the state object when adding to history, then when popstate is trigger, this function would be called with the state object as a parameter.
function pushState(title,url,callback)
{
var state = {
Url : url,
Title : title,
};
if(window[callback] && typeof window[callback] === 'function')
{
state.callback = callback;
}
history.pushState(state,state.Title,state.Url);
}
You could easily extend this to suit your needs.
And Finally says:
I'd have thought the browser would be happy to step back to an entry that was created by a normal request.
I found an explanation of that strange browser's behavior here. The explanation is
you should save the state when your site is loaded the first time and thereafter every time it changes state
I tested this - it works.
It means there is no need in loading your content based on window.location.
I hope I don't mislead.

Categories

Resources