Updating a model attribute - backbone.js

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.

Related

Update model within collection where model attribute is x

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.

Pagination with collection.fetch() in backbone.marionette

I am trying to implement pagination with backbone.marionette's
compositeView by first doing an initial fetch:
collection.fetch({reset: true})
To load additional models into the same collection,
collection.fetch({reset: false, remove: false})
is used.
I saw that on each use of fetch() with {reset: false}
muliple add events are triggered (for every model added.)
Thats fine basically, but when looking into the annotated source
of the compositeView's appendHtml function in marionette the follwing
statement is given:
buffering happens on reset events and initial renders in order to
reduce the number of inserts into the document, which are expensive.
While this is great for the initial fetch (only 1 browser reflow), every
subsequent fetch with {reset: false} could get pretty expensive
(reflow on every single model thats added). It really seems to
append() the itemView every single time - causing a reflow
(compositeView.isBuffering is false).
Is there any way to "batch" process the models and do a single insert
(same as done on the reset event)?
Does this need an overwrite in the appendHtml implementation
and implement the batch logic by myself?
(I assume you don't want to use Backbone.paginator, for some reason. If that isn't the case, use the plugin ;-D).
You could use a technique similar to the filtered collection used here: https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/entities/common.js#L2 (instanciation at https://github.com/davidsulc/marionette-gentle-introduction/blob/master/assets/js/apps/contacts/list/list_controller.js#L13).
Basically: you provide a "paginated collection" to the view, and what the paginated collection does is:
initially, return a "cloned" copy of the original collection
when you fetch on the original collection, add the fetched events in "batch" to the cloned collection, which will trigger the view to refresh (because it's listening to the cloned collection, not the original)
To minimize reflows, you can try the following:
silence the events that the collection listens to ("add", etc.)
when a group of models gets added, create a new instance of a collection view with only the new models
render the new collection view (but don't show it anywhere), and manually append the newView.el to the right place in the DOM
destroy the newView
Unfortunately, once you need to start optimizing, you tend to need to handle a bunch of things by hand...

Should model know its collection?

A collection knows its models, but is there a way for the model to get to its collection?
I would need a way to get to the "adjacent" models in a collection. The following may give a better idea of the problem:
I'm trying to implement a simple app that ranks items based on an "priority" attribute in the model and maintains a list of them. Each model view has buttons for increasing or decreasing the priority, which should alter the ordering of the list accordingly.
Simply adding or subtracting one from the current value will work as long as the priority-attributes are continuous, for example:
1,2,3,4,5,6
But there is also a delete button, which allows deleting any model from the list. It can lead to priority-attributes like:
1,2,5,6
Now changing the priority of 5 to 4 wont change the order of the list as it's still greater than 2.
It's undocumented, but the models will automatically have a collection property that points to the collection. I don't know the details of how that works if a model is in multiple collections (perhaps the collection property is not changed once it's set).
To solve the specific problem you mentioned, you can listen for remove events on the collection and adjust things accordingly. In this case you don't even need that collection property, because both the model and collection will be passed to the remove listener.
var handler = function ( model, collection, options ) {
// ...
};
collection.on( 'remove', handler );
The Backbone.Collection docs also say this:
the index at which the model is being removed from the collection is available as options.index
The options argument to remove listeners is not documented where the signature for remove listeners is documented, but it is passed (in 0.9.2 anyway).

Should views set model data?

I'm just trying to figure out a probably simple question.
Should views set model data directly or only call model methods that change their own data?
like everything else in software development, "it depends".
if you're using form inputs in your views and you just need to get the data from those inputs in to the models, set the data directly. You can do this any number of ways, including "change" events from the input fields, like this:
MyView = Backbone.View.extend({
events: {
"change #name", "setName"
},
setName: function(e){
var val = $(e.currentTarget).val();
this.model.set({name: val});
}
});
On the other hand, if you're kicking off business logic and other code that happens to set the data in the model (but really only does this as part of the business logic), then you should call a method on the model.
A "state machine" would be a good example of when you would do this. Or, in an image gallery that I wrote, I had some logic around the selection of an image for display. If an image is already selected, don't select it again. I put this logic in a method on my image model:
Image = Backbone.Model.extend({
select: function(){
if (!this.get("selected")){
this.set({selected: true});
}
}
});
As illustrated here, I like to run by the simple rule that if I have zero logic around the call to set, then I set it directly from wherever I am. If there is any logic related to the model, around the set, then I put it in the model.
Either way, when you do want to set data, you should use the set method. Bypassing this and setting the model's attributes directly through model.attributes will prevent a lot of Backbone's code from firing and potentially cause problems for you.
That depends on your programming style. I always set them directly. If the Law of Demeter sounds good to you and you're into object orientation (and possibly with a Java/Microsoft -stack background), then the style would be to create getter/setter methods.
If you on the other hand is in the "Size is the Enemy" camp (I also recommend Jeff Atwood's comments) the you certainly should set model data directly (as I hinted before, I'm in this camp myself).
That being said, Backbone.js models already have getter and setter methods .get and .set. You should not manipulate the .attributes member directly. I'm not as sure you shouldn't read from it, I tend to do that now and then and haven't run problems because of that yet.

Backbone.js model validation only on set->save (not on fetch)

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.

Resources