Backbone.js EL and Template in the View - javascript

So i'm very new to backbone.js and not so good at JavaScript in general, so I was wondering if someone could explain to me why
I cannot define my EL property, and Template property in my view, and then use this.template in my render. Instead I have to define the template and el in my render function.
var ProductView = Backbone.View.extend({
el: $('#product-list'),
initialize: function() {
this.el.html('<span style="color:white">loading...</span>');
}, // end initialize
render: function(collection) {
// // assign the template
this.template = $('#product_template');
// Where the template will be placed
this.el = $('#product-list');
// Add the collection to the main object
this.collection = collection;
// add tthe data to the html variable
var html = this.template.tmpl(this.collection.toJSON());
// place the html in the element.
this.el.html(html);
// not even sure what the hell this is.
return this;
} // end render
});

The problem isn't in the way you're defining el or template, it's in how you're setting the call back. In Workspace, your router, you're setting the callback for your collection refresh event like this:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
The problem is, you're setting a method as a callback, but you're not providing a context object as the third argument to bind - so the method is called, but this in the method refers to the global object, not the view. So this.el is undefined, because it's not looking at the view instance at all. Try:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render, this.view);
and see how that goes.
(I made a jsFiddle to demonstrate that the el and template were set properly under normal circumstances, though it doesn't actually include the fix above, which is hard to mock up without the server-side data: http://jsfiddle.net/nrabinowitz/QjgS9/)

You can't do this:
var ProductView = Backbone.View.extend({
el: $('#product-list'),
// ...
and get anything useful in el as #product-list probably isn't even present in the DOM when your ProductView is built; so trying to use $('#product-list') for el is simply the classic "I forgot to use $(document).ready()" problem dressed up in Backbone. Using $('#product-list') for el should work if #product-list is around when you define your ProductView though.
You can do this though:
var ProductView = Backbone.View.extend({
el: '#product-list',
// ...
and then say $(this.el) when you need to do things inside your view methods. Not only is $(this.el) the usual way of using el but it also works and that's sort of important.
The same issues apply to #product_template.
Looking at your code I see this:
// INstantiate the view
this.view = new ProductView();
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
Presumably the render is being triggered by the reset event. But, and this is a big but, the render method isn't bound to the right this anywhere so this won't be the ProductView when render is called and this won't have anything that you expected it to; hence your bizarre "undefined" error.
You could use _.bindAll in your initialize:
initialize: function() {
_.bindAll(this, 'render');
// ...
but usually you'd want to give the view a collection when you create it and the view would bind itself to the events so your structure will still be a bit odd.
You can also supply a context (AKA this) when you call bind:
collection.bind('reset', this.render, this);

Related

How to avoid a memory leak when instantiating child views in Backbone.js

myView = Backbone.View.extend({
//event binding etc etc
render: function() {
//render some DOM
}
})
anotherView = Backbone.View.extend({
events: {
'click .selector doThis'
},
createNewView: function() {
var view = new myView();
}
})
createNewView may be called multiple times. My understanding is that the variable view will not necessarily be removed by JavaScript's built-in garbage collection because it references objects/code which still exists when the createNewView function completes.
Is this correct? How to deal with this?
My current approach is to initialise myView once at the level of my app:
myApp.view = new myView()
Then in createNewView I just call render on this:
myApp.view.render()
Essentially, I only ever have one of them and I re-use it.
An alternative approach is to track the creation of sub views in an array and then I call .remove() on each one in turn when I know they are no longer needed.
Am I on the right track?
It occurs to me that the second approach is better because if myView created bound callbacks on other objects with listenTo, these would not be removed simply by re-assigning the variable. That is, if I am calling new to instantiate a new instance of the view, I should call remove() on the being discarded instance first... It seems.
In your example, you don't put the view's el into the DOM, so nothing is referencing the view, then it will be collected by the garbage collection.
One good thing to ensure a view isn't bound to something anymore is to call .remove() on it. It will remove:
the view's el from the DOM,
the jQuery DOM events
the Backbone event listeners.
The Backbone .remove source:
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
remove: function() {
this._removeElement();
this.stopListening();
return this;
},
// Remove this view's element from the document and all event listeners
// attached to it. Exposed for subclasses using an alternative DOM
// manipulation API.
_removeElement: function() {
this.$el.remove();
},
As mentioned by mu is too short in the comments (and myself in almost every other answers), you should always favor listenTo over on or bind to avoid memory leaks and ease unbinding events.
When rendering child views, nested inside a parent view, a good practice is to keep an array of the child views to later call .remove() on each of them.
A simple list view might look like this:
var ListView = Backbone.View.extend({
initialize: function() {
// Make an array available to keep all the child views
this.childViews = [];
},
addOne: function(model) {
var view = new Backbone.View({ model: model });
// as you create new views, keep a reference into the array.
this.childViews.push(view);
this.$el.append(view.render().el);
},
renderList: function() {
// replace the view content completely with the template
this.$el.html(this.templates());
// then cleanup
this.cleanup();
// then render child views
this.collection.each(this.addOne, this);
return this;
},
cleanup: function() {
// quick way to call remove on all views of an array
_.invoke(this.childViews, 'remove');
// empty the array
this.childViews = [];
},
});
Though if other objects are listening to it, it won't be collected and may be a leak. It's important to keep track of the references and delete them all when you don't need it anymore.

Why is .$el needed, and what does it point to?

This is probably more of a javascript question, but I'm going through this backbone tutorial and was wondering why is there a .$el at the end of this line, and which element is it point to?
self.$el.append((new BlogView({ model: blog})).render().$el);
Here is the full code below.....
var BlogsView = Backbone.View.extend({
model: blogs,
el: $('.blogs-list'),
initialize: function() {
this.model.on('add', this.render, this);
},
render: function() {
var self = this;
this.$el.html('');
_.each(this.model.toArray(), function(blog) {
self.$el.append((new BlogView({ model: blog})).render().$el);
});
return this;
}
});
$el is a reference for the DOM element for the view. Backbone views are not DOM elements themselves, they are generic javascript objects which have a property called $el which holds the DOM element which is what you actually see on the webpage. You can think of a backbone view as a controller of sorts for the DOM element, and when you add event listeners to your view, define render, etc, you are always acting on its DOM element stored in $el ($el is the same DOM element as el, the former just plays nicely with jQuery). In this case, your view is BlogView and if we break new BlogView({ model: blog})).render().$el up:
new BlogView - creating an instance of your view, backbone will automatically create a DOM element for your view and store it in yourView.$el
.render() - telling the view to render its HTML inside the $el element. In Backbone, our render function is where we generate HTML markup/format data and "draw" the view by shoving this markup into our view's $el.
render().$el - render() returns this which is just our view itself, so calling render().$el is like saying "render my view and return my DOM element.
self.$el.append(..) - this block of code is thus given our DOM element $el which then inserts it.
so putting it all together we get: new BlogView({ model: blog})).render().$el which first creates our view, renders our view and returns our view's DOM element which can be appended to the page, manipulated, etc.

Backbone model objects.... where are they stored so that the DOM can interact with them?

I just realized that I have no idea what the heck I am doing when it comes to backbone. I came to this realization when trying to figure out a cogent strategy for removing the view's event listeners on the model. Then I asked "well, where is the model anyways now that the view has been rendered to the DOM?" and then I asked "how is this model object that I created inside a function body, and is therefore out of scope now that I have rendered the view to the DOM, maintaining state?"
AHHHHHHHH!!!!!!!!!
Ex.
View Constructor
Timeclock.Views.JobNewView = Backbone.View.extend({
template: JST['jobs/_form'],
events:{
'blur #job_form :input':'assignValue'
},
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
this.listenTo(this.model, 'updated-location', this.updatedLocation);
this.listenTo(this.model, 'sync', this.renderJobView);
this.listenTo(this.model, 'invalid', this.displayModelErrors);
this.listenTo($(window), 'hashchange', this.clearListeners);
},
render: function(){
this.$el.html(this.template({attributes: this.model.attributes}));
this.$el.find('#address_fields').listenForAutoFill();
return this;
},
assignValue: function(e){
var $field = $(e.currentTarget)
var attr_name = $field.attr('name');
var value = $field.val();
this.model.set(attr_name, value);
}...
});
Function rendering view to the DOM
renderCollaboratingView: function(e){
var job = this.model;
var $row = $(e.currentTarget);
job.set({customer_id: $row.data('id')});
var model_view = new this.ViewConstructor({model: job});
$container.html(model_view.render().el);
}
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
I understand that backbone views are just a wrapper to declaratively write DOM listeners but how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
I can think of two ways:
1) The model object is bound to the DOM through a jquery object. All the event listeners that I declare in my view all know where the underlying model object lives on the jquery object(the 'model' attribute?).
2) Backbone is creating some object namespace that the view knows about where it stores models and collections that back the DOM. I have a feeling it's #1 but who knows.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place. If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM? Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
Any help that can be given would be greatly apprecaited. Having an existential crisis.
Thanks
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
Backbone Models and Views are simply Javascript objects that live in-memory in the scope of the page (like any other Javascript). If you were to do ...
var name = 'Peter';
var person = new Backbone.Model({ name: 'Peter' });
var view = new Backbone.View({ model: person } );
... then name, person, and view are all just objects in memory. They have no relation to jQuery; they have no relation to the DOM. The View happens to be able to create DOM elements if you implement render(), but even then those elements don't ever have to ever be attached to the page's live DOM at all.
... how are DOM events acting on the underlying model object in the example above? As soon as the renderCollaboratingView() function has exited how is the model that I passed to the view still being interacted with?
Based on the code you've shown, the model isn't being interacted with directly. Your events hash ...
events:{
'blur #job_form :input':'assignValue'
},
... does say that any time a blur event happens in the job_form element, it will call a method on the view called assignValue. That method may interact with the model (it probably does, right?), but DOM events don't directly cause interaction with the model at all.
If backbone views are really just jquery objects then aren't jquery listeners removed from DOM elements when the element backing the jquery object is removed from the DOM?
Backbone's listeners are wholly different than jQuery listeners. They listen for Backbone-centric events. See here for the list of built-in events that Backbone components fire. A View's events hash is a nice convention that is used to listen for DOM events; it's basically a nice wrapper around the jQuery concept of event delegation.
Do I only need to remove the listeners if I am going to not destroy the view entirely and save it for later use?
If you don't remove listeners, they will continue to run whenever the related event happens, regardless of whether the listening component is changing the page. Suppose you had a Backbone.View that did something like this:
var MyView = Backbone.View.extend({
// ...
events: {
// Don't do this!
'click': '_onClick'
},
// ...
_onClick: function() {
this.$el.append('Clicked!');
}
});
Any time any click DOM event happens on the page, this view will append the text Clicked! to its internal DOM element. When the view is attached to the page's DOM, Clicked! would appear on every click. When the view was removed from the DOM, the function would still run on every click... but since the View's internal root element wasn't attached to anything the function would have no effect.
It's a type of a memory leak, as any instance of MyView will ever be cleared up by the garbage collector. But the particularly nefarious side effect is it also uses CPU time to do something that is completely worthless. Now imagine if the event listener did anything of consequence. Performance of the page will suffer.
JavaScript has garbage collection. Objects do not get destroyed then they go out of scope. An Object X get garbage collected by the runtime system, when it sees that nobody is having a reference (or is pointing) to X.
A Backbone View is also an object. An object can store reference to another object.
In your renderCollaboratingView, you wrote :
var model_view = new this.ViewConstructor({model: job});
this model_view is your view's object. You passed your job which is your model you got from :
renderCollaboratingView: function(e){
var job = this.model;
....
}
You can look at this line in backbone annotated code : BackBone View Options. (I would suggest to look at the link after you have read the answer)
The line is :
var viewOptions = ['model', 'collection', 'el', 'id',
'attributes', 'className', 'tagName', 'events'];
and then Backbone View is defined as : BackBone View
It is :
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
options || (options = {});
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};
Look at line :
_.extend(this, _.pick(options, viewOptions));
and your code :
var model_view = new this.ViewConstructor({model: job});
So how is the model that I am passing to the view object persisted so that the DOM interactions can set attribute values on the underlying model object?
If you merge the dots : You are passing a model to your view. You can also pass other like 'collection', 'el', 'id', ... in viewOptions.
They get pick from your passed object {model: job} and extended in the view object.
This is how your view object has reference to the model that it was given.
Once again, I got here because I was trying to understand why I need to remove the listeners on the model that I passed into view in the first place.
As i said, just removing a view-object from DOM is not going to destroy it. You would have to remove all references of view-object that other objects (here model) have.
When you said :
initialize: function(options){
this.listenTo(this.model, 'failed-request', this.failedLocationRequest);
....
in your view. You told the model to call your view-object's failedLocationRequest on model's event failed-request. This is possible only when your model's object would store reference to view's object. So, your view is not destroyed.
view-object(s) not in dom would continue receiving such events from models and all other places where they registered (except dom) and would do things in the background, that you just never wanted. Definitely not what you wanted..
simple advice, call remove on your view. BackBone View remove
and read stopListening

The proper way of binding Backbone.js async fetch operation results to a view

I am wondering if there are any pointers on the best way of "fetching" and then binding a collection of data to a view within Backbone.js.
I'm populating my collection with the async fetch operation and on success binding the results to a template to display on the page. As the async fetch operation executes off the main thread, I one loses reference to the backbone view object (SectionsView in this case). As this is the case I cannot reference the $el to append results. I am forced to create another DOM reference on the page to inject results. This works but I'm not happy with the fact that
I've lost reference to my view when async fetch is executed, is there a cleaner way of implementing this ? I feel that I'm missing something...Any pointers would be appreciated.
SectionItem = Backbone.Model.extend({ Title: '' });
SectionList = Backbone.Collection.extend({
model: SectionItem,
url: 'http://xxx/api/Sections',
parse: function (response) {
_(response).each(function (dataItem) {
var section = new SectionItem();
section.set('Title', dataItem);
this.push(section);
}, this);
return this.models;
}
});
//Views---
var SectionsView = Backbone.View.extend(
{
tagName : 'ul',
initialize: function () {
_.bindAll(this, 'fetchSuccess');
},
template: _.template($('#sections-template').html()),
render: function () {
var sections = new SectionList();
sections.fetch({ success: function () { this.SectionsView.prototype.fetchSuccess(sections); } }); //<----NOT SURE IF THIS IS THE BEST WAY OF CALLING fetchSuccess?
return this;
},
fetchSuccess: function (sections) {
console.log('sections ' + JSON.stringify(sections));
var data = this.template({
sections: sections.toJSON()
});
console.log(this.$el); //<-- this returns undefined ???
$('#section-links').append(data); //<--reference independent DOM div element to append results
}
}
);
darthal, why did you re-implement parse? It seems to do exactly what Backbone does by default (receive an array of models from the AJAX call and create the models + add them to the collection).
Now on to your question... you are supposed to use the reset event of the Collection to do the rendering. You also have an add and remove when single instances are added or deleted, but a fetch will reset the collection (remove all then add all) and will only trigger one event, reset, not many delete/add.
So in your initialize:
this.collection.on("reset", this.fetchSuccess, this);
If you are wondering where the this.collection is coming from, it's a param you need to give to your view when you create it, you can pass either a model or a collection or both and they will automatically be added to the object (the view)'s context. The value of this param should be an instance of SectionList.
You'll also have to update fetchSuccess to rely on this.collection instead of some parameters. Backbone collections provide the .each method if you need to iterate over all the models to do stuff like appending HTML to the DOM.
In most cases you don't need a fetchSuccess, you should just use your render: when the collection is ready (on 'reset'), render the DOM based on the collection.
So to summarize the most general pattern:
The collection should be independent from the view: you give the collection as a param to the view creation, the collection shouldn't be created from a specific view.
You bind the View to the collection's reset event (+add, remove if you need) to run a render()
this.collection.on("reset", this.render, this);
You do a fetch on the collection, anytime (probably when you init your app).
A typical code to start the app would look something like this:
var sections = new SectionList();
var sectionsView = new SectionsView({collection: sections});
sections.fetch();
Because you bound the reset event in the view's initialize, you don't need to worry about anything, the view's render() will run after the fetch.

remove from collection bind to remove from view

I have a backbone collection and when I remove a model from the collection, I want it to remove the item from a list in the view.
My collection is pretty basic
MyApp.Collections.CurrentEvents = Backbone.Collection.extend({
model: MyApp.Models.Event
});
and in my views I have
MyApp.Views.CurrentEventItem = Backbone.View.extend({
el: 'div.current_event',
initialize: function(){
event = this.model;
_.bindAll(this, "remove");
MyApp.CurrentEvents.bind('remove',this.remove); //the collection holding events
this.render();
},
// yeah, yeah, render stuff here
remove: function(){
console.log(this);
$(this.el).unbind();
$(this.el).remove();
}
});
when I remove the model from the collection, it triggers the remove function, but the view is still on the page.
In the console, I can see the model, but I believe the model should have an 'el', but it doesn't.
My container code is
MyApp.Views.CurrentEventsHolder = Backbone.View.extend({
el: 'div#currentHolder',
initialize: function(){
MyApp.CurrentEvents = new MyApp.Collections.CurrentEvents();
MyApp.CurrentEvents.bind('new', this.add);
},
add: function(){
var add_event = new MyApp.Views.CurrentEventItem(added_event);
$('div#currentHolder').append(add_event.el);
}
});
for some reason in the add method I can't seem to use the $(this.el) before the append, though I'm not sure if that is the problem.
PROBLEM: MyApp.CurrentEvents.bind('remove',this.remove);
This triggers the remove() function every time any model is deleted from the collection.
This means that anytime a model is deleted, all the CurrentEventItem view instances will be deleted.
Now, about the view still being on the page:
It must have something to do with the way you appended/added/html-ed the view in the page. Check your other views, maybe if you have a CurrentEventsContainer view of some sort, check your code from there because with your current code, it does delete the view, albeit, all of them though.
RECOMMENDED FIX:
change your bindings to:
this.model.bind('remove',this.remove);
and make sure that when you instantiate it, pass on the model so that each view will have a corresponding model to it like so:
//...
addAllItem: function(){
_.each(this.collection, this.addOneItem())
},
addOneItem: function(model){
var currentEventItem = new MyApp.Views.CurrentEventItem({model:model});
//...more code..
}
//...
This makes things a lot easier to manage in my opinion.
UPDATE (from your updated question)
The reason you aren't able to use this.el is because you haven't passed the right context into the add() function. You need to add _.bindAll(this,'add') in your initialize() function to pass the right context, therefore making your this correct, allowing you to use this.el within the add function. Also, change your code to this:
MyApp.CurrentEvents.bind('new', this.add, this); this passes the right context. Also, why not use add instead as an event?
Continuing what I said in the comments, the way you've implemented this right now will remove all the CurrentEventItem views from the page when any of them is removed from the collection. The reason for this is the following:
MyApp.CurrentEvents.bind('remove',this.remove);
What this essentially says is, "every time the remove event is triggered on the collection, call this.remove." So, every time you instantiate one of these views, you're also telling the collection to remove that view when the collection triggers a remove event. I've created a fiddle to show you the problem.
You're right that Backbone knows which model has been removed from a collection, but you're not taking advantage of that. You can do that like so:
removeFromView: function(model) {
// Check to make sure the model removed was this.model
if (model.cid === this.model.cid) {
$(this.el).unbind();
$(this.el).remove();
}
}
See how this minor adjustment changes the behavior? Check it out in action here.
If you follow this pattern, you should see the proper views being removed.

Categories

Resources