Search code examples
angularjsangularjs-module

How do you design a module that a user has to pass config data to when they depend on it?


I have an angular module that is basically a client side web service layer (work in progress). I have a module declaration, and a factory called "baseRequest". baseRequest is depended on by other factories in the module. BaseRequest needs to know the baseUrl of the WebApi2 controllers it will be calling into.

The script is Hosted on Site A: but will be consumed on Sites B, C, D, E, etc etc etc. As such there is no way for it reliably know it's own url without some configuration data.

Based on that I would like the user using the api to have to configure it when they add the script tag. I invision them having to call config on my module, but am not sure how to implement that on this module and confused by documentation on the subject:

(function () {
angular.module('xyzApi', []).run(['$http', function ($http) {
    //Configure defaults on $http here if need be
}]).factory('xyzApiEvents', ['$rootScope', function ($rootScope) {
    var evtNotFoundErr = 'xyzAPI Does not have an event called: ';
    var events = {
        "loggingIn": "xyzApi.loggedIn",
        "loggedOut": "xyzApi.loggingIn",
        "loggingIn": "xyzApi.loggingOut",
        "loggedOut": " xyzApi.loggedOut"
    }
    var factory = {
        raiseEvent: function (eventName, eventValue) {
            if (!events.hasOwnProperty(eventName))
                throw evtNotFoundErr + eventName;
            $rootScope.$broadcast(events[eventName], eventValue);
        },
        on: function (eventName, callBack) {
            if (!isFunction(callBack))
                throw 'xyzApi: xyzApiEvents.on(eventName, callBack)... callBack must be a function!';
            if (!events.hasOwnProperty(eventName))
                throw evtNotFoundErr + eventName;
            $rootScope.$on(events[eventName], function (evt, args) {
                callBack(evt, args);
            });
        }
    };
    return factory;
}]);
function isFunction(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}

})();

BaseRequest:

(function () {
    angular.module('xyzApi').factory('baseRequest', ['$q', '$http', '$log', 'xyzApiEvents', function ($q, $http, $log, xyzApiEvents) {
        var userOAuthToken = null;
        var httpConfig = { headers: {} };
        xyzApiEvents.on('loggingIn', function (event, oAuthToken) {
            userOAuthToken = oAuthToken;
            //set Authorization header for authorized web requests
            httpConfig.headers['Authorization'] = userOAuthToken;            
        });
        var baseUrl = '{{baseUrl}}'; //this is the url I need configured
        return {
            serviceUrl: function () {
                return baseUrl;
            },
            get: function (sQuery) {
                var deferred = $q.defer();
                var url = baseUrl + sQuery;
                $http.get(url, httpConfig).then(function(response) {
                    deferred.resolve(response);
                }, function (errorResponse) {
                    $log.error(errorReponse);
                    deferred.reject(errorResponse);
                }); //TODO Add Update Promise
                return deferred.promise
            },
            post: function (sQuery, postData) {
                var deferred = $q.defer();
                var url = baseUrl + sQuery;                
                $http({
                    method: 'POST',
                    url: url,
                    headers: httpConfig.headers,
                    data: postData
                }).then(function (response) {
                    deferred.resolve(response);
                }, function (errorResponse) {
                    $log.error(errorResponse);
                    deferred.reject(errorResponse);
                }); //TODO Add Update Promise
                return deferred.promise;
            }
        }
    }]);
})();

Solution (Thanks @Anzeo)

I have many separate services and I wrap them up into one service as the last script in the API: So I made that a provider instead of making baseRequest a provider. Also I had to change the name of my module to xyzDataWebServices because I wanted xyzApi to be what's used in controller/directives in consuming code and the provider cannot have the same name as it's module (that was causing issues).

(function () {
    angular.module('xyzDataWebServices').provider('xyzApi', function () {
        var baseUrl;
        this.setBaseUrl = function (value) {
            baseUrl = value;
        };
        this.$get = ['$log', 'baseRequest', 'xyzApiContext', 'xyzApiAuthentication', 'xyzApiLibrary', function xyzApiFactory($log, baseRequest, xyzApiContext, xyzApiAuthenication, xyzApiLibrary) {
            baseRequest.setServiceUrl(baseUrl);
            var factory = {
                context: xyzApiContext,
                authentication: xyzApiAuthenication,
                library: xyzApiLibrary
            }
            return factory;
        }];
    });
})();

Then to depend on it and configure the serviceUrl:

<script type="text/javascript">
    angular.module('app', ['ngAnimate', 'ngAria', 'ngMessages', 'ngTouch', 'ui.bootstrap', 'ui.router', 'xyzDataWebServices']).config(['$stateProvider', '$urlRouterProvider', 'xyzApiProvider', function ($stateProvider, $urlRouterProvider, xyzApiProvider) {
        xyzApiProvider.setBaseUrl('@Utility.DataWebServiceUrl');
        $urlRouterProvider.otherwise('/');
        $stateProvider.state('login', {
            url: '/',
            templateUrl: 'assets/templates/login.html'
        });
    }]).factory('loginModel', [function () {
        return {
            userName: null,
            password: null,
            stayLoggedIn: false
        };
    }]);
</script>

I should note this is MVC 5, Web Api 2, and Angular. So the above is in a Razor Template "Cshtml" and @Utility is Razor syntax to call that method on the Utility class. So when the view is transformed it puts the web service url there which is stored in a web.config file.

I also use web config transformations to change that Url based on what server I am deploying to.

In short, this url configuration is now config driven with deployments and I never have to change it.

Note* The reason I have everything wrapped in functions is because my API is split up into physical files: Such As

  • xyzApi.js
  • services\00_debounce.js
  • services\01_baseRequest.js
  • services\05_context.js
  • services\10_authentication.js
  • services\20_etc.js
  • xyzApiEnd.js

The numbers on the front are just to make it easy to make things alphabetical and are easy to change to change the bundle order.

xyzApi is first in bundle order, then services are bundled in alphabetical order with a wildcard, then xyzApiEnd is bundled last.


Solution

  • You can achieve a configurable library using Angular's Provider mechanism.

    Basically what you'll be doing is writing a wrapper around your service/factory/whatever. You can inject this wrapper as a provider inside a config statement (i.e. `$httpProvider).

    angular.module('xyzApi').provider('baseRequest', function () {
    });
    

    You can then expose methods to set variables for the service/factory you'll be using later (during the run phase).

    In your case you'd need to add a method to allow the user to set the baseUrl, like:

    angular.module('xyzApi').provider('baseRequest', function () {
        var baseUrl;
        return {
            setBaseUrl: function (providedBaseUrl) {
                baseUrl = providedBaseUrl;
            }
        }
    });
    

    Lastly, add the special $get method. This method is used by the $injector service to get an instance of the injected property.

    angular.module('xyzApi').provider('baseRequest', function () {
        var baseUrl;
        return {
            setBaseUrl: function (providedBaseUrl) {
                baseUrl = providedBaseUrl;
            },
            $get: function(/* here you can use other injected services */){
            }
        }
    });
    

    I always wrap the implementation of the service in a create function.

    Applying these changes on your code yields:

    angular.module('xyzApi').provider('baseRequest', function () {
        var baseUrl;
        return {
            setBaseUrl: function (providedBaseUrl) {
                baseUrl = providedBaseUrl;
            },
            $get: function ($q, $http, $log, xyzApiEvents) {
                return createBaseRequestFactory($q, $http, $log, xyzApiEvents, baseUrl);
            }
        };
    
        function createBaseRequestFactory($q, $http, $log, xyzApiEvents, baseUrl){
            var userOAuthToken = null;
            var httpConfig = {headers: {}};
            xyzApiEvents.on('loggingIn', function (event, oAuthToken) {
                userOAuthToken = oAuthToken;
                //set Authorization header for authorized web requests
                httpConfig.headers['Authorization'] = userOAuthToken;
            });
    
            return {
                serviceUrl: function () {
                    return baseUrl;
                },
                get: function (sQuery) {
                    var deferred = $q.defer();
                    var url = baseUrl + sQuery;
                    $http.get(url, httpConfig).then(function (response) {
                        deferred.resolve(response);
                    }, function (errorResponse) {
                        $log.error(errorReponse);
                        deferred.reject(errorResponse);
                    }); //TODO Add Update Promise
                    return deferred.promise
                },
                post: function (sQuery, postData) {
                    var deferred = $q.defer();
                    var url = baseUrl + sQuery;
                    $http({
                        method: 'POST',
                        url: url,
                        headers: httpConfig.headers,
    
    
                        data: postData
                    }).then(function (response) {
                        deferred.resolve(response);
                    }, function (errorResponse) {
                        $log.error(errorResponse);
                        deferred.reject(errorResponse);
                    }); //TODO Add Update Promise
                    return deferred.promise;
                }
            }
        }
    });