RxJS in AngularJS Services - angularjs

I'm learning RxJS and am trying to implement it in one of my existing applications. First thing I am trying to do is remove rootscope.broadcast/emit methods and replace them with BehaviorSubjects. This has worked fine if those events are subscribed to inside a controller. However, if I try to subscribe to those events in a service, they never fire. I can move the exact same subscription into a component/controller/etc and it works fine.
Is there a reason for this or should I be able to do this and I am just doing something wrong?
UPDATE 1
I have an event service that maintains events run at the top of the application like so:
(function () {
"use strict";
angular.module("app.common").service("eventService", EventService);
function EventService($window) {
"ngInject";
var defaultBehaviorSubjectValue = null;
var service = {
activate: activate,
onLogin: new Rx.BehaviorSubject(defaultBehaviorSubjectValue), //onnext by authService.login
onLogout: new Rx.BehaviorSubject(defaultBehaviorSubjectValue), //onnext by authService.logout
isOnline: new Rx.BehaviorSubject(defaultBehaviorSubjectValue) //onnext by authService.logout
};
return service;
function activate() {
service.onLogin.subscribe(function (userData) {
console.info("user logged in");
});
service.onLogout.subscribe(function (userData) {
console.info("Logging user out");
dispose();
});
$window.addEventListener("offline", function () {
console.info("Lost internet connection, going offline");
service.isOnline.onNext(false);
}, false);
$window.addEventListener("online", function () {
console.info("Regained internet connection, going online");
service.isOnline.onNext(true);
}, false);
}
function dispose() {
angular.forEach(service, function (event, index) {
if (service && service[event] && service[event].isDisposed === false) {
service[event]();
}
});
}
}
})();
I have a service that is waiting for that onLogin event to fire. The code is simliar to:
(function () {
angular.module("app.data").service("offlineProjectDataService", OfflineProjectDataService);
function OfflineProjectDataService(definitions, metaDataService, userService, eventService) {
"ngInject";
var onLoginSubscription = eventService.onLogin.subscribe(function (isLoaded) {
if (isLoaded) {
activate();
}
});
var service = {
activate: activate
}
return service;
function activate(){
//..some stuff
}
}
})();
The problem I'm having is that the onLogin event is not firing so my activate method is not being called. If I move that exact same subscription line to some other controller in my app, it DOES fire. So I don't think there is anything wrong with syntax.
Unless of course I'm missing something here that is probably painfully obvious to somebody.

Use RxJS in a Service
Build a service with RxJS Extensions for Angular.
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);
app.factory("DataService", function(rx) {
var subject = new rx.Subject();
var data = "Initial";
return {
set: function set(d){
data = d;
subject.onNext(d);
},
get: function get() {
return data;
},
subscribe: function (o) {
return subject.subscribe(o);
}
};
});
Then simply subscribe to the changes.
app.controller('displayCtrl', function(DataService) {
var $ctrl = this;
$ctrl.data = DataService.get();
var subscription = DataService.subscribe(function onNext(d) {
$ctrl.data = d;
});
this.$onDestroy = function() {
subscription.dispose();
};
});
Clients can subscribe to changes with DataService.subscribe and producers can push changes with DataService.set.
The DEMO on PLNKR.

Related

AngularJS call scope function to 'refresh' scope model

I've been struggling with this for a few days now and can't seem to find a solution.
I have a simple listing in my view, fetched from MongoDB and I want it to refresh whenever I call the delete or update function.
Although it seems simple that I should be able to call a previously declared function within the same scope, it just doesn't work.
I tried setting the getDispositivos on a third service, but then the Injection gets all messed up. Declaring the function simply as var function () {...} but it doesn't work as well.
Any help is appreciated.
Here's my code:
var myApp = angular.module('appDispositivos', []);
/* My service */
myApp.service('dispositivosService',
['$http',
function($http) {
//...
this.getDispositivos = function(response) {
$http.get('http://localhost:3000/dispositivos').then(response);
}
//...
}
]
);
myApp.controller('dispositivoController',
['$scope', 'dispositivosService',
function($scope, dispositivosService) {
//This fetches data from Mongo...
$scope.getDispositivos = function () {
dispositivosService.getDispositivos(function(response) {
$scope.dispositivos = response.data;
});
};
//... and on page load it fills in the list
$scope.getDispositivos();
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo);
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
};
$scope.removeDispositivo = function (id) {
dispositivosService.removerDispositivo(id);
$scope.getDispositivos(); //... here
};
$scope.editDispositivo = function (id) {
dispositivosService.editDispositivo(id);
$scope.getDispositivos(); //... and here.
};
}
]
);
On service
this.getDispositivos = function(response) {
return $http.get('http://localhost:3000/dispositivos');
}
on controller
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo).then(function(){
$scope.getDispositivos(); //it should reload the view here...
$scope.dispositivo = '';
});
};
None of the solutions worked. Later on I found that the GET request does execute, asynchronously however. This means that it loads the data into $scope before the POST request has finished, thus not including the just-included new data.
The solution is to synchronize the tasks (somewhat like in multithread programming), using the $q module, and to work with deferred objects and promises. So, on my service
.factory('dispositivosService',
['$http', '$q',
function($http, $q) {
return {
getDispositivos: function (id) {
getDef = $q.defer();
$http.get('http://myUrlAddress'+id)
.success(function(response){
getDef.resolve(response);
})
.error(function () {
getDef.reject('Failed GET request');
});
return getDef.promise;
}
}
}
}
])
On my controller:
$scope.addDispositivo = function() {
dispositivosService.addDispositivo($scope.dispositivo)
.then(function(){
dispositivosService.getDispositivos()
.then(function(dispositivos){
$scope.dispositivos = dispositivos;
$scope.dispositivo = '';
})
});
};
Being my 'response' object a $q.defer type object, then I can tell Angular that the response is asynchronous, and .then(---).then(---); logic completes the tasks, as the asynchronous requests finish.

AngularFire: how to set $location.path in onAuth()?

I am building an angular+firebase app with user authentication (with angularfire 0.8).
I need to use onAuth() auth event handler, since I will provide multiple authentication paths, included social, and want to avoid code duplication. Inside onAuth callback, I need to reset location.path to '/'.
Usually everything works nicely, but, if app is loaded on an already authenticated session (<F5>, for example), on $scope.$apply() I get "Error: [$rootScope:inprog] $apply already in progress"
(if I don't use $scope.$apply(), location path is not applyed to scope, and no page change happens).
I suspect I make some stupid mistake, but can't identify it...
This is my workflow:
app.controller('AuthCtrl', function ($scope, $rootScope, User) {
var ref = new Firebase(MY_FIREBASE_URL);
$scope.init = function () {
$scope.users = [];
User.all.$bindTo($scope, 'users').then(function () {
console.info('$scope.users bound:', $scope.users);
});
};
$scope.login = function () {
ref.authWithPassword({
email: $scope.user.email,
password: $scope.user.password,
}, function(err) {
if (err) {
console.error('Error during authentication:', err);
}
});
};
ref.onAuth(function(authData) {
if (authData) {
console.info('Login success');
var $rootScope.currentUser = $scope.users[authData.uid];
$location.path('/');
$scope.$apply();
} else {
console.info('Logout success');
}
});
};
app.factory('User', function ($firebase) {
var ref = $firebase(new Firebase(MY_FIREBASE_URL + 'users'));
return {
all: ref.$asObject()
};
});
Just as a reference, I want to post the solution I found, and I'm currently adopting:
$scope.init = function () {
$scope.params = $routeParams;
$scope.debug = CFG.DEBUG;
$scope.lastBuildDate = lastBuildDate;
$scope.error = null;
$scope.info = null;
$scope.users = [];
User.all.$bindTo($scope, 'users').then(function () {
// watch authentication events
refAuth.onAuth(function(authData) {
$scope.auth(authData);
});
});
...
};
...
I.e., it was enough to move the watch on authentication events inside the callback to bindTo on users object from Firebase.

How to integrate an AngularJS service with the digest loop

I'm trying to write an AngularJS library for Pusher (http://pusher.com) and have run into some problems with my understanding of the digest loop and how it works. I am writing what is essentially an Angular wrapper on top of the Pusher javascript library.
The problem I'm facing is that when a Pusher event is triggered and my app is subscribed to it, it receives the message but doesn't update the scope where the subscription was setup.
I have the following code at the moment:
angular.module('pusher-angular', [])
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window',
function ($window) {
var pusher = new $window.Pusher(apiKey, initOptions);
return pusher;
}];
})
.factory('Pusher', ['$rootScope', '$q', 'PusherService', 'PusherEventsService',
function ($rootScope, $q, PusherService, PusherEventsService) {
var client = PusherService;
return {
subscribe: function (channelName) {
return client.subscribe(channelName);
}
}
}
]);
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
})
}]);
and in another file that sets the app up:
angular.module('myApp', [
'pusher-angular'
]).
config(['PusherServiceProvider',
function(PusherServiceProvider) {
PusherServiceProvider
.setToken('API KEY')
.setOptions({});
}
]);
I've removed some of the code to make it more concise.
In the ItemListController the $scope.items variable doesn't update when a message is received from Pusher.
My question is how can I make it such that when a message is received from Pusher that it then triggers a digest such that the scope updates and the changes are reflected in the DOM?
Edit: I know that I can just wrap the subscribe callback in a $scope.$apply(), but I don't want to have to do that for every callback. Is there a way that I can integrate it with the service?
On the controller level:
Angular doesn't know about the channel.bind event, so you have to kick off the cycle yourself.
All you have to do is call $scope.$digest() after the $scope.items gets updated.
.controller('ItemListController', ['$scope', 'Pusher', function($scope, Pusher) {
$scope.items = [];
var channel = Pusher.subscribe('items')
channel.bind('new', function(item) {
console.log(item);
$scope.items.push(item);
$scope.$digest(); // <-- this should be all you need
})
Pusher Decorator Alternative:
.provider('PusherService', function () {
var apiKey = '';
var initOptions = {};
this.setOptions = function (options) {
initOptions = options || initOptions;
return this;
};
this.setToken = function (token) {
apiKey = token || apiKey;
return this;
};
this.$get = ['$window','$rootScope',
function ($window, $rootScope) {
var pusher = new $window.Pusher(apiKey, initOptions),
oldTrigger = pusher.trigger; // <-- save off the old pusher.trigger
pusher.trigger = function decoratedTrigger() {
// here we redefine the pusher.trigger to:
// 1. run the old trigger and save off the result
var result = oldTrigger.apply(pusher, arguments);
// 2. kick off the $digest cycle
$rootScope.$digest();
// 3. return the result from the the original pusher.trigger
return result;
};
return pusher;
}];
I found that I can do something like this and it works:
bind: function (eventName, callback) {
client.bind(eventName, function () {
callback.call(this, arguments[0]);
$rootScope.$apply();
});
},
channelBind: function (channelName, eventName, callback) {
var channel = client.channel(channelName);
channel.bind(eventName, function() {
callback.call(this, arguments[0]);
$rootScope.$apply();
})
},
I'm not really happy with this though, and it feels as though there must be something bigger than I'm missing that would make this better.

Using WebSockets with AngularJS and $broadcast

I've setup an AngularJS app using websockets and it seems to be working. Here is a summary of whats going on:
var app = angular.module('websocketApp',[]);
app.factory('WebSocket',function($rootScope) {
var websocket = new WebSocket(websocket_url);
var items = [];
websocket.onmessage = function(msg) {
items.push(JSON.parse(msg.data));
$rootScope.$broadcast('new_message');
}
return {
fetchItems: function() {
return items;
}
}
});
app.controller('ItemsCtrl',function($scope,WebSocket) {
$scope.$on('new_message',function() {
$scope.$apply(function() {
$scope.items = WebSocket.fetchItems();
});
});
});
My question is if anyone else has setup an Angular app using websockets and if this implementation is the correct way to go about it or if there is a better solution. I've read many cons on using $broadcast but this seems to be the correct usage of the $broadcast functionality.
The way you have done it seems fine. An alternative way I do it though is to store an event/callback array, and register the events on it that I want to receive specifically.
For example:
angular.module('myapp.services.socket', [])
.factory('io', ['$rootScope', 'globals', function ($rootScope, globals) {
var socket;
var curChannel;
var eventCache = [];
function isConnected() {
return socket && socket.socket.connected;
}
function on(eventName, callback) {
socket.on(eventName, function () {
var args = arguments;
$rootScope.$apply(function () {
callback.apply(socket, args);
});
});
}
function emit(eventName, data, callback) {
socket.emit(eventName, data, function () {
var args = arguments;
$rootScope.$apply(function () {
if (callback) {
callback.apply(socket, args);
}
});
});
}
return {
registerEvent: function(eventName, callback) {
eventCache.push({ name: eventName, cb: callback });
if(isConnected()){
on(eventName, callback);
}
},
emit: function (eventName, data, callback) {
// firstly check that the socket is connected
if(isConnected()){
emit(eventName, data, callback);
}else{
// connect to the server and subscribe upon connection
socket = io.connect(globals.api + ':80');
socket.on('connect', function(){
emit(eventName, data, callback);
// add the events from the cache
for(var i in eventCache){
on(eventCache[i].name, eventCache[i].cb);
}
});
}
}
};
}]);
This way, you can simply register event callbacks whenever you want, by injecting this service, and running:
io.registerEvent('some_event', function(){ /* some logic */ });

Angular service wire up not working

I have written a service, depending on an other service. But initialisation is not working.
You can find a plunker as showcase
Should be close to working... Any tipps?
Thanks in advance!
edit: The plunker is fixed now and can be used as reference.
You need to either change your testServiceMockConfig and testService from factory to service, for example:
.service('testServiceMockConfig', function ()
or keep them as factories and add return.this; at the bottom of both of them or restructure them like this (recommended):
angular.module('testServiceMockConfig', [])
.factory('testServiceMockConfig', function() {
console.log("setup cqrs mock config.");
return {
doLoadItems: function(callback) {
console.log("mock loading data");
if (!this.configuredLoadItems) {
throw Error("The mock is not configured to loadItems().");
}
callback(this.loadItemsError, this.loadItemsSuccess);
},
whenLoadItems: function(success, error) {
this.configuredLoadItems = true;
this.loadItemsSuccess = success;
this.loadItemsError = error;
}
};
});
I also assume that loadItems in testService should call:
testServiceMockConfig.doLoadItems(callback);
instead of:
testService.doLoadItems(callback);
As I see from your example,
you didn't define properly the factory. The this key used for service
in testService.doLoadItems(callback); replace with testServiceMockConfig.doLoadItems(callback);
The difference between service - factory - provider and definition you can find in this simple demo:
Fiddle
Fixed example:
angular.module('testServiceMockConfig', [])
.factory('testServiceMockConfig', function () {
console.log("setup cqrs mock config.");
return{
doLoadItems : function (callback) {
console.log("mock loading data");
if (!this.configuredLoadItems) {
throw Error("The mock is not configured to loadItems().");
}
callback(this.loadItemsError, this.loadItemsSuccess);
},
whenLoadItems : function (success, error) {
this.configuredLoadItems = true;
this.loadItemsSuccess = success;
this.loadItemsError = error;
}
}
});
angular.module('testService', ['testServiceMockConfig'])
.factory('testService', ['testServiceMockConfig', function (testServiceMockConfig) {
console.log("mock version. testServiceMockConfig: ");
return {
loadItems : function (callback) {
testServiceMockConfig.doLoadItems(callback);
}
}
}])
angular.module('ItemApp', ['testService'])
.controller('ItemsCtrl', ['$scope', 'testService', function ($scope, testService) {
$scope.text = 'No items loaded';
testService.loadItems(function (error, items) {
if (error) {
$scope.text = "Error happened";
}
$scope.text = '';
for (i = 0; i < items.length; i++) {
$scope.text = $scope.text + items[i].name;
}
})
}]);
Demo Plunker

Resources