collection add event firing on view initialisation - javascript

I have this code in my backbone application,
createNewProject: function(e) {
e.preventDefault();
this.createNewProject = new app.CreateNewProject({
collection: this.dropdownListProjectCollection
});
}
app.CreateNewProject = Backbone.View.extend({
el: '.modal',
template: _.template( $("#tpl-create-new-project").html() ),
events: {
'click input[type="submit"]' : 'saveBasicProject'
},
initialize: function() {
this.collection.on("add", console.log("added"));
this.$el.find("h4").text("Create new project");
this.$el.find(".modal-body").html( this.template() );
this.render();
return this;
},
I am trying to detect when a model is added to the collection, and then eventually fire a method to render that new record. However I am having problem in that the this.collection.on('add'...) seems to run when ever the view is initialised and again when a model is saved, how can I only run the method when the model is saved, and not the view being initialised.

I recommend you do this.listenTo(this.collection, ... in your view because when the view is removed it will remove the event and avoid memory leaks.
The add event will get fired on every .set on the collection. If you passed models on collection initialize this will happen ctor -> reset -> add -> set to add those models so you will get reset and add events.
What doesn't make any sense is that your collection should have initialize BEFORE you initialized the view so I don't know why you are getting that event unless you are subsequently adding more models.
To listen to a save event you can use these events on the collection:
request - when the request starts
sync - when the request has happened successfully
error - something went wrong
PS this is a shortcut for this.$el.find -> this.$('.someselector')

Related

Backbone.js: Fetch a collection of models and render them

I am learning JavaScript MVC application development using Backbone.js, and having issues rendering model collection in the view. Here's what I want to do:
After the page finishes loading, retrieves data from the server as model collection
Render them in the view
That's all I want to do and here is what I have so far:
$(function(){
"use strict";
var PostModel = Backbone.Model.extend({});
var PostCollection = Backbone.Collection.extend({
model: PostModel,
url: 'post_action.php'
});
var PostView = Backbone.View.extend({
el: "#posts-editor",
initialize: function(){
this.template = _.template($("#ptpl").html());
this.collection.fetch({data:{fetch:true, type:"post", page:1}});
this.collection.bind('reset', this.render, this);
},
render: function(){
var renderedContent = this.collection.toJSON();
console.log(renderedContent);
$(this.el).html(renderedContent);
return this;
}
});
var postList = new PostCollection();
postList.reset();
var postView = new PostView({
collection: postList
});
});
Problem
As far as I know, Chrome is logging the response from the server and it's in JSON format like I want it. But it does not render in my view. There are no apparent errors in the console.
The server has a handler that accepts GET parameters and echos some JSON:
http://localhost/blog/post_action.php?fetch=true&type=post&page=1
[
{
"username":"admin",
"id":"2",
"title":"Second",
"commentable":"0",
"body":"This is the second post."
},
{
"username":"admin",
"id":"1",
"title":"Welcome!",
"commentable":"1",
"body":"Hello there! welcome to my blog."
}
]
There are 2 potential problems with your code.
The event listener callback should be registered before calling the collection.fetch(). Otherwise, you might miss the first reset event as it might be triggered before the listener is registered.
The reset event is not enough to ensure that the view will re-render every time the collection gets updated.
Also, note that it is a good practice to use the object.listenTo() form to bind events as it will ensure proper unregistration when the view is closed. Otherwise, you may end up with what is known as Backbone zombies. Here is a solution.
this.listenTo( this.collection, 'reset add change remove', this.render, this );
this.collection.fetch({ data: { fetch:true, type:"post", page:1 } });
Note how you can register multiple events from the same object by separating them with whitespace.
change
this.collection.bind('reset', this.render, this);
to
this.collection.bind('sync', this.render, this);
The problem is you perform reset only once, in the beginning. And at that time you don't have anything to render. The next time, when you fetch your collection, reset event doesn't fire, because you fetch collection without option {reset: true}.
Change this line
this.collection.bind('reset', this.render, this);
to
this.listenTo(this.collection, 'reset', this.render);
When fetching your collection, the reset event is not fired by default anymore. (I believe since version 1.0)
In order to have Backbone fire the reset event when the collection has been fetched, you now have to call the fetch method like so:
this.collection.fetch({reset: true});

Backbone collection add method being fired twice

For the following code, the add event bound in the view fires twice (more if you add more elements to the collection at once).
http://jsfiddle.net/radu/GnG66/
App = window.App || {};
var Model = Backbone.Model.extend();
var Collection = Backbone.Collection.extend();
App.collection = new Collection({ model: Model });
var View = Backbone.View.extend({
events: {
'click': function() {
console.log('click');
App.collection.add([{
foo: 'foo'
}, {
bar: 'bar'
}]);
}
},
initialize: function() {
App.collection.on('add', function() {
console.log('Something has been added to the collection')
}, this);
}
});
$(function() {
App.view = new View({ el: '#test' });
});​
If instead of adding an array to the collection, you just pass several objects as arguments (basically just remove the square brackets), the event only fires once.
Is this by design and is there a way to override this behaviour without passing { silent : true } as an option?
The add event is fired once for each model added.
Collection.add can take an array of models, or a single model and some options.
In your example above, you are passing an array of two models in. Since the add event gets fired once for each model added, it fires twice.
When you pass in several objects, Backbone thinks the first object is a model and the second is a hash of options. That means only one model is being added, so it fires the add event once.
Sorry to resurrect this question from the dead, but I was having this problem too and wanted to post how I solved it. The problem with having 'add' trigger so many times for me was because I had a complex render function in my view that was listening for 'add'. This was causing serious performance issues.
I resolved it by creating a temporary collection using backbone's handy collection.clone() method on it, adding the new models to it, and then resetting the original collection with the temp collection's models property. The code looks like this:
// Create a temporary copy of searchResults
var temp = App.searchResults.clone();
// Add the new results
temp.add(newResults.models);
// Copy the new data over the old data
App.searchResults.reset(temp.models);
// Added this since reset triggers 'reset' and my view is listening for 'change add remove'
this.get('filtered_flights').trigger('change');
This sets off only ONE 'change' event instead of several 'add' events.

BackboneJS catch event OnAdd from initial load

I'm creating a collection without using fectch(), but with JSON data already available.
this.displays = new Displays(jQuery.parseJSON($('#temp_json').html()));
I need for each model of that collections to have a 'position' value setup, which should be the position of this model in this collection.
What I'm trying to do is to catch an event, where in the initial load each model is constructed from the JSON and added to the collection.
I'll then make something like:
theModel.set('position', this.length);
Unfortunately I can't find that event I should bind my collection to.
Also this collections's models contains other collection (...) where same should be done.
This might seems weird but I have to do this as later on in my view I'll peform things like:
var prototypeNames = [this.model.get('display').get('position'), this.model.get('position')];
Hope this is clear enough.
Thanks!
The Collection.add-function fires a add-event, which you can listen to with the on-function. The event passes the added model AND an options object, which contains some useful attributes (like the index where the model was added), as parameters. (documentation here)
So you'll do something like this:
collection.on('add', function(model, options) {
model.set('position', options.index);
});
if you want to find out more about the parameters the add-event passes, just log them, because the existing documentation is scant at best.
collection.on('add', function(model, options) {
console.log(options);
});
Hope this helps!
Solution that worked for me:
I used backboneRelational, which is awesome when working with actual model relationships.
I'm listening to "relational:add" which is triggered for each item of the collection when constructing it.
I then set some values on my item and trigger a new event 'postAdd' that can be listened to from my view.
window.Displays = Backbone.Collection.extend({
model: Display,
initialize: function(data, options){
this.on("relational:add", function(relModel){
relModel.set('pos',this.indexOf(relModel));
this.trigger('postAdd', relModel);
}, this);
},
});
You can override the default 'add' implementation of the collection to trigger a custom event which won't be silenced by Backbone.
var MyCollection = Backbone.Collection.extend({
// Override the default 'add' implementation...
add: function( models, options ) {
// Call the default implementation first...
Backbone.Collection.prototype.add.apply( this, arguments );
// Fire our custom events on the models...
while (model = models.shift()) {
model.trigger('customAdd', model, this, options);
}
return this;
}
});
You can now listen to the 'customAdd' event triggered on the model. This one will always be triggered, event when silent = true.
As a consequence, when an item is added an with {silent: false} two events will be triggered: 'add' and 'customAdd'.
I've used this technique to fire an event when a new collection has initially loaded and all models are created. Since the initialize method will be called before the models are created, I needed to override the 'reset' function to trigger my code to execute when the models were created.

How to disable Backbone.sync for a new model, and re enable sync *after* the user hits a save button

I have a Backbone collection model (with sub-models as elements)
and views to edit it.
I would like it that when the model is initially created, to "turn off"
sync, so the back end is never invoked until the user clicks on a
button, then I would like to "turn on" the sync, and invoke the save
method on the root model, in order to save it to the DB.
Once a model it saved, it should behave like a normal model.
The goal is to avoid saving until the user determines that he is happy with
what he has entered.
Backbone will initially look for a model's local sync function before going to Backbone.sync.
Backbone.js Documentation: The sync function may be overriden globally as Backbone.sync, or at a finer-grained level, by adding a sync function to a Backbone collection or to an individual model.
Therefore you can do this:
var MyModel = Backbone.Model.extend({
// New instances of this model will have a 'dud' sync function
sync: function () { return false; }
});
var MyView = Backbone.View.extend({
...
events : {
'click #my-button' : 'enableSync',
'click #my-save-button' : 'saveModel'
},
enableSync: function () {
// If this view's model is still pointing to our fake sync function,
// update it so that it references Backbone.sync going forward.
if (this.model.sync !== Backbone.sync) {
this.model.sync = Backbone.sync;
}
},
saveModel: function () {
// This won't actually do anything until we click '#my-button'
this.model.save();
}
...
});
var view = new MyView({ model: new MyModel() });

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