Compare commits

...

105 Commits

Author SHA1 Message Date
Matias Niemelä 75e876424d chore(CHANGELOG): update with changes for 1.4.8 2015-11-19 14:52:56 -08:00
Jason Bedard 19fab4a1d7 perf(copy): avoid regex in isTypedArray
Closes: #12054
2015-11-19 08:59:00 +00:00
Jason Bedard d1293540e1 perf(copy): only validate/clear user specified destination
Closes #12068
2015-11-19 08:58:47 +00:00
Peter Bacon Darwin 22f66025db fix(jqLite): deregister special mouseenter / mouseleave events correctly
Closes #12795
Closes #12799
2015-11-12 18:45:59 +00:00
rrsivabalan 6f8ddb6d43 fix($location): ensure $locationChangeSuccess fires even if URL ends with #
Closes #12175
Closes #13251
2015-11-12 13:40:29 +00:00
Eric Lee Carraway 34590e15d4 docs(readme): fix typo (setup => set up)
spell set up as two words
here, it is an adjective modifying the noun "environment"

Closes #13297
2015-11-12 10:31:06 +02:00
Eric Lee Carraway 83098b9add docs(contributing): fix typo (a unambiguous => an unambiguous)
use the article “an” before words that start with a vowel sound

Closes #13292
2015-11-11 14:26:39 +02:00
Peter Bacon Darwin 5d8861fb2f fix($compile): bind all directive controllers correctly when using bindToController
Previously only the first directive's controller would be bound correctly.

Closes #11343
Closes #11345
2015-11-10 20:56:33 +00:00
Georgios Kalpakas b9f7c453e0 fix($compile): evaluate against the correct scope with bindToController on new scope
Previously, the directive bindings were evaluated against the directive's
new (non-isolate) scope, instead of the correct (parent) scope.
This went unnoticed most of the time, since a property would be eventually
looked up in the parent scope due to prototypal inheritance. The incorrect
behaviour was exhibited when a property on the child scope was shadowing
that on the parent scope.

This commit fixes it.

Fixes #13021
Closes #13025
2015-11-10 20:56:33 +00:00
Jakub Torbicki 750344129e fix($compile): bind all directive controllers correctly when using bindToController
Previously only the first directive's controller would be bound correctly.

Closes #11343
Closes #11345
2015-11-10 20:56:33 +00:00
Peter Bacon Darwin 74da034077 fix($compile): fix scoping of transclusion directives inside replace directive
Closes #12975
Closes #12936
Closes #13244
2015-11-10 20:56:07 +00:00
Jason Bedard 91ef94d284 refactor($compile): simplify multi element directive check
Previously, we would check if an attribute indicates a multi-element
directive, now we only do this check if the attribute name actually
matches the multi-element name pattern.

Closes #12365
2015-11-10 20:48:33 +00:00
Martin Staffa ab9b021572 docs(changelog, migration): add BC notice for ngMessages evaluation
Introduced by

Closes #11616
Closes #12001
2015-11-06 17:19:16 +01:00
Martin Staffa b268c0b7b4 docs(changelog, migration): add BC notice for setting ngOptions as attribute
Caused by 7fda214c4f

Closes #13145
2015-11-06 17:19:16 +01:00
Doug Krugman b0c19f8b06 docs(guide/Concepts): remove unused refresh property
Closes #13257
2015-11-06 10:17:00 +02:00
jody tate bbc2a0ae48 docs(guide/Directives): change "it" to possessive
Closes #13253
2015-11-05 14:27:07 +02:00
Martin Staffa ca53dfcc18 docs(ngRepeat): add more info about watching and tracking
- mention $watchCollection
- highlight that track by "id" can improve render performance

Related #9508
2015-11-03 21:40:50 +01:00
Georgios Kalpakas ce6a96b0d7 perf(merge): remove unnecessary wrapping of jqLite element
Fixes https://github.com/angular/angular.js/commit/75292a6cb5e17d618902f7996e80eb3118eff7b0#commitcomment-14137538

Closes #13236
2015-11-03 17:49:54 +02:00
Peter Bacon Darwin d4b359f4b2 test(merge): fix check on jquery object 2015-11-02 20:13:14 +00:00
JonyD 8d841c3405 docs(ngRepeat): fix link to MDN
Closes #13226
2015-11-02 21:00:13 +01:00
Martin Staffa 2b285c75f4 docs(ngInclude): fix incorrect link 2015-11-02 21:00:12 +01:00
Martin Staffa 6e4464331d docs(tutorial/0 - Bootstrapping): mention that the setup must be completed
Closes #13106
2015-11-02 21:00:11 +01:00
Peter Bacon Darwin 2f8db1bf01 fix(merge): ensure that jqlite->jqlite and DOM->DOM
Previously we were wrapping DOM elements into jqlite objects when cloning
and vice versa.

Fixes https://github.com/angular/angular.js/pull/12286#discussion_r43656917
2015-11-02 19:56:13 +00:00
luckylooke 838cf4be3c fix(merge): clone elements instead of treating them like simple objects
Similar fix to #11720

Closes #12286
2015-11-02 17:22:26 +00:00
Matthew Hill de2a56bbc8 docs(angular-mocks): clarify angular.mock.module usage with objects
Closes #12354
2015-11-01 07:14:20 +00:00
Jason Bedard 55ad192e4a perf($compile): use static jquery data method to avoid creating new instances 2015-11-01 07:00:22 +00:00
Chris J. Lee 5b4713e43e chore(protractor-conf.js): remove dangling comma
Closes #13051
2015-11-01 06:46:08 +00:00
Peter Bacon Darwin 3fa9aba0cc chore(package.json): update dgeni-packages to 0.11.0 2015-10-31 20:44:55 +00:00
Peter Bacon Darwin 1bba358a75 chore(package.json): add commitizen, adapter and npm script
Closes #13194
2015-10-31 20:43:13 +00:00
Bert Verhelst 7a4124c298 docs($location): improve style
Closes #13072
2015-10-30 22:09:58 +01:00
Martin Staffa 2512a81e09 docs(error/ctreq): fix typo
Closes #13083
2015-10-30 22:09:58 +01:00
Michael George Attard 44c9d1616a docs($rootScope): improve clarity and consistency
Closes #13110
2015-10-30 22:09:57 +01:00
Pablo Villoslada Puigcerber 5758d73964 docs(select): document the multiple attribute
Add the `multiple` attribute to the documentation of the select directive.

Closes #13119
2015-10-30 20:41:58 +02:00
Sreenivasan K 6bd6dbff49 fix($animate): ensure leave animation calls close callback
Closes #12278
Closes #12096
Closes #13054
2015-10-29 07:55:36 +00:00
Stanislav Komanec 7170f9d9ca fix($resource): allow XHR request to be cancelled via timeout promise
Closes #12657
Closes #12675
Closes #10890
Closes #9332
2015-10-28 22:26:21 +00:00
Peter Bacon Darwin 1c0f721368 test($rootScope): ensure that only child scopes are disconnected
Related to #11786 and 8fe781fbe7
2015-10-28 22:06:25 +00:00
Alicia Lauerman 2a5a52a76c fix($cacheFactory): check key exists before decreasing cache size count
Previously, there was no check for the existence of an item in the
cache when calling `$cacheFactory.remove()` before modifying the cache size
count.

Closes #12321
Closes #12329
2015-10-28 21:50:17 +00:00
Georgios Kalpakas c690946469 fix($http): apply transformResponse even when data is empty
Note, that (as a by-product of the previous implementation) only non-empty
data was passed through the `transformResponse` pipeline. This is no
longer the case.

When using a custom `transformResponse` function, one should make sure it
can also handle an empty (i.e. falsy) `data` argument appropriately.

Fixes #12976
Closes #12979
2015-10-28 21:41:52 +00:00
Peter Bacon Darwin 87b0055c80 fix($rootScope): stop IE9 memory leak when destroying scopes
Ensure that all child scopes are completely disconnected when a parent is
destroyed.

Closes #10706
Closes #11786
2015-10-28 21:35:22 +00:00
Charlie-Hua 2116857a2a docs(ngModelOptions): add missing user.data result for updateOn: blur example
In the updateOn:blur example, there is an input for user.data but the
result is missing and nowhere to see how the value changes compared to user.name.

Closes #13129
2015-10-28 22:10:03 +01:00
Peter Bacon Darwin 0f58334b7b fix(ngOptions): skip comments and empty options when looking for options
Related #12952
Closes #12190
Closes #13029
Closes #13033
2015-10-28 18:33:08 +01:00
Stu Cox bcc257b459 docs($q): add a note re. difference in exception handling vs ES6
Closes #11472
Closes #13101
2015-10-28 08:18:00 +00:00
Ryan Hart 980fb395e4 docs(ngOptions): explain the caveats of using select as and track by together
Changes:

* Modify warning message to indicate that `track by` can be used with `select as`,
  but subject to certain limitations.
* Provide both a working and an non-working example.
* Explain why the latter does not work.

Closes #13007
2015-10-27 22:01:58 +02:00
Sam Rawlins 62ed26a84f docs($anchorScroll): fix link to HTML5 spec
Closes #13180
2015-10-27 20:40:43 +02:00
Marcy Sutton 59f1f4e19a fix(ngAria): don't add tabindex to radio and checkbox inputs
Closes #12492
Closes #13095
2015-10-27 17:52:02 +01:00
Andrew Austin cb51116dbd fix(ngInput): change URL_REGEXP to better match RFC3987
The URL_REGEXP in use to perform validation in ngInput is too restrictive and fails to
follow RFC3987. In particular, it only accepts ftp, http, and https scheme components and
rejects perfectly valid schemes such as "file", "mailto", "chrome-extension",
etc. The regex also requires the scheme to be followed by two "/" but the RFC says
0 to n are acceptable. This change fixes both of these issues to better align to
the standard.

Closes #11341
Closes #11381
2015-10-26 21:45:53 +00:00
Kuzminov Aleksandr Sergeevich c1f34e8eeb fix(jqLite): ensure mouseenter works with svg elements on IE
Closes #10259
Closes #10276
2015-10-26 21:27:04 +00:00
sevdog 7bf5429e3b docs($animateCss): add missing documentation for the structural option
Add missing documentation for structural option in `$animateCss` service

Closes #13049
2015-10-26 13:03:37 -07:00
Lucas Galfaso d3da55c40f fix(isArrayLike): handle jQuery objects of length 0
Closes: #13169
Closes: #13171
2015-10-26 18:01:15 +00:00
Jack Viers 70edec947c fix(Angular.js): fix isArrayLike for unusual cases
Closes #10186
Closes #8000
Closes #4855
Closes #4751
Closes #10272
2015-10-26 18:01:15 +00:00
Risan Bagja Pradana fe17c0e066 docs(tutorial): add a note about Chrome or Firefox not being available
Based on the current configuration, Karma will run the tests on both
Chrome and Firefox, which will result in an error if either browser is not
available on the user's machine. This commit adds a note and directions on
how to solve this.

Closes #13114
2015-10-26 15:44:43 +02:00
Lucas Mirelmann e403682444 fix($parse): evaluate once simple expressions in interpolations
For simple expressions without filters that have a stateless interceptor
then handle the 2nd phase parse evaluation using `inputs`.

TL;DR
This fixes the issue that interpolated simple expressions were evaluated twice
within one digest loop.

Long version, things happen in the following order:

* There was an overhaul on $interpolate, this overhaul changed $parse and
  incorporated the concept of an interceptor.
* Optimization on $parse landed  so expressions that have filters without
  parameters (or the parameters are constants) would be evaluated in 2 phases,
  first to evaluate the expression sans the filter evaluation and then with
  the filter evaluation. This also used interceptors [the second evaluation
  issue was added here]
* More optimizations on $parse landed and now expressions could be evaluated
  in 2 phases. One to get all the possible values that could change (lets call
  this state), the state was checked by $watch to know if an expression changed.
  The second to continue the evaluation (as long as this state is provided).
  This, once again, used interceptors

The last change, was supposed to fix the issue, but there was an assumption in
the existing code that the code would always generate the 2 phases functions,
but that is not true. If the expression is simple enough (just like the one in
your case) then the 2-phase evaluations functions are not generated. In this
case, if a stateless interceptor was added (just like what $interpolate adds)
then the state was not used and you see the function being evaluated twice.
This explains why, if you change the expression from
`Hello {{log('A')}} {{log('B')}}!` to `Hello {{log('A') + ' ' + log('B')}}!`,
then the repetition is not there.

Closes #12983
Closes #13002
2015-10-15 22:20:30 +02:00
zurin 27d441b0d6 docs(guide/Scopes): fix grammar
Added a comma to make reading more natural.

Closes #13084
2015-10-14 16:16:02 +03:00
Michael Salmon 8a944b0872 docs(guide/Directives): improve description of linking function
The `controller` and `transclude` parameters of the linking function were not
mentioned in the description, but used in the examples.
This commit improves the description and links to the `$compile` API docs
for more details.

Closes #13028
2015-10-14 10:53:12 +03:00
Martin Staffa 786a1a4429 docs(ngOptions): add info about preselecting complex models
Closes #12966
2015-10-08 15:56:17 +02:00
Chris J. Lee 8e5c4e92f7 test(ngResource): fix typos in tests
Closes #13044
2015-10-08 11:49:17 +03:00
Flavio Corpa Ríos 46d24ae4c8 docs(ngInclude): add workaround for using onload function with SVG in IE11
Closes #12493
Closes #13042
2015-10-07 23:04:31 +02:00
Jason Hopper 9dd33c09b1 docs(tutorial): update angular module versions to reflect tutorial files
Closes #12991
Closes #12992
2015-10-07 17:53:26 +02:00
Sugan Krishnan fea8240c81 docs($sce): fix typo
Closes #13030
2015-10-07 13:23:11 +01:00
Peter Bacon Darwin 3d2b1be211 refactor($compile): check removeWatches before calling
Previously we assigned `noop` if there was no function but there is no
performance advantage in doing this since the check would have to happen
either at assignment time or at call time.

Removing this use of `noop` makes the code clearer, IMO :-)

Closes #12528
2015-10-07 13:05:04 +01:00
Peter Bacon Darwin f08a0c5ad1 refactor($compile): initialize removeWatchCollection at the start
This check means that we don't have to keep checking whether the collection
has been created when adding a new watcher

Closes #12528
2015-10-07 13:05:03 +01:00
Peter Bacon Darwin 6f1e0ba563 refactor($compile): rename variables to clarify their purpose
Closes #12528
2015-10-07 13:05:03 +01:00
Jason Bedard 540338f9a5 refactor($compile): move $scope.$on('$destroy') handler out of initializeDirectiveBindings
Since only one of three invocations of `initializeDirectiveBindings` actually
adds a `$destroy` handler to the scope (the others just manually call unwatch
as needed), we can move that code out of this method.

This also has the benefit of simplifying what parameters need to be passed
through to the linking functions

Closes #12528
2015-10-07 13:05:03 +01:00
Martin Staffa 0e6a700807 Revert "fix(ngOptions): skip comments when looking for option elements"
This reverts commit 68d4dc5b71.
The fix only fixed a specific case and exhibited a flawed logic
(namely skipping every option if the emptyOption is a comment).
See https://github.com/angular/angular.js/issues/12190#issuecomment-145877914

Conflicts:
	test/ng/directive/ngOptionsSpec.js
2015-10-07 11:33:32 +02:00
Georgios Kalpakas 4fc40bc932 fix(limitTo): start at 0 if begin is negative and exceeds input length
Previously, specifying a negative `begin` whose abs value exceeds the
input's length, would behave unexpectedly (depending on the value of
`limit` relative to `begin`). E.g.:

```
limitToFilter('12345', 3, -7) === '1'
// but
limitToFilter('12345', 10, -7) === '123'
```

This commit fixes the unexpected behaviour, by setting `begin` to 0 in the
aforementioned cases. Thus, the previous examples become:

```
limitToFilter('12345', 3, -7) === limitToFilter('12345', 3, 0) === '123'
// and
limitToFilter('12345', 10, -7) === limitToFilter('12345', 10, 0) === '12345'
```

Fixes #12775
Closes #12781
2015-10-07 00:09:37 +03:00
Richard Harrington 216724b4cb docs(constant): fix pluralization
Closes #13024
2015-10-06 23:22:41 +03:00
Raghav 9bd1645970 docs($animate): fixed typo ("an animations" -> "any animations")
Closes #13020
2015-10-06 23:15:15 +03:00
Magnus Pedersen 3397a031a1 docs(ngOptions): rephrased a sentence for clarity
Closes #13010
2015-10-06 23:12:55 +03:00
Peter Bacon Darwin 5ec5aa7751 style(ngOptionsSpec): remove excess space
This was inadvertently added in 7b2ecf42c6
2015-10-06 14:36:47 +01:00
Peter Bacon Darwin bf5ac5261d style(ngOptions): fix missing closing brace
This was inadvertently added in 7b2ecf42c6
2015-10-06 14:20:37 +01:00
Peter Bacon Darwin 91b7cd9b74 fix(ngMock): reset cache before every test
We don't need to have values in the cache from previous tests. This was
causing failures in all subsequent tests when a single test failed due
to a memory leak.

Now that we reset the cache each time we do not need to store the cache
size at the start of each test

Closes #13013
2015-10-06 13:58:23 +01:00
Martin Staffa 7b2ecf42c6 fix(ngOptions): override select option registration
When ngOptions is present on a select, the option directive should not be able to
register options on the selectCtrl since this may cause errors during the
ngOptions lifecycle.

This can happen in the following cases:

- there is a blank option below the select element, an ngModel
directive, an ngOptions directive and some other directive on the select
element, which compiles the children of the select
(i.e. the option elements) before ngOptions is has finished linking.

- there is a blank option below the select element, an ngModel
directive, an ngOptions directive and another directive, which uses
templateUrl and replace:true.

What happens is:
- the option directive is compiled and adds an element `$destroy` listener
that will call `ngModel.$render` when the option element is removed.
- when `ngOptions` processes the option, it removes the element, and
triggers the `$destroy` listener on the option.
- the registered `$destroy` listener calls `$render` on `ngModel`.
- $render calls `selectCtrl.writeValue()`, which accesses the `options`
object in the `ngOptions` directive.
- Since `ngOptions` has not yet completed linking the `options` has not
yet been defined and we get an error.

This fix moves the registration code for the `option` directive into the
`SelectController.registerOption()` method, which is then overridden by
the `ngOptions` directive as a `noop`.

Fixes #11685
Closes #12972
Closes #12968
Closes #13012
2015-10-06 13:57:40 +01:00
Matias Niemelä 256d9a948c docs(ngAnimate): simplify $animateCss example code 2015-10-05 10:56:43 -07:00
spoonraker 99fc6cda98 docs(tutorial): updates for the text for animations in step 12
The grammar for the animation description has now been improved.

Closes #12740
2015-10-05 10:22:17 -07:00
Jason Hopper 690b69b9cd docs(tutorial): update tutorial copy to reflect updates to tutorial source @bower.json excerpt for animations
Code breaks if tutorial is followed without reset.
bower.js exceprt copy does not match source.
Changed to reflect in text body.

Closes #12993
2015-10-05 10:18:45 -07:00
Alexandr Gureev 4262f15e16 docs(ngAnimate): fix typos in examples
Closes #12995
2015-10-02 11:27:05 +03:00
John Zhang 3a8d1354ce docs($httpProvider): fix description of useLegacyPromiseExtensions
useLegacyPromiseExtensions's default value is true, and the  legacy
methods exist when it is set to true.

Closes #12974
2015-10-01 18:33:40 +02:00
Donghwan Kim f9387c6890 docs(guide/Running in Production): fix an incorrect indefinite article
Closes #12986
2015-10-01 18:33:29 +02:00
koyner 48d0ffcbc4 docs(guide/Forms): fix indentation.
Closes #12988
2015-10-01 18:23:51 +02:00
Martin Staffa 3485ba1e2b docs(guide/Using $location): note that the fakeBrowser is not for actual projects
Closes #12982
Closes #12987
2015-10-01 18:21:56 +02:00
Matias Niemelä 2f61145475 chore(CHANGELOG): update with changes for 1.4.7 2015-09-29 13:54:51 -07:00
Martin Staffa 8c618d896b docs($http): link to usage where config is mentioned; make drier
Linking to usage section makes it easier for beginners to find out what the config object looks like.
The General Usage section now features an example that actually uses $http(config), and the Shortcut Methods section has been moved so that it appears directly after.

Closes #12949
Closes #12950
2015-09-27 15:48:20 +02:00
Martin Staffa 68d4dc5b71 fix(ngOptions): skip comments when looking for option elements
When the empty/blank option has a directive that transcludes, ngIf for example,
a comment will be added into the select. Previously, ngOptions used this
comment as the empty option, which would mess up the displayed options.

Closes #12190
2015-09-27 15:48:13 +02:00
Martin Staffa 03a4a96cf9 test(ngOptions): clarify a test description 2015-09-27 15:48:06 +02:00
Stefan Krüger 655c52a621 docs(guide/Directives): let myTabs directive ctrl use inline array notation
modified `docsTabsExample` myTabs directive ctrl at
[Creating Directives that Communicate Example](https://docs.angularjs.org/guide/directive#creating-directives-that-communicate) so that it uses
[Inline Array Annotation](https://docs.angularjs.org/guide/di#inline-array-annotation)
and is compatible with
[Using Strict Dependency Injection](https://docs.angularjs.org/guide/di#using-strict-dependency-injection)

Closes #12767
2015-09-27 15:47:57 +02:00
Martin Staffa fa3ddba5f2 docs(ngModel): align $viewValue description with $setViewValue 2015-09-27 15:47:43 +02:00
Matias Niemelä c4a1b6124e docs($animateCss): options.transition should be options.transitionStyle 2015-09-24 10:06:22 -07:00
Matias Niemelä e52d731bfd feat($animateCss): add support for temporary styles via cleanupStyles
Some animations make use of the `from` and `to` styling only for the
lifetime of the animation. This patch allows for those styles to be
removed once the animation is closed automatically within `$animateCss`.

Closes #12930
2015-09-24 10:02:30 -07:00
Igor Minar 9b72843018 build(travis): make sauce connect process query a bit more specific 2015-09-23 14:01:32 -07:00
Georgios Kalpakas 9c1f8ea70b chore(check-node-modules): make check/reinstall node_modules work across platforms
The previous implementations (based on shell scripts) threw errors on
Windows, because it was not able to `rm -rf` 'node_modules' (due to the
255 character limit in file-paths).

This implementation works consistently across platforms and is heavily based on
'https://github.com/angular/angular/blob/3b9c08676a4c921bbfa847802e08566fb601ba7a/tools/npm/check-node-modules.js'.

Fixes #11143
Closes #11353

Closes #12792
2015-09-23 23:01:15 +03:00
Igor Minar 9fde5648e4 build(travis): fix typo in a comment 2015-09-23 11:01:00 -07:00
Igor Minar ea829620b2 build(travis): gracefully shut down the sauce connect tunnel after the tests are done running
This is to prevent sauce connect tunnel leaks.

Closes #12921
2015-09-23 09:40:27 -07:00
Martin Staffa 1731d091f8 docs(ngList): whitespace -> newline 2015-09-23 17:38:15 +02:00
Matias Niemelä 9d3704ca46 fix(ngAnimate): ensure anchoring uses body as a container when needed
Prior to this fix anchoring would allow for a container to be a document
node or something higher beyond the body tag. This patch makes it fall
back to body incase the rootElement node exists as a parent ancestor.

Closes #12872
2015-09-22 13:47:16 -07:00
Matias Niemelä 215dff34dd revert: chore(core): introduce $$body service
Relying on the body node to be present right at injection has
caused issues with unit testing as well as some animations on
the body element. Reverting this patch fixes these issues.

Closes #12874
2015-09-22 13:47:10 -07:00
Matias Niemelä fa8c399fad fix(ngAnimate): callback detection should only use RAF when necessary
Callbacks are detected within the internals of ngAnimate whenever an
animation starts and ends. In order to allow the user to set callbacks
the callback detection needs to happen during the next tick. Prior to
this fix we used $$rAF to do the tick detection, however, with this
patch we intelligently use $$postDigest to do that for us and then
only issue a call to `$$rAF` if necessary.
2015-09-22 13:47:04 -07:00
Peter Bacon Darwin 7295c60ffb fix(ngMessages): prevent race condition with ngAnimate
If `ngMessage` tried to add a message back in that was about to be removed
after an animation, the NgMessageController got confused and tried to detach
the newly added message, when the pending node was destroyed.

This change applies a unique `attachId` to the message object and its DOM
node when it is attached. This is then checked when a DOM node is being
destroyed to prevent unwanted calls to `detach`.

Closes #12856
Closes #12903
2015-09-22 20:53:40 +01:00
Martin Staffa fa01571036 docs(guide/Directives): fix link formatting
Closes #12909;
2015-09-22 13:12:52 +02:00
Martin Staffa dbc698517f fix(ngOptions): prevent frozen select ui in IE
In certain scenarios, IE10/11/Edge create unresponsive select elements.
The following contribute to the bug:
- There need to be at least 2 selects next to each other
- The option elements are added via javascript
- the option.value is accessed before it is set
- the option.label is added after the option.value has been set
- The first select is wrappend in an element with display: inline or
display: inline-block,

This cannot be tested in a unit-test or e2e test.

Closes #11314
Closes #11795
2015-09-22 13:05:06 +02:00
Lucas Galfaso a7f3761eda fix($parse): block assigning to fields of a constructor
Throw when assigning to a field of a constructor.

Closes #12860
2015-09-22 10:44:27 +01:00
Jason Bedard 5a98e806ef fix($compile): use createMap() for $$observe listeners when initialized from attr interpolation
Closes #10446
2015-09-21 19:05:20 +01:00
Ivan Verevkin 808f984ec0 docs($cacheFactory): fix call to isUndefined() in example
Closes #12899
2015-09-21 15:51:49 +03:00
Lucas Mirelmann 698af191de fix($parse): do not convert to string computed properties multiple times
Do not convert to string properties multiple times.
2015-09-19 22:21:59 +02:00
Sjur Bakka 7a413df5e4 feat($http): add $xhrFactory service to enable creation of custom xhr objects
Closes #2318
Closes #9319
Closes #12159
2015-09-18 19:52:50 +01:00
Peter Bacon Darwin 4994acd26e fix(filters): ensure formatNumber observes i18n decimal separators
Closes #10342
Closes #12850
2015-09-18 13:45:29 +01:00
90 changed files with 10581 additions and 790 deletions
+2 -1
View File
@@ -48,7 +48,7 @@ install:
- npm config set loglevel http
- npm install -g npm@2.5
# Instal npm dependecies and ensure that npm cache is not stale
- scripts/npm/install-dependencies.sh
- npm install
before_script:
- mkdir -p $LOGS_DIR
@@ -61,6 +61,7 @@ script:
- ./scripts/travis/build.sh
after_script:
- ./scripts/travis/tear_down_browser_provider.sh
- ./scripts/travis/print_logs.sh
notifications:
+169 -1
View File
@@ -1,3 +1,146 @@
<a name="1.4.8"></a>
# 1.4.8 ice-manipulation (2015-11-19)
## Bug Fixes
- **$animate:** ensure leave animation calls `close` callback
([6bd6dbff](https://github.com/angular/angular.js/commit/6bd6dbff4961a601c03e9465442788781d329ba6),
[#12278](https://github.com/angular/angular.js/issues/12278), [#12096](https://github.com/angular/angular.js/issues/12096), [#13054](https://github.com/angular/angular.js/issues/13054))
- **$cacheFactory:** check key exists before decreasing cache size count
([2a5a52a7](https://github.com/angular/angular.js/commit/2a5a52a76ccf60c6e8c5d881e90e11a2666a6d3c),
[#12321](https://github.com/angular/angular.js/issues/12321), [#12329](https://github.com/angular/angular.js/issues/12329))
- **$compile:**
- bind all directive controllers correctly when using `bindToController`
([5d8861fb](https://github.com/angular/angular.js/commit/5d8861fb2f203e8a688b6044cbd1140cd79fd049),
[#11343](https://github.com/angular/angular.js/issues/11343), [#11345](https://github.com/angular/angular.js/issues/11345))
- evaluate against the correct scope with bindToController on new scope
([b9f7c453](https://github.com/angular/angular.js/commit/b9f7c453e00d6938106f414952f74d5e5fdcb993),
[#13021](https://github.com/angular/angular.js/issues/13021), [#13025](https://github.com/angular/angular.js/issues/13025))
- fix scoping of transclusion directives inside replace directive
([74da0340](https://github.com/angular/angular.js/commit/74da03407782d679951cd8f693860cea214f2580),
[#12975](https://github.com/angular/angular.js/issues/12975), [#12936](https://github.com/angular/angular.js/issues/12936), [#13244](https://github.com/angular/angular.js/issues/13244))
- **$http:** apply `transformResponse` even when `data` is empty
([c6909464](https://github.com/angular/angular.js/commit/c690946469e09cfe6b774e63dbe14ace92ce6cb7),
[#12976](https://github.com/angular/angular.js/issues/12976), [#12979](https://github.com/angular/angular.js/issues/12979))
- **$location:** ensure `$locationChangeSuccess` fires even if URL ends with `#`
([6f8ddb6d](https://github.com/angular/angular.js/commit/6f8ddb6d4329441e8d4a856978413aa9b9bd918f),
[#12175](https://github.com/angular/angular.js/issues/12175), [#13251](https://github.com/angular/angular.js/issues/13251))
- **$parse:** evaluate once simple expressions only once
([e4036824](https://github.com/angular/angular.js/commit/e403682444fa08af4f3491badf2f3a10d7595699),
[#12983](https://github.com/angular/angular.js/issues/12983), [#13002](https://github.com/angular/angular.js/issues/13002))
- **$resource:** allow XHR request to be cancelled via a timeout promise
([7170f9d9](https://github.com/angular/angular.js/commit/7170f9d9ca765c578f8d3eb4699860a9330a0a11),
[#12657](https://github.com/angular/angular.js/issues/12657), [#12675](https://github.com/angular/angular.js/issues/12675), [#10890](https://github.com/angular/angular.js/issues/10890), [#9332](https://github.com/angular/angular.js/issues/9332))
- **$rootScope:** prevent IE9 memory leak when destroying scopes
([87b0055c](https://github.com/angular/angular.js/commit/87b0055c80f40589c5bcf3765e59e872bcfae119),
[#10706](https://github.com/angular/angular.js/issues/10706), [#11786](https://github.com/angular/angular.js/issues/11786))
- **Angular.js:** fix `isArrayLike` for unusual cases
([70edec94](https://github.com/angular/angular.js/commit/70edec947c7b189694ae66b129568182e3369cab),
[#10186](https://github.com/angular/angular.js/issues/10186), [#8000](https://github.com/angular/angular.js/issues/8000), [#4855](https://github.com/angular/angular.js/issues/4855), [#4751](https://github.com/angular/angular.js/issues/4751), [#10272](https://github.com/angular/angular.js/issues/10272))
- **isArrayLike:** handle jQuery objects of length 0
([d3da55c4](https://github.com/angular/angular.js/commit/d3da55c40f1e1ddceced5da51e364888ff9d82ff))
- **jqLite:**
- deregister special `mouseenter` / `mouseleave` events correctly
([22f66025](https://github.com/angular/angular.js/commit/22f66025db262417ebb78c1ce1f4d7058dca3fd3),
[#12795](https://github.com/angular/angular.js/issues/12795), [#12799](https://github.com/angular/angular.js/issues/12799))
- ensure mouseenter works with svg elements on IE
([c1f34e8e](https://github.com/angular/angular.js/commit/c1f34e8eeb5105767f6cbf4727b8c5664be2a261),
[#10259](https://github.com/angular/angular.js/issues/10259), [#10276](https://github.com/angular/angular.js/issues/10276))
- **limitTo:** start at 0 if `begin` is negative and exceeds input length
([4fc40bc9](https://github.com/angular/angular.js/commit/4fc40bc9320a1d5902e648b70fa79c7cf7e794c7),
[#12775](https://github.com/angular/angular.js/issues/12775), [#12781](https://github.com/angular/angular.js/issues/12781))
- **merge:**
- ensure that jqlite->jqlite and DOM->DOM
([2f8db1bf](https://github.com/angular/angular.js/commit/2f8db1bf01173b546a2868fc7b8b188c2383fbff))
- clone elements instead of treating them like simple objects
([838cf4be](https://github.com/angular/angular.js/commit/838cf4be3c671903796dbb69d95c0e5ac1516a06),
[#12286](https://github.com/angular/angular.js/issues/12286))
- **ngAria:** don't add tabindex to radio and checkbox inputs
([59f1f4e1](https://github.com/angular/angular.js/commit/59f1f4e19a02e6e6f4c41c15b0e9f3372d85cecc),
[#12492](https://github.com/angular/angular.js/issues/12492), [#13095](https://github.com/angular/angular.js/issues/13095))
- **ngInput:** change URL_REGEXP to better match RFC3987
([cb51116d](https://github.com/angular/angular.js/commit/cb51116dbd225ccfdbc9a565a66a170e65d26331),
[#11341](https://github.com/angular/angular.js/issues/11341), [#11381](https://github.com/angular/angular.js/issues/11381))
- **ngMock:** reset cache before every test
([91b7cd9b](https://github.com/angular/angular.js/commit/91b7cd9b74d72a48d844c5c3e0e9dee03405e0ca),
[#13013](https://github.com/angular/angular.js/issues/13013))
- **ngOptions:**
- skip comments and empty options when looking for options
([0f58334b](https://github.com/angular/angular.js/commit/0f58334b7b9a9d3d6ff34e9754961b6f67731fae),
[#12190](https://github.com/angular/angular.js/issues/12190), [#13029](https://github.com/angular/angular.js/issues/13029), [#13033](https://github.com/angular/angular.js/issues/13033))
- override select option registration to allow compilation of empty option
([7b2ecf42](https://github.com/angular/angular.js/commit/7b2ecf42c697eb8d51a0f2d73b324bd900139e05),
[#11685](https://github.com/angular/angular.js/issues/11685), [#12972](https://github.com/angular/angular.js/issues/12972), [#12968](https://github.com/angular/angular.js/issues/12968), [#13012](https://github.com/angular/angular.js/issues/13012))
## Performance Improvements
- **$compile:** use static jquery data method to avoid creating new instances
([55ad192e](https://github.com/angular/angular.js/commit/55ad192e4ab79295ab15ecaaf8f6b9e7932a0336))
- **copy:**
- avoid regex in `isTypedArray`
([19fab4a1](https://github.com/angular/angular.js/commit/19fab4a1d79d2445795273f1622344353cf4d104))
- only validate/clear if the user specifies a destination
([d1293540](https://github.com/angular/angular.js/commit/d1293540e13573eb9ea5f90730bb9c9710c345db),
[#12068](https://github.com/angular/angular.js/issues/12068))
- **merge:** remove unnecessary wrapping of jqLite element
([ce6a96b0](https://github.com/angular/angular.js/commit/ce6a96b0d76dd2e5ab2247ca3059d284575bc6f0),
[#13236](https://github.com/angular/angular.js/issues/13236))
## Breaking Changes
<a name="1.4.7"></a>
# 1.4.7 dark-luminescence (2015-09-29)
## Bug Fixes
- **$compile:** use createMap() for $$observe listeners when initialized from attr interpolation
([5a98e806](https://github.com/angular/angular.js/commit/5a98e806ef3c59916bb4668268125610b11effe8),
[#10446](https://github.com/angular/angular.js/issues/10446))
- **$parse:**
- block assigning to fields of a constructor
([a7f3761e](https://github.com/angular/angular.js/commit/a7f3761eda5309f76b73c6fb1d3173a270112899),
[#12860](https://github.com/angular/angular.js/issues/12860))
- do not convert to string computed properties multiple times
([698af191](https://github.com/angular/angular.js/commit/698af191ded2465ca4e0f97959b75fede5a531ab))
- **filters:** ensure `formatNumber` observes i18n decimal separators
([4994acd2](https://github.com/angular/angular.js/commit/4994acd26e582eec8a92b139bfc09ca79a9b8835),
[#10342](https://github.com/angular/angular.js/issues/10342), [#12850](https://github.com/angular/angular.js/issues/12850))
- **jqLite:** properly handle dash-delimited node names in `jqLiteBuildFragment`
([cdd1227a](https://github.com/angular/angular.js/commit/cdd1227a308edd34d31b67f338083b6e0c4c0db9),
[#10617](https://github.com/angular/angular.js/issues/10617), [#12759](https://github.com/angular/angular.js/issues/12759))
- **ngAnimate:**
- ensure anchoring uses body as a container when needed
([9d3704ca](https://github.com/angular/angular.js/commit/9d3704ca467081f16b71b011eb50c53d5cdb2f34),
[#12872](https://github.com/angular/angular.js/issues/12872))
- callback detection should only use RAF when necessary
([fa8c399f](https://github.com/angular/angular.js/commit/fa8c399fadc30b78710868fe59d2930fdc17c7a5))
- **ngMessages:** prevent race condition with ngAnimate
([7295c60f](https://github.com/angular/angular.js/commit/7295c60ffb9f2e4f32043c538ace740b187f565a),
[#12856](https://github.com/angular/angular.js/issues/12856), [#12903](https://github.com/angular/angular.js/issues/12903))
- **ngOptions:**
- prevent frozen select ui in IE
([dbc69851](https://github.com/angular/angular.js/commit/dbc698517ff620b3a6279f65d4a9b6e3c15087b9),
[#11314](https://github.com/angular/angular.js/issues/11314), [#11795](https://github.com/angular/angular.js/issues/11795))
## Features
- **$animateCss:** add support for temporary styles via `cleanupStyles`
([e52d731b](https://github.com/angular/angular.js/commit/e52d731bfd1fbb6c616125fbde2fb365722254b7),
[#12930](https://github.com/angular/angular.js/issues/12930))
- **$http:** add `$xhrFactory` service to enable creation of custom xhr objects
([7a413df5](https://github.com/angular/angular.js/commit/7a413df5e47e04e20a1c93d35922050bbcbfb492),
[#2318](https://github.com/angular/angular.js/issues/2318), [#9319](https://github.com/angular/angular.js/issues/9319), [#12159](https://github.com/angular/angular.js/issues/12159))
## Breaking Changes
<a name="1.4.6"></a>
# 1.4.6 multiplicative-elevation (2015-09-17)
@@ -1340,7 +1483,6 @@ mechanism.
- **ngMessages:** due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
The `ngMessagesInclude` attribute is now its own directive and that must
be placed as a **child** element within the element with the ngMessages
directive. (Keep in mind that the former behaviour of the
@@ -1363,6 +1505,26 @@ end of the container containing the ngMessages directive).
</div>
```
- **ngMessages:** due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
it is no longer possible to use interpolation inside the `ngMessages` attribute expression. This technique
is generally not recommended, and can easily break when a directive implementation changes. In cases
where a simple expression is not possible, you can delegate accessing the object to a function:
```html
<div ng-messages="ctrl.form['field_{{$index}}'].$error">...</div>
```
would become
```html
<div ng-messages="ctrl.getMessages($index)">...</div>
```
where `ctrl.getMessages()`
```javascript
ctrl.getMessages = function($index) {
return ctrl.form['field_' + $index].$error;
}
```
- **$http:** due to [5da1256](https://github.com/angular/angular.js/commit/5da1256fc2812d5b28fb0af0de81256054856369),
`transformRequest` functions can no longer modify request headers.
@@ -1818,6 +1980,12 @@ But in practice this is not what people want and so this change iterates over pr
in the order they are returned by Object.keys(obj), which is almost always the order
in which the properties were defined.
- **ngOptions:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
setting the ngOptions attribute expression after the element is compiled, will no longer trigger the ngOptions behavior.
This worked previously because the ngOptions logic was part of the select directive, while
it is now implemented in the ngOptions directive itself.
- **select:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
the `select` directive will now use strict comparison of the `ngModel` scope value against `option`
+1 -1
View File
@@ -71,7 +71,7 @@ chances of your issue being dealt with quickly:
* **Angular Version(s)** - is it a regression?
* **Browsers and Operating System** - is this a problem with all browsers or only IE8?
* **Reproduce the Error** - provide a live example (using [Plunker][plunker] or
[JSFiddle][jsfiddle]) or a unambiguous set of steps.
[JSFiddle][jsfiddle]) or an unambiguous set of steps.
* **Related Issues** - has a similar issue been reported before?
* **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
causing the problem (line of code or commit)
+1 -1
View File
@@ -305,7 +305,7 @@ module.exports = function(grunt) {
shell: {
"npm-install": {
command: path.normalize('scripts/npm/install-dependencies.sh')
command: 'node scripts/npm/check-node-modules.js'
},
"promises-aplus-tests": {
+1 -1
View File
@@ -21,7 +21,7 @@ piece of cake. Best of all?? It makes development fun!
Building AngularJS
---------
[Once you have your environment setup](http://docs.angularjs.org/misc/contribute) just run:
[Once you have your environment set up](http://docs.angularjs.org/misc/contribute) just run:
grunt package
-1
View File
@@ -92,7 +92,6 @@ var angularFiles = {
'angularModules': {
'ngAnimate': [
'src/ngAnimate/shared.js',
'src/ngAnimate/body.js',
'src/ngAnimate/rafScheduler.js',
'src/ngAnimate/animateChildrenDirective.js',
'src/ngAnimate/animateCss.js',
+1 -1
View File
@@ -8,7 +8,7 @@ but the required directive controller is not present on the current DOM element
To resolve this error ensure that there is no typo in the required controller name and that the required directive controller is present on the current element.
If the required controller is expected to be on a ancestor element, make sure that you prefix the controller name in the `require` definition with `^`.
If the required controller is expected to be on an ancestor element, make sure that you prefix the controller name in the `require` definition with `^`.
If the required controller is optionally requested, use `?` or `^?` to specify that.
+7 -5
View File
@@ -356,15 +356,15 @@ legacy browsers and hashbang links in modern browser:
### Example
Here you can see two `$location` instances, both in **Html5 mode**, but on different browsers, so
that you can see the differences. These `$location` services are connected to a fake browsers. Each
input represents the address bar of the browser.
Here you can see two `$location` instances that show the difference between **Html5 mode** and **Html5 Fallback mode**.
Note that to simulate different levels of browser support, the `$location` instances are connected to
a fakeBrowser service, which you don't have to set up in actual projects.
Note that when you type hashbang url into first browser (or vice versa) it doesn't rewrite /
Note that when you type hashbang url into the first browser (or vice versa) it doesn't rewrite /
redirect to regular / hashbang url, as this conversion happens only during parsing the initial URL
= on page reload.
In these examples we use `<base href="/base/index.html" />`
In these examples we use `<base href="/base/index.html" />`. The inputs represent the address bar of the browser.
#### Browser in HTML5 mode
<example module="html5-mode" name="location-html5-mode">
@@ -389,6 +389,7 @@ In these examples we use `<base href="/base/index.html" />`
<file name="app.js">
angular.module('html5-mode', ['fake-browser', 'address-bar'])
// Configure the fakeBrowser. Do not set these values in actual projects.
.constant('initUrl', 'http://www.example.com/base/path?a=b#h')
.constant('baseHref', '/base/index.html')
.value('$sniffer', { history: true })
@@ -538,6 +539,7 @@ In these examples we use `<base href="/base/index.html" />`
<file name="app.js">
angular.module('hashbang-mode', ['fake-browser', 'address-bar'])
// Configure the fakeBrowser. Do not set these values in actual projects.
.constant('initUrl', 'http://www.example.com/base/index.html#!/path?a=b#h')
.constant('baseHref', '/base/index.html')
.value('$sniffer', { history: false })
+1 -2
View File
@@ -348,8 +348,7 @@ The following example shows how this is done with Angular:
return {
currencies: currencies,
convert: convert,
refresh: refresh
convert: convert
};
}]);
</file>
+16 -7
View File
@@ -43,8 +43,7 @@ mirrors the process of compiling source code in
Before we can write a directive, we need to know how Angular's {@link guide/compiler HTML compiler}
determines when to use a given directive.
Similar to the terminology used when an [element **matches** a selector]
(https://developer.mozilla.org/en-US/docs/Web/API/Element.matches), we say an element **matches** a
Similar to the terminology used when an [element **matches** a selector](https://developer.mozilla.org/en-US/docs/Web/API/Element.matches), we say an element **matches** a
directive when the directive is part of its declaration.
In the following example, we say that the `<input>` element **matches** the `ngModel` directive
@@ -588,14 +587,24 @@ want to reuse throughout your app.
In this example we will build a directive that displays the current time.
Once a second, it updates the DOM to reflect the current time.
Directives that want to modify the DOM typically use the `link` option.
`link` takes a function with the following signature, `function link(scope, element, attrs) { ... }`
where:
Directives that want to modify the DOM typically use the `link` option to register DOM listeners
as well as update the DOM. It is executed after the template has been cloned and is where
directive logic will be put.
`link` takes a function with the following signature,
`function link(scope, element, attrs, controller, transcludeFn) { ... }`, where:
* `scope` is an Angular scope object.
* `element` is the jqLite-wrapped element that this directive matches.
* `attrs` is a hash object with key-value pairs of normalized attribute names and their
corresponding attribute values.
* `controller` is the directive's required controller instance(s) or its own controller (if any).
The exact value depends on the directive's require property.
* `transcludeFn` is a transclude linking function pre-bound to the correct transclusion scope.
<div class="alert alert-info">
For more details on the `link` option refer to the {@link ng.$compile#-link- `$compile` API} page.
</div>
In our `link` function, we want to update the displayed time once a second, or whenever a user
changes the time formatting string that our directive binds to. We will use the `$interval` service
@@ -903,7 +912,7 @@ to which tab is active.
restrict: 'E',
transclude: true,
scope: {},
controller: function($scope) {
controller: ['$scope', function($scope) {
var panes = $scope.panes = [];
$scope.select = function(pane) {
@@ -919,7 +928,7 @@ to which tab is active.
}
panes.push(pane);
};
},
}],
templateUrl: 'my-tabs.html'
};
})
+1 -1
View File
@@ -383,7 +383,7 @@ In the following example we create two directives:
return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
var usernames = ['Jim', 'John', 'Jill', 'Jackie'];
ctrl.$asyncValidators.username = function(modelValue, viewValue) {
+23
View File
@@ -170,6 +170,25 @@ other inline messages situated as children within the `ngMessages` container dir
Depending on where the `ngMessagesInclude` directive is placed it will be prioritized inline with the other messages
before and after it.
Also due to [c9a4421f](https://github.com/angular/angular.js/commit/c9a4421fc3c97448527eadef1f42eb2f487ec2e0),
it is no longer possible to use interpolation inside the `ngMessages` attribute expression. This technique
is generally not recommended, and can easily break when a directive implementation changes. In cases
where a simple expression is not possible, you can delegate accessing the object to a function:
```html
<div ng-messages="ctrl.form['field_{{$index}}'].$error">...</div>
```
would become
```html
<div ng-messages="ctrl.getMessages($index)">...</div>
```
where `ctrl.getMessages()`
```javascript
ctrl.getMessages = function($index) {
return ctrl.form['field_' + $index].$error;
}
```
### ngOptions
The `ngOptions` directive has also been refactored and as a result some long-standing bugs
@@ -189,6 +208,10 @@ But in practice this is not what people want and so this change iterates over pr
in the order they are returned by Object.keys(obj), which is almost always the order
in which the properties were defined.
Also due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
setting the ngOptions attribute expression after the element is compiled, will no longer trigger the ngOptions behavior.
This worked previously because the ngOptions logic was part of the select directive, while
it is now implemented in the ngOptions directive itself.
### select
+1 -1
View File
@@ -44,7 +44,7 @@ and {@link angular.reloadWithDebugInfo `angular.reloadWithDebugInfo`}.
## Strict DI Mode
Using strict di mode in your production application will throw errors when a injectable
Using strict di mode in your production application will throw errors when an injectable
function is not
{@link di#dependency-annotation annotated explicitly}. Strict di mode is intended to help
you make sure that your code will work when minified. However, it also will force you to
+1 -1
View File
@@ -391,7 +391,7 @@ implementing custom event callbacks, or when working with third-party library ca
5. The {@link ng.$rootScope.Scope#$watch $watch} list is a set of expressions
which may have changed since last iteration. If a change is detected then the `$watch`
function is called which typically updates the DOM with the new value.
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes
6. Once the Angular {@link ng.$rootScope.Scope#$digest $digest} loop finishes,
the execution leaves the Angular and JavaScript context. This is followed by the browser
re-rendering the DOM to reflect any changes.
+5 -3
View File
@@ -6,12 +6,14 @@
<ul doc-tutorial-nav="0"></ul>
You are now ready to build the AngularJS phonecat app. In this step, you will become familiar
with the most important source code files, learn how to start the development servers bundled with
In this step of the tutorial, you will become familiar with the most important source code files of
the AngularJS phonecat app. You will also learn how to start the development servers bundled with
angular-seed, and run the application in the browser.
Before you continue, make sure you have set up your development environment and installed all necessary
dependencies, as described in {@link tutorial/index#get-started Get Started}.
In `angular-phonecat` directory, run this command:
In the `angular-phonecat` directory, run this command:
```
git checkout -f step-0
+14 -3
View File
@@ -195,8 +195,19 @@ to ensure that Karma and its necessary plugins are installed. You can do this by
To run the tests, and then watch the files for changes: `npm test`.
* Karma will start a new instance of Chrome browser automatically. Just ignore it and let it run in
the background. Karma will use this browser for test execution.
* Karma will start new instances of Chrome and Firefox browsers automatically. Just ignore them and
let them run in the background. Karma will use these browsers for test execution.
* If you only have one of the browsers installed on your machine (either Chrome or Firefox), make
sure to update the karma configuration file before running the test. Locate the configuration file
in `test/karma.conf.js`, then update the `browsers` property.
E.g. if you only have Chrome installed:
<pre>
...
browsers: ['Chrome'],
...
</pre>
* You should see the following or similar output in the terminal:
<pre>
@@ -250,7 +261,7 @@ browser is limited, which results in your karma tests running extremely slow.
<tr><th>row number</th></tr>
<tr ng-repeat="i in [0, 1, 2, 3, 4, 5, 6, 7]"><td>{{i+1}}</td></tr>
</table>
Extra points: try and make an 8x8 table using an additional `ng-repeat`.
* Make the unit test fail by changing `expect(scope.phones.length).toBe(3)` to instead use `toBe(4)`.
+2 -2
View File
@@ -37,12 +37,12 @@ We are using [Bower][bower] to install client-side dependencies. This step upda
"angular-mocks": "1.4.x",
"jquery": "~2.1.1",
"bootstrap": "~3.1.1",
"angular-route": "~1.4.0"
"angular-route": "1.4.x"
}
}
```
The new dependency `"angular-route": "~1.4.0"` tells bower to install a version of the
The new dependency `"angular-route": "1.4.x"` tells bower to install a version of the
angular-route component that is compatible with version 1.4.x. We must tell bower to download
and install this dependency.
+7 -6
View File
@@ -32,17 +32,18 @@ We are using [Bower][bower] to install client side dependencies. This step upda
"license": "MIT",
"private": true,
"dependencies": {
"angular": "~1.3.0",
"angular-mocks": "~1.3.0",
"angular": "1.4.x",
"angular-mocks": "1.4.x",
"jquery": "~2.1.1",
"bootstrap": "~3.1.1",
"angular-route": "~1.3.0",
"angular-resource": "~1.3.0"
"angular-route": "1.4.x",
"angular-resource": "1.4.x"
}
}
```
The new dependency `"angular-resource": "~1.3.0"` tells bower to install a version of the
angular-resource component that is compatible with version 1.3.x. We must ask bower to download
The new dependency `"angular-resource": "1.4.x"` tells bower to install a version of the
angular-resource component that is compatible with version 1.4.x. We must ask bower to download
and install this dependency. We can do this by running:
```
+13 -13
View File
@@ -36,20 +36,20 @@ We are using [Bower][bower] to install client side dependencies. This step upda
"license": "MIT",
"private": true,
"dependencies": {
"angular": "~1.3.0",
"angular-mocks": "~1.3.0",
"bootstrap": "~3.1.1",
"angular-route": "~1.3.0",
"angular-resource": "~1.3.0",
"angular": "1.4.x",
"angular-mocks": "1.4.x",
"jquery": "~2.1.1",
"angular-animate": "~1.3.0"
"bootstrap": "~3.1.1",
"angular-route": "1.4.x",
"angular-resource": "1.4.x",
"angular-animate": "1.4.x"
}
}
```
* `"angular-animate": "~1.3.0"` tells bower to install a version of the
angular-animate component that is compatible with version 1.3.x.
* `"jquery": "2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
* `"angular-animate": "1.4.x"` tells bower to install a version of the
angular-animate component that is compatible with version 1.4.x.
* `"jquery": "~2.1.1"` tells bower to install the 2.1.1 version of jQuery. Note that this is not an
Angular library, it is the standard jQuery library. We can use bower to install a wide range of 3rd
party libraries.
@@ -111,7 +111,7 @@ __`app/index.html`.__
```
<div class="alert alert-error">
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.3; jQuery 1.x is
**Important:** Be sure to use jQuery version 2.1 or newer when using Angular 1.4; jQuery 1.x is
not officially supported.
Be sure to load jQuery before all AngularJS scripts, otherwise AngularJS won't detect jQuery and
animations will not work as expected.
@@ -239,9 +239,9 @@ The name of the starting class is the name of the event that is fired (like `ent
The active class name is the same as the starting class's but with an `-active` suffix.
This two-class CSS naming convention allows the developer to craft an animation, beginning to end.
In our example above, elements expand from a height of **0** to **120 pixels** when items are added or moved,
around and collapsing the items before removing them from the list.
There's also a nice fade-in and fade-out effect that also occurs at the same time. All of this is handled
In our example above, elements are expanded from a height of **0** to **120 pixels** when they're added to the
list and are collapsed back down to **0 pixels** before being removed from the list.
There's also a nice fade-in and fade-out effect that occurs at the same time. All of this is handled
by the CSS transition declarations at the top of the example code above.
Although most modern browsers have good support for [CSS transitions](http://caniuse.com/#feat=css-transitions)
+8
View File
@@ -0,0 +1,8 @@
#!/bin/bash
set -e -o pipefail
echo "Shutting down Browserstack tunnel"
echo "TODO: implement me"
exit 1
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
set -e -o pipefail
echo "Shutting down Sauce Connect tunnel"
killall sc
while [[ -n `ps -ef | grep "sauce-connect-" | grep -v "grep"` ]]; do
printf "."
sleep .5
done
echo ""
echo "Sauce Connect tunnel has been shut down"
+3134 -9
View File
File diff suppressed because it is too large Load Diff
+4868 -56
View File
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -13,6 +13,11 @@
"npm": "~2.5"
},
"engineStrict": true,
"scripts": {
"preinstall": "node scripts/npm/check-node-modules.js --purge",
"postinstall": "node scripts/npm/copy-npm-shrinkwrap.js",
"commit": "git-cz"
},
"devDependencies": {
"angular-benchpress": "0.x.x",
"benchmark": "1.x.x",
@@ -20,8 +25,10 @@
"browserstacktunnel-wrapper": "~1.3.1",
"canonical-path": "0.0.2",
"cheerio": "^0.17.0",
"commitizen": "^2.3.0",
"cz-conventional-changelog": "^1.1.4",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.10.0",
"dgeni-packages": "^0.11.0",
"event-stream": "~3.1.0",
"grunt": "~0.4.2",
"grunt-bump": "~0.0.13",
@@ -76,5 +83,10 @@
"url": "https://github.com/angular/angular.js/blob/master/LICENSE"
}
],
"dependencies": {}
"dependencies": {},
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ config.specs = [
];
config.capabilities = {
browserName: 'chrome',
browserName: 'chrome'
};
exports.config = config;
+74
View File
@@ -0,0 +1,74 @@
// Implementation based on:
// https://github.com/angular/angular/blob/3b9c08676a4c921bbfa847802e08566fb601ba7a/tools/npm/check-node-modules.js
'use strict';
// Imports
var fs = require('fs');
var path = require('path');
// Constants
var PROJECT_ROOT = path.join(__dirname, '../../');
var NODE_MODULES_DIR = 'node_modules';
var NPM_SHRINKWRAP_FILE = 'npm-shrinkwrap.json';
var NPM_SHRINKWRAP_CACHED_FILE = NODE_MODULES_DIR + '/npm-shrinkwrap.cached.json';
// Run
_main();
// Functions - Definitions
function _main() {
var purgeIfStale = process.argv.indexOf('--purge') !== -1;
process.chdir(PROJECT_ROOT);
checkNodeModules(purgeIfStale);
}
function checkNodeModules(purgeIfStale) {
var nodeModulesOk = compareMarkerFiles(NPM_SHRINKWRAP_FILE, NPM_SHRINKWRAP_CACHED_FILE);
if (nodeModulesOk) {
console.log(':-) npm dependencies are looking good!');
} else if (purgeIfStale) {
console.log(':-( npm dependencies are stale or in an unknown state!');
console.log(' Purging \'' + NODE_MODULES_DIR + '\'...');
deleteDirSync(NODE_MODULES_DIR);
} else {
var separator = new Array(81).join('!');
console.warn(separator);
console.warn(':-( npm dependencies are stale or in an unknown state!');
console.warn('You can rebuild the dependencies by running `npm install`.');
console.warn(separator);
}
return nodeModulesOk;
}
function compareMarkerFiles(markerFilePath, cachedMarkerFilePath) {
if (!fs.existsSync(cachedMarkerFilePath)) return false;
var opts = {encoding: 'utf-8'};
var markerContent = fs.readFileSync(markerFilePath, opts);
var cachedMarkerContent = fs.readFileSync(cachedMarkerFilePath, opts);
return markerContent === cachedMarkerContent;
}
// Custom implementation of `rm -rf` that works consistently across OSes
function deleteDirSync(path) {
if (fs.existsSync(path)) {
fs.readdirSync(path).forEach(deleteDirOrFileSync);
fs.rmdirSync(path);
}
// Helpers
function deleteDirOrFileSync(subpath) {
var curPath = path + '/' + subpath;
if (fs.lstatSync(curPath).isDirectory()) {
deleteDirSync(curPath);
} else {
fs.unlinkSync(curPath);
}
}
}
+60
View File
@@ -0,0 +1,60 @@
'use strict';
// Imports
var fs = require('fs');
var path = require('path');
// Constants
var PROJECT_ROOT = path.join(__dirname, '../../');
var NODE_MODULES_DIR = 'node_modules';
var NPM_SHRINKWRAP_FILE = 'npm-shrinkwrap.json';
var NPM_SHRINKWRAP_CACHED_FILE = NODE_MODULES_DIR + '/npm-shrinkwrap.cached.json';
// Run
_main();
// Functions - Definitions
function _main() {
process.chdir(PROJECT_ROOT);
copyFile(NPM_SHRINKWRAP_FILE, NPM_SHRINKWRAP_CACHED_FILE, onCopied);
}
// Implementation based on:
// https://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js#answer-21995878
function copyFile(srcPath, dstPath, callback) {
var callbackCalled = false;
if (!fs.existsSync(srcPath)) {
done(new Error('Missing source file: ' + srcPath));
return;
}
var rs = fs.createReadStream(srcPath);
rs.on('error', done);
var ws = fs.createWriteStream(dstPath);
ws.on('error', done);
ws.on('finish', done);
rs.pipe(ws);
// Helpers
function done(err) {
if (callback && !callbackCalled) {
callbackCalled = true;
callback(err);
}
}
}
function onCopied(err) {
if (err) {
var separator = new Array(81).join('!');
console.error(separator);
console.error(
'Failed to copy `' + NPM_SHRINKWRAP_FILE + '` to `' + NPM_SHRINKWRAP_CACHED_FILE + '`:');
console.error(err);
console.error(separator);
}
}
-16
View File
@@ -1,16 +0,0 @@
#!/bin/bash
set -e
SHRINKWRAP_FILE=npm-shrinkwrap.json
SHRINKWRAP_CACHED_FILE=node_modules/npm-shrinkwrap.cached.json
if diff -q $SHRINKWRAP_FILE $SHRINKWRAP_CACHED_FILE; then
echo 'No shrinkwrap changes detected. npm install will be skipped...';
else
echo 'Blowing away node_modules and reinstalling npm dependencies...'
rm -rf node_modules
npm install
cp $SHRINKWRAP_FILE $SHRINKWRAP_CACHED_FILE
echo 'npm install successful!'
fi
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
# Has to be run from project root directory.
./lib/${BROWSER_PROVIDER}/teardown_tunnel.sh
+113 -94
View File
@@ -198,20 +198,24 @@ msie = document.documentMode;
* String ...)
*/
function isArrayLike(obj) {
if (obj == null || isWindow(obj)) {
return false;
}
// `null`, `undefined` and `window` are not array-like
if (obj == null || isWindow(obj)) return false;
// arrays, strings and jQuery/jqLite objects are array like
// * jqLite is either the jQuery or jqLite constructor function
// * we have to check the existance of jqLite first as this method is called
// via the forEach method when constructing the jqLite object in the first place
if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true;
// Support: iOS 8.2 (not reproducible in simulator)
// "length" in obj used to prevent JIT error (gh-11508)
var length = "length" in Object(obj) && obj.length;
if (obj.nodeType === NODE_TYPE_ELEMENT && length) {
return true;
}
return isString(obj) || isArray(obj) || length === 0 ||
typeof length === 'number' && length > 0 && (length - 1) in obj;
// NodeList objects (with `item` method) and
// other objects with suitable length characteristics are array-like
return isNumber(length) &&
(length >= 0 && (length - 1) in obj || typeof obj.item == 'function');
}
/**
@@ -356,6 +360,10 @@ function baseExtend(dst, objs, deep) {
dst[key] = new Date(src.valueOf());
} else if (isRegExp(src)) {
dst[key] = new RegExp(src);
} else if (src.nodeName) {
dst[key] = src.cloneNode(true);
} else if (isElement(src)) {
dst[key] = src.clone();
} else {
if (!isObject(dst[key])) dst[key] = isArray(src) ? [] : {};
baseExtend(dst[key], [src], true);
@@ -471,7 +479,7 @@ identity.$inject = [];
function valueFn(value) {return function() {return value;};}
function hasCustomToString(obj) {
return isFunction(obj.toString) && obj.toString !== Object.prototype.toString;
return isFunction(obj.toString) && obj.toString !== toString;
}
@@ -670,9 +678,9 @@ function isPromiseLike(obj) {
}
var TYPED_ARRAY_REGEXP = /^\[object (Uint8(Clamped)?)|(Uint16)|(Uint32)|(Int8)|(Int16)|(Int32)|(Float(32)|(64))Array\]$/;
var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
function isTypedArray(value) {
return TYPED_ARRAY_REGEXP.test(toString.call(value));
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
}
@@ -794,100 +802,111 @@ function arrayRemove(array, value) {
</file>
</example>
*/
function copy(source, destination, stackSource, stackDest) {
if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
"Can't copy! Making copies of Window or Scope instances is not supported.");
}
if (isTypedArray(destination)) {
throw ngMinErr('cpta',
"Can't copy! TypedArray destination cannot be mutated.");
}
function copy(source, destination) {
var stackSource = [];
var stackDest = [];
if (!destination) {
destination = source;
if (isObject(source)) {
var index;
if (stackSource && (index = stackSource.indexOf(source)) !== -1) {
return stackDest[index];
}
// TypedArray, Date and RegExp have specific copy functionality and must be
// pushed onto the stack before returning.
// Array and other objects create the base object and recurse to copy child
// objects. The array/object will be pushed onto the stack when recursed.
if (isArray(source)) {
return copy(source, [], stackSource, stackDest);
} else if (isTypedArray(source)) {
destination = new source.constructor(source);
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isRegExp(source)) {
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
destination.lastIndex = source.lastIndex;
} else if (isFunction(source.cloneNode)) {
destination = source.cloneNode(true);
} else {
var emptyObject = Object.create(getPrototypeOf(source));
return copy(source, emptyObject, stackSource, stackDest);
}
if (stackDest) {
stackSource.push(source);
stackDest.push(destination);
}
if (destination) {
if (isTypedArray(destination)) {
throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
}
} else {
if (source === destination) throw ngMinErr('cpi',
"Can't copy! Source and destination are identical.");
stackSource = stackSource || [];
stackDest = stackDest || [];
if (isObject(source)) {
stackSource.push(source);
stackDest.push(destination);
if (source === destination) {
throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
}
// Empty the destination object
if (isArray(destination)) {
destination.length = 0;
} else {
forEach(destination, function(value, key) {
if (key !== '$$hashKey') {
delete destination[key];
}
});
}
stackSource.push(source);
stackDest.push(destination);
return copyRecurse(source, destination);
}
return copyElement(source);
function copyRecurse(source, destination) {
var h = destination.$$hashKey;
var result, key;
if (isArray(source)) {
destination.length = 0;
for (var i = 0; i < source.length; i++) {
destination.push(copy(source[i], null, stackSource, stackDest));
for (var i = 0, ii = source.length; i < ii; i++) {
destination.push(copyElement(source[i]));
}
} else if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
destination[key] = copyElement(source[key]);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = copyElement(source[key]);
}
}
} else {
var h = destination.$$hashKey;
if (isArray(destination)) {
destination.length = 0;
} else {
forEach(destination, function(value, key) {
delete destination[key];
});
}
if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
destination[key] = copy(source[key], null, stackSource, stackDest);
}
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
destination[key] = copyElement(source[key]);
}
}
setHashKey(destination,h);
}
setHashKey(destination, h);
return destination;
}
function copyElement(source) {
// Simple values
if (!isObject(source)) {
return source;
}
// Already copied values
var index = stackSource.indexOf(source);
if (index !== -1) {
return stackDest[index];
}
if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
"Can't copy! Making copies of Window or Scope instances is not supported.");
}
var needsRecurse = false;
var destination;
if (isArray(source)) {
destination = [];
needsRecurse = true;
} else if (isTypedArray(source)) {
destination = new source.constructor(source);
} else if (isDate(source)) {
destination = new Date(source.getTime());
} else if (isRegExp(source)) {
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
destination.lastIndex = source.lastIndex;
} else if (isFunction(source.cloneNode)) {
destination = source.cloneNode(true);
} else {
destination = Object.create(getPrototypeOf(source));
needsRecurse = true;
}
stackSource.push(source);
stackDest.push(destination);
return needsRecurse
? copyRecurse(source, destination)
: destination;
}
return destination;
}
/**
+2
View File
@@ -72,6 +72,7 @@
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
$xhrFactoryProvider,
$LocationProvider,
$LogProvider,
$ParseProvider,
@@ -230,6 +231,7 @@ function publishExternalAPI(angular) {
$httpParamSerializer: $HttpParamSerializerProvider,
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
$xhrFactory: $xhrFactoryProvider,
$location: $LocationProvider,
$log: $LogProvider,
$parse: $ParseProvider,
+60 -34
View File
@@ -241,6 +241,14 @@ function jqLiteParseHTML(html, context) {
return [];
}
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
var jqLiteContains = Node.prototype.contains || function(arg) {
// jshint bitwise: false
return !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
};
/////////////////////////////////////////////
function JQLite(element) {
if (element instanceof JQLite) {
@@ -299,17 +307,23 @@ function jqLiteOff(element, type, fn, unsupported) {
delete events[type];
}
} else {
forEach(type.split(' '), function(type) {
if (isDefined(fn)) {
var listenerFns = events[type];
arrayRemove(listenerFns || [], fn);
if (listenerFns && listenerFns.length > 0) {
return;
}
}
removeEventListenerFn(element, type, handle);
delete events[type];
var removeHandler = function(type) {
var listenerFns = events[type];
if (isDefined(fn)) {
arrayRemove(listenerFns || [], fn);
}
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
removeEventListenerFn(element, type, handle);
delete events[type];
}
};
forEach(type.split(' '), function(type) {
removeHandler(type);
if (MOUSE_EVENT_MAP[type]) {
removeHandler(MOUSE_EVENT_MAP[type]);
}
});
}
}
@@ -764,6 +778,9 @@ function createEventHandler(element, events) {
return event.immediatePropagationStopped === true;
};
// Some events have special handlers that wrap the real handler
var handlerWrapper = eventFns.specialHandlerWrapper || defaultHandlerWrapper;
// Copy event handlers in case event handlers array is modified during execution.
if ((eventFnsLength > 1)) {
eventFns = shallowCopy(eventFns);
@@ -771,7 +788,7 @@ function createEventHandler(element, events) {
for (var i = 0; i < eventFnsLength; i++) {
if (!event.isImmediatePropagationStopped()) {
eventFns[i].call(element, event);
handlerWrapper(element, event, eventFns[i]);
}
}
};
@@ -782,6 +799,22 @@ function createEventHandler(element, events) {
return eventHandler;
}
function defaultHandlerWrapper(element, event, handler) {
handler.call(element, event);
}
function specialMouseHandlerWrapper(target, event, handler) {
// Refer to jQuery's implementation of mouseenter & mouseleave
// Read about mouseenter and mouseleave:
// http://www.quirksmode.org/js/events_mouse.html#link8
var related = event.relatedTarget;
// For mousenter/leave call the handler if related is outside the target.
// NB: No relatedTarget if the mouse left/entered the browser window
if (!related || (related !== target && !jqLiteContains.call(target, related))) {
handler.call(target, event);
}
}
//////////////////////////////////////////
// Functions iterating traversal.
// These functions chain results into a single
@@ -810,35 +843,28 @@ forEach({
var types = type.indexOf(' ') >= 0 ? type.split(' ') : [type];
var i = types.length;
while (i--) {
type = types[i];
var addHandler = function(type, specialHandlerWrapper, noEventListener) {
var eventFns = events[type];
if (!eventFns) {
events[type] = [];
if (type === 'mouseenter' || type === 'mouseleave') {
// Refer to jQuery's implementation of mouseenter & mouseleave
// Read about mouseenter and mouseleave:
// http://www.quirksmode.org/js/events_mouse.html#link8
jqLiteOn(element, MOUSE_EVENT_MAP[type], function(event) {
var target = this, related = event.relatedTarget;
// For mousenter/leave call the handler if related is outside the target.
// NB: No relatedTarget if the mouse left/entered the browser window
if (!related || (related !== target && !target.contains(related))) {
handle(event, type);
}
});
} else {
if (type !== '$destroy') {
addEventListenerFn(element, type, handle);
}
eventFns = events[type] = [];
eventFns.specialHandlerWrapper = specialHandlerWrapper;
if (type !== '$destroy' && !noEventListener) {
addEventListenerFn(element, type, handle);
}
eventFns = events[type];
}
eventFns.push(fn);
};
while (i--) {
type = types[i];
if (MOUSE_EVENT_MAP[type]) {
addHandler(MOUSE_EVENT_MAP[type], specialMouseHandlerWrapper);
addHandler(type, undefined, true);
} else {
addHandler(type);
}
}
},
+1 -1
View File
@@ -188,7 +188,7 @@ function setupModuleLoader(window) {
* @param {string} name constant name
* @param {*} object Constant value.
* @description
* Because the constant are fixed, they get applied before other provide methods.
* Because the constants are fixed, they get applied before other provide methods.
* See {@link auto.$provide#constant $provide.constant()}.
*/
constant: invokeLater('$provide', 'constant', 'unshift'),
+1 -1
View File
@@ -41,7 +41,7 @@ function $AnchorScrollProvider() {
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
* in the
* [HTML5 spec](http://dev.w3.org/html5/spec/Overview.html#the-indicated-part-of-the-document).
* [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
*
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
* match any anchor whenever it changes. This can be disabled by calling
+1 -1
View File
@@ -285,7 +285,7 @@ var $AnimateProvider = ['$provide', function($provide) {
* when an animation is detected (and animations are enabled), $animate will do the heavy lifting
* to ensure that animation runs with the triggered DOM operation.
*
* By default $animate doesn't trigger an animations. This is because the `ngAnimate` module isn't
* By default $animate doesn't trigger any animations. This is because the `ngAnimate` module isn't
* included and only when it is active then the animation hooks that `$animate` triggers will be
* functional. Once active then all structural `ng-` directives will trigger animations as they perform
* their DOM-related operations (enter, leave and move). Other directives such as `ngClass`,
+7
View File
@@ -43,6 +43,13 @@ var $CoreAnimateCssProvider = function() {
};
return function(element, options) {
// there is no point in applying the styles since
// there is no animation that goes on at all in
// this version of $animateCss.
if (options.cleanupStyles) {
options.from = options.to = null;
}
if (options.from) {
element.css(options.from);
options.from = null;
+8 -7
View File
@@ -67,10 +67,10 @@
$scope.keys = [];
$scope.cache = $cacheFactory('cacheId');
$scope.put = function(key, value) {
if (isUndefined($scope.cache.get(key))) {
if (angular.isUndefined($scope.cache.get(key))) {
$scope.keys.push(key);
}
$scope.cache.put(key, isUndefined(value) ? null : value);
$scope.cache.put(key, angular.isUndefined(value) ? null : value);
};
}]);
</file>
@@ -93,9 +93,9 @@ function $CacheFactoryProvider() {
var size = 0,
stats = extend({}, options, {id: cacheId}),
data = {},
data = createMap(),
capacity = (options && options.capacity) || Number.MAX_VALUE,
lruHash = {},
lruHash = createMap(),
freshEnd = null,
staleEnd = null;
@@ -223,6 +223,8 @@ function $CacheFactoryProvider() {
delete lruHash[key];
}
if (!(key in data)) return;
delete data[key];
size--;
},
@@ -237,9 +239,9 @@ function $CacheFactoryProvider() {
* Clears the cache object of any entries.
*/
removeAll: function() {
data = {};
data = createMap();
size = 0;
lruHash = {};
lruHash = createMap();
freshEnd = staleEnd = null;
},
@@ -399,4 +401,3 @@ function $TemplateCacheProvider() {
return $cacheFactory('templates');
}];
}
+80 -81
View File
@@ -1240,6 +1240,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
},
NG_ATTR_BINDING = /^ngAttr[A-Z]/;
var MULTI_ELEMENT_DIR_RE = /^(.+)Start$/;
compile.$$addBindingInfo = debugInfoEnabled ? function $$addBindingInfo($element, binding) {
var bindings = $element.data('$binding') || [];
@@ -1292,6 +1293,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return function publicLinkFn(scope, cloneConnectFn, options) {
assertArg(scope, 'scope');
if (previousCompileContext && previousCompileContext.needsNewScope) {
// A parent directive did a replace and a directive on this element asked
// for transclusion, which caused us to lose a layer of element on which
// we could hold the new transclusion scope, so we will create it manually
// here.
scope = scope.$parent.$new();
}
options = options || {};
var parentBoundTranscludeFn = options.parentBoundTranscludeFn,
transcludeControllers = options.transcludeControllers,
@@ -1437,11 +1446,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (nodeLinkFn.scope) {
childScope = scope.$new();
compile.$$addScopeInfo(jqLite(node), childScope);
var destroyBindings = nodeLinkFn.$$destroyBindings;
if (destroyBindings) {
nodeLinkFn.$$destroyBindings = null;
childScope.$on('$destroyed', destroyBindings);
}
} else {
childScope = scope;
}
@@ -1460,8 +1464,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = null;
}
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn,
nodeLinkFn);
nodeLinkFn(childLinkFn, childScope, node, $rootElement, childBoundTranscludeFn);
} else if (childLinkFn) {
childLinkFn(scope, node.childNodes, undefined, parentBoundTranscludeFn);
@@ -1530,13 +1533,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
}
var directiveNName = ngAttrName.replace(/(Start|End)$/, '');
if (directiveIsMultiElement(directiveNName)) {
if (ngAttrName === directiveNName + 'Start') {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}
var multiElementMatch = ngAttrName.match(MULTI_ELEMENT_DIR_RE);
if (multiElementMatch && directiveIsMultiElement(multiElementMatch[1])) {
attrStartName = name;
attrEndName = name.substr(0, name.length - 5) + 'end';
name = name.substr(0, name.length - 6);
}
nName = directiveNormalize(name.toLowerCase());
@@ -1775,7 +1776,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
$template = jqLite(jqLiteClone(compileNode)).contents();
$compileNode.empty(); // clear contents
childTranscludeFn = compile($template, transcludeFn);
childTranscludeFn = compile($template, transcludeFn, undefined,
undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
}
}
@@ -1817,8 +1819,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], newTemplateAttrs);
var unprocessedDirectives = directives.splice(i + 1, directives.length - (i + 1));
if (newIsolateScopeDirective) {
markDirectivesAsIsolate(templateDirectives);
if (newIsolateScopeDirective || newScopeDirective) {
// The original directive caused the current element to be replaced but this element
// also needs to have a new scope, so we need to tell the template directives
// that they would need to get their scope from further up, if they require transclusion
markDirectiveScope(templateDirectives, newIsolateScopeDirective, newScopeDirective);
}
directives = directives.concat(templateDirectives).concat(unprocessedDirectives);
mergeTemplateAttributes(templateAttrs, newTemplateAttrs);
@@ -1971,10 +1976,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return elementControllers;
}
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn,
thisLinkFn) {
var i, ii, linkFn, controller, isolateScope, elementControllers, transcludeFn, $element,
attrs;
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
attrs, removeScopeBindingWatches, removeControllerBindingWatches;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -1984,8 +1988,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrs = new Attributes($element, templateAttrs);
}
controllerScope = scope;
if (newIsolateScopeDirective) {
isolateScope = scope.$new(true);
} else if (newScopeDirective) {
controllerScope = scope.$parent;
}
if (boundTranscludeFn) {
@@ -2006,42 +2013,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
initializeDirectiveBindings(scope, attrs, isolateScope,
isolateScope.$$isolateBindings,
newIsolateScopeDirective, isolateScope);
}
if (elementControllers) {
// Initialize bindToController bindings for new/isolate scopes
var scopeDirective = newIsolateScopeDirective || newScopeDirective;
var bindings;
var controllerForBindings;
if (scopeDirective && elementControllers[scopeDirective.name]) {
bindings = scopeDirective.$$bindings.bindToController;
controller = elementControllers[scopeDirective.name];
if (controller && controller.identifier && bindings) {
controllerForBindings = controller;
thisLinkFn.$$destroyBindings =
initializeDirectiveBindings(scope, attrs, controller.instance,
bindings, scopeDirective);
}
removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
isolateScope.$$isolateBindings,
newIsolateScopeDirective);
if (removeScopeBindingWatches) {
isolateScope.$on('$destroy', removeScopeBindingWatches);
}
for (i in elementControllers) {
controller = elementControllers[i];
var controllerResult = controller();
}
if (controllerResult !== controller.instance) {
// If the controller constructor has a return value, overwrite the instance
// from setupControllers and update the element data
controller.instance = controllerResult;
$element.data('$' + i + 'Controller', controllerResult);
if (controller === controllerForBindings) {
// Remove and re-install bindToController bindings
thisLinkFn.$$destroyBindings();
thisLinkFn.$$destroyBindings =
initializeDirectiveBindings(scope, attrs, controllerResult, bindings, scopeDirective);
}
}
// Initialize bindToController bindings
for (var name in elementControllers) {
var controllerDirective = controllerDirectives[name];
var controller = elementControllers[name];
var bindings = controllerDirective.$$bindings.bindToController;
if (controller.identifier && bindings) {
removeControllerBindingWatches =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
var controllerResult = controller();
if (controllerResult !== controller.instance) {
// If the controller constructor has a return value, overwrite the instance
// from setupControllers
controller.instance = controllerResult;
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
removeControllerBindingWatches && removeControllerBindingWatches();
removeControllerBindingWatches =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
}
@@ -2101,10 +2100,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
function markDirectivesAsIsolate(directives) {
// mark all directives as needing isolate scope.
// Depending upon the context in which a directive finds itself it might need to have a new isolated
// or child scope created. For instance:
// * if the directive has been pulled into a template because another directive with a higher priority
// asked for element transclusion
// * if the directive itself asks for transclusion but it is at the root of a template and the original
// element was replaced. See https://github.com/angular/angular.js/issues/12936
function markDirectiveScope(directives, isolateScope, newScope) {
for (var j = 0, jj = directives.length; j < jj; j++) {
directives[j] = inherit(directives[j], {$$isolateScope: true});
directives[j] = inherit(directives[j], {$$isolateScope: isolateScope, $$newScope: newScope});
}
}
@@ -2251,7 +2255,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var templateDirectives = collectDirectives(compileNode, [], tempTemplateAttrs);
if (isObject(origAsyncDirective.scope)) {
markDirectivesAsIsolate(templateDirectives);
// the original directive that caused the template to be loaded async required
// an isolate scope
markDirectiveScope(templateDirectives, true);
}
directives = templateDirectives.concat(directives);
mergeTemplateAttributes(tAttrs, tempTemplateAttrs);
@@ -2300,7 +2306,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn = boundTranscludeFn;
}
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, linkNode, $rootElement,
childBoundTranscludeFn, afterTemplateNodeLinkFn);
childBoundTranscludeFn);
}
linkQueue = null;
});
@@ -2317,8 +2323,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
}
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn,
afterTemplateNodeLinkFn);
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
}
};
}
@@ -2427,7 +2432,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile: function() {
return {
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
var $$observers = (attr.$$observers || (attr.$$observers = {}));
var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
throw $compileMinErr('nodomevents',
@@ -2530,7 +2535,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// Copy over user data (that includes Angular's $scope etc.). Don't copy private
// data here because there's no public interface in jQuery to do that and copying over
// event listeners (which is the main use of private data) wouldn't work anyway.
jqLite(newNode).data(jqLite(firstElementToRemove).data());
jqLite.data(newNode, jqLite.data(firstElementToRemove));
// Remove data of the replaced element. We cannot just call .remove()
// on the element it since that would deallocate scope that is needed
@@ -2578,9 +2583,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// Set up $watches for isolate scope and controller bindings. This process
// only occurs for isolate scopes and new scopes with controllerAs.
function initializeDirectiveBindings(scope, attrs, destination, bindings,
directive, newScope) {
var onNewScopeDestroyed;
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
forEach(bindings, function(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -2642,14 +2646,13 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
var unwatch;
var removeWatch;
if (definition.collection) {
unwatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
unwatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
removeWatch = scope.$watch($parse(attrs[attrName], parentValueWatch), null, parentGet.literal);
}
onNewScopeDestroyed = (onNewScopeDestroyed || []);
onNewScopeDestroyed.push(unwatch);
removeWatchCollection.push(removeWatch);
break;
case '&':
@@ -2665,16 +2668,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
}
});
var destroyBindings = onNewScopeDestroyed ? function destroyBindings() {
for (var i = 0, ii = onNewScopeDestroyed.length; i < ii; ++i) {
onNewScopeDestroyed[i]();
return removeWatchCollection.length && function removeWatches() {
for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
removeWatchCollection[i]();
}
} : noop;
if (newScope && destroyBindings !== noop) {
newScope.$on('$destroy', destroyBindings);
return noop;
}
return destroyBindings;
};
}
}];
}
+2 -1
View File
@@ -11,7 +11,8 @@
// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
var URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
+7 -1
View File
@@ -34,7 +34,13 @@
* @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
* make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
* @param {string=} onload Expression to evaluate when a new partial is loaded.
*
* <div class="alert alert-warning">
* **Note:** When using onload on SVG elements in IE11, the browser will try to call
* a function with the name on the window element, which will usually throw a
* "function is undefined" error. To fix this, you can instead use `data-onload` or a
* different form that {@link guide/directive#normalization matches} `onload`.
* </div>
*
* @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
* $anchorScroll} to scroll the viewport after the content is loaded.
*
+1 -1
View File
@@ -66,7 +66,7 @@
* </file>
* </example>
*
* ### Example - splitting on whitespace
* ### Example - splitting on newline
* <example name="ngList-directive-newlines">
* <file name="index.html">
* <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
+13 -10
View File
@@ -22,7 +22,9 @@ var ngModelMinErr = minErr('ngModel');
* @ngdoc type
* @name ngModel.NgModelController
*
* @property {string} $viewValue Actual string value in the view.
* @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
* String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
* is set.
* @property {*} $modelValue The value in the model that the control is bound to.
* @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
the control reads value from the DOM. The functions are called in array order, each passing
@@ -1139,12 +1141,13 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
</label><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
<pre>user.data = <span ng-bind="user.data"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('optionsExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = { name: 'say', data: '' };
$scope.user = { name: 'John', data: '' };
$scope.cancel = function(e) {
if (e.keyCode == 27) {
@@ -1159,20 +1162,20 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
var other = element(by.model('user.data'));
it('should allow custom events', function() {
input.sendKeys(' hello');
input.sendKeys(' Doe');
input.click();
expect(model.getText()).toEqual('say');
expect(model.getText()).toEqual('John');
other.click();
expect(model.getText()).toEqual('say hello');
expect(model.getText()).toEqual('John Doe');
});
it('should $rollbackViewValue when model changes', function() {
input.sendKeys(' hello');
expect(input.getAttribute('value')).toEqual('say hello');
input.sendKeys(' Doe');
expect(input.getAttribute('value')).toEqual('John Doe');
input.sendKeys(protractor.Key.ESCAPE);
expect(input.getAttribute('value')).toEqual('say');
expect(input.getAttribute('value')).toEqual('John');
other.click();
expect(model.getText()).toEqual('say');
expect(model.getText()).toEqual('John');
});
</file>
</example>
@@ -1198,7 +1201,7 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
<file name="app.js">
angular.module('optionsExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = { name: 'say' };
$scope.user = { name: 'Igor' };
}]);
</file>
</example>
+72 -36
View File
@@ -33,19 +33,27 @@ var ngOptionsMinErr = minErr('ngOptions');
*
* ## Complex Models (objects or collections)
*
* **Note:** By default, `ngModel` watches the model by reference, not value. This is important when
* binding any input directive to a model that is an object or a collection.
* By default, `ngModel` watches the model by reference, not value. This is important to know when
* binding the select to a model that is an object or a collection.
*
* Since this is a common situation for `ngOptions` the directive additionally watches the model using
* `$watchCollection` when the select has the `multiple` attribute or when there is a `track by` clause in
* the options expression. This allows ngOptions to trigger a re-rendering of the options even if the actual
* object/collection has not changed identity but only a property on the object or an item in the collection
* changes.
* One issue occurs if you want to preselect an option. For example, if you set
* the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection,
* because the objects are not identical. So by default, you should always reference the item in your collection
* for preselections, e.g.: `$scope.selected = $scope.collection[3]`.
*
* Another solution is to use a `track by` clause, because then `ngOptions` will track the identity
* of the item not by reference, but by the result of the `track by` expression. For example, if your
* collection items have an id property, you would `track by item.id`.
*
* A different issue with objects or collections is that ngModel won't detect if an object property or
* a collection item changes. For that reason, `ngOptions` additionally watches the model using
* `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute.
* This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection
* has not changed identity, but only a property on the object or an item in the collection changes.
*
* Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
* if the model is an array). This means that changing a property deeper inside the object/collection that the
* first level will not trigger a re-rendering.
*
* if the model is an array). This means that changing a property deeper than the first level inside the
* object/collection will not trigger a re-rendering.
*
* ## `select` **`as`**
*
@@ -58,17 +66,13 @@ var ngOptionsMinErr = minErr('ngOptions');
* ### `select` **`as`** and **`track by`**
*
* <div class="alert alert-warning">
* Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
* Be careful when using `select` **`as`** and **`track by`** in the same expression.
* </div>
*
* Consider the following example:
*
* ```html
* <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected"></select>
* ```
* Given this array of items on the $scope:
*
* ```js
* $scope.values = [{
* $scope.items = [{
* id: 1,
* label: 'aLabel',
* subItem: { name: 'aSubItem' }
@@ -77,20 +81,33 @@ var ngOptionsMinErr = minErr('ngOptions');
* label: 'bLabel',
* subItem: { name: 'bSubItem' }
* }];
*
* $scope.selected = { name: 'aSubItem' };
* ```
*
* With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
* of the data source (to `item` in this example). To calculate whether an element is selected, we do the
* following:
* This will work:
*
* 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
* 2. Apply **`track by`** to the already selected value in `ngModel`.
* In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
* value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
* a wrong object, the selected element can't be found, `<select>` is always reset to the "not
* selected" option.
* ```html
* <select ng-options="item as item.label for item in items track by item.id" ng-model="selected"></select>
* ```
* ```js
* $scope.selected = $scope.items[0];
* ```
*
* but this will not work:
*
* ```html
* <select ng-options="item.subItem as item.label for item in items track by item.id" ng-model="selected"></select>
* ```
* ```js
* $scope.selected = $scope.items[0].subItem;
* ```
*
* In both examples, the **`track by`** expression is applied successfully to each `item` in the
* `items` array. Because the selected option has been set programmatically in the controller, the
* **`track by`** expression is also applied to the `ngModel` value. In the first example, the
* `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with
* no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`**
* expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
*
* @param {string} ngModel Assignable angular expression to data-bind to.
@@ -392,11 +409,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var optionTemplate = document.createElement('option'),
optGroupTemplate = document.createElement('optgroup');
return {
restrict: 'A',
terminal: true,
require: ['select', '?ngModel'],
link: function(scope, selectElement, attr, ctrls) {
function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
// if ngModel is not defined, we don't need to do anything
var ngModelCtrl = ctrls[1];
@@ -451,7 +465,6 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
unknownOption.remove();
};
// Update the controller methods for multiple selectable options
if (!multiple) {
@@ -579,11 +592,16 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
function updateOptionElement(option, element) {
option.element = element;
element.disabled = option.disabled;
if (option.value !== element.value) element.value = option.selectValue;
// NOTE: The label must be set before the value, otherwise IE10/11/EDGE create unresponsive
// selects in certain circumstances when multiple selects are next to each other and display
// the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
// See https://github.com/angular/angular.js/issues/11314 for more info.
// This is unfortunately untestable with unit / e2e tests
if (option.label !== element.label) {
element.label = option.label;
element.textContent = option.label;
}
if (option.value !== element.value) element.value = option.selectValue;
}
function addOrReuseElement(parent, current, type, templateElement) {
@@ -621,10 +639,15 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
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 === unknownOption_ ||
current.nodeType === NODE_TYPE_COMMENT ||
current.value === '')) {
current = current.nextSibling;
}
}
@@ -721,7 +744,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
}
}
}
return {
restrict: 'A',
terminal: true,
require: ['select', '?ngModel'],
link: {
pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
// Deactivate the SelectController.register method to prevent
// option directives from accidentally registering themselves
// (and unwanted $destroy handlers etc.)
ctrls[0].registerOption = noop;
},
post: ngOptionsPostLink
}
};
}];
+18 -8
View File
@@ -43,7 +43,7 @@
* Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
* when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
* keys in the order in which they were defined, although there are exceptions when keys are deleted
* and reinstated. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_issues
* and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
* If this is not desired, the recommended workaround is to convert your object into an array
* that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
@@ -53,15 +53,21 @@
*
* # Tracking and Duplicates
*
* When the contents of the collection change, `ngRepeat` makes the corresponding changes to the DOM:
* `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
* the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
*
* * When an item is added, a new instance of the template is added to the DOM.
* * When an item is removed, its template instance is removed from the DOM.
* * When items are reordered, their respective templates are reordered in the DOM.
*
* By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
* there are duplicates, it is not possible to maintain a one-to-one mapping between collection
* items and DOM elements.
* To minimize creation of DOM elements, `ngRepeat` uses a function
* to "keep track" of all items in the collection and their corresponding DOM elements.
* For example, if an item is added to the collection, ngRepeat will know that all other items
* already have DOM elements, and will not re-render them.
*
* The default tracking function (which tracks items by their identity) does not allow
* duplicate items in arrays. This is because when there are duplicates, it is not possible
* to maintain a one-to-one mapping between collection items and DOM elements.
*
* If you do need to repeat duplicate items, you can substitute the default tracking behavior
* with your own using the `track by` expression.
@@ -74,7 +80,7 @@
* </div>
* ```
*
* You may use arbitrary expressions in `track by`, including references to custom functions
* You may also use arbitrary expressions in `track by`, including references to custom functions
* on the scope:
* ```html
* <div ng-repeat="n in [42, 42, 43, 43] track by myTrackingFunction(n)">
@@ -82,10 +88,14 @@
* </div>
* ```
*
* If you are working with objects that have an identifier property, you can track
* <div class="alert alert-success">
* If you are working with objects that have an identifier property, you should track
* by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
* will not have to rebuild the DOM elements for items it has already rendered, even if the
* JavaScript objects in the collection have been substituted for new ones:
* JavaScript objects in the collection have been substituted for new ones. For large collections,
* this signifincantly improves rendering performance. If you don't have a unique identifier,
* `track by $index` can also provide a performance boost.
* </div>
* ```html
* <div ng-repeat="model in collection track by model.id">
* {{model.name}}
+58 -53
View File
@@ -2,6 +2,15 @@
var noopNgModelController = { $setViewValue: noop, $render: noop };
function chromeHack(optionElement) {
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
// Adding an <option selected="selected"> element to a <select required="required"> should
// automatically select the new element
if (optionElement[0].hasAttribute('selected')) {
optionElement[0].selected = true;
}
}
/**
* @ngdoc type
* @name select.SelectController
@@ -77,6 +86,8 @@ var SelectController =
}
var count = optionsMap.get(value) || 0;
optionsMap.put(value, count + 1);
self.ngModelCtrl.$render();
chromeHack(element);
};
// Tell the select control that an option, with the given value, has been removed
@@ -98,6 +109,39 @@ var SelectController =
self.hasOption = function(value) {
return !!optionsMap.get(value);
};
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
if (interpolateValueFn) {
// The value attribute is interpolated
var oldVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
if (isDefined(oldVal)) {
self.removeOption(oldVal);
}
oldVal = newVal;
self.addOption(newVal, optionElement);
});
} else if (interpolateTextFn) {
// The text content is interpolated
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
optionAttrs.$set('value', newVal);
if (oldVal !== newVal) {
self.removeOption(oldVal);
}
self.addOption(newVal, optionElement);
});
} else {
// The value attribute is static
self.addOption(optionAttrs.value, optionElement);
}
optionElement.on('$destroy', function() {
self.removeOption(optionAttrs.value);
self.ngModelCtrl.$render();
});
};
}];
/**
@@ -143,6 +187,8 @@ var SelectController =
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} multiple Allows multiple options to be selected. The selected values will be
* bound to the model as an array.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds required attribute and required validation constraint to
* the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
@@ -308,7 +354,13 @@ var selectDirective = function() {
restrict: 'E',
require: ['select', '?ngModel'],
controller: SelectController,
link: function(scope, element, attr, ctrls) {
priority: 1,
link: {
pre: selectPreLink
}
};
function selectPreLink(scope, element, attr, ctrls) {
// if ngModel is not defined, we don't need to do anything
var ngModelCtrl = ctrls[1];
@@ -378,7 +430,6 @@ var selectDirective = function() {
}
}
};
};
@@ -386,16 +437,6 @@ var selectDirective = function() {
// of dynamically created (and destroyed) option elements to their containing select
// directive via its controller.
var optionDirective = ['$interpolate', function($interpolate) {
function chromeHack(optionElement) {
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
// Adding an <option selected="selected"> element to a <select required="required"> should
// automatically select the new element
if (optionElement[0].hasAttribute('selected')) {
optionElement[0].selected = true;
}
}
return {
restrict: 'E',
priority: 100,
@@ -403,12 +444,12 @@ var optionDirective = ['$interpolate', function($interpolate) {
if (isDefined(attr.value)) {
// If the value attribute is defined, check if it contains an interpolation
var valueInterpolated = $interpolate(attr.value, true);
var interpolateValueFn = $interpolate(attr.value, true);
} else {
// If the value attribute is not defined then we fall back to the
// text content of the option element, which may be interpolated
var interpolateFn = $interpolate(element.text(), true);
if (!interpolateFn) {
var interpolateTextFn = $interpolate(element.text(), true);
if (!interpolateTextFn) {
attr.$set('value', element.text());
}
}
@@ -422,44 +463,8 @@ var optionDirective = ['$interpolate', function($interpolate) {
selectCtrl = parent.data(selectCtrlName) ||
parent.parent().data(selectCtrlName); // in case we are in optgroup
function addOption(optionValue) {
selectCtrl.addOption(optionValue, element);
selectCtrl.ngModelCtrl.$render();
chromeHack(element);
}
// Only update trigger option updates if this is an option within a `select`
// that also has `ngModel` attached
if (selectCtrl && selectCtrl.ngModelCtrl) {
if (valueInterpolated) {
// The value attribute is interpolated
var oldVal;
attr.$observe('value', function valueAttributeObserveAction(newVal) {
if (isDefined(oldVal)) {
selectCtrl.removeOption(oldVal);
}
oldVal = newVal;
addOption(newVal);
});
} else if (interpolateFn) {
// The text content is interpolated
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
attr.$set('value', newVal);
if (oldVal !== newVal) {
selectCtrl.removeOption(oldVal);
}
addOption(newVal);
});
} else {
// The value attribute is static
addOption(attr.value);
}
element.on('$destroy', function() {
selectCtrl.removeOption(attr.value);
selectCtrl.ngModelCtrl.$render();
});
if (selectCtrl) {
selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn);
}
};
}
+1
View File
@@ -214,6 +214,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
if (fractionSize > 0 && number < 1) {
formatedText = number.toFixed(fractionSize);
number = parseFloat(formatedText);
formatedText = formatedText.replace(DECIMAL_SEP, decimalSep);
}
}
+1 -1
View File
@@ -111,7 +111,7 @@ function limitToFilter() {
if (!isArray(input) && !isString(input)) return input;
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
begin = (begin < 0 && begin >= -input.length) ? input.length + begin : begin;
begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
if (limit >= 0) {
return input.slice(begin, begin + limit);
+27 -38
View File
@@ -345,9 +345,9 @@ function $HttpProvider() {
* Configure `$http` service to return promises without the shorthand methods `success` and `error`.
* This should be used to make sure that applications work without these methods.
*
* Defaults to false. If no value is specified, returns the current configured value.
* Defaults to true. If no value is specified, returns the current configured value.
*
* @param {boolean=} value If true, `$http` will return a normal promise without the `success` and `error` methods.
* @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
@@ -425,28 +425,18 @@ function $HttpProvider() {
*
*
* ## General usage
* The `$http` service is a function which takes a single argument — a configuration object —
* The `$http` service is a function which takes a single argument — a {@link $http#usage configuration object}
* that is used to generate an HTTP request and returns a {@link ng.$q promise}.
*
* ```js
* // Simple GET request example :
* $http.get('/someUrl').
* then(function(response) {
* // Simple GET request example:
* $http({
* method: 'GET',
* url: '/someUrl'
* }).then(function successCallback(response) {
* // this callback will be called asynchronously
* // when the response is available
* }, function(response) {
* // called asynchronously if an error occurs
* // or server returns response with an error status.
* });
* ```
*
* ```js
* // Simple POST request example (passing data) :
* $http.post('/someUrl', {msg:'hello word!'}).
* then(function(response) {
* // this callback will be called asynchronously
* // when the response is available
* }, function(response) {
* }, function errorCallback(response) {
* // called asynchronously if an error occurs
* // or server returns response with an error status.
* });
@@ -466,25 +456,16 @@ function $HttpProvider() {
* XMLHttpRequest will transparently follow it, meaning that the error callback will not be
* called for such responses.
*
* ## Writing Unit Tests that use $http
* When unit testing (using {@link ngMock ngMock}), it is necessary to call
* {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
* request using trained responses.
*
* ```
* $httpBackend.expectGET(...);
* $http.get(...);
* $httpBackend.flush();
* ```
*
* ## Shortcut methods
*
* Shortcut methods are also available. All shortcut methods require passing in the URL, and
* request data must be passed in for POST/PUT requests.
* request data must be passed in for POST/PUT requests. An optional config can be passed as the
* last argument.
*
* ```js
* $http.get('/someUrl').then(successCallback);
* $http.post('/someUrl', data).then(successCallback);
* $http.get('/someUrl', config).then(successCallback, errorCallback);
* $http.post('/someUrl', data, config).then(successCallback, errorCallback);
* ```
*
* Complete list of shortcut methods:
@@ -498,6 +479,17 @@ function $HttpProvider() {
* - {@link ng.$http#patch $http.patch}
*
*
* ## Writing Unit Tests that use $http
* When unit testing (using {@link ngMock ngMock}), it is necessary to call
* {@link ngMock.$httpBackend#flush $httpBackend.flush()} to flush each pending
* request using trained responses.
*
* ```
* $httpBackend.expectGET(...);
* $http.get(...);
* $httpBackend.flush();
* ```
*
* ## Deprecation Notice
* <div class="alert alert-danger">
* The `$http` legacy promise methods `success` and `error` have been deprecated.
@@ -655,7 +647,7 @@ function $HttpProvider() {
*
* There are two kinds of interceptors (and two kinds of rejection interceptors):
*
* * `request`: interceptors get called with a http `config` object. The function is free to
* * `request`: interceptors get called with a http {@link $http#usage config} object. The function is free to
* modify the `config` object or create a new one. The function needs to return the `config`
* object directly, or a promise containing the `config` or a new `config` object.
* * `requestError`: interceptor gets called when a previous interceptor threw an error or
@@ -1007,11 +999,8 @@ function $HttpProvider() {
function transformResponse(response) {
// make a copy since the response must be cacheable
var resp = extend({}, response);
if (!response.data) {
resp.data = response.data;
} else {
resp.data = transformData(response.data, response.headers, response.status, config.transformResponse);
}
resp.data = transformData(response.data, response.headers, response.status,
config.transformResponse);
return (isSuccess(response.status))
? resp
: $q.reject(resp);
+31 -5
View File
@@ -1,7 +1,32 @@
'use strict';
function createXhr() {
return new window.XMLHttpRequest();
/**
* @ngdoc service
* @name $xhrFactory
*
* @description
* Factory function used to create XMLHttpRequest objects.
*
* Replace or decorate this service to create your own custom XMLHttpRequest objects.
*
* ```
* angular.module('myApp', [])
* .factory('$xhrFactory', function() {
* return function createXhr(method, url) {
* return new window.XMLHttpRequest({mozSystem: true});
* };
* });
* ```
*
* @param {string} method HTTP method of the request (GET, POST, PUT, ..)
* @param {string} url URL of the request.
*/
function $xhrFactoryProvider() {
this.$get = function() {
return function createXhr() {
return new window.XMLHttpRequest();
};
};
}
/**
@@ -9,6 +34,7 @@ function createXhr() {
* @name $httpBackend
* @requires $window
* @requires $document
* @requires $xhrFactory
*
* @description
* HTTP backend used by the {@link ng.$http service} that delegates to
@@ -21,8 +47,8 @@ function createXhr() {
* $httpBackend} which can be trained with responses.
*/
function $HttpBackendProvider() {
this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) {
return createHttpBackend($browser, createXhr, $browser.defer, $window.angular.callbacks, $document[0]);
this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) {
return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]);
}];
}
@@ -46,7 +72,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
});
} else {
var xhr = createXhr();
var xhr = createXhr(method, url);
xhr.open(method, url, true);
forEach(headers, function(value, key) {
+5 -5
View File
@@ -574,9 +574,9 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
* Return hash fragment when called without any parameter.
* Returns the hash fragment when called without any parameters.
*
* Change hash fragment when called with parameter and return `$location`.
* Changes the hash fragment when called with a parameter and returns `$location`.
*
*
* ```js
@@ -597,8 +597,8 @@ var locationPrototype = {
* @name $location#replace
*
* @description
* If called, all changes to $location during current `$digest` will be replacing current history
* record, instead of adding new one.
* If called, all changes to $location during the current `$digest` will replace the current history
* record, instead of adding a new one.
*/
replace: function() {
this.$$replace = true;
@@ -918,7 +918,7 @@ function $LocationProvider() {
var oldUrl = $location.absUrl();
var oldState = $location.$$state;
var defaultPrevented;
newUrl = trimEmptyHash(newUrl);
$location.$$parse(newUrl);
$location.$$state = newState;
+51 -9
View File
@@ -38,20 +38,30 @@ var $parseMinErr = minErr('$parse');
function ensureSafeMemberName(name, fullExpression) {
if (name === "__defineGetter__" || name === "__defineSetter__"
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|| name === "__proto__") {
throw $parseMinErr('isecfld',
'Attempting to access a disallowed field in Angular expressions! '
+ 'Expression: {0}', fullExpression);
}
return name;
}
function getStringValue(name, fullExpression) {
// From the JavaScript docs:
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
// into a string via the toString method.
//
// So, to ensure that we are checking the same `name` that JavaScript would use,
// we cast it to a string, if possible
name = (isObject(name) && name.toString) ? name.toString() : name;
if (name === "__defineGetter__" || name === "__defineSetter__"
|| name === "__lookupGetter__" || name === "__lookupSetter__"
|| name === "__proto__") {
throw $parseMinErr('isecfld',
'Attempting to access a disallowed field in Angular expressions! '
// we cast it to a string, if possible.
// Doing `name + ''` can cause a repl error if the result to `toString` is not a string,
// this is, this will handle objects that misbehave.
name = name + '';
if (!isString(name)) {
throw $parseMinErr('iseccst',
'Cannot convert object to primitive value! '
+ 'Expression: {0}', fullExpression);
}
return name;
@@ -102,6 +112,16 @@ function ensureSafeFunction(obj, fullExpression) {
}
}
function ensureSafeAssignContext(obj, fullExpression) {
if (obj) {
if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor ||
obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
throw $parseMinErr('isecaf',
'Assigning to a constructor is disallowed! Expression: {0}', fullExpression);
}
}
}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
@@ -816,6 +836,8 @@ ASTCompiler.prototype = {
'ensureSafeMemberName',
'ensureSafeObject',
'ensureSafeFunction',
'getStringValue',
'ensureSafeAssignContext',
'ifDefined',
'plus',
'text',
@@ -824,6 +846,8 @@ ASTCompiler.prototype = {
ensureSafeMemberName,
ensureSafeObject,
ensureSafeFunction,
getStringValue,
ensureSafeAssignContext,
ifDefined,
plusFn,
expression);
@@ -967,6 +991,7 @@ ASTCompiler.prototype = {
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
@@ -1050,6 +1075,7 @@ ASTCompiler.prototype = {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -1175,6 +1201,10 @@ ASTCompiler.prototype = {
this.current().body.push(this.ensureSafeFunction(item), ';');
},
addEnsureSafeAssignContext: function(item) {
this.current().body.push(this.ensureSafeAssignContext(item), ';');
},
ensureSafeObject: function(item) {
return 'ensureSafeObject(' + item + ',text)';
},
@@ -1187,6 +1217,14 @@ ASTCompiler.prototype = {
return 'ensureSafeFunction(' + item + ',text)';
},
getStringValue: function(item) {
this.assign(item, 'getStringValue(' + item + ',text)');
},
ensureSafeAssignContext: function(item) {
return 'ensureSafeAssignContext(' + item + ',text)';
},
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
var self = this;
return function() {
@@ -1364,6 +1402,7 @@ ASTInterpreter.prototype = {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
ensureSafeObject(lhs.value, self.expression);
ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -1561,6 +1600,7 @@ ASTInterpreter.prototype = {
var value;
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
ensureSafeMemberName(rhs, expression);
if (create && create !== 1 && lhs && !(lhs[rhs])) {
lhs[rhs] = {};
@@ -1866,13 +1906,14 @@ function $ParseProvider() {
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
var watchDelegate = parsedExpression.$$watchDelegate;
var useInputs = false;
var regularWatch =
watchDelegate !== oneTimeLiteralWatchDelegate &&
watchDelegate !== oneTimeWatchDelegate;
var fn = regularWatch ? function regularInterceptedExpression(scope, locals, assign, inputs) {
var value = parsedExpression(scope, locals, assign, inputs);
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value, scope, locals);
} : function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = parsedExpression(scope, locals, assign, inputs);
@@ -1890,6 +1931,7 @@ function $ParseProvider() {
// If there is an interceptor, but no watchDelegate then treat the interceptor like
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
fn.$$watchDelegate = inputsWatchDelegate;
useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
}
+2
View File
@@ -53,6 +53,8 @@
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
* Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise.
*
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
* [The CommonJS Promise proposal](http://wiki.commonjs.org/wiki/Promises) describes a promise as an
+32 -16
View File
@@ -14,15 +14,15 @@
* exposed as $$____ properties
*
* Loop operations are optimized by using while(count--) { ... }
* - this means that in order to keep the same order of execution as addition we have to add
* - This means that in order to keep the same order of execution as addition we have to add
* items to the array at the beginning (unshift) instead of at the end (push)
*
* Child scopes are created and removed often
* - Using an array would be slow since inserts in middle are expensive so we use linked list
* - Using an array would be slow since inserts in the middle are expensive; so we use linked lists
*
* There are few watches then a lot of observers. This is why you don't want the observer to be
* implemented in the same way as watch. Watch requires return of initialization function which
* are expensive to construct.
* There are fewer watches than observers. This is why you don't want the observer to be implemented
* in the same way as watch. Watch requires return of the initialization function which is expensive
* to construct.
*/
@@ -64,7 +64,7 @@
* Every application has a single root {@link ng.$rootScope.Scope scope}.
* All other scopes are descendant scopes of the root scope. Scopes provide separation
* between the model and the view, via a mechanism for watching the model for changes.
* They also provide an event emission/broadcast and subscription facility. See the
* They also provide event emission/broadcast and subscription facility. See the
* {@link guide/scope developer guide on scopes}.
*/
function $RootScopeProvider() {
@@ -101,6 +101,29 @@ function $RootScopeProvider() {
$event.currentScope.$$destroyed = true;
}
function cleanUpScope($scope) {
if (msie === 9) {
// There is a memory leak in IE9 if all child scopes are not disconnected
// completely when a scope is destroyed. So this code will recurse up through
// all this scopes children
//
// See issue https://github.com/angular/angular.js/issues/10706
$scope.$$childHead && cleanUpScope($scope.$$childHead);
$scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
}
// The code below works around IE9 and V8's memory leaks
//
// See:
// - https://code.google.com/p/v8/issues/detail?id=2073#c26
// - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
$scope.$parent = $scope.$$nextSibling = $scope.$$prevSibling = $scope.$$childHead =
$scope.$$childTail = $scope.$root = $scope.$$watchers = null;
}
/**
* @ngdoc type
* @name $rootScope.Scope
@@ -897,16 +920,9 @@ function $RootScopeProvider() {
this.$on = this.$watch = this.$watchGroup = function() { return noop; };
this.$$listeners = {};
// All of the code below is bogus code that works around V8's memory leak via optimized code
// and inline caches.
//
// see:
// - https://code.google.com/p/v8/issues/detail?id=2073#c26
// - https://github.com/angular/angular.js/issues/6794#issuecomment-38648909
// - https://github.com/angular/angular.js/issues/1313#issuecomment-10378451
this.$parent = this.$$nextSibling = this.$$prevSibling = this.$$childHead =
this.$$childTail = this.$root = this.$$watchers = null;
// Disconnect the next sibling to prevent `cleanUpScope` destroying those too
this.$$nextSibling = null;
cleanUpScope(this);
},
/**
+1 -1
View File
@@ -484,7 +484,7 @@ function $SceDelegateProvider() {
* By default, Angular only loads templates from the same domain and protocol as the application
* document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
* $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
* protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
* protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
* them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
*
* *Please note*:
+44 -3
View File
@@ -186,8 +186,10 @@ var ANIMATE_TIMER_KEY = '$$animateCss';
*
* * `event` - The DOM event (e.g. enter, leave, move). When used, a generated CSS class of `ng-EVENT` and `ng-EVENT-active` will be applied
* to the element during the animation. Multiple events can be provided when spaces are used as a separator. (Note that this will not perform any DOM operation.)
* * `structural` - Indicates that the `ng-` prefix will be added to the event class. Setting to `false` or omitting will turn `ng-EVENT` and
* `ng-EVENT-active` in `EVENT` and `EVENT-active`. Unused if `event` is omitted.
* * `easing` - The CSS easing value that will be applied to the transition or keyframe animation (or both).
* * `transition` - The raw CSS transition style that will be used (e.g. `1s linear all`).
* * `transitionStyle` - The raw CSS transition style that will be used (e.g. `1s linear all`).
* * `keyframeStyle` - The raw CSS keyframe animation style that will be used (e.g. `1s my_animation linear`).
* * `from` - The starting CSS styles (a key/value object) that will be applied at the start of the animation.
* * `to` - The ending CSS styles (a key/value object) that will be applied across the animation via a CSS transition.
@@ -204,6 +206,10 @@ var ANIMATE_TIMER_KEY = '$$animateCss';
* * `staggerIndex` - The numeric index representing the stagger item (e.g. a value of 5 is equal to the sixth item in the stagger; therefore when a
* * `stagger` option value of `0.1` is used then there will be a stagger delay of `600ms`)
* * `applyClassesEarly` - Whether or not the classes being added or removed will be used when detecting the animation. This is set by `$animate` when enter/leave/move animations are fired to ensure that the CSS classes are resolved in time. (Note that this will prevent any transitions from occuring on the classes being added and removed.)
* * `cleanupStyles` - Whether or not the provided `from` and `to` styles will be removed once
* the animation is closed. This is useful for when the styles are used purely for the sake of
* the animation and do not have a lasting visual effect on the element (e.g. a colapse and open animation).
* By default this value is set to `false`.
*
* @return {object} an object with start and end methods and details about the animation.
*
@@ -324,6 +330,23 @@ function createLocalCacheLookup() {
};
}
// we do not reassign an already present style value since
// if we detect the style property value again we may be
// detecting styles that were added via the `from` styles.
// We make use of `isDefined` here since an empty string
// or null value (which is what getPropertyValue will return
// for a non-existing style) will still be marked as a valid
// value for the style (a falsy value implies that the style
// is to be removed at the end of the animation). If we had a simple
// "OR" statement then it would not be enough to catch that.
function registerRestorableStyles(backup, node, properties) {
forEach(properties, function(prop) {
backup[prop] = isDefined(backup[prop])
? backup[prop]
: node.style.getPropertyValue(prop);
});
}
var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var gcsLookup = createLocalCacheLookup();
var gcsStaggerLookup = createLocalCacheLookup();
@@ -424,6 +447,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
return function init(element, options) {
var restoreStyles = {};
var node = getDomNode(element);
if (!node
|| !node.parentNode
@@ -625,7 +649,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
stagger.animationDuration === 0;
}
applyAnimationFromStyles(element, options);
if (options.from) {
if (options.cleanupStyles) {
registerRestorableStyles(restoreStyles, node, Object.keys(options.from));
}
applyAnimationFromStyles(element, options);
}
if (flags.blockTransition || flags.blockKeyframeAnimation) {
applyBlocking(maxDuration);
@@ -692,6 +721,13 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
applyAnimationClasses(element, options);
applyAnimationStyles(element, options);
if (Object.keys(restoreStyles).length) {
forEach(restoreStyles, function(value, prop) {
value ? node.style.setProperty(prop, value)
: node.style.removeProperty(prop);
});
}
// the reason why we have this option is to allow a synchronous closing callback
// that is fired as SOON as the animation ends (when the CSS is removed) or if
// the animation never takes off at all. A good example is a leave animation since
@@ -886,7 +922,12 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
element.on(events.join(' '), onAnimationProgress);
applyAnimationToStyles(element, options);
if (options.to) {
if (options.cleanupStyles) {
registerRestorableStyles(restoreStyles, node, Object.keys(options.to));
}
applyAnimationToStyles(element, options);
}
}
function onAnimationExpired() {
+13 -4
View File
@@ -9,16 +9,25 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$$body', '$sniffer', '$$jqLite',
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $$body, $sniffer, $$jqLite) {
function isDocumentFragment(node) {
return node.parentNode && node.parentNode.nodeType === 11;
}
this.$get = ['$animateCss', '$rootScope', '$$AnimateRunner', '$rootElement', '$sniffer', '$$jqLite', '$document',
function($animateCss, $rootScope, $$AnimateRunner, $rootElement, $sniffer, $$jqLite, $document) {
// only browsers that support these properties can render animations
if (!$sniffer.animations && !$sniffer.transitions) return noop;
var bodyNode = getDomNode($$body);
var bodyNode = $document[0].body;
var rootNode = getDomNode($rootElement);
var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode);
var rootBodyElement = jqLite(
// this is to avoid using something that exists outside of the body
// we also special case the doc fragement case because our unit test code
// appends the $rootElement to the body after the app has been bootstrapped
isDocumentFragment(rootNode) || bodyNode.contains(rootNode) ? rootNode : bodyNode
);
var applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+44 -14
View File
@@ -66,15 +66,33 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return (nO.addClass && nO.addClass === cO.removeClass) || (nO.removeClass && nO.removeClass === cO.addClass);
});
this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$body', '$$HashMap',
this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
'$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
function($$rAF, $rootScope, $rootElement, $document, $$body, $$HashMap,
function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
$$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
var activeAnimationsLookup = new $$HashMap();
var disabledElementsLookup = new $$HashMap();
var animationsEnabled = null;
function postDigestTaskFactory() {
var postDigestCalled = false;
return function(fn) {
// we only issue a call to postDigest before
// it has first passed. This prevents any callbacks
// from not firing once the animation has completed
// since it will be out of the digest cycle.
if (postDigestCalled) {
fn();
} else {
$rootScope.$$postDigest(function() {
postDigestCalled = true;
fn();
});
}
};
}
// Wait until all directive and route-related templates are downloaded and
// compiled. The $templateRequest.totalPendingRequests variable keeps track of
// all of the remote templates being currently downloaded. If there are no
@@ -121,8 +139,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return mergeAnimationOptions(element, options, {});
}
function findCallbacks(element, event) {
function findCallbacks(parent, element, event) {
var targetNode = getDomNode(element);
var targetParentNode = getDomNode(parent);
var matches = [];
var entries = callbackRegistry[event];
@@ -130,6 +149,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
forEach(entries, function(entry) {
if (entry.node.contains(targetNode)) {
matches.push(entry.callback);
} else if (event === 'leave' && entry.node.contains(targetParentNode)) {
matches.push(entry.callback);
}
});
}
@@ -137,14 +158,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return matches;
}
function triggerCallback(event, element, phase, data) {
$$rAF(function() {
forEach(findCallbacks(element, event), function(callback) {
callback(element, phase, data);
});
});
}
return {
on: function(event, container, callback) {
var node = extractElementNode(container);
@@ -239,6 +252,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// These methods will become available after the digest has passed
var runner = new $$AnimateRunner();
// this is used to trigger callbacks in postDigest mode
var runInNextPostDigestOrNow = postDigestTaskFactory();
if (isArray(options.addClass)) {
options.addClass = options.addClass.join(' ');
}
@@ -459,7 +475,20 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
return runner;
function notifyProgress(runner, event, phase, data) {
triggerCallback(event, element, phase, data);
runInNextPostDigestOrNow(function() {
var callbacks = findCallbacks(parent, element, event);
if (callbacks.length) {
// do not optimize this call here to RAF because
// we don't know how heavy the callback code here will
// be and if this code is buffered then this can
// lead to a performance regression.
$$rAF(function() {
forEach(callbacks, function(callback) {
callback(element, phase, data);
});
});
}
});
runner.progress(event, phase, data);
}
@@ -502,7 +531,8 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
function areAnimationsAllowed(element, parentElement, event) {
var bodyElementDetected = isMatchingElement(element, $$body) || element[0].nodeName === 'HTML';
var bodyElement = jqLite($document[0].body);
var bodyElementDetected = isMatchingElement(element, bodyElement) || element[0].nodeName === 'HTML';
var rootElementDetected = isMatchingElement(element, $rootElement);
var parentAnimationDetected = false;
var animateChildren;
@@ -558,7 +588,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
if (!bodyElementDetected) {
// we also need to ensure that the element is or will be apart of the body element
// otherwise it is pointless to even issue an animation to be rendered
bodyElementDetected = isMatchingElement(parentElement, $$body);
bodyElementDetected = isMatchingElement(parentElement, bodyElement);
}
parentElement = parentElement.parent();
-7
View File
@@ -1,7 +0,0 @@
'use strict';
function $$BodyProvider() {
this.$get = ['$document', function($document) {
return jqLite($document[0].body);
}];
}
+11 -17
View File
@@ -2,7 +2,6 @@
/* global angularAnimateModule: true,
$$BodyProvider,
$$AnimateAsyncRunFactory,
$$rAFSchedulerFactory,
$$AnimateChildrenDirective,
@@ -291,7 +290,7 @@
* jQuery(element).fadeOut(1000, doneFn);
* }
* }
* }]
* }]);
* ```
*
* The nice thing about JS-based animations is that we can inject other services and make use of advanced animation libraries such as
@@ -322,7 +321,7 @@
* // do some cool animation and call the doneFn
* }
* }
* }]
* }]);
* ```
*
* ## CSS + JS Animations Together
@@ -344,7 +343,7 @@
* jQuery(element).slideIn(1000, doneFn);
* }
* }
* }]
* }]);
* ```
*
* ```css
@@ -364,16 +363,15 @@
* ```js
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
* return {
* enter: function(element, doneFn) {
* enter: function(element) {
* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
* var runner = $animateCss(element, {
* return $animateCss(element, {
* event: 'enter',
* structural: true
* }).start();
* runner.done(doneFn);
* });
* }
* }
* }]
* }]);
* ```
*
* The nice thing here is that we can save bandwidth by sticking to our CSS-based animation code and we don't need to rely on a 3rd-party animation framework.
@@ -385,19 +383,17 @@
* ```js
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
* return {
* enter: function(element, doneFn) {
* var runner = $animateCss(element, {
* enter: function(element) {
* return $animateCss(element, {
* event: 'enter',
* structural: true,
* addClass: 'maroon-setting',
* from: { height:0 },
* to: { height: 200 }
* }).start();
*
* runner.done(doneFn);
* });
* }
* }
* }]
* }]);
* ```
*
* Now we can fill in the rest via our transition CSS code:
@@ -742,8 +738,6 @@
* Click here {@link ng.$animate to learn more about animations with `$animate`}.
*/
angular.module('ngAnimate', [])
.provider('$$body', $$BodyProvider)
.directive('ngAnimateChildren', $$AnimateChildrenDirective)
.factory('$$rAFScheduler', $$rAFSchedulerFactory)
+2 -1
View File
@@ -235,7 +235,8 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
}
},
post: function(scope, elem, attr, ngModel) {
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem);
var needsTabIndex = shouldAttachAttr('tabindex', 'tabindex', elem)
&& !isNodeOneOf(elem, nodeBlackList);
function ngAriaWatchModelValue() {
return ngModel.$modelValue;
+8 -1
View File
@@ -325,6 +325,9 @@ angular.module('ngMessages', [])
controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
var ctrl = this;
var latestKey = 0;
var nextAttachId = 0;
this.getAttachId = function getAttachId() { return nextAttachId++; };
var messages = this.messages = {};
var renderLater, cachedCollection;
@@ -636,11 +639,15 @@ function ngMessageDirectiveFactory(restrict) {
$animate.enter(elm, null, element);
currentElement = elm;
// Each time we attach this node to a message we get a new id that we can match
// 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
// to deregister the message from the controller
currentElement.on('$destroy', function() {
if (currentElement) {
if (currentElement && currentElement.$$attachId === $$attachId) {
ngMessagesCtrl.deregister(commentNode);
messageCtrl.detach();
}
+3 -2
View File
@@ -2271,8 +2271,9 @@ if (window.jasmine || window.mocha) {
* @param {...(string|Function|Object)} fns any number of modules which are represented as string
* aliases or as anonymous module initialization functions. The modules are used to
* configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an
* object literal is passed they will be registered as values in the module, the key being
* the module name and the value being what is returned.
* object literal is passed each key-value pair will be registered on the module via
* {@link auto.$provide $provide}.value, the key being the string name (or token) to associate
* with the value on the injector.
*/
window.module = angular.mock.module = function() {
var moduleFns = Array.prototype.slice.call(arguments, 0);
+11 -2
View File
@@ -568,8 +568,17 @@ angular.module('ngResource', ['ng']).
undefined;
forEach(action, function(value, key) {
if (key != 'params' && key != 'isArray' && key != 'interceptor') {
httpConfig[key] = copy(value);
switch (key) {
default:
httpConfig[key] = copy(value);
break;
case 'params':
case 'isArray':
case 'interceptor':
break;
case 'timeout':
httpConfig[key] = value;
break;
}
});
+2 -1
View File
@@ -15,6 +15,7 @@
if (!element) return;
eventData = eventData || {};
var relatedTarget = eventData.relatedTarget || element;
var keys = eventData.keys;
var x = eventData.x;
var y = eventData.y;
@@ -84,7 +85,7 @@
x = x || 0;
y = y || 0;
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
pressed('alt'), pressed('shift'), pressed('meta'), 0, element);
pressed('alt'), pressed('shift'), pressed('meta'), 0, relatedTarget);
}
/* we're unable to change the timeStamp value directly so this
+103
View File
@@ -313,11 +313,19 @@ describe('angular', function() {
it('should throw an exception if a Scope is being copied', inject(function($rootScope) {
expect(function() { copy($rootScope.$new()); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy({child: $rootScope.$new()}, {}); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy([$rootScope.$new()]); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
}));
it('should throw an exception if a Window is being copied', function() {
expect(function() { copy(window); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy({child: window}); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
expect(function() { copy([window], []); }).
toThrowMinErr("ng", "cpws", "Can't copy! Making copies of Window or Scope instances is not supported.");
});
it('should throw an exception when source and destination are equivalent', function() {
@@ -334,6 +342,11 @@ describe('angular', function() {
hashKey(src);
dst = copy(src);
expect(hashKey(dst)).not.toEqual(hashKey(src));
src = {foo: {}};
hashKey(src.foo);
dst = copy(src);
expect(hashKey(src.foo)).not.toEqual(hashKey(dst.foo));
});
it('should retain the previous $$hashKey when copying object with hashKey', function() {
@@ -461,6 +474,7 @@ describe('angular', function() {
});
describe("extend", function() {
it('should not copy the private $$hashKey', function() {
var src,dst;
src = {};
@@ -471,6 +485,24 @@ describe('angular', function() {
});
it('should copy the properties of the source object onto the destination object', function() {
var destination, source;
destination = {};
source = {foo: true};
destination = extend(destination, source);
expect(isDefined(destination.foo)).toBe(true);
});
it('ISSUE #4751 - should copy the length property of an object source to the destination object', function() {
var destination, source;
destination = {};
source = {radius: 30, length: 0};
destination = extend(destination, source);
expect(isDefined(destination.length)).toBe(true);
expect(isDefined(destination.radius)).toBe(true);
});
it('should retain the previous $$hashKey', function() {
var src,dst,h;
src = {};
@@ -503,6 +535,17 @@ describe('angular', function() {
expect(dst.date).toBe(src.date);
});
it('should copy elements by reference', function() {
var src = { element: document.createElement('div'),
jqObject: jqLite("<p><span>s1</span><span>s2</span></p>").find("span") };
var dst = {};
extend(dst, src);
expect(dst.element).toBe(src.element);
expect(dst.jqObject).toBe(src.jqObject);
});
});
@@ -593,6 +636,25 @@ describe('angular', function() {
expect(isRegExp(dst.regexp)).toBe(true);
expect(dst.regexp.toString()).toBe(src.regexp.toString());
});
it('should copy(clone) elements', function() {
var src = {
element: document.createElement('div'),
jqObject: jqLite('<p><span>s1</span><span>s2</span></p>').find('span')
};
var dst = {};
merge(dst, src);
expect(dst.element).not.toBe(src.element);
expect(dst.jqObject).not.toBe(src.jqObject);
expect(isElement(dst.element)).toBeTruthy();
expect(dst.element.nodeName).toBeDefined(); // i.e it is a DOM element
expect(isElement(dst.jqObject)).toBeTruthy();
expect(dst.jqObject.nodeName).toBeUndefined(); // i.e it is a jqLite/jQuery object
});
});
@@ -1035,6 +1097,42 @@ describe('angular', function() {
});
});
describe('isArrayLike', function() {
it('should return false if passed a number', function() {
expect(isArrayLike(10)).toBe(false);
});
it('should return true if passed an array', function() {
expect(isArrayLike([1,2,3,4])).toBe(true);
});
it('should return true if passed an object', function() {
expect(isArrayLike({0:"test", 1:"bob", 2:"tree", length:3})).toBe(true);
});
it('should return true if passed arguments object', function() {
function test(a,b,c) {
expect(isArrayLike(arguments)).toBe(true);
}
test(1,2,3);
});
it('should return true if passed a nodelist', function() {
var nodes = document.body.childNodes;
expect(isArrayLike(nodes)).toBe(true);
});
it('should return false for objects with `length` but no matching indexable items', function() {
var obj = {
a: 'a',
b:'b',
length: 10
};
expect(isArrayLike(obj)).toBe(false);
});
});
describe('forEach', function() {
it('should iterate over *own* object properties', function() {
@@ -1074,6 +1172,11 @@ describe('angular', function() {
forEach(jqObject, function(value, key) { log.push(key + ':' + value.innerHTML); });
expect(log).toEqual(['0:s1', '1:s2']);
log = [];
jqObject = jqLite("<pane></pane>");
forEach(jqObject.children(), function(value, key) { log.push(key + ':' + value.innerHTML); });
expect(log).toEqual([]);
});
+7 -11
View File
@@ -1,4 +1,4 @@
/* global jQuery: true, uid: true */
/* global jQuery: true, uid: true, jqCache: true */
'use strict';
/**
@@ -12,6 +12,7 @@ if (window._jQuery) _jQuery.event.special.change = undefined;
if (window.bindJQuery) bindJQuery();
beforeEach(function() {
// all this stuff is not needed for module tests, where jqlite and publishExternalAPI and jqLite are not global vars
if (window.publishExternalAPI) {
publishExternalAPI(angular);
@@ -28,7 +29,10 @@ beforeEach(function() {
// reset to jQuery or default to us.
bindJQuery();
jqLiteCacheSizeInit();
// Clear the cache to prevent memory leak failures from previous tests
// breaking subsequent tests unnecessarily
jqCache = jqLite.cache = {};
}
angular.element(document.body).empty().removeData();
@@ -84,7 +88,6 @@ afterEach(function() {
}
}
// copied from Angular.js
// we need this method here so that we can run module tests with wrapped angular.js
function forEachSorted(obj, iterator, context) {
@@ -133,14 +136,7 @@ function dealoc(obj) {
function jqLiteCacheSize() {
var size = 0;
forEach(jqLite.cache, function() { size++; });
return size - jqLiteCacheSize.initSize;
}
jqLiteCacheSize.initSize = 0;
function jqLiteCacheSizeInit() {
jqLiteCacheSize.initSize = jqLiteCacheSize.initSize + jqLiteCacheSize();
return Object.keys(jqLite.cache).length;
}
+108 -30
View File
@@ -1193,21 +1193,45 @@ describe('jqLite', function() {
});
describe('mouseenter-mouseleave', function() {
var root, parent, sibling, child, log;
var root, parent, child, log;
beforeEach(function() {
function setup(html, parentNode, childNode) {
log = '';
root = jqLite('<div>root<p>parent<span>child</span></p><ul></ul></div>');
parent = root.find('p');
sibling = root.find('ul');
child = parent.find('span');
root = jqLite(html);
parent = root.find(parentNode);
child = parent.find(childNode);
parent.on('mouseenter', function() { log += 'parentEnter;'; });
parent.on('mouseleave', function() { log += 'parentLeave;'; });
child.on('mouseenter', function() { log += 'childEnter;'; });
child.on('mouseleave', function() { log += 'childLeave;'; });
});
}
function browserMoveTrigger(from, to) {
var fireEvent = function(type, element, relatedTarget) {
var evnt;
evnt = document.createEvent('MouseEvents');
var originalPreventDefault = evnt.preventDefault,
appWindow = window,
fakeProcessDefault = true,
finalProcessDefault;
evnt.preventDefault = function() {
fakeProcessDefault = false;
return originalPreventDefault.apply(evnt, arguments);
};
var x = 0, y = 0;
evnt.initMouseEvent(type, true, true, window, 0, x, y, x, y, false, false,
false, false, 0, relatedTarget);
element.dispatchEvent(evnt);
};
fireEvent('mouseout', from[0], to[0]);
fireEvent('mouseover', to[0], from[0]);
}
afterEach(function() {
dealoc(root);
@@ -1215,30 +1239,8 @@ describe('jqLite', function() {
it('should fire mouseenter when coming from outside the browser window', function() {
if (window.jQuery) return;
var browserMoveTrigger = function(from, to) {
var fireEvent = function(type, element, relatedTarget) {
var evnt;
evnt = document.createEvent('MouseEvents');
var originalPreventDefault = evnt.preventDefault,
appWindow = window,
fakeProcessDefault = true,
finalProcessDefault;
evnt.preventDefault = function() {
fakeProcessDefault = false;
return originalPreventDefault.apply(evnt, arguments);
};
var x = 0, y = 0;
evnt.initMouseEvent(type, true, true, window, 0, x, y, x, y, false, false,
false, false, 0, relatedTarget);
element.dispatchEvent(evnt);
};
fireEvent('mouseout', from[0], to[0]);
fireEvent('mouseover', to[0], from[0]);
};
setup('<div>root<p>parent<span>child</span></p><ul></ul></div>', 'p', 'span');
browserMoveTrigger(root, parent);
expect(log).toEqual('parentEnter;');
@@ -1253,6 +1255,28 @@ describe('jqLite', function() {
expect(log).toEqual('parentEnter;childEnter;childLeave;parentLeave;');
});
it('should fire the mousenter on SVG elements', function() {
if (window.jQuery) return;
setup(
'<div>' +
'<svg xmlns="http://www.w3.org/2000/svg"' +
' viewBox="0 0 18.75 18.75"' +
' width="18.75"' +
' height="18.75"' +
' version="1.1">' +
' <path d="M0,0c0,4.142,3.358,7.5,7.5,7.5s7.5-3.358,7.5-7.5-3.358-7.5-7.5-7.5-7.5,3.358-7.5,7.5"' +
' fill-rule="nonzero"' +
' fill="#CCC"' +
' ng-attr-fill="{{data.color || \'#CCC\'}}"/>' +
'</svg>' +
'</div>',
'svg', 'path');
browserMoveTrigger(parent, child);
expect(log).toEqual('childEnter;');
});
});
// Only run this test for jqLite and not normal jQuery
@@ -1407,6 +1431,60 @@ describe('jqLite', function() {
});
it('should correctly deregister the mouseenter/mouseleave listeners', function() {
var aElem = jqLite(a);
var onMouseenter = jasmine.createSpy('onMouseenter');
var onMouseleave = jasmine.createSpy('onMouseleave');
aElem.on('mouseenter', onMouseenter);
aElem.on('mouseleave', onMouseleave);
aElem.off('mouseenter', onMouseenter);
aElem.off('mouseleave', onMouseleave);
aElem.on('mouseenter', onMouseenter);
aElem.on('mouseleave', onMouseleave);
browserTrigger(a, 'mouseover', {relatedTarget: b});
expect(onMouseenter).toHaveBeenCalledOnce();
browserTrigger(a, 'mouseout', {relatedTarget: b});
expect(onMouseleave).toHaveBeenCalledOnce();
});
it('should call a `mouseenter/leave` listener only once when `mouseenter/leave` and `mouseover/out` '
+ 'are triggered simultaneously', function() {
var aElem = jqLite(a);
var onMouseenter = jasmine.createSpy('mouseenter');
var onMouseleave = jasmine.createSpy('mouseleave');
aElem.on('mouseenter', onMouseenter);
aElem.on('mouseleave', onMouseleave);
browserTrigger(a, 'mouseenter', {relatedTarget: b});
browserTrigger(a, 'mouseover', {relatedTarget: b});
expect(onMouseenter).toHaveBeenCalledOnce();
browserTrigger(a, 'mouseleave', {relatedTarget: b});
browserTrigger(a, 'mouseout', {relatedTarget: b});
expect(onMouseleave).toHaveBeenCalledOnce();
});
it('should call a `mouseenter/leave` listener when manually triggering the event', function() {
var aElem = jqLite(a);
var onMouseenter = jasmine.createSpy('mouseenter');
var onMouseleave = jasmine.createSpy('mouseleave');
aElem.on('mouseenter', onMouseenter);
aElem.on('mouseleave', onMouseleave);
aElem.triggerHandler('mouseenter');
expect(onMouseenter).toHaveBeenCalledOnce();
aElem.triggerHandler('mouseleave');
expect(onMouseleave).toHaveBeenCalledOnce();
});
it('should deregister specific listener within the listener and call subsequent listeners', function() {
var aElem = jqLite(a),
clickSpy = jasmine.createSpy('click'),
+32
View File
@@ -115,6 +115,38 @@ describe("$animateCss", function() {
expect(cancelSpy).toHaveBeenCalled();
expect(doneSpy).not.toHaveBeenCalled();
}));
it("should not bother applying the provided [from] and [to] styles to the element if [cleanupStyles] is present",
inject(function($animateCss, $rootScope) {
var animator = $animateCss(element, {
cleanupStyles: true,
from: { width: '100px' },
to: { width: '900px', height: '1000px' }
});
assertStyleIsEmpty(element, 'width');
assertStyleIsEmpty(element, 'height');
var runner = animator.start();
assertStyleIsEmpty(element, 'width');
assertStyleIsEmpty(element, 'height');
triggerRAF();
assertStyleIsEmpty(element, 'width');
assertStyleIsEmpty(element, 'height');
runner.end();
assertStyleIsEmpty(element, 'width');
assertStyleIsEmpty(element, 'height');
function assertStyleIsEmpty(element, prop) {
expect(element[0].style.getPropertyValue(prop)).toBeFalsy();
}
}));
});
});
+13
View File
@@ -133,6 +133,19 @@ describe('$cacheFactory', function() {
expect(cache.info().size).toBe(0);
}));
it('should only decrement size when an element is actually removed via remove', inject(function($cacheFactory) {
cache.put('foo', 'bar');
expect(cache.info().size).toBe(1);
cache.remove('undefined');
expect(cache.info().size).toBe(1);
cache.remove('hasOwnProperty');
expect(cache.info().size).toBe(1);
cache.remove('foo');
expect(cache.info().size).toBe(0);
}));
it('should return cache id', inject(function($cacheFactory) {
expect(cache.info().id).toBe('test');
+375
View File
@@ -947,6 +947,14 @@ describe('$compile', function() {
expect(child).toHaveClass('log'); // merged from replace directive template
}));
it('should interpolate the values once per digest',
inject(function($compile, $rootScope, log) {
element = $compile('<div>{{log("A")}} foo {{::log("B")}}</div>')($rootScope);
$rootScope.log = log;
$rootScope.$digest();
expect(log).toEqual('A; B; A; B');
}));
it('should update references to replaced jQuery context', function() {
module(function($compileProvider) {
$compileProvider.directive('foo', function() {
@@ -3577,6 +3585,22 @@ describe('$compile', function() {
});
});
it('should be able to interpolate attribute names which are present in Object.prototype', function() {
var attrs;
module(function() {
directive('attrExposer', valueFn({
link: function($scope, $element, $attrs) {
attrs = $attrs;
}
}));
});
inject(function($compile, $rootScope) {
$compile('<div attr-exposer to-string="{{1 + 1}}">')($rootScope);
$rootScope.$apply();
expect(attrs.toString).toBe('2');
});
});
it('should not initialize scope value if optional expression binding is not passed', inject(function($compile) {
compile('<div my-component></div>');
@@ -4390,6 +4414,301 @@ describe('$compile', function() {
});
it('should bind to multiple directives controllers via object notation (no scope)', function() {
var controller1Called = false;
var controller2Called = false;
module(function($compileProvider, $controllerProvider) {
$compileProvider.directive('foo', valueFn({
bindToController: {
'data': '=fooData',
'str': '@fooStr',
'fn': '&fooFn'
},
controllerAs: 'fooCtrl',
controller: function() {
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
expect(this.str).toBe('Hello, world!');
expect(this.fn()).toBe('called!');
controller1Called = true;
}
}));
$compileProvider.directive('bar', valueFn({
bindToController: {
'data': '=barData',
'str': '@barStr',
'fn': '&barFn'
},
controllerAs: 'barCtrl',
controller: function() {
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
expect(this.str).toBe('Hello, second world!');
expect(this.fn()).toBe('second called!');
controller2Called = true;
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.fn = valueFn('called!');
$rootScope.string = 'world';
$rootScope.data = {'foo': 'bar','baz': 'biz'};
$rootScope.fn2 = valueFn('second called!');
$rootScope.string2 = 'second world';
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
element = $compile(
'<div ' +
'foo ' +
'foo-data="data" ' +
'foo-str="Hello, {{string}}!" ' +
'foo-fn="fn()" ' +
'bar ' +
'bar-data="data2" ' +
'bar-str="Hello, {{string2}}!" ' +
'bar-fn="fn2()" > ' +
'</div>')($rootScope);
$rootScope.$digest();
expect(controller1Called).toBe(true);
expect(controller2Called).toBe(true);
});
});
it('should bind to multiple directives controllers via object notation (new iso scope)', function() {
var controller1Called = false;
var controller2Called = false;
module(function($compileProvider, $controllerProvider) {
$compileProvider.directive('foo', valueFn({
bindToController: {
'data': '=fooData',
'str': '@fooStr',
'fn': '&fooFn'
},
scope: {},
controllerAs: 'fooCtrl',
controller: function() {
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
expect(this.str).toBe('Hello, world!');
expect(this.fn()).toBe('called!');
controller1Called = true;
}
}));
$compileProvider.directive('bar', valueFn({
bindToController: {
'data': '=barData',
'str': '@barStr',
'fn': '&barFn'
},
controllerAs: 'barCtrl',
controller: function() {
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
expect(this.str).toBe('Hello, second world!');
expect(this.fn()).toBe('second called!');
controller2Called = true;
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.fn = valueFn('called!');
$rootScope.string = 'world';
$rootScope.data = {'foo': 'bar','baz': 'biz'};
$rootScope.fn2 = valueFn('second called!');
$rootScope.string2 = 'second world';
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
element = $compile(
'<div ' +
'foo ' +
'foo-data="data" ' +
'foo-str="Hello, {{string}}!" ' +
'foo-fn="fn()" ' +
'bar ' +
'bar-data="data2" ' +
'bar-str="Hello, {{string2}}!" ' +
'bar-fn="fn2()" > ' +
'</div>')($rootScope);
$rootScope.$digest();
expect(controller1Called).toBe(true);
expect(controller2Called).toBe(true);
});
});
it('should bind to multiple directives controllers via object notation (new scope)', function() {
var controller1Called = false;
var controller2Called = false;
module(function($compileProvider, $controllerProvider) {
$compileProvider.directive('foo', valueFn({
bindToController: {
'data': '=fooData',
'str': '@fooStr',
'fn': '&fooFn'
},
scope: true,
controllerAs: 'fooCtrl',
controller: function() {
expect(this.data).toEqualData({'foo': 'bar', 'baz': 'biz'});
expect(this.str).toBe('Hello, world!');
expect(this.fn()).toBe('called!');
controller1Called = true;
}
}));
$compileProvider.directive('bar', valueFn({
bindToController: {
'data': '=barData',
'str': '@barStr',
'fn': '&barFn'
},
scope: true,
controllerAs: 'barCtrl',
controller: function() {
expect(this.data).toEqualData({'foo2': 'bar2', 'baz2': 'biz2'});
expect(this.str).toBe('Hello, second world!');
expect(this.fn()).toBe('second called!');
controller2Called = true;
}
}));
});
inject(function($compile, $rootScope) {
$rootScope.fn = valueFn('called!');
$rootScope.string = 'world';
$rootScope.data = {'foo': 'bar','baz': 'biz'};
$rootScope.fn2 = valueFn('second called!');
$rootScope.string2 = 'second world';
$rootScope.data2 = {'foo2': 'bar2', 'baz2': 'biz2'};
element = $compile(
'<div ' +
'foo ' +
'foo-data="data" ' +
'foo-str="Hello, {{string}}!" ' +
'foo-fn="fn()" ' +
'bar ' +
'bar-data="data2" ' +
'bar-str="Hello, {{string2}}!" ' +
'bar-fn="fn2()" > ' +
'</div>')($rootScope);
$rootScope.$digest();
expect(controller1Called).toBe(true);
expect(controller2Called).toBe(true);
});
});
it('should evaluate against the correct scope, when using `bindToController` (new scope)',
function() {
module(function($compileProvider, $controllerProvider) {
$controllerProvider.register({
'ParentCtrl': function() {
this.value1 = 'parent1';
this.value2 = 'parent2';
this.value3 = function() { return 'parent3'; };
},
'ChildCtrl': function() {
this.value1 = 'child1';
this.value2 = 'child2';
this.value3 = function() { return 'child3'; };
}
});
$compileProvider.directive('child', valueFn({
scope: true,
controller: 'ChildCtrl as ctrl',
bindToController: {
fromParent1: '@',
fromParent2: '=',
fromParent3: '&'
},
template: ''
}));
});
inject(function($compile, $rootScope) {
element = $compile(
'<div ng-controller="ParentCtrl as ctrl">' +
'<child ' +
'from-parent-1="{{ ctrl.value1 }}" ' +
'from-parent-2="ctrl.value2" ' +
'from-parent-3="ctrl.value3">' +
'</child>' +
'</div>')($rootScope);
$rootScope.$digest();
var parentCtrl = element.controller('ngController');
var childCtrl = element.find('child').controller('child');
expect(childCtrl.fromParent1).toBe(parentCtrl.value1);
expect(childCtrl.fromParent1).not.toBe(childCtrl.value1);
expect(childCtrl.fromParent2).toBe(parentCtrl.value2);
expect(childCtrl.fromParent2).not.toBe(childCtrl.value2);
expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3());
expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3());
childCtrl.fromParent2 = 'modified';
$rootScope.$digest();
expect(parentCtrl.value2).toBe('modified');
expect(childCtrl.value2).toBe('child2');
});
}
);
it('should evaluate against the correct scope, when using `bindToController` (new iso scope)',
function() {
module(function($compileProvider, $controllerProvider) {
$controllerProvider.register({
'ParentCtrl': function() {
this.value1 = 'parent1';
this.value2 = 'parent2';
this.value3 = function() { return 'parent3'; };
},
'ChildCtrl': function() {
this.value1 = 'child1';
this.value2 = 'child2';
this.value3 = function() { return 'child3'; };
}
});
$compileProvider.directive('child', valueFn({
scope: {},
controller: 'ChildCtrl as ctrl',
bindToController: {
fromParent1: '@',
fromParent2: '=',
fromParent3: '&'
},
template: ''
}));
});
inject(function($compile, $rootScope) {
element = $compile(
'<div ng-controller="ParentCtrl as ctrl">' +
'<child ' +
'from-parent-1="{{ ctrl.value1 }}" ' +
'from-parent-2="ctrl.value2" ' +
'from-parent-3="ctrl.value3">' +
'</child>' +
'</div>')($rootScope);
$rootScope.$digest();
var parentCtrl = element.controller('ngController');
var childCtrl = element.find('child').controller('child');
expect(childCtrl.fromParent1).toBe(parentCtrl.value1);
expect(childCtrl.fromParent1).not.toBe(childCtrl.value1);
expect(childCtrl.fromParent2).toBe(parentCtrl.value2);
expect(childCtrl.fromParent2).not.toBe(childCtrl.value2);
expect(childCtrl.fromParent3()()).toBe(parentCtrl.value3());
expect(childCtrl.fromParent3()()).not.toBe(childCtrl.value3());
childCtrl.fromParent2 = 'modified';
$rootScope.$digest();
expect(parentCtrl.value2).toBe('modified');
expect(childCtrl.value2).toBe('child2');
});
}
);
it('should put controller in scope when controller identifier present but not using controllerAs', function() {
var controllerCalled = false;
var myCtrl;
@@ -5612,6 +5931,62 @@ describe('$compile', function() {
});
});
//see issue https://github.com/angular/angular.js/issues/12936
it('should use the proper scope when it is on the root element of a replaced directive template', function() {
module(function() {
directive('isolate', valueFn({
scope: {},
replace: true,
template: '<div trans>{{x}}</div>',
link: function(scope, element, attr, ctrl) {
scope.x = 'iso';
}
}));
directive('trans', valueFn({
transclude: 'content',
link: function(scope, element, attr, ctrl, $transclude) {
$transclude(function(clone) {
element.append(clone);
});
}
}));
});
inject(function($rootScope, $compile) {
element = $compile('<isolate></isolate>')($rootScope);
$rootScope.x = 'root';
$rootScope.$apply();
expect(element.text()).toEqual('iso');
});
});
//see issue https://github.com/angular/angular.js/issues/12936
it('should use the proper scope when it is on the root element of a replaced directive template with child scope', function() {
module(function() {
directive('child', valueFn({
scope: true,
replace: true,
template: '<div trans>{{x}}</div>',
link: function(scope, element, attr, ctrl) {
scope.x = 'child';
}
}));
directive('trans', valueFn({
transclude: 'content',
link: function(scope, element, attr, ctrl, $transclude) {
$transclude(function(clone) {
element.append(clone);
});
}
}));
});
inject(function($rootScope, $compile) {
element = $compile('<child></child>')($rootScope);
$rootScope.x = 'root';
$rootScope.$apply();
expect(element.text()).toEqual('child');
});
});
it('should not leak if two "element" transclusions are on the same element (with debug info)', function() {
+10
View File
@@ -2537,8 +2537,18 @@ describe('input', function() {
describe('URL_REGEXP', function() {
/* global URL_REGEXP: false */
it('should validate url', function() {
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
expect(URL_REGEXP.test('http://server:123/path')).toBe(true);
expect(URL_REGEXP.test('https://server:123/path')).toBe(true);
expect(URL_REGEXP.test('file:///home/user')).toBe(true);
expect(URL_REGEXP.test('mailto:user@example.com?subject=Foo')).toBe(true);
expect(URL_REGEXP.test('r2-d2.c3-p0://localhost/foo')).toBe(true);
expect(URL_REGEXP.test('abc:/foo')).toBe(true);
expect(URL_REGEXP.test('http:')).toBe(false);
expect(URL_REGEXP.test('a@B.c')).toBe(false);
expect(URL_REGEXP.test('a_B.c')).toBe(false);
expect(URL_REGEXP.test('0scheme://example.com')).toBe(false);
expect(URL_REGEXP.test('http://example.com:9999/~~``')).toBe(false);
});
});
});
+2 -2
View File
@@ -468,12 +468,12 @@ describe('ngClass animations', function() {
};
});
});
inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $$body) {
inject(function($compile, $rootScope, $browser, $rootElement, $animate, $document) {
$animate.enabled(true);
$rootScope.val = 'crazy';
element = angular.element('<div ng-class="val"></div>');
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
+126 -2
View File
@@ -2,7 +2,7 @@
describe('ngOptions', function() {
var scope, formElement, element, $compile;
var scope, formElement, element, $compile, linkLog;
function compile(html) {
formElement = jqLite('<form name="form">' + html + '</form>');
@@ -104,6 +104,53 @@ describe('ngOptions', function() {
});
});
beforeEach(module(function($compileProvider, $provide) {
linkLog = [];
$compileProvider
.directive('customSelect', function() {
return {
restrict: "E",
replace: true,
scope: {
ngModel: '=',
options: '='
},
templateUrl: 'select_template.html',
link: function(scope, $element, attributes) {
scope.selectable_options = scope.options;
}
};
})
.directive('oCompileContents', function() {
return {
link: function(scope, element) {
linkLog.push('linkCompileContents');
$compile(element.contents())(scope);
}
};
});
$provide.decorator('ngOptionsDirective', function($delegate) {
var origPreLink = $delegate[0].link.pre;
var origPostLink = $delegate[0].link.post;
$delegate[0].compile = function() {
return {
pre: origPreLink,
post: function() {
linkLog.push('linkNgOptions');
origPostLink.apply(this, arguments);
}
};
};
return $delegate;
});
}));
beforeEach(inject(function($rootScope, _$compile_) {
scope = $rootScope.$new(); //create a child scope because the root scope can't be $destroy-ed
$compile = _$compile_;
@@ -2029,7 +2076,9 @@ describe('ngOptions', function() {
expect(option.text()).toBe('is blank');
});
it('should support option without a value attribute', function() {
it('should be ignored when it has no value attribute', function() {
// The option value is set to the textContent if there's no value attribute,
// so in that case it doesn't count as a blank option
createSingleSelect('<option>--select--</option>');
scope.$apply(function() {
scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
@@ -2084,6 +2133,81 @@ describe('ngOptions', function() {
expect(element[0].selectedIndex).toEqual(0);
expect(scope.selected).toEqual([]);
});
it('should be possible to use ngIf in the blank option', function() {
var option;
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply(function() {
scope.values = [{name: 'A'}];
scope.isBlank = true;
});
expect(element.find('option').length).toBe(2);
option = element.find('option').eq(0);
expect(option.val()).toBe('');
expect(option.text()).toBe('blank');
scope.$apply(function() {
scope.isBlank = false;
});
expect(element.find('option').length).toBe(1);
option = element.find('option').eq(0);
expect(option.text()).toBe('A');
});
it('should be possible to use ngIf in the blank option when values are available upon linking',
function() {
var options;
scope.values = [{name: 'A'}];
createSingleSelect('<option ng-if="isBlank" value="">blank</option>');
scope.$apply('isBlank = true');
options = element.find('option');
expect(options.length).toBe(2);
expect(options.eq(0).val()).toBe('');
expect(options.eq(0).text()).toBe('blank');
scope.$apply('isBlank = false');
options = element.find('option');
expect(options.length).toBe(1);
expect(options.eq(0).text()).toBe('A');
}
);
it('should not throw when a directive compiles the blank option before ngOptions is linked', function() {
expect(function() {
createSelect({
'o-compile-contents': '',
'name': 'select',
'ng-model': 'value',
'ng-options': 'item for item in items'
}, true);
}).not.toThrow();
expect(linkLog).toEqual(['linkCompileContents', 'linkNgOptions']);
});
it('should not throw with a directive that replaces', inject(function($templateCache, $httpBackend) {
$templateCache.put('select_template.html', '<select ng-options="option as option for option in selectable_options"> <option value="">This is a test</option> </select>');
scope.options = ['a', 'b', 'c', 'd'];
expect(function() {
element = $compile('<custom-select ng-model="value" options="options"></custom-select>')(scope);
scope.$digest();
}).not.toThrow();
dealoc(element);
}));
});
+9
View File
@@ -612,6 +612,15 @@ describe('ngRepeat', function() {
expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|');
});
it('should expose iterator offset as $index when iterating over objects with length key value 0', function() {
element = $compile(
'<ul>' +
'<li ng-repeat="(key, val) in items">{{key}}:{{val}}:{{$index}}|</li>' +
'</ul>')(scope);
scope.items = {'misko':'m', 'shyam':'s', 'frodo':'f', 'length':0};
scope.$digest();
expect(element.text()).toEqual('misko:m:0|shyam:s:1|frodo:f:2|length:0:3|');
});
it('should expose iterator position as $first, $middle and $last when iterating over arrays',
function() {
+2
View File
@@ -62,6 +62,8 @@ describe('filters', function() {
it('should format according different separators', function() {
var num = formatNumber(1234567.1, pattern, '.', ',', 2);
expect(num).toBe('1.234.567,10');
num = formatNumber(1e-14, pattern, '.', ',', 14);
expect(num).toBe('0,00000000000001');
});
it('should format with or without fractionSize', function() {
+7 -4
View File
@@ -140,16 +140,19 @@ describe('Filter: limitTo', function() {
it('should return an empty array if Y exceeds input length', function() {
expect(limitTo(items, '3', 12)).toEqual([]);
expect(limitTo(items, 4, '-12')).toEqual([]);
expect(limitTo(items, -3, '12')).toEqual([]);
expect(limitTo(items, '-4', -12)).toEqual([]);
});
it('should return an empty string if Y exceeds input length', function() {
expect(limitTo(str, '3', 12)).toEqual("");
expect(limitTo(str, 4, '-12')).toEqual("");
expect(limitTo(str, -3, '12')).toEqual("");
expect(limitTo(str, '-4', -12)).toEqual("");
});
it('should start at 0 if Y is negative and exceeds input length', function() {
expect(limitTo(items, 4, '-12')).toEqual(['a', 'b', 'c', 'd']);
expect(limitTo(items, '-4', -12)).toEqual(['e', 'f', 'g', 'h']);
expect(limitTo(str, 4, '-12')).toEqual("tuvw");
expect(limitTo(str, '-4', -12)).toEqual("wxyz");
});
it('should return the entire string beginning from Y if X is positive and X+Y exceeds input length', function() {
+7
View File
@@ -233,6 +233,13 @@ describe('$httpBackend', function() {
expect(MockXhr.$$lastInstance.withCredentials).toBe(true);
});
it('should call $xhrFactory with method and url', function() {
var mockXhrFactory = jasmine.createSpy('mockXhrFactory').andCallFake(createMockXhr);
$backend = createHttpBackend($browser, mockXhrFactory, $browser.defer, callbacks, fakeDocument);
$backend('GET', '/some-url', 'some-data', noop);
expect(mockXhrFactory).toHaveBeenCalledWith('GET', '/some-url');
});
describe('responseType', function() {
+19
View File
@@ -1391,6 +1391,25 @@ describe('$http', function() {
expect(callback).toHaveBeenCalledOnce();
expect(callback.mostRecentCall.args[0]).toBe('RESP-FIRST:V1');
});
it('should apply `transformResponse` even if the response data is empty', function(data) {
var callback = jasmine.createSpy('transformResponse');
var config = {transformResponse: callback};
$httpBackend.expect('GET', '/url1').respond(200, undefined);
$httpBackend.expect('GET', '/url2').respond(200, null);
$httpBackend.expect('GET', '/url3').respond(200, '');
$http.get('/url1', config);
$http.get('/url2', config);
$http.get('/url3', config);
$httpBackend.flush();
expect(callback.callCount).toBe(3);
expect(callback.calls[0].args[0]).toBe(undefined);
expect(callback.calls[1].args[0]).toBe(null);
expect(callback.calls[2].args[0]).toBe('');
});
});
});
+25
View File
@@ -2141,6 +2141,31 @@ describe('$location', function() {
})
);
it('should fire $locationChangeSuccess when browser location changes to URL which ends with #',
inject(function($location, $browser, $rootScope, $log) {
$location.url('/somepath');
$rootScope.$apply();
expect($browser.url()).toEqual('http://server/#/somepath');
expect($location.url()).toEqual('/somepath');
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('start', newUrl, oldUrl);
});
$rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
$log.info('after', newUrl, oldUrl);
});
$browser.url('http://server/#');
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['start', 'http://server/', 'http://server/#/somepath']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/', 'http://server/#/somepath']);
})
);
it('should allow redirect during browser url change',
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
+38
View File
@@ -2692,6 +2692,15 @@ describe('parser', function() {
});
});
it('should prevent the exploit', function() {
expect(function() {
scope.$eval('(1)[{0: "__proto__", 1: "__proto__", 2: "__proto__", 3: "safe", length: 4, toString: [].pop}].foo = 1');
}).toThrow();
if (!msie || msie > 10) {
expect((1)['__proto__'].foo).toBeUndefined();
}
});
it('should prevent the exploit', function() {
expect(function() {
scope.$eval('' +
@@ -2703,6 +2712,35 @@ describe('parser', function() {
'');
}).toThrow();
});
it('should prevent assigning in the context of a constructor', function() {
expect(function() {
scope.$eval("''.constructor.join");
}).not.toThrow();
expect(function() {
scope.$eval("''.constructor.join = ''.constructor.join");
}).toThrow();
expect(function() {
scope.$eval("''.constructor[0] = ''");
}).toThrow();
expect(function() {
scope.$eval("(0).constructor[0] = ''");
}).toThrow();
expect(function() {
scope.$eval("{}.constructor[0] = ''");
}).toThrow();
// foo.constructor is the object constructor.
expect(function() {
scope.$eval("foo.constructor[0] = ''", {foo: {}});
}).toThrow();
// foo.constructor is not a constructor.
expect(function() {
scope.$eval("foo.constructor[0] = ''", {foo: {constructor: ''}});
}).not.toThrow();
expect(function() {
scope.$eval("objConstructor = {}.constructor; objConstructor.join = ''");
}).toThrow();
});
});
it('should call the function from the received instance and not from a new one', function() {
+30
View File
@@ -1211,6 +1211,36 @@ describe('Scope', function() {
expect(child.parentModel).toBe('parent');
expect(child.childModel).toBe('child');
}));
if (msie === 9) {
// See issue https://github.com/angular/angular.js/issues/10706
it('should completely disconnect all child scopes on IE9', inject(function($rootScope) {
var parent = $rootScope.$new(),
child1 = parent.$new(),
child2 = parent.$new(),
grandChild1 = child1.$new(),
grandChild2 = child1.$new();
child1.$destroy();
$rootScope.$digest();
expect(isDisconnected(parent)).toBe(false);
expect(isDisconnected(child1)).toBe(true);
expect(isDisconnected(child2)).toBe(false);
expect(isDisconnected(grandChild1)).toBe(true);
expect(isDisconnected(grandChild2)).toBe(true);
function isDisconnected($scope) {
return $scope.$$nextSibling === null &&
$scope.$$prevSibling === null &&
$scope.$$childHead === null &&
$scope.$$childTail === null &&
$scope.$root === null &&
$scope.$$watchers === null;
}
}));
}
});
+42 -3
View File
@@ -121,7 +121,7 @@ describe("ngAnimate $$animateCssDriver", function() {
var from, to, fromAnimation, toAnimation;
beforeEach(module(function() {
return function($rootElement, $$body) {
return function($rootElement, $document) {
from = element;
to = jqLite('<div></div>');
fromAnimation = { element: from, event: 'enter' };
@@ -129,8 +129,14 @@ describe("ngAnimate $$animateCssDriver", function() {
$rootElement.append(from);
$rootElement.append(to);
// we need to do this so that style detection works
$$body.append($rootElement);
var doc = $document[0];
// there is one test in here that expects the rootElement
// to superceed the body node
if (!$rootElement[0].contains(doc.body)) {
// we need to do this so that style detection works
jqLite(doc.body).append($rootElement);
}
};
}));
@@ -975,6 +981,39 @@ describe("ngAnimate $$animateCssDriver", function() {
expect(completed).toBe(true);
}));
it("should use <body> as the element container if the rootElement exists outside of the <body> tag", function() {
module(function($provide) {
$provide.factory('$rootElement', function($document) {
return jqLite($document[0].querySelector('html'));
});
});
inject(function($rootElement, $rootScope, $animate, $document) {
ss.addRule('.ending-element', 'width:9999px; height:6666px; display:inline-block;');
var fromAnchor = jqLite('<div></div>');
from.append(fromAnchor);
var toAnchor = jqLite('<div></div>');
to.append(toAnchor);
$rootElement.append(fromAnchor);
$rootElement.append(toAnchor);
var completed = false;
driver({
from: fromAnimation,
to: toAnimation,
anchors: [{
'out': fromAnchor,
'in': toAnchor
}]
}).start();
var clone = captureLog[2].element[0];
expect(clone.parentNode).toBe($document[0].body);
});
});
});
});
});
+149 -84
View File
@@ -35,12 +35,12 @@ describe("ngAnimate $animateCss", function() {
});
it("should return false if neither transitions or keyframes are supported by the browser",
inject(function($animateCss, $sniffer, $rootElement, $$body) {
inject(function($animateCss, $sniffer, $rootElement, $document) {
var animator;
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$sniffer.transitions = $sniffer.animations = false;
animator = $animateCss(element, {
@@ -54,13 +54,13 @@ describe("ngAnimate $animateCss", function() {
if (!browserSupportsCssAnimations()) return;
it("should not attempt an animation if animations are globally disabled",
inject(function($animateCss, $animate, $rootElement, $$body) {
inject(function($animateCss, $animate, $rootElement, $document) {
$animate.enabled(false);
var animator, element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
animator = $animateCss(element, {
duration: 10,
@@ -109,9 +109,9 @@ describe("ngAnimate $animateCss", function() {
describe("rAF usage", function() {
it("should buffer all requests into a single requestAnimationFrame call",
inject(function($animateCss, $$rAF, $rootScope, $$body, $rootElement) {
inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var count = 0;
var runners = [];
@@ -149,8 +149,8 @@ describe("ngAnimate $animateCss", function() {
};
});
});
inject(function($animateCss, $$rAF, $$body, $rootElement) {
$$body.append($rootElement);
inject(function($animateCss, $$rAF, $document, $rootElement) {
jqLite($document[0].body).append($rootElement);
function makeRequest() {
var element = jqLite('<div></div>');
@@ -169,10 +169,10 @@ describe("ngAnimate $animateCss", function() {
describe("animator and runner", function() {
var animationDuration = 5;
var element, animator;
beforeEach(inject(function($animateCss, $rootElement, $$body) {
beforeEach(inject(function($animateCss, $rootElement, $document) {
element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
animator = $animateCss(element, {
event: 'enter',
@@ -365,10 +365,10 @@ describe("ngAnimate $animateCss", function() {
{ timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration });
}
beforeEach(inject(function($rootElement, $$body) {
beforeEach(inject(function($rootElement, $document) {
element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
options = { event: 'enter', structural: true };
}));
@@ -638,9 +638,9 @@ describe("ngAnimate $animateCss", function() {
describe("staggering", function() {
it("should apply a stagger based when an active ng-EVENT-stagger class with a transition-delay is detected",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
ss.addRule('.ng-enter', 'transition:2s linear all');
@@ -679,9 +679,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should apply a stagger based when for all provided addClass/removeClass CSS classes",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.red-add-stagger,' +
'.blue-remove-stagger,' +
@@ -749,9 +749,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should block the transition animation between start and animate when staggered",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
ss.addRule('.ng-enter', 'transition:2s linear all;');
@@ -780,9 +780,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should block (pause) the keyframe animation between start and animate when staggered",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
ss.addRule('.ng-enter', prefix + 'animation:my_animation 2s;');
@@ -809,9 +809,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should not apply a stagger if the transition delay value is inherited from a earlier CSS class",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
@@ -828,9 +828,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should apply a stagger only if the transition duration value is zero when inherited from a earlier CSS class",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
ss.addRule('.transition-animation.ng-enter-stagger',
@@ -854,9 +854,9 @@ describe("ngAnimate $animateCss", function() {
it("should ignore animation staggers if only transition animations were detected",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
@@ -874,9 +874,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should ignore transition staggers if only keyframe animations were detected",
inject(function($animateCss, $$body, $rootElement) {
inject(function($animateCss, $document, $rootElement) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
ss.addRule('.transition-animation', prefix + 'animation:2s 5s my_animation;');
@@ -894,9 +894,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should start on the highest stagger value if both transition and keyframe staggers are used together",
inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:0.5s;' +
prefix + 'animation-delay:1s');
@@ -932,9 +932,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should apply the closing timeout ontop of the stagger timeout",
inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
ss.addRule('.ng-enter', 'transition:10s linear all;');
@@ -959,9 +959,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should apply the closing timeout ontop of the stagger timeout with an added delay",
inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
inject(function($animateCss, $document, $rootElement, $timeout, $browser) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:50s;');
@@ -986,9 +986,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should issue a stagger if a stagger value is provided in the options",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.ng-enter', 'transition:2s linear all');
var elm, i, elements = [];
@@ -1025,9 +1025,9 @@ describe("ngAnimate $animateCss", function() {
}));
it("should only add/remove classes once the stagger timeout has passed",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var element = jqLite('<div class="green"></div>');
$rootElement.append(element);
@@ -1052,13 +1052,13 @@ describe("ngAnimate $animateCss", function() {
describe("closing timeout", function() {
it("should close off the animation after 150% of the animation time has passed",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
ss.addRule('.ng-enter', 'transition:10s linear all;');
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, { event: 'enter', structural: true });
animator.start();
@@ -1075,13 +1075,13 @@ describe("ngAnimate $animateCss", function() {
}));
it("should close off the animation after 150% of the animation time has passed and consider the detected delay value",
inject(function($animateCss, $$body, $rootElement, $timeout) {
inject(function($animateCss, $document, $rootElement, $timeout) {
ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:30s;');
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, { event: 'enter', structural: true });
animator.start();
@@ -1098,13 +1098,13 @@ describe("ngAnimate $animateCss", function() {
}));
it("should still resolve the animation once expired",
inject(function($animateCss, $$body, $rootElement, $timeout, $animate, $rootScope) {
inject(function($animateCss, $document, $rootElement, $timeout, $animate, $rootScope) {
ss.addRule('.ng-enter', 'transition:10s linear all;');
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, { event: 'enter', structural: true });
@@ -1123,13 +1123,13 @@ describe("ngAnimate $animateCss", function() {
}));
it("should not resolve/reject after passing if the animation completed successfully",
inject(function($animateCss, $$body, $rootElement, $timeout, $rootScope, $animate) {
inject(function($animateCss, $document, $rootElement, $timeout, $rootScope, $animate) {
ss.addRule('.ng-enter', 'transition:10s linear all;');
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, { event: 'enter', structural: true });
@@ -1160,7 +1160,7 @@ describe("ngAnimate $animateCss", function() {
}));
it("should close all stacked animations after the last timeout runs on the same element",
inject(function($animateCss, $$body, $rootElement, $timeout, $animate) {
inject(function($animateCss, $document, $rootElement, $timeout, $animate) {
var now = 0;
spyOn(Date, 'now').andCallFake(function() {
@@ -1177,7 +1177,7 @@ describe("ngAnimate $animateCss", function() {
var element = jqLite('<div class="elm"></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
// timeout will be at 1500s
animate(element, 'red', doneSpy);
@@ -1218,11 +1218,11 @@ describe("ngAnimate $animateCss", function() {
}));
it("should not throw an error any pending timeout requests resolve after the element has already been removed",
inject(function($animateCss, $$body, $rootElement, $timeout, $animate) {
inject(function($animateCss, $document, $rootElement, $timeout, $animate) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.red', 'transition:1s linear all;');
@@ -1255,8 +1255,8 @@ describe("ngAnimate $animateCss", function() {
}
}));
return function($$body, $rootElement) {
$$body.append($rootElement);
return function($document, $rootElement) {
jqLite($document[0].body).append($rootElement);
};
}));
@@ -1340,7 +1340,7 @@ describe("ngAnimate $animateCss", function() {
});
it('should avoid applying the same cache to an element a follow-up animation is run on the same element',
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
function endTransition(element, elapsedTime) {
browserTrigger(element, 'transitionend',
@@ -1357,7 +1357,7 @@ describe("ngAnimate $animateCss", function() {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
startAnimation(element, 0.5, 'red');
expect(element.attr('style')).toContain('transition');
@@ -1377,14 +1377,14 @@ describe("ngAnimate $animateCss", function() {
}));
it("should clear cache if no animation so follow-up animation on the same element will not be from cache",
inject(function($animateCss, $rootElement, $$body, $$rAF) {
inject(function($animateCss, $rootElement, $document, $$rAF) {
var element = jqLite('<div class="rclass"></div>');
var options = {
event: 'enter',
structural: true
};
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, options);
expect(animator.$$willAnimate).toBeFalsy();
@@ -1396,11 +1396,11 @@ describe("ngAnimate $animateCss", function() {
}));
it('should apply a custom temporary class when a non-structural animation is used',
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$animateCss(element, {
event: 'super',
@@ -1416,10 +1416,10 @@ describe("ngAnimate $animateCss", function() {
describe("structural animations", function() {
they('should decorate the element with the ng-$prop CSS class',
['enter', 'leave', 'move'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$animateCss(element, {
event: event,
@@ -1433,10 +1433,10 @@ describe("ngAnimate $animateCss", function() {
they('should decorate the element with the ng-$prop-active CSS class',
['enter', 'leave', 'move'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, {
event: event,
@@ -1454,10 +1454,10 @@ describe("ngAnimate $animateCss", function() {
they('should remove the ng-$prop and ng-$prop-active CSS classes from the element once the animation is done',
['enter', 'leave', 'move'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var animator = $animateCss(element, {
event: event,
@@ -1511,10 +1511,10 @@ describe("ngAnimate $animateCss", function() {
they('should place a CSS transition block after the preparation function to block accidental style changes',
['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.cool-animation', 'transition:1.5s linear all;');
element.addClass('cool-animation');
@@ -1541,10 +1541,10 @@ describe("ngAnimate $animateCss", function() {
they('should not place a CSS transition block if options.skipBlocking is provided',
['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
inject(function($animateCss, $rootElement, $$body, $window) {
inject(function($animateCss, $rootElement, $document, $window) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.cool-animation', 'transition:1.5s linear all;');
element.addClass('cool-animation');
@@ -1582,10 +1582,10 @@ describe("ngAnimate $animateCss", function() {
they('should place a CSS transition block after the preparation function even if a duration is provided',
['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
ss.addRule('.cool-animation', 'transition:1.5s linear all;');
element.addClass('cool-animation');
@@ -1616,11 +1616,11 @@ describe("ngAnimate $animateCss", function() {
});
it('should allow multiple events to be animated at the same time',
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$animateCss(element, {
event: ['enter', 'leave', 'move'],
@@ -1688,10 +1688,10 @@ describe("ngAnimate $animateCss", function() {
they('should remove the class-$prop-add and class-$prop-active CSS classes from the element once the animation is done',
['enter', 'leave', 'move'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var options = {};
options.event = event;
@@ -1713,7 +1713,7 @@ describe("ngAnimate $animateCss", function() {
they('should allow the class duration styles to be recalculated once started if the CSS classes being applied result new transition styles',
['add', 'remove'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
var element = jqLite('<div></div>');
@@ -1728,7 +1728,7 @@ describe("ngAnimate $animateCss", function() {
}
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var options = {};
options[event + 'Class'] = 'natural-class';
@@ -1749,13 +1749,13 @@ describe("ngAnimate $animateCss", function() {
they('should force the class-based values to be applied early if no options.applyClassEarly is used as an option',
['enter', 'leave', 'move'], function(event) {
inject(function($animateCss, $rootElement, $$body) {
inject(function($animateCss, $rootElement, $document) {
ss.addRule('.blue.ng-' + event, 'transition:2s linear all;');
var element = jqLite('<div class="red"></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
var runner = $animateCss(element, {
addClass: 'blue',
@@ -1790,8 +1790,8 @@ describe("ngAnimate $animateCss", function() {
describe("options", function() {
var element;
beforeEach(module(function() {
return function($rootElement, $$body) {
$$body.append($rootElement);
return function($rootElement, $document) {
jqLite($document[0].body).append($rootElement);
element = jqLite('<div></div>');
$rootElement.append(element);
@@ -2741,10 +2741,10 @@ describe("ngAnimate $animateCss", function() {
describe("[easing]", function() {
var element;
beforeEach(inject(function($$body, $rootElement) {
beforeEach(inject(function($document, $rootElement) {
element = jqLite('<div></div>');
$rootElement.append(element);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
}));
it("should apply easing to a transition animation if it exists", inject(function($animateCss) {
@@ -2786,6 +2786,71 @@ describe("ngAnimate $animateCss", function() {
}));
});
describe("[cleanupStyles]", function() {
it("should cleanup [from] and [to] styles that have been applied for the animation when true",
inject(function($animateCss) {
var runner = $animateCss(element, {
duration: 1,
from: { background: 'gold' },
to: { color: 'brown' },
cleanupStyles: true
}).start();
assertStyleIsPresent(element, 'background', true);
assertStyleIsPresent(element, 'color', false);
triggerAnimationStartFrame();
assertStyleIsPresent(element, 'background', true);
assertStyleIsPresent(element, 'color', true);
runner.end();
assertStyleIsPresent(element, 'background', false);
assertStyleIsPresent(element, 'color', false);
function assertStyleIsPresent(element, style, bool) {
expect(element[0].style[style])[bool ? 'toBeTruthy' : 'toBeFalsy']();
}
}));
it('should restore existing overidden styles already present on the element when true',
inject(function($animateCss) {
element.css('height', '100px');
element.css('width', '111px');
var runner = $animateCss(element, {
duration: 1,
from: { height: '200px', 'font-size':'66px' },
to: { height: '300px', 'font-size': '99px', width: '222px' },
cleanupStyles: true
}).start();
assertStyle(element, 'height', '200px');
assertStyle(element, 'font-size', '66px');
assertStyle(element, 'width', '111px');
triggerAnimationStartFrame();
assertStyle(element, 'height', '300px');
assertStyle(element, 'width', '222px');
assertStyle(element, 'font-size', '99px');
runner.end();
assertStyle(element, 'width', '111px');
assertStyle(element, 'height', '100px');
expect(element[0].style.getPropertyValue('font-size')).not.toBe('66px');
function assertStyle(element, prop, value) {
expect(element[0].style.getPropertyValue(prop)).toBe(value);
}
}));
});
it('should round up long elapsedTime values to close off a CSS3 animation',
inject(function($animateCss) {
@@ -2812,13 +2877,13 @@ describe("ngAnimate $animateCss", function() {
describe('SVG', function() {
it('should properly apply transitions on an SVG element',
inject(function($animateCss, $rootScope, $compile, $$body, $rootElement) {
inject(function($animateCss, $rootScope, $compile, $document, $rootElement) {
var element = $compile('<svg width="500" height="500">' +
'<circle cx="15" cy="5" r="100" fill="orange" />' +
'</svg>')($rootScope);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$rootElement.append(element);
$animateCss(element, {
+173 -36
View File
@@ -26,12 +26,12 @@ describe("animations", function() {
});
});
inject(function($animate, $rootScope, $$body) {
inject(function($animate, $rootScope, $document) {
$animate.enabled(true);
element = jqLite('<div></div>');
$animate.enter(element, $$body);
$animate.enter(element, jqLite($document[0].body));
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
@@ -116,7 +116,7 @@ describe("animations", function() {
return overriddenAnimationRunner || defaultFakeAnimationRunner;
});
return function($rootElement, $q, $animate, $$AnimateRunner, $$body) {
return function($rootElement, $q, $animate, $$AnimateRunner, $document) {
defaultFakeAnimationRunner = new $$AnimateRunner();
$animate.enabled(true);
@@ -126,7 +126,7 @@ describe("animations", function() {
$rootElement.append(parent);
$rootElement.append(parent2);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
};
}));
@@ -749,7 +749,7 @@ describe("animations", function() {
}));
it("should disable all child animations for atleast one turn when a structural animation is issued",
inject(function($animate, $rootScope, $compile, $$body, $rootElement, $$AnimateRunner) {
inject(function($animate, $rootScope, $compile, $document, $rootElement, $$AnimateRunner) {
element = $compile(
'<div><div class="if-animation" ng-if="items.length">' +
@@ -759,7 +759,7 @@ describe("animations", function() {
'</div></div>'
)($rootScope);
$$body.append($rootElement);
jqLite($document[0].body).append($rootElement);
$rootElement.append(element);
var runner = new $$AnimateRunner();
@@ -890,7 +890,6 @@ describe("animations", function() {
});
$rootScope.$digest();
$animate.flush();
expect(capturedAnimation).toBeTruthy();
expect(runner1done).toBeFalsy();
@@ -910,7 +909,6 @@ describe("animations", function() {
});
$rootScope.$digest();
$animate.flush();
expect(capturedAnimation).toBeTruthy();
expect(runner2done).toBeFalsy();
@@ -990,8 +988,6 @@ describe("animations", function() {
var doneHandler = jasmine.createSpy('addClass done');
runner.done(doneHandler);
$animate.flush();
expect(doneHandler).not.toHaveBeenCalled();
$animate.removeClass(element, 'active-class');
@@ -1011,8 +1007,6 @@ describe("animations", function() {
var doneHandler = jasmine.createSpy('addClass done');
runner.done(doneHandler);
$animate.flush();
expect(doneHandler).not.toHaveBeenCalled();
$animate.addClass(element, 'active-class');
@@ -1284,8 +1278,8 @@ describe("animations", function() {
return new $$AnimateRunner();
};
});
return function($rootElement, $$body, $animate) {
$$body.append($rootElement);
return function($rootElement, $document, $animate) {
jqLite($document[0].body).append($rootElement);
parent = jqLite('<div class="parent"></div>');
element = jqLite('<div class="element"></div>');
child = jqLite('<div class="child"></div>');
@@ -1389,14 +1383,14 @@ describe("animations", function() {
}));
it('should allow an element to pinned elsewhere and still be available in animations',
inject(function($animate, $compile, $$body, $rootElement, $rootScope) {
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
var innerParent = jqLite('<div></div>');
$$body.append(innerParent);
jqLite($document[0].body).append(innerParent);
innerParent.append($rootElement);
var element = jqLite('<div></div>');
$$body.append(element);
jqLite($document[0].body).append(element);
$animate.addClass(element, 'red');
$rootScope.$digest();
@@ -1412,16 +1406,16 @@ describe("animations", function() {
}));
it('should adhere to the disabled state of the hosted parent when an element is pinned',
inject(function($animate, $compile, $$body, $rootElement, $rootScope) {
inject(function($animate, $compile, $document, $rootElement, $rootScope) {
var innerParent = jqLite('<div></div>');
$$body.append(innerParent);
jqLite($document[0].body).append(innerParent);
innerParent.append($rootElement);
var innerChild = jqLite('<div></div>');
$rootElement.append(innerChild);
var element = jqLite('<div></div>');
$$body.append(element);
jqLite($document[0].body).append(element);
$animate.pin(element, innerChild);
@@ -1456,17 +1450,17 @@ describe("animations", function() {
};
});
return function($$body, $rootElement, $animate) {
$$body.append($rootElement);
return function($document, $rootElement, $animate) {
jqLite($document[0].body).append($rootElement);
$animate.enabled(true);
};
}));
it('should trigger a callback for an enter animation',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
var callbackTriggered = false;
$animate.on('enter', $$body, function() {
$animate.on('enter', jqLite($document[0].body), function() {
callbackTriggered = true;
});
@@ -1480,12 +1474,12 @@ describe("animations", function() {
}));
it('should fire the callback with the signature of (element, phase, data)',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
var capturedElement;
var capturedPhase;
var capturedData;
$animate.on('enter', $$body,
$animate.on('enter', jqLite($document[0].body),
function(element, phase, data) {
capturedElement = element;
@@ -1519,7 +1513,6 @@ describe("animations", function() {
element = jqLite('<div></div>');
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(callbackTriggered).toBe(false);
}));
@@ -1544,13 +1537,13 @@ describe("animations", function() {
}));
it('should remove all the event-based event listeners when $animate.off(event) is called',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('<div></div>');
var count = 0;
$animate.on('enter', element, counter);
$animate.on('enter', $$body, counter);
$animate.on('enter', jqLite($document[0].body), counter);
function counter(element, phase) {
count++;
@@ -1572,13 +1565,13 @@ describe("animations", function() {
}));
it('should remove the container-based event listeners when $animate.off(event, container) is called',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('<div></div>');
var count = 0;
$animate.on('enter', element, counter);
$animate.on('enter', $$body, counter);
$animate.on('enter', jqLite($document[0].body), counter);
function counter(element, phase) {
if (phase === 'start') {
@@ -1592,7 +1585,7 @@ describe("animations", function() {
expect(count).toBe(2);
$animate.off('enter', $$body);
$animate.off('enter', jqLite($document[0].body));
$animate.enter(element, $rootElement);
$rootScope.$digest();
@@ -1638,13 +1631,13 @@ describe("animations", function() {
}));
it('should fire a `start` callback when the animation starts with the matching element',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('<div></div>');
var capturedState;
var capturedElement;
$animate.on('enter', $$body, function(element, phase) {
$animate.on('enter', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
@@ -1658,13 +1651,13 @@ describe("animations", function() {
}));
it('should fire a `close` callback when the animation ends with the matching element',
inject(function($animate, $rootScope, $rootElement, $$body) {
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('<div></div>');
var capturedState;
var capturedElement;
$animate.on('enter', $$body, function(element, phase) {
$animate.on('enter', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
@@ -1707,6 +1700,69 @@ describe("animations", function() {
expect(count).toBe(1);
}));
it('should always detect registered callbacks after one postDigest has fired',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('<div></div>');
var spy = jasmine.createSpy();
registerCallback();
var runner = $animate.enter(element, $rootElement);
registerCallback();
$rootScope.$digest();
registerCallback();
expect(spy.callCount).toBe(0);
$animate.flush();
// this is not 3 since the 3rd callback
// was added after the first callback
// was fired
expect(spy.callCount).toBe(2);
spy.reset();
runner.end();
$animate.flush();
// now we expect all three callbacks
// to fire when the animation ends since
// the callback detection happens again
expect(spy.callCount).toBe(3);
function registerCallback() {
$animate.on('enter', element, spy);
}
}));
it('should use RAF if there are detected callbacks within the hierachy of the element being animated',
inject(function($animate, $rootScope, $rootElement, $$rAF) {
var runner;
element = jqLite('<div></div>');
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
runner.end();
assertRAFsUsed(false);
var spy = jasmine.createSpy();
$animate.on('leave', element, spy);
runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
runner.end();
assertRAFsUsed(true);
function assertRAFsUsed(bool) {
expect($$rAF.queue.length)[bool ? 'toBeGreaterThan' : 'toBe'](0);
}
}));
it('leave: should remove the element even if another animation is called after',
inject(function($animate, $rootScope, $rootElement) {
@@ -1725,5 +1781,86 @@ describe("animations", function() {
expect(isElementRemoved).toBe(true);
}));
it('leave : should trigger a callback for an leave animation',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var callbackTriggered = false;
$animate.on('leave', jqLite($document[0].body), function() {
callbackTriggered = true;
});
element = jqLite('<div></div>');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(callbackTriggered).toBe(true);
}));
it('leave : 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('leave : should fire a `start` callback when the animation starts with the matching element',
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('leave : should fire a `close` callback when the animation ends with the matching element',
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);
}));
});
});
+2 -2
View File
@@ -836,8 +836,8 @@ describe('$$animation', function() {
element = jqLite('<div></div>');
parent = jqLite('<div></div>');
return function($$AnimateRunner, $q, $rootElement, $$body) {
$$body.append($rootElement);
return function($$AnimateRunner, $rootElement, $document) {
jqLite($document[0].body).append($rootElement);
$rootElement.append(parent);
mockedDriverFn = function(element, method, options, domOperation) {
-9
View File
@@ -1,9 +0,0 @@
'use strict';
describe('$$body', function() {
beforeEach(module('ngAnimate'));
it("should inject $document", inject(function($$body, $document) {
expect($$body).toEqual(jqLite($document[0].body));
}));
});
+34 -2
View File
@@ -7,12 +7,12 @@ describe('ngAnimate integration tests', function() {
var element, html, ss;
beforeEach(module(function() {
return function($rootElement, $document, $$body, $window, $animate) {
return function($rootElement, $document, $window, $animate) {
$animate.enabled(true);
ss = createMockStyleSheet($document, $window);
var body = $$body;
var body = jqLite($document[0].body);
html = function(element) {
body.append($rootElement);
$rootElement.append(element);
@@ -319,6 +319,38 @@ describe('ngAnimate integration tests', function() {
}
});
});
it('should trigger callbacks at the start and end of an animation',
inject(function($rootScope, $rootElement, $animate, $compile) {
ss.addRule('.animate-me', 'transition:2s linear all;');
var parent = jqLite('<div><div ng-if="exp" class="animate-me"></div></div>');
element = parent.find('div');
html(parent);
$compile(parent)($rootScope);
$rootScope.$digest();
var spy = jasmine.createSpy();
$animate.on('enter', parent, spy);
$rootScope.exp = true;
$rootScope.$digest();
element = parent.find('div');
$animate.flush();
expect(spy.callCount).toBe(1);
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
$animate.flush();
expect(spy.callCount).toBe(2);
dealoc(element);
}));
});
describe('JS animations', function() {
+12 -10
View File
@@ -616,16 +616,18 @@ describe('$aria', function() {
describe('tabindex', function() {
beforeEach(injectScopeAndCompiler);
it('should not attach to native controls', function() {
var element = [
$compile("<button ng-click='something'></button>")(scope),
$compile("<a ng-href='#/something'>")(scope),
$compile("<input ng-model='val'>")(scope),
$compile("<textarea ng-model='val'></textarea>")(scope),
$compile("<select ng-model='val'></select>")(scope),
$compile("<details ng-model='val'></details>")(scope)
];
expectAriaAttrOnEachElement(element, 'tabindex', undefined);
they('should not attach to native control $prop', {
'button': "<button ng-click='something'></button>",
'a': "<a ng-href='#/something'>",
'input[text]': "<input type='text' ng-model='val'>",
'input[radio]': "<input type='radio' ng-model='val'>",
'input[checkbox]': "<input type='checkbox' ng-model='val'>",
'textarea': "<textarea ng-model='val'></textarea>",
'select': "<select ng-model='val'></select>",
'details': "<details ng-model='val'></details>"
}, function(html) {
compileElement(html);
expect(element.attr('tabindex')).toBeUndefined();
});
it('should not attach to random ng-model elements', function() {
+44
View File
@@ -372,6 +372,50 @@ describe('ngMessages', function() {
expect(trim(element.text())).toEqual("Enter something");
}));
// issue #12856
it('should only detach the message object that is associated with the message node being removed',
inject(function($rootScope, $compile, $animate) {
// We are going to spy on the `leave` method to give us control over
// when the element is actually removed
spyOn($animate, 'leave');
// Create a basic ng-messages set up
element = $compile('<div ng-messages="col">' +
' <div ng-message="primary">Enter something</div>' +
'</div>')($rootScope);
// Trigger the message to be displayed
$rootScope.col = { primary: true };
$rootScope.$digest();
expect(messageChildren(element).length).toEqual(1);
var oldMessageNode = messageChildren(element)[0];
// Remove the message
$rootScope.col = { primary: undefined };
$rootScope.$digest();
// Since we have spied on the `leave` method, the message node is still in the DOM
expect($animate.leave).toHaveBeenCalledOnce();
var nodeToRemove = $animate.leave.mostRecentCall.args[0][0];
expect(nodeToRemove).toBe(oldMessageNode);
$animate.leave.reset();
// Add the message back in
$rootScope.col = { primary: true };
$rootScope.$digest();
// Simulate the animation completing on the node
jqLite(nodeToRemove).remove();
// We should not get another call to `leave`
expect($animate.leave).not.toHaveBeenCalled();
// There should only be the new message node
expect(messageChildren(element).length).toEqual(1);
var newMessageNode = messageChildren(element)[0];
expect(newMessageNode).not.toBe(oldMessageNode);
}));
it('should render animations when the active/inactive classes are added/removed', function() {
module('ngAnimate');
+34 -4
View File
@@ -156,7 +156,7 @@ describe("resource", function() {
});
it('should ignore slashes of undefinend parameters', function() {
it('should ignore slashes of undefined parameters', function() {
var R = $resource('/Path/:a/:b/:c');
$httpBackend.when('GET', '/Path').respond('{}');
@@ -181,7 +181,7 @@ describe("resource", function() {
R.get({a:6, b:7, c:8});
});
it('should not ignore leading slashes of undefinend parameters that have non-slash trailing sequence', function() {
it('should not ignore leading slashes of undefined parameters that have non-slash trailing sequence', function() {
var R = $resource('/Path/:a.foo/:b.bar/:c.baz');
$httpBackend.when('GET', '/Path/.foo/.bar.baz').respond('{}');
@@ -242,7 +242,7 @@ describe("resource", function() {
});
it('should not encode @ in url params', function() {
//encodeURIComponent is too agressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt
//encodeURIComponent is too aggressive and doesn't follow http://www.ietf.org/rfc/rfc3986.txt
//with regards to the character set (pchar) allowed in path segments
//so we need this test to make sure that we don't over-encode the params and break stuff like
//buzz api which uses @self
@@ -1308,7 +1308,7 @@ describe("resource", function() {
});
describe('resource', function() {
var $httpBackend, $resource;
var $httpBackend, $resource, $q;
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
@@ -1319,6 +1319,7 @@ describe('resource', function() {
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$resource = $injector.get('$resource');
$q = $injector.get('$q');
}));
@@ -1356,5 +1357,34 @@ describe('resource', function() {
);
});
it('should cancel the request if timeout promise is resolved', function() {
var canceler = $q.defer();
$httpBackend.when('GET', '/CreditCard').respond({data: '123'});
var CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'GET',
timeout: canceler.promise
}
});
CreditCard.query();
canceler.resolve();
expect($httpBackend.flush).toThrow(new Error("No pending request to flush !"));
canceler = $q.defer();
CreditCard = $resource('/CreditCard', {}, {
query: {
method: 'GET',
timeout: canceler.promise
}
});
CreditCard.query();
expect($httpBackend.flush).not.toThrow();
});
});