Using angularjs with turbolinks
Asked Answered
S

9

60

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!

Sandpiper answered 10/2, 2013 at 12:53 Comment(1)
Would it be a good idea to use turbolinks with MVC framework on the client. Is turbolinks not about reducing the Javascript to bare minimums to get an advantage of coding only in Ruby for serving client pages as well as run server code ?Spay
O
127

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

Orpha answered 18/3, 2013 at 23:34 Comment(10)
A gave the bootstrap technique a try. However I ran into an issue where multiple events are now getting create on my directives. For example if turbolinks loads a view with: ng-click="save()" then reloads the view... my save() method now fire three times. Any ideas?Exonerate
I was able to resolve the multiple event issues and possibly other buggy scenarios by moving Angular into the <head> section of our admin (thus, preventing multiple instantiations of my scripts). It's not a public-facing experience so I'm not too concerned about the pitfalls of loading JS in the html head section.Exonerate
This method was working great for me with angularjs 1.0.6. But after upgrading to angularjs 1.2.0.rc3, I get this in the console after navigating to a new page via turbolinks: Uncaught Error: [ng:btstrpd] App Already Bootstrapped with this Element 'document'. Apparently an element can only be bootstrapped once. Changing angular.bootstrap(document, ['YourApplication']) to angular.bootstrap("body", ['YourApplication']) seems to work without any issues.Zsa
I had to use "body", but also had to remove the "ready" from the $(document).on line, and added a $ -> insteadUndoing
I used 'body' but added unless $('body').hasClass('ng-scope') (coffeescript!) after angular.bootstrapSemiyearly
Great answer! If using Turbolinks 5, the event name has to be changed to $(document).on "turbolinks:load", -> …. Thanks!Lemley
Didn't work, and why are you using coffeescript instead of js? For general usage, javascript would be preferred, so I don't have to convert it.Thermo
Confirmed working on Rails/Turbolinks 5 and Angular 1.5.8 using $(document).on('turbolinks:load', function() { angular.bootstrap(document.body, ['myApp']); }) Thanks!Neath
I've updated the answer for rails 5 (turbolinks 5) and angular 1.5.8, and I added a demo application on github. Feel free to inspect the commits to see the separate steps.Orpha
I just ran into an issue with this method. I had an interval running in a service and this caused it to start a new interval on every page load, and the old ones wouldn't die off, causing them to stack up. Came up with a very dirty solution but would love to hear any other ideas. https://mcmap.net/q/330318/-turbolinks-with-angular/…Neath
E
9

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

Elysha answered 11/8, 2014 at 18:38 Comment(2)
It's useful in a very large app to minimize the conflictionPinchbeck
Appreciate the feedback :)Elysha
L
4

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.

Longship answered 12/1, 2014 at 16:0 Comment(0)
L
2

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

Lend answered 3/10, 2013 at 0:6 Comment(5)
Really finding it hard to adopt Coffeescript! The non-coffee version of this is much cleaner to me.Miki
This works for me, except for if you go to the page directly. sucks!Scharaga
Yes, to be more clear, this only covers pages loaded by Turbolinks. You'll also want to include $(document).on('ready', bootstrapAngular).Lend
No, I got you ALL beat. You SHOULD be listening for ready AND page:load, BUT you should NOT use ng-app. When angular sees ng-app, it will try to load your module, but it's too soon. Use another custom attribute, like "my-app" instead of "ng-app" to avoid this problem. My issue was it was trying to load up my module, BEFORE the previous code actually loaded in my module.Scharaga
This works better than the accepted solution. You get downvote for not using javascript and an upvote for the right solution.Thermo
P
1

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
Pash answered 10/2, 2013 at 13:37 Comment(2)
I am using angular js only on few pages of my app. And i though that there is any way to do it without abandonning turbolinksSandpiper
I have tried hooking into the callbacks, it causes problems with window.history and location. I am having a difficult time articulating them but I can tell you the implementation is not trivial and disabling turbolinks resolves the problem, ref: #30804736Ligialignaloes
U
1

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.

Utopia answered 8/1, 2014 at 22:32 Comment(1)
I should have read this closer before I posted, this at least partially answers my question in the post. Is there anything else useful to know about how Turbolinks works when I have an app that makes some use of a front-end framework?Dolorisdolorita
D
0

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?

Dolorisdolorita answered 22/2, 2015 at 22:19 Comment(0)
B
0

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.)

Buckingham answered 27/6, 2015 at 16:35 Comment(0)
F
0

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>
Fujimoto answered 28/4, 2016 at 22:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.