Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d764b581d | |||
| 57a37fcc20 | |||
| 7f6ba5534f | |||
| b24bfae585 | |||
| d3e123b0a6 | |||
| bf1acf7b21 | |||
| fdaf4d5e27 | |||
| 1c47abc462 | |||
| 5222703444 | |||
| 4b38b44c91 | |||
| aa28e48e17 | |||
| f31586db41 | |||
| 8337b9b2d4 | |||
| 85a53ea9cd | |||
| 2d87ef8f23 | |||
| 3df8b637f3 | |||
| 28d3126fe5 | |||
| ad21f8feaf | |||
| 7f2df141cd | |||
| 4e735e5363 | |||
| a9db6073c9 | |||
| e48e27aa2b | |||
| 832b383cbc | |||
| c4bff290e5 | |||
| 55b00148dd | |||
| 23550b5e27 | |||
| 97d2a08c5a | |||
| 7f6efb2822 | |||
| b50867001b | |||
| 8b9ce885d3 | |||
| 56dae6fa1b | |||
| 611dcbc035 | |||
| 2c9066e012 | |||
| 8ecc9357ef | |||
| 71dca7c4c2 | |||
| ce77c25b06 | |||
| a1188721e1 | |||
| 1917ff86c4 | |||
| c60e1960c6 | |||
| 268e71eb8b | |||
| 03e6fb3df5 | |||
| 4fc9cf6289 |
+1
-1
@@ -1,7 +1,7 @@
|
||||
language: node_js
|
||||
sudo: false
|
||||
node_js:
|
||||
- '4.2'
|
||||
- '4.4'
|
||||
|
||||
cache:
|
||||
directories:
|
||||
|
||||
@@ -1,3 +1,91 @@
|
||||
<a name="1.4.10"></a>
|
||||
# 1.4.10 benignant-oscillation (2016-03-16)
|
||||
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
- **core:** only call `console.log` when `window.console` exists
|
||||
([beb00e44](https://github.com/angular/angular.js/commit/beb00e44de947981dbe35d5cf7a116e10ea8dc67),
|
||||
[#14006](https://github.com/angular/angular.js/issues/14006), [#14007](https://github.com/angular/angular.js/issues/14007), [#14047](https://github.com/angular/angular.js/issues/14047))
|
||||
- **$animateCss:** cancel fallback timeout when animation ends normally
|
||||
([a60bbc12](https://github.com/angular/angular.js/commit/a60bbc12e8c5170e70d95f1b2c3e309b3b95cb84),
|
||||
[#13787](https://github.com/angular/angular.js/issues/13787))
|
||||
- **$compile:**
|
||||
- allow directives to have decorators
|
||||
([77cdc37c](https://github.com/angular/angular.js/commit/77cdc37c65491b551fcf01a18ab848a693c293d7))
|
||||
- properly denormalize templates when only one of the start/end symbols is different
|
||||
([2d44a681](https://github.com/angular/angular.js/commit/2d44a681eb912a81a8bc8e16a278c45dae91fa24),
|
||||
[#13848](https://github.com/angular/angular.js/issues/13848))
|
||||
- handle boolean attributes in `@` bindings
|
||||
([2ffbfb0a](https://github.com/angular/angular.js/commit/2ffbfb0ad0647d103ff339ee4b772b62d4823bf3),
|
||||
[#13767](https://github.com/angular/angular.js/issues/13767), [#13769](https://github.com/angular/angular.js/issues/13769))
|
||||
- **$parse:**
|
||||
- prevent assignment on constructor properties
|
||||
([f47e2180](https://github.com/angular/angular.js/commit/f47e218006029f39b4785d820b430de3a0eebcb0),
|
||||
[#13417](https://github.com/angular/angular.js/issues/13417))
|
||||
- preserve expensive checks when runnning `$eval` inside an expression
|
||||
([96d62cc0](https://github.com/angular/angular.js/commit/96d62cc0fc77248d7e3ec4aa458bac0d3e072629))
|
||||
- copy `inputs` for expressions with expensive checks
|
||||
([0b7fff30](https://github.com/angular/angular.js/commit/0b7fff303f46202bbae1ff3ca9d0e5fa76e0fc9a))
|
||||
- **$rootScope:** set no context when calling helper functions for `$watch`
|
||||
([ab5c7698](https://github.com/angular/angular.js/commit/ab5c7698bb106669ca31b5f79a95afa54d65c5f1))
|
||||
- **$route:** allow preventing a route reload
|
||||
([4bc30314](https://github.com/angular/angular.js/commit/4bc3031497447ad527356f12bd0ceee1d7d09db5),
|
||||
[#9824](https://github.com/angular/angular.js/issues/9824), [#13894](https://github.com/angular/angular.js/issues/13894))
|
||||
- **$routeProvider:** properly handle optional eager path named groups
|
||||
([6a4403a1](https://github.com/angular/angular.js/commit/6a4403a11845173d6a96232f77d73aa544b182af),
|
||||
[#14011](https://github.com/angular/angular.js/issues/14011))
|
||||
- **copy:** add support for copying `Blob` objects
|
||||
([863a4232](https://github.com/angular/angular.js/commit/863a4232a6faa92428df45cd54d5a519be2434de),
|
||||
[#9669](https://github.com/angular/angular.js/issues/9669), [#14064](https://github.com/angular/angular.js/issues/14064))
|
||||
- **dateFilter:** follow the CLDR on pattern escape sequences
|
||||
([f476060d](https://github.com/angular/angular.js/commit/f476060de6cc016380c0343490a184543f853652),
|
||||
[#12839](https://github.com/angular/angular.js/issues/12839))
|
||||
- **dateFilter, input:** fix Date parsing in IE/Edge when timezone offset contains `:`
|
||||
([571afd65](https://github.com/angular/angular.js/commit/571afd6558786d7b99e2aebd307b4a94c9f2bb87),
|
||||
[#13880](https://github.com/angular/angular.js/issues/13880), [#13887](https://github.com/angular/angular.js/issues/13887))
|
||||
- **input:** re-validate when partially editing date-family inputs
|
||||
([02929f82](https://github.com/angular/angular.js/commit/02929f82f30449301ff18fea84a6396a017683b1),
|
||||
[#12207](https://github.com/angular/angular.js/issues/12207), [#13886](https://github.com/angular/angular.js/issues/13886))
|
||||
- **select:** handle corner case of adding options via a custom directive
|
||||
([df6e7315](https://github.com/angular/angular.js/commit/df6e731506831a3dc7f44c9a90abe17515450b3e),
|
||||
[#13874](https://github.com/angular/angular.js/issues/13874), [#13878](https://github.com/angular/angular.js/issues/13878))
|
||||
- **ngOptions:** always set the 'selected' attribute for selected options
|
||||
([f87e8288](https://github.com/angular/angular.js/commit/f87e8288fb69526fd240a66a046f5de52ed204de),
|
||||
[#14115](https://github.com/angular/angular.js/issues/14115))
|
||||
- **ngAnimate:** properly cancel previously running class-based animations
|
||||
([3b27dd37](https://github.com/angular/angular.js/commit/3b27dd37a2cc8a52992784ece6b371023dadf792),
|
||||
[#10156](https://github.com/angular/angular.js/issues/10156), [#13822](https://github.com/angular/angular.js/issues/13822))
|
||||
- **ngAnimateChildren:** make it compatible with `ngIf`
|
||||
([dc158e7e](https://github.com/angular/angular.js/commit/dc158e7e40624ef94c66560386522ef7e991a9ce),
|
||||
[#13865](https://github.com/angular/angular.js/issues/13865), [#13876](https://github.com/angular/angular.js/issues/13876))
|
||||
- **ngMockE2E:** pass `responseType` to `$delegate` when using `passThrough`
|
||||
([947cb4d1](https://github.com/angular/angular.js/commit/947cb4d1451afa4f5090a693df5b1968dd0df70c),
|
||||
[#5415](https://github.com/angular/angular.js/issues/5415), [#5783](https://github.com/angular/angular.js/issues/5783))
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- **$locale:** Include original locale ID in $locale
|
||||
([e69f3550](https://github.com/angular/angular.js/commit/e69f35507e10c994708ce4f1efba7573951d1acd),
|
||||
[#13390](https://github.com/angular/angular.js/issues/13390))
|
||||
- **ngAnimate:** provide ng-[event]-prepare class for structural animations
|
||||
([796f7ab4](https://github.com/angular/angular.js/commit/796f7ab41487e124b5b0c02dbf0a03bd581bf073))
|
||||
|
||||
|
||||
## Performance Improvements
|
||||
|
||||
- **$compile:** avoid needless overhead when wrapping text nodes
|
||||
([946d9ae9](https://github.com/angular/angular.js/commit/946d9ae90bb31fe911ebbe1b80cd4c8af5a665c6))
|
||||
- **ngRepeat:** avoid duplicate jqLite wrappers
|
||||
([d04c38c4](https://github.com/angular/angular.js/commit/d04c38c48968db777c3ea6a177ce2ff0116df7b4))
|
||||
- **ngAnimate:**
|
||||
- avoid jqLite/jQuery for upward DOM traversal
|
||||
([ab95ba65](https://github.com/angular/angular.js/commit/ab95ba65c08b38cace83de6717b7681079182b45))
|
||||
- avoid `$.fn.data` overhead with jQuery
|
||||
([86416bcb](https://github.com/angular/angular.js/commit/86416bcbee2192fa31c017163c5d856763182ade))
|
||||
|
||||
|
||||
<a name="1.4.9"></a>
|
||||
# 1.4.9 implicit-superannuation (2016-01-21)
|
||||
|
||||
|
||||
@@ -350,7 +350,7 @@ base and the path that should be handled by the application.
|
||||
### Base href constraints
|
||||
|
||||
The `$location` service is not able to function properly if the current URL is outside the URL given
|
||||
as the base href. This can have subtle confusing consequencies...
|
||||
as the base href. This can have subtle confusing consequences...
|
||||
|
||||
Consider a base href set as follows: `<base href="/base/">` (i.e. the application exists in the "folder"
|
||||
called `/base`). The URL `/base` is actually outside the application (it refers to the `base` file found
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
@ngdoc overview
|
||||
@name Decorators
|
||||
@sortOrder 345
|
||||
@description
|
||||
|
||||
# Decorators in AngularJS
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** This guide is targeted towards developers who are already familiar with AngularJS basics.
|
||||
If you're just getting started, we recommend the {@link tutorial/ tutorial} first.
|
||||
</div>
|
||||
|
||||
## What are decorators?
|
||||
|
||||
Decorators are a design pattern that is used to separate modification or *decoration* of a class without modifying the
|
||||
original source code. In Angular, decorators are functions that allow a service, directive or filter to be modified
|
||||
prior to its usage.
|
||||
|
||||
## How to use decorators
|
||||
|
||||
There are two ways to register decorators
|
||||
|
||||
- `$provide.decorator`, and
|
||||
- `module.decorator`
|
||||
|
||||
Each provide access to a `$delegate`, which is the instantiated service/directive/filter, prior to being passed to the
|
||||
service that required it.
|
||||
|
||||
### $provide.decorator
|
||||
|
||||
The {@link api/auto/service/$provide#decorator decorator function} allows access to a $delegate of the service once it
|
||||
has been instantiated. For example:
|
||||
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function $logDecorator($delegate) {
|
||||
|
||||
var originalWarn = $delegate.warn;
|
||||
$delegate.warn = function decoratedWarn(msg) {
|
||||
msg = 'Decorated Warn: ' + msg;
|
||||
originalWarn.apply($delegate, arguments);
|
||||
};
|
||||
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
After the `$log` service has been instantiated the decorator is fired. The decorator function has a `$delegate` object
|
||||
injected to provide access to the service that matches the selector in the decorator. This `$delegate` will be the
|
||||
service you are decorating. The return value of the function *provided to the decorator* will take place of the service,
|
||||
directive, or filter being decorated.
|
||||
|
||||
<hr>
|
||||
|
||||
The `$delegate` may be either modified or completely replaced. Given a service `myService` with a method `someFn`, the
|
||||
following could all be viable solutions:
|
||||
|
||||
|
||||
#### Completely Replace the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var myDecoratedService = {
|
||||
// new service object to replace myService
|
||||
};
|
||||
return myDecoratedService;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Patch the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
var someFn = $delegate.someFn;
|
||||
|
||||
function aNewFn() {
|
||||
// new service function
|
||||
someFn.apply($delegate, arguments);
|
||||
}
|
||||
|
||||
$delegate.someFn = aNewFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
#### Augment the $delegate
|
||||
```js
|
||||
angular.module('myApp', [])
|
||||
|
||||
.config([ '$provide', function($provide) {
|
||||
|
||||
$provide.decorator('myService', [
|
||||
'$delegate',
|
||||
function myServiceDecorator($delegate) {
|
||||
|
||||
function helperFn() {
|
||||
// an additional fn to add to the service
|
||||
}
|
||||
|
||||
$delegate.aHelpfulAddition = helperFn;
|
||||
return $delegate;
|
||||
}
|
||||
]);
|
||||
}]);
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note that whatever is returned by the decorator function will replace that which is being decorated. For example, a
|
||||
missing return statement will wipe out the entire object being decorated.
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
Decorators have different rules for different services. This is because services are registered in different ways.
|
||||
Services are selected by name, however filters and directives are selected by appending `"Filter"` or `"Directive"` to
|
||||
the end of the name. The `$delegate` provided is dictated by the type of service.
|
||||
|
||||
| Service Type | Selector | $delegate |
|
||||
|--------------|-------------------------------|-----------------------------------------------------------------------|
|
||||
| Service | `serviceName` | The `object` or `function` returned by the service |
|
||||
| Directive | `directiveName + 'Directive'` | An `Array.<DirectiveObject>`<sub>{@link guide/decorators#drtvArray 1}</sub> |
|
||||
| Filter | `filterName + 'Filter'` | The `function` returned by the filter |
|
||||
|
||||
<small id="drtvArray">1. Multiple directives may be registered to the same selector/name</small>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**NOTE:** Developers should take care in how and why they are modifying the `$delegate` for the service. Not only
|
||||
should expectations for the consumer be kept, but some functionality (such as directive registration) does not take
|
||||
place after decoration, but during creation/registration of the original service. This means, for example, that
|
||||
an action such as pushing a directive object to a directive `$delegate` will likely result in unexpected behavior.
|
||||
|
||||
Furthermore, great care should be taken when decorating core services, directives, or filters as this may unexpectedly
|
||||
or adversely affect the functionality of the framework.
|
||||
</div>
|
||||
|
||||
### module.decorator
|
||||
|
||||
This {@link api/ng/type/angular.Module#decorator function} is the same as the `$provide.decorator` function except it is
|
||||
exposed through the module API. This allows you to separate your decorator patterns from your module config blocks. The
|
||||
main caveat here is that you will need to take note the order in which you create your decorators.
|
||||
|
||||
Unlike in the module config block (which allows configuration of services prior to their creation), the service must be
|
||||
registered prior to the decorator (see {@link guide/providers#provider-recipe Provider Recipe}). For example, the
|
||||
following would not work because you are attempting to decorate outside of the configuration phase and the service
|
||||
hasn't been created yet:
|
||||
|
||||
```js
|
||||
// will cause an error since 'someService' hasn't been registered
|
||||
angular.module('myApp').decorator('someService', ...);
|
||||
|
||||
angular.module('myApp').factory('someService', ...);
|
||||
```
|
||||
|
||||
## Example Applications
|
||||
|
||||
The following sections provide examples each of a service decorator, a directive decorator, and a filter decorator.
|
||||
|
||||
### Service Decorator Example
|
||||
|
||||
This example shows how we can replace the $log service with our own to display log messages.
|
||||
|
||||
<example module="myServiceDecorator" name="service-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('myServiceDecorator', []).
|
||||
|
||||
controller('Ctrl', [
|
||||
'$scope',
|
||||
'$log',
|
||||
'$timeout',
|
||||
function($scope, $log, $timeout) {
|
||||
var types = ['error', 'warn', 'log', 'info' ,'debug'], i;
|
||||
|
||||
for (i = 0; i < types.length; i++) {
|
||||
$log[types[i]](types[i] + ': message ' + (i + 1));
|
||||
}
|
||||
|
||||
$timeout(function() {
|
||||
$log.info('info: message logged in timeout');
|
||||
});
|
||||
}
|
||||
]).
|
||||
|
||||
directive('myLog', [
|
||||
'$log',
|
||||
function($log) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<ul id="myLog"><li ng-repeat="l in myLog" class="{{l.type}}">{{l.message}}</li></ul>',
|
||||
scope: {},
|
||||
compile: function() {
|
||||
return function(scope) {
|
||||
scope.myLog = $log.stack;
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
]).
|
||||
|
||||
config([
|
||||
'$provide',
|
||||
function($provide) {
|
||||
|
||||
$provide.decorator('$log', [
|
||||
'$delegate',
|
||||
function logDecorator($delegate) {
|
||||
|
||||
var myLog = {
|
||||
warn: function(msg) {
|
||||
log(msg, 'warn');
|
||||
},
|
||||
error: function(msg) {
|
||||
log(msg, 'error');
|
||||
},
|
||||
info: function(msg) {
|
||||
log(msg, 'info');
|
||||
},
|
||||
debug: function(msg) {
|
||||
log(msg, 'debug');
|
||||
},
|
||||
log: function(msg) {
|
||||
log(msg, 'log');
|
||||
},
|
||||
stack: []
|
||||
};
|
||||
|
||||
function log(msg, type) {
|
||||
myLog.stack.push({ type: type, message: msg.toString() });
|
||||
if (console && console[type]) console[type](msg);
|
||||
}
|
||||
|
||||
return myLog;
|
||||
|
||||
}
|
||||
]);
|
||||
|
||||
}
|
||||
]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<h1>Logs</h1>
|
||||
<my-log></my-log>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="style.css">
|
||||
li.warn { color: yellow; }
|
||||
li.error { color: red; }
|
||||
li.info { color: blue }
|
||||
li.log { color: black }
|
||||
li.debug { color: green }
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should display log messages in dom', function() {
|
||||
element.all(by.repeater('l in myLog')).count().then(function(count) {
|
||||
expect(count).toEqual(6);
|
||||
});
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Directive Decorator Example
|
||||
|
||||
Failed interpolated expressions in `ng-href` attributes can easily go unnoticed. We can decorate `ngHref` to warn us of
|
||||
those conditions.
|
||||
|
||||
<example module="urlDecorator" name="directive-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('urlDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.id = 3;
|
||||
$scope.warnCount = 0; // for testing
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
// matchExpressions looks for interpolation markup in the directive attribute, extracts the expressions
|
||||
// from that markup (if they exist) and returns an array of those expressions
|
||||
function matchExpressions(str) {
|
||||
var exps = str.match(/{{([^}]+)}}/g);
|
||||
|
||||
// if there isn't any, get out of here
|
||||
if (exps === null) return;
|
||||
|
||||
exps = exps.map(function(exp) {
|
||||
var prop = exp.match(/[^{}]+/);
|
||||
return prop === null ? null : prop[0];
|
||||
});
|
||||
|
||||
return exps;
|
||||
}
|
||||
|
||||
// remember: directives must be selected by appending 'Directive' to the directive selector
|
||||
$provide.decorator('ngHrefDirective', [
|
||||
'$delegate',
|
||||
'$log',
|
||||
'$parse',
|
||||
function($delegate, $log, $parse) {
|
||||
|
||||
// store the original link fn
|
||||
var originalLinkFn = $delegate[0].link;
|
||||
|
||||
// replace the compile fn
|
||||
$delegate[0].compile = function(tElem, tAttr) {
|
||||
|
||||
// store the original exp in the directive attribute for our warning message
|
||||
var originalExp = tAttr.ngHref;
|
||||
|
||||
// get the interpolated expressions
|
||||
var exps = matchExpressions(originalExp);
|
||||
|
||||
// create and store the getters using $parse
|
||||
var getters = exps.map(function(el) {
|
||||
if (el) return $parse(el);
|
||||
});
|
||||
|
||||
return function newLinkFn(scope, elem, attr) {
|
||||
// fire the originalLinkFn
|
||||
originalLinkFn.apply($delegate[0], arguments);
|
||||
|
||||
// observe the directive attr and check the expressions
|
||||
attr.$observe('ngHref', function(val) {
|
||||
|
||||
// if we have getters and getters is an array...
|
||||
if (getters && angular.isArray(getters)) {
|
||||
|
||||
// loop through the getters and process them
|
||||
angular.forEach(getters, function(g, idx) {
|
||||
|
||||
// if val is truthy, then the warning won't log
|
||||
var val = angular.isFunction(g) ? g(scope) : true;
|
||||
if (!val) {
|
||||
$log.warn('NgHref Warning: "' + exps[idx] + '" in the expression "' + originalExp +
|
||||
'" is falsy!');
|
||||
|
||||
scope.warnCount++; // for testing
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
// get rid of the old link function since we return a link function in compile
|
||||
delete $delegate[0].link;
|
||||
|
||||
// return the $delegate
|
||||
return $delegate;
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<a ng-href="/products/{{ id }}/view" id="id3">View Product {{ id }}</a>
|
||||
- <strong>id == 3</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ id + 5 }}/view" id="id8">View Product {{ id + 5 }}</a>
|
||||
- <strong>id + 5 == 8</strong>, so no warning<br>
|
||||
<a ng-href="/products/{{ someOtherId }}/view" id="someOtherId">View Product {{ someOtherId }}</a>
|
||||
- <strong style="background-color: #ffff00;">someOtherId == undefined</strong>, so warn<br>
|
||||
<a ng-href="/products/{{ someOtherId + 5 }}/view" id="someOtherId5">View Product {{ someOtherId + 5 }}</a>
|
||||
- <strong>someOtherId + 5 == 5</strong>, so no warning<br>
|
||||
<div>Warn Count: {{ warnCount }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should warn when an expression in the interpolated value is falsy', function() {
|
||||
var id3 = element(by.id('id3'));
|
||||
var id8 = element(by.id('id8'));
|
||||
var someOther = element(by.id('someOtherId'));
|
||||
var someOther5 = element(by.id('someOtherId5'));
|
||||
|
||||
expect(id3.getText()).toEqual('View Product 3');
|
||||
expect(id3.getAttribute('href')).toContain('/products/3/view');
|
||||
|
||||
expect(id8.getText()).toEqual('View Product 8');
|
||||
expect(id8.getAttribute('href')).toContain('/products/8/view');
|
||||
|
||||
expect(someOther.getText()).toEqual('View Product');
|
||||
expect(someOther.getAttribute('href')).toContain('/products//view');
|
||||
|
||||
expect(someOther5.getText()).toEqual('View Product 5');
|
||||
expect(someOther5.getAttribute('href')).toContain('/products/5/view');
|
||||
|
||||
expect(element(by.binding('warnCount')).getText()).toEqual('Warn Count: 1');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
|
||||
### Filter Decorator Example
|
||||
|
||||
Let's say we have created an app that uses the default format for many of our `Date` filters. Suddenly requirements have
|
||||
changed (that never happens) and we need all of our default dates to be `'shortDate'` instead of `'mediumDate'`.
|
||||
|
||||
<example module="filterDecorator" name="filter-decorator">
|
||||
<file name="script.js">
|
||||
angular.module('filterDecorator', []).
|
||||
|
||||
controller('Ctrl', ['$scope', function ($scope) {
|
||||
$scope.genesis = new Date(2010, 0, 5);
|
||||
$scope.ngConf = new Date(2016, 4, 4);
|
||||
}]).
|
||||
|
||||
config(['$provide', function($provide) {
|
||||
|
||||
$provide.decorator('dateFilter', [
|
||||
'$delegate',
|
||||
function dateDecorator($delegate) {
|
||||
|
||||
// store the original filter
|
||||
var originalFilter = $delegate;
|
||||
|
||||
// return our filter
|
||||
return shortDateDefault;
|
||||
|
||||
// shortDateDefault sets the format to shortDate if it is falsy
|
||||
function shortDateDefault(date, format, timezone) {
|
||||
if (!format) format = 'shortDate';
|
||||
|
||||
// return the result of the original filter
|
||||
return originalFilter(date, format, timezone);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
}]);
|
||||
</file>
|
||||
|
||||
<file name="index.html">
|
||||
<div ng-controller="Ctrl">
|
||||
<div id="genesis">Initial Commit default to short date: {{ genesis | date }}</div>
|
||||
<div>ng-conf 2016 default short date: {{ ngConf | date }}</div>
|
||||
<div id="ngConf">ng-conf 2016 with full date format: {{ ngConf | date:'fullDate' }}</div>
|
||||
</div>
|
||||
</file>
|
||||
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should default date filter to short date format', function() {
|
||||
expect(element(by.id('genesis')).getText())
|
||||
.toMatch(/Initial Commit default to short date: \d{1,2}\/\d{1,2}\/\d{2}/);
|
||||
});
|
||||
|
||||
it('should still allow dates to be formatted', function() {
|
||||
expect(element(by.id('ngConf')).getText())
|
||||
.toMatch(/ng-conf 2016 with full date format\: [A-Za-z]+, [A-Za-z]+ \d{1,2}, \d{4}/);
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
@@ -101,7 +101,7 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
|
||||
## Learning Resources
|
||||
|
||||
###Books
|
||||
### Books
|
||||
* [AngularJS: Up and Running](http://www.amazon.com/AngularJS-Running-Enhanced-Productivity-Structured/dp/1491901942) by Brad Green and Shyam Seshadri
|
||||
* [Mastering Web App Development](http://www.amazon.com/Mastering-Web-Application-Development-AngularJS/dp/1782161821) by Pawel Kozlowski and Pete Bacon Darwin
|
||||
* [AngularJS Directives](http://www.amazon.com/AngularJS-Directives-Alex-Vanston/dp/1783280336) by Alex Vanston
|
||||
@@ -113,7 +113,7 @@ This is a short list of libraries with specific support and documentation for wo
|
||||
* [Responsive Web Design with AngularJS](http://www.amazon.com/Responsive-Design-AngularJS-Sandeep-Kumar/dp/178439842X) by Sandeep Kumar Patel
|
||||
* [Professional AngularJS](http://www.amazon.com/Professional-AngularJS-Valeri-Karpov/dp/1118832078/)
|
||||
|
||||
###Videos:
|
||||
### Videos:
|
||||
* [egghead.io](http://egghead.io/)
|
||||
* [Angular on YouTube](http://youtube.com/angularjs)
|
||||
* [Firebase Foundations for AngularJS](http://blog.watchandcode.com/firebase-foundations/)
|
||||
|
||||
@@ -257,7 +257,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
|
||||
|
||||
2. **Watcher registration**
|
||||
|
||||
During template linking directives register {@link
|
||||
During template linking, directives register {@link
|
||||
ng.$rootScope.Scope#$watch watches} on the scope. These watches will be
|
||||
used to propagate model values to the DOM.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ if (process.env.BROWSER_PROVIDER === 'browserstack') {
|
||||
capabilitiesForBrowserStack({
|
||||
browserName: 'chrome',
|
||||
platform: 'MAC',
|
||||
version: '34'
|
||||
version: '49'
|
||||
}),
|
||||
capabilitiesForBrowserStack({
|
||||
browserName: 'firefox',
|
||||
@@ -18,7 +18,7 @@ if (process.env.BROWSER_PROVIDER === 'browserstack') {
|
||||
capabilitiesForBrowserStack({
|
||||
browserName: 'safari',
|
||||
platform: 'MAC',
|
||||
version: '7'
|
||||
version: '9'
|
||||
})
|
||||
];
|
||||
} else {
|
||||
@@ -28,8 +28,8 @@ if (process.env.BROWSER_PROVIDER === 'browserstack') {
|
||||
config.multiCapabilities = [
|
||||
capabilitiesForSauceLabs({
|
||||
browserName: 'chrome',
|
||||
platform: 'OS X 10.9',
|
||||
version: '34'
|
||||
platform: 'OS X 10.11',
|
||||
version: '48'
|
||||
}),
|
||||
capabilitiesForSauceLabs({
|
||||
browserName: 'firefox',
|
||||
@@ -37,8 +37,8 @@ if (process.env.BROWSER_PROVIDER === 'browserstack') {
|
||||
}),
|
||||
capabilitiesForSauceLabs({
|
||||
browserName: 'safari',
|
||||
platform: 'OS X 10.9',
|
||||
version: '7'
|
||||
platform: 'OS X 10.11',
|
||||
version: '9'
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ echo "#################################"
|
||||
echo "#### Jenkins Build ############"
|
||||
echo "#################################"
|
||||
|
||||
source scripts/jenkins/set-node-version.sh
|
||||
|
||||
# Enable tracing and exit on first failure
|
||||
set -xe
|
||||
|
||||
scripts/jenkins/set-node-version.sh
|
||||
|
||||
# This is the default set of browsers to use on the CI server unless overridden via env variable
|
||||
if [[ -z "$BROWSERS" ]]
|
||||
then
|
||||
|
||||
@@ -35,7 +35,7 @@ function init {
|
||||
}
|
||||
|
||||
function build {
|
||||
./set-node-version.sh
|
||||
source ./set-node-version.sh
|
||||
cd ../..
|
||||
|
||||
npm install -g grunt-cli
|
||||
|
||||
@@ -4,4 +4,4 @@
|
||||
source ~/.nvm/nvm.sh
|
||||
|
||||
# Use node.js at 4.2.x
|
||||
nvm install 4.2
|
||||
nvm install 4.4
|
||||
|
||||
+16
-6
@@ -466,12 +466,22 @@ noop.$inject = [];
|
||||
* functional style.
|
||||
*
|
||||
```js
|
||||
function transformer(transformationFn, value) {
|
||||
return (transformationFn || angular.identity)(value);
|
||||
};
|
||||
function transformer(transformationFn, value) {
|
||||
return (transformationFn || angular.identity)(value);
|
||||
};
|
||||
|
||||
// E.g.
|
||||
function getResult(fn, input) {
|
||||
return (fn || angular.identity)(input);
|
||||
};
|
||||
|
||||
getResult(function(n) { return n * 2; }, 21); // returns 42
|
||||
getResult(null, 21); // returns 21
|
||||
getResult(undefined, 21); // returns 21
|
||||
```
|
||||
* @param {*} value to be returned.
|
||||
* @returns {*} the value passed in.
|
||||
*
|
||||
* @param {*} value to be returned.
|
||||
* @returns {*} the value passed in.
|
||||
*/
|
||||
function identity($) {return $;}
|
||||
identity.$inject = [];
|
||||
@@ -1594,7 +1604,7 @@ function bootstrap(element, modules, config) {
|
||||
//Encode angle brackets to prevent input from being sanitized to empty string #8683
|
||||
throw ngMinErr(
|
||||
'btstrpd',
|
||||
"App Already Bootstrapped with this Element '{0}'",
|
||||
"App already bootstrapped with this element '{0}'",
|
||||
tag.replace(/</,'<').replace(/>/,'>'));
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -197,9 +197,9 @@ function setupModuleLoader(window) {
|
||||
* @ngdoc method
|
||||
* @name angular.Module#decorator
|
||||
* @module ng
|
||||
* @param {string} The name of the service to decorate.
|
||||
* @param {Function} This function will be invoked when the service needs to be
|
||||
* instantiated and should return the decorated service instance.
|
||||
* @param {string} name The name of the service to decorate.
|
||||
* @param {Function} decorFn This function will be invoked when the service needs to be
|
||||
* instantiated and should return the decorated service instance.
|
||||
* @description
|
||||
* See {@link auto.$provide#decorator $provide.decorator()}.
|
||||
*/
|
||||
|
||||
@@ -41,7 +41,7 @@ function $AnchorScrollProvider() {
|
||||
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
|
||||
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
|
||||
* in the
|
||||
* [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
|
||||
* [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-indicated-part-of-the-document).
|
||||
*
|
||||
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
|
||||
* match any anchor whenever it changes. This can be disabled by calling
|
||||
|
||||
+2
-2
@@ -782,7 +782,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
function parseIsolateBindings(scope, directiveName, isController) {
|
||||
var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
|
||||
|
||||
var bindings = {};
|
||||
var bindings = createMap();
|
||||
|
||||
forEach(scope, function(definition, scopeName) {
|
||||
if (definition in bindingCache) {
|
||||
@@ -1153,7 +1153,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
(nodeName === 'img' && key === 'src')) {
|
||||
// sanitize a[href] and img[src] values
|
||||
this[key] = value = $$sanitizeUri(value, key === 'src');
|
||||
} else if (nodeName === 'img' && key === 'srcset') {
|
||||
} else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
|
||||
// sanitize img[srcset] values
|
||||
var result = "";
|
||||
|
||||
|
||||
@@ -393,7 +393,7 @@ var inputType = {
|
||||
}]);
|
||||
</script>
|
||||
<form name="myForm" ng-controller="DateController as dateCtrl">
|
||||
<label for="exampleInput">Pick a between 8am and 5pm:</label>
|
||||
<label for="exampleInput">Pick a time between 8am and 5pm:</label>
|
||||
<input type="time" id="exampleInput" name="input" ng-model="example.value"
|
||||
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
|
||||
<div role="alert">
|
||||
|
||||
@@ -188,8 +188,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
|
||||
restrict: 'A',
|
||||
compile: function ngBindHtmlCompile(tElement, tAttrs) {
|
||||
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
|
||||
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
|
||||
return (value || '').toString();
|
||||
var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
|
||||
// Unwrap the value to compare the actual inner safe value, not the wrapper object.
|
||||
return $sce.valueOf(val);
|
||||
});
|
||||
$compile.$$addBindingClass(tElement);
|
||||
|
||||
@@ -197,9 +198,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
|
||||
$compile.$$addBindingInfo(element, attr.ngBindHtml);
|
||||
|
||||
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
|
||||
// we re-evaluate the expr because we want a TrustedValueHolderType
|
||||
// for $sce, not a string
|
||||
element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
|
||||
// The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
|
||||
var value = ngBindHtmlGetter(scope);
|
||||
element.html($sce.getTrustedHtml(value) || '');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -78,7 +78,11 @@ function classDirective(name, selector) {
|
||||
updateClasses(oldClasses, newClasses);
|
||||
}
|
||||
}
|
||||
oldVal = shallowCopy(newVal);
|
||||
if (isArray(newVal)) {
|
||||
oldVal = newVal.map(function(v) { return shallowCopy(v); });
|
||||
} else {
|
||||
oldVal = shallowCopy(newVal);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngRequired
|
||||
* @restrict A
|
||||
*
|
||||
* @description
|
||||
*
|
||||
|
||||
@@ -93,7 +93,9 @@ function currencyFilter($locale) {
|
||||
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
|
||||
* If this is not provided then the fraction size is computed from the current locale's number
|
||||
* formatting pattern. In the case of the default locale, it will be 3.
|
||||
* @returns {string} Number rounded to fractionSize and places a “,” after each third digit.
|
||||
* @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current
|
||||
* locale (e.g., in the en_US locale it will have "." as the decimal separator and
|
||||
* include "," group separators after each third digit).
|
||||
*
|
||||
* @example
|
||||
<example module="numberFilterExample">
|
||||
@@ -296,7 +298,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
|
||||
|
||||
// extract decimals digits
|
||||
if (integerLen > 0) {
|
||||
decimals = digits.splice(integerLen);
|
||||
decimals = digits.splice(integerLen, digits.length);
|
||||
} else {
|
||||
decimals = digits;
|
||||
digits = [0];
|
||||
@@ -304,11 +306,11 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
|
||||
|
||||
// format the integer digits with grouping separators
|
||||
var groups = [];
|
||||
if (digits.length > pattern.lgSize) {
|
||||
groups.unshift(digits.splice(-pattern.lgSize).join(''));
|
||||
if (digits.length >= pattern.lgSize) {
|
||||
groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
|
||||
}
|
||||
while (digits.length > pattern.gSize) {
|
||||
groups.unshift(digits.splice(-pattern.gSize).join(''));
|
||||
groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
|
||||
}
|
||||
if (digits.length) {
|
||||
groups.unshift(digits.join(''));
|
||||
|
||||
+1
-1
@@ -555,7 +555,7 @@ function $HttpProvider() {
|
||||
* That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
|
||||
* For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
|
||||
* function will be reflected on the scope and in any templates where the object is data-bound.
|
||||
* To prevent his, transform functions should have no side-effects.
|
||||
* To prevent this, transform functions should have no side-effects.
|
||||
* If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
|
||||
* </div>
|
||||
*
|
||||
|
||||
@@ -199,6 +199,11 @@ function $InterpolateProvider() {
|
||||
* </file>
|
||||
* </example>
|
||||
*
|
||||
* @knownIssue
|
||||
* It is currently not possible for an interpolated expression to contain the interpolation end
|
||||
* symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e.
|
||||
* an interpolated expression consisting of a single-quote (`'`) and the `' }}` string.
|
||||
*
|
||||
* @param {string} text The text with markup to interpolate.
|
||||
* @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
|
||||
* embedded expression in order to return an interpolation function. Strings with no
|
||||
|
||||
+1
-1
@@ -482,7 +482,7 @@ var locationPrototype = {
|
||||
* ```
|
||||
*
|
||||
* @param {(string|number)=} path New path
|
||||
* @return {string} path
|
||||
* @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter
|
||||
*/
|
||||
path: locationGetterSetter('$$path', function(path) {
|
||||
path = path !== null ? path.toString() : '';
|
||||
|
||||
+3
-3
@@ -13,15 +13,15 @@
|
||||
* [Kris Kowal's Q](https://github.com/kriskowal/q).
|
||||
*
|
||||
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
|
||||
* implementations, and the other which resembles ES6 promises to some degree.
|
||||
* implementations, and the other which resembles ES6 (ES2015) promises to some degree.
|
||||
*
|
||||
* # $q constructor
|
||||
*
|
||||
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
|
||||
* function as the first argument. This is similar to the native Promise implementation from ES6 Harmony,
|
||||
* function as the first argument. This is similar to the native Promise implementation from ES6,
|
||||
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
|
||||
*
|
||||
* While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are
|
||||
* While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
|
||||
* available yet.
|
||||
*
|
||||
* It can be used like so:
|
||||
|
||||
@@ -31,7 +31,7 @@ function $TemplateRequestProvider() {
|
||||
// are included in there. This also makes Angular accept any script
|
||||
// directive, no matter its name. However, we still need to unwrap trusted
|
||||
// types.
|
||||
if (!isString(tpl) || !$templateCache.get(tpl)) {
|
||||
if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
|
||||
tpl = $sce.getTrustedResourceUrl(tpl);
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
var activeAnimationsLookup = new $$HashMap();
|
||||
var disabledElementsLookup = new $$HashMap();
|
||||
var animationsEnabled = null;
|
||||
// $document might be mocked out in tests and won't include a real document.
|
||||
// Providing an empty object with hidden = true will prevent animations from running
|
||||
var rawDocument = $document[0] || {hidden: true};
|
||||
|
||||
function postDigestTaskFactory() {
|
||||
var postDigestCalled = false;
|
||||
@@ -331,12 +334,14 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
|
||||
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
|
||||
|
||||
var documentHidden = rawDocument.hidden;
|
||||
|
||||
// this is a hard disable of all animations for the application or on
|
||||
// the element itself, therefore there is no need to continue further
|
||||
// past this point if not enabled
|
||||
// Animations are also disabled if the document is currently hidden (page is not visible
|
||||
// to the user), because browsers slow down or do not flush calls to requestAnimationFrame
|
||||
var skipAnimations = !animationsEnabled || $document[0].hidden || disabledElementsLookup.get(node);
|
||||
var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
|
||||
var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
|
||||
var hasExistingAnimation = !!existingAnimation.state;
|
||||
|
||||
@@ -347,7 +352,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
}
|
||||
|
||||
if (skipAnimations) {
|
||||
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
|
||||
if (documentHidden) notifyProgress(runner, event, 'start');
|
||||
close();
|
||||
if (documentHidden) notifyProgress(runner, event, 'close');
|
||||
return runner;
|
||||
}
|
||||
|
||||
@@ -497,6 +505,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
markElementAnimationState(element, RUNNING_STATE);
|
||||
var realRunner = $$animation(element, event, animationDetails.options);
|
||||
|
||||
// this will update the runner's flow-control events based on
|
||||
// the `realRunner` object.
|
||||
runner.setHost(realRunner);
|
||||
notifyProgress(runner, event, 'start', {});
|
||||
|
||||
realRunner.done(function(status) {
|
||||
close(!status);
|
||||
var animationDetails = activeAnimationsLookup.get(node);
|
||||
@@ -505,11 +518,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
}
|
||||
notifyProgress(runner, event, 'close', {});
|
||||
});
|
||||
|
||||
// this will update the runner's flow-control events based on
|
||||
// the `realRunner` object.
|
||||
runner.setHost(realRunner);
|
||||
notifyProgress(runner, event, 'start', {});
|
||||
});
|
||||
|
||||
return runner;
|
||||
@@ -578,7 +586,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
|
||||
* d) the element is not a child of the $rootElement
|
||||
*/
|
||||
function areAnimationsAllowed(element, parentElement, event) {
|
||||
var bodyElement = jqLite($document[0].body);
|
||||
var bodyElement = jqLite(rawDocument.body);
|
||||
var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
|
||||
var rootElementDetected = isMatchingElement(element, $rootElement);
|
||||
var parentAnimationDetected = false;
|
||||
|
||||
@@ -127,7 +127,7 @@ function stripCommentsFromElement(element) {
|
||||
if (element instanceof jqLite) {
|
||||
switch (element.length) {
|
||||
case 0:
|
||||
return [];
|
||||
return element;
|
||||
break;
|
||||
|
||||
case 1:
|
||||
|
||||
@@ -410,6 +410,13 @@ angular.module('ngMessages', [])
|
||||
|
||||
$scope.$watchCollection($attrs.ngMessages || $attrs['for'], ctrl.render);
|
||||
|
||||
// If the element is destroyed, proactively destroy all the currently visible messages
|
||||
$element.on('$destroy', function() {
|
||||
forEach(messages, function(item) {
|
||||
item.message.detach();
|
||||
});
|
||||
});
|
||||
|
||||
this.reRender = function() {
|
||||
if (!renderLater) {
|
||||
renderLater = true;
|
||||
@@ -444,6 +451,7 @@ angular.module('ngMessages', [])
|
||||
function findPreviousMessage(parent, comment) {
|
||||
var prevNode = comment;
|
||||
var parentLookup = [];
|
||||
|
||||
while (prevNode && prevNode !== parent) {
|
||||
var prevKey = prevNode.$$ngMessageNode;
|
||||
if (prevKey && prevKey.length) {
|
||||
@@ -455,8 +463,11 @@ angular.module('ngMessages', [])
|
||||
if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) == -1) {
|
||||
parentLookup.push(prevNode);
|
||||
prevNode = prevNode.childNodes[prevNode.childNodes.length - 1];
|
||||
} else if (prevNode.previousSibling) {
|
||||
prevNode = prevNode.previousSibling;
|
||||
} else {
|
||||
prevNode = prevNode.previousSibling || prevNode.parentNode;
|
||||
prevNode = prevNode.parentNode;
|
||||
parentLookup.push(prevNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -664,8 +675,8 @@ function ngMessageDirectiveFactory(restrict) {
|
||||
// when we are destroying the node later.
|
||||
var $$attachId = currentElement.$$attachId = ngMessagesCtrl.getAttachId();
|
||||
|
||||
// in the event that the parent element is destroyed
|
||||
// by any other structural directive then it's time
|
||||
// in the event that the element or a parent element is destroyed
|
||||
// by another structural directive then it's time
|
||||
// to deregister the message from the controller
|
||||
currentElement.on('$destroy', function() {
|
||||
if (currentElement && currentElement.$$attachId === $$attachId) {
|
||||
|
||||
Vendored
+6
@@ -2480,6 +2480,12 @@ if (window.jasmine || window.mocha) {
|
||||
window.inject = angular.mock.inject = function() {
|
||||
var blockFns = Array.prototype.slice.call(arguments, 0);
|
||||
var errorForStack = new Error('Declaration Location');
|
||||
// IE10+ and PhanthomJS do not set stack trace information, until the error is thrown
|
||||
if (!errorForStack.stack) {
|
||||
try {
|
||||
throw errorForStack;
|
||||
} catch (e) {}
|
||||
}
|
||||
return isSpecRunning() ? workFn.call(currentSpec) : workFn;
|
||||
/////////////////////
|
||||
function workFn() {
|
||||
|
||||
@@ -7602,6 +7602,27 @@ describe('$compile', function() {
|
||||
|
||||
describe('img[srcset] sanitization', function() {
|
||||
|
||||
it('should not error if undefined', function() {
|
||||
var linked = false;
|
||||
module(function() {
|
||||
directive('setter', valueFn(function(scope, elem, attrs) {
|
||||
attrs.$set('srcset', 'http://example.com/');
|
||||
expect(attrs.srcset).toBe('http://example.com/');
|
||||
|
||||
attrs.$set('srcset', undefined);
|
||||
expect(attrs.srcset).toBeUndefined();
|
||||
|
||||
linked = true;
|
||||
}));
|
||||
});
|
||||
inject(function($compile, $rootScope) {
|
||||
element = $compile('<img setter></img>')($rootScope);
|
||||
|
||||
expect(linked).toBe(true);
|
||||
expect(element.attr('srcset')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT require trusted values for img srcset', inject(function($rootScope, $compile, $sce) {
|
||||
element = $compile('<img srcset="{{testUrl}}"></img>')($rootScope);
|
||||
$rootScope.testUrl = 'http://example.com/image.png';
|
||||
|
||||
@@ -142,6 +142,16 @@ describe('ngBind*', function() {
|
||||
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
|
||||
$rootScope.html = 'hello';
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should one-time bind if the expression starts with two colons', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-bind-html="::html"></div>')($rootScope);
|
||||
$rootScope.html = '<div onclick="">hello</div>';
|
||||
@@ -176,7 +186,18 @@ describe('ngBind*', function() {
|
||||
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should watch the string value to avoid infinite recursion', inject(function($rootScope, $compile, $sce) {
|
||||
it('should update html', inject(function($rootScope, $compile, $sce) {
|
||||
element = $compile('<div ng-bind-html="html"></div>')($rootScope);
|
||||
$rootScope.html = $sce.trustAsHtml('hello');
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('hello');
|
||||
$rootScope.html = $sce.trustAsHtml('goodbye');
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('goodbye');
|
||||
}));
|
||||
|
||||
it('should not cause infinite recursion for trustAsHtml object watches',
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/3932
|
||||
// If the binding is a function that creates a new value on every call via trustAs, we'll
|
||||
// trigger an infinite digest if we don't take care of it.
|
||||
@@ -188,6 +209,33 @@ describe('ngBind*', function() {
|
||||
expect(angular.lowercase(element.html())).toEqual('<div onclick="">hello</div>');
|
||||
}));
|
||||
|
||||
it('should handle custom $sce objects', function() {
|
||||
function MySafeHtml(val) { this.val = val; }
|
||||
|
||||
module(function($provide) {
|
||||
$provide.decorator('$sce', function($delegate) {
|
||||
$delegate.trustAsHtml = function(html) { return new MySafeHtml(html); };
|
||||
$delegate.getTrustedHtml = function(mySafeHtml) { return mySafeHtml.val; };
|
||||
$delegate.valueOf = function(v) { return v instanceof MySafeHtml ? v.val : v; };
|
||||
return $delegate;
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile, $sce) {
|
||||
// Ref: https://github.com/angular/angular.js/issues/14526
|
||||
// Previous code used toString for change detection, which fails for custom objects
|
||||
// that don't override toString.
|
||||
element = $compile('<div ng-bind-html="getHtml()"></div>')($rootScope);
|
||||
var html = 'hello';
|
||||
$rootScope.getHtml = function() { return $sce.trustAsHtml(html); };
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('hello');
|
||||
html = 'goodbye';
|
||||
$rootScope.$digest();
|
||||
expect(angular.lowercase(element.html())).toEqual('goodbye');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when $sanitize is available', function() {
|
||||
beforeEach(function() { module('ngSanitize'); });
|
||||
|
||||
|
||||
@@ -409,6 +409,22 @@ describe('ngClass', function() {
|
||||
expect(e2.hasClass('even')).toBeTruthy();
|
||||
expect(e2.hasClass('odd')).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should support mixed array/object variable with a mutating object',
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-class="classVar"></div>')($rootScope);
|
||||
|
||||
$rootScope.classVar = [{orange: true}];
|
||||
$rootScope.$digest();
|
||||
expect(element).toHaveClass('orange');
|
||||
|
||||
$rootScope.classVar[0].orange = false;
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(element).not.toHaveClass('orange');
|
||||
})
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
describe('ngClass animations', function() {
|
||||
|
||||
@@ -28,5 +28,10 @@ describe('ngSrcset', function() {
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('srcset')).toBe('http://example.com/image1.png 1x,unsafe:javascript:doEvilStuff() 2x');
|
||||
}));
|
||||
|
||||
it('should not throw an error if undefined', inject(function($rootScope, $compile) {
|
||||
element = $compile('<img ng-attr-srcset="{{undefined}}">')($rootScope);
|
||||
$rootScope.$digest();
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,11 @@ describe('filters', function() {
|
||||
|
||||
it('should format according to different patterns', function() {
|
||||
pattern.gSize = 2;
|
||||
var num = formatNumber(1234567.89, pattern, ',', '.');
|
||||
var num = formatNumber(99, pattern, ',', '.');
|
||||
expect(num).toBe('99');
|
||||
num = formatNumber(888, pattern, ',', '.');
|
||||
expect(num).toBe('888');
|
||||
num = formatNumber(1234567.89, pattern, ',', '.');
|
||||
expect(num).toBe('12,34,567.89');
|
||||
num = formatNumber(1234.56, pattern, ',', '.');
|
||||
expect(num).toBe('1,234.56');
|
||||
|
||||
@@ -81,6 +81,39 @@ describe('$templateRequest', function() {
|
||||
}).not.toThrow();
|
||||
}));
|
||||
|
||||
it('should accept empty templates and refuse null or undefined templates in cache',
|
||||
inject(function($rootScope, $templateRequest, $templateCache, $sce) {
|
||||
|
||||
// Will throw on any template not in cache.
|
||||
spyOn($sce, 'getTrustedResourceUrl').andReturn(false);
|
||||
|
||||
expect(function() {
|
||||
$templateRequest('tpl.html'); // should go through $sce
|
||||
$rootScope.$digest();
|
||||
}).toThrow();
|
||||
|
||||
$templateCache.put('tpl.html'); // is a no-op, so $sce check as well.
|
||||
expect(function() {
|
||||
$templateRequest('tpl.html');
|
||||
$rootScope.$digest();
|
||||
}).toThrow();
|
||||
$templateCache.removeAll();
|
||||
|
||||
$templateCache.put('tpl.html', null); // makes no sense, but it's been added, so trust it.
|
||||
expect(function() {
|
||||
$templateRequest('tpl.html');
|
||||
$rootScope.$digest();
|
||||
}).not.toThrow();
|
||||
$templateCache.removeAll();
|
||||
|
||||
$templateCache.put('tpl.html', ''); // should work (empty template)
|
||||
expect(function() {
|
||||
$templateRequest('tpl.html');
|
||||
$rootScope.$digest();
|
||||
}).not.toThrow();
|
||||
$templateCache.removeAll();
|
||||
}));
|
||||
|
||||
it('should keep track of how many requests are going on',
|
||||
inject(function($rootScope, $templateRequest, $httpBackend) {
|
||||
|
||||
|
||||
@@ -395,6 +395,19 @@ describe("animations", function() {
|
||||
expect(capturedAnimation).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should not attempt to perform an animation on an empty jqLite collection',
|
||||
inject(function($rootScope, $animate) {
|
||||
|
||||
element.html('');
|
||||
var emptyNode = jqLite(element[0].firstChild);
|
||||
|
||||
$animate.addClass(emptyNode, 'some-class');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(capturedAnimation).toBeFalsy();
|
||||
})
|
||||
);
|
||||
|
||||
it('should perform the leave domOperation if a text node is used',
|
||||
inject(function($rootScope, $animate) {
|
||||
|
||||
@@ -1256,7 +1269,7 @@ describe("animations", function() {
|
||||
expect(element).not.toHaveClass('green');
|
||||
}));
|
||||
|
||||
it('should automatically cancel out class-based animations if the element already contains or doesn\' contain the applied classes',
|
||||
it('should automatically cancel out class-based animations if the element already contains or doesn\'t contain the applied classes',
|
||||
inject(function($animate, $rootScope) {
|
||||
|
||||
parent.append(element);
|
||||
@@ -1391,6 +1404,36 @@ describe("animations", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not run animations if the document is unavailable', function() {
|
||||
var capturedAnimation;
|
||||
|
||||
module(function($provide) {
|
||||
$provide.value('$document', {});
|
||||
|
||||
$provide.factory('$$animation', function($$AnimateRunner) {
|
||||
return function(element, method, options) {
|
||||
capturedAnimation = arguments;
|
||||
return new $$AnimateRunner();
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
$animate.enabled(true);
|
||||
|
||||
var spy = jasmine.createSpy();
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
var runner = $animate.enter(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(capturedAnimation).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('[ng-animate-children]', function() {
|
||||
var parent, element, child, capturedAnimation, captureLog;
|
||||
beforeEach(module(function($provide) {
|
||||
@@ -2137,5 +2180,169 @@ describe("animations", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when animations are skipped, disabled, or invalid', function() {
|
||||
|
||||
var overriddenAnimationRunner;
|
||||
var capturedAnimation;
|
||||
var capturedAnimationHistory;
|
||||
var defaultFakeAnimationRunner;
|
||||
var parent;
|
||||
var parent2;
|
||||
|
||||
beforeEach(module(function($provide) {
|
||||
overriddenAnimationRunner = null;
|
||||
capturedAnimation = null;
|
||||
capturedAnimationHistory = [];
|
||||
|
||||
$provide.value('$$animation', function() {
|
||||
capturedAnimationHistory.push(capturedAnimation = arguments);
|
||||
return overriddenAnimationRunner || defaultFakeAnimationRunner;
|
||||
});
|
||||
|
||||
return function($rootElement, $q, $animate, $$AnimateRunner, $document) {
|
||||
defaultFakeAnimationRunner = new $$AnimateRunner();
|
||||
$animate.enabled(true);
|
||||
|
||||
element = jqLite('<div class="element">element</div>');
|
||||
parent = jqLite('<div class="parent1">parent</div>');
|
||||
parent2 = jqLite('<div class="parent2">parent</div>');
|
||||
|
||||
$rootElement.append(parent);
|
||||
$rootElement.append(parent2);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
};
|
||||
}));
|
||||
|
||||
|
||||
it('should trigger all callbacks if a follow-up structural animation takes over a running animation',
|
||||
inject(function($animate, $rootScope) {
|
||||
|
||||
parent.append(element);
|
||||
var moveSpy = jasmine.createSpy();
|
||||
var leaveSpy = jasmine.createSpy();
|
||||
|
||||
$animate.on('move', parent2, moveSpy);
|
||||
$animate.on('leave', parent2, leaveSpy);
|
||||
|
||||
$animate.move(element, parent2);
|
||||
|
||||
$rootScope.$digest();
|
||||
$animate.flush();
|
||||
|
||||
expect(moveSpy.calls.length).toBe(1);
|
||||
expect(moveSpy.mostRecentCall.args[1]).toBe('start');
|
||||
|
||||
$animate.leave(element);
|
||||
$rootScope.$digest();
|
||||
$animate.flush();
|
||||
|
||||
expect(moveSpy.calls.length).toBe(2);
|
||||
expect(moveSpy.mostRecentCall.args[1]).toBe('close');
|
||||
|
||||
expect(leaveSpy.calls.length).toBe(2);
|
||||
expect(leaveSpy.calls[0].args[1]).toBe('start');
|
||||
expect(leaveSpy.calls[1].args[1]).toBe('close');
|
||||
}));
|
||||
|
||||
|
||||
it('should not trigger callbacks for the previous structural animation if a follow-up structural animation takes over before the postDigest',
|
||||
inject(function($animate, $rootScope) {
|
||||
|
||||
var enterDone = jasmine.createSpy('enter animation done');
|
||||
|
||||
var enterSpy = jasmine.createSpy();
|
||||
var leaveSpy = jasmine.createSpy();
|
||||
|
||||
$animate.on('enter', parent, enterSpy);
|
||||
$animate.on('leave', parent, leaveSpy);
|
||||
|
||||
$animate.enter(element, parent).done(enterDone);
|
||||
expect(enterDone).not.toHaveBeenCalled();
|
||||
|
||||
var runner = $animate.leave(element);
|
||||
$animate.flush();
|
||||
expect(enterDone).toHaveBeenCalled();
|
||||
|
||||
expect(enterSpy).not.toHaveBeenCalled();
|
||||
expect(leaveSpy.calls.length).toBe(1);
|
||||
expect(leaveSpy.mostRecentCall.args[1]).toBe('start');
|
||||
|
||||
leaveSpy.reset();
|
||||
runner.end();
|
||||
$animate.flush();
|
||||
|
||||
expect(enterSpy).not.toHaveBeenCalled();
|
||||
expect(leaveSpy.calls.length).toBe(1);
|
||||
expect(leaveSpy.mostRecentCall.args[1]).toBe('close');
|
||||
}));
|
||||
|
||||
|
||||
it('should not trigger the callback if animations are disabled on the element',
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
|
||||
var callbackTriggered = false;
|
||||
var spy = jasmine.createSpy('enter');
|
||||
$animate.on('enter', jqLite($document[0].body), spy);
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
$animate.enabled(element, false);
|
||||
var runner = $animate.enter(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
|
||||
$animate.flush(); // Flushes the animation frames for the callbacks
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
|
||||
it('should not trigger the callbacks if the animation is skipped because there are no class-based animations and no structural animation',
|
||||
inject(function($animate, $rootScope) {
|
||||
|
||||
parent.append(element);
|
||||
var classSpy = jasmine.createSpy('classChange');
|
||||
$animate.on('addClass', element, classSpy);
|
||||
$animate.on('removeClass', element, classSpy);
|
||||
element.addClass('one three');
|
||||
|
||||
$animate.addClass(element, 'one');
|
||||
$animate.removeClass(element, 'four');
|
||||
|
||||
$rootScope.$digest();
|
||||
$animate.flush();
|
||||
expect(classSpy).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
|
||||
describe('because the document is hidden', function() {
|
||||
beforeEach(module(function($provide) {
|
||||
var doc = jqLite({
|
||||
body: document.body,
|
||||
hidden: true
|
||||
});
|
||||
$provide.value('$document', doc);
|
||||
}));
|
||||
|
||||
it('should trigger callbacks for an enter animation',
|
||||
inject(function($animate, $rootScope, $rootElement, $document) {
|
||||
|
||||
var callbackTriggered = false;
|
||||
var spy = jasmine.createSpy();
|
||||
$animate.on('enter', jqLite($document[0].body), spy);
|
||||
|
||||
element = jqLite('<div></div>');
|
||||
var runner = $animate.enter(element, $rootElement);
|
||||
$rootScope.$digest();
|
||||
|
||||
$animate.flush(); // Flushes the animation frames for the callbacks
|
||||
|
||||
expect(spy.calls.length).toBe(2);
|
||||
expect(spy.calls[0].args[1]).toBe('start');
|
||||
expect(spy.calls[1].args[1]).toBe('close');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -445,6 +445,126 @@ describe('ngMessages', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngMessage nested nested inside elements', function() {
|
||||
|
||||
it('should not crash or leak memory when the messages are transcluded, the first message is ' +
|
||||
'visible, and ngMessages is removed by ngIf', function() {
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('messageWrap', function() {
|
||||
return {
|
||||
transclude: true,
|
||||
scope: {
|
||||
col: '=col'
|
||||
},
|
||||
template: '<div ng-messages="col"><ng-transclude></ng-transclude></div>'
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
inject(function($rootScope, $compile) {
|
||||
|
||||
element = $compile('<div><div ng-if="show"><div message-wrap col="col">' +
|
||||
' <div ng-message="a">A</div>' +
|
||||
' <div ng-message="b">B</div>' +
|
||||
'</div></div></div>')($rootScope);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.show = true;
|
||||
$rootScope.col = {
|
||||
a: true,
|
||||
b: true
|
||||
};
|
||||
});
|
||||
|
||||
expect(messageChildren(element).length).toBe(1);
|
||||
expect(trim(element.text())).toEqual('A');
|
||||
|
||||
$rootScope.$apply('show = false');
|
||||
|
||||
expect(messageChildren(element).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not crash when the first of two nested messages is removed', function() {
|
||||
inject(function($rootScope, $compile) {
|
||||
|
||||
element = $compile(
|
||||
'<div ng-messages="col">' +
|
||||
'<div class="wrapper">' +
|
||||
'<div remove-me ng-message="a">A</div>' +
|
||||
'<div ng-message="b">B</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)($rootScope);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = {
|
||||
a: true,
|
||||
b: false
|
||||
};
|
||||
});
|
||||
|
||||
expect(messageChildren(element).length).toBe(1);
|
||||
expect(trim(element.text())).toEqual('A');
|
||||
|
||||
var ctrl = element.controller('ngMessages');
|
||||
var deregisterSpy = spyOn(ctrl, 'deregister').andCallThrough();
|
||||
|
||||
var nodeA = element[0].querySelector('[ng-message="a"]');
|
||||
jqLite(nodeA).remove();
|
||||
$rootScope.$digest(); // The next digest triggers the error
|
||||
|
||||
// Make sure removing the element triggers the deregistration in ngMessages
|
||||
expect(trim(deregisterSpy.mostRecentCall.args[0].nodeValue)).toBe('ngMessage: a');
|
||||
expect(messageChildren(element).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not crash, but show deeply nested messages correctly after a message ' +
|
||||
'has been removed', function() {
|
||||
inject(function($rootScope, $compile) {
|
||||
|
||||
element = $compile(
|
||||
'<div ng-messages="col" ng-messages-multiple>' +
|
||||
'<div class="another-wrapper">' +
|
||||
'<div ng-message="a">A</div>' +
|
||||
'<div class="wrapper">' +
|
||||
'<div ng-message="b">B</div>' +
|
||||
'<div ng-message="c">C</div>' +
|
||||
'</div>' +
|
||||
'<div ng-message="d">D</div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
)($rootScope);
|
||||
|
||||
$rootScope.$apply(function() {
|
||||
$rootScope.col = {
|
||||
a: true,
|
||||
b: true
|
||||
};
|
||||
});
|
||||
|
||||
expect(messageChildren(element).length).toBe(2);
|
||||
expect(trim(element.text())).toEqual('AB');
|
||||
|
||||
var ctrl = element.controller('ngMessages');
|
||||
var deregisterSpy = spyOn(ctrl, 'deregister').andCallThrough();
|
||||
|
||||
var nodeB = element[0].querySelector('[ng-message="b"]');
|
||||
jqLite(nodeB).remove();
|
||||
$rootScope.$digest(); // The next digest triggers the error
|
||||
|
||||
// Make sure removing the element triggers the deregistration in ngMessages
|
||||
expect(trim(deregisterSpy.mostRecentCall.args[0].nodeValue)).toBe('ngMessage: b');
|
||||
expect(messageChildren(element).length).toBe(1);
|
||||
expect(trim(element.text())).toEqual('A');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when including templates', function() {
|
||||
they('should work with a dynamic collection model which is managed by ngRepeat',
|
||||
{'<div ng-messages-include="...">': '<div ng-messages="item">' +
|
||||
@@ -651,6 +771,37 @@ describe('ngMessages', function() {
|
||||
expect(trim(element.text())).toEqual("C");
|
||||
}));
|
||||
|
||||
|
||||
it('should properly detect a previous message, even if it was registered later',
|
||||
inject(function($compile, $rootScope, $templateCache) {
|
||||
$templateCache.put('include.html', '<div ng-message="a">A</div>');
|
||||
var html =
|
||||
'<div ng-messages="items">' +
|
||||
'<div ng-include="\'include.html\'"></div>' +
|
||||
'<div ng-message="b">B</div>' +
|
||||
'<div ng-message="c">C</div>' +
|
||||
'</div>';
|
||||
|
||||
element = $compile(html)($rootScope);
|
||||
$rootScope.$apply('items = {b: true, c: true}');
|
||||
|
||||
expect(element.text()).toBe('B');
|
||||
|
||||
var ctrl = element.controller('ngMessages');
|
||||
var deregisterSpy = spyOn(ctrl, 'deregister').andCallThrough();
|
||||
|
||||
var nodeB = element[0].querySelector('[ng-message="b"]');
|
||||
jqLite(nodeB).remove();
|
||||
|
||||
// Make sure removing the element triggers the deregistration in ngMessages
|
||||
expect(trim(deregisterSpy.mostRecentCall.args[0].nodeValue)).toBe('ngMessage: b');
|
||||
|
||||
$rootScope.$apply('items.a = true');
|
||||
|
||||
expect(element.text()).toBe('A');
|
||||
})
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
describe('when multiple', function() {
|
||||
|
||||
Vendored
+45
@@ -919,6 +919,51 @@ describe('ngMock', function() {
|
||||
});
|
||||
}).toThrow('test message');
|
||||
}));
|
||||
|
||||
describe('error stack trace when called outside of spec context', function() {
|
||||
// - Chrome, Firefox, Edge, Opera give us the stack trace as soon as an Error is created
|
||||
// - IE10+, PhantomJS give us the stack trace only once the error is thrown
|
||||
// - IE9 does not provide stack traces
|
||||
var stackTraceSupported = (function() {
|
||||
var error = new Error();
|
||||
if (!error.stack) {
|
||||
try {
|
||||
throw error;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return !!error.stack;
|
||||
})();
|
||||
|
||||
function testCaller() {
|
||||
return inject(function() {
|
||||
throw new Error();
|
||||
});
|
||||
}
|
||||
var throwErrorFromInjectCallback = testCaller();
|
||||
|
||||
if (stackTraceSupported) {
|
||||
describe('on browsers supporting stack traces', function() {
|
||||
it('should update thrown Error stack trace with inject call location', function() {
|
||||
try {
|
||||
throwErrorFromInjectCallback();
|
||||
} catch (e) {
|
||||
expect(e.stack).toMatch('testCaller');
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
describe('on browsers not supporting stack traces', function() {
|
||||
it('should not add stack trace information to thrown Error', function() {
|
||||
try {
|
||||
throwErrorFromInjectCallback();
|
||||
} catch (e) {
|
||||
expect(e.stack).toBeUndefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user