Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 514639b585 | |||
| 9cd9956dcb | |||
| c7813e9ebf | |||
| ef91b04cdd | |||
| 1acd97e18f | |||
| 513199ee9f | |||
| 33f3c40e93 | |||
| 696cb95d5e | |||
| 457fd21a1a | |||
| 3c6dfbf67d | |||
| 3277b885c4 | |||
| 48a256d04b | |||
| 0579430799 | |||
| 39ac68dac1 | |||
| 87fb44a5d3 | |||
| 5c76b406f7 |
+106
-78
@@ -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
@@ -264,12 +264,17 @@ module.exports = function(grunt) {
|
||||
],
|
||||
options: {
|
||||
disallowed: [
|
||||
'fit',
|
||||
'iit',
|
||||
'xit',
|
||||
'fthey',
|
||||
'tthey',
|
||||
'xthey',
|
||||
'fdescribe',
|
||||
'ddescribe',
|
||||
'xdescribe'
|
||||
'xdescribe',
|
||||
'it.only',
|
||||
'describe.only'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`}.
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
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}.
|
||||
|
||||
|
||||
|
||||
@@ -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 & 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
@@ -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
@@ -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 & 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]();
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Vendored
+11
-9
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user