AngularJS - Watch service changes not updating view - angularjs

Im working on angularjs 1.4. Im trying to have some frontend-cache collection that updates the view when new data is inserted. I have checked other answers from here Angularjs watch service object but I believe Im not overwriting the array, meaning that the reference is the same.
The code is quite simple:
(function(){
var appCtrl = function($scope, $timeout, SessionSvc){
$scope.sessions = {};
$scope.sessions.list = SessionSvc._cache;
// Simulate putting data asynchronously
setTimeout(function(){
console.log('something more triggered');
SessionSvc._cache.push({domain: "something more"});
}, 2000);
// Watch when service has been updated
$scope.$watch(function(){
console.log('Watching...');
return SessionSvc._cache;
}, function(){
console.log('Modified');
}, true);
};
var SessionSvc = function(){
this._cache = [{domain: 'something'}];
};
angular.module('AppModule', [])
.service('SessionSvc', SessionSvc)
.controller('appCtrl', appCtrl);
})();
I thought that the dirty checking would have to catch the changes without using any watcher. Still I put the watcher to check if anything gets executed once the setTimeout function is triggered. I just dont see that the change is detected.
Here is the jsbin. Im really not understanding sth or doing a really rockie mistake.

You need to put $scope.$apply(); at the bottom of your timeout to trigger an update. Alternatively you can use the injectable $timeout service instead of setTimeout and $apply will automatically get called.
jsbin

Related

Utilising $scope.apply()

I have the following which works fine, drawing info from a RESTful api feed
app.controller('servicesController', ['$scope', '$location', '$http', '$interval',
function($scope, $location, $http, $interval) {
var getData = function() {
// Initialize $scope using the value of the model attribute, e.g.,
$scope.url = "https://(remote link to JSON api)";
$http.get($scope.url).success(function(data) {
$scope.listOfServices = data.runningServices; // get data from json
});
};
getData();
$interval(getData(), 10000);
}
]);
However my view is not updating every 10 seconds as expected. I have read that I need to use $scope.apply() somewhere in this above code.
I tried placing the following (in the appropriate place above)
$http.get($scope.url).success(function(data) {
$scope.listOfServices = data.runningServices; // get data from json
$scope.apply(); //I also tried $scope.runningServices.apply()
});
$scope.apply is not your problem, the scope will be digested automatically at the end of the $http request and $interval. Certain actions automatically "inform" Angular that the scope may have changed and trigger a digest; only if you're writing "non-Angular" code may you have to explicitly trigger a scope digest, since otherwise Angular wouldn't notice any changes.
No, your issue is that you're calling getData(), and then have its return value (undefined) execute every ten seconds. Which is obviously nonsense. You just want to pass the function itself to $interval:
$interval(getData, 10000);
// look ma, ^^^^^, no parentheses

setInterval inside ng-controller won't work without $http

I got this strange problem. For some bizzare reason, my setInterval won't work without $http.get() inside it. Currently, $http is set as a dependency for controller hosting said interval. But if I get rid of them (both dependency and call itself) setInterval stops working. I have no idea how to fix this.
Here is code for the controller
main.controller('timeCtrl', function($scope, $http, clockService) {
$scope.time = clockService.timeBase();
setInterval(function() {
$http.get();
$scope.time = clockService.timeBase();
}, 500);
});
After removing the dependancy, it looks like this
main.controller('timeCtrl', function($scope, clockService) {
$scope.time = clockService.timeBase();
setInterval(function() {
$scope.time = clockService.timeBase();
}, 500);
});
But it doesn't work. What the heck is wrong?
setInterval is JS function and it won't trigger digest cycle so angular won't see that something have changed.
try using it's angular version - $interval
P.S. when a promise is resolved it triggers digest cycle so that call to $http did the job, but it's obviously not the way to do what you want

Why is angular $apply required here?

Please consider the following angularjs code for a controller:
(function (app) {
var controller = function ($scope, $state, datacontext) {
$scope.$parent.manageTitle = "Account Management";
$scope.accounts = [];
var init = function () {
getRecords();
};
var getRecords = function () {
return datacontext.getAccounts().then(function (data) {
$scope.$apply(function () {
$scope.accounts = data;
});
});
};
init();
};
app.controller("accountsCtrl", ["$scope", "$state", "datacontext", controller]);
})(angular.module("app"));
Removing the $scope.$apply wrapper and leaving just the "$scope.accounts = data" in the getRecords method breaks the code. The data is retrieved but the ng-repeat directive in the html is not automatically updated. I'm trying to get my arms around the entire $apply/$digest model, but it sure seems to be that the $apply should NOT be required in this case.
Am I doing something wrong?
Thanks.
<------------------------------------------ EDIT ---------------------------------------->
Ok, thanks for the responses. Here is the datacontext. It uses Breeze. I still can't figure out what the problem is - - I just don't see why $apply is required in the code, above.
(function (app) {
var datacontext = function () {
'use strict';
breeze.config.initializeAdapterInstance('modelLibrary', 'backingStore', true);
breeze.config.initializeAdapterInstance("ajax", "angular", true);
breeze.NamingConvention.camelCase.setAsDefault();
var service;
var manager = new breeze.EntityManager('api/ProximityApi');
var entityQuery = breeze.EntityQuery;
var queryFailed = function (error) {
};
var querySuccess = function (data) {
return data.results;
};
var getAccounts = function () {
var orderBy = 'accountName';
return entityQuery.from('Accounts')
.select('id, accountName')
.orderBy(orderBy)
.using(manager)
.execute()
.then(querySuccess, queryFailed);
};
service = {
getAccounts: getAccounts
};
return service;
};
app.factory('datacontext', [datacontext]);
})(angular.module('app'));
Thanks again!
Thanks for your answers. Jared - you're right on the money. By default, Breeze does not use angular $q promises, but uses third-party Q.js promises instead. Therefore, I needed $apply to synchronize the VM to the view. Recently however, the Breeze folks created angular.breeze.js, which allows the Breeze code to use angular promises, instead. By including the angular.breeze module in the application, all Breeze code will use native angular promises and $http instead.
This solved my problem and I could remove the $apply call.
See: http://www.breezejs.com/documentation/breeze-angular-service
The reason that you need to use the $apply function is the result of using Breeze to to return the data. the $apply function is used to get angular to run a digest on all the internal watches and update the scope accordingly. This is not needed when all changes occur in the angular scope as it does this digest automatically. In your code, because you are using Breeze the changes are taking place outside the angular scope, thus you will need to get angular to manually run the digest, and this is true for anything that takes place out side of angular (jQuery, other frameworks ect...). It is true that Breeze is using promises to update the data, however Angular does not know how to handle the changes after the promise returns because it is out side the scope. If you were using an angular service with promises then the view would be updated automatically. If your code is working correctly as is then it would be the correct way to use $apply in this way.
The only thing I might suggest is to change the way you are calling the apply to make sure that it will only run if another digest is not currently in progress as this can cause digest errors. I suggest you call the function as such:
if(!$scope.$$phase){$scope.$apply(function () {
$scope.accounts = data;
});
Or the other option would be to write a custom wrapper around the $apply function like this SafeApply

What do I need to do to get $broadcast running?

I have been working with the excelent ngStorage plugin for angular.
When setting it up you can declare a $scope-node connected to the localstorage like this:
$scope.$store = $localStorage;
$scope.$store is now accessible in all controllers etc.
I want to remove some stuff from localstorage and access it using broadcast instead.
In my init I performed:
$scope.taskarr = [];
$rootScope.$broadcast('taskarrbroad',$scope.taskarr);
What is required in order to add, remove and $watch this array, none of the mentioned seem to work.
Here, nothing happens
controller('textController', function($scope,$routeParams){
$scope.$watch('taskarrbroad.length', function(){
console.log($scope.taskarr.map(function(task){
return task.content;
}).join('\n'));
})
})
Here I can access $scope.taskarr and update it, but the view isn't updated. $scope.$apply() didn't help either (the timeout is because it's already within a digest.
controller('stateSwitchController', function($scope, $routeParams, $timeout){
$scope.taskarr = $scope.$store[$routeParams.state].taskarr || [];
console.log($scope.taskarr);
$timeout(function() {
$scope.$apply();
})
}).
$broadcast is a way to send events to other parts of your application. When you broadcast an event, someone else has to listen to that even with $on(). Something like:
// Some controller
$rootScope.$broadcast('my-event', eventData);
// Some other controller
$scope.$on('my-event', function() {
console.log('my-event fired!')
});
$watch is something else, it's not an event listener per se, it's a way to attach a function that gets called when that value changes, and that value has to be on the scope. So your watch should look like this:
$scope.$watch('taskarr.length', function(){
});
Since you've named the array taskarr on the scope.

angularfireCollection: know when the data is fully loaded

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;
}

Resources