Caveat: I'm working with a backend that I don't have full control over, so I'm wrestling with a few considerations within Backbone that might be better addressed elsewhere...unfortunately, I have no choice but to handle them here!
So, my problem is that I'd really like to validate user input from a form (when I set values with it on Backbone models), but the models I receive from the API on newly created objects (via posts that ONLY accept a name, and ONLY return a name and object id) will fail my validation checks.
As example, when a new object is created in the database, two key fields are populated as empty strings (and so when Backbone hits the API and populates the models, it populates those keys with empty strings). When the user saves these objects back, post-edit, I'd like to force them to enter values for these two keys -- which is very easy, given Backbone's built in validation method.
The problem, of course, is that the validation is firing on both fetch->set (unwanted behavior) and set->save (desired behavior) -- and so newly created models won't load at all...Backbone collects them, validation fails, and errors fire.
So, my question is: is there a "Backbone-y" way to only validate the models on set->save, not on fetch->set? Could I use a specific trigger to work around this?
Any ideas would be greatly appreciated.
Backbone.Model.set won't perform validation if you pass in { silent: true }, and fetch will pass any options through to set, so you could either override fetch or write your own fetchSilent method that passes that in an options object.
However, you might run into a slight gotcha with Backbone.Collection.fetch, because when it receives attributes from the server, it doesn't create the new models with set. Instead, it creates a new model with model = new this.model(attrs, {collection: this}); and then performs validation if there's a validate method on the object.
This is a little annoying. You can get around it by defining a parse method on your collection (if you're using one) that creates a model silently (using {silent: true}), because when Backbone.Collection.add receives a fully formed Backbone model, it won't run the validation. (see the _add and _prepareModel methods in the annotated source).
It's a little annoying that the collection works that way, but (for now at least) it is what it is.
Instead of overriding fetch you can do another thing:
When you validate your model, check for model.silent and only validate if that doesn't exist.
So you do the following when you want to fetch a model:
var test = new MyModel({ id: '123', silent: true });
// in your Model validate function
validate: function(attrs) {
if (!attrs.silent) {
// validate logic here
}
}
Then you can fetch the model. After you get your model you can unset silent, for future validation.
Related
I am practising Backbone js. I have a collection consisting of 10 models. In just one model, I want to change 2 of the attributes. I am using the device local storage. I have tried the following code (this is just part of the larger script):
this.collection.forEach(function(user) {
if (user.get('subject') === 'Physics') {
user.set({'title': 'hdrodynamics'});
user.save();
console.log(JSON.stringify(user));
return;
}
});
I consider this method inefficient. What happens as the collection length increases. I believe there is a better way than this.
You can use collection.findWhere, which:
directly returns only the first model in the collection that matches the passed attributes.
Which can reduce the number of iterations run.
You can also use model.save with attributes to avoid doing the extra 'set'.
// Stops when matching model is found
var physicsModel = this.collection.findWhere({ subject: 'Physics' });
// might want to do some null-checking...
// Performs a set prior to POSTing, firing correct 'change' events
physicsModel.save({ title: 'hydrodynamics' });
Note: A more efficient way to do this would be to avoid the lookup in the collection. For example, if this model should 'save' by some action from the user, you could leverage the model's view to gain access to the correct model (i.e. this.model.save()). This of course is implementation dependent.
Within backbone I have a collection. Where I want to update a model or models within it if a model has a particular attribute value.
I know I can do something to the extent of
var model = myCollection.where({some_attr:'something'});
where model becomes the child object from the collection. However beyond that I seem to get lost. As none of the backbone functions seem to work on it. Example, I can't:
model.set({other_attr: 'changed value'});
model.save();
I just get an Uncaught TypeError. model.set is not a function
again.. Example sake.
So overall I want to find the model, update it, and then save it and its changes to the server? Idea's? Assume I have no workable ID to make getting and setting a little easier, hence why I need to do a where and update like that.
I would recommend using where and always working with the array of models it passes. When you're done updating the models you can make a sync call on the collection to bulk update those changes back to the server.
myCollection.
where({some_attr:'something'}).
map(function(model) {
model.set({other_attr: 'changed value'});
});
myCollection.sync('update');
The statement
var model = myCollection.where({some_attr:'something'});
returns an array of models, not a single model. Assuming only one model will match your criteria, you could use
var model = myCollection.findWhere({some_attr:'something'});
If your collection could have multiple matching models, then you'll want to stick with .where but iterate over the array.
I'm trying to find a generic solution to commit/rollback a model alongside ng-resource.
When to commit/save model:
Successful http write methods (PUT, POST, PATCH).
When to rollback/reset model:
Failing http write methods.
User deciding to cancel their local changes (before PUT).
I've found pieces of this solution scattered, but no comprehensive strategy.
My initial strategy was to make use of the default ng-resource cache. Controller starts with GETting an item. If a subsequent PUT/POST/PATCH failed (caught either in the controller's promise, or in the $resource.<write_method>'s interceptor.responseError, re-run the GET that first got the object, and replace my model with that, as on initial load. However, this doesn't help if there's a successful PUT, followed by a failed one, because I'd be replacing my model with a stale object. The same problem occurs when a user tries to cancel their form changes before submitting... the last successful GET may be stale.
I've got a model with ~10 properties, and multiple <form>s (for html/design's sake) that all interact with this one object that gets PUT upon any of the forms being submitted:
{'location_id': 1234,
'address': {
'line1': '123 Fake St'}
...
},
'phone': '707-123-4567',
...
}
Models already have nice rollback features, but getting ng-resource to touch them seems tricky, and I really want to reset the entire object/model. Using a custom cache alongside my $resource instance seems like a decent way to go, but is equally tricky to give user-interaction a way to rollback. Another idea was to store a separate object when loading into scope: $scope.location = respData; $scope._location = angular.copy(respData); that I could load from when wanting to roll back, by way of $scope.location = $scope._location;, but I really don't want to clutter my controllers site-wide with these shadow objects/functions (DRY).
The question I have is about how to store a collection of models to a RESTful back end.
I'm using Backbone.js, and I'm considering to either:
Use Async.js parallel method and post / put each model separately in a loop, after which a general callback method is triggered;
Send a collection of objects to the back end, and use a database transaction to make sure that all models are properly saved with a single commit;
The first method seems to cause a lot of overhead, because I have to make different calls to save the models.
But when considering the second approach, Laravel4 does not by default allows to perform a post / put on a collection.
What would be your preferred approach, and more importantly, why?
The second approach is definitely the way to go in my opinion, it reduces the latency and saves bandwidth if you use it with a large number of models since all the HTTP stuff (headers, etc) will be sent only once.
In your controller, use something like this (it may not work, I don't have access to any Laravel installation to test it) :
public function postCollection() {
$collection = Input::get("collection");
DB::transaction(function() {
foreach ($collection as $data) {
// In this example we assume it's a collection of users
// Of course in a real app you would also do input validation
$user = User::create(["name" => $data->name, "email" => $data->email]);
}
})
// Example success response, will be automatically serialized to JSON
return ["status" => "success"];
}
This loops over the collection element of your JSON input, which should be a list of models. Then it should obviously do validation and possibly other stuff. The whole loop is wrapped into a DB::transaction(), which will rollback everything if an exception occurs inside of it.
I added a model to my collection and did model.save The data gets saved on the server.
After this I call collection.fetch({add:true}). Ideally only one model should be returned from server ie the newly added one but I'm seeing the entire collection being propagated back in chrome developers tool.
I'm confused. how does this actually work ?
As an add-on to Cyril N.'s answer (which describes essentially the default behavior, and should be enough I guess), I'd like to explain more thoroughly the role of the 3 flags:
add: whether or not Backbone should create and add to the collection the models which don't exist yet (ie, whose id are not in the collection)
remove: whether or not Backbone should remove from the collection the models which were not brought back from the server (ie, whose id were not is the brought back data)
merge: whether or not Backbone should update the models which are not in the first two categories (already in the collection, and brought back from the server)
However, I like to expand a bit further about the general behavior of the method (it uses the Collection#set method as a callback, so it's more the Collection#set's behavior). Backbone prepares the models, which means it creates fake, volatile models at the beginning of the method (they're only volatile if they're not added). This can lead to unexpected behaviors: because it creates models, the initialize method of those are executed.
Also, as a side-note, since Backbone 1.0, the Collection#reset flag was introduced if you want to reset your collection when fetching (therefore it uses this method as a callback). Before that, it was the default behavior of the Collection#fetch method.
Well, with both answers you should have every info you need I guess.
When you call collection.fetch({add:true}, Backbone will request the server with the complete list of items in the collection (hence the entire collection you see in the Chrome developers tool), but then, instead of reloading the entire collection in the Collection Object in Backbone (Javascript), Backbone will performs a "smart" update of the collection.
The set method performs a "smart" update of the collection with the
passed list of models. If a model in the list isn't yet in the
collection it will be added; if the model is already in the collection
its attributes will be merged; and if the collection contains any
models that aren't present in the list, they'll be removed. All of the
appropriate "add", "remove", and "change" events are fired as this
happens. If you'd like to customize the behavior, you can disable it
with options: {add: false}, {remove: false}, or {merge: false}.
Source: http://backbonejs.org/#Collection-set
All the smart work is made client side, in Javascript, but for that, Backbone request the entire collection from the server to compare it with what it has in local (client side).
(I hope I'm clear ;)).