On first load, I want an application to retrieve data asynchronously on first load from X number of http requests (X is based on the number of elements in fruits), and update a directive that shows how many items have been retrieved. When all items are retrieved, an event is triggered to cause that directive/dom element is hide itself.
What is the best way to accomplish this with angular? the following is what I think in terms of what the responsibilities of the service, controller, and directive are. Is this right? Or should there be a different/better way to do this?
APP.service('myService', function($http) {
this.fruits = ['Apple', 'Bannana', 'Pear', 'Orange'];
this.processedFruit = [];
});
APP.controller('myController', ['$scope', '$http', 'myService', function($scope,$http) {
$scope.$emit('LOAD');
// Should the following be in the service instead of the controller?
for(var i = 0; i < myService.fruits; i++) {
$http.get('someurl/'+fruits[i]).success(function(response) {
myService.processedFruit.push(response);
// Somehow tell the "statusofloading" directive to update its (Completed $http Requests)?
});
}
// Once all requests are finished $scope.$emit('UNLOAD') somehow to the directive?;
}]);
APP.directive('statusofloading', function() {
return {
scope:true,
restrict:'E',
link: function($scope,e,a) {
//Completed $http Requests
$scope.completeRequests = 0;
// Total $http Requests
$scope.totalRequests = // Get the number from the service somehow from (length of this.fruits);
$scope.$on('LOAD', function(){$scope.loading=true});
$scope.$on('UNLOAD', function(){$scope.loading=false});
},
replace: true,
template:"<h1>({{completedRequests}}) / ({{totalRequests}}) </h1>"
}
})
$emit takes a second parameter where you can pass whatever you want.
$scope.$emit('LOAD',{totalRequests: myService.fruits});
Then in your directive:
$scope.$on('LOAD', function(args){
$scope.loading=true;
$scope.totalRequests = args.totalRequests;
});
Then you tell the directive when a request is completed and it knows when all are done and it needs to hide itself. Make sure to notify the directive of failed requests as well as successful ones.
Also take a look at angular-busy for another more generic approach to the problem.
Related
I'm coding a site for my friend's band that uses Angular as well as the SoundCloud API and I have not been able to get past this one problem.
In my first controller I was able to update the view with expressions populated by the soundcloud users JSON with a simple $http.get.
In my second controller, I wanted to grab each track + the track stats and put them in their own html paragraph with ng-repeat. However, when I do this, ng-repeat loops the appropriate amount of times (45X) yet only 3 of the created elements are populated with the track info. There are about 40 blank elements then the first three songs displayed as they should be followed by another blank section.
Here is my code:
(function(){
angular.module("ninety", [])
.controller("bandInfo", ['$http', function($http){
var ninetystuff = this;
ninetystuff.data = [];
$http.get('https://api.soundcloud.com/users/23749941.json?client_id=b4809581f93dc4d3308994300923b660').success(function(data){
ninetystuff.data = data;
});
}])
.controller("music", ['$http', '$scope', function($http, $scope){
var ninetyshit = this;
ninetyshit.data = [];
$scope.show = false;
SC.initialize({
client_id: "b4809581f93dc4d3308994300923b660"
});
SC.get('/users/23749941/tracks').then(function(tracks){
ninetyshit.data = tracks;
$scope.show = true;
});
$scope.playTrack = function(track) {
SC.oEmbed(track, {
auto_play: true,
maxheight: 200
}).then(function(embed){
$("#player").empty();
$("#player").append(embed.html);
});
};
}])
.directive('scrollToPlayer', function() {
return {
restrict: 'A',
link: function(scope, $elm, attr) {
$elm.on('click', function() {
$('html,body, #bg').animate({scrollTop: 400 }, 1000);
});
}
};
});
})();
I've tried creating a service to handle the promise returned from the 'GET' request but I had the same result.
I finally figured it out, I was parsing through the JSON data the wrong way. I should have provided my html code in the question. I was using ng-repeat through the data[0] array (ie - ng-repeat="index in data[0]") when I simply needed to loop through the returned data itself (ie- ng-repeat="index in data"). Silly mistake, I definitely over complicated things for myself. The fact that the wrong solution I was using displayed some but not all of the tracks made me falsely believe I was looping correctly when I wasn't.
Hope this helps any Angular noobs who are having similar issues. Lesson learned.
I am new to Angularjs. I want to take my custom directive ' attributes and make http call and generate graph using the response.
<lineGraph query="select * from load.webserver[0-9]"></lineGraph> <!-- query is sent over http to db -->
My angularjs code goes like this,
var app = angular.module('app', []);
//Factory to get data from influxDB over http
app.factory('influxFactory', ['$q', function($q) {
.... code for querying db using http
})
//
app.controller('mycontroller', [ '$scope', 'influxFactory', function($scope, influxFactory){
scope.constructGraphData = function(query){
influxFactory.getDataFromDBOverHTTP().then(function(data){
... graph data is constructed here
$scope.graphdata = constructed_data // graph data to be plotted is stored and need to be given to drawFlotChart
})
}
})
app.directive('lineGraph', function () {
function drawFlotChart(graphdata){
... flot code to draw the graph
}
return {
restrict: 'E',
templateUrl: 'templates/linegraph.html',
link: function(scope, elt, attr){
// Take value of "query" in <myLineDirective query="load.we...
scope.constructGraphData(attr.query)
/*** How do I capture the $scope.graphdata here ... its "undefined" since http response takes time. Need help. ***/
}
})
I can't use $watch('graphdata', function()) since 'graphdata' gets modified multiple times while getting constructed and hence there function() gets executed multiple times
I tried with having "scope: {" but no luck.
Can somebody help on this ?
You can use $q in your controller's constructGraphData function. Create a deferred object at the beginning of the function, return its promise and resolve with constructed_data when it's ready. Then, in your directive, call scope.constructGraphData(attr.query).then(...) to use the graphdata.
I wonder if I can call controller method from service.
I know that Service is singleton and I can't inject $scope to the service.
In my case I manage Google Maps in service and want to open modal Dialog when user right clicks on Polygon.
As I know, to open/create new instance of dialog, somehow Service must notify controller to do that.
This is a template with controller + method and service: Template
var myApp = angular.module('myApp', []);
function MyCtrl($scope, gridService, $timeout) {
// how to call "foo" method from service?
$scope.foo = function(){
alert('called from service');
};
}
myApp.service('gridService', ['$timeout', function ( $timeout) {
var grid = {
fetching: false,
pristine: true,
pageType: 'Edit'
}
return {
gridSetup: function () {
return grid;
},
setGridSetup: function (newGrid) {
}
}
}]);
Thanks,
The answer is simple: you don't.
The service exists to manipulate data, nothing else. And it really shouldn't care "why" it's doing what it's doing. You ask the service to do something and wait for a response.
Personally I prefer using promises to resolve async operations (i.e. to notify the controller about a state change) since it's heavily supported by many angular services like $http.
But feel free to use callbacks of you wish.
Usually you do not need to call controller from the service - in general the single service could be used by any controller so service shall know nothing about them. In most cases controller calls to the service in react to some user's action and then you need to update view from controller somehow when service did its work (get response from server, etc.). I see following general ways how to do it.
1. Use callbacks.
//controller
$scope.onButtonClick = function() {
$scope.label = "wait, loading...";
function onSuccess(result) {
$scope.label = "done! result is " + result;
}
myService.doSomeWork(param1, param2, onSuccess);
}
//service
doSomeWork: function(param1, param2, onSuccess) {
$.get({...}, onSuccess);
}
So you provide a callback for each action.
2. Subscribe on events
You may use jQuery for events subscribing/triggering
//controller
$(myService).on('update', function() {
$scope.lastUpdateTime = new Date();
});
$scope.onButtonClick = function() {
myService.doUpdate();
}
//service
doUpdate: function() {
$.get({...}, function onOk() {
$(this).trigger('update');
});
}
3. Use promises
A lot of built-in angular services return promise objects, you may use them too:
//controller
myService.doSomething(param1).then(function(result) {
$scope.result = result;
});
//service
doSomething: function(param1) {
return $http.get({...});
}
4. Share some data
An example is $resource service - for example when you call query method it returns empty array-like object that could be safely put to scope and then fills it with values when http request is done.
//controller
$scope.options = myService.options;
$scope.onClick = function() { myService.update() }
//service
options: [],
update: function() {
var self = this;
$http.get({...}).success(function(res) {
self.options.splice(0, self.options.length); //to keep same array
self.options.push.apply(self.options, res.data.options);
});
}
In all these cases services and controllers are separated, services could be used with any controller and you may easily write unit-tests on services that will not break if you change your controller/view part somehow.
A possible solution would be to have a dialog service which you can inject into the grid service. So when the user right clicks on the polygon the handler would call open on the dialog service.
Take a look at the modal service on angular ui as an example.
I've spent the night on trying to figure this out and have finally decided to give up and ask for help.
I'm building a web-app with AngularJS that is designed to work with flakey connections (mobiles).
I'm trying to implement the functionality for a user to add a object (whether that's an appointment, book, etc is irrelevant) to the server.
Service that handles syncing objects with the server:
angular.module('App')
.service('syncUp', function syncUp($http, $q, app) {
this.addObject = function addObject(object) {
var deferred = $q.defer();
app.inSync = false;
var httpConfig = {
method: 'POST',
url: 'http://myurl.dev/app_dev.php/api/add-object',
data: object
}
function persist() { setTimeout(function() {
$http(httpConfig).
success(function(data, status) {
app.inSync = true;
deferred.resolve(data.id);
}).
error(function(data, status) {
app.inSync = false;
persist();
});
}, 3000);
};
persist();
return deferred.promise;
}
});
'app' service that the status bar is bound to:
'use strict';
angular.module('App')
.service('app', function app($http, $q) {
this.inSync = true;
});
Template binding to the 'app' service inSync property:
<div class="status" ng-class="{'insync':inSync}"></div>
Specific object service that sends data from the controller to the syncUp service:
this.addBook = function(book)
{
var tempId = syncUp.generateUid();
this.books[tempId] = book;
this.books[tempId].tempId = tempId;
syncUp.addObject({
'type': 'book',
'data': this.books[tempId]
}).then(function(newId) {
booksRef[newId] = book;
delete booksRef[tempId];
}, function() {});
}
Everything is working as it should (data is being persisted to the server and the ID is being returned and replacing the tempId just fine. The problem is, when the inSync key on the 'app' service is updated, the class isn't added/removed from the div as it should be with ng-class in the template. If I load another route, that will force iterate through whatever internal cycle angular is doing and update the class on the template.
I've tried all manner of $apply() solutions, moving where the app.inSync key is set back to true, looping a function watching it. It's being set in all the right places (from debugging I know it's set back to true correctly), I just can't figure out how to make the change appear on the UI.
I tried:
$rootScope.$apply(function() {
app.inSync = true;
});
Which gave me an error (already running a digest, or something).
So I tried the 'safeApply' version that has been circulated on many answers/blogs, which didn't throw the error, but didn't work either.
As far as I can figure out, the UI should be updated when promises are resolved (both the http and my syncUp.addObject promise are resolved, so I'm not sure why it's not working.
Any ideas? I need to keep the current implementation of promises to be able to set the returned ID from the server on the added object, to avoid a circular-dependency issue between the syncUp and object angular services.
Edit:
And the status bar directive:
angular.module('App')
.directive('navigation', function (app) {
return {
templateUrl: '/app/views/navigation.html',
restrict: 'E',
link: function (scope, element, attrs) {
scope.inSync = app.inSync;
}
}
});
References you make in templates refer to objects on the current $scope. Services do usually not create or add anything to the $scope, so putting properties on a service, will not make them available to the template. To get stuff on the $scope, you need to use a controller. You can use the ng-controller directive to reference a controller, you'll find examples of this in the first AngularJS tutorials.
What you should do is create a controller and have it listen for events from the service. Here's an example of how to do that.
That's the nice way; You might also be able to get away with it by putting the inSync = true on the $rootScope as such;
service('syncUp', function syncUp($http, $q, app, $rootScope) {
// (...)
$rootScope.inSync = true;
It looks like you're hoping to see bindings operating between a service ('app') and a template. It's hard to tell if we're not seeing the entire picture. Going on that assumption, you need to refactor so that you are setting up bindings on a controller.
I would expect the controller setup to look something like this:
angular.module('App')
.controller('app', function app($http, $q, $scope) {
$scope.inSync = true;
});
Now you will have two-way binding hooked-up on the 'inSync' property.
Otherwise, your template looks fine.
If I'm off base, please update your question with more context, or better yet make a fiddle to boil down the problem.
How do I make it such that all the data "MyService" needs to retrieve is retrieved before my 'control' directive is created or some function to actually add those items is called? If not, is there some other recommended way involving controllers, etc. Have yet to see a basic example of similar sequence. Note that data may have more values added to it via other functions, in which a method would have to be called explicitly for those new data elements.
Service:
// var app = angular.module...
app.service('MyService', function($http) {
this.data = [];
// Called once to first initalize the data
$http.get('data.json').then(function data(response) {
this.data.push(response);
}
})
Directive (with its own isolate controller? or point to the main app controller)?:
myDirective.directive('control', ['Params', 'MyService', function(Params, MyService) {
// Call on a method "addData(MyService.data) when the data is actually loaded
}]);
I don't think there is any way to stop directive form initiating and wait for a async function call of a service. However, several alternatives here:
1. Use ng-route and set up a resolve with $routeProvider.
API: http://docs.angularjs.org/api/ngRoute.$routeProvider
Basically, you return a promise in a route's resolver, the route's view will wait for that promise to resolve. After resolve, it will load the view and initiate whatever controller or directive within the view.
2. $watch the data property of MyService.
When data changes, do whatever you want
$scope.$watch(function() {
return MyService.data;
}, function(newVal, oldVal) {
// do something
}, true);
3. Trigger a ready event on MyService, listen to MyService in your directive.
This requires you to include some event library such as 'EventEmitter`, and mix it into MyService.
4. $broadcast ready event on $rootScope from MyService, and listen to it in your directive.
app.service('MyService', function($http, $rootScope) {
this.data = [];
var _this = this;
// Called once to first initalize the data
$http.get('data.json').then(function data(response) {
_this.data.push(response);
$rootScope.$broadcast('MyServiceReady');
}
})
myDirective.directive('control', ['Params', 'MyService', function(Params, MyService) {
return function(scope) {
scope.$on('MyServiceReady', function() {
// do something
});
};
}]);