I am trying to learn Backbone.js and have stuck at an issue. model.save is not updating my model value, though model.fetch updates the value fine.
Please see my code below and let me know if I am doing anything wrong here:
Model
var Person = Backbone.Model.extend({
url: "CreatePerson",
defaults: {
name: "",
age: 0
},
initialize: function(){
this.on("change:name",function(){
alert("Updated value is: "+this.get("name"));
});
}
});
I am creating an instance of this model on my html page, below is the code:
var person = new Person({name:"manish"});
person.save(person,{
wait: true,
success: function(){
alert("Data saved: "+person.toJSON().name);
},
error: function(){
alert("Sorry, something wrong went with the system");
}
});
person.fetch({
success: function(){
alert("Data Fetched: "+person.get("name"));
}
});
On both save and fetch, I am returning the following JSON data from the server:
{"name":"Logan","age":23}
Interestingly, for save, the change event is not fired and the alert box gives me the old model value (i.e manish), whereas when fetch function executes, the change event is fired and the fetch callback gives me the new value (i.e logan).
Can someone help me in identifying what I am doing wrong here?
From backbonejs.org
http://backbonejs.org/#Model-save
Calling save with new attributes will cause a "change" event
immediately, and a "sync" event after the server has acknowledged the
successful change. Pass {wait: true} if you'd like to wait for the
server before setting the new attributes on the model.
Sounds like the server hasn't returned that it is finished. Removing wait:true should make the changed event trigger.
There is error in your code:
person.save(person,{
You should provide object of attributes to your save call or just the options, but not the model itself. Remove person, and check if it works. wait:true should be set, to ensure that you change to a proper data validated by server.
Related
I call set method multiple times and change several attributes. Then I want to send the changed data to the server with {patch: true}.
I can use model.save(attrs, {patch: true});, but I do not know attrs. I can't use model.toJSON() (unneeded fields) or model.changedAttributes() (only last set) to obtain attrs.
How can I do this?
According to changedAttributes:
Optionally, an external attributes hash can be passed in, returning the attributes in that hash which differ from the model.
So you could try caching the state of model using toJSON before you start modifying. Once your modifications are done, pass the new state to changedAttributes method to retrieve changed attributes hash and then send a patch request. Something like
var oldAttrs = model.toJSON();
// ...do modifications here
var changedAttrs = model.changedAttributes(oldAttrs);
dataTosend = model.pick(_.keys(changedAttrs));
model.save(dataTosend, {patch: true});
Bind a listener for model
If you are setting value in view, listener should be like (better write it in initialize function)
this.listenTo(this.model, "change", this.onModelValueChange);
And your listener function
onModelValueChange: function(model, args) {
model.save(args.changed, {patch: true});
}
While TJ has the right answer, I have a better way to achieve what he suggest.
Instead of making a raw clone of the attributes, I prefer to keep a master copy of the model at the start of the app or the initialize of the view.
this.master = this.model.clone();
// ... then changes are made to this.model
When ready to save, use the master model to compare the attributes and retrieve the changes directly.
var changes = this.master.changedAttributes(this.model.attributes);
if (changes !== false) this.model.save(changes, { patch: true });
With this, you can skip the dataTosend = model.pick(_.keys(changedAttrs)) altogether since changes already is an object of all the differences with the initial state of the master model.
If it's a view that is re-used after the model save:
var View = Backbone.View.extend({
initialize: function() {
this.updateMaster();
},
saveChanges: function() {
var changes = this.master.changedAttributes(this.model.attributes);
if (changes !== false) {
this.model.save(changes, {
patch: true,
context: true,
success: this.updateMaster
});
}
},
updateMaster: function() {
this.master = this.model.clone();
},
});
I have a User model in a Backbone application that makes an ajax request. In the error callback, I wish to set an error message to pass to the view. However, if I try do
this.set({errors: result.errors});
I'm told "this" doesn't have a method set. In this case, I believe "this" is the ajax response object (rather than the User model which does have a set method)
Object {url: "/users.json", isLocal: false, global: true, type: "POST", contentType: "application/x-www-form-urlencoded; charset=UTF-8"…}
However, I also tried to do
this.model.set({errors: result.errors});
but it said I can't call "set" of undefined. I'm assuming it doesn't make sense to say "this.model" from within the model, but, as mentioned above, if I just say "this," it refers to the response object.
Is this the wrong way to go about it?
I am assuming you are doing something like this when you are saving your model
model.save({
success: function() {},
error: function() {
this.set({ errors: result.errors });
}
});
If that is the case, then you can change this.set to model.set, and everything will work.
However it doesn't really make that much sense to be storing the error message as a model attribute.
The model will fire an event when its save call fails on the server (check out the backbone events catalogue).
Therefore if you have a view with an attached model, you can tell the view to listen to this error event.
var MyView = Backbone.View.extend({
initialize: function() {
// if your using backbone v0.9.10
this.listenTo(this.model, 'error', this.handleModelError);
// or for earlier versions
this.model.on('error', this.handleModelError, this);
},
handleModelError: function(model, xhr, options) {
// show an error message, or whatever
}
});
var view = new MyView({ model: aModel });
// if the server returns an error, view.handleModelError will be called
aModel.save();
I think this probably loses context. Try using var self = this. Something like:
var self = this;
model.save("author", "F.D.R.",
{error: function()
{
self.model.set({errors: result.errors});
}});
demo fiddle (with problem) http://jsfiddle.net/mjmitche/UJ4HN/19/
I have a collection defined like this
var Friends = Backbone.Collection.extend({
model: Friend,
localStorage: new Backbone.LocalStorage("friends-list")
});
As far as I'm aware, that's all I need to do to get local storage to work (in addition to including it below backbone.js)
One thing I wasn't sure about, does the name "friends-list" have to correspond to a DOM element? I'm trying to save the "friends-list" so I called it that in local storage, however, localstorage doesn't seem to require passing a class or an id.
Here's a fiddle where you can see what I'm trying to do http://jsfiddle.net/mjmitche/UJ4HN/19/
On my local site, I'm adding a couple friends, refreshing the page, but the friends are not re-appearing.
Update
I've also done the following in my code on my local site
console.log(Backbone.LocalStorage);
and it's not throwing an error.
My attempt to debug
I tried this code (taken from another SO answer) in the window.AppView but nothing came up in the console.
this.collection.fetch({}, {
success: function (model, response) {
console.log("success");
},
error: function (model, response) {
console.log("error");
}
})
From the fine manual:
Quite simply a localStorage adapter for Backbone. It's a drop-in replacement for Backbone.Sync() to handle saving to a localStorage database.
This LocalStorage plugin is just a replacement for Backbone.Sync so you still have to save your models and fetch your collections.
Since you're not saving anything, you never put anything into your LocalStorage database. You need to save your models:
showPrompt: function() {
var friend_name = prompt("Who is your friend?");
var friend_model = new Friend({
name: friend_name
});
//Add a new friend model to our friend collection
this.collection.add(friend_model);
friend_model.save(); // <------------- This is sort of important.
},
You might want to use the success and error callbacks on that friend_model.save() too.
Since you're not fetching anything, you don't initialize your collection with whatever is in your LocalStorage database. You need to call fetch on your collection and you probably want to bind render to its "reset" event:
initialize: function() {
_.bindAll(this, 'render', 'showPrompt');
this.collection = new Friends();
this.collection.bind('add', this.render);
this.collection.bind('reset', this.render);
this.collection.fetch();
},
You'll also need to update your render to be able to render the whole collection:
render: function() {
var $list = this.$('#friends-list');
$list.empty();
this.collection.each(function(m) {
var newFriend = new FriendView({ model: m });
$list.append(newFriend.render().el);
});
$list.sortable();
return this;
}
You could make this better by moving the "add one model's view" logic to a separate method and bind that method to the collection's "add" event.
And a stripped down and fixed up version of your fiddle: http://jsfiddle.net/ambiguous/haE9K/
I wish to read a whole database table to fill a Backbone.js Collection, before updating a View.
I am using fetch and listening to the reset event.
My problem is the reset event fires up before the http request is made to the server.
My question is: how can I render the view AFTER the data is received back from the server on a fetch?
Here is a jsfiddle showing the problem (with a debugger placed at reset):
http://jsfiddle.net/GhaPF/16/
The code:
$(document).ready(function() {
var Item = Backbone.Model.extend({
urlRoot : './items'
});
var ItemList = Backbone.Collection.extend({
model: Item,
url: './items/',
});
var ItemListView = Backbone.View.extend({
el: 'body',
initialize: function(myitemList) {
this.itemlist = myitemList;
this.itemlist.bind('reset', this.debuggThis());
},
debuggThis: function() {
debugger;
},
render: function() {
},
events: {
"keypress #new-item": "createOnEnter"
},
createOnEnter: function(e) {
}
});
$("#new-item").focus();
var itemlist = new ItemList();
var myitemListView = new ItemListView(itemlist);
itemlist.fetch();
});
The following code works, but it just doesn't feel like proper backbone.js (MVC) code since it would be placed outside of the View definition:
itemlist.fetch().complete(function(){
Maybe the issue is this line:
this.itemlist.bind('reset', this.debuggThis());
Should actually be:
this.itemlist.bind('reset', this.debuggThis);
Your debugThis function was getting run at the time you set up the listener for the 'reset' event - not when the event is triggered. This was telling JavaScript that you wanted debugThis to return a callback function instead of having debugThis "be" the callback function.
Also, orangewarp's comment about passing 'this' as the third parameter is probably relevant too. Sot it would end up as:
this.itemlist.bind('reset', this.debuggThis, this);
That's strange. When you fetch() the reset event should be triggered AFTER your collection is populated. So I'm thinking the phenomena that reset happens before the http request is fired up may not be what you think it is.
Instead of using the complete... you could always just use the success callback option like this:
itemlist.fetch({
success: function() {
// Whatever code you want to run.
itemlist.debuggThis();
}
});
Also, when binding your reset you probably want this:
this.itemlist.bind('reset', this.debuggThis, this);
I am beginning to learn Backbone.js and I started with this boilerplate and made an example by loading JSON data from a static file on disk and display it in an html table.
Then I tried to bind an event on a button which is supposed to delete an element from the collection, then from the DOM. The thing works fine, the click triggers the destroy method, the remove event is triggered on the collection, but nothing comes out of the success or error callbacks of destroy
Anybody have a clue ?
The model :
define([
'underscore',
'backbone'
], function(_, Backbone) {
var memberModel = Backbone.Model.extend({
url: "/members.json",
defaults: {
email: "",
firstname: "",
lastname: "",
gender: "",
language: "",
consent: false,
guid: "",
creationDate: ""
},
initialize: function(){
}
});
return memberModel;
});
The view :
define([
'jquery',
'underscore',
'backbone',
'mustache',
'collections/members',
'text!templates/members/page.html'
], function($, _, Backbone, Mustache, membersCollection, membersPageTemplate){
var membersPage = Backbone.View.extend({
el: '.page',
initialize: function(){
this.members = new membersCollection();
this.members.on('remove', function(){
// works fine
$('.members-body tr').first().remove();
console.log('removed from collection');
});
},
render: function () {
var that = this;
this.members.fetch({success: function(){
var wrappedMembers = {"members" : that.members.toJSON()};
that.$el.html(Mustache.render(membersPageTemplate, wrappedMembers));
$('#delete-member').click(function(){
that.members.at(0).destroy({
// prints nothing!!!
success: function(){ console.log('sucess'); },
error: function(){ console.log('error'); }
});
});
}});
}
});
return membersPage;
});
I agree that this is odd. I'm not entirely sure what's happening yet, but here is what I suspect...
Your Destroy() calls aren't returning valid JSON.
Watching firebug,fiddler, or whatever, what do your destroy() responses look like?
I'm also curious what that is when your delete-click function is triggered.
Does destroy return false or a jqXHR object?
There is a bit a disconnect in backbone (at least i was for me at first). When calling destroy() or fetch/save for that matter, the call is async. Immediately after the call the delete event fires and your collection can respond. But, digging a bit deeper in the documentation, another event is fired up confirmation of the delete:
and a "sync" event, after the server has successfully acknowledged the model's deletion
So your collection is acting on the presumption that the delete has succeeded. If you want your collection to respond to a confirmed delete, look for the sync event.
This leaves one nagging point -- why isn't your error handler firing? Well, the callbacks are designed to respond to the confirmation of a delete. But I bet the JS call stack doesn't know how to interpret the response it's getting back, as a success or error. You likely stumbled across the realities of AJAX. I've found in backbone, jquery, and bunch of other frameworks that you can confuse the ajax error handling. Google "jquery ajax error not called" and you'll find a host of different scenarios where the error event isn't triggered.
UPDATE
After the comments back and forth...two things are happening. First, your model is perceived as 'new' which means calls to Destroy() don't make server requests. As a result your success/error don't fire. See this commit.
With that said, I don't think you consider your model new (not persisted to the server). You need to do one of two things. Either include a property named id OR in your model map your models ID (guid i assume) to the ID of the model. Mapping is easy by appling the following line to your model: idAttribute: "guid". You can see more on the idAttribute here.