3

I am trying to get Bootstrap Native to work with Turbolinks 5 in a Rails 5 app. When I first load the page, the Bootstrap drop down menu works fine, but after navigating to another page, the Bootstrap drop down no longer works. It is as if Bootstrap's event listeners get disconnected.

I have seen several questions addressing this in issue with respect to Bootstrap's jQuery implementation, however, I am interested in using Bootstrap Native and eliminating jQuery from my JS stack.

Here are some specifics:

application.js

# app/assets/javascript/application.js
//= require turbolinks
//= require rails-ujs
//= require polyfill
//= require bootstrap-native

application layout

# views/layout/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
    </div>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload', 'data-turbolinks-eval': 'false' %>
  </body>
</html>

bootstrap-native This is the Bootstrap Native package.

Removing 'data-turbolinks-eval': 'false' from my Javascript tag, thereby re-evaluating the all of the app's Javascript on every Turbolink navigation, does solve the Bootstrap Native problem, but it but it causes rails-ujs to throw an exception.

Any thoughts on how to solve this?

Tom Aranda
  • 5,919
  • 11
  • 35
  • 51

4 Answers4

3

BSN developer here, thinking why not add this custom code outside the BSN library, for easier maintenance?

var container = document.getElementById('myContainer');
document.addEventListener('turbolinks:load', function(){
  container = container || document.getElementById('myContainer');
  Array.prototype.forEach.call(container.querySelectorAll('[data-spy="affix"]'), function(element){ new Affix(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-dismiss="alert"]'), function(element){ new Alert(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="buttons"]'), function(element){ new Button(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-ride="carousel"]'), function(element){ new Carousel(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="collapse"]'), function(element){ new Collapse(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="dropdown"]'), function(element){ new Dropdown(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="modal"]'), function(element){ new Modal(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="popover"]'), function(element){ new Popover(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-spy="scroll"]'), function(element){ new ScrollSpy(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="tab"]'), function(element){ new Tab(element) });
  Array.prototype.forEach.call(container.querySelectorAll('[data-toggle="tooltip"]'), function(element){ new Tooltip(element) });
},false);

You should only look into the specific container turbolinks update, as you don't update anything else and you need this to be as fast as possible.

I also updated the wiki page to include an example for general purpose.

UPDATE Starting with BSN version 2.0.20, you can use the library in your site <head> without any additional scripting required, and you can do this turbolinks easier:

var container = document.getElementById('myContainer');
document.addEventListener('turbolinks:load', function(){
  container = container || document.getElementById('myContainer');
  BSN.initCallback(container);
}, false);

If you type in BSN hit Enter in your console, you get the following object:

BSN = {
  version: '2.0.20',
  initCallback: function(lookup){},
  supports: [ /* an array with supported components */ ]
}

Please remember that you don't have to use myContainer as the ID attribute for your turbolinks target, you can use anything to make that element unique, I would suggest a selector like data-function="turbolinks-autoload".

Download latest master and give it a try.

thednp
  • 4,401
  • 4
  • 33
  • 45
  • I'll try this solution. One problem I ran into in the past is since Turbolinks loads everything between the `` tags via AJAX, then event listeners are not cleared. Consequently, event listeners will keep piling up on these components. Maybe there is a way to clear the listeners. – Tom Aranda Oct 31 '17 at 13:30
  • Modern browsers would clear garbage right away, but if you want to really support legacy browsers with polyfills and stuff, you need to fork/build your own version of BSN to make use of some `destroy()` public methods. – thednp Oct 31 '17 at 13:59
  • This update broke my code. It throws an `Uncaught TypeError: Cannot read property 'querySelectorAll' of null` on line 3. Wrapping my entire page in a `
    ` tag eliminated the error, but still disabled the dropdown. Moving `var container...` inside the listener solved the problem. However, since turbolinks reloads everything that is between the `` tags anyway, I don't think this speeds things up much. I liked your previous answer better. I'd recommend rolling back.
    – Tom Aranda Nov 02 '17 at 14:45
  • I have to disagree here. For your specific case and everything similar, looking into a single specific container instead of the entire document is much better, in any case any day of the week. I updated my code to get you back to work. – thednp Nov 02 '17 at 19:46
1

When working with Turbolinks, you'll most likely want to put your application JavaScript file in the <head>:

# views/layout/application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <%= csrf_meta_tags %>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
    </div>
  </body>
</html>

Although this seems to go against the traditional best practices, the performance win with Turbolinks is that the script is only loaded on the initial page load.

If the bootstrap native library initialises its plugins/functions on DOMContentLoaded, you may also need to manually call these functions on turbolinks:load, and subsequently tear them down (usually on turbolinks:before-cache) if necessary.

Dom Christie
  • 4,152
  • 3
  • 23
  • 31
  • Thanks. Bootstrap Native uses the [IIFE pattern](https://developer.mozilla.org/en-US/docs/Glossary/IIFE) to setup on load. I’ll try adding the listener you recommended. – Tom Aranda Oct 21 '17 at 16:17
1

Major Revision

In the wake of the accepted answer by a member of the Bootstrap Native development team, I have revised the solution I implemented in my app. My solution differs slightly from the accepted solution because my listener will search the entire DOM for Bootstrap components. The accepted solution, by contrast, will only search within an HTML tag that has id="myContainer. The accepted solution will execute faster because it only searches a subset of the DOM. However, it requires the developer to wrap the relevant Bootstrap components in a tag with the myContainer id.

Either solution works. My solution will run a little slower, but leads to easier coding and is less prone to developer induced bugs. Here are the details:

app/assets/javascript/application.js

//= require turbolinks
//= require rails-ujs
//= require polyfill
//= require bootstrap-native
//= require bootstrap-native-turbolinks

app/assets/javascript/bootstrap-native-turbolinks.js

document.addEventListener('turbolinks:load', function(){
  Array.prototype.forEach.call(document.querySelectorAll('[data-spy="affix"]'), function(element){ new Affix(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-dismiss="alert"]'), function(element){ new Alert(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="buttons"]'), function(element){ new Button(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-ride="carousel"]'), function(element){ new Carousel(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="collapse"]'), function(element){ new Collapse(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="dropdown"]'), function(element){ new Dropdown(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="modal"]'), function(element){ new Modal(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="popover"]'), function(element){ new Popover(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-spy="scroll"]'), function(element){ new ScrollSpy(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="tab"]'), function(element){ new Tab(element) });
  Array.prototype.forEach.call(document.querySelectorAll('[data-toggle="tooltip"]'), function(element){ new Tooltip(element) });
},false);

vendor/assets/javascripts/

Finally, I moved my application javascript tag to the <head> and load it asynchronously without tuborlinks-eval and with turbolinks-track. This configures the Javascript to run once on the initial page load. The turbolinks:load listener is called on every turbolinks page visit and attaches the Bootstrap Native event listeners to the appropriate components in the DOM.

app/views/layouts/application.html.erb

...
<head>
  ...
  <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload', 'data-turbolinks-eval': 'false', async: true %>
</head>
...

As of this writing, this solution is in a production Rails 5.1 app.

Incidentally, all of Javascript listed in application.js, when concatenated and minified, is less than 20KB. By contrast, jQuery by itself is 86KB minified. Bootstrap Native can significantly improve download times for your app.

Tom Aranda
  • 5,919
  • 11
  • 35
  • 51
0
var bsn = require('bootstrap.native/dist/bootstrap-native-v4');

document.addEventListener('turbolinks:load', () => {
    BSN.initCallback();
});
Ostap Brehin
  • 3,240
  • 3
  • 25
  • 28