Backbone filter collection and rendering it - backbone.js

I'm still quite new to backbone, so I'm sorry if there is any gross error in what I'm doing.
What I'm trying to do seems very simple: getting a collection of models from the db and do some filters on it. Let's say we are trying to filter hotels. Once I have my main collection, I would like to filter them for price, stars, and so on (pretty much what you can find in yelp or tripadvisor or so) - and of course, I want to "reset" the filters once the user uncheck the different checkboxes.
So far, I have 3 views:
- one view based on the panel where the results will be displayed
- one view based on a template that represents each item (each hotel)
- one view will all the filters I want to use.
The problem I am having is that I am bot able to keep my collection in such a state that I am able to revert my filters or to refresh the view with the new collection.
Can you please help me to understand where my problem is? And what should I do?
<script>
// model
var HotelModel = Backbone.Model.extend({});
// model view
var ItemView = Backbone.View.extend({
tagName : 'div',
className : 'col-sm-4 col-lg-4 col-md-4',
template : _.template($('#hotelItemTemplate').html()),
initialize : function() {
this.model.bind('change', this.render, this);
this.model.bind('remove', this.remove, this);
},
render : function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
//view list
var HotelListView = Backbone.View.extend({
el : '#paginated-content',
events : {
"scroll" : "fetch"
},
initialize : function(options) {
var items = this.collection;
items.on('add', this.add, this);
items.on('all', this.render, this);
},
add : function(item) {
var view = new ItemView({
model : item
});
$('#paginated-content').append(view.render().el);
},
fetch : function() {
this.collection.getNextPage();
}
});
// filterign menu
var FilteringView = Backbone.View.extend({
el : '#filtering',
// just examples of one of the filters that user can pick
events : {
'click #price_less_100 ' : 'updateValue',
'click #five_stars ' : 'updateStars',
},
template : _.template($('#filteringTemplate').html()),
initialize : function() {
this.collection.on('reset', this.render, this);
this.collection.on('sync', this.render, this);
},
render : function() {
this.$el.html(this.template);
return this;
},
updateValue : function(e) {
//this is something I'm not using at the moment but that it contains a copy of the collection with the filters
var filtered = new FilteredCollection(coursesPaginated);
// if is checked
if (e.currentTarget.checked) {
var max = 100;
var filtered2 = _.filter(this.collection.models, function(item) {
return item.get("price") < max;
});
//not used at the moment
//filtered.filterBy('price', function(item) {
// return item.get('price') < 100;
//});
//this does not work
//this.collection.reset(filtered2);
//so I do this
this.collection.set(filtered2);
} else{
// here, i would like to have something to put the collection in a state before this filter was applied
//something that I do not use at the moment
//filtered.removeFilter('price');
}
},
updateStars : function(e) {
//do something later
}
});
// collection
var HotelCollection = Backbone.PageableCollection.extend({
model : HotelModel,
// Enable infinite paging
mode : "server",
url : '{{url("/api/hotels")}}',
// Initial pagination states
state : {
pageSize : 15,
sortKey : "updated",
order : 1
},
// You can remap the query parameters from `state` keys from
// the default to those your server supports
queryParams : {
totalPages : null,
totalRecords : null,
sortKey : "sort"
},
parse : function(response) {
$('#hotels-area').spin(false);
this.totalRecords = response.total;
this.totalPages = Math.ceil(response.total / this.perPage);
return response.data;
}
});
$(function() {
hotelsPaginated = new HotelCollection();
var c = new HotelListView({
collection : hotelsPaginated
});
$("#paginated-content").append(c.render().el);
hotelsPaginated.fetch();
});
It seems to me that it is not so easy to do filtering like this using backbone. If someone has other suggestion,please do.
Thank you!

My solution for this:
Main Collection which fetched from server periodically.
Filtered Collection which resets each time you use filtering.
Main view which used to render filtered collection (example new MainView({collection: filteredCollection};)) Also there must be
handler for collection 'reset' event.
Filter view which have a model containing filter values and which triggers Filtered Collection reset with new values.
Everithing easy.
Sorry for no code examples, not on work)

Related

Backbone.js - fetch method does not fire reset event

I'm beginning with Backbone.js and trying to build my first sample app - shopping list.
My problem is when I fetch collection of items, reset event isn't probably fired, so my render method isn't called.
Model:
Item = Backbone.Model.extend({
urlRoot : '/api/items',
defaults : {
id : null,
title : null,
quantity : 0,
quantityType : null,
enabled : true
}
});
Collection:
ShoppingList = Backbone.Collection.extend({
model : Item,
url : '/api/items'
});
List view:
ShoppingListView = Backbone.View.extend({
el : jQuery('#shopping-list'),
initialize : function () {
this.listenTo(this.model, 'reset', this.render);
},
render : function (event) {
// console.log('THIS IS NEVER EXECUTED');
var self = this;
_.each(this.model.models, function (item) {
var itemView = new ShoppingListItemView({
model : item
});
jQuery(self.el).append(itemView.render().el);
});
return this;
}
});
List item view:
ShoppingListItemView = Backbone.View.extend({
tagName : 'li',
template : _.template(jQuery('#shopping-list-item').html()), // set template for item
render : function (event) {
jQuery(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
Router:
var AppRouter = Backbone.Router.extend({
routes : {
'' : 'show'
},
show : function () {
this.shoppingList = new ShoppingList();
this.shoppingListView = new ShoppingListView({
model : this.shoppingList
});
this.shoppingList.fetch(); // fetch collection from server
}
});
Application start:
var app = new AppRouter();
Backbone.history.start();
After page load, collection of items is correctly fetched from server but render method of ShoppingListView is never called. What I am doing wrong?
Here's your problem:
" When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true}" Backbone Docs
So, you want to fire the fetch with the reset option:
this.shoppingList.fetch({reset:true}); // fetch collection from server
As an aside, you can define a collection attribute on a view:
this.shoppingList = new ShoppingList();
this.shoppingListView = new ShoppingListView({
collection : this.shoppingList // instead of model: this.shoppingList
});
Are you using Backbone 1.0? If not, ignore this, otherwise, you may find what the doc says about the Collection#fetch method interesting.
To quote the changelog:
"Renamed Collection's "update" to set, for parallelism with the similar model.set(), and contrast with reset. It's now the default updating mechanism after a fetch. If you'd like to continue using "reset", pass {reset: true}"
So basically, you're not making a reset here but an update, therefore no reset event is fired.

Change event is not getting trigger when updating model in backbone js

I have a view called layerPanel that is using screenData model. Now on model.set i get update event from model itself, but its not working on view.
MODEL
var screenData = Backbone.Model.extend({
initialize : function() {
_.bindAll(this,"update");
this.bind('change:vdata', this.update);
},
update: function() {
var obj = this.vdata;
alert("update");
},
vdata:[{id : 0, title : "Welcome to Persieve 0"}]
});
VIEW
var layerPanel = Backbone.View.extend({
el: "#allLayers",
model: new screenData(),
initialize: function() {
this.render();
this.model.bind('change:vdata', this.render);
},
render: function() {
this.template = _.template(LayersTpl, {splitData: this.model.vdata});
this.$el.html(this.template);
return this;
}
});
Here is how I set values in Model.
screendata = new screenData;
var obj = screendata.vdata;
obj[obj.length] = {id : $(".bullet").length, title : "Welcome to Persieve"};
var tempData = [];
for ( var index=0; index<obj.length; index++ ) {
if ( obj[index]) {
tempData.push( obj );
}
}
obj = tempData;
screendata.set({vdata:[obj]});
The event should fire. But your render wont work as the 'this' context needs setting.
try:
this.model.bind('change:vdata', this.render, this);
or even better, use listenTo and the context is implicit (+ you can clean up easily this.remove())
Edit. From the edit you made above, I can see that you are creating a new screendata instance. The binding you created is for a different instance model: new screenData() .
You must reference the binded object and set it if you want the event to trigger.
If all the model setting happens in the actual model. Call this.set({vdata:[obj]});

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 - Filtering a collection and resetting view

I'm using Backbone.js with jquery and rails. I have a model Player and collection Players. On init I load all Players to view with PlayersView.
I would like to add the ability to filter players by Position and re-render the PlayersView accordingly. I have given it a shot but currently it just reloads all Players and doesn't filter. Please let me know if/how I can revise! Thanks.
/* Models/Collections */
var Player = Backbone.Model.extend({
url: function() {
return this.id ? '/projects/' + this.id : '/projects';
},
initialize: function(){
},
});
var Players = Backbone.Collection.extend({
model: Player,
url: '/players.json',
sortPosition : function(position){
return _(this.filter(function(data) {
return data.get("position") == position;
}));
},
});
and
// view // players //
var PlayersView = Backbone.View.extend({
el: '#players',
events: {
'click .position': 'sort',
},
initialize: function(){
this.bind("reset", this, 'render', 'sort'); // remember: every function that uses 'this' as the current object should be in here
this.render();
},
render: function(){
$('#players').show();
players.each(function(player){
firstname = player.get('first_name');
lastname = player.get('last_name');
position = player.get('position');
team = player.get('team');
$('#players-list').append('<li><span class="player-position">' + position + '</span><span class="player-name">' + firstname + ' ' + lastname + '</span><span class="player-team">' + team + '</span></li>')
});
return this;
},
sort: function(e){
var position = $(e.currentTarget).attr('data-pos');
this.render(players.sortPosition(position));
},
});
It seems like my problem is that i'm calling 'render' again which takes all players and doesn't accept a filtered set. Let me know if i can help with more code!
Thanks much.
i have no time to fully debug your code,
since there is also a few pieces (declaration) missing.
but i already see a few things that can be improved,
1) you pass players.sortPosition(position) into the render function, but your render function does not accept any arguments.
i think it is better to do them separately,
replace your view's collection by the sorted collection, and invoke render again:
sort: function(e) {
this.collection = players.sortPosition(position);
this.render();
}
2) you have a weird construction of the eventbinding in your initialize function
i've never seen it like this:
initialize: function(){
this.bind("reset", this, 'render', 'sort'); // remember: every function that uses 'this' as the current object should be in here
this.render();
},
what exactly is your intention?, if you just want to use this within the bound function,
you can use _.bindAll(this, "function1", function2", ... , "render"); if you want to bind to a certain event like "reset" i would write: this.bind('reset', this.render, this);
I know this is post is a bit old, but you can use Backbone's comparator to sort the collection.
var Players = Backbone.Collection.extend({
model: Player,
url: '/players.json',
initialize : function(){
_.bindAll(this);
this.fetch();
},
comparator: function(player){
return player.get('position')
}
});
This way, whenever you create an instance of this collection, the models in the collection are sorted by position automatically.

backbone.js - accessing a model from a click event

I have a BoardView containing a CellCollection of CellModels. I fetch the collection from the db and then create the CellViews.
This all works swimmingly until I try to access a CellModel via a click event on the BoardView. I can't get to the underlying models at all... only the views. Is there a way to do this?
I've attempted to include the relevant code below:
CellModel = Backbone.Model.extend({});
CellCollection = Backbone.Collection.extend({
model : CellModel
});
CellView = Backbone.View.extend({
className : 'cell',
});
BoardView = Backbone.View.extend({
this.model.cells = new CellCollection();
render : function() {
this.cellList = this.$('.cells');
return this;
},
allCells : function(cells) {
this.cellList.html('');
this.model.cells.each(this.addCell);
return this;
},
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
},
events : {
'click .cell' : 'analyzeCellClick',
},
analyzeCellClick : function(e) {
// ?????????
}
});
I need the click to "happen" on the BoardView, not the CellView, because it involves board-specific logic.
Good question! I think the best solution would be to implement an
EventBus aka EventDispatcher
to coordinate all events among the different areas of your application.
Going that route seems clean, loosely coupled, easy to implement, extendable and it is actually suggested by the backbone documentation, see Backbone Docs
Please also read more on the topic here and here because (even though I tried hard) my own explanation seems kind of mediocre to me.
Five step explanation:
Create an EventBus in your main or somewhere else as a util and include/require it
var dispatcher = _.clone(Backbone.Events); // or _.extends
Add one or more callback hanlder(s) to it
dispatcher.CELL_CLICK = 'cellClicked'
Add a trigger to the Eventlistener of your childView (here: the CellView)
dispatcher.trigger(dispatcher.CELL_CLICK , this.model);
Add a Listener to the Initialize function of your parentView (here: the BoardView)
eventBus.on(eventBus.CARD_CLICK, this.cardClick);
Define the corresponding Callback within of your parentView (and add it to your _.bindAll)
cellClicked: function(model) {
// do what you want with your data here
console.log(model.get('someFnOrAttribute')
}
I can think of at least two approaches you might use here:
Pass the BoardView to the CellView at initialization, and then handle the event in the CellView:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function(opts) {
this.parent = opts.parent
},
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
// pass the relevant CellModel to the BoardView
this.parent.analyzeCellClick(this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell,
parent : this
}).render();
this.cellList.append(view.el);
},
analyzeCellClick : function(cell) {
// do something with cell
}
});
This would work, but I prefer to not have views call each other's methods, as it makes them more tightly coupled.
Attach the CellModel id to the DOM when you render it:
var CellView = Backbone.View.extend({
className : 'cell',
render: function() {
$(this.el).data('cellId', this.model.id)
// I assume you're doing other render stuff here as well
}
});
var BoardView = Backbone.View.extend({
// ...
analyzeCellClick : function(evt) {
var cellId = $(evt.target).data('cellId'),
cell = this.model.cells.get(cellId);
// do something with cell
}
});
This is probably a little cleaner, in that it avoids the tight coupling mentioned above, but I think either way would work.
I would let the CellView handle the click event, but it will just trigger a Backbone event:
var CellView = Backbone.View.extend({
className : 'cell',
initialize: function() {
_.bindAll(this, 'analyzeCellClick');
}
events : {
'click' : 'analyzeCellClick',
},
analyzeCellClick : function() {
this.trigger('cellClicked', this.model);
}
});
var BoardView = Backbone.View.extend({
// ...
addCell : function(cell) {
var view = new Views.CellView({
model : cell
}).render();
this.cellList.append(view.el);
view.bind('cellClicked', function(cell) {
this.analyzeCellClick(cell);
};
},
analyzeCellClick : function(cell) {
// do something with cell
}
});

Resources