I am trying to do something more abstract with Resolve but failing to get the output. Looks like I am missing something.
Problem : I need to load the lookup table data before the view loads. So, I am using resolve in ui-router which is working great for each lookup. I thought its better to make a collection of lookups into a single object but its failing. Here is the code.
Not working code:
resolve: {
lookups: ['GenericFactory', function(genericFactory) {
return {
StateList: genericFactory.GetStateList().then(function(response){return response.data;}),
EmployeeTypeList: genericFactory.GetEmployeeType().then(function(response){return response.data;})
}
}]
}
Working code:
resolve: {
StateList: ['GenericFactory', function(genericFactory) {
return genericFactory.GetStateList()
}],
EmployeeTypeList: ['GenericFactory', function(genericFactory) {
return genericFactory.GetEmployeeType()
}]
}
From the docs:
An optional map of dependencies which should be injected into the controller. If any of these dependencies are promises, the router will wait for them all to be resolved before the controller is instantiated.
I'd say you can't make the non-working code work that way since you are trying to return an object. Since the object is not a promise by itself, the rule above does not apply.
Maybe this will work - you can fire up these requests, wrap them in a promise of its own and return that (haven't been tested):
lookups: ['$q', 'GenericFactory', function($q, genericFactory) {
var deferred = $q.defer();
var q1 = genericFactory.GetStateList().then(function(response){return response.data;}),
var q2 = genericFactory.GetEmployeeType().then(function(response){return response.data;})
$q.all([q1, q2]).then(function (res) {
var obj = {
StateList = res[0];
EmployeeTypeList = res[1];
}
deferred.resolve(obj);
});
return deferred.promise;
}]
Related
Okay, I have three relevant files for my question/s..
App.js - app.config
In my app.config passing value in the resolve using $stateParams is possible and I attain it. But how about the value is from the controller? I need the provCode value to use it in my php select query.
.state("editStudent", {
abstract: true,
url:"/student/:id",
controller: "EditStudentController",
templateUrl: "views/students/editStudent.html",
resolve: {
dataservice: 'dataservice',
student: function(dataservice, $stateParams){
var data ={
id : $stateParams.id
};
return dataservice.getStudent(data);
},
provinceData: function(dataservice){
return dataservice.getProvince();
},
municipalityData: function(dataservice){
var provCode = {
provCode : '0456' // <--------------------- this one
}
return dataservice.getCity(provCode);
}
}
})
EditStudentController.js - app.controller
In my app.controller, I use the angular.forEach to find province code or id to use it php select query. As you can see I tried to create function app.getCity but it is not working as I expected.
console.log(provinceData);
$scope.provinceData = provinceData;
var retProv = $scope.studentInfo.province;
angular.forEach(provinceData, function(value, key) {
if(value.provDesc == retProv){
$scope.studentInfo.province = provinceData[key];
$scope.provCode = provinceData[key].provCode;
console.log($scope.provCode);
}
}); // angular.forEach
var provCode = $scope.provCode;
// Test if it is working but sadly, it is not
// $scope.getCity = function(){
// dataservice.getCity(provCode);
// }
Dataservice.js - app.factory
In my app.factory there are currently 3 functions. The other 2 functions works expectedly but the factory.getCity() is my problem right now. As you can see there is a parameter provCode, this parameter is from my app.config state resolve and it has been assigned a hardcoded/static value.
factory.getStudent = function(data){
return $http.post("php/students/viewOneStudent.php",data).then(function(response){
student = response.data[0];
console.log(student);
return student;
})
}
factory.getProvince = function(){
return $http.get('php/address/province.php').then(function(response){
provinceData = response.data;
console.log(provinceData);
return provinceData;
})
}
factory.getCity = function(provCode){
return $http.post('php/address/cities.php', provCode).then(function(response){
municipalityData = response.data;
console.log(municipalityData);
return municipalityData;
})
}
My Question is:
If it is possible to pass a value from controller to a resolve?
If it is possible, how can I do that? because I tried my very best in trial and error approach.
If it is not possible, what is my option/s to achieve what I want?
Note
I tried to do it without resolve, getting data and posting again for php querying, it is also works BUT the problem is sometimes data doesn't load when the view is loaded. As far as I know, read and understand from different sources in the internet, there is a time that view is loaded but the "work on processing the data" is not finished yet..
I am pretty new and still learning AngularJs. When I am practicing and learning AngularJs coding, I love how it works especially the routing, I am impressed with it. So, I am trying my very best to understand AngularJs and its complexity.
UPDATE
I tried different approach of how I can get my expected results. But I can't get to it, I encountered an error. This is the link of my other approach.
Undefined property: stdClass: in using AngularJS + PHP
Let's say I have a service deal with Firebase operation:
angular.module('myApp').factory('taskOfferService', ['FURL', '$firebaseArray', '$firebaseObject', '$q', 'taskService', taskOfferService]);
function taskOfferService(FURL, $firebaseArray, $firebaseObject, $q, taskService) {
var ref = new Firebase(FURL);
var Offer = {
acceptOffer: function(taskId, offerId, runnerId) {
var offerRef = ref.child('taskOffers').child(taskId).child(offerId);
var taskUpdate = $q.defer();
offerRef.update({accepted: true}, function(error) {
if (error) {
console.log('Update offer accepted value failed!');
} else {
var taskRef = ref.child('tasks').child(taskId);
taskRef.update({status: "assigned", runner: runnerId}, function(error) {
if (error) {
console.log('Update task status failed!');
taskUpdate.reject(false);
} else {
taskUpdate.resolve(true);
}
});
}
});
return taskUpdate.promise;
},
};
return Offer;
}
})();
And I have a controller need to call this service and need to wait for a promise when update successful - to call the toaster.pop:
$scope.acceptOffer = function(offerId, runnerId) {
taskOfferService.acceptOffer($scope.selectedTask.$id, offerId, runnerId).then(function(){
toaster.pop('success', 'Offer is accepted.');
});
};
This code work and it is follow the suggestion from Firebase docs. But as you can see in my service, in order to get a promise, I need to use the update callback inside an update callback...My point is, Firebase SDK do not return a promise when done and I need to create this promise and it is A LOT OF CODES...
If I use firebaseObject (angularFire 1.0), it will run into issues listed here: saving new property overwrites firebase object
And using ANTIPATTERN according to #sinan, the code can be way clearer:
acceptOffer: function(taskId, offerId, runnerId) {
var o = $firebaseObject(ref.child('taskOffers').child(taskId).child(offerId));
o.$loaded().then(function(){
o.accepted = true;
o.$save().then(function(){
var oTask = $firebaseObject(ref.child('tasks').child(taskId));
oTask.$loaded().then(function(){
oTask.status = "assigned";
oTask.runner = runnerId;
oTask.$save();
});
})
},
The point is, using "ANTIPATTERN", I can utilize $save() - which return an promise so no $q service is need inside my firebase service. It looks a lot clearer in my opinion. Both method works.
BUT, according to doc:
"The $loaded() method should be used with care as it's only called once after initial load. Using it for anything but debugging is usually a poor practice."
I just find myself using $loaded A LOT! Please advice the best way to go about this.
Firebase has a JavaScript SDK that exposes the platform's functionality to JavaScript environments. AngularFire is a library on top of that SDK, that makes it easier to bind Firebase data to an AngularJS web interface.
Your example here is a plain data manipulation operation. You're not binding the data to the screen, so you should not need AngularFire. I also see no need for using promises.
As far as I can tell, this does the exact same thing as your last script:
acceptOffer: function(taskId, offerId, runnerId) {
var offer = ref.child('taskOffers').child(taskId).child(offerId);
offer.update({ accepted: true }, function() {
var task = ref.child('tasks').child(taskId);
task.update({ status: "unassigned", runner: runnerId });
});
}
Not only is this shorter, it also prevents downloading the data, just to then update it.
And the best part? Since AngularFire is built on top of the regular Firebase JavaScript SDK, they interoperate perfectly. So if in another somewhere you actually display the task or offer through an AngularJS view that watches to a $firebaseObject, it will show the updated value straight away.
Update
If you need to do something when the acceptOffer method is done saving, you can pass in a callback:
acceptOffer: function(taskId, offerId, runnerId, callback) {
var offer = ref.child('taskOffers').child(taskId).child(offerId);
offer.update({ accepted: true }, function(error) {
if (!error) {
var task = ref.child('tasks').child(taskId);
task.update({ status: "unassigned", runner: runnerId }, callback);
}
else {
callback(error)
}
});
}
You then invoke it like:
taskOfferService.acceptOffer($scope.selectedTask.$id, offerId, runnerId, function(error) {
if (!error) {
toaster.pop('success', 'Offer is accepted.');
}
else {
console.error('Something went wrong: '+error);
}
});
You could definitely also promisify the acceptOffer method. But this is not necessary.
So to summarise I am using angular-ui router resolve function to retrieve state specific data. However it doesn't seem to full wait for the promise to resolve:
state('parent.ChildState', {
url: '/myUrl?param1¶m1',
templateUrl: 'views/list.view.html',
controller: 'MyController',
resolve: {
data: resolveData
}
}).
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
DataService.myDataObj = DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
});
return DataService.myDataObj;
// Note I have also tried returning directly the DataService.get call however this makes all the below console log statements as undefined (see below for the controller code to know what I mean). So I do the assignment first and then return that.
}
Now in the controller I had a function that executes on load like so:
function ExpensesController(DataService) {
$scope.viewData = DataService;
initData();
function initData() {
// this generally logs a ngResource and shows the full data obj on console
console.log($scope.viewData.myDataObj);
// this gets undefined on console
console.log($scope.viewData.myDataObj.someField1);
// this log fine, however why do I need to do promise
// resolve? should be resolved already right?
$scope.viewData.myDataObj.$promise.then(function() {
console.log($scope.viewData.myDataObj.someField1);
});
As your required data to resolve is async, you need to return a promise and add return statement inside your callback function.
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters, function(result, headers) {
DataService.myDataObj = result;
return DataService.myDataObj
});
}
You can read ui-router resolve docs more about how resolver works and when they should return promise or pure values.
I don;t know if I have got your problem :), but here is what I feel is wrong
1) in the resolve return a promise, it should not be resolved
function resolveData($stateParams, Utils) {
var filters = Utils.getFilters($stateParams);
return DataService.get(filters);
}
2) In the controller you should inject the data that is declared in resolve not the DataService so your controller should be
function ExpensesController(data) {
$scope.viewData = data;
}
I've tried to strip out the details and make this fairly generalized...
Using 1.2 rc2 my code worked fine, after updating to 1.2 stable and correcting for $parse changes I've run into a binding problem. Before the update, the following code worked without any issues. updateChildObject() gets called from the html page.
.when('/the-page/', {
controller: function($scope, serviceResults, FactoryService) {
$scope.object.childObject = serviceResults;
// this function used to work. Now assigns the function to the
// scope rather than the results
$scope.updateChildObject = function(args) {
$scope.object.childObject = FactoryService.getSomethingFromServer(args);
};
},
resolve: {
serviceResults: function(FactoryService) {
return FactoryService.getSomethingFromServer(args);
}
}
Since this is failing now ($scope.object.childObject appears to get set as the function and not the results) I believe the appropriate way to solve it is through a promise. (Note, the service itself is using a promise successfully.) However, I'm having difficulty getting the $scope to update when the promise is resolved.
I believe the following code is along the right track. $q is injected in the controller.
...
$scope.updateChildObject = function(args) {
var defer = $q.defer();
defer.promise.then(function() {
return FactoryService.getSomethingFromServer(args);
});
$scope.object.childObject = defer.resolve();
};
...
So can anyone tell my what I'm doing wrong here? Promises are just one of those things that haven't really clicked for me yet.
Just as an alternative to your answer: you say FactoryService is already successfully using a promise, and in that case it seems like you don't need an additional promise in updateChildObject too. You could update FactoryService.getSomethingFromServer(args) to return a promise (i.e. with return defer.promise; at the end and defer.resolve(results); in the async bit), and then simplify updateChildObject to just:
$scope.updateChildObject = function(args) {
FactoryService.getSomethingFromServer(args).then(function(results) {
$scope.object.childObject = results;
}
};
Also, it's worth knowing that Angular 1.2 intentionally breaks automatic promise unwrapping that was in earlier versions: https://github.com/angular/angular.js/issues/4158 . It used to be the case that this code
$scope.updateChildObject = function(args) {
$scope.object.childObject = FactoryService.getSomethingFromServer(args);
};
would work identically to the one above (assuming getSomethingFromServer returns a promise), but not anymore. This might be the issue you're running into with 1.2
Figured out what I was doing wrong. Definitely was a promise issue in that I just wasn't using them correctly. The following solved it:
...
$scope.updateChildObject = function(args) {
var defer = $q.defer();
defer.promise.then(function(results) {
$scope.object.childObject = results;
});
defer.resolve(FactoryService.getSomethingFromServer(args));
};
...
So defer.resolve calls what's to be resolved. promise.then() passes the results to the next action. So simple.
I am currently populating $scope arrays like this:
$scope.categories = [];
$scope.getCategories = function() {
ItemService.getCategories().success(function(categories) {
$scope.categories = categories;
});
}
$scope.getCategories();
Is this really the easiest way to do it?
Do you actually need a getCategories function ? If this is the only place it's called, then you can just remove and leave the service call in directly.
Otherwise your code is as short as it gets for using promises I think, if you're using v1.2. Before 1.2, angular did have automatic promise unwrapping. That is, instead of
//$scope.getCategories = function() {
ItemService.getCategories().success(function(categories) {
$scope.categories = categories;
});
//}
You could do this instead, which seems much more elegant:
//$scope.getCategories = function() {
$scope.categories = ItemService.getCategories();
//}
Issues with this mean it's recently been removed though - https://github.com/angular/angular.js/issues/4158 . I think it's possible an alternative will be added in the future if there's a good way to do it though.
You could simply use the resolve property of your router (assuming you're using a router) to inject the results from ItemService.getCategories() into your controller. resolve properties block the route from completing until they're fully resolved--it unwraps promises automatically. The code would look something like:
angular.module('MyModule').config(function ($routeProvider) {
$routeProvider.when('/mypage', {
templateUrl: '/mypage.html',
controller: 'MyController',
resolve: {
categories: function (ItemService) { return ItemService.getCategories(); }
}
});
});
angular.module('MyModule').controller('MyController', function (categories, $scope) {
$scope.categories = categories;
});