how to unset an _id backbone model attribute? - javascript

I would like to unset() the _id attribute from an instance of a model, to make a POST request using the save() model method.
But i get a Uncaught TypeError: Object [object Object] has no method 'call' backbone-min.js because of this line:
myModel.unset('_id');
I am using idAttribute: "_id" so i tried:
myModel.unset('id');
But it doesn't unset the _id attribute.

Using model.unset('_id') should work fine. My guess is that the error is thrown by a change event listener, either in your code or some library code. In order to not trigger events you can use the silent:true option.
However, if you simply want to force the model.save() method to perform a POST, you don't need to unset the _id attribute.
Instead override the model.isNew method. Backbone uses this to determine whether a model is new (and should be POSTed) or existing (and should be PUT). Overriding the method to always return true will make your model to be POSTed every time:
isNew: function() { return true; }

Backbone stores the attributes in an object called attributes within the model. The attribute _id, although representative of the ID of that model is not what is used to determine whether a model is new.
There is a property called id (sibling of attributes) which is what is used to make the isNew() determination.
If you want to force a POST, you'll need to delete the id property:
var id = model.id;
model.unset('_id');
delete model.id;
model.save(); // this will do a POST

Related

backbone.stickit and html-form: How to save (patch) only changed attributes?

tl;dr
How to use backbone.stickit with a html form to change an existing model fetched from the server and only PATCH the changed attributes (changed by user input within the html form) to the server?
/tl;dr
I'm using backbone.stickit in a backbone.js application to bind a model to a HTML-form which is part of a backbone view. This works fine so far, but it becomes a little bit complicated if I'm going to save the bound model. This is because I want to use the PATCH-method and only send the changed attributes to the server. I try to illustrate what I've done so far:
Fetching the model from Server
user = new User(); //instatiate a new user-model
user.fetch(); //fetching the model from the server
console.log(user.changedAttributes()); // Returns ALL attributes, because model was empty
The last line indicates my problem, because I thought I can used the changedAtrributes() method later to get the attributes which need a patch on the server. So I tried this workaround which I found here
user.fetch({
success: function (model, response, options) {
model.set({});
}
});
user.changedAtrributes(); //Returns now "false"
Do stickit-bindings
Now I render my view and call the stickit() method on the view, to do the bindings:
//Bindings specified in the view:
[...]
bindings: {
"#username" : "username"
"#age" : "age"
}
[...]
//within the render method of the view
this.stickit();
The bindings work fine and my user model gets updated, but changedAttributes() remain empty all the time.
Save the model to the server
If the user has made all required changes, the model should be saved to the server. I want to use the PATCH method and only send the changed attributes to the server.
user.save(null, {patch:true}); //PATCH method is used but ALL attributes are sent to the server
OR
user.save(user.changedAttributes(),{patch : true});
With the second approach there are different outcomes:
if I didn't use the user.set({}) woraround, all attributes get PATCHED to the server
if I use the user.set({}) woraround the return value of changedAttributes() is "false" and all attributes are PUT to the server
if I call a user.set("age","123") before calling save(), then only the age attribute is PATCHED to the server
So outcome 3 is my desired behaviour, but there are 2 problems with this: First stickit doesn't seem to use the set() method on the model to update the attributes if they are changed within the html-form. And second, if you call set() with one attribute and afterwards with another, only the second attributes is returned by changedAttributes().
Maybe I just overseen something in the backbone or backbone.stickit docs, so I didn't get the desired behaviour working. Any ideas about that?
NOTE: As found out the problem wasn't directly related to backbone.stickit, more to backbone itself.
Solved this problem on my own, maybe this helps someone who may stumble upon this question:
Backbone only keep track of unchanged attributes, but not of unsaved attributes. So with
model.changedAttributes();
you will only get the attributes of the model, which was changed since the last
model.set("some_attribute","some_value")
Finally I stumbled upon backbone.trackit which is a backbone.js plugin maintained by the creator of backbone.stickit. With this plugin you can track unsaved attributes (all attributes which have changed since the last model.save()) and then use them in the save-method of a model. Example (my usecase):
Backbone.View.extend({
bindings: {
"#name" : "name",
"#age" : "age"
},
initialize: function () {
this.model = new User();
this.model.fetch({
success: function (model, response, options) {
//this tells backbone.stickit to track unsaved attributes
model.startTracking();
}
});
},
render: function () {
this.$el.html(tmpl);
this.stickit();
return this;
},
onSaveUserToServer: function () {
//first argument: only unsaved attributes, second argument: tell backbone to PATCH
this.model.save(this.model.unsavedAttributes(), { patch: true });
});
});

Backbone: Adding a model to a collection from a collection view?

I have some code where I want a NoteCollectionView to add a new Note to the NoteCollection. This is triggered by a function newNote in the NoteCollectionView:
newNote: function(data) {
var note = new Note(data);
this.collection.add(note);
},
I'm still very new to backbone, and I want to make sure this syncs with the server. The concerns I have are:
1) Will simply adding this note to the collection trigger a save() from the server, and update the model with the ID that the server gives it? Or,
2) If the server does not update my model and give me an actual ID, how do I save the model with note.save() and get back an ID from the server?
To address your first question, no, .add will not trigger any kind of call to the server; it will only add a model to a collection.
However, you do have a couple options. One would be to create the new note model, save it to the database, and then add it to the collection:
newNote: function(data) {
var note = new Note(data);
note.save();
this.collection.add(note);
}
The second option is to simply use Backbone's collection.create method. Give it a hash of attributes and it will
Create the model
Save it to the database
Add it to the collection
All in one fell swoop, like so:
newNote: function(data) {
return this.collection.create(data);
}
collection.create also returns the newly created model, illustrated by my return statement above.

How do I tell backbone that the model is not new

I have an object that's also saved in the server and I'm creating a Backbone model from that object.
But when I save the model, it's doing a PUT request, which is not what I want. How to tell Backbone that the data is already in the server without doing a fetch?
Backbone determines the newness of a model by checking if an id is set :
isNew model.isNew()
Has this model been saved to the server yet? If the model does not yet have an id, it is considered to be new.
And when you save a model,
if it is new, a POST request will be emitted,
if it is an update (an id has been set), a PUT request will be sent
Backbone Sync documentation
And as noted by #JayC in the comments :
If there's an issue that the id can't literally be id, you can use idAttribute to say which is the "identity" or key field.
Adding my two cents here, hope it avoids some hair pulling I had to do.
Setting a model's id property directly via constructor to false or null won't do the trick, you have to actually remove it from memory via via delete
For example, I just struggled to copy attributes from one model type to another type as a new model:
copy = Trip.clone()
#doesn't unset the id attribute
schedule = new models.Schedule(_.extend(copy.attributes, {id:null, trip_id:id})
#does unset the id attribute
delete schedule.id
schedule.save null, success: =>
# back from POST vs PUT
...

Backbone.js and localStorage plugin relation between collection and model -- based on official 'todo' example

Here's the official sample app:
http://documentcloud.github.com/backbone/docs/todos.html
I am confused about the relationship between the collection and its property localStorage = new Store(..)
Shouldn't this be in the model because you can't do a collection.save() anyway?
In addition, I tried implementing something like it, and it doesn't work
var Person = Backbone.Model.extend({
defaults: {
name:'no-name',
age:0
}
});
var Persons = Backbone.Collection.extend({
model: Person,
localStorage: new Store('Persons'),
initialize: function(){
console.log('collection initialized');
}
});
window.people = new Persons();
var p1 = new Person({name:'JC',age:24});
p1.save({text:'hello'}); //<--- Uncaught TypeError: Cannot read property 'localStorage' of undefined
Can anyone help me figure this out?
It's actually the .create() function of a collection that allows the collection to "save" to localStorage.
source-code of todo sample:
createOnEnter: function(e) {
var text = this.input.val();
if (!text || e.keyCode != 13) return;
Todos.create({text: text});
this.input.val('');
},
This then allows the model instance to manipulate it using the .save({attr:value}) function.
Calling modelInstance.save() without a defined localStorage property in the model's constructor function will cause the error:
Uncaught TypeError: Cannot read property 'localStorage' of undefined
However, being that the model is now saved in the localStorage through the collectionInstance.create() method, modelInstance.save({attr:value}) can now be used to modify it.
So, in conclusion, Models only has the save() function which allows persistence, but the Collection object has the create() function that allows persistence.
In order to use these, REST urls within the collection and model must be properly set up or the localStorage plugin must be instantiated within the Constructor Function of either (depending on setup)
I had a similar problem in that I wanted to simply 'save' a collection that I had loaded from LocalStorage. I wrote a save() method on my Collections that simply looped through each model and called model.save().
MyCollection.save = ->
#each (model) ->
model.save()
There's a big drawback to this, however, with regards to Backbone.LocalStorage. You loose all the great benefits of using Collection.set({ models... }); to update your collection (pulling in an online update, or something) with all the add/merge/delete goodness. Removing the model from your collection at runtime doesn't delete it from local storage, and manually identifying unmatched models and destroying them somewhat defeats the purpose of Backbone.Collection.set();
One solution I found was to augment Backbone such that Backbone.Collection.set() uses destroy() instead of remove() on the models it finds to be missing. (see line 705 of BB 1.0.0)
Another solution, which I wound up going with, was to have all models listen for their own 'remove' event and call their own 'destroy' method when it happens. This allows Backbone.Collection.set()'s removals to become permanent.
class Model extends Backbone.Model
initialize: ->
#on 'remove', #destroy
Both of these mean you can't "remove" a model without destroying it permanently, which was fine for me. If you want, you could probably create some special conditions under which this occurs, and manage it that way.

Fetching a single Backbone model from server

Say I have a route setup:
'photos/:id' : 'showPhoto'
and somebody shares the url: www.mysite.com/photos/12345 with a friend.
When their friend clicks on the shared link, showPhoto gets called back with 12345 passed as the id. I cant figure out how to fetch the model from the server, because even when setting its id property and calling fetch(), backbone thinks that the model isNew and so the ajax request url is just /photos instead of /photos/12345:
showPhoto: (id) ->
photo = new models.Photo _id:id
photo.fetch #does a GET to /photos, I would have expected it to request /photos/12345
success: () ->
render photo view etc...
Photo = Backbone.Model.extend
idAttribute: '_id'
urlRoot: '/photos'
The model Photo is usually part of a collection, but in this scenario someone is visiting the site directly and only expects to see data for one photo, so the collection is not instantiated in this state of the app.
Is the solution to load the entire collection of photos and then use collection.getById(id)? This just seems way too inefficient when I just want to load the properties for one model.
if you don't have the model as part of a collection, you have to tell the model the full url manually. it won't auto-append the id to the urlRoot that you've specified. you can specify a function as the urlRoot to do this:
Photo = Backbone.Model.extend({
urlRoot: function(){
if (this.isNew()){
return "/photos";
} else {
return "/photos/" + this.id;
}
}
});
Backbone uses the id of the model to determine if it's new or not, so once you set that, this code should work correctly. if it doesn't, you could always check for an id in the if-statement instead of checking isNew.
You do not need to tell backbone whether or not to append the id the url. Per the documentation: http://backbonejs.org/#Model-fetch, you may simply set the urlRoot to the equivalent of the url in a collection.
Backbone will automatically append the desired id to the url, provided you use one of the following methods:
model.set("id", 5); //After initialized
model = new Backbone.Model({id: 5}); //New model
If you manually set the id in the attributes hash or directly on the model, backbone won't be aware of it.
model.id = 5; //Don't do this!
there's already a similar question: "How do I fetch a single model in Backbone?"
my answer there should work for you (and it's in coffeescript)
also remember to check Backbone Model#url documentation, it's all explained there
I would bootstrap the collection (by rendering the following to the page) with just one model in it like this:
photos = new PhotoCollection();
photos.reset([ #Html.ToJson(Model) ]);
Note that the server side code that I use is ASP.Net MVC so use something specific to your server side architecture. Also note that the square brackets are important as they take your singular model and wrap it in an array.
Hope that's helpful.

Categories