I have the following service in my app:
uaInProgressApp.factory('uaProgressService',
function(uaApiInterface, $timeout, $rootScope){
var factory = {};
factory.taskResource = uaApiInterface.taskResource()
factory.taskList = [];
factory.cron = undefined;
factory.updateTaskList = function() {
factory.taskResource.query(function(data){
factory.taskList = data;
$rootScope.$digest
console.log(factory.taskList);
});
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.startCron = function () {
factory.cron = $timeout(factory.updateTaskList, 5000);
}
factory.stopCron = function (){
$timeout.cancel(factory.cron);
}
return factory;
});
Then I use it in a controller like this:
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
$scope.taskList = uaProgressService.taskList;
});
}
);
So basically my service update factory.taskList every 5 seconds and I linked this factory.taskList to $scope.taskList. I then tried different methods like $apply, $digest but changes on factory.taskList are not reflected in my controller and view $scope.taskList.
It remains empty in my template. Do you know how I can propagate these changes ?
While using $watch may solve the problem, it is not the most efficient solution. You might want to change the way you are storing the data in the service.
The problem is that you are replacing the memory location that your taskList is associated to every time you assign it a new value while the scope is stuck pointing to the old location. You can see this happening in this plunk.
Take a heap snapshots with Chrome when you first load the plunk and, after you click the button, you will see that the memory location the scope points to is never updated while the list points to a different memory location.
You can easily fix this by having your service hold an object that contains the variable that may change (something like data:{task:[], x:[], z:[]}). In this case "data" should never be changed but any of its members may be changed whenever you need to. You then pass this data variable to the scope and, as long as you don't override it by trying to assign "data" to something else, whenever a field inside data changes the scope will know about it and will update correctly.
This plunk shows the same example running using the fix suggested above. No need to use any watchers in this situation and if it ever happens that something is not updated on the view you know that all you need to do is run a scope $apply to update the view.
This way you eliminate the need for watchers that frequently compare variables for changes and the ugly setup involved in cases when you need to watch many variables. The only issue with this approach is that on your view (html) you will have "data." prefixing everything where you used to just have the variable name.
Angular (unlike Ember and some other frameworks), does not provide special wrapped objects which semi-magically stay in sync. The objects you are manipulating are plain javascript objects and just like saying var a = b; does not link the variables a and b, saying $scope.taskList = uaProgressService.taskList does not link those two values.
For this kind of link-ing, angular provides $watch on $scope. You can watch the value of the uaProgressService.taskList and update the value on $scope when it changes:
$scope.$watch(function () { return uaProgressService.taskList }, function (newVal, oldVal) {
if (typeof newVal !== 'undefined') {
$scope.taskList = uaProgressService.taskList;
}
});
The first expression passed to the $watch function is executed on every $digest loop and the second argument is the function which is invoked with the new and the old value.
I'm not sure if thats help but what I am doing is bind the function to $scope.value. For example
angular
.module("testApp", [])
.service("myDataService", function(){
this.dataContainer = {
valA : "car",
valB : "bike"
}
})
.controller("testCtrl", [
"$scope",
"myDataService",
function($scope, myDataService){
$scope.data = function(){
return myDataService.dataContainer;
};
}]);
Then I just bind it in DOM as
<li ng-repeat="(key,value) in data() "></li>
This way you can avoid to using $watch in your code.
No $watch or etc. is required. You can simply define the following
uaInProgressApp.controller('ua.InProgressController',
function ($scope, $rootScope, $routeParams, uaContext, uaProgressService) {
uaContext.getSession().then(function(){
uaContext.appName.set('Testing house');
uaContext.subAppName.set('In progress');
uaProgressService.startCron();
});
$scope.getTaskList = function() {
return uaProgressService.taskList;
};
});
Because the function getTaskList belongs to $scope its return value will be evaluated (and updated) on every change of uaProgressService.taskList
Lightweight alternative is that during controller initialization you subscribe to a notifier pattern set up in the service.
Something like:
app.controller('YourCtrl'['yourSvc', function(yourSvc){
yourSvc.awaitUpdate('YourCtrl',function(){
$scope.someValue = yourSvc.someValue;
});
}]);
And the service has something like:
app.service('yourSvc', ['$http',function($http){
var self = this;
self.notificationSubscribers={};
self.awaitUpdate=function(key,callback){
self.notificationSubscribers[key]=callback;
};
self.notifySubscribers=function(){
angular.forEach(self.notificationSubscribers,
function(callback,key){
callback();
});
};
$http.get('someUrl').then(
function(response){
self.importantData=response.data;
self.notifySubscribers();
}
);
}]);
This can let you fine tune more carefully when your controllers refresh from a service.
Like Gabriel Piacenti said, no watches are needed if you wrap the changing data into an object.
BUT for updating the changed service data in the scope correctly, it is important that the scope value of the controller that uses the service data does not point directly to the changing data (field). Instead the scope value must point to the object that wraps the changing data.
The following code should explain this more clear. In my example i use an NLS Service for translating. The NLS Tokens are getting updated via http.
The Service:
app.factory('nlsService', ['$http', function($http) {
var data = {
get: {
ressources : "gdc.ressources",
maintenance : "gdc.mm.maintenance",
prewarning : "gdc.mobMaint.prewarning",
}
};
// ... asynchron change the data.get = ajaxResult.data...
return data;
}]);
Controller and scope expression
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.get.maintenance}}</span>
</div>
The above code works, but first i wanted to access my NLS Tokens directly (see the following snippet) and here the values did not become updated.
app.controller('MenuCtrl', function($scope, nlsService)
{
$scope.NLS = nlsService.get;
}
);
<div ng-controller="MenuCtrl">
<span class="navPanelLiItemText">{{NLS.maintenance}}</span>
</div>
Related
I'm having problems to watch multiple variables in a Service when some changes are made on it by other controllers.
I've the following:
angular.module('carApp').controller('CarListCtrl', function ($scope, CarService) {
$scope.carList = CarService.carList
$scope.initalAmount = CarService.initialAmount
$scope.startDate = CarService.startDate
$scope.$watchGroup(['carList', 'initialAmount', 'startDate'],
function (newValues, oldValues, $scope) {
console.log(newValues);
});
});
Other controllers update the values in the Service all the time but the watchGroup never fires up...
I've create a simple watch targeting the service directly to check if work, and it's working ..., so I imagine that the watchGroup should target the service variable directly but I can't find how to do that....
here is the simple watch that works:
$scope.$watch(function () {
return CarService.carList
}, function (newVal, oldVal) {
console.log(newVal);
});
What should I do to make it works with the multiple service variables?
UPDATE 1:
Just a update... if I try the watchgroup with just one element, for example $scope.$watchGroup(['carList'], ... it works, so I tried with each one and it works every time, but as soon as I add one more element it stop working... very annoying...
Tks again guys!
Just to close this, people from angularjs github help me out: here is the anwser for anyone who need:
Each value inside a watchGroup array can be a expression or a function, so you can use three different functions inside your watchGroup. http://plnkr.co/edit/nMmPt808xAFXqjJ6yEoc?p=preview
$scope.$watchGroup([
function() {
return myService.valueOne()
},
function() {
return myService.valueTwo()
}
], function(newValues, oldValues) {
$scope.valueOne = newValues[0]
$scope.valueTwo = newValues[1]
})
Your first example is possibly not working because your other controllers assign new values to initialAmount and startDate in your service, which means you have different objects with different values in your controller and your startDate. It´probably works with the carList, because you are only adding / removing items, which means it remains the same object in your controller and your service.
I've got a potentially really dumb question, but how do I modify variables up in $rootScope in Angular? I've got a slide-in sidebar that I want to change the content on whenever someone clicks on a thumbnail, and I figured the easiest way to handle where the data in the sidebar comes from/the sidebar visibility would either be in global values, or in $rootScope. I'm trying to keep everything as simple as possible, but I just don't know how to handle modifying global variables.
My angular code surrounding this is:
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', '$rootScope',
function ($scope, $rootScope) {
$scope.isDetail = $rootScope.detail_visible.value;
$scope.url = $rootScope.currentUrl.value;
$scope.hide = function($rootScope) {
$rootScope.detail_visible.value = false;
};
}]);
and the connecting HTML is
<div id="detail_box" ng-class="{d_show: isDetail, d_hide: !isDetail}">
<div ng-include="url + 'detail.html'"></div>
</div>
In essence, I'm trying to make it so that when you click on a thumbnail, it changes the currentUrl value from 'visual/design/1/' to whatever they've clicked on (like, 'music/solo/2' or whatever) then changes the value of detail_visible to false, so that the classes on my sidebar switch and I get a nice little slide-in, with fresh content loaded via ng-include which I kind of love a thousand times more than I thought I would. I've been banging my head against this for about three hours now, breaking everything else on this app whenever I get the chance. What am I screwing up here? Alternatively, is there a better way of doing this?
My reason for using global variables is that I have multiple thumbnails in multiple controllers, and I want each one to be able to dynamically change the URL in my ng-include.
For your question, you change the $rootScope variable simple by referencing it with
$rootScope.detail_visible.value = newValue;
but you dont need to inject $rootScope to your function:
$scope.hide = function() { //without $rootScope
$rootScope.detail_visible.value = false;
};
But, I would suggest you to implement a service and not to pollute the rootscope for such task.
https://docs.angularjs.org/guide/services
Object properties of scopes are inherited -- in your controller, you should be able to modify $scope.detail_visible.value and see it affect the $rootScope. You still have to initialize it on the $rootScope in .run() though.
app.run(function($rootScope) {
$rootScope.currentUrl = { value: 'visual/design/1/' };
$rootScope.detail_visible = { value: true };
});
app.controller('navController', ['$scope', function ($scope, $rootScope) {
$scope.hide = function() { // don't need to pass an argument
$scope.detail_visible.value = false;
};
}]);
view:
<div id="detail_box" ng-class="{d_show: currentUrl.value, d_hide: !currentUrl.value}">
<div ng-include="currentUrl.value + 'detail.html'"></div>
</div>
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!!!
EDIT: As asked, I'll explain a bit more efficiently !
I've been sitting in front of an annoying problem recently, which is that whenever I update a value inside a directive, the controllers I'm not currently "in" are the only ones to be updated properly.
Scenario example: Profile page is made of two controllers. Navbar_controller which is just currently displaying the user name :
<div ng-if="Auth.isAuthenticated">Hello, {{Auth.getCurrentUser().name}}</div>
The second controller , Profile_controller is here to update user values. This is a simple function in the angular first controller, which updates CurrentUser:
$scope.updateUser = function (type, form) {
if (!$scope.modif)
return ;
$http.put('/api/users/' + Auth.getCurrentUser()._id + '/update', {type:type, modif:$scope.modif})
.success(function (data, status) {
$scope.user = Auth.setNewUser(data);
})
.error(function () {
console.log("error");
});
};
When I update, for example, the name. I can see that the database has been modified properly. And indeed, navbar_controller got the update because a new name is printed in the div. However, Profile_controller doesn't get the update: the name printed in the profile page didn't change.
Here are the two basic functions in Auth.service.js :
getCurrentUser: function() {
return currentUser;
},
// 'user' is the data retrieved in http put request dot success
setNewUser: function(user) {
currentUser = user;
$rootScope.$broadcast(); // Navbar_controller is updated with or without this line
return currentUser;
}
Anyway, if I look at the navbar and its controller, which is calling Auth.getCurrentUser() method, the user values are instantly modified. I'e been using an ugly method consisting in modifying the controller values manually or by refreshing the page... But this isn't the way to go, right ?
There must be something with "$rootScope.$broadcast();", but I'm really new to Angular and other questions on stackoverflow are too specific to help me understand properly.
Thank you !
Your question was a little difficult to understand, but I think the problem is that you are reference a changing object in your various controllers. Here is an example to explain:
Service:
var myObject = { ... };
return {
getObject() { return myObject; }
setObject(obj) { myObject = obj; }
};
Controller 1:
$scope.myObjA = Service.getObject();
Controller 2:
$scope.myObjB = Service.getObject();
Now on initialisation both controllers will be referencing the same object, so if you changed a property inside either controller (eg. $scope.myObjB.name = 'bob';), then the other controller would also see the name.
However if you changed the object itself in a controller (eg. Service.setObject(newObj);), then the controller will be referencing the new object, while the other controller will still be referencing the old one.
You can fix this by wrapping your service object in a container:
var cont = {
user: ...
};
function getContainer() { return cont; }
function setNewUser(user) { cont.user = user; }
Then inside your controllers, get the container (not the user):
$scope.cont = Service.getContainer();
And inside your html:
<div>{{cont.user.name}}</div>
Now when you update the user, all attached controllers will be updated.
Well I'd try to change and store the user information in $rootScope, for your scenario could be a good fit.
getCurrentUser: function() {
$rootScope.currentUser===undefined ? 'no User': $rootScope.currentUser;
},
setNewUser: function(user) {
$rootScope.currentUser = user;
//$rootScope.$broadcast(); no need to broadcast
return getCurrentUser();
}
in that way currentUser will be updated in different scopes as needed!
I'll quote AnuglarJs FAQ regarding to $rootscope:
$rootScope exists, but it can be used for evil
Occasionally there are pieces of data that you want to make global to
the whole app. For these, you can inject $rootScope and set values on
it like any other scope. Since the scopes inherit from the root scope,
these values will be available to the expressions attached to
directives like ng-show just like values on your local $scope.
Of course, global state sucks and you should use $rootScope sparingly,
like you would (hopefully) use with global variables in any language.
In particular, don't use it for code, only data. If you're tempted to
put a function on $rootScope, it's almost always better to put it in a
service that can be injected where it's needed, and more easily
tested.
Conversely, don't create a service whose only purpose in life is to
store and return bits of data.
I am writing a small Angular web application and have run into problems when it comes to loading the data. I am using Firebase as datasource and found the AngularFire project which sounded nice. However, I am having trouble controlling the way the data is being displayed.
At first I tried using the regular implicit synchronization by doing:
angularFire(ref, $scope, 'items');
It worked fine and all the data was displayed when I used the model $items in my view. However, when the data is arriving from the Firebase data source it is not formatted in a way that the view supports, so I need to do some additional structural changes to the data before it is displayed. Problem is, I won't know when the data has been fully loaded. I tried assigning a $watch to the $items, but it was called too early.
So, I moved on and tried to use the angularfireCollection instead:
$scope.items = angularFireCollection(new Firebase(url), optionalCallbackOnInitialLoad);
The documentation isn't quite clear what the "optionalCallbackOnInitialLoad" does and when it is called, but trying to access the first item in the $items collection will throw an error ("Uncaught TypeError: Cannot read property '0' of undefined").
I tried adding a button and in the button's click handler I logged the content of the first item in the $items, and it worked:
console.log($scope.items[0]);
There it was! The first object from my Firebase was displayed without any errors ... only problem is that I had to click a button to get there.
So, does anyone know how I can know when all the data has been loaded and then assign it to a $scope variable to be displayed in my view? Or is there another way?
My controller:
app.controller('MyController', ['$scope', 'angularFireCollection',
function MyController($scope, angularFireCollection) {
$scope.start = function()
{
var ref = new Firebase('https://url.firebaseio.com/days');
console.log("start");
console.log("before load?");
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
console.log("start() out");
};
$scope.start();
//wait for changes
$scope.$watch('items', function() {
console.log("items watch");
console.log($scope.items[0]); //undefined
});
$scope.testData = function()
{
console.log($scope.items[0].properties); //not undefined
};
}
]);
My view:
<button ng-click="testData()">Is the data loaded yet?</button>
Thanks in advance!
So, does anyone know how I can know when all the data has been loaded
and then assign it to a $scope variable to be displayed in my view? Or
is there another way?
Remember that all Firebase calls are asynchronous. Many of your problems are occurring because you're trying to access elements that don't exist yet. The reason the button click worked for you is because you clicked the button (and accessed the elements) after they had been successfully loaded.
In the case of the optionalCallbackOnInitialLoad, this is a function that will be executed once the initial load of the angularFireCollection is finished. As the name implies, it's optional, meaning that you don't have to provide a callback function if you don't want to.
You can either use this and specify a function to be executed after it's loaded, or you can use $q promises or another promise library of your liking. I'm partial to kriskowal's Q myself. I'd suggest reading up a bit on asynchronous JavaScript so you get a deeper understanding of some of these issues.
Be wary that this:
$scope.items = angularFireCollection(ref, function()
{
console.log("loaded?");
console.log($scope.items[0]); //undefined
});
does correctly specify a callback function, but $scope.items doesn't get assigned until after you've ran the callback. So, it still won't exist.
If you just want to see when $scope.items has been loaded, you could try something like this:
$scope.$watch('items', function (items) {
console.log(items)
});
In my project I needed to know too when the data has been loaded. I used the following approach (implicit bindings):
$scope.auctionsDiscoveryPromise = angularFire(firebaseReference.getInstance() + "/auctionlist", $scope, 'auctionlist', []);
$scope.auctionsDiscoveryPromise.then(function() {
console.log("AuctionsDiscoverController auctionsDiscoveryPromise resolved");
$timeout(function() {
$scope.$broadcast("AUCTION_INIT");
}, 500);
}, function() {
console.error("AuctionsDiscoverController auctionsDiscoveryPromise rejected");
});
When the $scope.auctionsDiscoveryPromise promise has been resolved I'm broadcasting an event AUCTION_INIT which is being listened in my directives. I use a short timeout just in case some services or directives haven't been initialized yet.
I'm using this if it would help anyone:
function getAll(items) {
var deferred = $q.defer();
var dataRef = new Firebase(baseUrl + items);
var returnData = angularFireCollection(dataRef, function(data){
deferred.resolve(data.val());
});
return deferred.promise;
}