AngularJS Directive Isolate Scope Not Updating Parent - javascript

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

Related

Third party async callback in AngularJS 1.5 Component

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

Is there a way to render a list without using $scope or $digest in angularjs?

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/

AngularJS, apply changes to the view from the factory

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.

Why does $timeout affect how $stateParams is resolved in a ui-bootstrap modal?

Following the ui-router FAQ entry, I recently implemented a generic AngularJS service that turns any ui-router state definition into a state definition that wraps a ui-bootstrap modal.
As a part of this, the resolve object needs to be pushed down from the state definition to the $modal.open() options. The major problem with this is that the $stateParams that is injected into these resolve functions are those of the previous state. After many hacky attempts at solving this, I found that simply wrapping the $modal.open() call in a $timeout block results in the desired behavior.
In general, I'd like to understand why this works the way that it does, whether or not it's an acceptable solution, and if there are any caveats involved. In the past I've been able to resolve several Angular timing issues by simply wrapping a block of code in $timeout, and it makes me nervous since I'm really not sure why it works. I'm guessing that a $timeout forces the block to run after the current digest cycle ends, but I'm not too confident about that.
I created a Plunker that demonstrates this -- if you remove the $timeout and invoke the modal, the parameter will not resolve.
Note: As a caveat, I MUST be able to resolve $stateParams properly in the modal resolves -- I have many existing controllers and state definitions that I'd rather not have to go back and refactor.
Follow-up: I have created ui-router issue 1649 to request a resolution to this issue -- also linked there is a minimal Plunker that only uses $injector.invoke to demonstrate the issue with no modal at all.
This is happening because you have not transitioned yet to the new state when secondparam is being resolved. The $timeout puts your code at the end of the queue, the state transition happens then executes your code with the expected state. You can tell by logging or alerting the current state in your resolve config:
secondparam: ['$stateParams', function($stateParams) {
alert($state.$current.url); //here
return $stateParams.secondparam;
}]
Unfortunately the documentation for onenter and onexit does not clearly tell when in the lifecycle they are invoked: https://github.com/angular-ui/ui-router/wiki#onenter-and-onexit-callbacks. However this post gives some indication:
These callbacks give us the ability to trigger an action on a new view
or before we head out to another state.
I think you'd be better off using a controller and opening your modal there instead of the $timeout (where I believe your context will be window/global).
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
template: 'showing modal',
controller: function($scope, $modal, $state){
var modalInstance = $modal.open({
template: '<div class="alert alert-success" role="alert">Remove the $timeout and this will not resolve: {{secondparam}}</div>',
resolve: {
secondparam: ['$stateParams', function($stateParams) {
console.log($state.$current.url, $stateParams); //you are in the new state
return $stateParams.secondparam; //'secondparam' IS resolved, even without $timeout
}]
},
controller: function($scope, secondparam) {
$scope.secondparam = secondparam;
}
});
modalInstance.result.finally(function() {
$state.go('^');
});
}
});
Note that I had to add <div ui-view></div> to the demo state to get the controller to instantiate (ui-router nested route controller isn't being called).
$stateProvider.state('demo', {
url: '/demo/:firstparam',
template: '<button class="btn btn-primary" ui-sref=".modal({ secondparam: 2 })">Show Modal</button>' +
'<button class="btn btn-primary" ui-sref="demo.contacts">Show Contacts</button><div ui-view></div>'
});
http://plnkr.co/edit/zdSkAsEyIef0tWxSTC5p?p=preview
It could be an unintended behavior, or something "by design" of onEnter.
The solution is to inject $stateParams into the onEnter handler - it would point to the params of the second state (as per documentation) of onEnter.
$stateProvider.state('demo.modal', {
url: '/modal/:secondparam',
onEnter: showModal,
// ...
});
function showModal($state, $stateParams) {
var secondparam = $stateParams.secondparam; // equals 2
...
// BUT, the state has not yet transitioned
var secondParamFromState = $state.params.secondparam; // is undefined
...
}
The $timeout allows the state to transition, and the the resolve of the $modal gets injected the current params, which would be for the second state.
EDIT: updated plunker

angularjs directives using templateUrl - initialisation sequence

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.

Categories

Resources