Integration of $q in scopes? - javascript

I've read on several places that $q is gracefully integrated in scope in Angular JS.
Suppose that you have this:
var superService = function() {
var deferred = $q.defer();
deferred.resolve(['foo', 'bar']);
return deferred.promise;
};
Of course, $q is useless here but if I use $timeout or run a $http call, the result is the same.
If I do this:
superService().then(function(data) {
$scope.result = data;
});
It's ok. But if I do that:
$scope.result = superService();
It's also supposed to be ok. But in my case, $scope.result contains 3 elements (they are visible in my template with a ng-repeat): "then", "catch" and "finally" functions, I guess... instead of ['foo', 'bar'] of course.
My concrete example:
angular.module('myModule', [])
.factory('HelloWorld', function($q, $timeout) {
var getMessages = function() {
var deferred = $q.defer();
deferred.resolve(['Hello', 'world']);
return deferred.promise;
};
return {
getMessages: getMessages
};
})
.controller('HelloCtrl', function($scope, HelloWorld) {
$scope.messages = HelloWorld.getMessages();
//HelloWorld.getMessages().then(function(data) {
// $scope.messages = data;
//});
});
Any idea here?

Automatic promise unwrapping has been deprecated and will soon be removed. See: https://github.com/angular/angular.js/commit/5dc35b527b3c99f6544b8cb52e93c6510d3ac577

Related

AngularJS $scope dropping value after it is assigned

I am hoping someone can help me understand an annoying problem I am having with $scope in AngularJS. Please see the comments in my code below:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
});
});
The call to the WEB API returns a valid object which is assigned to $scope.data. I display an alert to make sure that $scope.data.MyObject.Property exists, which it does. The expected value is displayed.
Now when I try accessing $scope.data.MyObject.Property in the $viewContentLoaded code, the $scope.data.MyObject is no longer in the $scope. The console reports the following:
HTML1300: Navigation occurred.
File: route.html
TypeError: Unable to get property 'MyObject' of undefined or null reference
at Anonymous function (http://server/script/route.js:43:13)
at Anonymous function (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:158:234)
at e (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:45:348)
at Anonymous function (https://ajax.googleapis.com/ajax/libs/angularjs/1.5.7/angular.min.js:48:275)
Why is $scope dropping the value of $scope.data.MyObject? What makes this problem even more frustrating is if I put an alert(""); in the $viewContentLoaded code, the $scope.data.MyObject value is no longer undefined. What is going on here?
You need to know the timing of how your code get executed.
This is fixed code with some logging:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
console.log(1);
var promise = $http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
console.log(2);
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
console.log(3);
// d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property;
promise.then(function () {
console.log(4);
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property;
});
}, 0);
});
});
You may expect the result logs is 1234, but actually it can be 1324. In later case, the code in $viewContentLoaded is executed before the $http.get success. So it $scope.data is still null.
The solution is using Promise (or $q in angular world). So that you can wait for the result of $http.get. You have guarantee that 4 is always executed after 2 (assuming it succeeded).
Well, this behavior is because JavaScript code is get executed async. so better to include that code once promise is resolved.
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
}).error(function (data) {
alert(data);
});
});
This will work :)
Cheers!
The $http request is ansynchronous. It may not complete before your $viewContentLoaded event is fired. ( I guess this event fires after DOM is loaded and does not wait for http requests to complete, I may be wrong).
Why not do something like this:
app.controller('MyController', function ($scope, $routeParams, $http, $timeout) {
$scope.id = $routeParams.id;
$http.get("http://server/api/Blah/GetData/" + $scope.id).success(function (data) {
$scope.data = data;
alert($scope.data.MyObject.Property); //displays the expected value. - Not Undefined or null
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
}).error(function (data) {
alert(data);
});
$scope.$on('$viewContentLoaded', function () {
});
Since http get is async function. you have to use promises to wait until http get fetches the result.
you can do this by following code.
make a service.
app.factory('myService', function($http) {
var getData = function(id) {
// Angular $http() and then() both return promises themselves
return $http({method:"GET", url:"http://server/api/Blah/GetData/" + id}).then(function(result){
// What we return here is the data that will be accessible
// to us after the promise resolves
return result.data; //or may be return result only.
});
};
return { getData: getData };
});
in your controller
app.controller('MyController', function ($scope, $routeParams, $http, $timeout,myService) {
$scope.id = $routeParams.id;
var Data=myService.getData($scope.id);
Data.then(function(result){
$scope.data.MyObject.Property=result;//or result.data may be
$scope.$on('$viewContentLoaded', function () {
$timeout(function () {
var d = document.getElementById("iframe");
d.contentDocument.documentElement.innerHTML = $scope.data.MyObject.Property; //Now MyObject is magically undefined.
}, 0);
});
});
To begin with, the declaration of the controller is missing elements. It should be:
app.controller('MyController', ["$scope" , "$routeParams" , "$http" , function ($scope, $routeParams, $http, $timeout) {...
Check Dependency Injection in Angular's docs.
Try this and, if still not working, update your question with the new code and some loggings.

AngularJS - refactoring controller into service - understanding self/this and scope

I'm new to AngularJS and my JavaScript knowledge right now isn't that strong, so I apologise in advance whilst I wear the white belt and expose my ignorance.
I'm having some difficulty refactoring the following controller into a service, primarily for seperation of concerns and pushing the logic further down the stack, and getting my controller skinny again.
airBnbClone.controller('SpacesCtrl', ['$http', '$location', function( $http, $location) {
var self = this;
self.spaces = [];
self.currentspace;
self.create = function(space) {
$http.post('http://localhost:3000/api/spaces', space).success(function(data) {
self.spaces.push(data);
self.showSpace(data.space);
});
};
self.getSpaces = function(){
$http.get('http://localhost:3000/api/spaces.json').then(function(response){
self.spaces = response.data.reverse();
});
};
self.showSpace = function(space){
$http.get('http://localhost:3000/api/spaces/' + space.id).then(function(response){
self.currentspace = response.data;
$location.path('/spaces/' + space.id)
});
};
}]);
After some refactoring, my code now looks like this:
airBnbClone.controller('SpacesCtrl', ['$http', '$location', 'spacesService', function( $http, $location, spacesService) {
var self = this;
self.create = function(space) {
spacesService.createSpace(space);
};
self.getSpaces = function(){
spacesService.getSpaces();
};
self.showSpace = function(space){
spacesService.showSpace(space);
};
}])
.service('spacesService', ['$http', '$location', function($http, $location){
var self = this;
self.spaces = [];
self.currentspace;
this.createSpace = function(space){
$http.post('http://localhost:3000/api/spaces', space).success(function(data) {
self.spaces.push(data);
self.showSpace(data.space);
});
}
this.getSpaces = function(){
$http.get('http://localhost:3000/api/spaces.json').then(function(response){
self.spaces = response.data.reverse();
});
};
this.showSpace = function(space){
$http.get('http://localhost:3000/api/spaces/' + space.id).then(function(response){
self.currentspace = response.data;
$location.path('/spaces/' + space.id)
});
};
}]);
Whereas before I refactored, my code was working as intended. My main view had an ng-init='spacescontroller.getSpaces()', with my ng-controller='SpacesCtrl as spacescontroller' pulling in my list of spaces. When I clicked on a particular space, it would then go to show that particular space as intended.
Now, with my refactored code my default view shows nothing at all, and when I do create a space, it seems like it can't update the original self.spaces array sitting in the controller.
Basically, I'm unsure of how to refactor these methods into services. Should my self.spaces and self.currentspace objects stay in the controller, or become properties of the injected service? Which is the preferred method for storing state in this case, and why?
Since my code doesn't render a view anymore, why is this the case? I apologise if my questions are quite circular, I've been going for days on this and despite consulting many different sources, I'm starting to feel very confused.
Consider returning promises from your service.
app.service('spacesService', function($http) {
this.getSpaces = function(){
var url = 'http://localhost:3000/api/spaces.json';
var promise = $http.get(url)
.then(function(response){
//return for chaining
return response.data.reverse();
});
return promise;
});
});
The one of the advantages of returning promises is that they retain error information.
In your controller:
airBnbClone.controller('SpacesCtrl', function($http, $location, spacesService) {
var self = this;
self.spaces = [];
self.getSpaces = function() {
var promise = spacesService.getSpaces();
promise.then(function onFulfilled(spaces) {
self.spaces = spaces;
}).catch(function onRejected(response) {
console.log(response.status);
});
};
};
For more information on the advantages of using promises, see Why are Callbacks from Promise .then Methods an Anti-Pattern?.
You need a way to bring the results of the GET request from the service to the controller.
I left the logic in the controller and moved all the HTTP requests to the service. And I added a callback that will fire when the results are ready to let the controller make use of the results.
airBnbClone.controller('SpacesCtrl', ['$http', '$location', 'spacesService', function($http, $location, spacesService) {
var self = this;
self.spaces = [];
self.currentspace;
self.create = function(space) {
spacesService.createSpace(space, function(data) {
self.spaces.push(data);
self.showSpace(data.space);
});
};
self.getSpaces = function() {
spacesService.getSpaces(function(spaces) {
self.spaces = spaces;
});
};
self.showSpace = function(space) {
spacesService.showSpace(space, function(currentspace) {
self.currentspace = currentspace;
$location.path('/spaces/' + space.id)
});
};
}])
.service('spacesService', ['$http', function($http) {
var self = this;
this.createSpace = function(space, cb) {
$http.post('http://localhost:3000/api/spaces', space).success(function(data) {
cb(data);
});
}
this.getSpaces = function(cb) {
$http.get('http://localhost:3000/api/spaces.json').then(function(response) {
var spaces = response.data.reverse();
cb(spaces);
});
};
this.showSpace = function(space, cb) {
$http.get('http://localhost:3000/api/spaces/' + space.id).then(function(response) {
var currentspace = response.data;
cb(currentspace);
});
};
}]);
The usage of service service assumes that a new object instance is injected that holds its own state. The typical use for that is an instance of model class ('model' like in MVC model).
The refactoring is on the right track. Considering that spacesService is a model, it can be just assigned to controller (instead of numerous wrappers for each of model's methods):
this.spacesService = spacesService;
// or any readable property name, e.g.
// this.spaces = spacesService;
So it could be reached from the view with
spacescontroller.spacesService.getSpaces()
The exception here is showSpace method. The part with $location.path has nothing to do with model state, and the fact that it does some routing indicates that it belongs to controller and shouldn't be extracted from it. So it can be separated to spacesService.getSpace(id) and showSpace in controller.

How to pass variable from function inside controller to controller scope?

I have the following controller:
myApp.controller('myCtrl', ['$scope', '$rootScope', '$location', 'myService',
function($scope, $rootScope, $location, myService) {
$scope.myArray = [];
$scope.myFunction = function() {
if (something) {
setTimeout(function(){
$scope.myFunction();
},500);
} else {
var itm = $rootScope.var;
for (var i in itm) {
if (itm.hasOwnProperty(i)) {
$scope.myArray.push(itm[i].value);
}
}
// first console.log
console.log($scope.myArray);
}
}
$scope.myFunction();
// second console.log
console.log($scope.myArray);
}
]);
In the example above the second console.log gets printed before the first one. Why is this the case? Is there a way to make the controller wait for the function to be executed/returned and only after that proceed to execute the rest of the code?
Without seeing how everything is being implemented. This is the best I can help you with. If you want a controller to do something only if a promise is successful you can wrap your code around the request. In the plunkr I have written a sample $http service that has a fake request to myFunction that uses $q.
I would suggest using a factory to share data between controller instead of $rootScope. $rootScope is hard to manage throughout big SPA's. The Plunkr has commented options you can mess with to change between $rootScope and using a Factory.
Service below
app.service('Service', Service);
function Service($q, $rootScope, Factory) {
var deferred = $q.defer();
this.myFunction = function(){
//Using factory to persit data instead of $rootScope
//var itm = Factory.myArray;
var itm = $rootScope.var;
var array = [];
//Item isnt set return error
if(itm === undefined || itm === null) deferred.reject("$rootScope.var is not set")
//Changed this a bit didnt know what $rootScope.var actually was
for (var i in itm) {
array.push(itm[i]);
}
deferred.resolve(array);
return deferred.promise;
}
return this;
}
The first thing the controller does is initializes a request to Service.myFunction() and waits for a success or error callback. After the success you can process and do anything you'd like with the data returned from the promise. If there is an error you can handle it as you see fit.
app.controller('controller', controller);
function controller(Service, $rootScope) {
/* jshint validthis: true */
var vm = this;
vm.myArray = [];
vm.request = "";
//Un-Comment this to return success or error
$rootScope.var = [1,2,3,4,5,6];
//This is a fake http request
Service.myFunction().then(
//if the promise was resolved or $http was a success
//initilize the controller
function(data) {
vm.myArray = (data)
},
//if the promise was resolved or $http was a success
//initilize the controller
function(err) {
vm.request = (err)
})
}
Plunkr

Pass a variable to a service with AngularJS?

I am building an app to track movies and their info, I am new to Angular, and I cant not really sure how to pass a variable to this service.
I want the url to be a variable instead of hardcoded. whats the best way to do it?
tmdb.service('tmdbService', function($http, $q){
var deferred = $q.defer();
$http.get('https://api.themoviedb.org/3/movie/popular?api_key=jkhkjhkjhkjh').then(function(data){
deferred.resolve(data);
});
this.getMovies = function(){
return deferred.promise;
}
});
tmdb.controller("tmdbController", function($scope, tmdbService){
var promise = tmdbService.getMovies();
promise.then(function(data){
$scope.movies = data;
// console.log($scope.movies);
})
});
There is no need (in this case) to use $q.defer() because $http already returns a promise. So, your service code can be simplified to:
tmdb.service('tmdbService', function($http){
this.getMovies = function(){
return $http.get('https://api.themoviedb.org/3/movie/popular?api_key=jkhkjhkjhkjh');
}
});
Then, if you want to send a parameter you can do this:
tmdb.service('tmdbService', function($http){
this.getMovies = function(movieId){
return $http.get('https://api.themoviedb.org/' + movieId + '/movie/popular?api_key=jkhkjhkjhkjh');
}
});
In your controller, you can now send in the movieId:
tmdb.controller("tmdbController", function($scope, tmdbService){
tmdbService.getMovies(3).then(function(response){
$scope.movies = response.data;
// console.log($scope.movies);
})
});
I usually do this in the following way which I feel is neat and more readable:
tmdb.service('tmdbService', [function($http) {
return { //simple mapping of functions which are declared later
fn1: fn1,
fn2: fn3
}
function f1(param) { //param can be the url in your case
//fn code example
return $http.post(param).success(function(response) {
return response.data;
})
}
function f2(param) {
}
}]
And in your controller, using the service:
tmdb.controller('tmdbController', ['$scope', 'tmdbService', function($scope, tmdbService) {
tmdbService.f1(url).then(function(data) {
//handle the data here
})
}])
There are several ways you can go about achieving this goal. In my opinion there is really no right/wrong way; what is right is absolutely dependent on your need and this may change as you application grows.
Especially for large applications, you can define a module to manage urls and inject this module into your index application.
Another way is to define a service to manage your urls. In this case you also have to inject this service into any other services/controllers etc where you may need it.
The disadvantage to this is that this service is only available to the angular module its defined within or at most must be accessed via that module.
So using the service style here is how it may be implemented.
tmdb.service('urlService', function () {
this.urls = {
url1: 'https://api.themoviedb.org/3/movie/popular?api_key=jkhkjhkjhkjh',
url2: 'anotherUrl'
};
});
tmdb.service('tmdbService', ['$http', '$q', 'urlService', function ($http, $q, urlService) {
var deferred = $q.defer();
$http.get(urlService.url.url1).then(function (data) {
deferred.resolve(data);
});
this.getMovies = function () {
return deferred.promise;
}
}]);
Again there is no absolute right/wrong way; it depends.
I hope you find this helpful.
Cheers!

Angular/Javascript Scope

I'm trying to expose the data obtained from the success method of a promise. In short, I don't know how to grab $scope.storedData. As it is right now, it is undefined.
genericService.js
myApp.factory('genericService', function($http){
return $http.jsonp('http://foo.com/bar.json')
.success(function(data){
return data;
})
.error(function(err){
return err;
});
});
genericController.js
myApp.controller('genericController', ['$scope','genericService',
function($scope, genericService){
genericService.success(function(data){
$scope.storeData(data);
});
$scope.storedData; // Undefined here.
$scope.storeData = function(whatever){
$scope.storedData = whatever;
}
console.log('data stored is: ', $scope.storedData); // Still undefined
}]);
How do I expose $scope.storedData to the scope outside of storeData() or genericService.success()?
Note: I don't want to use $watch. I want to overcome this scope issue fairly un-Angularly... because it should be possible.
There are 2 things I typically do:
I use models that define the expected response and will generally init my controller with an empty model.
I use a variable to track my state.
Here's an example of what my controller might look like:
myApp.controller('genericController', GenericController);
GenericController.$inject = [
'$scope',
'genericService'
];
function GenericController(
$scope,
genericService
) {
$scope.loadData = loadData;
$scope.storeData = storeData;
init();
///////////////////
function init() {
$scope.isLoaded = false;
$scope.storedData = {}; // if you use a model class, a new instance of this works best.
}
function loadData() {
genericService.success(function(data){
$scope.storeData(data);
$scope.isLoaded = true;
});
}
function storeData(whatever) {
$scope.storedData = whatever;
}
}

Categories