What events are triggered when calling fetch() on a Backbone.js collection? - backbone.js

In my backbone.js app, there is a Trips collection that holds Trip models, which is working with LocalStorage. I am able to call Trips.create(form_attributes) to create and save a trip to the Todos store.
When I first load my app, I call Trips.fetch({ success: trips_fetch_success }), and trips_fetch_success receives a response that shows the Trip models that the Trips collection holds.
I have tried to bind refresh and change events to the Trips collection, but these events are not being caught, making me believe I have the wrong idea about which events Trips.fetch triggers.
My question: which events should Trips.fetch trigger? And are the events triggered on the collection or on each of the individual Trip models?

Collection.fetch() will call reset on success, which in turn will trigger a 'reset' event. Any subscribers to the collections reset event should receive the event.
The key here is "on success." I had this problem, only to discover that backbone was silently swallowing my errors messages. Pass in an error handler that, at least, logs to console.log(), and see what's happening:
trips.fetch({error: function() { console.log(arguments); }});
(Note: Old versions of backbone.js will trigger "refresh" instead of "reset")

If you are using backbone 1.0, you'll need to pass reset:true in the fetch() call in order to bind with the reset event:
trips.fetch({reset: true});

As of backbone 1.0, model.fetch() triggers a 'sync'. That's what you should bind to.
Here's the relevant part from the backbone.js source where the 'sync' event is fired:
fetch: function(options) {
options = options ? _.clone(options) : {};
if (options.parse === void 0) options.parse = true;
var model = this;
var success = options.success;
options.success = function(resp) {
if (!model.set(model.parse(resp, options), options)) return false;
if (success) success(model, resp, options);
// HERE'S THE TRIGGER!
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},

Related

Backbone Marionette: Rendering Collection in ItemView

I was unable to find any posts relevant to this error. I am attempting to render a Backbone Collection in a Marionette ItemView. The template is rendered, however, the data related to the collection is not rendered in the template. I am getting no errors or other indicators. For reasons I do not understand, using setTimeout() on App.mainRegion.show(overView). However, I know that that is not an acceptable solution. Could someone give me some insight on how to make an ItemView for a Collection properly render in this case? Here is my simplified code:
My Collection to be rendered:
About.Collection = Backbone.Collection.extend({
url: '/api/about',
idAttribute: '_id',
});
Involved View definitions:
About.ListView = Marionette.CollectionView.extend({
tagName: 'ul',
itemView: App.About.ListItemView,
});
About.OverView = Marionette.ItemView.extend({
tagName: 'div',
className: 'inner',
template: _.template('<h2>About Overview</h2><p><%= items %></p>'),
});
My relevant execution code:
var API = {
getAbouts: function() {
var abouts = new App.About.Collection();
abouts.fetch();
return abouts;
},
...
}
var abouts = API.getAbouts();
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Correctly renders collection data
App.listRegion.show(aboutsListView);
// Does not render collection data
App.mainRegion.show(aboutsOverView);
// unless
setTimeout(function() {App.mainRegion.show(aboutsOverView)}, 50);
For those who are interested, I am using an ItemView with the eventual intent to display aggregate data of About.Collection. I will be happy to provide additional information, if needed.
It's an issue with the asynchronous nature of the fetch call on your collection. The data for the collection has not returned when you show the two views. If you update the execution part of your code something like the following (untested), you should be on the right tracks:
var API = {
getAbouts: function() {
// Just return the new collection here
return new App.About.Collection();
},
...
}
// Fetch the collection here and show views on success
var abouts = API.getAbouts().fetch({
success: function() {
var aboutsListView = new App.About.ListView({collection: abouts }),
aboutsOverView = new App.About.OverView({collection: abouts});
// Should render collection data now
App.listRegion.show(aboutsListView);
// Should render collection data now
App.mainRegion.show(aboutsOverView);
}
});
The abouts.fetch call is asynchronous, and a significant amount of time elapses before the collection receives data from the server. This is the order in which things are happening:
You call getAbouts, which itself calls abouts.fetch to make GET call to server for collection.
The listRegion.show and mainRegion.show calls are made, rendering the 2 views with the empty collection (the collection hasn't received a response from the server yet).
The GET call eventually returns, and the collection is populated with data.
Only the aboutsListView re-renders to show the data (see below for the reason).
The reason that only the aboutsListView re-renders is that the Marionette CollectionView automatically listens for the collection's reset event, which is fired when the collection's contents are replaced.
You can fix this by simply adding an initialize function to your OverView, so that view also re-renders in response to the same event:
// add to About.OverView:
initialize: function() {
this.listenTo(this.collection, 'reset', this.render);
}
That will take care of it.

Prevent Backbone event from triggering more than once, how to check if event is registered already?

In my router object, I created an event object to share among my views
I pass the event object to my views
I register events to this shared object like this
var productCatalogView = Backbone.View.extend({
initialize: function (options) {
//bind alert event to sharedEvents
options.sharedEvents.bind("alert", this.alert,this);
},
alert: function () {
alert('alerted');
}
});
//The following view triggers the alert event
var testView = Backbone.View.extend({
initialize: function (options) {
this.sharedEvents = options.sharedEvents;
},
events: {
'click #test': 'triggerAlert'
},
triggerAlert: function (e) {
this.sharedEvents.trigger("alert", null);
}
});
THE PROBLEM:
The problem I experience is that the first time I click on the button which triggers the alert event (second view), the alert event gets called once (good), this causes the first view to be re-rendered by triggering the route passing search parameters, therefore creating the first view and binding the sharedEvents again, hence when I trigger the alert event a second time, it gets triggered twice (bad), the next time I repeat the same process, it gets triggered 3 times, and so on and so forth. I guess it has to do with the event binding in the first view, it occurs more than once, i.e each time the view is initialized (if I am correct)
please how can I make the binding of the event occur once.
Here is my router which shows how I initilze the views:
var Router = Backbone.Router.extend({
sharedEvents:_.extend({},Backbone.Events),
catalog: function (id) {
//....unecessary code left out
var productView = new ProductView({sharedEvents:this.sharedEvents});
this.renderView(productView);
this.renderView(new testView({sharedEvents: this.sharedEvents }));
}
renderView: function (view) {
if (null != this.currentView) {
this.currentView.undelegateEvents();
// this.currentView.remove();
}
this.currentView = view;
this.currentView.render();
}
});
I have tried this solution but problem persists, thanks
Try using Backbone.Events' listenTo method instead of the bind method. Then, in your renderView(), call this.currentView.remove instead of this.currentView.undelegateEvents.
Rationale:
I believe in your renderView() method, you are using undelegateEvents() thinking it releases all event listeners created by your view. It only releases events bound on to your view's $el element. However, using remove() on the view releases events bound to the $el as well as events created using this.listenTo() (and this.listenOnce()).
Now once you render another view, the old currentView will be properly released and you'll only get one alert.

model.save not updating model value

I am trying to learn Backbone.js and have stuck at an issue. model.save is not updating my model value, though model.fetch updates the value fine.
Please see my code below and let me know if I am doing anything wrong here:
Model
var Person = Backbone.Model.extend({
url: "CreatePerson",
defaults: {
name: "",
age: 0
},
initialize: function(){
this.on("change:name",function(){
alert("Updated value is: "+this.get("name"));
});
}
});
I am creating an instance of this model on my html page, below is the code:
var person = new Person({name:"manish"});
person.save(person,{
wait: true,
success: function(){
alert("Data saved: "+person.toJSON().name);
},
error: function(){
alert("Sorry, something wrong went with the system");
}
});
person.fetch({
success: function(){
alert("Data Fetched: "+person.get("name"));
}
});
On both save and fetch, I am returning the following JSON data from the server:
{"name":"Logan","age":23}
Interestingly, for save, the change event is not fired and the alert box gives me the old model value (i.e manish), whereas when fetch function executes, the change event is fired and the fetch callback gives me the new value (i.e logan).
Can someone help me in identifying what I am doing wrong here?
From backbonejs.org
http://backbonejs.org/#Model-save
Calling save with new attributes will cause a "change" event
immediately, and a "sync" event after the server has acknowledged the
successful change. Pass {wait: true} if you'd like to wait for the
server before setting the new attributes on the model.
Sounds like the server hasn't returned that it is finished. Removing wait:true should make the changed event trigger.
There is error in your code:
person.save(person,{
You should provide object of attributes to your save call or just the options, but not the model itself. Remove person, and check if it works. wait:true should be set, to ensure that you change to a proper data validated by server.

Backbone.js MVC way to render the view AFTER the data is received back from the server on a fetch?

I wish to read a whole database table to fill a Backbone.js Collection, before updating a View.
I am using fetch and listening to the reset event.
My problem is the reset event fires up before the http request is made to the server.
My question is: how can I render the view AFTER the data is received back from the server on a fetch?
Here is a jsfiddle showing the problem (with a debugger placed at reset):
http://jsfiddle.net/GhaPF/16/
The code:
$(document).ready(function() {
var Item = Backbone.Model.extend({
urlRoot : './items'
});
var ItemList = Backbone.Collection.extend({
model: Item,
url: './items/',
});
var ItemListView = Backbone.View.extend({
el: 'body',
initialize: function(myitemList) {
this.itemlist = myitemList;
this.itemlist.bind('reset', this.debuggThis());
},
debuggThis: function() {
debugger;
},
render: function() {
},
events: {
"keypress #new-item": "createOnEnter"
},
createOnEnter: function(e) {
}
});
$("#new-item").focus();
var itemlist = new ItemList();
var myitemListView = new ItemListView(itemlist);
itemlist.fetch();
});​
The following code works, but it just doesn't feel like proper backbone.js (MVC) code since it would be placed outside of the View definition:
itemlist.fetch().complete(function(){
Maybe the issue is this line:
this.itemlist.bind('reset', this.debuggThis());
Should actually be:
this.itemlist.bind('reset', this.debuggThis);
Your debugThis function was getting run at the time you set up the listener for the 'reset' event - not when the event is triggered. This was telling JavaScript that you wanted debugThis to return a callback function instead of having debugThis "be" the callback function.
Also, orangewarp's comment about passing 'this' as the third parameter is probably relevant too. Sot it would end up as:
this.itemlist.bind('reset', this.debuggThis, this);
That's strange. When you fetch() the reset event should be triggered AFTER your collection is populated. So I'm thinking the phenomena that reset happens before the http request is fired up may not be what you think it is.
Instead of using the complete... you could always just use the success callback option like this:
itemlist.fetch({
success: function() {
// Whatever code you want to run.
itemlist.debuggThis();
}
});
Also, when binding your reset you probably want this:
this.itemlist.bind('reset', this.debuggThis, this);

Backbone reset event in collection

How does Backbone reset event works?
As far as I understand
Remove all models from collection
Add newly "fetched" models to collection
Fires reset event
In my case each model draw something on SVG so I should call remove function before removing model from collection. Which event is triggered when model is removed from collection?
As #Paul noted, there is no predefined event fired before a reset. However, you can provide your own by overriding the reset method on your collection. For example,
var SVGCollection = Backbone.Collection.extend({
reset: function(models, options) {
options = options || {};
if (!options.silent) {
this.trigger('prereset', this, options);
}
Backbone.Collection.prototype.reset.call(this, models, options);
}
});
And a sample usage
var c = new SVGCollection([
{id: 1},
{id: 2}
]);
c.on('prereset', function() {
console.log(c.pluck('id'));
});
c.on('reset', function() {
console.log(c.pluck('id'));
});
c.reset({id: 3});
See http://jsfiddle.net/nikoshr/8vV7Y/ for a demo
You could also trigger events on each model.
You're correct that reset is fired after the old models have been removed and the new models have been added.
There isn't an event fired for when a model is removed from a collection through the reset method.
You might have to keep a reference to the old models outside of the collection, and then when the reset event is fired, you will have reference to those models so you can call the remove function for them on SVG.

Resources