Compare commits

..

16 Commits

Author SHA1 Message Date
Peter Bacon Darwin 514639b585 docs(CHANGELOG): add 1.5.3 release notes 2016-03-25 20:01:45 +00:00
Peter Bacon Darwin 9cd9956dcb feat($compile): add more lifecycle hooks to directive controllers
This change adds in the following new lifecycle hooks, which map in some
way to those in Angular 2:

 * `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
   are the names of the bound properties that have changed, and the values are an object of the form
   `{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component such as
   cloning the bound value to prevent accidental mutation of the outer value.
 * `$onDestroy` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
   external resources, watches and event handlers.
 * `$postLink` - Called after this controller's element and its children been linked. Similar to the post-link
   function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
   Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
   they are waiting for their template to load asynchronously and their own compilation and linking has been
   suspended until that occurs.

Closes #14127
Closes #14030
Closes #14020
Closes #13991
Closes #14302
2016-03-25 12:56:08 +00:00
Martin Staffa c7813e9ebf fix(ngAnimate): run structural animations with cancelled out class changes
When multiple animations on the same element are queued before a $digest passes,
the animator tries to create as few actual animations as possible by joining / canceling
redundant animations. Class-based animations for example are cancelled when the classes that
are added and removed are the same, and the result is no class-change. This however must only
happen if there's no structural animation currently queued.

Fixes #14249
2016-03-24 00:13:16 +01:00
Martin Staffa ef91b04cdd fix(ngMessages): don't crash when nested messages are removed
Under specific circumstances, ngMessages would go into an infinite loop and crash the
browser / page:
- At least two ngMessage elements are wrapped inside another element (e.g. ngTransclude)
- The first message is currently visible
- The first message is removed (e.g. when the whole ngMessages element is removed by an ngIf)

When a message is removed, it looks for a previous message - in this specific case it would misidentify
the second message for a previous message, which would then cause the first message to be marked as the
second message's next message, resulting in an infinite loop, and crash.

This fix ensures that when searching for previous messages, ngMessage walks the DOM in a way so
that messages that come after the current message are never identified as previous messages.

This commit also detaches and destroys all child ngMessage elements when the ngMessages element is
destroyed, which should improve performance slightly.

Fixes #14183
Closes #14242
2016-03-24 00:13:16 +01:00
Alex Chuev 1acd97e18f docs(guide/component): add missing closing bracket
Closes #14299
2016-03-23 23:58:35 +02:00
Daniel Herman 513199ee9f fix($compile): workaround a GC bug in Chrome < 50
In the version of V8 used in Chrome < 50, the parent of template nodes for
`transclude: "element"` directives would be improperly garbage collected
despite still having been referenced via `parentNode`.

This bug surfaced due to the introduction of lazy transclusion (652b83e),
and appears under certain circumstances when using directive start and end elements.

It should be removed some time after Chrome 50 has been released.

Fixes #14041
Closes #14286
2016-03-23 22:06:17 +01:00
pmadruga 33f3c40e93 docs(error/$compile.baddir): mention "components" in directive name error
Closes #14212
2016-03-23 22:18:54 +02:00
Steve Mao 696cb95d5e docs($q): mention ES2015 (as a "synonym" for ES6) and remove "harmony"
Closes #14294
2016-03-22 12:09:02 +02:00
Georgios Kalpakas 457fd21a1a fix($sniffer): fix history sniffing in Chrome Packaged Apps
Although `window.history` is present in the context of Chrome Packaged Apps, it is not allowed to
access `window.history.pushState` or `window.history.state`, resulting in errors when trying to
"sniff" history support.
This commit fixes it by detecting a Chrome Packaged App (through the presence of
`window.chrome.app.runtime`). Note that `window.chrome.app` is present in the context of "normal"
webpages as well, but it doesn't have the `runtime` property, which is only available to packaged
apps (e.g. see https://developer.chrome.com/apps/api_index).

(It also also contains some style changes for making the structure and layout of `$sniffer` tests
 more consistent.)

Fixes #11932

Closes #13945
2016-03-22 11:57:18 +02:00
Wassim Chegham 3c6dfbf67d docs(guide/component-router): fix typos
Closes #14278
2016-03-22 02:18:44 +02:00
Owen Craig 3277b885c4 fix(formatNumber): handle small numbers correctly when gSize !== lgSize
By using `>=` when comparing the number length to `lgSize`, we'll provide the correct value, when
formatting numbers with different `lgSize` than `gSize`.

Fixes #14289

Closes #14290
2016-03-22 00:11:26 +02:00
Georgios Kalpakas 48a256d04b test(TzDate): fix test in Australia
Probably due to implementation differences in browsers for pre-DST period (see
https://github.com/angular/angular.js/issues/5017 and especially
https://github.com/angular/angular.js/issues/5017#issuecomment-90775226 for context), some
`TzDate` tests had different behavior on different Timezones/Regions (e.g. failed in Australia,
which started to observe DST in 1971).
Since the used year (`1970`) didn't have any particular significance, this commit fixes the issue
by using a year that is more consistently handled by browsers (`2000`).

Fixes #14272

Closes #14285
2016-03-21 20:45:35 +02:00
surya prakash singh 0579430799 docs(input[time]): fix a typo in the example
Closes #14220
2016-03-21 01:26:17 +02:00
Rongduan Zhu 39ac68dac1 docs(guide/component-router): changed path to match diagram
Closes #14277
2016-03-21 00:00:12 +02:00
Georgios Kalpakas 87fb44a5d3 docs(CHANGELOG.md): rearrange v1.5.1 to be right below v1.5.2
Moved the `v1.5.1` section above the `v1.4.10` one, so that it is right below the `v1.5.2` section
for easier reference. Also removed an empty "Breaking Changes" sub-section.

Closes #14283
2016-03-20 22:54:53 +02:00
Georgios Kalpakas 5c76b406f7 chore(ci-checks): fix the ddescribe-iit task for Jasmine 2
Closes #14276
2016-03-20 22:26:06 +02:00
22 changed files with 1215 additions and 375 deletions
+106 -78
View File
@@ -1,7 +1,39 @@
<a name="1.5.3"></a>
# 1.5.3 diplohaplontic-meiosis (2016-03-25)
## Bug Fixes
- **$compile:** workaround a GC bug in Chrome < 50
([513199ee](https://github.com/angular/angular.js/commit/513199ee9f1c8eef1240983d6e52c824404adb98),
[#14041](https://github.com/angular/angular.js/issues/14041), [#14286](https://github.com/angular/angular.js/issues/14286))
- **$sniffer:** fix history sniffing in Chrome Packaged Apps
([457fd21a](https://github.com/angular/angular.js/commit/457fd21a1a0c10c66245c32a73602f3a09038bda),
[#11932](https://github.com/angular/angular.js/issues/11932), [#13945](https://github.com/angular/angular.js/issues/13945))
- **formatNumber:** handle small numbers correctly when `gSize` !== `lgSize`
([3277b885](https://github.com/angular/angular.js/commit/3277b885c4dec3edd51b8e8c3d1776057d6d4d1d),
[#14289](https://github.com/angular/angular.js/issues/14289), [#14290](https://github.com/angular/angular.js/issues/14290))
- **ngAnimate:** run structural animations with cancelled out class changes
([c7813e9e](https://github.com/angular/angular.js/commit/c7813e9ebf793fe89380dcad54e8e002fafdd985),
[#14249](https://github.com/angular/angular.js/issues/14249))
- **ngMessages:** don't crash when nested messages are removed
([ef91b04c](https://github.com/angular/angular.js/commit/ef91b04cdd794f308617bca7ebd0b1b747e4f7de),
[#14183](https://github.com/angular/angular.js/issues/14183), [#14242](https://github.com/angular/angular.js/issues/14242))
## Features
- **$compile:** add more lifecycle hooks to directive controllers
([9cd9956d](https://github.com/angular/angular.js/commit/9cd9956dcbc8382e8e8757a805398bd251bbc67e),
[#14127](https://github.com/angular/angular.js/issues/14127), [#14030](https://github.com/angular/angular.js/issues/14030), [#14020](https://github.com/angular/angular.js/issues/14020), [#13991](https://github.com/angular/angular.js/issues/13991), [#14302](https://github.com/angular/angular.js/issues/14302))
<a name="1.5.2"></a>
# 1.5.2 differential-recovery (2016-03-18)
This release reverts a breaking change that accidentally made it into the 1.5.1 release. See [fee7bac3](https://github.com/angular/angular.js/commit/fee7bac392db24b6006d6a57ba71526f3afa102c) for more info.
This release reverts a breaking change that accidentally made it into the 1.5.1 release. See
[fee7bac3](https://github.com/angular/angular.js/commit/fee7bac392db24b6006d6a57ba71526f3afa102c)
for more info.
## Bug Fixes
@@ -10,9 +42,81 @@ This release reverts a breaking change that accidentally made it into the 1.5.1
([ce7f4000](https://github.com/angular/angular.js/commit/ce7f400011e1e2e1b9316f18ce87b87b79d878b4))
## Breaking Changes
<a name="1.5.1"></a>
# 1.5.1 equivocal-sophistication (2016-03-16)
## Bug Fixes
- **core:** only call `console.log` when `window.console` exists
([ce138f3c](https://github.com/angular/angular.js/commit/ce138f3c552f8bf741721ab8d10994ed35a4b2f5),
[#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))
- **$compile:** allow directives to have decorators
([0728cc2f](https://github.com/angular/angular.js/commit/0728cc2f2bb04d5dbdfca41f3afacea16c75ee07))
- **$resource:** fix parse errors on older Android WebViews
([df8db7b4](https://github.com/angular/angular.js/commit/df8db7b446b5bae83afef457d706d2805e597f29),
[#13989](https://github.com/angular/angular.js/issues/13989))
- **$routeProvider:** properly handle optional eager path named groups
([c0797c68](https://github.com/angular/angular.js/commit/c0797c68866c9ef8ff3c2f6985e6eb9374346151),
[#14011](https://github.com/angular/angular.js/issues/14011))
- **copy:** add support for copying `Blob` objects
([e9d579b6](https://github.com/angular/angular.js/commit/e9d579b608c2be8fdcf0326d0679a76bb9ae5b6e),
[#9669](https://github.com/angular/angular.js/issues/9669), [#14064](https://github.com/angular/angular.js/issues/14064))
- **dateFilter:** correctly format BC years
([e36205f5](https://github.com/angular/angular.js/commit/e36205f5af82b69362def7d2b6eeeb038f592311))
- **formatNumber:** allow negative fraction size
([e046c170](https://github.com/angular/angular.js/commit/e046c170bcf677f26e61af6470cb5fd2f751c969),
[#13913](https://github.com/angular/angular.js/issues/13913))
- **input:** re-validate when partially editing date-family inputs
([e383804c](https://github.com/angular/angular.js/commit/e383804c4ab62278fbaf4fdfaa03caeacff77fc4),
[#12207](https://github.com/angular/angular.js/issues/12207), [#13886](https://github.com/angular/angular.js/issues/13886))
- **input\[date\]:** support years with more than 4 digits
([d76951f1](https://github.com/angular/angular.js/commit/d76951f1747abd2da6e320d4ff9019f170d9793f),
[#13735](https://github.com/angular/angular.js/issues/13735), [#13905](https://github.com/angular/angular.js/issues/13905))
- **ngOptions:** always set the 'selected' attribute for selected options
([9f5a1722](https://github.com/angular/angular.js/commit/9f5a172291ff6926dcd246f0972288916a4c9bf6),
[#14115](https://github.com/angular/angular.js/issues/14115))
- **ngRoute:** allow `ngView` to be included in an asynchronously loaded template
([8237482d](https://github.com/angular/angular.js/commit/8237482d49e76e2c4994fe6207e3c9799ef04163),
[#1213](https://github.com/angular/angular.js/issues/1213), [#6812](https://github.com/angular/angular.js/issues/6812), [#14088](https://github.com/angular/angular.js/issues/14088))
- **ngMock:**
- attach `$injector` to `$rootElement` and prevent memory leak due to attached data
([75373dd4](https://github.com/angular/angular.js/commit/75373dd4bdae6c6035272942c69444c386f824cd),
[#14022](https://github.com/angular/angular.js/issues/14022), [#14094](https://github.com/angular/angular.js/issues/14094), [#14098](https://github.com/angular/angular.js/issues/14098))
- don't break if `$rootScope.$destroy()` is not a function
([50ed8712](https://github.com/angular/angular.js/commit/50ed8712566d601c9fb76b71f7b534b5bc803a36),
[#14106](https://github.com/angular/angular.js/issues/14106), [#14107](https://github.com/angular/angular.js/issues/14107))
- **ngMockE2E:** pass `responseType` to `$delegate` when using `passThrough`
([d16faf9f](https://github.com/angular/angular.js/commit/d16faf9f2b9bd2b85d95e71d902cec0269282f2c),
[#5415](https://github.com/angular/angular.js/issues/5415), [#5783](https://github.com/angular/angular.js/issues/5783))
## Features
- **$compile:** add custom annotations to the controller
([0c800930](https://github.com/angular/angular.js/commit/0c8009300b819c39c5e4892856724a731a8dcda6),
[#14114](https://github.com/angular/angular.js/issues/14114))
- **$controllerProvider:** add a `has()` method for checking the existence of a controller
([bb9575db](https://github.com/angular/angular.js/commit/bb9575dbd3428176216355df7b2933d2a72783cd),
[#13951](https://github.com/angular/angular.js/issues/13951), [#14109](https://github.com/angular/angular.js/issues/14109))
- **dateFilter:** add support for STANDALONEMONTH in format (`LLLL`)
([3e5b25b3](https://github.com/angular/angular.js/commit/3e5b25b33f278376def432698c704b1807fdb8c0),
[#13999](https://github.com/angular/angular.js/issues/13999), [#14013](https://github.com/angular/angular.js/issues/14013))
- **ngMock:** add `sharedInjector()` to `angular.mock.module`
([a46ab60f](https://github.com/angular/angular.js/commit/a46ab60fd5bf94896f0761e858ef38b998eb0f80),
[#14093](https://github.com/angular/angular.js/issues/14093), [#10238](https://github.com/angular/angular.js/issues/10238))
## Performance Improvements
- **ngRepeat:** avoid duplicate jqLite wrappers
([632e15a3](https://github.com/angular/angular.js/commit/632e15a3afdcd30168700cec1367bd81966400d4))
- **ngAnimate:**
- avoid jqLite/jQuery for upward DOM traversal
([35251bd4](https://github.com/angular/angular.js/commit/35251bd4ce23251b5e9a2860cf414726c194721e))
- avoid `$.fn.data` overhead with jQuery
([15915e60](https://github.com/angular/angular.js/commit/15915e606fdf5114592db1a0a5e3f12e639d7cdb))
<a name="1.4.10"></a>
# 1.4.10 benignant-oscillation (2016-03-16)
@@ -102,82 +206,6 @@ This release reverts a breaking change that accidentally made it into the 1.5.1
([86416bcb](https://github.com/angular/angular.js/commit/86416bcbee2192fa31c017163c5d856763182ade))
<a name="1.5.1"></a>
# 1.5.1 equivocal-sophistication (2016-03-16)
## Bug Fixes
- **core:** only call `console.log` when `window.console` exists
([ce138f3c](https://github.com/angular/angular.js/commit/ce138f3c552f8bf741721ab8d10994ed35a4b2f5),
[#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))
- **$compile:** allow directives to have decorators
([0728cc2f](https://github.com/angular/angular.js/commit/0728cc2f2bb04d5dbdfca41f3afacea16c75ee07))
- **$resource:** fix parse errors on older Android WebViews
([df8db7b4](https://github.com/angular/angular.js/commit/df8db7b446b5bae83afef457d706d2805e597f29),
[#13989](https://github.com/angular/angular.js/issues/13989))
- **$routeProvider:** properly handle optional eager path named groups
([c0797c68](https://github.com/angular/angular.js/commit/c0797c68866c9ef8ff3c2f6985e6eb9374346151),
[#14011](https://github.com/angular/angular.js/issues/14011))
- **copy:** add support for copying `Blob` objects
([e9d579b6](https://github.com/angular/angular.js/commit/e9d579b608c2be8fdcf0326d0679a76bb9ae5b6e),
[#9669](https://github.com/angular/angular.js/issues/9669), [#14064](https://github.com/angular/angular.js/issues/14064))
- **dateFilter:** correctly format BC years
([e36205f5](https://github.com/angular/angular.js/commit/e36205f5af82b69362def7d2b6eeeb038f592311))
- **formatNumber:** allow negative fraction size
([e046c170](https://github.com/angular/angular.js/commit/e046c170bcf677f26e61af6470cb5fd2f751c969),
[#13913](https://github.com/angular/angular.js/issues/13913))
- **input:** re-validate when partially editing date-family inputs
([e383804c](https://github.com/angular/angular.js/commit/e383804c4ab62278fbaf4fdfaa03caeacff77fc4),
[#12207](https://github.com/angular/angular.js/issues/12207), [#13886](https://github.com/angular/angular.js/issues/13886))
- **input\[date\]:** support years with more than 4 digits
([d76951f1](https://github.com/angular/angular.js/commit/d76951f1747abd2da6e320d4ff9019f170d9793f),
[#13735](https://github.com/angular/angular.js/issues/13735), [#13905](https://github.com/angular/angular.js/issues/13905))
- **ngOptions:** always set the 'selected' attribute for selected options
([9f5a1722](https://github.com/angular/angular.js/commit/9f5a172291ff6926dcd246f0972288916a4c9bf6),
[#14115](https://github.com/angular/angular.js/issues/14115))
- **ngRoute:** allow `ngView` to be included in an asynchronously loaded template
([8237482d](https://github.com/angular/angular.js/commit/8237482d49e76e2c4994fe6207e3c9799ef04163),
[#1213](https://github.com/angular/angular.js/issues/1213), [#6812](https://github.com/angular/angular.js/issues/6812), [#14088](https://github.com/angular/angular.js/issues/14088))
- **ngMock:**
- attach `$injector` to `$rootElement` and prevent memory leak due to attached data
([75373dd4](https://github.com/angular/angular.js/commit/75373dd4bdae6c6035272942c69444c386f824cd),
[#14022](https://github.com/angular/angular.js/issues/14022), [#14094](https://github.com/angular/angular.js/issues/14094), [#14098](https://github.com/angular/angular.js/issues/14098))
- don't break if `$rootScope.$destroy()` is not a function
([50ed8712](https://github.com/angular/angular.js/commit/50ed8712566d601c9fb76b71f7b534b5bc803a36),
[#14106](https://github.com/angular/angular.js/issues/14106), [#14107](https://github.com/angular/angular.js/issues/14107))
- **ngMockE2E:** pass `responseType` to `$delegate` when using `passThrough`
([d16faf9f](https://github.com/angular/angular.js/commit/d16faf9f2b9bd2b85d95e71d902cec0269282f2c),
[#5415](https://github.com/angular/angular.js/issues/5415), [#5783](https://github.com/angular/angular.js/issues/5783))
## Features
- **$compile:** add custom annotations to the controller
([0c800930](https://github.com/angular/angular.js/commit/0c8009300b819c39c5e4892856724a731a8dcda6),
[#14114](https://github.com/angular/angular.js/issues/14114))
- **$controllerProvider:** add a `has()` method for checking the existence of a controller
([bb9575db](https://github.com/angular/angular.js/commit/bb9575dbd3428176216355df7b2933d2a72783cd),
[#13951](https://github.com/angular/angular.js/issues/13951), [#14109](https://github.com/angular/angular.js/issues/14109))
- **dateFilter:** add support for STANDALONEMONTH in format (`LLLL`)
([3e5b25b3](https://github.com/angular/angular.js/commit/3e5b25b33f278376def432698c704b1807fdb8c0),
[#13999](https://github.com/angular/angular.js/issues/13999), [#14013](https://github.com/angular/angular.js/issues/14013))
- **ngMock:** add `sharedInjector()` to `angular.mock.module`
([a46ab60f](https://github.com/angular/angular.js/commit/a46ab60fd5bf94896f0761e858ef38b998eb0f80),
[#14093](https://github.com/angular/angular.js/issues/14093), [#10238](https://github.com/angular/angular.js/issues/10238))
## Performance Improvements
- **ngRepeat:** avoid duplicate jqLite wrappers
([632e15a3](https://github.com/angular/angular.js/commit/632e15a3afdcd30168700cec1367bd81966400d4))
- **ngAnimate:**
- avoid jqLite/jQuery for upward DOM traversal
([35251bd4](https://github.com/angular/angular.js/commit/35251bd4ce23251b5e9a2860cf414726c194721e))
- avoid `$.fn.data` overhead with jQuery
([15915e60](https://github.com/angular/angular.js/commit/15915e606fdf5114592db1a0a5e3f12e639d7cdb))
<a name="1.5.0"></a>
# 1.5.0 ennoblement-facilitation (2016-02-05)
+6 -1
View File
@@ -264,12 +264,17 @@ module.exports = function(grunt) {
],
options: {
disallowed: [
'fit',
'iit',
'xit',
'fthey',
'tthey',
'xthey',
'fdescribe',
'ddescribe',
'xdescribe'
'xdescribe',
'it.only',
'describe.only'
]
}
},
+3 -3
View File
@@ -1,8 +1,8 @@
@ngdoc error
@name $compile:baddir
@fullName Invalid Directive Name
@fullName Invalid Directive/Component Name
@description
This error occurs when the name of a directive is not valid.
This error occurs when the name of a directive or component is not valid.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
Directives and Components must start with a lowercase character and must not contain leading or trailing whitespaces.
+30
View File
@@ -0,0 +1,30 @@
@ngdoc error
@name $compile:infchng
@fullName Unstable `$onChanges` hooks
@description
This error occurs when the application's model becomes unstable because some `$onChanges` hooks are causing updates which then trigger
further calls to `$onChanges` that can never complete.
Angular detects this situation and prevents an infinite loop from causing the browser to become unresponsive.
For example, the situation can occur by setting up a `$onChanges()` hook which triggers an event on the component, which subsequently
triggers the component's bound inputs to be updated:
```html
<c1 prop="a" on-change="a = -a"></c1>
```
```js
function Controller1() {}
Controller1.$onChanges = function() {
this.onChange();
};
mod.component('c1', {
controller: Controller1,
bindings: {'prop': '<', onChange: '&'}
}
```
The maximum number of allowed iterations of the `$onChanges` hooks is controlled via TTL setting which can be configured via
{@link ng.$compileProvider#onChangesTtl `$compileProvider.onChangesTtl`}.
+10 -10
View File
@@ -33,7 +33,7 @@ Here is a table of the main concepts used in the Component Router.
## Component-based Applications
It recommended to develop AngularJS applications as a hierarchy of Components. Each Component
It is recommended to develop AngularJS applications as a hierarchy of Components. Each Component
is an isolated part of the application, which is responsible for its own user interface and has
a well defined programmatic interface to the Component that contains it. Take a look at the
{@link guide/component component guide} for more information.
@@ -124,9 +124,9 @@ This process continues until we run out of **Routing Components** or consume the
![Routed Components](img/guide/component-routes.svg)
In the previous diagram can see that the URL `/heros/2` has been matched against the `App`, `Heroes` and
In the previous diagram, we can see that the URL `/heros/4` has been matched against the `App`, `Heroes` and
`HeroDetail` **Routing Components**. The **Routers** for each of the **Routing Components** consumed a part
of the URL: "/", "/heroes" and "/2" respectively.
of the URL: "/", "/heroes" and "/4" respectively.
The result is that we end up with a hierarchy of **Routing Components** rendered in **Outlets**, via the
{@link ngOutlet} directive, in each **Routing Component's** template, as you can see in the following diagram.
@@ -462,7 +462,7 @@ to display list and detail views of Heroes and Crises.
## Install the libraries
It is simplest to use npm to install the **Component Router** module. For this guide we will also install
It is easier to use npm to install the **Component Router** module. For this guide we will also install
AngularJS itself via npm:
```bash
@@ -485,7 +485,7 @@ Just like any Angular application, we load the JavaScript files into our `index.
## Create the `app` module
In the app.js file, create the main application module `app` which depends upon the `ngComponentRouter`
In the app.js file, create the main application module `app` which depends on the `ngComponentRouter`
module, which is provided by the **Component Router** script.
```js
@@ -494,10 +494,10 @@ angular.module('app', ['ngComponentRouter'])
We must choose what **Location Mode** the **Router** should use. We are going to use HTML5 mode locations,
so that we will not have hash-based paths. We must rely on the browser to provide `pushState` support,
which is true of most modern browsers. See {@link $locationProvider#html5Mode} for more information.
which is true for most modern browsers. See {@link $locationProvider#html5Mode} for more information.
<div class="alert alert-info">
Using HTML5 mode means that we can have clean URLs for our application routes but it does require that our
Using HTML5 mode means that we can have clean URLs for our application routes. However, HTML5 mode does require that our
web server, which hosts the application, understands that it must respond with the index.html file for
requests to URLs that represent all our application routes. We are going to use the `lite-server` web server
to do this for us.
@@ -550,7 +550,7 @@ Bootstrap the Angular application and add the top level App Component.
# Implementing the AppComponent
In the previous section we created a single top level **App Component**. Let's now create some more
In the previous section we have created a single top level **App Component**. Let's now create some more
**Routing Components** and wire up **Route Config** for those. We start with a Heroes Feature, which
will display one of two views.
@@ -590,7 +590,7 @@ of this view will be rendered.
### ngLink
We have used the `ng-link` directive to create a link to navigate to the Heroes Component. By using this
directive we don't need to know what the actual URL will be. We can leave the Router to generate that for us.
directive we don't need to know what the actual URL will be. We can let the Router generate that for us.
We have included a link to the Crisis Center but have not included the `ng-link` directive as we have not yet
implemented the CrisisCenter component.
@@ -765,7 +765,7 @@ function HeroListComponent(heroService) {
Running the application should update the browser's location to `/heroes` and display the list of heroes
returned from the `heroService`.
By returning a promise for the list of heroes from `$routerOnActivate()` we can delay activation of the
By returning a promise for the list of heroes from `$routerOnActivate()` we can delay the activation of the
Route until the heroes have arrived successfully. This is similar to how a `resolve` works in {@link ngRoute}.
+25 -1
View File
@@ -29,7 +29,7 @@ and link functions are unavailable
Components can be registered using the `.component()` method of an Angular module (returned by {@link module `angular.module()`}). The method takes two arguments:
* The name of the Component (as string).
* The Component config object (note that, unlike the `.directive()` method, this method does **not** take a factory function.
* The Component config object. (Note that, unlike the `.directive()` method, this method does **not** take a factory function.)
<example name="heroComponentSimple" module="heroApp">
<file name="index.js">
@@ -147,6 +147,30 @@ components should follow a few simple conventions:
}
```
- **Components have a well-defined lifecycle**
Each component can implement "lifecycle hooks". These are methods that will be called at certain points in the life
of the component. The following hook methods can be implemented:
* `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
had their bindings initialized (and before the pre &amp; post linking functions for the directives on
this element). This is a good place to put initialization code for your controller.
* `$onChanges(changesObj)` - Called whenever one-way bindings are updated. The `changesObj` is a hash whose keys
are the names of the bound properties that have changed, and the values are an object of the form
`{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component such as
cloning the bound value to prevent accidental mutation of the outer value.
* `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
external resources, watches and event handlers.
* `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
they are waiting for their template to load asynchronously and their own compilation and linking has been
suspended until that occurs.
This hook can be considered analogous to the `ngAfterViewInit` and `ngAfterContentInit` hooks in Angular 2.
Since the compilation process is rather different in Angular 1 there is no direct mapping and care should
be taken when upgrading.
By implementing these methods, you component can take part in its lifecycle.
- **An application is a tree of components:**
Ideally, the whole application should be a tree of components that implement clearly defined inputs
and outputs, and minimize two-way data binding. That way, it's easier to predict when data changes and what the state
+8 -9
View File
@@ -86,7 +86,14 @@ function Browser(window, document, $log, $sniffer) {
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
pendingLocation = null;
pendingLocation = null,
getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
try {
return history.state;
} catch (e) {
// MSIE can reportedly throw when there is no state (UNCONFIRMED).
}
};
cacheState();
lastHistoryState = cachedState;
@@ -194,14 +201,6 @@ function Browser(window, document, $log, $sniffer) {
fireUrlChange();
}
function getCurrentState() {
try {
return history.state;
} catch (e) {
// MSIE can reportedly throw when there is no state (UNCONFIRMED).
}
}
// This variable should be used *only* inside the cacheState function.
var lastCachedState = null;
function cacheState() {
+137 -6
View File
@@ -293,9 +293,23 @@
* `true` if the specified slot contains content (i.e. one or more DOM nodes).
*
* The controller can provide the following methods that act as life-cycle hooks:
* * `$onInit` - Called on each controller after all the controllers on an element have been constructed and
* * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
* had their bindings initialized (and before the pre &amp; post linking functions for the directives on
* this element). This is a good place to put initialization code for your controller.
* * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
* `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
* object of the form `{ currentValue: ..., previousValue: ... }`. Use this hook to trigger updates within a component
* such as cloning the bound value to prevent accidental mutation of the outer value.
* * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
* external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
* the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
* components will have their `$onDestroy()` hook called before child components.
* * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
* function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
* Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
* they are waiting for their template to load asynchronously and their own compilation and linking has been
* suspended until that occurs.
*
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
@@ -928,11 +942,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function assertValidDirectiveName(name) {
var letter = name.charAt(0);
if (!letter || letter !== lowercase(letter)) {
throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
throw $compileMinErr('baddir', "Directive/Component name '{0}' is invalid. The first character must be a lowercase letter", name);
}
if (name !== name.trim()) {
throw $compileMinErr('baddir',
"Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
"Directive/Component name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
name);
}
}
@@ -1207,6 +1221,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
var TTL = 10;
/**
* @ngdoc method
* @name $compileProvider#onChangesTtl
* @description
*
* Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
* assuming that the model is unstable.
*
* The current default is 10 iterations.
*
* In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
* in several iterations of calls to these hooks. However if an application needs more than the default 10
* iterations to stabilize then you should investigate what is causing the model to continuously change during
* the `$onChanges` hook execution.
*
* Increasing the TTL could have performance implications, so you should not change it without proper justification.
*
* @param {number} limit The number of `$onChanges` hook iterations.
* @returns {number|object} the current limit (or `this` if called as a setter for chaining)
*/
this.onChangesTtl = function(value) {
if (arguments.length) {
TTL = value;
return this;
}
return TTL;
};
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
@@ -1215,6 +1259,36 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = document.createElement('div');
var onChangesTtl = TTL;
// The onChanges hooks should all be run together in a single digest
// When changes occur, the call to trigger their hooks will be added to this queue
var onChangesQueue;
// This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
function flushOnChangesQueue() {
try {
if (!(--onChangesTtl)) {
// We have hit the TTL limit so reset everything
onChangesQueue = undefined;
throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
}
// We must run this hook in an apply since the $$postDigest runs outside apply
$rootScope.$apply(function() {
for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
onChangesQueue[i]();
}
// Reset the queue to trigger a new schedule next time there is a change
onChangesQueue = undefined;
});
} finally {
onChangesTtl++;
}
}
function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
@@ -2080,6 +2154,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);
// Support: Chrome < 50
// https://github.com/angular/angular.js/issues/14041
// In the versions of V8 prior to Chrome 50, the document fragment that is created
// in the `replaceWith` function is improperly garbage collected despite still
// being referenced by the `parentNode` property of all of the child nodes. By adding
// a reference to the fragment via a different property, we can avoid that incorrect
// behavior.
// TODO: remove this line after Chrome 50 has been released
$template[0].$$parentNode = $template[0].parentNode;
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name, {
// Don't pass in:
@@ -2362,10 +2447,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
// Trigger the `$onInit` method on all controllers that have one
// Handle the init and destroy lifecycle hooks on all controllers that have them
forEach(elementControllers, function(controller) {
if (isFunction(controller.instance.$onInit)) {
controller.instance.$onInit();
var controllerInstance = controller.instance;
if (isFunction(controllerInstance.$onInit)) {
controllerInstance.$onInit();
}
if (isFunction(controllerInstance.$onDestroy)) {
controllerScope.$on('$destroy', function callOnDestroyHook() {
controllerInstance.$onDestroy();
});
}
});
@@ -2402,6 +2493,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
);
}
// Trigger $postLink lifecycle hooks
forEach(elementControllers, function(controller) {
var controllerInstance = controller.instance;
if (isFunction(controllerInstance.$postLink)) {
controllerInstance.$postLink();
}
});
// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
@@ -2997,6 +3096,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// only occurs for isolate scopes and new scopes with controllerAs.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
var changes;
forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -3012,6 +3112,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
var oldValue = destination[scopeName];
recordChanges(scopeName, value, oldValue);
destination[scopeName] = value;
}
});
@@ -3083,6 +3185,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
destination[scopeName] = parentGet(scope);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
var oldValue = destination[scopeName];
recordChanges(scopeName, newParentValue, oldValue);
destination[scopeName] = newParentValue;
}, parentGet.literal);
@@ -3103,6 +3207,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
function recordChanges(key, currentValue, previousValue) {
if (isFunction(destination.$onChanges) && currentValue !== previousValue) {
// If we have not already scheduled the top level onChangesQueue handler then do so now
if (!onChangesQueue) {
scope.$$postDigest(flushOnChangesQueue);
onChangesQueue = [];
}
// If we have not already queued a trigger of onChanges for this controller then do so now
if (!changes) {
changes = {};
onChangesQueue.push(triggerOnChangesHook);
}
// If the has been a change on this property already then we need to reuse the previous value
if (changes[key]) {
previousValue = changes[key].previousValue;
}
// Store this change
changes[key] = {previousValue: previousValue, currentValue: currentValue};
}
}
function triggerOnChangesHook() {
destination.$onChanges(changes);
// Now clear the changes so that we schedule onChanges when more changes arrive
changes = undefined;
}
return removeWatchCollection.length && function removeWatches() {
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
removeWatchCollection[i]();
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -323,7 +323,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
// format the integer digits with grouping separators
var groups = [];
if (digits.length > pattern.lgSize) {
if (digits.length >= pattern.lgSize) {
groups.unshift(digits.splice(-pattern.lgSize).join(''));
}
while (digits.length > pattern.gSize) {
+3 -3
View File
@@ -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:
+5 -1
View File
@@ -17,6 +17,10 @@
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
// Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
// the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
@@ -61,7 +65,7 @@ function $SnifferProvider() {
// so let's not use the history API also
// We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
// jshint -W018
history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
history: !!(hasHistoryPushState && !(android < 4) && !boxee),
// jshint +W018
hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
+5
View File
@@ -82,6 +82,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
});
rules.cancel.push(function(element, newAnimation, currentAnimation) {
// cancel the animation if classes added / removed in both animation cancel each other out,
// but only if the current animation isn't structural
if (currentAnimation.structural) return false;
var nA = newAnimation.addClass;
var nR = newAnimation.removeClass;
var cA = currentAnimation.addClass;
+14 -3
View File
@@ -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);
}
}
}
@@ -669,8 +680,8 @@ function ngMessageDirectiveFactory() {
// 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) {
+23
View File
@@ -545,9 +545,32 @@ describe('browser', function() {
currentHref = fakeWindow.location.href;
});
it('should not access `history.state` when `$sniffer.history` is false', function() {
// In the context of a Chrome Packaged App, although `history.state` is present, accessing it
// is not allowed and logs an error in the console. We should not try to access
// `history.state` in contexts where `$sniffer.history` is false.
var historyStateAccessed = false;
var mockSniffer = {histroy: false};
var mockWindow = new MockWindow();
var _state = mockWindow.history.state;
Object.defineProperty(mockWindow.history, 'state', {
get: function() {
historyStateAccessed = true;
return _state;
}
});
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer);
expect(historyStateAccessed).toBe(false);
});
describe('in IE', runTests({msie: true}));
describe('not in IE', runTests({msie: false}));
function runTests(options) {
return function() {
beforeEach(function() {
+387 -28
View File
@@ -206,7 +206,7 @@ describe('$compile', function() {
module(function() {
expect(function() {
directive('BadDirectiveName', function() { });
}).toThrowMinErr('$compile','baddir', "Directive name 'BadDirectiveName' is invalid. The first character must be a lowercase letter");
}).toThrowMinErr('$compile','baddir', "Directive/Component name 'BadDirectiveName' is invalid. The first character must be a lowercase letter");
});
inject(function($compile) {});
});
@@ -216,7 +216,7 @@ describe('$compile', function() {
expect(function() {
directive(name, function() { });
}).toThrowMinErr(
'$compile','baddir', 'Directive name \'' + name + '\' is invalid. ' +
'$compile','baddir', 'Directive/Component name \'' + name + '\' is invalid. ' +
"The name should not contain leading or trailing whitespaces");
}
assertLeadingOrTrailingWhitespaceInDirectiveName(' leadingWhitespaceDirectiveName');
@@ -3514,6 +3514,391 @@ describe('$compile', function() {
});
});
describe('controller lifecycle hooks', function() {
describe('$onInit', function() {
it('should call `$onInit`, if provided, after all the controllers on the element have been initialized', function() {
function check() {
/*jshint validthis:true */
expect(this.element.controller('d1').id).toEqual(1);
expect(this.element.controller('d2').id).toEqual(2);
}
function Controller1($element) { this.id = 1; this.element = $element; }
Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
function Controller2($element) { this.id = 2; this.element = $element; }
Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
angular.module('my', [])
.directive('d1', valueFn({ controller: Controller1 }))
.directive('d2', valueFn({ controller: Controller2 }));
module('my');
inject(function($compile, $rootScope) {
element = $compile('<div d1 d2></div>')($rootScope);
expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce();
expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
});
});
});
describe('$onDestroy', function() {
it('should call `$onDestroy`, if provided, on the controller when its scope is destroyed', function() {
function TestController() { this.count = 0; }
TestController.prototype.$onDestroy = function() { this.count++; };
angular.module('my', [])
.directive('d1', valueFn({ scope: true, controller: TestController }))
.directive('d2', valueFn({ scope: {}, controller: TestController }))
.directive('d3', valueFn({ controller: TestController }));
module('my');
inject(function($compile, $rootScope) {
element = $compile('<div><d1 ng-if="show[0]"></d1><d2 ng-if="show[1]"></d2><div ng-if="show[2]"><d3></d3></div></div>')($rootScope);
$rootScope.$apply('show = [true, true, true]');
var d1Controller = element.find('d1').controller('d1');
var d2Controller = element.find('d2').controller('d2');
var d3Controller = element.find('d3').controller('d3');
expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([0,0,0]);
$rootScope.$apply('show = [false, true, true]');
expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,0,0]);
$rootScope.$apply('show = [false, false, true]');
expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,0]);
$rootScope.$apply('show = [false, false, false]');
expect([d1Controller.count, d2Controller.count, d3Controller.count]).toEqual([1,1,1]);
});
});
it('should call `$onDestroy` top-down (the same as `scope.$broadcast`)', function() {
var log = [];
function ParentController() { log.push('parent created'); }
ParentController.prototype.$onDestroy = function() { log.push('parent destroyed'); };
function ChildController() { log.push('child created'); }
ChildController.prototype.$onDestroy = function() { log.push('child destroyed'); };
function GrandChildController() { log.push('grand child created'); }
GrandChildController.prototype.$onDestroy = function() { log.push('grand child destroyed'); };
angular.module('my', [])
.directive('parent', valueFn({ scope: true, controller: ParentController }))
.directive('child', valueFn({ scope: true, controller: ChildController }))
.directive('grandChild', valueFn({ scope: true, controller: GrandChildController }));
module('my');
inject(function($compile, $rootScope) {
element = $compile('<parent ng-if="show"><child><grand-child></grand-child></child></parent>')($rootScope);
$rootScope.$apply('show = true');
expect(log).toEqual(['parent created', 'child created', 'grand child created']);
log = [];
$rootScope.$apply('show = false');
expect(log).toEqual(['parent destroyed', 'child destroyed', 'grand child destroyed']);
});
});
});
describe('$postLink', function() {
it('should call `$postLink`, if provided, after the element has completed linking (i.e. post-link)', function() {
var log = [];
function Controller1() { }
Controller1.prototype.$postLink = function() { log.push('d1 view init'); };
function Controller2() { }
Controller2.prototype.$postLink = function() { log.push('d2 view init'); };
angular.module('my', [])
.directive('d1', valueFn({
controller: Controller1,
link: { pre: function(s, e) { log.push('d1 pre: ' + e.text()); }, post: function(s, e) { log.push('d1 post: ' + e.text()); } },
template: '<d2></d2>'
}))
.directive('d2', valueFn({
controller: Controller2,
link: { pre: function(s, e) { log.push('d2 pre: ' + e.text()); }, post: function(s, e) { log.push('d2 post: ' + e.text()); } },
template: 'loaded'
}));
module('my');
inject(function($compile, $rootScope) {
element = $compile('<d1></d1>')($rootScope);
expect(log).toEqual([
'd1 pre: loaded',
'd2 pre: loaded',
'd2 post: loaded',
'd2 view init',
'd1 post: loaded',
'd1 view init'
]);
});
});
});
describe('$onChanges', function() {
it('should call `$onChanges`, if provided, when a one-way (`<`) or interpolation (`@`) bindings are updated', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop1': '<', 'prop2': '<', 'other': '=', 'attr': '@' }
});
module('my');
inject(function($compile, $rootScope) {
// Setup a watch to indicate some complicated updated logic
$rootScope.$watch('val', function(val, oldVal) { $rootScope.val2 = val * 2; });
// Setup the directive with two bindings
element = $compile('<c1 prop1="val" prop2="val2" other="val3" attr="{{val4}}"></c1>')($rootScope);
// There should be no changes initially
expect(log).toEqual([]);
// Update val to trigger the onChanges
$rootScope.$apply('val = 42');
// Now we should have a single changes entry in the log
expect(log).toEqual([
{
prop1: {previousValue: undefined, currentValue: 42},
prop2: {previousValue: undefined, currentValue: 84}
}
]);
// Clear the log
log = [];
// Update val to trigger the onChanges
$rootScope.$apply('val = 17');
// Now we should have a single changes entry in the log
expect(log).toEqual([
{
prop1: {previousValue: 42, currentValue: 17},
prop2: {previousValue: 84, currentValue: 34}
}
]);
// Clear the log
log = [];
// Update val3 to trigger the "other" two-way binding
$rootScope.$apply('val3 = 63');
// onChanges should not have been called
expect(log).toEqual([]);
// Update val4 to trigger the "attr" interpolation binding
$rootScope.$apply('val4 = 22');
// onChanges should not have been called
expect(log).toEqual([
{
attr: {previousValue: '', currentValue: '22'}
}
]);
});
});
it('should pass the original value as `previousValue` even if there were multiple changes in a single digest', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop': '<' }
});
module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop="a + b"></c1>')($rootScope);
// We add this watch after the compilation to ensure that it will run after the binding watchers
// therefore triggering the thing that this test is hoping to enfore
$rootScope.$watch('a', function(val) { $rootScope.b = val * 2; });
// There should be no changes initially
expect(log).toEqual([]);
// Update val to trigger the onChanges
$rootScope.$apply('a = 42');
// Now the change should have the real previous value (undefined), not the intermediate one (42)
expect(log).toEqual([{prop: {previousValue: undefined, currentValue: 126}}]);
// Clear the log
log = [];
// Update val to trigger the onChanges
$rootScope.$apply('a = 7');
// Now the change should have the real previous value (126), not the intermediate one, (91)
expect(log).toEqual([{ prop: {previousValue: 126, currentValue: 21}}]);
});
});
it('should only trigger one extra digest however many controllers have changes', function() {
var log = [];
function TestController1() { }
TestController1.prototype.$onChanges = function(change) { log.push(['TestController1', change]); };
function TestController2() { }
TestController2.prototype.$onChanges = function(change) { log.push(['TestController2', change]); };
angular.module('my', [])
.component('c1', {
controller: TestController1,
bindings: {'prop': '<'}
})
.component('c2', {
controller: TestController2,
bindings: {'prop': '<'}
});
module('my');
inject(function($compile, $rootScope) {
// Create a watcher to count the number of digest cycles
var watchCount = 0;
$rootScope.$watch(function() { watchCount++; });
// Setup two sibling components with bindings that will change
element = $compile('<div><c1 prop="val1"></c1><c2 prop="val2"></c2></div>')($rootScope);
// There should be no changes initially
expect(log).toEqual([]);
// Update val to trigger the onChanges
$rootScope.$apply('val1 = 42; val2 = 17');
expect(log).toEqual([
['TestController1', {prop: {previousValue: undefined, currentValue: 42}}],
['TestController2', {prop: {previousValue: undefined, currentValue: 17}}]
]);
// A single apply should only trigger three turns of the digest loop
expect(watchCount).toEqual(3);
});
});
it('should cope with changes occuring inside `$onChanges()` hooks', function() {
var log = [];
function OuterController() { }
OuterController.prototype.$onChanges = function(change) {
log.push(['OuterController', change]);
// Make a change to the inner component
this.b = 72;
};
function InnerController() { }
InnerController.prototype.$onChanges = function(change) { log.push(['InnerController', change]); };
angular.module('my', [])
.component('outer', {
controller: OuterController,
bindings: {'prop1': '<'},
template: '<inner prop2="$ctrl.b"></inner>'
})
.component('inner', {
controller: InnerController,
bindings: {'prop2': '<'}
});
module('my');
inject(function($compile, $rootScope) {
// Setup the directive with two bindings
element = $compile('<outer prop1="a"></outer>')($rootScope);
// There should be no changes initially
expect(log).toEqual([]);
// Update val to trigger the onChanges
$rootScope.$apply('a = 42');
expect(log).toEqual([
['OuterController', {prop1: {previousValue: undefined, currentValue: 42}}],
['InnerController', {prop2: {previousValue: undefined, currentValue: 72}}]
]);
});
});
it('should throw an error if `$onChanges()` hooks are not stable', function() {
function TestController() {}
TestController.prototype.$onChanges = function(change) {
this.onChange();
};
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: {'prop': '<', onChange: '&'}
});
module('my');
inject(function($compile, $rootScope) {
// Setup the directive with bindings that will keep updating the bound value forever
element = $compile('<c1 prop="a" on-change="a = -a"></c1>')($rootScope);
// Update val to trigger the unstable onChanges, which will result in an error
expect(function() {
$rootScope.$apply('a = 42');
}).toThrowMinErr('$compile', 'infchng');
dealoc(element);
element = $compile('<c1 prop="b" on-change=""></c1>')($rootScope);
$rootScope.$apply('b = 24');
$rootScope.$apply('b = 48');
});
});
it('should log an error if `$onChanges()` hooks are not stable', function() {
function TestController() {}
TestController.prototype.$onChanges = function(change) {
this.onChange();
};
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: {'prop': '<', onChange: '&'}
})
.config(function($exceptionHandlerProvider) {
// We need to test with the exceptionHandler not rethrowing...
$exceptionHandlerProvider.mode('log');
});
module('my');
inject(function($compile, $rootScope, $exceptionHandler) {
// Setup the directive with bindings that will keep updating the bound value forever
element = $compile('<c1 prop="a" on-change="a = -a"></c1>')($rootScope);
// Update val to trigger the unstable onChanges, which will result in an error
$rootScope.$apply('a = 42');
expect($exceptionHandler.errors.length).toEqual(1);
expect($exceptionHandler.errors[0].toString()).toContain('[$compile:infchng] 10 $onChanges() iterations reached.');
});
});
});
});
describe('isolated locals', function() {
var componentScope, regularScope;
@@ -5323,32 +5708,6 @@ describe('$compile', function() {
});
});
it('should call `controller.$onInit`, if provided after all the controllers have been constructed', function() {
function check() {
/*jshint validthis:true */
expect(this.element.controller('d1').id).toEqual(1);
expect(this.element.controller('d2').id).toEqual(2);
}
function Controller1($element) { this.id = 1; this.element = $element; }
Controller1.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
function Controller2($element) { this.id = 2; this.element = $element; }
Controller2.prototype.$onInit = jasmine.createSpy('$onInit').and.callFake(check);
angular.module('my', [])
.directive('d1', valueFn({ controller: Controller1 }))
.directive('d2', valueFn({ controller: Controller2 }));
module('my');
inject(function($compile, $rootScope) {
element = $compile('<div d1 d2></div>')($rootScope);
expect(Controller1.prototype.$onInit).toHaveBeenCalledOnce();
expect(Controller2.prototype.$onInit).toHaveBeenCalledOnce();
});
});
describe('should not overwrite @-bound property each digest when not present', function() {
it('when creating new scope', function() {
module(function($compileProvider) {
+5 -1
View File
@@ -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');
+219 -218
View File
@@ -1,10 +1,9 @@
'use strict';
describe('$sniffer', function() {
function sniffer($window, $document) {
/* global $SnifferProvider: false */
$window.navigator = {};
$window.navigator = $window.navigator || {};
$document = jqLite($document || {});
if (!$document[0].body) {
$document[0].body = window.document.body;
@@ -12,14 +11,84 @@ describe('$sniffer', function() {
return new $SnifferProvider().$get[2]($window, $document);
}
describe('history', function() {
it('should be true if history.pushState defined', function() {
expect(sniffer({history: {pushState: noop, replaceState: noop}}).history).toBe(true);
var mockWindow = {
history: {
pushState: noop,
replaceState: noop
}
};
expect(sniffer(mockWindow).history).toBe(true);
});
it('should be false if history or pushState not defined', function() {
expect(sniffer({history: {}}).history).toBe(false);
expect(sniffer({}).history).toBe(false);
expect(sniffer({history: {}}).history).toBe(false);
});
it('should be false on Boxee box with an older version of Webkit', function() {
var mockWindow = {
history: {
pushState: noop
},
navigator: {
userAgent: 'boxee (alpha/Darwin 8.7.1 i386 - 0.9.11.5591)'
}
};
expect(sniffer(mockWindow).history).toBe(false);
});
it('should be false on Chrome Packaged Apps', function() {
// Chrome Packaged Apps are not allowed to access `window.history.pushState`.
// In Chrome, `window.app` might be available in "normal" webpages, but `window.app.runtime`
// only exists in the context of a packaged app.
expect(sniffer(createMockWindow()).history).toBe(true);
expect(sniffer(createMockWindow(true)).history).toBe(true);
expect(sniffer(createMockWindow(true, true)).history).toBe(false);
function createMockWindow(isChrome, isPackagedApp) {
var mockWindow = {
history: {
pushState: noop
}
};
if (isChrome) {
var chromeAppObj = isPackagedApp ? {runtime: {}} : {};
mockWindow.chrome = {app: chromeAppObj};
}
return mockWindow;
}
});
it('should not try to access `history.pushState` in Chrome Packaged Apps', function() {
var pushStateAccessCount = 0;
var mockHistory = Object.create(Object.prototype, {
pushState: {get: function() { pushStateAccessCount++; return noop; }}
});
var mockWindow = {
chrome: {
app: {
runtime: {}
}
},
history: mockHistory
};
sniffer(mockWindow);
expect(pushStateAccessCount).toBe(0);
});
});
@@ -28,11 +97,10 @@ describe('$sniffer', function() {
var mockDocument, mockDivElement, $sniffer;
beforeEach(function() {
mockDocument = {createElement: jasmine.createSpy('createElement')};
mockDocument.createElement.and.callFake(function(elm) {
if (elm === 'div') return mockDivElement;
});
var mockCreateElementFn = function(elm) { if (elm === 'div') return mockDivElement; };
var createElementSpy = jasmine.createSpy('createElement').and.callFake(mockCreateElementFn);
mockDocument = {createElement: createElementSpy};
$sniffer = sniffer({}, mockDocument);
});
@@ -83,7 +151,6 @@ describe('$sniffer', function() {
describe('vendorPrefix', function() {
it('should return the correct vendor prefix based on the browser', function() {
inject(function($sniffer, $window) {
var expectedPrefix;
@@ -101,237 +168,171 @@ describe('$sniffer', function() {
});
});
it('should still work for an older version of Webkit', function() {
module(function($provide) {
var doc = {
body: {
style: {
WebkitOpacity: '0'
}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.vendorPrefix).toBe('webkit');
});
});
it('should still work for an older version of Webkit', function() {
var mockDocument = {
body: {
style: {
WebkitOpacity: '0'
}
}
};
expect(sniffer({}, mockDocument).vendorPrefix).toBe('webkit');
});
});
describe('animations', function() {
it('should be either true or false', function() {
inject(function($sniffer) {
expect($sniffer.animations).not.toBeUndefined();
});
});
it('should be either true or false', inject(function($sniffer) {
expect($sniffer.animations).toBeDefined();
}));
it('should be false when there is no animation style', function() {
module(function($provide) {
var doc = {
body: {
style: {}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.animations).toBe(false);
});
var mockDocument = {
body: {
style: {}
}
};
expect(sniffer({}, mockDocument).animations).toBe(false);
});
it('should be true with vendor-specific animations', function() {
module(function($provide) {
var animationStyle = 'some_animation 2s linear';
var doc = {
body: {
style: {
WebkitAnimation: animationStyle,
MozAnimation: animationStyle
}
var animationStyle = 'some_animation 2s linear';
var mockDocument = {
body: {
style: {
WebkitAnimation: animationStyle,
MozAnimation: animationStyle
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.animations).toBe(true);
});
}
};
expect(sniffer({}, mockDocument).animations).toBe(true);
});
it('should be true with w3c-style animations', function() {
module(function($provide) {
var doc = {
body: {
style: {
animation: 'some_animation 2s linear'
}
var mockDocument = {
body: {
style: {
animation: 'some_animation 2s linear'
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.animations).toBe(true);
});
}
};
expect(sniffer({}, mockDocument).animations).toBe(true);
});
it('should be true on android with older body style properties', function() {
module(function($provide) {
var doc = {
body: {
style: {
webkitAnimation: ''
}
}
};
var win = {
navigator: {
userAgent: 'android 2'
}
};
$provide.value('$document', jqLite(doc));
$provide.value('$window', win);
});
inject(function($sniffer) {
expect($sniffer.animations).toBe(true);
});
});
it('should be true when an older version of Webkit is used', function() {
module(function($provide) {
var doc = {
body: {
style: {
WebkitOpacity: '0'
}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.animations).toBe(false);
});
});
});
describe('transitions', function() {
it('should be either true or false', function() {
inject(function($sniffer) {
expect($sniffer.transitions).not.toBeUndefined();
});
});
it('should be false when there is no transition style', function() {
module(function($provide) {
var doc = {
body: {
style: {}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.transitions).toBe(false);
});
});
it('should be true with vendor-specific transitions', function() {
module(function($provide) {
var transitionStyle = '1s linear all';
var doc = {
body: {
style: {
WebkitTransition: transitionStyle,
MozTransition: transitionStyle
}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.transitions).toBe(true);
});
});
it('should be true with w3c-style transitions', function() {
module(function($provide) {
var doc = {
body: {
style: {
transition: '1s linear all'
}
}
};
$provide.value('$document', jqLite(doc));
});
inject(function($sniffer) {
expect($sniffer.transitions).toBe(true);
});
});
it('should be true on android with older body style properties', function() {
module(function($provide) {
var doc = {
body: {
style: {
webkitTransition: ''
}
}
};
var win = {
navigator: {
userAgent: 'android 2'
}
};
$provide.value('$document', jqLite(doc));
$provide.value('$window', win);
});
inject(function($sniffer) {
expect($sniffer.transitions).toBe(true);
});
});
});
describe('history', function() {
it('should be true on Boxee box with an older version of Webkit', function() {
module(function($provide) {
var doc = {
body: {
style: {}
}
};
var win = {
history: {
pushState: noop
},
navigator: {
userAgent: 'boxee (alpha/Darwin 8.7.1 i386 - 0.9.11.5591)'
}
};
$provide.value('$document', jqLite(doc));
$provide.value('$window', win);
});
inject(function($sniffer) {
expect($sniffer.history).toBe(false);
});
});
});
it('should provide the android version', function() {
module(function($provide) {
var win = {
var mockWindow = {
navigator: {
userAgent: 'android 2'
}
};
$provide.value('$document', jqLite({}));
$provide.value('$window', win);
var mockDocument = {
body: {
style: {
webkitAnimation: ''
}
}
};
expect(sniffer(mockWindow, mockDocument).animations).toBe(true);
});
inject(function($sniffer) {
expect($sniffer.android).toBe(2);
it('should be true when an older version of Webkit is used', function() {
var mockDocument = {
body: {
style: {
WebkitOpacity: '0'
}
}
};
expect(sniffer({}, mockDocument).animations).toBe(false);
});
});
describe('transitions', function() {
it('should be either true or false', inject(function($sniffer) {
expect($sniffer.transitions).toBeOneOf(true, false);
}));
it('should be false when there is no transition style', function() {
var mockDocument = {
body: {
style: {}
}
};
expect(sniffer({}, mockDocument).transitions).toBe(false);
});
it('should be true with vendor-specific transitions', function() {
var transitionStyle = '1s linear all';
var mockDocument = {
body: {
style: {
WebkitTransition: transitionStyle,
MozTransition: transitionStyle
}
}
};
expect(sniffer({}, mockDocument).transitions).toBe(true);
});
it('should be true with w3c-style transitions', function() {
var mockDocument = {
body: {
style: {
transition: '1s linear all'
}
}
};
expect(sniffer({}, mockDocument).transitions).toBe(true);
});
it('should be true on android with older body style properties', function() {
var mockWindow = {
navigator: {
userAgent: 'android 2'
}
};
var mockDocument = {
body: {
style: {
webkitTransition: ''
}
}
};
expect(sniffer(mockWindow, mockDocument).transitions).toBe(true);
});
});
describe('android', function() {
it('should provide the android version', function() {
var mockWindow = {
navigator: {
userAgent: 'android 2'
}
};
expect(sniffer(mockWindow).android).toBe(2);
});
});
});
+27 -2
View File
@@ -1104,7 +1104,8 @@ describe("animations", function() {
$animate.removeClass(element, 'active-class');
$rootScope.$digest();
expect(doneHandler).toHaveBeenCalled();
// true = rejected
expect(doneHandler).toHaveBeenCalledWith(true);
}));
it('should cancel the previously running removeClass animation if a follow-up addClass animation is using the same class value',
@@ -1123,7 +1124,8 @@ describe("animations", function() {
$animate.addClass(element, 'active-class');
$rootScope.$digest();
expect(doneHandler).toHaveBeenCalled();
// true = rejected
expect(doneHandler).toHaveBeenCalledWith(true);
}));
it('should merge a follow-up animation that does not add classes into the previous animation (pre-digest)',
@@ -1198,6 +1200,29 @@ describe("animations", function() {
expect(capturedAnimation[2].addClass).toBe('blue');
}));
it('should NOT cancel a previously joined addClass+structural animation if a follow-up ' +
'removeClass animation is using the same class value (pre-digest)',
inject(function($animate, $rootScope) {
var runner = $animate.enter(element, parent);
$animate.addClass(element, 'active-class');
var doneHandler = jasmine.createSpy('enter done');
runner.done(doneHandler);
expect(doneHandler).not.toHaveBeenCalled();
$animate.removeClass(element, 'active-class');
$rootScope.$digest();
expect(capturedAnimation[1]).toBe('enter');
expect(capturedAnimation[2].addClass).toBe(null);
expect(capturedAnimation[2].removeClass).toBe(null);
expect(doneHandler).not.toHaveBeenCalled();
}));
});
describe('should merge', function() {
+38
View File
@@ -756,5 +756,43 @@ describe('ngAnimate integration tests', function() {
expect(child.attr('style')).toContain('50px');
});
});
it('should execute the enter animation on a <form> with ngIf that has an ' +
'<input type="email" required>', function() {
var animationSpy = jasmine.createSpy();
module(function($animateProvider) {
$animateProvider.register('.animate-me', function() {
return {
enter: function(element, done) {
animationSpy();
done();
}
};
});
});
inject(function($animate, $rootScope, $compile) {
element = jqLite(
'<div>' +
'<form class="animate-me" ng-if="show">' +
'<input ng-model="myModel" type="email" required />' +
'</form>' +
'</div>');
html(element);
$compile(element)($rootScope);
$rootScope.show = true;
$rootScope.$digest();
$animate.flush();
expect(animationSpy).toHaveBeenCalled();
});
});
});
});
+151
View File
@@ -485,6 +485,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').and.callThrough();
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.calls.mostRecent().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').and.callThrough();
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.calls.mostRecent().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">' +
@@ -691,6 +811,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').and.callThrough();
var nodeB = element[0].querySelector('[ng-message="b"]');
jqLite(nodeB).remove();
// Make sure removing the element triggers the deregistration in ngMessages
expect(trim(deregisterSpy.calls.mostRecent().args[0].nodeValue)).toBe('ngMessage: b');
$rootScope.$apply('items.a = true');
expect(element.text()).toBe('A');
})
);
});
describe('when multiple', function() {
+11 -9
View File
@@ -26,17 +26,19 @@ describe('ngMock', function() {
it('should fake getLocalDateString method', function() {
//0 in -3h
var t0 = new angular.mock.TzDate(-3, 0);
expect(t0.toLocaleDateString()).toMatch('1970');
var millenium = new Date('2000').getTime();
//0 in +0h
var t1 = new angular.mock.TzDate(0, 0);
expect(t1.toLocaleDateString()).toMatch('1970');
// millenium in -3h
var t0 = new angular.mock.TzDate(-3, millenium);
expect(t0.toLocaleDateString()).toMatch('2000');
//0 in +3h
var t2 = new angular.mock.TzDate(3, 0);
expect(t2.toLocaleDateString()).toMatch('1969');
// millenium in +0h
var t1 = new angular.mock.TzDate(0, millenium);
expect(t1.toLocaleDateString()).toMatch('2000');
// millenium in +3h
var t2 = new angular.mock.TzDate(3, millenium);
expect(t2.toLocaleDateString()).toMatch('1999');
});