Compare commits

...

76 Commits

Author SHA1 Message Date
Georgios Kalpakas 25d4e5cca4 fix($http): pass event object to eventHandlers/uploadEventHandlers
Fixes #14436
2016-04-15 14:09:39 +01:00
Peter Bacon Darwin c67112563f revert: refactor($compile): move setting of controller data to single location
Reverted from commit 21d148aedc since it caused
the Angular Material tabs directive to fail.
2016-04-15 14:09:05 +01:00
Jannick Fahlbusch 2b2ec26a75 chore(docsApp): open plnkr.co with HTTPS
Plnkr should be opened via HTTPS instead of HTTP to supress warnings about an insecure connection

Closes #14445
2016-04-15 14:55:13 +02:00
cloverharvest 41f90e5fb0 docs($http): fix a typo (his --> this)
Closes #14430
2016-04-15 01:25:09 +03:00
Adam Demuri a85c591d36 docs(component): document 'require' in angular.component
Closes #14429
2016-04-15 01:12:43 +03:00
Peter Bacon Darwin f276ad0d51 docs(CHANGELOG): add release notes for 1.5.4 2016-04-14 09:13:48 +01:00
Pete Bacon Darwin 33f817b99c feat($componentController): provide isolated scope if none is passed (#14425)
Closes #14425
2016-04-13 18:34:39 +01:00
Martin Staffa 6a4124d0fb perf(ngOptions): use documentFragment to populate select
This changes the way option elements are generated when the ngOption collection changes.
Previously, we would re-use option elements when possible (updating their text and
label). Now, we first remove all currently displayed options and the create new options for the
collection. The new options are first appended to a documentFragment, which is in the end appended
to the selectElement.

Using documentFragment improves render performance in IE with large option collections
(> 100 elements) considerably.

Creating new options from scratch fixes issues in IE where the select would become unresponsive
to user input.

Fixes #13607
Fixes #13239
Fixes #12076
2016-04-13 18:42:10 +02:00
Georgios Kalpakas 26d1b34321 docs(ngView): add known issue about asynchronously loaded ngView
Closes #14424
2016-04-13 18:45:40 +03:00
Georgios Kalpakas 7fba6b603f revert: "fix(ngRoute): allow ngView to be included in an asynchronously loaded template"
This reverts commit 5e37b2a7fd.
Eagerly loading `$route`, could break tests, because it might request the root or default route
template (something `$httpBackend` would know nothing about).

It will be re-applied for `v1.6.x`, with a breaking change notice and possibly a way to disable
the feature is tests.

Fixes #14337
2016-04-13 18:45:06 +03:00
Jason Bedard c115b37c33 perf($compile): use createMap() for directive bindings to allow fast forEach
Closes #12529
2016-04-13 12:05:30 +03:00
Georgii Dolzhykov d20ba95e28 docs(Module): fix parameter names for .decorator()
Closes #14413
2016-04-12 14:52:18 +03:00
aortyl 7945e5010a docs(guide/scope): add comma for readability
Closes #14411
2016-04-11 22:22:36 +03:00
Jason Bedard 21d148aedc refactor($compile): move setting of controller data to single location
Closes #13421
2016-04-11 18:54:41 +01:00
Georgios Kalpakas 4c8aeefb62 fix($compile): do not use noop() as controller for multiple components
Currently, custom annotations are copied from the CDO onto the controller constructor.
Using `noop()` when no controller has been specified, pollutes it with custom annotations and
makes one component's annotations available to all other components that have `noop()` as their
controller.

Fixes #14391
Closes #14402
2016-04-11 18:43:17 +01:00
a510 d384834fde fix($injector): ensure functions with overridden toString() are annotated properly
Closes #14361
2016-04-11 13:03:56 +03:00
David Rodenas Pico f975d8d448 fix(ngClass): fix watching of an array expression containing an object
Closes #14405
2016-04-11 12:40:36 +03:00
Georgios Kalpakas 2602daf993 refactor($compile): remove unnecessary call to isDefined()
(As discussed in https://github.com/angular/angular.js/pull/14406/files#r59131398.)
2016-04-11 12:33:07 +03:00
Gene McCulley 9f681c459a docs(numberFilter): fix the description of the returned value
Closes #14408
2016-04-11 12:28:08 +03:00
Peter Bacon Darwin d9448dcb9f fix($compile): still trigger $onChanges even if the inner value already matches the new value
Closes #14406
2016-04-10 20:31:01 +01:00
Jurko Gospodnetić e9c718a465 fix(ngMock): fix collecting stack trace in inject() on IE10+, PhantomJS
Add support for collecting current stack trace information in browsers
(e.g. IE10+, PhantomJS) that do not automatically store the current stack trace
information in a newly created `Error` object's `stack` property, but
only add it there once the `Error` gets thrown.

The original implementation works fine in Firefox & Chrome, but fails on IE10+
and PhantomJS where it, for example, breaks Karma's error reporting in cases
when an exception is thrown in a test like the following:

```
it('the holy crusade', inject(function() {
  var x = {};
  x.holyGrail();
}));
```

In this case, the ngMock `inject()` implementation would incorrectly add the
word `undefined` at the end of the collected error stack trace information,
thus causing the main error description to be reported back to Karma as
`undefined`.

The added test makes sure this functionality:

- works as expected in browsers supporting JavaScript stack trace
  collection, e.g. Chrome, Firefox, IE10+, Opera & PhantomJS
- does not add any bogus stack track information in browsers that do
  not support JavaScript stack trace collection, e.g. IE9

Fixes #13591
Closes #13592

Closes #13593
2016-04-09 20:51:22 +03:00
Georgios Kalpakas 4dc44f7205 test(helpers): fix error message generation for toHaveBeenCalledOnce[With] matchers
Jasmine 2.4 (maybe earlier) does not support returning an array containing both the "normal" and the
negative error messages. It will always concatenate them.

Closes #14275
2016-04-09 17:06:47 +03:00
Jan Niehusmann 499e1b2adf fix($compile): handle boolean attributes in @ bindings
Commit db5e0ff handles initial values of boolean attributes. The same
change needs to be applied inside the attrs.$observe() call.

Closes #14070
2016-04-09 16:53:41 +03:00
Peter Bacon Darwin 28c6c4dae2 docs(jqlite): add known issue
Closes #14251
2016-04-08 21:27:21 +01:00
Peter Bacon Darwin 791148a328 chore(package.json): update dgeni-packages to 0.12.0
This gives us `@knownissues` tags
2016-04-08 21:27:05 +01:00
Peter Bacon Darwin 01b1845088 feat($http): support handling additional XHR events
Closes #14367
Closes #11547
Closes #1934
2016-04-08 19:16:56 +01:00
Chris Chua 56c861c9e1 feat($http): support handling additional XHR events
Closes #11547
Closes #1934
2016-04-08 19:16:49 +01:00
Martin Staffa 451e0f6175 docs(README): fix typo 2016-04-08 19:00:20 +02:00
Martin Staffa aee7f1c72f docs(guide/accessibility): make jshint happy 2016-04-08 18:55:22 +02:00
Pablo Iván G. Soto 5fe28a0422 docs(README): add https links, improve style 2016-04-08 18:37:04 +02:00
Martin Staffa f56586d9a9 docs(guide/Accessibility): fix markdown errors, tweak layout 2016-04-08 17:32:13 +02:00
mohamed amr ec0baadcb6 feat(ngAria): add support for aria-readonly based on ngReadonly
Closes #14140
Closes #14077
2016-04-08 17:32:12 +02:00
Martin Staffa 9147d5c04a docs(ngComponentRouter): add note about shims needed for IE 2016-04-08 17:32:12 +02:00
Josh cb801378c9 docs(ngComponentRouter): fix typo
Simple typo fix from `betweent he` to `between the`

Closes #14396
2016-04-08 15:47:01 +02:00
Martin Staffa 79604f4628 fix(ngAnimate): remove event listeners only after all listeners have been called
The fix for removing the event callbacks on destroy introduced in
ce7f400011 removed the events too early, so that the event callbacks
for the "close" phase in "leave" animations were not called.

This commit fixes the behavior so that the event callbacks are only removed during on('$destroy')
when no animation is currently active on the element. When an animation is active, the event callbacks
will be removed after all callbacks have run, and if the element has no parent (has been removed from
the DOM).

Closes #14321
2016-04-08 15:47:00 +02:00
Martin Staffa bf6cb8ab0d feat(ngAnimate): let $animate.off() remove all listeners for an element 2016-04-08 15:46:59 +02:00
Andrew de74b3fd85 docs(guide/Components): fix small single letter typo
line 136: 'not' should be 'note'

Closes #14390
2016-04-08 01:32:00 +02:00
Martin Staffa c7a92d2a9a fix(ngAnimate): fire callbacks when document is hidden
Since commit a3a7afd3aa, animations are not run
when the document is hidden (only their structural or class change effects are executed).
However, some libraries rely on the $animate.on() callbacks to be called even when no actual animation
runs.
This commit restores the behavior for the ngAnimate.$animate functions.
Note that callbacks still won't be called if animations are disabled, because this would be be a potential
breaking change, as some applications might rely on this implementation.

Fixes #14120
2016-04-08 01:31:59 +02:00
Martin Staffa da284fc354 test(ngAnimate): test calling callbacks for various constellations 2016-04-08 01:31:58 +02:00
Martin Staffa 90da3059ce fix(ngAnimate): fire callbacks in the correct order for certain skipped animations 2016-04-08 01:31:58 +02:00
SHAHRUKH-KHAN 651fd05eca docs(angular.equals): add example
This Pull requests improves the doc by adding a example to `angular.equals` function.

Closes #14232
2016-04-07 15:17:54 +02:00
glenr4 85d7e09bef docs(ngAnimate): fix toggle button in example
The toggle button code on line 153 only sets bool to true, rather than toggling it.
The proposed change fixes this.

Closes #14387
2016-04-07 15:17:54 +02:00
andykuszyk 243a57648b docs(misc/Getting Started): fix markdown for headings
Closes #14353
2016-04-07 15:17:54 +02:00
Maciej Kołodziejczak 158dec330c docs(ngComponentRouter): fix a typo
Closes #14357
2016-04-07 15:17:53 +02:00
Andrew a1e63c8d4a docs(guide/Components): clarify output events with extra example and note
Add additional line from example which demonstrates using the snake cased attribute binding in
parent component template.
Add note clarifying camelCase to snake-case requirement to use the Output binding callback feature.

Closes #14365
2016-04-07 15:17:53 +02:00
Artur 8cc9978d15 docs(guide/Components): fix typo, improve style
Closes #14384
2016-04-07 15:17:52 +02:00
Robin Janssens 78a404da0e docs($httpBackend): update response data types
Updated docs to reflect that response data can either be an array, object _or_ a string
Technically, response data can be anything that can be handled by angular.copy,
but since string and JSON data is most commonly mocked, the main types are sufficient.

Closes #14346
2016-04-07 15:17:52 +02:00
Michał Gołębiowski f1bb8369b5 refactor(jshint): don't assume browser-only globals
(cherry-picked from ddad26402b)

Fixes #13442
Closes #14345
2016-04-06 20:43:06 +02:00
Peter Bacon Darwin 76b6941b4c docs(componentRouter): add custom installation instructions 2016-04-05 21:10:19 +01:00
Peter Bacon Darwin e72990dc37 fix($compile): don't throw if controller is named 2016-04-04 21:26:13 +01:00
Peter Bacon Darwin f338e96ccc feat($compile): put custom annotations on DDO
Closes #14369
Closes #14279
Closes #14284
2016-04-04 20:07:31 +01:00
Peter Bacon Darwin 0ad2b70862 fix($compile): ensure that $onChanges hook is called correctly
Due to the way that we instantiate controllers, the `$onChanges` hook
was not always available at the time we were trying to trigger the initial
call to this hook. For instance, if the hook was actually defined inside
the constructor function.

This commit fixes that but also fixes the fact that the initial call was
being made in the postDigest anyway, which was incorrect because the
it should have been made before the `$onInit` call.

Closes #14355
Closes #14359
2016-04-01 17:41:41 +01:00
Georgios Kalpakas 5261a374a9 test($browser): fix typo in property name (histroy --> history)
(The semantics of the test aren't affected, because we just needed a falsy value.)
2016-03-31 13:30:25 +03:00
Martin Staffa 832eba5fc9 fix(ngOptions): set select value when model matches disabled option
When ngModel is set to a value that matches a disabled option, ngOptions will now select the option
in the select element, which will set select.val() to the option hash value and visually
show the option value / label as selected in the select box. Previously, disabled
options forced the unknown value.
The previous behavior is inconsistent with both default HTML behavior and select with
ngModel but without ngOptions. Both allow disabled values to be selected programmatically.

A common use case for this behavior is an option that was previously valid, but has
been disabled, and cannot be selected again.

This commit removes a duplicate test, and all other tests that previously checked that disabled
options are not set have been adjusted to the ensure the opposite.

Fixes #12756
2016-03-30 11:42:07 +02:00
cscport 2b569fc0b2 docs(angular.bootstrap): fix capitalization in error message
Closes #14325
2016-03-27 15:43:37 +03:00
Lucas Mirelmann ee6aeb08bf docs(ngParseExt): Fix package name 2016-03-27 00:00:14 +01:00
Peter Bacon Darwin 8d43d8b8e7 feat($compile): add isFirstChange() method to onChanges object
Closes #14318
Closes #14323
2016-03-26 20:06:13 +00:00
Lucas Mirelmann d08f5c6986 feat(ngParseExt): New ngParseExt module
New ngParseExt module

Including this module into an application will extend $parse to allow identifiers
following ES6 identifiers
2016-03-26 20:41:11 +01:00
Lucas Mirelmann 3e7fa19197 feat($parse): Add the ability to define the identifier characters
Add the ability to define the identifier starts and identifier continue characters
2016-03-26 20:41:01 +01:00
Lucas Mirelmann 5d379db72b chore(bower): Add parse-ext repository 2016-03-26 20:40:49 +01:00
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
100 changed files with 7096 additions and 5469 deletions
+8 -3
View File
@@ -1,19 +1,24 @@
{
"bitwise": true,
"esversion": 6,
"immed": true,
"newcap": true,
"noarg": true,
"noempty": true,
"nonew": true,
"trailing": true,
"maxlen": 200,
"boss": true,
"eqnull": true,
"expr": true,
"globalstrict": true,
"laxbreak": true,
"loopfunc": true,
"strict": "global",
"sub": true,
"undef": true,
"indent": 2
"indent": 2,
"globals": {
"ArrayBuffer": false,
"Uint8Array": false
}
}
+189 -78
View File
@@ -1,7 +1,122 @@
<a name="1.5.4"></a>
# 1.5.4 graduated-sophistry (2016-04-14)
## Bug Fixes
- **$compile:**
- do not use `noop()` as controller for multiple components
([4c8aeefb](https://github.com/angular/angular.js/commit/4c8aeefb624de7436ad95f3cd525405e0c3f493e),
[#14391](https://github.com/angular/angular.js/issues/14391), [#14402](https://github.com/angular/angular.js/issues/14402))
- still trigger `$onChanges` even if the inner value already matches the new value
([d9448dcb](https://github.com/angular/angular.js/commit/d9448dcb9f901ceb04deda1d5f3d5aac8442a718),
[#14406](https://github.com/angular/angular.js/issues/14406))
- handle boolean attributes in `@` bindings
([499e1b2a](https://github.com/angular/angular.js/commit/499e1b2adf27f32d671123f8dceadb3df2ad84a9),
[#14070](https://github.com/angular/angular.js/issues/14070))
- don't throw if controller is named
([e72990dc](https://github.com/angular/angular.js/commit/e72990dc3714c8b847185ddb64fd5fd00e5cceab))
- ensure that `$onChanges` hook is called correctly
([0ad2b708](https://github.com/angular/angular.js/commit/0ad2b70862d49ecc4355a16d767c0ca9358ecc3e),
[#14355](https://github.com/angular/angular.js/issues/14355), [#14359](https://github.com/angular/angular.js/issues/14359))
- **$injector:** ensure functions with overridden `toString()` are annotated properly
([d384834f](https://github.com/angular/angular.js/commit/d384834fdee140a716298bd065f304f8fba4725e),
[#14361](https://github.com/angular/angular.js/issues/14361))
- **ngAnimate:**
- remove event listeners only after all listeners have been called
([79604f46](https://github.com/angular/angular.js/commit/79604f462899c118a99d610995083ff82d38aa35),
[#14321](https://github.com/angular/angular.js/issues/14321))
- fire callbacks when document is hidden
([c7a92d2a](https://github.com/angular/angular.js/commit/c7a92d2a9a436dddd65de721c9837a93e915d939),
[#14120](https://github.com/angular/angular.js/issues/14120))
- fire callbacks in the correct order for certain skipped animations
([90da3059](https://github.com/angular/angular.js/commit/90da3059cecfefaecf136b01cd87aee6775a8778))
- **ngClass:** fix watching of an array expression containing an object
([f975d8d4](https://github.com/angular/angular.js/commit/f975d8d4481e0b8cdba553f0e5ad9ec1688adae8),
[#14405](https://github.com/angular/angular.js/issues/14405))
- **ngMock:** fix collecting stack trace in `inject()` on IE10+, PhantomJS
([e9c718a4](https://github.com/angular/angular.js/commit/e9c718a465d28b9f2691e3acab944f7c31aa9fb6),
[#13591](https://github.com/angular/angular.js/issues/13591), [#13592](https://github.com/angular/angular.js/issues/13592), [#13593](https://github.com/angular/angular.js/issues/13593))
- **ngOptions:** set select value when model matches disabled option
([832eba5f](https://github.com/angular/angular.js/commit/832eba5fc952312e6b99127123e6e75bdf729006),
[#12756](https://github.com/angular/angular.js/issues/12756))
## Features
- **$compile:**
- put custom annotations on DDO
([f338e96c](https://github.com/angular/angular.js/commit/f338e96ccc739efc4b24022eae406c3d5451d422),
[#14369](https://github.com/angular/angular.js/issues/14369), [#14279](https://github.com/angular/angular.js/issues/14279), [#14284](https://github.com/angular/angular.js/issues/14284))
- add `isFirstChange()` method to onChanges object
([8d43d8b8](https://github.com/angular/angular.js/commit/8d43d8b8e7aacf97ddb9aa48bff25db57249cdd5),
[#14318](https://github.com/angular/angular.js/issues/14318), [#14323](https://github.com/angular/angular.js/issues/14323))
- **$componentController:** provide isolated scope if none is passed (#14425)
([33f817b9](https://github.com/angular/angular.js/commit/33f817b99cb20e566b381e7202235fe99b4a742a),
[#14425](https://github.com/angular/angular.js/issues/14425))
- **$http:**
- support handling additional XHR events
([01b18450](https://github.com/angular/angular.js/commit/01b18450882da9bb9c903d43c0daddbc03c2c35d) and
[56c861c9](https://github.com/angular/angular.js/commit/56c861c9e114c45790865e5635eaae8d32eb649a),
[#14367](https://github.com/angular/angular.js/issues/14367), [#11547](https://github.com/angular/angular.js/issues/11547), [#1934](https://github.com/angular/angular.js/issues/1934))
- **$parse:** add the ability to define the identifier characters
([3e7fa191](https://github.com/angular/angular.js/commit/3e7fa19197c54a764225ad27c0c0bf72263daa8d))
- **ngAnimate:** let $animate.off() remove all listeners for an element
([bf6cb8ab](https://github.com/angular/angular.js/commit/bf6cb8ab0d157083a1ed55743e3fffe728daa6f3))
- **ngAria:** add support for aria-readonly based on ngReadonly
([ec0baadc](https://github.com/angular/angular.js/commit/ec0baadcb68a4fa8da27d76b7e6a4e0840acd7fa),
[#14140](https://github.com/angular/angular.js/issues/14140), [#14077](https://github.com/angular/angular.js/issues/14077))
- **ngParseExt:** new ngParseExt module
([d08f5c69](https://github.com/angular/angular.js/commit/d08f5c698624f6243685b16f2d458cb9a980ebde))
## Performance Improvements
- **$compile:** use createMap() for directive bindings to allow fast `forEach`
([c115b37c](https://github.com/angular/angular.js/commit/c115b37c336f3a5936187279057b29c76078caf2),
[#12529](https://github.com/angular/angular.js/issues/12529))
- **ngOptions:** use `documentFragment` to populate `select` options
([6a4124d0](https://github.com/angular/angular.js/commit/6a4124d0fb17cd7fc0e8bf5a1ca4d785a1d11c1c),
[#13607](https://github.com/angular/angular.js/issues/13607), [#13239](https://github.com/angular/angular.js/issues/13239), [#12076](https://github.com/angular/angular.js/issues/12076))
<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 +125,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 +289,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)
+16 -3
View File
@@ -135,6 +135,9 @@ module.exports = function(grunt) {
ngMock: {
files: { src: 'src/ngMock/**/*.js' },
},
ngParseExt: {
files: { src: 'src/ngParseExt/**/*.js' },
},
ngResource: {
files: { src: 'src/ngResource/**/*.js' },
},
@@ -231,7 +234,11 @@ module.exports = function(grunt) {
dest: 'build/angular-aria.js',
src: util.wrap(files['angularModules']['ngAria'], 'module')
},
'promises-aplus-adapter': {
parseext: {
dest: 'build/angular-parse-ext.js',
src: util.wrap(files['angularModules']['ngParseExt'], 'module')
},
"promises-aplus-adapter": {
dest:'tmp/promises-aplus-adapter++.js',
src:['src/ng/q.js', 'lib/promises-aplus/promises-aplus-test-adapter.js']
}
@@ -249,7 +256,8 @@ module.exports = function(grunt) {
resource: 'build/angular-resource.js',
route: 'build/angular-route.js',
sanitize: 'build/angular-sanitize.js',
aria: 'build/angular-aria.js'
aria: 'build/angular-aria.js',
parseext: 'build/angular-parse-ext.js'
},
@@ -264,12 +272,17 @@ module.exports = function(grunt) {
],
options: {
disallowed: [
'fit',
'iit',
'xit',
'fthey',
'tthey',
'xthey',
'fdescribe',
'ddescribe',
'xdescribe'
'xdescribe',
'it.only',
'describe.only'
]
}
},
+18 -13
View File
@@ -8,20 +8,21 @@ synchronizes data from your UI (view) with your JavaScript objects (model) throu
binding. To help you structure your application better and make it easy to test, AngularJS teaches
the browser how to do dependency injection and inversion of control.
Oh yeah and it helps with server-side communication, taming async callbacks with promises and
deferreds. It also makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
piece of cake. Best of all?? It makes development fun!
It also helps with server-side communication, taming async callbacks with promises and deferreds,
and it makes client-side navigation and deeplinking with hashbang urls or HTML5 pushState a
piece of cake. Best of all? It makes development fun!
* Web site: http://angularjs.org
* Tutorial: http://docs.angularjs.org/tutorial
* API Docs: http://docs.angularjs.org/api
* Developer Guide: http://docs.angularjs.org/guide
* Web site: https://angularjs.org
* Tutorial: https://docs.angularjs.org/tutorial
* API Docs: https://docs.angularjs.org/api
* Developer Guide: https://docs.angularjs.org/guide
* Contribution guidelines: [CONTRIBUTING.md](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md)
* Dashboard: http://dashboard.angularjs.org
* Dashboard: https://dashboard.angularjs.org
Building AngularJS
---------
[Once you have your environment set up](http://docs.angularjs.org/misc/contribute) just run:
[Once you have set up your environment](https://docs.angularjs.org/misc/contribute), just run:
grunt package
@@ -37,8 +38,12 @@ To execute end-to-end (e2e) tests, use:
grunt package
grunt test:e2e
To learn more about the grunt tasks, run `grunt --help` and also read our
[contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
To learn more about the grunt tasks, run `grunt --help`
Contribute & Develop
--------------------
We've set up a separate document for our [contribution guidelines](https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md).
[![Analytics](https://ga-beacon.appspot.com/UA-8594346-11/angular.js/README.md?pixel)](https://github.com/igrigorik/ga-beacon)
@@ -48,7 +53,7 @@ What to use AngularJS for and when to use it
AngularJS is the next generation framework where each component is designed to work with every other component in an interconnected way like a well-oiled machine. AngularJS is JavaScript MVC made easy and done right. (Well it is not really MVC, read on, to understand what this means.)
#### MVC, no, MV* done the right way!
MVC, short for Model-View-Controller, is a design pattern, i.e. how the code should be organized and how the different parts of an application separated for proper readability and debugging. Model is the data and the database. View is the user interface and what the user sees. Controller is the main link between Model and View. These are the three pillars of major programming frameworks present on the market today. On the other hand AngularJS works on MV*, short for Model-View-_Whatever_. The _Whatever_ is AngularJS's way of telling that you may create any kind of linking between the Model and the View here.
MVC, short for Model-View-Controller, is a design pattern, i.e. how the code should be organized and how the different parts of an application separated for proper readability and debugging. Model is the data and the database. View is the user interface and what the user sees. Controller is the main link between Model and View. These are the three pillars of major programming frameworks present on the market today. On the other hand AngularJS works on MV*, short for Model-View-_Whatever_. The _Whatever_ is AngularJS's way of telling that you may create any kind of linking between the Model and the View here.
Unlike other frameworks in any programming language, where MVC, the three separate components, each one has to be written and then connected by the programmer, AngularJS helps the programmer by asking him/her to just create these and everything else will be taken care of by AngularJS.
+4
View File
@@ -120,6 +120,10 @@ var angularFiles = {
'ngMessages': [
'src/ngMessages/messages.js'
],
'ngParseExt': [
'src/ngParseExt/ucd.js',
'src/ngParseExt/module.js'
],
'ngResource': [
'src/ngResource/resource.js'
],
+4 -1
View File
@@ -19,9 +19,12 @@
"dump": false,
/* e2e */
"protractor": false,
"browser": false,
"element": false,
"by": false,
"$": false,
"$$": false,
/* testabilityPatch / matchers */
"inject": false,
@@ -39,4 +42,4 @@
"browserTrigger": false,
"jqLiteCacheSize": false
}
}
}
+1 -1
View File
@@ -153,7 +153,7 @@ angular.module('examples', [])
postData.description = ctrl.example.name;
formPostData('http://plnkr.co/edit/?p=preview', newWindow, postData);
formPostData('https://plnkr.co/edit/?p=preview', newWindow, postData);
});
};
+1
View File
@@ -54,6 +54,7 @@ module.exports = new Package('angularjs', [
.config(function(parseTagsProcessor) {
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/tutorial-step'));
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/sortOrder'));
parseTagsProcessor.tagDefinitions.push(require('./tag-defs/installation'));
})
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
name: 'installation'
};
@@ -0,0 +1,92 @@
{% extends "base.template.html" %}
{% block content %}
<h1>
{% if doc.title %}{$ doc.title | marked $}{% else %}{$ doc.name | code $}{% endif %}
</h1>
{% if doc.installation or doc.installation == '' %}
{$ doc.installation | marked $}
{% else %}
<h2>Installation</h2>
<p>First include {$ doc.packageFile | code $} in your HTML:</p>
{% code %}
<script src="angular.js">
<script src="{$ doc.packageFile $}">
{% endcode %}
<p>You can download this file from the following places:</p>
<ul>
<li>
<a href="https://developers.google.com/speed/libraries/devguide#angularjs">Google CDN</a><br>
e.g. {$ ("//ajax.googleapis.com/ajax/libs/angularjs/X.Y.Z/" + doc.packageFile) | code $}
</li>
<li>
<a href="http://bower.io">Bower</a><br>
e.g. {% code %}bower install {$ doc.packageName $}@X.Y.Z{% endcode %}
</li>
<li>
<a href="http://code.angularjs.org/">code.angularjs.org</a><br>
e.g. {% code %}"//code.angularjs.org/X.Y.Z/{$ doc.packageFile $}"{% endcode %}
</li>
</ul>
<p>where X.Y.Z is the AngularJS version you are running.</p>
<p>Then load the module in your application by adding it as a dependent module:</p>
{% code %}
angular.module('app', ['{$ doc.name $}']);
{% endcode %}
<p>With that you&apos;re ready to get started!</p>
{% endif %}
{$ doc.description | marked $}
{% if doc.knownIssueDocs %}
<div class="known-issues">
<h2 id="known-issues">Known Issues</h2>
<table class="definition-table">
<tr><th>Name</th><th>Description</th></tr>
{% for issueDoc in doc.knownIssueDocs -%}
<tr>
<td>{$ issueDoc.id | link(issueDoc.name, issueDoc) $}</td>
<td>
{% for issue in issueDoc.knownIssues -%}
{$ issue | marked $}
{% endfor -%}
</td>
</tr>
{% endfor -%}
</table>
</div>
{% endif %}
<div class="component-breakdown">
<h2>Module Components</h2>
{% for componentGroup in doc.componentGroups %}
<div>
<h3 class="component-heading" id="{$ componentGroup.groupType | dashCase $}">{$ componentGroup.groupType | title $}</h3>
<table class="definition-table">
<tr>
<th>Name</th>
<th>Description</th>
</tr>
{% for component in componentGroup.components %}
<tr>
<td>{$ component.id | link(component.name, component) $}</td>
<td>{$ component.description | firstParagraph | marked $}</td>
</tr>
{% endfor %}
</table>
</div>
{% endfor %}
</div>
{% if doc.usage %}
<h2>Usage</h2>
{$ doc.usage | marked $}
{% endif %}
{% endblock %}
+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`}.
+90 -69
View File
@@ -10,7 +10,7 @@ The goal of ngAria is to improve Angular's default accessibility by enabling com
[ARIA](http://www.w3.org/TR/wai-aria/) attributes that convey state or semantic information for
assistive technologies used by persons with disabilities.
##Including ngAria
## Including ngAria
Using {@link ngAria ngAria} is as simple as requiring the ngAria module in your application. ngAria hooks into
standard AngularJS directives and quietly injects accessibility support into your application
@@ -20,7 +20,7 @@ at runtime.
angular.module('myApp', ['ngAria'])...
```
###Using ngAria
### Using ngAria
Most of what ngAria does is only visible "under the hood". To see the module in action, once you've
added it as a dependency, you can test a few things:
* Using your favorite element inspector, look for attributes added by ngAria in your own code.
@@ -28,12 +28,13 @@ added it as a dependency, you can test a few things:
* Fire up a screen reader such as VoiceOver or NVDA to check for ARIA support.
[Helpful screen reader tips.](http://webaim.org/articles/screenreader_testing/)
##Supported directives
## Supported directives
Currently, ngAria interfaces with the following directives:
* {@link guide/accessibility#ngmodel ngModel}
* {@link guide/accessibility#ngdisabled ngDisabled}
* {@link guide/accessibility#ngrequired ngRequired}
* {@link guide/accessibility#ngreadonly ngReadonly}
* {@link guide/accessibility#ngvaluechecked ngChecked}
* {@link guide/accessibility#ngvaluechecked ngValue}
* {@link guide/accessibility#ngshow ngShow}
@@ -57,12 +58,62 @@ attributes (if they have not been explicitly specified by the developer):
* aria-valuenow
* aria-invalid
* aria-required
* aria-readonly
###Example
### Example
<example module="ngAria_ngModelExample" deps="angular-aria.js">
<file name="index.html">
<style>
<file name="index.html">
<form ng-controller="formsController">
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
aria-label="Custom Checkbox" show-attrs>
<span class="icon" aria-hidden="true"></span>
Custom Checkbox
</some-checkbox>
</form>
</file>
<file name="script.js">
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
.controller('formsController', function($scope){
$scope.checked = false;
$scope.toggleCheckbox = function(){
$scope.checked = !$scope.checked;
};
})
.directive('someCheckbox', function(){
return {
restrict: 'E',
link: function($scope, $el, $attrs) {
$el.on('keypress', function(event){
event.preventDefault();
if(event.keyCode === 32 || event.keyCode === 13){
$scope.toggleCheckbox();
$scope.$apply();
}
});
}
};
})
.directive('showAttrs', function() {
return function($scope, $el, $attrs) {
var pre = document.createElement('pre');
$el.after(pre);
$scope.$watch(function() {
var $attrs = {};
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
if (item.name !== 'show-$attrs') {
$attrs[item.name] = item.value;
}
});
return $attrs;
}, function(newAttrs, oldAttrs) {
pre.textContent = JSON.stringify(newAttrs, null, 2);
}, true);
};
});
</file>
<file name="style.css">
[role=checkbox] {
cursor: pointer;
display: inline-block;
@@ -81,58 +132,7 @@ attributes (if they have not been explicitly specified by the developer):
pre {
white-space: pre-wrap;
}
</style>
<div>
<form ng-controller="formsController">
<some-checkbox role="checkbox" ng-model="checked" ng-class="{active: checked}"
ng-disabled="isDisabled" ng-click="toggleCheckbox()"
aria-label="Custom Checkbox" show-attrs>
<span class="icon" aria-hidden="true"></span>
Custom Checkbox
</some-checkbox>
</form>
</div>
<script>
var app = angular.module('ngAria_ngModelExample', ['ngAria'])
.controller('formsController', function($scope){
$scope.checked = false;
$scope.toggleCheckbox = function(){
$scope.checked = !$scope.checked;
}
})
.directive('someCheckbox', function(){
return {
restrict: 'E',
link: function($scope, $el, $attrs) {
$el.on('keypress', function(event){
event.preventDefault();
if(event.keyCode === 32 || event.keyCode === 13){
$scope.toggleCheckbox();
$scope.$apply();
}
});
}
}
})
.directive('showAttrs', function() {
return function($scope, $el, $attrs) {
var pre = document.createElement('pre');
$el.after(pre);
$scope.$watch(function() {
var $attrs = {};
Array.prototype.slice.call($el[0].attributes, 0).forEach(function(item) {
if (item.name !== 'show-$attrs') {
$attrs[item.name] = item.value;
}
});
return $attrs;
}, function(newAttrs, oldAttrs) {
pre.textContent = JSON.stringify(newAttrs, null, 2);
}, true);
}
});
</script>
</file>
</file>
</example>
ngAria will also add `tabIndex`, ensuring custom elements with these roles will be reachable from
@@ -147,7 +147,7 @@ To ease the transition between native inputs and custom controls, ngAria now sup
The original directives were created for native inputs only, so ngAria extends
support to custom elements by managing `aria-checked` for accessibility.
###Example
### Example
```html
<custom-checkbox ng-checked="val"></custom-checkbox>
@@ -169,7 +169,7 @@ using ngAria with {@link ng.ngDisabled ngDisabled} will also
add `aria-disabled`. This tells assistive technologies when a non-native input is disabled, helping
custom controls to be more accessible.
###Example
### Example
```html
<md-checkbox ng-disabled="disabled"></md-checkbox>
@@ -181,8 +181,10 @@ Becomes:
<md-checkbox disabled aria-disabled="true"></md-checkbox>
```
>You can check whether a control is legitimately disabled for a screen reader by visiting
<div class="alert alert-info">
You can check whether a control is legitimately disabled for a screen reader by visiting
[chrome://accessibility](chrome://accessibility) and inspecting [the accessibility tree](http://www.paciellogroup.com/blog/2015/01/the-browser-accessibility-tree/).
</div>
<h2 id="ngrequired">ngRequired</h2>
@@ -191,7 +193,7 @@ The boolean `required` attribute is only valid for native form controls such as
as required, using ngAria with {@link ng.ngRequired ngRequired} will also add
`aria-required`. This tells accessibility APIs when a custom control is required.
###Example
### Example
```html
<md-checkbox ng-required="val"></md-checkbox>
@@ -203,9 +205,28 @@ Becomes:
<md-checkbox ng-required="val" aria-required="true"></md-checkbox>
```
<h2 id="ngreadonly">ngReadonly</h2>
The boolean `readonly` attribute is only valid for native form controls such as `input` and
`textarea`. To properly indicate custom element directives such as `<md-checkbox>` or `<custom-input>`
as required, using ngAria with {@link ng.ngReadonly ngReadonly} will also add
`aria-readonly`. This tells accessibility APIs when a custom control is read-only.
### Example
```html
<md-checkbox ng-readonly="val"></md-checkbox>
```
Becomes:
```html
<md-checkbox ng-readonly="val" aria-readonly="true"></md-checkbox>
```
<h2 id="ngshow">ngShow</h2>
>The {@link ng.ngShow ngShow} directive shows or hides the
The {@link ng.ngShow ngShow} directive shows or hides the
given HTML element based on the expression provided to the `ngShow` attribute. The element is
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
@@ -222,7 +243,7 @@ screen reader users won't accidentally focus on "mystery elements". Managing tab
child control can be complex and affect performance, so its best to just stick with the default
`display: none` CSS. See the [fourth rule of ARIA use](http://www.w3.org/TR/aria-in-html/#fourth-rule-of-aria-use).
###Example
### Example
```css
.ng-hide {
display: block;
@@ -242,7 +263,7 @@ Becomes:
<h2 id="nghide">ngHide</h2>
>The {@link ng.ngHide ngHide} directive shows or hides the
The {@link ng.ngHide ngHide} directive shows or hides the
given HTML element based on the expression provided to the `ngHide` attribute. The element is
shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
@@ -283,11 +304,11 @@ Becomes:
<h2 id="ngmessages">ngMessages</h2>
The new ngMessages module makes it easy to display form validation or other messages with priority
The ngMessages module makes it easy to display form validation or other messages with priority
sequencing and animation. To expose these visual messages to screen readers,
ngAria injects `aria-live="assertive"`, causing them to be read aloud any time a message is shown,
regardless of the user's focus location.
###Example
### Example
```html
<div ng-messages="myForm.myName.$error">
@@ -305,7 +326,7 @@ Becomes:
</div>
```
##Disabling attributes
## Disabling attributes
The attribute magic of ngAria may not work for every scenario. To disable individual attributes,
you can use the {@link ngAria.$ariaProvider#config config} method. Just keep in mind this will
tell ngAria to ignore the attribute globally.
@@ -343,7 +364,7 @@ tell ngAria to ignore the attribute globally.
</file>
</example>
##Common Accessibility Patterns
## Common Accessibility Patterns
Accessibility best practices that apply to web apps in general also apply to Angular.
+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}.
+27 -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">
@@ -133,6 +133,8 @@ components should follow a few simple conventions:
For a deletion, that means the component doesn't delete the `hero` itself, but sends it back to
the owner component via the correct event.
```html
<!-- note that we use snake case for bindings in the template as usual -->
<editable-field on-update="$ctrl.update('location', value)"></editable-field><br>
<button ng-click="$ctrl.onDelete({hero: $ctrl.hero})">Delete</button>
```
- That way, the parent component can decide what to do with the event (e.g. delete an item or update the properties)
@@ -147,6 +149,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, isFirstChange() }`. 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, your component can hook into 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
+1 -1
View File
@@ -257,7 +257,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
2. **Watcher registration**
During template linking directives register {@link
During template linking, directives register {@link
ng.$rootScope.Scope#$watch watches} on the scope. These watches will be
used to propagate model values to the DOM.
+4 -4
View File
@@ -16,9 +16,9 @@ becoming an Angular expert.
starter app with a directory layout, test harness, and scripts to begin building your application.
#Further Steps
# Further Steps
##Watch Videos
## Watch Videos
If you havent had a chance to watch the videos from the homepage, please check out:
@@ -29,13 +29,13 @@ If you havent had a chance to watch the videos from the homepage, please chec
And visit our [YouTube channel](http://www.youtube.com/user/angularjs) for more AngularJS video presentations and
tutorials.
##Subscribe
## Subscribe
* Subscribe to the [mailing list](http://groups.google.com/forum/?fromgroups#!forum/angular). Ask questions here!
* Follow us on [Twitter](https://twitter.com/intent/follow?original_referer=http%3A%2F%2Fangularjs.org%2F&region=follow_link&screen_name=angularjs&source=followbutton&variant=2.0)
* Add us to your circles on [Google+](https://plus.google.com/110323587230527980117/posts)
##Read more
## Read more
The AngularJS documentation includes the {@link guide/index Developer Guide} covering concepts and the
{@link ./api API Reference} for syntax and usage.
+44 -5
View File
@@ -1,5 +1,9 @@
"use strict";
var fs = require('fs');
var _ = require('lodash');
var stripJsonComments = require('strip-json-comments');
var gulp = require('gulp');
var log = require('gulp-util').log;
var concat = require('gulp-concat');
@@ -25,6 +29,23 @@ var ignoredFiles = '!src/angular.bind.js';
var assets = 'app/assets/**/*';
var getJshintConfig = function(filepath) {
return JSON.parse(stripJsonComments(fs.readFileSync(filepath, {encoding: 'utf-8'})));
};
var getMergedJshintConfig = function(filepath) {
// "extends" doesn't work in configuration passed by an object, we need to do the extending ourselves.
var config = getJshintConfig(filepath);
var baseConfig = getJshintConfig('../.jshintrc-base');
_.merge(config, baseConfig);
delete config.extends;
// Examples don't run in strict mode; accept that for now.
config.strict = false;
return config;
};
var copyComponent = function(component, pattern, sourceFolder, packageFile) {
pattern = pattern || '/**/*';
sourceFolder = sourceFolder || bowerFolder;
@@ -90,17 +111,35 @@ gulp.task('assets', ['bower'], function() {
gulp.task('doc-gen', ['bower'], function() {
var dgeni = new Dgeni([require('./config')]);
return dgeni.generate().catch(function(error) {
return dgeni.generate().catch(function() {
process.exit(1);
});
});
// JSHint the example and protractor test files
gulp.task('jshint', ['doc-gen'], function() {
gulp.src([outputFolder + '/ptore2e/**/*.js', outputFolder + '/examples/**/*.js'])
.pipe(jshint())
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'));
var examplesConfig = getMergedJshintConfig('../docs/app/test/.jshintrc');
// Some tests use `alert` which is not assumed to be available even with `"browser": true`.
examplesConfig.globals.alert = false;
var protractorConfig = getMergedJshintConfig('../docs/app/e2e/.jshintrc');
return merge(
gulp.src([
outputFolder + '/examples/**/*.js',
'!' + outputFolder + '/examples/**/protractor.js',
])
.pipe(jshint(examplesConfig))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail')),
gulp.src([
outputFolder + '/ptore2e/**/*.js',
outputFolder + '/examples/**/protractor.js',
])
.pipe(jshint(protractorConfig))
.pipe(jshint.reporter('jshint-stylish'))
.pipe(jshint.reporter('fail'))
);
});
+4
View File
@@ -9,4 +9,8 @@ npm run test-i18n
node src/closureSlurper.js
npm run test-i18n-ucd
echo "Generating ngParseExt"
node ucd/src/extract.js
+53
View File
@@ -0,0 +1,53 @@
var extractValues = require('../src/extractValues.js').extractValues;
var stream = require('stream');
function StringStream(str) {
return new stream.Readable({
read: function(n) {
this.push(str);
str = null;
}
});
}
describe('extractValues', function() {
it('should extract the values from the xml', function(done) {
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><char cp="0002" IDS="Y"></char><char cp="0003" IDS="N"></char></repertoire></ucd>';
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
expect(values).toEqual({ IDS_Y : [ [ '0001', '0002' ] ] });
done();
});
});
it('should extract the values from the xml if the last element matches', function(done) {
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><char cp="0002" IDS="Y"></char><char cp="0003" IDS="Y"></char></repertoire></ucd>';
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
expect(values).toEqual({ IDS_Y : [ [ '0001', '0003' ] ] });
done();
});
});
it('should support `reserved`', function(done) {
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><reserved first-cp="0002" last-cp="0005" IDS="N"></reserved><char cp="0006" IDS="Y"></char></repertoire></ucd>';
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
done();
});
});
it('should support `surrogate`', function(done) {
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><surrogate first-cp="0002" last-cp="0005" IDS="N"></surrogate><char cp="0006" IDS="Y"></char></repertoire></ucd>';
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
done();
});
});
it('should support `noncharactere`', function(done) {
var str = '<ucd><repertoire><char cp="0000" IDS="N"></char><char cp="0001" IDS="Y"></char><noncharacter first-cp="0002" last-cp="0005" IDS="N"></noncharacter><char cp="0006" IDS="Y"></char></repertoire></ucd>';
extractValues(StringStream(str), {'IDS': 'Y'}, function(values) {
expect(values).toEqual({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ] });
done();
});
});
});
+54
View File
@@ -0,0 +1,54 @@
var generateCodeModule = require('../src/generateCode.js');
var generateCode = generateCodeModule.generateCode;
var generateFunction = generateCodeModule.generateFunction;
describe('generateFunction', function() {
it('should generate function with ranges', function() {
expect(generateFunction([ [ '0001', '0003' ] ], 'IDS_Y')).toEqual('\
function IDS_Y(cp) {\n\
if (0x0001 <= cp && cp <= 0x0003) return true;\n\
return false;\n\
}\n');
});
it('should generate function with multiple ranges', function() {
expect(generateFunction([ [ '0001', '0003' ], [ '0005', '0009'] ], 'IDS_Y')).toEqual('\
function IDS_Y(cp) {\n\
if (0x0001 <= cp && cp <= 0x0003) return true;\n\
if (0x0005 <= cp && cp <= 0x0009) return true;\n\
return false;\n\
}\n');
});
it('should generate function with unique values', function() {
expect(generateFunction([ [ '0001', '0001' ], [ '0005', '0009'] ], 'IDS_Y')).toEqual('\
function IDS_Y(cp) {\n\
if (cp === 0x0001) return true;\n\
if (0x0005 <= cp && cp <= 0x0009) return true;\n\
return false;\n\
}\n');
});
});
describe('generateCode', function() {
it('should generate the function for all the values', function() {
expect(generateCode({ IDS_Y : [ [ '0001', '0001' ], [ '0006', '0006' ] ], IDC_Y : [ [ '0002', '0002' ], [ '0007', '0007' ] ] })).toEqual('\
/******************************************************\n\
* Generated file, do not modify *\n\
* *\n\
*****************************************************/\n\
"use strict";\n\
function IDS_Y(cp) {\n\
if (cp === 0x0001) return true;\n\
if (cp === 0x0006) return true;\n\
return false;\n\
}\n\
function IDC_Y(cp) {\n\
if (cp === 0x0002) return true;\n\
if (cp === 0x0007) return true;\n\
return false;\n\
}\n\
');
});
});
+28
View File
@@ -0,0 +1,28 @@
"use strict";
var fs = require('fs');
var zlib = require('zlib');
var extractValues = require('./extractValues').extractValues;
var generateCode = require('./generateCode').generateCode;
var generateextractValues = require('./extractValues').extractValues;
// ID_Start and ID_Continue
var propertiesToExtract = {'IDS': 'Y', 'IDC': 'Y'};
function main() {
extractValues(
fs.createReadStream('./ucd/src/ucd.all.flat.xml.gz').pipe(zlib.createGunzip()),
propertiesToExtract,
writeFile);
function writeFile(validRanges) {
var code = generateCode(validRanges);
try {
var stats = fs.lstatSync('../src/ngParseExt');
} catch (e) {
fs.mkdirSync('../src/ngParseExt');
}
fs.writeFile('../src/ngParseExt/ucd.js', code);
}
}
main();
+58
View File
@@ -0,0 +1,58 @@
/**
* Extract values from a stream.
*/
exports.extractValues = extractValues;
var sax = require('sax/lib/sax');
var saxStrict = true;
var saxOptions = {};
var validXMLTagNames = { char: 'Y', reserved: 'Y', surrogate: 'Y', noncharacter: 'Y'};
function extractValues(stream, propertiesToExtract, callback) {
var saxStream = sax.createStream(saxStrict, saxOptions);
var firstValid = {};
var lastValid = {};
var keys = Object.keys(propertiesToExtract);
var keyValues = keys.map(function(k) { return propertiesToExtract[k]; });
var validRanges = {};
for (var i in keys) {
validRanges[keys[i] + '_' + keyValues[i]] = [];
}
saxStream.onopentag = onOpenTag;
stream
.pipe(saxStream)
.on('end', doCallback);
function onOpenTag(node) {
var property;
if (validXMLTagNames[node.name]) {
for (var i in keys) {
property = keyValues[i];
if (node.attributes[keys[i]] === property) validProperty(keys[i] + '_' + property, node);
else invalidProperty(keys[i] + '_' + property);
}
}
}
function validProperty(property, node) {
if (!firstValid[property]) firstValid[property] =
node.attributes.cp || node.attributes['first-cp'];
lastValid[property] = node.attributes.cp || node.attributes['last-cp'];
}
function invalidProperty(property) {
if (!firstValid[property]) return;
validRanges[property].push([firstValid[property], lastValid[property]]);
firstValid[property] = null;
}
function doCallback() {
for (var i in keys) {
property = keys[i] + '_' + keyValues[i];
invalidProperty(property);
}
callback(validRanges);
}
}
+33
View File
@@ -0,0 +1,33 @@
exports.generateCode = generateCode;
exports.generateFunction = generateFunction;
function generateCode(validRanges) {
var code = '/******************************************************\n' +
' * Generated file, do not modify *\n' +
' * *\n' +
' *****************************************************/\n' +
'"use strict";\n';
var keys = Object.keys(validRanges);
for (var i in keys) {
code += generateFunction(validRanges[keys[i]], keys[i]);
}
return code;
}
function generateFunction(positiveElements, functionName) {
var result = [];
result.push('function ', functionName, '(cp) {\n');
positiveElements.forEach(function(range) {
if (range[0] === range[1]) {
result.push(' if (cp === 0x', range[0], ')');
} else {
result.push(' if (0x', range[0], ' <= cp && cp <= 0x', range[1], ')');
}
result.push(' return true;\n');
});
result.push(' return false;\n}\n');
return result.join('');
}
Binary file not shown.
+229 -995
View File
File diff suppressed because it is too large Load Diff
+2332 -3499
View File
File diff suppressed because it is too large Load Diff
+10 -6
View File
@@ -17,7 +17,8 @@
"preinstall": "node scripts/npm/check-node-modules.js --purge",
"postinstall": "node scripts/npm/copy-npm-shrinkwrap.js",
"commit": "git-cz",
"test-i18n": "jasmine-node i18n/spec"
"test-i18n": "jasmine-node i18n/spec",
"test-i18n-ucd": "jasmine-node i18n/ucd/spec"
},
"devDependencies": {
"angular-benchpress": "0.x.x",
@@ -29,7 +30,7 @@
"commitizen": "^2.3.0",
"cz-conventional-changelog": "1.1.4",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.11.0",
"dgeni-packages": "^0.12.0",
"event-stream": "~3.1.0",
"glob": "^6.0.1",
"grunt": "~0.4.2",
@@ -38,7 +39,7 @@
"grunt-contrib-compress": "~0.12.0",
"grunt-contrib-connect": "~0.8.0",
"grunt-contrib-copy": "~0.6.0",
"grunt-contrib-jshint": "~0.10.0",
"grunt-contrib-jshint": "^1.0.0",
"grunt-ddescribe-iit": "~0.0.1",
"grunt-jasmine-node": "git://github.com/vojtajina/grunt-jasmine-node.git#fix-grunt-exit-code",
"grunt-jscs": "^2.1.0",
@@ -47,7 +48,7 @@
"gulp": "~3.8.0",
"gulp-concat": "^2.4.1",
"gulp-foreach": "0.0.1",
"gulp-jshint": "~1.4.2",
"gulp-jshint": "^2.0.0",
"gulp-rename": "^1.2.0",
"gulp-sourcemaps": "^1.2.2",
"gulp-uglify": "^1.0.1",
@@ -55,7 +56,8 @@
"jasmine-core": "^2.4.0",
"jasmine-node": "^2.0.0",
"jasmine-reporters": "~1.0.1",
"jshint-stylish": "~1.0.0",
"jshint": "^2.9.1",
"jshint-stylish": "^2.1.0",
"karma": "^0.13.19",
"karma-browserstack-launcher": "^0.1.8",
"karma-chrome-launcher": "^0.2.2",
@@ -75,10 +77,12 @@
"q-io": "^1.10.9",
"qq": "^0.3.5",
"rewire": "~2.1.0",
"sax": "^1.1.1",
"semver": "~4.0.3",
"shelljs": "~0.3.0",
"sorted-object": "^1.0.0",
"stringmap": "^0.2.2"
"stringmap": "^0.2.2",
"strip-json-comments": "^2.0.1"
},
"licenses": [
{
+1
View File
@@ -11,6 +11,7 @@ REPOS=(
angular-message-format
angular-messages
angular-mocks
angular-parse-ext
angular-resource
angular-route
angular-sanitize
+2 -1
View File
@@ -1,7 +1,8 @@
{
"extends": "../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
/* auto/injector.js */
"createInjector": false,
+43 -7
View File
@@ -102,6 +102,7 @@
* @ngdoc module
* @name ng
* @module ng
* @installation
* @description
*
* # ng (core module)
@@ -168,7 +169,7 @@ var
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
*/
msie = document.documentMode;
msie = window.document.documentMode;
/**
@@ -978,6 +979,41 @@ function shallowCopy(src, dst) {
* @param {*} o1 Object or value to compare.
* @param {*} o2 Object or value to compare.
* @returns {boolean} True if arguments are equal.
*
* @example
<example module="equalsExample" name="equalsExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form novalidate>
<h3>User 1</h3>
Name: <input type="text" ng-model="user1.name">
Age: <input type="number" ng-model="user1.age">
<h3>User 2</h3>
Name: <input type="text" ng-model="user2.name">
Age: <input type="number" ng-model="user2.age">
<div>
<br/>
<input type="button" value="Compare" ng-click="compare()">
</div>
User 1: <pre>{{user1 | json}}</pre>
User 2: <pre>{{user2 | json}}</pre>
Equal: <pre>{{result}}</pre>
</form>
</div>
</file>
<file name="script.js">
angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) {
$scope.user1 = {};
$scope.user2 = {};
$scope.result;
$scope.compare = function() {
$scope.result = angular.equals($scope.user1, $scope.user2);
};
}]);
</file>
</example>
*/
function equals(o1, o2) {
if (o1 === o2) return true;
@@ -1024,8 +1060,8 @@ var csp = function() {
if (!isDefined(csp.rules)) {
var ngCspElement = (document.querySelector('[ng-csp]') ||
document.querySelector('[data-ng-csp]'));
var ngCspElement = (window.document.querySelector('[ng-csp]') ||
window.document.querySelector('[data-ng-csp]'));
if (ngCspElement) {
var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
@@ -1100,7 +1136,7 @@ var jq = function() {
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
name = el.getAttribute(prefix + 'jq');
break;
}
@@ -1165,7 +1201,7 @@ function toJsonReplacer(key, value) {
val = undefined;
} else if (isWindow(value)) {
val = '$WINDOW';
} else if (value && document === value) {
} else if (value && window.document === value) {
val = '$DOCUMENT';
} else if (isScope(value)) {
val = '$SCOPE';
@@ -1617,11 +1653,11 @@ function bootstrap(element, modules, config) {
element = jqLite(element);
if (element.injector()) {
var tag = (element[0] === document) ? 'document' : startingTag(element);
var tag = (element[0] === window.document) ? 'document' : startingTag(element);
//Encode angle brackets to prevent input from being sanitized to empty string #8683
throw ngMinErr(
'btstrpd',
"App Already Bootstrapped with this Element '{0}'",
"App already bootstrapped with this element '{0}'",
tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
}
+1 -1
View File
@@ -3,4 +3,4 @@
* (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, document, undefined) {
(function(window) {
+3 -3
View File
@@ -1,5 +1,5 @@
jqLite(document).ready(function() {
angularInit(document, bootstrap);
jqLite(window.document).ready(function() {
angularInit(window.document, bootstrap);
});
})(window, document);
})(window);
+2 -1
View File
@@ -57,6 +57,7 @@
/**
* @ngdoc module
* @name auto
* @installation
* @description
*
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
@@ -70,7 +71,7 @@ var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
function extractArgs(fn) {
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
var fnText = Function.prototype.toString.call(fn).replace(STRIP_COMMENTS, ''),
args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
return args;
}
+7 -4
View File
@@ -114,6 +114,9 @@
* - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top
* parent element is reached.
*
* @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See
* https://github.com/angular/angular.js/issues/14251 for more information.
*
* @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery.
* @returns {Object} jQuery object.
*/
@@ -240,7 +243,7 @@ function jqLiteBuildFragment(html, context) {
}
function jqLiteParseHTML(html, context) {
context = context || document;
context = context || window.document;
var parsed;
if ((parsed = SINGLE_TAG_REGEXP.exec(html))) {
@@ -266,7 +269,7 @@ function jqLiteWrapNode(node, wrapper) {
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
var jqLiteContains = Node.prototype.contains || function(arg) {
var jqLiteContains = window.Node.prototype.contains || function(arg) {
// jshint bitwise: false
return !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
@@ -538,8 +541,8 @@ var JQLitePrototype = JQLite.prototype = {
}
// check if document is already loaded
if (document.readyState === 'complete') {
setTimeout(trigger);
if (window.document.readyState === 'complete') {
window.setTimeout(trigger);
} else {
this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
// we can not use jqLite since we are not done loading and jQuery could be loaded later.
+3 -3
View File
@@ -197,9 +197,9 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#decorator
* @module ng
* @param {string} The name of the service to decorate.
* @param {Function} This function will be invoked when the service needs to be
* instantiated and should return the decorated service instance.
* @param {string} name The name of the service to decorate.
* @param {Function} decorFn This function will be invoked when the service needs to be
* instantiated and should return the decorated service instance.
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
+1 -1
View File
@@ -3,4 +3,4 @@
* (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
(function(window, angular) {
+6 -1
View File
@@ -326,6 +326,9 @@ var $AnimateProvider = ['$provide', function($provide) {
* // remove all the animation event listeners listening for `enter`
* $animate.off('enter');
*
* // remove listeners for all animation events from the container element
* $animate.off(container);
*
* // remove all the animation event listeners listening for `enter` on the given element and its children
* $animate.off('enter', container);
*
@@ -334,7 +337,9 @@ var $AnimateProvider = ['$provide', function($provide) {
* $animate.off('enter', container, callback);
* ```
*
* @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...)
* @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move,
* addClass, removeClass, etc...), or the container element. If it is the element, all other
* arguments are ignored.
* @param {DOMElement=} container the container element the event listener was placed on
* @param {Function=} callback the callback function that was registered as the listener
*/
+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() {
+203 -29
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, isFirstChange() }`. 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
@@ -832,6 +846,9 @@
var $compileMinErr = minErr('$compile');
function UNINITIALIZED_VALUE() {}
var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
/**
* @ngdoc provider
* @name $compileProvider
@@ -856,7 +873,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function parseIsolateBindings(scope, directiveName, isController) {
var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*(\w*)\s*$/;
var bindings = {};
var bindings = createMap();
forEach(scope, function(definition, scopeName) {
if (definition in bindingCache) {
@@ -928,11 +945,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);
}
}
@@ -1030,6 +1047,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* See {@link ng.$compile#-bindtocontroller- `bindToController`}.
* - `transclude` `{boolean=}` whether {@link $compile#transclusion content transclusion} is enabled.
* Disabled by default.
* - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to
* this component's controller. The object keys specify the property names under which the required
* controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}.
* - `$...` additional properties to attach to the directive factory function and the controller
* constructor function. (This is used by the component router to annotate)
*
@@ -1075,7 +1095,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
*/
this.component = function registerComponent(name, options) {
var controller = options.controller || noop;
var controller = options.controller || function() {};
function factory($injector) {
function makeInjectable(fn) {
@@ -1089,7 +1109,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
var template = (!options.template && !options.templateUrl ? '' : options.template);
return {
var ddo = {
controller: controller,
controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
template: makeInjectable(template),
@@ -1100,14 +1120,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
restrict: 'E',
require: options.require
};
// Copy annotations (starting with $) over to the DDO
forEach(options, function(val, key) {
if (key.charAt(0) === '$') ddo[key] = val;
});
return ddo;
}
// Copy any annotation properties (starting with $) over to the factory function
// TODO(pete) remove the following `forEach` before we release 1.6.0
// The component-router@0.2.0 looks for the annotations on the controller constructor
// Nothing in Angular looks for annotations on the factory function but we can't remove
// it from 1.5.x yet.
// Copy any annotation properties (starting with $) over to the factory and controller constructor functions
// These could be used by libraries such as the new component router
forEach(options, function(val, key) {
if (key.charAt(0) === '$') {
factory[key] = val;
controller[key] = val;
// Don't try to copy over annotations to named controller
if (isFunction(controller)) controller[key] = val;
}
});
@@ -1207,6 +1240,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',
@@ -1214,7 +1277,37 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
$controller, $rootScope, $sce, $animate, $$sanitizeUri) {
var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = document.createElement('div');
var specialAttrHolder = window.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);
@@ -1515,7 +1608,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (debugInfoEnabled) {
content = ' ' + (directiveName || '') + ': ' + (comment || '') + ' ';
}
return document.createComment(content);
return window.document.createComment(content);
};
return compile;
@@ -1538,7 +1631,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var domNode = $compileNodes[i];
if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
jqLiteWrapNode(domNode, $compileNodes[i] = document.createElement('span'));
jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span'));
}
}
@@ -2080,6 +2173,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:
@@ -2220,7 +2324,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = directive;
}
/* jshint -W021 */
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
/* jshint +W021 */
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
@@ -2284,7 +2390,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
attrs, removeScopeBindingWatches, removeControllerBindingWatches;
attrs, scopeBindingInfo;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -2323,11 +2429,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope,
isolateScope.$$isolateBindings,
newIsolateScopeDirective);
if (removeScopeBindingWatches) {
isolateScope.$on('$destroy', removeScopeBindingWatches);
if (scopeBindingInfo.removeWatches) {
isolateScope.$on('$destroy', scopeBindingInfo.removeWatches);
}
}
@@ -2338,8 +2444,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var bindings = controllerDirective.$$bindings.bindToController;
if (controller.identifier && bindings) {
removeControllerBindingWatches =
controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
} else {
controller.bindingInfo = {};
}
var controllerResult = controller();
@@ -2348,8 +2456,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// from setupControllers
controller.instance = controllerResult;
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
removeControllerBindingWatches && removeControllerBindingWatches();
removeControllerBindingWatches =
controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches();
controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
}
@@ -2362,10 +2470,19 @@ 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.$onChanges)) {
controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
}
if (isFunction(controllerInstance.$onInit)) {
controllerInstance.$onInit();
}
if (isFunction(controllerInstance.$onDestroy)) {
controllerScope.$on('$destroy', function callOnDestroyHook() {
controllerInstance.$onDestroy();
});
}
});
@@ -2402,6 +2519,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) {
@@ -2807,7 +2932,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
switch (type) {
case 'svg':
case 'math':
var wrapper = document.createElement('div');
var wrapper = window.document.createElement('div');
wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
return wrapper.childNodes[0].childNodes;
default:
@@ -2951,7 +3076,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// - remove them from the DOM
// - allow them to still be traversed with .nextSibling
// - allow a single fragment.qSA to fetch all elements being removed
var fragment = document.createDocumentFragment();
var fragment = window.document.createDocumentFragment();
for (i = 0; i < removeCount; i++) {
fragment.appendChild(elementsToRemove[i]);
}
@@ -2997,6 +3122,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// only occurs for isolate scopes and new scopes with controllerAs.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
var initialChanges = {};
var changes;
forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -3011,7 +3138,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
if (isString(value)) {
if (isString(value) || isBoolean(value)) {
var oldValue = destination[scopeName];
recordChanges(scopeName, value, oldValue);
destination[scopeName] = value;
}
});
@@ -3026,6 +3155,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// the value to boolean rather than a string, so we special case this situation
destination[scopeName] = lastValue;
}
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
break;
case '=':
@@ -3081,9 +3211,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentGet = $parse(attrs[attrName]);
destination[scopeName] = parentGet(scope);
initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newParentValue) {
destination[scopeName] = newParentValue;
removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
if (newValue === oldValue) {
// If the new and old values are identical then this is the first time the watch has been triggered
// So instead we use the current value on the destination as the old value
oldValue = destination[scopeName];
}
recordChanges(scopeName, newValue, oldValue);
destination[scopeName] = newValue;
}, parentGet.literal);
removeWatchCollection.push(removeWatch);
@@ -3103,15 +3240,52 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
return removeWatchCollection.length && function removeWatches() {
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
removeWatchCollection[i]();
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] = new SimpleChange(previousValue, currentValue);
}
}
function triggerOnChangesHook() {
destination.$onChanges(changes);
// Now clear the changes so that we schedule onChanges when more changes arrive
changes = undefined;
}
return {
initialChanges: initialChanges,
removeWatches: removeWatchCollection.length && function removeWatches() {
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
removeWatchCollection[i]();
}
}
};
}
}];
}
function SimpleChange(previous, current) {
this.previousValue = previous;
this.currentValue = current;
}
SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
/**
* Converts all accepted directives format into proper directive name.
+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">
+5 -1
View File
@@ -78,7 +78,11 @@ function classDirective(name, selector) {
updateClasses(oldClasses, newClasses);
}
}
oldVal = shallowCopy(newVal);
if (isArray(newVal)) {
oldVal = newVal.map(function(v) { return shallowCopy(v); });
} else {
oldVal = shallowCopy(newVal);
}
}
}
};
+1 -1
View File
@@ -292,7 +292,7 @@ var ngIncludeFillContentDirective = ['$compile',
// support innerHTML, so detect this here and try to generate the contents
// specially.
$element.empty();
$compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
$compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
function namespaceAdaptedClone(clone) {
$element.append(clone);
}, {futureParentElement: $element});
+44 -100
View File
@@ -245,7 +245,7 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
// jshint maxlen: 100
var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
function parseOptionsExpression(optionsExp, selectElement, scope) {
@@ -406,8 +406,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
var optionTemplate = document.createElement('option'),
optGroupTemplate = document.createElement('optgroup');
var optionTemplate = window.document.createElement('option'),
optGroupTemplate = window.document.createElement('optgroup');
function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
@@ -432,7 +432,10 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var options;
var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
// This stores the newly created options before they are appended to the select.
// Since the contents are removed from the fragment when it is appended,
// we only need to create it once.
var listFragment = $document[0].createDocumentFragment();
var renderEmptyOption = function() {
if (!providedEmptyOption) {
@@ -467,7 +470,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
var option = options.getOptionFromViewValue(value);
if (option && !option.disabled) {
if (option) {
// Don't update the option when it is already selected.
// For example, the browser will select the first option by default. In that case,
// most properties are set automatically - except the `selected` attribute, which we
@@ -529,7 +532,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
if (value) {
value.forEach(function(item) {
var option = options.getOptionFromViewValue(item);
if (option && !option.disabled) option.element.selected = true;
if (option) option.element.selected = true;
});
}
};
@@ -581,6 +584,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
emptyOption = jqLite(optionTemplate.cloneNode(false));
}
selectElement.empty();
// We need to do this here to ensure that the options object is defined
// when we first hit it in writeNgOptionsValue
updateOptions();
@@ -590,6 +595,12 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// ------------------------------------------------------------------ //
function addOptionElement(option, parent) {
var optionElement = optionTemplate.cloneNode(false);
parent.appendChild(optionElement);
updateOptionElement(option, optionElement);
}
function updateOptionElement(option, element) {
option.element = element;
@@ -606,133 +617,66 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
if (option.value !== element.value) element.value = option.selectValue;
}
function addOrReuseElement(parent, current, type, templateElement) {
var element;
// Check whether we can reuse the next element
if (current && lowercase(current.nodeName) === type) {
// The next element is the right type so reuse it
element = current;
} else {
// The next element is not the right type so create a new one
element = templateElement.cloneNode(false);
if (!current) {
// There are no more elements so just append it to the select
parent.appendChild(element);
} else {
// The next element is not a group so insert the new one
parent.insertBefore(element, current);
}
}
return element;
}
function removeExcessElements(current) {
var next;
while (current) {
next = current.nextSibling;
jqLiteRemove(current);
current = next;
}
}
function skipEmptyAndUnknownOptions(current) {
var emptyOption_ = emptyOption && emptyOption[0];
var unknownOption_ = unknownOption && unknownOption[0];
// We cannot rely on the extracted empty option being the same as the compiled empty option,
// because the compiled empty option might have been replaced by a comment because
// it had an "element" transclusion directive on it (such as ngIf)
if (emptyOption_ || unknownOption_) {
while (current &&
(current === emptyOption_ ||
current === unknownOption_ ||
current.nodeType === NODE_TYPE_COMMENT ||
(nodeName_(current) === 'option' && current.value === ''))) {
current = current.nextSibling;
}
}
return current;
}
function updateOptions() {
var previousValue = options && selectCtrl.readValue();
// We must remove all current options, but cannot simply set innerHTML = null
// since the providedEmptyOption might have an ngIf on it that inserts comments which we
// must preserve.
// Instead, iterate over the current option elements and remove them or their optgroup
// parents
if (options) {
for (var i = options.items.length - 1; i >= 0; i--) {
var option = options.items[i];
if (option.group) {
jqLiteRemove(option.element.parentNode);
} else {
jqLiteRemove(option.element);
}
}
}
options = ngOptions.getOptions();
var groupMap = {};
var currentElement = selectElement[0].firstChild;
var groupElementMap = {};
// Ensure that the empty option is always there if it was explicitly provided
if (providedEmptyOption) {
selectElement.prepend(emptyOption);
}
currentElement = skipEmptyAndUnknownOptions(currentElement);
options.items.forEach(function updateOption(option) {
var group;
options.items.forEach(function addOption(option) {
var groupElement;
var optionElement;
if (isDefined(option.group)) {
// This option is to live in a group
// See if we have already created this group
group = groupMap[option.group];
groupElement = groupElementMap[option.group];
if (!group) {
if (!groupElement) {
// We have not already created this group
groupElement = addOrReuseElement(selectElement[0],
currentElement,
'optgroup',
optGroupTemplate);
// Move to the next element
currentElement = groupElement.nextSibling;
groupElement = optGroupTemplate.cloneNode(false);
listFragment.appendChild(groupElement);
// Update the label on the group element
groupElement.label = option.group;
// Store it for use later
group = groupMap[option.group] = {
groupElement: groupElement,
currentOptionElement: groupElement.firstChild
};
groupElementMap[option.group] = groupElement;
}
// So now we have a group for this option we add the option to the group
optionElement = addOrReuseElement(group.groupElement,
group.currentOptionElement,
'option',
optionTemplate);
updateOptionElement(option, optionElement);
// Move to the next element
group.currentOptionElement = optionElement.nextSibling;
addOptionElement(option, groupElement);
} else {
// This option is not in a group
optionElement = addOrReuseElement(selectElement[0],
currentElement,
'option',
optionTemplate);
updateOptionElement(option, optionElement);
// Move to the next element
currentElement = optionElement.nextSibling;
addOptionElement(option, listFragment);
}
});
// Now remove all excess options and group
Object.keys(groupMap).forEach(function(key) {
removeExcessElements(groupMap[key].currentOptionElement);
});
removeExcessElements(currentElement);
selectElement[0].appendChild(listFragment);
ngModelCtrl.$render();
+1 -1
View File
@@ -34,7 +34,7 @@ var SelectController =
//
// We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
self.unknownOption = jqLite(document.createElement('option'));
self.unknownOption = jqLite(window.document.createElement('option'));
self.renderUnknownOption = function(val) {
var unknownVal = '? ' + hashKey(val) + ' ?';
self.unknownOption.val(unknownVal);
+4 -2
View File
@@ -93,7 +93,9 @@ function currencyFilter($locale) {
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
* @returns {string} Number rounded to fractionSize and places a “,” after each third digit.
* @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current
* locale (e.g., in the en_US locale it will have "." as the decimal separator and
* include "," group separators after each third digit).
*
* @example
<example module="numberFilterExample">
@@ -323,7 +325,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) {
+32 -2
View File
@@ -555,7 +555,7 @@ function $HttpProvider() {
* That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
* For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
* function will be reflected on the scope and in any templates where the object is data-bound.
* To prevent his, transform functions should have no side-effects.
* To prevent this, transform functions should have no side-effects.
* If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
* </div>
*
@@ -801,6 +801,12 @@ function $HttpProvider() {
* - **headers** `{Object}` Map of strings or functions which return strings representing
* HTTP headers to send to the server. If the return value of a function is null, the
* header will not be sent. Functions accept a config object as an argument.
* - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object.
* To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`.
* The handler will be called in the context of a `$apply` block.
* - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload
* object. To bind events to the XMLHttpRequest object, use `eventHandlers`.
* The handler will be called in the context of a `$apply` block.
* - **xsrfHeaderName** `{string}` Name of HTTP header to populate with the XSRF token.
* - **xsrfCookieName** `{string}` Name of cookie containing the XSRF token.
* - **transformRequest**
@@ -1259,11 +1265,35 @@ function $HttpProvider() {
}
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
config.withCredentials, config.responseType);
config.withCredentials, config.responseType,
createApplyHandlers(config.eventHandlers),
createApplyHandlers(config.uploadEventHandlers));
}
return promise;
function createApplyHandlers(eventHandlers) {
if (eventHandlers) {
var applyHandlers = {};
forEach(eventHandlers, function(eventHandler, key) {
applyHandlers[key] = function(event) {
if (useApplyAsync) {
$rootScope.$applyAsync(callEventHandler);
} else if ($rootScope.$$phase) {
callEventHandler();
} else {
$rootScope.$apply(callEventHandler);
}
function callEventHandler() {
eventHandler(event);
}
};
});
return applyHandlers;
}
}
/**
* Callback registered to $httpBackend():
+9 -1
View File
@@ -54,7 +54,7 @@ function $HttpBackendProvider() {
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
// TODO(vojta): fix the signature
return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();
@@ -114,6 +114,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
xhr.onerror = requestError;
xhr.onabort = requestError;
forEach(eventHandlers, function(value, key) {
xhr.addEventListener(key, value);
});
forEach(uploadEventHandlers, function(value, key) {
xhr.upload.addEventListener(key, value);
});
if (withCredentials) {
xhr.withCredentials = true;
}
+86 -8
View File
@@ -150,7 +150,7 @@ Lexer.prototype = {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
} else if (this.isIdent(ch)) {
} else if (this.isIdentifierStart(this.peekMultichar())) {
this.readIdent();
} else if (this.is(ch, '(){}[].,;:?')) {
this.tokens.push({index: this.index, text: ch});
@@ -194,12 +194,49 @@ Lexer.prototype = {
ch === '\n' || ch === '\v' || ch === '\u00A0');
},
isIdent: function(ch) {
isIdentifierStart: function(ch) {
return this.options.isIdentifierStart ?
this.options.isIdentifierStart(ch, this.codePointAt(ch)) :
this.isValidIdentifierStart(ch);
},
isValidIdentifierStart: function(ch) {
return ('a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' === ch || ch === '$');
},
isIdentifierContinue: function(ch) {
return this.options.isIdentifierContinue ?
this.options.isIdentifierContinue(ch, this.codePointAt(ch)) :
this.isValidIdentifierContinue(ch);
},
isValidIdentifierContinue: function(ch, cp) {
return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch);
},
codePointAt: function(ch) {
if (ch.length === 1) return ch.charCodeAt(0);
/*jshint bitwise: false*/
return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
/*jshint bitwise: true*/
},
peekMultichar: function() {
var ch = this.text.charAt(this.index);
var peek = this.peek();
if (!peek) {
return ch;
}
var cp1 = ch.charCodeAt(0);
var cp2 = peek.charCodeAt(0);
if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) {
return ch + peek;
}
return ch;
},
isExpOperator: function(ch) {
return (ch === '-' || ch === '+' || this.isNumber(ch));
},
@@ -248,12 +285,13 @@ Lexer.prototype = {
readIdent: function() {
var start = this.index;
this.index += this.peekMultichar().length;
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
if (!(this.isIdent(ch) || this.isNumber(ch))) {
var ch = this.peekMultichar();
if (!this.isIdentifierContinue(ch)) {
break;
}
this.index++;
this.index += ch.length;
}
this.tokens.push({
index: start,
@@ -1183,7 +1221,13 @@ ASTCompiler.prototype = {
},
nonComputedMember: function(left, right) {
return left + '.' + right;
var SAFE_IDENTIFIER = /[$_a-zA-Z][$_a-zA-Z0-9]*/;
var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
if (SAFE_IDENTIFIER.test(right)) {
return left + '.' + right;
} else {
return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]';
}
},
computedMember: function(left, right) {
@@ -1746,6 +1790,7 @@ function $ParseProvider() {
'null': null,
'undefined': undefined
};
var identStart, identContinue;
/**
* @ngdoc method
@@ -1762,17 +1807,50 @@ function $ParseProvider() {
literals[literalName] = literalValue;
};
/**
* @ngdoc method
* @name $parseProvider#setIdentifierFns
* @description
*
* Allows defining the set of characters that are allowed in Angular expressions. The function
* `identifierStart` will get called to know if a given character is a valid character to be the
* first character for an identifier. The function `identifierContinue` will get called to know if
* a given character is a valid character to be a follow-up identifier character. The functions
* `identifierStart` and `identifierContinue` will receive as arguments the single character to be
* identifier and the character code point. These arguments will be `string` and `numeric`. Keep in
* mind that the `string` parameter can be two characters long depending on the character
* representation. It is expected for the function to return `true` or `false`, whether that
* character is allowed or not.
*
* Since this function will be called extensivelly, keep the implementation of these functions fast,
* as the performance of these functions have a direct impact on the expressions parsing speed.
*
* @param {function=} identifierStart The function that will decide whether the given character is
* a valid identifier start character.
* @param {function=} identifierContinue The function that will decide whether the given character is
* a valid identifier continue character.
*/
this.setIdentifierFns = function(identifierStart, identifierContinue) {
identStart = identifierStart;
identContinue = identifierContinue;
return this;
};
this.$get = ['$filter', function($filter) {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
expensiveChecks: false,
literals: copy(literals)
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
},
$parseOptionsExpensive = {
csp: noUnsafeEval,
expensiveChecks: true,
literals: copy(literals)
literals: copy(literals),
isIdentifierStart: isFunction(identStart) && identStart,
isIdentifierContinue: isFunction(identContinue) && identContinue
};
var runningChecksEnabled = false;
+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
+1 -1
View File
@@ -6,7 +6,7 @@
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
var urlParsingNode = document.createElement("a");
var urlParsingNode = window.document.createElement("a");
var originUrl = urlResolve(window.location.href);
+2 -1
View File
@@ -2,8 +2,9 @@
"extends": "../../.jshintrc-base",
"maxlen": false, /* ngAnimate docs contain wide tables */
"newcap": false,
"browser": true,
"globals": {
"window": false,
"angular": false,
"noop": false,
+54 -17
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;
@@ -169,7 +174,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
var contains = Node.prototype.contains || function(arg) {
var contains = window.Node.prototype.contains || function(arg) {
// jshint bitwise: false
return this === arg || !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
@@ -194,6 +199,23 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return matches;
}
function filterFromRegistry(list, matchContainer, matchCallback) {
var containerNode = extractElementNode(matchContainer);
return list.filter(function(entry) {
var isMatch = entry.node === containerNode &&
(!matchCallback || entry.callback === matchCallback);
return !isMatch;
});
}
function cleanupEventListeners(phase, element) {
if (phase === 'close' && !element[0].parentNode) {
// If the element is not attached to a parentNode, it has been removed by
// the domOperation, and we can safely remove the event callbacks
$animate.off(element);
}
}
var $animate = {
on: function(event, container, callback) {
var node = extractElementNode(container);
@@ -205,26 +227,33 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// Remove the callback when the element is removed from the DOM
jqLite(container).on('$destroy', function() {
$animate.off(event, container, callback);
var animationDetails = activeAnimationsLookup.get(node);
if (!animationDetails) {
// If there's an animation ongoing, the callback calling code will remove
// the event listeners. If we'd remove here, the callbacks would be removed
// before the animation ends
$animate.off(event, container, callback);
}
});
},
off: function(event, container, callback) {
if (arguments.length === 1 && !angular.isString(arguments[0])) {
container = arguments[0];
for (var eventType in callbackRegistry) {
callbackRegistry[eventType] = filterFromRegistry(callbackRegistry[eventType], container);
}
return;
}
var entries = callbackRegistry[event];
if (!entries) return;
callbackRegistry[event] = arguments.length === 1
? null
: filterFromRegistry(entries, container, callback);
function filterFromRegistry(list, matchContainer, matchCallback) {
var containerNode = extractElementNode(matchContainer);
return list.filter(function(entry) {
var isMatch = entry.node === containerNode &&
(!matchCallback || entry.callback === matchCallback);
return !isMatch;
});
}
},
pin: function(element, parentElement) {
@@ -338,12 +367,14 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
var documentHidden = $document[0].hidden;
// this is a hard disable of all animations for the application or on
// the element itself, therefore there is no need to continue further
// past this point if not enabled
// Animations are also disabled if the document is currently hidden (page is not visible
// to the user), because browsers slow down or do not flush calls to requestAnimationFrame
var skipAnimations = !animationsEnabled || $document[0].hidden || disabledElementsLookup.get(node);
var skipAnimations = !animationsEnabled || documentHidden || disabledElementsLookup.get(node);
var existingAnimation = (!skipAnimations && activeAnimationsLookup.get(node)) || {};
var hasExistingAnimation = !!existingAnimation.state;
@@ -354,7 +385,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
if (skipAnimations) {
// Callbacks should fire even if the document is hidden (regression fix for issue #14120)
if (documentHidden) notifyProgress(runner, event, 'start');
close();
if (documentHidden) notifyProgress(runner, event, 'close');
return runner;
}
@@ -504,6 +538,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
markElementAnimationState(element, RUNNING_STATE);
var realRunner = $$animation(element, event, animationDetails.options);
// this will update the runner's flow-control events based on
// the `realRunner` object.
runner.setHost(realRunner);
notifyProgress(runner, event, 'start', {});
realRunner.done(function(status) {
close(!status);
var animationDetails = activeAnimationsLookup.get(node);
@@ -512,11 +551,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
notifyProgress(runner, event, 'close', {});
});
// this will update the runner's flow-control events based on
// the `realRunner` object.
runner.setHost(realRunner);
notifyProgress(runner, event, 'start', {});
});
return runner;
@@ -533,7 +567,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
forEach(callbacks, function(callback) {
callback(element, phase, data);
});
cleanupEventListeners(phase, element);
});
} else {
cleanupEventListeners(phase, element);
}
});
runner.progress(event, phase, data);
+1 -1
View File
@@ -130,7 +130,7 @@
* <div ng-show="bool" class="fade">
* Show and hide me
* </div>
* <button ng-click="bool=true">Toggle</button>
* <button ng-click="bool=!bool">Toggle</button>
*
* <style>
* .fade.ng-hide {
+10 -3
View File
@@ -16,7 +16,7 @@
*
* For ngAria to do its magic, simply include the module `ngAria` as a dependency. The following
* directives are supported:
* `ngModel`, `ngChecked`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
* `ngModel`, `ngChecked`, `ngReadonly`, `ngRequired`, `ngValue`, `ngDisabled`, `ngShow`, `ngHide`, `ngClick`,
* `ngDblClick`, and `ngMessages`.
*
* Below is a more detailed breakdown of the attributes handled by ngAria:
@@ -25,8 +25,9 @@
* |---------------------------------------------|----------------------------------------------------------------------------------------|
* | {@link ng.directive:ngModel ngModel} | aria-checked, aria-valuemin, aria-valuemax, aria-valuenow, aria-invalid, aria-required, input roles |
* | {@link ng.directive:ngDisabled ngDisabled} | aria-disabled |
* | {@link ng.directive:ngRequired ngRequired} | aria-required |
* | {@link ng.directive:ngChecked ngChecked} | aria-checked |
* | {@link ng.directive:ngRequired ngRequired} | aria-required
* | {@link ng.directive:ngChecked ngChecked} | aria-checked
* | {@link ng.directive:ngReadonly ngReadonly} | aria-readonly ||
* | {@link ng.directive:ngValue ngValue} | aria-checked |
* | {@link ng.directive:ngShow ngShow} | aria-hidden |
* | {@link ng.directive:ngHide ngHide} | aria-hidden |
@@ -91,6 +92,7 @@ function $AriaProvider() {
var config = {
ariaHidden: true,
ariaChecked: true,
ariaReadonly: true,
ariaDisabled: true,
ariaRequired: true,
ariaInvalid: true,
@@ -108,6 +110,7 @@ function $AriaProvider() {
*
* - **ariaHidden** `{boolean}` Enables/disables aria-hidden tags
* - **ariaChecked** `{boolean}` Enables/disables aria-checked tags
* - **ariaReadonly** `{boolean}` Enables/disables aria-readonly tags
* - **ariaDisabled** `{boolean}` Enables/disables aria-disabled tags
* - **ariaRequired** `{boolean}` Enables/disables aria-required tags
* - **ariaInvalid** `{boolean}` Enables/disables aria-invalid tags
@@ -170,6 +173,7 @@ function $AriaProvider() {
* The full list of directives that interface with ngAria:
* * **ngModel**
* * **ngChecked**
* * **ngReadonly**
* * **ngRequired**
* * **ngDisabled**
* * **ngValue**
@@ -209,6 +213,9 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
.directive('ngChecked', ['$aria', function($aria) {
return $aria.$$watchExpr('ngChecked', 'aria-checked', nodeBlackList, false);
}])
.directive('ngReadonly', ['$aria', function($aria) {
return $aria.$$watchExpr('ngReadonly', 'aria-readonly', nodeBlackList, false);
}])
.directive('ngRequired', ['$aria', function($aria) {
return $aria.$$watchExpr('ngRequired', 'aria-required', nodeBlackList, false);
}])
+31 -3
View File
@@ -1,8 +1,36 @@
/**
* @ngdoc module
* @name ngComponentRouter
* @installation
* ## Installation
*
* Currently, the **Component Router** module must be installed via `npm`, it is not yet available
* on Bower or the Google CDN.
*
* ```bash
* npm install @angular/router --save
* ```
*
* Include `angular_1_router.js` in your HTML:
* ```html
* <script src="/node_modules/@angular/router/angular1/angular_1_router.js"></script>
*```
*
* You also need to include ES6 shims to support running on Internet Explorer:
* ```html
* <!-- IE required polyfills, in this exact order -->
* <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.33.3/es6-shim.min.js"></script>
* <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.20/system-polyfills.js"></script>
* <script src="https://npmcdn.com/angular2/es6/dev/src/testing/shims_for_IE.js"></script>
* ```
*
* Then load the module in your application by adding it as a dependent module:
*
* ```js
* angular.module('app', ['ngComponentRouter']);
* ```
*
* @description
* The new Angular Router
*/
/**
@@ -41,7 +69,7 @@
*
* There is only one instance of this type in a Component Router application injectable as the
* {@link $rootRouter} service. This **Router** is associate with the **Top Level Component**
* ({@link $routerRootComponent}). It acts as the connection betweent he **Routers** and the **Location**.
* ({@link $routerRootComponent}). It acts as the connection between the **Routers** and the **Location**.
*/
/**
@@ -62,7 +90,7 @@
* @name RouteDefinition
* @description
*
* Each item in a the **RouteConfig** for a **Routing Component** is an instance of
* Each item in the **RouteConfig** for a **Routing Component** is an instance of
* this type. It can have the following properties:
*
* * `path` or (`regex` and `serializer) - defines how to recognize and generate this route
+3 -2
View File
@@ -1,7 +1,8 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false
}
}
}
+3 -2
View File
@@ -2,9 +2,10 @@
"extends": "../../.jshintrc-base",
"bitwise": false, /* locale files use bitwise operators */
"maxlen": false, /* locale files are generated from a 3rd party library that has long lines */
"browser": true,
"globals": {
"window": false,
"angular": false
},
"-W041": false
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"goog": false // see src/module_closure.prefix
}
+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) {
+3 -2
View File
@@ -1,9 +1,10 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"expect": false,
"jQuery": false
}
}
}
+50 -18
View File
@@ -1321,12 +1321,15 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
}
// TODO(vojta): change params to: method, url, data, headers, callback
function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType) {
function $httpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
xhr.$$events = eventHandlers;
xhr.upload.$$events = uploadEventHandlers;
function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
? data
@@ -1416,12 +1419,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* order to change how a matched request is handled.
*
* - respond
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}`
* ```js
* {function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}
* ```
* The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
* return an array containing response status (number), response data (Array|Object|string),
* response headers (Object), and the text for the status (string). The respond method returns
* the `requestHandler` object for possible overrides.
*/
$httpBackend.when = function(method, url, data, headers, keys) {
var definition = new MockHttpExpectation(method, url, data, headers, keys),
@@ -1606,12 +1611,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* order to change how a matched request is handled.
*
* - respond
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}`
* ```
* { function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}
* ```
* The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
* return an array containing response status (number), response data (Array|Object|string),
* response headers (Object), and the text for the status (string). The respond method returns
* the `requestHandler` object for possible overrides.
*/
$httpBackend.expect = function(method, url, data, headers, keys) {
var expectation = new MockHttpExpectation(method, url, data, headers, keys),
@@ -2005,6 +2012,20 @@ function MockXhr() {
};
this.abort = angular.noop;
// This section simulates the events on a real XHR object (and the upload object)
// When we are testing $httpBackend (inside the angular project) we make partial use of this
// but store the events directly ourselves on `$$events`, instead of going through the `addEventListener`
this.$$events = {};
this.addEventListener = function(name, listener) {
if (angular.isUndefined(this.$$events[name])) this.$$events[name] = [];
this.$$events[name].push(listener);
};
this.upload = {
$$events: {},
addEventListener: this.addEventListener
};
}
@@ -2174,8 +2195,8 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
* A service that can be used to create instances of component controllers.
* <div class="alert alert-info">
* Be aware that the controller will be instantiated and attached to the scope as specified in
* the component definition object. That means that you must always provide a `$scope` object
* in the `locals` param.
* the component definition object. If you do not provide a `$scope` object in the `locals` param
* then the helper will create a new isolated scope as a child of `$rootScope`.
* </div>
* @param {string} componentName the name of the component whose controller we want to instantiate
* @param {Object} locals Injection locals for Controller.
@@ -2185,7 +2206,7 @@ angular.mock.$ControllerDecorator = ['$delegate', function($delegate) {
* @return {Object} Instance of requested controller.
*/
angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compileProvider) {
this.$get = ['$controller','$injector', function($controller,$injector) {
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
var directives = $injector.get(componentName + 'Directive');
@@ -2203,6 +2224,9 @@ angular.mock.$ComponentControllerProvider = ['$compileProvider', function($compi
}
// get the info of the component
var directiveInfo = candidateDirectives[0];
// create a scope if needed
locals = locals || {};
locals.$scope = locals.$scope || $rootScope.$new(true);
return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs);
};
}];
@@ -2326,11 +2350,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* `respond` or `passThrough` again in order to change how a matched request is handled.
*
* - respond
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}`
* ```
* { function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers, params)}
* ```
* The respond method takes a set of static data to be returned or a function that can return
* an array containing response status (number), response data (string), response headers
* (Object), and the text for the status (string).
* an array containing response status (number), response data (Array|Object|string), response
* headers (Object), and the text for the status (string).
* - passThrough `{function()}` Any request matching a backend definition with
* `passThrough` handler will be passed through to the real backend (an XHR request will be made
* to the server.)
@@ -2892,6 +2918,12 @@ angular.mock.$RootScopeDecorator = ['$delegate', function($delegate) {
window.inject = angular.mock.inject = function() {
var blockFns = Array.prototype.slice.call(arguments, 0);
var errorForStack = new Error('Declaration Location');
// IE10+ and PhanthomJS do not set stack trace information, until the error is thrown
if (!errorForStack.stack) {
try {
throw errorForStack;
} catch (e) {}
}
return wasInjectorCreated() ? workFn.call(currentSpec) : workFn;
/////////////////////
function workFn() {
+45
View File
@@ -0,0 +1,45 @@
'use strict';
/**
* @ngdoc module
* @name ngParseExt
* @packageName angular-parse-ext
* @description
*
* # ngParseExt
*
* The `ngParseExt` module provides functionality to allow Unicode characters in
* identifiers inside Angular expressions.
*
*
* <div doc-module-components="ngParseExt"></div>
*
* This module allows the usage of any identifier that follows ES6 identifier naming convention
* to be used as an identifier in an Angular expression. ES6 delegates some of the identifier
* rules definition to Unicode, this module uses ES6 and Unicode 8.0 identifiers convention.
*
*/
/* global angularParseExtModule: true,
IDS_Y,
IDC_Y
*/
function isValidIdentifierStart(ch, cp) {
return ch === '$' ||
ch === '_' ||
IDS_Y(cp);
}
function isValidIdentifierContinue(ch, cp) {
return ch === '$' ||
ch === '_' ||
cp === 0x200C || // <ZWNJ>
cp === 0x200D || // <ZWJ>
IDC_Y(cp);
}
angular.module('ngParseExt', [])
.config(['$parseProvider', function($parseProvider) {
$parseProvider.setIdentifierFns(isValidIdentifierStart, isValidIdentifierContinue);
}]);
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,7 +1,8 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false
}
}
}
+3 -2
View File
@@ -1,8 +1,9 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"ngRouteModule": false
}
}
}
+7
View File
@@ -26,6 +26,13 @@ ngRouteModule.directive('ngView', ngViewFillContentFactory);
*
* The enter and leave animation occur concurrently.
*
* @knownIssue If `ngView` is contained in an asynchronously loaded template (e.g. in another
* directive's templateUrl or in a template loaded using `ngInclude`), then you need to
* make sure that `$route` is instantiated in time to capture the initial
* `$locationChangeStart` event and load the appropriate view. One way to achieve this
* is to have it as a dependency in a `.run` block:
* `myModule.run(['$route', function() {}]);`
*
* @scope
* @priority 400
* @param {string=} onload Expression to evaluate whenever the view updates.
+1 -5
View File
@@ -17,11 +17,7 @@
*/
/* global -ngRouteModule */
var ngRouteModule = angular.module('ngRoute', ['ng']).
provider('$route', $RouteProvider).
// Ensure `$route` will be instantiated in time to capture the initial
// `$locationChangeSuccess` event. This is necessary in case `ngView` is
// included in an asynchronously loaded template.
run(['$route', angular.noop]),
provider('$route', $RouteProvider),
$routeMinErr = angular.$$minErr('ngRoute');
/**
+3 -2
View File
@@ -1,8 +1,9 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"htmlSanitizeWriter": false
}
}
}
+2 -2
View File
@@ -344,7 +344,7 @@ function htmlParser(html, handler) {
mXSSAttempts--;
// strip custom-namespaced attributes on IE<=11
if (document.documentMode <= 11) {
if (window.document.documentMode) {
stripCustomNsAttrs(inertBodyElement);
}
html = inertBodyElement.innerHTML; //trigger mXSS
@@ -484,7 +484,7 @@ function htmlSanitizeWriter(buf, uriValidator) {
* @param node Root element to process
*/
function stripCustomNsAttrs(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.nodeType === window.Node.ELEMENT_NODE) {
var attrs = node.attributes;
for (var i = 0, l = attrs.length; i < l; i++) {
var attrNode = attrs[i];
+3 -2
View File
@@ -1,7 +1,8 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"includes": false,
"asyncForEach": false,
@@ -19,4 +20,4 @@
"$runner": false,
"callerFile": false
}
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ angular.scenario.matcher = angular.scenario.matcher || function(name, fn) {
*/
angular.scenario.setUpAndRun = function(config) {
var href = window.location.href;
var body = _jQuery(document.body);
var body = _jQuery(window.document.body);
var output = [];
var objModel = new angular.scenario.ObjectModel($runner);
+5 -5
View File
@@ -5,7 +5,7 @@
(function(previousOnLoad) {
var prefix = (function() {
var filename = /(.*\/)angular-bootstrap.js(#(.*))?/;
var scripts = document.getElementsByTagName("script");
var scripts = window.document.getElementsByTagName("script");
for (var j = 0; j < scripts.length; j++) {
var src = scripts[j].src;
if (src && src.match(filename)) {
@@ -16,11 +16,11 @@
})();
function addScript(path) {
document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
window.document.write('<script type="text/javascript" src="' + prefix + path + '"></script>');
}
function addCSS(path) {
document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
window.document.write('<link rel="stylesheet" type="text/css" href="' + prefix + path + '"/>');
}
window.onload = function() {
@@ -32,7 +32,7 @@
addCSS("../../css/angular-scenario.css");
addScript("../../lib/jquery/jquery.js");
document.write(
window.document.write(
'<script type="text/javascript">' +
'var _jQuery = jQuery.noConflict(true);' +
'</script>'
@@ -54,7 +54,7 @@
addScript("output/Xml.js");
// Create the runner (which also sets up the global API)
document.write(
window.document.write(
'<script type="text/javascript">' +
' var $runner = new angular.scenario.Runner(window);' +
'</script>');
+1 -1
View File
@@ -3,5 +3,5 @@
* (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, document){
(function(window){
var _jQuery = window.jQuery.noConflict(true);
+3 -3
View File
@@ -2,7 +2,7 @@ bindJQuery();
publishExternalAPI(angular);
var $runner = new angular.scenario.Runner(window),
scripts = document.getElementsByTagName('script'),
scripts = window.document.getElementsByTagName('script'),
script = scripts[scripts.length - 1],
config = {};
@@ -14,9 +14,9 @@ angular.forEach(script.attributes, function(attr) {
});
if (config.autotest) {
JQLite(document).ready(function() {
JQLite(window.document).ready(function() {
angular.scenario.setUpAndRun(config);
});
}
})(window, document);
})(window);
+8 -8
View File
@@ -61,7 +61,7 @@
evnt = new TransitionEvent(eventType, eventData);
}
catch (e) {
evnt = document.createEvent('TransitionEvent');
evnt = window.document.createEvent('TransitionEvent');
evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0);
}
}
@@ -74,14 +74,14 @@
evnt = new AnimationEvent(eventType, eventData);
}
catch (e) {
evnt = document.createEvent('AnimationEvent');
evnt = window.document.createEvent('AnimationEvent');
evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0);
}
}
} else if (/touch/.test(eventType) && supportsTouchEvents()) {
evnt = createTouchEvent(element, eventType, x, y);
} else {
evnt = document.createEvent('MouseEvents');
evnt = window.document.createEvent('MouseEvents');
x = x || 0;
y = y || 0;
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
@@ -120,12 +120,12 @@
if ('_cached' in supportsTouchEvents) {
return supportsTouchEvents._cached;
}
if (!document.createTouch || !document.createTouchList) {
if (!window.document.createTouch || !window.document.createTouchList) {
supportsTouchEvents._cached = false;
return false;
}
try {
document.createEvent('TouchEvent');
window.document.createEvent('TouchEvent');
} catch (e) {
supportsTouchEvents._cached = false;
return false;
@@ -135,12 +135,12 @@
}
function createTouchEvent(element, eventType, x, y) {
var evnt = new Event(eventType);
var evnt = new window.Event(eventType);
x = x || 0;
y = y || 0;
var touch = document.createTouch(window, element, Date.now(), x, y, x, y);
var touches = document.createTouchList(touch);
var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y);
var touches = window.document.createTouchList(touch);
evnt.touches = touches;
+1 -1
View File
@@ -199,7 +199,7 @@ angular.scenario.dsl('binding', function() {
*/
angular.scenario.dsl('input', function() {
var chain = {};
var supportInputEvent = 'oninput' in document.createElement('div') && !(msie && msie <= 11);
var supportInputEvent = 'oninput' in window.document.createElement('div') && !msie;
chain.enter = function(value, event) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'",
+3 -2
View File
@@ -1,8 +1,9 @@
{
"extends": "../../.jshintrc-base",
"browser": true,
"globals": {
"window": false,
"angular": false,
"ngTouch": false
}
}
}
+6
View File
@@ -241,6 +241,12 @@ describe('injector', function() {
expect($f_n0.$inject).toEqual(['$a_']);
});
it('should handle functions with overridden toString', function() {
function fn(a) {}
fn.toString = function() { return 'fn'; };
expect(annotate(fn)).toEqual(['a']);
expect(fn.$inject).toEqual(['a']);
});
it('should throw on non function arg', function() {
expect(function() {
+58 -36
View File
@@ -131,25 +131,38 @@ beforeEach(function() {
return {
compare: function(actual) {
if (arguments.length > 1) {
throw new Error('toHaveBeenCalledOnce does not take arguments, use toHaveBeenCalledWith');
throw new Error('`toHaveBeenCalledOnce` does not take arguments, ' +
'use `toHaveBeenCalledOnceWith`');
}
if (!jasmine.isSpy(actual)) {
throw new Error('Expected a spy, but got ' + jasmine.pp(actual) + '.');
}
var count = actual.calls.count();
var pass = count === 1;
var message = function() {
var msg = 'Expected spy ' + actual.identity() + ' to have been called once, but was ',
count = this.actual.calls.count();
return [
count === 0 ? msg + 'never called.' :
msg + 'called ' + count + ' times.',
msg.replace('to have', 'not to have') + 'called once.'
];
var msg = 'Expected spy ' + actual.and.identity() + (pass ? ' not ' : ' ') +
'to have been called once, but ';
switch (count) {
case 0:
msg += 'it was never called.';
break;
case 1:
msg += 'it was called once.';
break;
default:
msg += 'it was called ' + count + ' times.';
break;
}
return msg;
};
return {
pass: actual.calls.count() == 1,
pass: pass,
message: message
};
}
@@ -158,43 +171,52 @@ beforeEach(function() {
toHaveBeenCalledOnceWith: function(util, customEqualityTesters) {
return {
compare: function(actual) {
var expectedArgs = Array.prototype.slice.call(arguments, 1);
compare: generateCompare(false),
negativeCompare: generateCompare(true)
};
function generateCompare(isNot) {
return function(actual) {
if (!jasmine.isSpy(actual)) {
throw new Error('Expected a spy, but got ' + jasmine.pp(actual) + '.');
}
var message = function() {
if (actual.calls.count() != 1) {
if (actual.calls.count() === 0) {
return [
'Expected spy ' + actual.identity() + ' to have been called once with ' +
jasmine.pp(expectedArgs) + ' but it was never called.',
'Expected spy ' + actual.identity() + ' not to have been called with ' +
jasmine.pp(expectedArgs) + ' but it was.'
];
}
return [
'Expected spy ' + actual.identity() + ' to have been called once with ' +
jasmine.pp(expectedArgs) + ' but it was called ' + actual.calls.count() + ' times.',
'Expected spy ' + actual.identity() + ' not to have been called once with ' +
jasmine.pp(expectedArgs) + ' but it was.'
];
var expectedArgs = Array.prototype.slice.call(arguments, 1);
var actualCount = actual.calls.count();
var actualArgs = actualCount && actual.calls.argsFor(0);
var pass = (actualCount === 1) && util.equals(actualArgs, expectedArgs);
if (isNot) pass = !pass;
var message = function() {
var msg = 'Expected spy ' + actual.and.identity() + (isNot ? ' not ' : ' ') +
'to have been called once with ' + jasmine.pp(expectedArgs) + ', but ';
if (isNot) {
msg += 'it was.';
} else {
return [
'Expected spy ' + actual.identity() + ' to have been called once with ' +
jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(actual.calls.argsFor(0)),
'Expected spy ' + actual.identity() + ' not to have been called once with ' +
jasmine.pp(expectedArgs) + ' but was called with ' + jasmine.pp(actual.calls.argsFor(0))
];
switch (actualCount) {
case 0:
msg += 'it was never called.';
break;
case 1:
msg += 'it was called with ' + jasmine.pp(actualArgs) + '.';
break;
default:
msg += 'it was called ' + actualCount + ' times.';
break;
}
}
return msg;
};
return {
pass: actual.calls.count() === 1 && util.equals(actual.calls.argsFor(0), expectedArgs),
pass: pass,
message: message
};
}
};
};
}
},
toBeOneOf: function() {
+2 -2
View File
@@ -968,7 +968,7 @@ describe('jqLite', function() {
describe('text', function() {
it('should return null on empty', function() {
it('should return `""` on empty', function() {
expect(jqLite().length).toEqual(0);
expect(jqLite().text()).toEqual('');
});
@@ -1036,7 +1036,7 @@ describe('jqLite', function() {
describe('html', function() {
it('should return null on empty', function() {
it('should return `undefined` on empty', function() {
expect(jqLite().length).toEqual(0);
expect(jqLite().html()).toEqual(undefined);
});
+3 -2
View File
@@ -1,3 +1,4 @@
// When runnint the modules test, then the page should not be bootstrapped.
window.name = "NG_DEFER_BOOTSTRAP!";
'use strict';
// When running the modules test, then the page should not be bootstrapped.
window.name = "NG_DEFER_BOOTSTRAP!";
+6
View File
@@ -88,6 +88,9 @@ describe('$anchorScroll', function() {
return function($window) {
forEach(elmSpy, function(spy, id) {
var count = map[id] || 0;
// TODO(gkalpak): `toHaveBeenCalledTimes()` works correctly with 0 since
// https://github.com/jasmine/jasmine/commit/342f0eb9a38194ecb8559e7df872c72afc0fe52e
// Fix when we upgrade to a version that contains the fix.
if (count > 0) {
expect(spy).toHaveBeenCalledTimes(count);
} else {
@@ -386,6 +389,9 @@ describe('$anchorScroll', function() {
return function($rootScope, $window) {
inject(expectScrollingTo(identifierCountMap));
// TODO(gkalpak): `toHaveBeenCalledTimes()` works correctly with 0 since
// https://github.com/jasmine/jasmine/commit/342f0eb9a38194ecb8559e7df872c72afc0fe52e
// Fix when we upgrade to a version that contains the fix.
if (list.length > 0) {
expect($window.scrollBy).toHaveBeenCalledTimes(list.length);
} else {
+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 = {history: 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() {
+558 -31
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');
@@ -2711,6 +2711,25 @@ describe('$compile', function() {
expect(checkedVal).toEqual(true);
});
});
it('should handle updates to @ bindings on BOOLEAN attributes', function() {
var componentScope;
module(function($compileProvider) {
$compileProvider.directive('test', function() {
return {
scope: {checked: '@'},
link: function(scope, element, attrs) {
componentScope = scope;
attrs.$set('checked', true);
}
};
});
});
inject(function($compile, $rootScope) {
$compile('<test></test>')($rootScope);
expect(componentScope.checked).toBe(true);
});
});
});
@@ -3514,6 +3533,507 @@ 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);
expect(log).toEqual([
{
prop1: jasmine.objectContaining({currentValue: undefined}),
prop2: jasmine.objectContaining({currentValue: undefined}),
attr: jasmine.objectContaining({currentValue: ''})
}
]);
// Clear the initial changes from the log
log = [];
// 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: jasmine.objectContaining({currentValue: 42}),
prop2: jasmine.objectContaining({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: jasmine.objectContaining({previousValue: 42, currentValue: 17}),
prop2: jasmine.objectContaining({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: jasmine.objectContaining({previousValue: '', currentValue: '22'})
}
]);
});
});
it('should trigger `$onChanges` even if the inner value already equals the new outer value', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop1': '<' }
});
module('my');
inject(function($compile, $rootScope) {
element = $compile('<c1 prop1="val"></c1>')($rootScope);
$rootScope.$apply('val = 1');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: undefined, currentValue: 1})});
element.isolateScope().$ctrl.prop1 = 2;
$rootScope.$apply('val = 2');
expect(log.pop()).toEqual({prop1: jasmine.objectContaining({previousValue: 1, currentValue: 2})});
});
});
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; });
expect(log).toEqual([{prop: jasmine.objectContaining({currentValue: undefined})}]);
// Clear the initial values from the log
log = [];
// 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: jasmine.objectContaining({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: jasmine.objectContaining({previousValue: 126, currentValue: 21})}]);
});
});
it('should trigger an initial onChanges call for each binding with the `isFirstChange()` returning true', function() {
var log = [];
function TestController() { }
TestController.prototype.$onChanges = function(change) { log.push(change); };
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop': '<', attr: '@' }
});
module('my');
inject(function($compile, $rootScope) {
$rootScope.$apply('a = 7');
element = $compile('<c1 prop="a" attr="{{a}}"></c1>')($rootScope);
expect(log).toEqual([
{
prop: jasmine.objectContaining({currentValue: 7}),
attr: jasmine.objectContaining({currentValue: '7'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(true);
expect(log[0].attr.isFirstChange()).toEqual(true);
log = [];
$rootScope.$apply('a = 9');
expect(log).toEqual([
{
prop: jasmine.objectContaining({previousValue: 7, currentValue: 9}),
attr: jasmine.objectContaining({previousValue: '7', currentValue: '9'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(false);
expect(log[0].attr.isFirstChange()).toEqual(false);
});
});
it('should trigger an initial onChanges call for each binding even if the hook is defined in the constructor', function() {
var log = [];
function TestController() {
this.$onChanges = function(change) { log.push(change); };
}
angular.module('my', [])
.component('c1', {
controller: TestController,
bindings: { 'prop': '<', attr: '@' }
});
module('my');
inject(function($compile, $rootScope) {
$rootScope.$apply('a = 7');
element = $compile('<c1 prop="a" attr="{{a}}"></c1>')($rootScope);
expect(log).toEqual([
{
prop: jasmine.objectContaining({currentValue: 7}),
attr: jasmine.objectContaining({currentValue: '7'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(true);
expect(log[0].attr.isFirstChange()).toEqual(true);
log = [];
$rootScope.$apply('a = 10');
expect(log).toEqual([
{
prop: jasmine.objectContaining({previousValue: 7, currentValue: 10}),
attr: jasmine.objectContaining({previousValue: '7', currentValue: '10'})
}
]);
expect(log[0].prop.isFirstChange()).toEqual(false);
expect(log[0].attr.isFirstChange()).toEqual(false);
});
});
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);
// Clear out initial changes
log = [];
// Update val to trigger the onChanges
$rootScope.$apply('val1 = 42; val2 = 17');
expect(log).toEqual([
['TestController1', {prop: jasmine.objectContaining({currentValue: 42})}],
['TestController2', {prop: jasmine.objectContaining({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() {
this.prop1 = 0;
}
OuterController.prototype.$onChanges = function(change) {
log.push(['OuterController', change]);
// Make a change to the inner component
this.b = this.prop1 * 2;
};
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);
// Clear out initial changes
log = [];
// Update val to trigger the onChanges
$rootScope.$apply('a = 42');
expect(log).toEqual([
['OuterController', {prop1: jasmine.objectContaining({currentValue: 42})}],
['InnerController', {prop2: jasmine.objectContaining({currentValue: 84})}]
]);
});
});
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 +5843,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) {
@@ -10011,15 +10505,15 @@ describe('$compile', function() {
}));
});
it('should add additional annotations to the controller constructor', function() {
var myModule = angular.module('my', []).component('myComponent', {
it('should expose additional annotations on the directive definition object', function() {
angular.module('my', []).component('myComponent', {
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0].controller).toEqual(jasmine.objectContaining({
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
$canActivate: 'canActivate',
$routeConfig: 'routeConfig',
$customAnnotation: 'XXX'
@@ -10027,6 +10521,39 @@ describe('$compile', function() {
});
});
it('should support custom annotations if the controller is named', function() {
angular.module('my', []).component('myComponent', {
$customAnnotation: 'XXX',
controller: 'SomeNamedController'
});
module('my');
inject(function(myComponentDirective) {
expect(myComponentDirective[0]).toEqual(jasmine.objectContaining({
$customAnnotation: 'XXX'
}));
});
});
it('should provide a new empty controller if none is specified', function() {
angular.
module('my', []).
component('myComponent1', {$customAnnotation1: 'XXX'}).
component('myComponent2', {$customAnnotation2: 'YYY'});
module('my');
inject(function(myComponent1Directive, myComponent2Directive) {
var ctrl1 = myComponent1Directive[0].controller;
var ctrl2 = myComponent2Directive[0].controller;
expect(ctrl1).not.toBe(ctrl2);
expect(ctrl1.$customAnnotation1).toBe('XXX');
expect(ctrl1.$customAnnotation2).toBeUndefined();
expect(ctrl2.$customAnnotation1).toBeUndefined();
expect(ctrl2.$customAnnotation2).toBe('YYY');
});
});
it('should return ddo with reasonable defaults', function() {
angular.module('my', []).component('myComponent', {});
module('my');
+16
View File
@@ -409,6 +409,22 @@ describe('ngClass', function() {
expect(e2.hasClass('even')).toBeTruthy();
expect(e2.hasClass('odd')).toBeFalsy();
}));
it('should support mixed array/object variable with a mutating object',
inject(function($rootScope, $compile) {
element = $compile('<div ng-class="classVar"></div>')($rootScope);
$rootScope.classVar = [{orange: true}];
$rootScope.$digest();
expect(element).toHaveClass('orange');
$rootScope.classVar[0].orange = false;
$rootScope.$digest();
expect(element).not.toHaveClass('orange');
})
);
});
describe('ngClass animations', function() {
+21 -44
View File
@@ -771,7 +771,7 @@ describe('ngOptions', function() {
});
it('should not select disabled options when model changes', function() {
it('should select disabled options when model changes', function() {
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
@@ -792,11 +792,14 @@ describe('ngOptions', function() {
scope.$apply('selected = 1');
options = element.find('option');
expect(element.val()).toEqualUnknownValue('?');
expect(options.length).toEqual(5);
expect(options.eq(0).prop('selected')).toEqual(true);
// jQuery returns null for val() when the option is disabled, see
// https://bugs.jquery.com/ticket/13097
expect(element[0].value).toBe('number:1');
expect(options.length).toEqual(4);
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(4).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
});
@@ -816,11 +819,14 @@ describe('ngOptions', function() {
scope.$apply('selected = 1');
var options = element.find('option');
expect(element.val()).toEqualUnknownValue('?');
expect(options.length).toEqual(5);
expect(options.eq(0).prop('selected')).toEqual(true);
// jQuery returns null for val() when the option is disabled, see
// https://bugs.jquery.com/ticket/13097
expect(element[0].value).toBe('number:1');
expect(options.length).toEqual(4);
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(4).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
// Now enable that option
scope.$apply(function() {
@@ -861,7 +867,7 @@ describe('ngOptions', function() {
});
it('should not select disabled options when model changes', function() {
it('should select disabled options when model changes', function() {
scope.options = [
{ name: 'a', value: 0 },
{ name: 'b', value: 1, unavailable: true },
@@ -886,14 +892,14 @@ describe('ngOptions', function() {
scope.$apply('selected = [1,3]');
options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(true);
// Now only select the disabled option
scope.$apply('selected = [1]');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
});
@@ -917,7 +923,7 @@ describe('ngOptions', function() {
var options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(true);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(false);
@@ -1940,6 +1946,8 @@ describe('ngOptions', function() {
scope.options[1].unavailable = false;
});
options = element.find('option');
expect(scope.options[1].unavailable).toEqual(false);
expect(options.eq(1).prop('disabled')).toEqual(false);
});
@@ -2535,37 +2543,6 @@ describe('ngOptions', function() {
expect(element.find('option')[1].selected).toBeTruthy();
});
it('should not write disabled selections from model', function() {
scope.selected = [30];
scope.options = [
{ name: 'white', value: '#FFFFFF' },
{ name: 'one', value: 1, unavailable: true },
{ name: 'notTrue', value: false },
{ name: 'thirty', value: 30, unavailable: false }
];
createSelect({
'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
'ng-model': 'selected',
'multiple': true
});
var options = element.find('option');
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(true);
scope.$apply(function() {
scope.selected.push(1);
});
expect(options.eq(0).prop('selected')).toEqual(false);
expect(options.eq(1).prop('selected')).toEqual(false);
expect(options.eq(2).prop('selected')).toEqual(false);
expect(options.eq(3).prop('selected')).toEqual(true);
});
it('should update model on change', function() {
createMultiSelect();
+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');
+11
View File
@@ -241,6 +241,17 @@ describe('$httpBackend', function() {
});
it('should set up event listeners', function() {
var progressFn = function() {};
var uploadProgressFn = function() {};
$backend('GET', '/url', null, callback, {}, null, null, null,
{progress: progressFn}, {progress: uploadProgressFn});
xhr = MockXhr.$$lastInstance;
expect(xhr.$$events.progress[0]).toBe(progressFn);
expect(xhr.upload.$$events.progress[0]).toBe(uploadProgressFn);
});
describe('responseType', function() {
it('should set responseType and return xhr.response', function() {
+31 -1
View File
@@ -1,5 +1,7 @@
'use strict';
/* global MockXhr: false */
describe('$http', function() {
var callback, mockedCookies;
@@ -1019,7 +1021,7 @@ describe('$http', function() {
});
describe('scope.$apply', function() {
describe('callbacks', function() {
it('should $apply after success callback', function() {
$httpBackend.when('GET').respond(200);
@@ -1047,6 +1049,34 @@ describe('$http', function() {
$exceptionHandler.errors = [];
}));
it('should pass the event handlers through to the backend', function() {
var progressFn = jasmine.createSpy('progressFn');
var uploadProgressFn = jasmine.createSpy('uploadProgressFn');
$httpBackend.when('GET').respond(200);
$http({
method: 'GET',
url: '/some',
eventHandlers: {progress: progressFn},
uploadEventHandlers: {progress: uploadProgressFn}
});
$rootScope.$apply();
var mockXHR = MockXhr.$$lastInstance;
expect(mockXHR.$$events.progress).toEqual(jasmine.any(Function));
expect(mockXHR.upload.$$events.progress).toEqual(jasmine.any(Function));
var eventObj = {};
spyOn($rootScope, '$digest');
mockXHR.$$events.progress(eventObj);
expect(progressFn).toHaveBeenCalledOnceWith(eventObj);
expect($rootScope.$digest).toHaveBeenCalledTimes(1);
mockXHR.upload.$$events.progress(eventObj);
expect(uploadProgressFn).toHaveBeenCalledOnceWith(eventObj);
expect($rootScope.$digest).toHaveBeenCalledTimes(2);
});
});
+31
View File
@@ -95,6 +95,37 @@ describe('parser', function() {
expect(spaces).toEqual(noSpaces);
});
it('should use callback functions to know when an identifier is valid', function() {
function getText(t) { return t.text; }
var isIdentifierStart = jasmine.createSpy('start');
var isIdentifierContinue = jasmine.createSpy('continue');
isIdentifierStart.and.returnValue(true);
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
isIdentifierContinue.and.returnValue(true);
var tokens = lex.lex('πΣε').map(getText);
expect(tokens).toEqual(['πΣε']);
isIdentifierContinue.and.returnValue(false);
tokens = lex.lex('πΣε').map(getText);
expect(tokens).toEqual(['π', 'Σ', 'ε']);
});
it('should send the unicode characters and code points', function() {
function getText(t) { return t.text; }
var isIdentifierStart = jasmine.createSpy('start');
var isIdentifierContinue = jasmine.createSpy('continue');
isIdentifierStart.and.returnValue(true);
isIdentifierContinue.and.returnValue(true);
var lex = new Lexer({csp: false, isIdentifierStart: isIdentifierStart, isIdentifierContinue: isIdentifierContinue});
var tokens = lex.lex('\uD801\uDC37\uD852\uDF62\uDBFF\uDFFF');
expect(isIdentifierStart).toHaveBeenCalledTimes(1);
expect(isIdentifierStart.calls.argsFor(0)).toEqual(['\uD801\uDC37', 0x10437]);
expect(isIdentifierContinue).toHaveBeenCalledTimes(2);
expect(isIdentifierContinue.calls.argsFor(0)).toEqual(['\uD852\uDF62', 0x24B62]);
expect(isIdentifierContinue.calls.argsFor(1)).toEqual(['\uDBFF\uDFFF', 0x10FFFF]);
});
it('should tokenize undefined', function() {
var tokens = lex("undefined");
var i = 0;
+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);
});
});
});
+483 -116
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() {
@@ -1257,7 +1282,7 @@ describe("animations", function() {
expect(element).not.toHaveClass('green');
}));
it('should automatically cancel out class-based animations if the element already contains or doesn\' contain the applied classes',
it('should automatically cancel out class-based animations if the element already contains or doesn\'t contain the applied classes',
inject(function($animate, $rootScope) {
parent.append(element);
@@ -1871,6 +1896,68 @@ describe("animations", function() {
expect(count).toBe(3);
}));
it('should remove all event listeners for an element when $animate.off(element) is called',
inject(function($animate, $rootScope, $rootElement, $document, $$rAF) {
element = jqLite('<div></div>');
var otherElement = jqLite('<div></div>');
$rootElement.append(otherElement);
var count = 0;
var runner;
$animate.on('enter', element, counter);
$animate.on('leave', element, counter);
$animate.on('addClass', element, counter);
$animate.on('addClass', otherElement, counter);
function counter(element, phase) {
count++;
}
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
runner.end();
runner = $animate.addClass(element, 'blue');
$rootScope.$digest();
$animate.flush();
runner.end();
$$rAF.flush();
expect(count).toBe(4);
$animate.off(element);
runner = $animate.enter(element, $rootElement);
$animate.flush();
expect(capturedAnimation[1]).toBe('enter');
runner.end();
runner = $animate.addClass(element, 'red');
$animate.flush();
expect(capturedAnimation[1]).toBe('addClass');
runner.end();
runner = $animate.leave(element);
$animate.flush();
expect(capturedAnimation[1]).toBe('leave');
runner.end();
// Try to flush all remaining callbacks
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present');
expect(count).toBe(4);
// Check that other elements' event listeners are not affected
$animate.addClass(otherElement, 'green');
$animate.flush();
expect(count).toBe(5);
}));
it('should fire a `start` callback when the animation starts with the matching element',
inject(function($animate, $rootScope, $rootElement, $document) {
@@ -1912,63 +1999,96 @@ describe("animations", function() {
expect(capturedElement).toBe(element);
}));
it('should remove all event listeners when the element is removed',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('<div></div>');
they('should remove all event listeners when the element is removed via $prop',
['leave()', 'remove()'], function(method) {
inject(function($animate, $rootScope, $rootElement, $$rAF) {
var count = 0;
var runner;
element = jqLite('<div></div>');
$animate.on('enter', element, counter);
$animate.on('addClass', element[0], counter);
var count = 0;
var enterSpy = jasmine.createSpy();
var addClassSpy = jasmine.createSpy();
var runner;
function counter(element, phase) {
if (phase === 'start') {
count++;
$animate.on('enter', element, enterSpy);
$animate.on('addClass', element[0], addClassSpy);
function counter(element, phase) {
if (phase === 'start') {
count++;
}
}
}
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.flush();
runner.end(); // Otherwise the class animation won't run because enter is still in progress
expect(count).toBe(1);
$animate.flush();
expect(enterSpy.calls.count()).toBe(1);
expect(enterSpy.calls.mostRecent().args[1]).toBe('start');
capturedAnimation = null;
runner.end(); // Otherwise the class animation won't run because enter is still in progress
$$rAF.flush();
expect(enterSpy.calls.count()).toBe(2);
expect(enterSpy.calls.mostRecent().args[1]).toBe('close');
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
enterSpy.calls.reset();
capturedAnimation = null;
$animate.flush();
expect(count).toBe(2);
runner = $animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
capturedAnimation = null;
$animate.flush();
expect(addClassSpy.calls.count()).toBe(1);
expect(addClassSpy.calls.mostRecent().args[1]).toBe('start');
element.remove();
runner.end();
$$rAF.flush();
expect(addClassSpy.calls.count()).toBe(2);
expect(addClassSpy.calls.mostRecent().args[1]).toBe('close');
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
addClassSpy.calls.reset();
capturedAnimation = null;
expect(capturedAnimation).toBeTruthy();
if (method === 'leave()') {
runner = $animate.leave(element);
$animate.flush();
runner.end();
} else if (method === 'remove()') {
element.remove();
}
$animate.flush();
runner.end(); // Otherwise the class animation won't run because enter is still in progress
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
expect(count).toBe(2);
$animate.flush();
expect(enterSpy.calls.count()).toBe(0);
capturedAnimation = null;
runner.end(); // Otherwise the class animation won't run because enter is still in progress
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present'); // Try to flush any callbacks
expect(enterSpy.calls.count()).toBe(0);
$animate.addClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
capturedAnimation = null;
$animate.flush();
expect(count).toBe(2);
}));
$animate.addClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.flush();
expect(addClassSpy.calls.count()).toBe(0);
runner.end();
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present'); // Try to flush any callbacks
expect(addClassSpy.calls.count()).toBe(0);
expect(enterSpy.calls.count()).toBe(0);
});
});
it('should always detect registered callbacks after one postDigest has fired',
inject(function($animate, $rootScope, $rootElement) {
@@ -2033,104 +2153,187 @@ describe("animations", function() {
}
}));
it('leave: should remove the element even if another animation is called after',
inject(function($animate, $rootScope, $rootElement) {
describe('for leave', function() {
var outerContainer = jqLite('<div></div>');
element = jqLite('<div></div>');
outerContainer.append(element);
$rootElement.append(outerContainer);
it('should remove the element even if another animation is called afterwards',
inject(function($animate, $rootScope, $rootElement) {
var runner = $animate.leave(element, $rootElement);
$animate.removeClass(element,'rclass');
$rootScope.$digest();
runner.end();
$animate.flush();
var outerContainer = jqLite('<div></div>');
element = jqLite('<div></div>');
outerContainer.append(element);
$rootElement.append(outerContainer);
var isElementRemoved = !outerContainer[0].contains(element[0]);
expect(isElementRemoved).toBe(true);
}));
var runner = $animate.leave(element, $rootElement);
$animate.removeClass(element,'rclass');
$rootScope.$digest();
runner.end();
$animate.flush();
it('leave : should trigger a callback for an leave animation',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var isElementRemoved = !outerContainer[0].contains(element[0]);
expect(isElementRemoved).toBe(true);
}));
var callbackTriggered = false;
$animate.on('leave', jqLite($document[0].body), function() {
callbackTriggered = true;
});
they('should trigger callbacks when the listener is on the $prop element', ['same', 'parent'],
function(elementRelation) {
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var listenerElement, callbackSpy = jasmine.createSpy();
element = jqLite('<div></div>');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
element = jqLite('<div></div>');
listenerElement = elementRelation === 'same' ? element : jqLite($document[0].body);
$animate.on('leave', listenerElement, callbackSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
$$rAF.flush();
expect(callbackTriggered).toBe(true);
}));
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('start');
callbackSpy.calls.reset();
it('leave : should not fire a callback if the element is outside of the given container',
inject(function($animate, $rootScope, $$rAF, $rootElement) {
runner.end();
$$rAF.flush();
var callbackTriggered = false;
var innerContainer = jqLite('<div></div>');
$rootElement.append(innerContainer);
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('close');
});
}
);
$animate.on('leave', innerContainer,
function(element, phase, data) {
callbackTriggered = true;
});
it('should trigger callbacks for a leave animation',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
element = jqLite('<div></div>');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
var callbackSpy = jasmine.createSpy();
$animate.on('leave', jqLite($document[0].body), callbackSpy);
expect(callbackTriggered).toBe(false);
}));
element = jqLite('<div></div>');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
it('leave : should fire a `start` callback when the animation starts with the matching element',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
$$rAF.flush();
element = jqLite('<div></div>');
expect(callbackSpy).toHaveBeenCalled();
expect(callbackSpy.calls.count()).toBe(1);
}));
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
it('should trigger a callback for an leave animation (same element)',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
var callbackSpy = jasmine.createSpy();
expect(capturedState).toBe('start');
expect(capturedElement).toBe(element);
}));
element = jqLite('<div></div>');
$animate.on('leave', element, callbackSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
it('leave : should fire a `close` callback when the animation ends with the matching element',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
$$rAF.flush();
element = jqLite('<div></div>');
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('start');
callbackSpy.calls.reset();
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
runner.end();
$$rAF.flush();
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
runner.end();
$$rAF.flush();
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('close');
}));
expect(capturedState).toBe('close');
expect(capturedElement).toBe(element);
}));
it('should not fire a callback if the element is outside of the given container',
inject(function($animate, $rootScope, $$rAF, $rootElement) {
var callbackTriggered = false;
var innerContainer = jqLite('<div></div>');
$rootElement.append(innerContainer);
$animate.on('leave', innerContainer,
function(element, phase, data) {
callbackTriggered = true;
});
element = jqLite('<div></div>');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
expect(callbackTriggered).toBe(false);
}));
it('should fire a `start` callback when the animation starts',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
element = jqLite('<div></div>');
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(capturedState).toBe('start');
expect(capturedElement).toBe(element);
}));
it('should fire a `close` callback when the animation ends',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
element = jqLite('<div></div>');
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
runner.end();
$$rAF.flush();
expect(capturedState).toBe('close');
expect(capturedElement).toBe(element);
}));
it('should remove all event listeners after all callbacks for the "leave:close" phase have been called',
inject(function($animate, $rootScope, $rootElement, $$rAF) {
var leaveSpy = jasmine.createSpy();
var addClassSpy = jasmine.createSpy();
element = jqLite('<div></div>');
$animate.on('leave', element, leaveSpy);
$animate.on('addClass', element, addClassSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$animate.flush();
runner.end();
$$rAF.flush();
expect(leaveSpy.calls.mostRecent().args[1]).toBe('close');
$animate.addClass(element, 'blue');
$animate.flush();
runner.end();
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present');
expect(addClassSpy.calls.count()).toBe(0);
}));
});
they('should trigger a callback for a $prop animation if the listener is on the document',
['enter', 'leave'], function($event) {
@@ -2166,5 +2369,169 @@ describe("animations", function() {
});
});
describe('when animations are skipped, disabled, or invalid', function() {
var overriddenAnimationRunner;
var capturedAnimation;
var capturedAnimationHistory;
var defaultFakeAnimationRunner;
var parent;
var parent2;
beforeEach(module(function($provide) {
overriddenAnimationRunner = null;
capturedAnimation = null;
capturedAnimationHistory = [];
$provide.value('$$animation', function() {
capturedAnimationHistory.push(capturedAnimation = arguments);
return overriddenAnimationRunner || defaultFakeAnimationRunner;
});
return function($rootElement, $q, $animate, $$AnimateRunner, $document) {
defaultFakeAnimationRunner = new $$AnimateRunner();
$animate.enabled(true);
element = jqLite('<div class="element">element</div>');
parent = jqLite('<div class="parent1">parent</div>');
parent2 = jqLite('<div class="parent2">parent</div>');
$rootElement.append(parent);
$rootElement.append(parent2);
jqLite($document[0].body).append($rootElement);
};
}));
it('should trigger all callbacks if a follow-up structural animation takes over a running animation',
inject(function($animate, $rootScope) {
parent.append(element);
var moveSpy = jasmine.createSpy();
var leaveSpy = jasmine.createSpy();
$animate.on('move', parent2, moveSpy);
$animate.on('leave', parent2, leaveSpy);
$animate.move(element, parent2);
$rootScope.$digest();
$animate.flush();
expect(moveSpy.calls.count()).toBe(1);
expect(moveSpy.calls.mostRecent().args[1]).toBe('start');
$animate.leave(element);
$rootScope.$digest();
$animate.flush();
expect(moveSpy.calls.count()).toBe(2);
expect(moveSpy.calls.mostRecent().args[1]).toBe('close');
expect(leaveSpy.calls.count()).toBe(2);
expect(leaveSpy.calls.argsFor(0)[1]).toBe('start');
expect(leaveSpy.calls.argsFor(1)[1]).toBe('close');
}));
it('should not trigger callbacks for the previous structural animation if a follow-up structural animation takes over before the postDigest',
inject(function($animate, $rootScope) {
var enterDone = jasmine.createSpy('enter animation done');
var enterSpy = jasmine.createSpy();
var leaveSpy = jasmine.createSpy();
$animate.on('enter', parent, enterSpy);
$animate.on('leave', parent, leaveSpy);
$animate.enter(element, parent).done(enterDone);
expect(enterDone).not.toHaveBeenCalled();
var runner = $animate.leave(element);
$animate.flush();
expect(enterDone).toHaveBeenCalled();
expect(enterSpy).not.toHaveBeenCalled();
expect(leaveSpy.calls.count()).toBe(1);
expect(leaveSpy.calls.mostRecent().args[1]).toBe('start');
leaveSpy.calls.reset();
runner.end();
$animate.flush();
expect(enterSpy).not.toHaveBeenCalled();
expect(leaveSpy.calls.count()).toBe(1);
expect(leaveSpy.calls.mostRecent().args[1]).toBe('close');
}));
it('should not trigger the callback if animations are disabled on the element',
inject(function($animate, $rootScope, $rootElement, $document) {
var callbackTriggered = false;
var spy = jasmine.createSpy('enter');
$animate.on('enter', jqLite($document[0].body), spy);
element = jqLite('<div></div>');
$animate.enabled(element, false);
var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush(); // Flushes the animation frames for the callbacks
expect(spy).not.toHaveBeenCalled();
}));
it('should not trigger the callbacks if the animation is skipped because there are no class-based animations and no structural animation',
inject(function($animate, $rootScope) {
parent.append(element);
var classSpy = jasmine.createSpy('classChange');
$animate.on('addClass', element, classSpy);
$animate.on('removeClass', element, classSpy);
element.addClass('one three');
$animate.addClass(element, 'one');
$animate.removeClass(element, 'four');
$rootScope.$digest();
$animate.flush();
expect(classSpy).not.toHaveBeenCalled();
}));
describe('because the document is hidden', function() {
beforeEach(module(function($provide) {
var doc = jqLite({
body: document.body,
hidden: true
});
$provide.value('$document', doc);
}));
it('should trigger callbacks for an enter animation',
inject(function($animate, $rootScope, $rootElement, $document) {
var callbackTriggered = false;
var spy = jasmine.createSpy();
$animate.on('enter', jqLite($document[0].body), spy);
element = jqLite('<div></div>');
var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush(); // Flushes the animation frames for the callbacks
expect(spy.calls.count()).toBe(2);
expect(spy.calls.argsFor(0)[1]).toBe('start');
expect(spy.calls.argsFor(1)[1]).toBe('close');
}));
});
});
});
});
+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();
});
});
});
});
+60
View File
@@ -396,6 +396,66 @@ describe('$aria', function() {
});
});
describe('aria-readonly', function() {
beforeEach(injectScopeAndCompiler);
they('should not attach itself to native $prop controls', {
input: '<input ng-readonly="val">',
textarea: '<textarea ng-readonly="val"></textarea>',
select: '<select ng-readonly="val"></select>',
button: '<button ng-readonly="val"></button>'
}, function(tmpl) {
var element = $compile(tmpl)(scope);
scope.$apply('val = true');
expect(element.attr('readonly')).toBeDefined();
expect(element.attr('aria-readonly')).toBeUndefined();
});
it('should attach itself to custom controls', function() {
compileElement('<div ng-readonly="val"></div>');
expect(element.attr('aria-readonly')).toBe('false');
scope.$apply('val = true');
expect(element.attr('aria-readonly')).toBe('true');
});
it('should not attach itself if an aria-readonly attribute is already present', function() {
compileElement('<div ng-readonly="val" aria-readonly="userSetValue"></div>');
expect(element.attr('aria-readonly')).toBe('userSetValue');
});
it('should always set aria-readonly to a boolean value', function() {
compileElement('<div ng-readonly="val"></div>');
scope.$apply('val = "test angular"');
expect(element.attr('aria-readonly')).toBe('true');
scope.$apply('val = null');
expect(element.attr('aria-readonly')).toBe('false');
scope.$apply('val = {}');
expect(element.attr('aria-readonly')).toBe('true');
});
});
describe('aria-readonly when disabled', function() {
beforeEach(configAriaProvider({
ariaReadonly: false
}));
beforeEach(injectScopeAndCompiler);
it('should not add the aria-readonly attribute', function() {
compileElement("<input ng-model='val' readonly>");
expect(element.attr('aria-readonly')).toBeUndefined();
compileElement("<div ng-model='val' ng-readonly='true'></div>");
expect(element.attr('aria-readonly')).toBeUndefined();
});
});
describe('aria-required', function() {
beforeEach(injectScopeAndCompiler);
+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() {
+77 -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');
});
@@ -912,6 +914,51 @@ describe('ngMock', function() {
});
}).toThrow('test message');
}));
describe('error stack trace when called outside of spec context', function() {
// - Chrome, Firefox, Edge, Opera give us the stack trace as soon as an Error is created
// - IE10+, PhantomJS give us the stack trace only once the error is thrown
// - IE9 does not provide stack traces
var stackTraceSupported = (function() {
var error = new Error();
if (!error.stack) {
try {
throw error;
} catch (e) {}
}
return !!error.stack;
})();
function testCaller() {
return inject(function() {
throw new Error();
});
}
var throwErrorFromInjectCallback = testCaller();
if (stackTraceSupported) {
describe('on browsers supporting stack traces', function() {
it('should update thrown Error stack trace with inject call location', function() {
try {
throwErrorFromInjectCallback();
} catch (e) {
expect(e.stack).toMatch('testCaller');
}
});
});
} else {
describe('on browsers not supporting stack traces', function() {
it('should not add stack trace information to thrown Error', function() {
try {
throwErrorFromInjectCallback();
} catch (e) {
expect(e.stack).toBeUndefined();
}
});
});
}
});
});
});
@@ -2042,6 +2089,27 @@ describe('ngMock', function() {
}).toThrowError('Too many components found');
});
});
it('should create an isolated child of $rootScope, if no `$scope` local is provided', function() {
function TestController($scope) {
this.$scope = $scope;
}
module(function($compileProvider) {
$compileProvider.component('test', {
controller: TestController
});
});
inject(function($componentController, $rootScope) {
var $ctrl = $componentController('test');
expect($ctrl.$scope).toBeDefined();
expect($ctrl.$scope.$parent).toBe($rootScope);
// check it is isolated
$rootScope.a = 17;
expect($ctrl.$scope.a).toBeUndefined();
$ctrl.$scope.a = 42;
expect($rootScope.a).toEqual(17);
});
});
});
});
-31
View File
@@ -1027,34 +1027,3 @@ describe('ngView animations', function() {
));
});
});
describe('ngView in async template', function() {
beforeEach(module('ngRoute'));
beforeEach(module(function($compileProvider, $provide, $routeProvider) {
$compileProvider.directive('asyncView', function() {
return {templateUrl: 'async-view.html'};
});
$provide.decorator('$templateRequest', function($timeout) {
return function() {
return $timeout(angular.identity, 500, false, '<ng-view></ng-view>');
};
});
$routeProvider.when('/', {template: 'Hello, world !'});
}));
it('should work correctly upon initial page load',
// Injecting `$location` here is necessary, so that it gets instantiated early
inject(function($compile, $location, $rootScope, $timeout) {
var elem = $compile('<async-view></async-view>')($rootScope);
$rootScope.$digest();
$timeout.flush(500);
expect(elem.text()).toBe('Hello, world !');
dealoc(elem);
})
);
});
-15
View File
@@ -23,21 +23,6 @@ describe('$route', function() {
dealoc(element);
});
it('should be loaded upon initial load (even if `ngView` is loaded async)', function() {
module(function($routeProvider) {
$routeProvider.when('/', {template: 'Hello, world !'});
});
inject(function($location, $rootScope) {
$location.path('/');
$rootScope.$digest();
});
inject(function($route) {
expect($route.current).toBeDefined();
});
});
it('should allow cancellation via $locationChangeStart via $routeChangeStart', function() {
module(function($routeProvider) {
$routeProvider.when('/Edit', {