Backbone.js - fetch method does not fire reset event - backbone.js

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.

Related

Backbone filter collection and rendering it

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)

Marionette ItemView not re-rendering on model change from collection view

I have created a collection passing a collection view and a collection. The collection references a model I have created. when fetching the collection the items get rendered succesfully, but when the models change, the itemViews are not being re-rendered as expected.
for example, the itemAdded function in the tweetCollectionView is called twice ( two models are added on fetch ) but even though the parse function returns different properties over time for those models ( I assume this would call either a change event on the collection, or especially a change event on the model, which I have tried to catch in the ItemView ) the itemChanged is never called, and the itemViews are never re-rendered, which i would expect to be done on catching the itemViews model change events.
The code is as follows below:
function TweetModule(){
String.prototype.parseHashtag = function() {
return this.replace(/[#]+[A-Za-z0-9-_]+/g, function(t) {
var tag = t;
return "<span class='hashtag-highlight'>"+tag+"</span>";
});
};
String.prototype.removeLinks = function() {
var urlexp = new RegExp( '(http|ftp|https)://[\w-]+(\.[\w-]+)+([\w.,#?^=%&:/~+#-]*[\w#?^=%&/~+#-])?' );
return this.replace( urlexp, function(u) {
var url = u;
return "";
});
};
var TweetModel = Backbone.Model.extend({
idAttribute: 'id',
parse: function( model ){
var tweet = {},
info = model.data;
tweet.id = info.status.id;
tweet.text = info.status.text.parseHashtag().removeLinks();
tweet.name = info.name;
tweet.image = info.image_url;
tweet.update_time_full = info.status.created_at;
tweet.update_time = moment( tweet.update_time_full ).fromNow();
return tweet;
}
});
var TweetCollection = Backbone.Collection.extend({
model: TweetModel,
url: function () {
return '/tweets/game/1'
}
});
var TweetView = Backbone.Marionette.ItemView.extend({
template: _.template( require('./templates/tweet-view.html') ),
modelEvents:{
"change":"tweetChanged"
},
tweetChanged: function(){
this.render();
}
})
var TweetCollectionView = Marionette.CompositeView.extend({
template: _.template(require('./templates/module-twitter-feed-view.html')),
itemView: TweetView,
itemViewContainer: '#tweet-feed',
collection: new TweetCollection([], {}),
collectionEvents: {
"add": "itemAdded",
"change": "itemChanged"
},
itemAdded: function(){
console.log('Item Added');
},
itemChanged: function(){
console.log("Changed Item!");
}
});
this.startInterval = function(){
this.fetchCollection();
this.interval = setInterval( this.fetchCollection, 5000 );
}.bind(this);
this.fetchCollection = function(){
this.view.collection.fetch();
this.view.render();
}.bind(this);
//build module here
this.view = new TweetCollectionView();
this.startInterval();
};
I may be making assumptions as to Marionette handles event bubbling, but according to the docs, I have not seen anything that would point to this.
Inside your CollectionView, do
this.collection.trigger ('reset')
after model have been added.
This will trigger onRender () method in ItemView to re-render.
I know I'm answering an old question but since it has a decent number of views I thought I'd answer it correctly. The other answer doesn't address the problem with the code and its solution (triggering a reset) will force all the children to re-render which is neither required nor desired.
The problem with OP's code is that change is not a collection event which is why the itemChanged method is never called. The correct event to listen for is update, which according to the Backbone.js catalog of events is a
...single event triggered after any number of models have been added
or removed from a collection.
The question doesn't state the version of Marionette being used but going back to at least version 2.0.0 CollectionView will intelligently re-render on collection add, remove, and reset events. From CollectionView: Automatic Rendering
When the collection for the view is "reset", the view will call render
on itself and re-render the entire collection.
When a model is added to the collection, the collection view will
render that one model in to the collection of child views.
When a model is removed from a collection (or destroyed / deleted),
the collection view will destroy and remove that model's child view
The behavior is the same in v3.

How to properly update subviews on Marionette when using constantly updating JSON

I have a single page app built with Marionette that has a main view with a list of subviews.
The JSON which holds all application data is updated constantly. I've tried to separate region show code so that it will be run just once and not on every render.
Now the render event is fired on every timeout loop even though the JSON is static data and therefore change event should not call render. What is wrong? I assume it has something to do with .set but is there any other way to load the response from an array variable to the subview collection, since fetch will allow only url attribute and will not accept array variable?
This example is an extremely simplified version of the application code to concentrate on this specific problem.
Controller:
var Controller = {
showMainView: function(id){
// create model and fetch data on startup
var mainElement = new mainElement();
var mainElementFetched = mainElement.fetch({url: 'http://json.json'});
// fetch done, create view, show view in region, setTimeout
mainElementFetched.done(function(data){
var mainElementView = mainElementView({model:mainElement});
App.mainRegion.show(mainElementView);
setTimeout(updateJSON, 5000);
}
// timeOut loop to check for JSON changes
var updateJSON = function(){
mainElement.fetch({url: 'http://json.json'});
App.timeOut = setTimeout(updateJSON, 5000);
}
}
}
MainElement Model:
MainElement = Backbone.Model.extend({
parse : function(response){
// parsing code
return response;
}
});
MainElementView (Layout):
MainElementView = Backbone.Marionette.Layout.extend({
template: "#main-template",
initialize:function(){
//create collection for subelements
this.subElementCollection = new SubElementCollection();
//listen to change event, and fire callback only when change in model is detected
this.model.on('change', this.render, this);
},
regions:{
subsection : ".subsection"
},
onShow: function(){
// show subelements in subsection region when mainelementview is shown on screen, but not show on every render
this.subsection.show(new SubElementCompositeView({collection:this.subElementCollection}))
},
onRender : function(){
var response = this.model.response;
// get subelements when change event fires and parse the response
this.subElementCollection.set(response,{parse:true});
}
});
SubElement Model, Collection, ItemView, CompositeView:
SubElement = Backbone.Model.extend({});
SubElementCollection = Backbone.Collection.extend({
model:SubElement,
comparator : function(model){
var price = model.get('price');
return -(price);
},
parse:function(response){
// parsing code to get data to models from differents parts of JSON
return response;
}
});
SubElementItemView = Backbone.Marionette.ItemView.extend({
template: "#subelement-template",
tagName: "tr"
});
SubElementCompositeView = Backbone.Marionette.CompositeView.extend({
template: "#subelements-template",
tagName : "table",
itemView:SubElementItemView,
itemViewContainer : "tbody",
initialize: function(){
this.collection.on('change', this.render, this);
},
appendHtml : function(collectionView,itemView,index){
// SORTING CODE
},
onRender:function(collectionView,itemView,index){
// ADD IRRELEVANT EXTERNAL STUFF TO TEMPLATE AFTER RENDER
}
});
Check out the documentation. It says:
A "change" event will be triggered if the server's state differs from the current attributes.
In your MainElementView where you bind to your model's change event: this.model.on('change', this.render, this);, you are actually saying every time your model changes, call render.
If re-drawing the whole view is too slow, due to the amount of changes. Why don't you make your rendering more fine-grained? For example you could listen for specific change events and just change the DOM elements which need changing:
this.model.on('change:Name', function () {
this.$('[name=Name]').html(this.model.get('Name'));
}, this);
It is more work to set this up, but you could make it a bit cleverer by matching the model property names to your DOM element name or something.

Add model to collection after fetching it

Im having trouble figuring out how to populate a model's attributes from the server and then add the populated model to a collection and have that collection rendered by a view. Here's the code I have:
var movieDetails = new cinephile.Models.MovieDetailsModel({ id: movie.get('id') });
this.collection.add(movieDetails);
Inside of the MovieDetailsModel:
cinephile.Models.MovieDetailsModel = Backbone.Model.extend({
url: function()
{
return '/cinephile/api/index.php?action=getMovieDetails&movieId=' + this.id;
},
initialize: function()
{
this.fetch();
}
});
And this.collection is just a collection with the model set to be a cinephile.Models.MovieDetailsModel
I am listening for items to be added to the collection and when they are, the following is executed:
displayMovie: function(movie)
{
var view = new cinephile.Views.MovieView({
model: movie,
className: 'movie clearfix',
template: JST['app/scripts/templates/MovieView.ejs'],
});
this.$("#my-movies").append(view.el);
},
MovieView looks like this:
cinephile.Views.MovieView = Backbone.View.extend({
initialize: function(options)
{
this.template = options.template;
this.render();
},
render : function()
{
this.$el.html(this.template(this.model.attributes));
return this;
},
});
The problem I have is that the template I'm using is trying to access an attribute of the model that is undefined. Im pretty sure it's undefined because the MoveDetailsModel hasn't finished fetching before the model is added to the collection and subsequently rendered to the view.
How can I solve this issue? I'd like to be able to create a MovieDetailsModel that takes in an id, use that id to get the movie details from the server and then add the populated model to a collection and then render that collection to the screen.
Any help is appreciated.
Backbone fetch returns a jqXHR object, which is a Deferred objects Promise.
When fetch is called, the attributes are not populated yet. Promise objects have a don
ejqXHR function, where a callback can be passed to be executed once the request is done.
I would recommend moving the fetch into another method not the constructor, because there You can return the jqXHR object and access its done function.
Here is an example:
var movieDetails = new cinephile.Models.MovieDetailsModel({ id: movie.get('id') });
var promise = movieDetails.fetch();
promise.done(function() {
var view = new cinephile.Views.MovieView({model: movieDetails});
view.render();
});

Backbone.js - custom collection not working

In the backbone.js, for the purpose of filtering the data, i am fetching by click the element.
and i am saving the collection as newcollection. but i unable to get any data.
what is wrong with my code...
my code :
taskListPhraseI.collection = Backbone.Collection.extend({ // collection fetching
model:taskListPhraseI.model,
url : 'data/data.json',
});
taskListPhraseI.allView = Backbone.View.extend({
el:$('.boardHolder'),
events:{
'click span.green' : 'filterIt'
},
initialize:function(){
var that = this;_.bindAll(this);
this.collection = new taskListPhraseI.collection(); //initial stage i am fetching
this.collection.fetch({success:that.render});
this.on('change:filterType', this.setNewType); //on click trigger my custom method to get the collection again
//this.on('reset:filterType', this.setNewType);
},
setNewType:function(){
var newCollection = new taskListPhraseI.collection(); // my custom collection
newCollection.fetch(); // fetching
this.collection.reset(newCollection,{ silent: true }) // triggering rest
var filterType = this.filterType,
filtered = _.filter(this.collection.models, function (item) {
return item.get("dueDays") === filterType;
});
console.log(newCollection.models); // not working... why?
console.log(this.collection.models);// works
this.collection.reset(filtered);
},
or the way i am doing wrong.. to filter the collection
any one guide me a correct way of process...
thanks in advance
fetch is async. Execute your code after collection will be fetched
newCollection.fetch({context:this}).done(function() {
// your code
})
Plus this is not correct reset method usage:
this.collection.reset(newCollection,{ silent: true })
Use this way:
this.collection.reset(newCollection.toJSON(), {silent:true})
EDIT (Added example)
HTML
<button>change filter</button>
JS
var url1 = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=vpetrychuk&count=9'
var url2 = 'https://api.twitter.com/1/statuses/user_timeline.json?screen_name=derickbailey&count=9'
var collection = new (Backbone.Collection.extend({
url : url1,
resetWithFilter : function(key, value) {
var query = {};
query[key] = value;
this.reset(this.where(query));
}
}));
// fetch initial data
collection.fetch({dataType:'jsonp'});
$(':button').on('click', function() {
// change url and fetch another data
collection.url = url2;
collection.fetch({dataType:'jsonp'}).done(function(response) {
console.log('items count before filter:', collection.length);
// now reset collection with selected filter
collection.resetWithFilter('id_str', '294429621640912896');
console.log('items count after filter:', collection.length)
});
});
Fiddle: http://jsfiddle.net/vpetrychuk/N4ZKm/

Resources