Change Hash without updating history stack - javascript

Is it possible to alter the hash of a page without calling the hashchange event?
As in my app I want to do something like:
$(window).hashchange( function(){
hash = ...; // here I change the hash to something else
window.location.hash = hash; // then update it
});
The obvious problem with the code above is that it will cause a loop as window.location.hash will call the hashchange event and then so and so on.
However in my app I only change the hash on a condition:
$(window).hashchange( function(){
var hash = window.location.hash;
var lastChar = hash.substr(-1);
if(lastChar != '/') {
// Add trailing slash
hash = hash.replace(/\/?$/, '/');
// Update the hash
window.location.hash = hash;
}
});
So that it only gets called IF a trailing slash isn't found, and then once it's added it then it won't enter the conditional again so it won't do the loop issue mentioned above.
BUT it will still call the hashchange event! And two problems arise from this:
The History is broken as the previous page is the current page and the browser gets stuck in an infinite stack of the current page.
I do an AJAX request in my app on hashchange so it will do the call twice just to add the trailing slash.
Is it possible to add that slash to the hash without it calling the event?
Perhaps doing window.unbind('hashchange'). Would that work? And also needs to work in HTML4 browsers so the HTML5 History API isn't an option here.

In HTML5 browsers you can use window.history.replaceState() to update the hash without causing a new history entry to be created.
According to the MDN docs, neither .pushState() or .replaceState() will trigger a hashchange event.

Doing this seems to work:
// Unbind hashchange
$(window).unbind( 'hashchange');
// Update the hash
window.location.hash = hash;
// Rebind hashchange
$(window).bind( 'hashchange');
Any thoughts on this? Such as unknown problems I may have etc.

Related

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.

Check if window.history.go successful?

Is there anyway to check if a window.history.go command is successful in changing the window.location or not?
i.e. If I do a window.history.go(-5) when there are only 3 pages in the history stack, the browser will do nothing.
Is there a way to check if that happens and run other code? An error callback, of sorts.
Thanks.
For an immediate response, first you'll want to check history.length to make sure it is at least 6, e.g. to go -5. Apart from that, I think the only way is to use setTimeout and if the script is still running, the callback will be executed.
Not really a JS expert, but if you want to perform some action when the user goes back or forward, you could use URL hashes and trigger some function using the jQuery onhashchange event. This will not give you the position in history, and i'm also not sure about cross-browser compatibility, but it did the job for me so far.
$(window).on('load' function(){
var hash = parent.top.location.hash;
if(hash == '' || hash == '#' || hash == null){
//if none, set a hash and reload page
parent.top.location.hash = '#/some/hash';
parent.top.location.reload(true);//use true if you dont want to use cached items
}
});
$(window).on('hashchange', function(){
do_something(parent.top.location.hash);
});
function do_something(hash){
//this function will be executed each time the '#' changes
console.log('hash changed to '+hash);
}

Javascript: Need to detect when a user revisits the page with a changed hash tag

Problem is (at least with my current browser), changing the hash tag and entering the new url doesn't trigger a refresh of the page, and thus I'm not aware offhand how to detect when this hash tag change has been made.
Is there an elegant way to do this?
Check for a new location.hash
This is a nasty-ish solution, but you can poll the URL for a change every so often:
var hash = window.location.hash;
function poll() {
if (window.location.hash != hash) {
hash = window.location.hash;
// Hash has changed. Do stuff here.
}
}
setInterval(poll, 100);

Can you use hash navigation without affecting history?

I'm afraid it might be impossible but is there a way to change the hash value of a URL without leaving an entry in the browser's history and without reloading? Or do the equivalent?
As far as specifics go, I was developing some basic hash navigation along the lines of:
//hash nav -- works with js-tabs
var getHash = window.location.hash;
var hashPref = "tab-";
function useHash(newHash) {
//set js-tab according to hash
newHash = newHash.replace('#'+hashPref, '');
$("#tabs li a[href='"+ newHash +"']").click();
}
function setHash(newHash) {
//set hash according to js-tab
window.location.hash = hashPref + newHash;
//THIS IS WHERE I would like to REPLACE the location.hash
//without a history entry
}
// ... a lot of irrelavent tabs js and then....
//make tabs work
$("#tabs.js-tabs a").live("click", function() {
var showMe = $(this).attr("href");
$(showMe).show();
setHash(showMe);
return false;
});
//hash nav on ready .. if hash exists, execute
if ( getHash ){
useHash(getHash);
}
Using jQuery, obviously. The idea is that in this specific instance 1) making the user go back over every tab change could effectively 'break the back button' by piling up needless references, and 2) not retaining which tab they're currently on if they hit refresh is an annoyance.
location.replace("#hash_value_here"); worked fine for me until I found that it doesn't work on IOS Chrome. In which case, use:
history.replaceState(undefined, undefined, "#hash_value")
history.replaceState() operates exactly like history.pushState() except that replaceState() modifies the current history entry instead of creating a new one.
Remember to keep the # or the last part of the url will be altered.
location.replace("#hash_value_here");
The above seems to do what you're after.
Edit: It's been a couple years now, and browsers have evolved.
#Luxiyalu's answer is the way to go
--Old Answer--
I too think it is impossible (at this time). But why do you need to change the hash value if you are not going to use it?
I believe the main reason why we use the hash value as programmers is to let the user bookmark our pages, or to save a state in the browser history. If you don't want to do any of this, then just save the state in a variable, and work from there.
I think that the reason to use a hash is to work with a value that is out of our control. If you don't need it, then it probably means you have everything under your control, so just store the state in a variable and work with it. (I like repeating myself)
I hope this helps you out. Maybe there's an easier solution to your problem.
UPDATE:
How about this:
Setup a first hash, and make sure it gets saved in the browser history.
When a new tab gets selected, do window.history.back(1), that will make the history go back from your first init hash.
Now you set the new hash, therefore the tabbing will only make one entry in the history.
You'll probably have to use some flags, to know if the current entry can be "deleted" by going back, or if you just skip the first step.
And to make sure, that your loading method for the "hash" doesn't execute, when you force the history.back.
You can always create an event listener to catch click events on the hyperlink and in the callback function put e.preventDefault(), that should prevent the browser from inserting it into the history.

How can I detect changes in location hash?

I am using Ajax and hash for navigation.
Is there a way to check if the window.location.hash changed like this?
http://example.com/blah#123 to http://example.com/blah#456
It works if I check it when the document loads.
But if I have #hash based navigation it doesn't work when I press the back button on the browser (so I jump from blah#456 to blah#123).
It shows inside the address box, but I can't catch it with JavaScript.
The only way to really do this (and is how the 'reallysimplehistory' does this), is by setting an interval that keeps checking the current hash, and comparing it against what it was before, we do this and let subscribers subscribe to a changed event that we fire if the hash changes.. its not perfect but browsers really don't support this event natively.
Update to keep this answer fresh:
If you are using jQuery (which today should be somewhat foundational for most) then a nice solution is to use the abstraction that jQuery gives you by using its events system to listen to hashchange events on the window object.
$(window).on('hashchange', function() {
//.. work ..
});
The nice thing here is you can write code that doesn't need to even worry about hashchange support, however you DO need to do some magic, in form of a somewhat lesser known jQuery feature jQuery special events.
With this feature you essentially get to run some setup code for any event, the first time somebody attempts to use the event in any way (such as binding to the event).
In this setup code you can check for native browser support and if the browser doesn't natively implement this, you can setup a single timer to poll for changes, and trigger the jQuery event.
This completely unbinds your code from needing to understand this support problem, the implementation of a special event of this kind is trivial (to get a simple 98% working version), but why do that when somebody else has already.
HTML5 specifies a hashchange event. This event is now supported by all modern browsers. Support was added in the following browser versions:
Internet Explorer 8
Firefox 3.6
Chrome 5
Safari 5
Opera 10.6
Note that in case of Internet Explorer 7 and Internet Explorer 9 the if statment will give true (for "onhashchange" in windows), but the window.onhashchange will never fire, so it's better to store hash and check it after every 100 millisecond whether it's changed or not for all versions of Internet Explorer.
if (("onhashchange" in window) && !($.browser.msie)) {
window.onhashchange = function () {
alert(window.location.hash);
}
// Or $(window).bind( 'hashchange',function(e) {
// alert(window.location.hash);
// });
}
else {
var prevHash = window.location.hash;
window.setInterval(function () {
if (window.location.hash != prevHash) {
prevHash = window.location.hash;
alert(window.location.hash);
}
}, 100);
}
EDIT -
Since jQuery 1.9, $.browser.msie is not supported. Source: http://api.jquery.com/jquery.browser/
There are a lot of tricks to deal with History and window.location.hash in IE browsers:
As original question said, if you go from page a.html#b to a.html#c, and then hit the back button, the browser doesn't know that page has changed. Let me say it with an example: window.location.href will be 'a.html#c', no matter if you are in a.html#b or a.html#c.
Actually, a.html#b and a.html#c are stored in history only if elements '<a name="#b">' and '<a name="#c">' exists previously in the page.
However, if you put an iframe inside a page, navigate from a.html#b to a.html#c in that iframe and then hit the back button, iframe.contentWindow.document.location.href changes as expected.
If you use 'document.domain=something' in your code, then you can't access to iframe.contentWindow.document.open()' (and many History Managers does that)
I know this isn't a real response, but maybe IE-History notes are useful to somebody.
Firefox has had an onhashchange event since 3.6. See window.onhashchange.
I was using this in a react application to make the URL display different parameters depending what view the user was on.
I watched the hash parameter using
window.addEventListener('hashchange', doSomethingWithChangeFunction);
Then
function doSomethingWithChangeFunction () {
let urlParam = window.location.hash; // Get new hash value
// ... Do something with new hash value
};
Worked a treat, works with forward and back browser buttons and also in browser history.
You could easily implement an observer (the "watch" method) on the "hash" property of "window.location" object.
Firefox has its own implementation for watching changes of object, but if you use some other implementation (such as Watch for object properties changes in JavaScript) - for other browsers, that will do the trick.
The code will look like this:
window.location.watch(
'hash',
function(id,oldVal,newVal){
console.log("the window's hash value has changed from "+oldval+" to "+newVal);
}
);
Then you can test it:
var myHashLink = "home";
window.location = window.location + "#" + myHashLink;
And of course that will trigger your observer function.
Another great implementation is jQuery History which will use the native onhashchange event if it is supported by the browser, if not it will use an iframe or interval appropriately for the browser to ensure all the expected functionality is successfully emulated. It also provides a nice interface to bind to certain states.
Another project worth noting as well is jQuery Ajaxy which is pretty much an extension for jQuery History to add ajax to the mix. As when you start using ajax with hashes it get's quite complicated!
var page_url = 'http://www.yoursite.com/'; // full path leading up to hash;
var current_url_w_hash = page_url + window.location.hash; // now you might have something like: http://www.yoursite.com/#123
function TrackHash() {
if (document.location != page_url + current_url_w_hash) {
window.location = document.location;
}
return false;
}
var RunTabs = setInterval(TrackHash, 200);
That's it... now, anytime you hit your back or forward buttons, the page will reload as per the new hash value.
I've been using path.js for my client side routing. I've found it to be quite succinct and lightweight (it's also been published to NPM too), and makes use of hash based navigation.
path.js NPM
path.js GitHub
SHORT and SIMPLE example
Click on buttons to change hash
window.onhashchange = () => console.log(`Hash changed -> ${window.location.hash}`)
<button onclick="window.location.hash=Math.random()">hash to Math.Random</button>
<button onclick="window.location.hash='ABC'">Hash to ABC</button>
<button onclick="window.location.hash='XYZ'">Hash to XYZ</button>

Categories

Resources