So I have a page in which a user is selecting from a list of available options via checkboxes.
I have 3 collections: One for possible list of options, one for the currently saved options, and a clone of the saved options. I am using the clone list to add/remove options as they click on checkboxes.
var possibleOptions = new Backbone.Collection("/options");
var currentUserOptions = new Backbone.Collection("/useroptions", { id: 2 });
var clonedUserOptions = new Backbone.Collection(currentUserOptions.toJSON());
The reason this approach was taken is that a user can cancel out of the options page mid edit so want the options persisted upon clicking a save button. The clonedOptions are updated correctly as options are checked/unchecked. However, when I try to update the real list nothing seems to happen.
currentUserOptions.update(clonedUserOptions.toJSON());
My expectation was that backbone would trigger post request for the new models and deletes for each missing model according to the docs (http://documentcloud.github.com/backbone/#Collection-update). Please let me know if I am misunderstanding how this is suppose to work. A simple working example of the right approach would be greatly appreciated.
Thanks,
CF
It is my understand that you have to update the server with individual model calls when the collection changes (as people have mentioned). There are events to cover these changes. However, you can send the entire collection using sync. When each model is added or removed you can flag it as new and deleted and when you are ready to save send the entire collection. Your server methods will have to take care of determining the appropriate add or delete operation then when the collection is sent.
Here is an example of doing a bulk sync. Instead of using this.collection.remove() you may just want to flag as deleted as I mentioned. This is pretty much the only way the server would know what to delete. Technically you could delete everything and then just add what is sent in the bulk update :) Also you might have to do another get or return a collection from the server when you save to update what has actually been removed. I am not sure this helps in your case, but I have used it for pages that "save when done" one button concept.
var one = new model({ id: this.collection.length + 1});
var two = new model({ id: this.collection.length + 2 });
var three = new model({ id: this.collection.length + 3 });
var four = new model({ id: this.collection.length + 4 });
var five = new model({ id: this.collection.length + 5 });
this.collection.add(one.toJSON());
this.collection.add(two.toJSON());
this.collection.add(three.toJSON());
this.collection.add(four.toJSON());
this.collection.add(five.toJSON());
this.collection.sync('update', this.collection, { success: function () { console.log(this.collection.length); } });
this.collection.remove(one);
this.collection.remove(two);
this.collection.sync('update', this.collection, { success: function () { console.log(this.collection.length); } });
And in your case:
currentUserOptions.sync('update', currentUserOptions, { success: function () { //do something } });
Related
I have a grid with remote data (php/mysql/json) and use a form to insert records or to edit this data.
I use the api configuration of the proxy/store. I use MVC architecture.
So, all very simple (in pseudo code):
get selected model form grid or create model
frm.loadRecord()
frm.updateRecord()
frm.getRecord().save()
and all works fine, but I noticed in the browser console that after the POST (works fine, calls either the url configured with create or the url configured with update), the store calls (GET) the url configured with retrieve twice. These calls are identical.
So functionally all works fine and I could ignore it, but now I've noticed I want it fixed.
Can anyone help me where to look? Thanks in advance.
Details:
It's all really basic:
In the controller of the gridpanel:
updateRow: function (gridpanel) {
var sm = gridpanel.getSelectionModel();
var record = sm.getLastSelected();
this.showForm(record);
}
and
showForm: function (record) {
...
formpanel.show();
var frm = formpanel.getForm();
frm.loadRecord(record);
}
In the controller of the formpanel:
submit: function(frm) {
frm.updateRecord();
frm.getRecord().save();
}
When I remove the save action the GET requests aren't called, so this seems to trigger them.
In the store:
api: {
create: '../php/api/customers.php?request=create',
read: '../php/api/customers.php?request=retrieve&scope=summary',
update: '../php/api/customers.php?request=update',
destroy: '../php/api/customers.php?request=delete'
}
The screenshot:
So basically, I created a Backfire app. As soon as the page is loaded, I am iterating over my Backbone.Firebase.Collection instance.
And then rendering the results onto the page... the problem is that obviously the collection instance does not contain all the data that is on firebase.
So, I put a set-timeout of 1000ms to make it so that the collection has time get its data from Firebase. This way of doing it is terrible... sometimes the data is synced up other times it is not.
How can I have an event be triggered as soon as the "BackFire Collection Model" has its data from firebase?
$(document).ready(function() {
var myCatalogApp = new CatalogApp()
var catalog = new Catalog()
setTimeout(function(){myCatalogApp.displayCollection(catalog)},1000);
//To give catalog time to asynchronously retrieve database entries
myCatalogApp.initialize()
})
var Catalog = Backbone.Firebase.Collection.extend({
model: Item,
firebase: "https://mycatalogmaker.firebaseIO.com/items"
});
All of my problems would be solved if Backfire would let me do a fetch method... but they have it disabled for Backfire objects
Try this :
$(document).ready(function() {
var myCatalogApp = new CatalogApp()
var catalog = new Catalog();
catalog.bind('sync', function(){myCatalogApp.displayCollection(catalog);});
myCatalogApp.initialize();
})
Context
The situation as follows: Users can upload files in an application. They can do this at any time (and number of times).
I would like to show a spinner when any uploading is being done, and remove it when no uploading is happening at the moment.
My approach
The uploads are handles by an external file upload plugin (like blueimp) and on it's add method I grab the jqXHR object and add these to a backbone collection (which are images in my application, so I use this in combination with Marionette's collectionviews).
The following is part of a function called in an onRender callback of a Marionette Itemview:
// Get the file collection
var uploadFiles = SomeBackBoneCollection;
// Track how many deferreds are expected to finish
var expected = 0;
// When an image is added, get the jqXHR object
uploadFiles.bind('add', function(model) {
// Get jqXHR object and call function which tracks it
trackUploads(model.get('jqXHR'));
// Do something to show the spinner
console.log('start the spinner!');
// Track amount of active deferreds
expected++;
}, this);
// Track the uploads
function trackUploads(jqXHR) {
$.when(jqXHR).done(function(){
// A deferred has resolved, subtract it
expected--;
// If we have no more active requests, remove the spinner
if (expected === 0) {
console.log('disable the spinner!');
}
});
}
Discussion
This method works very well, although I'm wondering if there are any other (better) approaches.
What do you think about this method? Regarding this method, do you see any up- or downsides? Any other methods or suggestions anyone?
For example, it might be great to have some kind of array/object to which you can keep passing deferreds, and that a $.when is somehow monitoring this collection and resolves if at any moment everything is done. However, this should work such that you can keep passing deferred objects at any given time.
you can do this via events.
I am assuming each file is an instance of this model:
App.Models.File = Backbone.Model.extend({});
before the user upload the file, you are actually creating a new model, and save it.
uploadedFiles.create(new App.Models.File({...}));
so in your upload view...
//listen to collection events
initialize: function() {
//'request' is triggered when an ajax request is sent
this.listenTo(this.collection, 'request', this.renderSpinner);
//when the model is saved, sync will be triggered
this.listenTo(this.collection, 'sync', this.handleCollectionSync);
}
renderSpinner: function() {
//show the spinner if it is not already being shown.
}
ok, so, in 'handleCollectionSync' function, you want to decide if we wanna hide the spinner.
so how do we know if there're still models being uploaded? you check if there're new models in the collection (not saved models)
so in your collection, add a helper method:
App.Collections.Files = Backbone.Collection.extend({
//if there's a new model, return true
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.isNew();
}).length > 0;
}
});
back to your view:
handleCollectionSync: function() {
//if there's no unsaved models
if(!this.collection.hasUnsavedModels()){
//removespinner
}
}
this should solve your problem assuming all the uploads are successful. you may want to complete this with error handling cases - it depends on what you wanna do with error case, but as long as you are not retrying it right away, you should remove it from the collection.
==========================================================================================
Edit
I'm thinking, if you allow the user to upload a file multiple times, you are not really creating new models, but updating existing ones, so the previous answer would not work. to work around this, I would track the status on the model itself.
App.Models.File = Backbone.Model.extend({
initialize: function() {
this.uploading = false; //default state
this.on('request', this.setUploading);
this.on('sync error', this.clearUploading);
}
});
then setUploading method should set uploading to true, clearUploading should change it to false;
and in your collection:
hasUnsavedModels: function() {
return this.filter(function(model) {
return model.uploading;
}).length > 0;
}
so in your view, when you create a new file
uploadNewFile: function(fileAttributes) {
var newFile = new App.Model.File(fileAttributes);
this.collection.add(newFile);
newFile.save();
}
I believe 'sync' and 'request' events are triggered on the collection too when you save models inside of it. so you can still listenTo request, sync, and error events on the collection, in the view.
I'm trying to get the backbone.js DELETE request to fire, but don't see any requests being made in my console.
I have collection model like so:
var Model = Backbone.Model.extend(
{
urlRoot: '/test',
defaults:{}
});
var TableList = Backbone.Collection.extend(
{
url: '/test',
model: Model
});
In my view I'm running this:
this.model.destroy();
Everything seems to be running fine, I can see output coming from the remove function that calls the destroy so I know it's getting there plus it also successfully runs an unrender method that I have. Can't see any requests being made to the sever though?
If I am not mistaken, you have to have an id property on your model to ensure that it hits the correct url. IE if your model was...
var Model = Backbone.Model.extend({
url: '/some/url'
});
var model = new Model({
id: 1
});
model.destroy(); // I THINK it will now try and DELETE to /some/url/1
Without an id it doesn't know how to build the url correctly, typically you'd fetch the model, or create a new one and save it, then you'd have a Url...
See if that helps!
I found the issue to my problem, thought not a solution yet. I'm not sure this is a bug with backbone or not, but I'm using ajaxSetup and ajaxPrefilter. I tried commenting it out and it worked. I narrowed it down to the ajaxSetup method and the specifically the use of the data parameter to preset some values.
Have you tried using success and error callbacks?
this.model.destroy({
success : _.bind(function(model, response) {
...some code
}, this),
error : _.bind(function(model, response) {
...some code
}, this);
});
Might be instructive if you're not seeing a DELETE request.
I have two set of collections. One is for the categories and the other is for the Items. I ned to wait for the categories to finish fetching everything for me to set the category for the Items to be fetched.
Also i everytime i click a category i must re-fetch a new Items Collection because i have a pagination going on everytime i click on a category it doesn't refresh or re-fetch the collection so the pagination code is messing with the wrong collection. Any ideas?
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch();
this.itemCollection = new ItemCollection();
this.itemCollection.fetch();
Just ran into a similar situation. I ended up passing jquery.ajax parameters to the fetch() call. You can make the first fetch synchronous. From the backbone docs:
jQuery.ajax options can also be passed directly as fetch options
Your code could be simplified to something like:
this.categoryCollection.fetch({async:false});
this.itemCollection.fetch();
One quick way would be to just pass a callback into the first fetch() call that invokes the second. fetch() takes an options object that supports a success (and error) callback.
var self = this;
this.categoryCollection = new CategoryCollection();
this.categoryCollection.fetch({
success: function () {
self.itemCollection = new ItemCollection();
self.itemCollection.fetch();
}
});
Not the most elegant, but it works. You could probably do some creative stuff with deferreds since fetch() returns the jQuery deferred that gets created by the $.ajax call that happens.
For the pagination issue, it's difficult to tell without seeing what your pagination code is doing. You're going to have to roll the pagination stuff yourself since Backbone doesn't support it natively. What I'd probably do is create a new Collection for the page criteria that are being queried and probably create a server action I could hit that would support the pagination (mapping the Collection's url to the paginated server action). I haven't put a ton of thought into that, though.
I had to react to this thread because of the answers there.
This is ONLY WAY OF DOING THIS RIGHT!!!
this.categoryCollection = new CategoryCollection();
this.itemCollection = new ItemCollection();
var d1 = this.categoryCollection.fetch();
var d2 = this.itemCollection.fetch();
jQuery.when(d1, d2).done(function () {
// moment when both collections are populated
alert('YOUR COLLECTIONS ARE LOADED :)');
});
By doing that you are fetching both collections at same time and you can have event when both are ready. So you don't wait to finish loading first collections in order to fetch other, you are not making ajax calls sync etc that you can see in other answers!
Here is a doc on Deferred objects.
Note: in this example case when one or more deferred object fails it's not covered. If you want to cover that case also beside .done you will have to add .fail callback on .when and also add error handler that will mark failed d1 or d2 in this example.
I am using RelationalModel and I created a queued fetch, that only calls the 'change' event when done loading:
var MySuperClass = Backbone.RelationalModel.extend({
//...
_fetchQueue : [],
fetchQueueingChange : function(name){
//Add property to the queue
this._fetchQueue.push(name);
var me = this;
//On fetch finished, remove it
var oncomplete = function(model, response){
//From _fetchQueue remove 'name'
var i = me._fetchQueue.indexOf(name);
me._fetchQueue.splice(i, 1);
//If done, trigger change
if (me._fetchQueue.length == 0){
me.trigger('change');
}
};
this.get(name).fetch({
success: oncomplete,
error : oncomplete
});
},
//...
});
The class would call:
this.fetchQueueingChange('categories');
this.fetchQueueingChange('items');
I hope you can improve on this, it worked well for me.
I ended up with the same problem today and figured out a solution to this:
var self = this;
this.collection = new WineCollection();
this.collection.url = ApiConfig.winetards.getWineList;
this.collection.on("reset", function(){self.render()});
this.collection.fetch({reset: true});
Now when the fetch on the collection is complete a "reset" is triggered and upon "reset" call the render() method for the view.
Using {async: false} is not the ideal way to deal with Backbone's fetch().
just set jQuery to become synchronous
$.ajaxSetup({
async: false
});
this.categoryCollection.fetch();
this.itemCollection.fetch();
$.ajaxSetup({
async: true
});
This is the simplest solution, I guess. Of course, starting new requests while these fetches run will be started as synchronous too, which might be something you don't like.