I'm trying angularjs for the first time and created a service that I use to make ajax calls to my application API and retrieve a paginated list. I called it "GridService". The service is injected into a controller and everything works great! Until I tried to use the same service twice inside two different controllers on the same page. To my horror, the second instance of the service overwrites the first (doh)
For example; I render two partials as follows :
<div id="location_areas" class="container tab-pane fade" ng-controller="areasController" ng-init="initialise()">
#include('areas._areas')
</div>
<div id="location_people" class="container tab-pane fade" ng-controller="peopleController" ng-init="initialise()">
#include('people._people')
</div>
and I inject the service into a controller as follows and link to the service properties
angular.module('areasController', [])
.controller('areasController', function($scope, $attrs, $http, AreaService, GridService, HelperService) {
$scope.areas = function() { return GridService.getListing() }
$scope.totalPages = function() { return GridService.getTotalPages() }
$scope.currentPage = function() { return GridService.getCurrentPage() }
$scope.columns = function() { return GridService.getColumns() }
Lastly, my abbreviated service is as simple as
angular.module('gridService', [])
.provider('GridService', function($http) {
var columns = {};
var filters = {};
var listing = [];
var totalPages = 0;
var range;
var currentPage = 1;
return {
/**
* Get the requested data (JSON format) from storage then populate the class properties which
* are bound to equivalent properties in the controller.
*
* #return void
*/
list : function(url,pageNumber,data) {
url = url+'?page='+pageNumber;
for (var key in data) {
if( angular.isArray( data[key] ) )
{
angular.forEach( data[key], function( value ) {
url = url+'&'+key+'[]='+value;
});
}
else
{
url = url+'&'+key+'='+data[key];
}
}
$http({ method: 'GET', url: url })
.then(function(response) {
listing = response.data.data;
totalPages = response.data.last_page;
range = response.data.last_page;
currentPage = response.data.current_page;
// Pagination Range
var pages = [];
for(var i=1;i<=response.data.last_page;i++) {
pages.push(i);
}
range = pages;
});
},
Obviously I have boobed (doh). Is it possible to create this scenario or have I misunderstood angularjs architecture?
Please update your question with the relevant code for your service. Services in Angular are by definition Singletons. They should not have any private state. If they do have state, it should be state that is means to be shared between two controllers.
If you are just making $http requests in your service, it should just return the promise to the calling controllers -- not causing any "overlap".
UPDATE
It looks like you a missing a few lines from your service. It looks like it contains columns, filters, etc.
So this is the problem, it is a singleton. What you should do it break it up into two different classes, the network layer that still makes the AJAX call to get the data -- and a factory that returns a new instance of your Grid configuration.
gridFactory.$inject = ['$http'];
function gridFactory($http) {
return function(url, pageNumber, data) {
// put your for (var key in data) logic here...
return new Grid($http);
}
}
function Grid($http) {
var self = this;
$http({ method: 'GET', url: url })
.then(function(response) {
self.listings = data.listings;
self.totalPages = data.total_pages;
...
// put your grid config here following this pattern, attaching each to
// the 'self' property which is the prototype of the constructor.
}
}
So now the factory will return a new instance of the 'Grid' object for every time you call the factory. You need to call the factory like gridFactory(url, pageNumber, data);
Related
I'm trying to pull data from an external JSON file and display it for the user to see. Through various actions, the user would then be able to change the data returned from the JSON file, without writing those changes to the file (in this example, incrementing values by one by clicking on a div). I've created a promise service that successfully pulls the data and displays it. I can even get it so the data can be changed in individual controllers.
This is where I get stuck: I cannot find a way to make any changes to the data in the PromiseService, so changes cannot propagate globally. How do I make it that any change in the promise data at the controller level will be reflected in the PromiseService and, thus, reflected in any data binding in the app? I'm new to promises, so I'm open to a completely different approach.
Plunker
HTML:
<body ng-app="pageApp" ng-controller="pageCtrl" nd-model="items">
{{items}}
<div class="button" ng-controller="buttonCtrl" ng-click="incrementValues()">
Click to increment:
<br>{{items}}
</div>
</body>
PromiseService:
pageApp.factory('PromiseService', function($http) {
var getPromise = function() {
return $http.get('items.json').then(function(response) {
return response.data;
});
};
return {
getPromise: getPromise
};
});
Button Controller (Page Controller in Plunker):
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.incrementValues = function()
{
PromiseService.getPromise().then(function(data) {
$scope.items = data;
for(var i = 0; i < data.items.length; i++)
{
data.items[i]['value']++;
}
}).catch(function() {
});
};
});
The incrementValues function works successfully the first time, but each consecutive click re-pulls the promise and resets the data. To sum up: how do I reflect the incremented values in the PromiseService, as opposed to local variables?
You could add to your factory a private property where you store the items. Then create 3 different methods to update and access to that property.
pageApp.factory('PromiseService', function($http) {
var items = {}; // [] in case it is an array
var updateData = function(updatedData){
items = updatedData;
}
var getUpdateData = function(){
return items;
}
var getPromise = function() {
return $http.get('items.json').then(function(response) {
items = response.data;
return response.data;
});
};
return {
getPromise: getPromise,
updateData : updateData,
getUpdateData : getUpdateData
};
});
pageApp.controller('buttonCtrl', function($scope, PromiseService) {
$scope.items = [];
//You should call this method to retrieve the data from the json file
$scope.getData = function(){
PromiseService.getPromise().then(function(data) {
$scope.items = data;
}).catch(function() {
});
}
$scope.incrementValues = function(){
for(var i = 0; i < $scope.items.length; i++){
$scope.items[i]['value']++;
}
PromiseService.updateData($scope.items); //This could be skipped in case you do not want to 'store' these changes.
};
});
Then in others controller you could use the same service to retrieve the updated Data like this:
$scope.items = PromiService.PromiseService();
In the future you could also create a new method to update the json itself instead of stored internally
Your function creates a new $http call every time it's called, and thus returns a new promise, encspsulating new data, every time it's called.
You need to return the same promise every time:
var thePromise = $http.get('items.json').then(function(response) {
return response.data;
});
var getPromise = function() {
return thePromise;
};
I have a simple factory in AngularJS:
(function(){
'use strict';
angular
.module('myModule', [])
.factory('myService', service);
function service(){
var products= function(p1, p2, p3, ..., pn) {
var url = "http://something.url/api/action";
var data = {
'p1': p1,
'p2': p2,
...
'pn': pn,
}
// return data
return $http
.post(url, data)
.then(function (response) {
return response.data;
});
}
return {
Products : products
};
}
})();
I use this service inside a controller like this:
myInjectedService
.Products(vm.p1, vm.p1, ... , vm.pn)
.then(successCallbackFn)
.catch(failureCallbackFn);
Each parameter (p1, ..., pn) are used to filter the final result. This works like a charm! But with a little drawback: there are to many accepted arguments for Products and is really difficult to know if I'm sending the right parameters and this sounds a little error prone. What I would is a fluent API for service that make everything more human readable, this would be great:
myInjectedService
.Products()
.FilterById(p1)
.WhereCategoryIs(p2)
...
.WhereSomethingElseIs(pn)
.Send()
.then(successCallbackFn)
.catch(failureCallbackFn);
Previously the task of HTTP call was handled by Products call. Right now Products(), only make an empty query (i.e. {}). Each subsequent FilterByX will enrich the query (i.e. {'productId': 'xxx-yyy-1111'}). Calling Send() will make the real HTTP POST call. This call will use the data builded through various filter applied. How can I do that? I'm playing with prototype but without success.
You can archieve what you want by define a new class and use prototype like this.
In a fluent method, remember to return the object itself.
function service(){
var products = function(url) {
// Define a new Product class
var Products = function() {
this.url = url;
this.data = {};
};
// Add the function
Products.prototype.FilterById = function(id) {
this.data.id = id;
// To make it fluent, return the object itself
return this;
};
Products.prototype.FilterByCategory = function(category) {
this.data.category = category;
return this;
};
Products.prototype.send = function() {
console.log(this.data);
};
// Return an instance of the Products class
return new Products();
};
return {
Products : products
};
};
service().Products().FilterById(1).FilterByCategory("Item").send();
You can read more about it here: https://www.sitepoint.com/javascript-like-boss-understanding-fluent-apis/
On the code below, I am trying to reference the photos array in the appService service from the pictureCtrl controller, but it won't work well. The only thing that works is $scope.photoService referencing appService when I try to call the model on the HTML. When I try to call currentPhoto model on the HTML it won't show anything.
I would like to get the currentPhoto model on the HTML page referencing the first index of the photos array which will be an object and when I click on the favorite button on HTML it will run the sendFeedback method and will change currentphoto. How can do it?
.controller('pictureCtrl', function ($scope, appService) {
// angular.extend($scope, appService);
$scope.photoService = appService;
$scope.photos = appService.photos;
$scope.currentPhoto = appService.photos[0];
$scope.sendFeedback = function (bool) {
if(bool) {}//add it to favorite
var randomIndex = Math.round(Math.random() * flickrData.photos.length-1);
$scope.currentPhoto = angular.copy($scope.service.flickrData.photos[randomIndex]);
};
})
.factory('appService', function ($http) {
var photoService = {};
photoService.photos = [];
photoService.feelingText = "";
photoService.getFlickrPictures = function (text) {
console.log('this has been called');
return $http({
method: "JSONP",
url: apiUrl+'&text='+text+'&format=json&jsoncallback=JSON_CALLBACK'
}).then(function (data) {
photoService.photos = data.data.photos.photo;
console.log('flickrData', photoService.photos);
}, function(err){
throw err;
});
};
return photoService;
}
It seems that the photos array never has been populated. Since you are using a factory, the thing that will get injected into your controller will be a fresh instance of photoService.
You will only see photos if you first call your function to fetch the data from the $http service.
I would create a function loadPhotos on your service that would return the data as a promise to your controller.
I have a situation where i am getting data on scroll from a service. Now i need to filter data using popular data and latest post
Here is my service:
App.factory('Serviec', function ($http, $rootScope) {
var Hututoo = function () {
this.items = [];
this.busy = false;
this.after = 'Serviec_0';
};
Serviec.prototype.nextPage = function () {
if (this.busy) return;
this.busy = true;
// return undefined
console.log($rootScope.listtype);
$http.get(baseurl + 'ajax/gethome?after=' + this.after).success(function (data) {
var items = data;
for (var i = 0; i < 5; i++) {
this.items.push(items[i]);
// debugger;
}
this.after = "Hututoo_" + this.items.length;
this.busy = false;
}.bind(this));
};
return Serviec;
});
In controller:
$scope.data= new Serviec();
$scope.listtype= 'latest';
$scope.changelist = function(str){
$rootScope.listtype = str;
$scope.data.items=[];
$scope.data.after = 'Serviec_0';
$http.post(baseurl+"ajax/gethome","after="+$scope.hututoo.after+"&list="+str).success(function(data){
$scope.data.items = data;
});
}
Html
<li ng-click="expression = 'latest';changelist('latest');" ng-class="{latest_icon:expression == 'latest'}">Latest Hoot</li>
<li ng-click="expression = 'popular';changelist('popular');" ng-class="{popular_icon:expression == 'popular'}">Popular Hoots</li>
So with these click i need to order data .I have to make http call to get data according to user click.
I was thinking that i can make a scope data that define listing type and get it in factory.
How can i inject this scope in Serviec Factory. I have tried it using rootscope. initally list type is set to latest , but it shows undefined. So what would be the best method achieve this?
Update:
Now i can access scope data in angular service, but small issue comes here is on list click previous item's in scope doesn't get empty and new items get pushed into the scope.
So demand is on list click previous data become zero and new get pushed into the scope.
Pass the $rootScope to controller and set the listtype as required.
App.controller('MainCtrl', function($scope, $rootScope, Hututoo) {
$scope.hututoo = new Hututoo();
$scope.listtype= 'latest';
$scope.changelist = function(str){
$rootScope.listtype= str;
$scope.hututoo = new Hututoo();
$scope.hututoo.nextPage();
}
});
Plunker
Avoid using $rootScope -- it's bad practice, much like using the head object in pure JS. You're already able to share data between the factory and controller, so why not just make listtype a property of the factory:
var Hututoo = function () {
...
this.listtype = 'latest';
};
and use it in your controller as you are other properties:
$scope.changelist = function(str){
$scope.hututoo.listtype = str;
...
};
Demo <-- ajax requests don't work for obvious reasons
$scope is not available to inject in services however you can pass it using parameters like so.
app.factory('Hututoo', function ($resource) {
var somePrivateVar = [];
return {
set: function(scopeVar){
somePrivateVar.push(scopeVar);
},
get: function(){
return somePrivateVar;
}
}
});
then in controller
Hututoo.set($scope.anyVar);
How can I use the totalResults outside of the function that Im setting it? I just cant wrap my head around how to do it, I need to use the totalResults that I gather from my database and use in another function to calculate the amount of pages. I do this so I dont load all the data to the client but I still need to know the total count of rows in the database table.
My json looks like:
Object {total: 778, animals: Array[20]}
Angular:
var app = angular.module('app', []);
app.controller('AnimalController', ['$scope', 'animalSrc', function($scope, animalSrc)
{
$scope.animals = [];
var skip = 0;
var take = 20;
var totalResults = null;
//$scope.totalResults = null;
$scope.list = function()
{
animalSrc.getAll(skip, take, function(data) {
$scope.animals = $scope.animals.concat(data.animals);
// I need to be able to use this outside of function ($scope.list)
totalResults = data.total;
//$scope.totalResults = data.total;
});
};
$scope.showMore = function()
{
skip += 20;
$scope.list();
};
$scope.hasMore = function()
{
//
};
// Outputs null, should be the total rows from the $http request
console.log(totalResults);
}]);
app.factory('animalSrc', ['$http', function($http)
{
// Private //
return {
getAll: function(skip, take, callback)
{
$http({
method: 'GET',
url: 'url' + skip + '/' + take
}).
success(function(data) {
callback(data);
}).
error(function(data) {
console.log('error: ' + data);
});
}
};
}]);
You need to start thinking asynchronously. Your console.log is called before the $http has returned and totalResults has been set. Therefore, totalResults will always be null.
You need to find some way to delay the call to console.log so that the $http call can finish before you run console.log. One way to do this would be to put the console.log call inside your callback function so that it is definitely called after $http's success.
A more elegant way to do this is to use promises. angular.js implements $q, which is similar to Q, a promise library.
http://docs.angularjs.org/api/ng.$q
Instead of creating a callback function in getAll, you return a promise. Inside $http success, you resolve the promise with the data. Then, in your controller, you have a function that is called when the promise is resolved. Promises are nice because they can be passed around and they allow you to control the flow of your asynchronous code without blocking.
Here's a boilerplate I was just working on for myself for similar setup where data is an object that needs to be split into more than one scope item. Issue you weren't grasping is storing the data within the service, not just using service to retrieve data. Then the data items are available across multple controllers and directives by injecting service
app.run(function(MyDataService){
MyDataService.init();
})
app.factory('MyDataService',function($http,$q){
var myData = {
deferreds:{},
mainDataSchema:['count','items'],
init:function(){
angular.forEach(myData.mainDataSchema,function(val,idx){
/* create deferreds and promises*/
myData.deferreds[val]=$q.defer();
myData[val]= myData.deferreds[val].promise
});
/* load the data*/
myData.loadData();
},
loadData:function(){
$http.get('data.json').success(function(response){
/* create resolves for promises*/
angular.forEach(myData.mainDataSchema,function(val,idx){
myData.deferreds[val].resolve(response[val]);
});
/* TODO -create rejects*/
})
}
}
return myData;
})
app.controller('Ctrl_1', function($scope,MyDataService ) {
$scope.count = MyDataService.count;
$scope.items =MyDataService.items;
});
app.controller('Ctrl_2', function($scope,MyDataService ) {
$scope.items =MyDataService.items;
$scope.count = MyDataService.count;
});
Plunker demo