How can I prevent $apply already in progress on this case? - angularjs

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)

Related

AngularJS: Retrieve mysql data in electron and publish it to AngularJS scope

I'm trying to retrieve a list of data from mysql database by using electron and bind it to a list in the controllers scope. I'm using mysql2. Here is my controller:
$scope.carList = [];
mysql.execute("SELECT * FROM cars").spread(function(results){
$scope.carList = results;
console.log(results);
})
I do get the results back, but the in the view carList remains empty. How can I solve this problem?
I just added a button to my view and bound it to a check function like this:
$scope.check = function(){
console.log($scope.carList);
}
After I click on the button, my list in the views gets populated. Now my question would be how can I have my list populated on the start of the controller rather than wait for an event ro make it happen?
I think mysql.execute("").spread(fn) promise is not a part of the AngularJS digest cycle. You did not provide enough code to fully reproduce your problem but I think by triggering a new digest cycle it should work for you. E.g. try it with $timeout which triggers a new digest cycle.
$scope.carList = [];
mysql.execute("SELECT * FROM cars").spread(function(results){
$timeout(function () {
$scope.carList = results;
});
})
I would prefer to create a AngularJS service which handles your electron mysql in a nice way. You could globally apply your $scopes in it, right after finishing your mysql procedures which are not a part of your digest cycle.
Approach by using AngularJS promises
var myApp = angular.module('myApp', []);
myApp.controller('MyCtrl', function($scope, $q) {
$scope.carList = [];
getCars.then(function(cars) {
$scope.carList = cars;
});
function getCars() {
var deferred = $q.defer();
mysql.execute("SELECT * FROM cars").spread(function(results) {
deferred.resolve(results);
});
return deferred.promise;
}
});

AngularJS - Watch service changes not updating view

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

How to update view angularjs from data server (api/nodejs/mongodb) by click

I would like to know how can I update a view from data server after an update with ng-click.
I use Daftmonk Mean stack generator.
I opened an other topic with code but no answer, maybe I'm on the wrong way update/refresh angularjs ng-repeat with data from server
check out $watch and $apply
$watch will watch for a change and call $apply with a button
function MyController($scope) {
$scope.myVar = 1;
$scope.$watch('myVar', function() {
alert('hey, myVar has changed!');
});
$scope.changeValueButton = function() {
$scope.myVar = 2; // This will trigger $watch expression to kick in
};
$scope.updateButton = function() {
$scope.apply();
};
}

How can I tell when rendering has completed on ngRepeat

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

How to trigger a method when Angular is done adding scope updates to the DOM?

I am looking for a way to execute code when after I add changes to a $scope variable, in this case $scope.results. I need to do this in order to call some legacy code that requires the items to be in the DOM before it can execute.
My real code is triggering an AJAX call, and updating a scope variable in order to update the ui. So I currently my code is executing immediately after I push to the scope, but the legacy code is failing because the dom elements are not available yet.
I could add an ugly delay with setTimeout(), but that doesn't guarantee that the DOM is truly ready.
My question is, is there any ways I can bind to a "rendered" like event?
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [{id: 1, name: "one"},{id: 2, name: "two"},{id: 3, name: "three"}];
$scope.results = [];
$scope.loadResults = function(){
for(var i=0; i < resultsToLoad.length; i++){
$scope.results.push(resultsToLoad[i]);
}
}
function doneAddingToDom(){
// do something awesome like trigger a service call to log
}
}]);
angular.bootstrap(document, ['myApp']);
Link to simulated code: http://jsfiddle.net/acolchado/BhApF/5/
Thanks in Advance!
The $evalAsync queue is used to schedule work which needs to occur outside of current stack frame, but before the browser's view render. -- http://docs.angularjs.org/guide/concepts#runtime
Okay, so what's a "stack frame"? A Github comment reveals more:
if you enqueue from a controller then it will be before, but if you enqueue from directive then it will be after. -- https://github.com/angular/angular.js/issues/734#issuecomment-3675158
Above, Misko is discussing when code that is queued for execution by $evalAsync is run, in relation to when the DOM is updated by Angular. I suggest reading the two Github comments before as well, to get the full context.
So 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 you need to run something after the browser renders, or after a controller updates a model, use $timeout(..., 0);
See also https://stackoverflow.com/a/13619324/215945, which also has an example fiddle that uses $evalAsync().
I forked your fiddle.
http://jsfiddle.net/xGCmp/7/
I added a directive called emit-when. It takes two parameters. The event to be emitted and the condition that has to be met for the event to be emitted. This works because when the link function is executed in the directive, we know that the element has been rendered in the DOM. My solution is to emit an event when the last item in the ng-repeat has been rendered.
If we had an all Angular solution, I would not recommend doing this. It is kind of hacky. But, it might be an okey solution for handling the type of legacy code that you mention.
var myApp = angular.module('myApp', []);
myApp.controller("myController", ['$scope', function($scope){
var resultsToLoad = [
{id: 1, name: "one"},
{id: 2, name: "two"},
{id: 3, name: "three"}
];
function doneAddingToDom() {
console.log(document.getElementById('renderedList').children.length);
}
$scope.results = [];
$scope.loadResults = function(){
$scope.results = resultsToLoad;
// If run doneAddingToDom here, we will find 0 list elements in the DOM. Check console.
doneAddingToDom();
}
// If we run on doneAddingToDom here, we will find 3 list elements in the DOM.
$scope.$on('allRendered', doneAddingToDom);
}]);
myApp.directive("emitWhen", function(){
return {
restrict: 'A',
link: function(scope, element, attrs) {
var params = scope.$eval(attrs.emitWhen),
event = params.event,
condition = params.condition;
if(condition){
scope.$emit(event);
}
}
}
});
angular.bootstrap(document, ['myApp']);
Using timeout is not the correct way to do this. Use a directive to add/manipulate the DOM. If you do use timeout make sure to use $timeout which is hooked into Angular (for example returns a promise).
If you're like me, you'll notice that in many instances $timeout with a wait of 0 runs well before the DOM is truly stable and completely static. When I want the DOM to be stable, I want it to be stable gosh dang it. And so the solution I've come across is to set a watcher on the element (or as in the example below the entire document), for the "DOMSubtreeModified" event. Once I've waited 500 milliseconds and there have been no DOM changes, I broadcast an event like "domRendered".
IE:
//todo: Inject $rootScope and $window,
//Every call to $window.setTimeout will use this function
var broadcast = function () {};
if (document.addEventListener) {
document.addEventListener("DOMSubtreeModified", function (e) {
//If less than 500 milliseconds have passed, the previous broadcast will be cleared.
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
//This will only fire after 500 ms have passed with no changes
$rootScope.$broadcast('domRendered')
}, 500)
});
//IE stupidity
} else {
document.attachEvent("DOMSubtreeModified", function (e) {
clearTimeout(broadcast)
broadcast = $window.setTimeout(function () {
$rootScope.$broadcast('domRendered')
}, 500)
});
}
This event can be hooked into, like all broadcasts, like so:
$rootScope.$on("domRendered", function(){
//do something
})
I had a custom directive and I needed the resulting height() property of the element inside my directive which meant I needed to read it after angular had run the entire $digest and the browser had flowed out the layout.
In the link function of my directive;
This didn't work reliably, not nearly late enough;
scope.$watch(function() {});
This was still not quite late enough;
scope.$evalAsync(function() {});
The following seemed to work (even with 0ms on Chrome) where curiously even ẁindow.setTimeout() with scope.$apply() did not;
$timeout(function() {}, 0);
Flicker was a concern though, so in the end I resorted to using requestAnimationFrame() with fallback to $timeout inside my directive (with appropriate vendor prefixes as appropriate). Simplified, this essentially looks like;
scope.$watch("someBoundPropertyIexpectWillAlterLayout", function(n,o) {
$window.requestAnimationFrame(function() {
scope.$apply(function() {
scope.height = element.height(); // OK, this seems to be accurate for the layout
});
});
});
Then of course I can just use a;
scope.$watch("height", function() {
// Adjust view model based on new layout metrics
});
interval works for me,for example:
interval = $interval(function() {
if ($("#target").children().length === 0) {
return;
}
doSomething();
$interval.cancel(interval);
}, 0);

Resources