I am trying to understand HTML5 history object. Here is a simple example which I started off with.
function addDialog(){
document.getElementById('d').style.display ='';
history.pushState({name:"changed"},"","#newURL");
}
window.onpopstate = function(e){
alert(e.state);
}
I have a div with an id d for which display property is none. On clicking a link, I will display the div and change the history so that new url will be loaded.
When I copy paste the new url, popstate event is fired and I get null for e.state.
From what I understand, if I load the new url http://example.com#newURL, e.state should point to the object which I pushed using pushstate.
Please correct me if I am wrong and also I would like to know when e.state gets populated.
As I tested, the e.state only get the pop state that you add to the history when you click in the Back or Forward button. Otherwise, it will give you the null.
You can use parameters to your URL so you can test if the request came from a history call or from an URL in the location bar.
onpopstate = function(event) {
alert('popEvent: ' + event);
if(event.state){
setupPage(event.state);
} else {
setupPage(getQStringParam('zid'));
}
}
Related
I'm following some tutorials to learn how to use the history events in JS to deal with what is essentially paging on one of my forms. I have added the following code to an onClick event that changes the page:
state = {'actionCode': 'pageChange', 'pageNum': pageNum};
window.history.pushState(state, 'Page ' + pageNum, '/subF/fileName.cfm#page' + pageNum);
console.log(state);
I tried a number of other variations originally, including blank or null title and url arguments.
I then added this to the bottom of my JS file to see what I have to work with:
function checkState(e) {
console.log(e);
console.log(history.state);
}
$(function() {
window.onpopstate = checkState;
});
What I expected to see after changing 'pages' (running the first snippet of code) and then clicking the back button was a e.state object containing actionCode and pageNum variables. Instead, I see the state appear as null even though the object itself appears to hold the data immediately after passing said object to pushState:
I get the same null value when dumping history.state, so I assume the problem is with the push and not the get, or that I'm completely misunderstanding how these functions work.
What I expected to be able to do was add code to checkState that looks at the 'actionCode' and takes appropriate action based on that, reference the variables I know will exist in the state object for that particular actionCode.
In order for the onpopstate event to get triggered, you have to perform an actual "change history" action, i.e. clicking the browser BACK/FORWARD button or manually calling history.back() / history.forward() / history.go(). Simply pushing/replacing a state won't trigger an event.
You can try this:
<script>
window.onpopstate = function(event) {
console.log(event);
};
const state1 = {'actionCode': 'pageChange', 'pageNum': 1};
const state2 = {'actionCode': 'pageChange', 'pageNum': 2};
history.pushState(state1, 'Page ' + state1.pageNum, '/subF/fileName.cfm#page' + state1.pageNum);
history.pushState(state2, 'Page ' + state2.pageNum, '/subF/fileName.cfm#page' + state2.pageNum);
history.go(-1);
</script>
Here, the invocation of history.go(-1) will load the previous page from the session history thus firing an onpopstate event and you will see the state is there.
You can learn more about the peculiarities here: MDN page.
If you're trying to simulate handling of location/state change for new entries, you'll have to manually fire a PopStateEvent(or any custom one with a respective handler).
const event = new PopStateEvent('popstate', { state: state });
window.dispatchEvent(event);
or simply:
window.dispatchEvent(new Event('popstate'));
I have a function named back() which will be used for ajax calls. Actually I have an array stack contains last 5 search results and that back function will switch to the previous result set (according to that array stack) and it even changes the URL using window.history.pushState() when you click on the back button.
That back button I was talking about, is an element inside the browser which revokes back() function. Now I want to revoke back() function also when user click on the back button of the browser. Something like this:
window.onhashchange = function() {
back(); // this function also changes the url
}
But sadly window.onhashchange will be revokes twice when I click on the back of the browser. Because window.onhashchange will be revoked when you change the URL using window.history.pushState().
Anyway, how can I detect what things changes the URL? Either my JS code or the back button of the browser?
You can use performance.navigation.type
At any given point, for example on document.onload, you can read the value of type and, if it's:
0 The page was accessed by following a link, a bookmark, a form submission, a script, or typing the URL in the address bar.
1 The page was accessed by clicking the Reload button or via the Location.reload() method.
2 The page was accessed by navigating into the history.
255 any other way.
Just beware that support is limited according to the compatibilty table.
However, from the looks of it, it seems the table is outdated. It says it is not supported on chrome and I just tested it and works as expected on my chrome version (67.0)
One of solution is to implement onunload event with localstorage option.
This is from my head maybe you will need correction but this is base !
var history = [];
window.onload = function(){
var handler;
if ( localStorage.getItem('history') == null ) {
// FIRST TIME
history[0] = window.location.href;
localStorage.setItem("history", JSON.stringify(history));
}
else {
handler = localStorage.getItem('history');
handler = JSON.parse(handler);
history = handler;
// Just compare now
if (history[history.length-1] == window.location.href) {
// no change
} else {
history.push(window.location.href);
}
}
}
window.onunload = function(){
localStorage.setItem('history', JSON.stringify(history));
}
Note :
Since 25 May 2011, the HTML5 specification states that calls to
window.alert(), window.confirm(), and window.prompt() methods may be
ignored during this event. See the HTML5 specification for more
details.
I’ve made a one page site. When user clicks on the menu buttons, content is loaded with ajax.
It works fine.
In order to improve SEO and to allow user to copy / past URL of different content, i use
function show_content() {
// change URL in browser bar)
window.history.pushState("", "Content", "/content.php");
// ajax
$content.load("ajax/content.php?id="+id);
}
It works fine. URL changes and the browser doesn’t reload the page
However, when user clicks on back button in browser, the url changes and the content have to be loaded.
I've done this and it works :
window.onpopstate = function(event) {
if (document.location.pathname == '/4-content.php') {
show_content_1();
}
else if (document.location.pathname == '/1-content.php') {
show_content_2();
}
else if (document.location.pathname == '/6-content.php') {
show_content_();
}
};
Do you know if there is a way to improve this code ?
What I did was passing an object literal to pushState() on page load. This way you can always go back to your first created pushState. In my case I had to push twice before I could go back. Pushing a state on page load helped me out.
HTML5 allows you to use data-attributes so for your triggers you can use those to bind HTML data.
I use a try catch because I didn't had time to find a polyfill for older browsers. You might want to check Modernizr if this is needed in your case.
PAGELOAD
try {
window.history.pushState({
url: '',
id: this.content.data("id"), // html data-id
label: this.content.data("label") // html data-label
}, "just content or your label variable", window.location.href);
} catch (e) {
console.log(e);
}
EVENT HANDLERS
An object filled with default information
var obj = {
url: settings.assetsPath, // this came from php
lang: settings.language, // this came from php
historyData: {}
};
Bind the history.pushState() trigger. In my case a delegate since I have dynamic elements on the page.
// click a trigger -> push state
this.root.on("click", ".cssSelector", function (ev) {
var path = [],
urlChunk = document.location.pathname; // to follow your example
// some data-attributes you need? like id or label
// override obj.historyData
obj.historyData.id = $(ev.currentTarget).data("id");
// create a relative path for security reasons
path.push("..", obj.lang, label, urlChunk);
path = path.join("/");
// attempt to push a state
try {
window.history.pushState(obj.historyData, label, path);
this.back.fadeIn();
this.showContent(obj.historyData.id);
} catch (e) {
console.log(e);
}
});
Bind the history.back() event to a custom button, link or something.
I used .preventDefault() since my button is a link.
// click back arrow -> history
this.back.on("click", function (ev) {
ev.preventDefault();
window.history.back();
});
When history pops back -> check for a pushed state unless it was the first attempt
$(window).on("popstate", function (ev) {
var originalState = ev.originalEvent.state || obj.historyData;
if (!originalState) {
// no history, hide the back button or something
this.back.fadeOut();
return;
} else {
// do something
this.showContent(obj.historyData.id);
}
});
Using object literals as a parameter is handy to pass your id's. Then you can use one function showContent(id).
Wherever I've used this it's nothing more than a jQuery object/function, stored inside an IIFE.
Please note I put these scripts together from my implementation combined with some ideas from your initial request. So hopefully this gives you some new ideas ;)
I used (https://github.com/browserstate/history.js) and have a piece of code like this
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
alert('Inside History.Adapter.bind: ' + State.data.myData);
});
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
if I invoke manageHistory() after my ajax call, then History.Adapter.bind callback method get invoked correctly. However, if I click the browser back and then click forward button that result in page navigation from B to A, then A back to B, call back method inside History.Adapter.bind does not get invoked. This happen on both chrome and IE9. Anyone know how to fix this issue, I need to get the State after click browser back and then forward, to update my DOM. Please help
Note: I use version 1.7.1 jquery.history.js for html4 browser IE9
UPDATE (May 3 2013): Talk a bit more about my requirement
Sorry I was busy with other task, that not until now that I have sometimes to look at this issue. So oncomplete of an ajax call, I took that information that return to me, and push it into state. My requirement is: that if I navigate from page A to page B, and then on page B, I execute numbers of ajax requests that result in DOM manipulation (let call each ajax request that result in DOM manipulation a state). If I click the back button, I should go back to page A, and if I click the forward button, I should go to page B with the last state that I was in.
So my thought was, oncomplete of my ajax request, I would replace the last state in history with my current state (my json object is the html that I receive back from the ajax request). If I use push state, let say I am on page B, and I click one button, my page now change to aaa, I pushState aaa. Then if I click other button, my page now change to bbb, I pushState bbb. If I click the back button now, I would still be on page B, with my state in History.Adapter.bind(window, 'statechange', function() {}) show as aaa, if I click back button again, i would go to page A. I do not want this behavior. I want that if I am on page B with state bbb, and click back, I would go to page A, and if I click forward, I would go to page B with state bbb. I hope this make more sense. Please help
What you need to do is as follows:
If the page you came from is another page (e.g. you open page B and the previous page was page A) you do a pushState.
If on the other hand the previous page was the same page as you're currently on you will simply do a replaceState, replacing the information with information about your current state.
(the tracking of the previous page you will need to do manually in some variable)
This means that if you go from A to B (pushState) to new state of B (replaceState), back button (goes to state info of A) and next forward (goes to the newest state of B). Additionally if you open A the first time you need to do a replaceState with information about the state of A so that A will have some state at least (otherwise it will have an empty data object).
It might be useful to look at this answer of mine, which is simply about the way you would built an ordered history stack with pushStates. Not exactly what you want of course, but it's a bit simpler written and together with the information in this answer it should get you the behaviour you want.
I see that you are using History.replaceState which remove the last history state in the stack and replace it by the state given in parameters.
I am using History.pushState in my website, and doesn't face such an issue because this function doesnt pull the last state but add the new state above it. It is making the back-forward buttons work correcly.
I hope it helps you.
Edit: Using the example of change of select tag as event listenner:
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistory(url, data, uniqueId)
});
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
// Launch the ajax call and update DOM using State.data.myData
});
Also,
relpaceState will always erase B state.
Use pushState for A and B states.
And use replacestate for aaa, bbb, ccc states.
function manageHistoryBack(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.replaceState({myData: data}, null, '?stateHistory=' + uniqueId);
}
function manageHistory(url, data, uniqueId){
var History = window.History;
if ( !History.enabled ) { return false; }
History.pushState({myData: data}, null, '?stateHistory=' + uniqueId);
}
// Event listenner triggering aaa,bbb,ccc
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistoryBack(url, data, uniqueId)
});
// Event listenner triggering A, B
$('select.select').change(function(e) {
e.preventDefault();
// get url
// get data
// get uniqueId
manageHistory(url, data, uniqueId)
});
History.Adapter.bind(window, 'statechange', function() {
var State = History.getState();
// Launch the ajax call and update DOM using State.data.myData
});
Good luck
I'm using history.JS (latest) with Jquery (latest) to load and replace just a portion of a website, this is all working, currently I'm only trying to get it working in modern browsers so I'm not fiddling with the hash changes.
Everything seems to work, however when I click the back button on the browser (latest FF & Chrome) the page does not change (although the url and title do change). I've had a google and a look on here but I can't see what is happening.
Looking on stack overflow I found this page: Restoring content when clicking back button with History.js which seems to be asking a similar question. I've add the loaded contents of the #left_col (which is the div being replaced) to the state data, but I'm not really sure where to go from there, I know I need to reload that data when the state changes, but I can't see how.
var History = window.History;
var origTitle = document.title;
if ( !History.enabled ) {
return false;
}
History.Adapter.bind(window,'statechange',function(){
var State = History.getState();
History.log(State.data, State.title, State.url);
});
$('.ajaxload').live("click", function() {
History.pushState({state:1,leftcol:$('#left_col').html()}, origTitle, $(this).attr("href"));
$('#left_col').load($(this).attr("rel"));
return false;
});
I'd really appreciate any help!
update:
I managed to get the page to change on the user clicking back, but it doesn't load the right state (it seems to go two states back rather than one), the code I've added to the above code is:
window.addEventListener('popstate', function(event) {
var State = History.getState();
$('#left_col').html(State.data.leftcol);
});
It turns out I needed to update the page on statechange using History.js, not poState as I'd thought. below is my full (and working) code for anyone who may be having the same issue:
var History = window.History;
var origTitle = document.title;
if ( !History.enabled ) { return false; }
History.pushState({state:$(this).attr('data-state'),leftcol:$('#left_col').html()}, origTitle, $(this).attr("href")); // save initial state to browser history
function updateContent(data) {
if(data == null) return; // check if null (can be triggered by Chrome on page load)
$('#left_col').html(data); // replace left col with new (or old from history) data
History.pushState({state:$(this).attr('data-state'),leftcol:$('#left_col').html()}, origTitle, $(this).attr("href")); // save this state to browser history
}
History.Adapter.bind(window,'statechange',function(){ // use this NOT popstate (history.JS)
var State = History.getState();
//History.log(State.data, State.title, State.url);
updateContent(State.data.leftcol); // call update content with data for left col from saved state
});
$('.ajaxload').live("click", function() { // attach click event, get html and send it to updateContent
$.get($(this).attr("rel"), updateContent);
return false;
});
You are correct when you say that you need to reload the data when the state changes, in that you will have to have the javascript undo the changes made or render the contents again from the original state.
This will probably better suit your agenda:
https://github.com/thorsteinsson/jquery-routes
Edit:
You might also consider using backbone.js (http://backbonejs.org/) as it will help you create structure and abstract code in a way that makes it easier to understand what needs to be done.
Backbone comes with it's own url router and so called views.