Search code examples
angularjsasynchronousangular-promiseangular-servicesngresource

AngularJS: Multiple depending services with asynchronous requests


I want to build an angular app that mostly serves as displaying data, but in a slightly dynamic way. I have multiple JSON files, with the same structure, but different content, one for each language:

res/information_en.json

[
    {
        "name": "Jobs",
        "data": [
            {
                "title": "Title",
                "employer": "Employer",
                "description": "Description",
                "begin": "2015-12",
                "end": "2016-12"
            }
        ]
    },{
        "name": "Personal",
        "data": [
            {
                "firstname": "Christian",
                "lastname": "Steinmeyer"
            }
        ]
    }
]

The German version:

res/information_de.json

[
    {
        "name": "Jobs",
        "data": [
            {
                "title": "Titel",
                "employer": "Arbeitgeber",
                "description": "Beschreibung",
                "begin": "2015-12",
                "end": "2016-12"
            }
        ]
    },{
        "name": "Personal",
        "data": [
            {
                "firstname": "Christian",
                "lastname": "Steinmeyer"
            }
        ]
    }
]

Additionally, I have another JSON file, that keeps track of all the languages:

res/languages.json

[
    {
        "name": "English",
        "short": "en",
        "active": true
    },{
        "name": "Deutsch",
        "short": "de",
        "active": false
    }
]

What I want, essentially, is for the user to be able to choose the language, the information should be displayed in, from the available ones, given by res/languages.json. For that, I have created a first service:

app/services/language-service.js

(function(){ 
  'use strict';

  angular.module('gulpAngularCv').factory('LanguageService', LanguageService);

  /** @ngInject */
  function LanguageService($log, $q, $resource, toastr) {

    var service = {};

    service.getLanguages = getLanguages;

    service.select = select;

    service.getActiveLanguage = getActiveLanguage;




    var initialized = false;

    var languages = [];

    function getLanguages(){
        if (initialized){
            return languages;
        } else {
            initialize().then(
                function success(result){
                    angular.forEach(result, function addLanguage(language){
                        languages.push(language);
                    })
                    initialized = true;
                }, function fail(reject){
                    $log.error("Loading 'res/languages.json' failed.");
                    $log.error(reject);
                    toastr.error('Make sure, it is formatted correctly.', 'Loading language file failed!');

                }
            );
            return languages;
        }
    }

    function initialize(){
        var deferred = $q.defer();
        $resource('res/languages.json').query(
            function success(result){
                deferred.resolve(result);
            }, function fail(reject){
                deferred.reject(reject);
            }
        );
        return deferred.promise;
    }

    function select(language){
        // iterate over all languages
        // deactivate, if active and activate if equal to parameter
    }

    function getActiveLanguage(){
        for (var i = 0; i < languages.length; i++){
            if (languages[i].active){
                return languages[i];
            }
        }
    }

    return service;

  }
})();

This, for itself works like a charm, when called from a controller. But as I said, I wanted to be able to load the information from the according json file, as well. Which I try with the next service:

app/services/information-service.js

(function(){ 
  'use strict';

  angular.module('gulpAngularCv').factory('InformationService', InformationService);

  /** @ngInject */
  function InformationService($log, $q, $resource, toastr, LanguageService) {

    var service = {};

    service.getInformation = getInformation;




    var initialized = {};

    var information = [];

    function getInformation(){
        var language = LanguageService.getActiveLanguage();
        if (initialized === language){
            return information;
        } else {
            initialize(language).then(
                function success(result){
                    angular.forEach(result, function addInformation(information){
                        information.push(information);
                    })
                    initialized = language;
                }, function fail(reject){
                    $log.error("Loading 'res/information_" + language.short + ".json' failed.");
                    $log.error(reject);
                    toastr.error('Make sure, it is formatted correctly.', 'Loading information file failed!');

                }
            );
            return information;
        }
    }

    function initialize(language){
        var deferred = $q.defer();
        $resource("res/information_" + language.short + ".json").query(
            function success(result){
                deferred.resolve(result);
            }, function fail(reject){
                deferred.reject(reject);
            }
        );
        return deferred.promise;
    }

    return service;

  }
})();

I, basically, do the same thing but this time, it won't work, because it seems, this service is injected first, even though it depends on the other one. I get the following error in my browser's console:

TypeError: Cannot read property 'short' of undefined
    at initialize (http://localhost:3000/app/services/information-service.js:44:48)
    at Object.getInformation (http://localhost:3000/app/services/information-service.js:25:13)
    at new MainController (http://localhost:3000/app/main/main-controller.js:12:40)
    at invoke (http://localhost:3000/bower_components/angular/angular.js:4535:17)
    at Object.instantiate (http://localhost:3000/bower_components/angular/angular.js:4543:27)
    at http://localhost:3000/bower_components/angular/angular.js:9395:28
    at link (http://localhost:3000/bower_components/angular-route/angular-route.js:977:26)
    at invokeLinkFn (http://localhost:3000/bower_components/angular/angular.js:9039:9)
    at nodeLinkFn (http://localhost:3000/bower_components/angular/angular.js:8533:11)
    at compositeLinkFn (http://localhost:3000/bower_components/angular/angular.js:7929:13) <div ng-view="" class="ng-scope">

As I see it, this error is weird, because the promised should already be resolved, by the time of the call, the way I implemented it.

For the sake of integrity, here the MainController as well:

app/main/main-controller.js

(function() {
  'use strict';

  angular
    .module('gulpAngularCv')
    .controller('MainController', MainController);

  /** @ngInject */
  function MainController(InformationService) {
    var vm = this;

    vm.categories = InformationService.getInformation();
  }
})();

I've looked at this and this question already, as well as the official documentation, but they only got me so far...


Solution

  • After all, I believe the cause of my problem was visibility and scopes in Javascript. In the information service (posted in the question), I used a "global" variable with the name information, but within the angular for each loop in the getInformation() method, I create a local variable with the same name, so that nothing ever was added to my original variable, as I intended to. For completion, I'll add my final implementation of the two services again. Note, that I not only solved the bug, but also did some refactoring in the information service (below). This solution works, as I intend it to.

    app/services/language-service.js
    
    (function(){ 
      'use strict';
    
      angular.module('gulpAngularCv').factory('LanguageService', LanguageService);
    
      /** @ngInject */
      function LanguageService($log, $q, $resource, toastr) {
    
        var service = {};
    
        service.getLanguages = getLanguages;
    
        service.select = select;
    
        service.getActiveLanguage = getActiveLanguage;
    
    
    
    
        var initialized = false;
    
        var languages = [];
    
        function getLanguages(){
            if (initialized){
                return languages;
            } else {
                initialize().then(
                    function success(result){
                        $log.debug("Loaded languages from 'res/languages.json'");
                        $log.debug(result);
                        angular.forEach(result, function addLanguage(language){
                            $log.debug(language);
                            languages.push(language);
                        })
                        initialized = true;
                    }, function fail(reject){
                        $log.error("Loading 'res/languages.json' failed.");
                        $log.error(reject);
                        toastr.error('Make sure, it is formatted correctly.', 'Loading language file failed!');
    
                    }
                );
                return languages;
            }
        }
    
        function initialize(){
            var deferred = $q.defer();
            $resource('res/languages.json').query(
                function success(result){
                    deferred.resolve(result);
                }, function fail(reject){
                    deferred.reject(reject);
                }
            );
            return deferred.promise;
        }
    
        function select(language){
            // iterate over all languages
            // deactivate, if active and activate if equal to parameter
        }
    
        function getActiveLanguage(){
            for (var i = 0; i < languages.length; i++){
                if (languages[i].active){
                    return languages[i];
                }
            }
        }
    
        return service;
    
      }
    })();
    

    and the information service:

    app/services/information-service.js
    
    (function(){ 
      'use strict';
    
      angular.module('gulpAngularCv').factory('InformationService', InformationService);
    
      /** @ngInject */
      function InformationService($log, $q, $resource, $rootScope, toastr, LanguageService) {
    
        var service = {};
    
        service.getCategories = getCategories;
    
    
        var model = {};
        model.initialized = null;
        model.categories = [];
    
        function getCategories(){
            $log.debug("getCategories");
            loadData();
            return model.categories;
        }
    
        function loadData(){
            var language = LanguageService.getActiveLanguage();
            if (language && model.initialized !== language){
                initialize(language).then(
                    function success(data){
                        model.categories.length = 0;
                        angular.forEach(data.categories, function addInformation(datum){
                            $log.debug(datum);
                            model.categories.push(datum);
                        })
                    }, function fail(reject){
                        toastr.error('Make sure, it is formatted correctly.', 'Loading information file failed!');
                    }
                );
            }
        }
    
        function initialize(language){
            var deferred = $q.defer();
            $resource("res/information_" + language.short + ".json").get(
                function success(result){
                    deferred.resolve(result);
                    model.initialized = language;
                }, function fail(reject){
                    deferred.reject(reject);
                }
            );
            return deferred.promise;
        }
    
        return service;
    
      }
    })();