How do I properly use $el and .remove() in backbone? - backbone.js

I'm still confused by this. The common paradigm I see emerge is...
view.render()
With render set to
function () { this.$el.html( this.template() ) }
How does this behavior mesh with remove? This renders the view.. but then logically you'll call .remove() to unbind the events and ditch it.. If you use that though, you've ditched the parent container ($el), and can't re-render. The docs need to be explicit then that the convention is to not this.$el.html( this.template() ), but to render the template by calling .html() on some non-el parent element. You can either call .render() using the views own $el and also use .remove().
How does these two work together?

The best practice I see is slightly different. Leave the el property off entirely -- let backbone create its own element. The render method should look like this:
render: function() {
this.$el.html(this.template(some_data_from_somewhere));
return this;
}
and then you call render from the router, thusly:
some_container.append(view.render().el)
If you do it this way, then the default remove implementation:
remove: function() {
this.$el.remove();
this.stopListening();
return this;
}
starts to make more sense. The view created and has sole control of its el so it makes perfect sense for the view to destroy the el when it is being destroyed. The view's owner creates the view and adds it to the page; then later, the view's owner destroys the view (by calling remove) when the owner is done with it. This approach makes the view nicely self-contained.
Of course, if your view is expecting to bind to an existing el or have someone give it an el when it is created, then you're going to have to provide a custom remove to unbind DOM events (by calling this.undelegateEvents() presumably), remove the contents of this.el (but not el itself), etc. Similarly, if your view has child views then you'd provide a remove that would call remove on all the children before cleaning itself up.

I don't think I fully understand your questions; but I think if you look at the sections titled:
Decouple Views from other DOM elements
and
Cleaning Up: Avoiding Memory Leaks and Ghost Views
form the following link, then you might just find the answer that you were looking for.
http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/
It was only until recently that I started adding my view to the DOM from outside of the view itself meaning I don't pass in an el: when creating the view. I just have he view render the DOM element from within the view to memory and call render().$el from the calling code to render the view just like the article states.

Related

Availability of UI elements in Marionette.View

I'd just like to understand the decisions behind Backbone.Marionette's view regarding UI elements.
When instantiating a Marionette.View on an existing DOM element, like this:
view = new Marionette.ItemView({
el: "#element",
ui : {
whatever : "#whatever"
}
});
I am able to access view.$el, the jquery selector inside view.initialize, so far so good.
However, when I try to access view.ui.whatever, I only have access to the selector, ie the string "#whatever" instead of the actual $("#whatever") jquery selector.
The reason for this is because Marionette.View.bindUIElements() is only called on render and not before initialize.
I would like to know if you think this behaviour is logic and why?
I am only asking in the case of attaching of the view to an existing el, if the view is created with a template, I do understand why the binding is in render().
Attaching a view to an existing element is the exception. The normal view lifecycle involves calling render, and without doing that there would be nothing for the UI elements to bind to.
Just call this.bindUIElements() in your initialize method when you need to attach a view to an existing element.
When I am working with Marionette, I put the code that has to access the ui elements inside the onShow method. This event is fired after the dom is ready and the elements are ready to be manipulated. Inside this method, your ui.whatever will now be pointing to an element and not a string.
I think you have that problem because you have to access to the jQuery element with
this.ui.whatever
Because "this" is already a view instance.
See: http://marionettejs.com/docs/v2.4.4/marionette.itemview.html#organizing-ui-elements

Test Driving Backbone view events

I am trying to test drive a view event using Jasmine and the problem is probably best explained via code.
The view looks like:
App.testView = Backbone.View.extend({
events: { 'click .overlay': 'myEvent' },
myEvent: function(e) {
console.log('hello world')
}
The test looks something like:
describe('myEvent', function() {
it('should do something', function() {
var view = new App.testView();
view.myEvent();
// assertion will follow
});
});
The problem is that the view.myEvent method is never called (nothing logs to the console). I was trying to avoid triggering from the DOM. Has anyone had similar problems?
(Like I commented in the question, your code looks fine and should work. Your problem is not in the code you posted. If you can expand your code samples and give more info, we can take another look at it. What follows is more general advice on testing Backbone views.)
Calling the event handler function like you do is a legitimate testing strategy, but it has a couple of shortcomings.
It doesn't test that the events are wired up correctly. What you're testing is that the callback does what it's supposed to, but it doesn't test that the action is actually triggered when your user interacts with the page.
If your event handler needs to reference the event argument or the test will not work.
I prefer to test my views all the way from the event:
var view = new View().render();
view.$('.overlay').click();
expect(...).toEqual(...);
Like you said, it's generally not advisable to manipulate DOM in your tests, so this way of testing views requires that view.render does not attach anything to the DOM.
The best way to achieve this is leave the DOM manipulation to the code that's responsible for initializing the view. If you don't set an el property to the view (either in the View.extend definition or in the view constructor), Backbone will create a new, detached DOM node as view.el. This element works just like an attached node - you can manipulate its contents and trigger events on it.
So instead of...
View.extend({el: '#container'});
...or...
new View({el:'#container'});
...you should initialize your views as follows:
var view = new View();
$("#container").html(view.render().el);
Defining your views like this has multiple benefits:
Enables testing views fully without attaching them to DOM.
The views become reusable, you can create multiple instances and render them to different elements.
If your render method does some complicated DOM manipulation, it's faster to perform it on an detached node.
From a responsibility point of view you could argue that a view shouldn't know where it's placed, in the same way a model should not know what collection it should be added to. This enforces better design of view composition.
IMHO, this view rendering pattern is a general best practice, not just a testing-related special case.

Does a backbone view linger on after deletion of the corresponding DOM node?

We have some legacy non-backbone code in our web application. Although we attach views to existing DOM elements there is still some yet-to-be-refactored code that deletes certain DOM elements i.e. the delete call doesn't go through the view but is more like a jQuery call $('#domID').remove();
I have a nagging feeling that the backbone view probably hangs around as a zombie, but I don't have a way to see it? Is this harmful? Should we make it priority to refactor and have all the deletes go via the view and call view.remove() and view.unbind() for proper deletion?
Would the view be garbage collected if the DOM node is deleted independently? I guess if not if it's bound to some event, but what if it isn't?
The view will only linger on if there is a reference to it somewhere. There are four sources of stray references to consider:
Bindings to models and collections, i.e. this.collection.on('reset', this.render) and such.
Bindings to DOM objects through the view's events.
Bindings to DOM objects through direct $(...).on(...) calls.
Plain old variable references such as this.current_view = new V(...).
(1) is normally handled by the view's remove method and you have to call remove yourself, there's nothing in Backbone or jQuery that can do this for you. For example: http://jsfiddle.net/ambiguous/e574Z/
(2) is easy. Backbone views use a single delegate call to bind the view's events to the view's el. So, if you remove the view's el through a simple $(x).remove() then the event reference goes away. However, if you're attaching different views to the same el, you'll need to call undelegateEvents to detach the delegate; this would normally be done in a remove method:
remove: function() {
this.undelegateEvents();
return this;
}
but, again, you have to call remove yourself somewhere.
(3) is rare but sometimes necessary in the case of window scroll events, body click events for dialogs, and things like that. Of course, you have to clean these up yourself as Backbone can't know what you're doing behind its back and the elements that you're binding to would be outside of the view's el (or you'd be in (2)). Where would you clean these up? The remove method of course.
(4) is, as always, up to you. Usually, this sort of thing is handled like this:
if(this.current_view)
this.current_view.remove();
this.current_view = null;
Yes, there's remove again.
So, if all you have are things like (2), then $('#domID').remove(); will be fine and shouldn't leave any zombies; in fact, the default remove implementation is just this.$el.remove() and the documentation says as much:
remove view.remove()
Convenience function for removing the view from the DOM. Equivalent to calling $(view.el).remove();
However, you probably have some things like (1) involved as well so adding/updating all your remove methods and calling view.remove() to remove views would be a good idea.

how to handle multiple views within a single view with backbonejs

I'm new to Backbone and I don't fully understand it yet, and I've come across a situation I can't find any documentation on. What I if I have a view that contains multiple views? For example, I have a view called StackView. The purpose of this view is to neatly lay out a set of cards. It manages the animation of adding, removing, and adjusting cards in the stack. Each card is a CardView. How would I handle this? I've seen people talk about views within views by simple creating a variable in the view and assigning the View instance to that variable. Should I just be adding an array of CardViews in a variable of a StackView?
That's what I do, and it works well. Here's a snippet of a View I use in an application. I've re-written it back into regular javascript from my coffeescript, so I apologize for any typos:
render: function() {
var _this = this;
this.$el.html(this.template());
this.listItemViews = [];
// for each model in the collection, create a new sub-view and add
// it to the parent view
this.collection.each(function(model){
var view = new App.Views.Projects.ListItem({model:model}); // create the new sub-view
_this.listItemViews.push(view); // add it to the array
_this.$('#project-table tbody').append(view.render().$el); // append its rendered element to the parent view's DOM
});
return this;
}
This lets my Table view maintain a reference to all the listItemView views.
Of course, if you do this, you should make sure to properly remove these child views and unbind any events when you remove the parent view.

Backbonejs: How to unbind collection bindings when a view is removed?

I've got a backbonejs application with two view. It kind of looks like this:
<body>
<header></header>
<div id="content"></div>
</body>
Every time a view is loaded the app overwrites the current view by completely overwriting the contents of #content.
// Like this...
$('#content').html( new primaryView().render() );
// ...and this.
$('#content').html( new secondaryView().render() );
The application has a global collection.
App.Collection();
The secondary view modifies itself depending on the global collection. Therefor it binds a function to the 'add' event' on App.Collection in the views initialize function;
App.Collection.bind('add', function(){
console.log('Item added');
});
Which result in my problem. Every time the secondary view is loaded a new function is binded to App.Collection's add event. If I go from the primary view to the secondary view three times, the function will fire three times everytime an item is added to App.Collection.
What am I doing wrong?
I can see how I would do it if there was an uninitialize function on views.
I can see how I could do it if I never removed a view once it was loaded.
I can see how I would do it if I could namespace events like in Jquery. (by unbinding before binding).
You can generalize your problem quite a bit. Basically, you are writing an event-driven app, and in such app events should be taken care of.
Check out this post to see a recommended way to work with event handlers in backbone.
Depending on the situation, you can use initialize and render methods to handle different aspects of creating a view. For instance, you can put your binding inside the initialize
initialize: function() {
App.Collection.bind('add', function(){
this.view.render()
});
}
which only fires when the view is created. This binds your render method to the add event. Then in your render method you can actually create the html.
This prevents the binding from happening every time you need to re-render.

Resources