With all the examples of services, factories, using $scope, using Controller as, I'm getting a bit confused. I have a simple ng-if expression that's returning undefined because the data to evaluate isn't ready yet:
<div ng-if="ctrl.AlreadyBoughtShoe('ShoeId')"> ... </div>
...
<script>
(function() {
var MyApp = angular.module("MyApp", []);
MyApp.controller("MyAppController", function($http, $timeout, ShoeService) {
var x = this
loadRemoteData();
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes){
applyRemoteData(Shoes);
});
}
function applyRemoteData(Shoes) {
x.Shoes = Shoes;
}
// FAILS HERE - Get undefined on the array
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
});
MyApp.service("ShoesService", function($http, $q){
return({
GetShoes: GetShoes
});
function GetShoes() {
var request = $http({
method: "GET",
url: /MyUrl",
cache: false,
headers: $myHeaders
});
return( request.then(handleSuccess, handleError));
}
function handleError( response ) {
if (!angular.isObject(response.data) || !response.data.message) {
return( $q.reject( "An unknown error occurred." ) );
}
return( $q.reject(response.data.message) );
}
function handleSuccess( response ) {
return( response.data );
}
});
})();
</script>
Also, if it's relevant, in this particular case it has nothing to do with shoes... and the data is a person object, and there's no ng-repeat going on, so the ID for the "shoe" is manually typed in. I jumbled up my actual code to simplify this so I can understand the best way to deal with it, but that ng-if needs to evaluate after the data is ready to be evaluated.
I'm not sure how to best used promises or whatever else I need in this style of format, which I found an example of somewhere on the web a while back.
This is happening because of the asynchronous nature of your service call in ShoeService. Your error is occurring due to code being called before x.Shoes = Shoes is resolved, essentially iterating over undefined. Try moving your logic into the then callback of your service. For example...
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes) {
applyRemoteData(Shoes);
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
});
}
You can probably move this to the end of applyRemoteData also if you wish. Either way you will need to execute this logic after you resolve x.Shoes
You are right - when this code runs, x.Shoes is undefined. Change:
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < x.Shoes.length; i++) {
// Do stuff
}
}
to:
x.AlreadyBoughtShoe = function(shoeId) {
for (var i = 0; i < (x.Shoes || []).length; i++) {
// Do stuff
}
}
You have multiple options.
Evaluate the ng-if to false by default until you receive the data. You keep your AlreadyBoughtShoe method but you first check if you have data. If you don't have data yet just return false. You won't have an error anymore and when your promise is resolved your HTML should reflect that.
You can delay controller initialization until your promise is resolved.
Maybe setting semaphore or something simillar can help.
Promise evaluates after some period of time, and setting variable to true after succesfull call may help. Then add that variable to the ng-if condition, which would evaluate function only when variable is true, so the promise returned.
Set variable to and condition, which would evaluate when both are true.
<div ng-if="ctrl.loaded && ctrl.AlreadyBoughtShoe('ShoeId')"> ... </div>
Then set variable to true on success ( by default is set to false because of javascript ).
function loadRemoteData() {
ShoeService.GetShoes().then(function(Shoes){
x.loaded = true;
applyRemoteData(Shoes);
});
}
This may help.
Related
I have an angular application that implements factory functions to handle some API requests for a global object that is implemented in almost all controllers.
factory.loadCart = function() {
var deferred;
deferred = $q.defer();
httpService.get({
service: 'cocacola',
param1: 'userCart',
guid: sessionStorage.token
}, function(r) {
if (r.error == 0) {
$rootScope.user.cart = r.result;
deferred.resolve(r.result);
} else {
deferred.reject("Error al cargar el carrito.")
}
}, function(errorResult) {
deferred.reject(errorResult);
});
return deferred.promise;
}
In the code I set the value of user.cart property as the result of the request. When I go to another controller that also implements this factory method (in this way)...
CartFactory.loadCart().then(function(response) {
$rootScope.user.cart = response;
$scope.cart = $rootScope.user.cart;
if ($rootScope.user.cart.productos.length == 0) {
$state.go('main.tienda');
} else {
getCards();
$rootScope.showCart = false;
}
}, function(error) {
$scope.loading = false;
$scope.showMe = false;
$state.go('main.tienda');
console.log(error);
});
... and go back to the first controller, the user.cart property is undefined and I can't proceed to execute the other functions that are defined as factory methods since the $rootScope.user.cart property is undefined and required as a parameter to these other functions. Also, the $rootScope.user.cart property gets its value after I refresh the browser (but I can't keep this as a solution), I'm very new to Angular so any help will be really appreciated, this is driving me nuts!
I've always found that $rootScope was somewhat difficult to work with, and something of an antipattern in AngularJS... it's like putting variables on the global scope in vanilla JS.
Is there any reason you wouldn't avoid the whole $rootScope.user.cart issue by just keeping cart in CartFactory and then putting an API in CartFactory to getCart, then return whatever cart is to any interested controller?
where you setting 'user' property on $rootScope? can you please put the whole code and always before reading the nested properties from object, check for undefined, in your case put if($rootScope.user) { // your logic }
I have the following code
if (testNavigation() && $scope.selections.somethingChanged) {
return false;
}
in the testNavigation I am calling modal dialog and if I answer Ok in that dialog, I re-set somethingChanged to false. My problem is that when the code is executed, the testNavigation and modal dialog is by-passed and executed later and therefore my test is not working as I need it to work. What should I change in order for my logic to properly work, e.g. first invoke my modal dialog, and proceed accordingly in the main function?
This is my testNavigation method:
var testNavigation = function()
{
if ($scope.selections.somethingChanged) {
var modal = $modal.open({
controller: function ($scope) {
$scope.okClass = 'btn-primary',
$scope.okLabel = resourceFactory.getResource('Labels', 'yes'),
$scope.cancelLabel = resourceFactory.getResource('Labels', 'cancel'),
$scope.title = resourceFactory.getResource('Labels', 'unsavedChanges'),
$scope.message = resourceFactory.getResource('Messages', 'unsavedChanges');
},
templateUrl: '/app/templates/modal'
});
modal.result.then(function () {
$scope.selections.somethingChanged = false;
});
}
return true;
}
I'll try to add more details. I have LoadView() and New() functions in the Index page controller. In both of these functions I need to do the following:
if $scope.selections.somethingChanged = false I need to proceed with the logic.
if $scope.selections.somethingChanged = true I need to pop up a modal dialog asking if I want to go ahead and Cancel my changes or Stay on the current page. If I answer Yes, I want to go ahead with the logic.
So, that's the purpose of the separate testNavigation function. In the languages where each function call is sequential, that would work as I intended. But it doesn't work this way in AngularJS / JavaScript and I am not sure how to make it to work the way I need. We tried few ideas with $q service but didn't get the result.
Make testNavigation() always return a promise (either with the result of the modal, or with false straight away, when somethingChanged is false and you don't want to ask the question. Proceed when this promise is resolved:
var testNavigation = function() {
if ($scope.selections.somethingChanged) {
var modal = [...]
return modal.result; // returns a promise that will be resolved when the modal is closed
} else {
return $q.when(true); // returns a promise that will be resolved straight away with true
}
}
// when used:
testNavigation().then(function() {
...do stuff...
})
Without knowing what your test looks like, this is kind of difficult, but from what it looks like, you're creating a race condition.
You call testNavigation() which always returns true,
but because $scope.selections.somethingChanged is set to false at some point in the future,
$scope.selections.somethingChanged may not finish before the end of that evaluation- so while you're setting $scope.selections.somethingChanged to false in testNavigation, it may or may not be false when the second if is performed:
if( testNavigation() && // returns true no matter what.
$scope.selections.somethingChanged // this is changed with a promise within testNavigation. that promise could either complete or not complete before this is evaluated with the if.
){
return false;
}
var testNavigation = function() {
if ($scope.selections.somethingChanged) {
var modal = $modal.open({
// details
});
modal.result.then(function() {
// this is async and could take place any time between {locationA} and never.
$scope.selections.somethingChanged = false;
});
//{locationA}
}
return true;
}
I can imagine that producing some weird results in tests.
I have been stuck on this for quite a while now and cannot figure out why the value is not being returned. I am using Angular $resource to make a GET request to an API.
My $resource factory looks like this:
.factory("Bookings", function ($resource) {
return $resource("www.example/bookings_on_date/:day", {});
})
I have tried to implement promises but am unable to do so correctly.
function getBookings(day){
return Bookings.get({"day": day}).$promise.then(function(data) {
console.log(data.total)
return data.total;
});
}
$scope.todaysBookings = getBookings(TODAY);
$scope.tomorrowsBookings = getBookings(TOMORROW);
When I view either console.log($scope.todaysBookings) or $scope.tomorrowBookings in the console it returns undefined.
I have also tried everything from this jsfiddle but unfortunately have not had any luck.
I think it should be like this:
function getBookings(day) {
return Bookings.get({"day": day}).$promise.then(function(data) {
return data.total;
});
}
getBookings(TODAY).then(function(total) {
$scope.todaysBookings = total;
});
getBookings(TOMORROW).then(function(total) {
$scope.tomorrowsBookings = total;
});
Update: I think next code style could help you to prevent next extending method problems:
function getBookings(args) {
return Bookings.get(args).$promise;
}
getBookings({"day": TODAY}).then(function(data) {
$scope.todaysBookings = data.total;
});
getBookings({"day": TOMORROW}).then(function(data) {
$scope.tomorrowsBookings = data.total;
});
A little advantages here:
Pass object into function could help you easily pass different
arguments into method and arguments are very close to method call (a
little bit easy to read);
Return complete response from function
could help you to process different data (method could replay
different response depends on arguments, but it's not good practise
in this case);
p.s. Otherwise, you could remove function declaration and code like this (to keep it as simple, as possible):
Bookings.get({"day": TODAY}).$promise.then(function(data) {
$scope.todaysBookings = data.total;
});
Bookings.get({"day": TOMORROW}).$promise.then(function(data) {
$scope.tomorrowsBookings = data.total;
});
I'm using m.request in a project, and since I have a request that can be potentially long running, I want to run it with background:true. However, it seems like the value never gets set to the generated m.prop.
I've made a jsFiddle with an example based on this Stack Overflow answer: http://jsfiddle.net/u5wuyokz/9/
What I expect to happen is that the second call to the view should have the response value in ctrl.test.data(), but it seems to still have undefined. At Point A in the code, it logs the correct value. However, at Point B, it logs false, undefined and then true, undefined.
I'm not sure if I'm doing something incorrectly, or if this the expected behavior.
Code from the jsFiddle:
var requestWithFeedback = function(args) {
var completed = m.prop(false)
var complete = function(value) {
completed(true)
return value
}
args.background = true
return {
data: m.request(args).then(complete, complete).then(function(value) {
//Point A
console.log(value);
m.redraw()
return value
}),
ready: completed
}
};
var mod = {
controller : function() {
this.test = requestWithFeedback({
method : "POST",
url : "/echo/json/",
serialize: serialize,
config: asFormUrlEncoded,
data : {
json : "{\"name\" : \"testing\"}"
}
});
},
view : function(ctrl) {
//Point B
console.log(ctrl.test.ready(), ctrl.test.data());
return m("div", ctrl.test.ready() ? 'loaded' : 'loading');
}
};
Edit: The problem is that m.redraw is called before the data is assigned. Instead you could create a m.prop for data and let the ajax request assign that value when completed. requestWithFeedback will then look like this:
var requestWithFeedback = function(args) {
var data = m.prop()
args.background = true
m.request(args).then(data).then(function() { m.redraw() })
return {
data: data,
ready: function() {return !!data()}
}
};
Here's a modified version of your fiddle using this code: http://jsfiddle.net/u5wuyokz/11/
When using background: true, Mithril's components branch or any other system that executes controllers in the same 'tick' as views, m.requests made in the controller will not have resolved by the time they are invoked by their respective views.
It is therefore recommended to always use the initialValue property of m.request if you're using background: true. The canonical example is to have an initial value of [] when you make a request for a list of entries:
var projectsModule = {
controller(){
this.projects = m.request( {
background : true,
initialValue : [],
url : '/projects.json'
} );
},
view( ctrl ){
return m( 'ul', ctrl.projects.map(
project => m( 'li', project.name )
) )
}
}
This solves the practical problems of views breaking when they expect to be able to work with m.request return values in a generic way, but doesn't address the more complex case in your example, where a ready flag is desirable to indicate a 'loading' state. I have a generic API that consumes a model getter function and an optional initial value. It has next, then, hasResolved, isPending, get and set methods: this allows views to be more flexible in the way they query the state of asynchronous models. hasResolved indicates whether the method has ever resolved, and next returns a promise that will resolve either with the current request, or when the next request is resolved if isPending is false.
I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.