Backbone: reverse collection order with comparator - javascript

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.

Related

Backbone views which don't know about their container, models to be fetched via AJAX, no UI/UX trade-offs and maintainable code

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.

Adding a Collection to a ul in backbonejs

I'm just learning js/backbonejs and I have a simple question, Please feel free to direct me to a duplicate.
I have a Collection, I've populated it and I can access it in the console by doing the standard:
collection1.at(1).get('name');
I can also loop through the values by doing:
for(vars i = 0; i < collection1.size(); i++)
{
console.log(collection1.at(i).get('name'));
}
I have four buttons and have views on them and functions that correctly output something to the console when i click on them. When i click on the Show all button i want to display every model in the collection along with the data it has (id,name,fame);
How would i go about doing this? I know i have to have a
<ul id = "gottaChangeThis"></ul>
How would I be able to add something like this to it:
<li><%=id%><%=name%><%=fame%></li>
Any help or redirection would be helpful, Thanks
The basic architecture could include a Backbone.View that accepts your Collection. On render, iterate through the Models in the collection, and for each one render a different Backbone.View (to render the <li>) and append it to the parent <ul> element.
As an alternative, consider using Marionette. It's a Backbone framework/extension that gives you additional objects as a means to eliminate a lot of boilerplate. In your case, you'd want a Marionette.CollectionView with a childView specified. This childView could be a Marionette.ItemView, such that when you render the CollectionView, it automagically instantiates and renders a childView for each Model in your Collection.

Count or Select Backbone View Instances

Let's say I'm trying to create a toDo application, where clicking each toDo opens an edit form for each toDoItem. I only want a maximum of one edit form open at any one time, so right now I am doing this in the edit method of the toDoItem view:
edit: function (e) {
e.preventDefault();
if ($('.editForm').length == 0) {
//create form model and view
}
}
That works, but doesn't seem very Backbone-y. Is there are way to select or count all instances of a particular view (in this case, the form-view)?
AFAIK, there are no utilities method in Backbone.View to count instances of a particular Views. Here are some ideas...
Maybe each of your TODO form is tied to a Model? In that case, you can have a model.set/get 'editing' and a collection.isAlreadyEditing() which would filter the models on this field:
(collection.filter(function(model){ return model.get("editing") }).length > 0
That would allow you to use on change:editing events throughout your views to control the logic and have convenient helpers functions in the collection to define some behavior of all those TODO as a whole. This would be one way to implement something closer to a Controller pattern in Backbone.
Another common thing in backbone is to keep an array of all the subviews when you instanciate them, so you could just do a:
_.any(subViews, function(view){return view.editing; })
Assuming that you keep a editing flag in your subviews when it gets toggled.
You can have your views listen to a toggleEdit event with the id or the model or something identifying what is being edited, sometimes the event handler can be as simple as a toggleClass("open", model==this.model)...
I am sure there are millions of other ideas. But counting jQuery selected elements is probably not very high on the list!

Collection sorting with comparator only works after fetch is complete?

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).

backbone.js - collections and views

I understand how to get a collection together, or an individual model. And I can usually get a model's data to display. But I'm not clear at all how to take a collection and get the list of models within that collection to display.
Am I supposed to iterate over the collection and render each model individually?
Is that supposed to be part of the collection's render function?
Or does the collection just have it's own view and somehow I populate the entire collection data into a view?
Just speaking generally, what is the normal method to display a list of models?
From my experience, it's the best to keep in your collection view references to each model's view.
This snippet from the project I'm currently working on should help you get the idea better:
var MyElementsViewClass = Backbone.View.extend({
tagName: 'table',
events: {
// only whole collection events (like table sorting)
// each child view has it's own events
},
initialize: function() {
this._MyElementViews = {}; // view chache for further reuse
_(this).bindAll('add');
this.collection.bind('add', this.add);
},
render: function() {
// some collection rendering related stuff
// like appending <table> or <ul> elements
return this;
},
add: function(m) {
var MyElementView = new MyElementViewClass({
model: m
});
// cache the view
this._MyElementViews[m.get('id')] = MyElementView;
// single model rendering
// like appending <tr> or <li> elements
MyElementView.render();
}
});
Taking this approach allows you to update views more efficiently (re-rendering one row in the table instead of the whole table).
I think there are two ways to do it.
Use a view per item, and manipulate the DOM yourself. This is what the Todos example does. It's a nice way to do things because the event handling for a single model item is clearer. You also can do one template per item. On the downside, you don't have a single template for the collection view as a whole.
Use a view for the whole collection. The main advantage here is that you can do more manipulation in your templates. The downside is that you don't have a template per item, so if you've got a heterogeneous collection, you need to switch in the collection view template code -- bletcherous.
For the second strategy, I've done code that works something like this:
var Goose = Backbone.Model.extend({ });
var Gaggle = Backbone.Collection.extend({
model: Goose;
};
var GaggleView = Backbone.View.extend({
el: $('#gaggle'),
template: _.template($('#gaggle-template').html()),
render: function() {
$(this.el).html(this.template(this.model.toJSON()));
}
};
var g = new Gaggle({id: 69);
g.fetch({success: function(g, response) {
gv = new GaggleView({model: g});
gv.render();
}});
The template code would look something like this:
<script type="text/template" id="gaggle-template">
<ul id="gaggle-list">
<% _.each(gaggle, function(goose) { %>
<li><%- goose.name %></li>
<% }); %>
</ul>
</script>
Note that I use the _ functions (useful!) in the template. I also use the "obj" element, which is captured in the template function. It's probably cheating a bit; passing in {gaggle: [...]} might be nicer, and less dependent on the implementation.
I think when it comes down to it the answer is "There are two ways to do it, and neither one is that great."
The idea of backbone is that view rendering is event driven.
Views attach to Model data change events so that when any data in the model changes the view updates itself for you.
What you're meant to do with collections is manipulate a collection of models at the same time.
I would recommend reading the annotated example.
I've also found this a confusing part of the Backbone framework.
The example Todos code is an example here. It uses 4 classes:
Todo (extends Backbone.Model). This represents a single item to be todone.
TodoList (extends Backbone.Collection). Its "model" property is the Todo class.
TodoView (extends Backbone.View). Its tagName is "li". It uses a template to render a single Todo.
AppView (extends Backbone.View). Its element is the "#todoapp" . Instead of having a "model" property, it uses a global TodoList named "Todos" (it's not clear why...). It binds to the important change events on Todos, and when there's a change, it either adds a single TodoView, or loops through all the Todo instances, adding one TodoView at a time. It doesn't have a single template for rendering; it lets each TodoView render itself, and it has a separate template for rendering the stats area.
It's not really the world's best example for first review. In particular, it doesn't use the Router class to route URLs, nor does it map the model classes to REST resources.
But it seems like the "best practice" might be to keep a view for each member of the collection, and manipulate the DOM elements created by those views directly.

Categories