Reputation: 1079
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.
Upvotes: 1
Views: 623
Reputation: 1079
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
Upvotes: 0
Reputation: 48968
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.
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”?
Upvotes: 2
Reputation: 2753
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.
Upvotes: 3
Reputation: 664
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.
Upvotes: 1