In the first part we went over all the main problems of the transition to the new version, and in this we will touch on why we did it.
As mentioned earlier, the main reason for the transition can be a significant increase in the speed of the application: 4.3 times faster manipulations with DOM and 3.5 times faster $digest
cycles (compared to 1.2), as Jeff Cross and Brian Ford stated at ngEurope conference .
However, this speed is for the most part acquired not through internal optimizations and magic, but by providing tools that allow you to write code more efficiently.
Let's look at these tools!
It’s not news that Angulyar spends a significant amount of resources on information that facilitates debugging, such as adding classes.
DOM elements (for example, the ng-binding
and ng-isolated-scope
classes) or attaching various methods to access the scope
(for example, .scope()
and .isolateScope()
) to them.
All this is useful and necessary for the work of tools such as Protractor and Batarang, but is this data needed in production?
Starting from version 1.3 , debug info can be disabled:
app.config(['$compileProvider', function ($compileProvider) { $compileProvider.debugInfoEnabled(false); }]);
But what to do if we need to launch production, and debug is disabled?
Here, the .reloadWithDebugInfo()
method of the angular
object will save us, and since the angular
object is global, we can execute this code simply from the console:
angular.reloadWithDebugInfo();
With version 1.3 , the $applyAsync
service came, in many respects similar in its mechanics to the already existing $evalAsync
service:
Simply put, it adds an expression to the queue, then it waits (setsTimeout (..., 0), in modern browsers it is about 10 milliseconds), and if no other expression is added to the queue in the past time, it starts$rootScope.$digest()
.
This allows you to run multiple parallel expressions that affect the DOM, being sure that they will run in the same $ digest cycle and not worrying about the frequent use of $ apply.
What is the difference from $ evalAsync?
The main difference is that $applyAsync
itself executes the entire expression queue at the beginning of the $digest
loop, before dirty checking, which allows it to be executed only once, while the $evalAsync
runs during a dirty check (more precisely, in the the beginning of the dirty check loop), and any expression added to the queue (outside $ watch) will start the $ digest loop again, which will lead to the repeated execution of the expression in the same $digest
bit later.
However, the true benefits of using this tool can be seen in the internal services of the angular, for example, in $httpProvider
.
The main difference between the $http
service and other ways to make an XHR request is the $apply
call when it is completed.
The problem lies in the set of parallel queries, each of which causes $apply
at its completion, which leads to brakes.
The problem is solved with the arrival of $applyAsync
in version 1.3 :
app.config(function ($httpProvider) { $httpProvider.useApplyAsync(true); });
This code includes the use of the $applyAsync
inside $httpProvider
.
This allows you to run $ apply only once, when all promises of simultaneous queries will be resolved
, which gives a significant performance boost.
One of the main performance problems in AngularJS is the huge amount of $watch
's due to the fact that two-way binding is applied to all expressions, but not all data require it.
For static data, one-way binding is sufficient, without hanging yesterday, and before this was solved by custom directives, but everything changed in version 1.3 .
Starting with version 1.3 , a new syntax is available in the form ::
at the beginning of an expression.
Any expression starting with ::
will be perceived as one-way binding and will no longer be monitored (unwatch) as soon as the data in the expression becomes stable and passes the first $digest
cycle.
For example:
{{:: foo }} <button ng-bind=":: foo"></button> <ul> <li ng-repeat=":: foo in bar"></li> </ul> <custom-directive two-way-bind-property=":: foo"><custom-directive>
Data stability:
Data is considered unstable as long as it is undefined
. Any other data, be it NaN
, false
, ''
, []
or null
, is considered stable and will result in unwatch
expressions.
This is necessary for static data that is not available during the first $ digest cycle.
For example, if the data comes from the server, you can simply keep the variable value undefined
while waiting for a request. All this time, the pattern of this expression will live and only after setting data other than undefined
, will it give ends.
Treat expressions with ::
as a constant: once it is set, it is no longer possible to change it.
Update non-updateable:
Suppose we have an expression that is not updated in 99 cases out of 100 (that is, we didn’t fall for it), but sometimes it is required. How to be? I wondered if the bind-once expression could be updated by force.
No, you can't :) However, you can write your own attribute directive, which will force the entire directive to be re-compiled in response to certain events. An example is available here .
This part of the performance ends, and you can talk about just nice buns:
Version 1.3 presented in addition to the ng-model
auxiliary directive ng-model-options
, which is responsible for when the model is updated.
The update time depends on two factors:
1) updateOn
- special events (events) for which the model is updated, for example, it can be blur
, click
or some kind of custom event. The default is always default
, meaning that each control will use its own event. If you want to extend the standard event by adding your {event: "default customEvent"}
, do not forget to add to the default
list, for example: {event: "default customEvent"}
.
2) debounce
- delay in updating the model in anticipation of new data (default 0, i.e. instantly). If you specify the {debounce: 300}
input and enter 3 characters with an interval of less than 300 milliseconds, your model (and therefore various modifiers / validators) will be updated only once. In addition, debounce
can be combined with events by specifying a delay for each of them, for example: {event: "default customEvent", debounce: {default: 0, customEvent: 400}}
.
This allows us to get rid of many bicycles (farewell, setTimeout / clearTimeout) and significantly improves performance (after all, we get rid of the useless restart of $digest
, and accordingly all $watchers
), and also reduces the number of false positives for asynchronous validation (however, $http
the service is smart enough not to send spam with queries, but wait for stable data).
The allowInvalid flag allows you to set $modelValue
, even if the value is not valid for it (by default, while the value is not valid, the model is written undefined
, which does not allow, for example, to know the intermediate value)
The setterGetter flag allows you to set your own function as ngModel
, a kind of intermediary between ngModel.modelValue
and ngModel.viewValue
acting as a setter and getter. A live example on plunker .
timezone allows you to set the time zone for time related controls ( date
or time
), for example, '+0430'
will mean '4 hours 30 minutes GTM'. By default, the browser time zone is taken.
Sometimes when manually recording a model, you need to ignore the delay set in the events and perform the update instantly. For this there is a method ngModelCtrl.$commitViewValue()
.
If it is necessary to undo all changes and hangs in the debounce
process, there is a $rollbackViewValue()
method (formerly $cancelUpdate()
), which customizes the view to the current state of the model.
As an example of using this feature, official documentation suggests inpat, changes in which can be rolled back by pressing ESC.
One of the major improvements in terms of usability is the validation of forms.
Previously, we had to implement the validation mechanism through ndModel.$formatters
and ndModel.$parsers
, affecting the validation result directly, through ndModel.$setValidity()
, and the implementation of asynchronous checks delivered a separate joy.
Innovation affected the performance:
After all, the previously validating functions were launched with each update in the DOM ( $parsers
) or model ( $formatters
), often affecting each other’s values, thereby restarting the test loop again.
In the new version, validation is launched only if the model is changed and only if there is no error in this model ( {parse: true
). The influence on the model or the representation of the control inside the validator is also excluded, which has a positive effect on the speed of the application.
No, they have not been removed, these are other tools for other things, and they are still needed.
Both $formatters
and $parsers
are arrays containing handler functions that take a value and pass it along the chain to the next handler function. Each link in the chain can modify the value before passing it on.
$ formatters
Every time the model changes, $formatters
handlers in the array in reverse order, from end to beginning. The last value passed is responsible for how the model is represented in the DOM. In other words, $formatters
is responsible for how ngModelCtrl.$modelValue
will be converted to ngModelCtrl.$viewValue
.
$ parsers
Each time the control reads a value from the DOM, $parsers
through the array of handlers in the normal order, from the beginning to the end, passing the value along the chain. The last value transferred is responsible for how the value is represented in the model. In other words, $parsers
is responsible for how ngModelCtrl.$viewValue
will be converted to ngModelCtrl.$modelValue
.
Where is it used?
First of all, they are used in their work by the angular. For example, if you create a control with validation at the minimum length, and then check the $formatters
, you will notice that it is not empty, but already contains one processing function that converts the value from DOM to a string.
The example above is the use of handlers for preprocessing (sanitize) values, if we want to be sure that it will fit into the model only in a strictly defined form.
The other two popular ways to use it are two-way filtering of values ​​(for example, when the user enters "10, 000" into the input, and the model stores "10,000" and vice versa) and creating masks (for example, when the user must fill in the phone mask "+7 (000 ) 000-00-00 ", and in the model we will store" 70000000000 ").
As can be seen from the examples - an indispensable tool.
Exhaustive information about the old validation methods can be obtained from these articles on Habré:
The fact is that returning undefined
from ndModel.$parsers
now causes ngModelCtrl.$modelValue
to be ngModelCtrl.$modelValue
to undefined
and adding {parse: false}
to ngModelCtrl.$modelValue
, i.e. disability field.
In this case, the validators ( $validators
and $asyncValidators
) do not even begin their work.
This behavior can be disabled in ngModelOptions
by setting the allowInvalid
flag to true
.
Starting from version 1.3 , we have the tools for convenient synchronous and asynchronous checking.
It is also worth mentioning that since the work of new validators is tied to updating the model, it also depends on ngModelOptions
, which were mentioned above.
For synchronous validation, the new version offers us the ndModel.$validators
, expanding it with a validator function.
The validator function must return true
or false
for valid and non-valid values, respectively.
Example:
ngModel.$validators.integer = function(modelValue, viewValue) { // , if (ctrl.$isEmpty(modelValue)) { return true; } if (INTEGER_REGEXP.test(viewValue)) { return true; // } return false; // };
For asynchronous validation, the ngModelCtrl.$asyncValidators
collection is used, with the same logic expanding it with a validator function.
The main differences of the asynchronous version:
resolve()
or reject()
, for valid and non-valid values, respectively.Promises in AngularJS are generated by a special $q
service, as well as services like $timeout
and $http
, which basically also use $q
.
The collection contains no more than one promise returned by one validator. This means that if validation is called several times, only the promise from the last call of the validator will be counted, regardless of the result of the previous promises.
From the moment a promise is transmitted by the validator function, and until resolved ( resolve()
or reject()
), the ngModelCtrl.$pending
field stores the name of the validator, and ngModelCtrl.$valid
ngModelCtrl.$invalid
and ngModelCtrl.$invalid
are equal to undefined
Be careful with this feature: as long as validation is in FormCtrl.$errors
form will not be valid, and you will not see any errors in FormCtrl.$errors
Errors, but you can see "waiting" validators in FormCtrl.$pending
.
ngModelCtrl.$asyncValidators.username = function(modelValue, viewValue) { // , if (ctrl.$isEmpty(modelValue)) { return $q.when(); } var def = $q.defer(); // $timeout(function() { if (usernames.indexOf(modelValue) === -1) { def.resolve(); // , } else { def.reject(); // , } }, 2000); return def.promise; }; ngModelCtrl.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { var value = modelValue || viewValue; // , return $http.get('/api/users/' + value). then(function resolved() { // , , return $q.reject('exists'); }, function rejected() { // , , return true; }); };
It is worth using asynchronous validation with fields that use value modifiers (via $formatters
and $parsers
): this can cause multiple operations, as well as false validation or invalidation of the field.
var pendingPromise; ngModelCtrl.$asyncValidators.checkPhoneUnique = function (modelValue) { if (pendingPromise) { return pendingPromise; } var deferred = $q.defer(); if (modelValue) { pendingPromise = deferred.promise; $http.post('/', {value: modelValue}) .success(function (response) { if (response.Result === ' ') { deferred.resolve(); } else { deferred.reject(); } }).error(function () { deferred.reject(); }).finally(function () { pendingPromise = null; }); } else { deferred.resolve(); } return deferred.promise; };
Module ngMessages
designed to facilitate the display of messages on the page.
Despite the fact that this module can be conveniently used for any messages on the page, it is usually used to display errors in the forms. To understand what problems this tool solves, let's see what pain the old way of displaying errors gave us, and compare it with the new one.
For the comparison example, we will use this form for adding a comment:
<form name="commentForm"> <ul class="warnings" ng-if="commentForm.$error && commentForm.$dirty"> ... </ul> <label>:</label> <input type="text" name="username" ng-model="comment.username" required> <label>:</label> <textarea name="message" ng-model="comment.message" minlength="5" maxlength="500"></textarea> </form>
Let's leave the implementation directly validation and consider only the display of messages about the necessary conditions:
.errors
block.Now imagine how we display errors for these fields:
<form name="commentForm"> <ul class="warnings" ng-if="commentForm.$error && commentForm.$dirty"> <span ng-if="commentForm.message.$error.minlength"> 5 </span> <span ng-if="commentForm.username.$error.maxlength"> 500 </span> <span ng-if="commentForm.username.$error.required"> </span> </ul> <label>:</label> ... <label>:</label> ... </form>
Well, as long as things don't look so bad, right?
But why are nicknames and comments all displayed at once? Let's fix this by adding a sequential mapping:
<form name="commentForm"> <ul class="warnings" ng-if="commentForm.$error && commentForm.$dirty"> <span ng-if="commentForm.message.$error.minlength && commentForm.username.$valid"> 5 </span> ... </ul> <label>:</label> ... <label>:</label> ... </form>
Do you still think: “this is not so bad”? Imagine that the fields on the page are not 2, but 20, and each has at least 5 messages. In a similar style, our page will quickly turn into a trash of conditions.
Of course, there are best practices for the implementation of this task, special directives and crutches that extend the behavior of the FormController
(for example, in our project all controls were extended by the showError
property that kept the current error), but they all lose in convenience to the methods we will talk about further .
ngMesssages
supplied as a separate module, and before we start working with it, we need to connect it. Download or install the module through the package manager and connect to the project:
<script src="path/to/angular-messages.js"></script>
And add depending:
angular.module('myApp', ['ngMessages']);
Basic work with this module comes down to working with two directives:
ng-messages
- the container containing our messagesng-message
- direct messageng-messages
takes as an argument a collection, the keys of which will check and show already ng-message
, which take as an argument for comparison a string or expression (starting with 1.4 ).
Let's repeat the example of the same form for adding a comment, but using ngMessages
:
<div ng-messages="commentForm.message.$error" class="warnings"> <p ng-message="minlength"> 5 </p> <p ng-message="maxlength"> 500 </p> <p ng-message="required"> </p> </div>
ngMessages
can ngMessages
be used as an element:
<ng-messages for="commentForm.message.$error" class="warnings"> <ng-message when="minlength"> 5 </ng> <ng-message when="maxlength"> 500 </ng> <ng-message when="required"> </ng> </ng-messages>
And so, right ngMessages
solved two problems for us:
switch
like list.In addition, the problem of prioritizing the output of messages is also being solved. Everything is simple here: messages are displayed according to their location in the DOM.
Multiple messages can be enabled by adding the ng-messages-multiple
attribute to the ng-messages-multiple
directive:
<ng-messages ng-messages-multiple for="commentForm.message.$error"> ... </ng-messages>
– ngMessages
, , , :
Version 1.3 uses ng-messages-include
as an additional attribute ng-messages
, while in later versions ng-messages-include
it is a self-contained child directive along with ng-message
:
1.3
<ng-messages ng-messages-include="length-message" for="commentForm.message.$error"> </ng-messages>
1.4+
<ng-messages for="commentForm.message.$error"> <ng-messages-include="length-message"></ng-messages-include> </ng-messages>
(, ) , :
<script type="script/ng-template" id="length-message"> <ng-message when="minlength"> </ng-message> </script> ... <ng-messages for="commentForm.message.$error"> <ng-messages-include="length-message"></ng-messages-include> </ng-messages> <ng-messages for="anotherForm.someField.$error"> <ng-messages-include="length-message"></ng-messages-include> </ng-messages>
, , . .
, 1.4 , ng-message-exp
:
// error = {type: required, message: ' '}; <ng-messages for="commentForm.message.$error"> <ng-message-exp="error.type"> {{ error.message }} </ng-message-exp> </ng-messages>
ng-message-exp
, ng-message
, ( expression
). , , , AJAX .
– . ng-messages
!
:
ng-messages
, controller as
, scope
.
, this.something
. .
For example:
app.directive('someDirective', function () { return { scope: { name: '=' }, controller: function () { this.name = 'Foo' }, controllerAs: 'ctrl' ... }; });
name
.
:
$scope.$watch('name', function (newValue) { this.name = newValue; }.bind(this));
, , , , ?
1.3 :
bindToController
.
app.directive('someDirective', function () { return { scope: { name: '=' }, controller: function () { this.name = 'Foo' }, bindToController: true, ... }; });
ctrl.name
$scope.name
.
1.4 :
app.directive('someDirective', function () { return { scope: true, bindToController: { name: '=' }, controller: function () { this.name = 'Foo' }, ... }; });
scope
bindToController
.
, bindToController
, , , scope
, scope
.
- scope
, true
. bindToController
scope
.
{{ expression | filter }}
, , «» ( expression
), ( filter
). , , , .
1.3 : , . , , $digest
, , . , , .
: , , - ? , , «» , ?
1.3 ( stateless
) ( stateful
) . stateless
. , $stateful
true
.
Example:
angular.module('myApp', []) .filter('customFilter', ['someService', function (someService) { function customFilter(input) { // someService input += someService.getData(); return input; } customFilter.$stateful = true; return customFilter; }]);
Breaking change:
, . , , .
dateFilter
weeks
That's all. - , .
, markdown. - html.
, , :)
Source: https://habr.com/ru/post/281721/
All Articles