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.
I would like to do something like this:
app.config(function($routeProvider){
$routeProvider.when('products/list', {
controller: 'ProductListCtrl',
templateUrl : 'products/list/view.html',
resolve : { data : function(){
...
},
loadingTemplateUrl : 'general/loader.html'
}
});
I would like to have the loading page in a different view.
This would make the code in the view and controller of every page cleaner, (no <...ng-include ng-show="loading"...>). This would also mean that I don't have to $scope.$watch the data for changes. Is there a clean solution to do something similar (not necessarily in the .config method) or an alternative library to do this?
Assuming you want to show some general template for all state transitions while the data is resolved, my suggestion is to listen to the events fired by the routing library. This allows to use one central point to handle all state transitions instead of polluting the routing config (which I think will not be that easy to do).
Please see the docs for $routeChangeStart, $routeChangeSuccess and of course $routeChangeError at the angular router docs
Maybe someone could be interested in what I did: I created a new service and a new view directive. It could seem like a lot of work, but doing this was much easier than I had expected. The new service enables me to separate the main view from the loading view, that I could reuse in all pages of the application. I also provided the possibility to configure an error template url and error controller, for when the loading failed.
The Angular $injector, $templateRequest and $controller services do most of the work. I just had to connect a directive, that depends on these services, to the right event ($locationChangeSuccess), and to the promise, retrieved (using $q.all) from the resolve object's functions. This connection was done in the route service. The service selects the right template url and comtroller, and passes it on for the directive to handle.
A shortened version (with the getCurrentConfig method left out):
RouteService:
(function () {
'use strict';
// provider:
angular.module('pikcachu')
.provider('pikaRouteService', [function () {
var routeConfigArray;
var otherwiseRouteConfig;
//configuration methods
this.when = function (url, routeConfig){
routeConfigArray.push({url: url, routeConfig: routeConfig});
return this;
}
this.otherwise = function(routeConfig){
otherwiseRouteConfig = routeConfig;
return this;
}
// service factory:
this.$get = ['$rootScope', '$location', '$q', '$injector', '$templateRequest',
function ($rootScope, $location, $q, $injector, $templateRequest) {
function RouteService() {
this.setViewDirectiveUpdateFn = function(){ /*...*/ }
function init(){
$rootScope.$on('$locationChangeSuccess', onLocationChangeSuccess);
}
function onLocationChangeSuccess(){
// get the configuration based on the current url
// getCurrentConfig is a long function, because it involves parsing the templateUrl string parameters, so it's left out for brevity
var currentConfig = getCurrentConfig($location.url());
if(currentConfig.resolve !== undefined){
// update view directive to display loading view
viewDirectiveUpdateFn(currentConfig.loadingTemplateUrl, currentConfig.loadingController);
// resolve
var promises = [];
var resolveKeys = [];
for(var resolveKey in currentConfig.resolve){
resolveKeys.push(resolveKey);
promises.push($injector.invoke(resolve[resolveKey]));
}
$q.all(promises).then(resolveSuccess, resolveError);
function resolveSucces(resolutionArray){
// put resolve results in an object
var resolutionObject = {};
for(var i = 0; i< promises.length;++i){
resolved[resolveKeys[i]] = resolutionArray[i];
}
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
function resolveError(){
viewDirectiveUpdateFn(currentConfig.errorTemplateUrl, currentConfig.errorController);
}
}
}
init();
}
return new RouteService();
}]
})();
View directive
(function () {
'use strict';
angular.module('pikachu')
.directive('pikaView', ['$templateRequest', '$compile', '$controller', 'pikaRouteService', function ($templateRequest, $compile, $controller, pikaRouteService) {
return function (scope, jQdirective, attrs) {
var viewScope;
function init() {
pikaRouteService.listen(updateView);
}
function updateView(templateUrl, controllerName, resolved) {
if(viewScope!== undefined){
viewScope.$destroy();
}
viewScope = scope.$new();
viewScope.resolved = resolved;
var controller = $controller(controllerName, { $scope: viewScope });
$templateRequest(templateUrl).then(onTemplateLoaded);
function onTemplateLoaded(template, newScope) {
jQdirective.empty();
var compiledTemplate = $compile(template)(newScope);
jQdirective.append(compiledTemplate);
}
}
init();
};
}
]);
})();
Here is the code snippet about an Angular Controller. I'm trying to learn Angular from this github project
The questionable part is located in function addStock. I have already defined $scope.watchlist in the initilizations part, however, if I remove the re-declaration inside $scope.addStock function, $scope.watchlist will not be populated with right values. Can any Angular experts point me out?
If you want to see my full project code - here is the link.
angular.module('stockCatApp')
.controller('WatchlistCtrl', function ($scope, $routeParams, $modal, WatchlistService, CompanyService) {
// Initializations
$scope.companies = CompanyService.query();
$scope.watchlist = WatchlistService.query($routeParams.listId);
$scope.stocks = $scope.watchlist.stocks;
$scope.newStock = {};
var addStockModal = $modal({
scope: $scope,
template: 'views/templates/addstock-modal.html',
show: false
});
$scope.showStockModal = function () {
addStockModal.$promise.then(addStockModal.show);
};
$scope.addStock = function () {
//The following line needs to be put here again in order to retrieve the right value
$scope.watchlist = WatchlistService.query($routeParams.listId);
///////////////////////////////////////////////////////////////////////
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
addStockModal.hide();
$scope.newStock = {};
};
});
Can you do a console.log($scope.watchlist); after both of your $scope binds to see if the data and its type match?
$scope.watchlist = WatchlistService.query($routeParams.listId);
console.log($scope.watchlist);
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
console.log($scope.watchlist);
Feel free to post the logs.
It appears as if you are calling $scope.addStock() from a modal. In which case the modal does not necessarily inherit the parent scope. It depends on which versions of everything you are using. It looks like you are using angular-strap (I looked in your bower.json). If you look in their $modal source code you will find this:
https://github.com/mgcrea/angular-strap/blob/master/src/modal/modal.js
var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new();
So, when you are opening your modal you are creating a new scope. You might be able to access the original scope using $parent though.
I have a quite hard time to build a (maybe non-trivial) directive for an SPA. Basically I need to be able to pour data into the directive from any controller and the directive is supposed to show a live graph.
I've read this post and I would like to use a shared object on the isolated scope of this directive anyway.
So I tried to do smth like this:
Wrapping template:
<div ng-controller="WrappingCtrl">
<timeline-chart d3API="d3API"><timeline-chart>
</div>
In the 'wrapping' controller:
$scope.d3API = {};
$scope.d3API.options = {}; //for d3Config
$scope.d3API.currentValue = 3; //asynchronous!!!
Finally to use the shared object d3API in the directive's link method I tried e.g. this:
//in the directive:
scope: { //nice, but does it help??
d3API: '='
}
and:
var data = [1, 2];
var updateTimeAxis = function() {
var newValue;
if (data.length) {
newValue = (data[data.length - 1] !== scope.d3API.currentValue) ? scope.d3API.currentValue : data[data.length - 1];
data.push(newValue);
} else {
console.warn('problem in updateTimeAxis: no data length');
}
};
To gain some simplicity for this question I've created a fiddle, note, that none of both are working:
http://jsfiddle.net/MalteFab/rp55vjc8/3/
http://jsfiddle.net/MalteFab/rp55vjc8/5/
The value in the directive's template is not updated - what am I doing wrong? Any help is appreciated.
Your fiddle mostly works, you just need to update your controller to use $timeout:
app.controller('anyCtrl', function($scope, $timeout) {
show('anyCtrl');
$scope.bound = {};
$timeout(function() {
$scope.bound.says = 'hello';
}, 200);
});
Forked fiddle: http://jsfiddle.net/wvt1f1zt/
Otherwise no digest occurs so angular doesn't know something changed. Based on what you're actual problem is, I'm assuming you're not using timeout vs $timeout, but if your coding style is to intermix angular with "normal" javascript, you may be running into the same kind of scenario.
A good article for reference for telling angular about what your doing is here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
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.