I have a basic backbone collection of models.
The view I'm working within displays information about the model, allowing edits.
In the render of my view I capture the model based on a passed in 'id'.
render: function() {
this.model = myCollection.get(this.options.passedInId);
// do the render...
}
I then have a click event which updates the model and calls the render to re-render with the updates
updateModel: function() {
var me = this;
this.model.set('someFlag', true);
this.model.save(this.model.toJSON(), {
success: function(model, resp) {
me.render();
}
}
My problem is when it comes back through the render the second time the get from the collection returns a different instance of the model (I can see a different cId on it) which does not contain my changed "someFlag" property. Therefore my edits don't show up when the view is re-rendered. I know there might be a more effecient way of handling this but my question is why does this occur? Shouldn't the model fetched from the collection include the edits I made on that model?
Only other thing is the "myCollection" in this example may have been reset between the initial get and the next get after the edit, but the id is still present and it finds a model just one without any of the updates.
My issue was the collection was being reset between the render method and the updateModel method.
This causes the model to get out of sync with the collection to correct the problem all I needed to do was bind on the reset and make sure my model gets updated with the "new" version. I added this to my render.
var me = this;
this.collection.on('reset', function () {
me.model = this.get(me.model.id);
};
Related
I'm trying to make an application with Backbonejs and this is the first time I use a Front-end Javascript framework, except for JQuery.
I didn't yet understand how the rendering works.
My Example:
render: function() {
var events = this.collection.fetch({
success: function (model, response) {
console.log("Response is " + response);
var events = model.toJSON();
console.log(events.length);
console.log(model.toJSON());
return model.toJSON();
},
error: function(){
console.log("Errore during data fetch");
}
});
this.$el.html(this.template({events:this.collection.toJSON()}));
console.log("Event list: " + events.length);
},
The code above is the render callback of my view.
Inside the success collection fetch I get the data in json format from my API and I successfully log it on the console, but outside the fetch I don't have this data anymore and my view collection seems to be just an empty Backbone object.
Can somebody explain what I'm doing wrong and how rendering works ?
Enrico :)
Well, what render basically does it just inject some html code into the view's el (element).
It could be done by using template engine such as handlebars or mustache or just using the generic backbone (actually underscore) template, as you did in your code.
What we usually do, is initializing the view the way that it listens to the model it is attached to, or collection in your case.
It is not a very good practice to fetch data within render, as it should only use the already fetched data.
So what you could do, is to initialize your view this way:
var View = Backbone.View.extend({
initialize : function(){
this.listenTo(this.collection, "change", this.render);
},
redner : function(){
this.$el.html(this.template({events:this.collection.toJSON()}));
}
});
In this code, the view listens to changes occurring on the collection attached to it. When I say changes, I mean changes to any of the models in the collection, including when the collection is fetched for the first time.
So the listenTo method will fire the render method everytime there's a change in your collection, and by that the whole view will re-render.
This obviously happens when the collection is fetched for the first time.
And for the fetching itself? once youv'e binded the change event to the view, you can fetch it anywhere in your app, not necessarily inside the view.
I have a backbone page which behaves as follows.
Collection - > Models -> Views
In that I have a collection which contains search results of N length. Each of these models is tied to an instance of a view which in this case is a row of data being displayed.
I want to be able to toggle each row from their 'details' view to their 'advanced' view which contains more information. At the moment I have the parent view rendering N number of views for each model. I can toggled the change in state by updating the model and listening to the change event and then only re-rendering the part of the view I have clicked on. I noticed a problem whilst doing this. The problem is the viewport jump to the top of the page which isn't great UX.
Whilst debugging this I noticed something strange
The parent view's (page that holds the search results) render function is being called, which in turn is calling the render function of each of the rows. I think this is what's causing each of the child views to re-render.
Here are some code examples to demonstrate the problem:
// The child view's render and initialis
var SearchInviteRow = Backbone.View.extend({
tagName: "invite-section-result-row",
initialize: function(params){
this.template = templateHTML;
this.extendedTemplate = extendedTemplate;
this.listenTo(this.model, 'change', this.render);
},
events : {
"click .toggle-attachments" : "renderWithAttachments"
},
render: function(){
var view = this, render;
var that = this;
if(!this.model.get("expand")){
var rendered = Mustache.render(view.template, that.model.toJSON());
this.$el.html(rendered);
} else {
var rendered = Mustache.render(view.extendedTemplate, that.model.toJSON());
this.$el.html(rendered);
}
return this;
},
close: function(){
this.remove();
},
renderWithAttachments: function(){
if( !this.model.get("expand") ) {
this.model.set( {"expand" : true } );
this.model.getAttachments();
} else {
this.model.set( {"expand" : false } );
}
}
});
This is the part of the parent's render that iterates over the collection appending the rows to the search tiles.
for (var i = 0; i < this.collection.length; i++) {
view.subViewArray[i] = new SearchInviteRow({
model: this.collection.at(i)
});
this.$(".invite-section-inside").append(view.subViewArray[i].render().el);
}
What I can't work out is why the parent's render function is being called which is causing my other problems.
There are two ways this can happen, one is if you are listening to the backbone events on your collection the second is if it's a DOM event that's being triggered.
With the backbone events, if you look at the documentation you will see that events triggered on a model in a collection will also be triggered on the collection itself
Any event that is triggered on a model in a collection will also be
triggered on the collection directly, for convenience. This allows you
to listen for changes to specific attributes in any model in a
collection, for example: documents.on("change:selected", ...)
That being the case, you should either check in the events callback whether you want to act on it (see what triggered it, perhaps passing in a extra flag when triggering the event ), or make sure you are only listening to the specific events you are interested in acting on. For example instead of listening to you collections generic change event you might want to listen to the more specific version of it (change:someProperty).
With DOM events, this can easily happen if you are listening to the same selector in your parents view as in your child's view, since the parent's root el is also a parent to the child's el.
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.
I'm going through the process of learning Backbone.js and I've come across a few things that look like they work... but behind the scenes, they might be causing some problems. One such issue is design patterns for swapping views.
I have a menu on the left (and corresponding view) that contains a list of users. Each user, when clicked, will display their corresponding list of movies to the right into another view. I do this by getting the model of the clicked user, building a new Movie collection based on that model, and rendering a new Movie View (to the right). If I click on another user, it does the same thing: it gets the user model, builds a new Movie collection, and renders a new Movie View to the right, replacing the DOM element. This looks fine -- but I'm worried about all of the new objects/bindings that are being created, and potential issues that could arise. So, what are my alternatives?
(1) Should I be trying to have the view redraw when the collection changes? How do I do this if I'm creating new collections?
(2) Or should I be unbinding everything when another user is clicked?
Here is my userlist view:
Window.UsersList = Backbone.View.extend({
el: $("#users"),
initialize: function(){
this.collection.bind('reset', this.render, this);
},
render: function(){
var users = [];
this.collection.each(function(user){
users.push(new UsersListItem({model: user}).render().el);
},this);
$(this.el).html(users);
return this;
}
});
In my UsersListItem view I capture the click event and call show_user_movies in my controller:
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
var movie_collection = usermodel.fetchMovies();
movie_list = new MovieList({collection: movie_collection, model: usermodel});
movie_list.render();
},
Thanks for any suggestions.
Just re-use the same MovieList view along with it's associated collection, using reset(models) to update the models in the collection, which should re-render the view. You can use the same pattern you have above with your MovieList view binding to the collection's reset event and re-rendering itself at that time. Note that usermodel.fetchMovies() doesn't follow the backbone asynchronous pattern of taking success/error callbacks, so I don't think the code works as is (maybe you simplified for the purpose of this question), but the point is when the new set of models arrives from the server, pass it to movie_list.collection.reset and you're good to go. This way you don't have to worry about unbinding events and creating new views.
show_user_movies: function(usermodel){
// fetchMovies() lazy-loads and returns a new collections of movies
movie_list.collection.reset(usermodel.fetchMovies().models);
},
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.