Backbone.js: View with Reference to Model and Collection ... smell? - backbone.js

I have a view for individual Customer models that takes a customer and collection of customers on construction. The requirement of the customer model makes sense -- it is a view for a customer. The requirement of the collection ... I'm not sure if that is a smell or not and would love feedback!
The reason it needs the collection is so that a button in the view can remove the customer from the customers collection and that the view can also listen to collection remove events to see if it has to be unrendered (it was successfully removed from the collection).
var CustomerView = Backbone.View.extend({
events: {
'click button.delete': 'remove'
},
initialize: function() {
_.bindAll(this, 'render', 'unrender', 'remove', 'removed');
this.model.bind('change', this.render);
this.collection.bind('remove', this.removed);
}
// render / unrender removed for brevity
remove: function () {
this.collection.remove(this.model);
},
removed: function (customer) {
if (this.model === customer) {
this.unrender();
}
}
});
Here is how the view is created:
var CustomersView = Backbone.View.extend({
initialize: function () {
_.bindAll(this, 'appendCustomer');
this.model.customers.bind('add', this.appendCustomer);
},
appendCustomer: function (customer) {
var customerView = new CustomerView({
model: customer,
collection: this.model.customers
});
$(this.el).append(customerView.render().el);
}
});
I was thinking I could somehow wire up the CustomerView in this appendCustomer method instead of simply giving the customers collection to the CustomerView wholesale.
Thoughts? Thanks!

If you are adding these models through the collection, you may want to investigate using the model's collection property. This would take the place of the customers association that you plan to give it.
Given the above, you may only need to pass the model into the view. And if you need to access the collection in the template or otherwise, its just 'this.model.collection'.
Also, you can then simply call the 'destroy' method on the model and it will be removed from the collection. You would then use the same event binding strategy you have now except with this built-in collection.

You just have to think in events instead of explicit method calls.
In this example I'm using a custom event.
// simplified code
var CustomerView = Backbone.View.extend({
events: {
'click button.delete': 'remove'
},
initialize: function() {
this.model.bind( "app:remove", this.removed );
},
remove: function () {
this.model.trigger( "app:remove" );
},
removed: function (customer) {
this.unrender();
}
});
// The collection it self removes the model
var CustomersCollection = Backbone.Collection.extend({
initialize: function(){
this.on( "app:remove", this.remove )
}
});

Related

Backbone.js - delete views

This is my second day trying to use backbone and im completely lost. I am following this tutorial - http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-3/
What I have done is loaded a contacts list and rendered it to the screen, but if you look at my render1 function - this takes a form input and appends it to my template. The problem is that I can't delete these items after they are created - the others can be deleted. help please?
var ContactView = Backbone.View.extend({
tagName: "contacts",
className: "contact-container",
template: $("#contactTemplate").html(),
initialize: function(){
this.model.on('change', this.render, this);
this.model.on('add', this.render1, this);
this.model.on('destroy', this.remove, this);
},
events: {
'click .deleteUser': 'delete'
},
test: function () {
alert("here");
},
delete: function () {
this.model.destroy();
},
render: function () {
console.log(this);
var tmpl = _.template(this.template);
$(this.el).html(tmpl(this.model.toJSON()));
temp = tmpl(this.model.toJSON());
console.log(temp);
return this;
},
render1: function () {
console.log(this);
var tmpl = _.template(this.template);
temp = tmpl(this.model.toJSON());
temp='<contacts class="contact-container">'+temp+'</contacts>';
console.log(temp);
$("#contacts").append(temp);
$(this.el).html(tmpl(this.model.toJSON()));
return this;
}
});
var AddPerson = Backbone.View.extend({
el: $("#addPerson"),
// el: $("form/"),
events: {
'click': 'submit',
'submit': 'submit'
},
submit: function(e) {
// alert("here");
e.preventDefault();
this.collection = new Directory();
// var data = (contacts[0]);
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
}
});
seasick, there are quite a few issues in this code.
var contact = new Contact(contacts[0]);
var contactView = new ContactView({model: contact});
this.collection.add(contact);
this.collection.add(contactView);
Contact is a Backbone.Model but ContactView is a Backbone.View. Yet, you are adding both to the this.collection (which I assume is a Backbone.Collection of Contact?). See the problem here? In Backbone, there is no such concept of a 'collection of views'. You just get one concept: views, that are tied to a model.
So, here, you create a Contact and you add it to the Collection. That is all! It takes care of the Model part. The rendering part needs to be handled with events and renders.
When you add a model to a collection (this.collection.add(contact)), the collection will trigger a 'add' event, that you can hook to with a .on to create a new ContactView and append it to the DOM somewhere.
So when you write this...
this.model.on('add', this.render1, this);
You are actually saying 'When the Contact model triggers an add event, run render1', which isn't what you want, what you probably want is a collection.on('add', ...). The model will never trigger an add event (well, you could make it trigger one, but it wouldn't be an expected behavior!), the add/remove events are at the collection level.
In other words, you are missing some binding on the collection in your AddPerson view to 'react' to adding a new Contact to the collection. The code of the function bound to the add event should probably look a bit like:
onAdd: function(newContact){
var newContactView = new ContactView({model: newContact});
$("#contacts").append(newContactView.render().el);
}
There are other issues in your code, but I guess an outline of the steps to take would be like:
Remove the binding to add in ContactView: ContactView is only concerned with one contact, not how to manage multiple contacts. This is probably why you are having issues with only some (the first one?) of the contacts 'working'
Move that logic to the AddContact view which seems to be more concerned with the Collection of contacts. Use the collection 'add' event to create new ContactView and append them to the DOM
Hope this helps!

backbone .bind('change', _.bind(this.render, this)) not registering

I have a measure model, that is made up of two collections, a beats collections, and a measureRep[resentation] collection. Each collection is made of beat models and representation models respectively.
Whenever a measureRep[resentation] collection changes (by addition or subtraction of a representation model), I want the measureView (which has the measure model, and therefore the measureRep[resentation] collection) to re-render itself using the render function().
I am adding a new model in another View by the following function:
var representationModel = new RepresentationModel({representationType: newRepType});
StageCollection.get(cid).get('measures').models[0].get('measureRepresentations').add(representationModel);
I can see that before and after the addition that the measure and its measureRep collection are getting added correctly, however, the bind call on the measureView is not registering the change and calling the render function. I even put the bind on the model, to show that the backend is getting updated, however, it doesn't respond. This leads me to believe that the View and the models are decoupled, but that doesn't make sense, since it originally renders from the model. Here are the relevant files:
measureView.js [View]:
define([...], function(...){
return Backbone.View.extend({
initialize: function(options){
if (options) {
for (var key in options) {
this[key] = options[key];
}
this.el = '#measure-container-'+options.parent.cid;
}
window.css = this.collectionOfRepresentations; //I use these to attach it to the window to verify that the are getting updated correctly
window.csd = this.model; // Same
_.bindAll(this, 'render'); // I have optionally included this and it hasn't helped
this.collectionOfRepresentations.bind('change', _.bind(this.render, this));
this.render();
},
render: function(){
// Make a template for the measure and append the MeasureTemplate
var measureTemplateParameters = { ... };
var compiledMeasureTemplate = _.template( MeasureTemplate, measureTemplateParameters );
// If we are adding a rep, clear the current reps, then add the template
$(this.el).html('');
$(this.el).append( compiledMeasureTemplate )
// for each rep in the measuresCollection
_.each(this.collectionOfRepresentations.models, function(rep, repIndex) {
var measureRepViewParamaters = { ... };
new MeasureRepView(measureRepViewParamaters);
}, this);
return this;
},
...
});
});
measure.js [Model]:
define([ ... ], function(_, Backbone, BeatsCollection, RepresentationsCollection) {
var MeasureModel = Backbone.Model.extend({
defaults: {
beats: BeatsCollection,
measureRepresentations: RepresentationsCollection
},
initialize: function(){
var logg = function() { console.log('changed'); };
this.measureRepresentations.bind('change', logg);
this.bind('change', logg);
}
});
return MeasureModel;
});
representations.js [Collection]:
define([ ... ], function($, _, Backbone, RepresentationModel){
var RepresentationsCollection = Backbone.Collection.extend({
model: RepresentationModel,
initialize: function(){
}
});
return RepresentationsCollection;
});
I have also tried registering the bind on the measure model, and not its child collection, but neither work.
_.bindAll(this, 'render');
this.model.bind('change', _.bind(this.render, this));
see: https://stackoverflow.com/a/8175141/1449799
In order to detect additions of models to a collection, you need to listen for the add event (not the change event, which will fire when a model in the collection is changed http://documentcloud.github.io/backbone/#Events-catalog ).
so try:
this.measureRepresentations.bind('add', logg);

Bind a view to collections in backbone.js

I'm semi-new to backbone. I'm trying to bind a collection to a view so that when a new model is added to a collection, the view is updated. I think when you do this with models you can bind to the model's change event. But how do you do the same with collections?
App.Views.Hotels = Backbone.View.extend({
tagName: 'ul',
render: function() {
this.collection.each(this.addOne, this);
var floorplanView = new App.Views.Floorplans({collection:floorplanCollection});
$('.floorplans').html(floorplanView.render().el);
return this;
},
events: {'click': 'addfloorplan'},
addOne: function(hotel) {
var hotelView = new App.Views.Hotel ({model:hotel});
this.$el.append(hotelView.render().el);
},
addfloorplan: function() {
floorplanCollection.add({"name": "another floorplan"});
}
});
App.Collections.Floorplans = Backbone.Collection.extend({
model: App.Models.Floorplan,
initialize: function () {
this.bind( "add", function() {console.log("added");} );
}
});
The click event fires and adds to the collection. But how do I get it to update the view?
You can listen to the collection's add event, which fires when a new item is added to the collection. In modern versions of Backbone, the method listenTo is preferred to bind or on for listening to events. (Read de documentation for more info)
For example, in your case this should do the trick:
App.Views.Hotels = Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection,'add', this.addOne);
},
//rest of code
Hope this helps!
Here is a nice tutorial I had followed long ago.
An Intro to Backbone.js: Part 3 – Binding a Collection to a View
It helps you define a DonutCollectionView that will, when given a collection of donuts, render an UpdatingDonutView for each donut.

Backbone.js need help connecting view events with other views

I'm building a simple app that implements a "faceting" or "filtering" interface -- no data writing. I want users to select facets (checkboxes) on the left to reduce the result set on the right.
I've got 2 separate collections (facets and incentive). These are being populated from a single json object that contains an array comprised of 2 items. Each 2 items help create the model, view, and collection for these two parts of the page.
I need to get events in the facet view to update the result of the incentives collection. What's the best way to connect these two together. My test app can be found here. I am using Handlebars for templating.
see it here: DEMO SITE
The Main content area is made up of "car incentive" like this.
//define Incentive model
var Incentive = Backbone.Model.extend({
defaults: {
photo: "car.png"
}
});
//define individual incentive view
var IncentiveView = Backbone.View.extend({
tagName: "article",
className: "incentive-container",
template: Handlebars.compile($("#incentive-template").html()),
initialize: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//define Incentives collection
var Incentives = Backbone.Collection.extend({
model: Incentive,
url: 'data/incentives3.json',
parse: function(response) {
return response.incentiveHolder;
}
});
var IncentivesView = Backbone.View.extend({
el: $(".incentives-container"),
initialize: function() {
this.collection = new Incentives();
// When the contents of the collection are set, render the view.
this.collection.on('reset', function() {
incentives = this.collection.models;
this.render();
}, this);
this.collection.fetch();
this.collection.on("reset", this.render, this);
},
render: function() {
this.$el.find("article")
.remove();
_.each(this.collection.models, function(item) {
this.renderIncentive(item);
}, this);
},
renderIncentive: function(item) {
var incentiveView = new IncentiveView({
model: item
});
this.$el.append(incentiveView.render()
.el);
}
});
The facet model/collection is detailed below. You can see in FacetView -- I've registered a "change" event when a user selects a checkbox. I need to some how use this data to either 1) hide each incentive that does not contain this facet value or 2) remove the model from the FacetCollection (??). I want users to be able to quickly toggle these checkboxes on/off a bunch -- is removing/adding slower (or less efficient) then hiding/showing with css?
Also, you can see from my displayIncentives() function that "this" -- is an instance of the view -- but how do I access the "value" of the checkbox that was just clicked?
If I can get this value -- then I could examine the incentive collection, iterate over each item -- and ask it it's "incentive" array "contains" this value of the checkbox that was just click. If return false, I would hide (or remove?) the item and proceed.
// ======================================
// FACETS
// ======================================
//define Facet model
var Facet = Backbone.Model.extend({});
//define individual facet view
var FacetView = Backbone.View.extend({
tagName: "div",
className: "facet-group",
template: Handlebars.compile($("#facets-template").html()),
initialize: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
events: {
"change input[type=checkbox]": "displayIncentives"
},
displayIncentives: function() {
console.log(this);
}
});
//define Facets collection
var Facets = Backbone.Collection.extend({
model: Facet,
url: 'data/incentives3.json',
parse: function(response) {
return response.facetsHolder;
}
});
var FacetsView = Backbone.View.extend({
el: $(".facets-container"),
initialize: function() {
this.collection = new Facets();
// When the contents of the collection are set, render the view.
this.collection.on('reset', function() {
this.render();
}, this);
this.collection.fetch();
},
render: function() {
_.each(this.collection.models, function(item) {
this.renderFacet(item);
}, this);
},
renderFacet: function(item) {
var facetView = new FacetView({
model: item
});
this.$el.append(facetView.render().el);
}
});
Finally -- my json object is located here ... I have the freedom at the moment to help design how this object is structure in the next phase of this project. Any suggestions greatly appreciated.
json object
What a question! Actually many questions in one!
I can tell you the logic I'm using to perform exactly the same task, and which actually works (see here)
You basically must have 2 collections: a collection of facets, and a collection of items.
Collection of facets
At the beginning it is empty (no facets selected). When you check one, then you add it to the facet collection. When adding a facet, you filter the collection of items in some way (you can trigger an event listend by the collection of items or - better - just call a method like itemCollection.facet_filter() where facet_filter() is a method defined by you in the item collection object).
Good.
Collection of item
It's an ordinary collection. You just add a facet_filter() method which filters the list and trigger a reset event. This reset event is listened by FacetsView which re-renders the list.
the facet_filter()method should be something like this.
filter_results: function(facets){
var filteredColletion= new Backbone.Collection(this.models);
_.each(facets,function(facet){
filteredColletion.where({facet.key: facet.value});
});
this.reset(returnList);
}
}
This is the logic I use and it works.

Backbone custom event trigger not being recognised?

I'm learning Backbone.js for the first time and I'm having an issue trying to get a custom event from triggering (or from the View from recognising when it's been triggered)?
You can see my Collection code here: https://github.com/Integralist/Backbone-Playground/blob/master/Assets/Scripts/App/main.js#L72-86 which when initialized it triggers a custom collection:init event.
var Contacts = Backbone.Collection.extend({
model: Contact,
initialize: function(){
this.trigger('collection:init');
this.bind('add', this.model_added, this);
},
model_added: function(){
console.log('A new model has been created so trigger an event for the View to update the <select> menu');
}
});
But later on in my View where I'm listening for that event I can't get the function populate to fire: https://github.com/Integralist/Backbone-Playground/blob/master/Assets/Scripts/App/main.js#L90-107
var ContactsView = Backbone.View.extend({
initialize: function(){
console.log(contacts.models, 'get initial model data and populate the select menu?');
},
events: {
'collection:init': 'populate',
'change select': 'displaySelected'
},
populate: function(){
console.log('populate the <select> with initial Model data');
},
displaySelected: function (event) {
console.log('get model data and display selected user', event);
}
});
Any ideas what I'm doing wrong?
The events hash in a view is used to bind events from the DOM to your view, e.g. events raised by the elements in your rendered view. To listen to events raised by your collection, you will have to set them manually:
var ContactsView = Backbone.View.extend({
initialize: function(){
contacts.on("collection:init",this.populate,this);
}
...
});
Note that you are using a global contacts variable, I would advise to use Backbone mechanisms and pass your collection to the constructor, as you do with the el:
var ContactsView = Backbone.View.extend({
initialize: function(){
console.log(this.collection.models);
this.collection.on("collection:init",this.populate,this);
}
...
});
var contacts_view = new ContactsView({
el: $('#view-contacts'),
collection:contacts
});
As #mu said in the comments, as is, your event won't do anything since you trigger it in the initialize method of the collection, which is automatically called by the constructor of the collection therefore before you can bind anything in the view. See this Fiddle to visualize the call order : http://jsfiddle.net/yRuCN/
Trigger it elsewhere, or, if I read correctly your intent, you (probably) want to use the built in reset event:
var ContactsView = Backbone.View.extend({
initialize: function(){
this.collection.on("reset",this.populate,this);
}
...
});
See http://jsfiddle.net/yRuCN/1/ for an example with potential uses.

Resources