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
Related
I've built a small service to handle errorMessages in my application. Its publicly available on the rootScope and is able to add new messages to my page as needed. Now the need to have clickable links in the messages have arisen.
Questions:
How do I dynamically add JavaScript that is handled by angular to the messages that are created?
I've added onclicks that work, but ng-click seem to not be handled.
The Js that I would like to run is in the controller that created the message in the first place. How do I make sure that I end up in the correct scope when clicking a link in an error message?
If the function adding a message is another service, how do I solve that?
And the service I'm playing around with:
var myApp = angular.module('myApp', []);
function errorHandlingFactory() {
this.messages = [];
this.addMessage = function (messageText, type) {
var message = this.messages.push({messageText: messageText, type: type, closeable: false});
};
this.getHtmlContent = function(messageId) {
return this.messages[messageId].messageText;
}
this.removeMessage = function (messageId) {
this.messages.splice(messageId, 1);
};
this.clearMessages = function() {
this.messages = [];
};
}
myApp.service('errorHandling', function () {
return new errorHandlingFactory();
});
myApp.run(function($rootScope, errorHandling) {
// Attach global error handling object to our rootScope
$rootScope.errorFactory = errorHandling;
});
// Usage from controller
$rootScope.errorFactory.addMessage('The message to be added', 'warning');
To make it a bit easier to understand, I've created a jsfiddle to look at.
http://jsfiddle.net/kxsmL25h/7/
What I would like to do is when the link in message is clicked, the function desiredCallback is run on the GenericTestController.
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;
});
I have defined two AngularJS services ... one is for the YouTube Player API, and other for the YouTube iFrame Data API. They look like this:
angular.module('myApp.services',[]).run(function() {
var tag = document.createElement('script');
tag.src = "//www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
})
.factory('YtPlayerApi', ['$window', '$rootScope', function ($window, $rootScope) {
var ytplayer = {"playerId":null,
"playerObj":null,
"videoId":null,
"height":390,
"width":640};
$window.onYouTubeIframeAPIReady = function () {
$rootScope.$broadcast('loadedApi');
};
ytplayer.setPlayerId = function(elemId) {
this.playerId=elemId;
};
ytplayer.loadPlayer = function () {
this.playerObj = new YT.Player(this.playerId, {
height: this.height,
width: this.width,
videoId: this.videoId
});
};
return ytplayer;
}])
.factory('YtDataApi', ['appConfig','$http', function(cfg,$http){
var _params = {
key: cfg.youtubeKey
};
var api="https://www.googleapis.com/youtube/v3/";
var yt_resource = {"api":api};
yt_resource.search = function(query, parameters) {
var config = {
params: angular.extend(angular.copy(_params),
{maxResults: 10,
part: "snippet"}, parameters)
};
return $http.get(api + "search?q=" + query, config);
};
return yt_resource;
}]);
(also note that the 'setPlayerId' function of my player service is called by a custom directive ... but that's not important for my question).
So, here's the issue. I need to ensure that the Player API code is loaded before I set the video id and create the player, which is why I have it broadcasting the 'loadedApi' message. And this works great, if I then in my controller pass a hard-coded video id, like this:
function ReceiverCtrl($scope,$rootScope,$routeParams,ytplayer,ytdataapi) {
$scope.$on('loadedApi',function () {
ytplayer.videoId='voNEBqRZmBc';
ytplayer.loadPlayer();
});
}
However, my video IDs won't be determined until I make an API call with the data api service, so I ALSO have to ensure that the results of that call have come back. And that's where I'm running into problems ... if I do something like this:
$scope.$on('loadedApi',function () {
ytdataapi.search("Mad Men", {'topicId':$routeParams.topicId,
'type':'video',
'order':'viewCount'})
.success(function(apiresults) { // <-- this never gets triggered
console.log(apiresults); // <-- likewise, this obviously doesn't either
});
});
Then the interaction with the data service never happens for some reason. I know the data service works just fine, for when I un-nest it from the $on statement, it returns the api results. But sometimes latency makes it so that the results don't come back fast enough to use them in the player service. Any thoughts on what I can do to make the data search after receiving the message that the player API is ready, but still keep the two services as two separate services (because other controllers only use one or the other, so I don't want them dependent on each other at the service level)?
Figured it out; I had to call $scope.$apply(), like this:
function ReceiverCtrl($scope,$rootScope,$routeParams,ytplayer,ytdataapi) {
$scope.$on('loadedApi',function () {
ytdataapi.search("",{'topicId':$routeParams.topicId,'type':'video','maxResults':1,'order':'viewCount'}).success(function(apiresults) {
ytplayer.videoId=apiresults.items[0].id.videoId;
ytplayer.loadPlayer();
});
$scope.$apply();
});
}
Is there anyone who could shed light on why this works, though? $scope.$digest() also works ... but I thought those methods were only used when you need to update bindings because of some javascript code that Angular isn't aware of. Is the nesting I've got here doing that (I wouldn't think it should, as my ytdataapi service is using $http)?
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");
}
}
}
}]);
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;
};
});