Making jQuery works with Turbolinks - ruby-on-rails

I have a Rails 4 app, which uses Turbolinks. My understanding is that Turbolinks breaks jQuery code, as Turbolinks does not load new page, but only get new elements.
Therefore, navigating to new page may not trigger .ready, although it always triggers .page:load, and thus new jQuery code won't initialize.
I have a lot of jQuery code, so I don't want to modify my jQuery code to be compatible with Turbolinks.
Is it possible to add a javascript code to my application.js that overwrites .ready event to include page:load as well? How should I do it?

Rather than wait for $(document).ready to fire for your jQuery, just use page:load instead:
$(document).on 'page:load' ->
<your code>
Alternatively, you can set up the jquery.turbolinks gem: https://github.com/kossnocorp/jquery.turbolinks

With turbolinks 5.0.0, the events changed to turbolinks:load. See full list of turbolinks events.
The documentation recommends following code:
document.addEventListener("turbolinks:load", function() {
// ...
})
The jquery.turbolinks fork located at https://github.com/dalpo/jquery.turbolinks already reflects these changes and allows for a seamless drop-in of turbolinks. Nevertheless, I would go for the turbolinks:load event to have full control and not require another library.

i had to use the page:change event:
js:
$(document).on('page:change', function () {
<code here>
});
coffee script:
$(document).on 'page:change' ->
<code here>

With TurboLinks 5 / Rails 5 ... I would recommend instantiating DataTables like this.
It will prevent the heading and footer paging from showing up multiple times when the back button is used.
$(document).on 'turbolinks:load', ->
tableElementIds = [
'### TABLE ID HERE ###'
]
i = 0
while i < tableElementIds.length
tableElementId = tableElementIds[i]
if $.isEmptyObject($.find(tableElementId))
i++
continue
table = undefined
if $.fn.DataTable.isDataTable(tableElementId)
table = $(tableElementId).DataTable()
else
table = $(tableElementId).DataTable(### OPTIONS HERE ###)
document.addEventListener 'turbolinks:before-cache', ->
table.destroy()
return
i++
return

TLDR; Here is how the conventional approach works
$(document).ready(function() {
$("#tbl-account").tableSorter();
});
It uses jQuery to initialize a table-sorting plugin once the document finishes loading. One noticeable thing here is not teardown and re-run of this js when page component switches by Turbolink. There isn't any. There didn't need to be back in the day because the browser handled the cleanup. However, in a single-page application like Turbolinks, the browser doesn't handle it. You, the developer, have to manage the initialization and cleanup of your JavaScript behaviors.
When people try to port traditional web apps to Turbolinks, they often run into problems because their JS never cleans up after itself.
All Turbolinks-friendly JavaScript needs to:
Initialize itself when a page is displayed
Clean up after itself before Turbolinks navigates to a new page.
Capturing Events
Turbolinks provides its own events that you can capture to set up and tear down your JavaScript. Let's start with the tear-down:
```js
document.addEventListener('turbolinks:before-render', () => {
Components.unloadAll();
});
```
The `turbolinks:before-render` event fires before each pageview except the very first one. That's perfect because on the first pageview there's nothing to tear down.
The events for initialization are a little more complicated. We want our event handler to runs:
On the initial page load
On any subsequent visit to a new page
Here's how we capture those events:
// Called once after the initial page has loaded
document.addEventListener(
'turbolinks:load',
() => Components.loadAll(),
{
once: true,
},
);
// Called after every non-initial page load
document.addEventListener('turbolinks:render', () =>
Components.loadAll(),
);
Thanks to Starr Horne for writing the article on migrating from jquery/pjax to turbolinks

Related

Making offline.js work with turbolinks

I am using offline.js with turbolinks and on initial page load it works fine. But if I click another links then this wont work until I refresh the page. I can see the Offline.state is down but still the view is not showing. Is there any way to manually trigger the popup window?
Update
Other than the offline.js file the only js I have is this
var
$online = $('.online'),
$offline = $('.offline');
Offline.on('confirmed-down', function () {
$online.fadeOut(function () {
$offline.fadeIn();
});
});
Offline.on('confirmed-up', function () {
$offline.fadeOut(function () {
$online.fadeIn();
});
});
This is an old question, but I ran into the same problem, maybe it can help someone.
offline.js creating these dom elements on page load inside the body HTML
<div class="offline-ui offline-ui-up">
<div class="offline-ui-content"></div>
</div>
The reason why offline.js not working after page change is that on-page change the body HTML replaced with the new content returned by the server and the code above removed.
This is how Turbolinks works, so page load will be not triggered and the offline.js dom elements will be not created.
One solution will be to warp the offline.js inside a function and call it on every page change, but it will cause eventually memory leak (as "offline" and "online" event listener will be added to 'window' on each change)
Other solution will be to save the 'offline-ui' HTML before the new page loaded and bring it back after load:
# will fire before page change and save offline.js UI
document.addEventListener("turbolinks:before-render", function() {
if (!document.offlineHtml) {
document.offlineHtml = $('.offline-ui');
}
});
# will fire afterload and will check if there is a UI to append to the body
document.addEventListener("turbolinks:load", function() {
if (document.offlineHtml) {
$(document.body).append(document.offlineHtml);
}
});
At the moment this is the best way that I could find to fix that.
This could be a turbolinks issue. In app/assets/javascripts/application.js, wrap your javascript code within:
$(document).on('turbolinks:load', function() {
// your code
});

TurboLinks with Angular

I'm running Angular 1.6 along with TurboLinks 5. For the most part, things are working well. I disabled TurboLinks cache and am manually bootstrapping Angular per some of the suggestions on this post: Using angularjs with turbolinks
I have run into one issue though where I have an $interval running within a service. When changing pages via TurboLinks, Angular bootstraps again and the service creates a new interval, but the old one continues to run! Every time a page change event occurs, a new interval is created and they keep piling on top of each other.
I tried destroying the angular app when a TurboLinks link is clicked (using the code below), but that seems to cause the whole Angular app to quit working. I also can't seem to get a reference to the older interval after a page reload.
var app = angular.module('app', []);
// Bootstrap Angular on TurboLinks Page Loads
document.addEventListener('turbolinks:load', function() {
console.log('Bootstrap Angular');
angular.bootstrap(document.body, ['app']);
});
// Destroy the app before leaving -- Breaks angular on subsequent pages
document.addEventListener('turbolinks:click', function() {
console.log('Destroy Angular');
var $rootScope = app.run(['$rootScope', function($rootScope) {
$rootScope.$destroy();
}]);
});
Without the $rootScope.$destroy() on the turbolinks:click event, everything else appears to be working as expected.
I know I could fire off an event here and kill the interval in the service, but ideally I'd like some way where this is automatically handled and ensured nothing is accidentally carried over between TurboLinks requests. Any ideas?
After a lot of trial and error, this does the job, but is not exactly what I'm looking for. Would like to hear if anyone else has any suggestions. It would be ideal if this could happen automatically without a service having to remember to cancel it's own intervals. Also accessing $rootScope in this manner just feels so dirty...
// Broadcast Event when TurboLinks is leaving
document.addEventListener('turbolinks:before-visit', function(event) {
console.log('TurboLinks Leaving', event);
var $body = angular.element(document.body);
var $rootScope = $body.injector().get('$rootScope');
$rootScope.$apply(function () {
$rootScope.$broadcast('page.leaving');
});
});
I'm then injecting $rootScope into my service as well as keeping a reference to the interval. Once it hears the page.leaving event, it cancels the interval:
var self = this;
self.interval = $interval(myFunction, intervalPoll);
....
$rootScope.$on('page.leaving', function() {
$interval.cancel(self.interval);
});
So this gets the job done... but would love to find a better way. Credit for accessing $rootScope this way came from here: How to access/update $rootScope from outside Angular

Rails 4 breaks Stellar.js after link

My rails 4 app breaks stellar.js after link_to.
I know that is something related with turbolinks.
I installed jquery.turbolinks and my js functions from uikit are all working now.
But the stellar.js still don't..
It works when I type the address in browser: 0.0.0.0:3000, but do not after clicking any link.
I am calling stellar.js from my assets/javascripts/my-js.js like this:
$(function(){
$.stellar();
});
I also tried with no success:
function initialize() {
$.stellar();
}
$(document).ready(initialize);
$(document).on('page:load', initialize);
Thank you
Your issue is most certainly related to Turbolinks. I have a feeling that when you call $.stellar() after a Turbolink request, the call has no effect since it's already attached to the window object and that doesn't go away during a the request. Try destroying it prior to initializing it again in the initialize function.
var initialize = function() {
$.stellar( 'destroy' );
$.stellar();
}
$(document).ready( initialize );
$(document).on( 'page:load', initialize );
You can also bind the destroy method to the turbolink before-cache event :
$(document).on "turbolinks:before-cache", ->
$.stellar('destroy')

Getting error on $.mobile.changePage(): Uncaught TypeError: Cannot call method 'trigger' of undefined

I have a JQM apps and I am incorporating Backbone.
Since my initial javascript code is huge, I am only extracting what I believe is problematic.
I am following the advices and calls steps cited here:
jqm-config.js from http://coenraets.org/blog/2012/03/using-backbone-js-with-jquery-mobile/
http://jquerymobile.com/test/docs/pages/backbone-require.html
I have a major problem, and this is the behaviour, the problem comes from this code:
var r = Backbone.Router.extend
router: ...
"page": "pageDisplay"
...
pageDisplay: function(){
c = new AView(); // Backbone.View ...fetch() data...
$(c.el).page(); // Call to JQM to add its extra stuff; seems done correctly
$.mobile.changePage( "#" + c.id, {changeHash: false}); // line 50
}
When following the links of <a href="#page" >, I come as expected to the
page "#page" properly processed. But once there, if I click a refresh, which is indirectly reprocessed by the same router rule, I end up with the following error:
Uncaught TypeError: Cannot call method 'trigger' of undefined
I downloaded the jquery mobile development code and observed this:
// JQM1.1.2 - Line #3772 Show a specific page in the page container.
$.mobile.changePage = function( toPage, options ) {
if ( isPageTransitioning ) {
pageTransitionQueue.unshift(arguments );
return;
}
var settings = $.extend( {}, $.mobile.changePage.defaults, options);
// Make sure we have a pageContainer to work with.
settings.pageContainer = settings.pageContainer || $.mobile.pageContainer;
// Make sure we have a fromPage.
settings.fromPage = settings.fromPage || $.mobile.activePage;
// Line #3788
var mpc = settings.pageContainer, // Line #3789
pbcEvent = new $.Event("pagebeforechange" ),
triggerData = { toPage: toPage, options: settings };
// Let listeners know we're about to change the current page.
mpc.trigger( pbcEvent, triggerData ); // Line #3794
The Uncaught TypeError is caused by Line #3794, because mpc is undefined.
So, from JQM, In the Chrome inspector, I can see also that settings.fromPage is undefined and settings.pageContainer is undefined. I kind of imagine, that JQM cannot make an assumption on the fromPage, and therefore, cannot proceed on my refresh. All the options I have tried on the $mobile.changePage() have not succeed. I am out of ideas.
UPDATE/ Online site with the minimum to reproduce the problem:
apartindex, access the website with the bug
Any help will be appreciated.
The dextoInit function that calls the router code is called in $(document).ready() which does not guarantee that the jQuery mobile page has actually been set up successfully. But the router code calls $.mobile.changePage which depends on jQuery Mobile being initialized.
Putting it into mobileinit or pageinit should work.
(Unfortunately I can't modify the code and test it easily.)
Although, this fix it for the moment, it does have drawbacks. See below.
$(document).bind("pageinit", function(){
console.log('bindtomobileinit: event pageinit received');
if ( !window.AppNode.router ){
window.AppNode.router = new AppNode.singletons.router();
console.log("mobileRouter.js: Starting b history");
console.log('mobileRouter.js: About to launch Backbone history');
Backbone.history.start();
}
});
Registering to pageinit has a weird effect of being fired twice. I see that 2 nodes have been added to the Dom: the default "loading" jquery mobile div (related to pageinit:1), and my data-role page (pageinit:2). So on a "refresh browser click", my situation leaves me waiting for a first pageinit, creating an unexpected jquery mobile dom element (a default page created to display the waiting JQM circle animation), which trigger the router creation, and allows the Backbone.history call which then deal with my "" home page. The second pageinit do not interfere with the settings since I execute it only once.
I am really disappointed by this setup. I will leave this question for now, since it does sort of work.
I've found the source of the problem to be jquery-mobile version 1.3.0. When I fall back to either JSM 1.2.0 or 1.2.1, the "Uncaught TypeError: Cannot call method 'trigger' of undefined" problem goes away.
BTW, I am not using Backbone.
I had fixed this problem by using method append(), but not html()
$('body').append(view.render().$el);
I was able to resolve this issue by changing the page data property from "data-role" to "data-mobile-page" as what is referenced in line 4042 of jqm 1.3.2
fromPage.data( "mobile-page" )._trigger( "beforehide", null, { nextPage: toPage } );
Setting
$.mobile.autoInitializePage = true;
In your jquery mobile config file, some place like:
$(document).on("mobileinit", function () {...});
May help.

Jquery calls not working in $viewContentLoaded of Angular

Unable to call jquery functions in $viewContentLoaded event of Angular controller, here is the code for the same.
$scope.$on('$viewContentLoaded', function() {
jQuery.growlUI('Growl Notification', 'Saved Succesfully');
jQuery('#category').tree()
});
Is any configuration required here?? I tried even noConflict(); var $jq = jQuery.noConflict();
Does it require any other configuration?
Thanks,
Abdul
First thing first, don't do DOM manipulation from controller. Instead do it from directives.
You can do same thing in directive link method. You can access the element on which directive is applied.
Make sure you load jquery before angularjs scripts, then grawlUI, three, angularJS and finally your application script. Below is directive sample
var app = angular.module("someModule", []);
app.directive("myDirective", function () {
return function (scope, element, attrs) {
$.growlUI('Growl Notification', 'Saved Succesfully');
element.tree();
};
});
angularjs has built in jQuery lite.
if you load full jquery after angular, since jQuery is already defined, the full jquery script will skip execution.
==Update after your comment==
I reviewed again your question after comment and realised that content which is loaded trough ajax is appended to some div in your angular view. Then you want to apply element.tree() jquery plugin to that content. Unfortunately example above will not work since it is fired on linking which happened before your content from ajax response is appended to element with directive I showed to you. But don't worry, there is a way :) tho it is quick and dirty but it is just for demo.
Let's say this is your controller
function ContentCtrl($scope, $http){
$scope.trees=[];
$scope.submitSomethingToServer=function(something){
$http.post("/article/1.html", something)
.success(function(response,status){
// don't forget to set correct order of jquery, angular javascript lib load
$.growlUI('Growl Notification', 'Saved Succesfully');
$scope.trees.push(response); // append response, I hope it is HTML
});
}
}
Now, directive which is in controller scope (it uses same scope as controller)
var app = angular.module("someModule", []);
app.directive("myDirective", function () {
return function (scope, element, attrs) {
scope.$watch("trees", function(){
var newParagraph=$("<p>" + scope.trees[scope.trees.length-1] + "</p>" ); // I hope this is ul>li>ul>li...or what ever you want to make as tree
element.append(newParagraph);
newParagraph.tree(); //it will apply tree plugin after content is appended to DOM in view
});
};
});
The second approach would be to $broadcast or $emit event from controller (depends where directive is, out or in scope of controller) after your ajax completes and you get content from server. Then directive should be subscribed to this event and handle it by receiving passed data (data=content as string) and do the rest as I showed you above.
The thing is, threat that content from ajax as data all the way it comes to directive, then inject it to element in which you want to render it and apply tree plugin to that content.

Resources