AngularJs / Propagating efficiently WebSockets events/messages to various controllers - javascript

Using AngularJs, I wrote a factory aiming to handle a WebSocket connection.
Here's its code:
.factory('WebSocketConnection', function () {
var service = {};
service.callbacks = [];
service.connect = function() {
if(service.ws)
return;
var ws = new WebSocket("ws://localhost:9000/ws");
ws.onmessage = function (message) {
angular.forEach(service.callbacks, function(callback){
callback(message);
});
};
service.ws = ws;
};
service.send = function(message) {
service.ws.send(message);
};
service.subscribe = function(callback) {
service.callbacks.push(callback);
};
return service;
});
Basically, it allows each Angular components like controller or factory/service to register a specific callback, in order to handle messages; hence the callbacks array.
Here's the interesting excerpt of a listening controller:
WebSocketConnection.subscribe(function (message) {
$scope.$apply(function () {
var data = angular.fromJson(message.data);
$scope.notifications.push(data);
});
});
So the callbacks array would contain this function.
But...what if I won't need this controller any more at some time? (for instance when I navigate to other page based on another controller)
I would be forced to delete the callback item (this function) from the array each time usage of the controller is left, to avoid useless, maybe conflicting, process of this callback as long as any further messages are handled.
Not handy...
I thought about a way to broadcast event from the $rootScope from the factory, so that a specific controller doesn't have to manage his listeners/subscriptions itself.
But I don't want to involve all the scope tree, including all scopes that are not concerned.
What would be a good practice?
Note: I need to achieve a relation 1-N where 1 is the WebSocket handler (factory) and N, any parallel alive Angular components, each needing to listen to messages.

I would suggest maintaining your model in a service or factory object. This would enable you to interact with the data as it exists there and not be dependent on the state of your application (what controllers exist) when a message is recieved. It can also allow you to enforce the concept of:
$scope has model instead of $scope as model
That might look something like this:
ws.onmessage = function(event) {
$rootScope.$apply(function(){
Service.notifications.push(event.data);
}
}
and
angular.module('MyApp').controller('MyCtrl', ['$scope', 'Service',
function($scope, Service) {
$scope.notifications = Service.notifications; //references are ===
}])
To enable flexibility you can use data contained in the message to determine what injectable/methods need to be updated, then use the $injector.

Thanks to #calebboyd who made remind me the existence of the $scope's destroy event, I think I have found a good way to achieve my requirement.
A semi-automatic way to let the controller unsubscribe itself would be to add this piece of code:
$scope.$on("$destroy",function() {
WebSocketConnection.unsubscribe($scope.$id);
});
Subscribing mechanism would look like this:
WebSocketConnection.subscribe($scope.$id, function (message) { //note the $scope.$id parameter
$scope.$apply(function () {
var data = angular.fromJson(message.data);
$scope.notifications.push(data);
});
});
Therefore, the full factory code would be:
.factory('WebSocketConnection', function () {
var service = {};
service.callbacks = {}; //note the object declaration, not Array
service.connect = function() {
if(service.ws)
return;
var ws = new WebSocket("ws://localhost:9000/ws");
ws.onmessage = function (message) {
angular.forEach(service.callbacks, function(callback){
callback(message);
});
};
service.ws = ws;
};
service.send = function(message) {
service.ws.send(message);
};
service.subscribe = function(concernedScopeId, callback) {
service.callbacks[concernedScopeId] = callback;
};
service.unsubscribe = function(concernedScopeId) {
delete service.callbacks[concernedScopeId];
};
return service;
});
And that does the trick: each useless callback acting as listener can then be detected and deleted.

Related

Use $timeout to wait service data resolved

I am trying to pass data from directive to controller via service, my service looks like this:
angular
.module('App')
.factory('WizardDataService', WizardDataService);
WizardDataService.$inject = [];
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
wizardFormData = newFormData;
};
var getWizardData = function () {
return wizardFormData;
};
var resetWizardData = function () {
//To be called when the data stored needs to be discarded
wizardFormData = {};
};
return {
setWizardData: setWizardData,
getWizardData: getWizardData,
resetWizardData: resetWizardData
};
}
But when I try to get data from controller it is not resolved (I think it waits digest loop to finish), So I have to use $timeout function in my controller to wait until it is finished, like this:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
vm.getStoredData = WizardDataService.getWizardData();
$scope.$watchCollection(function () {
console.log("getStoredData callback: " + JSON.stringify(vm.getStoredData));
return vm.getStoredData;
}, function () {
});
}, 300);
Despite of the fact that it works, what I am interested in is, if there is a better way to do this, also if this is bug free and the main question, why we use 300 delay and not 100 (for example) for $timeout and if it always will work (maybe for someone it took more time than 300 to get data from the service).
You can return a promise from your service get method. Then in your controller, you can provide a success method to assign the results. Your service would look like this:
function getWizardData() {
var deferred = $q.defer();
$http.get("/myserver/getWizardData")
.then(function (results) {
deferred.resolve(results.data);
}),
function () {
deferred.reject();
}
return deferred.promise;
}
And in your ng-controller you call your service:
wizardService.getWizardData()
.then(function (results) {
$scope.myData = results;
},
function () { });
No timeouts necessary. If your server is RESTFULL, then use $resource and bind directly.
Use angular.copy to replace the data without changing the object reference.
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
angular.copy(newFormData, wizardFormData);
};
From the Docs:
angular.copy
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
Usage
angular.copy(source, [destination]);
-- AngularJS angular.copy API Reference
This way the object reference remains the same and any clients that have that reference will get updated. There is no need to fetch a new object reference on every update.

Executing angular services in some other scope?

I am designing a RealTime system using Angular and Socket.IO.
I am using two different factory one for Subscribing to the topic and other for writing logic for its callback function and subscribing it there.
pubsub.js
app.factory('PubSub', function(){
return{
subscribe: function(event, callback){
socket.on(event, callback);
}, //end subscribe
}
})
orders.js
app.factory('Orders', function(PubSub){
return{
subscribeOrderUpdate : function(){
//Subscribing
PubSub.subscribe('onUpdateOrder', this.onOrderUpdate);
},
//Callback function
onOrderUpdate: function(order){
var result = this.findOrder(order.Id);
console.log('Order has been updated');
},
findOrder(OrderId){
//logic to find order based on orderId
}
};
});
Now when I am calling this from my controller
//Controller..
app.controller('OrdersPage', function($scope, Orders){
Orders.subscribeOrderUpdate();
/* Shows Error this.findOrder() is not a function*/
});
Now when I am calling this factory its displaying error that this.findOrder is not a function.
After searching I found out that since my callback function onOrderUpdate
is getting called in PubSub factory scope thus its showing error because this.findOrder is defined in Orders factory scope.
So how should I define it so that this.findOrder gets called in its right scope.
Try re-factoring your Orders service to something like..
app.factory('Orders', function(PubSub){
var service = {
subscribeOrderUpdate : subscribeOrderUpdate
};
return service;
//////////////////////////////
function subscribeOrderUpdate(){
//Subscribing
PubSub.subscribe('onUpdateOrder', onOrderUpdate);
}
function onOrderUpdate(order){
var result = findOrder(order.Id);
console.log('Order has been updated');
}
function findOrder(OrderId){
//logic to find order based on orderId
}
});
If you need to use your findOrder method directly anywhere else you can expose it just like the subscribeOrderUpdate method but it doesn't look like you need to so I removed it along with the onOrderUpdate. Best to only expose what you need.

Angular - Update scope when change in Factory data

I'm using a factory to poll a particular web service. This web service is used to update data any the factory. I initiate this factory in the main controller, and populate a scope variable through a factory function. The variable initializes correctly, and I get the right data on the screen, but I'm struggling on getting the data to bind automatically.
Edit for additional notes:
The reason this code is in a Factory is that I plan on using the factory data across multiple views.
Here is what I have so far:
App.factory('metrics', function($http, $q, $timeout){
var service;
var users = [{laps:[]}];
var updateMetrics = function(){
//updates the users array in the factory
};
service.load = function (){
var deferred = $q.defer();
$http.get('http://www.example.com/api/random').success(function(data) {
var temp_array = data.split("\n");
updateMetrics(0, temp_array);
deferred.resolve({status: 'good'});
$timeout(service.load,3000);
});
return deferred.promise;
};
service.lastLapInfo = function(){
var lastlap = [];
for (var i=0; i<users.length;i++)
{
var lap = users[i].laps[users[i].laps.length-1];
lastlap.push(lap);
}
return lastlap;
};
return service;
});
App.controller('mainController', function($scope, metrics) {
metrics.load().then(function(response){
$scope.$watch(function () { return metrics.lastLapInfo() }, function (newVal, oldVal) {
if (newVal !=oldVal)
{
$scope.users=metrics.lastLapInfo();
}
});
});
});
When I try the above, I get an error saying '10 $digest() iterations reached'. I don't see how that's possible, asI'm not calling the watch function multiple times.
Any suggestions (or other means to accomplish what I'm trying to do?)
If you're not 100% set on using $watch, a pattern that I prefer is to bind new instances of (not references to) modules to the current scope and keep the controllers strictly as components used for wiring together the project's views and models. This excludes the use of $watch, even for coordinating data across modules. I prefer to use $rootScope's $broadcast, $emit and $on methods within modules/factories (after passing in $rootScope as a service, which may or may not work for all situations, though it has for all that I've come across) rather than the comparatively sluggish $watch or $watchCollection methods. Using the latter makes me feel dirty inside... But I digress.
Would something like the following work in your situation?
App.factory('metrics', function($http, $q, $timeout){
var service;
service.users = [{laps:[]}];
service.updateMetrics = function(){
// updates the users array in the current instance of `metrics`
// ex:
// this.users = updatedMetrics;
// don't do:
// service.users = updatedMetrics;
};
service.load = function (){
var deferred = $q.defer();
$http.get('http://www.example.com/api/random').success(function(data) {
var temp_array = data.split("\n");
this.updateMetrics(0, temp_array);
deferred.resolve({status: 'good'});
$timeout(service.load,3000);
}.bind(this));
return deferred.promise;
};
service.lastLapInfo = function(){
var lastlap = [];
for (var i=0; i<this.users.length;i++)
{
var lap = this.users[i].laps[this.users[i].laps.length-1];
lastlap.push(lap);
}
return lastlap;
};
return service;
});
App.controller('mainController', function($scope, metrics) {
$scope.metrics = angular.copy(metrics);
$scope.metrics.load();
});
By setting $scope.metrics = angular.copy(metrics), we are creating a new instance of metrics, rather than setting $scope.metrics as a reference to metrics ($scope.metrics = metrics). This has several benefits, including that you can now use multiple instances of the same module in the controller (ie $scope.foo = angular.copy(foo); $scope.bar = angular.copy(foo); since the objects bound to $scope are completely new objects, rather than references to the same module.
Another benefit is that the instance of metrics attached to $scope can be used to call methods on metrics which can allow any changes to metrics to automatically be applied to your controller's views. I frequently faced odd issues when trying to get this to work when not using angular.copy or $.extend, seemingly because changes to the referenced module attached to $scope were not always being registered.

Updating angular.js service object without extend/copy possible?

I have 2 services and would like to update a variable in the 1st service from the 2nd service.
In a controller, I am setting a scope variable to the getter of the 1st service.
The problem is, the view attached to the controller doesn't update when the service variable changes UNLESS I use angular.extend/copy. It seems like I should just be able to set selectedBuilding below without having to use extend/copy. Am I doing something wrong, or is this how you have to do it?
controller
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
$scope.building = BuildingsService.getSelectedBuilding();
});
service 1
app.factory('BuildingsService', function() {
var buildingsList = [];
var selectedBuilding = {};
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
angular.extend(selectedBuilding, _.find(
buildingsList, {'building_id': buildingId})
);
};
var getSelectedBuilding = function() {
return selectedBuilding;
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
service 2
app.factory('AnotherService', function(BuildingsService) {
...
// something happens, gives me a building id
BuildingsService.setSelectedBuilding(building_id);
...
});
Thanks in advance!
When you execute this code:
$scope.building = BuildingsService.getSelectedBuilding();
$scope.building is copied a reference to the same object in memory as your service's selectedBuilding. When you assign another object to selectedBuilding, the $scope.building still references to the old object. That's why the view is not updated and you have to use angular.copy/extend.
You could try the following solution to avoid this problem if you need to assign new objects to your selectedBuilding:
app.factory('BuildingsService', function() {
var buildingsList = [];
var building = { //create another object to **hang** the reference
selectedBuilding : {}
}
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
//just assign a new object to building.selectedBuilding
};
var getSelectedBuilding = function() {
return building; //return the building instead of selectedBuilding
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
With this solution, you have to update your views to replace $scope.building bindings to $scope.building.selectedBuilding.
In my opinion, I will stick to angular.copy/extend to avoid this unnecessary complexity.
I dont believe you need an extend in your service. You should be able to watch the service directly and respond to the changes:
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
// first function is evaluated on every $digest cycle
$scope.$watch(function(scope){
return BuildingsService.getSelectedBuilding();
// second function is a callback that provides the changes
}, function(newVal, oldVal, scope) {
scope.building = newVal;
}
});
More on $watch: https://code.angularjs.org/1.2.16/docs/api/ng/type/$rootScope.Scope

Add methods to a collection returned from an angular resource query

I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.

Categories

Resources