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

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.

Related

Two Factories. One should know when there is a change in Other

Before i state my question, i may be completely wrong with how i am using factories in my use case. Happy to know better alternatives.
So, I have two Factories.
Factory1 takes care of hitting the server, getting and storing that data.
Factory2 does not make any REST Calls. But stores a set of data that would be useful while making REST calls.
Simple question would be, why not Factory1 store that data instead of Factory2. The motive behind Factory2 is that this data would be used by lot of other factories/controllers. So, to remove redundancy this looks a better option.
For now,
When a REST call is to be made from Factory1, i can simply do Factory2.getData and then use it. But my use-case is bit more complex. I need Factory1 to fire calls whenever there is a change in Factory2.getData(). We would use $scope normally in case of controllers etc. But how to achieve the same thing here ?
Basically, How to do we know in Factory1 when there is a change in Factory2's data ?
Code:
servicesModule.factory('Factory2', function() {
var fObj = {},
filters;
fObj.setFilters = function(fs) {
filters = fs;
};
fObj.getFilters = function() {
return filters;
};
return fObj;
});
servicesModule.factory('Factory1', ['$http', '$q', 'Factory2', function($http, $q, Factory2) {
var fObj = {},
data;
fObj.fetchData = function(params) {
var filters = Factory2.getFilters();
// REST call here and set data to result
}
fObj.getData = function(){
return data;
}
}]);
So, a bit of digging pointed in the direction of $rootScope.
servicesModule.factory('Factory2', ['$rootScope', function($rootScope) {
var fObj = {},
filters;
fObj.setFilters = function(fs) {
filters = fs;
$rootScope.$emit("FiltersUpdated", fs);
};
fObj.getFilters = function() {
return filters;
};
return fObj;
}]);
servicesModule.factory('Factory1', ['$http', '$q', 'Factory2', '$rootScope', function($http, $q, Factory2, $rootScope) {
var fObj = {},
data;
$rootScope.$on('FiltersUpdated', function(event, data) {
fObj.fetchData();
})
fObj.fetchData = function(params) {
var filters = Factory2.getFilters();
// REST call here and set data to result
}
fObj.getData = function(){
return data;
}
}]);
I would still love to hear others' opinions.

Why isn't $scope updated when bound properties of services are updated by other services?

I have one service for handling data, and one for logic.
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
this.stuff = angular.copy(s);
}
});
The data service has a set function and a data property.
app.service('LogicService', function(DataService, $http) {
DataService.setStuff(["apple", "banana"]);
$http.get("./data.json").then(function(res){
DataService.setStuff(res.data.stuff);
});
});
I am assigning a property of the data service to the controller for binding to the DOM.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.stuff = DataService.stuff;
//This is the only way I could get it to work, but isn't this JANKY?
//$scope.$watch(
// function(){
// return DataService.stuff
// },
// function(n,o){
// $scope.stuff = n;
// })
})
If I 'seed' the data service when the logic service is instantiated, and then later update it following an $http call, the DOM reflects the 'seeded' or initial value, but does not update.
Is there something fundamental I am missing in my understanding of the digest loop?
If I add a $watch function in my controller, all is well, but this seems yucky.
//FIXED//
#scott-schwalbe 's method of using Object.asign() works nicely, preserves my original structure, and is one line.
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
Working Plunker
(sorry for titlegore)
If your data property is an object and is binded to the scope, then the scope will update whenever the object changes as long as you don't dereference it (eg data = x). Are you reassigning data object on the $http call?
An alternative to your current code to keep the reference using Object.assign
app.service('DataService', function() {
this.stuff = [false];
this.setStuff = function(s){
Object.assign(this.stuff, s);
}
});
var app = angular.module('plunker', []);
app.controller('MainCtrl', function($scope, DataService) {
$scope.message = "Hello, World!";
//Get stuff data from your service, this way you stuff lives in your service
//And can be accessed everywhere in your app.
//It also makes your controller thin. Which is the top priority
$scope.stuff = DataService.getStuff();
//Or async
DataService.getStuffAsync()
.then(function(val){
$scope.asycStuff = val;
});
this.clickFromAButton = function(){
DataService.setStuff(["apple", "banana"]);
};
});
app.service('DataService', function() {
this.stuff = [false];
this.asyncStuff;
this.setStuff = function(s){
this.stuff = angular.copy(s);
};
this.getStuff = function(){
return this.stuff;
};
this.getStuffAsync = function(){
//If i already fetched the data from $http, get it from the service.
return this.asyncStuff || $http.get("./data.json").then(function(res){
//When i fetch it for the first time I set the data in my service
this.asyncStuff = res.data;
//and I return the data
return res.data;
});
};
});
This is a good 'pattern' to follow ;)
Instead of putting "stuff" on scope. Put your DataService object on scope.
app.controller('MainCtrl', function($scope, DataService, LogicService ) {
$scope.message = "Hello, World!";
$scope.DataService = DataService;
//$scope.stuff = DataService.stuff;
HTML
<body ng-controller="MainCtrl">
{{DataService.stuff}}
</body>
The $interpolate service will automatically places a $watch on DataService.stuff. Thus there is no need to do it inside your controller.
The DEMO on PLNKR.

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;
}
}

How to include/inject functions which use $scope into a controller in angularjs?

I am trying to include a library of functions, held in a factory, into a controller.
Similar to questions like this:
Creating common controller functions
My main controller looks like this:
recipeApp.controller('recipeController', function ($scope, groceryInterface, ...){
$scope.groceryList = [];
// ...etc...
/* trying to retrieve the functions here */
$scope.groceryFunc = groceryInterface; // would call ng-click="groceryFunc.addToList()" in main view
/* Also tried this:
$scope.addToList = groceryInterface.addToList();
$scope.clearList = groceryInterface.clearList();
$scope.add = groceryInterface.add();
$scope.addUp = groceryInterface.addUp(); */
}
Then, in another .js file, I have created the factory groceryInterface. I've injected this factory into the controller above.
Factory
recipeApp.factory('groceryInterface', function(){
var factory = {};
factory.addToList = function(recipe){
$scope.groceryList.push(recipe);
... etc....
}
factory.clearList = function() {
var last = $scope.prevIngredients.pop();
.... etc...
}
factory.add = function() {
$scope.ingredientsList[0].amount = $scope.ingredientsList[0].amount + 5;
}
factory.addUp = function(){
etc...
}
return factory;
});
But in my console I keep getting ReferenceError: $scope is not defined
at Object.factory.addToList, etc. Obviously I'm guessing this has to do with the fact that I'm using $scope in my functions within the factory. How do I resolve this? I notice that in many other examples I've looked at, nobody ever uses $scope within their external factory functions. I've tried injecting $scope as a parameter in my factory, but that plain out did not work. (e.g. recipeApp.factory('groceryInterface', function(){ )
Any help is truly appreciated!
Your factory can't access your $scope, since it's not in the same scope.
Try this instead:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.addToList = groceryInterface.addToList;
$scope.clearList = groceryInterface.clearList;
$scope.add = groceryInterface.add;
$scope.addUp = groceryInterface.addUp;
}
recipeApp.factory('groceryInterface', function () {
var factory = {};
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
factory.clearList = function() {
var last = this.prevIngredients.pop();
}
});
Alternatively, you can try using a more object oriented approach:
recipeApp.controller('recipeController', function ($scope, groceryInterface) {
$scope.groceryFunc = new groceryInterface($scope);
}
recipeApp.factory('groceryInterface', function () {
function Factory ($scope) {
this.$scope = $scope;
}
Factory.prototype.addToList = function (recipe) {
this.$scope.groceryList.push(recipe);
}
Factory.prototype.clearList = function() {
var last = this.$scope.prevIngredients.pop();
}
return Factory;
});
You cannot use $scope in a factory as it is not defined. Instead, in your factory functions change the properties of the object the factory is returning, e.g.
factory.addToList = function (recipe) {
this.groceryList.push(recipe);
}
these will then get passed on to your $scope variable
$scope.addToList = groceryInterface.addToList;
// ... = groceryInterface.addToList(); would assign to `$scope.addToList` what is returned, instead of the function itself.
This isn't the exact answer for this question, but I had a similar issues that I solved by simply passing $scope as an argument to a function in my factory. So it won't be the normal $scope, but $scope at the time the function in the factory is called.
app.controller('AppController', function($scope, AppService) {
$scope.getList = function(){
$scope.url = '/someurl'
// call to service to make rest api call to get data
AppService.getList($scope).then(function(res) {
// do some stuff
});
}
});
app.factory('AppService', function($http, $q){
var AppService = {
getList: function($scope){
return $http.get($scope.url).then(function(res){
return res;
});
},
}
return AppService;
});

AngularJS Load Data from Service

I am having a problem getting data from a service populated into my view. I have a service defined as such
app.factory('nukeService', function($rootScope, $http) {
var nukeService = {};
nukeService.nuke = {};
//Gets the list of nuclear weapons
nukeService.getNukes = function() {
$http.get('nukes/nukes.json')
.success(function(data) {
nukeService.nukes = data;
});
return nukeService.nukes;
};
return nukeService;
});
and my controller
function NavigationCtrl($scope, $http, nukeService){
/*$http.get('nukes/nukes.json').success(function(data) {
$scope.nukes = data;
});*/
$scope.nukes = nukeService.getNukes();
}
If I use the $http.get from the controller the data populates fine, however, if I try to call the data from the service, I get nothing. I understand that the query is asynchronous but I am having a hard time understanding how to populate the $scope variable once the data is returned. I could use $rootscope to broadcast an event and listen for it in the controller but this does not seem like the correct way to accomplish this. I would really appreciate any advice on how to do this the correct way.
I think this should solve your problem
app.factory('nukeService', function($rootScope, $http) {
var nukeService = {};
nukeService.data = {};
//Gets the list of nuclear weapons
nukeService.getNukes = function() {
$http.get('nukes/nukes.json')
.success(function(data) {
nukeService.data.nukes = data;
});
return nukeService.data;
};
return nukeService;
});
function NavigationCtrl($scope, $http, nukeService){
$scope.data = nukeService.getNukes();
//then refer to nukes list as `data.nukes`
}
This is a problem with object reference.
when you calls nukeService.getNukes() you are getting a reference to a object a then your variable $scope.nukes refers that memory location.
After the remote server call when you set nukeService.nukes = data; you are not changing the object a instead you are changing nukeService.nukes from referencing object a to object b. But your $scope.nukes does not know about this reassignment and it still points to object a.
My solution in this case is to pass a object a with property data and then only change the data property instead of changing reference to a
This should be as follows. As mentioned by NickWiggill's comment, undefined will be assigned to nukeService.data if we do not return promise.
app.factory('nukeService', function($rootScope, $http) {
var nukeService = {};
//Gets the list of nuclear weapons
nukeService.getNukes = function() {
return $http.get('nukes/nukes.json');
};
return nukeService;
});
function NavigationCtrl($scope, $http, nukeService){
nukeService.getNukes().then(function(response){
$scope.data = response.data;
});
}
What I do is that I expose the data straight from the service, and have a method which initializes this data. What is wrong with this?
Service:
app.factory('nukeService', function($scope, $http) {
var data = {};
data.nukes = [];
//Gets the list of nuclear weapons
var getNukes = function() {
$http.get('nukes/nukes.json').success(function(data) {
data.nukes = data;
});
};
// Fill the list with actual nukes, async why not.
getNukes();
return {
data : data
// expose more functions or data if you want
};
});
Controller:
function NavigationCtrl($scope, nukeService){
$scope.data = nukeService.data;
//then refer to nukes list as `$scope.data.nukes`
}

Categories