1

I have this data

{
  "config": {
    "RESTAPIURL": "http://myserver/myrestsite"
  }
}

and I have this factory that reads that data

'use strict';

angular.module('myApp').factory('api',
  ["$http", "$q",
  function ($http, $q) {

    function _getConfiguration() {
      var deferred = $q.defer();
      $http.get('/scripts/constants/config.json')
      .success(function (data) {
        deferred.resolve(data);
      })
      .error(function (data, status) {
        deferred.reject(data, status);
      });
      return deferred.promise;
    }

    function _restApiUrl() {
      // this doesn't work either. _getConfiguration() doesn't resolve here.
      return _getConfiguration().RESTAPIURL + '/api/';
    }

    return {
      URL: _restApiUrl
    }
  }
  ]
);

Then to use it

'use strict';

angular.module('myApp').factory('AuthService', function ($http, $q, api,NotificationService) {

    function _get(creds) {

        var deferred = $q.defer();

        $http({method: 'GET', url: api.URL() + api.AUTH, headers: {
            'Authorization': 'Basic '+creds}
        })
        .success(function (data, status, results, headers) {
            deferred.resolve(results);
        })
        .error(function (data, status) {
            NotificationService.redirect(status);
            deferred.reject(data, status);
        });
        return deferred.promise;
    }

    return {
        get:_get
    };
});

So when I'm using it I am doing api.URL() and it's not working.

It used to be hard coded URL so to call it used to be api.URL. I really don't want to go through the whole app and convert everything to api.URL().then(...). That would suck.

So how can I nail down this value as a "property" instead of an asynchronous promise that has to be called over and over?

Call it once, fine. Get the value. Put it somewhere. Use the value. Don't ever call the $http again after that.

EDIT

This is turning up to be one of the most successful questions I've ever asked, and I am gratefully going through each answer in turn. Thank each one of you.

toddmo
  • 20,682
  • 14
  • 97
  • 107
  • 2
    To avoid having to handle everything in an asyc way take a look at ui-router and how it resolves promises before state changes. https://github.com/angular-ui/ui-router – Reactgular Oct 14 '15 at 15:48
  • Is this api.URL() purely used for loading data from a server? As in, you don't intend to show the actual value across your website? – Gavin van Gent Oct 14 '15 at 15:51
  • Maybe this will help: http://stackoverflow.com/questions/22825706/angularjs-load-config-on-app-start#answer-22825996. Most common way ive seen suggested is loading your data, setting your variables then manually bootstrapping your app. – ste2425 Oct 14 '15 at 15:54
  • @ThinkingMedia, I respect your reputation, but I am sorry, I totally don't see how that relates to what I'm trying to do. Can you provide an example that connects the dots? – toddmo Oct 14 '15 at 15:58
  • @ste2425, thanks, but that requires major changes to all my services. Going along with that example, can you see a way to load `data.contacts` in one place instead of in each and every service, and do it one time? (then set $rootScope) or something) – toddmo Oct 14 '15 at 16:01
  • @GavinvanGent, it's the url to the rest site. All the services of the angular web site then need to use it to call the various api of the rest site. Thanks! – toddmo Oct 14 '15 at 16:02
  • @toddmo You have stuff X that your app needs, but that stuff X needs to be requested from the server at different times for different reasons. You end up with a bunch of services that just return promises that resolve to stuff X. When you switch to resolving on state changes then you have stuff X already downloaded. It's now data that can be injected into child states, directives and controllers. There is no more need to use promises since ui-router resolved those promises for you. You could create an abstract state called "users" that resolves the user creds and after can inject that data. – Reactgular Oct 14 '15 at 18:40
  • 1
    @ThinkingMedia, That is cool! So on other S/O questions where they say "you can't do sync $http in angular", you can go score up some points by bringing this up :) – toddmo Oct 14 '15 at 19:49

4 Answers4

2

Adding a bit to what @ThinkingMedia was saying in the comment, with ui-router when defining controllers you can add a resolve parameter.

In it you can specify some promises that have to resolve before the controller is instantiated, thus you are always sure that the config object is available to the controller or other services that the controller is using.

You can also have parent/child controllers in ui-router so you could have a RootController that resolves the config object and all other controllers inheriting from RootController

.state('root', {
    abstract: true,
    template: '<ui-view></ui-view>',
    controller: 'RootController',
    resolve:{
      config: ['api', function(api){
        return api.initialize();
      }       
    }
  });

and your api factory:

angular.module('myApp').factory('api',
  ["$http", "$q",
  function ($http, $q) {
    var _configObject = null;

    function initialize() {
      return $http.get('/scripts/constants/config.json')
      .then(function (data) {
          _configObject = data;
          return data;
      });
    }

    // you can call this in other services to get the config object. No need to initialize again
    function getConfig() {
      return _configObject;
    }

    return {
      initialize: initialize,
      getConfig: getConfig
    }
  }
  ]
);
sirrocco
  • 7,975
  • 4
  • 59
  • 81
  • 1
    Bra! Shit works! (My `getConfig` is `URL`). Do you think there's any way to make it so I can just say `api.URL` or am I stuck with `api.URL()`? – toddmo Oct 14 '15 at 19:19
  • you are stuck with api.url() :). Have you thought about setting the url variable from the server when the html is generated? It would save you a call to the server. – sirrocco Oct 15 '15 at 03:21
  • You should probably add another question with an example ... it's pretty difficult to give an answer like this .. – sirrocco Oct 15 '15 at 14:35
  • 1
    I figured out how to do one resolve depending on another as per [this article](https://medium.com/opinionated-angularjs/advanced-routing-and-resolves-a2fcbf874a1c) – toddmo Oct 15 '15 at 15:05
1

I would pass a callback to the getURL method, and save the URL when it returns. Then I would attach any subsequent requests to that callback. Here I am assuming that you are doing something similar with api.AUTH that you don't have a reference to in your code.

Pass a callback to the getURL method in the api service.

angular.module('myApp').factory('api', ["$http", "$q",

function ($http, $q) {

    function _getConfiguration() {
        var deferred = $q.defer();
        $http.get('/scripts/constants/config.json')
            .success(function (data) {
            deferred.resolve(data);
        })
            .error(function (data, status) {
            deferred.reject(data, status);
        });
        return deferred.promise;
    }

    return {
        getURL: function (cb) {
            var that = this;
            if (that.URL) {
                return cb(that.URL);
            }

            _.getConfiguration().then(function (data) {
                that.URL = data.config.RESTAPIURL + "/api";
                cb(that.URL);
            });
        }
    }
}]);

And in your AuthService, wrap your _get inside a callback like this:

angular.module('myApp').factory('AuthService', function ($http, $q, api, NotificationService) {

    function _get(creds) {
        var deferred = $q.defer();
        var getCallback = function (url) {

            $http({
                method: 'GET',
                url: url + api.AUTH,
                headers: {
                    'Authorization': 'Basic ' + creds
                }
            })
                .success(function (data, status, results, headers) {
                deferred.resolve(results);
            })
                .error(function (data, status) {
                NotificationService.redirect(status);
                deferred.reject(data, status);
            });
        };
        api.getURL(getCallback);
        return deferred.promise;
    }

    return {
        get: _get
    };
});
show-me-the-code
  • 1,553
  • 1
  • 12
  • 14
  • Thank you very much. I can see how this would work. However it's more intrusive in the consuming services than having to change `URL` to `URL()`. But thanks! – toddmo Oct 14 '15 at 19:25
1

Why don't you initialize the factory when the app is loading and put the variable onto another property? Something like this:

angular.module('myApp').factory('api', ["$http", "$q",
  function ($http, $q) {
    // store URL in a variable within the factory
    var _URL;

    function _initFactory() {
      var deferred = $q.defer();
      $http.get('/scripts/constants/config.json')
      .success(function (data) {

        // Set your variable after the data is received
        _URL = data.RESTAPIURL;
        deferred.resolve(data);

      });
      return deferred.promise;
    }

    function getURL() {
        return _URL;
    }

    return {
      initFactory: _initFactory,
      URL: getURL
    }
  }
  ]
);


// While the app is initializing a main controller, or w/e you may do, run initFactory
//...
api.initFactory().then(
  // may not need to do this if the URL isn't used during other initialization
)
//...

// then to use the variable later
function _get(creds) {

    var deferred = $q.defer();

    $http({method: 'GET', url: api.URL + api.AUTH, headers: {
        'Authorization': 'Basic '+creds}
    })
    .success(function (data, status, results, headers) {
        deferred.resolve(results);
    })
    return deferred.promise;
 }
CDelaney
  • 1,238
  • 4
  • 19
  • 36
  • This won't work, what if the http call takes 3 seconds? The app will be done initializing long before that – sirrocco Oct 14 '15 at 17:12
  • @sirrocco That's what my comment after `api.initFactory().then(` is about. During initialization it would have to do this asynchronously if other things depend on it. – CDelaney Oct 14 '15 at 18:44
  • The return statement from the api factory, which sets the `URL` property, happens before `URL` variable has a value. It's called during the compile phase. Therefore, the `URL` property is always `undefined`. Try it if you want to in JSFiddle and see if you get that result. – toddmo Oct 14 '15 at 18:58
  • @toddmo Good point. That's hardly an issue though, it simply needs to go into a getter. Edited – CDelaney Oct 14 '15 at 19:08
  • Ok, here's where this approach is broken. Not saying it can't be fixed. But MainCtrl is not the only entry point into the application. Any refresh on a particular page blows away `api.URL`. MainCtrl is not a sure fire entry point on any app where you can refresh individual pages, which is all apps, I think. Any thoughts? – toddmo Oct 14 '15 at 22:06
  • If you don't have a top-level controller that runs before any others in the app, you could either add a top-level route and resolve the the endpoint similar to the accepted answer, or you could add an `app.run(...)` block after your app definition. – CDelaney Oct 15 '15 at 13:58
1

I see you haven't used any $resource's here, but I'm hoping you have a good understanding of them:

in factories/delay-resource.js:

'use strict'

angular.module('myApp').factory('delayResource', ['$resource', '$q',
function($resource, $q){
    var _methods = ['query', 'get', 'delete', 'remove', 'save'];

    var shallowClearAndCopy = function(src, dst) {
            dst = dst || {};

            angular.forEach(dst, function(value, key){
                delete dst[key];
            });

            for (var key in src) {
                if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
                    dst[key] = src[key];
                }
            }

            return dst;
        }

    var delayResourceFactory = function(baseUrlPromise, url, paramDefaults){
        var _baseUrlPromise = baseUrlPromise,
            _url = url,
            _paramDefaults = paramDefaults;

        var DelayResource = function(value){
            shallowClearAndCopy(value || {}, this);
        };

        _methods.forEach(function(method){
            DelayResource[method] = function(params, successCB, errCB, progressCB){
                if (angular.isFunction(params)) {
                    progressCB = successCB;
                    errCB = errHandlers;
                    successCB = params;
                    errHandlers = params = null;
                }
                else if (!params || angular.isFunction(params)){
                    progressCB = errCB;
                    errCB = successCB;
                    successCB = errHandlers;
                    params = {};
                }

                var _makeResultResource = function(url){
                    var promise = $resource(url, _paramDefaults)[method](params);

                        (promise.$promise || promise).then(
                            function successHandler(){
                                var data = arguments[0];

                                if (isInstance){
                                    if (angular.isArray(data))
                                        for (var i = 0; i < data.length; i++)
                                            data[i] = new DelayResource(data[i])
                                    else if (angular.isObject(data))
                                        data = new DelayResource(data)
                                }

                                successCB.apply(successCB, arguments)
                                resultDelay.resolve.apply(resultDelay.resolve, arguments)
                            },
                            function(err){
                                errCB.apply(errCB, arguments)
                                resultDelay.reject.apply(resultDelay.reject, args)
                            },
                            function(){
                                progressCB.apply(progressCB, arguments)
                                resultDelay.notify.apply(resultDelay.notify, arguments)
                            }
                        )
                }

                var isInstance = this instanceof DelayResource,
                    resultDelay = $q.defer();

                if (!angular.isString(_baseUrlPromise) && angular.isFunction(_baseUrlPromise.then))
                    _baseUrlPromise.then(
                        function successCb(apiObj){
                            _makeResultResource(apiObj.RESTAPIURL + _url)
                        },
                        function successCb(){
                            throw 'ERROR - ' + JSON.stringify(arguments, null, 4)
                        })
                else
                    _makeResultResource(_baseUrlPromise.RESTAPIURL + _url);

                return resultDelay.promise;
            };


            DelayResource.prototype['$' + method] = function(){
                var value = DelayResource[method].apply(DelayResource[method], arguments);
                return value.$promise || value;
            }
        });

        return DelayResource;
    }

    return delayResourceFactory;
}]);

This will be the base factory that all requests to that REST API server will go through.

Then we need a factories/api-resource.js:

angular.module('myApp').factory('apiResource', ['delayResource', 'api', function (delayResource, api) {
    return function (url, params) {
        return delayResource(api.URL(), url, params);
    };
}])

Now all factories created will just have to call the apiResource to get a handle on a resource that will communicate with the REST API

Then in a file like factories/account-factory.js

angular.module('myApp').factory('AuthRoute', ['apiResource', 'api', function (apiResource, api) {
     return apiResource(api.AUTH);
}]);

Now in factories/auth-service.js:

'use strict';

angular.module('myApp').factory('AuthService', ['$q', 'AuthRoute', 'NotificationService', function ($q, AuthRoute, api, NotificationService) {
    function _get(creds) {
        var deferred = $q.defer();

        AuthRoute.get()
            .then(
                function successCb(results){
                    deferred.resolve(results);
                },
                function errCb(){
                    // cant remember what comes into this function
                    // but handle your error appropriately here

                    //NotificationService.redirect(status);
                    //deferred.reject(data, status);
                }
            );

        return deferred.promise;
    }

    return {
        get:_get
    };
}]);

As you can imagine, I haven't been able to test it yet, but this is the basis. I'm going to try create a scenario that will allow me to test this. In the mean time, feel free to ask questions or point out mistakes made

Late Addition Forgot to add this:

'use strict';

angular.module('myApp').factory('api', ["$http", "$q", function ($http, $q) {
  var restApiObj,
      promise;

  function _getConfiguration() {
    if (restApiObj)
      return restApiObj;

    if (promise)
      return promise;

    promise = $http.get('/scripts/constants/config.json')
        .then(function (data) {
          restApiObj = data;
          promise = null;
          return data;
        },
        function (data, status) {
          restApiObj = null;
          promise = null;
        });
    return promise;
  }

  return {
    URL: _getConfiguration
  }
}]);

Continuing with the ui-router scenario

.state('member-list', {
    url: '/members?limit=&skip='
    templateUrl: '/views/members/list.html',
    controller: 'MemberListCtrl',
    resolve:{
      members: ['$stateParams', 'MembersLoader', function($stateParams,MembersLoader){
        return MembersLoader({skip: $stateParams.skip || 0, limit: $stateParams.limit || 10});
      }       
    }
 });

factory

.factory('MemberRoute', ['apiResource', function(apiResource){
    return apiResource('/members/:id', { id: '@id' });
}])
.factory('MembersLoader', ['MembersRoute', function(MembersRoute){
    return function(params){
        return MemberRoute.query(params);
    };
}])
.factory('MemberFollowRoute', ['apiResource', 'api', function(apiResource, api){
    return apiResource(api.FOLLOW_MEMBER, { id: '@id' });
}])

controller

.controller('MemberListCtrl', ['$scope', 'members', 'MemberRoute', 'MemberFollowRoute', function($scope, members, MemberRoute, MemberFollowRoute){
    $scope.members = members;

    $scope.followMember = function(memberId){
        MemberFollowRoute.save(
            { id: memberId },
            function successCb(){
                //Handle your success, possibly with notificationService
            },
            function errCb(){
                // error, something happened that doesn't allow you to follow memberId
                //handle this, possibly with notificationService
            }
        )
    };

    $scope.unfollowMember = function(memberId){
        MemberFollowRoute.delete(
            { id: memberId },
            function successCb(){
                //Handle your success, possibly with notificationService
            },
            function errCb(){
                // error, something happened that doesn't allow you to unfollow memberId
                //handle this, possibly with notificationService
            }
        )
    };
}]);

With all this code above, you will never need to do any sort of initialization on app start, or in some abstract root state. If you were to destroy your API config every 5 mins, there would be no need to manually re-initialize that object and hope that something isn't busy or in need of it while you fetch the config again.

Also, if you look at MembersRoute factory, the apiResource abstracts/obscures the api.URL() that you were hoping not to have to change everywhere. So now, you just provide the url that you want to make your request to, (eg: /members/:id or api.AUTH) and never have to worry about api.URL() again :)

Gavin van Gent
  • 127
  • 1
  • 9