backbone.js set model internal attributes hash directly - backbone.js

Using Backbone.js I know it's highly recommended to set a model's property using the set method, and NOT by directly modifying the attributes internal hash.
However, apart from not firing the "change" event automatically, what other cons or "side-effects" are there in modifying the internal hash directly?
The problem I'm facing is that while the set method takes an object literal, I need to assign the left hand side using a variable determined at run-time. Thanks.
myModel.set({
myProperty : myValue; //myProperty is a variable, so this is invalid syntax
})
//vs
myModel.attributes[myProperty] = myValue; //myProperty is a variable that can be evaluated

Well, if you look at the annotated source code, you'll find that set does a lot.
What if you extended Backbone.Model with a function that does it for you:
Backbone.Model.prototype.setByName = function(key, value, options) {
var setter = {};
setter[key] = value;
this.set(setter, options);
};
Then, you can just do what you want directly on the model:
var model = new Backbone.Model();
model.setByName(myProperty, "bar");
That feels like a better solution to me.
Edit
As #earl3s pointed out, this is no longer necessary in more recent versions of Backbone. Today, you can just call model.set(myProperty, "bar") and it does what you want.

In the annotated source code mentioned by Brian Genisio you can read the following lines:
"Handle both "key", value and {key: value} -style arguments.".
So you can just use model.set(myProperty,"bar",options).
Perhaps they've added this feature after the post of Brian Genisio.. i dunno.

Related

Set model value programmatically in Angular.js

I'm an author of angular-input-modified directive.
This directive is used to track model's value and allows to check whether the value was modified and also provides reset() function to change value back to the initial state.
Right now, model's initial value is stored in the ngModelController.masterValue property and ngModelController.reset() function is provided. Please see the implementation.
I'm using the following statement: eval('$scope.' + modelPath + ' = modelCtrl.masterValue;'); in order to revert value back to it's initial state. modelPath here is actually a value of ng-model attribute. This was developed a way back and I don't like this approach, cause ng-model value can be a complex one and also nested scopes will break this functionality.
What is the best way to refactor this statement? How do I update model's value directly through the ngModel controller's interface?
The best solution I've found so far is to use the $parse service in order to parse the Angular's expression in the ng-model attribute and retrieve the setter function for it. Then we can change the model's value by calling this setter function with a new value.
Example:
function reset () {
var modelValueSetter = $parse(attrs.ngModel).assign;
modelValueSetter($scope, 'Some new value');
}
This works much more reliably than eval().
If you have a better idea please provide another answer or just comment this one. Thank you!
[previous answer]
I had trouble with this issue today, and I solved it by triggering and sort of hijacking the $parsers pipeline using a closure.
const hijack = {trigger: false; model: null};
modelCtrl.$parsers.push( val => {
if (hijack.trigger){
hijack.trigger = false;
return hijack.model;
}
else {
// .. do something else ...
})
Then for resetting the model you need to trigger the pipeline by changing the $viewValue with modelCtrl.$setViewValue('newViewValue').
const $setModelValue = function(model){
// trigger the hijack and pass along your new model
hijack.trigger = true;
hijack.model = model;
// assuming you have some logic in getViewValue to output a viewValue string
modelCtrl.$setViewValue( getViewValue(model) );
}
By using $setViewValue(), you will trigger the $parsers pipeline. The function I wrote in the first code block will then be executed with val = getViewValue(model), at which point it would try to parse it into something to use for your $modelValue according the logic in there. But at this point, the variable in the closure hijacks the parser and uses it to completely overwrite the current $modelValue.
At this point, val is not used in the $parser, but it will still be the actual value that is displayed in the DOM, so pick a nice one.
Let me know if this approach works for you.
[edit]
It seems that ngModel.$commitViewValue should trigger the $parsers pipeline as well, I tried quickly but couldn't get it to work.

Binding doesn't update when the original object changes. Is copying the only/best solution?

I'm running into the same problem as stated here.
Binding doesn't update when the original object changes.
I'm looking for more opinions on how to approach this problem.
I would think this would be a very common problem. Pretty much all ajax request in spa's return json which is used to create js objects. New references are created to point to these objects and/or existing references are updated to point to these new objects.
Is copying the object properties from the new object to the existing object the only/best solution? What if the object is a big collection or has a deep graph? It seems like copying wouldn't work well. Is there a way to monitor/watch the reference?
Am I thinking about this wrong?
As request i've setup a jsfiddle to simulate the issue.
https://jsfiddle.net/ho69ofog/
var newObject = {
"subObject": {
"subProperty": Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 5)
}
};
myObject = newObject;
Notice that the only scope value that is updating is when the whole factory object attached to the scope. As mentioned above and in the referenced issue Binding doesn't update when the original object changes , copying the object instead of creating a new object(inside the updateTimer function) will cause all scope values to update.
It's not the only solution, but it's one of the simplest.
Here's another. Use a $watch in the controller to detect when the object has changed.
$scope.$watch(function(){
return myFactory.getObject();
},
function(newValue){
$scope.subObject = angular.isDefined(newValue) ? newValue.subObject : undefined;
});

Which arguments are passed to handler bound to Backbone.Model change event?

I can't seem to find any documentation for this and playing around with the code hasn't helped me understand this completely.
I have Backbone model and in my my I bind a handler to the model change event:
var myModel = new ModelA();
var myView = new ViewA({
model: myModel
})
//in my view I have
this.listenTo(this.model, "change", this.handleChange);
Can someone please explain to me what arguments are passed to this.handleChange? I see
there are 2 arguments, model & value, but what are they exactly?
What happens when I bind to a specific attribute, like so:
this.listenTo(this.model, "change:attr", this.handleChange);
When I unset an attribute from the model using myModel.unset("attr");, what are the values passed to handleChange? I see that in some cases value is undefined and sometimes it has 1 attribute unset = true
Any help or point to relevant documentation will be appreciated.
From the code:
Event change:attr:
this.trigger('change:' + changes[i], this, current[changes[i]], options);
Event change:
this.trigger('change', this, options);
So in the first case, the arguments are: the model, the value, and the options used (both external and internal options (for example, unset: true for your unset call)). In the second case, as it doesn't concern a particular attribute, the arguments are: the model and the options.

Cleanest way to destroy every Model in a Collection in Backbone?

On the first attempt I wrote
this.collection.each(function(element){
element.destroy();
});
This does not work, because it's similar to ConcurrentModificationException in Java where every other elements are removed.
I tried binding "remove" event at the model to destroy itself as suggested Destroying a Backbone Model in a Collection in one step?, but this will fire 2 delete requests if I call destroy on a model that belongs to a collection.
I looked at underscore doc and can't see a each() variant that loops backwards, which would solve the removing every element problem.
What would you suggest as the cleanest way to destroy a collection of models?
Thanks
You could also use a good, ol'-fashioned pop destroy-in-place:
var model;
while (model = this.collection.first()) {
model.destroy();
}
I recently ran into this problem as well. It looks like you resolved it, but I think a more detailed explanation might also be useful for others that are wondering exactly why this is occurring.
So what's really happening?
Suppose we have a collection (library) of models (books).
For example:
console.log(library.models); // [object, object, object, object]
Now, lets go through and delete all the books using your initial approach:
library.each(function(model) {
model.destroy();
});
each is an underscore method that's mixed into the Backbone collection. It uses the collections reference to its models (library.models) as a default argument for these various underscore collection methods. Okay, sure. That sounds reasonable.
Now, when model calls destroy, it triggers a "destroy" event on the collection as well, which will then remove its reference to the model. Inside remove, you'll notice this:
this.models.splice(index, 1);
If you're not familiar with splice, see the doc. If you are, you can might see why this is problematic.
Just to demonstrate:
var list = [1,2];
list.splice(0,1); // list is now [2]
This will then cause the each loop to skip elements because the its reference to the model objects via models is being modified dynamically!
Now, if you're using JavaScript < 1.6 then you may run into this error:
Uncaught TypeError: Cannot call method 'destroy' of undefined
This is because in the underscore implementation of each, it falls back on its own implementation if the native forEach is missing. It complains if you delete an element mid-iteration because it still tries to access non-existent elements.
If the native forEach did exist, then it would be used instead and you would not get an error at all!
Why? According to the doc:
If existing elements of the array are changed, or deleted, their value as passed to callback will be the value at the time forEach visits them; elements that are deleted are not visited.
So what's the solution?
Don't use collection.each if you're deleting models from the collection. Use a method that will allow you to work on a new array containing the references to the models. One way is to use the underscore clone method.
_.each(_.clone(collection.models), function(model) {
model.destroy();
});
I'm a bit late here, but I think this is a pretty succinct solution, too:
_.invoke(this.collection.toArray(), 'destroy');
Piggybacking on Sean Anderson answer.
There is a direct access to backbone collection array, so you could do it like this.
_.invoke(this.collection.models, 'destroy');
Or just call reset() on the collection with no parameters, destroy metod on the models in that collection will bi triggered.
this.collection.reset();
http://backbonejs.org/#Collection-models
This works, kind of surprised that I can't use underscore for this.
for (var i = this.collection.length - 1; i >= 0; i--)
this.collection.at(i).destroy();
I prefer this method, especially if you need to call destroy on each model, clear the collection, and not call the DELETE to the server. Removing the id or whatever idAttribute is set to is what allows that.
var myCollection = new Backbone.Collection();
var models = myCollection.remove(myCollection.models);
_.each(models, function(model) {
model.set('id', null); // hack to ensure no DELETE is sent to server
model.destroy();
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="http://underscorejs.org/underscore-min.js"></script>
<script src="http://backbonejs.org/backbone-min.js"></script>
You don't need underscore and for loop for this.
this.collection.slice().forEach(element => element.destroy());

underscore.js filter function

I'm attempting to learn backbone.js and (by extension) underscore.js, and I'm having some difficulty understanding some of the conventions. While writing a simpel search filter, I thought that something like below would work:
var search_string = new RegExp(query, "i");
var results = _.filter(this, function(data){
return search_string.test(data.get("title"));
}));
But, in fact, for this to work I need to change my filter function to the following:
var search_string = new RegExp(query, "i");
var results = _(this.filter(function(data){
return search_string.test(data.get("title"));
}));
Basically, I want to understand why the second example works, while the first doesn't. Based on the documentation (http://documentcloud.github.com/underscore/#filter) I thought that the former would have worked. Or maybe this just reflects some old jQuery habits of mine... Can anyone explain this for me?
I'd guess that you're using a browser with a native Array#filter implementation. Try these in your console and see what happens:
[].filter.call({ a: 'b' }, function(x) { console.log(x) });
[].filter.call([1, 2], function(x) { console.log(x) });
The first one won't do anything, the second will produce 1 and 2 as output (http://jsfiddle.net/ambiguous/tkRQ3/). The problem isn't that data is empty, the problem is that the native Array#filter doesn't know what to do when applied to non-Array object.
All of Underscore's methods (including filter) use the native implementations if available:
Delegates to the native filter method, if it exists.
So the Array-ish Underscore methods generally won't work as _.m(collection, ...) unless you're using a browser that doesn't provide native implementations.
A Backbone collection is a wrapper for an array of models, the models array is in c.models so you'd want to:
_.filter(this.models, function(data) { ... });
Backbone collections have several Underscore methods mixed in:
Backbone proxies to Underscore.js to provide 28 iteration functions on Backbone.Collection.
and one of those is filter. These proxies apply the Underscore method to the collection's model array so c.filter(...) is the same as _.filter(c.models, ...).
This mixing-in is probably what's confusing the "should I use the native method" checks that Underscore is doing:
if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
You can use _.filter on a plain old object (_.filter({a:'b'}, ...)) and get sensible results but it fails when you _.filter(backbone_collection, ...) because collections already have Underscore methods.
Here's a simple demo to hopefully clarify things: http://jsfiddle.net/ambiguous/FHd3Y/1/
For the same reason that $('#element') works and $#element doesn't. _ is the global variable for the underscore object just like $ is the global variable for the jQuery object.
_() says look in the _ object. _filter says look for a method named _filter.

Resources