The AngularJS framework has some interesting code solutions. Today we will look at two of them - how scopes and directives work.
The first thing everyone teaches about AngularJS is that directives must interact with the DOM. And most of all a beginner is confused by the process of interaction between scopes, directives and controllers. In this article, we will look at the details of how the scopes work and the life cycle of an Angular application.
If in the next picture you do not understand something - this article is for you.
')
(The article deals with AngularJS 1.3.0)AngularJS uses scopes to abstract directive communication and DOM. There are also areas of visibility at the controller level. Scopes are simple JavaScript objects (plain old JavaScript objects, POJOs). They add a bunch of "internal" properties, which are preceded by one or two $ characters. Those with the $$ prefix do not need to be used in the code too often - usually their use indicates a misunderstanding of the application.
So what are these areas of visibility?
In AngularJS jargon, visibility does not mean what is meant by this in JS code. Usually, a scope is understood to mean a block of code that contains a context, various variables, and so on. For example:
function eat (thing) { console.log(' ' + thing); } function nuts (peanut) { var hazelnut = 'hazelnut';
However, these are not the scopes that AngularJS is talking about.
Inheritance of scopes in AngularJS
The scope in AngularJS is also a context, but only in the understanding of AngularJS. In AngularJS, a scope is associated with an element and all its child elements, and the element is not necessarily directly related to the scope. Elements are assigned scopes in one of three ways.
The first is if the scope is created at the element by the controller or directive.
<nav ng-controller='menuCtrl'>
The second is if the element has no scope, it inherits it from the parent
<nav ng-controller='menuCtrl'> <a ng-click='navigate()'> !</a> </nav>
The third is if the element is not part of the ng-app, then it does not belong to any scope
<head> <h1></h1> </head> <main ng-app='PonyDeli'> <nav ng-controller='menuCtrl'> <a ng-click='navigate()'> !</a> </nav> </main>
To understand what area of ​​visibility an element belongs to, go through the element tree from the inside to the outside, using three rules. Does he create a new scope? Then he is associated with it. Does he have a parent? We check the parent. If it does not enter the ng-app, then no scope.
Calling the internal properties of AngularJS scopes
Let's go through some typical properties. To do this, I will open Chrome and go to the application I'm working on. Then I open the Developer tools to view the properties of the item. Did you know that $ 0 gives
access to the last selected element in the Elements panel? $ 1 - the previous selected item, etc. $ 0 we will use most often.
angular.element wraps each DOM element either in jQuery or in jqLite. After that, you have access to the scope () function, which returns the scope of the element. Combine this with $ 0 and get a frequently used command:
angular.element($0).scope()
Since jQuery is being used, $ ($ 0) .scope () will also work. Now let's see what properties are available in a typical scope - those that are written starting from $.
for (o in $ ($ 0) .scope ()) o [0] == '$' && console.log (o)
Studying the insides of the AngularJS scope
I will list the properties that this command derived, grouped by functionality. Let's start with the simple ones.
$id $root $parent , null, scope == scope.$root $$childHead , null $$childTail , null $$prevSibling , null $$nextSibling , null
Navigating with these properties is inconvenient. $ Parent can sometimes come in handy, but there are always more convenient ways to access parent elements. For example, using the events that we will look at in the next part of the list.
Event Model in AngularJS Scope
The following properties help define events and subscribe to them.
$$listeners , $on(evt, fn) fn evt $emit(evt, args) evt, , $parent, $rootScope $broadcast(evt, args) evt, ,
At startup, event handlers receive the event object and any arguments passed to $ emit or $ broadcast. How can events be used?
The directive can use them to report an important event. In the example, the event is triggered by pressing a button.
angular.module('PonyDeli').directive('food', function () { return { scope: {
I set the namespace events. This prevents the intersection of names, and helps to understand where events come from or what event you subscribe to. Suppose you are interested in analytics and you want to track all food clicks through Mixpanel. Instead of littering the controller or directive, you can make a separate directive to track clicks, which will be a separate thing in itself.
angular.module('PonyDeli').directive('foodTracker', function (mixpanelService) { return { link: function (scope, element, attrs) { scope.$on('food.order, function (e, type) { mixpanelService.track('food-eater', type); }); } }; });
Service implementation is not important here - it would simply serve as a wrapper for the client API from Mixpanel. HTML would look like the one below, and I would add another controller containing all the necessary types of food. To complete the example, I’ll add ng-repeat so that I can list my food without copying code. Simply output them in a foodTypes cycle, which is available in the foodCtrl scope.
<ul ng-app='PonyDeli' ng-controller='foodCtrl' food-tracker> <li food type='type' ng-repeat='type in foodTypes'></li> </ul> angular.module('PonyDeli').controller('foodCtrl', function ($scope) { $scope.foodTypes = ['', '', '']; });
See CodePen for a working example.
But do you need an event to which anything can connect? Wouldn't it be enough service? In this case, you can do so. It can be argued that events are needed because you do not know in advance who else will subscribe to food.order, which means that the use of events is more far-sighted in terms of the development of the application. You can also say that the food tracking directive is not needed because it does not interact with the DOM, but only waits for an event, so it can be replaced with a service.
And these are true remarks, in this case. But when other components need to communicate with food.order, the need for events will become clear. In real life, events are most useful when you need to connect multiple scopes.
Elements on the same level usually have difficulty communicating with each other, and they usually do so through their parent. As a result, this translates into broadcasting from $ rootScope, which is listened to by all who need it:
<body ng-app='PonyDeli'> <div ng-controller='foodCtrl'> <ul food-tracker> <li food type='type' ng-repeat='type in foodTypes'></li> </ul> <button ng-click='deliver()'> !</button> </div> <div ng-controller='deliveryCtrl'> <span ng-show='received'> , . </span> </div> </body>
angular.module('PonyDeli').controller('foodCtrl', function ($rootScope) { $scope.foodTypes = ['', '', '']; $scope.deliver = function (req) { $rootScope.$broadcast('delivery.request', req); }; }); angular.module('PonyDeli').controller('deliveryCtrl', function ($scope) { $scope.$on('delivery.request', function (e, req) { $scope.received = true;
You can also
watch the work on CodePen .
I would say that events should be used when you expect a view to change in response to an event, and services when views are not changing.
If you have two components communicate through $ rootScope, then it is better to use $ rootScope. $ Emit and $ rootScope. $ On instead of $ broadcast. Then the event is distributed only among $ rootScope. $$ listeners, and will not waste time on passing all the $ rootScope child nodes that do not have handlers for this event. In the example, the service uses $ rootScope for events, not limited to a specific scope. It provides a subscribe method for subscribing to listen for events.
angular.module('PonyDeli').factory("notificationService", function ($rootScope) { function notify (data) { $rootScope.$emit("notificationService.update", data); } function listen (fn) { $rootScope.$on("notificationService.update", function (e, data) { fn(data); }); }
And this is also
on CodePen .
Digest
AngularJS data binding works through a loop that
tracks changes and triggers events. There are several methods in the $ digest loop. First, it is scope. $ Digest, recursively digesting changes in the current scope and child areas.
$digest() $$phase – [null, '$apply', '$digest']
Do not run the digest, if you are already in the digest phase - this will lead to unpredictable consequences. What is said about the digest in the
documentation :
Runs all watchers in the current scope and its child areas. Since the observer’s listener can change the model, $ digest () calls observers until their listeners stop executing. This can lead to falling into an infinite loop. Therefore, the function will throw out the error 'Maximum number of iterations reached', if their number exceeds 10.
Usually $ digest () is not called directly from controllers or directives. It is necessary to call $ apply () (usually this is done from within directives), which itself will already cause $ digest ().
So $ digest processes all observers, and then all those observers who are called by previous observers, until they cease to be executed. Two questions remain:
- who are the observers?
- what causes $ digest?
Perhaps you already know what “observer” is and used scope. $ Watch, and maybe even scope. $ WatchCollection. The $$ watchers property contains all observers from the scope.
$watch(watchExp, listener, objectEquality) $watchCollection $$watchers
Observers are the most important aspect of AngularJS, but their call needs to be initiated in order for the data binding to work properly. Example:
<body ng-app='PonyDeli'> <ul ng-controller='foodCtrl'> <li ng-bind='prop'></li> <li ng-bind='dependency'></li> </ul> </body>
angular.module('PonyDeli').controller('foodCtrl', function ($scope) { $scope.prop = ' '; $scope.dependency = ' !'; $scope.$watch('prop', function (value) { $scope.dependency = 'prop "' + value + '"! '; }); setTimeout(function () { $scope.prop = ' '; }, 1000); });
This means that we have an 'initial value', and we expect that the second line of HTML will change to 'prop contains “a different value”! oh wow, isn't it? And one would expect the first line to change to a different value. Why doesn't it change?
Much of what you create in HTML creates an observer as a result. In our case, each ng-bind directive creates an observer for the property. It updates in HTML when prop and dependency change. Therefore, there are three observers in our code — one for each ng-bind, and one for the controller. How does AngularJS know that the property has been updated after a timeout? You can remind him of this by adding a digest call to the timer callback:
setTimeout(function () { $scope.prop = ' '; $scope.$digest(); }, 1000);
I saved two CodePen examples - one
without the $ digest , and the second
with it . But a
better way is to use the $ timeout
service instead of setTimeout. It allows error handling and performs $ apply ().
$timeout(function () { $scope.prop = ' '; }, 1000);
$apply(expr) , $digest $rootScope
Now about the one who causes $ digest. These functions are called by AngularJS itself at strategic locations in the code. They can be called directly, or by calling $ apply (). Most framework directives call these functions. They call observers, and observers update the interface.
Let's look at the list of properties associated with the $ digest loop that can be detected in scope.
$eval(expression, locals) $evalAsync(expression) $$asyncQueue , digest $$postDigest(fn) fn digest $$postDigestQueue $$postDigest(fn)
Here are some more scope properties that deal with its life cycle and are commonly used for internal purposes. But in some cases it may be necessary to create new scopes with $ new.
$$isolateBindings ( , { options: '@megaOptions' } $new(isolate) , $destroy . $$destroyed
In the second part of the article, we will look at directives, isolated scopes, transclusions, anchored functions, compilers, directive controllers, and more.