⬆️ ⬇️

Link, $ observe and $ watch functions in directives executed in the context of AngularJS

When you run your code inside a controller or service, you don’t have to worry about calling $ apply (), since the code runs inside the context of Angular. By this is meant that Angulyar understands that your code is in the process of execution and will perform a dirty check (check-check) after its completion. When you are inside the directive, Angulyr's world view is a bit more limited; now the directive should take care of calling $ apply () (or calling $ apply () with something like $ timeout) when it is necessary to inform Angulyar about changes in the view model (i.e. $ scope). However, determining when to do this is a bit more difficult, because some aspects of the directive are actually executed within the context of Angulyar.



If you have already created your own directives, you can be sure that you have seen one of two messages:



$apply is already in progress.

$digest is already in progress.



These error messages indicate that you are trying to tell Angulyar about the code that he already knows. At best, just see this error in the console. At worst, a recursive fiasco will occur that will break the browser (one of the few flaws in Firebug).

')

Most likely you got this error because you were inside a directive, and tried to tell Angulyar about all the changes in $ scope that occurred inside the directive. And, although the philosophy of your actions is correct, there are parts of the directive that Angulyar has already monitored. In particular, Angulyar already knows about:



- binding function

- $ observe () handlers for attributes

- $ watch () handlers for $ scope



If you make changes to the $ scope within the code that is executed synchronously in the linking function or asynchronously within the $ observe () and $ watch () handlers, then the actions are already performed in the context of Angular. This means that Angulyar will perform a dirty check after the execution of your code and will cause additional $ digest cycles if necessary.



To demonstrate this, I wrote a small directive that tracks the image upload event. If the image is already loaded during the execution of the directive, the handler is called immediately - in the context of Angulyar. If the image has not yet been loaded, the handler is invoked asynchronously and Angular should be explicitly notified of the changes.



Note: $ observe () and $ watch () handlers are shown for demonstration purposes. They do nothing.



Sample code
 <!doctype html> <html ng-app="Demo"> <head> <meta charset="utf-8" /> </head> <body> <ul ng-controller="ListController"> <!--     src- --> <li ng-repeat="image in images"> <p>Loaded: {{ image.complete }}</p> <img ng-src="{{ image.source }}" bn-load="imageLoaded( image )" /> </li> <!--     src- --> <li> <p>Loaded: {{ staticImage.complete }}</p> <img src="4.png" bn-load="imageLoaded( staticImage )" /> </li> </ul> <!--  jQuery  AngularJS --> <script type="text/javascript" src="//code.jquery.com/jquery-1.9.0.min.js"></script> <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js"></script> <script type="text/javascript"> //    var Demo = angular.module( "Demo", [] ); //     Demo.controller("ListController", function( $scope ) { //     $scope.imageLoaded = function( image ) { image.complete = true; }; //   .     //   data-URIs,     $scope.images = [{ complete: false, source: "1.png" }, { complete: false, source: "2.png" }, { complete: false, source: "3.png" }]; //    $scope.staticImage = { complete: false, source: "4.png" }; }); //  ,      Demo.directive( "bnLoad", function() { //   DOM   . function link( $scope, element, attributes ) { //      // $digest     $apply() function handleLoadSync() { logWithPhase( "handleLoad - Sync" ); $scope.$eval( attributes.bnLoad ); } //     //  $digest,     //    function handleLoadAsync() { logWithPhase( "handleLoad - Async" ); $scope.$apply( function() { handleLoadSync(); }); } //         . function logWithPhase( message ) { console.log( message, ":", $scope.$$phase ); } // ,     . //       //    Data URI, //         if ( element[0].src && element[0].complete ) { handleLoadSync(); //     -    // (..    ). } else { element.on( "load.bnLoad", handleLoadAsync ); } //       //  ,   //       attributes.$observe("src", function( srcAttribute ) { logWithPhase( "$observe : " + srcAttribute ); }); //      //       . // :        ; //       . $scope.$watch("( image || staticImage ).complete", function( newValue ) { logWithPhase( "$watch : " + newValue ); }); //      $scope.$on("$destroy", function() { element.off( "load.bnLoad" ); }); } //    return({ link: link, restrict: "A" }); } ); </script> </body> </html> 




As you can see, three images (inside ngRepeat) are loaded dynamically and one is statically loaded. Loading of all four images is monitored using the bnLoad directive and the phases of the life cycle are recorded in the log.



The static image - 4.png - will be loaded by the time the directive is executed and causes the first handler to be triggered, which then displays the following in the console:



  handleLoad - Sync: $ apply
 $ observe: 4.png: $ digest
 $ watch: true: $ digest
 $ observe: 1.png: $ digest
 $ watch: false: $ digest
 $ observe: 2.png: $ digest
 $ watch: false: $ digest
 $ observe: 3.png: $ digest
 $ watch: false: $ digest
 $ observe: 1.png: $ digest
 $ observe: 2.png: $ digest
 $ observe: 3.png: $ digest
 handleLoad - Async: null
 handleLoad - Sync: $ apply
 $ watch: true: $ digest
 handleLoad - Async: null
 handleLoad - Sync: $ apply 
 $ watch: true: $ digest
 handleLoad - Async: null
 handleLoad - Sync: $ apply
 $ watch: true: $ digest 


I know that it is not clear from which side to approach, so I will point out a few key points:



The first response of handleLoadSync () was caused by a static image. Please note that it happened already inside the $ apply phase, in which Angulyar performs a dirty check. This is because it was called in the link function link (), which is already in the context of Angulyar.



All $ observe () and $ watch () handlers are inside the $ digest phase of Angulery’s dirty life cycle. Once there, you don’t need to tell Angulyar about any changes in $ scope. The angular will automatically perform a dirty check after each $ digest cycle.



All images loaded asynchronously called the handleLoadAsync () method, which uses the $ apply () method to tell Angulyar about these changes. That is why all subsequent handleLoadSync () methods are in the $ apply phase - they were called from the handleLoadAsync () handler.



As mentioned earlier, Angulyar's directives are very powerful, but with a subversion you have to puff to put your head back. The execution time in the Angulyar directive is crucial to its functionality, and this is one of the most difficult things to understand correctly. Add something like CSS transitions — which only have partial browser support — and you'll quickly notice that problems with $ digest occur in one browser, but not in another. We hope that this study will help a little better understand the cause of such problems.



Translator's Note: A living example demonstrating the phases of the life cycle of Angulyar

Source: https://habr.com/ru/post/188760/



All Articles