Search code examples
javascriptangularjspromiseangular-promiseangular-http

Http Service Factory Returns Undefined to Controller in Angular


I have this service, that initialize brands and actualBrand variables.

.factory('brandsFactory', ['$http', 'ENV', function brandsFactory($http, ENV){

    var actualBrand = {};
    var brands = getBrands(); //Also set the new actualBrand by default

    function getBrands(){
        var URL;
        console.log('MOCKS enable? ' +  ENV.mocksEnable);
        if(ENV.mocksEnable){
           URL = ENV.apiMock + ENV.getBrandsMock;
        }else{
            URL = ENV.apiURL + ENV.getBrands;
        }

       $http.get(URL).
        success(function(data){
            console.log('data is :' +  data);
            actualBrand = data[0];
            console.log('Actual brand is ' + actualBrand);
            return data;
        }).
        error(function(){
        console.log('Error in BrandsController');
     });

    }

    var setActualBrand = function(activeBrand) {
        actualBrand = activeBrand;
    };

    var isSetAsActualBrand = function(checkBrand) {
        return actualBrand === checkBrand;
    };

    return {
        brands : brands,
        actualBrand : actualBrand
    }; 

And my controller looks like this:

/* BrandController to manage the html content of a single brand */
    .controller('BrandCController', function(brandsFactory, $scope){

        $scope.brands = brandsFactory.brands;
        console.log('brands in controller is ' + $scope.brands + ' -- ' + brandsFactory.brands);

    });

The problem in the controller is that it gets undefined in brandsFactory.brands because its loaded before the service.

Why can I do to solve this? Im really new in angular and javascript, maybe Im doing lots of thing wrong. Any help I would be grateful, thank you.


Solution

  • Im really new in angular and javascript, maybe Im doing lots of thing wrong. Any help I would be grateful, thank you.

    You are correct -- you are doing lot's of things wrong. Let's start with the getBrands function:

    //Erroneously constructed function returns undefined
    //
    function getBrands(){
        var URL;
        console.log('MOCKS enable? ' +  ENV.mocksEnable);
        if(ENV.mocksEnable){
           URL = ENV.apiMock + ENV.getBrandsMock;
        }else{
            URL = ENV.apiURL + ENV.getBrands;
        }
    
       $http.get(URL).
        success(function(data){
            console.log('data is :' +  data);
            actualBrand = data[0];
            console.log('Actual brand is ' + actualBrand);
            //SUCCESS METHOD IGNORES RETURN STATEMENTS
            return data;
        }).
        error(function(){
        console.log('Error in BrandsController');
     });
    

    The return data statement is nested inside a function which is the argument of an $http .success method. It does not return data to the getBrands function. Since there is no return statement in the getBrands body, the getBrands function returns undefined.

    In addition the .success method of the $http service ignores return values. Thus the $http service will return a promise that resolves to a response object instead of a data object. To return a promise that resolves to a data object, use the .then method.

    //Correctly constructed function that returns a promise
    //that resolves to a data object
    //
    function getBrands(){
        //return the promise
        return (
            $http.get(URL)
                //Use .then method
                .then (function onFulfilled(response){
                    //data is property of response object
                    var data = response.data;
                    console.log('data is :' +  data);
                    var actualBrand = data[0];
                    console.log('Actual brand is ' + actualBrand);
                    //return data to create data promise
                    return data;
                })
         )
    }
    

    With a return statement inside the onFulfilled function and a return statement in the body of the getBrands function, the getBrands function will return a promise that resolves fulfilled with data (or resolves rejected with a response object.)

    In the controller, resolve the promise with the .then and .catch methods.

    brandsFactory.getBrands().then(function onFulfilled(data){
        $scope.brand = data;
        $scope.actualBrand = data[0];
    }).catch( function onRejected(errorResponse){
        console.log('Error in brands factory');
        console.log('status: ', errorResponse.status);
    });
    

    Deprecation Notice1

    The $http legacy promise methods .success and .error have been deprecated. Use the standard .then method instead.


    UPDATE

    I use $q promise to solve it. I use the .then in the controller, but I couldn't make it work in the factory. If you want to check it. And thanks for the feedback.

    //Classic `$q.defer` Anti-Pattern
    //
    var getBrands = function(){
    
        var defered = $q.defer();
        var promise = defered.promise;
    
        $http.get(URL)
        .success(function(data){
            defered.resolve(data);
        })
        .error(function(err){
            console.log('Error in BrandsController');
            defered.reject(err);
        });
    
       return promise;    
    };//End getBrands
    

    This is a classic $q.defer Anti-Pattern. One of the reasons that the .sucesss and .error methods were deprecated was that they encourage this anti-pattern as a result of the fact that those methods ignore return values.

    The major problem with this anti-pattern is that it breaks the $http promise chain and when error conditions aren't handled properly the promise hangs. The other problem is that often programmers fail to create a new $q.defer on subsequent invocations of the function.

    //Same function avoiding $q.defer   
    //
    var getBrands = function(){
        //return derived promise
        return (
            $http.get(URL)
              .then(function onFulfilled(response){
                var data = response.data;
                //return data for chaining
                return data;
            }).catch(function onRejected(errorResponse){
                console.log('Error in BrandsController');
                console.log('Status: ', errorResponse.status;
                //throw value to chain rejection
                throw errorResponse;
            })
        )
    };//End getBrands
    

    This example shows how to log a rejection and throw the errorResponse object down the chain to be used by other rejection handlers.

    For more information on this, see Angular execution order with $q.

    Also, Why is angular $http success/error being deprecated?.

    And, Is this a “Deferred Antipattern”?