Can you use hash navigation without affecting history? - javascript

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.

Related

Getting Backbutton to work in single page website and implementing "speaking" URLs

I have a single page website and would like to achieve the following:
back button working as if it was a normal website
and instead of say,
www.mysite.com/index.php?p=#this-is-a-great-product
I'd like to have this url
www.mysite.com/this-is-a-great-product
while still having back button working properly.
Regarding 1.) I use the following code ive found which works great:
<!-- Getting BackButton to work properly -->
<script type="text/javascript">
var times = 0;
function doclick() {
times++;
}
function doclick() {
times++;
location.hash = times;
}
window.onhashchange = function() {
if (location.hash.length > 0) {
times = parseInt(location.hash.replace('#',''),10);
} else {
times = 0;
}
}
</script>
…but of course it just changes any anchors to /#1, then /#2 and so forth ro get the backbutton to work. But as I'm not a programmer I don't know how to change it… :(
Regarding 2.) i can add in htaccess this:
>RewriteEngine On
>RewriteRule ^([^/.]+)/?$ /index.php?page=$1
and this changes /index.php?p=products to /products.
So how do I change the above code (under 1.) so it doesn't change all anchors to #1, #2, etc. but instead references / uses the urls I achieved under 2, like
www.mysite.com/this-is-a-great-product
And (probably a very dumb question, but a very important one) -given I use only the new url links on my site- is there any danger that this still might result in duplicate content in any way?
Regarding this, should I (for that reason or any other) sefreferential my single page index.php to itself using rel canonical link=index.php?
Thanks so much in advance!
As mentioned, you will want to use the HTML5 History API. Please note, this API is relatively new and therefore browser support is a concern. At the time of writing, approximately 71% of global Internet users have support for it (see http://caniuse.com/#feat=history for browser support information). Therefore, you will want to ensure you have a fall-back solution for this. You will likely want to use the older #! solution that was popular before the HTML 5 History API was adopted.
If you use the history API to replace, for example, example.com/#!settings with example.com/settings and a user bookmarks that nicer URL, then when they go to visit it, their browser will make a request to the server for /settings (which doesn't actually exist in the web server's context). Therefore, you will need to make sure your web server has some redirection rules (i.e. RewriteEngine) such that it can take the pretty URLs and redirect them to the #! version (and then if the user's browser supports the history API it can replace that with the nice URL).
If you aren't very comfortable programming yourself, I'd recommend using a JavaScript library that does a lot of the work for you. I did some quick searching and discovered the following, though there might be better ones out there: https://github.com/browserstate/history.js
Basically i have created a small prototype on jsfiddle which tracks all the urls accessed via ajax calls.
Also contains navigation to access links back and forth .
How It Actually Works:
I have created a global array called history, which keeps track of all urls accessed via ajax in sequence.
also there a global index defined to keep track of the url being accessed when navigating back and forth the links in history array.
There is History section at the bottom of the jsfiddle, which shows the sequence in which the links are accessed by capturing the link names and posting them in the order in which they were accessed.
JS Code:
$(function () {
var history = [];
var index = 0;
$('.links').on('click', function () {
$('#history').append($(this).text());
var address = $(this).attr('data-ref');
index += 1;
history[index] = address;
$('.links').attr('disabled', 'disabled');
loadExternalPage(address);
console.log('list:' + history);
});
$('#back').on('click', function () {
console.log(index);
index -= 1;
console.log(index);
console.log(history[index]);
loadExternalPage(history[index]);
});
$('#forward').on('click', function () {
console.log(index);
index += 1;
console.log(index);
console.log(history[index]);
loadExternalPage(history[index]);
});
var loadExternalPage = function (address) {
console.log(history[index]);
$('#result-section').load(address, function () {
console.log('data-loaded');
$('.links').removeAttr('disabled');
});
};
});
Live Demo # JSFiddle:http://jsfiddle.net/dreamweiver/dpwmcu0b/8/
Note: This solution is far from being perfect, so dont consider it as final solution but rather use it as a base to build upon
On using BACK and FORWARD functions in the browser top-left button:
In principle, there is no great problem with this as long as you work with the existing storage object (a stack) for previously visited web pages on your browser. This object is the history object and you can see what is in it anytime by right-clicking and selecting "Inspect", then selecting the "Console" tab, then enter window.history and enter.
Check out the Browser Object Model (BOM) section of Pro Java For Web Developers (Frisbee) for the background to the history object. (Just a few pages, an easy read, don't worry.) Just remember that in this process you are storing the new page that you move to, not the old page that you are leaving !
For a simple SPA example, look at this example. codepen.io/tamjk/pen/NWxWOxL
In regard to the URL, the method that the history object uses to load a new page state into the history stack, i.e. pushState(...), has an optional third parameter for associating a dummy URL for each web page that is stored.
Personally, when I first sorted out the BACK & FORWARD functions, I did not use dummy URLs as the browser was being confused by them and I had enough to do sorting out the history sequence using just the first two parameters, i.e.
the state object - a JSON holding enough data to recreate the page stored
a title for the page I expect that you could also use a dummy URL but I will leave that to the student as an exercise, as they say.
But you can add the URL of the new page if you want to.
In the example above, for the state object I just used the IDs of the page's nav link and its content element.
For the title, I programmatically changed the HTML's page title element with each change of page. I did this after noticing that the browser listed the previous pages according to the title element in the HTML code.
Unfortunately, this title does not show up on CodePen when you right-click on the browser BACK and FORWARD buttons due to CodePen's system not allowing it. But it will show on your own sites.
It's important that whatever method you use to store current web page states when using the navbar links to navigate, you DO NOT ADD page states to the browser history when you arrive at them using BACK or FORWARD buttons. Otherwise your history stack will have repetitions of entries going back and deletion of entries going forward.
In the CodePen, this was achieved by having the addToHistory(..) function separate to and outside the scope of the switchPage(...) function. This allows you use of the switchPage function in both normal navbar navigation and browser BACK/FORWARD navigation. The third parameter of switchPage(...) is a boolean indicating if the page is to be stored in history or not.
Anyway, this is just something to get you started.

HTML5 history disabling forward button

I am writing a single page javascript application using the HTML5 History API. The application loads content via Ajax and internally maintains state information on the foreground screen using a screen stack.
I want to enable navigation with the back button, but I never want to the forward button to be enabled.
A couple quick bits of information:
The user should only ever be able to go back, never forward
Pressing browser back button closes the current page screen user is on and reloads the previous one
Project is targeted towards the latest version of Chrome only, so other browsers implementations are not important
I am using native JavaScript and jQuery only, I would like to do this without History.js
When I load a new screen I run the following line:
history.pushState(screenData, window.document.title, "#");
I bind to the popstate event via jQuery:
$(window).bind("popstate", function(event) {
if (event.originalEvent.state != null) {
// Logic that loads the previous screen using my screen stack
}
});
My application's history management is working, however when I go back the forward button is enabled. I need to figure out how to remove data from history on the popstate event.
Can I do this with replaceState? I'm not sure how to go about doing this...
The accepted answer solves the problem to disable the forward button, but creates a new annoying issue "the page navigated back to" is inserted in duplicate into the history (as indicated in the answers comments).
Here is how solve the question "diabled forward button" and to avoid the "duplicate" back-button-issue.
//setup the popstate EventListener that reacts to browser history events
window.addEventListener("popstate",function(event){
// In order to remove any "forward"-history (i.e. disable forward
// button), this popstate's event history state (having been navigated
// back to) must be insert _again_ as a new history state, thereby
// making it the new most forwad history state.
// This leaves the problem that to have this popstate event's history
// state to become the new top, it would now be multiple in the history
//
// Effectively history would be:
// * [states before..] ->
// * [popstate's event.state] ->
// * [*newly pushed _duplicate_ of popstate's event.state ]
//
// To remove the annoyance/confusion for the user to have duplicate
// history states, meaning it has to be clicked at least twice to go
// back, we pursue the strategy of marking the current history state
// as "obsolete", as it has been inserted _again_ as to be the most
// forward history entry.
//
// the popstate EventListener will hence have to distinguish 2 cases:
//
// case A) "popstate event is _not_ an obsolete duplicate"...
if( typeof event.state == "object"
&& event.state.obsolete !== true)
{
//...so we _firstly_ mark this state as to from now on "obsolete",
// which can be done using the history API's replaceState method
history.replaceState({"obsolete":true},"");
// and _secondly_ push this state _one_more_time_ to the history
// which will solve the OP's desired "disable forward button" issue
history.pushState(event.state,"");
}
// case B: there is the other case that the user clicked "back" and
// encounters one of the duplicate history event entries that are
// "obsolete" now.
if( typeof event.state == "object"
&& event.state.obsolete === true)
{
//...in which case we simply go "back" once more
history.back()
// by this resolving the issue/problem that the user would
// be counter-intuively needing to click back multiple times.
// > we skip over the obsolete duplicates, that have been the
// the result of necessarily pushing a history state to "disable
// forward navigation"
}
},false);
Bad Part
To really disable the forward button, you would have to be able to delete browser history, which is not allowed by all javascript implementations because it would allow sites to delete the entire history, which would never be in the interest of the user.
Good Part
This is a bit tricky, but I guess it could work if you want to do custom history. You could just use pushState in the popstate event to make your actual page the topmost history entry. I assume the way you handle your history, your window will never unload. This allows you to keep track of the user history yourself:
var customHistory = [];
Push every page you load with history.pushState(screenData, window.document.title, "#");, like you did before. Only you add the state to your custom history, too:
history.pushState(screenData, window.document.title, "#");
customHistory.push({data: screenData, title: window.document.title, location: '#'});
now if you have a popstate event, you just pop you custom history and push it to the topmost entry:
window.onpopstate = function(e) {
var lastEntry = customHistory.pop();
history.pushState(lastEntry.data, lastEntry.title, lastEntry.location);
// load the last entry
}
Or in jQuery
$(window).on('popstate', function(e) {
var lastEntry = customHistory.pop();
history.pushState(lastEntry.data, lastEntry.title, lastEntry.location);
// load the last entry
});
Just use following jquery to disable forward button:
$( document ).ready( function(){
history.pushState(null, document.title, location.href);
});
NOTE:
This code was tested and worked fine without showing any problems, however
I would incentivize developers to test it more before going to production with the code.
If HTML5 history.replaceState() is used anywhere in your application,
the code below might now work.
I created a custom function in order to disable the forward button.
Here is the code (it doesn't work with the hash routing strategy):
<script>
(function() {
// function called after the DOM has loaded
disableForwardButton();
})();
function disableForwardButton() {
var flag, loop = false;
window.addEventListener('popstate', function(event) {
if (flag) {
if (history.state && history.state.hasOwnProperty('page')) {
loop = true;
history.go(-1);
} else {
loop = false;
history.go(-1);
}
} else {
history.pushState({
page: true
},
null,
null
);
}
flag = loop ? true : !flag;
});
window.onclick = function(event) {
flag = false;
};
}
</script>
As Redrif pointed out in the comments of the accepted answer, the problem is that you have to double click the back button in order to navigate back to the page which is tedious and impractical.
Code explanation: each time you click the back button you need to create an additional history element so that the the current page which you are located on
points to the newly created history page. In that way there is no page to go forward to since the pushState is the last state (picture it as the last element in the array) therefore your forward button will always be disabled.
The reason why it was mandatory to introduce the loop variable is because you can have a scenario where you go back to a certain page and the pushState code occurs which creates the last history element and instead going back again you choose to click on some link again and again go back the previous page which now creates an additional history element. In other words, you have something like this:
[page1, page2, page2, page2]
now, once on page2 (index 3 element) and you click the back button again you will get to the page2 again index 1 element and you do not want that. Remember that you can have an array of x page2 elements hence the loop false variable was introduced to resolve that particular case, with it you jump all the way from page2 to page 1 no matter how many page2 elements are their in the array.

Cancel/Abort/Confirm an HTML 5 state change event (onpopstate)

With unload event of window, it is possible to show the user a confirmation dialog , let's say in a situation where there is an ongoing request you are waiting for to finish and navigating away from the page will terminate that request.
Is there a way to accomplish this with onpopstate of HTML5 history API? Or any other way with the same outcome?
I guess you could modify the behavior of pushState to ask for confirmation before pushing a new state :
// Store original pushState
var _pushState = window.history.pushState;
// Some bad global variable to determine if confirmation is needed
var askForConfirm = true;
// Modify pushState behavior
window.history.pushState = function() {
if(!askForConfirm || confirm('Are you sure you want to quit this page ?')) {
// Call original pushState
_pushState.apply(window.history,arguments);
}
};
I don't know if it helps in your situation, but Sammy.js, a popular hash-routing library has a before handler. I've used it in my application to record the previously accessed hash, and if it's the hash I want to stop them from leaving, return false will keep them on that page. You still need to rewrite the URL to display the previous page, but it seems to be working.
See my answer in this other thread for more info.

Page history - back button exists?

Is it possible check if there is a value for history.go(-1)? I know you can't access history.previous directly.
I am trying to stay away from document.referrer because I know it can be blocked in some instances.
Here is what I am trying to do. I have an error page, on this page I would like to either have a BACK button (if it's not the only item in history) or a close button (if it is).
if (history.length) {
//There is history to go back to
history.go(-1);
}
Actually, history.length is always one or more, since the current page counts. Also, if you have a forward history (i.e. you used the back button), those pages also count. So you need a more complicated check:
if( (1 < history.length) && document.referrer ) {
There is no cross-browser approach to accomplish this. Document.Referrer may be set even if no history entry exists.
I came up with the following "hack". It utilizes the onbeforeunload event to detect whether the browser starts leaving the page or not. If it does not in a certain timespan it'll just redirect to the fallback.
window.goBack = function goBack(fallback){
var useFallback = true;
window.onbeforeunload = function(){
useFallback = false;
}
window.history.back();
setTimeout(function(){
if (useFallback){ window.location.href = fallback; }
}, 100);
}
You can call this function using goBack("fallback.example.org").
One of the use cases is that you may want to add a back button to any page and also want to make sure that this back button works even if the user goes directly to this page (e.g. by bookmark, direct link etc).
So either it does perform a history.back() or if there is no entry, it'll redirect to a fallback.
If the history has a length greater than 0, then it contains at least one history point.
if (history.length)
function test() {
document.URL = document.referrer;
}

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