TurboLinks with Angular - ruby-on-rails

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

Related

Why does Adobe Analytics call fail to fire even though DTM Switch shows the Satellite call?

I'm trying to attach a DTM Event Based Rule to a Social Share button from Add This, and it's not working.
I have other rules on the same page which are working fine, so I'm confident all the setup basics are correct.
In fact it almost works... In the log below... why does DTM Switch report event13 but then it doesn't show up in the Adobe Analytics Server Call?
Still not fully clear why it partially works (as opposed to not working at all), but the problem seems to be caused by attempting to bind Event Based Rules to elements that were injected into the DOM via Javascript (such as the AddThis API).
Solved by using a custom event handler to dispatch a Direct Call Rule:
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(event) {
onElementInserted("body", '.at-share-btn', function(element) {
$(element).one('click', function() {
var network = $($(this).find('title')[0]).text();
window.digitalData.event.socialNetwork = network;
_satellite.track('social-network');
return true;
});
});
});
</script>
where onElementInserted() is borrowed from jquery detecting div of certain class has been added to DOM
Is it an s.tl() beacon? Is event13 set in custom code? I'd doublecheck that s.linkTrackEvents is set to allow event13- see Omniture events is not firing/sending data via DTM when using s.tl tracking methods for more info on that.

Rails 5 beta Turbolinks events not firing, progress bar not working

I'm kind of new to Rails and web development as a whole.
I'm trying to set up the default progress bar in Turbolinks for rails just like described here.
At first, I thought it might have to do with the fact that the progress bar only shows if a page takes longer than 500ms to load. I tried testing this using sleep 10 in my controller but it doesn't seem to work.
Then I tried debugging the actual turbolink events to see if the callbacks are registering using vanilla js straight in my application.js:
document.addEventListener("turbolinks:load", function () {
console.log("load");
});
document.addEventListener("turbolinks:request-start", function () {
console.log("request start");
});
document.addEventListener("turbolinks:visit", function () {
console.log("visit");
});
document.addEventListener("turbolinks:render", function () {
console.log("render");
});
For some reason, only the turbolinks:load function seems to work.
What am I not seeing or doing wrong?
Thanks.

Making jQuery works with Turbolinks

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

jQuery does not work in IE9 in first attemp; on pressing F5 (refresh) it works

(I have already asked this question; here I am adding more details)
I have return a jQuery which returns the text entered in input element on change event. This jQuery works fine in FireFox but fails in Internet Explorer (IE9).
<script>
$(document).ready(function () {
$("#UserName").change(function () {
alert("Text Entered Is :"+$("#UserName").val());
});
});
</script>
1) I am using ASP.NET MVC; to reach the page having above jquery I am using Html.ActionLink
2) On IE when I reach on the page of above jQuery it does not work; but when I press F5 and refresh the page it works.
3) On Firefox I do not need to refresh the page; it works on very first attempt.
Please help...
The $(document).ready() event depends on an event called DOMContentLoaded (in Chrome, not really sure about firefox, but it is there by some name).
This event is fired as soon as the HTML has been loaded and all the relevant files have been brought in (the CSS and the JS files).. Chrome, Safari (WebKit) and Firefox(Gecko) are pretty predictable at firing this particular event. It bubbles up the stack like clockwork and once caught, you can play fiddle with it, for all anyone cares.
However, as far as IE is concerned, up till version 7 (I think) they never implemented this event. Given that, the implementation in the recent versions is not so efficient as well.
I have faced this problem time and again.
One workaround for this that has worked everytime is to fire the event manually at the end of the HTML:
<script type="text/javascript">
try{jQuery.ready();}catch(e){}
</script>
Hope this helps
Edit
A great way of seeing whether the event gets fired is to add somekind of action in the code. SOmething like:
<script>
$(document).ready(function () {
alert("Loaded");
$("#UserName").change(function () {
alert("Text Entered Is :"+$("#UserName").val());
});
});
</script>
alert blocks the execution of the code, due the single threaded nature of Javascript. Therefore, you can use this to get a pseudo-stacktrace of your code.

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.

Resources