Where should I put AngularJS scope $watchers? - angularjs

I'm trying to get changes to an AngularJS scope to trigger actions in a model. To do this I use $scope.$watch() in my item controller, and plug those controllers into directives. The problem I'm having is that when an item appears more than once on a page I'm getting multiple watchers for the same piece of data.
For example:
angular.module("MyApp", [])
.factory('ItemsModel', function() {
var item1 = { triggers: 0, catches: 0 };
var item2 = { triggers: 0, catches: 0 };
return {
getItems: function() {
return [item1, item2, item1];
}
};
})
.controller('AppCtrl', function($scope, ItemsModel) {
$scope.items = ItemsModel.getItems();
})
.controller('ItemCtrl', function($scope) {
$scope.trigger = function() {
$scope.item.triggers++;
};
$scope.$watch('item.triggers', function(newValue, oldValue) {
if(newValue != oldValue)
$scope.item.catches++;
});
})
.directive('item', function() {
return {
scope: {
item: "="
},
controller: 'ItemCtrl'
};
});
Here item1 appears twice in the items array, so I end up with two controllers watching the same piece of data. If triggers were changes to a property, and catches were saves back to a server, then I'd be saving each change to item1 twice. If an item appeared three times, it would save three times.
I feel like the change listener should go in the model, but models don't have any access to the scope. Iterating over each item and adding behavior in the AppCtrl doesn't make a lot of sense to me. So where to the watchers go?
I've also put this in a fiddle here: http://jsfiddle.net/nicholasstephan/p56MT/3/
Thanks

I haven't used the controller specified in the directive like you have here, but an alternative way is to just setup a watch in the directive on item. This way the watch will only get tripped once for the change on item in the directive.

Related

Update $scope variable that is used in ng-repeat, from other controller

I have a controller HomeworkPageController where I get all the topics from MongoDB using method getAllMainTopics from TopicService. $scope.topics is then used to show all topics. I have a button that open a modal where a new topic is add in MongoDB. The modal is using another controller AddTopicController. How can I update $scope.topics from HomeworkPageController in AddTopicController ? I want to do this because after I close the modal, the list of all topics should be refreshed, it must contain the topic that has been added. I tried to use HomeworkPageController in AddTopicController and then call the method getAllMainTopics but the $scope.topics from html is not updated. Thanks.
Here is HomeworkPageController:
app.controller('HomeworkPageController', ['$scope','TopicService',
function ($scope, TopicService) {
$scope.topics = [];
$scope.getAllMainTopics = function () {
TopicService.getAllMainTopics('homework')
.success(function(data) {
$scope.topics = data;
}
$scope.addTopic = function () {
ModalService.openModal({
template: "templates/addTopic.html",
controller: 'AddTopicController'
});
}
]);
Here is AddTopicController:
app.controller('AddTopicController', ['$scope','$controller', '$timeout','TopicService', '$modalInstance',
function ($scope, $controller, $timeout,TopicService, $modalInstance) {
var homeworkPageController = $scope.$new();
$controller('HomeworkPageController',{$scope : homeworkPageController });
$scope.save = function() {
TopicService.saveTopic(data)
.success(function(result){
homeworkPageController.getAllMainTopics();
$modalInstance.close();
})
}
}]);
Here is the view where I use $scope.topics:
<div class="homework-content-topic-list" ng-repeat="topic in topics">
<label> {{ topic.subject }} </label>
</div
You should probably keep your list of topics in a service and then inject that service into both controllers. This way you would be able to access and update the topics in both of your controllers. It could look something like
app.controller('HomeworkPageController', ['$scope','TopicService',
function ($scope, TopicService) {
$scope.topics = TopicService.topics;
// Do stuff here
]);
Then you just need to modify your TopicService to have it's methods work on the stored object.
you can solve this by two methods
1)look at the example given in ui-bootstrap's website. They have given an example that will suit your requirement - plunker. There are three items in the modal - item1, item2, item3. If you select one of those items and click 'ok', the selected item is sent to the main controller through "resolve" attribute in the $scope.open function.
2)You can write a custom service that acts as a bridge to the two controllers and you can write getter and setter methods in the service.
angular.module('app').service('popupPageService', function() {
var topics;
var setDetails = function(param) {
topics = param;
};
var getDetails = function() {
return topics;
};
return {
setDetails: setDetails,
getDetails: getDetails,
};
});
call the setDetails function in the AddTopicController and once when you come out of the modal, update your $scope.topics in HomeworkPageController by pushing the new value added (getDetails)

Angularjs - how to watch for changes in DOM and apply a new style to it?

I have a very simple angular app that pushes data in without refreshing the page using setInterval. Now, how can I listen or watch for new data/changes, so that if the new value/data differ from the previous one a new css style will be applied to that particular new value (for example it will change the font color to red).
My code is below:
view:
<h1>{{title}}</h1>
<ul>
<li ng-repeat="friend in friends"><strong>Name: </strong>{{friend.name}} : {{friend.username}}</li>
</ul>
data:
angular
.module ('myApp')
.factory ('Friends', ['$http', function ($http) {
return {
get: function () {
return $http.get ('users.json').then (function (response) {
return response.data;
});
}
};
}]);
Controller:
angular
.module ('myApp')
.controller ('summaryCtrl', ['$scope', 'Friends', function ($scope, Friends) {
$scope.title = "Friends";
$scope.loadData = function () {
Friends.get ().then (function (data) {
$scope.friends = data;
});
};
//initial load
$scope.loadData();
var timer = setInterval(function(){
$scope.loadData();
},5000);
}]);
many thanks
Use $interval instead of setInterval, since it triggers a digest loop it will update your data automatically
angular
.module ('myApp')
.controller ('summaryCtrl', ['$scope', 'Friends', '$interval' function ($scope, Friends, $interval) {
$scope.title = "Friends";
$scope.loadData = function () {
Friends.get ().then (function (data) {
$scope.friends = data;
});
};
//initial load
$scope.loadData();
var timer = $interval(function(){
$scope.loadData();
},5000);
}]);
My recommendation would be to manually compare each friend item and assign a changeFlag whenever the data has changed.
To start, keep a reference to the old data and whenever new data comes in, compare the two, like this:
var oldData = undefined; // Somewhere in initialization.
...
Friends.get().then(function (response) {
var newData = response;
if (oldData && JSON.stringify(oldData) != JSON.stringify(newData))
{
$scope.friends = newData;
$scope.$apply(); // Force the entire page to be redrawn. You can do style bindings to change a style.
}
oldData = response;
}
This will get you half-way to your goal. You will only be refreshing the page whenever something has changed, but there is no indication as to which friend has changed. I imagine this is what you are attempting to accomplish. You want to highlight those friends that have changed.
To do this we could simply create a comparison function that applies a flag to each object that has changed. However, this code assumes that some property on each friend remains fixed. This is normally why an id property is given to each item in a database. I'm going to assume you have an id property for each friend that never changes regardless if their name, age, email, etc. does.
var changeFlagFriendsObjects = function(oldData, newData) {
var idToOldDataMap = {};
oldData.forEach(function (friend) {
idToOldDataMap[friend.id] = friend;
});
newData.forEach(function (friend) {
var oldFriendData = idToOldDataMap[friend.id];
friend.changeFlag = JSON.stringify(oldFriendData) != JSON.stringify(friend);
});
};
// You would call changeFlagFriendsObjects in the other example above. I'm sure this would be easy to figure out how to place.
Regarding binding styles in the HTML to properties, see here.
An example would be like the following:
<!-- Apply the 'highlight' style when changeFlag is true -->
<li ng-repeat="friend in friends" ng-style="highlight={changeFlag: true}"><strong>Name: </strong>{{friend.name}} : {{friend.username}}</li>

How to access/update $rootScope from outside Angular

My application initializes an object graph in $rootScope, like this ...
var myApp = angular.module('myApp', []);
myApp.run(function ($rootScope) {
$rootScope.myObject = { value: 1 };
});
... and then consumes data from that object graph (1-way binding only), like this ...
<p>The value is: {{myObject.value}}</p>
This works fine, but if I subsequently (after page rendering has completed) try to update the $rootScope and replace the original object with a new one, it is ignored. I initially assumed that this was because AngularJS keeps a reference to the original object, even though I have replaced it.
However, if I wrap the the consuming HTML in a controller, I am able to repeatedly update its scope in the intended manner and the modifications are correctly reflected in the page.
myApp.controller('MyController', function ($scope, $timeout) {
$scope.myObject = { value: 3 };
$timeout(function() {
$scope.myObject = { value: 4 };
$timeout(function () {
$scope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Is there any way to accomplish this via the $rootScope, or can it only be done inside a controller? Also, is there a more recommended pattern for implementing such operations? Specifically, I need a way to replace complete object graphs that are consumed by AngularJS from outside of AngularJS code.
Thanks, in advance, for your suggestions,
Tim
Edit: As suggested in comments, I have tried executing the change inside $apply, but it doesn't help:
setTimeout(function() {
var injector = angular.injector(["ng", "myApp"]);
var rootScope = injector.get("$rootScope");
rootScope.$apply(function () {
rootScope.myObject = { value: 6 };
});
console.log("rootScope updated");
}, 5000);
Except for very, very rare cases or debugging purposes, doing this is just BAD practice (or an indication of BAD application design)!
For the very, very rare cases (or debugging), you can do it like this:
Access an element that you know is part of the app and wrap it as a jqLite/jQuery element.
Get the element's Scope and then the $rootScope by accessing .scope().$root. (There are other ways as well.)
Do whatever you do, but wrap it in $rootScope.$apply(), so Angular will know something is going on and do its magic.
E.g.:
function badPractice() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.scope().$root; // 2
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice :(';
});
}
See, also, this short demo.
EDIT
Angular 1.3.x introduced an option to disable debug-info from being attached to DOM elements (including the scope): $compileProvider.debugInfoEnabled()
It is advisable to disable debug-info in production (for performance's sake), which means that the above method would not work any more.
If you just want to debug a live (production) instance, you can call angular.reloadWithDebugInfo(), which will reload the page with debug-info enabled.
Alternatively, you can go with Plan B (accessing the $rootScope through an element's injector):
function badPracticePlanB() {
var $body = angular.element(document.body); // 1
var $rootScope = $body.injector().get('$rootScope'); // 2b
$rootScope.$apply(function () { // 3
$rootScope.someText = 'This is BAD practice too :(';
});
}
After you update the $rootScope call $rootScope.$apply() to update the bindings.
Think of modifying the scopes as an atomic operation and $apply() commits those changes.
If you want to update root scope's object, inject $rootScope into your controller:
myApp.controller('MyController', function ($scope, $timeout, $rootScope) {
$rootScope.myObject = { value: 3 };
$timeout(function() {
$rootScope.myObject = { value: 4 };
$timeout(function () {
$rootScope.myObject = { value: 5 };
}, 1000);
}, 1000);
});
Demo fiddle

Unsure why changes to a factory property are not being pushed to the page

This is a simplistic example of a problem I am having. I am clearly missing something in my understanding of Angular.
A Plunker is here: http://plnkr.co/edit/VLqA22dDTgk5PyPlCOGH?p=preview
And a copy-paste of the pertinent bits below:
<div ng-controller="myController">
<div>message: <label ng-model="message"></label></div>
<div></div><button ng-click="start()">Get Message</button></div>
</div>
var app = angular.module("app", []);
app.service('GetMessage', function() {
var message;
var start = function () {
this.message = 'Hello World';
};
return {
message: this.message,
start: start
}
});
app.controller('myController', function ($scope, GetMessage) {
$scope.message = GetMessage.message;
$scope.start = function () {
GetMessage.start();
console.warn('started..');
};
});
I would expect that the label directive would be 2-way bound to the factory's message property, so that when the start() function is called and the message is updated, that the page would be too.
To update the label in this way, do I need to broadcast an event to $rootScope, listen for it in the controller, and then update the label? It seems a very manual way of doing it.. surely there is a better way.
Thank you.
You are currently using a primitive data type, which means the following line will copy the value into a new variable:
$scope.message = GetMessage.message;
Updating one will not affect the other.
An easy solution is to use an object instead:
var message = { value: '' };
var start = function() {
message.value = 'Hello World';
};
And:
$scope.message = GetMessage.message;
Now the reference to the object would be copied into a new variable instead and both would refer to the same object.
Another issue is that you are using ngModel on a label to display the value, which will not work. ngModel is normally used on input, select and textarea elements.
You can instead use ng-bind:
<label ng-bind="message.value"></label>
Or the less verbose shortcut:
<label>{{message.value}}</label>
Demo: http://plnkr.co/edit/rUgN94DAIOfQGI8tr9kl?p=preview
If you prefer to keep using primtive values you need to handle it another way. For example by using events like you mentioned. Another solution is to register a watcher to watch for changes and update the scope variable:
app.controller('myController', function($scope, GetMessage) {
var watchExpression = function() {
return GetMessage.message;
};
var listener = function(newValue, oldValue) {
if (newValue === oldValue) return;
$scope.message = newValue;
}
$scope.$watch(watchExpression, listener);
$scope.start = function() {
GetMessage.start();
console.warn('started..');
};
});
Demo: http://plnkr.co/edit/klSl1DCUlQI3Z5ih16sq?p=preview

How to watch service data in AngularJS?

myApp.directive('myView', ['myService',function($scope) {
return {
restrict : 'C',
template : '',
templateUrl : 'myView.html',
controller : function($scope,myService){
$scope.items = myService.$data;
$scope.$watch('myService.$data' , function(newVal, oldVal, scope) {
console.log("Watching items ...");
});
},
};
}]);
I am using the above code, where I want to watch myService.$data which is defined in its service. $watch gets called first time when controller is loaded, Elsewhere in the code I update the service variable $data. This gets reflected in UI as I am using ng-repeat directive and binding with $data.
According to my understating before UI gets updated, this particular watch has to be called again, but it is not. So I assume I am not using watch properly.
Just do:
$scope.$watch(
function() { return myService.$data; },
function(...) { /*same*/ }
);
BUT:
If $data is an object you may want to add ,true to the watch, so that you get notified even if an inner property changes:
$scope.$watch(
function() { return myService.$data; },
function(...) { /*same*/ },
true
);
The declarations in your code are wrong. It should be (only changes shown):
myApp.directive('myView', ['myService',function(myService) { // NOT $scope
...
controller: ["$scope", function($scope) { // YOU GET myService FROM THE DIRECTIVE

Resources