I am working on a large scale Angular app that is tested using selenium webdriver. My concern is that the transcluding takes time, and I need feedback to let me know when transcluding finishes. This would allow me to wait until that trigger has fired to grab additional information. Is there a way to do this? Would something like ng-repeat-end always get processed after everything has been loaded?
There is no way to tell that the DOM has been completely rendered, but you can get an event triggered when the last element was $compiled and added to the DOM with a simple directive:
.directive('last', function() {
return {
link: function(scope) {
if(scope.$last) {
$scope.emit('ngRepeat.finished');
//or really anything you want to do
}
}
}
});
Usage:
<div ng-repeat="item in items" last>
The thing to be aware of is that if "items" is changed, the ng-repeat is rebuilt, so you'll get another event.
I'm unclear on if you want to get an event after an ng-repeat, or if you just want to wait until you can be guaranteed it has finished.
This is a driver wait that waits for loading in jQuery and $http, combined with the digest/render cycle in angular (you can chop out the part that isn't about digest/render if you don't need it). It's wrapped in a commented example of how you'd plug it into a C# selenium driver call, which should translate to whichever language you are using.
/*var pageLoadWait = new WebDriverWait(WebDriver, TimeSpan.FromSeconds(timeout));
pageLoadWait.Until<bool>(
(driver) =>
{
return (bool)JS.ExecuteScript(
#"*/
try {
if (document.readyState !== 'complete') {
return false; // Page not loaded yet
}
if (window.jQuery) {
if (window.jQuery.active) {
return false;
} else if (window.jQuery.ajax && window.jQuery.ajax.active) {
return false;
}
}
if (window.angular) {
if (!window.qa) {
// Used to track the render cycle finish after loading is complete
window.qa = {
doneRendering: false
};
}
// Get the angular injector for this app (change element if necessary)
var injector = window.angular.element('body').injector();
// Store providers to use for these checks
var $rootScope = injector.get('$rootScope');
var $http = injector.get('$http');
var $timeout = injector.get('$timeout');
// Check if digest
if ($rootScope.$$phase === '$apply' || $rootScope.$$phase === '$digest' || $http.pendingRequests.length !== 0 || $rootScope.$$applyAsyncQueue.length > 0) {
window.qa.doneRendering = false;
return false; // Angular digesting or loading data
}
if (!window.qa.doneRendering) {
// Set timeout to mark angular rendering as finished
$timeout(function() {
window.qa.doneRendering = true;
}, 0);
return false;
}
}
return true;
} catch (ex) {
return false;
}
/*");
});*/
EDIT: After comment from Jackie, I noticed some situations where the page didn't fully render, adding || $rootScope.$$applyAsyncQueue.length > 0 appears to have fixed this.
I think the answer above is great but I think the point here is that even though the ng-repeat directive isolates scope, it has a scope variable that will tell you if it is the last one. If you are firing an event or something on the angular side the previous answer is probably the best ides. However, if your motives are purely testing based (like mine) this was enough...
ng-class="{'last': $last}"
Related
I use angularjs and I have a problem with ng-if when I use a function that returns true or false in two API, the browser is freezes
self.isShow= function() {
service.getKioskLogByKioskId([stateParams.kioskId], function (data) {
self.kioskLog = data;
service.getKiosk([stateParams.kioskId], function (kiosk) {
self.kiosk = kiosk;
debugger
if (self.kioskLog.isConnected && self.kiosk.isActive)
return true;
else
return false;
});
});
}
and in html
ng-if="self.isShow()"
Angular's ng-if can't be an async, it expects to get true/false in synchronous manner.
Don't forget that EVERY angular directive creates a "watch" that will be invoked as part of angular's dirty check mechanism, If you make a "heavy" operation on it, it will stuck the browser.
Basically there are 2 wrong things in your code, the first one, your isShow is not returning boolean value at all, (it always returns undefined).
The second one, you are probably making an API call inside the service.getKioskLogByKioskId method.
In order to solve both of the issues, you can make the service.getKioskLogByKioskId call inside the constructor of you controller, (if it is a component there is $onInit lifecycle hook for that), then save the async result on the controller, and use it the view.
It should look something like that:
class MyController {
constructor(stateParams) {
this.stateParams = stateParams;
this.isShow = false; // initial value
}
$onInit() {
const self =this;
service.getKiosk([stateParams.kioskId], function (kiosk) {
self.kiosk = kiosk;
debugger
if (self.kioskLog.isConnected && self.kiosk.isActive)
self.isShow = true;
else
self.isShow = false;
});
}
}
// view.html
<div ng-if="$ctrl.isShow"></div>
I have this example which a basic list with the option to remove items.
When the user tries to remove something, a confirmation is required. But also, to demonstrate which item will be deleted I've changed the table row colour conditionally.
The problem is, I could not make the colour of the selected row change without using $scope.$apply() before the confirm() statement.
$scope.removeEntry = function(index) {
$scope.entries[index].toBeRemoved = true;
$scope.$apply();
if (confirm("Are you sure you want to delete this item?") === true) {
$scope.entries.splice(index, 1);
}else{
$scope.entries[index].toBeRemoved = false;
}
};
But this gives me:
Error: [$rootScope:inprog] $apply already in progress
Am I missing something or is there any better way to do it and preventing this?
I've already tried almost all suggestions on this answer without success.
A solution to your case is to use $timeout from angular: http://plnkr.co/edit/ZDkGMqmwtxh7HSvBEWYp?p=preview
Here is a post on the $apply vs $timeout discussion: Angular $scope.$apply vs $timeout as a safe $apply
$scope.removeEntry = function(index) {
$scope.entries[index].toBeRemoved = true;
$timeout(function() {
if (confirm("Are you sure you want to delete this item?") === true) {
$scope.entries.splice(index, 1);
}else{
$scope.entries[index].toBeRemoved = false;
}
})
};
You must have messed up in implementing it properly.
One more solution to help you out this problem. You could use $evalAsync from Angular.
var app = angular.module('plunker', [])
.controller('ListController', ['$scope', '$timeout', function($scope, $timeout) {
$scope.entries = [{name:"potatoes"},
{name:"tomatoes"},
{name:"flour"},
{name:"sugar"},
{name:"salt"}];
$scope.removeEntry = function(index) {
$scope.entries[index].toBeRemoved = true;
$evalAsync(function() {
if (confirm("Are you sure you want to delete this item?") === true) {
$scope.entries.splice(index, 1);
}else{
$scope.entries[index].toBeRemoved = false;
}
})
};
}]);
Choosing between $evalAsync and $timeout depends on your circumstance:
If code is queued using $evalAsync from a directive, it should run after the DOM has been manipulated by Angular, but before the browser renders.
If code is queued using $evalAsync from a controller, it should run before the DOM has been manipulated by Angular (and before the browser renders) -- rarely do you want this
if code is queued using $timeout, it should run after the DOM has been manipulated by Angular, and after the browser renders (which may cause flicker in some cases)
I'm trying to get my head around sharing data between multiple controllers, but couldn't find out yet how this is supposed to work (the angular way). I have create a Data service that look something like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
/// ... do async stuff
$rootScope.$broadcast("Data::filtered");
},
brush: function(brushed) {
/// ... do async stuff
$rootScope.$broadcast("Data::brushed");
},
load: function() {
/// ... do async stuff
$rootScope.$broadcast("Data::loaded");
}
};
});
Next I want to reuse and update data from this service, so I use it in my controller as follows:
angular.module('myapp.controllers')
.controller('FilterCtrl', function ($scope, $rootScope, DataSet) {
$scope.safeApply = function(fn) {
var phase = this.$root.$$phase;
if(phase == '$apply' || phase == '$digest') {
if(fn && (typeof(fn) === 'function')) {
fn();
}
} else {
this.$apply(fn);
}
};
function updateBrushed() {
$scope.safeApply(function() {
$scope.brushed = DataSet.brushed;
});
};
$scope.brushed = [];
$scope.keepSelected = function() {
DataSet.filter(DataSet.FilterMethod.KEEP);
};
$scope.removeSelected = function() {
DataSet.filter(DataSet.FilterMethod.REMOVE);
};
$scope.$on('Data::brushed', updateBrushed);
$scope.$on('Data::filtered', updateBrushed);
});
The problem I have is basically illustrated by the use of the saveApply call. Basically I got this code from here: https://coderwall.com/p/ngisma. What I don't understand though is why I need it. As far as I can see, I'm 'within' $angular when updating the DataSet service. Nevertheless, the view for the Filter controller doesn't get updated without a call to saveApply ($apply doesn't work at all because than I run into the apply already in progress issue).
So, basically my question boils down to: is the approach above a good way to share data, and if so how is notification of changes in the service supposed to work?
Update: Based on Julian Hollman his suggestion I came to the following solution: http://jsfiddle.net/Ljfadvru/7/. This more or less illustrates the full workflow I was working on, though some of it is automatically induced in the fiddle, as opposed to user-interaction based in my real application. What I like about this approach is that it only sends signals when all data is updated.
Working with references, as suggested by Ed Hinchliffe, is nice as well. However, I'm working on a web visualization framework and I'm expecting tens of thousands of items. Clearing arrays and pushing new elements (which seem to me the consequence of this proposal) is really not feasible (if I understand this paradigm well, it would also result in a re-rendering of my vis for every single change). I stand corrected though if there are suggestions for further improvement.
$broadcast doesn't trigger an $apply and I bet your "async stuff" is not $http from angular.
So something happens outside of angular and angular doesn't know that something has changed.
In my opinion the best thing in that case is to write a wrapper for your async code and trigger $apply after date came back from the backend. Don't do it in the controller.
To be honest, I'm not sure quite sure about exactly what is going on with the digest loops in your particular scenario, but I don't think you are approaching this the right way.
The 'angular' way, is to use promises.
Your service should be more like this:
angular.module('myapp.services')
.service('DataSet', function($rootScope) {
return {
filter: function(filterMethod) {
var returnData = []
$http.get('/some/stuff').then(function(data){
for(i in data){
returnData.push(data[i]);
}
});
return returnData;
}
};
});
This sets up an empty placeholder object (returnData) that can be immediately passed to the controller, but a reference is kept so that when the data returns you can retrospectively populate that object. Because the controller and the service reference the same object, it'll 'just work'.
This way you don't have to worry about dealing with $digest or $apply or $broadcast.
You controller can just call $scope.filtered = DataSet.filter();
EDIT
If you want to be able to access the exact same data from multiple controllers:
angular.module('myapp.services')
.factory('DataSet', function($http) {
var cache = {
filtered: []
}
return {
getFiltered: function(){
if(cache.filtered.length) return cache.filtered;
$http.get('/some/url/').then(function(data){
for(i in data){
cache.filtered.push(data[i]);
}
});
}
};
});
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.
I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.
What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.
Here's the code where I set my watch:
$scope.fieldcontainer = Message.get({id: $scope.entityId },
function(message,headers) {
$scope.$watch('fieldcontainer',
function() {
console.log("model is dirty.");
if ($scope.visibility.saveButton) {
$('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
}
}, true);
});
I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...
The first time the listener is called, the old value and the new value will be identical. So just do this:
$scope.$watch('fieldcontainer', function(newValue, oldValue) {
if (newValue !== oldValue) {
// do whatever you were going to do
}
});
This is actually the way the Angular docs recommend handling it:
After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization
set a flag just before the initial load,
var initializing = true
and then when the first $watch fires, do
$scope.$watch('fieldcontainer', function() {
if (initializing) {
$timeout(function() { initializing = false; });
} else {
// do whatever you were going to do
}
});
The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.
I realize this question has been answered, however I have a suggestion:
$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
if (typeof old_fieldcontainer === 'undefined') return;
// Other code for handling changed object here.
});
Using flags works but has a bit of a code smell to it don't you think?
During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.
$scope.$watch('fieldcontainer',
function(newValue, oldValue) {
if (newValue && oldValue && newValue != oldValue) {
// here what to do
}
}), true;
Just valid the state of the new val:
$scope.$watch('fieldcontainer',function(newVal) {
if(angular.isDefined(newVal)){
//Do something
}
});