i cant get my head around what is the recommended way to communicate from a service to a custom directive. The custom directive is an interactive svg graphic, which on user interaction calls a method of an injected service to retrieve new data. This should happen in an asynchronous manner. I read here and there that events are in general not the recommend way to communicate in angularjs. Should I use a callback function? Or?
Thanks buddies
martin
You inject the service into the directive, and then the directive calls methods on the service passing in argument values as parameters.
To let a directive know that a service method has completed asynchronously, have the service method return a promise object.
http://jsfiddle.net/gGhtD/5/
var myApp = angular.module('myApp', []);
//myApp.directive('myDirective', function() {});
myApp.factory('myService', function ($q, $timeout) {
return {
doSomething: function (msg) {
var d = $q.defer();
$timeout(function () {
d.resolve("resolved: " + msg);
}, 1500);
return d.promise;
}
}
});
function MyCtrl($scope, myService) {
$scope.callService = function () {
$scope.sent = new Date();
$scope.msg = "";
$scope.timestamp = "";
myService.doSomething("some value")
.then(function (data) {
$scope.timestamp = new Date();
$scope.msg = data;
});
}
}
Related
Short question is: do we need to use var self = this; in a AngularJS Factory and Service?
I have seen AngularJS code for factory or service that uses a var self = this; to remember itself (the service object), and then inside of the functions of the service object, use self to refer to itself. (kind of like how we are afraid we will lose what this is, so we set self = this to use it later, by the principle of closure.
However, I have found that we can safely use this. Why? Because we get back a singleton service object, and when we invoke myService.getNumber(), the this is bound to the service object myService, so it does work. Example:
If it is a service:
https://jsfiddle.net/yx2s3e72/
angular.module("myApp", [])
.service("myService", function() {
console.log("Entering factory");
this.s = "hello";
this.getLength = function() {
return this.s.length;
};
})
.controller("myController", function(myService) {
console.log("Entering controller");
var vm = this;
vm.s = myService.s;
vm.length = myService.getLength();
});
or if it is a factory:
https://jsfiddle.net/935qmy44/
angular.module("myApp", [])
.factory("myService", function() {
console.log("Entering factory");
return {
s: "hello",
getLength: function() {
return this.s.length;
}
};
})
// controller code not repeated here...
It works too. So is it true that we really don't need to set a var self = this; in a custom AngularJS factory or service?
You may lose the reference to this, therefore the reference is saved in the self var.
angular.module('myApp', [])
.run(function (MyService) {
MyService.method();
})
.service('MyService', function ($timeout) {
this.key = 'value';
this.method = function () {
console.log(this) // service
$timeout(function () {
console.log(this); // window because callback is called from the window
}, 0);
}
});
angular.bootstrap(document.querySelector('#app'), ['myApp']);
See JSFiddle
Since I wrote firebase-factory separately from RecipeController, I have an error in my Test.
TypeError: Cannot read property '$loaded' of undefined.
$loaded is a method in firebase...
test.js
describe('RecipeController', function() {
beforeEach(module('leChef'));
var $controller;
beforeEach(inject(function(_$controller_){
$controller = _$controller_;
}));
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
});
firebase-factory.js
app.factory("Recipes", ["$firebaseArray",
function($firebaseArray) {
var ref = new Firebase("https://fiery-inferno-8595.firebaseio.com/recipes/");
return $firebaseArray(ref);
}
]);
recipe-controller.js
app.controller("RecipeController", ["$scope", "toastr", "$location", "$routeParams", "$compile", "Recipes",
function($scope, toastr, $location, $routeParams, $compile, Recipes) {
$scope.recipes.$loaded().then(function(payload) {
$scope.recipe = payload.$getRecord($routeParams.id);
$scope.html = $scope.recipe.instructions;
if (typeof $scope.recipe.reviews === "undefined") {
$scope.recipe.reviews = [{}];
}
$scope.calculateAverage = function(AverageData){
var sum = 0;
if ($scope.recipe.reviews.length > 1) {
for(var i = 1; i < $scope.recipe.reviews.length; i++){
sum += parseInt($scope.recipe.reviews[i].stars, 10);
}
var average = sum/($scope.recipe.reviews.length-1);
var roundedAverage = Math.round(average);
return {
average: roundedAverage,
markedStars: new Array(roundedAverage)
};
} else {
return sum;
}
};
});
]);
In your RecipeController definition, you immediately call:
$scope.recipes.$loaded().then(function(payload) { ... }
...assuming that $scope.recipes is defined and has a property of $loaded -- which is not the case.
In your test:
describe("$scope.calculateAverage", function() {
it("calculates average correctly", function() {
var $scope = {};
var controller = $controller('RecipeController', { $scope: $scope });
$scope.calculateAverage();
expect(average).toBe(sum/(Recipes.reviews.length-1));
});
});
...you define scope as an empty object, then inject it into your controller.
Assuming you are using Jasmine as a test framework, you could create a spy like this:
var $scope = {
recipes: {
$loaded: function() { /* this is a mock function */ }
}
};
var deferred = $q.defer();
deferred.resolve({
/* this is the data you expect back from $scope.recipes.$loaded */
});
var promise = deferred.promise;
spyOn($scope.recipes, '$loaded').and.returnValue(promise);
This is just one of many ways you could stub out that function and control the data you get in your test. It assumes a basic understanding of the $q service and the Promise API.
Best Practices
It is best not to attach data to the $scope service. I would recommend reading up on the controllerAs syntax, if you're not familiar with it.
TL;DR: A controller is just a JavaScript "class", and the definition function is its constructor. Use var vm = this; and then attach variables to the instance reference vm (as in "view model", or whatever you want to call it) instead.
Rather than relying on $scope.recipes to have been defined elsewhere, you should explicitly define it in your controller. If recipes are defined in another controller, create a service that both controllers can share.
I'm not able to get the data binding between controller and service working.
I have a controller and a factory which makes an HTTP call. I would like to be able to call the factory method from other services and see the controller attributes get updated. I tried different options but none of them seem to be working. Any ideas would be greatly appreciated.
Please see the code here:
http://plnkr.co/edit/d3c16z?p=preview
Here is the javascript code.
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope) {
$scope.name = 'World';
});
app.controller('EventDetailCtrl', ['$http', 'EventDetailSvc', '$scope',
function ($http, EventDetailSvc, $scope) {
this.event = EventDetailSvc.event;
EventDetailSvc.getEvent();
console.log(self.event);
$scope.$watch(angular.bind(this, function () {
console.log('under watch');
console.log(this.event);
return this.event;
}), function (newVal, oldVal) {
console.log('under watch2');
console.log(newVal);
this.event = newVal;
});
}])
.factory('EventDetailSvc', ['$http', function ($http) {
var event = {};
var factory = {};
factory.getEvent = function() {
$http.get('http://ip.jsontest.com')
.then(function (response) {
this.event = response.data;
console.log('http successful');
console.log(this.event);
return this.event;
}, function (errResponse) {
console.error("error while retrieving event");
})
};
factory.event = event;
return factory;
}]);
It seems to me that you have nested the event object inside of a factory object. You should be returning event directly instead wrapping it with factory. As it stands now you would need to call EventDetailSvc.factory.event to access your object.
I have an angular service and a controller interacting. The service usings the $interval to poll the server. I know this returns a promise, however it uses $http to make an call to the server, which ALSO returns a promise and the chaining of the promises is not happening the way I would expect.
SERVICE
(function () {
'use strict';
var serviceId = "notificationService";
angular.module('app').factory(serviceId, ['helpersService', '$interval', '$http', function (helpersService, $interval, $http) {
var defaultOptions = {
url: undefined,
interval: 1000
};
var myIntervalPromise = undefined;
var displayedNotifications = [];
function onNotificationSuccess(response) {
//alert("in success");
displayedNotifications.push(response.data);
return response.data;
}
function onNotificationFailed(response) {
alert("in Failure");
throw response.data || 'An error occurred while attempting to process request';
}
function initializeNotificationService(configOptions) {
var passedOptions = $.extend({}, defaultOptions, configOptions);
if (passedOptions.url) {
myIntervalPromise = $interval(
function() {
//console.log(passedOptions.url);
//return helpersService.getAjaxPromise(passedOptions);
//promise.then(onNotificationSuccess, onNotificationFailed);
$http({
method: 'POST',
url: passedOptions.url
}).then(onNotificationSuccess, onNotificationFailed);
}, passedOptions.interval);
//alert("in initializeNotificationService");
return myIntervalPromise;
}
//return myIntervalPromise;
}
//$scope.$on('$destroy', function() {
// if (angular.isDefined(myIntervalPromise)) {
// $interval.cancel(myIntervalPromise);
// myIntervalPromise = undefined;
// }
//});
return {
// methods
initializeNotificationService: initializeNotificationService,
//properties
displayedNotifications : displayedNotifications
};
}]);
})();
CONTROLLER
(function () {
'use strict';
var controllerId = 'MessageCtrl';
//TODO: INVESTIGATE HAVING TO PASS $INTERVAL TO HERE TO DESTROY INTERVAL PROMISE.
//TODO: HAS TO BE A WAY TO MOVE THAT INTO THE SERVICE
angular.module('app').controller(controllerId, ['notificationService', '$scope', '$interval', function (notificationService, $scope, $interval) {
var vm = this;
// tied to UI element
vm.notifications = [];
vm.initialize = function () {
// initialize tyhe notification service here
var intervalPromise = notificationService.initializeNotificationService({ url: 'api/userProfile/getNotifications', interval: 5000 });
intervalPromise.then(
function (response) {
// NEVER GETS CALLED
var s = "";
//vm.notifications.push(response);
// alert("successful call");
},
function (response) {
var s = "";
// THIS GETS CALLED WHEN THE PROMISE IS DESTROYED
// response = canceled
//alert("failure to call");
},
function(iteration) {
console.log(notificationService.displayedNotifications);
// This gets called on every iteration of the $interval in the service
vm.notifications = notificationService.displayedNotifications;
}
);
// TODO: SEE COMMENT AT TOP OF CONTROLLER
$scope.$on('$destroy', function () {
if (angular.isDefined(intervalPromise)) {
$interval.cancel(intervalPromise);
intervalPromise = undefined;
}
});
};
vm.alertClicked = function (alert) {
alert.status = 'old';
};
// call to init the notification service here so when the controller is loaded the service is initialized
vm.initialize();
}]);
})();
The way this ends up flowing, and I'll do my best to show flow here
1) SERVICE - $interval makes the call with the $http BOTH OF THESE SEEM TO RETURN THEIR OWN PROMISES ACCORDING TO THE DOCS
2) CONTROLLER - intervalPromise's NOTIFY callack is called
3) SERVICE - onNotificationSuccess callback of $http is called
WHAT DOESN'T HAPPEN THAT I WOULD EXPECT
4) CONTROLLER - intervalPromise success callback is never called
Should the return response.data in the onNotificationSuccess handler within the service trigger the then chain in the Controller? It's aware that the promise is returned or seemingly cause the notify callback in the controller is called each time $interval executes, so I'm confused as to where the chain is broken.
IDEAL
$interval calls with $http, the promise from $http is passed up to the controller
then with each iteration new messages are added to the service on a successful call by $interval, then in the controller onsuccess I can check the property of the service and update the UI. Where am I losing the method chain?
I would recommend breaking the usage of $interval outside of service and use it directly in your controller.
The service being provided is the ability to get data from the server and the interval is the means in which to get the data, which is more indicative of the user interface's requirements as to how often the data is retrieved.
What you appear to be doing is to wrap the functionality of the $interval service which is causing a complication for you.
Note: after creating a quick plnkr the report progress event of $interval returns the iteration number (times called) and no other parameters.
Ended up with everything in the controller...
(function () {
'use strict';
var controllerId = 'NotificationCtrl';
angular.module('app').controller(controllerId, ['helpersService', '$scope', '$interval', function (helpersService, $scope, $interval) {
var vm = this;
var intervalPromise = undefined;
// tied to UI element
vm.notifications = [];
function onNotificationSuccess(response) {
//alert("in success");
vm.notifications.push.apply(vm.notifications, response.data);
return response.data;
}
function onNotificationFailed(response) {
//alert("in Failure");
throw response.data || 'An error occurred while attempting to process request';
}
vm.initialize = function () {
intervalPromise = $interval(
function () {
var promise = helpersService.getAjaxPromise({ url: 'api/userProfile/getNotifications' });
promise.then(onNotificationSuccess, onNotificationFailed);
}, 5000);
$scope.$on('$destroy', function () {
if (angular.isDefined(intervalPromise)) {
$interval.cancel(intervalPromise);
intervalPromise = undefined;
}
});
};
vm.alertClicked = function (alert) {
//alert.status = 'old';
};
// call to init the notification service here so when the controller is loaded the service is initialized
vm.initialize();
}]);
})();
I have a service wrapped around WebSocket, I wanted to do it with promises and coupling requests with responses, here is what I came up with:
(function () {
var app = angular.module('mainModule');
app.service('$wsService', ['$q', '$rootScope', '$window', function($q, $rootScope, $window) {
var self = this;
// Keep all pending requests here until they get responses
var callbacks = {};
// Create a unique callback ID to map requests to responses
var currentCallbackId = 0;
var ws = new WebSocket("ws://127.0.0.1:9090");
this.webSocket = ws;
ws.onopen = function(){
$window.console.log("WS SERVICE: connected");
};
ws.onmessage = function(message) {
listener(JSON.parse(message.data));
};
var listener = function (messageObj) {
// If an object exists with callback_id in our callbacks object, resolve it
if(callbacks.hasOwnProperty(messageObj.Request.ID)) {
$rootScope.$apply(
callbacks[messageObj.Request.ID].cb.resolve(messageObj));
delete callbacks[messageObj.Request.ID];
}
};
// This creates a new callback ID for a request
var getCallbackId = function () {
currentCallbackId += 1;
if(currentCallbackId > 10000) {
currentCallbackId = 0;
}
return currentCallbackId;
};
//sends a request
var sendRequest = function (request, callback) {
var defer = $q.defer();
var callbackId = getCallbackId();
callbacks[callbackId] = {
time: new Date(),
cb:defer
};
request.ID = callbackId;
$window.console.log("WS SERVICE: sending " + JSON.stringify(request));
ws.send(JSON.stringify(request));
if(typeof callback === 'function') {
defer.promise.then(function(data) {
callback(null, data);
},
function(error) {
callback(error, null);
});
}
return defer.promise;
};
this.exampleCommand = function(someObject, callback){
var promise = sendRequest(someObject, callback);
return promise;
};
}]);
}());
And I use it in a controller like so:
(function () {
'use strict';
var app = angular.module('mainModule');
app.controller('someController', ['$scope', '$wsService', function ($scope, $wsService) {
$scope.doSomething = function(){
$wsService.exampleCommand(
{/*some data for the request here*/},
function(error, message){
//do something with the response
}
);
};
}]);
}());
After implementing this, I have been told that the service should not really operate on any kind of scope. So my question is - how would I go about removing the $rootScope from the service? I am not even sure if I should get rid of it, and if the conventions say I should, how to approach it. Thanks
I have been told that the service should not really operate on any kind of scope.
Who told you that? It's completely wrong.
Your service is receiving callbacks outside of a digest cycle from the websocket. To work with angular, those updates need to be applied inside a digest cycle - this is exactly what you're doing.
For reference, see the built in $http service. That wraps XMLHttpRequest analogously to how you're wrapping web sockets and it depends on $rootScope for exactly the functionality your code depends on $rootScope for.
Your code demonstrates the canonical use of $rootScope inside a service.