简体   繁体   中英

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.

The reason why it is undefined is actually because of the asynchronous nature of $http requests. There is nothing (well, basically nothing) you can do to change this behaviour. You are going to have to resolve this using the most common way (and if you continue with Angular and most web development languages, this will become a long-standing practices) with promises.

What is a promise? There are many guidelines online for what it is. The most basic way to explain it to you is that a promise will not execute until the asynchronous operation returns.

https://docs.angularjs.org/api/ng/service/$q
http://andyshora.com/promises-angularjs-explained-as-cartoon.html

Right now, when you console.log the property, it may/may not have already been loaded (in fact, it probably hasn't) thus you get undefined .

Furthermore, I am not exactly sure about this, but you are using the factory provider, yet you don't return anything from the provider function, thus you are using it as if it were a service. I'm not 100% sure if Angular allows this, but certainly the best practice way is to return the instance of what you want to create from the factory.

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 Notice 1

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”?

I think the issue you are having is that your controller is trying to access a property that is being set by an $http.get call. $http.get returns a promise, which is an asynchronous call to the URL you are providing to get your brands. That means that the controller making the call will not wait for the brands to be loaded.

There are a few options for solving this issue, but you can simply return the promise from the service and resolve it in the controller.

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

           return $http.get(URL);
         });

Then in your controller you can put the success and error functions as so...

brandsFactory.brands().then(function(response){
  $scope.brand = response.data;
  $scope.actualBrand = response.data[0];
}, function(err){
   console.log('Error in brands factory');
});

There are other options as well (such as lazy loading or putting the factory directly on your scope), but you can look into those if you'd like.

I solve it using $q promise.

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

        var actualBrand = {};

        var getBrands = function(){
            var URL;
            var defered = $q.defer();
            var promise = defered.promise;

            if(ENV.mocksEnable){
                URL = ENV.apiMock + ENV.getBrandsMock;
            }else{
                URL = ENV.apiURL + ENV.getBrands;
            }

            $http.get(URL)
            .success(function(data){
                actualBrand = data[0];
                defered.resolve(data);
            })
            .error(function(err){
                console.log('Error in BrandsController');
                defered.reject(err);
            });

            return promise;    
        };//End getBrands

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

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

        return {
            setActualBrand : setActualBrand,
            getBrands : getBrands
        }; 

    }])//End Factory

And in my controller:

 /* BrandController to manage the html content of a single brand */
    .controller('BrandCController', function(brandsFactory, $scope){
        $scope.brands = [];
        $scope.actualBrand = {};
        $scope.setActualBrand = function(brand){
            brandsFactory.setActualBrand(brand);
            $scope.actualBrand = brand;
        };

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

If I can improve my answer let me know. I use it all the information I got from the previous answers. Thank you anyone who helps.

UPDATE

Removing the $q.defer Anti-Pattern in my factory.

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

    var actualBrand = {};
    var getBrands = function(){
        var URL;

        if(ENV.mocksEnable){
            URL = ENV.apiMock + ENV.getBrandsMock;
        }else{
            URL = ENV.apiURL + ENV.getBrands;
        }

       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);
            return errorResponse;
        })
        );

    };//End getBrands

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

    return {
        setActualBrand : setActualBrand,
        getBrands : getBrands
    }; 
}]);//End Factory

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM