When working on a project, as these things tend to happen, we came across a situation where we were stumped on how to update certain UI elements when other things were done. For example, the navigation contains a counter of how many pending activities are due today. At any point in time during usage of the app, a user might schedule an activity for later today, and the count section would need to call the API to generate a count and the drop-down items associated with it.
How can I make a navigation controller pull the new list of activities when the main controller makes a change?
See this code for an example.
<div ng-app="TestApp">
<nav ng-controller="navigationController">
<p>The navigation count is: {{items.length}}</p>
</nav>
<div ng-controller="mainController">
<p>The main count is: {{items.length}}</p>
<p>
<button ng-click="addItem()" type="button">Add item.</button>
</p>
</div>
</div>
<script>
var app = angular.module('TestApp', []);
app.factory("api", function() {
return {
update: function() {
return ['a', 'b', 'c', 'd', 'e'];
}
};
});
app.factory("sharedFactory", function(api) {
var obj = {};
obj.items = ["a"];
obj.update = function() {
obj.items = api.update();
};
return obj;
});
app.controller("mainController", function(sharedFactory, $scope) {
$scope.items = sharedFactory.items;
$scope.addItem = function() {
sharedFactory.update();
};
});
app.controller("navigationController", function(sharedFactory, $scope) {
$scope.items = sharedFactory.items;
});
</script>
Our current solution was to create a callback service that other controllers could subscribe to, and then when an activity was created have those callbacks run as needed. This works nicely, but I'm nervous that I'm "doing it wrong".
We're switching to the Angular UI Router, now, so I'm curious if there's a better way of doing so in it. Right now our navigation handler is a stateless controller that hooks into our callback service still.
A nice way to handle this could be to use $scope.$on to listen for events, and $scope.$emit to fire an event going up the scope or $scope.$broadcast to fire an even going down the scope.
In each piece of the UI that needs to be updated can be listening with $scope.$on and update itself when an event is fired, like your user scheduling an event for later today.
Angular docs for $on, $emit and $broadcast
Though I generally think that registering scope values on a controller with a service is the best way to accomplish another option would be to use a factory and set a property of that on scope.
angular.module('app').factory('myService', function() {
var myService = {};
service.count = 0;
/// other service functions
return myService;
}
angular.module('app').controller('myController', function(myService) {
this.count = myService.count;
}
However you feel about MVC, you could use angular's internals to automatically do this:
https://jsfiddle.net/gkmtkxpm/
var myApp = angular.module('myApp', []);
myApp.factory('counter', function() {
return {
count: 0
};
});
myApp.controller('CounterController', function (counter) {
var vm = this;
vm.counter = counter;
vm.increment = function() {
vm.counter.count = vm.counter.count + 1;
};
});
edit:
Concerning your updated question, see the updated fiddle:
https://jsfiddle.net/gkmtkxpm/1/
Related
There are a lot of references that discuss this, but I just need someone to confirm if this is right or not. If i have a service which I want to share information with a controller, and the controller should update on changes to the service I need to return an object from the service, like:
.factory('myService', ['$http', function($http) {
var data = {};
var service = {
constant: 1234,
getData: function() {
return data;
},
doCalculation: function() {
service.constant = data.const*25;
},
requestData: function() {
return $http.get('/blah')
.then(function( response ) {
data = response.data;
}
}
}
return service;
}])
Now to pass it to a controller for use and have it update if requestData is invoked again during maybe a route resolve() I would and can't do:
.controller('myCtrl', ['myService', function(myService) {
var self = this;
// PART 1
self.data = myService.constant; // this is not bound and will not update, yes?
self.data1 = myService.getData(); // this is not bound and will not update, yes?
// So, the above would be assigned or invoked only once on init of controller and
// would have to reset manually by assigning either a value or the result of the
// the function call again
self.myService = myService; // pass entire service
// Now, in controller functions or in the UI I can invoke the functions or access
// values, and those results will be bound and update on changes to the service
// since I've passed it in its entirety
self.getData = function() {
return self.myService.getData();
}
// PART 2
self.getData = myService.getData; // would you ever do this?
// You wouldn't have to pass the entire service if it had a bunch of different
// parts that maybe you didn't want to be available...
}]);
PART 1
<div ng-show="myCtrl.myService.doCalculation() === someNumber">You can't see me unless doCalculation resolves to someNumber</div>
PART 2
<div ng-show="myCtrl.getData() === anotherNumber">Would this work? and would you want to do this?</div>
I just can't seem to get the concept of how sharing data between a service(s) and a controller(s) works, and when it is working and when it won't. If all you can do is say correct, wrong, wrong, oh man so wrong, that's kewl, but if you can also say and this is why, I'd be ecstatic to put this away as finally resolved so I don't keep questioning it.
I wouldn't go too far here..
A controller is your view's helper. You need to generate vars and functions on your scope to help your view accomplish things.
Your business model however, is something that you would like to have one reference.
What I do is create my business model on a service, so multiple entities can share it(e.g. other services, directives, controllers etc.).
When my controller kicks in, I add a pointer to the model from the service and use the same reference between them. I bind the models properties to the view.
So:
A controller has it's own methods(dont share the service's methods). A controllers method should be short and use a service method as a helper.
A controller should have a reference to the business model which is created by a service. All your ajax calls should come from the service and populate\send the model that the service is holding.
A controller should have basic view functions(e.g. decide which css class to apply to an element). When you submit a form, the controller function should call the service's submit which will perform you ajax call.
Example:
Html:
<div ng-app="app">
<div ng-controller="myCtrl">
<input type="text" ng-model="Model.propA" />
<br/>
<input type="text" ng-model="Model.propB" />
<div ng-show="ShouldShowSecondDiv()">Second Div.</div>
<br/>
<button ng-click="SubmitForm()">Submit</button>
</div>
</div>
JS:
var app = angular.module('app', []);
app.controller('myCtrl', function ($scope, myService) {
// simple controller "view method".
$scope.ShouldShowSecondDiv = function () {
return $scope.Model.propB > 12;
};
// "complex" "non-view method" -> use service.
$scope.SubmitForm = function () {
myService.SubmitModelToServer();
};
// get the ref. from the service.
$scope.Model = myService.GetModel();
});
app.service('myService', function () {
this.Model = {};
// perform an ajax to get the model or whatever.
this.GetModel = function () {
this.Model = {
propA: 'Im prop A',
propB: 12
};
return this.Model;
};
// submit it to the server via $http. Check the log and you will see the binding(if you changed a value in the view).
this.SubmitModelToServer = function () {
console.log("ajax or whatever....submitted");
console.log(this.Model);
};
});
JSFIDDLE.
So let's say I have a controler that dppends on a news service. Whenever news is published, the controller would like to diisplay the latest news. I'd rather not use $broadcast and $on, because this does weird things to the way components are coupled. Components which don't depend on news could still listen for these events.
So, here's what I'd like to be able to do:
myApp.controller("myController", ["news", function(news){
news.onPublish.addListener(function(story){
... Show the latest juicy story.
});
}]);
news.onPublish would be an Event instance, with Event defined as follows:
Event = function(){
var listeners = [];
this.addListener = function(l){
listeners.push(l);
}
this.trigger = function(){
var params = arguments
listeners.map(function(l){
l.apply(undefined, params);
});
}
return this
}
Is this a good way to implement communication between services and other components? Also, would it be good to call $rootScope.$apply at the end of Event.trigger so that any changes the listeners made will be picked up?
The best is to write common data storing factories that do the job for you. Here's a working example: http://jsfiddle.net/9L5xL8sx/ which shows how this works. The NewsService factory can be used across several Angular modules, and within the same module, as shown in my example.
Here's the JS:
var app = angular.module("TestSharing", []);
app.factory("NewsService", [function() {
var articles = [];
var makeNews = function (text) {
articles.push({text: text});
};
var getNews = function() {
return articles;
};
return {
get: getNews,
make: makeNews
};
}]);
app.controller("FirstCtrl", ["$scope", "NewsService", function($scope, NewsService) {
$scope.articles = function () {
return NewsService.get();
};
}]);
app.controller("SecondCtrl", ["$scope", "NewsService", function($scope, NewsService) {
$scope.articletext = "";
$scope.submit = function () {
NewsService.make($scope.articletext);
};
}])
The HTML:
<div ng-app="TestSharing">
<div ng-controller="FirstCtrl">
<span ng-repeat="article in articles()">{{article.text}}<br/></span>
</div>
<div ng-controller="SecondCtrl">
<input type="text" ng-model="articletext"/>
<button ng-click="submit()">Make some news</button>
</div>
</div>
Also, services like these can also share events. For example, if you'd used the factory to expose an object called somethingNew, which merely contained if something new had happened in one of the Controllers it had been shared in, you could achieve the same effect. The idea would be to only listen for changes where you'd want (using something like $scope.$watch(NewsService.somethingNew, function (now, then) {…})) and that would be just as easy.
Not able to figure out what the bug in this code is.I've tried to only post the relevant parts of the code here.
Controller
myApp.controller('MessageCtrl', function ($scope, notificationService, $rootScope) {
$scope.notificationService = notificationService;
$scope.msgCount = 0;
$scope.notificationService.subscribe({channel : 'my_channel'});
$rootScope.$on('pubnub:msg',function(event,message){
$scope.msgCount = $scope.msgCount + 1;
//$scope.$digest();
});
});
My Notification Angular Service
myApp.factory('notificationService',['$rootScope', function($rootScope) {
var pubnub = PUBNUB.init({
publish_key : '..',
subscribe_key : '..'
});
var notificationService = {
subscribe : function(subscription) {
pubnub.subscribe({
channel : subscription.channel,
message : function(m){
$rootScope.$broadcast('pubnub:msg', m);
}
});
}
};
return notificationService;
}]);
And the template :
<div>
Count = {{msgCount}}
</div>
The problem :
Using console logs & using karma tests I have confirmed that the $rootScope.$on method in MessageCtrl is getting called when I do a $broadcast from Notification Service. And that the msgCount variable is getting incremented. However, I don't see the updated value being reflected in the template without running a $scope.$digest() . I am pretty sure I shouldn't be needing to have to call $scope.$digest , ie Angular should be providing me this binding.
Interestingly, when I tried a $rootScope.$broadcast from another controller, the msgCount in the template got incremented without having to call $scope.$digest().
Can anyone kindly help me here. Thank you.
Update
Thanks to Peter and looking at the google group discussion, wrapping the $broadcast in an $apply did the trick.
$rootScope.$apply(function(){
$rootScope.$broadcast('pubnub:question', m);
});
It seems that your $broadcast happens outside AngularJS and you need to notify your app about it with calling $apply(), but better do it in the notificationService.
As for $broadcast and $on trigger a apply/digest you can read in this post. Brief overview of AngularJs source files make me sure that $broadcast does not auto-apply changes (look here ). $broadcast just calling listeners and nothing else.
Please, take a look at this simple example on jsFiddle .
The template
<div ng-controller="myCtrl">
<p>Count: {{ count }}</p>
<button ng-click="fireEvent()">Fire Event</button>
</div>
The controller
angular.module("app", [])
.controller('myCtrl', function($scope, $rootScope, notificationService) {
$scope.count = 0;
notificationService.subscribe();
$rootScope.$on('event', function() {
console.log("event listener");
$scope.count++;
});
$scope.fireEvent = function() {
// it is ok here due to ngClick directve
$rootScope.$broadcast('event', true);
};
})
And factory
.factory('notificationService',['$rootScope', function($rootScope) {
var notificationService = {
subscribe : function() {
setInterval(function(){
console.log("some event happend and broadcasted");
$rootScope.$broadcast('event', true);
// angular does not know about this
//$rootScope.$apply();
}, 5000);
}
};
return notificationService;
}]);
Of course in both cases you will see that event listener fires, but ngClick fires $digest and your notificationService does not.
Also you can get some info about sources that will start the digest cicle in this nice answer https://stackoverflow.com/a/12491335/1274503
I am trying to call a method of second controller in first controller by using scope variable. This is a method in my first controller:
$scope.initRestId = function(){
var catapp = document.getElementById('SecondApp');
var catscope = angular.element(catapp).scope();
catscope.rest_id = $scope.user.username;
catscope.getMainCategories();
};
I am able to set the value of rest_id but I cannot call getMainCategories for some reason. The console shows this error:
TypeError: Object # has no method 'getMainCategories'
Is there a way to call the above method?
Edit:
I used the following approach to load two apps at the same time;
angular.bootstrap(document.getElementById('firstAppID'), ['firstApp']);
angular.bootstrap(document.getElementById('secondAppID'), ['secondApp']);
I could definitely use a service here, but I wanted to know if there are any other options to do the same!
The best approach for you to communicate between the two controllers is to use events.
Scope Documentation
In this check out $on, $broadcast and $emit.
In general use case the usage of angular.element(catapp).scope() was designed for use outside the angular controllers, like within jquery events.
Ideally in your usage you would write an event in controller 1 as:
$scope.$on("myEvent", function (event, args) {
$scope.rest_id = args.username;
$scope.getMainCategories();
});
And in the second controller you'd just do
$scope.initRestId = function(){
$scope.$broadcast("myEvent", {username: $scope.user.username });
};
Edit: Realised it was communication between two modules
Can you try including the firstApp module as a dependency to the secondApp where you declare the angular.module. That way you can communicate to the other app.
Here is good Demo in Fiddle how to use shared service in directive and other controllers through $scope.$on
HTML
<div ng-controller="ControllerZero">
<input ng-model="message" >
<button ng-click="handleClick(message);">BROADCAST</button>
</div>
<div ng-controller="ControllerOne">
<input ng-model="message" >
</div>
<div ng-controller="ControllerTwo">
<input ng-model="message" >
</div>
<my-component ng-model="message"></my-component>
JS
var myModule = angular.module('myModule', []);
myModule.factory('mySharedService', function($rootScope) {
var sharedService = {};
sharedService.message = '';
sharedService.prepForBroadcast = function(msg) {
this.message = msg;
this.broadcastItem();
};
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
return sharedService;
});
By the same way we can use shared service in directive. We can implement controller section into directive and use $scope.$on
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
And here three our controllers where ControllerZero used as trigger to invoke prepForBroadcast
function ControllerZero($scope, sharedService) {
$scope.handleClick = function(msg) {
sharedService.prepForBroadcast(msg);
};
$scope.$on('handleBroadcast', function() {
$scope.message = sharedService.message;
});
}
function ControllerOne($scope, sharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'ONE: ' + sharedService.message;
});
}
function ControllerTwo($scope, sharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'TWO: ' + sharedService.message;
});
}
The ControllerOne and ControllerTwo listen message change by using $scope.$on handler.
Each controller has it's own scope(s) so that's causing your issue.
Having two controllers that want access to the same data is a classic sign that you want a service. The angular team recommends thin controllers that are just glue between views and services. And specifically- "services should hold shared state across controllers".
Happily, there's a nice 15-minute video describing exactly this (controller communication via services): video
One of the original author's of Angular, Misko Hevery, discusses this recommendation (of using services in this situation) in his talk entitled Angular Best Practices (skip to 28:08 for this topic, although I very highly recommended watching the whole talk).
You can use events, but they are designed just for communication between two parties that want to be decoupled. In the above video, Misko notes how they can make your app more fragile. "Most of the time injecting services and doing direct communication is preferred and more robust". (Check out the above link starting at 53:37 to hear him talk about this)
I recently chose AngularJS over ember.js for a project I am working on, and have been very pleased with it so far. One nice thing about ember is its built in support for "computed properties" with automatic data binding. I have been able to accomplish something similar in Angular with the code below, but am not sure if it is the best way to do so.
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// "Computed Property"
$scope.$watch(navigation.getCurrentPageNumber, function(newVal, oldVal, scope) {
scope.currentPageNumber = newVal;
});
$scope.totalPages = navigation.getTotalPages();
}]);
// 'navigation' service
angular.module('mathSkills.services', [])
.factory('navigation', function() {
var currentPage = 0,
pages = [];
return {
getCurrentPageNumber: function() {
return currentPage + 1;
},
getTotalPages: function() {
return pages.length;
}
};
});
// HTML template
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber}} of {{totalPages}}
</div>
I would like for the UI to update whenever the currentPage of the navigation service changes, which the above code accomplishes.
Is this the best way to solve this problem in AngularJS? Are there (significant) performance implications for using $watch() like this? Would something like this be better accomplished using custom events and $emit() or $broadcast()?
While your self-answer works, it doesn't actually implement computed properties. You simply solved the problem by calling a function in your binding to force the binding to be greedy. I'm not 100% sure it'd work in all cases, and the greediness might have unwanted performance characteristics in some situations.
I worked up a solution for a computed properties w/dependencies similar to what EmberJS has:
function ngCreateComputedProperty($scope, computedPropertyName, dependentProperties, f) {
function assignF($scope) {
var computedVal = f($scope);
$scope[computedPropertyName] = computedVal;
};
$scope.$watchCollection(dependentProperties, function(newVal, oldVal, $scope) {
assignF($scope);
});
assignF($scope);
};
// in some controller...
ngCreateComputedProperty($scope, 'aSquared', 'a', function($scope) { return $scope.a * $scope.a } );
ngCreateComputedProperty($scope, 'aPlusB', '[a,b]', function($scope) { return $scope.a + $scope.b } );
See it live: http://jsfiddle.net/apinstein/2kR2c/3/
It's worth noting that $scope.$watchCollection is efficient -- I verified that "assignF()" is called only once even if multiple dependencies are changed simultaneously (same $apply cycle).
"
I think I found the answer. This example can be dramatically simplified to:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', ['navigation', '$scope', function (navigation, $scope) {
// Property is now just a reference to the service's function.
$scope.currentPageNumber = navigation.getCurrentPageNumber;
$scope.totalPages = navigation.getTotalPages();
}]);
// HTML template
// Notice the first binding is to the result of a function call.
<div id=problemPager ng-controller=nav>
Problem {{currentPageNumber()}} of {{totalPages}}
</div>
Note that with ECMAScript 5 you can now also do something like this:
// Controller
angular.module('mathSkills.controller', [])
.controller('nav', function(navigation, $scope) {
$scope.totalPages = navigation.getTotalPages();
Object.defineProperty($scope, 'currentPageNumber', {
get: function() {
return navigation.getCurrentPageNumber();
}
});
]);
//HTML
<div ng-controller="nav">Problem {{currentPageNumber}} of {{totalPages}}</div>