I try to display error messages from $exceptionHandler factory with a lifespan of 5 seconds. So I have a view with the following code
<div class="messages">
<div class="message" ng-repeat="message in errors">
{{message}}
</div>
</div>
and factory
services.factory('$exceptionHandler',function ($injector) {
return function(exception, cause) {
var $rootScope = $injector.get('$rootScope');
$rootScope.errors = $rootScope.errors || [];
$rootScope.errors.push(exception.message);
setTimeout(function(){
$rootScope.errors.splice(0,1);
}, 5000);
};
});
The messages were displayed fine, but after removing them from array, they are still present on view. I think I need to do something with $digest and $apply, but I don't understand what. Need help!
You are using setTimeout when you should be using angular's $timeout service.
When the callback is triggered by setTimeout angular doesn't know that it has to refresh the html. If you use $timeout instead then it will know to rtun a digest loop after the callback has completed and your page should update correctly.
You could also explicitly trigger the digest loop from inside the callback, sometimes you have to do that, but for timeouts just use the service provided.
Related
Before AngularJS 1.5, in directives or views, the way to make sure a change would be picked up by angular (using $digest cycle) when the change was issued from a third party async callback was to run your code in a $scope.$apply() call.
With components, as far as I understood, the idea is to get rid of $scope and just bind the template to the component's controller. I'm trying to transition to writing components instead of views, without relying on $scope. Let's say I have the following code:
function CompController(TPApi) {
let self = this;
this.endCallback = false;
TPApi.call(data, () => this.endCallback = true );
}
angular.module('app', []).component('myComp', {
controller: CompController,
template: '<div><span ng-show="$ctrl.endCallback">Callback Called</span></div>'
})
The problem here is that ng-show is double binded to the controller, but without using $scope.$apply() for the callback, the change won't be picked up by the ng-show since the $digest cycle won't be triggered. This is very much an annoyance because I would have to then inject $scope in the controller and call $apply, but that defeats the purpose of relying on $scope in the first place.
I guess a way would be to encapsulate the TPApi with the $q service thus making sure the $digest cycle is called when the callback is issued. But what if at some point I want to transition to using the new native Promise API instead of $q?
Is there a smart way to do this without triggering $digest or is angular 1 just inherently flawed because of $scope and $digest?
Since you are calling a third party API, You will have to let angular to update and recognize the new data arrived. If you don't want to use $scope, you can wrap your code with $timeout (Which triggers the digest cycle for you)
function CompController(TPApi) {
let self = this;
this.endCallback = false;
TPApi.call(data, () => $timeout(() => this.endCallback = true, 0));
}
In my controller I have this:
myApp.controller(function(){
var list;
for (var i in data) { // This has more than 5000 objects
list[i] = new MyObject(data[i]);
}
// At this point, it is very fast to populate the list
$scope.list = list;
$scope.$apply() // It is here where it hangs for a long time and freezes the app
})
Is there a way to avoid this? In my view I'm not doing any changes to those objects. I just have to display them.
Since you are manipulating your list within a controller, you don't need to call $scope.$apply().
Angular has made sure of one thing, that all its directives and code
that is wrapped inside of angulars context, a $apply() cycle is called
within the digest loop that it runs continuously.
So in your case, the controller is basically wrapped inside the angulars context, which results in implicit calling of the digest loop which invokes the $apply() function, thereby resulting in your view being updated.
Note: If you wish to call $apply manually, then it would be better if you wrap your list inside of $apply() and invoke it with a 1ms delay, so as to not get a digest loop is already running error:
$scope.$apply(function(
$scope.list = list;
));
For more information you can refer the following link:
Angular JS Apply and Digest Cycle
https://www.sitepoint.com/understanding-angulars-apply-digest/
I've got a directive that has a model bound 2-ways, when calling a save() method via ng-click the parent scope isn't updated unless I call $scope.$apply() which then throws the $apply already in progress error.
I'm using ngResource, and the event has a listener calling $scope.model.$save();
Is there a work-around for this? Or am I doing something completely wrong?
.directive('editable', function(){
return {
restrict: 'AE',
templateUrl: '/assets/partials/editable.html',
scope: {
value: '=editable',
field: '#fieldType'
},
controller: function($scope){
...
$scope.save = function(){
$scope.value = $scope.editor.value;
$scope.$emit('saved');
$scope.toggleEditor();
};
}
};
})
UPDATE
It looks like it is updating the parent after all but that the emit is being fired before the digest has finished completing. I can force it to the end of the stack using $timeout but it feels a bit hacky. Is there a better way?
$scope.$on('saved', function(){
$timeout(function(){
$scope.contact.$update();
}, 0);
});
How are you calling $scope.save ? If you use one of the angular directives, like ng-click, angular will automatically run a digest cycle for you and therefore you don't need to call $scope.$apply(). Internally, angular checks if a digest is already in progress before starting another cycle, so it will handle the issue of digest already in progress for you if you use one of the built-in directives. For example, put this in your directive's template...
<button ng-click="save()">Save</button>
If you need to call save from the controller or link function, you can do a little hack to prevent the 'digest already in progress' error. Use the $timeout service to defer the call to the end of the call stack...
$scope.save();
$timeout(function() {
$scope.$apply();
}, 0);
We are setting the timeout to 0, so there is no real delay, but this is still enough to push the $apply call to the end of the current call stack, which allows the digest that is in progress to finish first and prevents the error. This is not ideal and could imply a larger issue with your design, but sometimes you just have to make it work
I have scenario where I want to send a broadcast event in one controller and have the controller for a directive receive the message. The event is sent immediately on controller startup, and the issue is that because the directive is loading the view using templateUrl, it happens asynchronously, so the event is broadcast before the directive controller is intialised. This problem doesn't happen if the view is in the main page body, but I guess there could still be an issue with controller initialisation order.
I am using the following code in my main controller:
$rootScope.$broadcast("event:myevent");
I have reproduced the issue here: http://jsfiddle.net/jugglingcats/7Wf8N.
You can see in the Javascript console that the main controller is initialised before the controller for the directive, and it never sees the event.
So my question is whether there is a way to wait until all controllers are initialised before broadcasting an event?
Many thanks
I have created a working version. I actually feel that it is a very unclean way to do it, but I could not come up with something better: http://jsfiddle.net/7Wf8N/3/
What I did is this: In the directive I added some code which will increase a counter in $rootScope upon initialization. I use a counter because as you said, you want to wait for more than one controller:
$rootScope.initialized = ( $rootScope.initialized||0 ) +1;
In the "RegularCtrl" I added a watch on this counter and if the counter reaches the correct value (everything is initialized) I send the event:
$rootScope.$watch('initialized', function() {
if ( $rootScope.initialized == 1 ) {
$rootScope.$broadcast("event:myevent");
}
});
Are you ng-view? If so, you have the $viewContentLoaded event available. This will fire after all the dom is loaded.
http://docs.angularjs.org/api/ngRoute.directive:ngView
function MyCtrl($scope, $rootScope) {
$scope.$on('$viewContentLoaded', function() {
$rootScope.$broadcast('event:myevent');
});
}
If you aren't using ng-view, you could just set a variable and use data-binding to your directive.
I am new to AngularJS and am having trouble to get resource's promise to re-render the view when the data is loaded. This results in the {{}} getting replaced with blanks (from the promise) but then the view just stays that way.
I have seen a lot of examples of problems with promises being solved with $apply, $digest, $watch etc., and am open to these suggestions be would love to know the root problem with my approach. Here is my controller code
<div ng-controller='pook'>
<p>There are {{posts.length}} posts</p>
<div ng-repeat='post in posts'>
<h3>{{post.title}}</h3>
<div>{{post.text}}</div>
<a href='/readPost/{{post.id}}'>More</a>
| -
<a href='/editPost/{{post.id}}'>Edit</a>
| -
<a href='/deletePost/{{post.id}}'>Delete</a>
</div>
</div>
<script type='text/javascript'>function pook($scope, $http, $route, $routeParams, $resource)
{
/*Approach #1 does work
$http.post('/').success(function(data, status, headers, config)
{
$scope.posts = data.posts;
});
*/
var User = $resource('/');
/*Approach #2 does work
User.save('', function(data)
{
$scope.posts = data.posts;
})
*/
//Approach #3 does NOT work -> renders blanks and never re-renders on loaded data
$scope.posts = User.save().posts
}
</script>
My loaded scripts are
//ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.min.js'
//ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular-resource.min.js'
Description of the promise behavior that I want is here
http://docs.angularjs.org/api/ngResource.$resource
Change your fetch to:
var User = $resource('/');
$scope.postsResponse = User.$save();
and your bind to:
<div ng-repeat='post in posts'>
<h3>{{postResponse.post.title}}</h3>
<div>{{postResponse.post.text}}</div>
</div>
As angular is based on promises, it is possible to bind the response of an $http or $resource action directly to the HTML, and when it gets resolved, angular changes the properties.
If possible, remove the wrapper from the server response ({posts: []} to []). If you do this, you'll need to hint the resource that the response is an array. Look at the docs for more info.
The error happens because when you call a method on the resource, it returns a hollow object, so there is no .posts at it. But if you bind it to your code, angular will wait until it gets there.
This is all you should do at front end, are you sure the server answers what is expected? Are you sure you need a post to retrieve the data?