I am using this.collection.each() to iterate through the collection fetched from the backend.
Problem: I notice that when I bind the reset event of the collection to the render method of the view in the initialize method and place a console.log() within this.collection.each, I see the console output as expected.
However, If I dont do the binding above, and simply use this.render() within initialize, the console.log() does not output anything. This seems really strange to me, can anyone provide an explaination?
I also placed a console.log(this.collection); just before the loop, and this always outputs the collection correctly! I was guessing that the collection has not been populated on initialization of the View, but that will cause console.log(this.collection); to not show anything.
This Works
SimilarPhotoListView = Backbone.View.extend({
el: '#modal_similar_items',
initialize: function() {
this.collection.on('reset', this.render, this);
},
render: function() {
console.log(this.collection);
this.collection.each(function(photo, index) {
console.log('hello');
}, this);
return this;
}
});
This does not output from within this.collection.each()
SimilarPhotoListView = Backbone.View.extend({
el: '#modal_similar_items',
initialize: function() {
this.render();
},
render: function() {
console.log(this.collection);
this.collection.each(function(photo, index) {
console.log('hello');
}, this);
return this;
}
});
Both classes are instantiated via:
renderSimilarPosts: function() {
this.similarPhotoList = new SimilarPhotoCollection();
this.similarPhotoListView = new SimilarPhotoListView({ collection: this.similarPhotoList });
this.similarPhotoList.fetch({
data: {post_id: this.model.id},
processData: true
});
}
When you initialize your view, this.similarPhotoList is an empty collection. Therefore, when you create your similarPhotoListView, you're passing it an empty collection. similarPhotoListView.initialize calls render thus with an empty collection, all before the collection is populated by fetch.
The reason the first method works is because reset is triggered in collection.fetch. From the backbone source:
fetch:
...
options.success = function(resp, status, xhr) {
collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);
if (success) success(collection, resp);
};
...
initialize runs on instantiation, therefore you run render before you even pass in your collection. Additionally, render should not be called directly from initialize
Related
Here is my backbone model constructor
define([], function(){
return Backbone.Model.extend({
urlRoot:'/dummy-api/Instances',
defaults:{
name:null,
read:false,
write:false
},
initialize: function () {
this.fetch();
console.log("after init "+this.get("id")+" name="+this.get("name"));
}
})
});
and at /dummy-api/Instances/1 is have put this
{"id":1,"name":"bangladesh"}
And I have attached this model to a view with this
define(['models/instance.js'], function(Model){
View = Backbone.View.extend({
initialize: function() {
this.model = new Model({
id:1
});
});
return new View();
});
The URL is getting called, and it's content is as above, I can see that in firebug, but "name" isnt getting set.
I know I can provide a parse function, which as I am using sequelize-restful-extended I may need to do, but I'd first like backbone to read and set from a fixed file. The doco is straight forward enough, what I have should work, so am I doing something else bad ?
You're logging the values before the model.fetch has completed.
Set a callback instead to log the values after fetch has successfully completed, and it should work as expected.
define([], function(){
return Backbone.Model.extend({
urlRoot:'/dummy-api/Instances',
defaults:{
name:null,
read:false,
write:false
},
initialize: function () {
this.fetch({
success: function() {
console.log("after init "+this.get("id")+" name="+this.get("name"));
}.bind(this)
});
}
})
});
This is necessary because this.fetch() executes an XMLHttpRequest asynchronously, and continues on to the next line of code while that request is executed by your browser in a separate "thread" (for all intents and purposes).
I have a backbone view that has a single click event to update a collection. In my console, I can see the object being updated and the number of models being returned is changing, however the view stays static after the event is fired for the first time and any second attempt to fire it gives me the error TypeError: text is undefined. For reference, I have my script loading at the bottom of the page and the template (using underscore) is above it.
Here's my view
app.MyView = Backbone.View.extend({
el: 'body',
events: {
'click #submit': 'fetchData'
},
initialize: function() {
this.collection = new app.MyCollection();
// On my first attempt, I tried having a the render tied to the sync event when the view initiated, although the sync event doesn't actually occur until something is clicked
// this.collection.on('sync', this.render, this);
},
render: function() {
console.log('rendering');
var schedule = $('#schedule');
var template = _.template($('#times-template').html());
schedule.html(template({ collection: this.collection.toJSON() }));
},
fetchData: function() {
that = this;
stationQuery.station_start = $('#start').val();
stationQuery.station_end = $('#end').val();
var query = stationQuery;
this.collection.fetch({
data: query,
success: function(collection, response) {
console.log('fetching data');
console.log(collection);
// attempt #2 - moving the sync event to the success callback of fetch doesnt allow the view to update a second time either
// collection.on('sync', that.render, that);
},
error: function(collection, response) {
console.log('error on fetch', response);
}
});
},
});
app.myView = new app.MyView;
// Third attempt, this time checking if listenTo will allow the view to update every time the event is fired. It still only works on the first time and fails to render for consecutive clicks, even though the console is showing updated models
app.myView.listenTo(app.myView.collection, 'sync', app.myView.render);
Below is a working code, I just added initial call to fetch data at the end
app.myView.fetchData();
And uncommented your .on('sync', ... inside initialize
this.collection.on('sync', this.render, this);
I tested it with jsbin so you can see what's was wrong for you. As a result I see initial rendering of fetched data and clicking the #submit will re-fetch and re-render view.
app.MyView = Backbone.View.extend({
el: 'body',
events: {
'click #submit': 'fetchData'
},
initialize: function() {
this.collection = new app.MyCollection();
// Here is a binding to each fetch data and on success render view
this.collection.on('sync', this.render, this);
},
render: function() {
console.log('rendering');
var schedule = $('#schedule');
var template = _.template($('#times-template').html());
schedule.html(template({ collection: this.collection.toJSON() }));
},
fetchData: function() {
that = this;
var stationQuery = {};
stationQuery.station_start = $('#start').val();
stationQuery.station_end = $('#end').val();
var query = stationQuery;
this.collection.fetch({
data: query,
success: function(collection, response) {
console.log('fetching data');
},
error: function(collection, response) {
console.log('error on fetch', response);
}
});
},
});
app.myView = new app.MyView;
// Here is first call to fetch data and on success render view
app.myView.fetchData();
I have a JSON file, that I need to parse it into collection and render it to HTML pageand then I need to add a button, that will sort this collection and redraw it on page.
That the code, that I made:
That's the part about model, collection and sorting:
var Profile = Backbone.Model.extend();
var ProfileList = Backbone.Collection.extend({
model: Profile,
url: 'profiles.json',
selectedStrategy: "count",
comparator: function (property){
return selectedStrategy.apply(model.get(property));
},
strategies: {
count: function (model) {return model.get("count");},
name: function (model) {return model.get("name");}
},
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
},
initialize: function () {
this.changeSort("count");
},
});
It's the View and the Button:
var ProfileView = Backbone.View.extend({
el: "body",
template: _.template($('#profileTemplate').html()),
Sort: null,
initialize: function() {
this.Sort = new ReSortView();
this.bind('all', this.render());
},
render: function() {
_.each(this.model.models, function(profile){
var profileTemplate = this.template(profile.toJSON());
$(this.el).append(profileTemplate);
}, this);
return this;
},
ReSort: function (){
console.log("111");
this.model.changeSort("name");
},
events: {
"click .Sort": "ReSort",
//"click.NSort": "NSort"
},
});
var ReSortView = Backbone.View.extend({
el: $("#Sort")
});
var AppView = Backbone.View.extend({
el: "body",
initialize: function() {
var profiles = new ProfileList();
var profilesView = new ProfileView({
model: profiles
});
profiles.bind('all', function () {
profilesView.render();
});
profiles.fetch({success: function (model,resp) { console.log(resp);}});
}
});
var App = new AppView();
});
The question is why when I run it, everything seems to be ok, but the sorting does't work, and FireBug saying nothing and Button just writing into the consol.
P.S. I'm new in WEB developing and exactly in JS\Backbone.js
Just changing the comparator:
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
}
won't re-sort the collection, the collection has no way of know that the comparator has changed unless you tell it. You need to force the issue by calling sort:
changeSort: function (sortProperty) {
this.comparator = this.strategies[sortProperty];
this.sort();
}
And a few other things while I'm here:
Your initial comparator:
comparator: function (property){
return selectedStrategy.apply(model.get(property));
}
is invalid (unless you have a global selectedStrategy defined somewhere), you should probably just leave it out and let initialize set it up by calling changeSort.
this.bind('all', this.render()); does nothing useful, bind wants a function as the second argument but this.render() calls the render method. You probably don't want a this.bind call there at all and if you do, you'd want to say this.bind('all', this.render).
Views handle the collection option similarly to how the handle the model option in their constructor:
There are several special options that, if passed, will be attached directly to the view: model, collection, el, id, className, tagName and attributes.
so, if your view is collection-based, you'd want to say new View({ collection: ... }) and use this.collection instead of this.model to avoid confusion.
Collections have various Underscore functions built-in so don't say:
_.each(this.model.models, ...
when you can say this instead:
this.collection.each(...
View's have a jQuery wrapped version of el built in so you can use this.$el instead of $(this.el) (which rebuilds the jQuery wrapper each time you call it).
You are calling the changeSort method on the model but that method is on your collection (as it should be)
I found an example of some Backbone.js code that I then adopted to my needs.
The render function of CommentListView is called before any content is fetched. It seems that it not called again when there are content to render.
The backend returns two results, so that is not the problem.
// Models
window.Comment = Backbone.Model.extend();
window.CommentCollection = Backbone.Collection.extend({
model:Comment,
url:"/api/comments/cosmopolitan"
});
// Views
window.CommentListView = Backbone.View.extend({
tagName:'ul',
initialize:function () {
this.model.bind("reset", this.render, this);
},
render:function (eventName) {
console.log(this.model.models);
_.each(this.model.models, function (comment) {
console.log(comment);
$(this.el).append(new CommentListItemView({model:comment}).render().el);
}, this);
return this;
}
});
window.CommentListItemView = Backbone.View.extend({
tagName:"li",
template:_.template($('#tpl-comment-list-item').html()),
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
});
// Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.commentList.fetch();
$('#sidebar').html(this.commentListView.render().el);
}
});
var app = new AppRouter();
Backbone.history.start();
The behavior of fetch has changed a bit in Backbone 1.0.0. From 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}.
And Collection#fetch says:
fetch collection.fetch([options])
Fetch the default set of models for this collection from the server, setting them on the collection when they arrive. [...] When the model data returns from the server, it uses set to (intelligently) merge the fetched models, unless you pass {reset: true},
Your initialize just binds to "reset":
this.model.bind("reset", this.render, this);
You can either bind to the "add", "remove", and "change" events that Collection#set will generate or you can explicitly ask for a "reset" event when you fetch:
this.commentList.fetch({ reset: true });
A couple other things while I'm here:
Since your CommentListView view is using a collection rather than a model, you might want to call it collection:
this.commentListView = new CommentListView({collection: this.commentList});
and then refer to this.collection inside the view. See View#initialize for details on how view constructors handle their arguments.
Collections have various Underscore methods mixed in so you can say this.collection.each(function(model) { ... }) instead of _.each(this.model.models, ...).
Views maintain a cached version of the jQuery-wrapped el in $el so you can say this.$el instead of $(this.el).
Be careful with things like console.log(this.model.models). The console usually grabs a live reference so what shows up in the console will be the state of this.model.models when you look rather than when console.log is called. Using console.log(this.model.toJSON()) is more reliable when faced with timing and AJAX issues.
You might want to switch to listenTo instead of bind (AKA on) as that is less susceptible to memory leaks.
Usually is use to create a listener for the fetch, when fetch is complete and change the model or collection there is a callback. try this:
var AppRouter = Backbone.Router.extend({
routes:{
"":"list"
},
list:function () {
this.commentList = new CommentCollection();
this.commentListView = new CommentListView({model:this.commentList});
this.listenTo(this.commentList,'change', this.makeRender);
this.commentList.fetch();
},
makeRender: function(){
$('#sidebar').html(this.commentListView.render().el);
}
});
I am using Backbone.js and trying to populate my model using fetch(). The problem I am having is that the returned data is not populating my model. I have found a similar question here. The difference is that inside of my success function I am not seeing any data changes nor is a 'change' event being fired.
The code:
Model
window.Company = Backbone.Model.extend({
urlRoot: "/api/company",
defaults:{
"id":null,
"name":"",
"address":"",
"city":"",
"state":"",
"phone":""
},
events: {
'change': 'doChange'
},
doChange: function(event) {
alert('company changed');
}
})
The Router
var AppRouter = Backbone.Router.extend({
routes:{
"":"home",
"company/:id":"companyDetails"
},
initialize:function () {
var user = new User();
this.headerView = new HeaderView({
model: user
});
$('.header').html(this.headerView.el);
console.log("router initialized.");
},
companyDetails: function (id) {
var company = new Company({
id: id
});
company.fetch({
success: function(){
console.log('company.id is ' + company.id);
console.log('company.name is ' + company.name);
console.log('company.address is ' + company.address);
$("#content").html(new CompanyView({
model: company
}).el);
}
});
}
});
JSON
{"address":"555 Main St","name":"Confused Technologies","id":"8dc206cc-1524-4623-a6cd-97c185a76392","state":"CO","city":"Denver","zip":"80206","phone":"5551212"}
The name and address are always undefined. I have to be overlooking something simple???
Edit
Including the view that erroneously left out passing the model to the template.
View
window.CompanyView = Backbone.View.extend({
initialize:function () {
this.render();
console.log('CompanyView initialized');
},
render:function (eventName) {
$(this.el).html(this.template());
return this;
}
})
The attributes are not stored directly on the model. They are stored in an attributes hash, so you would access them through company.attributes, though company.get(attribute) is the way it's usually done. Along the same lines, you would pass company.toJSON() to your template function, as that returns a cloned hash of the model's attributes.
As for your change event not firing, I assume you mean the change: doChange in the model's events hash. Backbone Models do not actually do anything with an events hash. That's for delegating DOM events on Backbone Views. I bet if you put company.on("change", function (model) { console.log(model.toJSON()); }) before your fetch call and removed the success callback, you'd see your model in the console.
Also, I don't think your $("#content").html... line is going to work like you expect. I'd rewrite your router callback like this:
companyDetails: function (id) {
var company = new CompanyView({
el: "#content",
model: new Company({ id: id })
});
// This line would be better in your view's initialize, replacing company with this.
company.listenTo(company.model, "change", company.render);
company.model.fetch();
}
CompanyView#render would typically pass this.model.toJSON() to a template function that returns html, and pass that to this.$el.html(). So something like this.$el.html(this.template(this.model.toJSON()));
OK. The problem with not updating my model was as far as I can tell an async issue. I updated the success callback to include the data parameter like so:
success: function (data) {
$('#content').html(new CompanyView({
model: data
}).el);
}
Note that I am not passing the company object as the model rather the raw returned data. This solved my model problem.
I mentioned in a comment that this started with my underscore template variables `<%= name %>' etc... being empty. I changed my view to this:
window.CompanyView = Backbone.View.extend({
initialize:function () {
this.render();
console.log('CompanyView initialized');
},
render:function (eventName) {
$(this.el).html(this.template(this.model.toJSON()));
return this;
}
})
Those to things got both my model updated and variables propagating to the template.