Backbone.js need help connecting view events with other views - backbone.js

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.

Related

Backbone pre rendering of collection models

I want to perform an action, clearing parent element, after a collection has fetched his models but prior to the models rendering.
I've stumbled upon before and after render methods yet they are model specific, which will cause my parent element to clear before every model rendering.
I'm able of course to perform the action pre-fetching yet I want it to occur when fetch is done and before models are rendered.
I tried using reset and change events listening on the collection yet both resulted unwanted end result.
Reset event seamed to go in that direction yet the passed argument was the entire collection and not a single model from the collection, therefore using the add event callback wasn't possible due to difference in argument type (collection and not a model as required)
Any ideas how to invoke a callback when fetch a collection fetch is successful yet models are yet to be rendered?
The model contains the returned attributes while collection contains url for fetching and parse method to return argument wrapped object.
Below is the code I use to render the collection view, which is basically rendering each model's view within the collection.
Collection View
---------------
var FoosView = Backbone.View.extend({
el: '#plans',
events: {
//'click tr': 'rowClick'
},
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
_.bindAll(this, "render");
this.render();
},
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
...
});
The model view
--------
var FooView = Backbone.View.extend({
tagName: 'li',
initialize: function(options) {
this.options = options || {};
this.tpl = _.template(fooTpl);
},
render: function() {
var data = this.model.toJSON();
this.$el.html(this.tpl(data));
return this;
}
});
Thanks in advance.
OK, I think I understand your question and here is a proposed solution. You are now listening to the reset event on your collection and calling this.renderAll. this.renderAll will take the list of models from the collection and render them to the page, but only AFTER the list element has been emptied. Hope this helps :).
var FoosView = Backbone.View.extend({
el: '#plans',
collection: yourCollection, // A reference to the collection.
initialize: function() {
this.listenTo(this.collection, 'add', this.renderNew);
this.listenTo(this.collection, 'reset', this.renderAll);
},
renderAll: function() {
// Empty your list.
this.$el.empty();
var _views = []; // Create a list for you subviews
// Create your subviews with the models in this.collection.
this.collection.each(function(model) {
_views.push(new FooView({model: model});
});
// Now create a document fragment so as to not reflow the page for every subview.
var container = document.createDocumentFragment();
// Append your subviews to the container.
_.each(_views, function(subview) {
container.appendChild(subview.render().el);
});
// Append the container to your list.
this.$el.append(container);
},
// renderNew will only run on the collections 'add' event.
renderNew: function(FooModel) {
var item = new FooView({model: FooModel});
this.$el.prepend(item.render().$el);
}
});
I am forced to assume a few things about you html, but I think the above code should be enough to get you up and running. Let me know if it works.
I'm not totally sure about what you are asking but have you tried:
MyCollection.fetch({
success: function(models,response) {
//do stuff here
}
});
Also you may be interested taking a look at http://backbonejs.org/#Model-parse
Hope it helps!
Edit: there is no direct link between fetching and rendering my bet is that you binded rendering to model change.
USE===============>>>> http://backbonejs.org/#Model-parse

How to track the number of models in a collection and stay in sync with the server changes Backbone.js using AMD

Backbone.js newbie here.
General question: What is the best practice to track the number of models in a collection in order to display it on the UI? My use cases can involve changes on the server side so each time the collection is sync'd I need to be able to update the UI to the correct number from storage.
I'm using Backbone.js v1.0.0 and Underscore v1.4.4 from the amdjs project and Require.js v2.1.6.
Specific example: Simple shopping cart showing "number of items in the cart" that continually updates while the user is adding/removing items. In this example I'm almost there but (1) my code is always one below the real number of models and (2) I feel that there is a much better way to do this!
Here's my newbie code.
First, I have a collection of items that the user can add to their cart with a button. (NOTE: all AMD defines and returns are removed in code examples for brevity.)
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
//other rendering stuff here
..............
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
that.renderPackages(item);
});
}
renderPackages: function(item){
var packageView = new PackageView({
model: item
});
this.$el.append(packageView.render().el);
},
Next I have the view for each individual item in the cart PackageView which is called by the PackagesView code above. I have a "add to cart" button for each Package that has a "click" event tied to it.
var PackageView = Backbone.View.extend({
tagName:"div",
template:$(packageTemplate).html(),
events: {
"click .addToCart": "addToCart"
},
render:function () {
var tmpl = _.template(this.template);
this.$el.html(tmpl(this.model.toJSON()));
return this;
},
addToCart:function(){
cartView = new CartView();
cartView.collection.create(new CartItemModel(this.model));
}
Finally, I have a CartView that has a collection of all the items in the cart. I tried adding a listenTo method to react to changes to the collection, but it didn't stay in sync with the server either.
var CartView = Backbone.View.extend({
el: $("#page"),
initialize:function(){
this.collection = new CartCollection();
this.collection.fetch({
reset: true
});
this.listenTo(this.collection, 'add', this.updateCartBanner);
this.collection.on("reset", this.render, this);
},
render: function(){
$('#cartCount').html(this.collection.length);
},
updateCartBanner: function(){
//things did not work here. Just putting this here to show something I tried.
}
End result of specific example: The .create works correctly, PUT request sent, server adds the data to the database, "reset" event is called. However, the render() function in CartView does not show the right # of models in the collection. The first time I click a "add to cart" button the $('#cartCount') element does not get populated. Then anytime after that it does get populated but I'm minus 1 from the actual count on the server. I believe this is because I have a .create and a .fetch and the .fetch is happening before the .create finishes so I'm always 1 behind the server.
End result, I'm not structuring this the right way. Any hints in the right direction would be helpful!
You can try like this:
collection.on("add remove reset sync", renderCallback)
where renderCallback is function which refresh your UI.
Found an answer to my question, but could definitely be a better method.
If I change my code so instead of a separate view for each model in the collection as I have above, I have one view that iterates over all the models and draws then it will work. I still need to call a .create followed by a .fetch with some unexpected behavior, but the end result is correct.
Note that in this code I've completely done away with the previous PackageView and everything is drawn by PackagesView now.
var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
"click .addToCart": "addToCart"
},
initialize: function(id) {
this.collection = new PackagesCollection([],{id: id.id});
this.collection.fetch({
reset: true
});
this.collection.on("reset", this.render, this);
},
render: function(){
var that = this;
var tmpl = _.template($(packageTemplate).html());
//loop through models in collection and render each one
_.each(this.collection.models, function(item){
$(that.el).append(tmpl(item.toJSON()));
});
},
addToCart:function(e){
var id= $(e.currentTarget).data("id");
var item = this.collection.get(id);
var cartCollection = new CartCollection();
var cartItem = new CartItemModel();
cartCollection.create(new CartItemModel(item), {
wait: true,
success: function() {
console.log("in success create");
console.log(cartCollection.length);
},
error:function() {
console.log("in error create");
console.log(cartCollection.length);
}
});
cartCollection.fetch({
wait: true,
success: function() {
console.log("in success fetch");
console.log(cartCollection.length);
$('#cartCount').html(cartCollection.length);
},
error:function() {
console.log("in error fetch");
console.log(cartCollection.length);
}
});
Result: The $('#cartCount') in the .fetch callback injects the correction number of models. Unexpectedly, along with the correct .html() value the Chrome console.log return is (server side had zero models in the database to start with):
in error create PackagesView.js:88
0 PackagesView.js:89
in success fetch PackagesView.js:97
1
And I'm getting a 200 response from the create, so it should be "success" for both callbacks. I would have thought that the Backbone callback syntax for create and fetch were the same. Oh well, it seems to work.
Any feedback on this method is appreciated! Probably a better way to do this.
Incidentally this goes against the general advice here, although I do have a "very simple list" so perhaps its OK in the long run.

What is better way to store and access list of views in backbone.js?

I've been using backbone for quite some time now, and each time I get dynamic lists of views that have their own events and behaviors, I wonder how should they be stored. I've two ways and my thoughts on them are..
Store views internally in another view. This requires overhead in proper filtering, but is sort-of independent from DOM + might have better memory usage
Just generate views, put them in DOM and trigger events of views with jquery, like $('#someviewid').trigger('somecustomfunction'); - easier to write and access but dependencies are harder to see and I'm not certain that view/model gets deleted if I just remove DOM node
What would you recommend?
So here is expanded second example, where new views are just appended to internal html and storyViews themselves are forgotten. But If I want to access specific view from this list, I would have to use DOM attributes, like id or data and then trigger view functions with jquery accessors
Devclub.Views.StoriesList = Backbone.View.extend({
initialize: function () {
this.collection.bind('reset', this.reset, this);
this.collection.fetch();
},
reset: function (modelList) {
$(this.el).html('');
var me = this;
$.each(modelList.models, function (i, model) {
me.add(model);
});
},
add: function (model) {
var contact_model = new Devclub.Models.Story(model);
var view = new Devclub.Views.Story({
model: contact_model
});
var storyView = view.render().el;
$(this.el).append(storyView);
}
});
In contrast, I could instead store same view list in an array and iterate over it if I want to call some view methods directly
I think you should keep a reference of the child views. Here is an example from the book by Addy Osmani.
Backbone.View.prototype.close = function() {
if (this.onClose) {
this.onClose();
}
this.remove(); };
NewView = Backbone.View.extend({
initialize: function() {
this.childViews = [];
},
renderChildren: function(item) {
var itemView = new NewChildView({ model: item });
$(this.el).prepend(itemView.render());
this.childViews.push(itemView);
},
onClose: function() {
_(this.childViews).each(function(view) {
view.close();
});
} });
NewChildView = Backbone.View.extend({
tagName: 'li',
render: function() {
} });

Backbone view event atacched to all views

I'm doing my first application in backbone and i get a strange thing happening trying to attach an event.
I got this code so far:
//View for #girl, EDIT action
GirlEditView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
console.log(attr);
this.render();
},
render: function() {
var template = _.template( $("#girl_edit").html(), this.variables );
$(this.el).html( template );
$("#edit_girl").modal('show');
}
});
//View for #girl
GirlView = Backbone.View.extend({
initialize: function(el, attr) {
this.variables = attr;
this.render();
},
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
},
events: {
"click p.modify": "modify"
},
modify: function() {
//calls to modify view
new GirlEditView({el : $("#edit_girl")}, this.variables);
}
});
//One girl from the list
Girl = Backbone.Model.extend({
initialize: function() {
this.view = new GirlView({el : $("#content")}, this.attributes );
}
});
//all the girls
Girls = Backbone.Collection.extend({
model: Girl,
});
//do magic!
$(document).ready(function() {
//Underscore template modification
_.templateSettings = {
escape : /\{\[([\s\S]+?)\]\}/g,
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
}
//get initial data and fill the index
var list = [];
$.getJSON('girls.json', function(data) {
list = [];
$.each(data, function(key, val) {
list.push( new Girl(val) );
});
var myGirls = new Girls(list);
console.log( myGirls.models);
});
});
As you can see.
I'm using a collection to store all the girls and the data comes from a REST api in ruby.
Each girls create a new model instance and inside i attached a view instance.
I don't know if it's a good practice but i can't think a better way to do it.
Each view makes a content with a unique id. girl-1 girl-2 and go on.
Now, the template have a edit button.
My original idea is to attack the onclick event and trigger the edit view to get rendered.
That is working as expected.
The proble so far is:
When the events triggers, all the collection (girls) fire the edit view, not the one that "owns" the rendered view.
My question is what i'm doing wrong?
Thanks a lot
All the edit-views come up because all the GirlViews are using the same el:
this.view = new GirlView({el : $("#content")}, this.attributes );
and then you render be appending more HTML:
render: function() {
var template = _.template( $("#girl_template").html(), this.variables );
$(this.el).html( $(this.el).html() + template );
}
Backbone events are bound using delegate on the view's el. So, if multiple views share the same el, you'll have multiple delegates attached to the same DOM element and your events will be a mess of infighting.
You have things a little backwards: models do not own views, views watch models and collections and respond to their events. You'll see this right in the documentation:
constructor / initialize new View([options])
[...] There are several special options that, if passed, will be attached directly to the view: model, collection, [...]
Generally, you create a collection, c, and then create the view by handing it that collection:
var v = new View({ collection: c })
or you create a model, m, and then create a view wrapped around that model:
var v = new View({ model: m })
Then the view binds to events on the collection or model so that it can update its display as the underlying data changes. The view also acts as a controller in Backbone and forwards user actions to the model or collection.
Your initialization should look more like this:
$.getJSON('girls.json', function(data) {
$.each(data, function(key, val) {
list.push(new Girl(val));
});
var myGirls = new Girls(list);
var v = new GirlsView({ collection: myGirls });
});
and then GirlsView would spin through the collection and create separate GirlViews for each model:
var _this = this;
this.collection.each(function(girl) {
var v = new GirlView({ model: girl });
_this.$el.append(v.render().el);
});
Then, GirlView would render like this:
// This could go in initialize() if you're not certain that the
// DOM will be ready when the view is created.
template: _.template($('#girl_template').html()),
render: function() {
this.$el.html(this.template(this.model.toJSON());
return this;
}
The result is that each per-model view will have its own distinct el to localize the events. This also makes adding and removing a GirlView quite easy as everything is nicely wrapped up in its own el.

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

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 )
}
});

Resources