-
Notifications
You must be signed in to change notification settings - Fork 621
Turbolinks should follow same-page anchor links without reloading the page #75
Comments
I agree. We should ignore clicks on links that are anchors to the current page, and let them fall through to the browser. |
Here is a workaround I tried to write to prevent Turbolinks from making unnecessary network calls (it is a compiled version of a gist I wrote in TypeScript). It works except for the scenario where if you're on a page like document.addEventListener("turbolinks:before-visit", function (event) {
// console.info(`Network Call Avoidance Workaround (for Turbolinks) attempting to short-circuit network calls.`);
var origin = window.location.href; // Current URL
var destination = event.data.url; // Destination URL
// Make sure both destination and origin urls end with "/" unless they contain an "#"
if ((origin.match(/#/) === null) && origin[origin.length - 1] !== "/")
origin = origin + '/';
if ((destination.match(/#/) === null) && destination[destination.length - 1] !== "/")
destination = destination + '/';
// console.info(`Origin: ${origin}`);
// console.info(`Destination: ${destination}`);
// Prevent Turbolinks from doing anything when clicking on a link to the current page
if (origin === destination) {
// console.info("Turbolinks stopped since origin and destination URLs are identical.");
event.preventDefault();
if (origin.match(/#/) !== null) {
// console.info(`Letting browser navigate to: ${destination}`);
window.location.href = destination;
}
else {
// Gives user feedback for clicking link to current page.
// console.info(`Giving user fake feedback since link points to current page.`)
var tpb = new Turbolinks.ProgressBar();
tpb.setValue(0);
tpb.show();
tpb.setValue(0.2);
setTimeout(function () { tpb.setValue(1); tpb.hide(); }, 100);
}
return;
}
var shorterLength = Math.min(origin.length, destination.length);
// To handle the cases where both origin and destination URLs contain "#".
// Only intended to prevent page reloads for anchor links within current page.
if (((origin.match(/#/) !== null) && (destination.match(/#/) !== null)) &&
(origin.indexOf("#") === destination.indexOf("#"))) {
// console.info("Detected that both origin and destination have same index for #");
shorterLength = Math.min(origin.indexOf("#"), destination.indexOf("#"));
}
// console.info(`shorterLength: ${shorterLength}`);
// See if the first `shorterLength` characters of both the origin and destination URLS are the same.
// If these characters are not the same, do nothing.
if (origin.substring(0, shorterLength) !== destination.substring(0, shorterLength)) {
// console.info(`Turbolinks not stopped since first ${shorterLength} characters of origin and destination URL are different.`);
return;
}
// console.info(`The first ${shorterLength} chars of both origin and destination URLs are identical`);
// If the destination URL contains an "#" immediately after the first `shorterLength` characters, let
// the browser navigate to the URL. In this case the origin URL would be desination URL without the hash.
if (destination.length > shorterLength && destination[shorterLength] === "#") {
// console.info("Turbolinks stopped since destination URL is a bookmark on the current page.");
event.preventDefault();
// console.info(`Letting browser navigate to: ${destination}`);
window.location.href = destination;
return;
}
// If the origin URL contains an "#" immediately after the first `shorterLength` characters, do nothing.
// In this case the destination URL would be the origin URL without the hash.
if (origin.length > shorterLength && origin[shorterLength] === "#") {
// console.info("Turbolinks stopped since trying to navigate to current page's URL without hash.");
event.preventDefault();
return;
}
// Note that either destination or origin is substring within the other (starting at index 0) if this function hasn't already returned.
// console.info("Turbolinks not stopped since there is no reason to do so.");
}); |
Here is another workaround (it is a compiled version of a gist (same gist mentioned in previous comment) I wrote in TypeScript). I will probably use this one in production. It just scrolls to bookmark links on the same page (it does not change the URL (or change browser history) but that's fine (for my purposes at least) since Turbolinks preserves scroll position while navigating through history anyways). Warning: This workaround makes the assumption that your link will only contain a single /// <reference path="jquery/jquery.d.ts"/>
document.addEventListener("turbolinks:before-visit", function (event) {
// console.info(`Network Call Avoidance Workaround (for Turbolinks) attempting to short-circuit network calls.`);
var origin = window.location.href; // Current URL
var destination = event.data.url; // Destination URL
// console.info(`Original Origin: ${origin}`);
// console.info(`Original Destination: ${destination}`);
// Make sure both destination and origin urls do not end with "/"
// If the contain a '#', ensure URLs do not have a '/' before the '#' (https://tools.ietf.org/html/rfc3986#section-3.4)
if (origin.match(/#/) === null) {
if (origin[origin.length - 1] === "/") {
origin = origin.substring(0, origin.length - 1);
}
}
else {
var hashIndex = origin.indexOf('#');
// console.info(hashIndex);
if (hashIndex > 0 && origin[hashIndex - 1] === "/")
origin = "" + origin.substring(0, (hashIndex - 1)) + origin.substring(hashIndex);
}
if (destination.match(/#/) === null) {
if (destination[destination.length - 1] === "/") {
destination = destination.substring(0, destination.length - 1);
}
}
else {
var hashIndex = destination.indexOf('#');
// console.info(hashIndex);
if (hashIndex > 0 && destination[hashIndex - 1] === "/")
destination = "" + destination.substring(0, (hashIndex - 1)) + destination.substring(hashIndex);
}
// console.info(`Modified Origin: ${origin}`);
// console.info(`Modified Destination: ${destination}`);
// Do not prevent Turbolinks from following links to the same URL (can affect forms)
if (origin === destination)
return;
var shorterLength = Math.min(origin.length, destination.length);
// To handle the cases where both origin and destination URLs contain "#".
// Only intended to prevent page reloads for anchor links within current page.
if (((origin.match(/#/) !== null) && (destination.match(/#/) !== null)) &&
(origin.indexOf("#") === destination.indexOf("#"))) {
// console.info("Detected that both origin and destination have same index for #");
shorterLength = Math.min(origin.indexOf("#"), destination.indexOf("#"));
}
// console.info(`shorterLength: ${shorterLength}`);
// See if the first `shorterLength` characters of both the origin and destination URLS are the same.
// If these characters are not the same, do nothing.
if (origin.substring(0, shorterLength) !== destination.substring(0, shorterLength)) {
// console.info(`Turbolinks not stopped since first ${shorterLength} characters of origin and destination URL are different.`);
return;
}
// console.info(`The first ${shorterLength} chars of both origin and destination URLs are identical`);
// If the destination URL contains an "#" immediately after the first `shorterLength` characters, let
// the browser navigate to the URL. In this case the origin URL would be desination URL without the hash.
if (destination.length > shorterLength && destination[shorterLength] === "#") {
// console.info("Turbolinks stopped since destination URL is a bookmark on the current page.");
event.preventDefault();
// This will mess up browser history and prevent you from going back after moving forward
// window.location.href = destination;
var urlHashMinusHash = destination.substring(destination.indexOf('#')).substring(1);
// console.info(`Letting browser scroll to: ${urlHashMinusHash}`);
var elem = document.getElementById(urlHashMinusHash);
if (elem != null)
elem.scrollIntoView();
return;
}
// If the origin URL contains an "#" immediately after the first `shorterLength` characters, do nothing.
// In this case the destination URL would be the origin URL without the hash.
if (origin.length > shorterLength && origin[shorterLength] === "#") {
// console.info("Turbolinks stopped since trying to navigate to current page's URL without hash.");
event.preventDefault();
$("html, body").animate({ scrollTop: 0 }, "slow");
return;
}
// Note that either destination or origin is substring within the other (starting at index 0) if this function hasn't already returned.
// console.info("Turbolinks not stopped since there is no reason to do so.");
}); |
@preetpalS I tried your 2nd code snippet on my app, and while it did indeed prevent Turbolinks from intercepting hash changes which was awesome, it had the side effect of blocking I'm doing a remote form submission which reloads the same page after submit, and after adding this block of code, I still see the progress bar animate, but the Turbolinks A fix might be to change line 50 |
@glennfu Try out the updated version of the 2nd code snippet. Another completely different workaround that might work better would be to add an event listener on the |
I just realized a 2nd use case for having the I can't think of a way to update this code to satisfy that without having access to the |
The quick solution I have come up with (not fully tested);
I figure if the anchor exist then the node is also not visitable. |
Waiting for news 👍 |
Thanks for reminding me to post the changes I have made.
|
The only workaround that initially worked for me was to change in Gemfile:
11.Jul.2017: After digging more, I found that the problem I was having was not with links to anchors but a feature in turbolinks-rails that shows in navigator what is sent by |
@sstephenson, @packagethief, @wshostak & @preetpalS what can I do to help move this forward? Unfortunately I don't have the JS expertise to solve this. This is quite a critical flaw of turbolinks. |
Another fix which does not involve modifying Turbolinks internal methods $(document).on('turbolinks:click', function (event) {
if (event.target.getAttribute('href').charAt(0) === '#') {
return event.preventDefault()
}
}) |
@domchristie, I have tested your solution and it still breaks back behaviour for me. |
@mrjlynch ah: it does not break "Back" behaviour … sometimes! I seem to get different behaviours, although I am not able to reproduce them consistently. Navigating "Back" either takes me back to the top of the page after a Turbolinks page load (which seems acceptable), or it updates the address bar to the previous URL but does not update the scroll position. |
@domchristie @mrjlynch I've just come across this issue, currently working around using |
@domchristie's solution (below) is the best solution I have found yet. $(document).on('turbolinks:click', function (event) {
if (event.target.getAttribute('href').charAt(0) === '#') {
return event.preventDefault()
}
}) Why?
<a href="#anchor">
<i class="icon"></i>
</a> The problem to fix:
|
Can anyone publish a small demo project demonstrating the problem? Right now in my app, with the latest version of Turbolinks, hash changes are ignored, which is actually correct and preferable for me. I don't actually want the back button to affect the hash in my project so I don't have a valid use case of my own to test against. However if someone can put up something that clearly shows the problem I'd be interested in tinkering with a solution that actually leverages the Turbolinks cache and the restorationData that stores the scroll position. My theory is that if in the "restore" process I can convince Turbolinks to skip the re-render if the only part of the url changing is the anchor, then maybe we can have a better solution. We'd also want to make sure that the default behavior of ignoring hash changes can remain as an option as well. I think a good test case would be to have a page that goes from /url1 -> /url2 -> /url2#anchor1 -> /url2#anchor2 -> /url1 and make sure that the history and navigation all play nicely. |
@glennfu Here is the test case project as requested. As you can see by monitoring the XHRs, clicking on same-page anchor links causes requests to be made. This is probably acceptable in many cases, but given that the content should exist on the page, there shouldn't be a need to make extraneous requests. To further demonstrate the problems, here is another case: a blog with lazily-loaded comments. Here is what happens:
Clicking on a same-page anchor link with |
Thanks for your work on Turbolinks 5. So far it's VERY awesome. An important business side effect of this behavior is that analytics for pages with same-page anchor links can be overstated due to the 'turbolinks:load' event firing after clicks on same-page anchor links. This is common with a Table of Contents or other intra-page linking. For example, in order to implement Google Analytics (analytics.js) on a Turbolinks app, we're doing: $(document).on('turbolinks:load', function(event) {
ga('set', 'page', window.location.pathname);
ga('send', 'pageview');
}); Because Turbolinks is intercepting the clicks on same-page anchor links (and doing more work than it should), the 'turbolinks:load' event is fired too many times and page views are being overstated. With my Table of Contents use case, 'turbolinks:load' events fire for:
and 1 page view becomes 3 page views. |
FYI, you can simply add |
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Turbolinks 5 doesn't follow the browser's standard behaviour of ignoring links pointing to "#", so we're preventing the turbolinks events in this situation: turbolinks/turbolinks#75
Hello everyone! My target is to make the back button action to close the modal instead of return to the previous page. So, when I click on the browser back the for are full reloaded. the doc says which Do you know how can prevent reload pages by native navigation (browser back button)? I hope which points are clear 🙂 |
Clicking an anchor link causes a full turbolinks visit. This is a longstanding issue that might never be fixed. (turbolinks/turbolinks#75) But while a general solution might have various complexities yadda yadda, we don't really care and can just do a simple fix, since we're running a vanilla website and not a serious application.
Couldn't this be resolved by adding
https://github.com/turbolinks/turbolinks/blob/master/src/controller.ts#L299 clickbubbled already skips if no link is returned.
https://github.com/turbolinks/turbolinks/blob/master/src/controller.ts#L210 EDIT: Just learned one gotcha is hash changes trigger popstate events... As a workaround in my project, I'm keeping track of currentPath (window.location.pathname + window.location.search) and returning from the popstate handler if they match. |
Turbolinks is no longer under active development so no one should expect this issue to be resolved. On a positive note, it looks like this issue was fixed in the successor to this library (hotwired/turbo#125). |
Consider the following HTML link to an element with an id of
bookmark
:<a href="#bookmark" >Bookmark within page</a>
Turbolinks currently intercepts this link and prevents the browser from following it unless you add an annotation on the link (
data-turbolinks="false"
). Once Turbolinks has intercepted the link, it makes another HTTP request for the same location again (but with the id appended at the end (/url#bookmark
)) instead of just scrolling to the anchored element. Since bookmark links within the same page normally do not result in an HTTP request, one could argue that the default behavior of Turbolinks for links that have anhref
attribute beginning with#
results in worse performance (due to the network request) and violates the principle of least surprise.Is it possible to disable Turbolinks for all links that have an
href
attribute beginning with#
without having to manually annotate them withdata-turbolinks="false"
?I am currently using turbolinks (5.0.0.beta2) (turbolinks-source (5.0.0.beta4)) with Rails 4.2.6.
The text was updated successfully, but these errors were encountered: