Angular $watch on a recursive service function - javascript

I hate asking questions that there's already good info for but I'm missing something to keep my implementation from working.
My scenario is using a recursive function on a service to load my data in iterative chunks. All the data is captured by the scope but only the first set is displayed unless you navigate away and then back again. Clearly, I need to $watch my scope. I just can't figure out how to do so.
AccountService runs a method called getAccountsByPage which is passed an argument of 1. That function then calls itself with a value of 2, and so forth.
$routeProvider.when('/accounts/', {
...
controller: function ($scope, AccountService) {
var accounts = $scope.accounts = AccountService.getAccountsByPage(1);
$scope.$watch('accounts', function(newVal, oldVal) {
console.log(newVal, oldVal);
});
}
});
console outputs: undefined, undefined
[Object, Object, Object...] undefined
To be clear, getting the data isn't the problem. Updating the view is. Angular says not to use $watch on the controller but it seems that everyone does so...

From the code you've posted, $scope.accounts only ever gets set once when the controller first gets instantiated. It doesn't matter what AccountService.getAccountsByPage is doing underneath. Whatever it returns will be put into $scope.accounts, and trigger the $watch that one time. It won't be triggered again until $scope.accounts changes, which I don't see happening anywhere from the code posted.

Related

Understanding $resource and controllers

I'm trying to understand the following pieces of code. If I understand correctly, below I have three resource objects that interact with REST server data sources (these objects are CategorySrv, ArticleSrv and SearchSrv respectively).
app.factory('CategoryService', function($resource) {
return $resource('categories');
});
app.factory('ArticleService', function($resource) {
return $resource('articles');
});
app.factory('SearchService', function($resource) {
return $resource('articles/search');
});
Now, the question I'd like to ask if when is the controller's code executed? If I loaded the page, it would probably run only once. When will it run the next time? Controller's code doesn't run in a loop, but only when its methods are called from the view, or the portion of the page attached to this controller is loaded (AFAIK).
When this happens, the resource objects listed above are injected into the argument list and controller's function is called:
app.controller('AppCtrl', function($scope, $location, CategoryService, ArticleService, CartService) {
CategoryService.query(function(response) {
$scope.categories = response;
});
ArticleService.query(function(response) {
$scope.articles = response;
});
CartService.get(function(response) {
$scope.cartInfo = response;
});
Do I understand this correctly? Also, what's the difference between get and query above?
Yes. The code won't be running in a loop, only when the controller is loaded.
$resource gives you the option to use get or query, the difference being that the query will immediately return an empty array or object depending on whether isArray is set to true. This helps with rendering so that no errors are thrown when angular expects an array or object but instead gets undefined like you would for async actions. When the response comes back with your data, the digest cycle will run since $scope has changed and it will subsequently re-render your view.
docs: https://docs.angularjs.org/api/ngResource/service/$resource

Accessing factory properties in AngularJS + Chrome Apps APIs

I'm attempting to build a Chrome App using AngularJS, and one of the abilities I need is to monitor the available network interfaces through chrome.system.network.getNetworkInterfaces. Currently, I am trying to store this data in a factory and inject it into the view controller:
(pared down as much as possible)
Factory:
exampleApp.factory('exampleFactory', ['$http', function ($http) {
var service = {};
service.network_interfaces = 'Detecting network connections...';
chrome.system.network.getNetworkInterfaces(function(interfaces){
service.network_interfaces = interfaces;
})
// This outputs the expected array containing interface data
// in service.network_interfaces
console.log(service)
return service;
}]);
Controller:
exampleApp.controller('MainCtrl', ['$scope', '$http', 'exampleFactory', function($scope, $http, exampleFactory) {
// This outputs the expected array in network_interfaces, identical
// to the console output within the factory
console.log(exampleFactory)
// But then this outputs the initial 'Detecting network connections...'
// string set in the factory
console.log(exampleFactory.network_interfaces)
// And {{network_interfaces}} and {{factory_state}} in the view keep
// 'Detecting network connections...' as the value for network_interfaces
// permanently
$scope.factory_state = exampleFactory;
$scope.network_interfaces = exampleFactory.network_interfaces;
}]);
So:
The factory seems to be returning a good service object, but I'm not sure why exampleFactory and exampleFactory.network_interfaces would have the different states they do between the controller and factory, and especially within the controller itself (regardless of the order they're called in).
I've attempted a lot of different solutions with the hypothesis that it's an asynch issue, but I would think there'd be no appreciable latency on the getNetworkInterfaces method and if there were that everything is set up correctly for Angular to update the {{network_interfaces}} and {{factory_state}} view bindings once data is returned.
I've also tried wrapping various functions in the factory with $rootScope.$apply as a shot in the dark, but with the same results as above.
I've searched around a lot to discover whatever concept it is I've obviously missed, but I think I'm overlooking something fundamental. How to I get the getNetworkInterfaces() data into my controller in a useful state?
Your assumption in #2 is the problem. There will always be a spin of the JavaScript event loop in between a call to an asynchronous method and the invocation of its callback. If there weren't, all sorts of things would subtly break in clients calling the method. This means you are encountering a common problem in Angular development: that you don't get notified that something changed, because the change didn't happen within the context of Angular's digest cycle.
To fix this: try setting up a watch, then calling $scope.apply() within the getNetworkInterfaces() callback. The apply() is guaranteed to happen outside the digest cycle so you shouldn't get an apply-in-apply error.
Alternatively, post a message to yourself when then callback is done. This is better if you're a student of the "if you're using apply() your code is broken" school of thought.
Finally, consider a Promise that you call after the callback. This doesn't quite fit how you've set up your code.
(Also, why call the async method at all in the factory?)
Try watching on network_interfaces to know when it's modified
exampleApp.controller('MainCtrl', ['$scope', '$http', 'exampleFactory', function($scope, $http, exampleFactory) {
// This outputs the expected array in network_interfaces, identical
// to the console output within the factory
console.log(exampleFactory)
// But then this outputs the initial 'Detecting network connections...'
// string set in the factory
console.log(exampleFactory.network_interfaces)
// And {{network_interfaces}} and {{factory_state}} in the view keep
// 'Detecting network connections...' as the value for network_interfaces
// permanently
$scope.factory_state = exampleFactory;
$scope.network_interfaces = exampleFactory.network_interfaces;
$scope.$watch('factory_state.network_interfaces', function() {
console.log($scope.factory_state)
});
}]);

Using remote data in AngularJS app

Can someone ELI5 how to grab json data via GET in Angular, and have it available to a controller -
I'm currently doing this:
app.factory('dataRepo', function($http) {
return {
getData: function() {
return $http.get('/data.json');
}
};
});
app.controller('mainCtrl', function ($scope, $http, _, dataRepo) {
dataRepo.getData().success(function(d) {
$scope.data= d;
console.log($scope.data)
});
console.log($scope.data);
/* more stuff in here using $scope.data
});
The first console.log call logs the data, the second [outside the success function] logs undefined, and writes prior to the other call.
How do I get the data before the app kicks off, or how do I defer the remainder of the code until `$scope.data is populated?? I've looked at promises, and can get the result I need doing what I know is a really dodgy implementation and I don't want to persist with it
U can register a watcher on data and get notified about every change. If data is an array, the third parameter is a boolean watching objects more deeply.
$scope.$watch('data', function(newValue, oldValue) {
}, /*true*/);
Thats because the $http.get call is asynchronous, aka the script continues without calling the callback directly. Therefore the second call to console.log() is executed first and $scope.data is still undefined at this point. Later when the http request is completed the function block inside success() is executed and $scope.data becomes populated.
Although promises make handeling with asynchronous code much easier it doesn't make it synchronous like you'll generally code on the backend.
A solution could be to place the logic which requires the data inside the success() callback.
If you need the data outside the mainCtrl you can use the $watch method like #angabriel suggested, but this code needs to handle de data in all cases. 1: undefined, 2: populated.
3: changed again?
If you're using angular routes to create the controller (and not ng-controller="mainCtrl")
You can use the resolve property to delay the controller/view until the data is loaded shown here

Clear $scope on logout in Angular js

In my controller I am storing data as $scope.$parent.dossierSummaries = data;
but after log out and login the application $scope.$parent.dossierSummaries retains the same old data.
I am doing this on log out
.success( function( response, status ) {
if ( response.status > 0 ) {
var u = $rootScope.user.username;
$cookieStore.remove('myapp');
$rootScope.user = { username: '', role: 0 };
success(u);
}
else {
error(response.messages);
}
})
.error( function( response, status ) {
error(['There was an error logging you out.']);
});
in angularJS, you shouldn't set the variable directly to a controller but you should retrieve it from a service instead. So whenever you load a controller you should write a init() function to get value of that model. So everytime you will have the correct data from server.
Code example and docs : http://docs.angularjs.org/guide/dev_guide.services.creating_services
Another approach to manually tracking and cleaning things up would be to broadcast a 'logout' event on the rootScope (or other custom event). Then listen for the event either in your controller or in your service to clean up the data.
Broadcast:
$rootScope.broadcast('logout');
Watching for an event (in a service for example):
$rootScope.on('logout',function(){
dossiers = [];
});
I don't think there is any effective way to achieve it. Any object (controller, directive,filter or as a matter of fact any js object) can hold reference to another object (in your case user), and one cannot determine easily who all are holding reference.
The reference would only get release if you do it either explicitly or when the object holder the reference is destroyed.
What you can try is
$rootScope.user.username='';
$rootScope.role=0;
Assuming some object are tracking this specific object the data would be cleared now.
if you don't mind a slight screen flickering on logout, you can refresh the page using a method like this:
$window.location.replace($window.location.toString().split('#')[0]);
this would clear out all the $scope and $rootScope variables, and in my case has solved the issue.
If you want to clear the $scope, I think you can use it's constructor method or proto (prototype), which is used in constructor. I believe you can do that to reset the scope to initial state. If someone knows any more on this, feel free to comment.

angularjs passing variables into controller

I have my angular controller setup like most of the examples shown in the docs such that it is a global function. I assume that the controller class is being called when the angular engine sees the controller tag in the html.
My issue is that i want to pass in a parameter to my controller and i don't know how to do that because I'm not initializing it. I see some answers suggesting the use of ng-init. But my parameter is not a trivial string - it is a complex object that is being loaded by another (non-angular) part of my js. It is also not available right on load but takes a while to come along.
So i need a way to pass this object, when it finally finishes loading, into the controller (or scope) so that the controller can interact with it.
Is this possible?
You can use a service or a factory for this, combined with promises:
You can setup a factory that returns a promise, and create a global function (accessible from 3rd-party JS) to resolve the promise.
Note the $rootScope.$apply() call. Angular won't call the then function of a promise until an $apply cycle. See the $q docs.
app.factory('fromoutside', function($window, $q, $rootScope) {
var deferred = $q.defer();
$window.injectIntoAngularWorld = function(obj) {
deferred.resolve(obj);
$rootScope.$apply();
};
return deferred.promise;
});
And then in your controller, you can ask for the fromoutside service and bind to the data when it arrives:
app.controller('main', function($scope, fromoutside) {
fromoutside.then(function(obj) {
$scope.data = obj;
});
});
And then somewhere outside of Angular:
setTimeout(function() {
window.injectIntoAngularWorld({
A: 1,
B: 2,
C: 3
});
}, 2000);
Here's a fiddle of this.
Personally, I feel this is a little bit cleaner than reaching into an Angular controller via the DOM.
EDIT: Another approach
Mark Rajcok asked in a comment if this could be modified to allow getting data more than once.
Now, getting data more than once could mean incremental updates, or changing the object itself, or other things. But the main things that need to happen are getting the data into the Angular world and then getting the right angular scopes to run their $digests.
In this fiddle, I've shown one way, when you might just be getting updates to an Array from outside of angular.
It uses a similar trick as the promise example above.
Here's the main factory:
app.factory('data', function($window) {
var items = [];
var scopes = [];
$window.addItem = function(item) {
items.push(item);
angular.forEach(scopes, function(scope) {
scope.$digest();
});
};
return {
items: items,
register: function(scope) { scopes.push(scope); }
};
Like the previous example, we attach a function to the $window service (exposing it globally). The new bit is exposing a register function, which controllers that want updates to data should use to register themselves.
When the external JS calls into angular, we just loop over all the registered scopes and run a digest on each to make sure they're updated.
In your non-angular JavaScript, you can get access to the scope associated with a DOM element as follows:
angular.element(someDomElement).scope().someControllerFunction(delayedData);
I assume you can find someDomElement with a jQuery selector or something.

Categories

Resources