Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular factory returning a promise

Tags:

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?

like image 658
Markus Johansson Avatar asked Mar 03 '15 10:03

Markus Johansson


4 Answers

$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).

like image 165
squiroid Avatar answered Oct 19 '22 23:10

squiroid


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>
like image 27
New Dev Avatar answered Oct 19 '22 22:10

New Dev


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

  1. All controllers will be executed only after settings are loaded
  2. 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.

like image 27
Yura Fedoriv Avatar answered Oct 20 '22 00:10

Yura Fedoriv


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

like image 24
Arsen Avatar answered Oct 20 '22 00:10

Arsen