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

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;
});

Related

Mutating object properties within an array with Polymer

I not sure how to solve this issue. I am sure someone will know this very quickly.
I have an array of objects and modifying a property. I have a firebase listener 'child_changed'. When firebase is updated need to update the array. Here is the code below.
dbRefList.on('child_changed', function(snap) {
var len = this.grocerylist.length;
for(var i=len; i--;) {
if(this.grocerylist[i].key === snap.key) {
this.set(['grocerylist', i, 'selected'], snap.val().selected);
}
}
this.notifyPath('grocerylist', this.grocerylist.slice());
}.bind(this));
When the array is modified I want the template repeat-dom to trigger. I know this.set will not trigger array mutation sub properties but again I am not sure how to solve this. I done research and tried so many solutions.
I can force a render on the template dom-repeat but I would prefer the data binding way.
So this code (just the this.set you have in there now) should cause the value of grocerylist.i.selected to update inside the dom-repeat (assuming it's bound in there so it's actually showing up).
What behavior are you seeing? Are you trying to filter or sort the list based on the selected value? In that case, you might need to add observe="selected" on the dom-repeat.
(Also—have you confirmed that the child-changed callback is being called with the this value you expect—the element—rather than window or something else?)
You should be able to force a refresh by doing this.grocerylist = this.grocerylist.slice() or this.set('grocerylist', this.grocerylist.slice()); ... notifyPath doesn't work here because notifyPath doesn't change the value, it notifies the element about a prior change (the second argument is effectively ignored).

Binding to property of model attribute in Backbone.js

Disclaimer: I'm new to Backbone.js (coming from AngularJS), so I may have an inaccurate mental model of how this is supposed to work.
I have an object, characterNodes, which I'm making an attribute on my model. characterNodes looks something like this:
var characterNodes = {
character_1: {
stories: [// list of Stories]
},
character_2: {
stories: [// list of Stories]
}
...
}
My Backbone Model looks something like this:
var StoryGraph = joint.dia.Graph.extend({
initialize: function() {
// Call parent constructor
StoryGraph.__super__.initialize.apply(this, []);
this.set('characterNodes', characterNodes);
this.on('change:characterNodes', function() {
alert('test');
});
}
});
Each Story has a property "isUnlocked" which is changed elsewhere in the application. I want to fire an event (ie. that is, the alert 'test' should pop up) whenever this property is changed. With the code as it is above, the event never seems to fire.
I can't get a clear understanding from the Backbone docs whether this is supposed to work - does on('change:characterNodes') fire whenever any property (or sub-property, or sub-sub-property, etc) of characterNodes changes? Or only when the pointer to the object changes, that is, when it's replaced with another object? Or am I doing something else wrong? Thanks!
Backbone doesn't do any magic, basically, the change event is fired only if you set the "characterNodes" to a new object. If you're changing a nested property of that object, Backbone doesn't know it happened. You have three options: a) Change the whole object (e.g. by creating a copy), b) fire the change event manually (m.trigger("change:characterNodes")) whenever you change a nested property, c) Do not use nested objects for this. Have "character1_Stories" as a top level property.
Options c) is preferable. Try to keep properties in your models flat. Option a) is also fine but it has the disadvantage of having to copy the object. Option b) is not recommended. This is because Backbone keeps track of the previous value of the model properties (m.previous("characterNodes")). If you change a nested property, the previous value will have the same reference to the same object as the new value, therefore, it won't reflect its previous state.
Try to call a function instead define the function, and pass the third argument to keep the context call.
Something like this:
this.on('change:characterNodes', this.AlertSomething, this);
Hope it helps.

angularfire $bind on change appears to replace the object?

angularfire's three way binding is replacing the object on "change" instead of updating the object. The effect is if you are writing very simplistic examples it works fine but in the real world, you would like to reference that bound object in other objects. This is undesirable. For example:
var myComplexObject = {}:
var fbUrl = new Firebase(url);
$scope.items = $firebase(fbUrl);
$scope.items.$on("loaded", function() {
$scope.items.$bind($scope, "remoteItems").then( function(unbind){
myComplexObject.myData = $scope.remoteItems;
});
Initial state: myComplextObject.myData === $scope.remoteItems evals to: true;
This pseudo code will work ( 3way binding functionality ) up until there is a 'change' event ( which could be trigger by remote change ). Once the change occurrs, $scope.remoteItems will be populated but now the connection to myComplexObjext is broken. Hence:
Post change state: myComplextObject.myData === $scope.remoteItems evals to: false;
Any thoughts here? Is this a function of javascript? In C it would easier to implement with pointers, maybe this is not possible in javascript. I know that I could add a watch and re-assign but this is undesirable and unmanageable. As I have created a service that frontends all connectivity to firebase.
If I have missed something conceptually let me know.

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());

backbone.js set model internal attributes hash directly

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.

Resources