Angular binding to async result - angularjs

I have tried various ways but nothing works for me. What I have:
app.controller('someCtrl',
...
$scope.load = function () {
client.loadAgreements().
then(function (response) {
$scope.unapprovedCount = response.data[StatusTypeEnum.Unapproved];
$scope.approvedCount = response.data[StatusTypeEnum.Approved];
$scope.$apply();
});
};
Html part:
<button text="'({{unapprovedCount}})'" ...
<button text="'({{approvedCount}})'"...
When I do:
app.controller('someCtrl',
...
$scope.unapprovedCount = 777;
$scope.load = function () {
client.loadAgreements().
then(function (response) {
$scope.unapprovedCount = response.data[StatusTypeEnum.Unapproved];
$scope.approvedCount = response.data[StatusTypeEnum.Approved];
$scope.$apply();
});
};
I can see 777 when page renders. However I don't see any updates from promise. Variable is assigned but view is not updated. And if I add apply method then I am getting error:
Error: [$rootScope:inprog] $digest already in progress
What else can I try, as I am new to angular and don't know how to do this?
EDIT:
I am not sure if this matters but actually this button uses directive:
<button ng-fas-button-with-color-indicator text"'Approved ({{unapprovedCount}})'" color="red" ...
And text is rendered in some div.

Remove
$scope.$apply();
from your code and check in your console the value of
response.data[StatusTypeEnum.Unapproved]

Why you need the $scope.$apply()?
You have to remove it from your code.
The only need of this function is when you are using a third party lib to manipulate variable from the scope, outside of angular's watch.
When you use apply and the digest cycle is ok, nothing changed, will raise this error.

Related

Assign $scope variables inside a promise then method for angular datatables directive?

I am not sure what the issue is, but it seems the datatables directive is being initialized before my $scope is defined. What's more, it seems the variables getting set twice.
I have a userService that retrieves the column definitions and data from my server. The getUserList() method returns a promise. I use the then() method to set the $scope variables that will be used by the datatables directive. It seems the directive is requesting the variables before the request completes. Also, it seems the variables are getting set twice, because in Chrome's dev console I see the "test" log twice.
If I use static data (not coming from the server) and place the $scope variables outside the getUserList() promise it works fine.
$scope.indexList = function () {
userService.getUserList().then(function (data) {
$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function () {
console.log("test");
var deferred = $q.defer();
deferred.resolve(data.Data);
return deferred.promise;
});
$scope.dtColumns = [];
angular.forEach(data.DataTablesColumns, function (i, v) {
$scope.dtColumns.push(DTColumnBuilder.newColumn(i.Id)
.withTitle(i.DisplayName)
.renderWith(actionsHtml));
});
});
}
This is how I am setting the datatables directive:
<div ng-init="indexList()">
<table datatable="" dt-options="dtOptions" dt-columns="dtColumns"class="row-border hover"></table>
</div>
The directive code is executed as soon as the page loads. And since your $scope variables are defined in the promise, they are not available at the time of page load.
The directive does not wait for the request to complete because requests are async in nature. If you want the directive variable to be updated when the request completes you have to set a $watch(or $watchGroup if you want to watch multiple variables) on the $scope.variables in your link function as follows:
link: function(scope, element, attrs) {
scope.$watch('dtOptions', function(newval, oldval) {
//your directive code involving scope variables here.
})
}
It seems I have to make 2 calls to my server to do what I am after. This works, but I feel there should be a way to make a single call to the server and get back then entire result. After some reading the options and column builders can take a promise.
$scope.indexList = function () {
$scope.dtOptions = DTOptionsBuilder.fromFnPromise(function () {
return userService.getUserList(ngRequestGlobabls.context.organization).then(function(data) {
return data.Data;
});
});
$scope.dtColumns = userService.getUserList(ngRequestGlobabls.context.organization).then(function (data) {
columns = [];
angular.forEach(data.DataTablesColumns, function(i, v) {
columns.push(DTColumnBuilder.newColumn(i.Id).withTitle(i.DisplayName).renderWith(actionsHtml));
});
return columns;
});

How to track events on $scope.$broadcast?

I'm using angular-timer and I'm just a little confused how to track its events. For example, I want to do something after time is up, but I can't see any events on console log.
vm.add20Seconds = function() {
$scope.$broadcast('timer-add-cd-seconds', 20);
}
$scope.$on('timer-add-cd-seconds', function (event, data) {
console.log(data); // 'Some data'
});
The console is empty.
https://github.com/siddii/angular-timer/blob/master/examples/angularjs-add-countdown-seconds.html
As the code given in link is not seems to be updated, I think you changed it to use controllerAs syntax. So your button html will use vm alias while calling controller method. Assuming you used ng-controller="MyAppController as vm"
Markup
<button type="button" ng-click="vm.add20Seconds()">Add 20 Seconds</button>
Else wanted to use $scope in your controller then simply change method to $scope.add20Seconds instead of vm.add20Seconds
Update
To get call a function after 20 seconds over, you could use $timeout service here, that will call and specified callback when mentioned $timeout completed.
Code
vm.add20Seconds = function() {
$scope.$broadcast('timer-add-cd-seconds', 20);
}
var myCallbackAfterTimeout = function(){
//add your code.
}
$scope.$on('timer-add-cd-seconds', function (event, data) {
console.log(data); // 'Some data'
$timeout(myCallbackAfterTimeout, data); //data is nothing but timeout milliseconds
});
Include $timeout dependency in your controller before using it.
if you are looking for a good article about using the scope tree As A Publish And Subscribe (Pub/Sub) mechanism in angularJS please check this link

Errors binding checkbox to $scope

I have a checkbox, like:
<input type="checkbox" ng-model="isPreCheckIn" />
I'm getting isPreCheckin (boolean) from a service which uses $q and either returns from the server or localStorage (if it exists).
The call in the controller looks like:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.isPreCheckin = !canCheckIn ? true : false;
});
And deviceSettings.canCheckIn looks like:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
return dfrd.resolve(localStorage.canCheckIn);
});
return dfrd.promise;
};
So, on first page load, the checkbox doesn't bind correctly to isPreCheckIn; in fact, if I do a {{isPreCheckIn}}, it doesn't either. If I switch off of that page and go back, it works.
It appears that canCheckIn is outside of angular, based on that assumption, you need to wrap your assignment within $scope.apply:
deviceSettings.canCheckIn().then(function (canCheckIn) {
$scope.$apply(function(){
$scope.isPreCheckin = !canCheckIn ? true : false;
});
});
This tells angular to recognize the changes on your $scope and apply to your UI.
I think you should wrap the following in a $apply:
function canCheckIn() {
var dfrd = $q.defer();
LoadSettings().then(function (success) {
scope.$apply(function() {
dfrd.resolve(localStorage.canCheckIn);
}
});
return dfrd.promise;
};
It sounds like a timing issue. You may need to put a resolve clause in your route to give this call time to run and then pass in the result as a DI value. Without knowing which router you are using it is impossible to give you an accurate answer, but you might look at the video on egghead.io regarding routes and resolve.

Angularjs : Why I need to click on a button to get my view updated?

I'm trying to developpe a chrome extension with angularjs and I have a strange behaviour when I try to initialize the $scope with the url of the active tab.
Here the code of my controller:
var app = angular.module('app', ['app.service']);
app.controller('ItemCtrl', function ($scope, chromeHelper) {
$scope.website = "No result!";
// Does not work until I click on something :-/
chromeHelper.getActiveTabDomain(function (domain) {$scope.website = domain; });
});
So when I try to initialize directly the $scope.website member it doesn't succeed but when I click on the button aftewards $scope.website then updates.
I really don't understand why.
Here is the code of my Chromehelper service:
var service = angular.module('app.service', []);
service.factory('chromeHelper', function() {
var chromeHelper = {};
chromeHelper.getActiveTabDomain = function (callback){
chrome.tabs.query({'active': true}, function(tabs){
if(tabs && tabs.length > 0) callback(getDomainFrom(tabs[0].url));
});
};
return chromeHelper;
});
function getDomainFrom(url) {
return url.match(/:\/\/(.[^/]+)/)[1];
}
Thank you very much in advance!
The OP solved the problem (see comment above) by adding $scope.$apply() at the end of the callback:
// Does not work until I click on something :-/
chromeHelper.getActiveTabDomain(function(domain) {
$scope.website = domain;
$scope.$apply(); // <-- adding this line did the trick
});
A short explanation for anyone landing on this page with a similar problem:
From the AngularJS docs on 'scope' (more specifically from the section titled 'Scope Life Cycle'):
Model mutation
For mutations to be properly observed, you should make them only within the scope.$apply(). (Angular APIs do this implicitly, so no extra $apply call is needed when doing synchronous work in controllers, or asynchronous work with $http or $timeout services.
See, also, this short demo.

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