I have a sample single-page-application in Backbone that I am messing around with. The idea is that I have a button that will trigger a refresh on a view. I am trying to get the event handler to remember 'this' as the backbone view, not the element that it was called on. No matter how much docs I read, I cant seem to make it past this mental hump.
in my view, I have
initialize: function() {'
//i am trying to say, call render on this view when the button is clicked. I have tried all of these calls below.
//this.$("#add-tweet-button").click(this.render);
//this.$("#add-button").bind("click", this.render);
}
When the render function is called, the 'this' element is the button. I know what im missing is pretty easy, can someone help me out with it? Also, is this sound as coding conventions go?
If you use the View's 'delegateEvents' functionality, the scoping is taken care of for you:
var yourView = Backbone.View.extend({
events: {
"click #add-tweet-button" : "render"
},
render: function() {
// your render function
return this;
}
});
This only works with elements that are 'under' the View's El. But, your example shows this.$(...), so I'm assuming this is the case.
#Edward M Smith is right, although if you need to handle a jquery event of element outside the scope of your View you might write it that way :
var yourView = Backbone.View.extend({
initialize: function() {
var self = this;
$("button").click(function () {
self.render.apply(self, arguments);
});
},
render: function(event) {
// this is your View here...
}
});
Related
I have a number of different "control elements" on my application: dropdowns, tabs, menus, etc. On same pages, there are many of the same control. When writing JavaScript to handle the different events associated with each of these controls, I'm trying to make my code as DRY as possible. One of the challenges is modularizing my JQuery code so that events that occur within a specific control only effect that control.
Take this initial code for example, all it does is open a dropdown menu when it is clicked. I'm used to writing just a ton of different anonymous functions triggered by different events so this type of JQuery is really new to me.
var dropdown = {
init: function() {
$(".dropdown").click(".dropdown", dropdown.openDropdown);
},
openDropdown: function() {
$(this).children(".dropdown-menu").show();
$(this).addClass("open");
}
}
$(document).ready(dropdown.init);
My question is, within this dropdown variable, I want to be able to save/track different pieces of the dropdown control currently being acted upon. For example, I might want to write:
var menu = $(this).children(".dropdown-menu");
somewhere in this chunk so that I could refer back to this menu while calling different functions. I just cannot figure out syntactically how to do this. Any help/guidance is welcomed! Thanks.
Something I like about coffeescript is how it allows you to easily create classes. Classes in coffee are just a simplified way of generating "modules" using javascript's prototypal inheritance. More on that here: http://coffeescript.org/#classes
But how YOU could implement more modular jQuery code is by doing something like this:
http://jsfiddle.net/x858q/2/
var DropDown = (function(){
// constructor
function DropDown(el){
this.el = $(el);
this.link = this.el.find("a");
this.menu = this.el.find(".dropdown-menu");
this.bindClick();
}
// method binding click event listener
DropDown.prototype.bindClick = function(){
var _this = this;
this.link.click(function(e){
_this.openDropDown();
e.preventDefault();
});
};
// click event handler
DropDown.prototype.openDropDown = function(){
this.menu.show();
this.link.addClass("open");
};
return DropDown;
})();
$(function(){
// init each .dropdown element as a new DropDown
$(".dropdown").each(function(){
new DropDown(this);
});
});
You've touched on a pattern I've been leaning towards more and more. Basically, create a JavaScript object that acts as a controller given a root element on the page. Since this "dropdown" is pretty generic, it could probably have access to the whole page and be perfectly happy. I would also recommend making these "modules" instantiable objects, as this allows you to write unit tests easier:
function DropdownModule() {
this.handleClick = this.handleClick.bind(this);
}
DropdownModule.prototype = {
element: null,
$element: null
constructor: DropdownModule,
init: function(element) {
this.setElement(element);
this.$element.on("click", ".dropdown", this.handleClick);
},
handleClick: function(event) {
var $dropdown = $(event.currentTarget);
$dropdown.children(".dropdown-menu").show();
$dropdown.addClass("open");
this.someOtherFunction($dropdown);
},
someOtherFunction($dropdown) {
// do something with $dropdown
},
setElement: function(element) {
this.element = element;
this.$element = $(element);
}
}
Then to use it, just throw this anywhere after the definition for Dropdown:
var dropdown = new Dropdown()
.init(document.documentElement);
The document.documentElement property refers to the <html> tag and is available the moment JavaScript begins executing.
As a side note, I've built a whole framework around this approach: Foundry. Other frameworks, like Angular, take a similar approach as well.
What you want sounds like exactly what jQuery UI has already implemented in their Widget Factory.
I'd highly recommend you check it out since what you'd end up with it something like
$.widget( 'dropdown', {
_create: function() {
this.element.addClass( 'dropdown' );
this._on({
'click': '_clicked'
});
},
_clicked: function( event ) {
// `this` is an instance of dropdown here, not the element
this.clicked = !this.clicked;
this.element.toggleClass( 'clicked', this.clicked );
},
_destroy: function() {
this.element.removeClass( 'dropdown' );
}
});
Then you would use it like any other jQuery UI Widget
$( '#some-element' ).dropdown();
I'm struggling with getting the concept of memory management with single page applications. This is my code:
var FilterModel = Backbone.Model.extend({});
var taskView = Backbone.View.extend({
template: _.template('<h1><%= title %></h1>'),
initialize: function(){
this.render();
this.listenTo(this.model, 'destroy', this.remove);
console.log(this.model)
},
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
},
events:{
'click h1': 'removeView'
},
removeView: function(){
this.model.destroy();
console.log('removed');
}
});
var filterModel = new FilterModel({title: 'Test'});
var taskview = new taskView({model:filterModel});
// I make heap snapshot before and after the change!
setTimeout(function(){
$("h1").click()}, 3000
)
$('body').append(taskview.$el);
I was told by numerous articles that using "remove" and "destroy" would clean up any memory leaks when removing the DOM tree.
But Chrome profile utility tells otherwise. I get detached DOM elements no matter what I do.
UPDATE!!!
After trying a few things in the responses I still get this in Google Chrome:
Here is jsfiddle: http://jsfiddle.net/HUVHX/
taskview is still holding a strong reference to this.el, although it is not connected to the dom. This is not a memory leak because taskview is held strongly also by it's variable
To test my assumption just add:
removeView: function(){
this.model.destroy();
this.el = undefined;
this.$el = undefined;
}
Another approach is to undef taskview var
EDIT:
When I change: "click h1" : "removeView" To "click": "removeView" it solves the detached dom node leak.
I suspect this has something to do with jquery selector caching.
You can see in backbone code, the difference is in calling jquery on function with a selector:
if (selector === '') {
this.$el.on(eventName, method);
} else {
this.$el.on(eventName, selector, method);
}
I tried to trace the cache deep into jquery code, with no luck.
So Janck, you can fin your answer here:
Backbone remove view and DOM nodes
The problems is that you have to do more than just remove you model and view.
You need to properly destroy all of the events and other bindings that are hanging around when you try to close your views.
I don't know if you know about Marionette.js (Backbone.Marionette), but it's a great extension to Backbone to handle this Zombie Views and to create robust JS applications.
You can read some articles about this as well, they were pointed in the Stackoverflow link that I posted.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
http://lostechies.com/derickbailey/2012/03/19/backbone-js-and-javascript-garbage-collection/
But the logic is this: If a View is listening a model, then the contrary also occurs, so you'll always get a instance of your View in your DOM.
I have been playing with backbone and trying to learn it. I'm stuck at this point for a while. Not able to figure out what's wrong with following code?
render: function() {
this.$el.empty();
// render each subview, appending to our root element
_.each(this._views, function(sub_view) {
this.$el.append(sub_view.render().el); // Error on this line
});
You have got context issue. this you are refering to doesn't contain the $el you are looking for. You can fix this by declaring a self variable that points to appropriate this. Following code should work for you.
render: function() {
var self = this; //Added this line to declare variable (self) that point to 'this'
this.$el.empty();
_.each(this._views, function(sub_view) {
self.$el.append(sub_view.render().el); //Used 'self' here instead 'this'
});
Side Note: As you are leaning backbone also you should know about a very commong JavaScript problem with document reflow. You are render a view for every single model in the collection. It can lead to performance issues and especially on old computers and mobile devices. You can optimise your code by rendering everything in container and appending it once, rather than updating DOM each time. Here is an example:
render: function() {
this.$el.empty();
var container = document.createDocumentFragment();
_.each(this._views, function(sub_view) {
container.appendChild(sub_view.render().el)
});
this.$el.append(container);
}
I'm doing this inside one of my Views:
render: function($options) {
...
this.collection.on('reset', _(function() {
this.render($options);
}).bind(this));
....
}
The problem is, whenever reset as well as the re-rendering has been triggered, a new reset binding will be created, resulting 2x, 4x, 8x, etc. times of re-rendering as it goes on.
It's a bit tricky to move the binding into the initialize section (which should solve this issue), however since it's not an option, is there any other solution available, like having Backbone checking if this event has been bound before, or something?
Moving your binding to initialize would be best but assuming that you have good reasons not to, you could just set a flag:
initialize: function() {
var _this = this;
this._finish_initializing = _.once(function($options) {
_this.collection.on('reset', function() {
_this.render($options);
});
});
//...
},
render: function($options) {
this._finish_initializing($options);
//...
}
There are lots of different ways to implement the flag, _.once just nicely hides the flag checking. You could also trigger an event in render have a listener that unbinds itself:
initialize: function() {
var finish_initializing = function($options) {
/* your binding goes here ... */
this.off('render', finish_initializing);
};
this.on('render', finish_initializing, this);
},
render: function($options) {
this.trigger('render', $options);
//...
}
That's the same logic really, just dressed up in different clothes. You could also use an explicit flag and an if in render or assign a function to this._finish in initialize and that function would delete this._finish.
like having Backbone checking if this event has been bound before, or something?
Sure..
!!this.collection._events["render"]
Backbone doesn't expose most of the API required to make it useful. That's alright, use it anyway.
First, define your event handler function as a named function
var self = this;
var onReset = function() {
self.render($options);
}
Then, defensively unbind the function each time render is called
this.collection.off('reset', onReset);
this.collection.on('reset', onReset);
I recently accomplished this using a javascript variable.
Outside of any functions, I declared:
var boundalready =0
Then, inside the function:
if (boundalready == 0){
boundalready = 1;
bind(this);
};
This worked for me pretty well.
I would like to update part of my view when the user types into a input field. Initially I bound to the keyup event listener within the View's events field, and that worked well:
window.AppView = Backbone.View.extend({
el: $("#myapp"),
events: {
"keyup #myInput": "updateSpan",
}, ...
updateSpan: function() {
this.span.text(this.input.val());
}, ...
});
But then I realised that keyup updated too often and made the app slow. So I decided to use the typeWatch plugin so the event would only fire the user stopped typing. But now I don't know how to set the custom event listener in Backbone. Currently I have this:
window.AppView = Backbone.View.extend({
initialize: {
var options = {
callback: function(){
alert('event fired');
this.updateSpan;
},
wait:750
}
this.input.typeWatch(options);
}, ...
updateSpan: function() {
this.span.text(this.input.val());
}, ...
});
Two questions:
I see the alert, but updateSpan is not being fired. I think I'm using this incorrectly in the callback, but how should I do it?
Is initialize now the right place to set the typeWatch event listener, or can I continue to use the events field as I did before?
You aren't actually calling updateSpan, and you're right that this wont be the correct thing. Easiest way to solve it is to just capture the view into another variable first:
var v = this;
var options = {
callback: function() {
alert('event fired');
v.updateSpan();
},
wait: 750
};
this.input.typeWatch(options);
As for your second question, usually I will attach functionality like this in initialize if it's on the base element and in render if it's not, so I think in this case you've probably got it right.