Variable scope when using $http service in AngularJS - javascript

I'm hoping someone can shed some light on the scope of variables when using the $http AngularJS service.
My code looks like this:
app.controller('TestPlanRequestCtrl', function($scope, $http) {
$scope.tableData = []; // Populate the table with this array
$scope.tpRequests = null;
$http.get('common/data/requests.json').success(function(data) {
$scope.tpRequests = data.TPRequests;
});
Next I want to run a loop to put my data into an array like so:
for (var i = 0; i < $scope.tpRequests.length; ++i) {
var requestObj= {
requestNum: $scope.tpRequests[i].RequestNumber;
}
$scope.tableData.push(requestObj);
}
This works great if the for loop is inside the function called from the success method, but I think it would be cleaner to keep it outside the call. If I have the loop outside the call, I get the error:
Error: $scope.tpRequests is null
I don't understand why tpRequests is populated in the get call, and then the data is gone after the get call ends. I'm guessing it is considering the $scope.tpRequests inside the function call to be a different one than the one I declared above the $http.get(). What's the correct way to do this?

You can use a $watch on $scope.tpRequests to do your data manipulation, however I would recommend just doing it in the success promise if there are no other ways to trigger the watch and simply check for null/undefined before operating.
A great primer on use cases for $watch and $watchCollection:
http://www.bennadel.com/blog/2566-scope-watch-vs-watchcollection-in-angularjs.htm

Related

AngularJS Service - Make $resource return data right away instead of a promise so clients don't have to use .then()

This is a simple problem but seems tricky due to asynchronous nature of promises.
With in my data service, I want to ensure that data is retrieved back from the server before moving on to next step as other functions/clients depend on that data.
I don't want clients to register callbacks or use .then() and certainly not use $timeout when requesting the data.
How can I make sure that the controller and service that depend on my Data service get the data right away when requested? I've explained my problem using the code below. I would very much like to continue on my current approach if possible.
AngularJS Version: 1.4+
My Current Approach
//Data Service
App.factory('HealthyFoodService', function($resource, $q) {
var fruitsPromise = $resource('api/food/fruits', {}).query().$promise;
var veggiesPromise = $resource('api/food/veggies',{}).query().$promise;
var fruitsData, veggiesData;
//Ensure $q populates the data before moving on to next statement.
$q.all([fruitsPromise, veggiesPromise]).then(function(data) {
fruitsData = data[0];
veggiesData = data[1];
}
function getCitrusFruits() {
var citrusFruits;
var allFrutis = fruitsData;
//code breaks here because fruitsData is still undefined when called from a controller or another service.
//some logic for this function
return citrusFruits;
}
function getLeafyVeggies() {
var leafyVeggies;
var allVeggies = veggiesData;
//code breaks here because veggieData is still undefined when called from a controller or another service.
//some logic for this function
return leafyVeggies;
}
function getLeafyVeggyByName(name) {
//this function is called from other services and controllers.
var leafyVeggies = getLeafyVeggies();
return leafyVeggies[name];
}
return {
getCitrusFruits: getCitrusFrutis,
getLeafyVeggies: getLeafyVeggies,
getLeafyVeggyByName: getLeafyVeggyByName
});
Below are the two clients. One is a controller and another is a service. They both need the data right away as following statements depend on the returned data.
//Controller
App.controller('LeafyVeggieController', function(HealthyFoodService) {
//Ideally I just'like to do something like below instead of calling `.then()` and registering callbacks.
var leafyVeggies = FoodService.getLeafyVeggies();
//leafyVeggies is undefined because data is not available yet;
});
//Another service depending on HealthyFoodService- similar scenario
App.factory('LeafyVeggieReportService', function(HealthyFoodService) {
function generateLeafyVeggieReport() {
var name = 'spinach';
var veggieInfo = HealthyFoodService.getLeafyVeggieByName(spinach);
//veggieInfo is undefined
//logic that need data.
});
My Previous Approach
Below is how I had it partially working before but I wasn't happy about using .then() everytime I needed the data.(Even with in the same service)
App.factory('HealthyFoodService', function($resource, $q) {
//resource variables;
function getLeafyVeggies() {
return $q.all([veggiesPromise]).then(function(data) {
//logic
return leafyVeggies;
});
}
function getLeafyVeggieByName() {
var leafyVeggies = getLeafyVeggies().then(function(data) {
return data;
}
//some logic
//still causes issues when called from another service because above call doesn't get the data right away.
}
return {
getLeafyVeggies: getLeafyVeggies,
getLeafyVeggieByName: getLeafyVeggieByName
}
//controller
App.controller('LeafyVeggieController', function(HealthyFoodService) {
var leafyVeggies = HealthyFoodService.getLeafyVeggies().then(function(data) {
return data;
});
//controller related logic
});
Update
I'm using ui-router as well, so I'm aware that I can use resolve:{} in $stateProvider to inject the data directly into the controller. The puzzle is how to get the data when I make a request from another service or from another function with in the same service without having to use .then().
Solution
Using $q.all([]) in my client services that depend on my Data service has done the trick for me. I have used $q.all([]) whenever I'm in a situation where I need all the data to be present before start processing the logic.
I still have to use .then() on my clients, but by using $q.all([]), I can still slightly simulate a synchronous flow without breaking any asynchronous principles.
I don't think this is possible. There is inherent latency in network operations that you need to wait for. Not doing so results in the application continuing before the data is available.
That being said, most of the native model binding operations will implicitly wait on promises, so there would be no need to .then if no further manipulation of the data is necessary before passing to the view. You can also use the transformResponse method of ngResource to help with this.
Another option might be to shift the complexity to the resolve methods in the route config. In that case you would handle the .then in the resolve and pass the resolved data to your controller. This will keep your controllers cleaner, but still requires you resolve the promises in the route config.
Try having the service hold your data and have the controller reference that data, instead of trying to pass it to your controller scope. Resolve the promise inside the service like so:
App.factory("HealthyFoodService", function($resource,$q) {
var service = {};
service.data = {
fruitsData: [],
veggiesData: []
}
$resource('api/food/fruits', {}).query().$promise.then(function(data) {
$service.data.fruitsData = data[0];
$service.data.veggiesData = data[1];
})
service.getCitrusFruits = function() {
var citrusFruits;
// Perform some logic on service.data.fruitsData
return citrusFruits;
}
return service;
})
In you controller, you can talk to the service like so:
App.controller("FruitController", function($scope, HealthyFoodService) {
// Now you can access the service data directly via $scope.hfs.fruitsData
$scope.hfs = HealthyFoodService.data;
// Or we can create a local copy of the data using the getCitrusFruits function
$scope.citrusFruits = HealthyFoodService.getCitrusFruits();
})

cannot use http response array after putting it in $scope

I am using AngularJs in my application. I make a http call and getting the response. The response contains an array which I am putting as resultinside the $scope object. I am placing a watch on some attribute and try to access the stored object put inside the $scope object. If I print the result object, I see that it contains the array, but when I try to use the array properties such as length it throws an error. I am also not able to use other array methods such as results.data[0].
Please let me know where I am going wrong. Some code for understanding purpose:
var processResponse = function (result) {
$scope.results = result.data;
}
$scope.$watch('attribute', function(newVal) {
console.log($scope.results.length)
});
Depend how you call this. If the result is a promisse you can try do it:
result.then(successCallback, errorCallback);
Just try to declare/init $scope.results = []; at the top of your controller.
Then the method length couldn't crash ;)
Since it was asynchronous request, I had to take care of null by the following code:
if ($scope.results && $scope.results.data){
console.log($scope.results.data.length);
}

Factory returning same value

I am trying to get songs from soundcloud, I am using some input to set value and send it to my factory to get all the related list of songs and display it.
The issue is the the first time all works correctly, but when I am trying to input new values I am getting same results as first time.
My code looks like:
.controller('DashCtrl', function ($scope, SongsService) {
$scope.formData = {};
$scope.searchSong = function () {
SongsService.setData($scope.formData.songName);
};
UPDATE
the factory :
.factory('SongsService', function ($rootScope) {
var List = {};
List.setData = function (tracks) {
var page_size = 6;
SC.get('/tracks', {limit: page_size, linked_partitioning: 1}, function (tracks) {
// page through results, 100 at a time
List = tracks;
$rootScope.$broadcast('event:ItemsReceived');
});
};
List.getItems = function () {
return List;
};
return List;
}).value('version', '0.1');
Thanks for help!
It's hard to tell without a plunkr reproducing the issue and showing all your relevant code, but I think your problem is that you're overwriting the List variable in the async answer, and this List (I assume) is the object you originally returned from your factory.
I see two noteworthy concepts here:
the fact that angular factories are effectively singletons
and that javascript objects are passed by reference-by-value (see call-by-sharing, or one of many stackoverflow discussions).
An angular factory is a singleton, meaning the factory function will only be called once, before the first injection, and every controller it's injected into will work with the same object reference it returned. If you overwrite this object reference, well, the previous value (which the controller has) is still a reference to the original object.
Edit: In fact, by overwriting List you're creating a new object which doesn't even have a setData method anymore!
You probably want to make List private to SongsService, and return a more complex object from the factory that captures List in a closure, and offers some public getter/setter methods for it. (If you insist on replacing the contents of the returned List variable, empty the object and extend it with the new properties - including this method again. But this is much more work, and not a nice solution.)
In Angular Service constructors and Factory methods are singleton objects. You need to return a method that you can call. Your code examples are incomplete so it is hard to tell what is going on. What is returned by your factory method, the List object?
If so, when the first call is completed, it overwrites the List object so that the setData method can't be called a second time. What is the SC object, I can not see in your example how you are injecting it. You probably want to fix that too.
Consider this possible solution.
Service
Songs.$inject = ['$http'];
function Songs($http) {
this.$http = $http;
}
Songs.prototype.getSongs = function(searchTerm) {
return this.$http.get('http://someendpoint/songs', {searchTerm: searchTerm});
}
service('songs', Songs);
Controller
DashController.$inect = ['songs'];
functionDashController(songs) {
this.songs = songs;
this.results = [];
}
DashController.prototype.searchSongs = function(searchTerm) {
var self = this;
this.songs.getSongs(searchTerm).then(function(results) {
this.results = results;
});
}
controller('DashController', DashController);
This is example uses the best practice controllerAs syntax explained here: http://toddmotto.com/digging-into-angulars-controller-as-syntax/
I found the issue,
I got same results all the time because I didnt use cooreclty the api of soundcloud, I didnt send the title on the api... also you are correct, I should not set the list as empty..I should set some value to the list...

Angular $scope values set in reverse order

I have been working on Angular for one month now. I have been struggling with scoping and keeping state between views.
I don't know how to debug in angular, so I use the console to track the values of the scope when a view is loaded.
This is one of my controllers:
.controller('LegDetailCtrl',function($scope,$stateParams, LegService,ControllerService){
$scope.reservation = ControllerService.getReservation();
LegService.getLeg($stateParams.legId).then(function(data){
ControllerService.setLeg(data.data);
$scope.leg = data.data;
console.log('Check leg',angular.toJson($scope.leg));
});
$scope.seats = LegService.getSeats();
$scope.pax = LegService.getPax();
$scope.user = ControllerService.getUser();
$scope.reservation.leg = $scope.leg;
$scope.reservation.client = $scope.user;
$scope.reservation.pax = $scope.pax;
console.log('Check scope leg', angular.toJson($scope.leg));
})
As I understand, in JS the execution is made in order from top to bottom (not sure of this). I think this way, I am processing and then setting the $scope.leg value, then I use it to feed the $scope.reservation object.
To me, the correct console output would be:
log Check leg, {aJsonObject}
log Check scope leg, {anotherJsonObject}
But what I get is this:
log Check scope leg,
log Check leg, {aJsonObject}
So, it looks like it sets all the values to the scope and then, executes the LegService.getLeg() method.
How do I make this to run in the correct order?
Thanks in advance.
If you're using chrome there's a great debugging tool for AngularJS apps called Batarang
.
To solve your problem you can chain your promises like below.
function LegDetailCtrl($stateParams, LegService, ControllerService){
var vm=this;
vm.reservation=ControllerService.getReservation();
LegService
.getLeg($stateParams.legId)
.then(function(data){
ControllerService.setLeg(data.data);
vm.leg=data.data;
return data.data;
})
.then(function(data) {
var user=ControllerService.getUser();
var pax=LegService.getPax();
var seats=LegService.getSeats();
vm.seats=seats
vm.pax=pax
vm.user=user
vm.reservation.leg=vm.leg;
vm.reservation.client=user
vm.reservation.pax=pax
});
}
LegDetailCtrl.$inject=['$stateParams', 'LegService', 'ControllerService'];
angular
.module('yourModule')
.controller('LegDetailCtrl', LegDetailCtrl)
The .then( in the following line
LegService.getLeg($stateParams.legId).then(function(data){
is an asynchonous call to the function inside the then-block (which contains the "Check leg")
The execution is deferred till the event loop of javascript is empty (javascript is only single threaded).
That means that the code of the main function is executed first (result is "Check scope leg")
and after that the async call inside the then-block is executed ("Check leg")
If you ran this code outside of the browser, you'd find that the console.log call within the callback you passed to then is never called at all. That is because the callback is not called until the promise returned by getLeg is resolved, and that won't happen until the next angular $digest cycle.

executing Angular JS code after some jQuery.ajax has executed

I want to update an Angular scope with data returned by some jQuery ajax call. The reason why I want to call the Ajax from outside Angular is that a) I want the call to return as fast as possible, so it should start even before document.ready b) there are multiple calls that initialize a complex model for a multiple-page web app; the calls have dependencies among themselves, and I don't want to duplicate any logic in multiple Angular controllers.
This is some code from the controller. Note that the code is somewhat simplified to fit here.
$scope.character = {};
$scope.attributeArray = [];
$scope.skillArray = [];
The reasoning for this is that a character's attributes and skills come as objects, but I display them using ng-repeat, so I need them as arrays.
$scope.$watch('character',function(){
$scope.attributeArray = getAttributeArray($scope.character);
$scope.skillArray = getSkillArray($scope.character);
});
In theory, when $scope.character changes, this piece of code updates the two arrays.
Now comes the hard part. I've tried updating $scope.character in two ways:
characterRequestNotifier.done(function() { // this is a jQuery deferred object
$scope.$apply(function(){ // otherwise it's happening outside the Angular world
$scope.character = CharacterRepository[characterId]; // initialized in the jquery ajax call's return function
});
});
This sometimes causes $digest is already in progress error. The second version uses a service I've written:
repository.getCharacterById($routeParams.characterId, function(character){
$scope.character = character;
});
, where
.factory('repository', function(){
return {
getCharacterById : function(characterId, successFunction){
characterRequestNotifier.done(function(){
successFunction( CharacterRepository[characterId] );
});
}
};
});
This doesn't always trigger the $watch.
So finally, the question is: how can I accomplish this task (without random errors that I can't identify the source of)? Is there something fundamentally wrong with my approaches?
Edit:
Try this jsfiddle here:
http://jsfiddle.net/cKPMy/3/
This is a simplified version of my code. Interestingly, it NEVER triggers the $watch when the deferred is resolved.
It is possible to check whether or not it is safe to call $apply by checking $scope.$$phase. If it returns something truthy--e.g. '$apply' or '$digest'--wrapping your code in the $apply call will result in that error message.
Personally I would go with your second approach, but use the $q service--AngularJS's promise implementation.
.factory('repository', function ($q) {
return {
getCharacterById : function (characterId) {
var deferred = $q.defer();
characterRequestNotifier.done(function () {
deferred.resolve(CharacterRepository[characterId]);
});
return deferred.promise;
}
};
});
Since AngularJS has native support for this promise implementation it means you can change your code to:
$scope.character = repository.getCharacterById(characterId);
When the AJAX call is done, the promise is resolved and AngularJS will automatically take care of the bindings, trigger the $watch etc.
Edit after fiddle was added
Since the jQuery promise is used inside the service, Angular has no way of knowing when that promise is resolved. To fix it you need to wrap the resolve in an $apply call. Updated fiddle. This solves the fiddle, I hope it solves your real problem too.

Categories

Resources