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));
}
Related
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 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.
I am creating a game where the first thing that needs to happen is some state is loaded in from an external JSON file - the contents of one of my directives are dependent on this data being available - because of this, I would like to delay applying the directive until after the data has loaded. I have written the following:
window.addEventListener('mythdataLoaded', function (e) {
// Don't try to create characters until mythdata has loaded
quest.directive('character', function() {
return {
restrict: 'A',
scope: {
character: '#'
},
controller: 'CharacterCtrl',
templateUrl: 'partials/character.html',
replace: true,
link: function(scope, element) {
$(document).on('click', '#'+scope.character, function () {
$('#'+scope.character+'-popup').fadeToggle();
});
}
};
});
});
// Load in myth data
var myth_data;
$.getJSON("js/mythdata_playtest.json", function(json) {
myth_data = json;
window.dispatchEvent(new Event('mythdataLoaded'));
});
However, it appears that my directive's link function never runs - I'm thinking this is because angular has already executed the part of it's cycle where directives are compiled/linked by the time this directive gets added. Is there some way to force angular to compile this directive after it is created? I googled around a bit, and some people suggested adding $compile to the link function for similar issues - but the link function is never run, so that doesn't work for this case. Thanks!
It seems to me it would be better to always configure the directive, to do the JSON call in the directive, and attach logic to the element in the JSON call's success handler. This would, if I understand you correctly, do what you want.
AngularJS is meant as a framework, not a library, so using it in the way you mentioned is not recommended. Exactly as you mentioned, AngularJS does a lot of things for you when it runs. AngularJS, by default, runs on document loaded, and your $.getJSON callback arrives after that. When AngularJS runs it does all its magic with compiling the content and all that.
As a sidenote, it's also more the Angular way to use $http over $.getJSON.
I think you're thinking about this the wrong way. A major ideology in angular is that you set up declarative elements and let it react to the state of the scope.
What I think you might want to do is pass in what you need through the directive scope, and use other angular built in directives to hide or show your default ("non directive") state until the scope gets set from the controller for example.
Example:
You want a box to be hidden until an api call comes back. Your directive sets special styles on your element (not hidden). Instead of delaying to dynamically set your directive, you can pass in a scope var with a default value and use something like ng-show="data.ready" in your directive template to handle the actual dom stuff.
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 my angular controller setup like most of the examples shown in the docs such that it is a global function. I assume that the controller class is being called when the angular engine sees the controller tag in the html.
My issue is that i want to pass in a parameter to my controller and i don't know how to do that because I'm not initializing it. I see some answers suggesting the use of ng-init. But my parameter is not a trivial string - it is a complex object that is being loaded by another (non-angular) part of my js. It is also not available right on load but takes a while to come along.
So i need a way to pass this object, when it finally finishes loading, into the controller (or scope) so that the controller can interact with it.
Is this possible?
You can use a service or a factory for this, combined with promises:
You can setup a factory that returns a promise, and create a global function (accessible from 3rd-party JS) to resolve the promise.
Note the $rootScope.$apply() call. Angular won't call the then function of a promise until an $apply cycle. See the $q docs.
app.factory('fromoutside', function($window, $q, $rootScope) {
var deferred = $q.defer();
$window.injectIntoAngularWorld = function(obj) {
deferred.resolve(obj);
$rootScope.$apply();
};
return deferred.promise;
});
And then in your controller, you can ask for the fromoutside service and bind to the data when it arrives:
app.controller('main', function($scope, fromoutside) {
fromoutside.then(function(obj) {
$scope.data = obj;
});
});
And then somewhere outside of Angular:
setTimeout(function() {
window.injectIntoAngularWorld({
A: 1,
B: 2,
C: 3
});
}, 2000);
Here's a fiddle of this.
Personally, I feel this is a little bit cleaner than reaching into an Angular controller via the DOM.
EDIT: Another approach
Mark Rajcok asked in a comment if this could be modified to allow getting data more than once.
Now, getting data more than once could mean incremental updates, or changing the object itself, or other things. But the main things that need to happen are getting the data into the Angular world and then getting the right angular scopes to run their $digests.
In this fiddle, I've shown one way, when you might just be getting updates to an Array from outside of angular.
It uses a similar trick as the promise example above.
Here's the main factory:
app.factory('data', function($window) {
var items = [];
var scopes = [];
$window.addItem = function(item) {
items.push(item);
angular.forEach(scopes, function(scope) {
scope.$digest();
});
};
return {
items: items,
register: function(scope) { scopes.push(scope); }
};
Like the previous example, we attach a function to the $window service (exposing it globally). The new bit is exposing a register function, which controllers that want updates to data should use to register themselves.
When the external JS calls into angular, we just loop over all the registered scopes and run a digest on each to make sure they're updated.
In your non-angular JavaScript, you can get access to the scope associated with a DOM element as follows:
angular.element(someDomElement).scope().someControllerFunction(delayedData);
I assume you can find someDomElement with a jQuery selector or something.