I discovered that when I call a service method within my controller and pass to it an object as a parameter, any changes that are done to that object (inside service method) are also made to the original object from my controller.
I always thought that controller data should stay unchanged until I changed it inside promise win/error event and only if I need to.
JS sample:
// Code goes here
var app = angular.module('App', []);
app.controller('AppCtrl', function($scope, simpleService){
$scope.data = { d: 1, c: 10};
$scope.clickMe = function(){
simpleService.clickMe($scope.data).then(function(res){
alert($scope.data.d);
})
.catch(function(err){
alert($scope.data.d);
});
}
});
app.factory('simpleService', function($q){
var simpleServiceMethods = {};
simpleServiceMethods.clickMe = function(data){
var deffered = $q.defer();
//data = JSON.parse(JSON.stringify(data)); - solution: clone data without references
data.d = 1111;
deffered.reject();
return deffered.promise;
}
return simpleServiceMethods;
});
Plunker demo: http://plnkr.co/edit/nHz2T7D2mJ0zXWjZZKP3?p=preview
I believe this is the nature of angular's databinding. If you want to pass the details of a $scope variable you could make use of angular's cloning capability with copy or update your services to work slightly differently by creating a copy on the service side. Normal CRUD style applications you'd normally be passing the id of an entity, receiving a new entity or posting changes which may in most cases already be present client side.
Related
I have a controller (call it "A") where I get a value from the webserver. When I get this value, I store it in a Service.
In another controller (call it "B") I have to get this value from the service everytime it is stored in the service. And this value must appear in the view (updated).
My usual solution is:
I emit an event everytime I store the value in the service. then in the controller B I listen to this event and then i get the value from the service.
I know there are other solutions, like the scope.$watch/apply but I don't know which is better.
Can you suggest me which way is better?
Push Values from a Service with RxJS
One alterantive to $rootScope.broadcast is to build a service with RxJS Extensions for Angular:
<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);
app.factory("DataService", function(rx) {
var subject = new rx.Subject();
var data = "Initial";
return {
set: function set(d){
data = d;
subject.onNext(d);
},
get: function get() {
return data;
},
subscribe: function (o) {
return subject.subscribe(o);
}
};
});
Then simply subscribe to the changes.
app.controller('displayCtrl', function(DataService) {
var $ctrl = this;
$ctrl.data = DataService.get();
var subscription = DataService.subscribe(function onNext(d) {
$ctrl.data = d;
});
this.$onDestroy = function() {
subscription.dispose();
};
});
Clients can subscribe to changes with DataService.subscribe and producers can push changes with DataService.set.
The DEMO on PLNKR.
Watchers are called everytime a $digest or an $apply cycle is done. It has more impact on your application than a local event like you are doing.
If you can use services to control communication between directives and/or controllers, it's better.
As far as i know, there's 4 ways to handle communication between controller and/or directives:
Using a service (like you do)
Rely on the $apply cycle with $watch
Use the angular event system (with scope.$emit or scope.$broadcast)
Be very dirty and use a global variable
Using a service is the best way. Especially if you handle a "one-to-one" communication.
I have a service defined which do the db related queries/updates. I have defined the controller which does the data parsing for the angular elements by getting the objects from the service. I would like to keep each scope different
How can I pass the data from service to controller using ngResource.
Sample Service:
app.factory("ioHomeService", ["$rootScope","$resource", function($rootScope,$resource) {
var svc = {};
var home = $resource('/home/getAll');
var dbData= home.get();
svc.getRooms = function() {
return dbData;
};
return svc;
}]);
Sample Controller:
app.controller("homeCtrl",["$scope","$mdDialog","ioHomeService",function($scope,$mdDialog,ioHome){
$scope.dbData = ioHome.getRooms();
//Here UI specific objects/data is derived from dbData
}]);
After the DB is queried and the results are avialble the dbData in service is reflecting the data from DB, but the Controller cannot get that data
It is important to realize that invoking a $resource object method
immediately returns an empty reference (object or array depending on
isArray). Once the data is returned from the server the existing
reference is populated with the actual data. This is a useful trick
since usually the resource is assigned to a model which is then
rendered by the view. Having an empty object results in no rendering,
once the data arrives from the server then the object is populated
with the data and the view automatically re-renders itself showing the
new data. This means that in most cases one never has to write a
callback function for the action methods.
From https://docs.angularjs.org/api/ngResource/service/$resource
Since the 'ioHome.getRooms();' is being called before the $resource has returned the data you are getting dbData as an empty reference
app.factory("ioHomeService", ["$rootScope","$resource", function($rootScope,$resource) {
var svc = {
dbData : {}
};
var home = $resource('/home/getAll');
var svc.dbData.rooms = home.get();
return svc;
}]);
Controller
app.controller("homeCtrl",["$scope","$mdDialog","ioHomeService",function($scope,$mdDialog,ioHome){
$scope.dbData = ioHome.dbData;
//You can access the rooms data using $scope.dbData.roooms
//Here UI specific objects/data is derived from dbData
}]);
You would have to return the service object , like so :
app.factory("ioHomeService", ["$rootScope","$resource", function($rootScope,$resource) {
var svc = {};
var home = $resource('/home/getAll');
var dbData= home.get();
svc.getRooms = function() {
return dbData;
};
return svc; //here
}]);
Currently the getRooms method is not visible to your controller.
Why does $scope.events.push(data); update both the scope and the factory?
Why does EventFactory.setEvent(data); update both the factory and the scope?
Summary:
$scope.events.push(data); //should only update scope but also updates factory
EventFactory.setEvent(data); //should only update factory but also updates scope
If I don't uncomment one of those lines then I get the same effect as doing:
$scope.events.push(data);
$scope.events.push(data);
or
EventFactory.setEvent(data);
EventFactory.setEvent(data);
Basically $scope.events.push(data); and EventFactory.setEvent(data); update both the factory and the scope.
I want $scope.events.push(data); to only update the scope.
I want EventFactory.setEvent(data); to only update the factory.
app.js (contains the factory)
...
app.factory("EventFactory", function($http){
var events = [];
var init = function(){
return $http.get("api/events", {cache:true})
.then(
function(response){
events = response.data;
return response.data;
});
};
var getEvents = function(){
return events;
};
var setEvent = function(data){
events.push(data);
};
return {
init: init,
getEvents: getEvents,
setEvent: setEvent
}
});
...
EventListCtrl.js (controller)
angular.module("EventListCtrl", []).controller("EventListController", function($scope, EventFactory, socket){
$scope.events = [];
EventFactory.init().then(function(events){
$scope.events = events;
});
socket.on("event created", function(data){
$scope.events.push(data); // should only update scope but updates factory as well
EventFactory.setEvent(data); // should only update factory but updates scope as well
});
});
I have tested the code and the socket is not the problem, maybe some binding has taken place that I have no understanding of as yet.
Any help is greatly appreciated.
Thank You
This is happening because you are making $scope.events = events in your controller instead of $scope.events = angular.copy(events);
(you could instead make the copy in your service, which would be more DRY)
More plainly put, this is happening because you are making object references; not object clones/copies. (Arrays are technically objects)
You are sending socket event of "Event created" to others and probably receiving it yourself also so your scope also updates
More over
All services in angularjs are singletons
And you have only one copy of the service everywhere and thats how it works when you change some thing in factory it will be updated elsewhere too
In EventListCtrl controller you are doing the shallow copy by doing:
$scope.events = events;
where both of the objects are having the same reference.
To make deep copy out of it, angular provides us copy() method:
angular.copy(source, [destination]);
source: The source that will be used to make a copy. Can be any type, including primitives, null, and undefined
destination(optional): Destination into which the source is copied. If provided, must be of the same type as source.
In your case it should be:
$scope.events = angular.copy(events);
I am writing my first non-trival Angular App and I have hit a snag with a directive. The directive takes data from a controller's scope and applies it to Google Chart. The chart is not the issue - which is to say it works fine with dummy data - it is access to the properties of the scope object which were obtained via http:
I am accessing data returned via an API in a service which utilizes $http:
dashboardServices.factory('SearchList', ['$http','$q',
function($http, $q){
return {
getSearchDetails:function(searchType, resultType){
return $http.get("api/searches/"+searchType+"/"+resultType)
.then(function(response){
if (typeof(response.data === 'object')) {
return response.data;
} else {
return $q.reject(response.data);
}
},function(response){
$q.reject(response.data);
});
}
}
}]);
In my controller, I am taking the response from this service and attaching to my scope via the promises' "then" method:
dashboardControllers.controller('DashboardCtrl', ['$scope', 'SearchList',
function($scope, SearchList){
$scope.searchData = {};
$scope.searchData.chartTitle="Search Result Performance"
SearchList.getSearchDetails("all", "count").then(function(response){
$scope.searchData.total = response.value; //value is the key from my API
});
SearchList.getSearchDetails("no_results", "count").then(function(response){
$scope.searchData.noResults = response.value;
});
}]);
To an extent this works fine, i can then use the 2-way binding to print out the values in the view AS TEXT. Note: I want to be able to write the values as text as I am trying to use a single scope object for both the chart and the textual data.
{{searchData.total | number}}
As mentioned, I have written a directive that will print a specific chart for this data, in this directive ONLY the $scope.searchData.chartTitle property is accessible. The values that were set in the then functions are not accessible in the directive's link method:
Here is the directive:
statsApp.directive('searchResultsPieChart', function(){
return{
restrict : "A",
scope:{
vals:'#vals'
},
link: function($scope, $elem, $attr){
var dt_data = $scope.vals;
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
}
});
Here is how I am using the directive in the view:
<div search-results-pie-chart vals="{{searchData}}"></div>
I can see that the issue is that the numeric values are not available to the directive despite being available when bound to the view.
Clearly the directive needs to be called later when these items are available or via some callback (or perhaps an entirely different approach), unfortunately i am not sure why this is the case or how to go about solving.
Any help would be greatly appreciated. I hope this makes sense.
I think the following will help you.
First change the directive scope binding for vals to use = instead of # (see this question for good explanation of the differences - basically # interpolates the value whereas = binds to the variable in the parent scope)
Then, move the part of the directive that creates the graph into a render function within your link function.
Then, $watch vals for any changes, then call the render function with the new values
You would also have to slightly change the approach of using ele[0], as you'll need to clear out the contents of it and add a new element with the new chart when the data changes (otherwise many charts will be added as the data changes!)
Here is an example of what to do in your link function with regard to the $watch and new render function (changing the $scope binding like I mentioned is not shown):
$scope.$watch('vals', function(newVals, oldVals) {
return $scope.render(newVals);
}, true);
$scope.render = function (dt_data) {
var dt = new google.visualization.DataTable();
dt.addColumn("string","Result Type")
dt.addColumn("number","Total")
dt.addRow(["Successful Searches",dt_data.total]);
dt.addRow(["No Results",dt_data.noResults]);
var options = {};
options.title = $scope.vals.title;
var googleChart = new google.visualization.PieChart($elem[0]);
googleChart.draw(dt,options)
}
Hope this helps you out!!!
I have a service called 'player' and I need to update the service when a flash object is finished loading.
mySongPlayer.factory('player', function() {
var isPlayerLoaded = false;
var playerHolder = '';
window.playerReady = function(thePlayer) {
playerHolder = window.document[thePlayer.id];
addListeners();
isPlayerLoaded = true;
}
var flashvars = {
file:"",
autostart:"true",
skin: "/skins/glow/glow.zip",
}
var params = {
allowfullscreen:"false",
allowscriptaccess:"always"
}
var attributes = {
id:"player1",
name:"player1"
}
swfobject.embedSWF("/player.swf", "player_placeholder", "100%", "40", "9.0.115", false, flashvars, params, attributes);
var playObj;
return playObj || (playObj = {
currentId: 'test', currentUrl: 'url', playerHolder: ''
});
});
I know how to access the service using
angular.element(DOMElement).injector().get('player')
but it returns a new instance of 'player' while I need to update the instance already created in the module. Is there a way to do this? I only want one instance of the player, but I need to initialize it from outside javascript.
Well, I can't really see all of what you're doing, but you're probably about 1/2 way there.
Here is a working plunk of what I'm about to describe
injector.get() should be returning the same instance of Service as what's in your app. You're probably just seeing an issue that makes you think you have a different instance.
So what you need to do is:
Make sure that what you're putting in your scope from your service is an object reference. If it's a primitive type, it won't be the same reference so it won't update.
Be sure to get the scope off of your angular element with angular.element(DOMElement).scope() and then call $apply() on it.
Here's the code:
app.controller('MainCtrl', function($scope, myService) {
// Set a var on the scope to an object reference of the
// (or from the) service.
$scope.myService = myService;
});
app.factory('myService', function(){
return {
foo: 'bar'
};
});
//do a little something to change the service externally.
setTimeout(function(){
//get your angular element
var elem = angular.element(document.querySelector('[ng-controller]'));
//get the injector.
var injector = elem.injector();
//get the service.
var myService = injector.get('myService');
//update the service.
myService.foo = 'test';
//apply the changes to the scope.
elem.scope().$apply();
}, 2000)
Other thoughts:
You probably want to inject $window into your service, rather than use the window object, to maintain the testability of your service.
A directive is probably a better choice for DOM manipulation such as the creation of a Flash movie player.