I have been using Backbone on a new project and so far have loved it, but I have come to a blocker that I can't seem to get around.
Without trying to explain my whole domain model, I have found that when you save a model, the response comes back from the server and gets parsed again, creating new sub objects and therefore breaking and event bindings I had previously put on the object.
For instance, if I save ContentCollection (its a Backbone.Model not a collection) when it comes back from the server, the response gets parsed and creates a new Collection in this.contentItems, which breaks all the binding I had on this.contentItems. Is there any way to get around this? Tell backbone not to parse the response somehow? Grab the bindings off the original list, and then re-attach them to the new list?
App.ContentCollection = Backbone.Model.extend({
urlRoot: '/collection',
initialize: function() {
},
parse: function(resp, xhr) {
this.contentItems = new App.ContentList(resp.items)
this.subscriptions = new App.SubscriptionList(resp.subscriptions)
return resp
},
remove: function(model){
this.contentItems.remove(model)
this.save({'removeContentId':model.attributes.id})
},
setPrimaryContent: function(model){
this.save({'setPrimaryContent':model.attributes.id})
}
})
Has anyone run into this before?
I think the issue here is the way you're using the parse() method. Backbone just expects this method to take a server response and return a hash of attributes - not to change the object in any way. So Backbone calls this.parse() within save(), not expecting there to be any side-effects - but the way you've overridden .parse(), you're changing the model when the function is called.
The way I've dealt with this use case in the past is to initialize the collections when you first call fetch(), something like:
App.ContentCollection = Backbone.Model.extend({
initialize: function() {
this.bind('change', initCollections, this);
},
initCollections: function() {
this.contentItems = new App.ContentList(resp.items);
this.subscriptions = new App.SubscriptionList(resp.subscriptions);
// now you probably want to unbind it,
// so it only gets called once
this.unbind('change', initCollections, this)
},
// etc
});
Related
I have an app where we are using model.fetch() to pull JSON from the server, and we have this as our render function that fires when the model changes:
if(_.isUndefined(this.model.get("id_number"))){
this.template = initialTemplate;
} else if(this.model.get("id_number") == 0) {
this.template = templateA;
} else {
this.template = templateB;
}
return BaseView.prototype.render.call(this);
On pageload, we don't do model.fetch() yet and get the initialTemplate. When a user changes an input, we fetch and get new model data that can have an ID of 0 or something else.
Now there is also a chance the server JSON might change to an empty {} and if so we need to revert to showing the initialTemplate. The problem is it appears that if that's the case, model.fetch() doesn't return anything and nothing changes. Same thing if id_number is undefined. (It does work if it's null.)
Is there a solution so Backbone will fetch an empty data set?
The solution for this was something Stephen mentioned: a success function. I added this to the model.fetch() function:
success: function(model, response /*jshint unused: false */) {
if (_.isEmpty(response)) {
view.model.set("id_number", undefined);
}
},...
FYI to anyone who uses this in the future: Backbone won't let you pass only response to the function because it expects model first. You can pass model and not use it, but you'll need the comment above to pass JSHint.
Set defaults in your model
Here is documentation.
http://backbonejs.org/#Model-defaults
I have a controller:
var layout = new LayoutView();
App.holder1.show(layout);
var my_view = new myView({id: options})
layout.holder1.show();
console.log(my_view.model.get('name')) <---- I want this
I want to get my_view.model.get('name') however, the issue is I get undefined. I have console.log the model and it is populated ok, however I think it's because it's not fully loaded yet when I try the get.
This is my current thisView:
var thisView = Backbone.Marionette.ItemView.extend({
initialize: function (options) {
this.model.fetch();
},
model: new myModel(),
template: testExampleTemplate,
});
return thisView;
You'll have the object populated only after the success callback function:
initialize: function (options) {
this.model.fetch({
success: function(model){
console.log(model.get('name'));
};
});
}
Listen for an event. "change" or "reset" will work.
viewInstance.model.on("change", function(){
viewInstance.model.get("nameOfAttribute");
// do something
});
http://backbonejs.org/#Events-catalog
There's a few ways to approach this. First, you could listen for a change event from the model in the view, and do whatever it is you need when the change event fires. If you need to do something no matter what, you have a couple of options: you could write an implementation for you model's parse method that fires an event your view listens for and does something in response, or you can do something in the success callback for the fetch method itself (passed as an option to fetch). I can provide an example if I understand better which approach makes sense for your situation.
is it possible to pass the questions variable into the view render?
Ive attempted calling this.render inside the success on the fetch however I got an error, presumably it's because this. is not at the correct scope.
app.AppView = Backbone.View.extend({
initialize: function(){
var inputs = new app.Form();
inputs.fetch({
success: function() {
var questions = inputs.get(0).toJSON().Questions;
app.validate = new Validate(questions);
app.validate.questions();
}, // End Success()
error: function(err){
console.log("Couldn't GET the service " + err);
}
}); // End Input.fetch()
this.render();
}, // End Initialize
render: function(){
el: $('#finder')
var template = _.template( $("#form_template").html(), {} );
this.$el.html(template);
}
The success callback is called with a different this object than your View instance.
The easiest way to fix it is to add something like this before you call inputs.fetch:
var self = this;
And then inside the success callback:
self.render();
I'm not quite sure what you're trying to achieve, but if your problem is calling render from the success callback, you have two options, Function#bind or assigning a self variable.
For more information about "self" variable, see var self = this? . An example:
var self = this;
inputs.fetch({
success: function () {
self.render();
},
...
});
You should probably do some reading on JavaScript scopes, for example "Effective Javascript" or search the topic ( for example this MDN article ) online to get a better idea what happens there.
For Function#bind(), see the MDN article about it. With Backbone I suggest you use Underscore/LoDash's _.bind instead though, to make sure it works even where Function#bind() is not supported.
As for more high-level concepts, the fetching operation looks like it belongs to the model or router level and instead you should assign the questions variable as the model of your view. Ideally views don't do data processing / fetching, they're just given a model that has the methods necessary to perform any data transformations you might need.
The views shouldn't even need to worry about where the model comes from, this is normally handled by a router in case of single page applications or some initialization code on the page.
I'm working on a toy backbone.js application, a library application to perform CRUD operations on a library. Here is the book model and the library collection (of books)
var Book = Backbone.Model.extend({
url: function() {
return '/api/books' + this.get('id');
}
});
var Library = Backbone.Collection.extend({
model : Book,
url : '/api/books'
});
This seems pretty straightforward. Next, I obviously want to be able to show the book, so I have a BookView...
var BookView = Backbone.View.extend({
tagName: 'li',
render: function() {
this.$el.html(this.model.get('author'));
$('#list-of-books').append(this.$el);
}
});
all the render method does is append an li to the end of an unordered list with an id of list-of-books that is in my html.
Next, unsurprisingly, if I add the following code, I get a list with one item (the name of the author of the book with id=4)
var a_book = new Book();
a_book.url = '/api/books/4';
a_book.fetch({
success: function() {
var bookView = new BookView({ model: a_book });
bookView.render();
}
});
Here's where I don't understand. I add the following code and nothing happens:
var some_books = new Library();
some_books.fetch();
some_books.forEach(function(book) {
alert('why is this function not being run');
var view = new BookView({ model: book });
view.render();
});
For some reason, I can't even get that loop code to run, I don't even see that alert pop out to the screen. Let me know if you understand what's wrong with this code and how I can make the list render properly. Thanks
You are calling .render inside fetch. Hence the execution stops over there itself. For loop wont run after fetch as it has returned already.
.fetch() is an asynchronous call, so you may not have the collection data populated when you are calling .forEach().
You should either call the rendering code in the success callback or (even better) render the view on events (reset,...) on the collection.
BTW, you have a typo in the url() function:
url: function() {
return '/api/books/' + this.get('id');
}
Why are you defining the url function for the Book?, since you use the model attribute in the Library collection, url for the Book will be set correctly by default.
Is there a standard way to deal with non-saveable values in Backbone.
e.g.
MyModel = Backbone.extend(Backbone.Model, {
initialize: function () {
this.set({'inches': this.get('mm') / 25});
}
})
If I call save() on this model it will throw an error as there is no corresponding database field for inches. I can think of a few ways to fix this, but am wondering if there's a tried and tested approach generally best used for this?
At the moment my preferred solution is to extend Backbone's toJSON method and to allow passing of a boolean parameter dontCleanup to allow for it to still return all the model's values (including the non saveable ones) when it's needed e.g. for passing to a template.
I like Peter Lyon's idea. I've thought about that a few times, but never actually put it in place. For all the ways that I have handled this, though, here are my two favorites:
Non-"attribute" values
View Models
Non-Attribute Values
This one is simple: don't store the values you need in the model's standard attributes. Instead, attach it directly to the object:
myModel.someValue = "some value";
The big problem here is that you don't get all of the events associated with calling set on the model. So I tend to wrap this up in a method that does everything for me. For example, a common method I put on models is select to say that this model has been selected:
MyModel = Backbone.Model.extend({
select: function(){
if (!this.selected){
this.selected = true;
this.trigger("change:selected", this, this.selected);
}
}
});
In your case, I'm not sure this would be a good approach. You have data that needs to be calculated based on the values that are in your attributes already.
For that, I tend to use view models.
View models.
The basic idea is that you create a backbone model that is persist-able, as you normally would. But the you come along and create another model that inherits from your original one and adds all the data that you need.
There are a very large number of ways that you can do this. Here's what might be a very simple version:
MyModel = Backbone.Model.Extend({ ... });
MyViewModel = function(model){
var viewModel = Object.create(model);
viewModel.toJSON = function(){
var json = model.toJSON();
json.inches = json.mm / 25;
return json;
};
return viewModel;
});
The big benefit of wrapping this with Object.create is that you now have a prototypal inheritance situation, so all of your standard functionality from the model is still in place. We've just overridden the toJSON method on the view model, so that it returns the JSON object with the inches attribute.
Then in a view that needs this, you would wrap your model in the initialize function:
MyView = Backbone.View.extend({
initialize: function(){
this.model = MyViewModel(this.model);
},
render: function(){
var data = this.model.toJSON(); // returns with inches
}
});
You could call new MyViewModel(this.model) if you want, but that's not going to do anything different, in the end, because we're explicitly returning an object instance from the MyViewModel function.
When your view's render method calls toJSON, you'll get the inches attribute with it.
Of course, there are some potential memory concerns and performance concerns with this implementation, but those can be solved easily with some better code for the view model. This quick and dirty example should get you down the path, though.
I think this should do it. Define your Model defaults as your valid schema and then return only the subset of this.attributes that is valid during toJSON.
var Model = Backbone.Model.extend({
defaults: {
foo: 42,
bar: "bar"
},
toJSON: function () {
var schemaKeys = _.keys(this.defaults);
var allowedAttributes = {};
_.each(this.attributes, function (value, key) {
if (_.include(schemaKeys, key)) {
allowedAttributes[key] = value;
}
return allowedAttributes;
}
});
Note that _.pick would make the code a bit shorter once you have underscore 1.3.3 available. I haven't seen a "tried and tested" convention in my travels through the backbone community, and since backbone leaves so many options open, sometimes conventions don't emerge, but we'll see what this stackoverflow question yields.
Dealing with non-persisted attributes in Backbone.js has been doing my head in for a while, particularly since I've started using ember/ember-data, which handles the various situations through computed properties, ember-data attributes, or controllers.
Many solutions suggest customising the toJSON method. However, some popular Backbone plugins (particularly those that deal with nested models), implement their own toJSON method, and make a call to Backbone.Model.prototype.toJSON to obtain an object representation of a model's attributes. So by overwriting the toJSON method in a model definition, you'll lose some (potentially crucial) features of those plugins.
The best I've come up with is to include an excludeFromJSON array of keys in the model definition, and overwrite the toJSON method on Backbone.Model.prototype itself:
Backbone.Model.prototype.toJSON = function() {
var json = _.clone(this.attributes),
excludeFromJSON = this.excludeFromJSON;
if(excludeFromJSON) {
_.each(excludeFromJSON, function(key) {
delete json[key];
});
}
return json;
};
MyModel = Backbone.Model.extend({
excludeFromJSON: [
'inches'
]
});
In this way, you'll only have to define the non-persisted keys (if you forget to do so, you'll soon be reminded when your server throws an error!). toJSON will behave as normal if no excludeFromJSON property is present.
In your case, inches is a computed property, derived from mm, so it makes sense to implement this as a method on your model (ensuring that the value for inches is correct when mm is changed):
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
However, this has the downside of being accessed differently to everyother attribute. Ideally you'll want to keep it consistent with accessing other attributes. This can be achieved by extending the default get method:
var getMixin = {
get: function(attr) {
if(typeof this[attr] == 'function') {
return this[attr]();
}
return Backbone.Model.prototype.get.call(this, attr);
}
};
MyModel = Backbone.Model.extend({
inches: function() {
return this.get('mm') / 25;
}
});
_.extend(MyModel.prototype, getMixin);
Which will let you do:
new MyModel().get('inches');
This approach doesn't touch the underlying attributes hash, meaning that inches will not appear in the toJSON representation, unless you set the value of inches later on, in which case you'll need something like the excludeFromJSON array.
If you have the need to set the inches value, you may also want to listen for changes and adjust the value of mm:
MyModel = Backbone.Model.extend({
initialize: function() {
this.on('change:inches', this.changeInches, this);
},
inches: function() {
return this.get('mm') / 25;
},
changeInches: function() {
this.set('mm', this.attributes.inches * 25);
}
});
_.extend(MyModel.prototype, getMixin);
See the complete example on JSBin.
It's also worth noting that the (official?) purpose of the toJSON method has recently been redefined as preparing a model for syncing with a server. For this reason, calling toJSON should always return only the "persistable" (or "saveable") attributes.