📜 ⬆️ ⬇️

AngularJS: Migration from 1.2 to 1.4, Part 1

A lot of articles have been written about the advantages of the transition from version 1.2. However, according to statistics, more than 45% of sites still use version 1.2, only 31% switched to a newer 1.3 and only 5% use 1.4.

And this is when space ships plow the expanses of the universe, version 1.2.0 was released almost two years ago, version 1.3.0 - a year ago, version 1.4.0 - this spring, and 1.5.0 is already in beta.


')
As a rule, large projects from transition restrain the opacity of this process and the scarcity of materials on this subject.
In the official guide you can find only a small piece of all possible problems, and blogs, as a rule, only retell it.

In this article we will talk about what you can encounter when migrating to new versions, and analyze the most problematic places.

Unfortunately, the post came out too big, so in this part I will focus on breaking changes of this transition, and we will talk about features and merits in the next part. But I note that the main advantage of the transition is a significant increase in the speed of work, as well as a solid list of fixed bugs (after all, the latest fixes for 1.2 were a year ago at 1.2.28).

I divided this post into two parts. One of them is my personal experience of translating two projects into new versions (I hid it under the spoiler), the second is a list of problems found, read and translated with explanations and examples.

The history of our transition
The first relatively large project that required a transition (from 1.2 to 1.3) was the E-Learning platform, where, despite more than 12,000 lines of code (mostly directives), no problems arose.

The second project required migration from version 1.2.16 to 1.4.4 and was significantly larger (about 75,000 lines of pure angular), and product specifics (accounting) implied complex relationships, many forms and inevitable problems, but previous experience inspired.

The first result of the migration was expected to get a non-working application with a bunch of errors in the console, but the application started, and there were no problems. It was extremely strange, so I began my journey through the official guide, where, however, none of the problems described were reproduced. Having decided that the job was done, I gave the task to the QA and automatic tests.

And the next day I received 9 bugs, then another 9, and more. The insidiousness of all the errors found was not in the fact that they break the work of the application, but in the fact that it imperceptibly changes its behavior.

Here, Change Log and discussions inside the commits found there became a friend and companion in search of reasons.

First of all, asynchronous checks on the server side, which did not work with statuses, but with responses in the form of an “OK” text primitive, fell off. Not a single break change was notified about this, but there was a corresponding bug fix .

Tip: Check your work with XHR requests. If the server does not send you an object, but a primitive with the “application / json” headers, you will have problems.

The next came the spinners embedded in the pop-ups, ceasing to correctly determine the width of the parent. The problem was two things. First, the spinners lay in ng-show , which means they were initialized before the parent container could be displayed. Secondly, the spinner showed / hid, following the attribute through $ observe . For some reason, in the old version, the attribute changed later than the parent became : visible , and in the new one, vice versa.

Tip:
  1. Do not store dynamic directives for which the moment of initialization is important in ng-show / ng-hide , use ng-if for this. This advice is also relevant for version 1.2.
  2. Do not use $ observe to track attributes if you need to monitor external changes to other data (DOM or $ scope). Use $ watch (function () {}, function () {}) for this . Moreover, both options add their votcher to a dirty check, the only difference is in the conditions of calling a callback.


The same problem affected the conditions in the expressions inside $ eval : some blocks began to disappear. But the problem turned out to be another bug-fix and was already covered in Habré .

Tip: Do not use the undocumented features of the framework, examine the code for use when checking the following values ​​in $ scope : 'f' , '0' , 'false' , 'no' , 'n' , '[]' .

The main problem that has arisen is validation. Starting with version 1.3 , new methods for checking forms have appeared in the anguly, and with them new pitfalls. But there are problems that will affect you, even if you do not plan to use the new validation. These are the maxlength / minlength directives and the new $ setValidity logic that threatens to make your form permanently invalid (this is what happened to us).

It was found that we kept in ngModelCtrl. $ Error the special property showError (for conveniently displaying errors one by one), which led to a permanent invalidation of the form, because in the new version one of the validity conditions is the empty hash ngModelCtrl. $ Error .

Tip: Do not put in properties that start with $ nothing that is not listed in the official API.

Another problem arose with inputs that use a mask (for phone, contract numbers, etc.) in tandem with the ng-maxlength / ng-minlength directives .

Both of these directives now check ngModelCtrl. $ ViewValue instead of ngModelCtrl. $ ModelValue , which means that the maximum length for the value “xx-xx” is no longer 4, but 5 (taking into account the “-” character). I would have had to rewrite hundreds of verification rules throughout the application, so it was decided to replace all ng-maxlength with a custom model-maxlength with autocorrect , which would only check the model again. And this decision was terrible! Angulyar reserved himself the maxlength attribute limiter, which means we could no longer limit the number of characters entered. As a result, it was decided to change all the rules for new ones, taking into account the mask symbols. However, the custom model-minlength directive found its use in directives where there are predefined characters (for example, "+7" for the phone), allowing you to check only the model without the prefixes set in ngModelCtrl. $ ViewValue .

Tip: If you use a mask or otherwise manipulate the value of ngModelCtrl. $ ViewValue , change the validation taking into account mask characters. Use the placeholder attribute for the predefined values ​​in the input with the ng-minlength check or replace the check with the custom one. The working code of such a directive is in the break changes list.

The second wave of problems went after the transition to our new validation (goodbye, $ formatters and $ parsers ). We are faced with the fact that a number of forms again became permanently invalid.

With synchronous validations ( $ validators ), the problem came to light quickly. It consisted in the fact that the fields contained hidden fields via ng-show with their validation checks. Due to the peculiarities of working in the old version ( $ formatters and $ parsers ), these fields were not included in the form verification, however, in the new version ( $ validators ) this results in a hidden non-valid field.

Tip: If you have forms with hidden dynamic fields, then hide these fields or show them through ng-if , do not use ng-show / ng-hide .

Remark: How asynchronous validators work
Asynchronous validators are stored in the ngModelCtrl. $ AsyncValidators collection and are a function that returns a Promise . Promises return various services (for example, $ timeout and $ http), and are also generated by the special service $ q . The validity of the field depends on the resolved or rejected return. Asynchronous validation starts only after all synchronous validators become valid. At the time of the validator call, its validity ( $ setValidity ) will be equal to undefined , and the name of the expected validator will appear in ngModelCtrl. $ Pending . As soon as the resolve of the promise resolves, its validity will be set to null .

If you call the validation function several times, the last promise will wipe the previous ones. This means that if the field was validated twice, and the first promise was resolved and the second rejected, the field would be invalid.

An example from the API documentation:
ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) { var value = modelValue || viewValue; // Lookup user by username return $http.get('/api/users/' + value). then(function resolved() { //username exists, this means validation fails return $q.reject('exists'); }, function rejected() { //username does not exist, therefore this validation passes return true; }); }; 


With asynchronous validation, it turned out to be more difficult: the form was invalid, but did not contain any errors.

Here, due to the nature of the mask (via $ formatters and $ parsers ), asynchronous validation was called before the completion of synchronous validations, and it was called several times in 1 changed symbol. This caused a multiple creation bug of promises, which led to the fact that the last promise was not resolved or rejected. Accordingly, the input received infinite pending, and the form was invalid without any errors.

An example of building a validator for such cases
  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; }; 


Tip: Test the behavior of asynchronous validators: make sure the return promise will always be resolved or rejected.

The strangest problem of validation was the custom directive for entering the amount (it looked like two input lines for rubles and kopecks, giving to ngModel the ready result of concatenating these numbers). It was not only invalid, but it also reported an error to the console from the depths of the framework.

Time was spent on the problem fairly, the problem was again in ng-maxlength, and the solution was simple: add directives in $ formatters to convert the value of $ viewValue to a string.

But why the problem arose at all? Let's figure it out!

It's all about $ compile : the behavior of the main directives is described inside. For example, for input and textarea , the inputDirective control is called , which takes attr.type and, based on it, calls one of the functions in the inputType collection. For text types, this is textInputType . He, in turn, passes our control into the function stringBasedInputType , which adds in $ formatters the code that converts our value to a string.

Thus, when ngModel is not tied to some existing base element such as input , but, for example, hangs on a simple diva or custom directive (in our case it is a directive to input the amount), the data is thrown into $ viewModel "as is", which causes an error for filter directives like maxlength, which for their work use the property .length , which is absent in numbers.

Working example on Plunker .

Tip: All custom directives that work with numbers must have the appropriate formatter to convert numbers to strings.

Total:


As a result, about 6 iterations of tests were driven off and 54 problems were found, but most of them had a similar nature. Part of the problem could not be at all, do not use some parts of the code for their work bugs and undocumented features. A total of 56 commits and 3 weeks of working time per person were spent on migration, taking into account refactoring and the transition to a new validation.


Part One: Breaking Changes


Immediately, I note that from now on we are losing support for IE8. However, it can be returned by connecting the necessary polyfills.

Job $ parse


.bind, .call and .apply


You can no longer call .bind , .call, and .apply inside an expression (for example, {{}} ).

This allows you to be sure that the behavior of existing functions cannot be changed.

__proto__


Since version 1.3, the deprecated ( deprecated ) property __proto__ has been removed.

Previously, it could be used to access global prototypes.

Object


It is forbidden to use Object inside expressions.

This is due to the ability to execute arbitrary code in expressions.
For example:
 ''.sub.call.call( ({})["constructor"].getOwnPropertyDescriptor(''.sub.__proto__, "constructor").value, null, "alert('evil')" )() 


If someone needs Object.key or some other method, pass it through scope .

{define, lookup} {Getter, Setter}


Since version 1.3 , the {define, lookup} {Getter, Setter} properties, which allowed to execute arbitrary code inside an expression, are prohibited.

If you need these properties, wrap them in the controller and push them into the scope with your hands.

$ parseProvider

[commit]

Removed obsolete $ parseProvider.unwrapPromises and $ parseProvider.logPromiseWarnings methods .

$ interpolate


The functions returned by $ interpolate no longer contain an array of .parts . Instead, they contain:


toBoolean


In an angular, the true implementation of toBoolean () is used to test for truth, which equates to false some non-standard values ​​in the form of the following lines:

'f' , '0' , 'false' , 'no' , 'n' , '[]'

Starting with version 1.3 , only the same values ​​as in the normal JS are equal to false:

false , null , undefined , NaN , 0 , ""

About it wrote on Habré

For example:
  $scope.isEnabled = 'no'; 

<1.3:

  {{ isEnabled ? ' ' : '' }} 

1.3+:

  {{ isEnabled ? '' : ' ' }} 




Helpers


.copy ()


Previously, when working with copy objects, I copied all the properties of the object, including those in the prototype, which led to the loss of the prototype chain (except for Date, RegExp and Array).

Starting with version 1.3 , it copies only its own properties (something like iteration with hasOwnProperty ), and then refers to the prototype of the original.

For example:
  var Foo = function() {}; Foo.prototype.bar = 1; var foo = new Foo(); var fooCopy = angular.copy(foo); foo.bar = 3; 

<1.3:

  console.log(foo instanceof Foo); // => true console.log(fooCopy instanceof Foo); // => false console.log(foo.bar); // => 3 console.log(fooCopy.bar); // => 1 

1.3+:

  console.log(foo instanceof Foo); // => true console.log(fooCopy instanceof Foo); // => true console.log(foo.bar); // => 3 console.log(fooCopy.bar); // => 3 

IE8: Object.create and Object.getPrototypeOf polyfills are required

.forEach ()


Previously, if the array increased in the process of iteration, then the cycle went through the newly appeared elements too.

Starting with version 1.3 , it caches the number of elements in the array and passes only through them, here it is closer to the native Array.forEach .

For example:
  var foo = [1, 2]; 


<1.3:

  angular.forEach(foo, function (value, key) { foo.push(null); // =>     null    console.log(value); }); 

1.3+:

  angular.forEach(foo, function(value, key) { foo.push(null); // =>  1,  2    console.log(value); }); 


.toJson ()


The salt of this helper is primarily in the fact that it does not serialize all the data, but only those that do not begin with the special character $.

Starting with version 1.3 , it does not serialize only properties whose names begin with $$.

For example:
  var foo = {bar: 1, baz: 2, $qux: 3}; 

<1.3:

  angular.toJson(value); // => {"bar": 1} 

1.3+:

 angular.toJson(value); // => {"bar": 1, "$bar": 2} 




jqLite


Briefly about the main thing:

Select


Controller


SelectController is now one abstraction for the Select directive and for the ngOptions directive.

This means that now ngOptions can be removed from Select , without fear that it could somehow affect it.

Different variations of the Select directive have their own SelectController.writeValue and SelectController.readValue methods , which are responsible for working with the $ viewValue of the <select> tag and its children <option> .

value for ngOptions


Previously, in ngOptions, the surrogate key used the index or the item key in the passed collection.

Starting with version 1.4 , this is done using the hashKey call for the item in the collection.

Accordingly, if you read the value directly from the DOM , then problems may arise.

For example:
 <select ng-model="model" ng-option="i in items"></select> 

<1.4:

  <option value="1">a</option> <option value="2">b</option> <option value="3">c</option> <option value="4">d</option> 

1.4+:

  <option value="string:a">a</option> <option value="string:b">b</option> <option value="string:c">c</option> <option value="string:d">d</option> 


Comparing ngModel with option value


Starting with version 1.4 , the select directive begins to compare the value of option and ngModel , using strict comparison.

This means that the value 1 is not equivalent to "1" as it is not equivalent to the value false or true .
If you put the value 1 in the model, you get unknown option .

To avoid this, you need to put a string in the model, for example, scope.model = "1" .

If the model needs exactly a number, it is proposed to use conversion via formatters and parsers.

Example:
 ngModelCtrl.$parsers.push(function(value) { return parseInt(value, 10); //    }); ngModelCtrl.$formatters.push(function(value) { return value.toString(); //    }); 


Sorting


As in the case of ngRepeat , sorting in alphabetical order no longer works, but corresponds in sequence to the call to Object.keys (obj) .


ngRepeat


Sorting

[commit] [issue] [holy war]

Previously, ngRepeat, sorting an object, sorted it alphabetically by keys. Starting with version 1.4 , it returns it in an order dependent on the browser, as if you were sorting it out for key in obj .

This is due to the fact that browsers usually return object keys in the order in which they were declared, unless the keys were deleted or reinstalled.

To iterate over an object, it is proposed to use custom filters that convert the object into an array.


$ compile


controllerAs, bindToController


In version 1.3 bindToController was introduced. Starting with version 1.4 , an object can be passed into it to specify an isolated scope.

In this regard, now the object returned from the controller constructor overwrites the scope.

The views that used the controllerAs syntax no longer receive a reference to the function itself, but to the object it returns.

If the directive uses bindToController , then all previous bindings are reinstalled into the new controller, all installed watches are deleted ( unwatch ).

Expression '&' in isolated scope


Previously, a function with an & was always created, even if the attribute along with an expression was missing (in this case, a function was created that returns undefined ).

Starting at 1.4 , the behavior & approached @ . Now, if the expression is missing, then the corresponding method in $ scope is also missing. When you access it, you get undefined instead of a function that returns undefined .

Replace directive property


Starting from 1.3 , it becomes deprecated and should be removed in the next major release.

This is explained by the fact that there are some problems with merge attributes.

More details:
If you combine

  <div ng-class="{hasHeader: true}"></div> 

WITH

  <div ng-class="{active: true}"></div> 

That will get

  <div ng-class="{active: true}{hasHeader: true}"></div> 

With the corresponding error that the expression is not valid.

And an insufficient level of encapsulation of such directives and in general.

Holivar on this topic is available here .

$ observer


Starting from version 1.3 , we finally got a convenient way to remove the attribute observatory: the destructor function returns when you call attr.observe (as watch does). Previously, he returned a link to the function of the Observer.

Now, in order to have a link to the function of the Observer, you must save it somewhere beforehand.

For example:
<1.3:

 directive('directiveName', function() { return { link: function(scope, elm, attr) { var observer = attr.$observe('someAttr', function(value) { console.log(value); }); } }; }); 

As it is now:

 directive('directiveName', function() { return { link: function(scope, elm, attr) { var observer = function(value) { console.log(value); }; var destructor = attr.$observe('someAttr', observer); destructor(); //   } }; }); 


Access to isolated scope from outside


It is no longer possible to get the isolated scope property through the attribute of the element where the isolated directive is defined.

For example:
The following directive is given:

  app.controller('testController', function($scope) { $scope.controllerScope = true; }); app.directive('testDirective', function() { return { template:'<span ng-if="directiveScope">world!</span>', scope: {directiveScope: '='}, controller: function($scope) {}, replace: true, restrict: 'E' } }); 

<1.3:

  Hello <test-directive directive-scope="controllerScope"></test-directive> // Hello 

1.3+:

  Hello <test-directive directive-scope="controllerScope"></test-directive> // Hello world! 




ngModelController


$ setViewValue ()


The behavior of $ setViewValue () has changed a bit, now it does not pass changes to $ modelValue immediately, as before.

Now the model is updated depending on the two settings ngModelOptions , and in particular:


By default, updateOn is default , and debounce is 0 , so $ modelValue runs as before, instantly.
However, you should take into account the features described above when working with old code.

$ commitViewValue



If you want to update $ modelValue instantly at all costs, ignoring updateOn and debounce , use $ commitViewValue () .

$ commitViewValue () takes no arguments. Previously, he had an undocumented argument, revalidate , used by
in private api as a hack for the forced launch of revalidation and related processes, even if $$ lastCommittedViewValue
not updated, but in recent versions it is removed.

$ cancelUpdate ()


Renamed to $ rollbackViewValue () .

The call allows you to “roll back” $ viewValue to the state $$ lastCommittedViewValue , cancel all debounce processes that are in progress, and redraw the view (for example, input).

For example:
<1.3:
 $scope.resetWithCancel = function (e) { $scope.myForm.myInput.$cancelUpdate(); $scope.myValue = ''; }; 


1.3+:
 $scope.resetWithCancel = function (e) { $scope.myForm.myInput.$rollbackViewValue(); $scope.myValue = ''; }; 


input: date, time, datetime-local, month, week


Since version 1.3, Angulyar normally supports HTML5 inputs associated with numbers.

In the ng-model of such inputs there must be a strictly Date object

In older browsers that do not support these inputs, the user will see the text. In such cases, he will have to enter the correct ISO format for the required date.


Validation


$ Error collection

[commit]

Previously, arbitrary properties could be stored in $ error by controlling the validity of the control manually via $ setValidity .

Starting with version 1.3 , final validation depends on whether the $ error hash is empty. Throwing a property into ngModelCtrl. $ Error manually and not removing it from there manually, you will get permanently invalid control, regardless of the value of this property.

result in $ setValidity

[commit]

$ setValidity allows you to set the validity of certain properties of the control, taking two arguments: name and result .

Previously, result was always cast to true or false , regardless of what was passed there.

Starting with version 1.3 , $ setValidity begins to distinguish between false , undefined and null passed to result . It is necessary now to take care of the fact that the result is exactly the boolean value.

The values undefined and null are used, for example, internally for asynchronous validators. So, if not all synchronous validators are valid, then the asynchronous values ​​will be set to null . If the synchronous validators are ready and asynchronous validation has begun, then as long as the pending is going on, the value of the validator will be set to undefined .

$ parsers and undefined.

[commit]

Previously, you could prokidykivat undefined in the chain $ parsers if, for example, you want to break it.

Starting with version 1.3 , the parsers no longer handle undefined and make the control invalid, setting the value {parse: true} to $ error .

This is done to prevent parsers from running in cases where $ viewValue ( not yet installed )

ngPattern

[commit]

Starting from 1.4.5 , the ngPattern directive performs validation based on $ viewValue (previously based on $ modelValue ), before the $ parsers chain works .

This is related to the problem when input [date] and input [number] are not validated due to the fact that the parsers have converted $ viewValue to Date and Number respectively.

If you use $ viewValue modifiers with this directive and you need to check $ modelValue as before, then you should use a custom directive.

For example:
  .directive('patternModelOverwrite', function patternModelOverwriteDirective() { return { restrict: 'A', require: '?ngModel', priority: 1, compile: function() { var regexp, patternExp; return { pre: function(scope, elm, attr, ctrl) { if (!ctrl) return; attr.$observe('pattern', function(regex) { /** * The built-in directive will call our overwritten validator * (see below). We just need to update the regex. * The preLink fn guaranetees our observer is called first. */ if (isString(regex) && regex.length > 0) { regex = new RegExp('^' + regex + '$'); } if (regex && !regex.test) { //The built-in validator will throw at this point return; } regexp = regex || undefined; }); }, post: function(scope, elm, attr, ctrl) { if (!ctrl) return; regexp, patternExp = attr.ngPattern || attr.pattern; //The postLink fn guarantees we overwrite the built-in pattern validator ctrl.$validators.pattern = function(value) { return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value); }; } }; } }; }); 


ngMinlength / ngMaxlength


Starting from 1.3 , the ngMinlength and ngMaxlength directives perform validation based on $ viewValue (previously based on $ modelValue ).

This can lead to incorrect validation when using these directives along with directives that modify the $ viewValue , for example, the masks for entering the phone.

To avoid problems, there are two solutions:

  1. Change the number of maximum characters in accordance with $ viewValue (for example, masks of the form “xx-xx”, if the model contains only “xxxx”, should be taken into account as maxlength = “5” , not 4, as it was before)
  2. Use your own custom directives that check $ modelValue . However, there may be problems with maxlength , because, according to the specification, it limits the number of characters entered, so you have to implement your limit.


I recommend for most cases to use the first option as the least problematic.
The second option may be useful for minLength. In cases when there is an optional mask with input where n characters are pre-entered (for example, phone with the set "+7" installed), this is due to the fact that minLength does not validate the field only as long as it is empty.

Custom maxlength example
  (function (angular) { 'use strict'; angular .module('mainModule') .directive('maxModelLength', maxlengthDirective); function maxlengthDirective () { return { restrict: 'A', require: '?ngModel', link: function (scope, elm, attr, ctrl) { if (!ctrl) { return; } var maxlength = -1; attr.$observe('maxModelLength', function (value) { var intVal = parseInt(value); maxlength = isNaN(intVal) ? -1 : intVal; ctrl.$validate(); }); ctrl.$validators.maxlength = function (modelValue, viewValue) { return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (String(modelValue).length <= maxlength); }; /* *  ,      - maxlength *      ,      * */ elm.bind('keydown keypress', function (event) { var stringModel = String(ctrl.$modelValue); if (maxlength > 0 && !ctrl.$isEmpty(ctrl.$modelValue) && stringModel.length >= maxlength) { if ([8, 37, 38, 39, 40, 46].indexOf(event.keyCode) === -1) { event.preventDefault(); } } }); } }; } })(angular); 


Custom minlength example
  (function (angular) { 'use strict'; angular .module('mainModule') .directive('minModelLength', minlengthDirective); function minlengthDirective () { return { restrict: 'A', require: '?ngModel', link: function (scope, elm, attr, ctrl) { if (!ctrl) { return; } var minlength = 0; attr.$observe('minModelLength', function (value) { minlength = parseInt(value) || 0; ctrl.$validate(); }); ctrl.$validators.minlength = function (modelValue, viewValue) { return ctrl.$isEmpty(modelValue) || String(modelValue).length >= minlength; }; } }; } })(angular); 


Virtual ngModel issue:

If you use ngMinlength / ngMaxlength on an element that is not intended for direct data entry (for example, at the root of a directive that contains several inputs that work with the root ngModel ), and use numerical data, you will get incorrect data validation (there will always be an error) .

More specifically, a number will always be stored in $ viewValue , which the validator cannot verify, since can't get it .length .

Why it happens?
The $ compile contains the main directives. For example, for input and textarea , the inputDirective control is called , which takes attr.type and, based on it, calls one of the functions in the inputType collection. For text types, this is, respectively, textInputType , it, in turn, passes our control to the function stringBasedInputType , which adds to $ formatters the code for converting our value to a string.

When ngModel is not tied to some existing base element like input , but, for example, hangs on a simple diva or custom directive, the data is thrown into the $ viewModel “as it is”, without additional conversion to the string, which causes an error in the directives filters like ngMaxlength .

Based on this, all custom directives that work with numbers must have the appropriate formatter for converting numbers to strings.

Working example on Plunker .



Scopes and Digests


$ id


Now integer compounded.

Previously, due to fears that the numbers might not be enough to count the scope 's, they decided to use $ id for the string (and in fact it is an array of the form [' 0 ',' 0 ',' 0 ']), but the concerns about this the bill didn't come true.

Instead, we got some extra load (adds a few milliseconds) when creating a large amount of scope 's (for example, when working with large tables). Going to prime numbers solves this problem.

For example:
<1.3: [ Plunker]

 console.log($rootScope.$id); // => 001 

1.3+: [ Plunker]

 console.log($rootScope.$id); // => 1 


broadcast and emit


Now set the currentScope to null as soon as the event reaches the end of the distribution chain.

This is due to a hard -to- track bug when using event.currentScope incorrectly , when someone tries to access it from an asynchronous function.
Previously, event.currentScope in this case was equal to the last $ scope in the chain, imperceptibly leading to incorrect application operation.
Now in a similar case when using event.currentScope there will be an error.

For asynchronous access to event.currentScope, you now need to use event.targetScope .

For example:
scope:

  001 ($rootScope) â”” 002 ($scope of ParentCtrl) â”” 003 ($scope of ChildCtrl) â”” 004 ($scope of GrandChildCtrl) 

customEvent GrandChildCtrl

<1.3: [ Plunker]

  .controller('ParentCtrl', function($scope, $timeout) { $scope.$on('customEvent', function(event) { console.log(event.currentScope); // $id  002 $timeout(function() { console.log(event.targetScope) // => $id  004 console.log(event.currentScope) // => $id  001 }); }) }) .controller('ChildCtrl', function($scope, $timeout) { $scope.$on('customEvent', function(event) { console.log(event.currentScope); // $id  003 $timeout(function() { console.log(event.targetScope) // => $id  004 console.log(event.currentScope) // => $id  001 }); }) }) 

1.3+: [ Plunker]

  .controller('ParentCtrl', function($scope, $timeout) { $scope.$on('customEvent', function(event) { console.log(event.currentScope); // $id  2 $timeout(function() { console.log(event.targetScope) // => $id  4 console.log(event.currentScope) // => null }); }) }) .controller('ChildCtrl', function($scope, $timeout) { $scope.$on('customEvent', function(event) { console.log(event.currentScope); // $id  3 $timeout(function() { console.log(event.targetScope) // => $id  4 console.log(event.currentScope) // => null }); }) }) 




http and resource


JSON primitives

[commit]

Starting with version 1.3 , responses with Content-Type: application / json containing primitives start parsing as JSON.

In general, this is a bug fix, it allows you to avoid some crutches when working with the answer, but in some cases it may break the existing code.

For example:
«OK» , , .

<1.3:

  response === 'OK' // => false response === '"OK"' // => true 

1.3+:

  response === 'OK' // => true response === '"OK"' // => false 


$ http transformRequest


Starting with version 1.4 , the transformRequest function is no longer supported and does not change the request headers. Instead, use the headers property and the getter functions corresponding to the desired header in the request parameters .

The function of the first argument pretends to object the config , which allows to determine and establish the headlines dynamically.

For example:
'X-MY_HEADER'

<1.4:

  function requestTransform(data, headers) { headers = angular.extend(headers(), { 'X-MY_HEADER': 'test' }); return angular.toJson(data); } 

1.4+:

  $http.get(url, { headers: { 'X-MY_HEADER': function(config) { return 'test'; } } }) 


$ http interceptor


The responseInterceptors collection in $ httpProvider already had the status of deprecated and had two different APIs (one of which is not completely obvious), which led to various embarrassments.

Starting with version 1.3 , this collection [deleted], as well as its functionality.

Instead, a new, transparent API is available for registering interceptors.

For example:
myHttpInterceptor .

< 1.3: [ Plunker]

  $provide.factory('myHttpInterceptor', function($q, dependency1, dependency2) { return function(promise) { return promise.then(function(response) { //  success return response; }, function(response) { //  error if (canRecover(response)) { return responseOrNewPromise } return $q.reject(response); }); } }); $httpProvider.responseInterceptors.push('myHttpInterceptor'); 

1.3+: [ Plunker]

  $provide.factory('myHttpInterceptor', function($q) { return { response: function(response) { //  success return response; }, responseError: function(response) { //  error if (canRecover(response)) { return responseOrNewPromise } return $q.reject(response); } }; }); $httpProvider.interceptors.push('myHttpInterceptor'); 


$ httpBackend and JSONP


Now the angular catches errors in the “success” events, an empty response (missing data in the callback) in JSONP does not lead to an error (it previously generated an error and set the status to -2).

It is interesting to know (or not) that:
onload onerror JSONP script .
jQuery event .

, , .

$.data(«events») , jqLite .

IE8: The onreadystatechanged event is no longer supported .

$ resource


If you call toJson () on the $ resource instance , it will contain the $ promise and $ resolved properties that were previously cut during serialization, as well as all properties starting with a single $ .

According to the toJson () change described above , properties starting with $ are no longer serialized. Now only those properties that start with $$ are serialized .

Based on this, it can be expected that the serialized $ resource will contain these properties, but this is not the case. Specifically, he cuts these two properties during the serialization itself.

All other properties, including those added by the user, will be serialized and will be contained in the final json .


$ inject


Modules: .config () and .provider ()


Previously, it was possible to call .config () before .provider () worked.

Since version 1.3 , this behavior is impossible, .config () will always be called only after all the .provider () modules have worked.

For example:
  app .provider('$rootProvider1', function() { console.log('Provider 1'); this.$get = function() {}; }) .config(function() { console.log('Config'); }) .provider('$rootProvider2', function() { console.log('Provider 2'); this.$get = function() {}; }); 

<1.3: [ Plunker]

Provider 1, Config, Provider 2

1.3+: [ Plunker]

Provider 1, Provider 2, Config



ngAnimate


All methods


Previously, for all $ animate methods, the last argument was a done callback , which was executed at the end of the animation.
Starting with version 1.3 , a set of options styles is passed there , which is applied to the element.

Instead of done, all functions now return a promise whose resolve means the completion of the animation.

animate.enter () and animate.move ()


These methods have four arguments (element, parent, after, options).

Previously, if the after argument was not specified, then a new element was added after the specified element , and if it was specified, after after .

The problem is that with a similar API it is impossible to add a new element to the beginning of the parent container .

Starting with version 1.3 , if the after argument is not specified, then this element is added to the beginning of the parent container .

Accordingly, it is now necessary to always indicate after which particular element you want to insert a new one.

For example:
$animate.enter

<1.3:

  //     `element` `$animate.enter(element, parent);` 

1.3+:

  //      `parent` `$animate.enter(element, parent);` //     `element` `$animate.enter(element, parent, angular.element(parent[0].lastChild));` 




Filters


Internal context

[commit]

Previously, all filters had an undocumented feature: the internal context of their control this referred to $ scope in which this filter was invoked.

Why you can not do this, and how to do it correctly:
, , $scope . , ng-repeat .

Andy Joslin's :

  yourModule.filter("as", function($parse) { return function(value, path) { return $parse(path).assign(this, value); }; }); 

1.3 , $scope ( this ) undefined .

$scope , ** **, $scope 10%, $scope $digest
Error: 10 $digest() iterations reached. Aborting! .

:

  • : , .
  • $scope .

ng-repeat:

item in (filterResults = (items | filter:query)) , , as : item in items | filter:query as filterResults) .

filter


Now it works only with arrays.

With version 1.4, an attempt to call a filter on an object will result in an error. Previously, it simply "quietly" returned an empty array.

To iterate over an object, it is proposed to use custom filters that convert the object into an array.

limitTo


, limitTo (, undefined ), .

1.4 , , .. .


ngCookies


$cookies


1.4 , $cookies , /, API :


This is due to data synchronization bugs, when there are no longer actual data in the object. Which means that you can no longer use votchery, tracking changes Cookies through an object.

Such manipulations were necessary in the past, for example, for communication between browser tabs, but these days there are more convenient tools, such as localStorage .

$ cookieStore


Starting from version 1.4 , the $ cookieStore service received the status of deprecated , all useful logic was transferred to the $ cookies service , and the $ cookieStore request now returns the instance of the $ cookies service .


Conclusion:


There will be no problems with the transition unless the project uses an excessive number of crutches of non-trivial solutions based on bugs and undocumented features of the framework.
It will be completely painless for those who have not used the means of an angular to animate and validate the application.

In the next article I will talk about all the benefits of new versions and how to improve performance with their help.

If you have met with any other interesting problems during the transition, please share them in the comments.

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


All Articles