How to keep the AngularJS template updated while getting socket updates? - 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.

Related

Build fluent service in AngularJS

I have a simple factory in AngularJS:
(function(){
'use strict';
angular
.module('myModule', [])
.factory('myService', service);
function service(){
var products= function(p1, p2, p3, ..., pn) {
var url = "http://something.url/api/action";
var data = {
'p1': p1,
'p2': p2,
...
'pn': pn,
}
// return data
return $http
.post(url, data)
.then(function (response) {
return response.data;
});
}
return {
Products : products
};
}
})();
I use this service inside a controller like this:
myInjectedService
.Products(vm.p1, vm.p1, ... , vm.pn)
.then(successCallbackFn)
.catch(failureCallbackFn);
Each parameter (p1, ..., pn) are used to filter the final result. This works like a charm! But with a little drawback: there are to many accepted arguments for Products and is really difficult to know if I'm sending the right parameters and this sounds a little error prone. What I would is a fluent API for service that make everything more human readable, this would be great:
myInjectedService
.Products()
.FilterById(p1)
.WhereCategoryIs(p2)
...
.WhereSomethingElseIs(pn)
.Send()
.then(successCallbackFn)
.catch(failureCallbackFn);
Previously the task of HTTP call was handled by Products call. Right now Products(), only make an empty query (i.e. {}). Each subsequent FilterByX will enrich the query (i.e. {'productId': 'xxx-yyy-1111'}). Calling Send() will make the real HTTP POST call. This call will use the data builded through various filter applied. How can I do that? I'm playing with prototype but without success.
You can archieve what you want by define a new class and use prototype like this.
In a fluent method, remember to return the object itself.
function service(){
var products = function(url) {
// Define a new Product class
var Products = function() {
this.url = url;
this.data = {};
};
// Add the function
Products.prototype.FilterById = function(id) {
this.data.id = id;
// To make it fluent, return the object itself
return this;
};
Products.prototype.FilterByCategory = function(category) {
this.data.category = category;
return this;
};
Products.prototype.send = function() {
console.log(this.data);
};
// Return an instance of the Products class
return new Products();
};
return {
Products : products
};
};
service().Products().FilterById(1).FilterByCategory("Item").send();
You can read more about it here: https://www.sitepoint.com/javascript-like-boss-understanding-fluent-apis/

AngularJS Unable to repeat asynchronous function inside of service

I am building a form that can send and clear cached JSON data using AngularJS to a RESTful web service. When I click submit, the current form data is cached in a JSON object, I then send that data through the web service. Once it is sent I clear the cache.
I have a function I wish to use in multiple controllers (sending cached data). It loops through the cache, sending each JSON object one at a time. When I have the function in a single controller, the code works fine. But once I wrap it in a service and call it in through a controller, my loop no longer works. It runs fine if there is only 1 cache object. But if there are more than 1, the "this.sendCache()" will not fire. I believe this has something to do with the asynchronous function and am not sure what to do about it. Help is greatly appreciated!!!
Controller
app.controller('FormCtrl', function($scope, $filter, $window, getData, Post, randomString, $cordovaProgress, $cordovaDialogs, scService) {
$scope.submitEntry = function() {
var frmData = new Post($scope.postData);
var postCount = window.localStorage.getItem("localPostCount");
postCount ++;
window.localStorage.setItem("localPostCount", postCount);
window.localStorage.setItem("post" + postCount, JSON.stringify(frmData));
scService.sendCache();
};
});
Service
app.service('scService', function ($window, Post, $cordovaProgress, $cordovaDialogs, $timeout) {
this.sendCache = function () {
if(window.Connection){
if(navigator.connection.type != Connection.NONE) {
var postCount = window.localStorage.getItem("localPostCount");
var curCacheObj = new Post(JSON.parse(window.localStorage.getItem("post" + postCount) || '{}'));
if (postCount > 0) {
curCacheObj.$save().then(function(response) {
var servResponse = JSON.stringify(response);
if (servResponse.indexOf("#xmlns:ns3") > -1) {
console.log("Post " + postCount + " sent!");
}
else {
console.log("Unable to post at this time!");
}
}).then(function() {
window.localStorage.removeItem("post" + postCount);
postCount --;
window.localStorage.setItem("localPostCount", postCount);
}).then(function() {
console.log(postCount); //log shows 1
if (postCount > 0) {
this.sendCache(); //yet this won't fire again!
}
else {
$cordovaDialogs.alert('Submission recorded successfully', 'Success', 'OK').then(function() {
console.log('Submission Success');
$window.location.href= 'index.html';
});
}
});
}
else {
$cordovaDialogs.alert('Submission recorded successfully', 'Success', 'OK').then(function() {
console.log('Submission Success');
$window.location.href= 'index.html';
});
}
}
else {
$cordovaDialogs.alert('Your current connection is too slow. Sync at a later time by returning to the app with a better connection.', 'Submission Stored', 'OK').then(function() {
console.log('Submission Cached');
$window.location.href= 'index.html';
});
}
}
};
});
It is actually a very common JS issue, and not related to AngularJS itself. this is not what you think it is. Inside the .then() callback, your context has changed so it is no longer pointing to your service but to the response context from the async call. Therefore, the function sendCache() does not actually exist and cannot be called that way.
All you need to do is take a reference to self at the top of your service:
var self = this;
Then use self instead of this in your call:
self.sendCache();
Note that this is not causing trouble for you with variables like postCount because they're locally defined within the closure and do not require this to be referenced. But if you were to define this.postCount or other variables in your service, you would need to do the same thing.

Controlling order of execution in angularjs

I have inherited an angular app and now need to make a change.
As part of this change, some data needs to be set in one controller and then used from another. So I created a service and had one controller write data into it and one controller read data out of it.
angular.module('appRoot.controllers')
.controller('pageController', function (myApiService, myService) {
// load data from API call
var data = myApiService.getData();
// Write data into service
myService.addData(data);
})
.controller('pageSubController', function (myService) {
// Read data from service
var data = myService.getData();
// Do something with data....
})
However, when I go to use data in pageSubController it is always undefined.
How can I make sure that pageController executes before pageSubController? Or is that even the right question to ask?
EDIT
My service code:
angular.module('appRoot.factories')
.factory('myService', function () {
var data = [];
var addData = function (d) {
data = d;
};
var getData = function () {
return data;
};
return {
addData: addData,
getData: getData
};
})
If you want your controller to wait untill you get a response from the other controller. You can try using $broadcast option in angularjs.
In the pagecontroller, you have to broadcast your message "dataAdded" and in the pagesubcontroller you have to wait for the message using $scope.$on and then process "getData" function.
You can try something like this :
angular.module('appRoot.controllers')
.controller('pageController', function (myApiService, myService,$rootScope) {
// load data from API call
var data = myApiService.getData();
// Write data into service
myService.addData(data);
$rootScope.$broadcast('dataAdded', data);
})
.controller('pageSubController', function (myService,$rootScope) {
// Read data from service
$scope.$on('dataAdded', function(event, data) {
var data = myService.getData();
}
// Do something with data....
})
I would change your service to return a promise for the data. When asked, if the data has not been set, just return the promise. Later when the other controller sets the data, resolve the previous promises with the data. I've used this pattern to handle caching API results in a way such that the controllers don't know or care whether I fetched data from the API or just returned cached data. Something similar to this, although you may need to keep an array of pending promises that need to be resolved when the data does actually get set.
function MyService($http, $q, $timeout) {
var factory = {};
factory.get = function getItem(itemId) {
if (!itemId) {
throw new Error('itemId is required for MyService.get');
}
var deferred = $q.defer();
if (factory.item && factory.item._id === itemId) {
$timeout(function () {
deferred.resolve(factory.item);
}, 0);
} else {
$http.get('/api/items/' + itemId).then(function (resp) {
factory.item = resp.data;
deferred.resolve(factory.item);
});
}
return deferred.promise;
};
return factory;
}

angularjs send message from service

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");
}
}
}
}]);

angular JS - communicate between non-dependend services

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;
};
});

Resources