I have been trying out backbone.js and have been stymied when I create a new model object then call model.save(). I am expecting the backbone.js default behavior to update the model object with the id from the database but it is not. Is this not supposed to happen? I have verified that I am getting a post with the attributes in json format. My server saves the json to a table and then returns the json with a new id field to backbone.js. Is this correct? Should my server return the entire new object or just the id or what?
//contents of the POST from backbone.js
{ "text":"this is a test" }
//reply from my server
{ id:"15", text:"this is a test" }
My sample code is below
var SQLRow = Backbone.Model.extend({
table:"",
urlRoot:'db',
url:function () {
return "/" + this.urlRoot + "?table=" + this.table +
"&id=" + this.attributes.id;
}
});
var Xtra = SQLRow.extend ({
table:'Xtra'
});
var row = new Xtra({
text: "this is a test"
});
alert(row.url());
row.save()
alert("row:" + row.get("id"));
Tough to tell from your post. Two ideas :
1) the response from the server isn't successful What does your save call return ?
2) Your "id" attribute is named something other than ID. To account for the different name add the following to your model :
idAttribute : "MyModelsID",
EDIT
You're likely facing a timing issue, where the alert fires before the ID has returned. Instead of your last two lines try this :
row.save( null,
{
success : function(model, response) { alert(model.get('id'); }
}
);
ALTERNATIVE
As #mu_is_too_short mentioned, another way is to listen for the change even on the model and respond to the event. (i was just trying to keep the answer as close to your code as possible). But something like the following pseudo code should get you started...
var myView = Backbone.View.extend({
....
initialize : function () {
this.collection.bind('change', this.SOME_LISTENING_FUNC );
}
});
OR, if you're in a collection/view-less world something like this creates a listenr ...
row.on('change', function() { /* do stuff */ }, this);
This answer is based on one comment of Cjolly in the answer above.
It is essential for making the Backbone.Model.save([attributes],[options]) successful in assiging the model with the newly generated model's id from the server, that the server returns the model's id in a JSON string like this { "id" : <the id> }. (note it is "id" and not id).
In essence backbone rightly expects a JSON string and in contrast to how objects may be defined in Javascript without quoted keys, JSON requires the object keys to be quoted (see JSON Spec - does the key have to be surrounded with quotes?)
Since according to Cjolly's comment this has been the essential problem, I want to hightlight this solution in an second answer. Partially because I was hit by the very same problem and only by reading througth the comments I was able to receive the insight.
I've faced the same issue and what I've found is that my validate function of the saved model actually invalidates the model returned from the back end. That's why my fields were not updated properly.
Maybe its a little outtimed, but today I had the same missing id.
It turns out, that the server just sends a Header 'Location' with a redirect containing the new id, but dosen't return the persisted object.
Adding the object to the response was the solution.
It seems, that not returning the object is standard behavier with Roo(Spring) generated Json-Controllers.
Related
I have a backbone model which has Backbone Collections in it. When I save the model and if it is success then my model object is properly structured as it was. But when error occurs (say validation error), in error callback the model object is modified (Collections inside model object are converted into Array). As a result all my functions defined for that Collections are now "undefined" and gives me error.
save : function() {
this.model.save(_.extend(originalModel.toJSON() || {}, this.model
.toJSON()), {
success : this.onSaveSuccess,
error: this.onSaveError,
include : []
});
},
onSaveSuccess : function(model) {
//Here the model is properly structured
},
onSaveError : function(model, response) {
// Here the model is modified, all collections are now array
//I have to explicitly call my parse method to re structure it.
model = model.parse(model.attributes);
}
I would like to know why is this happening. Am I doing something wrong here ?
For the sake of this example, let's assume the attribute of the model that holds the collection is called "people". It isn't clearly documented, but model.save(attributes) actually behaves like:
model.set(attributes);
model.save();
Here's the relevant annotated source of save(...). What your code is doing is first setting the "people" attribute to the array of people, then attempting to save it. When the save fails, your model has the array, not the collection, as the value of "people".
I suspect your end point is returning the full representation of the model on success, and your model is correctly parsing that representation & re-building the Collection at that point. But your error handler won't do that automatically.
As an aside, in my experience Models that contain Collections are hard to manage & reason about. I've had better luck having a Model that contains an array of data, and then having a method on that Model to build a Collection on the fly. Something like:
var MyModel = Backbone.Model.extend({
// ...
getPeople: function() {
// See if we've previously built this collection
if (!this._peopleCollection) {
var people = this.get('people');
this._peopleCollection = new Backbone.Collection(people);
}
return this._peopleCollection;
}
});
This removes the Collection concept from the server communication (where it's pretty unnecessary), while also providing a smarter data layer of your application (smart Models are a good thing).
The solution for this is passing wait:true in options. This will not modify until and unless server returns a valid response.
save : function() {
this.model.save(_.extend(originalModel.toJSON() || {}, this.model
.toJSON()), {
success : this.onSaveSuccess,
error: this.onSaveError,
**wait:true**
include : []
});
},
I've two controllers one is "Upload" which deals with images uploads and other is "Page" whid deals with the creation of pages of CMS now if in my "Upload" controller I load both the models i.e 'image_m' which deals with image upload and "page_m" which deals with the pages creation I've highlighted the relevant code my problem is if I access the variables in the view
$this->data['images'] = $this->image_m->get(); sent by this I can access in foreach loop as "$images->image_title, $images->image_path" etc
But the variable sent by this line ***$this->data['get_with_images'] = $this->page_m->get_no_parents();*** as $get_with_images->page_name, $get_with_images->page_id etc produces given error
A PHP Error was encountered
Severity: Notice
Message: Trying to get property of non-object
Filename: upload/index.php
Line Number: 20
what is the difference between these two access levels one for $image & other for $get_with_images because I can only access its values as $get_with_images
class Upload extends Admin_Controller {
public function __construct() {
parent::__construct();
***$this->load->model('image_m');
$this->load->model('page_m');***
}
public function index($id = NULL) {
//var_dump($this->data['images'] = $this->image_m->get_with_images());
//$this->data['images'] = $this->image_m->get_with_images();
***$this->data['images'] = $this->image_m->get();***
$this->data['subview'] = 'admin/upload/index';
if ($id) {
$this->data['image'] = $this->image_m->get($id);
count($this->data['image']) || $this->data['errors'][] = 'Page Could not be found';
}
$id == NULL || $this->data['image'] = $this->image_m->get($id);
/*this calls the page_m model function to load all the pages from pages table*/
***$this->data['get_with_images'] = $this->page_m->get_no_parents();***
You are not posting all your code so its hard to tell but is it because you used $this-> in the controller, but you haven't done the same thing in the view?
In this case i would recommend not using $this-> because its not necessary. Also its much better to check for errors etc when you call the model so do something like
if ( ! $data['images'] = $this->image_m->get($id) ) {
// Failure -- show an appropriate view for not getting any images
// am showing $data in case you have other values that are getting passed
$this->load->view( 'sadview', $data ); }
else {
// Success -- show a view to display images
$this->load->view( 'awesomeview', $data ); }
so we are saying if nothing came back - the ! is a negative - then show the failure view. Else $data['images'] came back, and it will be passed to the view. note i have not had to use $this-> for anything and it won't be needed in the view.
Would also suggest using separate methods - have one method to show all images and a separate method like returnimage($id) to show an image based on a specific validated $id.
====== Edit
You can access as many models as you want and pass that data to the View. You have a different issue - the problem is that you are waiting until the View to find out - and then it makes it more difficult to figure out what is wrong.
Look at this page and make sure you understand the differences between query results
http://ellislab.com/codeigniter/user-guide/database/results.html
When you have problems like this the first thing to do is make a simple view, and echo out directly from the model method that is giving you problems. Its probably something very simple but you are having to look through so much code that its difficult to discover.
The next thing is that for every method you write, you need to ask yourself 'what if it doesn't return anything?' and then deal with those conditions as part of your code. Always validate any input coming in to your methods (even links) and always have fallbacks for any method connecting to a database.
On your view do a var_dump($get_with_images) The error being given is that you are trying to use/access $get_with_images as an object but it is not an object.
or better yet on your controller do a
echo '<pre>';
var_dump($this->page_m->get_no_parents());
exit();
maybe your model is not returning anything or is returning something but the data is not an object , maybe an array of object that you still need to loop through in some cases.
I'm using a composite key in my model and generate the ID based on my composite key:
app.Assignment = Backbone.Model.extend({
idAttribute : [ 'personId', 'jobId' ],
parse : function(resp) {
resp.id = resp.personId + "_" + resp.jobId;
return resp;
}
});
but Backbone still thinks that all instances of Assignment are new, allthough I'm setting the id in the parse method when fetching them from the API. As a result Backbone does no DELETEs and does a POST instead of PUT on updates. How can I work around this or what is the "right way" to do it?
Update:
Looks like replacing resp.id with this.id solves the issue.
The results of the parse method of a Backbone.Model are passed to the set method, which sets the attributes of the model. The point of confusion for you I think is that the model's ID isn't one of it's attributes; it's one of its properties.
So, what happens is this:
Your raw data comes back from the server and is passed to parse
That same raw data, now augmented with a id attribute, is passed to set
set looks and your idAttribute ([ 'personId', 'jobId' ]) and all of the keys in the raw data
Since none of those keys match the idAttribute, none of them are used as the model's ID, and thus you get your problem.
Your solution of setting this.id inside parse works, but it might cause problems down the road because parse is generally designed to operate on it's input (the raw data), not to modify the model itself; that part is supposed to happen next when set is called. A cleaner solution would instead be to do something like the following:
app.Assignment = Backbone.Model.extend({
// note that no idAttribute is specified, leaving it as the default "id"
parse : function(resp) {
resp.id = resp.personId + "_" + resp.jobId;
return resp;
}
}
Or, if you want a different ID attribute ...
app.Assignment = Backbone.Model.extend({
idAttribute: 'personAndJobId',
parse : function(resp) {
resp.personAndJobId = resp.personId + "_" + resp.jobId;
return resp;
}
}
Aside from the idAttribute issues here, you can always force Backbone to use a certain HTTP method via the type options passed to save().
model.save(null, { type: 'put' })
I've never work with composite ID in Backbone, but I think this could be an easy answer to your problem:
initialize: function() {
this.set("id", this.generateID());
},
generateID = function () {
return this.personId + + "_" + this.jobId;
}
With this code in you Backbone model definition you are creating a unique ID for each model and you shouldn't have problems for update and save it (and you don't need to set any idAttribute).
I know this error has come up a few times, but I'm still not sure how to make this work appropriately..
My magic begins here :
var list_edit_member_view = new app.views.ListMemberEdit({
el: $("#enterprise_member_list_edit_container"),
list_ids: list_ids
});
list_edit_member_view.render();
And this loads this View (ListMemberEdit.js) which has this in the render() :
this.list_edit_member_view = new app.views.CollectionView({
el: $("#enterprise_member_list_edit_container"),
collection: app.peers,
list_item: app.views.ListMemberEditSelection,
list_item_options: {list_ids: this.options.list_ids}
});
Which loads a CollectionView view that renders its list_item_options as model views.. It is within this file (ListMemberEditSelection.js), that when I perform this.destroy, it will return :
Uncaught Error: A "url" property or function must be specified
So this makes me think that the Model or the Model URL is not being defined.. I'm just not sure where to put this since it works very similar to my other partials that are doing roughly the same thing..
Any thoughts? My apologies for the vagueness. Let me know if there's anything else you would like to look at!
I'm curious if its possible to see where this URL attribute would be written within the Object Model or Collection itself.
This is because destroy() function will call Backbone.sync to update the server too, not only your models in the frontend. http://backbonejs.org/#Model-destroy
So, if you're using REST to sync your data, you'll need to set a url property in your model so Backbone know where to send request:
Backbone.Model.extend({
url: "http://myapi.com/"
})
To allow more flexibility, you can also set a urlRoot: http://backbonejs.org/#Model-urlRoot
I had a similar problem, I removed the "id":"" from my models default values and the problem was solved.
I did receive similar error
Try this: I am just making an assumption what your model might look like
window.MyModel = Backbone.Model.extend({
url: function(){
return this.instanceUrl;
},
initialize: function(props){
this.instanceUrl = props.url;
}
}
Please look at this question that I had posted myself for more details: https://stackoverflow.com/a/11700275/405117
I am providing this reference as the answers here helped me better understand
Hope this helps!
I created my web site having 2 types of users: admin and user. So, I created 3 pages mainpag.html, admin.html, user.html. and separate models, views, collections, routers.js files for each of them. After logging in, as I am sending users to separate HTML pages with different models, I can't automatically get user model. so I did like this:
First, I made AJAX call to server, asking for the _id (username in session, so I can get id)
from the id, I fetched the model, by model.fetch(), then I got my usermodel with all attributes.
then in the success callback of fetch, I did model.save({weight: "somevalue"}). According to me, it should update right, as the model is already available, that attribute weight also available with some old value, but it is sending POST request, also when I tried model.isNew(), it returned true. Where am I wrong? how can I update my model? I will post more details if required.
More details:
If I remove that save method, then I am getting correct attributes in the model.
If I don't remove that save method, that success and error callbacks are also appearing as attributes in the model.
Code:
addWeight : (e)->
arr=new Array()
arr['_id']=app._id
console.log "asdasd"
console.log arr
console.log arr['_id']
#user_model =new UserModel(arr)
#user_model.fetch({
success : (model,res,options) =>
console.log model
console.log res
arr=new Array()
arr['_id']=e.target.id
#arr['action']='weight' #means , update weight
##user_model.setArr(arr)
##user_model.set({weight : arr['_id']})
console.log "new : "+#user_model.isNew()
#user_model.save({weight : e.target.id})
##user_model.save({
# success : (model,res,options) =>
# console.log "model updated: "+JSON.stringify(model)
# console.log "Res : "+JSON.stringify(res)
# error : (model,res,options) =>
# console.log "Error : "+JSON.stringify(res)
#})
error : (model,res,options) =>
console.log "Error "
})
the above code is written in coffeescript, so even if you don't know coffeescript, don't worry, you can understand easily, and those # mean, it is a comment. here we follow indentation instead of braces.
one more doubt, a model's URL must be changed dynamically according to the requirement, right? what is the best way to achieve that? I am doing like this:
I am populating "array" containing the required fields that should be present in the URL. In model, s init func, I am using #arr=arr, then in URLs function, I check like this.
url : ->
if #arr['id']
"/user/#{#id}"
Is my approach right, or any better approach is there for dynamically setting URLs. Or can I directly set the URLs like this:
#user_model.setUrl "/someurl/someid" //this setUrl method is available in model's definition
#user_model.fetch() or save() or watever that needs url
Just a hunch, but you mentioned that you call model.fetch() to retrieve the _id field. Be sure to either return an id field instead _id (notice the underscore).
The call to model.isNew() returning true is an indicator that the id property was never set from the model.fetch() call.
I look forward to a possible further explanation with your code...
Looking at your code:
/* The model needs an 'id' attribute in order to marked as not new */
#user_model = new UserModel(id: arr['_id'])
Actually if you call
model.set({weight: "somevalue"});
It will update the value in the model, but it won't send a POST request
model.save(attribute);
Actually calls Backbone.sync as you probably know.
EDIT :
You might want ot set
m = Backbone.Model.extend({
idAttribute: '_id'
});
to every model, because the isNew method actually checks if the model has id attribute
Regarding to this you could see here that .set doesn't call backbone.sync here : http://jsfiddle.net/5M9HH/1/