Cleanly deploying PJAX or Turbolinks application - ruby-on-rails

If I have an application that uses PJAX or Turbolinks then I am seeing a problem when new code is deployed to the server - say Heroku. The issue is that users who are accessing the application will continue to use the Javascript from the previous version of the application (since it is already loaded into the browser), but will get the HTML pages from the new version. Sometimes the new HTML code assumes the new Javascript is loaded so things don't work properly.
Have others noticed this problem? What do you do about it? It seems like this would be a common problem for single-page Javascript applications (like those based on Backbone and Ember) too. Meteor at least seamlessly upgrades the code in the client, as a way to deal with this problem.

https://github.com/rails/turbolinks/#asset-change-detection looks like the answer. In your script tags, put data-turbolinks-track. Then I assume what happens is that when turbolinks loads a page, it looks for that script tag, and if the URL has changed (it will change automatically if you use the asset pipeline) then it reloads the whole page for you.

taking pivotaltracker as an example, they check for the client connection and force the user to reload their clients if a new version arrived.
as long as you don't provide backwards compatibility within your app, there is nothing else you can do.

If you're using Pjax, you can add
<meta http-equiv="x-pjax-version" content="v123">
to your header. You can use a server defined value for the content attribute, and then before you deploy, just make sure that value is updated. Once it's deployed, subsequent Pjax requests will see this header has been updated, cancel it's Ajax request, and pull down a full page reload.
For Turbolinks 5, you can add data-turbolinks-track="reload" to your script and stylesheet links in the header. Turbolinks will watch these files for changes on each request. Then upon deployment, just like Pjax, it will request a full page reload.
Classic Turbolinks just requires the attribute data-turbolinks-track instead.
You can read more and see other functions on Pjax's GitHub page: https://github.com/defunkt/jquery-pjax or Turbolinks: https://github.com/turbolinks/turbolinks.
For Turbolinks Classic: https://github.com/turbolinks/turbolinks-classic

Related

Using angularjs with turbolinks

I am trying to use Angularjs framework in my app with turbolinks. After page change it do not initialize new eventlisteners. Is it any way to make it work? Thanks in advance!
AngularJS vs. Turbolinks
Turbolinks as well as AnguluarJS can both be used to make a web application respond faster, in the sense that in response to a user interaction something happens on the web page without reloading and rerendering the whole page.
They differ in the following regard:
AngularJS helps you to build a rich client-side application, where you write a lot of JavaScript code that runs on the client machine. This code makes the site interactive to the user. It communicates with the server-side backend, i.e. with the Rails app, using a JSON API.
Turbolinks, on the other hand, helps to to make the site interactive without requiring you to code JavaScript. It allows you to stick to the Ruby/Rails code run on the server-side and still, "magically", use AJAX to replace, and therefore rerender, only the parts of the page that have changed.
Where Turbolinks is strong in allowing you use this powerful AJAX mechanism without doing anything by hand and just code Ruby/Rails, there might come a stage, as your application grows, where you would like to integrate a JavaScript framework such as AngularJS.
Especially in this intermedium stage, where you would like to successively integrate AngularJS into your application, one component at a time, it can make perfectly sense to run Angular JS and Turbolinks together.
How to use AngularJS and Turbolinks together
Use callback to manually bootstrap Angular
In your Angular code, you have a line defining your application module, something like this:
# app/assets/javascripts/angular-app.js.coffee
# without turbolinks
#app = angular.module 'MyApplication', ['ngResource']
This code is run when the page is loaded. But since Turbolinks just replaces a part of the page and prevents an entire page load, you have to make sure, the angular application is properly initialized ("bootstrapped"), even after such partial reloads done by Turbolinks. Thus, replace the above module declaration by the following code:
# app/assets/javascripts/angular-app.js.coffee
# with turbolinks
#app = angular.module 'MyApplication', ['ngResource']
$(document).on 'turbolinks:load', ->
angular.bootstrap document.body, ['MyApplication']
Don't bootstrap automatically
You often see in tutorials how to bootstrap an Angular app automatically by using the ng-app attribute in your HTML code.
<!-- app/views/layouts/my_layout.html.erb -->
<!-- without turbolinks -->
<html ng-app="YourApplication">
...
But using this mechanism together with the manual bootstrap shown above would cause the application to bootstrap twice and, therefore, would brake the application.
Thus, just remove this ng-app attribute:
<!-- app/views/layouts/my_layout.html.erb -->
<!-- with turbolinks -->
<html>
...
Further Reading
AngularJS bootstrapping: http://docs.angularjs.org/guide/bootstrap
Railscasts on Turbolinks (explains callbacks): http://railscasts.com/episodes/390-turbolinks
Demo application: https://github.com/fiedl/rails-5-angular-and-turbolinks-demo
Turbolinks attempt to optimize rendering of pages and would conflict with normal bootstraping of AngularJS.
If you are using Turbolinks in some places of your app and some parts use Angular. I propose this elegant solution:
Each link to a page that is angularapp (where you use ng-app="appname") should have this attribute:
<a href="/myapp" data-no-turbolink>Say NO to Turbolinks</a>.
The second - mentioned on Stackoverflow is explicitly reloading/bootstrapping every ng-app by handling page:load event. I would that's intrusive, not to mention you're potentially loading something that isn't on a page hence wasting resources.
I've personally used the above solution.
Hope it helps
In case of bug
Uncaught Error: [ng:btstrpd] App Already Bootstrapped with this
Element 'document'
after upgrade to angular 1.2.x you can use below to fix problem.
angular.module('App').run(function($compile, $rootScope, $document) {
return $document.on('page:load', function() {
var body, compiled;
body = angular.element('body');
compiled = $compile(body.html())($rootScope);
return body.html(compiled);
});
});
In previous post #nates proposed to change angular.bootstrap(document, ['YourApplication']) to angular.bootstrap("body", ['YourApplication']) but this causes a flash of uncompiled content.
Add the following event handler to your application.
Coffeescript:
bootstrapAngular = ->
$('[ng-app]').each ->
module = $(this).attr('ng-app')
angular.bootstrap(this, [module])
$(document).on('page:load', bootstrapAngular)
Javascript:
function bootstrapAngular() {
$('[ng-app]').each(function() {
var module = $(this).attr('ng-app');
angular.bootstrap(this, [module]);
});
};
$(document).on('page:load', bootstrapAngular);
This will cause the angular application to be started after each page loaded by Turbolinks.
Credit to https://gist.github.com/ayamomiji/4736614
Turbolinks doesn't quite make sense with an client side MVC framework. Turbolinks is used to to strip out the all but the body from server response. With client-side MVC you should just be passing JSON to the client, not HTML.
In any event, turbolinks creates its own callbacks.
page:load
page:fetch
page:restore
page:change
The jquery.turbolinks plugin can trigger bootstrapping of modules via ng-app directives. If you're trying to manually bootstrap your modules, jquery.turbolinks can lead to ng:btstrpd errors. One caveat I've found is that jquery.turbolinks relies on the page:load event, which can trigger before any new page-specific <script> tags finish running. This can lead to $injector:nomod errors if you include module definitions outside of the application.js. If you really want your modules defined in separate javascript files that are only included on certain pages, you could just disable turbolinks on any links to those specific pages via data-no-turbolink.
Based on the comments I've seen, the only valid scenario for using both together in a way where Angular would conflict with Turbolinks (such as where I allow Angular to handle some of the routing) is if I have an existing application that I'm trying to port to Angular.
Personally, if I were to do this from scratch, I think the best solution would be to decide what should handle the routing and stick with that. If Angular, than get rid of Turbolinks -> it won't do much for you if you have something close to a single-page app. If you allow Rails to handle the routing, then just use Angular to organize client-side behavior that can't be processed by the server when serving up the templates.
Am I missing a scenario, here? It doesn't seem elegant to me to try to split the routing responsibilities between different frameworks, even in a large application... Is there some other scenario where Turbolinks would interfere with Angular other than refreshing the page or navigating to a new route?
Using Turbolinks and AngularJS together
+1 to #fiedl for a great answer. But my preference is to make use of page:change in concert with page:load because this affords some flexibility: the DOM can receive a page:load event from sources other than turbolinks, so you might not want to have the same callback fire.
Watching for a page:change, then a page:load should restrict your callback behaviour to solely turbolinks-instigated events.
function boostrapAngularJS () {
angular.bootstrap(document.body, ['My Application']);
addCallbackToPageChange();
}
function addCallbackToPageChange() {
angular.element(document).one('page:change', function () {
angular.element(this).one('page:load', boostrapAngularJS);
});
}
addCallbackToPageChange();
(This will allow/require you to keep your ng-app declaration in your html, as normal when working with AngularJS.)
Turbolinks automatically fetches the page, swaps in its <body>, and merges its <head>, all without incurring the cost of a full page load.
https://github.com/turbolinks/turbolinks#turbolinks
So, instead of append ng-app directive on the <html> element, we can just do it on the <body> element.
<html>
<body ng-app=“yourApplicationModuleName">
</body>
</html>

MVC Bundling with HTTPS IE7

I have successfully implemented MVC bundling for my MVC application. There is one problem with the run time which runs under HTTPS.
I am sure there is a problem because when I switch the debug field to false the user gets the warning message "This page contains secure and nonsecure items. Do you wish to proceed?
I know that I can turn this prompt off using the security setting in IE. I would like to know if there is something I can do to the application so that bundled scripts and styles come through the secure pipe.
If you use the Scripts.Render helper to include the bundle it will use the same HTTP scheme as the one used to request the main page. So if the main request was done over HTTPS then this helper will generate a <script> element using HTTPS. You could use the Net tab of FireBug to see which resources are served through HTTP and HTTPS and be able to isolate the problem.
Thank you for this suggestion. I figured out that the problem was coming from modernizr-1.7.js
The strange thing was that this problem only occurs when modernizr is bundled. I removed modernizr because we don't really need it.

How to get Back button working between remote and local jQuery Mobile pages?

I have a local jQuery Mobile project going (inside PhoneGap, thus file:// protocol) where I sometimes need to fetch external pages (using http://) from a server where the content too are jQM pages with almost identical markup (except for the content, which is generated from a CMS).
Setting $.mobile.allowCrossDomainPages to true gives me the page, and that is all right. Going Back, however, fails. I get stuck in a place where /www/index.html is not found on the server (like, doh, of course..). Is there a way to "remember" where I came from, taking me back to the local html page I originally came from?
We just added a docs page on PhoneGap in jQuery Mobile for RC3 that should help you out quite a bit:
http://jquerymobile.com/test/docs/pages/phonegap.html

Wrong layout used on first load after switching to/from mobile version of application

I have 2 application layouts: application.html.haml and application.mobile.haml. But when I switch from mobile to the non-mobile version the mobile layout is still used for the first load. In Firebug console I see that the non-mobile view was properly returned, however the browser and the Firebug HTML tab show the mobile layout. Any idea what's going? It's fine after a refresh.
The issue turned out to be caused by JQuery Mobile.
I found the answer here: http://jquerymobile.com/demos/1.0a3/docs/pages/docs-pages.html
JQuery Mobile doesn't do full page reloads unless it's told to, so although the full html was returned, JQuery Mobile just replaced the page portion.
The key portion from the docs is:
"It's important to note if you are linking from a mobile page that was loaded via Ajax to a page with multiple internal pages, you need to add a rel="external" or data-ajax="false" to the link. This tells the framework to do a full page reload to clear out the Ajax hash in the URL. This is critical because Ajax pages use the hash (#) to track the Ajax history, while multiple internal pages use the hash to indicate internal pages so there will be a conflicts."
Recently, I encountered a similar problem.
In my case, Rails4's turbolinks suffered. Similarly as JQuery Mobile, turbolinks only updates the body part of a full page without updating the head part of the html when an intra-site link is clicked. I simply removed the require line for turbolinks in my application.js and the problem disappeared.

Rails and javascript cache

I'll start to develop a new app that uses a lot of heavy js librarys (prototype, scriptaculous, tinymce and so on).
Someone told me to make all the app using ajax, so all the js files will be loaded only once.
My question is, I really must do everything on ajax?
Lets say if I call myapp.com/projects and projects use all those js files, then I click on a "show" link and I'm redirected to myapp.com/projects/1 on this redirect, all js will be reloaded again?
No, your Javascript files will not be loaded again, they will be cached on the client.
But yes, your application will need to check with the server at every page load, the server often responding that the scripts have not changed. By using AJAX, you reduce the number of connections to the server. You can reduce the number of connections by concating all you Javascript files into one.
Note however that AJAX will add some new problems, like forcing you to track memory leaks as your application will never unload its objects if you never reload a new page.
If you are not at ease with Javascript, I strongly suggest sticking with the "old" model of reloading the page everytime. If you have performance issues, you can deal with them later.
Reloading Images, Scripts, etc...
Short answer: yes
Long answer: depends
When you view a page, your browser will request the HTML, once it has the HTML it will start to load external references (images, scripts, etc...). When it goes to request an image or a script, your browser may send a header which says when it got it last and stored it in its cache. The web server may respond with a 304 Not Modified code which tells the browser to use the cached version saving it from downloading it again.
Even if the browser doesn't use these headers, it will still be caching, it just won't know when the cache should expire. When you use the rails helpers to include images and scripts, it will append a number onto the end of the url which is unique to scripts contents. So if you change the contents, a new url will be used forcing the browser to get the updated version.
Use Google's Ajax Libraries API! Google now hosts the most popular js libraries including Prototype, Scriptaculous and jQuery. Once they host a specific version they are committed to hosting that version indefinitely.
There is a small Rails plugin by Ryan Heath at github:
script/plugin install git://github.com/rpheath/google_ajax_libraries_api.git
Then in your views instead of using the default
<%= javascript_include_tag :defaults =>
use this instead:
<%= google_jquery =>
<%= google_prototype =>
<%= google_scriptaculous =>
You can specify versions if you want. Check out Ryan's readme at github for more information.
This way you don't have to bother setting up an asset host (at least not for your standard javascript) and save yourself a truckload of bandwidth!

Resources