Compare commits

...

20 Commits

Author SHA1 Message Date
Pete Bacon Darwin be240b1176 docs(changelog): add 1.7.4 release notes 2018-09-07 09:57:37 +01:00
Martin Staffa 61b33543ff fix(ngAria.ngClick): preventDefault on space/enter only on non-interactive elements
Fixes #16664
Closes #16680
2018-09-06 15:59:19 +02:00
Martin Staffa 1dcba9cd88 chore(benchpress): add ngRepeat animation benchmark
Closes #13976
2018-09-06 15:46:25 +02:00
Michał Gołębiowski-Owczarek 8cd54d7794 docs(version-support-status): remove outdated info, resolve inconsistencies
Closes #16684
2018-09-06 15:42:24 +02:00
Martin Staffa 3105b2c26a fix(ngAnimate): remove prepare classes with multiple structural animations
Closes #16681
Closes #16677
2018-09-06 15:42:18 +02:00
Craig Johnson bc5a48d4a4 docs(guide): grammar correction in security guide
Closes #16683
2018-09-06 15:42:00 +02:00
George Kalpakas 2bbc7c464f refactor(ngMocks): simplify routeToRegExp by assuming path has query/hash stripped off
Closes #16672
2018-08-26 00:00:03 +03:00
George Kalpakas e85f91d582 refactor(ngMocks): clean up MockHttpExpectation 2018-08-26 00:00:01 +03:00
George Kalpakas 862a78dfd2 refactor(ngMocks): ignore query/hash when extracting path params for MockHttpExpectation 2018-08-25 23:59:59 +03:00
George Kalpakas bd772abf34 refactor(ngMocks): clean up MockHttpExpectation#params() 2018-08-25 23:59:57 +03:00
George Kalpakas b074d719ae refactor(ngRoute): do not unnecessarily return originalPath in routeToRegExp 2018-08-25 23:59:56 +03:00
George Kalpakas a43a40b778 test(ngMocks): use correct method name in $httpBackend test 2018-08-25 23:59:54 +03:00
Susisu 2ceeb739f3 fix($route): correctly extract path params if path contains question mark or hash
The `routeToRegExp()` function, introduced by 840b5f0, could not extract
path params if the path contained question mark or hash. Although these
characters would normally be encoded in the path, they are decoded by
`$location.path()`, before being passed to the RegExp returned by
`routeToRegExp()`.

`routeToRegExp()` has to be able to deal with both encoded URL and
decoded path, because it is being shared between `ngRoute` and
`ngMocks`.

This commit fixes the issue, by introducing an `isUrl` option that
allows creating an appropriate RegExp for each usecase.
2018-08-25 23:59:53 +03:00
George Kalpakas fa715abf45 chore(doc-gen): upgrade dgeni-packages to 0.26.5
Related: #16671, angular/dgeni-packages#271
2018-08-23 15:08:29 +03:00
George Kalpakas f943e377e8 docs(angular.copy): fix formatting
Using `<br>` messes formatting (due to a bug in
`dgeni`/`dgeni-packages`). This started breaking in c387e0d79.

Fixes #16671
2018-08-21 11:46:06 +03:00
Martin Staffa 30084c1369 fix(ngHref): allow numbers and other objects in interpolation
Interpolated content in ngHref must be stringified before being passed to $$sanitizeUri by $sce. Before 1.7.x, the sanitization had happened on the already interpolated value inside $compile.

Closes #16652
Fixes #16626
2018-08-20 20:05:20 +02:00
Martin Staffa 668a33da34 fix(select): allow to select first option with value undefined
Previously, the value observer incorrectly assumed a value had changed even if
it was the first time it was set, which caused it to remove an option with
the value `undefined` from the internal option map.

Fixes #16653
Closes #16656
2018-08-08 09:18:10 +02:00
John Mantas 19e2347759 docs(ngRepeat): redundant "and" on line 77
Closes #16657
2018-08-07 16:21:28 +03:00
George Kalpakas f17292e1b5 docs(guide/migration): fix typos, format inline code 2018-08-06 15:52:32 +03:00
Martin Staffa 77b4330011 docs(select): remove solved known issue
The issue in question has been resolved some time in 2017.
The bug report is still open, but the behavior has changed:
https://bugzilla.mozilla.org/show_bug.cgi?id=126379

Let's hope they have tests for this!

Related #9134
2018-08-03 18:20:53 +02:00
27 changed files with 762 additions and 279 deletions
+24
View File
@@ -1,3 +1,27 @@
<a name="1.7.4"></a>
# 1.7.4 interstellar-exploration (2018-09-07)
## Bug Fixes
- **ngAria.ngClick:** prevent default event on space/enter only for non-interactive elements
([61b335](https://github.com/angular/angular.js/commit/61b33543ff8e7f32464dec98a46bf0a35e9b03a4),
[#16664](https://github.com/angular/angular.js/issues/16664),
[#16680](https://github.com/angular/angular.js/issues/16680))
- **ngAnimate:** remove the "prepare" classes with multiple structural animations
([3105b2](https://github.com/angular/angular.js/commit/3105b2c26a71594c4e7904efc18f4b2e9da25b1b),
[#16681](https://github.com/angular/angular.js/issues/16681),
[#16677](https://github.com/angular/angular.js/issues/16677))
- **$route:** correctly extract path params if the path contains a question mark or a hash
([2ceeb7](https://github.com/angular/angular.js/commit/2ceeb739f35e01fcebcabac4beeeb7684ae9f86d))
- **ngHref:** allow numbers and other objects in interpolation
([30084c](https://github.com/angular/angular.js/commit/30084c13699c814ff6703d7aa2d3947a9b2f7067),
[#16652](https://github.com/angular/angular.js/issues/16652),
[#16626](https://github.com/angular/angular.js/issues/16626))
- **select:** allow to select first option with value `undefined`
([668a33](https://github.com/angular/angular.js/commit/668a33da3439f17e61dfa8f6d9b114ebde8c9d87),
[#16653](https://github.com/angular/angular.js/issues/16653),
[#16656](https://github.com/angular/angular.js/issues/16656))
<a name="1.7.3"></a>
# 1.7.3 eventful-proposal (2018-08-03)
@@ -0,0 +1,9 @@
'use strict';
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
.config(function($animateProvider) {
$animateProvider.classNameFilter(/animate-/);
})
.run(function($rootScope) {
$rootScope.fileType = 'classfilter';
});
@@ -0,0 +1,6 @@
'use strict';
angular.module('repeatAnimateBenchmark', [])
.run(function($rootScope) {
$rootScope.fileType = 'noanimate';
});
+7
View File
@@ -0,0 +1,7 @@
'use strict';
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
.run(function($rootScope) {
$rootScope.fileType = 'default';
});
+24
View File
@@ -0,0 +1,24 @@
/* eslint-env node */
'use strict';
module.exports = function(config) {
config.set({
scripts: [
{
id: 'angular',
src: '/build/angular.js'
},
{
id: 'angular-animate',
src: '/build/angular-animate.js'
},
{
id: 'app',
src: 'app.js'
},
{
src: 'common.js'
}]
});
};
+120
View File
@@ -0,0 +1,120 @@
'use strict';
(function() {
var app = angular.module('repeatAnimateBenchmark');
app.config(function($compileProvider, $animateProvider) {
if ($compileProvider.debugInfoEnabled) {
$compileProvider.debugInfoEnabled(false);
}
});
app.run(function($animate) {
if ($animate.enabled) {
$animate.enabled(true);
}
});
app.controller('DataController', function($scope, $rootScope, $animate) {
var totalRows = 500;
var totalColumns = 20;
var data = $scope.data = [];
function fillData() {
if ($animate.enabled) {
$animate.enabled($scope.benchmarkType !== 'globallyDisabled');
}
for (var i = 0; i < totalRows; i++) {
data[i] = [];
for (var j = 0; j < totalColumns; j++) {
data[i][j] = {
i: i
};
}
}
}
benchmarkSteps.push({
name: 'enter',
fn: function() {
$scope.$apply(function() {
fillData();
});
}
});
benchmarkSteps.push({
name: 'leave',
fn: function() {
$scope.$apply(function() {
data = $scope.data = [];
});
}
});
});
app.directive('disableAnimations', function($animate) {
return {
link: {
pre: function(s, e) {
$animate.enabled(e, false);
}
}
};
});
app.directive('noop', function($animate) {
return {
link: {
pre: angular.noop
}
};
});
app.directive('baseline', function($document) {
return {
restrict: 'E',
link: function($scope, $element) {
var document = $document[0];
var i, j, row, cell, comment;
var template = document.createElement('span');
template.setAttribute('ng-repeat', 'foo in foos');
template.classList.add('ng-scope');
template.appendChild(document.createElement('span'));
template.appendChild(document.createTextNode(':'));
function createList() {
for (i = 0; i < $scope.data.length; i++) {
row = document.createElement('div');
$element[0].appendChild(row);
for (j = 0; j < $scope.data[i].length; j++) {
cell = template.cloneNode(true);
row.appendChild(cell);
cell.childNodes[0].textContent = i;
cell.ng339 = 'xxx';
comment = document.createComment('ngRepeat end: bar in foo');
row.appendChild(comment);
}
comment = document.createComment('ngRepeat end: foo in foos');
$element[0].appendChild(comment);
}
}
$scope.$watch('data.length', function(newVal) {
if (newVal === 0) {
while ($element[0].firstChild) {
$element[0].removeChild($element[0].firstChild);
}
} else {
createList();
}
});
}
};
});
})();
+70
View File
@@ -0,0 +1,70 @@
<div ng-app="repeatAnimateBenchmark" ng-cloak>
<div ng-controller="DataController">
<div class="container-fluid">
<p>
Tests rendering of an ngRepeat with 500 elements.<br>
Animations can be enabled / disabled in different ways.<br>
Two tests require reloading the app with different module / app configurations.
</p>
<div><label><input type="radio" ng-model="benchmarkType" value="none">none: </label></div>
<div><label><input type="radio" ng-model="benchmarkType" value="baseline">baseline (vanilla Javascript): </label></div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="enabled">enabled : </label> (requires <a href="./">app.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default' && fileType !== 'classfilter'" value="globallyDisabled">globally disabled:</label> (requires <a href="./">app.js</a> or <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="disabledParentElement">disabled by $animate.enabled() on parent element: </label> (requires <a href="./">app.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'noanimate'" value="noanimate">Without ngAnimate:</label> (requires <a href="?app=app-noanimate.js">app-noanimate.js</a>)</div>
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'classfilter'" value="disabledClassFilter">disabled by classNameFilter on element:</label> (requires <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
<ng-switch on="benchmarkType">
<baseline ng-switch-when="baseline">
</baseline>
<div ng-switch-when="noanimate">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="enabled">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="globallyDisabled">
<div noop>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="disabledClassFilter">
<div noop>
<div ng-repeat="row in data">
<span class="disable-animations" ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
<div ng-switch-when="disabledParentElement">
<div disable-animations>
<div ng-repeat="row in data">
<span ng-repeat="column in row">
<span>{{column.i}}</span>
</span>
</div>
</div>
</div>
</ng-switch>
</div>
</div>
</div>
+127 -111
View File
@@ -25,7 +25,7 @@ Additionally, we have removed some long-deprecated modules and APIs.
The most notable changes are:
- $resource has now support for request and requestError interceptors
- `$resource` has now support for request and requestError interceptors
- Several deprecated features have been removed:
- the `$controllerProvider.allowGlobals()` flag
@@ -36,8 +36,8 @@ The most notable changes are:
- the complete `ngScenario` module
Please note that feature development (without breaking changes) has happened in parallel on the
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those features
that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those
features that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
<br />
@@ -48,11 +48,11 @@ Below is the full list of breaking changes:
<a name="migrate1.6to1.7-ng-directives"></a>
### Core: _Directives_
<a name="migrate1.6to1.7-ng-directives-form"></a>
#### **form**
**Due to [223de5](https://github.com/angular/angular.js/commit/223de59e988dc0cc8b4ec3a045b7c0735eba1c77)**,
forms will now set $submitted on child forms when they are submitted.
forms will now set `$submitted` on child forms when they are submitted.
For example:
```
<form name="parentform" ng-submit="$ctrl.submit()">
@@ -63,15 +63,16 @@ For example:
</form>
```
Submitting this form will set $submitted on "parentform" and "childform".
Submitting this form will set `$submitted` on "parentform" and "childform".
Previously, it was only set on "parentform".
This change was introduced because mixing form and ngForm does not create
This change was introduced because mixing `form` and `ngForm` does not create
logically separate forms, but rather something like input groups.
Therefore, child forms should inherit the submission state from their parent form.
#### **input[radio]** and **input[checkbox]**
**Due to [656c8f](https://github.com/angular/angular.js/commit/656c8fa8f23b1277cc5c214c4d0237f3393afa1e)**,
`input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event.
Most apps should not be affected, as "change" is automatically fired by browsers after "click"
@@ -84,10 +85,10 @@ Two scenarios might need migration:
Before this change, custom click event listeners on radio / checkbox would be called after the
input element and `ngModel` had been updated, unless they were specifically registered before
the built-in click handlers.
After this change, they are called before the input is updated, and can call event.preventDefault()
to prevent the input from updating.
After this change, they are called before the input is updated, and can call
`event.preventDefault()` to prevent the input from updating.
If an app uses a click event listener that expects ngModel to be updated when it is called, it now
If an app uses a click event listener that expects `ngModel` to be updated when it is called, it now
needs to register a change event listener instead.
- Triggering click events:
@@ -95,50 +96,52 @@ needs to register a change event listener instead.
Conventional trigger functions:
The change event might not be fired when the input element is not attached to the document. This
can happen in **tests** that compile input elements and
trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method,
the change event will not be fired when the input isn't attached to the document.
can happen in **tests** that compile input elements and trigger click events on them. Depending on
the browser (Chrome and Safari) and the trigger method, the change event will not be fired when the
input isn't attached to the document.
Before:
```js
it('should update the model', inject(function($compile, $rootScope) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
it('should update the model', inject(function($compile, $rootScope) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
```
With this patch, `$rootScope.checkbox` might not be true, because the click event
hasn't triggered the change event. To make the test, work append the inputElm to the app's
`$rootElement`, and the `$rootElement` to the `$document`.
With this patch, `$rootScope.checkbox` might not be true, because the click event hasn't triggered
the change event. To make the test, work append `inputElm` to the app's `$rootElement`, and the
`$rootElement` to the `$document`.
After:
```js
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
$rootElement.append(inputElm);
$document.append($rootElement);
$rootElement.append(inputElm);
$document.append($rootElement);
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
expect($rootScope.checkbox).toBe(true);
});
```
#### **input\[number\]**
**Due to [aa3f95](https://github.com/angular/angular.js/commit/aa3f951330ec7b10b43ea884d9b5754e296770ec)**,
`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.
This affects apps that use `$parsers` or `$formatters` to transform the input / model value.
If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:
If you rely on the `$modelValue` validation, you can overwrite the `min`/`max` validator from a
custom directive, as seen in the following example directive definition object:
```
```js
{
restrict: 'A',
require: 'ngModel',
@@ -154,11 +157,11 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
#### **ngModel, input**
**Due to [74b04c](https://github.com/angular/angular.js/commit/74b04c9403af4fc7df5b6420f22c9f45a3e84140)**,
*Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month",
"time", "datetime-local", "week", no longer set `ngModelController.$error[inputType]`, and
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no
longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" no longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and
`ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers
@@ -166,6 +169,7 @@ and custom parsers easier.
#### **ngModelOptions**
**Due to [55ba44](https://github.com/angular/angular.js/commit/55ba44913e02650b56410aa9ab5eeea5d3492b68)**,
the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added
as an update trigger by the different input directives automatically.
@@ -179,24 +183,24 @@ See the following example:
Pre-1.7:
'mouseup' is also debounced by 500 milliseconds because 'default' is applied:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { 'default': 500, 'blur': 0 }
}
}"
```
1.7:
The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { '*': 500, 'blur': 0 }
}
}"
```
In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced:
```
```html
ng-model-options="{
updateOn: 'default blur mouseup',
debounce: { 'default': 500 }
@@ -207,14 +211,15 @@ ng-model-options="{
#### **ngStyle**
**Due to [15bbd3](https://github.com/angular/angular.js/commit/15bbd3e18cd89b91f7206a06c73d40e54a8a48a0)**,
previously the use of deep watch by ng-style would trigger styles to be
re-applied when nested state changed. Now only changes to direct
properties of the watched object will trigger changes.
the use of deep-watching in `ngStyle` has changed. Previously, `ngStyle` would trigger styles to be
re-applied whenever nested state changed. Now, only changes to direct properties of the watched
object will trigger changes.
<a name="migrate1.6to1.7-ng-services"></a>
### Core: _Services_
#### **$compile**
**Due to [38f8c9](https://github.com/angular/angular.js/commit/38f8c97af74649ce224b6dd45f433cc665acfbfb)**,
@@ -238,16 +243,16 @@ migrating to AngularJS 1.7.0 shouldn't require any further action.
3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need
to first migrate your code so that the flag can be flipped to `false`. The
instructions on how to do that are available in the "Migrating from 1.5 to 1.6"
guide:
https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
guide: https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)`
statement.
<hr />
**Due to [6ccbfa](https://github.com/angular/angular.js/commit/6ccbfa65d60a3dc396d0cf6da21b993ad74653fd)**,
the `xlink:href` security context for SVG's `a` and `image` elements has been lowered.
In the unlikely case that an app relied on RESOURCE_URL whitelisting for the
In the unlikely case that an app relied on `RESOURCE_URL` whitelisting for the
purpose of binding to the `xlink:href` property of SVG's `<a>` or `<image>`
elements and if the values do not pass the regular URL sanitization, they will
break.
@@ -258,37 +263,39 @@ To fix this you need to ensure that the values used for binding to the affected
`imgSrcSanitizationWhitelist` (for `<image>` elements).
<hr />
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
deepWatch is no longer used in in literal one-way bindings.
Previously when a literal value was passed into a directive/component via
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
deep-watching is no longer used in literal one-way bindings.
Previously, when a literal value was passed into a directive/component via
one-way binding it would be watched with a deep watcher.
For example, for `<my-component input="[a]">`, a new instance of the array
would be passed into the directive/component (and trigger $onChanges) not
would be passed into the directive/component (and trigger `$onChanges`) not
only if `a` changed but also if any sub property of `a` changed such as
`a.b` or `a.b.c.d.e` etc.
This also means a new but equal value for `a` would NOT trigger such a
change.
Now literal values use an input-based watch similar to other directive/component
Now, literal values use an input-based watch similar to other directive/component
one-way bindings. In this context inputs are the non-constant parts of the
literal. In the example above the input would be `a`. Changes are only
triggered when the inputs to the literal change.
literal. In the example above, the input would be `a`. Changes are only
triggered, when the inputs to the literal change.
<hr />
**Due to [1cf728](https://github.com/angular/angular.js/commit/1cf728e209a9e0016068fac2769827e8f747760e)**,
`base[href]` was added to the list of RESOURCE_URL context attributes.
`base[href]` was added to the list of `RESOURCE_URL` context attributes.
Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to
be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s
RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same
be trusted as a `RESOURCE_URL`. Now, `baseUrl` will be sent to `$sce`'s
`RESOURCE_URL` checks. By default, it will break unless `baseUrl` is of the same
origin as the application document.
Refer to the
[`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce)
for more info on how to trust a value in a RESOURCE_URL context.
for more info on how to trust a value in a `RESOURCE_URL` context.
Also, concatenation in trusted contexts is not allowed, which means that the
following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`.
@@ -315,10 +322,10 @@ except for the simplest of cases):
**Due to ([c2b8fa](https://github.com/angular/angular.js/commit/c2b8fab0a480204374d561d6b9b3d47347ac5570))**,
the arguments of `$watchGroup` callbacks have changed.
Previously when using `$watchGroup` the entries in `newValues` and
Previously, when using `$watchGroup`, the entries in `newValues` and
`oldValues` represented the *most recent change of each entry*.
Now the entries in `oldValues` will always equal the `newValues` of the previous
Now, the entries in `oldValues` will always equal the `newValues` of the previous
call of the listener. This means comparing the entries in `newValues` and
`oldValues` can be used to determine which individual expressions changed.
@@ -343,7 +350,7 @@ Now the `oldValue` will always equal the previous `newValue`:
Note the last call now shows `a === 2` in the `oldValues` array.
This also makes the `oldValue` of one-time watchers more clear. Previously
This also makes the `oldValue` of one-time watchers more clear. Previously,
the `oldValue` of a one-time watcher would remain `undefined` forever. For
example `$scope.$watchGroup(['a', '::b'], fn)` would previously:
@@ -367,7 +374,7 @@ Where now the `oldValue` will always equal the previous `newValue`:
#### **$interval**
**Due to [a8bef9](https://github.com/angular/angular.js/commit/a8bef95127775d83d80daa4617c33227c4b443d4)**,
`$interval.cancel() will throw an error if called with a promise that was not generated by
`$interval.cancel()` will throw an error if called with a promise that was not generated by
`$interval()`. Previously, it would silently do nothing.
Before:
@@ -393,7 +400,7 @@ $interval.cancel(promise); // Interval canceled.
#### **$timeout**
**Due to [336525](https://github.com/angular/angular.js/commit/3365256502344970f86355d3ace1cb4251ae9828)**,
`$timeout.cancel() will throw an error if called with a promise that was not generated by
`$timeout.cancel()` will throw an error if called with a promise that was not generated by
`$timeout()`. Previously, it would silently do nothing.
Before:
@@ -417,10 +424,11 @@ $timeout.cancel(promise); // Timeout canceled.
#### **$cookies**
**Due to [73c646](https://github.com/angular/angular.js/commit/73c6467f1468353215dc689c019ed83aa4993c77)**,
the `$cookieStore`service has been removed. Migrate to the $cookies service. Note that
for object values you need to use the `putObject` & `getObject` methods as
`get`/`put` will not correctly save/retrieve them.
the `$cookieStore`service has been removed. Migrate to the `$cookies` service. Note that
for object values you need to use the `putObject` & `getObject` methods, as
`get`/`put` will not correctly save/retrieve the object values.
Before:
```js
@@ -433,29 +441,31 @@ $cookieStore.remove('name');
#### **$templateRequest**
**Due to [c617d6](https://github.com/angular/angular.js/commit/c617d6dceee5b000bfceda44ced22fc16b48b18b)**,
give tpload error namespace has changed. Previously the `tpload` error was namespaced to `$compile`.
If you have code that matches errors of the form `[$compile:tpload]` it will no
longer run. You should change the code to match
`[$templateRequest:tpload]`.
the `tpload` error namespace has changed. Previously, the `tpload` error was namespaced to
`$compile`. If you have code that matches errors of the form `[$compile:tpload]` it will no longer
run. You should change the code to match `[$templateRequest:tpload]`.
<hr />
**Due to ([fb0099](https://github.com/angular/angular.js/commit/fb00991460cf69ae8bc7f1f826363d09c73c0d5e)**,
the service now returns the result of `$templateCache.put()` when making a server request to the
template. Previously it would return the content of the response directly.
This now means if you are decorating `$templateCache.put()` to manipulate the template, you will
now get this manipulated result also on the first `$templateRequest` rather than only on subsequent
calls (when the template is retrived from the cache).
In practice this should not affect any apps, as it is unlikely that they rely on the template being
`$templateRequest()` now returns the result of `$templateCache.put()` when making a server request
for a template. Previously, it would return the content of the response directly.
This means that if you are decorating `$templateCache.put()` to manipulate the template, you will
now get this manipulated result also on the first `$templateRequest()` call rather than only on
subsequent calls (when the template is retrieved from the cache).
In practice, this should not affect any apps, as it is unlikely that they rely on the template being
different in the first and subsequent calls.
#### **$animate**
**Due to [16b82c](https://github.com/angular/angular.js/commit/16b82c6afe0ab916fef1d6ca78053b00bf5ada83)**,
$animate.cancel(runner) now rejects the underlying
promise and calls the catch() handler on the runner
returned by $animate functions (enter, leave, move,
addClass, removeClass, setClass, animate).
Previously it would resolve the promise as if the animation
had ended successfully.
`$animate.cancel(runner)` now rejects the underlying promise and calls the `catch()` handler on the
runner returned by `$animate` functions (`enter`, `leave`, `move`, `addClass`, `removeClass`,
`setClass`, `animate`).
Previously, it would resolve the promise as if the animation had ended successfully.
Example:
@@ -468,7 +478,7 @@ runner.cancel();
```
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
To migrate, add a catch() handler to your animation runners.
To migrate, add a `catch()` handler to your animation runners.
#### **$controller**
@@ -479,26 +489,29 @@ has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()`
method that could enable this behavior, has been removed.
This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope
is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and
register your controller via the Module API or the $controllerProvider, e.g.
is considered bad practice. To migrate, remove the call to `$controllerProvider.allowGlobals()` in
the config, and register your controller via the Module API or the `$controllerProvider`, e.g.:
```
```js
angular.module('myModule', []).controller('myController', function() {...});
// or
angular.module('myModule', []).config(function($controllerProvider) {
$controllerProvider.register('myController', function() {...});
});
```
#### **$sce**
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
if you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
if you use `attrs.$set` for URL attributes (`a[href]` and `img[src]`) there will no
longer be any automated sanitization of the value. This is in line with other
programmatic operations, such as writing to the innerHTML of an element.
programmatic operations, such as writing to the `innerHTML` of an element.
If you are programmatically writing URL values to attributes from untrusted
input then you must sanitize it yourself. You could write your own sanitizer or copy
input, then you must sanitize it yourself. You could write your own sanitizer or copy
the private `$$sanitizeUri` service.
Note that values that have been passed through the `$interpolate` service within the
@@ -507,40 +520,43 @@ these values again.
<hr/>
Due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
binding {@link ng.$sce#trustAs trustAs()} and the short versions
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to
{@link ng.ngSrc}, {@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to {@link ng.ngSrc},
{@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
```js
$scope.imgThumbFn = function(id) {
return $sce.trustAsResourceUrl(someService.someUrl(id));
};
$scope.imgThumbFn = function(id) {
return $sce.trustAsResourceUrl(someService.someUrl(id));
};
```
```html
<img ng-src="{{imgThumbFn(imgId)}}">
<img ng-src="{{ imgThumbFn(imgId) }}" />
```
This is because {@link ng.$interpolate} is now responsible for sanitizing
the attribute value, and its watcher receives a new object from `trustAs()`
on every digest.
To migrate, compute the trusted value only when the input value changes:
```js
$scope.$watch('imgId', function(id) {
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
});
$scope.$watch('imgId', function(id) {
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
});
```
```html
<img ng-src="{{imgThumb}}">
<img ng-src="{{ imgThumb }}" />
```
<a name="migrate1.6to1.7-ng-filters"></a>
### Core: _Filters_
#### **orderBy**
**Due to [1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**,
when using `orderBy` to sort arrays containing `null` values, the `null` values
will be considered "greater than" all other values, except for `undefined`.
@@ -565,8 +581,9 @@ orderByFilter(['a', undefined, 'o', null, 'z']);
#### **jqLite**
**Due to [b7d396](https://github.com/angular/angular.js/commit/b7d396b8b6e8f27a1f4556d58fc903321e8d532a)**,
removeData() no longer removes event handlers.
`removeData()` no longer removes event handlers.
Before this commit `removeData()` invoked on an element removed its event
handlers as well. If you want to trigger a full cleanup of an element, change:
@@ -591,22 +608,20 @@ elem.remove();
will remove event handlers as well.
#### **Angular**
#### **Helpers**
**Due to [1daa4f](https://github.com/angular/angular.js/commit/1daa4f2231a89ee88345689f001805ffffa9e7de)**,
the helper functions `angular.lowercase` `and angular.uppercase` have been removed.
the helper functions `angular.lowercase` and `angular.uppercase` have been removed.
These functions have been deprecated since 1.5.0. They are internally
used, but should not be exposed as they contain special locale handling
(for Turkish) to maintain internal consistency regardless of user-set locale.
Developers should generally use the built-ins `toLowerCase` and `toUpperCase`
Developers should generally use the built-in methods `toLowerCase` and `toUpperCase`
or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases.
Further, we generally discourage using the angular.x helpers in application code.
<hr />
**Due to [e3ece2](https://github.com/angular/angular.js/commit/e3ece2fad9e1e6d47b5f06815ff186d7e6f44948)**,
`angular.isArray()` now supports Array subclasses.
@@ -627,18 +642,19 @@ be able to handle these objects better when copying or watching.
### ngAria
**Due to [6d5ef3](https://github.com/angular/angular.js/commit/6d5ef34fc6a974cde73157ba94f9706723dd8f5b)**,
the ngAria directive no longer sets aria-* attributes on input[type="hidden"] with ngModel.
This can affect apps that test for the presence of aria attributes on hidden inputs.
`ngAria` no longer sets `aria-*` attributes on `input[type="hidden"]` with `ngModel`.
This can affect apps that test for the presence of ARIA attributes on hidden inputs.
To migrate, remove these assertions.
In actual apps, this should not have a user-facing effect, as the previous behavior
was incorrect, and the new behavior is correct for accessibility.
<a name="migrate1.6to1.7-ngResource"></a>
### ngResource
#### **$resource**
**Due to [ea0585](https://github.com/angular/angular.js/commit/ea0585773bb93fd891576e2271254a17e15f1ddd)**,
the behavior of interceptors and success/error callbacks has changed.
@@ -720,6 +736,7 @@ User.get({id: 2}, onSuccess, onError);
```
<hr />
**Due to [240a3d](https://github.com/angular/angular.js/commit/240a3ddbf12a9bb79754031be95dae4b6bd2dded)**,
`$http` will be called asynchronously from `$resource` methods
(regardless if a `request`/`requestError` interceptor has been defined).
@@ -758,7 +775,6 @@ it('...', function() {
```
<a name="migrate1.6to1.7-ngScenario"></a>
### ngScenario
@@ -766,7 +782,7 @@ it('...', function() {
the angular scenario runner end-to-end test framework has been
removed from the project and will no longer be available on npm
or bower starting with 1.7.0.
It was deprecated and removed from the documentation in 2014.
It has been deprecated and removed from the documentation since 2014.
Applications that still use it should migrate to
[Protractor](http://www.protractortest.org).
Technically, it should also be possible to continue using an
@@ -778,10 +794,10 @@ not changed. However, we do not guarantee future compatibility.
### ngTouch
**Due to [11d9ad](https://github.com/angular/angular.js/commit/11d9ad1eb25eaf5967195e424108207427835d50)**,
the `ngClick` directive from the ngTouch module has been removed, and with it the
the `ngClick` directive of the `ngTouch` module has been removed, and with it the
corresponding `$touchProvider` and `$touch` service.
If you have included ngTouch v1.5.0 or higher in your application, and have not
If you have included `ngTouch` v1.5.0 or higher in your application, and have not
changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch`
service, then there are no migration steps for your code. Otherwise you must remove references to
the provider and service.
+1 -1
View File
@@ -102,7 +102,7 @@ For more information please visit {@link $http#json-vulnerability-protection JSO
Bear in mind that calling `$http.jsonp` gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
instant remote code execution in your application: the result of these requests is handed off
to the browser as regular `<script>` tag.
to the browser as a regular `<script>` tag.
## Strict Contextual Escaping
+6 -21
View File
@@ -7,26 +7,9 @@
This page describes the support status of the significant versions of AngularJS.
<div class="alert alert-info">
AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.
On July 1, 2018 AngularJS entered a 3 year Long Term Support period.
</div>
### Until July 1st 2018
Any version branch not shown in the following table (e.g. 1.5.x) is no longer being developed.
<table class="dev-status table table-bordered">
<thead>
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
</thead>
<tbody>
<tr class="security"><td><span>1.2.x</span></td><td>Security patches only</td><td>Last version to provide IE 8 support</td></tr>
<tr class="stable"><td><span>1.6.x</span></td><td>Patch Releases</td><td>Minor features, bug fixes, security patches - no breaking changes</td></tr>
<tr class="current"><td><span>1.7.x</span></td><td>Active Development</td><td>1.7.0 (not yet released) will be the last release of AngularJS to contain breaking changes</td></tr>
</tbody>
</table>
### After July 1st 2018
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
<table class="dev-status table table-bordered">
@@ -36,7 +19,7 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
<tbody>
<tr class="security">
<td><span>1.2.x</span></td>
<td>Long Term Support</td>
<td>Security patches only</td>
<td>Last version to provide IE 8 support</td>
</tr>
<tr class="stable">
@@ -49,14 +32,16 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
### Long Term Support
On July 1st 2018, we will enter a Long Term Support period for AngularJS.
On July 1st 2018, AngularJS entered a Long Term Support period for AngularJS.
At this time we will focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
We now focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
* A security flaw is detected in the 1.7.x branch of the framework
* One of the major browsers releases a version that will cause current production applications using AngularJS 1.7.x to stop working
* The jQuery library releases a version that will cause current production applications using AngularJS 1.7.x to stop working.
AngularJS 1.2.x will get a new version if and only if a new severe security issue is discovered.
### Blog Post
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
+1 -1
View File
@@ -33,7 +33,7 @@
"cross-spawn": "^4.0.0",
"cz-conventional-changelog": "1.1.4",
"dgeni": "^0.4.9",
"dgeni-packages": "^0.26.2",
"dgeni-packages": "^0.26.5",
"eslint-plugin-promise": "^3.6.0",
"event-stream": "~3.1.0",
"glob": "^6.0.1",
+5 -4
View File
@@ -792,15 +792,16 @@ function arrayRemove(array, value) {
* * If `source` is identical to `destination` an exception will be thrown.
*
* <br />
*
* <div class="alert alert-warning">
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
* and on `destination`) will be ignored.
* </div>
*
* @param {*} source The source that will be used to make a copy.
* Can be any type, including primitives, `null`, and `undefined`.
* @param {(Object|Array)=} destination Destination into which the source is copied. If
* provided, must be of the same type as `source`.
* @param {*} source The source that will be used to make a copy. Can be any type, including
* primitives, `null`, and `undefined`.
* @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
* must be of the same type as `source`.
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
+1 -1
View File
@@ -74,7 +74,7 @@
* For example, if an item is added to the collection, `ngRepeat` will know that all other items
* already have DOM elements, and will not re-render them.
*
* All different types of tracking functions, their syntax, and and their support for duplicate
* All different types of tracking functions, their syntax, and their support for duplicate
* items in collections can be found in the
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
*
+1 -13
View File
@@ -383,7 +383,7 @@ var SelectController =
if (optionAttrs.$attr.ngValue) {
// The value attribute is set by ngValue
var oldVal, hashedVal = NaN;
var oldVal, hashedVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
var removal;
@@ -556,18 +556,6 @@ var SelectController =
* {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
*
* @knownIssue
*
* In Firefox, the select model is only updated when the select element is blurred. For example,
* when switching between options with the keyboard, the select model is only set to the
* currently selected option when the select is blurred, e.g via tab key or clicking the mouse
* outside the select.
*
* This is due to an ambiguity in the select element specification. See the
* [issue on the Firefox bug tracker](https://bugzilla.mozilla.org/show_bug.cgi?id=126379)
* for more information, and this
* [Github comment for a workaround](https://github.com/angular/angular.js/issues/9134#issuecomment-130800488)
*
* @example
* ### Simple `select` elements with static options
*
+1 -1
View File
@@ -440,7 +440,7 @@ function $SceDelegateProvider() {
// If we get here, then we will either sanitize the value or throw an exception.
if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
// we attempt to sanitize non-resource URLs
return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL);
return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
} else if (type === SCE_CONTEXTS.RESOURCE_URL) {
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
return maybeTrusted;
+2 -3
View File
@@ -113,8 +113,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
// TODO(matsko): document the signature in a better way
return function(element, event, options) {
var node = getDomNode(element);
options = prepareAnimationOptions(options);
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
@@ -186,8 +184,9 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
forEach(groupedAnimations, function(animationEntry) {
var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
var extraClasses = options.addClass;
extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
var cacheKey = $$animateCache.cacheKey(node, event, extraClasses, options.removeClass);
var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass);
toBeSortedAnimations.push({
element: element,
+6 -2
View File
@@ -389,8 +389,12 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
var keyCode = event.which || event.keyCode;
if (keyCode === 13 || keyCode === 32) {
// Prevent the default browser behavior (e.g. scrolling when pressing spacebar).
event.preventDefault();
// If the event is triggered on a non-interactive element ...
if (nodeBlackList.indexOf(event.target.nodeName) === -1) {
// ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
// See https://github.com/angular/angular.js/issues/16664
event.preventDefault();
}
scope.$apply(callback);
}
+109 -89
View File
@@ -1771,8 +1771,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* See {@link ngMock.$httpBackend#when `when`} for more info.
*/
$httpBackend.whenRoute = function(method, url) {
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
var parsed = parseRouteUrl(url);
return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.keys);
};
/**
@@ -1955,8 +1955,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* See {@link ngMock.$httpBackend#expect `expect`} for more info.
*/
$httpBackend.expectRoute = function(method, url) {
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
var parsed = parseRouteUrl(url);
return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys);
};
@@ -2084,6 +2084,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
};
});
}
function parseRouteUrl(url) {
var strippedUrl = stripQueryAndHash(url);
var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true};
return routeToRegExp(strippedUrl, parseOptions);
}
}
function assertArgDefined(args, index, name) {
@@ -2092,110 +2098,124 @@ function assertArgDefined(args, index, name) {
}
}
function stripQueryAndHash(url) {
return url.replace(/[?#].*$/, '');
}
function MockHttpExpectation(method, url, data, headers, keys) {
function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders,
expectedKeys) {
function getUrlParams(u) {
var params = u.slice(u.indexOf('?') + 1).split('&');
return params.sort();
}
this.data = expectedData;
this.headers = expectedHeaders;
function compareUrl(u) {
return (url.slice(0, url.indexOf('?')) === u.slice(0, u.indexOf('?')) &&
getUrlParams(url).join() === getUrlParams(u).join());
}
this.data = data;
this.headers = headers;
this.match = function(m, u, d, h) {
if (method !== m) return false;
if (!this.matchUrl(u)) return false;
if (angular.isDefined(d) && !this.matchData(d)) return false;
if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
this.match = function(method, url, data, headers) {
if (expectedMethod !== method) return false;
if (!this.matchUrl(url)) return false;
if (angular.isDefined(data) && !this.matchData(data)) return false;
if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false;
return true;
};
this.matchUrl = function(u) {
if (!url) return true;
if (angular.isFunction(url.test)) return url.test(u);
if (angular.isFunction(url)) return url(u);
return (url === u || compareUrl(u));
this.matchUrl = function(url) {
if (!expectedUrl) return true;
if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url);
if (angular.isFunction(expectedUrl)) return expectedUrl(url);
return (expectedUrl === url || compareUrlWithQuery(url));
};
this.matchHeaders = function(h) {
if (angular.isUndefined(headers)) return true;
if (angular.isFunction(headers)) return headers(h);
return angular.equals(headers, h);
this.matchHeaders = function(headers) {
if (angular.isUndefined(expectedHeaders)) return true;
if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers);
return angular.equals(expectedHeaders, headers);
};
this.matchData = function(d) {
if (angular.isUndefined(data)) return true;
if (data && angular.isFunction(data.test)) return data.test(d);
if (data && angular.isFunction(data)) return data(d);
if (data && !angular.isString(data)) {
return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
this.matchData = function(data) {
if (angular.isUndefined(expectedData)) return true;
if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data);
if (expectedData && angular.isFunction(expectedData)) return expectedData(data);
if (expectedData && !angular.isString(expectedData)) {
return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data));
}
// eslint-disable-next-line eqeqeq
return data == d;
return expectedData == data;
};
this.toString = function() {
return method + ' ' + url;
return expectedMethod + ' ' + expectedUrl;
};
this.params = function(u) {
return angular.extend(parseQuery(), pathParams());
this.params = function(url) {
var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1);
var strippedUrl = stripQueryAndHash(url);
function pathParams() {
var keyObj = {};
if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj;
var m = url.exec(u);
if (!m) return keyObj;
for (var i = 1, len = m.length; i < len; ++i) {
var key = keys[i - 1];
var val = m[i];
if (key && val) {
keyObj[key.name || key] = val;
}
}
return keyObj;
}
function parseQuery() {
var obj = {}, key_value, key,
queryStr = u.indexOf('?') > -1
? u.substring(u.indexOf('?') + 1)
: '';
angular.forEach(queryStr.split('&'), function(keyValue) {
if (keyValue) {
key_value = keyValue.replace(/\+/g,'%20').split('=');
key = tryDecodeURIComponent(key_value[0]);
if (angular.isDefined(key)) {
var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
if (!hasOwnProperty.call(obj, key)) {
obj[key] = val;
} else if (angular.isArray(obj[key])) {
obj[key].push(val);
} else {
obj[key] = [obj[key],val];
}
}
}
});
return obj;
}
function tryDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch (e) {
// Ignore any invalid uri component
}
}
return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl));
};
function compareUrlWithQuery(url) {
var urlWithQueryRe = /^([^?]*)\?(.*)$/;
var expectedMatch = urlWithQueryRe.exec(expectedUrl);
var actualMatch = urlWithQueryRe.exec(url);
return !!(expectedMatch && actualMatch) &&
(expectedMatch[1] === actualMatch[1]) &&
(normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2]));
}
function normalizeQuery(queryStr) {
return queryStr.split('&').sort().join('&');
}
function extractParamsFromPath(strippedUrl) {
var keyObj = {};
if (!expectedUrl || !angular.isFunction(expectedUrl.test) ||
!expectedKeys || !expectedKeys.length) return keyObj;
var match = expectedUrl.exec(strippedUrl);
if (!match) return keyObj;
for (var i = 1, len = match.length; i < len; ++i) {
var key = expectedKeys[i - 1];
var val = match[i];
if (key && val) {
keyObj[key.name || key] = val;
}
}
return keyObj;
}
function extractParamsFromQuery(queryStr) {
var obj = {},
keyValuePairs = queryStr.split('&').
filter(angular.identity). // Ignore empty segments.
map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); });
angular.forEach(keyValuePairs, function(pair) {
var key = tryDecodeURIComponent(pair[0]);
if (angular.isDefined(key)) {
var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true;
if (!hasOwnProperty.call(obj, key)) {
obj[key] = val;
} else if (angular.isArray(obj[key])) {
obj[key].push(val);
} else {
obj[key] = [obj[key], val];
}
}
});
return obj;
}
function tryDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch (e) {
// Ignore any invalid uri component
}
}
}
function createMockXhr() {
+2 -1
View File
@@ -225,6 +225,7 @@ function $RouteProvider() {
}
routes[path] = angular.extend(
routeCopy,
{originalPath: path},
path && routeToRegExp(path, routeCopy)
);
@@ -235,7 +236,7 @@ function $RouteProvider() {
: path + '/';
routes[redirectPath] = angular.extend(
{redirectTo: path},
{originalPath: path, redirectTo: path},
routeToRegExp(redirectPath, routeCopy)
);
}
+9 -9
View File
@@ -3,15 +3,16 @@
/* global routeToRegExp: true */
/**
* @param path {string} path
* @param opts {Object} options
* @return {?Object}
* @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.)
* @param {Object} opts - Options.
* @return {Object} - An object containing an array of path parameter names (`keys`) and a regular
* expression (`regexp`) that can be used to identify a matching URL and extract the path
* parameter values.
*
* @description
* Normalizes the given path, returning a regular expression
* and the original path.
* Parses the given path, extracting path parameter names and a regular expression to match URLs.
*
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
* Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`.
*/
function routeToRegExp(path, opts) {
var keys = [];
@@ -21,11 +22,11 @@ function routeToRegExp(path, opts) {
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
var optional = option === '?' || option === '*?';
var star = option === '*' || option === '*?';
keys.push({ name: key, optional: optional });
keys.push({name: key, optional: optional});
slash = slash || '';
return (
(optional ? '(?:' + slash : slash + '(?:') +
(star ? '([^?#]+?)' : '([^/?#]+)') +
(star ? '(.+?)' : '([^/]+)') +
(optional ? '?)?' : ')')
);
})
@@ -36,7 +37,6 @@ function routeToRegExp(path, opts) {
}
return {
originalPath: path,
keys: keys,
regexp: new RegExp(
'^' + pattern + '(?:[?#]|$)',
+36
View File
@@ -79,6 +79,42 @@ describe('ngHref', function() {
}));
}
it('should bind numbers', inject(function($rootScope, $compile) {
element = $compile('<a ng-href="{{1234}}"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('1234');
}));
it('should bind and sanitize the result of a (custom) toString() function', inject(function($rootScope, $compile) {
$rootScope.value = {};
element = $compile('<a ng-href="{{value}}"></a>')($rootScope);
$rootScope.$digest();
expect(element.attr('href')).toEqual('[object Object]');
function SafeClass() {}
SafeClass.prototype.toString = function() {
return 'custom value';
};
$rootScope.value = new SafeClass();
$rootScope.$digest();
expect(element.attr('href')).toEqual('custom value');
function UnsafeClass() {}
UnsafeClass.prototype.toString = function() {
return 'javascript:alert(1);';
};
$rootScope.value = new UnsafeClass();
$rootScope.$digest();
expect(element.attr('href')).toEqual('unsafe:javascript:alert(1);');
}));
if (isDefined(window.SVGElement)) {
describe('SVGAElement', function() {
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
+39 -16
View File
@@ -1530,11 +1530,14 @@ describe('select', function() {
['a'],
NaN
], function(prop) {
scope.option1 = prop;
scope.option2 = 'red';
scope.selected = 'NOMATCH';
compile('<select ng-model="selected">' +
'<option ng-value="option1">{{option1}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
scope.$digest();
@@ -1571,10 +1574,12 @@ describe('select', function() {
NaN
], function(prop) {
scope.option = prop;
scope.option2 = 'red';
scope.selected = 'NOMATCH';
compile('<select ng-model="selected">' +
'<option ng-value="option">{{option}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
var selectController = element.controller('select');
@@ -1604,7 +1609,7 @@ describe('select', function() {
expect(scope.selected).toBe(null);
expect(element[0].selectedIndex).toBe(0);
expect(element.find('option').length).toBe(2);
expect(element.find('option').length).toBe(3);
expect(element.find('option').eq(0).prop('selected')).toBe(true);
expect(element.find('option').eq(0).val()).toBe(unknownValue(prop));
expect(element.find('option').eq(1).prop('selected')).toBe(false);
@@ -1617,6 +1622,7 @@ describe('select', function() {
expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE');
});
it('should interact with custom attribute $observe and $set calls', function() {
var log = [], optionAttr;
@@ -1638,26 +1644,43 @@ describe('select', function() {
optionAttr.$set('value', 'update');
expect(log[1]).toBe('update');
expect(element.find('option').eq(1).val()).toBe('string:update');
});
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.value = 'def';
scope.textvalue = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.value = 'def';
scope.textvalue = 'ghi';
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.textvalue = 'def';
scope.textvalue2 = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
scope.ngvalue = 'abc';
scope.textvalue = 'def';
scope.textvalue2 = 'ghi';
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
it('should select the first option if it is `undefined`', function() {
scope.selected = undefined;
scope.option1 = undefined;
scope.option2 = 'red';
compile('<select ng-model="selected">' +
'<option ng-value="option1">{{option1}}</option>' +
'<option ng-value="option2">{{option2}}</option>' +
'</select>');
expect(element).toEqualSelect(['undefined:undefined'], 'string:red');
});
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
});
describe('and select[multiple]', function() {
+81
View File
@@ -25,6 +25,7 @@ describe('ngAnimate integration tests', function() {
ss.destroy();
});
it('should cancel a running and started removeClass animation when a follow-up addClass animation adds the same class',
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
@@ -362,6 +363,7 @@ describe('ngAnimate integration tests', function() {
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
@@ -397,6 +399,7 @@ describe('ngAnimate integration tests', function() {
});
});
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
@@ -468,6 +471,84 @@ describe('ngAnimate integration tests', function() {
});
});
it('should remove the prepare classes when different structural animations happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
element = jqLite(
// Class animation on parent element is neeeded so the child elements get the prepare class
'<div id="outer" ng-class="{blue: cond}" ng-switch="cond">' +
'<div id="default" ng-switch-default></div>' +
'<div id="truthy" ng-switch-when="true"></div>' +
'</div>'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = false;
$rootScope.$digest();
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var truthySwitch = jqLite(parent[0].querySelector('#truthy'));
var defaultSwitch = jqLite(parent[0].querySelector('#default'));
expect(parent).not.toHaveClass('blue');
expect(parent).toHaveClass('blue-add');
expect(truthySwitch).toHaveClass('ng-enter-prepare');
expect(defaultSwitch).toHaveClass('ng-leave-prepare');
$animate.flush();
expect(parent).toHaveClass('blue');
expect(parent).not.toHaveClass('blue-add');
expect(truthySwitch).not.toHaveClass('ng-enter-prepare');
expect(defaultSwitch).not.toHaveClass('ng-leave-prepare');
});
});
it('should respect the element node for caching when animations with the same type happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
ss.addRule('.animate.ng-enter', 'transition:2s linear all;');
element = jqLite(
'<div>' +
'<div>' +
'<div id="noanimate" ng-if="cond"></div>' +
'</div>' +
'<div>' +
'<div id="animate" class="animate" ng-if="cond"></div>' +
'</div>' +
'</div>'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var noanimate = jqLite(parent[0].querySelector('#noanimate'));
var animate = jqLite(parent[0].querySelector('#animate'));
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).toHaveClass('ng-enter');
$animate.closeAndFlush();
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).not.toHaveClass('ng-enter');
});
});
it('should pack level elements into their own RAF flush', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
+29
View File
@@ -1,5 +1,7 @@
'use strict';
/* globals nodeBlackList false */
describe('$aria', function() {
var scope, $compile, element;
@@ -952,6 +954,33 @@ describe('$aria', function() {
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
});
they('should not prevent default keyboard action if an interactive $type element' +
'is nested inside ng-click', nodeBlackList, function(elementType) {
function createHTML(type) {
return '<' + type + '></' + type + '>';
}
compileElement(
'<section>' +
'<div ng-click="onClick($event)">' + createHTML(elementType) + '</div>' +
'</section>');
var divElement = element.find('div');
var interactiveElement = element.find(elementType);
// Use browserTrigger because it supports event bubbling
// 13 Enter
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 13});
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
clickEvents = [];
// 32 Space
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 32});
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
}
);
it('should trigger a click in browsers that provide `event.which` instead of `event.keyCode`',
function() {
compileElement(
+3 -3
View File
@@ -2251,7 +2251,7 @@ describe('ngMock', function() {
}
);
they('should ignore query params when matching in ' + routeShortcut + ' $prop method', methods,
function() {
function(method) {
angular.forEach([
{route: '/route1/:id', url: '/route1/Alpha', expectedParams: {id: 'Alpha'}},
{route: '/route2/:id', url: '/route2/Bravo/?', expectedParams: {id: 'Bravo'}},
@@ -2268,14 +2268,14 @@ describe('ngMock', function() {
], function(testDataEntry) {
callback.calls.reset();
var paramsSpy = jasmine.createSpy('params');
hb[routeShortcut](this, testDataEntry.route).respond(
hb[routeShortcut](method, testDataEntry.route).respond(
function(method, url, data, headers, params) {
paramsSpy(params);
// status, response, headers, statusText, xhrStatus
return [200, 'path', { 'x-header': 'foo' }, 'OK', 'complete'];
}
);
hb(this, testDataEntry.url, undefined, callback);
hb(method, testDataEntry.url, undefined, callback);
hb.flush();
expect(callback).toHaveBeenCalledOnceWith(200, 'path', 'x-header: foo', 'OK', 'complete');
expect(paramsSpy).toHaveBeenCalledOnceWith(testDataEntry.expectedParams);
+40
View File
@@ -77,5 +77,45 @@ describe('$routeParams', function() {
});
});
it('should correctly extract path params containing hashes and/or question marks', function() {
module(function($routeProvider) {
$routeProvider.when('/foo/:bar', {});
$routeProvider.when('/zoo/:bar/:baz/:qux', {});
});
inject(function($location, $rootScope, $routeParams) {
$location.path('/foo/bar?baz');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz'});
$location.path('/foo/bar?baz=val');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz=val'});
$location.path('/foo/bar#baz');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar#baz'});
$location.path('/foo/bar?baz#qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz#qux'});
$location.path('/foo/bar?baz=val#qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar?baz=val#qux'});
$location.path('/foo/bar#baz?qux');
$rootScope.$digest();
expect($routeParams).toEqual({bar: 'bar#baz?qux'});
$location.path('/zoo/bar?p1=v1#h1/baz?p2=v2#h2/qux?p3=v3#h3');
$rootScope.$digest();
expect($routeParams).toEqual({
bar: 'bar?p1=v1#h1',
baz: 'baz?p2=v2#h2',
qux: 'qux?p3=v3#h3'
});
});
});
});
+3 -3
View File
@@ -1727,9 +1727,9 @@ detective@^4.0.0:
acorn "^3.1.0"
defined "^1.0.0"
dgeni-packages@^0.26.2:
version "0.26.2"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.26.2.tgz#dac22d7e861d4d72ed42af5272714f42b6b5bf3d"
dgeni-packages@^0.26.5:
version "0.26.5"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.26.5.tgz#a76da27b40ce92dfc37a9e629ef9f1d3897f6bde"
dependencies:
canonical-path "0.0.2"
catharsis "^0.8.1"