Set model value programmatically in Angular.js - angularjs

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.

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).

What is the right way to select by value in a dropdown?

I'm having a bit of an issue with Angular and selecting an item from a dropdown and making Angular update the model.
I've been searching through StackOverflow, but only with partial luck.
The problem is that when i'm manually setting a value on a model, my dropdown updates correct, but the model itself does not update;
$scope.setSelected = function(){
$scope.selected.id = 15;
//$scope.$apply();// $apply already in progress
}
From various answers on StackOverflow, I've found out that Angular does not know about this update and the suggested answer is to run either a $scope.$apply() or a $scope.$digest(), but both throw a $apply already in progress exception. I have a feeling that this is not the correct way for me to do it, since it doesn't make sense, that you have to trigger an event to select by value.
What is the correct way to select an item in a dropdown by a given value?
Full code example can be found at https://jsfiddle.net/c2x3jvut/
When clicking the "Select dinnerman" button, the dropdown updates correct, but the shown model and when clicking "Log selected" it only shows the selected model, but with an updated id.
You can use $filter to get whole object instead of each property.
$scope.setSelected = function(){
$scope.selected = $filter('filter')($scope.persons,15,'id')[0];
//$scope.selected.id = 15;
//$scope.$apply();// $apply already in progress
}
Here is the modified version
ng's select binds its value based on its ngModel, which is being manipulated incorrectly in the example. The correct method is to change the reference of the ngModel rather than the select's id:
$scope.setSelected = function () {
$scope.selected = $scope.persons[1]; // yes
// $scope.selected.id = 15; // no
};
Fiddle might have been updated to show the effects.
Not sure I understand your question, but still...
You may include in each entry of the dropdown:
...ng-click="setSelected(<value>)"...
and, of course, add a parameter to the function.
By the way, are you aware that in the fiddle example you are selecting ID 15 in function setSelected?
You are just updating the current model with an ID. But when you see carefully the name and age remains same. Further you need to update the whole object as below:
$scope.setSelected = function(){
$scope.selected.id = 15;
$scope.selected.name = "Dinner-man";
$scope.selected.age = 20;
}
Or anyother way to directly updates the object.

triggering $onChanges for updated one way binding

I'm really happy with the "new" $onChanges method you can implement in a component's controller. However it only seems to be triggered when the bound variable is overwritten from outside my component, not (for instance) when an item is added to an existing array
It this intended behaviour or a bug? Is there another way of listening to updates to my input bindings, besides doing a $scope.$watch on it?
I'm using Angular 1.5.3
First TL;DR
For an array that is bounded via one-way binding, a watch expression is added that does not check for object equality but uses reference checking. This means that adding an element to the array will never fire the '$onChanges' method, since the watcher will never be 'dirty'.
I've created a plnkr that demonstrates this:
http://plnkr.co/edit/25pdLE?p=preview
Click the 'add vegetable in outer' and 'change array reference in outer' and look at the 'Number of $onChanges invocation'. It will only change with the latter button.
Complete explanation
To fully grasp what is going on, we should check the angular code base. When a '<' binding is found, the following code is used to set up a watch expression.
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
attrs[attrName] = void 0;
}
if (optional && !attrs[attrName]) break;
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
// IMPORTANT PART //
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
// ------------- //
removeWatchCollection.push(removeWatch);
break;
The important part here is how the 'scope.$watch' expression is set up. The only parameters passed are the parsed expression and the listener function. The listener function is fired once the '$watch' is found dirty in the digest cycle. If it is fired, the listener will execute the 'recordChanges' method. This records an '$onChanges' callback task that will be executed in the '$postDigest' phase and notify all components that are listening for the '$onChanges' lifecycle hook to tell them if the value has changed.
What's important to keep in mind here, if the '$watcher' is never dirty, the '$onChanges' callback is not triggered. But even more importantly, by the way the '$watch' expression is created, it will NEVER be dirty, UNLESS the reference changes. If you wanted to check for equality between objects instead of reference, you should pass an extra third parameter that asks for this:
$watch: function(watchExp, listener, objectEquality, prettyPrintExpression)
As this is not the case here with the way the one way binding is set up, it will ALWAYS check for reference.
This means, if you add an element to an array, the reference is not changed. Meaning the '$watcher' will never be dirty, meaning the '$onChanges' method will not be called for changes to the array.
To demonstrate this, I've created a plnkr:
http://plnkr.co/edit/25pdLE?p=preview
It contains two components, outer and inner.
Outer has primitive string value that can be changed through an input box and an array that can be extended by adding an element or have its reference changed.
Inner has two one-way bounded variables, the value and the array. It listens for all changes.
this.$onChanges = setType;
function setType() {
console.log("called");
vm.callCounter++;
}
If you type into the input field, the '$onChanges' callback is fired every time. This is logical and expected, since a string is primitive so it cannot be compared by reference, meaning the '$watcher' will be dirty, and the '$onChanges' lifecycle hook fired.
If you click the 'Add vegetable in outer', it will execute the following code:
this.changeValueArray = function() {
vm.valueArray.push("tomato");
};
Here we just add a value to the existing bounded array. We're working by reference here, so the '$watcher' is not fired and there is no callback. You will not see the counter increment or the 'called' statement in your console.
Note: If you click the 'Add something to the array' inside the inner component, the array in outer component also changes. This is logical, since we are updating the exact same array by reference. So even though it is a one-way binding, the array can be updated from inside the inner component.
If you change the reference in the outer component by clicking 'Change array reference in outer', the '$onChanges' callback is fired as expected.
As to answer your question: Is this intended behaviour or a bug? I guess this is intended behaviour. Otherwise they would have given you the option to define your '<' binding in a way that it would check for object equality. You can always create an issue on github and just ask the question if you'd like.

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.

Understanding Backbone Model set, validate and change callbacks

The Backbone documentation says:
Model.set will fail if validation fails - it won't set the value therefore it won't trigger any callback. We can pass { silent: true } to Model.set - then it will set the value but won't trigger any callback neither.
So,
Why does Backbone Model require a valid state to simply set an attribute value? What if we want to set attributes as the user interacts with the UI, but the model is not valid yet? It means change callbacks are unavailable unless we pass { silent: true } then manually trigger the change?!
Please say you know a better way of handling this :)
I'm not sure how to answer the Why questions but you could say that there are arguments for why it is good that set runs validations. For instance, it makes it dead simple to do client side validation in real time.
If your problem could be solved by only validating the value that is currently being changed by the user, you can do that by combining your validate method with the hasChanged method.
For example something like this:
Backbone.Model.extend({
defaults : { name : "" },
validate : function (attrs) {
var errors = {};
if(this.hasChanged("name") && attr.name.length == 0) {
errors.name = "Need a name yo!";
}
//...
if(_.keys(errors).length > 0) {
return errors;
}
}
})
In Backbone whenever you call set on model , It keeps track of what attributes of model has been changed and what attributes are newly added.Calling validate allows it be more efficient in doing it .Passing {silent:true} as options in set function causes validate and change to not execute so if doesnt fire any change events.
If you want to set attributes as the user interacts with the UI, but the model is not valid yet
In this case you can set the change in a plain object make sure object keys are sames as model's attribute and then at some point just set in your model.
var uiChanges = {name:'x'}; //just fill it with your changes
ur_model.set(uiModel); //then set it ,this way it fires change events only once
To check the diff between your plain object and model you can use the
ur_model.changedAttributes(uiChanges);
changedAttributes -
Return an object containing all the attributes that have changed, or false if there are no changed attributes.
You can further use it save only those attributes that have changed rather than saving entire model again.

Resources