How to watch a parent controller variable changed by a child - angularjs

I'm trying to save user.preferences everytime they change, so, user.preferences is an App variable.
HTML
...
ng-controller="AppController as App"
...
On app.js
vm.preferences = localStorage.preferences === undefined ? initialPreferences : JSON.parse(localStorage.preferences) ;
$scope.$watch('App.preferences',function() {
localStorage.preferences = JSON.stringify(vm.preferences);
});
The child controller CAN change the variable but the watcher doesn't trigger. Any clue? Thanks in advance

Googled a little more, if you wan't to watch an entire Object you must specify the third argument of $watch: objectEquality.
From AngularJS Docs:
Compare for object equality using angular.equals instead of comparing for reference equality.
Watching an entire object:
$scope.$watch('App.preferences',function() {
localStorage.preferences = JSON.stringify(vm.preferences);
},true);

Related

How to detect change in model for multidimensional array in angular js

I want to fire an event when my multidimensional scope variable get changed i.e. element added, removed etc. I tried to do so using $scope.$watch and $scope.$watchCollection, but its not working.
This is the scenario.
$scope.variable=[{ID:1,text:'abc'},{ID:1,text:'abc'}];
$scope.content='';
$scope.$watch('$scope.variable',function(){
$scope.content=$scope.variable[0].text;
});
Any help ?
add true as an argument
$scope.$watch('variable',function(){
$scope.content=$scope.variable[0].text;
}, true);
NB: no need for $scope in argument

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.

How to display the value of other controllers in one view dynamically

I am working on an application were index page and inside that I am showing multiple views, In index nav bar I have to show notification count and User name which comes from 2 different controllers,
I am able to display the notification count and User name successfully, but the issue is the values are not changing dynamically.
We need to refresh the page for the new values.
What can I do in this situation can any one please guide me.
I'm guessing you're watching the value directly and not by some object wrapper. In this case javascript isn't actually updating the variable, but assigning a complete new one. Anything out of the function scope that updates the variable will never receive the new value.
The solution is simple; wrap the value in an object and share/inject that object.
angular.module('myApp')
.value('myPageState', {
notificationCount: 0
});
angular.module('myApp')
.controller('myController', function($scope, myPageState) {
$scope.myPageState = myPageState;
});
<div class="my-notification-thingy"> {{ myPageState.notificationCount }} </div>
You can achieve it by:
Maintain those values in rootScope so that you
will have the two way binding.
Making use of emit to notify the parent controller about value changes. This will work only if those two controllers are present in child elements.
In child controllers fire event on value update:
$scope.$emit('valueChanged', {value: val});
In parent controller receive event value:
$scope.$on('valueChanged', function(event, args) {
console.log(args.value);
});

Binding variables from Service/Factory to Controllers

I have a variable that will be used by one or more Controllers, changed by Services.
In that case, I've built a service that keeps this variable in memory, and share between the controllers.
The problem is: Every time that the variable changes, the variables in the controllers aren't updated in real time.
I create this Fiddle to help. http://jsfiddle.net/ncyVK/
--- Note that the {{countService}} or {{countFactory}} is never updated when I increment the value of count.
How can I bind the Service/Factory variable to $scope.variable in the Controller? What I'm doing wrong?
You can't bind variables. But you can bind variable accessors or objects which contain this variable. Here is fixed jsfiddle.
Basically you have to pass to the scope something, which can return/or holds current value. E.g.
Factory:
app.factory('testFactory', function(){
var countF = 1;
return {
getCount : function () {
return countF; //we need some way to access actual variable value
},
incrementCount:function(){
countF++;
return countF;
}
}
});
Controller:
function FactoryCtrl($scope, testService, testFactory)
{
$scope.countFactory = testFactory.getCount; //passing getter to the view
$scope.clickF = function () {
$scope.countF = testFactory.incrementCount();
};
}
View:
<div ng-controller="FactoryCtrl">
<!-- this is now updated, note how count factory is called -->
<p> This is my countFactory variable : {{countFactory()}}</p>
<p> This is my updated after click variable : {{countF}}</p>
<button ng-click="clickF()" >Factory ++ </button>
</div>
It's not good idea to bind any data from service,but if you need it anymore,I suggest you those following 2 ways.
1) Get that data not inside your service.Get Data inside you controller and you will not have any problem to bind it.
2) You can use AngularJs Events feature.You can even send data to through that event.
If you need more with examples here is the article which maybe can help you.
http://www.w3docs.com/snippets/angularjs/bind-value-between-service-and-controller-directive.html

$watch not being triggered on array change

I'm trying to figure out why my $watch isn't being triggered. This is a snippet from the relevant controller:
$scope.$watch('tasks', function (newValue, oldValue) {
//do some stuff
//only enters here once
//newValue and oldValue are equal at that point
});
$scope.tasks = tasksService.tasks();
$scope.addTask = function (taskCreationString) {
tasksService.addTask(taskCreationString);//modifies tasks array
};
On my view, tasks is clearly being updated correctly as I have its length bound like so:
<span>There are {{tasks.length}} total tasks</span>
What am I missing?
Try $watch('tasks.length', ...) or $watch('tasks', function(...) { ... }, true).
By default, $watch does not check for object equality, but just for reference. So, $watch('tasks', ...) will always simply return the same array reference, which isn't changing.
Update: Angular v1.1.4 adds a $watchCollection() method to handle this case:
Shallow watches the properties of an object and fires whenever any of the properties change (for arrays this implies watching the array items, for object maps this implies watching the properties). If a change is detected the listener callback is fired.
Very good answer by #Mark. In addition to his answer, there is one important functionality of $watch function you should be aware of.
With the $watch function declaration as follows:
$watch(watch_expression, listener, objectEquality)
The $watch listener function is called only when the value from the current watch expression (in your case it is 'tasks') and the previous call to watch expression are not equal. Angular saves the value of the object for later comparison. Because of that, watching complex options will have disadvantageous memory and performance implications. Basically the simpler watch expression value the better.
I would recommend trying
$scope.$watch('tasks | json', ...)
That will catch all changes to the tasks array, as it compares the serialized array as a string.
For one dimensional arrays you may use $watchCollection
$scope.names = ['igor', 'matias', 'misko', 'james'];
$scope.dataCount = 4;
$scope.$watchCollection('names', function(newNames, oldNames) {
$scope.dataCount = newNames.length;
});

Resources