Angular factory returning a promise - angularjs

When my app starts I load some settings from a server. Most of my controllers need this before anything useful can be done. I want to simplify the controller's code as much as possible. My attempt, which doesn't work, is something like this:
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http, $q) {
var deferred = $q.defer();
$http.get('/api/public/settings/get').success(function(data) {
$rootScope.settings = data;
deferred.resolve();
});
return deferred.promise;
}]);
app.controller('SomeCtrl', ['$rootScope', 'settings', function($rootScope, settings) {
// Here I want settings to be available
}]);
I would like to avoid having a lot of settings.then(function() ...) everywhere.
Any ideas on how to solve this in a nice way?

$http itself return promise you don't need to bind it inside the $q this is not a good practice and considered as Anti Pattern.
Use:-
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http) {
return $http.get('/api/public/settings/get')
}]);
app.controller('SomeCtrl', ['settings',$scope, function(settings,$scope) {
settings.then(function(result){
$scope.settings=result.data;
});
}]);
Your way can be done as :-
app.factory('settings', ['$rootScope', '$http', '$q', function($rootScope, $http, $q) {
var deferred = $q.defer();
$http.get('/api/public/settings/get').success(function(data) {
deferred.resolve(data);
});
return deferred.promise;
}]);
app.controller('SomeCtrl', ['$scope', 'settings', function($scope, settings) {
settings.then(function(data){
$scope.settings=data;
})
}]);
Don't overload $rootScope if you wanted it you need to use $watch for the changes in $rootScope(Not recommended).

Somewhere you would need to "wait".
The only built-in way in Angular to completely absolve the controller from having to wait on its own for async data to be loaded is to instantiate a controller with $routeProvider's route's resolve property (or the alternative $stateProvider of ui.router). This will run controller only when all the promises are resolved, and the resolved data would be injected.
So, ng-route alternative - plunker:
$routeProvider.when("/", {
controller: "SomeCtrl",
templateUrl: "someTemplate.html",
resolve: {
settings: function(settingsSvc){
return settingsSvc.load(); // I renamed the loading function for clarity
}
});
Then, in SomeCtrl you can add settings as an injectable dependency:
.controller("SomeCtrl", function($scope, settings){
if (settings.foo) $scope.bar = "foo is on";
})
This will "wait" to load someTemplate in <div ng-view></div> until settings is resolved.
The settingsSvc should cache the promise so that it won't need to redo the HTTP request. Note, that as mentioned in another answer, there is no need for $q.defer when the API you are using (like $http) already returns a promise:
.factory("settingsSvc", function($http){
var svc = {settings: {}};
var promise = $http.get('/api/public/settings/get').success(function(data){
svc.settings = data; // optionally set the settings data here
});
svc.load = function(){
return promise;
}
return svc;
});
Another approach, if you don't like the ngRoute way, could be to have the settings service broadcast on $rootScope an event when settings were loaded, and controllers could react to it and do whatever. But that seems "heavier" than .then.
I guess the third way - plunker - would be to have an app-level controller "enabling" the rest of the app only when all the dependencies have preloaded:
.controller("AppCtrl", function($q, settingsSvc, someOtherService){
$scope.loaded = false;
$q.all([settingsSvc.load, someOtherService.prefetch]).then(function(){
$scope.loaded = true;
});
});
And in the View, toggle ng-if with loaded:
<body ng-controller="AppCtrl">
<div ng-if="!loaded">loading app...</div>
<div ng-if="loaded">
<div ng-controller="MainCtrl"></div>
<div ng-controller="MenuCtrl"></div>
</div>
</body>

Fo ui-router this is easily done with having an application root state with at least this minimum definition
$stateProvider
.state('app', {
abstract: true,
template: '<div ui-view></div>'
resolve: {
settings: function($http){
return $http.get('/api/public/settings/get')
.then(function(response) {return response.data});
}
}
})
After this you can make all application states inherit from this root state and
All controllers will be executed only after settings are loaded
All controllers will gain access to settings resolved value as possible injectable.
As mentioned above resolve also works for the original ng-route but since it does not support nesting the approach is not as useful as for ui-router.

You can manually bootstrap your application after settings are loaded.
var initInjector = angular.injector(["ng"]);
var $http = initInjector.get("$http");
var $rootScope = initInjector.get("$rootScope");
$http.get('/api/public/settings/get').success(function(data) {
$rootScope.settings = data;
angular.element(document).ready(function () {
angular.bootstrap(document, ["app"]);
});
});
In this case your whole application will run only after the settings are loaded.
See Angular bootstrap documentation for details

Related

angular function call before html view is load

I am working on angularjs (1.6) and want to made a functionality in angular service, its call when a controller call and its service have an ajax code like
app.service('myServ', function($http, $window){
this.backdoor=function(){
$http({
method : 'get',
url : 'web_services/backdoor.php'
}).then(function(res){
// console.log(res.data);
// console.log(res.data.length);
if(res.data.length==0)
{
$window.location.href="index.html";
}
});
}});
and my controller code is :
app.controller('myCtrl', function($scope, $http, $window, myServ, $routeParams){
myServ.backdoor();
});
so the above code (service) is check a user session is created or not, but the problem is when session is not created on server side then my html page load for a second then server will call $window.location.href so please help me about the right way to do this....
I believe you need a resolve in angular.config. Its job is to run some code before you are being redirected to your route/state (for ngRoute or ui.router).
To do that you would need to have:
app.service('myServ', function($http, $window){
this.backdoor=function(){
return $http({ // return the promise if you need to use the values in the controller
method : 'get',
url : 'web_services/backdoor.php'
}).then(function(res){
if(res.data.length==0){ $window.location.href="index.html"; }
else{ return res.data; } // return the values
});
}});
and main part:
app.config(['$routeProvider', function($routeProvider) {
$routeProvider
.when('/your_page/:route_params', {
templateUrl: 'your_page_partial.html',
controller: 'myCtrl',
resolve: {
resolvedVal: function resMyService(myServ, $routeParams){ // setting an injectable instance `resolvedVal`
/* use $routeParams values as parameters? */
return myServ.backdoor(); // calling your service
}
}}
);
}]);
Then it's enough to just inject your resolve into the controller:
app.controller('myCtrl', function($scope, $http, $window, resolvedVal){ // note: resolvedVal injection
$scope.my_data = resolvedVal;
});

$scope not getting defined properly

I am trying to attach notes to $scope instead of controller(this) in the following code:
angular.module('NoteWrangler')
.controller('NotesIndexController', ['$http', function($http) {
var controller = this;
$http({method: 'GET', url: '/notes'}).success(function(data) {
controller.notes = data;
});
}]);
So, when I define $scope like the following :
.controller('NotesIndexController', ['$scope', function($scope)
It doesn't work as I would need to define $http somewhere in order to use it in the code? So, where exactly I should define $scope then?
I have referred this documentation.
You don't "define" $scope, it is a framework object that is managed for you.
If you use the ControllerAs Syntax, your controller can be added to $scope as a property. Otherwise, you would need to refer to the $scope object that is injectable through Angular Dependency Injection. You can inject as many dependencies as you need for your app.
So, the two options which you have available to you are:
ControllerAs:
angular.module('NoteWrangler')
.controller('NotesIndexController', ['$http', function($http) {
var controller = this;
$http({method: 'GET', url: '/notes'}).success(function(data) {
controller.notes = data;
});
}]);
<div ng-controller="NotesIndexController as controller">
{{controller.notes}}
</div>
Direct $scope:
angular.module('NoteWrangler')
.controller('NotesIndexController', ['$scope', '$http', function($scope, $http) {
var controller = this;
$http({method: 'GET', url: '/notes'}).success(function(data) {
$scope.notes = data;
});
}]);
<div ng-controller="NotesIndexController">
{{notes}}
</div>
Note that the $scope object is implicit in the first example and not required in the controller code, and explicit in the second. Also note, $scope has some specific rules regarding primitives and Prototype Inheritance, so it is always recommended to use a dot in your HTML bindings, which the ControllerAs syntax enforces automatically.
Ultimately, the choice of which syntax to use is up to you, but it's recommended to be consistent throughout your application, and either always reference $scope or only reference it for special properties you can't reach any other way.
angular
.module('NoteWrangler')
.controller('NotesIndexController', ['$scope', '$http', function($scope, $http) {
$http({method: 'GET', url: '/notes'}).success(function(data) {
$scope.notes = data;
});
}]);
Not sure why you are using var controller = this; but you have not injected $http
You can use it like
.controller('NotesIndexController', ['$scope', '$http', function($scope, $http){
$http({method: 'GET', url: '/notes'})
.success(function(data) {
$scope.notes = data;
});
}]);

Angular Ui-router can't access $stateParams inside my controller

I am very beginner in Angular.js, I am using the Ui-router framework for routing.
I can make it work upto where I have no parameters in the url. But now I am trying to build a detailed view of a product for which I need to pass the product id into the url.
I did it by reading the tutorials and followed all the methods. In the tutorial they used resolve to fetch the data and then load the controller but I just need to send in the parameters into the controllers directly and then fetch the data from there. My code looks like below. when I try to access the $stateParams inside the controller it is empty. I am not even sure about whether the controller is called or not.
The code looks like below.
(function(){
"use strict";
var app = angular.module("productManagement",
["common.services","ui.router"]);
app.config(["$stateProvider","$urlRouterProvider",function($stateProvider,$urlRouterProvider)
{
//default
$urlRouterProvider.otherwise("/");
$stateProvider
//home
.state("home",{
url:"/",
templateUrl:"app/welcome.html"
})
//products
.state("productList",{
url:"/products",
templateUrl:"app/products/productListView.html",
controller:"ProductController as vm"
})
//Edit product
.state('ProductEdit',{
url:"/products/edit/:productId",
templateUrl:"app/products/productEdit.html",
controller:"ProductEditController as vm"
})
//product details
.state('ProductDetails',{
url:"/products/:productId",
templateUrl:"app/products/productDetailView.html",
Controller:"ProductDetailController as vm"
})
}]
);
}());
this is how my app.js looks like. I am having trouble on the last state, ProdcutDetails.
here is my ProductDetailController.
(function(){
"use strict";
angular
.module("ProductManagement")
.controller("ProductDetailController",
["ProductResource",$stateParams,ProductDetailsController]);
function ProductDetailsController(ProductResource,$stateParams)
{
var productId = $stateParams.productId;
var ref = $this;
ProductResource.get({productId: productId},function(data)
{
console.log(data);
});
}
}());
NOTE : I found lot of people have the same issue here https://github.com/angular-ui/ui-router/issues/136, I can't understand the solutions posted their because I am in a very beginning stage. Any explanation would be very helpful.
I created working plunker here
There is state configuration
$urlRouterProvider.otherwise('/home');
// States
$stateProvider
//home
.state("home",{
url:"/",
templateUrl:"app/welcome.html"
})
//products
.state("productList",{
url:"/products",
templateUrl:"app/products/productListView.html",
controller:"ProductController as vm"
})
//Edit product
.state('ProductEdit',{
url:"/products/edit/:productId",
templateUrl:"app/products/productEdit.html",
controller:"ProductEditController as vm"
})
//product details
.state('ProductDetails',{
url:"/products/:productId",
templateUrl:"app/products/productDetailView.html",
controller:"ProductDetailController as vm"
})
There is a definition of above used features
.factory('ProductResource', function() {return {} ;})
.controller('ProductController', ['$scope', function($scope){
$scope.Title = "Hello from list";
}])
.controller('ProductEditController', ['$scope', function($scope){
$scope.Title = "Hello from edit";
}])
.run(['$rootScope', '$state', '$stateParams',
function ($rootScope, $state, $stateParams) {
$rootScope.$state = $state;
$rootScope.$stateParams = $stateParams;
}])
.controller('ProductDetailController', ProductDetailsController)
function ProductDetailsController ($scope, ProductResource, $stateParams)
{
$scope.Title = "Hello from detail";
var productId = $stateParams.productId;
//var ref = $this;
console.log(productId);
//ProductResource.get({productId: productId},function(data) { });
return this;
}
ProductDetailsController.$inject = ['$scope', 'ProductResource', '$stateParams'];
Check it here
But do you know what is the real issue? Just one line in fact, was the trouble maker. Check the original state def:
.state('ProductDetails',{
...
Controller:"ProductDetailController as vm"
})
And in fact, the only important change was
.state('ProductDetails',{
...
controller:"ProductDetailController as vm"
})
I.e. controller instead of Controller (capital C at the begining)
The params in controller definition array should be strings
["ProductResource", "$stateParams"...
This should properly help IoC to inject the $stateParams
And even better:
// the info for IoC
// the style which you will use with TypeScript === angular 2.0
ProductDetailsController.$inject = ["ProductResource", "$stateParams"];
// this will just map controller to its name, parmas are defined above
.controller("ProductDetailController", ProductDetailsController);

Scope not updating in the view after promise is resolved

Even though there is a similar question for this Data is not getting updated in the view after promise is resolved but I am already using this approach and the view is not being updated.
I have a factory:
'use strict';
myApp
.factory('Factory1', [ '$http','$q','$location', '$rootScope', 'Service', function($http, $q, $location, $rootScope, Service){
return {
checkSomething: function(data){
var deferred = $q.defer(); //init promise
Service.checkSomething(data,function(response){
// This is a response from a get request from a service
deferred.resolve(response);
});
return deferred.promise;
}
};
}]);
I have a controller:
'use strict';
myApp
.controller('MyCtrl', ['$rootScope', '$scope', '$location','Service', 'Factory1' function($rootScope, $scope, $location, Service, Factory1) {
if(Service.someCheck() !== undefined)
{
// Setting the variable when view is loaded for the first time, but this shouldn't effect anything
$scope.stringToDisplay = "Loaded";
}
$scope.clickMe = function(){
Factory1.chechSomething($scope.inputData).then(function(response){
$scope.stringToDisplay = response.someData; // The data here is actually being loaded!
});
};
}]);
And the view:
<div class="app " ng-controller="MyCtrl">
{{stringToDisplay}}
<button class="button" ng-click="clickMe()">Update display</button>
</div>
But the data is not being updated in the view when I click on a button "Update display". Why?
Even though the $scope is being loaded with the data
EDIT:
Hm, it seems that I am getting a error when I try $scope.$apply() and it says:
[$rootScope:inprog] $digest already in progress
This might be a digest cycle issue. You could try:
...
$scope.stringToDisplay = response.someData;
$scope.$apply();
...
For the sake of completeness, here is a nice wrap-up of $scope.$apply.
Edit: I tried to reproduce the error in this fiddle, but cannot seem to find any issues. It works flawlessly without $scope.$apply. I use setTimeout in order to simulate asynchronous operations, which should not trigger a digest cycle by itself.
Try this instead of your function
$scope.clickMe =$scope.$apply(function(){
Factory1.chechSomething($scope.inputData).then(function(response){
$scope.stringToDisplay = response.someData; // The data here is actually being loaded!
});
});

Angular router resolve results in an unknown provider error

I am trying to use Angular's routing to resolve the necessary objects for the controller scope. I have read a few tutorials on how to do this but I still get an Unknown Provider error. The issue seems to be with project being injected into ProjectDetailCtrl.
app.js
var myApp = angular.module('myApp', ['ngRoute']);
myApp.config( function ($interpolateProvider, $routeProvider) {
$routeProvider
...
.when('/project/:projectId', {
templateUrl : 'partials/_project_detail.html',
controller: 'ProjectDetailCtrl',
resolve: {
project: function ($route, MyService) {
return MyService.get('projects/', $route.current.params.projectId).then(function(data) {
console.log('VERIFY DATA: ', data);
return data;
});
}
}
controllers.js
.controller('ProjectDetailCtrl', function ($scope, project) {
$scope.project = project;
}
Edit
services.js
.factory('MyService', function ($http, $q) {
var MyService = {
...
get: function (items_url, objId) {
var defer = $q.defer();
$http({method: 'GET',
url: api_url + items_url + objId}).
success(function (data, status, headers, config) {
defer.resolve(data);
}).error(function (data, status, headers, config) {
defer.reject(status);
});
return defer.promise;
},
Edit 2
The issue is apparently not with the Service method, as this also produces the error:
app.js
var myApp = angular.module('myApp', ['ngRoute']);
myApp.config( function ($interpolateProvider, $routeProvider) {
$routeProvider
...
.when('/project/:projectId', {
templateUrl : 'partials/_project_detail.html',
controller: 'ProjectDetailCtrl',
resolve: {
project: {title: 'foo'}
}
});
})
I can verify my resolve function is being returned properly, but Angular still complains that project is unidentified. What is this issue here? I have tried making my controllers into a module and passing that to the myApp module, but I still get the same Unidentified Provider issue for project.
Note: I am using Angular 1.2.9.
Edit 3: solution
So the issue was this line in my template:
<!-- WRONG: <div ng-controller="ProjectDetailCtrl">-->
<div>
<h2 ng-show="project">Project: <strong>{{ project.title }}</strong></h2>
</div>
Apparently the ng-controller directive cannot be use with resolve.
You forgot to add ngRoute as a dependency for your module. Thats why $routeProvider and $route service are undefined.
Update:
See this example. Problem was in ng-controller directive
You're returning the MyService.get, which is a promise, and then within the result of the promise, you're returning the data.... but to what?
You want to return the promise, but not the original promise... so you create your own promise using $q, and Angular will wait and resolve it for you before loading your route.
project: function ($route, MyService) {
var deferred = $q.defer();
MyService.get('projects/', $route.current.params.projectId).then(function(data) {
console.log('VERIFY DATA: ', data);
deferred.resolve(data);
});
return deferred.promise;
}
Or, assuming MyService.get returns a promise, you should be able to just do
project: function ($route, MyService) {
return MyService.get('projects/', $route.current.params.projectId);
}
And obviously inject $q into config
This question has been asked before, therefore it's a possible duplicate.
The top answer points out usage of controllers within markup to have caused this.
Search your codebase (markup or templates to be exact) for the term ng-controller.
Verify if there is a matching controller, as in this case ProjectDetailCtrl
Remove it from the markup
It solved the problem in my case.

Resources