Since I'm not totally sure on which level my issue actually is to be solved best, I'd like to summarise the path I went and the things I tried first:
It's more or less about $el (I think).
As most basic backbone examples state, I started with having the $el defined within its view, like
Invoice.InvoiceView = Backbone.View.extend({
el: $('#container'),
template: ..,
..
});
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
Following this article's advice, I switched to passing $el over to the view while calling the render()-method. Example:
$('#container').html( new WineListView({model: app.wineList}).render().el );
So far so good - but now render() gets called, while it maybe shouldn't (yet).
For example the View asynchronously fetches a model in its initialize()-routine. Adding a binding to reset or sync (e.g. like this.model.bind('sync', this.render, this)) makes sure, render() gets definitely called once the model is fetched, however above stated way, render() still might get called while the model isn't fetched yet.
Not nice, but working(TM), I solved that by checking for the model's existence of its primary key:
render: function() {
if(this.model.get('id')) {
...
}
However, what I didn't expect - and if it really isn't documented (at least I didn't find anything about it) I think it really should be - the fetch operation doesn't seem to be atomic. While the primary key ('id') might be already part of the model, the rest might not, yet. So there's no guarantee the model is fetched completely that way. But that whole checking seemed wrong anyway, so I did some research and got pointed to the deferred.done-callback which sounded exactly what I was looking for, so my code morphed into this:
render: render() {
var self = this;
this.model.deferred.done(function() {
self.model.get('..')
};
return this;
}
..
$('#container').html( new WineListView({model: app.wineList}).render().el);
It works! Nice, hu? Ehrm.. not really. It might be nice from the runtime-flow's point of view, but that code is quite cumbersome (to put it mildly..). But I'd even bite that bullet, if there wouldn't be that little, tiny detail, that this code sets (=replaces) the view instantly, but populates it later (due to the deferred).
Imagine you have two (full-page) views, a show and an edit one - and you'd like to instantly switch between the two (e.g. after hitting save in the edit-view it morphs into the show-view. But using above code it sets (=resets) the view immediately and then renders its content, once the deferred fires (as in, once fetching the model is completed).
This could be a short flickering or a long blank transition page. Either way, not cool.
So, I guess my question is: How to implement views, which don't know about their container, involve models which need to be fetched, views which should be rendered on demand (but only once the model is fetched completely), having no need to accept UI/UX trade-offs and - the cherry on the cake - having maintainable code in the end.
First of all, the first method you found is terrible (hard coding selector in view's constructor)
The second: new WineListView({model: app.wineList}).render().el is very common and ok. This requires you to return the reference to view from render method, and everyone seems to follow this, which is unnecessary.
The best method (imo) is to simply attach the views element to the container, like this
$('#container').html(new WineListView({model: app.wineList}).el);
The WineListView doesn't need to know about where it's going to be used, and whatever is initializing WineListView doesn't need to worry about when to render the WineListView view instance:
because the el is a live reference to an HTML Element, the view instance can modify it anytime it wants to, and the changes will reflect wherever it is attached in DOM/ when it gets attached in DOM.
For example,
WineListView = Backbone.View.extend({
initialize: function(){
this.render(); // maybe call it here
this.model.fetch({
success: _.bind(this,function(){
this.render(); // or maybe here
})
});
}
});
Regarding flickering: this hardly has to do anything with rendering or backbone, it's just that you're replacing one element with another and there will be an emptiness for a tiny bit of time even if your new view renders instantly. You should handle this using general techniques like transitions, loaders etc, or avoid having to switch elements (For example convert labels into inputs in the same view, without switching view)
First off, the linked example is outdated. It's using version 0.9.2,
whereas the current version (2016-04-19) is 1.3.3. I recommend
you have look at the change log and note the differences, there are many.
Using the el property is fine. Like everything though, there's a time and place.
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
I wouldn't define an el property on every view, but sometimes it makes sense, such as your example. Which is why, I'm assuming, Backbone allows the use of the el property. If you know container is already in the DOM, why not use it?
You have a few options:
The approach outlined in my original answer, a work-around.
fetch the model, and in the success callback, insert the view element into the DOM:
model.fetch({
success:function() {
$('#container').html(new View({model:model}).render().el);
}
});
Another work-around.
Define an el property on the view and fetch the model in the view initialize function. The new content will be rendered in the container element (also the view), when the content/model data is ready, by ready, I mean when the model has finished fetching from the server.
In short,
If you don't want to define an el property, go with number 1.
If you don't want to let the view fetch the model, go with number 2.
If you want to use the el property, go with number 3.
So, I guess my question is: How to implement views, which don't know about their container
In your example, I would use the el property, it's simple a solution with the least amount of code. Not using the el property here, turns into hacky work-arounds that involve more code (complexity) without adding any value (power).
Here's what the code looks like using el:
var Model = Backbone.Model.extend({url:'/model_url'});
var model = new Model();
// set-up a view
var View = Backbone.View.extend({
el:'#container',
template:'model_template',
initialize:function() {
this.model.fetch();
this.listenTo(this.model,'sync',this.render);
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var view = new View({model:model});
Check out the documentation for el.
Here is an updated working example.
If there is an obvious flicker because, your model takes a noticeable amount of time
to be fetched from the server...maybe you should think about displaying a loading bar/variation thereof
while fetching the model. Otherwise instead of seeing the flicker, it will appear the
application is slow, delayed, or hanging..but in reality - it's waiting to render the next view,
waiting for the model to finish fetching from the server. Sitting on old content, just waiting for
the model to load new data to show new content.
I'm building a Backbone app which displays a list of books, but when I add a new book, through the Edit view, he goes to the bottom of the list instead to the top. So basically I want to reverse the order of my collection with a Comparator but what I tried it's not working:
comparator: function (model) {
return -model.get('id');
}
Here is a JSFiddle with all the code: http://jsfiddle.net/swayziak/9R9zU/4/
I don't know if the problem is only related with the comparator or if I need to change something more in other part of the code.
Why not a simple prepend instead of append
this.$el.prepend(
Your comparator is looking for the model IDs:
comparator: function (model) {
return -model.get('id');
}
but none of your models have id attributes. Usually the id would come from the server so the server would supply the initial id values when bootstrapping the collection and then new ids would be assigned (by the server) when you save the model.
If you add ids to your data then things will start to make more sense.
You'll also need to adjust your fiddle to:
this.listenTo(this.collection, 'add', this.render);
instead of:
this.listenTo(this.collection, 'add', this.renderBook);
since your editing panel will kill off all the HTML and you'll need to re-render the whole collection.
Once you get past that, you'll find that your Edit link no longer works. That's because you're trying to re-use views while messing around with the content of their el's. Instead, you should:
Stop trying to re-use views, it is almost never worth the hassle.
Give each view its own unique el.
Call view.remove() to get rid of a view before putting a new one in the common container.
Then create the new view, render it, and put its el in the container.
You'll find that since all your views share a common container, you'll no longer need to bind your collection view to the collection's 'add' event, you'll be tossing and rebuilding the whole view instead.
I have a ListView and InstanceView defined in my Backbone code. The ListView is associated with a collection and it instantiates an InstanceView like so
render: function () {
this.collection.forEach(function(instance){
var commentHTML = new InstanceView({
model: instance
}).render();
renderedComments.push(commentHTML);
});
}
The new view instance goes out of scope after the render call finishes. What I've noticed is that the view persists in memory, though. I can tell because the events attached to it still fire long after the render method terminates.
So, does the view avoid gc because of its reference to the model object which, in turn, is referenced by the collection?
When the view is created events are registered on the model. The callbacks have references to the view, preventing it from being gc'ed.
Check out the backbone code on github. The events are hooked up in the delegateEventsmethod.
Objects are garbage collected whenever the JS engine feels like collecting them.
Your question, however, is actually different.
Just because you cannot access an object does not mean:
Nothing can, and
Things it attaches also go away.
Also, you explicitly add the object to collection, so it's not eligible for collection.
I have a simple collection of messages that I want to reverse sort on time (newest on top), using comparator:
...
this.comparator = function(message) {
var time = new Date(message.get("time")).getTime();
return time;
}
...
In my view, I use fetch and add event:
messages = new MessageCollection();
messages.fetch({update: true});
messages.on("add", this.appendMessage);
...
appendMessage: function(message) {
var messageView = new MessageView({
model: message
});
this.$el.prepend(messageView.render().el);
}
Sadly, the messages are not rendered in the order I am looking for, but in the original order they were in coming from the server.
Now, after some testing I found out that when I add all the messages at once (using reset), the order is as I expected.
messages.fetch();
messages.on("reset", this.appendCollection);
...
appendCollection: function(messages) {
messages.each(function(message) {
this.appendMessage(message);
}, this);
}
Even though I can understand this process since a collection probably can only figure out how it's supposed to be sorted after all models are added, this (the on("add") configuration) used to work in Backbone 0.9.2.
Am I missing something? Did the comparator method change, or the event model in regard to add? Or am I going at it the wrong way? Thanks!
You call appendMessage method when you add a model in collection. the appendMessage is being called in the order of adding models and not the actual order in the collection.
In the "add" case, the model is inserted in the right position in the collection, as it should be by "comparator" documentation). But then you are doing
this.$el.prepend(messageView.render().el);
which will put the html from the MessageView rendering at the top of the $el (which I assume is the CollectionView container).
The best way to also keep the Html respecting the sorted order would be to re-render the collection view, or scroll the collection view children and insert the added messageView at the right place (a bit more difficult to do).
I am trying to trace all of the zombies in my application and get better understanding of how the event binding happens.
I have a view that binds its collection "add" event to its render function.
_.bindAll(this, "render");
this.collection.bind("add", this.render);
So if I log something in my render function I can see in console that rendering happened twice right after user adds new model through UI. The console output looks like this:
rendering index.js?body=1 (line 88)
POST http://localhost:3000/tasks jquery.js?body=1 (line 8103)
rendering index.js?body=1 (line 88)
I wonder why is this happening. I know for a fact that model was added only once to the collection so that makes me think that event should only be fired once. Then I don't understand why render was executed twice.
I have looked in similar question here but it is different because I am using add event instead of change.
Did you instantiate your view twice? It might be 2 different views.
I think that you are calling render two times.
Yoy are doing something like this:
var yourView = new YourDefinedView();
yourView.render(); // this is your manual call to render
//here you call to the server and when data arrives is the second render
this.collection.fetch();
I do not think that render is the best method to bind when a collection receives a new item.
Check this example how we bind a specific event for add items from the Collection to the View:
http://danielarandaochoa.com/backboneexamples/blog/2012/02/22/real-world-usage-of-a-backbone-collection/
It turned out that there was an additional binding to render through Event Aggregator. It was added by another developer.