Executing angular services in some other scope? - javascript

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.

Related

Unable to add $scope to service in angularjs? [duplicate]

I have a Service:
angular.module('cfd')
.service('StudentService', [ '$http',
function ($http) {
// get some data via the $http
var path = 'data/people/students.json';
var students = $http.get(path).then(function (resp) {
return resp.data;
});
//save method create a new student if not already exists
//else update the existing object
this.save = function (student) {
if (student.id == null) {
//if this is new student, add it in students array
$scope.students.push(student);
} else {
//for existing student, find this student using id
//and update it.
for (i in students) {
if (students[i].id == student.id) {
students[i] = student;
}
}
}
};
But when I call save(), I don't have access to the $scope, and get ReferenceError: $scope is not defined. So the logical step (for me), is to provide save() with the $scope, and thus I must also provide/inject it to the service. So if I do that like so:
.service('StudentService', [ '$http', '$scope',
function ($http, $scope) {
I get the following error:
Error: [$injector:unpr] Unknown provider: $scopeProvider <- $scope <-
StudentService
The link in the error (wow that is neat!) lets me know it is injector related, and might have to do with order of declaration of the js files. I have tried reordering them in the index.html, but I think it is something more simple, such as the way I am injecting them.
Using Angular-UI and Angular-UI-Router
The $scope that you see being injected into controllers is not some service (like the rest of the injectable stuff), but is a Scope object. Many scope objects can be created (usually prototypically inheriting from a parent scope). The root of all scopes is the $rootScope and you can create a new child-scope using the $new() method of any scope (including the $rootScope).
The purpose of a Scope is to "glue together" the presentation and the business logic of your app. It does not make much sense to pass a $scope into a service.
Services are singleton objects used (among other things) to share data (e.g. among several controllers) and generally encapsulate reusable pieces of code (since they can be injected and offer their "services" in any part of your app that needs them: controllers, directives, filters, other services etc).
I am sure, various approaches would work for you. One is this:
Since the StudentService is in charge of dealing with student data, you can have the StudentService keep an array of students and let it "share" it with whoever might be interested (e.g. your $scope). This makes even more sense, if there are other views/controllers/filters/services that need to have access to that info (if there aren't any right now, don't be surprised if they start popping up soon).
Every time a new student is added (using the service's save() method), the service's own array of students will be updated and every other object sharing that array will get automatically updated as well.
Based on the approach described above, your code could look like this:
angular.
module('cfd', []).
factory('StudentService', ['$http', '$q', function ($http, $q) {
var path = 'data/people/students.json';
var students = [];
// In the real app, instead of just updating the students array
// (which will be probably already done from the controller)
// this method should send the student data to the server and
// wait for a response.
// This method returns a promise to emulate what would happen
// when actually communicating with the server.
var save = function (student) {
if (student.id === null) {
students.push(student);
} else {
for (var i = 0; i < students.length; i++) {
if (students[i].id === student.id) {
students[i] = student;
break;
}
}
}
return $q.resolve(student);
};
// Populate the students array with students from the server.
$http.get(path).then(function (response) {
response.data.forEach(function (student) {
students.push(student);
});
});
return {
students: students,
save: save
};
}]).
controller('someCtrl', ['$scope', 'StudentService',
function ($scope, StudentService) {
$scope.students = StudentService.students;
$scope.saveStudent = function (student) {
// Do some $scope-specific stuff...
// Do the actual saving using the StudentService.
// Once the operation is completed, the $scope's `students`
// array will be automatically updated, since it references
// the StudentService's `students` array.
StudentService.save(student).then(function () {
// Do some more $scope-specific stuff,
// e.g. show a notification.
}, function (err) {
// Handle the error.
});
};
}
]);
One thing you should be careful about when using this approach is to never re-assign the service's array, because then any other components (e.g. scopes) will be still referencing the original array and your app will break.
E.g. to clear the array in StudentService:
/* DON'T DO THAT */
var clear = function () { students = []; }
/* DO THIS INSTEAD */
var clear = function () { students.splice(0, students.length); }
See, also, this short demo.
LITTLE UPDATE:
A few words to avoid the confusion that may arise while talking about using a service, but not creating it with the service() function.
Quoting the docs on $provide:
An Angular service is a singleton object created by a service factory. These service factories are functions which, in turn, are created by a service provider. The service providers are constructor functions. When instantiated they must contain a property called $get, which holds the service factory function.
[...]
...the $provide service has additional helper methods to register services without specifying a provider:
provider(provider) - registers a service provider with the $injector
constant(obj) - registers a value/object that can be accessed by providers and services.
value(obj) - registers a value/object that can only be accessed by services, not providers.
factory(fn) - registers a service factory function, fn, that will be wrapped in a service provider object, whose $get property will contain the given factory function.
service(class) - registers a constructor function, class that will be wrapped in a service provider object, whose $get property will instantiate a new object using the given constructor function.
Basically, what it says is that every Angular service is registered using $provide.provider(), but there are "shortcut" methods for simpler services (two of which are service() and factory()).
It all "boils down" to a service, so it doesn't make much difference which method you use (as long as the requirements for your service can be covered by that method).
BTW, provider vs service vs factory is one of the most confusing concepts for Angular new-comers, but fortunately there are plenty of resources (here on SO) to make things easier. (Just search around.)
(I hope that clears it up - let me know if it doesn't.)
Instead of trying to modify the $scope within the service, you can implement a $watch within your controller to watch a property on your service for changes and then update a property on the $scope. Here is an example you might try in a controller:
angular.module('cfd')
.controller('MyController', ['$scope', 'StudentService', function ($scope, StudentService) {
$scope.students = null;
(function () {
$scope.$watch(function () {
return StudentService.students;
}, function (newVal, oldVal) {
if ( newValue !== oldValue ) {
$scope.students = newVal;
}
});
}());
}]);
One thing to note is that within your service, in order for the students property to be visible, it needs to be on the Service object or this like so:
this.students = $http.get(path).then(function (resp) {
return resp.data;
});
Well (a long one) ... if you insist to have $scope access inside a service, you can:
Create a getter/setter service
ngapp.factory('Scopes', function (){
var mem = {};
return {
store: function (key, value) { mem[key] = value; },
get: function (key) { return mem[key]; }
};
});
Inject it and store the controller scope in it
ngapp.controller('myCtrl', ['$scope', 'Scopes', function($scope, Scopes) {
Scopes.store('myCtrl', $scope);
}]);
Now, get the scope inside another service
ngapp.factory('getRoute', ['Scopes', '$http', function(Scopes, $http){
// there you are
var $scope = Scopes.get('myCtrl');
}]);
Services are singletons, and it is not logical for a scope to be injected in service (which is case indeed, you cannot inject scope in service). You can pass scope as a parameter, but that is also a bad design choice, because you would have scope being edited in multiple places, making it hard for debugging. Code for dealing with scope variables should go in controller, and service calls go to the service.
You could make your service completely unaware of the scope, but in your controller allow the scope to be updated asynchronously.
The problem you're having is because you're unaware that http calls are made asynchronously, which means you don't get a value immediately as you might. For instance,
var students = $http.get(path).then(function (resp) {
return resp.data;
}); // then() returns a promise object, not resp.data
There's a simple way to get around this and it's to supply a callback function.
.service('StudentService', [ '$http',
function ($http) {
// get some data via the $http
var path = '/students';
//save method create a new student if not already exists
//else update the existing object
this.save = function (student, doneCallback) {
$http.post(
path,
{
params: {
student: student
}
}
)
.then(function (resp) {
doneCallback(resp.data); // when the async http call is done, execute the callback
});
}
.controller('StudentSaveController', ['$scope', 'StudentService', function ($scope, StudentService) {
$scope.saveUser = function (user) {
StudentService.save(user, function (data) {
$scope.message = data; // I'm assuming data is a string error returned from your REST API
})
}
}]);
The form:
<div class="form-message">{{message}}</div>
<div ng-controller="StudentSaveController">
<form novalidate class="simple-form">
Name: <input type="text" ng-model="user.name" /><br />
E-mail: <input type="email" ng-model="user.email" /><br />
Gender: <input type="radio" ng-model="user.gender" value="male" />male
<input type="radio" ng-model="user.gender" value="female" />female<br />
<input type="button" ng-click="reset()" value="Reset" />
<input type="submit" ng-click="saveUser(user)" value="Save" />
</form>
</div>
This removed some of your business logic for brevity and I haven't actually tested the code, but something like this would work. The main concept is passing a callback from the controller to the service which gets called later in the future. If you're familiar with NodeJS this is the same concept.
Got into the same predicament. I ended up with the following. So here I am not injecting the scope object into the factory, but setting the $scope in the controller itself using the concept of promise returned by $http service.
(function () {
getDataFactory = function ($http)
{
return {
callWebApi: function (reqData)
{
var dataTemp = {
Page: 1, Take: 10,
PropName: 'Id', SortOrder: 'Asc'
};
return $http({
method: 'GET',
url: '/api/PatientCategoryApi/PatCat',
params: dataTemp, // Parameters to pass to external service
headers: { 'Content-Type': 'application/Json' }
})
}
}
}
patientCategoryController = function ($scope, getDataFactory) {
alert('Hare');
var promise = getDataFactory.callWebApi('someDataToPass');
promise.then(
function successCallback(response) {
alert(JSON.stringify(response.data));
// Set this response data to scope to use it in UI
$scope.gridOptions.data = response.data.Collection;
}, function errorCallback(response) {
alert('Some problem while fetching data!!');
});
}
patientCategoryController.$inject = ['$scope', 'getDataFactory'];
getDataFactory.$inject = ['$http'];
angular.module('demoApp', []);
angular.module('demoApp').controller('patientCategoryController', patientCategoryController);
angular.module('demoApp').factory('getDataFactory', getDataFactory);
}());
Code for dealing with scope variables should go in controller, and service calls go to the service.
You can inject $rootScope for the purpose of using $rootScope.$broadcast and $rootScope.$on.
Otherwise avoid injecting $rootScope. See
Common Pitfalls: $rootScope exists, but it can be used for evil.

Jasmine test private callback function

I know that calling private functions directly in unitTests is not a good practice and we must test the private code trough public methods.
I'm in a case that I don't know what to do to achieve what I want. I want to know if a callback function has been called from my interval. This is implemented in an angular controller.
function prepareInterval() {
self.callbacksData = [];
if(self.DynamicValuesList !== null) {
self.myPromise = $interval(callbackFunction, userInputInterval * 1000);
}
}
and my callback function only shows the data from the callbackFunction. I want to unitTest if that callbackFunction has been called but I can't.
I tried https://makandracards.com/makandra/32477-testing-settimeout-and-setinterval-with-jasmine
it('myUnitTest', function(){
//Prepare data
var controller = createController();
spyOn(controller, 'callbackFunction');
expect(controller.callbackFunction).not.toHaveBeenCalled();
});
The error that I'm getting is.
callbackFunction() method does not exist
EDIT: By the way I'm injecting the angular mock in the beforeEach function
I would take a slightly different approach here.
Obviously you don't want the callbackFunction itself to be exposed, so don't. Keep it private.
You do, however, return it as a value to your self instance.
self.myPromise = $interval(callbackFunction, userInputInterval * 1000);
So what you cán test, is that self.myPromise value. If that value is containing the interval, your interval has been set and thus you can be pretty sure your method has been called.
I expect that self object to be the controller, so you can just test the value of self.myPromise like this:
it('myUnitTest', function(){
//Prepare data
var controller = createController();
expect(controller.myPromise).toBe( /* undefined? */);
});
update
Just to test the interval value > 0:
You can try to refactor for testability. I'm not sure if the stringmatcher works on numbers though, and don't have time for a test myself now :)
function prepareInterval() {
self.callbacksData = [];
if(self.DynamicValuesList !== null) {
self.myPromise = $interval;
self.myPromise(callbackFunction, userInputInterval * 1000);
}
}
it('myUnitTest', function(){
//Prepare data
var controller = createController();
spyOn(controller, 'myPromise');
expect(controller.myPromise).toHaveBeenCalledWith(jasmine.any(Function), jasmine.stringMatching(/^[1-9][0-9]*$/));
});

AngularJs / Propagating efficiently WebSockets events/messages to various controllers

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.

$http promise in angular service

I am having a problem with promises in an angular service. I have a service with a method getArea which is supposed to return the name of a service-area. The service gets the service-areas from the API. When getArea gets the service-areas, it finds the name of the requested area, and should return it. However, my code does not work - I get into an infinite loop. I guess I have misunderstood how to use promises?
SupplierService:
var servicePromise;
var getServices = function(){
if( !servicePromise ){
servicePromise = $http.get('/api/services')
.then(function(res){
return res.data.data;
});
}
return servicePromise;
};
var myService = {
getServices : getServices,
getArea : function(questionnaireId){
getServices().then(function(services){
// ...
return "hello world";
});
}
};
return myService;
Controller:
$scope.supplierService = SupplierService;
View:
<div>
<b>Area:</b> {{ supplierService.getArea(r.questionnaireId) }}
</div
I expect the view to show "Area: hello world", but gets into an infinite loop.
Update 1: I have added getServices as a public function in the service, and can access it from the controller like this:
SupplierService.getServices().then(function(d){
$scope.services = d;
});
Therefore I guess the problem is in the getArea method?
Update 2: I was inspired by this answer https://stackoverflow.com/a/12513509/685352. I want to cache the result.
Update 3: Here is a plunker. If you try accessing supplierService.getArea(100) from the view - the browser will not respond.
Your service should look more like this:
var getServices = function(){
var deferred = $q.deferred();
$http.get('/api/services')
.then(function(res){
deferred.resolve(res.data)
});
return deferred.promise;
};
Notice when you create a deferred you must return the deferred.promise (the actual promise) and then when you're async call returns you must call deferred.resolve or deferred.rejected as appropriate (to trigger the success or error functions respectively)
Minor addition I have a plunkr showing a few ways of getting data from a service into your controllers since this is a common issue for devs coming into Angular
http://plnkr.co/edit/ABQsAxz1bNi34ehmPRsF?p=info
It's not absolute best practices since I tried to keep it as simple as possible, but basically showing three different ways to "share" your data keep in mind some of these methods rely on angular.copy which means the property of the service you store the data on must be an Object or an Array (primitive types won't work since the reference can't be shared).
Here's a rewrite including the function inline:
var myService = {
var dataLoaded = false;
var data = {}; //or = [];
getServices : function(){
var deferred = $q.defer();
if( !dataLoaded ){
$http.get('/api/services').then(function(res){
angular.copy(res.data, myService.data);
deferred.resolve(myService.data);
}, function(err){
deferred.reject("Something bad happened in the request");
});
}
else
{
deferred.resolve(myService.data);
}
return deferred.promise;
}
};
return myService;
To explain, I create a new promise using the $q service which you'll need to inject to the service function. This allows me to either resolve that promise with data I already have or to make the call to the service and resolve that data but in both cases when this is being used it's assumed you will just get a promise back and are therefore dealing with an async operation. If you have multiple data sets to load you can use an object to store the flags instead of a single boolean.
i think if you return the $http callback?
//$http.get('/someUrl').success(successCallback);
var getServices = function(){
return $http.get('/api/services');
};
getServices.success(function(services){
// ...
return "hello world";
});
}

Unset object property

I have a provider:
AdviceList.provider('$adviceList',function(){
this.$get = function ($rootScope,$document,$compile,$http,$purr){
function AdviceList(){
$http.post('../sys/core/fetchTreatments.php').success(function(data,status){
this.treatments = data;
console.log(this.treatments); // the correct object
});
this.adviceCategories = [
// available in the controller
];
}
return{
AdviceList: function(){
return new AdviceList();
}
}
}
});
Further, i have this controller:
AdviceList.controller('AdviceListCtrl',function($scope,$adviceList){
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; // undefined
});
Why is it, that the controller's $scope.treatments stays undefined, this.treatments inside the provider however, is filled correctly? Also, adviceCategories is available in my controller.
The call you get teatment is async in nature so the results may not have been populated when you try to assign them.
So here
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; //The treatments would only get filled after the server call is over.
You need to rewrite the code in a way that you assign it to your scope property on the success callback.
I will recommend you to simplify your code
1) Use simple factory method of angular instead of provider
2) return a promise to avoid using callbacks
AdviceList.service('adviceList', function ($http) {
return {
adviceList: function () {
return $http.post('../sys/core/fetchTreatments.php');
}
}
});
AdviceList.controller('AdviceListCtrl', function ($scope, $adviceList) {
adviceList.AdviceList().then(function (data) {
$scope.treatments = data //set value to data when data is recieved from server
});
});

Categories

Resources