Compare commits

...

56 Commits

Author SHA1 Message Date
Matias Niemelä 0681a5400e feat(ngAnimate): ensure JS animations recognize $animateCss directly
JS Animations now recognize the response object returned from a call to
`$animateCss`. We can now setup our JS animation code to fully rely on
$animateCss to take charge without having to call the doneFn callback on
our own.

```js
// before
.animation('.my-css-animation', function($animateCss) {
  return {
    enter: function(element, doneFn) {
      var animator = $animateCss(element, {
        event: 'enter',
        structural: true,
        from: { background: 'red' },
        to: { background: 'blue' }
      });
      animator.start().done(doneFn);
    }
  };
});

// now
.animation('.my-css-animation', function($animateCss) {
  return {
    enter: function(element) {
      return $animateCss(element, {
        event: 'enter',
        structural: true,
        from: { background: 'red' },
        to: { background: 'blue' }
      });
    }
  };
});
```
2015-05-07 14:33:28 -07:00
Matias Niemelä d5683d2116 fix($animateCss): ensure that an object is always returned even when no animation is set to run
Before in RC0 and RC1 $animateCss would not return anything if a
CSS-based animation was not detected. This was a messy API decision
which resulted in the user having to have an if statement to handle the
failure case. This patch ensures that an animator object with the start()
and end() functions is always returned. If an animation is not detected
then the preperatory CSS styles and classes are removed immediately and
the element is cleaned up, however a "dump" animator object is still
returned which allows for callbacks and promises to be applied.

The returned object now also contains a `valid` property which can be
examined to determine whether an animation is set to run on the element.

BREAKING CHANGE: The $animateCss service will now always return an
object even if the animation is not set to run. If your code is using
$animateCss then please consider the following code change:

```
// before
var animator = $animateCss(element, { ... });
if (!animator) {
  continueApp();
  return;
}
var runner = animator.start();
runner.done(continueApp);
runner.then(continueApp);

// now
var animator = $animateCss(element, { ... });
var runner = animator.start();
runner.done(continueApp);
runner.then(continueApp);
```
2015-05-07 14:33:20 -07:00
Matias Niemelä df24410c17 fix(ngAnimate): force use of ng-anchor instead of a suffixed -anchor CSS class when triggering anchor animations
This fix changes anchored animations in ngAnimate to not append a series
of CSS classes with a `-suffix` prefix to the anchor element. Use
the `ng-anchor` instead CSS class instead.

BREAKING CHANGE: Prior to this fix there were to ways to apply CSS
animation code to an anchor animation. With this fix, the suffixed
CSS -anchor classes are now not used anymore for CSS anchor animations.

Instead just use the `ng-anchor` CSS class like so:

```html
<div class="container-animation" ng-if="on">
   <div ng-animate-ref="1" class="my-anchor-element"></div>
</div>

<div class="container-animation" ng-if="!on">
   <div ng-animate-ref="1" class="my-anchor-element"></div>
</div>
```

**before**:
```css
/* before (notice the container-animation CSS class) */
.container-animation-anchor {
  transition:0.5s linear all;
}
```

**now**:
```css
/* now (just use `ng-anchor` on a class that both the
   elements that contain `ng-animate-ref` share) */
.my-anchor-element.ng-anchor {
  transition:0.5s linear all;
}
```
2015-05-07 14:03:54 -07:00
Matias Niemelä e6d053de09 fix(ngAnimate): rename ng-animate-anchor to ng-anchor
BREAKING CHANGE: if your CSS code made use of the `ng-animate-anchor`
CSS class for referencing the anchored animation element then your
code must now use `ng-anchor` instead.
2015-05-07 14:03:47 -07:00
Matias Niemelä e001400237 fix(ngAnimate): ensure that shared CSS classes between anchor nodes are retained
This patch ensures that all of the CSS classes that exist on both
anchor nodes (the nodes that contain a `ng-animate-ref` attribute)
are not removed from the cloned element during the anchor animation.
(Previously the `in` animation would accidentally remove the CSS
classes of the first element.)

Closes #11681
2015-05-07 14:03:31 -07:00
Matias Niemelä 1002b80a6f fix(ngAnimate): prohibit usage of the ng-animate class with classNameFilter
Since ngAnimate uses the `ng-animate` CSS class internally to track
state it is better to keep this as a reserved CSS class to avoid
accidentally adding / removing the CSS class when an animation is
started and closed.

BREAKING CHANGE: partially or fully using a regex value containing
`ng-animate` as a token is not allowed anymore. Doing so will trigger a
minErr exception to be thrown.

So don't do this:

```js
// only animate elements that contain the `ng-animate` CSS class
$animateProvider.classNameFilter(/ng-animate/);

// or partially contain it
$animateProvider.classNameFilter(/some-class ng-animate another-class/);
```

but this is OK:

```js
$animateProvider.classNameFilter(/ng-animate-special/);
```

Closes #11431
Closes #11807
2015-05-07 13:02:45 +01:00
Matias Niemelä 7bb01bae72 docs(ngAnimate): add docs for the usage of the ng-animate CSS class 2015-05-07 13:01:55 +01:00
Matias Niemelä f7e9ff1aba fix(ngAnimate): ensure that the temporary CSS classes are applied before detection
Prior to 1.4 the `ng-animate` CSS class was applied before the CSS
getComputedStyle detection was issued. This was lost in the 1.4
refactor, however, this patch restores the functionality.

Closes #11769
Closes #11804
2015-05-07 12:49:47 +01:00
Caitlin Potter f7b999703f fix(ngClass): add/remove classes which are properties of Object.prototype
Previously, ngClass and ngAnimate would track the status of classes using an ordinary object.
This causes problems when class names match names of properties in Object.prototype, including
non-standard Object.prototype properties such as 'watch' and 'unwatch' in Firefox. Because of
this shadowing, ngClass and ngAnimate were unable to correctly determine the changed status
of these classes.

In orderto accomodate this patch, some changes have been necessary elsewhere in the codebase,
in order to facilitate iterating, comparingand copying objects with a null prototype, or which
shadow the `hasOwnProperty` method

Summary:

- fast paths for various internal functions when createMap() is used
- Make createMap() safe for internal functions like copy/equals/forEach
- Use createMap() in more places to avoid needing hasOwnProperty()

R=@matsko

Closes #11813
Closes #11814
2015-05-06 19:45:04 -04:00
Kent C. Dodds b2ae35cd2c docs(error/nonassign): add optional binding example
Closes #11701
2015-05-06 17:50:10 +01:00
Matias Niemelä 64d05180a6 fix(ngAnimate): ensure that all jqLite elements are deconstructed properly
Prior to this fix if a form DOM element was fed into parts of the
ngAnimate queuing code it would attempt to detect if it is a jqLite
object in an unstable way which would allow a form element to return an
inner input element by index. This patch ensures that jqLite instances
are properly detected using a helper method.

Closes #11658
2015-05-05 16:39:28 -07:00
Peter Bacon Darwin b5a9053ba3 fix(ngOptions): ensure that tracked properties are always watched
Commit 47f9fc3e70 failed to account for changes to
the tracked value of model items in a collection where the select was `multiple`.

See https://github.com/angular/angular.js/pull/11743#discussion_r29424578

Closes #11784
2015-05-05 22:45:06 +01:00
Matias Niemelä db20b830fc fix(core): ensure that multiple requests to requestAnimationFrame are buffered
IE11 (and maybe some other browsers) do not optimize multiple calls to
rAF. This code makes that happen internally within the $$rAF service
before the next frame kicks in.

Closes #11791
2015-05-05 14:20:05 -07:00
Matias Niemelä 2aacc2d622 fix(ngAnimate): ensure animations are not attempted on text nodes
With the large refactor in 1.4.0-rc.0, the detection code failed to
filter out text nodes from animating. This fix ensures that now properly
happens.

Closes #11703
2015-05-05 14:18:18 -07:00
Vladimir Lugovsky bab474aa8b fix($compile): ensure directive names have no leading or trailing whitespace
Closes #11397
Closes #11772
2015-05-05 21:07:19 +01:00
Peter Bacon Darwin f2c94c61d1 style($http): add missing semi-colon 2015-05-05 20:44:59 +01:00
Lucas Galfaso 2420a0a77e fix($httpParamSerializerJQLike): follow jQuery logic for nested params
Closes #11551
Closes #11635
2015-05-05 20:32:59 +01:00
Peter Bacon Darwin 9711e3e10e docs(guide/i18n): fix internal link to MessageFormat Extensions section 2015-05-05 20:17:27 +01:00
Peter Bacon Darwin ae826b007c docs(angular.element): clarify when jquery must be loaded for Angular to use it
Closes #3716
2015-05-05 20:01:26 +01:00
Nick Anderson 2ea23e0685 test(ngRepeat): fix test setup for ngRepeat stability test
The repeated template contained `{{key}}:{{val}}` but the repeat expression
was `"item in items"`, so `key` and `val` were not actually available.

The tests were passing anyway, since they did not rely upon the actual
text content of the template.

Closes #11761
2015-05-05 19:58:16 +01:00
Peter Bacon Darwin f1663088c3 docs($location): fix trailing whitespace
Closes #11741
Closes #11744
2015-05-05 19:54:15 +01:00
Damien Nozay 84daf9752a docs($location): explain difference between $location.host() and location.host.
Closes #11741
Closes #11744
2015-05-05 19:43:01 +01:00
Rich Snapp 426a5ac054 fix(jqLite): check for "length" in obj in isArrayLike to prevent iOS8 JIT bug from surfacing
Closes #11508
2015-05-05 17:54:46 +01:00
Peter Bacon Darwin 6874cca158 docs($injector): add array annotation to all injectable parameters
Closes #11507
2015-05-05 14:57:51 +01:00
Kevin Brogan 34c1a68fa8 docs($provide): add array annotation type to $provide.decorator parameter
The $provide.decorator function, as per the documentation, "is called using
the auto.injector.invoke method and is therefore fully injectable."

The current @param contradicts this by stating that only a functions may
be used as an argument.

Closes #11507
2015-05-05 14:51:15 +01:00
Peter Bacon Darwin 1268b17bc1 test(ngOptions): remove unnnecessary var 2015-05-01 21:36:05 +01:00
Peter Bacon Darwin ae98dadf6d fix(ngOptions): ensure label is watched in all cases
Closes #11765
2015-05-01 21:22:31 +01:00
Martin Staffa a2a684fe24 docs(changelog): wrap jqLite example containing html with code block
This prevents the markdown parser from garbling the input and putting
out broken html.

Closes #11778
Fixes #11777
Fixes #11539
2015-05-01 21:00:02 +01:00
Kent C. Dodds 40e00cdf34 docs(ngJq): update to indicate common pitfall
change docs for ngJq so it mentions that the placement of the directive is important with regards to the angular script.

Closes #11779
Closes #11780
2015-05-01 09:43:40 -04:00
Peter Bacon Darwin dfa722a8a6 fix(ngOptions): iterate over the options collection in the same way as ngRepeat
In `ngRepeat` if the object to be iterated over is "array-like" then it only iterates
over the numerical indexes rather than every key on the object. This prevents "helper"
methods from being included in the rendered collection.

This commit changes `ngOptions` to iterate in the same way.

BREAKING CHANGE:

Although it is unlikely that anyone is using it in this way, this change does change the
behaviour of `ngOptions` in the following case:

* you are iterating over an array-like object, using the array form of the `ngOptions` syntax
(`item.label for item in items`) and that object contains non-numeric property keys.

In this case these properties with non-numeric keys will be ignored.

** Here array-like is defined by the result of a call to this internal function:
https://github.com/angular/angular.js/blob/v1.4.0-rc.1/src/Angular.js#L198-L211 **

To get the desired behaviour you need to iterate using the object form of the `ngOptions` syntax
(`value.label` for (key, value) in items)`).

Closes #11733
2015-05-01 12:22:24 +01:00
Leonardo Braga cc961888cd docs(ngModel): improve formatting of $modelValue
Closes #11483
2015-04-30 22:54:14 +02:00
Rodrigo Parra 69f4d0ff70 docs(ngSwitch): Replace tt tag with code tag
Use of tt is discouraged, see:
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tt
http://www.w3.org/wiki/HTML/Elements/tt

Closes #11509
2015-04-30 22:50:54 +02:00
Jeff Wesson 7a04968673 docs(form): replace obsolete tt element
Removes the [**obsolete** HTML Teletype Text Element `<tt>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/tt)
and replaces it with [`<code>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/code).
This adds more semanticity and is part of the [HTML5 specification](http://www.w3.org/TR/html5/text-level-semantics.html#the-code-element).

Closes #11570
2015-04-30 22:49:17 +02:00
Steve Mao f2f9843ea8 docs(ngCloak): remove information for ie7
IE7 is not supported. Also change `#template2` text to `'world'`.

Closes #11661
2015-04-30 22:47:23 +02:00
Ron Tsui 7d57961b63 style(docs): improve formatting in code comment
Closes #11674
2015-04-30 22:44:51 +02:00
thatType 477e4047f7 docs(contribute): transpose "however" and "it's"
Transpose "however" and "it's" on line 156 for slightly better readability

Closes #11686
2015-04-30 22:43:31 +02:00
Ryan Atkinson b35e744791 docs(ngAnimate): fix typo in 'greetingBox' directive
Closes #11766
Closes #11747
2015-04-30 22:42:02 +02:00
Peter Bacon Darwin d83bddcb79 chore(docs): include attribute type in directive usage
Closes #11415
2015-04-29 17:47:48 +01:00
Peter Bacon Darwin 5db6709f8d chore(utils.js): only set maximum stack size on non-win32 machines
Closes #4831
2015-04-29 16:32:10 +01:00
Brent Dearth f3b393258e docs(select): remove obsolete ngOptions equality check comments 2015-04-29 10:40:17 -04:00
Peter Bacon Darwin 47f9fc3e70 fix(ngOptions): use watchCollection not deep watch of ngModel
Using a deep watch caused Angular to enter an infinite recursion in the
case that the model contains a circular reference.  Using `$watchCollection`
instead prevents this from happening.

This change means that we will not re-render the directive when there is
a change below the first level of properties on the model object. Making
such a change is a strange corner case that is unlikely to occur in practice
and the directive is not designed to support such a situation. The
documentation has been updated to clarify this behaviour.

This is not a breaking change since in 1.3.x this scenario did not work
at all. Compare 1.3.15: http://plnkr.co/edit/zsgnhflQ3M1ClUSrsne0?p=preview
to snapshot: http://plnkr.co/edit/hI48vBc0GscyYTYucJ0U?p=preview

Closes #11372
Closes #11653
Closes #11743
2015-04-29 14:30:36 +01:00
Mike Calvanese 74eb17d7c8 fix(ngTouch): check undefined tagName for SVG event target
When target click element is an SVG, event.target.tagName and event.target.blur are undefined in Chrome v40 on iOS 8.1.3
2015-04-27 21:46:18 +01:00
gonengar c075126c2e docs(guide/Unit Testing): fixing the example for testing filter.
Hi there,
It seems that in the example which starts at line 256 there needs to
be an injection for $filter as in the previous example.

Closes #11410
2015-04-27 22:35:08 +02:00
Logesh Paul 6c632d9cb0 docs(*): definition list readability improvement
Closes #11398
Closes #11187
2015-04-27 22:35:06 +02:00
Viktor Zozulyak feeea8a1c8 docs(angular.injector): missing optional parameter mark
Closes #11528
2015-04-27 22:35:02 +02:00
yankee42 d20de4abe7 docs(ngModel): use arguments.length instead of angular.isDefined(newName) to distinguish getter/setter usage
Closes #11604
2015-04-27 22:34:59 +02:00
Adam 071b1bc790 docs(angular.element): css() api incompatibility.
"When a number is passed as the value, jQuery will convert it to a string and add px to the end of that string."
http://api.jquery.com/css/#css2

jqLite does not appear to do this.

I can submit if fix desired.

Closes #11614
2015-04-27 22:34:57 +02:00
Bruno Coelho 4089f538c3 docs(guide/Scopes): remove unnecessary parenthesis
Closes #11645
2015-04-27 22:34:54 +02:00
cexbrayat 03d4bbc16f docs(ngJq): fix directive usage 2015-04-27 22:34:51 +02:00
Michał Gołębiowski 6d07005b18 chore(docs): don't use Chrome Frame
Chrome Frame has stopped development with Chrome 32 release; we shouldn't rely
on it in the docs.

Closes #11742
2015-04-27 21:29:30 +01:00
Jaco Pretorius 266bc6520b feat($resource): include request context in error message
include the request context (method & url) in badcfg error message

Closes #11363
2015-04-27 19:08:35 +02:00
micellius f0dd7c0374 docs(CHANGELOG): change name for 1.4.0-rc.1
Align version name for 1.4.0-rc.1 to be compliant with version naming convention:
Sartorial Chronography => sartorial-chronography

Closes #11732
2015-04-27 19:01:46 +02:00
Adrian-Catalin Neatu f0b88e047e docs(guide/Migrating from Previous Versions): spelling mistake
Closes #11739
2015-04-27 18:56:48 +02:00
Emmanuel DEMEY ef2435d176 docs(guide/Accessibility): remove an extra "a" in the A11Y doc
Closes #11740
2015-04-27 18:54:41 +02:00
Mike Calvanese 95521876eb fix(ngTouch): don't prevent click event after a touchmove
Remove the touchmove handler so that resetState is not called on touchmove.
The touchend event handler already prevents the click from being triggered
if the distance moved exceeds the MOVE_TOLERANCE, so detection of touchmove
is not needed. Previously, because of resetState on touchmove, the click would
not be triggered even if the event coordinates changed by only 1px or 2px,
which seems to be very common for taps on mobile browsers.

Closes #10985
2015-04-27 14:28:09 +01:00
Kent C. Dodds 4eb16ae4b7 docs($q): improve documentation of promises that resolve with another promise
Adds short explanation of promise chaining and a link for further explanation.

Closes #11708
Closes #11712
2015-04-27 14:20:31 +01:00
56 changed files with 1329 additions and 368 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
<a name="v1.4.0-rc.1"></a>
# v1.4.0-rc.1 Sartorial Chronography (2015-04-24)
# v1.4.0-rc.1 sartorial-chronography (2015-04-24)
## Bug Fixes
@@ -4877,7 +4877,7 @@ For more info: http://blog.angularjs.org/2013/12/angularjs-13-new-release-approa
- properly toggle multiple classes
([4e73c80b](https://github.com/angular/angular.js/commit/4e73c80b17bd237a8491782bcf9e19f1889e12ed),
[#4467](https://github.com/angular/angular.js/issues/4467), [#6448](https://github.com/angular/angular.js/issues/6448))
- make jqLite('<iframe src="someurl">').contents() return iframe document, as in jQuery
- make `jqLite(<iframe src="someurl">').contents()` return iframe document, as in jQuery
([05fbed57](https://github.com/angular/angular.js/commit/05fbed5710b702c111c1425a9e241c40d13b0a54),
[#6320](https://github.com/angular/angular.js/issues/6320), [#6323](https://github.com/angular/angular.js/issues/6323))
- **numberFilter:** convert all non-finite/non-numbers/non-numeric strings to the empty string
@@ -9253,7 +9253,7 @@ with the `$route` service
mocks now part of `angular-mocks.js` (commit f5d08963)
### Bug Fixes
- <select> (one/multiple) could not chose from a list of objects (commit 347be5ae)
- `<select>` (one/multiple) could not chose from a list of objects (commit 347be5ae)
- null and other falsy values should not be rendered in the view (issue #242)
### Docs
+1 -1
View File
@@ -14,6 +14,6 @@ ng\:form {
visibility:hidden;
}
.ng-animate-anchor {
.ng-anchor {
position:absolute;
}
+6
View File
@@ -583,6 +583,12 @@ ul.events > li {
margin-bottom:40px;
}
.definition-table td {
padding: 8px;
border: 1px solid #eee;
vertical-align: top;
}
@media only screen and (min-width: 769px) and (max-width: 991px) {
.main-body-grid {
margin-top: 160px;
+1 -1
View File
@@ -6,7 +6,7 @@ var packagePath = __dirname;
var Package = require('dgeni').Package;
// Create and export a new Dgeni package called angularjs. This package depends upon
// the ngdoc,nunjucks and examples packages defined in the dgeni-packages npm module.
// the ngdoc, nunjucks, and examples packages defined in the dgeni-packages npm module.
module.exports = new Package('angularjs', [
require('dgeni-packages/ngdoc'),
require('dgeni-packages/nunjucks'),
@@ -3,7 +3,7 @@
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="Description"
content="AngularJS is what HTML would have been, had it been designed for building web-apps.
Declarative templates with data-binding, MVC, dependency injection and great
+60
View File
@@ -0,0 +1,60 @@
{% macro typeList(types) -%}
{% for typeName in types %}<a href="" class="{$ typeName | typeClass $}">{$ typeName | escape $}</a>{% endfor %}
{%- endmacro -%}
{%- macro paramTable(params) %}
<table class="variables-matrix input-arguments">
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{% for param in params %}
<tr>
<td>
{$ param.name $}
{% if param.alias %}| {$ param.alias $}{% endif %}
{% if param.optional %}<div><em>(optional)</em></div>{% endif %}
</td>
<td>
{$ typeList(param.typeList) $}
</td>
<td>
{$ param.description | marked $}
{% if param.defaultValue %}<p><em>(default: {$ param.defaultValue $})</em></p>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endmacro -%}
{%- macro directiveParam(name, type, join, sep) %}
{%- if type.optional %}[{% endif -%}
{$ name | dashCase $}{$ join $}{$ type.name $}{$ sep $}
{%- if type.optional %}]{% endif -%}
{% endmacro -%}
{%- macro functionSyntax(fn) %}
{%- set sep = joiner(', ') -%}
{% marked -%}
`{$ fn.name $}({%- for param in fn.params %}{$ sep() $}
{%- if param.type.optional %}[{% endif -%}
{$ param.name $}
{%- if param.type.optional %}]{% endif -%}
{% endfor %});`
{%- endmarked %}
{% endmacro -%}
{%- macro typeInfo(fn) -%}
<table class="variables-matrix return-arguments">
<tr>
<td>{$ typeList(fn.typeList) $}</td>
<td>{$ fn.description | marked $}</td>
</tr>
</table>
{%- endmacro -%}
+1 -1
View File
@@ -5,4 +5,4 @@
This error occurs when the name of a directive is not valid.
Directives must start with a lowercase character.
Directives must start with a lowercase character and must not contain leading or trailing whitespaces.
+17 -1
View File
@@ -36,9 +36,25 @@ Following are invalid uses of this directive:
```
To resolve this error, always use path expressions with scope properties that are two-way data-bound:
To resolve this error, do one of the following options:
- use path expressions with scope properties that are two-way data-bound like so:
```
<my-directive bind="some.property">
<my-directive bind="some[3]['property']">
```
- Make the binding optional
```
myModule.directive('myDirective', function factory() {
return {
...
scope: {
localValue: '=?bind' // <-- the '?' makes it optional
}
...
}
});
```
+1 -1
View File
@@ -43,7 +43,7 @@ Currently, ngAria interfaces with the following directives:
Most of ngAria's heavy lifting happens in the {@link ngModel ngModel}
directive. For elements using ngModel, special attention is paid by ngAria if that element also
has a a role or type of `checkbox`, `radio`, `range` or `textbox`.
has a role or type of `checkbox`, `radio`, `range` or `textbox`.
For those elements using ngModel, ngAria will dynamically bind and update the following ARIA
attributes (if they have not been explicitly specified by the developer):
+2 -2
View File
@@ -19,7 +19,7 @@ Angular supports i18n/l10n for {@link ng.filter:date date}, {@link ng.filter:num
{@link ng.filter:currency currency} filters.
Localizable pluralization is supported via the {@link ng.directive:ngPluralize `ngPluralize`
directive}. Additionally, you can use <a href="#MessageFormat">MessageFormat extensions</a> to
directive}. Additionally, you can use {@link guide/i18n#messageformat-extensions MessageFormat extensions} to
`$interpolate` for localizable pluralization and gender support in all interpolations via the
`ngMessageFormat` module.
@@ -142,7 +142,7 @@ displaying the date with a timezone specified by the developer.
<a name="MessageFormat"></a>
## MessageFormat extensions
## MessageFormat extensions
You can write localizable plural and gender based messages in Angular interpolation expressions and
`$interpolate` calls.
+1 -1
View File
@@ -29,7 +29,7 @@ to render error messages with ngMessages that are listed with a directive such a
involves pulling error message data from a server and then displaying that data via the mechanics of ngMessages. Be
sure to read the breaking change involved with `ngMessagesInclude` to upgrade your template code.
Other changes, such as the ordering of elements with ngRepeat and ngOptions, may also effect the behavior of your
Other changes, such as the ordering of elements with ngRepeat and ngOptions, may also affect the behavior of your
application. And be sure to also read up on the changes to `$cookies`. The migration jump from 1.3 to 1.4 should be
relatively straightforward otherwise.
+1 -1
View File
@@ -264,7 +264,7 @@ the `$digest` phase. This delay is desirable, since it coalesces multiple model
3. **Model mutation**
For mutations to be properly observed, you should make them only within the {@link
ng.$rootScope.Scope#$apply scope.$apply()}. (Angular APIs do this
ng.$rootScope.Scope#$apply scope.$apply()}. Angular APIs do this
implicitly, so no extra `$apply` call is needed when doing synchronous work in controllers,
or asynchronous work with {@link ng.$http $http}, {@link ng.$timeout $timeout}
or {@link ng.$interval $interval} services.
+5
View File
@@ -260,6 +260,11 @@ myModule.filter('length', function() {
});
describe('length filter', function() {
beforeEach(inject(function(_$filter_){
$filter= _$filter_;
}));
it('returns 0 when given null', function() {
var length = $filter('length');
expect(length(null)).toEqual(0);
+1 -1
View File
@@ -153,7 +153,7 @@ grunt test:unit --browsers Opera,Firefox
Note there should be _no spaces between browsers_. `Opera, Firefox` is INVALID.
During development it's however more productive to continuously run unit tests every time the source or test files
During development, however, it's more productive to continuously run unit tests every time the source or test files
change. To execute tests in this mode run:
1. To start the Karma server, capture Chrome browser and run unit tests, run:
+5 -1
View File
@@ -190,7 +190,7 @@ module.exports = {
shell.exec(
'java ' +
this.java32flags() + ' ' +
'-Xmx2g ' +
this.memoryRequirement() + ' ' +
'-cp bower_components/closure-compiler/compiler.jar' + classPathSep +
'bower_components/ng-closure-runner/ngcompiler.jar ' +
'org.angularjs.closurerunner.NgClosureRunner ' +
@@ -217,6 +217,10 @@ module.exports = {
}.bind(this));
},
memoryRequirement: function() {
return (process.platform === 'win32') ? '' : '-Xmx2g';
},
//returns the 32-bit mode force flags for java compiler if supported, this makes the build much faster
java32flags: function(){
+65 -19
View File
@@ -36,6 +36,7 @@
isUndefined: true,
isDefined: true,
isObject: true,
isBlankObject: true,
isString: true,
isNumber: true,
isDate: true,
@@ -175,6 +176,7 @@ var
splice = [].splice,
push = [].push,
toString = Object.prototype.toString,
getPrototypeOf = Object.getPrototypeOf,
ngMinErr = minErr('ng'),
/** @name angular */
@@ -200,7 +202,9 @@ function isArrayLike(obj) {
return false;
}
var length = obj.length;
// 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;
@@ -265,12 +269,25 @@ function forEach(obj, iterator, context) {
}
} else if (obj.forEach && obj.forEach !== forEach) {
obj.forEach(iterator, context, obj);
} else {
} else if (isBlankObject(obj)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in obj) {
iterator.call(context, obj[key], key, obj);
}
} else if (typeof obj.hasOwnProperty === 'function') {
// Slow path for objects inheriting Object.prototype, hasOwnProperty check needed
for (key in obj) {
if (obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key, obj);
}
}
} else {
// Slow path for objects which do not have a method `hasOwnProperty`
for (key in obj) {
if (hasOwnProperty.call(obj, key)) {
iterator.call(context, obj[key], key, obj);
}
}
}
}
return obj;
@@ -496,6 +513,16 @@ function isObject(value) {
}
/**
* Determine if a value is an object with a null prototype
*
* @returns {boolean} True if `value` is an `Object` with a null prototype
*/
function isBlankObject(value) {
return value !== null && typeof value === 'object' && !getPrototypeOf(value);
}
/**
* @ngdoc function
* @name angular.isString
@@ -779,7 +806,7 @@ function copy(source, destination, stackSource, stackDest) {
destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
destination.lastIndex = source.lastIndex;
} else if (isObject(source)) {
var emptyObject = Object.create(Object.getPrototypeOf(source));
var emptyObject = Object.create(getPrototypeOf(source));
destination = copy(source, emptyObject, stackSource, stackDest);
}
}
@@ -798,7 +825,7 @@ function copy(source, destination, stackSource, stackDest) {
stackDest.push(destination);
}
var result;
var result, key;
if (isArray(source)) {
destination.length = 0;
for (var i = 0; i < source.length; i++) {
@@ -818,21 +845,40 @@ function copy(source, destination, stackSource, stackDest) {
delete destination[key];
});
}
for (var key in source) {
if (source.hasOwnProperty(key)) {
result = copy(source[key], null, stackSource, stackDest);
if (isObject(source[key])) {
stackSource.push(source[key]);
stackDest.push(result);
if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
putValue(key, source[key], destination, stackSource, stackDest);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
putValue(key, source[key], destination, stackSource, stackDest);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
putValue(key, source[key], destination, stackSource, stackDest);
}
destination[key] = result;
}
}
setHashKey(destination,h);
}
}
return destination;
function putValue(key, val, destination, stackSource, stackDest) {
// No context allocation, trivial outer scope, easily inlined
var result = copy(val, null, stackSource, stackDest);
if (isObject(val)) {
stackSource.push(val);
stackDest.push(result);
}
destination[key] = result;
}
}
/**
@@ -913,14 +959,14 @@ function equals(o1, o2) {
} else {
if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
keySet = {};
keySet = createMap();
for (key in o1) {
if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
if (!equals(o1[key], o2[key])) return false;
keySet[key] = true;
}
for (key in o2) {
if (!keySet.hasOwnProperty(key) &&
if (!(key in keySet) &&
key.charAt(0) !== '$' &&
o2[key] !== undefined &&
!isFunction(o2[key])) return false;
@@ -957,17 +1003,17 @@ var csp = function() {
* @name ngJq
*
* @element ANY
* @param {string=} the name of the library available under `window`
* @param {string=} ngJq the name of the library available under `window`
* to be used for angular.element
* @description
* Use this directive to force the angular.element library. This should be
* used to force either jqLite by leaving ng-jq blank or setting the name of
* the jquery variable under window (eg. jQuery).
*
* Since this directive is global for the angular library, it is recommended
* that it's added to the same element as ng-app or the HTML element, but it is not mandatory.
* It needs to be noted that only the first instance of `ng-jq` will be used and all others
* ignored.
* Since angular looks for this directive when it is loaded (doesn't wait for the
* DOMContentLoaded event), it must be placed on an element that comes before the script
* which loads angular. Also, only the first instance of `ng-jq` will be used and all
* others ignored.
*
* @example
* This example shows how to force jqLite using the `ngJq` directive to the `html` tag.
+8 -7
View File
@@ -179,7 +179,7 @@ function annotate(fn, strictDi, name) {
* Return an instance of the service.
*
* @param {string} name The name of the instance to retrieve.
* @param {string} caller An optional string to provide the origin of the function call for error messages.
* @param {string=} caller An optional string to provide the origin of the function call for error messages.
* @return {*} The instance.
*/
@@ -190,8 +190,8 @@ function annotate(fn, strictDi, name) {
* @description
* Invoke the method and supply the method arguments from the `$injector`.
*
* @param {!Function} fn The function to invoke. Function parameters are injected according to the
* {@link guide/di $inject Annotation} rules.
* @param {Function|Array.<string|Function>} fn The injectable function to invoke. Function parameters are
* injected according to the {@link guide/di $inject Annotation} rules.
* @param {Object=} self The `this` for the invoked method.
* @param {Object=} locals Optional object. If preset then any argument names are read from this
* object first, before the `$injector` is consulted.
@@ -458,8 +458,8 @@ function annotate(fn, strictDi, name) {
* configure your service in a provider.
*
* @param {string} name The name of the instance.
* @param {function()} $getFn The $getFn for the instance creation. Internally this is a short hand
* for `$provide.provider(name, {$get: $getFn})`.
* @param {Function|Array.<string|Function>} $getFn The injectable $getFn for the instance creation.
* Internally this is a short hand for `$provide.provider(name, {$get: $getFn})`.
* @returns {Object} registered provider instance
*
* @example
@@ -494,7 +494,8 @@ function annotate(fn, strictDi, name) {
* as a type/class.
*
* @param {string} name The name of the instance.
* @param {Function} constructor A class (constructor function) that will be instantiated.
* @param {Function|Array.<string|Function>} constructor An injectable class (constructor function)
* that will be instantiated.
* @returns {Object} registered provider instance
*
* @example
@@ -593,7 +594,7 @@ function annotate(fn, strictDi, name) {
* object which replaces or wraps and delegates to the original service.
*
* @param {string} name The name of the service to decorate.
* @param {function()} decorator This function will be invoked when the service needs to be
* @param {Function|Array.<string|Function>} decorator This function will be invoked when the service needs to be
* instantiated and should return the decorated service instance. The function is called using
* the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable.
* Local injection arguments:
+2 -2
View File
@@ -39,7 +39,7 @@
* Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most
* commonly needed functionality with the goal of having a very small footprint.</div>
*
* To use jQuery, simply load it before `DOMContentLoaded` event fired.
* To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
*
* <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or
* jqLite; they are never raw DOM references.</div>
@@ -55,7 +55,7 @@
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
* - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`
* - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'.
* - [`data()`](http://api.jquery.com/data/)
* - [`detach()`](http://api.jquery.com/detach/)
* - [`empty()`](http://api.jquery.com/empty/)
+11 -1
View File
@@ -2,6 +2,7 @@
var $animateMinErr = minErr('$animate');
var ELEMENT_NODE = 1;
var NG_ANIMATE_CLASSNAME = 'ng-animate';
function mergeClasses(a,b) {
if (!a && !b) return '';
@@ -26,7 +27,9 @@ function splitClasses(classes) {
classes = classes.split(' ');
}
var obj = {};
// Use createMap() to prevent class assumptions involving property names in
// Object.prototype
var obj = createMap();
forEach(classes, function(klass) {
// sometimes the split leaves empty string values
// incase extra spaces were applied to the options
@@ -231,6 +234,13 @@ var $AnimateProvider = ['$provide', function($provide) {
this.classNameFilter = function(expression) {
if (arguments.length === 1) {
this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
if (this.$$classNameFilter) {
var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
if (reservedRegex.test(this.$$classNameFilter.toString())) {
throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
}
}
}
return this.$$classNameFilter;
};
+5
View File
@@ -802,6 +802,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!letter || letter !== lowercase(letter)) {
throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
}
if (name !== name.trim()) {
throw $compileMinErr('baddir',
"Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
name);
}
}
/**
+5 -5
View File
@@ -415,11 +415,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
<form name="myForm" ng-controller="FormController" class="my-form">
userType: <input name="input" ng-model="userType" required>
<span class="error" ng-show="myForm.input.$error.required">Required!</span><br>
<tt>userType = {{userType}}</tt><br>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
<code>userType = {{userType}}</code><br>
<code>myForm.input.$valid = {{myForm.input.$valid}}</code><br>
<code>myForm.input.$error = {{myForm.input.$error}}</code><br>
<code>myForm.$valid = {{myForm.$valid}}</code><br>
<code>myForm.$error.required = {{!!myForm.$error.required}}</code><br>
</form>
</file>
<file name="protractor.js" type="protractor">
+3 -1
View File
@@ -39,7 +39,9 @@ function classDirective(name, selector) {
}
function digestClassCounts(classes, count) {
var classCounts = element.data('$classCounts') || {};
// Use createMap() to prevent class assumptions involving property
// names in Object.prototype
var classCounts = element.data('$classCounts') || createMap();
var classesToUpdate = [];
forEach(classes, function(className) {
if (count > 0 || classCounts[className]) {
+1 -5
View File
@@ -33,17 +33,13 @@
* document; alternatively, the css rule above must be included in the external stylesheet of the
* application.
*
* Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they
* cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css
* class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below.
*
* @element ANY
*
* @example
<example>
<file name="index.html">
<div id="template1" ng-cloak>{{ 'hello' }}</div>
<div id="template2" ng-cloak class="ng-cloak">{{ 'hello IE7' }}</div>
<div id="template2" class="ng-cloak">{{ 'world' }}</div>
</file>
<file name="protractor.js" type="protractor">
it('should remove the template directive and css class', function() {
+11 -6
View File
@@ -504,7 +504,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* If the validity changes to invalid, the model will be set to `undefined`,
* unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
* If the validity changes to valid, it will set the model to the last available valid
* modelValue, i.e. either the last parsed value or the last value set from the scope.
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
*/
this.$validate = function() {
// ignore $validate before model is initialized
@@ -996,10 +996,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
var _name = 'Brian';
$scope.user = {
name: function(newName) {
if (angular.isDefined(newName)) {
_name = newName;
}
return _name;
// Note that newName can be undefined for two reasons:
// 1. Because it is called as a getter and thus called with no arguments
// 2. Because the property should actually be set to undefined. This happens e.g. if the
// input is invalid
return arguments.length ? (_name = newName) : _name;
}
};
}]);
@@ -1217,7 +1218,11 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
var _name = 'Brian';
$scope.user = {
name: function(newName) {
return angular.isDefined(newName) ? (_name = newName) : _name;
// Note that newName can be undefined for two reasons:
// 1. Because it is called as a getter and thus called with no arguments
// 2. Because the property should actually be set to undefined. This happens e.g. if the
// input is invalid
return arguments.length ? (_name = newName) : _name;
}
};
}]);
+107 -59
View File
@@ -31,11 +31,21 @@ var ngOptionsMinErr = minErr('ngOptions');
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
* <div class="alert alert-warning">
* **Note:** By default, `ngModel` compares by reference, not value. This is important when binding to an
* array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/). When using `track by`
* in an `ngOptions` expression, however, deep equality checks will be performed.
* </div>
* ## 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.
*
* 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.
*
* 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.
*
*
* ## `select` **`as`**
*
@@ -251,9 +261,13 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// Get the value by which we are going to track the option
// if we have a trackFn then use that (passing scope and locals)
// otherwise just hash the given viewValue
var getTrackByValue = trackBy ?
function(viewValue, locals) { return trackByFn(scope, locals); } :
function getHashOfValue(viewValue) { return hashKey(viewValue); };
var getTrackByValueFn = trackBy ?
function(value, locals) { return trackByFn(scope, locals); } :
function getHashOfValue(value) { return hashKey(value); };
var getTrackByValue = function(value, key) {
return getTrackByValueFn(value, getLocals(value, key));
};
var displayFn = $parse(match[2] || match[1]);
var groupByFn = $parse(match[3] || '');
var disableWhenFn = $parse(match[4] || '');
@@ -280,6 +294,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
return {
trackBy: trackBy,
getTrackByValue: getTrackByValue,
getWatchables: $parse(valuesFn, function(values) {
// Create a collection of things that we would like to watch (watchedArray)
// so that they can all be watched using a single $watchCollection
@@ -289,11 +304,11 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
Object.keys(values).forEach(function getWatchable(key) {
var locals = getLocals(values[key], key);
var selectValue = getTrackByValue(values[key], locals);
var selectValue = getTrackByValueFn(values[key], locals);
watchedArray.push(selectValue);
// Only need to watch the displayFn if there is a specific label expression
if (match[2]) {
if (match[2] || match[1]) {
var label = displayFn(scope, locals);
watchedArray.push(label);
}
@@ -315,17 +330,29 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// The option values were already computed in the `getWatchables` fn,
// which must have been called to trigger `getOptions`
var optionValues = valuesFn(scope) || [];
var optionValuesKeys;
var keys = Object.keys(optionValues);
keys.forEach(function getOption(key) {
// Ignore "angular" properties that start with $ or $$
if (key.charAt(0) === '$') return;
if (!keyName && isArrayLike(optionValues)) {
optionValuesKeys = optionValues;
} else {
// if object, extract keys, in enumeration order, unsorted
optionValuesKeys = [];
for (var itemKey in optionValues) {
if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
optionValuesKeys.push(itemKey);
}
}
}
var optionValuesLength = optionValuesKeys.length;
for (var index = 0; index < optionValuesLength; index++) {
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];
var locals = getLocals(value, key);
var viewValue = viewValueFn(scope, locals);
var selectValue = getTrackByValue(viewValue, locals);
var selectValue = getTrackByValueFn(viewValue, locals);
var label = displayFn(scope, locals);
var group = groupByFn(scope, locals);
var disabled = disableWhenFn(scope, locals);
@@ -333,13 +360,13 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
optionItems.push(optionItem);
selectValueMap[selectValue] = optionItem;
});
}
return {
items: optionItems,
selectValueMap: selectValueMap,
getOptionFromViewValue: function(value) {
return selectValueMap[getTrackByValue(value, getLocals(value))];
return selectValueMap[getTrackByValue(value)];
},
getViewValueFromOption: function(option) {
// If the viewValue could be an object that may be mutated by the application,
@@ -417,44 +444,54 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
};
selectCtrl.writeValue = function writeNgOptionsValue(value) {
var option = options.getOptionFromViewValue(value);
if (option && !option.disabled) {
if (selectElement[0].value !== option.selectValue) {
removeUnknownOption();
removeEmptyOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
option.element.setAttribute('selected', 'selected');
}
} else {
if (value === null || providedEmptyOption) {
removeUnknownOption();
renderEmptyOption();
} else {
removeEmptyOption();
renderUnknownOption();
}
}
};
selectCtrl.readValue = function readNgOptionsValue() {
var selectedOption = options.selectValueMap[selectElement.val()];
if (selectedOption && !selectedOption.disabled) {
removeEmptyOption();
removeUnknownOption();
return options.getViewValueFromOption(selectedOption);
}
return null;
};
// Update the controller methods for multiple selectable options
if (multiple) {
if (!multiple) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
var option = options.getOptionFromViewValue(value);
if (option && !option.disabled) {
if (selectElement[0].value !== option.selectValue) {
removeUnknownOption();
removeEmptyOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
option.element.setAttribute('selected', 'selected');
}
} else {
if (value === null || providedEmptyOption) {
removeUnknownOption();
renderEmptyOption();
} else {
removeEmptyOption();
renderUnknownOption();
}
}
};
selectCtrl.readValue = function readNgOptionsValue() {
var selectedOption = options.selectValueMap[selectElement.val()];
if (selectedOption && !selectedOption.disabled) {
removeEmptyOption();
removeUnknownOption();
return options.getViewValueFromOption(selectedOption);
}
return null;
};
// If we are using `track by` then we must watch the tracked value on the model
// since ngModel only watches for object identity change
if (ngOptions.trackBy) {
scope.$watch(
function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
function() { ngModelCtrl.$render(); }
);
}
} else {
ngModelCtrl.$isEmpty = function(value) {
return !value || value.length === 0;
@@ -486,6 +523,22 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
return selections;
};
// If we are using `track by` then we must watch these tracked values on the model
// since ngModel only watches for object identity change
if (ngOptions.trackBy) {
scope.$watchCollection(function() {
if (isArray(ngModelCtrl.$viewValue)) {
return ngModelCtrl.$viewValue.map(function(value) {
return ngOptions.getTrackByValue(value);
});
}
}, function() {
ngModelCtrl.$render();
});
}
}
@@ -512,11 +565,6 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// We will re-render the option elements if the option values or labels change
scope.$watchCollection(ngOptions.getWatchables, updateOptions);
// We also need to watch to see if the internals of the model changes, since
// ngModel only watches for object identity change
if (ngOptions.trackBy) {
scope.$watch(attr.ngModel, function() { ngModelCtrl.$render(); }, true);
}
// ------------------------------------------------------------------ //
+2 -2
View File
@@ -43,7 +43,7 @@
*
* @scope
* @priority 1200
* @param {*} ngSwitch|on expression to match against <tt>ng-switch-when</tt>.
* @param {*} ngSwitch|on expression to match against <code>ng-switch-when</code>.
* On child elements add:
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
@@ -60,7 +60,7 @@
<div ng-controller="ExampleController">
<select ng-model="selection" ng-options="item for item in items">
</select>
<tt>selection={{selection}}</tt>
<code>selection={{selection}}</code>
<hr/>
<div class="animate-switch-container"
ng-switch on="selection">
-6
View File
@@ -127,12 +127,6 @@ var SelectController =
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
* <div class="alert alert-warning">
* **Note:** By default, `ngModel` compares by reference, not value. This is important when binding to an
* array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/). When using `track by`
* in an `ngOptions` expression, however, deep equality checks will be performed.
* </div>
*
*/
var selectDirective = function() {
+45 -27
View File
@@ -9,34 +9,14 @@ var JSON_ENDS = {
};
var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
function paramSerializerFactory(jQueryMode) {
function serializeValue(v) {
if (isObject(v)) {
return isDate(v) ? v.toISOString() : toJson(v);
}
return v;
function serializeValue(v) {
if (isObject(v)) {
return isDate(v) ? v.toISOString() : toJson(v);
}
return function paramSerializer(params) {
if (!params) return '';
var parts = [];
forEachSorted(params, function(value, key) {
if (value === null || isUndefined(value)) return;
if (isArray(value) || isObject(value) && jQueryMode) {
forEach(value, function(v, k) {
var keySuffix = jQueryMode ? '[' + (!isArray(value) ? k : '') + ']' : '';
parts.push(encodeUriQuery(key + keySuffix) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
}
});
return parts.length > 0 ? parts.join('&') : '';
};
return v;
}
function $HttpParamSerializerProvider() {
/**
* @ngdoc service
@@ -51,7 +31,22 @@ function $HttpParamSerializerProvider() {
* * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
* */
this.$get = function() {
return paramSerializerFactory(false);
return function ngParamSerializer(params) {
if (!params) return '';
var parts = [];
forEachSorted(params, function(value, key) {
if (value === null || isUndefined(value)) return;
if (isArray(value)) {
forEach(value, function(v, k) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(value)));
}
});
return parts.join('&');
};
};
}
@@ -64,7 +59,30 @@ function $HttpParamSerializerJQLikeProvider() {
* Alternative $http params serializer that follows jQuery's [`param()`](http://api.jquery.com/jquery.param/) method logic.
* */
this.$get = function() {
return paramSerializerFactory(true);
return function jQueryLikeParamSerializer(params) {
if (!params) return '';
var parts = [];
serialize(params, '', true);
return parts.join('&');
function serialize(toSerialize, prefix, topLevel) {
if (toSerialize === null || isUndefined(toSerialize)) return;
if (isArray(toSerialize)) {
forEach(toSerialize, function(value) {
serialize(value, prefix + '[]');
});
} else if (isObject(toSerialize) && !isDate(toSerialize)) {
forEachSorted(toSerialize, function(value, key) {
serialize(value, prefix +
(topLevel ? '' : '[') +
key +
(topLevel ? '' : ']'));
});
} else {
parts.push(encodeUriQuery(prefix) + '=' + encodeUriQuery(serializeValue(toSerialize)));
}
}
};
};
}
+8
View File
@@ -414,11 +414,19 @@ var locationPrototype = {
*
* Return host of current url.
*
* Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
* // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
* @return {string} host of current url.
+5 -3
View File
@@ -140,9 +140,11 @@
* provide a progress indication, before the promise is resolved or rejected.
*
* This method *returns a new promise* which is resolved or rejected via the return value of the
* `successCallback`, `errorCallback`. It also notifies via the return value of the
* `notifyCallback` method. The promise cannot be resolved or rejected from the notifyCallback
* method.
* `successCallback`, `errorCallback` (unless that value is a promise, in which case it is resolved
* with the value which is resolved in that promise using
* [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
* It also notifies via the return value of the `notifyCallback` method. The promise cannot be
* resolved or rejected from the notifyCallback method.
*
* - `catch(errorCallback)` shorthand for `promise.then(null, errorCallback)`
*
+41 -3
View File
@@ -10,7 +10,7 @@ function $$RAFProvider() { //rAF
$window.webkitCancelRequestAnimationFrame;
var rafSupported = !!requestAnimationFrame;
var raf = rafSupported
var rafFn = rafSupported
? function(fn) {
var id = requestAnimationFrame(fn);
return function() {
@@ -24,8 +24,46 @@ function $$RAFProvider() { //rAF
};
};
raf.supported = rafSupported;
queueFn.supported = rafSupported;
return raf;
var cancelLastRAF;
var taskCount = 0;
var taskQueue = [];
return queueFn;
function flush() {
for (var i = 0; i < taskQueue.length; i++) {
var task = taskQueue[i];
if (task) {
taskQueue[i] = null;
task();
}
}
taskCount = taskQueue.length = 0;
}
function queueFn(asyncFn) {
var index = taskQueue.length;
taskCount++;
taskQueue.push(asyncFn);
if (index === 0) {
cancelLastRAF = rafFn(flush);
}
return function cancelQueueFn() {
if (index >= 0) {
taskQueue[index] = null;
index = null;
if (--taskCount === 0 && cancelLastRAF) {
cancelLastRAF();
cancelLastRAF = null;
taskQueue.length = 0;
}
}
};
}
}];
}
+3 -1
View File
@@ -20,6 +20,7 @@
"isElement": false,
"ELEMENT_NODE": false,
"NG_ANIMATE_CLASSNAME": false,
"NG_ANIMATE_CHILDREN_DATA": false,
"assertArg": false,
@@ -36,6 +37,7 @@
"packageStyles": false,
"removeFromArray": false,
"stripCommentsFromElement": false,
"extractElementNode": false
"extractElementNode": false,
"getDomNode": false
}
}
+37 -43
View File
@@ -39,16 +39,11 @@
* return {
* enter: function(element, doneFn) {
* var height = element[0].offsetHeight;
* var animation = $animateCss(element, {
* return $animateCss(element, {
* from: { height:'0px' },
* to: { height:height + 'px' },
* duration: 1 // one second
* });
*
* // if no possible animation can be triggered due
* // to the combination of options then `animation`
* // will be returned as undefined
* animation.start().done(doneFn);
* }
* }
* }]);
@@ -71,18 +66,13 @@
* return {
* enter: function(element, doneFn) {
* var height = element[0].offsetHeight;
* var animation = $animateCss(element, {
* return $animateCss(element, {
* addClass: 'red large-text pulse-twice',
* easing: 'ease-out',
* from: { height:'0px' },
* to: { height:height + 'px' },
* duration: 1 // one second
* });
*
* // if no possible animation can be triggered due
* // to the combination of options then `animation`
* // will be returned as undefined
* animation.start().done(doneFn);
* }
* }
* }]);
@@ -122,10 +112,11 @@
* styles using the `from` and `to` properties.
*
* ```js
* var animation = $animateCss(element, {
* var animator = $animateCss(element, {
* from: { background:'red' },
* to: { background:'blue' }
* });
* animator.start();
* ```
*
* ```css
@@ -158,10 +149,10 @@
* added and removed on the element). Once `$animateCss` is called it will return an object with the following properties:
*
* ```js
* var animation = $animateCss(element, { ... });
* var animator = $animateCss(element, { ... });
* ```
*
* Now what do the contents of our `animation` variable look like:
* Now what do the contents of our `animator` variable look like:
*
* ```js
* {
@@ -178,26 +169,14 @@
* applied to the element during the preparation phase). Note that all other properties such as duration, delay, transitions and keyframes are just properties
* and that changing them will not reconfigure the parameters of the animation.
*
* By calling `animation.start()` we do get back a promise, however, due to the nature of animations we may not want to tap into the default behaviour of
* animations (since they cause a digest to occur which may slow down the animation performance-wise). Therefore instead of calling `then` to capture when
* the animation ends be sure to call `done(callback)` (this is the recommended way to use `$animateCss` within JavaScript-animations).
* ### runner.done() vs runner.then()
* It is documented that `animation.start()` will return a promise object and this is true, however, there is also an additional method available on the
* runner called `.done(callbackFn)`. The done method works the same as `.finally(callbackFn)`, however, it does **not trigger a digest to occur**.
* Therefore, for performance reasons, it's always best to use `runner.done(callback)` instead of `runner.then()`, `runner.catch()` or `runner.finally()`
* unless you really need a digest to kick off afterwards.
*
* The example below should put this into perspective:
*
* ```js
* var animation = $animateCss(element, { ... });
*
* // remember that if there is no CSS animation detected on the element
* // then the value returned from $animateCss will be null
* if (animation) {
* animation.start().done(function() {
* // yaay the animation is over
* doneCallback();
* });
* } else {
* doneCallback();
* }
* ```
* Keep in mind that, to make this easier, ngAnimate has tweaked the JS animations API to recognize when a runner instance is returned from $animateCss
* (so there is no need to call `runner.done(doneFn)` inside of your JavaScript animation code). Check the [animation code above](#usage) to see how this works.
*
* @param {DOMElement} element the element that will be animated
* @param {object} options the animation-related options that will be applied during the animation
@@ -223,7 +202,7 @@
* `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.)
*
* @return {null|object} an object with a start method and details about the animation. If no animation is detected then a value of `null` will be returned.
* @return {object} an object with start and end methods and details about the animation.
*
* * `start` - The method to start the animation. This will return a `Promise` when called.
* * `end` - This method will cancel the animation and remove all applied CSS classes and styles.
@@ -472,7 +451,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
return stagger || {};
}
var bod = $document[0].body;
var bod = getDomNode($document).body;
var cancelLastRAFRequest;
var rafWaitQueue = [];
function waitUntilQuiet(callback) {
@@ -521,7 +500,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
function init(element, options) {
var node = element[0];
var node = getDomNode(element);
options = prepareAnimationOptions(options);
var temporaryStyles = [];
@@ -538,8 +517,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var maxDurationTime;
if (options.duration === 0 || (!$sniffer.animations && !$sniffer.transitions)) {
close();
return;
return closeAndReturnNoopAnimator();
}
var method = options.event && isArray(options.event)
@@ -586,8 +564,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
// there is no way we can trigger an animation since no styles and
// no classes are being applied which would then trigger a transition
if (!hasToStyles && !setupClasses) {
close();
return false;
return closeAndReturnNoopAnimator();
}
var cacheKey, stagger;
@@ -682,8 +659,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
if (maxDuration === 0 && !flags.recalculateTimingStyles) {
close();
return false;
return closeAndReturnNoopAnimator();
}
// we need to recalculate the delay value since we used a pre-emptive negative
@@ -711,6 +687,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
// TODO(matsko): for 1.5 change this code to have an animator object for better debugging
return {
$$willAnimate: true,
end: endFn,
start: function() {
if (animationClosed) return;
@@ -790,6 +767,23 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
}
}
function closeAndReturnNoopAnimator() {
runner = new $$AnimateRunner({
end: endFn,
cancel: cancelFn
});
close();
return {
$$willAnimate: false,
start: function() {
return runner;
},
end: endFn
};
}
function start() {
if (animationClosed) return;
+33 -16
View File
@@ -4,8 +4,7 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
$$animationProvider.drivers.push('$$animateCssDriver');
var NG_ANIMATE_SHIM_CLASS_NAME = 'ng-animate-shim';
var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-animate-anchor';
var NG_ANIMATE_ANCHOR_SUFFIX = '-anchor';
var NG_ANIMATE_ANCHOR_CLASS_NAME = 'ng-anchor';
var NG_OUT_ANCHOR_CLASS_NAME = 'ng-anchor-out';
var NG_IN_ANCHOR_CLASS_NAME = 'ng-anchor-in';
@@ -16,8 +15,8 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
// only browsers that support these properties can render animations
if (!$sniffer.animations && !$sniffer.transitions) return noop;
var bodyNode = $document[0].body;
var rootNode = $rootElement[0];
var bodyNode = getDomNode($document).body;
var rootNode = getDomNode($rootElement);
var rootBodyElement = jqLite(bodyNode.parentNode === rootNode ? bodyNode : rootNode);
@@ -44,15 +43,13 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
}
function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
var clone = jqLite(outAnchor[0].cloneNode(true));
var startingClasses = filterCssClasses(clone.attr('class') || '');
var anchorClasses = pendClasses(classes, NG_ANIMATE_ANCHOR_SUFFIX);
var clone = jqLite(getDomNode(outAnchor).cloneNode(true));
var startingClasses = filterCssClasses(getClassVal(clone));
outAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
inAnchor.addClass(NG_ANIMATE_SHIM_CLASS_NAME);
clone.addClass(NG_ANIMATE_ANCHOR_CLASS_NAME);
clone.addClass(anchorClasses);
rootBodyElement.append(clone);
@@ -113,7 +110,7 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
function calculateAnchorStyles(anchor) {
var styles = {};
var coords = anchor[0].getBoundingClientRect();
var coords = getDomNode(anchor).getBoundingClientRect();
// we iterate directly since safari messes up and doesn't return
// all the keys for the coods object when iterated
@@ -133,22 +130,36 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
}
function prepareOutAnimation() {
return $animateCss(clone, {
var animator = $animateCss(clone, {
addClass: NG_OUT_ANCHOR_CLASS_NAME,
delay: true,
from: calculateAnchorStyles(outAnchor)
});
// read the comment within `prepareRegularAnimation` to understand
// why this check is necessary
return animator.$$willAnimate ? animator : null;
}
function getClassVal(element) {
return element.attr('class') || '';
}
function prepareInAnimation() {
var endingClasses = filterCssClasses(inAnchor.attr('class'));
var classes = getUniqueValues(endingClasses, startingClasses);
return $animateCss(clone, {
var endingClasses = filterCssClasses(getClassVal(inAnchor));
var toAdd = getUniqueValues(endingClasses, startingClasses);
var toRemove = getUniqueValues(startingClasses, endingClasses);
var animator = $animateCss(clone, {
to: calculateAnchorStyles(inAnchor),
addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + classes,
removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + startingClasses,
addClass: NG_IN_ANCHOR_CLASS_NAME + ' ' + toAdd,
removeClass: NG_OUT_ANCHOR_CLASS_NAME + ' ' + toRemove,
delay: true
});
// read the comment within `prepareRegularAnimation` to understand
// why this check is necessary
return animator.$$willAnimate ? animator : null;
}
function end() {
@@ -229,7 +240,13 @@ var $$AnimateCssDriverProvider = ['$$animationProvider', function($$animationPro
options.onDone = animationDetails.domOperation;
}
return $animateCss(element, options);
var animator = $animateCss(element, options);
// the driver lookup code inside of $$animation attempts to spawn a
// driver one by one until a driver returns a.$$willAnimate animator object.
// $animateCss will always return an object, however, it will pass in
// a flag as a hint as to whether an animation was detected or not
return animator.$$willAnimate ? animator : null;
}
}];
}];
+13 -2
View File
@@ -145,9 +145,20 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
args.push(options);
var value = fn.apply(fn, args);
if (value) {
if (isFunction(value.start)) {
value = value.start();
}
// optional onEnd / onCancel callback
return isFunction(value) ? value : noop;
if (value instanceof $$AnimateRunner) {
value.done(onDone);
} else if (isFunction(value)) {
// optional onEnd / onCancel callback
return value;
}
}
return noop;
}
function groupEventedAnimations(element, event, options, animations, fnName) {
+31 -30
View File
@@ -117,7 +117,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
function findCallbacks(element, event) {
var targetNode = element[0];
var targetNode = getDomNode(element);
var matches = [];
var entries = callbackRegistry[event];
@@ -198,7 +198,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// (bool) - Global setter
bool = animationsEnabled = !!element;
} else {
var node = element.length ? element[0] : element;
var node = getDomNode(element);
var recordExists = disabledElementsLookup.get(node);
if (argCount === 1) {
@@ -221,11 +221,14 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
};
function queueAnimation(element, event, options) {
var node, parent;
element = stripCommentsFromElement(element);
var node = element[0];
if (element) {
node = getDomNode(element);
parent = element.parent();
}
options = prepareAnimationOptions(options);
var parent = element.parent();
// we create a fake runner with a working promise.
// These methods will become available after the digest has passed
@@ -235,7 +238,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
// a jqLite wrapper that contains only comment nodes... If this
// happens then there is no way we can perform an animation
if (!node) {
runner.end();
close();
return runner;
}
@@ -408,7 +411,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
close(!status);
var animationDetails = activeAnimationsLookup.get(node);
if (animationDetails && animationDetails.counter === counter) {
clearElementAnimationState(element);
clearElementAnimationState(getDomNode(element));
}
notifyProgress(runner, event, 'close', {});
});
@@ -435,7 +438,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
function closeChildAnimations(element) {
var node = element[0];
var node = getDomNode(element);
var children = node.querySelectorAll('[' + NG_ANIMATE_ATTR_NAME + ']');
forEach(children, function(child) {
var state = parseInt(child.getAttribute(NG_ANIMATE_ATTR_NAME));
@@ -454,19 +457,17 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
function clearElementAnimationState(element) {
element = element.length ? element[0] : element;
element.removeAttribute(NG_ANIMATE_ATTR_NAME);
activeAnimationsLookup.remove(element);
var node = getDomNode(element);
node.removeAttribute(NG_ANIMATE_ATTR_NAME);
activeAnimationsLookup.remove(node);
}
function isMatchingElement(a,b) {
a = a.length ? a[0] : a;
b = b.length ? b[0] : b;
return a === b;
function isMatchingElement(nodeOrElmA, nodeOrElmB) {
return getDomNode(nodeOrElmA) === getDomNode(nodeOrElmB);
}
function closeParentClassBasedAnimations(startingElement) {
var parentNode = startingElement[0];
var parentNode = getDomNode(startingElement);
do {
if (!parentNode || parentNode.nodeType !== ELEMENT_NODE) break;
@@ -492,7 +493,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
}
function areAnimationsAllowed(element, parent, event) {
function areAnimationsAllowed(element, parentElement, event) {
var bodyElementDetected = false;
var rootElementDetected = false;
var parentAnimationDetected = false;
@@ -500,17 +501,17 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
var parentHost = element.data(NG_ANIMATE_PIN_DATA);
if (parentHost) {
parent = parentHost;
parentElement = parentHost;
}
while (parent && parent.length) {
while (parentElement && parentElement.length) {
if (!rootElementDetected) {
// angular doesn't want to attempt to animate elements outside of the application
// therefore we need to ensure that the rootElement is an ancestor of the current element
rootElementDetected = isMatchingElement(parent, $rootElement);
rootElementDetected = isMatchingElement(parentElement, $rootElement);
}
var parentNode = parent[0];
var parentNode = parentElement[0];
if (parentNode.nodeType !== ELEMENT_NODE) {
// no point in inspecting the #document element
break;
@@ -525,7 +526,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
}
if (isUndefined(animateChildren) || animateChildren === true) {
var value = parent.data(NG_ANIMATE_CHILDREN_DATA);
var value = parentElement.data(NG_ANIMATE_CHILDREN_DATA);
if (isDefined(value)) {
animateChildren = value;
}
@@ -537,11 +538,11 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
if (!rootElementDetected) {
// angular doesn't want to attempt to animate elements outside of the application
// therefore we need to ensure that the rootElement is an ancestor of the current element
rootElementDetected = isMatchingElement(parent, $rootElement);
rootElementDetected = isMatchingElement(parentElement, $rootElement);
if (!rootElementDetected) {
parentHost = parent.data(NG_ANIMATE_PIN_DATA);
parentHost = parentElement.data(NG_ANIMATE_PIN_DATA);
if (parentHost) {
parent = parentHost;
parentElement = parentHost;
}
}
}
@@ -549,10 +550,10 @@ 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(parent, bodyElement);
bodyElementDetected = isMatchingElement(parentElement, bodyElement);
}
parent = parent.parent();
parentElement = parentElement.parent();
}
var allowAnimation = !parentAnimationDetected || animateChildren;
@@ -563,14 +564,14 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
details = details || {};
details.state = state;
element = element.length ? element[0] : element;
element.setAttribute(NG_ANIMATE_ATTR_NAME, state);
var node = getDomNode(element);
node.setAttribute(NG_ANIMATE_ATTR_NAME, state);
var oldValue = activeAnimationsLookup.get(element);
var oldValue = activeAnimationsLookup.get(node);
var newValue = oldValue
? extend(oldValue, details)
: details;
activeAnimationsLookup.put(element, newValue);
activeAnimationsLookup.put(node, newValue);
}
}];
}];
+16 -13
View File
@@ -1,7 +1,6 @@
'use strict';
var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
var NG_ANIMATE_CLASSNAME = 'ng-animate';
var NG_ANIMATE_REF_ATTR = 'ng-animate-ref';
var drivers = this.drivers = [];
@@ -62,7 +61,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
event: event,
structural: isStructural,
options: options,
start: start,
beforeStart: beforeStart,
close: close
});
@@ -88,15 +87,19 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
animationQueue.length = 0;
forEach(groupAnimations(animations), function(animationEntry) {
var startFn = animationEntry.start;
var closeFn = animationEntry.close;
// it's important that we apply the `ng-animate` CSS class and the
// temporary classes before we do any driver invoking since these
// CSS classes may be required for proper CSS detection.
animationEntry.beforeStart();
var operation = invokeFirstDriver(animationEntry);
var startAnimation = operation && operation.start; /// TODO(matsko): only recognize operation.start()
if (!startAnimation) {
var triggerAnimationStart = operation && operation.start; /// TODO(matsko): only recognize operation.start()
var closeFn = animationEntry.close;
if (!triggerAnimationStart) {
closeFn();
} else {
startFn();
var animationRunner = startAnimation();
var animationRunner = triggerAnimationStart();
animationRunner.done(function(status) {
closeFn(!status);
});
@@ -128,7 +131,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
var refLookup = {};
forEach(animations, function(animation, index) {
var element = animation.element;
var node = element[0];
var node = getDomNode(element);
var event = animation.event;
var enterOrMove = ['enter', 'move'].indexOf(event) >= 0;
var anchorNodes = animation.structural ? getAnchorNodes(node) : [];
@@ -173,9 +176,9 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
if (!anchorGroups[lookupKey]) {
var group = anchorGroups[lookupKey] = {
// TODO(matsko): double-check this code
start: function() {
fromAnimation.start();
toAnimation.start();
beforeStart: function() {
fromAnimation.beforeStart();
toAnimation.beforeStart();
},
close: function() {
fromAnimation.close();
@@ -241,7 +244,7 @@ var $$AnimationProvider = ['$animateProvider', function($animateProvider) {
}
}
function start() {
function beforeStart() {
element.addClass(NG_ANIMATE_CLASSNAME);
if (tempClasses) {
$$jqLite.addClass(element, tempClasses);
+90 -44
View File
@@ -224,6 +224,35 @@
*
* Stagger animations are currently only supported within CSS-defined animations.
*
* ### The `ng-animate` CSS class
*
* When ngAnimate is animating an element it will apply the `ng-animate` CSS class to the element for the duration of the animation.
* This is a temporary CSS class and it will be removed once the animation is over (for both JavaScript and CSS-based animations).
*
* Therefore, animations can be applied to an element using this temporary class directly via CSS.
*
* ```css
* .zipper.ng-animate {
* transition:0.5s linear all;
* }
* .zipper.ng-enter {
* opacity:0;
* }
* .zipper.ng-enter.ng-enter-active {
* opacity:1;
* }
* .zipper.ng-leave {
* opacity:1;
* }
* .zipper.ng-leave.ng-leave-active {
* opacity:0;
* }
* ```
*
* (Note that the `ng-animate` CSS class is reserved and it cannot be applied on an element directly since ngAnimate will always remove
* the CSS class once an animation has completed.)
*
*
* ## JavaScript-based Animations
*
* ngAnimate also allows for animations to be consumed by JavaScript code. The approach is similar to CSS-based animations (where there is a shared
@@ -334,17 +363,12 @@
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
* return {
* enter: function(element, doneFn) {
* var animation = $animateCss(element, {
* event: 'enter'
* });
*
* if (animation) {
* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
* var runner = animation.start();
* runner.done(doneFn);
* } else { //no CSS animation was detected
* doneFn();
* }
* // this will trigger `.slide.ng-enter` and `.slide.ng-enter-active`.
* var runner = $animateCss(element, {
* event: 'enter',
* structural: true
* }).start();
* runner.done(doneFn);
* }
* }
* }]
@@ -360,18 +384,14 @@
* myModule.animation('.slide', ['$animateCss', function($animateCss) {
* return {
* enter: function(element, doneFn) {
* var animation = $animateCss(element, {
* var runner = $animateCss(element, {
* event: 'enter',
* addClass: 'maroon-setting',
* from: { height:0 },
* to: { height: 200 }
* });
* }).start();
*
* if (animation) {
* animation.start().done(doneFn);
* } else {
* doneFn();
* }
* runner.done(doneFn);
* }
* }
* }]
@@ -399,10 +419,12 @@
* called `ng-animate-ref`.
*
* Let's say for example we have two views that are managed by `ng-view` and we want to show
* that there is a relationship between two components situated in different views. By using the
* that there is a relationship between two components situated in within these views. By using the
* `ng-animate-ref` attribute we can identify that the two components are paired together and we
* can then attach an animation, which is triggered when the view changes.
*
* Say for example we have the following template code:
*
* ```html
* <!-- index.html -->
* <div ng-view class="view-animation">
@@ -410,46 +432,70 @@
*
* <!-- home.html -->
* <a href="#/banner-page">
* <img src="./banner.jpg" ng-animate-ref="banner">
* <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
* </a>
*
* <!-- banner-page.html -->
* <img src="./banner.jpg" ng-animate-ref="banner">
* <img src="./banner.jpg" class="banner" ng-animate-ref="banner">
* ```
*
* Now, when the view changes (once the link is clicked), ngAnimate will examine the
* HTML contents to see if there is a match reference between any components in the view
* that is leaving and the view that is entering. It will then attempt to trigger a CSS
* animation on the `.view-animation-anchor` CSS class (notice how `.view-animation` is
* a shared CSS class on the ng-view element? This means that view-animation will apply to
* both the enter and leave animations).
* that is leaving and the view that is entering. It will scan both the view which is being
* removed (leave) and inserted (enter) to see if there are any paired DOM elements that
* contain a matching ref value.
*
* The two images match since they share the same ref value. ngAnimate will now apply a
* suffixed version of each of the shared CSS classes with `-anchor`. Therefore we will
* have a shared class of `view-animation-anchor` which we can use to setup our transition animation.
* The two images match since they share the same ref value. ngAnimate will now create a
* transport element (which is a clone of the first image element) and it will then attempt
* to animate to the position of the second image element in the next view. For the animation to
* work a special CSS class called `ng-anchor` will be added to the transported element.
*
* We can now attach a transition onto the `.view-animation-anchor` CSS class and then
* We can now attach a transition onto the `.banner.ng-anchor` CSS class and then
* ngAnimate will handle the entire transition for us as well as the addition and removal of
* any changes of CSS classes between the elements:
*
* ```css
* .view-animation-anchor {
* .banner.ng-anchor {
* /&#42; this animation will last for 1 second since there are
* two phases to the animation (an `in` and an `out` phase) &#42;/
* transition:0.5s linear all;
* }
* ```
*
* There are two stages for an anchor animation: `out` and `in`. The `out` stage happens first and that
* is when the element is animated away from its origin. Once that animation is over then the `in` stage
* occurs which animates the element to its destination. The reason why there are two animations is to
* give enough time for the enter animation on the new element to be ready.
* We also **must** include animations for the views that are being entered and removed
* (otherwise anchoring wouldn't be possible since the new view would be inserted right away).
*
* ```css
* .view-animation.ng-enter, .view-animation.ng-leave {
* transition:0.5s linear all;
* position:fixed;
* left:0;
* top:0;
* width:100%;
* }
* .view-animation.ng-enter {
* transform:translateX(100%);
* }
* .view-animation.ng-leave,
* .view-animation.ng-enter.ng-enter-active {
* transform:translateX(0%);
* }
* .view-animation.ng-leave.ng-leave-active {
* transform:translateX(-100%);
* }
* ```
*
* Now we can jump back to the anchor animation. When the animation happens, there are two stages that occur:
* an `out` and an `in` stage. The `out` stage happens first and that is when the element is animated away
* from its origin. Once that animation is over then the `in` stage occurs which animates the
* element to its destination. The reason why there are two animations is to give enough time
* for the enter animation on the new element to be ready.
*
* The example above sets up a transition for both the in and out phases, but we can also target the out or
* in phases directly via `ng-anchor-out` and `ng-anchor-in`.
*
* ```css
* .view-animation-anchor.ng-anchor-out {
* .banner.ng-anchor-out {
* transition: 0.5s linear all;
*
* /&#42; the scale will be applied during the out animation,
@@ -457,7 +503,7 @@
* transform: scale(1.2);
* }
*
* .view-animation-anchor.ng-anchor-in {
* .banner.ng-anchor-in {
* transition: 1s linear all;
* }
* ```
@@ -551,21 +597,21 @@
width:100%;
min-height:500px;
}
.view.ng-enter {
.view.ng-enter, .view.ng-leave,
.record.ng-anchor {
transition:0.5s linear all;
}
.view.ng-enter {
transform:translateX(100%);
}
.view.ng-enter.ng-enter-active {
.view.ng-enter.ng-enter-active, .view.ng-leave {
transform:translateX(0%);
}
.view.ng-leave {
transition:0.5s linear all;
}
.view.ng-leave.ng-leave-active {
transform:translateX(-100%);
}
.view-anchor {
transition:0.5s linear all;
.record.ng-anchor-out {
background:red;
}
</file>
</example>
@@ -601,7 +647,7 @@
* imagine we have a greeting box that shows and hides itself when the data changes
*
* ```html
* <greeing-box active="onOrOff">Hi there</greeting-box>
* <greeting-box active="onOrOff">Hi there</greeting-box>
* ```
*
* ```js
+26 -10
View File
@@ -16,6 +16,7 @@ var isElement = angular.isElement;
var ELEMENT_NODE = 1;
var COMMENT_NODE = 8;
var NG_ANIMATE_CLASSNAME = 'ng-animate';
var NG_ANIMATE_CHILDREN_DATA = '$$ngAnimateChildren';
var isPromiseLike = function(p) {
@@ -72,19 +73,30 @@ function removeFromArray(arr, val) {
}
function stripCommentsFromElement(element) {
if (element instanceof jqLite) {
switch (element.length) {
case 0:
return [];
break;
case 1:
// there is no point of stripping anything if the element
// is the only element within the jqLite wrapper.
// (it's important that we retain the element instance.)
if (element[0].nodeType === ELEMENT_NODE) {
return element;
}
break;
default:
return jqLite(extractElementNode(element));
break;
}
}
if (element.nodeType === ELEMENT_NODE) {
return jqLite(element);
}
if (element.length === 0) return [];
// there is no point of stripping anything if the element
// is the only element within the jqLite wrapper.
// (it's important that we retain the element instance.)
if (element.length === 1) {
return element[0].nodeType === ELEMENT_NODE && element;
} else {
return jqLite(extractElementNode(element));
}
}
function extractElementNode(element) {
@@ -234,3 +246,7 @@ function resolveElementClasses(existing, toAdd, toRemove) {
return classes;
}
function getDomNode(element) {
return (element instanceof angular.element) ? element[0] : element;
}
+2 -2
View File
@@ -581,8 +581,8 @@ angular.module('ngResource', ['ng']).
if (angular.isArray(data) !== (!!action.isArray)) {
throw $resourceMinErr('badcfg',
'Error in resource configuration for action `{0}`. Expected response to ' +
'contain an {1} but got an {2}', name, action.isArray ? 'array' : 'object',
angular.isArray(data) ? 'array' : 'object');
'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object',
angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url);
}
// jshint +W018
if (action.isArray) {
+7 -9
View File
@@ -1,6 +1,8 @@
'use strict';
/* global ngTouch: false */
/* global ngTouch: false,
nodeName_: false
*/
/**
* @ngdoc directive
@@ -66,7 +68,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
// double-tapping, and then fire a click event.
//
// This delay sucks and makes mobile apps feel unresponsive.
// So we detect touchstart, touchmove, touchcancel and touchend ourselves and determine when
// So we detect touchstart, touchcancel and touchend ourselves and determine when
// the user has tapped on something.
//
// What happens when the browser then generates a click event?
@@ -78,7 +80,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
// So the sequence for a tap is:
// - global touchstart: Sets an "allowable region" at the point touched.
// - element's touchstart: Starts a touch
// (- touchmove or touchcancel ends the touch, no click follows)
// (- touchcancel ends the touch, no click follows)
// - element's touchend: Determines if the tap is valid (didn't move too far away, didn't hold
// too long) and fires the user's tap handler. The touchend also calls preventGhostClick().
// - preventGhostClick() removes the allowable region the global touchstart created.
@@ -142,7 +144,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
lastLabelClickCoordinates = null;
}
// remember label click coordinates to prevent click busting of trigger click event on input
if (event.target.tagName.toLowerCase() === 'label') {
if (nodeName_(event.target) === 'label') {
lastLabelClickCoordinates = [x, y];
}
@@ -158,7 +160,7 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
event.preventDefault();
// Blur focused form elements
event.target && event.target.blur();
event.target && event.target.blur && event.target.blur();
}
@@ -229,10 +231,6 @@ ngTouch.directive('ngClick', ['$parse', '$timeout', '$rootElement',
touchStartY = e.clientY;
});
element.on('touchmove', function(event) {
resetState();
});
element.on('touchcancel', function(event) {
resetState();
});
+3
View File
@@ -22,3 +22,6 @@
/* global -ngTouch */
var ngTouch = angular.module('ngTouch', []);
function nodeName_(element) {
return angular.lowercase(element.nodeName || (element[0] && element[0].nodeName));
}
+83
View File
@@ -370,6 +370,21 @@ describe('angular', function() {
expect(copy(undefined, [1,2,3])).toEqual([]);
expect(copy({0: 1, 1: 2}, [1,2,3])).toEqual([1,2]);
});
it('should copy objects with no prototype parent', function() {
var obj = extend(Object.create(null), {
a: 1,
b: 2,
c: 3
});
var dest = copy(obj);
expect(Object.getPrototypeOf(dest)).toBe(null);
expect(dest.a).toBe(1);
expect(dest.b).toBe(2);
expect(dest.c).toBe(3);
expect(Object.keys(dest)).toEqual(['a', 'b', 'c']);
});
});
describe("extend", function() {
@@ -651,6 +666,38 @@ describe('angular', function() {
it('should return false when comparing an object and a Date', function() {
expect(equals({}, new Date())).toBe(false);
});
it('should safely compare objects with no prototype parent', function() {
var o1 = extend(Object.create(null), {
a: 1, b: 2, c: 3
});
var o2 = extend(Object.create(null), {
a: 1, b: 2, c: 3
});
expect(equals(o1, o2)).toBe(true);
o2.c = 2;
expect(equals(o1, o2)).toBe(false);
});
it('should safely compare objects which shadow Object.prototype.hasOwnProperty', function() {
/* jshint -W001 */
var o1 = {
hasOwnProperty: true,
a: 1,
b: 2,
c: 3
};
var o2 = {
hasOwnProperty: true,
a: 1,
b: 2,
c: 3
};
expect(equals(o1, o2)).toBe(true);
o1.hasOwnProperty = function() {};
expect(equals(o1, o2)).toBe(false);
});
});
@@ -980,6 +1027,42 @@ describe('angular', function() {
});
it('should safely iterate through objects with no prototype parent', function() {
var obj = extend(Object.create(null), {
a: 1, b: 2, c: 3
});
var log = [];
var self = {};
forEach(obj, function(val, key, collection) {
expect(this).toBe(self);
expect(collection).toBe(obj);
log.push(key + '=' + val);
}, self);
expect(log.length).toBe(3);
expect(log).toEqual(['a=1', 'b=2', 'c=3']);
});
it('should safely iterate through objects which shadow Object.prototype.hasOwnProperty', function() {
/* jshint -W001 */
var obj = {
hasOwnProperty: true,
a: 1,
b: 2,
c: 3
};
var log = [];
var self = {};
forEach(obj, function(val, key, collection) {
expect(this).toBe(self);
expect(collection).toBe(obj);
log.push(key + '=' + val);
}, self);
expect(log.length).toBe(4);
expect(log).toEqual(['hasOwnProperty=true', 'a=1', 'b=2', 'c=3']);
});
describe('ES spec api compliance', function() {
function testForEachSpec(expectedSize, collection) {
+15
View File
@@ -211,6 +211,21 @@ describe('$compile', function() {
});
inject(function($compile) {});
});
it('should throw an exception if a directive name has leading or trailing whitespace', function() {
module(function() {
function assertLeadingOrTrailingWhitespaceInDirectiveName(name) {
expect(function() {
directive(name, function() { });
}).toThrowMinErr(
'$compile','baddir', 'Directive name \'' + name + '\' is invalid. ' +
"The name should not contain leading or trailing whitespaces");
}
assertLeadingOrTrailingWhitespaceInDirectiveName(' leadingWhitespaceDirectiveName');
assertLeadingOrTrailingWhitespaceInDirectiveName('trailingWhitespaceDirectiveName ');
assertLeadingOrTrailingWhitespaceInDirectiveName(' leadingAndTrailingWhitespaceDirectiveName ');
});
inject(function($compile) {});
});
});
+26
View File
@@ -33,6 +33,32 @@ describe('ngClass', function() {
}));
it('should add new and remove old classes with same names as Object.prototype properties dynamically', inject(function($rootScope, $compile) {
/* jshint -W001 */
element = $compile('<div class="existing" ng-class="dynClass"></div>')($rootScope);
$rootScope.dynClass = { watch: true, hasOwnProperty: true, isPrototypeOf: true };
$rootScope.$digest();
expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('watch')).toBe(true);
expect(element.hasClass('hasOwnProperty')).toBe(true);
expect(element.hasClass('isPrototypeOf')).toBe(true);
$rootScope.dynClass.watch = false;
$rootScope.$digest();
expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('watch')).toBe(false);
expect(element.hasClass('hasOwnProperty')).toBe(true);
expect(element.hasClass('isPrototypeOf')).toBe(true);
delete $rootScope.dynClass;
$rootScope.$digest();
expect(element.hasClass('existing')).toBe(true);
expect(element.hasClass('watch')).toBe(false);
expect(element.hasClass('hasOwnProperty')).toBe(false);
expect(element.hasClass('isPrototypeOf')).toBe(false);
}));
it('should support adding multiple classes via an array', inject(function($rootScope, $compile) {
element = $compile('<div class="existing" ng-class="[\'A\', \'B\']"></div>')($rootScope);
$rootScope.$digest();
+152 -6
View File
@@ -180,6 +180,47 @@ describe('ngOptions', function() {
});
it('should not include properties with non-numeric keys in array-like collections when using array syntax', function() {
createSelect({
'ng-model':'selected',
'ng-options':'value for value in values'
});
scope.$apply(function() {
scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
scope.selected = scope.values[1];
});
var options = element.find('option');
expect(options.length).toEqual(3);
expect(options.eq(0)).toEqualOption('X');
expect(options.eq(1)).toEqualOption('Y');
expect(options.eq(2)).toEqualOption('Z');
});
it('should include properties with non-numeric keys in array-like collections when using object syntax', function() {
createSelect({
'ng-model':'selected',
'ng-options':'value for (key, value) in values'
});
scope.$apply(function() {
scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
scope.selected = scope.values[1];
});
var options = element.find('option');
expect(options.length).toEqual(5);
expect(options.eq(0)).toEqualOption('X');
expect(options.eq(1)).toEqualOption('Y');
expect(options.eq(2)).toEqualOption('Z');
expect(options.eq(3)).toEqualOption('A');
expect(options.eq(4)).toEqualOption(3);
});
it('should render an object', function() {
createSelect({
'ng-model': 'selected',
@@ -480,6 +521,30 @@ describe('ngOptions', function() {
});
it('should update the label if only the property has changed', function() {
// ng-options="value.name for value in values"
// ng-model="selected"
createSingleSelect();
scope.$apply(function() {
scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
scope.selected = scope.values[0];
});
var options = element.find('option');
expect(options.eq(0).prop('label')).toEqual('A');
expect(options.eq(1).prop('label')).toEqual('B');
expect(options.eq(2).prop('label')).toEqual('C');
scope.$apply('values[0].name = "X"');
options = element.find('option');
expect(options.eq(0).prop('label')).toEqual('X');
});
// bug fix #9714
it('should select the matching option when the options are updated', function() {
@@ -776,6 +841,54 @@ describe('ngOptions', function() {
});
it('should re-render if an item in an array source is added/removed', function() {
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item.id as item.label for item in arr'
});
scope.$apply(function() {
scope.selected = [10];
});
expect(element).toEqualSelectValue([10], true);
scope.$apply(function() {
scope.selected.push(20);
});
expect(element).toEqualSelectValue([10, 20], true);
scope.$apply(function() {
scope.selected.shift();
});
expect(element).toEqualSelectValue([20], true);
});
it('should handle a options containing circular references', function() {
scope.arr[0].ref = scope.arr[0];
scope.selected = [scope.arr[0]];
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item as item.label for item in arr'
});
expect(element).toEqualSelectValue([scope.arr[0]], true);
scope.$apply(function() {
scope.selected.push(scope.arr[1]);
});
expect(element).toEqualSelectValue([scope.arr[0], scope.arr[1]], true);
scope.$apply(function() {
scope.selected.pop();
});
expect(element).toEqualSelectValue([scope.arr[0]], true);
});
it('should support single select with object source', function() {
createSelect({
'ng-model': 'selected',
@@ -899,10 +1012,9 @@ describe('ngOptions', function() {
expect(element.val()).toEqual(['10']);
// Update the properties on the object in the selected array, rather than replacing the whole object
// Update the tracked property on the object in the selected array, rather than replacing the whole object
scope.$apply(function() {
scope.selected[0].id = 20;
scope.selected[0].label = 'new twenty';
});
// The value of the select should change since the id property changed
@@ -1042,7 +1154,7 @@ describe('ngOptions', function() {
}).not.toThrow();
});
it('should setup equality watches on ngModel changes if using trackBy', function() {
it('should re-render if the tracked property of the model is changed when using trackBy', function() {
createSelect({
'ng-model': 'selected',
@@ -1050,13 +1162,13 @@ describe('ngOptions', function() {
});
scope.$apply(function() {
scope.selected = scope.arr[0];
scope.selected = {id: 10, label: 'ten'};
});
spyOn(element.controller('ngModel'), '$render');
scope.$apply(function() {
scope.selected.label = 'changed';
scope.arr[0].id = 20;
});
// update render due to equality watch
@@ -1064,7 +1176,7 @@ describe('ngOptions', function() {
});
it('should not setup equality watches on ngModel changes if not using trackBy', function() {
it('should not re-render if a property of the model is changed when not using trackBy', function() {
createSelect({
'ng-model': 'selected',
@@ -1085,6 +1197,40 @@ describe('ngOptions', function() {
expect(element.controller('ngModel').$render).not.toHaveBeenCalled();
});
it('should handle options containing circular references (single)', function() {
scope.arr[0].ref = scope.arr[0];
createSelect({
'ng-model': 'selected',
'ng-options': 'item for item in arr track by item.id'
});
expect(function() {
scope.$apply(function() {
scope.selected = scope.arr[0];
});
}).not.toThrow();
});
it('should handle options containing circular references (multiple)', function() {
scope.arr[0].ref = scope.arr[0];
createSelect({
'ng-model': 'selected',
'multiple': true,
'ng-options': 'item for item in arr track by item.id'
});
expect(function() {
scope.$apply(function() {
scope.selected = [scope.arr[0]];
});
scope.$apply(function() {
scope.selected.push(scope.arr[1]);
});
}).not.toThrow();
});
});
+1 -1
View File
@@ -1084,7 +1084,7 @@ describe('ngRepeat', function() {
beforeEach(function() {
element = $compile(
'<ul>' +
'<li ng-repeat="item in items">{{key}}:{{val}}|></li>' +
'<li ng-repeat="item in items">{{item}}</li>' +
'</ul>')(scope);
a = {};
b = {};
+9 -1
View File
@@ -1992,6 +1992,8 @@ describe('$http param serializers', function() {
it('should serialize objects', function() {
expect(defSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov');
expect(jqrSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov');
expect(defSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
expect(jqrSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
});
});
@@ -2010,10 +2012,16 @@ describe('$http param serializers', function() {
expect(decodeURIComponent(jqrSer({a: 'b', foo: ['bar', 'baz']}))).toEqual('a=b&foo[]=bar&foo[]=baz');
});
it('should serialize objects by repeating param name with [kay] suffix', function() {
it('should serialize objects by repeating param name with [key] suffix', function() {
expect(jqrSer({a: 'b', foo: {'bar': 'barv', 'baz': 'bazv'}})).toEqual('a=b&foo%5Bbar%5D=barv&foo%5Bbaz%5D=bazv');
//a=b&foo[bar]=barv&foo[baz]=bazv
});
it('should serialize nested objects by repeating param name with [key] suffix', function() {
expect(jqrSer({a: ['b', {c: 'd'}], e: {f: 'g', 'h': ['i', 'j']}})).toEqual(
'a%5B%5D=b&a%5B%5D%5Bc%5D=d&e%5Bf%5D=g&e%5Bh%5D%5B%5D=i&e%5Bh%5D%5B%5D=j');
//a[]=b&a[][c]=d&e[f]=g&e[h][]=i&e[h][]=j
});
});
});
+40
View File
@@ -31,6 +31,46 @@ describe('$$rAF', function() {
expect(present).toBe(true);
}));
it('should only consume only one RAF if multiple async functions are registered before the first frame kicks in', inject(function($$rAF) {
if (!$$rAF.supported) return;
//we need to create our own injector to work around the ngMock overrides
var rafLog = [];
var injector = createInjector(['ng', function($provide) {
$provide.value('$window', {
location: window.location,
history: window.history,
webkitRequestAnimationFrame: function(fn) {
rafLog.push(fn);
}
});
}]);
$$rAF = injector.get('$$rAF');
var log = [];
function logFn() {
log.push(log.length);
}
$$rAF(logFn);
$$rAF(logFn);
$$rAF(logFn);
expect(log).toEqual([]);
expect(rafLog.length).toBe(1);
rafLog[0]();
expect(log).toEqual([0,1,2]);
expect(rafLog.length).toBe(1);
$$rAF(logFn);
expect(log).toEqual([0,1,2]);
expect(rafLog.length).toBe(2);
}));
describe('$timeout fallback', function() {
it("it should use a $timeout incase native rAF isn't suppored", function() {
var timeoutSpy = jasmine.createSpy('callback');
+24 -14
View File
@@ -58,6 +58,7 @@ describe("ngAnimate $$animateCssDriver", function() {
});
return {
$$willAnimate: true,
start: function() {
return runner;
}
@@ -124,7 +125,9 @@ describe("ngAnimate $$animateCssDriver", function() {
it("should not return anything if no animation is detected", function() {
module(function($provide) {
$provide.value('$animateCss', noop);
$provide.value('$animateCss', function() {
return { $$willAnimate: false };
});
});
inject(function() {
var runner = driver({
@@ -151,6 +154,7 @@ describe("ngAnimate $$animateCssDriver", function() {
$provide.factory('$animateCss', function($q, $$AnimateRunner) {
return function() {
return {
$$willAnimate: true,
start: function() {
return new $$AnimateRunner({
end: function() {
@@ -190,6 +194,7 @@ describe("ngAnimate $$animateCssDriver", function() {
var type = options.event || 'anchor';
closeLog[type] = closeLog[type] || [];
return {
$$willAnimate: true,
start: function() {
return new $$AnimateRunner({
end: function() {
@@ -252,6 +257,7 @@ describe("ngAnimate $$animateCssDriver", function() {
$provide.factory('$animateCss', function($$AnimateRunner) {
return function(element, details) {
return {
$$willAnimate: true,
start: function() {
animationLog.push([element, details.event]);
return new $$AnimateRunner();
@@ -429,14 +435,13 @@ describe("ngAnimate $$animateCssDriver", function() {
$provide.factory('$animateCss', function($$AnimateRunner) {
return function(element, options) {
var addClass = (options.addClass || '').trim();
if (addClass === expectedClass) {
return {
start: function() {
animationStarted = addClass;
return runner = new $$AnimateRunner();
}
};
}
return {
$$willAnimate: addClass === expectedClass,
start: function() {
animationStarted = addClass;
return runner = new $$AnimateRunner();
}
};
};
});
});
@@ -599,7 +604,7 @@ describe("ngAnimate $$animateCssDriver", function() {
expect(anchorDetails.event).toBeFalsy();
}));
it("should add the `ng-animate-anchor` class to the cloned anchor element",
it("should add the `ng-anchor` class to the cloned anchor element",
inject(function($rootElement, $$rAF) {
var fromAnchor = jqLite('<div></div>');
@@ -620,7 +625,7 @@ describe("ngAnimate $$animateCssDriver", function() {
}).start();
var clonedAnchor = captureLog.pop().element;
expect(clonedAnchor).toHaveClass('ng-animate-anchor');
expect(clonedAnchor).toHaveClass('ng-anchor');
}));
it("should add and remove the `ng-animate-shim` class on the in anchor element during the animation",
@@ -802,9 +807,9 @@ describe("ngAnimate $$animateCssDriver", function() {
captureLog.pop().runner.end();
$$rAF.flush();
var outAnimation = captureLog.pop();
var clonedAnchor = outAnimation.element;
var details = outAnimation.args[1];
var inAnimation = captureLog.pop();
var clonedAnchor = inAnimation.element;
var details = inAnimation.args[1];
var addedClasses = details.addClass.split(' ');
var removedClasses = details.removeClass.split(' ');
@@ -818,6 +823,11 @@ describe("ngAnimate $$animateCssDriver", function() {
expect(removedClasses).not.toContain('brown');
expect(removedClasses).not.toContain('black');
expect(removedClasses).not.toContain('red');
expect(removedClasses).not.toContain('blue');
inAnimation.runner.end();
expect(clonedAnchor).toHaveClass('red');
expect(clonedAnchor).toHaveClass('blue');
}));
+104 -9
View File
@@ -45,7 +45,7 @@ describe("ngAnimate $animateCss", function() {
duration: 10,
to: { 'background': 'red' }
});
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
}));
describe('when active', function() {
@@ -316,6 +316,96 @@ describe("ngAnimate $animateCss", function() {
options = { event: 'enter', structural: true };
}));
it("should always return an object even if no animation is detected",
inject(function($animateCss) {
ss.addRule('.some-animation', 'background:red;');
element.addClass('some-animation');
var animator = $animateCss(element, options);
expect(animator).toBeTruthy();
expect(isFunction(animator.start)).toBeTruthy();
expect(animator.end).toBeTruthy();
expect(animator.$$willAnimate).toBe(false);
}));
it("should close the animation immediately, but still return an animator object if no animation is detected",
inject(function($animateCss) {
ss.addRule('.another-fake-animation', 'background:blue;');
element.addClass('another-fake-animation');
var animator = $animateCss(element, {
event: 'enter',
structural: true
});
expect(element).not.toHaveClass('ng-enter');
expect(isFunction(animator.start)).toBeTruthy();
}));
they("should close the animation, but still accept $prop callbacks if no animation is detected",
['done', 'then'], function(method) {
inject(function($animateCss, $$rAF, $rootScope) {
ss.addRule('.the-third-fake-animation', 'background:green;');
element.addClass('another-fake-animation');
var animator = $animateCss(element, {
event: 'enter',
structural: true
});
var done = false;
animator.start()[method](function() {
done = true;
});
expect(done).toBe(false);
$$rAF.flush();
if (method === 'then') {
$rootScope.$digest();
}
expect(done).toBe(true);
});
});
they("should close the animation, but still accept recognize runner.$prop if no animation is detected",
['done(cancel)', 'catch'], function(method) {
inject(function($animateCss, $$rAF, $rootScope) {
ss.addRule('.the-third-fake-animation', 'background:green;');
element.addClass('another-fake-animation');
var animator = $animateCss(element, {
event: 'enter',
structural: true
});
var cancelled = false;
var runner = animator.start();
if (method === 'catch') {
runner.catch(function() {
cancelled = true;
});
} else {
runner.done(function(status) {
cancelled = status === false;
});
}
expect(cancelled).toBe(false);
runner.cancel();
if (method === 'catch') {
$rootScope.$digest();
}
expect(cancelled).toBe(true);
});
});
it("should use the highest transition duration value detected in the CSS class", inject(function($animateCss) {
ss.addRule('.ng-enter', 'transition:1s linear all;' +
'transition-duration:10s, 15s, 20s;');
@@ -1602,8 +1692,10 @@ describe("ngAnimate $animateCss", function() {
event: 'enter',
structural: true
};
var animator = $animateCss(element, options);
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
}));
it("should apply a transition and keyframe duration directly if both transitions and keyframe classes are detected",
@@ -1667,7 +1759,7 @@ describe("ngAnimate $animateCss", function() {
};
var animator = $animateCss(element, options);
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
}));
it("should override the delay value present in the CSS class",
@@ -2075,7 +2167,7 @@ describe("ngAnimate $animateCss", function() {
expect(element.css('width')).toBe('25px');
}));
it("should apply the union of from and to styles to the element if no animation is run",
it("should apply the union of from and to styles to the element if no animation will be run",
inject(function($animateCss, $rootElement) {
var options = {
@@ -2087,7 +2179,9 @@ describe("ngAnimate $animateCss", function() {
var animator = $animateCss(element, options);
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
animator.start();
expect(element.css('width')).toBe('15px');
expect(element.css('height')).toBe('50px');
}));
@@ -2240,7 +2334,7 @@ describe("ngAnimate $animateCss", function() {
};
var animator = $animateCss(element, options);
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
}));
it("should apply a transition if [from] styles are provided with a class that is added",
@@ -2265,7 +2359,7 @@ describe("ngAnimate $animateCss", function() {
};
var animator = $animateCss(element, options);
expect(animator).toBeTruthy();
expect(animator.$$willAnimate).toBeTruthy();
}));
it("should not apply an inline transition if no styles are provided",
@@ -2279,7 +2373,7 @@ describe("ngAnimate $animateCss", function() {
};
var animator = $animateCss(element, options);
expect(animator).toBeFalsy();
expect(animator.$$willAnimate).toBeFalsy();
}));
it("should apply a transition duration if the existing transition duration's property value is not 'all'",
@@ -2437,10 +2531,11 @@ describe("ngAnimate $animateCss", function() {
'</svg>');
var child = element.find('rect');
$animateCss(child, {
var animator = $animateCss(child, {
removeClass: 'class-of-doom',
duration: 0
});
animator.start();
var className = child[0].getAttribute('class');
expect(className).toBe('');
+60
View File
@@ -544,6 +544,66 @@ describe("ngAnimate $$animateJs", function() {
});
});
they("$prop should asynchronously render the $prop animation when a start/end animator object is returned",
allEvents, function(event) {
inject(function($$rAF, $$AnimateRunner) {
var runner;
animations[event] = function(element, a, b, c) {
return {
start: function() {
log.push('start ' + event);
return runner = new $$AnimateRunner();
}
};
};
runAnimation(event, function() {
log.push('complete');
});
if (event === 'leave') {
expect(log).toEqual(['start leave']);
runner.end();
$$rAF.flush();
expect(log).toEqual(['start leave', 'dom leave', 'complete']);
} else {
expect(log).toEqual(['dom ' + event, 'start ' + event]);
runner.end();
$$rAF.flush();
expect(log).toEqual(['dom ' + event, 'start ' + event, 'complete']);
}
});
});
they("$prop should asynchronously render the $prop animation when an instance of $$AnimateRunner is returned",
allEvents, function(event) {
inject(function($$rAF, $$AnimateRunner) {
var runner;
animations[event] = function(element, a, b, c) {
log.push('start ' + event);
return runner = new $$AnimateRunner();
};
runAnimation(event, function() {
log.push('complete');
});
if (event === 'leave') {
expect(log).toEqual(['start leave']);
runner.end();
$$rAF.flush();
expect(log).toEqual(['start leave', 'dom leave', 'complete']);
} else {
expect(log).toEqual(['dom ' + event, 'start ' + event]);
runner.end();
$$rAF.flush();
expect(log).toEqual(['dom ' + event, 'start ' + event, 'complete']);
}
});
});
they("$prop should asynchronously reject the before animation if the callback function is called with false", otherEvents, function(event) {
inject(function($$rAF, $rootScope) {
var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+88
View File
@@ -148,6 +148,27 @@ describe("animations", function() {
});
});
it('should throw a minErr if a regex value is used which partially contains or fully matches the `ng-animate` CSS class', function() {
module(function($animateProvider) {
assertError(/ng-animate/, true);
assertError(/first ng-animate last/, true);
assertError(/ng-animate-special/, false);
assertError(/first ng-animate-special last/, false);
assertError(/first ng-animate ng-animate-special last/, true);
function assertError(regex, bool) {
var expectation = expect(function() {
$animateProvider.classNameFilter(regex);
});
var message = '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "ng-animate" CSS class.';
bool ? expectation.toThrowMinErr('$animate', 'nongcls', message)
: expectation.not.toThrowMinErr('$animate', 'nongcls', message);
}
});
});
it('should complete the leave DOM operation in case the classNameFilter fails', function() {
module(function($animateProvider) {
$animateProvider.classNameFilter(/memorable-animation/);
@@ -245,6 +266,47 @@ describe("animations", function() {
expect(capturedAnimation).toBeFalsy();
}));
it('should not attempt to perform an animation on a text node element',
inject(function($rootScope, $animate) {
element.html('hello there');
var textNode = jqLite(element[0].firstChild);
$animate.addClass(textNode, 'some-class');
$rootScope.$digest();
expect(capturedAnimation).toBeFalsy();
}));
it('should perform the leave domOperation if a text node is used',
inject(function($rootScope, $animate) {
element.html('hello there');
var textNode = jqLite(element[0].firstChild);
var parentNode = textNode[0].parentNode;
$animate.leave(textNode);
$rootScope.$digest();
expect(capturedAnimation).toBeFalsy();
expect(textNode[0].parentNode).not.toBe(parentNode);
}));
it('should perform the leave domOperation if a comment node is used',
inject(function($rootScope, $animate, $document) {
var doc = $document[0];
element.html('hello there');
var commentNode = jqLite(doc.createComment('test comment'));
var parentNode = element[0];
parentNode.appendChild(commentNode[0]);
$animate.leave(commentNode);
$rootScope.$digest();
expect(capturedAnimation).toBeFalsy();
expect(commentNode[0].parentNode).not.toBe(parentNode);
}));
it('enter() should issue an enter animation and fire the DOM operation right away before the animation kicks off', inject(function($animate, $rootScope) {
expect(parent.children().length).toBe(0);
@@ -530,6 +592,32 @@ describe("animations", function() {
expect(itsOver).toBe(true);
}));
it('should immediately end a parent class-based form animation if a structural child is active',
inject(function($rootScope, $animate, $rootElement, $$rAF, $$AnimateRunner) {
parent.remove();
element.remove();
parent = jqLite('<form></form>');
$rootElement.append(parent);
element = jqLite('<input type="text" name="myInput" />');
$animate.addClass(parent, 'abc');
$rootScope.$digest();
// we do this since the old runner was already closed
overriddenAnimationRunner = new $$AnimateRunner();
$animate.enter(element, parent);
$rootScope.$digest();
$$rAF.flush();
expect(parent.attr('data-ng-animate')).toBeFalsy();
expect(element.attr('data-ng-animate')).toBeTruthy();
}));
it('should not end a pre-digest parent animation if it does not have any classes to add/remove',
inject(function($rootScope, $animate, $$rAF) {
+23
View File
@@ -734,6 +734,29 @@ describe('$$animation', function() {
expect(element).not.toHaveClass('ng-animate');
}));
it('should apply the `ng-animate` and temporary CSS classes before the driver is invoked', function() {
var capturedElementClasses;
module(function($provide) {
$provide.factory('mockedTestDriver', function() {
return function(details) {
capturedElementClasses = details.element.attr('class');
};
});
});
inject(function($$animation, $rootScope) {
$$animation(element, 'enter', {
tempClasses: 'temp-class-name'
});
$rootScope.$digest();
expect(capturedElementClasses).toMatch(/\bng-animate\b/);
expect(capturedElementClasses).toMatch(/\btemp-class-name\b/);
});
});
it('should perform the DOM operation at the end of the animation if the driver doesn\'t run it already',
inject(function($$animation, $rootScope) {
+2 -2
View File
@@ -1327,7 +1327,7 @@ describe('resource', function() {
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalled();
expect(failureSpy.mostRecentCall.args[0]).toMatch(
/^\[\$resource:badcfg\] Error in resource configuration for action `query`\. Expected response to contain an array but got an object/
/^\[\$resource:badcfg\] Error in resource configuration for action `query`\. Expected response to contain an array but got an object \(Request: GET \/Customer\/123\)/
);
});
@@ -1344,7 +1344,7 @@ describe('resource', function() {
expect(successSpy).not.toHaveBeenCalled();
expect(failureSpy).toHaveBeenCalled();
expect(failureSpy.mostRecentCall.args[0]).toMatch(
/^\[\$resource:badcfg\] Error in resource configuration for action `get`\. Expected response to contain an object but got an array/
/^\[\$resource:badcfg\] Error in resource configuration for action `get`\. Expected response to contain an object but got an array \(Request: GET \/Customer\/123\)/
);
});
+16 -4
View File
@@ -125,7 +125,7 @@ describe('ngClick (touch)', function() {
}));
it('should not click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) {
it('should not prevent click if a touchmove comes before touchend', inject(function($rootScope, $compile, $rootElement) {
element = $compile('<div ng-click="tapped = true"></div>')($rootScope);
$rootElement.append(element);
$rootScope.$digest();
@@ -140,11 +140,11 @@ describe('ngClick (touch)', function() {
browserTrigger(element, 'touchmove');
browserTrigger(element, 'touchend',{
keys: [],
x: 400,
y: 400
x: 15,
y: 15
});
expect($rootScope.tapped).toBeUndefined();
expect($rootScope.tapped).toEqual(true);
}));
it('should add the CSS class while the element is held down, and then remove it', inject(function($rootScope, $compile, $rootElement) {
@@ -171,6 +171,18 @@ describe('ngClick (touch)', function() {
expect($rootScope.tapped).toBe(true);
}));
it('should click when target element is an SVG', inject(
function($rootScope, $compile, $rootElement) {
element = $compile('<svg ng-click="tapped = true"></svg>')($rootScope);
$rootElement.append(element);
$rootScope.$digest();
browserTrigger(element, 'touchstart');
browserTrigger(element, 'touchend');
browserTrigger(element, 'click', {x:1, y:1});
expect($rootScope.tapped).toEqual(true);
}));
describe('the clickbuster', function() {
var element1, element2;