Backbone Collection communicating with Backbone Models - javascript

I'm trying to understand what the best practice is for communicating between the different components of a Backbone project. I've been re-implementing the Backbone version of TodoMVC and my question is around removing models from the collection object.
I understand that Backbone model instances trigger a change event whenever any of its model properties are modified via .set() and that the view associated with the model instance should .listenTo() changes from the model instance and then re-render. However, what's the best practice for communication between models and the collection object that houses it? For example, how should the communication work when a model is removed from the collection object?
Here's what I think: when a model is removed, I think the model instance should emit a custom event that the collection object is listening for and pass itself along. When the collection object hears this event, it should remove the model instance from the list (along with any event listeners attached to the model) and then the entire collection object should re-render itself. This re-rendering process will then create a new set of models and model-views.
Is this the best approach? I would love to hear your input! To me, this process of re-rendering sounds really expensive since you'll have to destroy the existing DOM elements, remove their event listeners, and then re-create them again.
UPDATE - 3/26/2015
To make this more concrete, I'll include the code that I have so far and point out where I feel my understanding is off.
File Structure
collections
a. todoList.coffee
models
a. todo.coffee
views
a. todoView.coffee
b. todoListView.coffee
app.coffee
app.coffee
window.app = app = window.app || {}
data = [
{
title: 'Eat dinner',
completed: false
}
{
title: 'Go to gym',
completed: true
}
]
app.todos = data.map (todo) -> new app.Todo todo
app.todoList = new app.TodoList app.todos
app.todoListView = new app.TodoListView
collection: app.todoList
app.$app = $('#todo-app')
$('#todo-app').append app.todoListView.render().el
todo.coffee
window.app = app = window.app || {}
app.Todo = Backbone.Model.extend
defaults:
title: ''
completed: false
toggleComplete: ->
this.set 'completed', !this.get 'completed'
todoList.coffee
window.app = app = window.app || {}
app.TodoList = Backbone.Collection.extend
model: app.Todo
initialize: () ->
# This is what I don't like - creating 'remove-todo' event
this.on 'remove-todo', this.removeTodoFromList
removeTodoFromList: (model) ->
this.remove model
getCompleted: ->
this.filter (model) -> model.completed
getNotCompleted: ->
this.filter (model) -> !model.completed
todoView.coffee
window.app = app = window.app || {}
app.TodoView = Backbone.View.extend
tagName: 'li'
events:
'click input' : 'checkComplete'
'click .delete' : 'removeTodo'
checkComplete: (e) ->
this.model.toggleComplete()
removeTodo: (e) ->
# I don't like how the collection is listening for this custom event 'remove-todo'
this.model.trigger 'remove-todo', this.model
initialize: ->
this.listenTo this.model, 'change:completed', () ->
this.render()
render: ->
template = _.template $('#todo-view').html()
this.$el.html template this.model.toJSON()
return this
todoListView.coffee
window.app = app = window.app || {}
app.TodoListView = Backbone.View.extend
tagName: 'ul'
className: 'todo-list'
initialize: ->
this.collection.on 'remove', (() ->
this.resetListView()
this.render()
), this
addOne: (model) ->
todoView = new app.TodoView
model: model
this.$el.append todoView.render().el
resetListView: ->
this.$el.html('')
render: ->
_.each this.collection.models, ((model) -> this.addOne model), this
return this
Explanation of Code
As you can see in my comments above, whenever a click happens on the remove button, my todoView triggers a custom event 'remove-todo'. The todoList collection listens to this event and removes the specific model from the collection. Since a 'remove' event is triggered whenever a collection removes a model, the todoListView listens for this 'remove' event and then re-renders. I feel like I'm off somewhere. Any advice?

It seems you are talking about views when you talk about models. When a model is removed from a collection you don't need a custom event, a "remove" event is triggered.
http://backbonejs.org/#Collection-remove
If you want to remove the corresponding view, use http://backbonejs.org/#View-remove this will manage the DOM and listener.
The re-rendering of view (I don't understand what you are talking about when you talk about rerendering collection) can be triggered if you listen to "remove" models from the collection, otherwise listen to http://backbonejs.org/#Collection-add, or http://backbonejs.org/#Model-hasChanged is you want to rerender your view when a model has changed.
I hope it helps.

Related

Backbone: adding model from one collection to another

Here is the general structure I have right now:
Collection 'A' which has view 'A'
to add a model, you can open a modal and choose from a list. The modal is it's own view, 'B', tied to its own collection, 'B'. the view 'B' is instantiated from view 'A' on a click event.
I was going to initialize view 'B' with a 'myParent' attribute so when a model is selected from collection b, I can say this from within view B:
this.myParent.collection.add(newModel).
I know this will work, but is this coupling things too tight that don't need to be? is there a good pattern for this? I was thinking about having several different events and triggers spread about different places but that seems to just complicate things without much added value.
I have come across a similar problem in an application which lists a collection of selectable models in a table. Based on the models which are selected, another view needs to determine which buttons to show to the user.
In this case, I went for an event driven approach. Every time a model from view A is selected, I fire an event which View B listens to and adds that model to Collection B.
For your situation, that could look something like this:
// Your collection view.
var ViewA = Backbone.View.extend({
initialize: function(){
Backbone.Events.on('modelSelected', this.onModelSelected, this);
this.listenTo(this.collection, 'add', this.renderList);
},
onModelSelected: function(model){
this.collection.add(model);
}
renderList: function(){
// Draw your collection
}
// Do some clean up of events
remove: function(){
Backbone.Events.off('modelSelected', this.onModelSelected, this);
Backbone.View.prototype.remove.call(this);
}
});
// Modal View
var ViewB = Backbone.View.extend({
initialize: function(){
this.collection = new YourCollection();
this.listenTo(this.collection, 'sync', this.renderCollection);
this.collection.fetch();
},
render: function(){
// Render your modal
return this;
},
renderCollection: function(){
// Render your collection
},
//
modalSubmit: function(){
var model = this.getSelectedModel();
Backbone.Events.trigger('modelSelected', modal);
this.close();
}
});
I should note that this comes with a few caveats. Firstly, I would recommend picking a robust and namespace event naming scheme. This is just an example. Secondly, fetching from the initialize function is often not ideal, but I'm leaving out the concept of controllers for brevity.

collection add event firing on view initialisation

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

Backbonejs Events for all collection items or Events per ItemView

What approach would be more efficient?
I have a Backbone.Collection so i Create a Backbone.View to render this collection. The CollectionView render method:
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item)
container.appendChild view.el
view.render()
$(el).append container
I can use the events in two forms.
1.- Set the events object in CollectionView, so i need to declare the action of select an item in the CollectionView and "rescue" the model that i selected.
CollectionView extends Backbone.View
events:
'click #itemView', 'onSelectItem'
onSelectItem: ->
##Get the model
##Show ItemDetailView
2.- Set the events object per itemView, so the select method don't need to retrieve the model.
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#Show ItemDetailView
Which of this options are better?
EDITED: I create a JSperf snippet http://jsperf.com/backbone-events-on-collectionview-or-per-itemview
JSperf show us that the ItemView approach is more faster, but is this the only metric of importance?
The second approach is much better. Performance aside, the code is much more straightforward. In 6 months, if you had to return to this code, would you think the event would be handled in the ItemView, or the CollectionView? It is a click event on the ItemView, so that's where I would go to look for how it is handled.
Is there a good reason to handle the event in the collection? If you needed the handle the event from the CollectionView, you could delegate the event to the collection. A little redirection, but, to me, this is far clearer. For example:
# The collection view
initialize: ->
#listenTo #, 'selected', #itemSelected
render: ->
container = document.createDocumentFragment()
#collection.each (item) ->
view = new ItemView(item, parent: #)
container.appendChild view.el
view.render()
$(el).append container
itemSelected: (model)->
# Do whatever you need to here, like
# show the ItemDetailView in the container
# The ItemView
ItemView extends Backbone.View
events:
'click #div','onSelect'
onSelect: ->
#options.parent.trigger('selected', #model, #)
#Show ItemDetailView
If I understand you correctly, option 2 seems the most sane. You will be referring to this.model as opposed to digging through the collection to find the model associated with the clicked view.

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.

Managing multiple instances of views in one page apps using Routes in Backbone.js

I am working on a single-page app using Backbone.js. An issue that has occurred to me is that since one is not reloading the page, that when one creates a instance of a View, then I assume, that the View object will remain in memory for the life of the app. This does not seem very efficient to me, since a particular view may no longer be needed if another route is called. However, a particular View may later need to be 'displayed' if one returns to that original route. So the question is, how to best manage views in Backbone with regards to routes?
In my app, many of the views are responsible for displaying a particular 'page' and as such share the same DOM element. When one of these 'page' views is called, it will replace the content in the DOM element previously put in place by the previous view. Thus the previous view is no longer needed.
Do I need to somehow manually destroy the previous View (or is this somehow handled by the Router object)? Or is it better to leave the views once they have been initialized?
Following sample code shows how views instances are being creating in the Router in the app.
/**
* View - List of contacts
*/
var ListContactsView = Backbone.View.extend({
el: '#content',
template: _.template($('#list-contacts-tpl').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection = new Contacts();
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
this.$el.hide();
this.$el.html(this.template({ contacts: this.collection }));
this.$el.fadeIn(500);
}
});
/**
* View - Display single contact
*/
var DisplayContactView = Backbone.View.extend({
el: '#content',
events: {
'click #delete-contact-button': 'deleteContact'
},
template: _.template($('#display-contact-tpl').html()),
initialize: function() {
_.bindAll(this, 'deleteContact', 'render');
// Create reference to event aggregator object.
if (typeof this.options.id === 'undefined') {
throw new Error('View DisplayContactView initialized without _id parameter.');
}
this.model = new Contact({ _id: this.options.id });
// Add parse method since parsing is not done by collection in this
// instance, as this model is not called in the scope of collection
// Contacts.
this.model.parse = function(response) {
return response.data;
};
this.model.bind('change', this.render);
this.model.fetch();
},
deleteContact: function(id) {
// Trigger deleteContact event.
this.eventAggregator.trigger('deleteContact', id);
},
render: function() {
this.$el.html(this.template({ contact: this.model.attributes }));
}
});
/**
* Page routes
*/
var $content = $('#content');
var ClientSideRouter = Backbone.Router.extend({
routes: {
'browse': 'browse',
'browse/view/:id': 'browseViewContact',
'orgs': 'orgs',
'orgs/:orgName': 'orgs',
'orgs/:orgName/:id': 'orgs',
'contact/add': 'addContact',
'contact/view/:id': 'viewContact',
'contact/delete/:id': 'confirmDelete',
'*path': 'defaultPage'
},
addContact: function() {
// Display contact edit form.
var editContactFormView = new EditContactFormView();
// Display email field in edit form.
},
browse: function() {
var listContactsView = new ListContactsView();
},
browseViewContact: function(id) {
var displayContactView = new DisplayContactView({ id: id });
},
defaultPage: function(path) {
$content.html('Default');
},
home: function() {
$content.html('Home');
},
viewContact: function(id) {
$.ajax({
url: '/contact/view/' + id,
dataType: 'html',
success: function(data) {
$content.html(data);
}
});
}
});
var clientSideRouter = new ClientSideRouter();
Backbone.history.start();
Routes do not destroy views
Routes provide you convenient manner to interact with url changes. By convenience i mean url semantics and context of current page. For example url #/!/create/ will invoke a method that should display a form to create a model. Context here is the view to create model.
Views should be managed by the developer
there still does not exists a well known manner to manage views in Backbone.js, but i prefer the way of global variables. This would ensure your view instances are available throughout application and all the modules have access to them. For example doing this
window.App.Contacts.ContactView = new App.Contacts.View.ContactView({model:BenContact}); will make view used to display Ben's contact information available to application modules through window object. All you need to do for any views that use same el is to destroy the ContactView and render the new view.
You have methods on view to remove them
Undelegate Events and Remove methods help you remove them. Inside the callback method that handles routes hash change events. For example in the callback method that handles #/!/view/all ( url to view all the contacts list) you might come across situation where both the views now use the same el so you should destroy the ContactView and render ListView so in the callback do this
App.Contacts.ContactView.undelegateEvents();
App.Contacts.ContactView.remove();
Since Backbone.js has no built in support for view compositions, there are several patterns that you could follow when it comes to keeping track of child views.
Derick Bailey illustrates extending Backbone.View to allow views to
clean up after themselves -
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Another alternative is to add on child views to a property of the
parent view and manually clean them up when the parent view state is
removed.
var ParentView = Backbone.View.extend({
initialize : function(){
this.childViews = [];
},
render: function(){
this.childViews.push(new ChildView);
}
});
A third alternative is to make the child views subscribe to events
that the parent views trigger, so that they can clean up when the
parent view publishes a "close" event.
Also I noticed from your code that you are actually fetching a model within your child view class. Ideally, I would suggest passing the model as a parameter to the constructor as this decouples the view from the data. It's more MVC-ish

Categories

Resources