Open Bootstrap tab programmatically with turbolinks - javascript

I'm tryng to open a Bootstrap 4 tab after page load. It does work if I refresh the page, but if I navigate within the site, I get a query selector empty error.
This is my code, that's basically a port I did of this https://webdesign.tutsplus.com/tutorials/how-to-add-deep-linking-to-the-bootstrap-4-tabs-component--cms-31180 because I'm using Bootstrap-native, so I had to rewrite it in vanilla Javascript.
document.addEventListener("turbolinks:load", function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
history.replaceState(null, null, url);
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
});
I placed it just before the closing body tag. If I write http://www.myURL#mytab and click refresh, the page refresh and the tab is changed, but if I get there from a (turbo)link, it does not find the tab to query select. I'm afraid the problem is with the "load" event, I tried different methods but I can't get it to work.

If you are including this code in a <script> at the end of the <body>, you probably don't need to listen for the turbolinks:load event. Why? On the first load, the browser will be able to query any elements positioned before the script element. On Turbolinks loads scripts in the <body> will have access to all rendered elements on the page.
It's worth adding that by calling document.addEventListener("turbolinks:load", …) in a body <script> element, the listener will be called on every subsequent page load, not just on the page where the script is rendered. If the
#nav-tab elements don't exist on a subsequent page load, then you'll see the querySelector error. What's more, if you include the script on more than one page, then the listener will be duplicated again and again and again, which is probably not what you want!
So the first step to fix your issue is to remove the event listener. We'll wrap your code in an immediately invoked function to prevent polluting the global scope:
;(function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
history.replaceState(null, null, url);
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
})();
The next thing to know is that Turbolinks manages its own cache of visited pages, so that when a user taps "Back", a page is rendered from this cache. To do this, it has a system for adding to the browser's own history stack. If you bypass the Turbolinks system, and call history.replaceState (or history.pushState) yourself, then you could end up breaking the "Back" navigations. Turbolinks doesn't have a documented way to manually add to its history stack, but you could try the following:
;(function() {
let url = location.href.replace(/\/$/, "");
if (location.hash) {
const hash = url.split("#");
document.querySelector('#nav-tab a[href="#'+hash[1]+'"]').Tab.show();
url = location.href.replace(/\/#/, "#");
Turbolinks
.controller
.replaceHistoryWithLocationAndRestorationIdentifier(url, Turbolinks.uuid())
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
})();
Note, this is undocumented so may not be publically available in future versions.
Finally, it might be worth considering including this snippet in your main application JavaScript bundle, and load it in the <head> rather than in the body. In this case you would need to use a `turbolinks:load handler. It might look something like:
document.addEventListener('turbolinks:load', function () {
let url = location.href.replace(/\/$/, "");
const hash = url.split("#");
const navLink = document.querySelector('#nav-tab a[href="#'+hash[1]+'"]')
if (location.hash && navLink) {
navLink.Tab.show();
url = location.href.replace(/\/#/, "#");
Turbolinks
.controller
.replaceHistoryWithLocationAndRestorationIdentifier(url, Turbolinks.uuid())
setTimeout(() => {
window.scrollTo(0,0);
}, 400);
}
});

Related

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);
});

Firefox Extension location.href not redirecting

I'm using something rather generic here, but essentially I want to be able to load a new tab at my desired URL when selecting my extension, then when at that tab, redirect to a new URL. (The add on should run some code at the first page before redirecting, but that's for another day).
The code I have at the moment is
var buttons = require('sdk/ui/button/action');
var tabs = require("sdk/tabs");
var button = buttons.ActionButton({
id: "redirect",
label: "redirect",
icon: {
"16": "./icon-16.png"
},
onClick: handleClick
});
function handleClick(state) {
tabs.open({
url: "http://www.google.com",
onReady: loadRedirect
});
function loadRedirect(tab) {
tab.attach({
contentScript: "location.href = 'www.youtube.com;'"
});
}
}
When running this however, the 2nd URL appends to the first, rather than replaces, and then gets stuck in an infinite load/refresh loop until I close the browser.
I assume I'm missing something absolutely obvious, but I wasn't able to find anything while searching around.
You are trying to change the location.href to a string that has no scheme. It is assumed that it is a URL within the current domain. Because it also does not start with a / it is assumed to be relative to the current page. Thus, it is appended to the current google.com URL. The page becomes ready; fires the ready event; and your onReady handler is called. Your handler then changes the URL, causing the page to be reloaded, and, again, fire the ready event, which starts the process over again. This is your infinite loop.
To actually get to www.youtube.com, you could change your code to:
function loadRedirect(tab) {
tab.attach({
contentScript: "location.href = 'https://www.youtube.com';"
});
}
Which could have been done without the need to inject a content script by assigning to tab.url:
function loadRedirect(tab) {
tab.url = 'https://www.youtube.com';
}
However, that will not prevent the infinite loop. There are many ways to do so. The easiest is to just remove the ready listener:
function loadRedirect(tab) {
tab.off('ready',loadRedirect);
tab.url = 'https://www.youtube.com';
}

How to load page only once

Below is my code but page load continuously,i want to load only once
window.onload = function () {
window.location.reload();
}
There are a few ways you could solve this, all of which require saving state across page loads. You could use cookies, localStorage, the location object itself, etc.
Here's a way that checks to see if there is a hash string 'reloaded' and, if not, adds it and reloads the page. Then, when it tries to execute again, the hash will be there and it will not reload:
if (location.hash.indexOf('reloaded') === -1) {
location.hash += 'reloaded';
location.reload();
}
$(document).ready(function(){
if(document.URL.indexOf("#")==-1){ //Check if the current URL contains '#'
url = document.URL+"#"; // use "#". Add hash to URL
location = "#";
location.reload(true); //Reload the page
}
});
Due to the if condition page will reload only once.
The other way to achieve this is :
(function()
{
if( window.localStorage )
{
if( !localStorage.getItem('firstLoad') )
{
localStorage['firstLoad'] = true;
window.location.reload();
}
else
localStorage.removeItem('firstLoad');
}
})();
window.onload = function ()
{
// for getting params value
function parse(val)
{
var result = "not found";
tmp = [];
location.search
.substr(1)
.split("&")
.forEach(function (item) {
tmp = item.split("=");
if (tmp[0] === val) result = decodeURIComponent(tmp[1]);
});
return result;
}
if(parse("load")!="once")
{
//sending parameter so next time it won't reload..
window.location.href += "?load=once";
window.location.reload();
}
}
By nature of visiting a page, It will only load once. You could change your code to prove this fact:
window.onload = function () {
alert("Loaded");
}
But, I would suggest the vapor.js route to detecting page load, that is, omit this onload call, because the lines of code in the onload function run after the page is loaded. I think you either don't know what your goal is or you have an entirely different problem you are trying to solve in a way that does not make sense
You built a loop,
site is loading
window.onload is triggered
reload is initiaded
site is (re-)loading
window.onload is triggered
reload is initiaded
.......
.......
Important fact for you to learn is that browsers run through your code from top to bottom and when you reload your page, the whole prozess repeats.
So every Time you reload, the window.onload event-listener is registered and calls the function attached to it, as soon as the window object is fully loaded.
There is no mechanism that tells the browser to stop.
if you would like run your Javascript code once the DOM is loaded, and you are looking for an browser independent solution i would recommend jQuery and its $( document ).ready() function.
with jQuery included to your Page:
$( document ).ready(function(){
//Code inside this function runs after your document is loaded
})

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.

Re-render a webpage

I was wondering if there was a way to render a webpage over again, thus calling all the onload events over again, and redrawing the css?
For example, say you changed the pathway of a javascript file that is linked to an onload event and the edited page needed to reload without a refresh in order for the change to take affect.
tested, now is working:
function fireEvent(element,event){
if (document.createEventObject){
// dispatch for IE
var evt = document.createEventObject();
return element.fireEvent('on'+event,evt)
}
else{
// dispatch for firefox + others
var evt = document.createEvent("HTMLEvents");
evt.initEvent(event, true, true ); // event type,bubbling,cancelable
return !element.dispatchEvent(evt);
}
}
setTimeout(function(){
var links = document.getElementsByTagName("link");
var st = [];
for(var x=0;x<links.length;x++)
if(links[x].getAttribute("rel") == "stylesheet")
{
st.push(links[x]);
links[x].wasAtt = links[x].getAttribute("href");
links[x].setAttribute("href", "");
}
setTimeout(function()
{
for(var x =0;x<st.length;x++)
st[x].setAttribute("href", st[x].wasAtt);
setTimeout(function(){
fireEvent(window, "load");
},1000);
},1000);
},5000); // test reload after five seconds
"reload without a refresh" sounds a bit confusing to me.
However I do not know if this is what you are looking for, but window.location.reload() triggers a page reload and thus will load your linked javascript files.
But changing linked files and reload the page is not a good thing to do. It would be better if your dynamic files are loaded in a dynamic way like using Ajax. So you do not need to reload the whole page. Frameworks like JQuery, ExtJS or others provide methods to do this easily.

Categories

Resources