I am working on a multiple file upload module and am stuck when it comes to communicating from my service to my controller without using $rootScope.
A directive watches the file input and hands the files onchange to a service, where they get uploaded and the upload progress monitored. Based on the response (from success, error and progress changes), the parent controller should show thumbs and progress.
I do not want to use $emit and $on on my $rootScope, since I need the pattern quiet often and only that one parent controller needs to know about the uploads.
I created a simplified(!) Plunkr with some additional information to better understand the problem.
Is there another way for my controller to react to changes (happening inside the service factory)?
Or maybe a completely different way of achieving such?
A proper way to handle async operations with Angular is using promises.
You could return a promise from the service call and resolve it to the src of the thumb. You could then not use a directive at all.
Your controller would use the service this way:
function ParentController($scope, imageService) {
$scope.change = function() {
$scope.src = imageService.change();
$scope.then(function(result) {
// the loading ended, but there is no need to set the src
// as it will already bind to src when ends.
}, function(err) {
// something went wrong
});
};
}
Your service:
app.factory('imageService', function($q, $rootScope) {
function doTheUploading(defer) {
if (everythingOk) {
defer.resolve(thumbSrcString);
} else {
defer.reject({something: 'an error ocurred'});
}
}
return {
change: function() {
// creates the promise
var defer = $q.defer();
doSomethingAsync(defer);
return defer.promise;
}
};
});
At least, your HTML should look like:
<div ng-controller="ParentController">
<img src="{{ src }}"><br><br>
<span class="btn" ng-click="change()">Change!</span>
</div>
Regarding the progress, you will need to go with callbacks or returning an object that would bring the promise and a progress indicator (i.e: return {promise: defer.promise, percent: 0}). The progress indicator should be then updated from inside the service.
You could also chain your promise if you need any transformation in the URL returned from the server before sets it to the src property, i.e.:
$scope.src = imageService.change().then(function(result) {
return 'http://dummyimage.com/' + result;
});
Updated your Plunker.
Related
I have the same issue with this post Pass Angular scope variable to Javascript . But I can't achive my solution with their answers.
My Angular Controller
angular.module('App').controller('HomeController', [
'$rootScope', '$scope', '$state', '$timeout', 'ReportService', 'MsgService',
function($rootScope, $scope, $state, $timeout, ReportService, MsgService) {
$scope.$on('$viewContentLoaded', function() {
console.log('HomeController');
$scope.get_locations();
});
// get locations
$scope.get_locations = function() {
var data = {};
// call http get to my api
MsgService.get_all_locations(data, function(response) {
if (response.code == 1) { // success
$scope.locations_array = response.data; // data that I want to access to script
} else {
alert(response.message);
}
});
}
}
]);
My Html
<div id="map" ng-controller="HomeController">{{locations_array}}</div> // {{locations_array}} scope have the result that I want
<script type="text/javascript">
$(document).ready(function() {
var data = $('[ng-controller="HomeController"]').scope().$parent.locations_array;
console.log(data); // underfined
//var $element = $('#map');
// var scope = angular.element($element).scope();
// console.dir(scope.$parent.locations_array); // underfined
});
</script>
I tried access from browser develop tool then It can access scope. But My code can't access this.
How to solve this?
The immediate problem here is a timing issue - you are trying to read the locations_array value off the scope long before the value is populated.
The sequence of events is something like this:
ready event for document triggers, and before Angular has even thought about starting, your inline JS code runs, trying to read the value from the scope, which doesn't exist yet.
Angular bootstraps your Angular application in response to the document's ready event (this may be before #1, depending on the order of scripts on the page). This will call the HomeController constructor, that only sets up a listener for the $viewContentLoaded event.
The $viewContentLoaded event gets broadcast, and you initiate an asynchronous request for the locations.
When that returns with the locations some time later, it populates them on the scope.
Don't rely on .scope()
In addition to the timing issues, there is another major problem with your solution - it relies on the debug information being included by AngularJS. Obviously, it is by default, but it is possible to disable this debug information for significant performance gains in production.
If someone else comes along, possibly after you have left, and tries to disable debug information to improve performance or for some other reason (it is a recommended practice in production), it will stop .scope() from working.
So by relying on .scope(), you are making it so that disabling debug info, a best practice and performance booster, is not possible now or in the future for your app, because it will break things. And it won't be at all obvious to that developer that it would break anything.
So relying on .scope() for anything other than debugging should always be a very last resort.
So what do I do instead?
Like I mentioned, this is a timing problem - you need to wait until the locations are eventually loaded before running code that relies on them.
Luckily, we have many options in JS to deal with asynchronous values - callbacks, promises, RxJS observables, etc. Pick your favourite.
Example: using a global promise
In your controller, create a promise on the global scope (icky, but it needs to be outside Angular somewhere), and resolve that promise with the location data when it is loaded.
var resolveLocations;
window.locationsPromise = new Promise(function (resolve) {
resolveLocations = resolve;
});
angular.module('App').controller('HomeController', [
'$rootScope', '$scope', '$state', '$timeout', 'ReportService', 'MsgService',
function($rootScope, $scope, $state, $timeout, ReportService, MsgService) {
$scope.$on('$viewContentLoaded', function() {
console.log('HomeController');
$scope.get_locations();
});
// get locations
$scope.get_locations = function() {
var data = {};
// call http get to my api
MsgService.get_all_locations(data, function(response) {
if (response.code == 1) { // success
resolveLocations(response.data); // resolve the promise
$scope.locations_array = response.data; // data that I want to access to script
} else {
alert(response.message);
}
});
}
}
]);
Then, your normal (non-angular) javascript (which needs to run after your Angular javascript file is loaded) could use that promise to do something with the data when available:
<script type="text/javascript">
$(document).ready(function() {
window.locationsPromise.then(function (locations_array) {
console.dir(locations_array);
// do something with the data
});
});
</script>
There is probably a better way
Without knowing why you think you need access to this data outside of Angular, it's hard to say for sure, but there are likely other better ways of handling the interplay between Angular code and other Javascript code that depends on it.
Maybe you create a directive to integrate a jQuery plugin, or another service, or whatever, but since AngularJS code is just normal JS, there is no need to think of them as separate from each other. You just have to get the timing right so you have the data available. Good luck!
I'm rather new to angular and I'm trying to integrate np-autocomplete in my application (https://github.com/ng-pros/np-autocomplete). However I can only get it to work when I'm passing a html string as a template inside the $scope.options and it doesn't work when I want to load it from a separate html.
the Code for my app looks as follows:
var eventsApp = angular.module('eventsApp',['ng-pros.directive.autocomplete'])
eventsApp.run(function($templateCache, $http) {
$http.get('test.html', {
cache: $templateCache
});
console.log($templateCache.get('test.html')) // --> returns undefined
setTimeout(function() {
console.log($templateCache.get('test.html')) // --> works fine
}, 1000);
//$templateCache.put('test.html', 'html string') //Would solve my issue in the controller,
//but I would rather prefer to load it from a separate html as I'm trying above
Inside my controller I am setting the options for autocomplete as follows:
controllers.createNewEventController = function ($scope) {
$scope.options = {
url: 'https://api.github.com/search/repositories',
delay: 300,
dataHolder: 'items',
searchParam: 'q',
itemTemplateUrl: 'test.html', // <-- Does not work
};
//other stuff...
}
however, it seems that test.html is undefined by the time np-autocomplete wants to use it (as it is also in first console.log above).
So my intuition tells me that the test.html is probably accessed in the controller before it is loaded in eventsApp.run(...). However I am not sure how to solve that?
Any help would be highly appreciated.
You are most likely correct in your assumption.
The call by $http is asynchronous, but the run block will not wait for it to finish. It will continue to execute and the execution will hit the controller etc before the template has been retrieved and cached.
One solution is to first retrieve all templates that you need then manually bootstrap your application.
Another way that should work is to defer the execution of the np-autocomplete directive until the template has been retrieved.
To prevent np-autocomplete from running too early you can use ng-if:
<div np-autocomplete="options" ng-if="viewModel.isReady"></div>
When the template has been retrieved you can fire an event:
$http.get('test.html', {
cache: $templateCache
}).success(function() {
$rootScope.$broadcast('templateIsReady');
});
In your controller listen for the event and react:
$scope.$on('templateIsReady', function () {
$scope.viewModel.isReady = true;
});
If you want you can stop listening immediately since the event should only fire once anyway:
var stopListening = $scope.$on('templateIsReady', function() {
$scope.viewModel.isReady = true;
stopListening();
});
How can I make an Angular service code "look synchronous"?
My questions arose when I cleaned my controller and put the business logic code into a service instead. So far so good. Now I would like to "wait" in the service function until all asynchronous calls have returned and then return. How can I do that?
To illustrate my problem, suppose you have a controller code which just:
requests some data from the backend
does some processing with the data and
hands the data over to the scope
Like that:
DataController before refactoring:
$scope.submitForm = function() {
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
$scope.data = data;
});
};
Pretty straightforward. Fetch data and fill scope.
After refactoring into controller + service, I ended up with:
DataController refactored:
$scope.submitForm = function() {
DataService.getData().then(function(data) {
$scope.data = data;
});
};
DataService refactored:
this.query = function() {
var dataDefer = $q.defer();
RestBackend.query('something').then(function(data) {
// do some additional things ...
...
dataDefer.resolve(data);
});
return dataDefer.promise;
};
I dislike the fact that I have to work with a promise in the controller also. I like promises but I want to keep the controller agnostic of this "implementation detail" of the service. This is what I would like the controller code to look like:
DataController (as it should be):
$scope.submitForm = function() {
$scope.data = DataService.getData();
};
You get the point? In the controller I don't want to care about promise or not. Just wait for the data to be fetched and then use it. Thus, I am looking for a possibility to implement the service like this:
query the data (asynchronously)
do not return until the data has been fetched
return the fetched data
Now item 2. is not clear to me: How can I "wait until data has been fetched" and only proceed afterwards? The goal is that the service function looks synchronous.
I too think your solution is fine.
Returning a promise is not an implementation detail of the service. It is part of the service's API (the "contract" between the service and the service-consumer).
The controller expects a promise that resolves with the data and handles that as it sees fit.
How that promise is constructed, how the data is fetched etc, these are the implementation details.
You can swap the service at any time with one that does totally different things as long as it fulfills the contract (i.e. returns a promise that resolves with the data onve ready).
That said, if you only use the data in the view (i.e. do not directly manipulate it in the controller right after it is fetched), which seems to be the case, you can use ngResources approach:
Return an empty array and populate it with the data once it is fetched:
$scope.data = DataService.getData();
// DataService refactored:
this.getData = function () {
var data = [];
RestBackend.query('something').then(function(responseData) {
// do some additional things ...
...
angular.forEach(responseData, function (item) {
data.push(item);
});
});
return data;
};
BTW, in your current (fine) setup, you need $q.defer(). You can just use promise-chaining:
this.query = function() {
return RestBackend.query('something').then(function(data) {
// do some additional things ...
...
return data;
});
};
I think what you have is a very good solution. You should not have to wait for promise to be resolved, it defeats the purpose of async javascript. Just ask yourself why do you need to make it run sync?
If you rely in html on promise to be resolve you can do something like this
<div class="alert alert-warning text-center" data-ng-hide="!data.$resolved">
Got data from service.
</div>
As you use ngRoute, I would recommend you to resolve you data in your route config, and the view will be loaded once all your data will be resolved.
$routeProvider
.when('/your-url', {
templateUrl: 'path/to/your/template.html',
controller: 'YourCtrl',
// that's the point !
resolve: {
superAwesomeData: function (DataService) {
return DataService.getData();
}
}
});
Now, superAwesomeData can be injected in your controller and it will contains the data, resolved.
angular.module('youModule')
.controller('YourCtrl', function (superAwesomeData) {
// superAwesomeData === [...];
});
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 have a controller like this:
function MyCtrl($scope) {
$scope.doSomething = function(){
alert("Do something!");
}
}
And I have multiple views which depend on this (ie multiple of the below):
<div ng-controller="MyCtrl">
...
</div>
The problem is, the data the controller depends on needs to be loaded in the background (the controller does not load that data), and a callback (dataIsReady()) will be called after the data is ready.
function dataIsReady(){
// TODO: call the doSomething() function
}
Now, I want to basically call the doSomething() function, which is inside MyCtrl, from the dataIsReady() function. How can I do that?
I think what you need is a data service, which you can then inject into your controller. You can call a function on your data service which will handle the retrieval of the data and return a "promise" which can then be used to trigger your callback function when the data has loaded.
Have a look at the following code which is a slightly modified version from egghead.io:
Plunker Demo (w/ local storage): http://plnkr.co/edit/9w2jTg?p=preview
var myApp = angular.module('myApp', []);
myApp.factory('AvengersService', function ($http) {
var AvengersService = {
getAsyncCast: function () {
// $http returns a promise, which has a then function, which also returns a promise
var promise = $http.get("avengers.json") // or some JSON service
.then(function (response) {
// The 'then' function here is an opportunity to modify the response
// The return value gets picked up by the 'then' in the controller.
return response.data;
});
// Return the promise to the controller
return promise;
}
};
return AvengersService;
});
myApp.controller('AvengersCtrl', function($scope, AvengersService) {
// Call the async method and then do something when the data is retrieved
AvengersService.getAsyncCast()
.then(function (asyncData) {
// Callback logic that depends on the data goes in here
console.info("Cast pulled async.");
$scope.avengers.cast = asyncData;
});
});
Hope that helps.
Notice: This approach in this answer is terribly wrong, one should not access to the scope of a controller outside of angular, or outside of controller at all. This would also be terribly slow if you try to call it several times. Other than that, it is fine. I am giving this answer because it is also the simplest way. I would never use that kind of code in production, though. The appropriate way is to write a service to communicate with the controller.
Given that you have defined $scope.doSomething in MyCtrl:
var scp = angular.element('[ng-controller="MyCtrl"]').scope();
scp.doSomething();
Will call doSomething method defined in the controller.