Twitter widget leaking memory on backbone view - javascript

I'm building a website using backbone and coffeScript, when i change from one section (innerpage) to another i replace the view rendered on the layout for the new one and call a close method on the old one. Everything seems to work fine, excep that to render the widget again on the home view, i need to call twttr.widgets.load() , and its creating a new widget everytime, but it seems the old ones are being retained, it doesn't matter if i remove the view or the iframe node, or make the container element empty. I've tried detaching the widget before removing the view, store it on a variable and use it to place the widget when the home view is called again, without the need to call the load method, but the iframe element is the only thing that is being kept in the variable (even though before removing the view it has everything on it), i've tried cloning the node, using the iframe contents, everything has failed so far. I can't seem to find a method to unload the twitter widget in the twttr obj, so i'm ending up with a nasty memory leak, it adds up to 3mb everytime it loads the home view, so this is not an option, specially for mobile, i can end up with 100mb being allocated to the ghost widgets.
root = window ? global
root.Backbone.View::close = ()->
#remove()
class Otalvaro.Views.BaseContent extends Backbone.View
initialize: ->
#model.bind('sync', #render, this)
Here is my home view( In this version i'm trying to save the iframe contents, the normal version, does not check for inMemory closes the view with #remove() and removes the iframe directly)
EDIT Note : Removing the view wasn't working here, since on every new instanciation, the widget will create a new iframe (the widget id is even increased), so i'm trying to keep only the homeview on memory, since is better having 1 iframe in memory than 30 ghost iframes.
class Otalvaro.Views.Home extends Otalvaro.Views.BaseContent
template: root.template('homeTemplate')
initialize: ->
super
#inMemory = no
closeTwitterWidget: ->
$('iframe').unbind().remove()
close: =>
#$el = #$el.clone()
#el = #$el[0]
#iframe = #$el.find('iframe').contents()
#inMemory = yes
render: (callback)->
if(#inMemory)
console.log '#iframe', #iframe
else
console.log 'not in memory, doing normal rendering'
if _.isEmpty(#model.toJSON())
console.log 'model is empty, fetching it now'
#model.fetch()
else
console.log 'model fetched, now rendering'
#$el.html( #template(#model.toJSON()) )
# With out the delay, the widget won't load everytime
_.delay( ->
console.log 'firing twttr.widgets.load()'
root.twttr.widgets.load() # <- Guilty of all charges
, 630)
if callback? then callback(#el)
this
This is part of the code of the layoutView, wich renders the views on route change:
class Otalvaro.Views.MainLayOut extends Backbone.View
el: '#mainContent'
closeOldViews: ->
if #oldViews?
for view in #oldViews
console.log 'view to be closed', view
view.close()
###
# Takes an array containing one or more view instance as argument, adds a fadeOut fx to hide the current content
# then it renders each view instance from the array and extracts its node element (el) and pushes it into an array
# that is later (after the fadeIn completes) added as the html content of the layoutView
###
show: (views)->
#fadeOut()
delay = 500
viewNodes = []
for view in views
node = view.render().el
viewNodes.push(node)
_.delay(
_.bind ->
#closeOldViews()
#$el.html(viewNodes)
#fadeIn()
#oldViews = views
, this
delay)
Router Handler for home, using an already instanciated view, (also tried instanciating the home view on route change, but didn't help)
home:->
#mainLayout.show([#homeView])
null
Edit: Added ->Home Template
<script id="homeTemplate" type="text/x-handlebars-template">
<blockquote id="homeQuote">
<p>Some Quote</p>
<footer>Catalina Otalvaro</footer>
</blockquote>
<div class="twitterFeed fadeIn">
Tweets by #kataotalvaro
</div>
</script>
I've read this couple of topics on the tiwtter developers forum, but they got no replies and i'm not very experienced with this, so if anybody outthere has any advice on how should i solve this , i really appreciate it, thanks for taking your time in reading my question :)
https://dev.twitter.com/discussions/30128
https://dev.twitter.com/discussions/5957

Your close method in Otalvaro.Views.Home is overriding the base close method, which means that every time you close that home view, you're not actually removing the view and, further, you're cloning the element:
#$el = #$el.clone();
It could be that the closeTwitterWidget would help with some of that by removing the iframe, but I don't see where that's being called.
Try changing the close method in your home view to just #remove() and see how it goes.
Also, is the Twitter widget definitely loading inside the element for your home view?

I managed to solve this issue, by storing the widget (<div> inside the widget iframe) on a property of my view, to get the widget is not as straightforward as it is to store any node from your document, and the storing must be made before the iframe container (main container) is empty or the iframe will be empty too. Now the main issue on this path, was to reappend the widget to the view, but i found out is not possible (or it seems that way) to add html content to an iframe that is not currently on the DOM, since that was my case because the iframe is actually just in the template of the view, i decided to attach the widget to a div, then all i needed was to take the iframe styling and add it to my main style (with some modifications), now when i rerender my home view is taking the elment from memory and there is no memory leak, i get around 6mb tops when navigating and never goes up, using the widget.load() it went as high as 50mbs. I don't think this is the ideal solution, its actually a hack, twitter should provide a way to unload the widget not just the load method. I hope this helps if somebody is on the same spot. Thanks #glortho for taking your time to answer =)
Here is the modified home view
class Otalvaro.Views.Home extends Otalvaro.Views.BaseContent
template: root.template('homeTemplate')
initialize: ->
super
#inMemory = no
#memoryRender = no
close: =>
unless #inMemory
#$iframe = $( $('.twitter-timeline').contents().find('body').html() )
#inMemory = yes
render: (callback)->
unless #memoryRender
if(#inMemory)
#$el.find('.twitterFeed').html(#$iframe)
#memoryRender = yes
else
console.log 'not in memory, doing normal rendering'
if _.isEmpty(#model.toJSON())
console.log 'model is empty, fetching it now'
#model.fetch()
else
console.log 'model fetched, now rendering'
#$el.html( #template(#model.toJSON()) )
_.delay( ->
console.log 'firing twttr.widgets.load()'
root.twttr.widgets.load()
, 630)
if callback? then callback(#el)
this

Related

Rails is creating multiple shopping cart records per click

I am following this guide for creating a shopping cart model:
https://richonrails.com/articles/building-a-shopping-cart-in-ruby-on-rails
I got it to work successfully, but still have a problem. When I load the page, and add an item, one is added, if I go to another page, and then load the home page again, through a side bar menu I have, I click a product, and that same product gets added 3 times to the shopping cart. I go to another page and return, 5 items per click, again 7 items per click.
I have no idea why this is happening, I don't even know what part of my code to show, so someone can help me.
If I reload the page (by clicking the address bar and enter), it goes back to adding one item per click.
Thanks in advance
EDIT: After first comment suggestion, here is the controller code.
def create
#invoice = current_invoice
#invoice_product = #invoice.invoice_products.new(invoice_product_params)
#invoice.save
session[:invoice_id] = #invoice.id
end
def update
#invoice = current_invoice
#invoice_product = #invoice.invoice_products.find(params[:id])
#invoice_product.update.attributes(order_item_params)
#invoice_products = #invoice.invoice_products
end
def destroy
#invoice = current_invoice
#invoice_product = #invoice.invoice_products.find(params[:id])
#invoice_product.destroy
#invoice_products = #invoice.invoice_products
end
private
def invoice_product_params
params.require(:invoice_product).permit(:id, :invoice_id, :product_id, :price, :tax, :value)
end
that same product gets added 3 times to the shopping cart. I go to
another page and return, 5 items per click, again 7 items per
click
This has all the hallmarks of Turbolinks and bad JS binding.
--
Let me explain...
Turbolinks makes following links in your web application faster.
Instead of letting the browser recompile the JavaScript and CSS
between each page change, it keeps the current page instance alive and
replaces only the body (or parts of) and the title in the head. Think
CGI vs persistent process.
In short, Turbolinks uses "Ajax" to load the <body> of the next page, replacing your current <body> content. Whilst this does speed up processing (by removing the need to recompile CSS/images), it causes havoc with JS bindings.
JS "binds" to elements in the DOM:
It expects there to be elements for it to bind to. This works very well in most cases:
element = document.getElementById("your_element").addEventListener('click', function() {
console.log('anchor');
});
However, the problem with using Turbolinks (and especially JQuery) is the binding can occur multiple times depending on how many times Turbolinks just loads new data into the DOM.
The issue is because your Javascript is not refreshing, but your DOM elements are, JS is treating them like new elements, thus triggering the function x number of times with each click. Kind of like n+1 I suppose.
--
In answer to your problem, the issue will lie with your JS bindings:
#app/assets/javascripts/application.js
bind = function() {
$("#shopping_cart").on("click", function(){
//payload
});
};
$(document).on("ready page:load", bind);
The above will you the "local" selection for the elements, and using the page:load Turbolinks hook, will make sure it refreshes each time Turbolinks is requested.
If you wanted to do it without having to redeclare each time Turbolinks is called, just delegate from the document:
#app/assets/javascripts/application.js
$(document).on("click", "#shopping_cart", function(){
//payload
});

Trouble forcing browser to treat button click as history traversal event

I am building a website that is navigated through a series of directional buttons. Right and left clicks move between different images associated with one project. Up and down clicks move between projects.
What I am trying to do is ensure that when a user clicks up or down (i.e. between projects) that this is registered as a history traversal event and that a new entry for the new project is visible within the browser's history.
The code that I have sets an event on the the click of the navigation buttons that makes the necessary changes to the pages content, and then I attempt to push the new page to the history object by calling this function (see here and here for background information):
var pushToHistory = function(url, pageTitle, html) {
history.pushState({'html':html, 'pageTitle':pageTitle}, '', url);
}
I supply the arguments to this function elsewhere by doing the following after the page has loaded the new content:
html = document.getElementsByClassName('main-frame')[0].innerHTML
pageTitle = 'http://mysite/'+newpath; // new path specifieds the new item.
this.pushToHistory( url, pageTitle, html);
Now the problem I am having is that the result of all of this is that both in Chrome and in Firefox the history is updated in a way that is mostly correct: the url in the history's state object is correct, as is this the content. So that if I click on one of these history events, the correct page is retrieved.
However, the page title, which is shown in the list of history items is incorrect. It is always the title of the page that was loaded when the site was intially loaded. So if I load mysite/a-project, and the title of the page is "My Site - A Project" that is always what appears in the box. I have also checked my code to ensure that the pageTitle object is correct, so dumping pageTitle before calling history.pushState() shows the correct title.
Any ideas?
I discovered the problem. I needed to set document.title first. That meant somewhere before I call history.pushState(), I needed to do this:
document.title = theNewTitle;

Backbone.js using el doesn't work but using $('selector') does. Why?

So, I admit that it might be tricky for somebody to follow my design and build patterns. But I will try best to describe the issue, although I feel this might be easier than getting you to understand my project :) But hell I've spent 3 days trying to figure this one out.
Summery of technical layout
I have a global view (lets call this GV, for global view), everything goes through this template.
I have navigation, this can call one of the 5 separate views on click.
Each of these views first uses the same view (DV for defaultView) which then extends this view and loads details from its own JST (OV for OwnView)
The problem is that, when clicking to load an OV the element that it is bound to load into (target) has not yet been rendered to the page. It is however available in the DOM, and in fact updates the DV with its OV but does not render to the page.
Brief overview - To give you a idea of how my views are set up and what extends what
GV - Loads: login, Home, Global Navigation
DV - Loads: the navigation for this section.
DV extends GV: SD.defaultView.extend
OV - Loads: h2, icon for the option and buttons
each OV extends DV so that I can keep all the click events in one view and not have to paste code all over: SD.defaultSexView.extend
The problem
OV loads into an element that is inside DV. GV and DV all load perfectly. I can click from, login -> home -> navigation -> through which would then load in the DV. This is where the interactions break down. All that is loaded is whatever was inside the JST for DV
Build
Just to give some background information:
I am using JST's to precompile all of my templates.
main.js loads all of my dependencies via require. It also handles my routes
sdFunctions.js has globals and other important things. But the most important is 2 global default views.
1: The GV that everything goes through initially
2: The DV that all options go through (The main navigation menu)
Outline
I know that for OV to load it must have the element that its loading into available. This is el: 'sexdetails'. This element gets loaded from the DV. So I realise that DV needs to be in the DOM for the OV to load. Otherwise it has no element to load into.
This is a console load. page is from the GV sexdetails gets loaded in from the DV and this is where the OV gets loaded into. all these consoles are out put in loading order. So from the top to the bottom.
The bottom seems to be seen in the last console.log which is greyed out as the element has been built with all the correct information. But for whatever reason it not output onto the page.
JS
Now the exciting parts :p
DV - This is the second defaultView, the one that handles the menu and where the click events are bound.
SD.defaultSexView = function(){
//set up homeview
var sexView = SD.defaultView.extend({
el: 'page',
jstemplate: JST['app/www/js/templates/sex/sexTemplate.ejs'],
events:{
"click sexoptions > *" : 'changeSex'
},
changeSex: function(elem){
var me = elem.currentTarget.localName;
$('.selected').removeClass('selected');// #update selected from bottom navigation
$(me).addClass('selected');
SD.pageLoad(elem); // #Call function that will re-render the page
},
render: function () {
var compiled = this.jstemplate();
this.$el.html(compiled);
}
});
SD.DSV = new sexView();
SD.DSV.render();
return sexView;
}();
Own View - each of the 5 files has one of these
//set up homeview
var wank = SD.defaultSexView.extend({
el: 'sexdetails',
jstemplate: JST['app/www/js/templates/sex.ejs'],
data: {
url: SD.HTTP+'stats/add',
sextype: 'wank',
header: 'SOME LOUD STRING!!!',
image: '/img/path.jpg'
},
render: function () {
c($('sexdetails'));
c(this.$el);
var compiled = this.jstemplate(this.data);
this.$el.html(compiled);
}
});
return wank;
All the code in full working order can be found here: http://m.sexdiaries.co.uk/ - don't be put off by the URL, I assure you its SFW. Everything is done with cartoons and in no crude.
Interestingly if I update the code to use: $('sexdetails').html(compiled); it will indeed display correctly, but none of the events will be bound correctly.
Sorry there is so much to take in, I'll be very very impressed if anybody takes the time out to read or understand this :)
I couldn't look into full code. But from my past experience I can say that you are rendering view on a element which might have been removed from page DOM. When you update this.$el.html() of a view, it will take away element from DOM but still maintain elements if referred in a closure. I can see that you are passing
SD.pageLoad(elem)
to some function which does rendering. Since elem is an object, it's passed by reference. even after you update view's template later with
var compiled = this.jstemplate();
this.$el.html(compiled);
element that is send to rendering function remains in memory, but not in DOM, so any element rendered on this element will not be displayed on page. Also you are using $ inside a view, but you should be using this.$. this will make sure that, code from a view will never change elements outside that view.
After taking a look at your code, I noticed that in your "Wank" view, that you are setting el to sexdetails, which doesn't fly. I wasn't able to dig into the details of why this is, but I assume it has something to do with lack of DOM specificity.
Setting el to "body content page" however, allowed to view to render without any issues, as this allowed to view to hook directly into the body of the page and lets the render function do the rest of the work for you.
Hope this helps!

Showing, Hiding, then re-showing Layouts breaks Events

Having trouble showing, hiding, and then re-showing Marionette Layouts. I believe this problem also applies to regular Backbone Views and Marionette ItemViews as well though.
In summary, I have a parent view. When it is initialized, it creates two child layouts that are meant to be used as tab content. The problem is that when tab content from one tab is shown, then content from another tab is shown instead, when the original tab content is shown again, the events do not work anymore.
The child layouts are created in the initialize function of the parent layout and re-used because their states need to be preserved when navigation moves back to them.
Here is a sample application that demonstrates what I am talking about:
Here is a video showing the broken events: Video Link
Thanks so much!
The problem is that you don't create a new istance of your sub-layouts, and just re-use the one you initiate in your main layout. So when you change the content of your region the events get unbinded as part of the close() function of Marionette's View.
You should change your initialize function like that:
initialize: function(){
_.bindAll(this);
// CREATE SUB LAYOUTS
this.tab1Layout = B.tab1Layout;
this.tab2Layout = B.tab2Layout;
},
And call the layouts in this way:
// EVENT HANDLERS
on_show_tab_1_click: function(event){
this.content.show(new this.tab1Layout());
},
on_show_tab_2_click: function(event){
this.content.show(new this.tab2Layout());
}
If you don't want to re-initialize the tab views on every tab change you could call view.delegateEvents() manually:
// views[] is array of initialized tab views
// Swap from displaying views[0] to views[1]
currentTabIndex = 1
this.myRegion.show(views[currentTabIndex])
views[currentTabIndex].delegateEvents()
For preserving state, another option is to render both tabs and simply hide the inactive tab region:
// Assume both regions have initialised views, tab2Region is hidden,
// tab1Region is shown.
// Swap between tabs:
this.tab1Region.$el.hide()
this.tab2Region.$el.show()

How to store a backbone.js view and then reuse it again

I am creating a web application using backbone.js. I have two tabs. Tab 1 is a backbone.js view and tab2 is another backbone view (of the same kind).
Both the views are loaded to the same div element in the page. Currently what I do is, when user change the tab, I detect it using the backbone router. Then router does a check to see if a view is already there, or should it be created.
I created a object manager object, who store the view object based on a key (key is the tab name)
App.ObjectManager.getContentView = function (tabId) {
return App.ObjectManager.ContentHash[tabId];
};
App.ObjectManager.setContentView = function(tabId, view) {
App.ObjectManager.ContentHash[tabId] = view;
};
App.ObjectManager.hasContentView = function(tabId) {
if(App.ObjectManager.ContentHash[tabId]==undefined) {
return 0;
}
else{
return 1;
}
};
Here is what I do to create and store the view to list object. This code is called every time a tab is clicked, and router detects the change.
$("#myDiv").html("");
if(App.ObjectManager.hasContentView(tabId)==0) { //View is not in the lookup table
console.log("View is not created yet. So we need to create");
content = new App.MyBackboneView(); //create a new view
App.ObjectManager.setContentView(tabId, content); //Store the view
}
else {
console.log("View is already created..So we read it and show it");
content = App.ObjectManager.getContentView(tabId);
}
//Now, we add the view to the main div
$("#myDiv").append(content.el);
The code above works by showing the view to the myDiv. and I can see the values of text boxes and radio buttons, etc being shown.
The only thing - event handling don't work. So, if I have a button in the view that is added an event handler, the event don't get called when the view is rendered from the saved object. First time when the view gets shown, click event is handling.
For eg, I have this button in the view "Say My Name!"
events: {
"click #say": "sayName"
},
sayName: function() {
console.log("I click say Name");
}
First time the view gets loaded, when I press the button, console show me the log. When I come back to the tab again, and this time view is shown from the list object, the click event don't work.
Is my code having any problem ?
Hope you can give me some way how to store the view and I can reuse the view again.
Thanks.
When creating a view you empty myDiv content
$("#myDiv").html("");
I think this action automatically removes all bindings that you set in your View earlier. It's actually quite a common problem with Backbone - this framework sets event listeners to dom elements in the view.
Probably easiest way to deal with this issue is to create a parent View for #myDiv as a whole and set events in there.
View structure may look like that:
TabsView (set events here)
|
- Tab View 1 (render content)
- Tab View 2 (render content)
I'm having the same problem, I have several "tab views" that show aggregated data, something like "my messages", "all messages", "all users" etc. I store views' objects in a hash, say 'views'.
Yes, indeed as Kane Cohen has mentioned:
$('#myDiv').html('') or $('#myDiv').$el.empty()
destroys all bindings... What worked for me is rather using "detach" as it doesn't destroy bindings (http://api.jquery.com/detach/), suppose previous displayed content was 'All messages', and user clicks on 'My messages':
views['All messages'].$el.detach()
$('#myDiv').append views['My messages'].el
I use coffescript...
P.s. Hope this helps, allthough I'm not satisfied with this approach...

Categories

Resources