I am trying to pass data from directive to controller via service, my service looks like this:
angular
.module('App')
.factory('WizardDataService', WizardDataService);
WizardDataService.$inject = [];
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
wizardFormData = newFormData;
};
var getWizardData = function () {
return wizardFormData;
};
var resetWizardData = function () {
//To be called when the data stored needs to be discarded
wizardFormData = {};
};
return {
setWizardData: setWizardData,
getWizardData: getWizardData,
resetWizardData: resetWizardData
};
}
But when I try to get data from controller it is not resolved (I think it waits digest loop to finish), So I have to use $timeout function in my controller to wait until it is finished, like this:
$timeout(function(){
//any code in here will automatically have an apply run afterwards
vm.getStoredData = WizardDataService.getWizardData();
$scope.$watchCollection(function () {
console.log("getStoredData callback: " + JSON.stringify(vm.getStoredData));
return vm.getStoredData;
}, function () {
});
}, 300);
Despite of the fact that it works, what I am interested in is, if there is a better way to do this, also if this is bug free and the main question, why we use 300 delay and not 100 (for example) for $timeout and if it always will work (maybe for someone it took more time than 300 to get data from the service).
You can return a promise from your service get method. Then in your controller, you can provide a success method to assign the results. Your service would look like this:
function getWizardData() {
var deferred = $q.defer();
$http.get("/myserver/getWizardData")
.then(function (results) {
deferred.resolve(results.data);
}),
function () {
deferred.reject();
}
return deferred.promise;
}
And in your ng-controller you call your service:
wizardService.getWizardData()
.then(function (results) {
$scope.myData = results;
},
function () { });
No timeouts necessary. If your server is RESTFULL, then use $resource and bind directly.
Use angular.copy to replace the data without changing the object reference.
function WizardDataService() {
var wizardFormData = {};
var setWizardData = function (newFormData) {
console.log("wizardFormData: " + JSON.stringify(wizardFormData));
angular.copy(newFormData, wizardFormData);
};
From the Docs:
angular.copy
Creates a deep copy of source, which should be an object or an array.
If a destination is provided, all of its elements (for arrays) or properties (for objects) are deleted and then all elements/properties from the source are copied to it.
Usage
angular.copy(source, [destination]);
-- AngularJS angular.copy API Reference
This way the object reference remains the same and any clients that have that reference will get updated. There is no need to fetch a new object reference on every update.
Related
I have a service that is making an AJAX request to the backend
Service:
function GetCompaniesService(options)
{
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url);
}
Controller:
var CompaniesOb = new GetCompanies();
CompaniesOb.CompaniesPromise.then(function(data){
$scope.Companies = data;
});
I want my service to handle the ".then" function instead of having to handle it in my controller, and I want to be able to have my controller act on that data FROM the service, after the promise inside the service has been resolved.
Basically, I want to be able to access the data like so:
var CompaniesOb = new GetCompanies();
$scope.Companies = CompaniesOb.Companies;
With the resolution of the promise being handled inside of the service itself.
Is this possible? Or is the only way that I can access that promise's resolution is from outside the service?
If all you want is to handle the response of $http in the service itself, you can add a then function to the service where you do more processing then return from that then function, like this:
function GetCompaniesService(options) {
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url).then(function(response) {
/* handle response then */
return response
})
}
But you'll still have use a promise in the controller, but what you get back will have already been handled in the service.
var CompaniesOb = new GetCompanies();
CompaniesOb.CompaniesPromise.then(function(dataAlreadyHandledInService) {
$scope.Companies = dataAlreadyHandledInService;
});
There is no problem to achieve that!
The main thing you have to keep in mind is that you have to keep the same object reference (and in javascript arrays are objects) in your service.
here is our simple HTML:
<div ng-controller = "companiesCtrl">
<ul ng-repeat="company in companies">
<li>{{company}}</li>
</ul>
</div>
Here is our service implementation:
serviceDataCaching.service('companiesSrv', ['$timeout', function($timeout){
var self = this;
var httpResult = [
'company 1',
'company 2',
'company 3'
];
this.companies = ['preloaded company'];
this.getCompanies = function() {
// we simulate an async operation
return $timeout(function(){
// keep the array object reference!!
self.companies.splice(0, self.companies.length);
// if you use the following code:
// self.companies = [];
// the controller will loose the reference to the array object as we are creating an new one
// as a result it will no longer get the changes made here!
for(var i=0; i< httpResult.length; i++){
self.companies.push(httpResult[i]);
}
return self.companies;
}, 3000);
}}]);
And finally the controller as you wanted it:
serviceDataCaching.controller('companiesCtrl', function ($scope, companiesSrv) {
$scope.companies = companiesSrv.companies;
companiesSrv.getCompanies();
});
Explanations
As said above, the trick is to keep the reference between the service and the controller. Once you respect this, you can totally bind your controller scope on a public property of your service.
Here a fiddle that wraps it up.
In the comments of the code you can try uncomment the piece that does not work and you will see how the controller is loosing the reference. In fact the controller will keep having a reference to the old array while the service will change the new one.
One last important thing: keep in mind that the $timeout is triggering a $apply() on the rootSCope. This is why our controller scope is refreshing 'alone'. Without it, and if you try to replace it with a normal setTimeout() you will see that the controller is not updating the company list.
To work around this you can:
don't do anything if your data is fetched with $http as it calls a $apply on success
wrap you result in a $timeout(..., 0);
inject $rootSCope in the service and call $apply() on it when the asynchronous operation is done
in the controller add a $scope.$apply() on the getCompanies() promise success
Hope this helps!
You can pass the $scope into GetCompanies and set $scope.Companies to the data in the service
function GetCompaniesService(options,scope)
{
this.url = '/company';
this.Companies = undefined;
this.CompaniesPromise = $http.get(this.url).then(function(res) {
scope.Companies = res;
});
}
You have to be careful about the order in which you then use the data. That's kind of the reason behind a promise to begin with.
I have 2 services and would like to update a variable in the 1st service from the 2nd service.
In a controller, I am setting a scope variable to the getter of the 1st service.
The problem is, the view attached to the controller doesn't update when the service variable changes UNLESS I use angular.extend/copy. It seems like I should just be able to set selectedBuilding below without having to use extend/copy. Am I doing something wrong, or is this how you have to do it?
controller
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
$scope.building = BuildingsService.getSelectedBuilding();
});
service 1
app.factory('BuildingsService', function() {
var buildingsList = [];
var selectedBuilding = {};
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
angular.extend(selectedBuilding, _.find(
buildingsList, {'building_id': buildingId})
);
};
var getSelectedBuilding = function() {
return selectedBuilding;
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
service 2
app.factory('AnotherService', function(BuildingsService) {
...
// something happens, gives me a building id
BuildingsService.setSelectedBuilding(building_id);
...
});
Thanks in advance!
When you execute this code:
$scope.building = BuildingsService.getSelectedBuilding();
$scope.building is copied a reference to the same object in memory as your service's selectedBuilding. When you assign another object to selectedBuilding, the $scope.building still references to the old object. That's why the view is not updated and you have to use angular.copy/extend.
You could try the following solution to avoid this problem if you need to assign new objects to your selectedBuilding:
app.factory('BuildingsService', function() {
var buildingsList = [];
var building = { //create another object to **hang** the reference
selectedBuilding : {}
}
// buildingsList populated up here
...
var setSelectedBuilding = function(buildingId) {
//just assign a new object to building.selectedBuilding
};
var getSelectedBuilding = function() {
return building; //return the building instead of selectedBuilding
};
...
return {
setSelectedBuilding: setSelectedBuilding,
getSelectedBuilding: getSelectedBuilding
}
});
With this solution, you have to update your views to replace $scope.building bindings to $scope.building.selectedBuilding.
In my opinion, I will stick to angular.copy/extend to avoid this unnecessary complexity.
I dont believe you need an extend in your service. You should be able to watch the service directly and respond to the changes:
app.controller('SelectedBuildingCtrl', function($scope, BuildingsService) {
// first function is evaluated on every $digest cycle
$scope.$watch(function(scope){
return BuildingsService.getSelectedBuilding();
// second function is a callback that provides the changes
}, function(newVal, oldVal, scope) {
scope.building = newVal;
}
});
More on $watch: https://code.angularjs.org/1.2.16/docs/api/ng/type/$rootScope.Scope
Using AngularJs, I wrote a factory aiming to handle a WebSocket connection.
Here's its code:
.factory('WebSocketConnection', function () {
var service = {};
service.callbacks = [];
service.connect = function() {
if(service.ws)
return;
var ws = new WebSocket("ws://localhost:9000/ws");
ws.onmessage = function (message) {
angular.forEach(service.callbacks, function(callback){
callback(message);
});
};
service.ws = ws;
};
service.send = function(message) {
service.ws.send(message);
};
service.subscribe = function(callback) {
service.callbacks.push(callback);
};
return service;
});
Basically, it allows each Angular components like controller or factory/service to register a specific callback, in order to handle messages; hence the callbacks array.
Here's the interesting excerpt of a listening controller:
WebSocketConnection.subscribe(function (message) {
$scope.$apply(function () {
var data = angular.fromJson(message.data);
$scope.notifications.push(data);
});
});
So the callbacks array would contain this function.
But...what if I won't need this controller any more at some time? (for instance when I navigate to other page based on another controller)
I would be forced to delete the callback item (this function) from the array each time usage of the controller is left, to avoid useless, maybe conflicting, process of this callback as long as any further messages are handled.
Not handy...
I thought about a way to broadcast event from the $rootScope from the factory, so that a specific controller doesn't have to manage his listeners/subscriptions itself.
But I don't want to involve all the scope tree, including all scopes that are not concerned.
What would be a good practice?
Note: I need to achieve a relation 1-N where 1 is the WebSocket handler (factory) and N, any parallel alive Angular components, each needing to listen to messages.
I would suggest maintaining your model in a service or factory object. This would enable you to interact with the data as it exists there and not be dependent on the state of your application (what controllers exist) when a message is recieved. It can also allow you to enforce the concept of:
$scope has model instead of $scope as model
That might look something like this:
ws.onmessage = function(event) {
$rootScope.$apply(function(){
Service.notifications.push(event.data);
}
}
and
angular.module('MyApp').controller('MyCtrl', ['$scope', 'Service',
function($scope, Service) {
$scope.notifications = Service.notifications; //references are ===
}])
To enable flexibility you can use data contained in the message to determine what injectable/methods need to be updated, then use the $injector.
Thanks to #calebboyd who made remind me the existence of the $scope's destroy event, I think I have found a good way to achieve my requirement.
A semi-automatic way to let the controller unsubscribe itself would be to add this piece of code:
$scope.$on("$destroy",function() {
WebSocketConnection.unsubscribe($scope.$id);
});
Subscribing mechanism would look like this:
WebSocketConnection.subscribe($scope.$id, function (message) { //note the $scope.$id parameter
$scope.$apply(function () {
var data = angular.fromJson(message.data);
$scope.notifications.push(data);
});
});
Therefore, the full factory code would be:
.factory('WebSocketConnection', function () {
var service = {};
service.callbacks = {}; //note the object declaration, not Array
service.connect = function() {
if(service.ws)
return;
var ws = new WebSocket("ws://localhost:9000/ws");
ws.onmessage = function (message) {
angular.forEach(service.callbacks, function(callback){
callback(message);
});
};
service.ws = ws;
};
service.send = function(message) {
service.ws.send(message);
};
service.subscribe = function(concernedScopeId, callback) {
service.callbacks[concernedScopeId] = callback;
};
service.unsubscribe = function(concernedScopeId) {
delete service.callbacks[concernedScopeId];
};
return service;
});
And that does the trick: each useless callback acting as listener can then be detected and deleted.
I have a provider:
AdviceList.provider('$adviceList',function(){
this.$get = function ($rootScope,$document,$compile,$http,$purr){
function AdviceList(){
$http.post('../sys/core/fetchTreatments.php').success(function(data,status){
this.treatments = data;
console.log(this.treatments); // the correct object
});
this.adviceCategories = [
// available in the controller
];
}
return{
AdviceList: function(){
return new AdviceList();
}
}
}
});
Further, i have this controller:
AdviceList.controller('AdviceListCtrl',function($scope,$adviceList){
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; // undefined
});
Why is it, that the controller's $scope.treatments stays undefined, this.treatments inside the provider however, is filled correctly? Also, adviceCategories is available in my controller.
The call you get teatment is async in nature so the results may not have been populated when you try to assign them.
So here
var adv = $adviceList.AdviceList();
$scope.treatments = adv.treatments; //The treatments would only get filled after the server call is over.
You need to rewrite the code in a way that you assign it to your scope property on the success callback.
I will recommend you to simplify your code
1) Use simple factory method of angular instead of provider
2) return a promise to avoid using callbacks
AdviceList.service('adviceList', function ($http) {
return {
adviceList: function () {
return $http.post('../sys/core/fetchTreatments.php');
}
}
});
AdviceList.controller('AdviceListCtrl', function ($scope, $adviceList) {
adviceList.AdviceList().then(function (data) {
$scope.treatments = data //set value to data when data is recieved from server
});
});
I have a resource that returns an array from a query, like so:
.factory('Books', function($resource){
var Books = $resource('/authors/:authorId/books');
return Books;
})
Is it possible to add prototype methods to the array returned from this query? (Note, not to array.prototype).
For example, I'd like to add methods such as hasBookWithTitle(title) to the collection.
The suggestion from ricick is a good one, but if you want to actually have a method on the array that returns, you will have a harder time doing that. Basically what you need to do is create a bit of a wrapper around $resource and its instances. The problem you run into is this line of code from angular-resource.js:
var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
This is where the return value from $resource is set up. What happens is "value" is populated and returned while the ajax request is being executed. When the ajax request is completed, the value is returned into "value" above, but by reference (using the angular.copy() method). Each element of the array (for a method like query()) will be an instance of the resource you are operating on.
So a way you could extend this functionality would be something like this (non-tested code, so will probably not work without some adjustments):
var myModule = angular.module('myModule', ['ngResource']);
myModule.factory('Book', function($resource) {
var service = $resource('/authors/:authorId/books'),
origQuery = service.prototype.$query;
service.prototype.$query = function (a1, a2, a3) {
var returnData = origQuery.call(this, a1, a2, a3);
returnData.myCustomMethod = function () {
// Create your custom method here...
return returnData;
}
}
return service;
});
Again, you will have to mess with it a bit, but that's the basic idea.
This is probably a good case for creating a custom service extending resource, and adding utility methods to it, rather than adding methods to the returned values from the default resource service.
var myModule = angular.module('myModule', []);
myModule.factory('Book', function() {
var service = $resource('/authors/:authorId/books');
service.hasBookWithTitle = function(books, title){
//blah blah return true false etc.
}
return service;
});
then
books = Book.list(function(){
//check in the on complete method
var hasBook = Book.hasBookWithTitle(books, 'someTitle');
})
Looking at the code in angular-resource.js (at least for the 1.0.x series) it doesn't appear that you can add in a callback for any sort of default behavior (and this seems like the correct design to me).
If you're just using the value in a single controller, you can pass in a callback whenever you invoke query on the resource:
var books = Book.query(function(data) {
data.hasBookWithTitle = function (title) { ... };
]);
Alternatively, you can create a service which decorates the Books resource, forwards all of the calls to get/query/save/etc., and decorates the array with your method. Example plunk here: http://plnkr.co/edit/NJkPcsuraxesyhxlJ8lg
app.factory("Books",
function ($resource) {
var self = this;
var resource = $resource("sample.json");
return {
get: function(id) { return resource.get(id); },
// implement whatever else you need, save, delete etc.
query: function() {
return resource.query(
function(data) { // success callback
data.hasBookWithTitle = function(title) {
for (var i = 0; i < data.length; i++) {
if (title === data[i].title) {
return true;
}
}
return false;
};
},
function(data, response) { /* optional error callback */}
);
}
};
}
);
Thirdly, and I think this is better but it depends on your requirements, you can just take the functional approach and put the hasBookWithTitle function on your controller, or if the logic needs to be shared, in a utilities service.