I have a service which creates a configuration object for an external component.
One of the config properties is an optional function that gets called when some event (non angular) gets triggered.
e.g.
{
eventHandler:function(e) { ... }
}
Inside this eventhandler I want to send a message to the current controller.
I tried getting instance of $rootService but it doesn't know about $broadCast.
update : the code (simplified version, to keep code short)
app.service('componentService',['$rootScope',
function($rootScope) {
this.getConfig = function() {
return {
transition:true,
... // other config parameters
clickHandler:function(e) { // event called by external library, e = event args
$rootScope.$broadCast("myEvent",e.value);
};
};
return {
getConfig : this.getConfig
}
}]);
http://plnkr.co/edit/BK4Vjk?p=preview
Check out the example I made above. It should work.
There's a few syntax errors in your snippet of code. I wasn't sure if it was because you were just quickly typing it or if they're really there.
Basically:
app.controller('MainCtrl', function($scope, componentService) {
var config = componentService.getConfig();
$('#nonAngular').bind('click', config.clickHandler);
$scope.$on('myEvent', function(e, value) {
console.log('This is the angular event ', e);
console.log('This is the value ', value)
});
});
app.service('componentService',['$rootScope',
function($rootScope) {
this.getConfig = function() {
return {
transition:true,
clickHandler:function(e) { // event called by external library, e = event args
$rootScope.$broadcast("myEvent", "Some value you're passing to the event broadcast");
}
}
}
}]);
Related
I'm trying to write a very simple message on the screen that when signalr send a value this message gets updated.
I have a very simple Hub:
public class Chat : Hub
{
public Task Send(string message)
{
return Clients.All.InvokeAsync("Send", message);
}
}
On the front-end I have the following html:
<div ng-app="myApp" ng-controller="myCtrl">
<p ng-bind="value"></p>
</div>
And the script is:
var app = angular.module('myApp', []);
app.controller('myCtrl', function ($scope) {
$scope.value = "My Test Value";
});
chatConnection.on('Send', (message) => {
app.scope.value = message;
});
What I am failing to understand is how do I access that value parameter so that I may update it.
EDIT:
The SignalR part works, the message comes through. The problem is that I am not sure how to update the value that is inside of that controller. app.scope.value = message;
Use observer pattern. Basically create a service which should do the following things:
Creates hub connection (handles start, reconnection, ....)
Support for example the following methods:
register for event (adds the caller to a list which contains
interested members.)
unregister for event (removes the caller from the list with the interesed members.)
In the case a new message from server inform all observers
Example:
(Pseudocode extracted from my solution (to big to show all)):
Server (here called hubWrapperService):
var yourHubProxy = $.connection.yourHub;
yourHubProxy.client.yourMethodWhichServerCanCall= function (params){
// Inform observers. Iterate to over all your observers and inform them
notifyObservers(params);
};
// Start connection.
$.connection.hub.start().done(function () {
...
});
}).fail(function (error) {...
});
function registerToEvent(callback) {
$log.debug("ServiceHub: getConnectionEvent called");
// Add caller to you observer list. (here serviceInitObservers)
}
function notifyServiceInitObservers() {
angular.forEach(serviceInitObservers, function (callback) {
callback.callback();
});
}
In your Controller (inject service and register for events):
hubWrapperServer.registerToEvent(function () {
serviceHub.getAllDevices().then(function (params) { // Do something in your controller
});
});
There is also a wrapper service available
https://github.com/JustMaier/angular-signalr-hub
poker.client.showAllCards = function (show) {
$scope.allCardsShowing = show;
$scope.$apply();
};
$scope.$apply(), refresh the Angularjs context, and this working for me.
I get this form article: Consensus: SignalR + AngularJS
I get updates from the Backend via socket connections. I want to have an automatically updating Frontend with AngularJS while using a data object for the data I got from the Backend.
What do I have?
Template:
Status: {{unit.getStatus()}}
Controller 1:
function ($scope, unitFactory) {
// register to unit factory to get the updates and do
// $scope.unit = data.getUnit();
}
Controller 2:
function ($scope, unitFactory) {
// register to unit factory to get the updates and do
// $scope.unit = data.getUnit();
// $scope.foo = data.getFoo();
}
Service:
function(requestFactory) {
var unit = {},
foo = {};
Sockets.socket('unit', function (response) {
unit = new Unit(response['data']);
foo = new Foo(response['foo']);
// This is the data object which has to be send to the controllers
var Data = {
getUnit: function () {
return unit;
},
getFoo: function() {
return foo;
}
// some more functions...
}
});
}
Sockets:
channel.on('data', function (event) {
try {
event = JSON.parse(event);
// successCallback is what is given as second parameter in the `Service`.
$rootScope.$apply(successCallback(event));
} catch (e) {
console.log('Error: ' + e);
}
});
How should it work together?
Socket update comes in and gets handled by the Sockets object
Sockets call the function which is registered in the Service
The callback function in Service process the data
MISSING The processed data wrapped in an object has to be delivered to the controllers
MISSING The controllers can do whatever they want to do with the data whenever there is a new update.
The template gets auto updated.
Can anyone help me with the MISSING parts? I tried a lot of different approaches but I ran to dead ends every time.
Have you tried returning a promise for the data, and then $state.reload() ?
Got it solved using the 'data model pattern':
Template 1 (used by Controller 1):
Status: {{unit.data.getStatus()}}
Template 2 (used by Controller 2):
Status: {{foo.data.getBar()}}
Controller 1:
function ($scope, unitFactory) {
$scope.unit = unitFactory.getUnit();
}
Controller 2:
function ($scope, unitFactory) {
$scope.unit = unitFactory.getUnit();
$scope.foo = unitFactory.getFoo();
}
Service:
function(requestFactory) {
var unit = { data:{} },
foo = { data:{} };
Sockets.socket('unit', function (response) {
unit.data = new Unit(response['data']);
foo.data = new Foo(response['foo']);
});
return
getUnit: function ()
return unit;
},
getFoo: function() {
return foo;
}
// some more functions...
}
}
Sockets:
channel.on('data', function (event) {
try {
event = JSON.parse(event);
// successCallback is what is given as second parameter in the `Service`.
$rootScope.$apply(successCallback(event));
} catch (e) {
console.log('Error: ' + e);
}
});
Since the data is stored in an object the data in the templates is updated (since it is a reference). I have to get used to these extra attributes data and it is not looking nice but it does its job.
I'm building an app in angularjs, where I have a central notification queue. Any controller can push into the queue and digest the messages.
I have built a service like:
angular.module('app').factory('notificationSvc', ['translateSvc', notification]);
function notification(translate) {
var notificationQ = [];
var service = {
add: add,
getAll: getAll
};
return service;
function add(message, type) {
notificationQ.push({
message: message,
type: type
});
}
function getAll() {
return notificationQ;
}
}
(One of the problems with this is that the notificationQ can be modified unsafely by calling svc.getAll()[3].message = "I have changed a message"; or something similar. I originally wanted a "push only" service with immutable messages, but this problem is outside of the scope of this question.)
If I digest this queue in a controller like:
$scope.notifications = svc.getAll();
$scope.current= 0; // currently visible in the panel
And use it like:
<div ng-repeat="notification in notifications" ng-show="$index == current">
<p>{{notification.message}}</p>
</div>
I can bind to it, see it changing and all is well. I can cycle through past notifications by changing the variable current.
The question:
When the queue gets a new element I want the $scope.index variable to change to notifications.length - 1. How do I do that?
I have seen examples using $rootScope.$broadcast('notificationsChanged'); and $scope.$on('notificationsChanged', function() { $scope.index = $scope.notifications.length - 1; });, but I did not really like the pattern.
I have a controller that knows about the service, has a direct reference to it, and yet we use $rootScope to communicate? Everything else sees the $rootScope, and all the events from different services will clutter up there.
Can't I just put the event on the service instead? Something like this.$broadcast('notificationsChanged') in the service and svc.$on('notificationsChanged', function() { ... }); in the controller.
Or would it be cleaner to watch the data directly? If yes, how? I don't like this as I was not planning on exposing the full array directly (I was planning on get(index) methods) it just sort of happened along the lines where I had no idea what I was doing and was happy that at least something works.
You could just manage events yourself. For example (untested):
function EventManager() {
var subscribers = [];
var service = {
subscribe: subscribe;
unsubscribe: unsubscribe;
publish: publish
}
return service;
function subscribe(f) {
subscribers.push(f);
return function() { unsubscribe(f); };
}
function unsubscribe(f) {
var index = subscribers.indexOf(f);
if (index > -1)
subscribers.splice(index, 1);
}
function publish(e) {
for (var i = 0; i < subscribers.length; i++) {
subscribers[i](e);
}
}
}
function notification(translate) {
var notificationQ = [];
var addEvent = new EventManager();
var service = {
add: add,
getAll: getAll,
onAdded: addEvent.subscribe;
};
return service;
function add(message, type) {
var notification = {
message: message,
type: type
};
notificationQ.push(notification);
addEvent.publish(notification);
}
function getAll() {
return notificationQ;
}
}
Then, from your controller:
...
var unsubscribe = notificationSvc.onAdded(function(n) { /* update */ });
Caveat: using this method the service will maintain a reference to the subscriber function that is passed to it using subscribe, so you have to manage the subscription using $scope.$on('$destroy', unsubscribe)
The notification approach would definitely work. Depending on your implementation it would be the right solution.
Another approach would be to watch the notifications array in your controller, like this:
$scope.$watchCollection('notifications', function(newValue, oldValue) {
$scope.index = newValue.length - 1;
});
This should work, because your controller receives a direct reference to the notifications array and therefore can watch it directly for changes.
As runTarm pointed out in the comments, you could also directly $watch the length of the array. If you're only interested in length changes this would be a more memory saving approach (since you don't need to watch the whole collection):
$scope.$watch('notifications.length', function (newLength) {
$scope.index = newLength - 1;
});
When I load a view, I'd like to run some initialization code in its associated controller.
To do so, I've used the ng-init directive on the main element of my view:
<div ng-init="init()">
blah
</div>
and in the controller:
$scope.init = function () {
if ($routeParams.Id) {
//get an existing object
});
} else {
//create a new object
}
$scope.isSaving = false;
}
First question: is this the right way to do it?
Next thing, I have a problem with the sequence of events taking place. In the view I have a 'save' button, which uses the ng-disabled directive as such:
<button ng-click="save()" ng-disabled="isClean()">Save</button>
the isClean() function is defined in the controller:
$scope.isClean = function () {
return $scope.hasChanges() && !$scope.isSaving;
}
As you can see, it uses the $scope.isSaving flag, which was initialized in the init() function.
PROBLEM: when the view is loaded, the isClean function is called before the init() function, hence the flag isSaving is undefined. What can I do to prevent that?
When your view loads, so does its associated controller. Instead of using ng-init, simply call your init() method in your controller:
$scope.init = function () {
if ($routeParams.Id) {
//get an existing object
} else {
//create a new object
}
$scope.isSaving = false;
}
...
$scope.init();
Since your controller runs before ng-init, this also solves your second issue.
Fiddle
As John David Five mentioned, you might not want to attach this to $scope in order to make this method private.
var init = function () {
// do something
}
...
init();
See jsFiddle
If you want to wait for certain data to be preset, either move that data request to a resolve or add a watcher to that collection or object and call your init method when your data meets your init criteria. I usually remove the watcher once my data requirements are met so the init function doesnt randomly re-run if the data your watching changes and meets your criteria to run your init method.
var init = function () {
// do something
}
...
var unwatch = scope.$watch('myCollecitonOrObject', function(newVal, oldVal){
if( newVal && newVal.length > 0) {
unwatch();
init();
}
});
Since AngularJS 1.5 we should use $onInit which is available on any AngularJS component. Taken from the component lifecycle documentation since v1.5 its the preferred way:
$onInit() - Called on each controller after all the controllers on an
element have been constructed and had their bindings initialized (and
before the pre & post linking functions for the directives on this
element). This is a good place to put initialization code for your
controller.
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope) {
//default state
$scope.name = '';
//all your init controller goodness in here
this.$onInit = function () {
$scope.name = 'Superhero';
}
});
Fiddle Demo
An advanced example of using component lifecycle:
The component lifecycle gives us the ability to handle component stuff in a good way. It allows us to create events for e.g. "init", "change" or "destroy" of an component. In that way we are able to manage stuff which is depending on the lifecycle of an component. This little example shows to register & unregister an $rootScope event listener $on. By knowing, that an event $on bound on $rootScope will not be unbound when the controller loses its reference in the view or getting destroyed we need to destroy a $rootScope.$on listener manually.
A good place to put that stuff is $onDestroy lifecycle function of an component:
var myApp = angular.module('myApp',[]);
myApp.controller('MyCtrl', function ($scope, $rootScope) {
var registerScope = null;
this.$onInit = function () {
//register rootScope event
registerScope = $rootScope.$on('someEvent', function(event) {
console.log("fired");
});
}
this.$onDestroy = function () {
//unregister rootScope event by calling the return function
registerScope();
}
});
Fiddle demo
Or you can just initialize inline in the controller. If you use an init function internal to the controller, it doesn't need to be defined in the scope. In fact, it can be self executing:
function MyCtrl($scope) {
$scope.isSaving = false;
(function() { // init
if (true) { // $routeParams.Id) {
//get an existing object
} else {
//create a new object
}
})()
$scope.isClean = function () {
return $scope.hasChanges() && !$scope.isSaving;
}
$scope.hasChanges = function() { return false }
}
I use the following template in my projects:
angular.module("AppName.moduleName", [])
/**
* #ngdoc controller
* #name AppName.moduleName:ControllerNameController
* #description Describe what the controller is responsible for.
**/
.controller("ControllerNameController", function (dependencies) {
/* type */ $scope.modelName = null;
/* type */ $scope.modelName.modelProperty1 = null;
/* type */ $scope.modelName.modelPropertyX = null;
/* type */ var privateVariable1 = null;
/* type */ var privateVariableX = null;
(function init() {
// load data, init scope, etc.
})();
$scope.modelName.publicFunction1 = function () /* -> type */ {
// ...
};
$scope.modelName.publicFunctionX = function () /* -> type */ {
// ...
};
function privateFunction1() /* -> type */ {
// ...
}
function privateFunctionX() /* -> type */ {
// ...
}
});
I am new in angular and encounter a catch-22:
Facts:
I have a service that logs my stuff (my-logger).
I have replaced the $ExceptionHandler (of angular), with my own implementation which forwards uncaught exceptions to my-logger service
I have another service, pusher-service, that needs to be notified whenever a fatal message is to be logged somewhere in my application using 'my-logger'.
Problem:
I can't have 'my-logger' be depend on 'pusher' since it will create circular dependency (as 'pusher' uses $http. The circle: $ExceptionHandler -> my-logger -> pusher -> $http -> $ExceptionHandler...)
My attempts:
In order to make these 2 services communicate with each other, I wanted to use $watch on the pusher-service: watches a property on $rootscope that will be updated in my-logger.
But, when trying to consume $rootScope in 'my-logger', in order to update the property on which the 'pusher' "watches", I fail on circular dependency as it turns out that $rootscope depends on $ExceptionHandler (the circle: $ExceptionHandler -> my-logger -> $rootScope -> $ExceptionHandler).
Tried to find an option to get, at runtime, the scope object that in its context 'my-logger' service works. can't find such an option.
Can't use broadcast as well, as it requires my-logger to get access to the scope ($rootScope) and that is impossible as seen above.
My Question:
Is there an angular way to have two services communicate through a 3rd party entity ?
Any idea how this can be solved ?
Use a 3rd service that acts as a notification/pubsub service:
.factory('NotificationService', [function() {
var event1ServiceHandlers = [];
return {
// publish
event1Happened: function(some_data) {
angular.forEach(event1ServiceHandlers, function(handler) {
handler(some_data);
});
},
// subscribe
onEvent1: function(handler) {
event1ServiceHandlers.push(handler);
}
};
}])
Above, I only show one event/message type. Each additional event/message would need its own array, publish method, and subscribe method.
.factory('Service1', ['NotificationService',
function(NotificationService) {
// event1 handler
var event1Happened = function(some_data) {
console.log('S1', some_data);
// do something here
}
// subscribe to event1
NotificationService.onEvent1(event1Happened);
return {
someMethod: function() {
...
// publish event1
NotificationService.event1Happened(my_data);
},
};
}])
Service2 would be coded similarly to Service1.
Notice how $rootScope, $broadcast, and scopes are not used with this approach, because they are not needed with inter-service communication.
With the above implementation, services (once created) stay subscribed for the life of the app. You could add methods to handle unsubscribing.
In my current project, I use the same NotificationService to also handle pubsub for controller scopes. (See Updating "time ago" values in Angularjs and Momentjs if interested).
Yes, use events and listeners.
In your 'my-logger' you can broadcast an event when new log is captured:
$rootScope.$broadcast('new_log', log); // where log is an object containing information about the error.
and than listen for that event in your 'pusher':
$rootScope.$on('new_log', function(event, log) {... //
This way you don't need to have any dependencies.
I have partially succeeded to solve the case:
I have created the dependency between 'my-logger' and 'pusher' using the $injector.
I used $injector in 'my-logger' and injected at "runtime" (means right when it is about to be used and not at the declaration of the service) the pusher service upon fatal message arrival.
This worked well only when I have also injected at "runtime" the $http to the 'pusher' right before the sending is to happen.
My question is why it works with injector in "runtime" and not with the dependencies declared at the head of the service ?
I have only one guess:
its a matter of timing:
When service is injected at "runtime", if its already exists (means was already initialized else where) then there is no need to fetch and get all its dependencies and thus the circle is never discovered and never halts the execution.
Am I correct ?
This is an easy way to publish/subscribe to multiple events between services and controllers
.factory('$eventQueue', [function() {
var listeners = [];
return {
// publish
send: function(event_name, event_data) {
angular.forEach(listeners, function(handler) {
if (handler['event_name'] === event_name) {
handler['callback'](event_data);
}
});
},
// subscribe
onEvent: function(event_name,handler) {
listeners.push({'event_name': event_name, 'callback': handler});
}
};
}])
consumers and producers
.service('myService', [ '$eventQueue', function($eventQueue) {
return {
produce: function(somedata) {
$eventQueue.send('any string you like',data);
}
}
}])
.controller('myController', [ '$eventQueue', function($eventQueue) {
$eventQueue.onEvent('any string you like',function(data) {
console.log('got data event with', data);
}])
.service('meToo', [ '$eventQueue', function($eventQueue) {
$eventQueue.onEvent('any string you like',function(data) {
console.log('I also got data event with', data);
}])
You can make your own generic event publisher service, and inject it into each service.
Here's an example (I have not tested it but you get the idea):
.provider('myPublisher', function myPublisher($windowProvider) {
var listeners = {},
$window = $windowProvider.$get(),
self = this;
function fire(eventNames) {
var args = Array.prototype.slice.call(arguments, 1);
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.on(): argument one must be a string.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
var eventListeners = listeners[eventName];
if(eventListeners && eventListeners.length) {
angular.forEach(eventListeners, function(listener) {
$window.setTimeout(function() {
listener.apply(listener, args);
}, 1);
});
}
});
return self;
}
function on(eventNames, handler) {
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.on(): argument one must be a string.');
}
if(!angular.isFunction(handler)) {
throw new Error('myPublisher.on(): argument two must be a function.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
if(listeners[eventName]) {
listeners[eventName].push(handler);
}
else {
listeners[eventName] = [handler];
}
});
return self;
}
function off(eventNames, handler) {
if(!angular.isString(eventNames)) {
throw new Error('myPublisher.off(): argument one must be a string.');
}
if(!angular.isFunction(handler)) {
throw new Error('myPublisher.off(): argument two must be a function.');
}
eventNames = eventNames.split(/ +/);
eventNames = eventNames.filter(function(v) {
return !!v;
});
angular.forEach(eventNames, function(eventName) {
if(listeners[eventName]) {
var index = listeners[eventName].indexOf(handler);
if(index > -1) {
listeners[eventName].splice(index, 1);
}
}
});
return self;
}
this.fire = fire;
this.on = on;
this.off = off;
this.$get = function() {
return self;
};
});