Compare commits

...

56 Commits

Author SHA1 Message Date
Caitlin Potter 16dfcb61ae fix(ngResource): don't convert literal values into Resource objects when isArray is true
Previously non-object literals would be thrown out of Resource responses with isArray===true, or
otherwise converted into Objects (in the case of string literals). The reason for this is because
shallowClearAndCopy iterates over keys, and copies keys into the destination. Iterating over String
keys results in integer keys, with a single-character value.

Not converting non-objects to Resources means that you lose the ability to perform Resource operations
on them. However, they become usable as strings, numbers, or booleans, which is important.

In the future, it would be useful to make these useful as Resources while still retaining their primitive
value usefulness.

Closes #6314
Closes #7741
2014-06-13 13:41:18 -07:00
rodyhaddad 65a44dd49c test(isWindow): add tests for isWindow 2014-06-13 11:35:27 -07:00
Igor Minar 35358fddc1 perf($compile): move ng-binding class stamping for interpolation into compile phase 2014-06-13 11:35:27 -07:00
Igor Minar fd5f389676 perf(ngBind): set the ng-binding class during compilation instead of linking 2014-06-13 11:35:27 -07:00
Igor Minar 41d2eba5f8 perf(jqLite): cache collection length for all methods that work on a single element
This affects jqLite#html, #text, #attr, #prop, #css and others.
2014-06-13 11:35:27 -07:00
Igor Minar 3cbc8e5563 test(jqLite): add a missing test for jqLite#text 2014-06-13 11:35:27 -07:00
Igor Minar 92489886dc perf(jqLite): improve performance of jqLite#text
This change is not compatible with IE8.
2014-06-13 11:35:27 -07:00
Igor Minar ea230ea91d chore: name the event callback used by ngClick and friends
This maskes looking at stack traces easier.

Since we generate the callbacks for each event type at runtime and we can't
set function's name because it's read-only, we have to use a generic name.
2014-06-13 11:35:27 -07:00
Igor Minar b68ac4cb4c perf: optimize internal isWindow call
Each window has a reference to itself, which is pretty unique so we can use
that to simplify our isWindow check
2014-06-13 11:35:27 -07:00
Igor Minar 31faeaa729 perf(jqLite): optimize adding nodes to a jqLite collection
This code is very hot and in most cases we are wrapping just a single Node so
we should optimize for that scenario.
2014-06-13 11:35:27 -07:00
Igor Minar e35abc9d2f perf(jqLite): optimize element dealocation
Iterate only over elements and not nodes since we don't attach data or handlers
to text/comment nodes.
2014-06-13 11:35:27 -07:00
Igor Minar a196c8bca8 fix(jqLite): data should store data only on Element and Document nodes
This is what jQuery does by default: https://github.com/jquery/jquery/blob/c18c6229c84cd2f0c9fe9f6fc3749e2c93608cc7/src/data/accepts.js#L16

We don't need to set data on text/comment nodes internally and if we don't
allow setting data on these nodes, we don't need to worry about cleaning
it up.

BREAKING CHANGE: previously it was possible to set jqLite data on Text/Comment
nodes, but now that is allowed only on Element and Document nodes just like in
jQuery. We don't expect that app code actually depends on this accidental feature.
2014-06-13 11:35:26 -07:00
Igor Minar ea9a130a43 perf(jqLite): don't use reflection to access expandoId
Since we allow only one copy of Angular to be loaded at a time it doesn't
make much sense randomly generate the expando property name and then be
forced to use slow reflective calles to retrieve the IDs.
2014-06-13 11:35:26 -07:00
Igor Minar 04468db441 perf(shallowCopy): use Object.keys to improve performance
This change is not IE8 friendly
2014-06-13 11:35:26 -07:00
Igor Minar 55991e33af perf(forEach): cache array length
Micro-optimization :-)

BREAKING CHANGE: forEach will iterate only over the initial number of items in
the array. So if items are added to the array during the iteration, these won't
be iterated over during the initial forEach call.

This change also makes our forEach behave more like Array#forEach.
2014-06-13 11:35:26 -07:00
Igor Minar 8c6a8171f9 perf(Scope): change Scope#id to be a simple number
In apps that create lots of scopes (apps with large tables) the uid generation
shows up in the profiler and adds a few milliseconds. Using simple counter
doesn't have this overhead.

I think the initial fear of overflowing and thus using string alphanum sequence
is unjustified because even if an app was to create lots of scopes non-stop,
you could create about 28.6 million scopes per seconds for 10 years before
you would reach a number that can't be accurately represented in JS

BREAKING CHANGE: Scope#$id is now of time number rather than string. Since the
id is primarily being used for debugging purposes this change should not affect
anyone.
2014-06-13 11:35:26 -07:00
XrXr 9971fbb3e0 docs(CONTRIBUTING.md): fix link to unit testing docs
The old link points to a page that doesn't exist

Closes #7830
2014-06-13 13:56:01 -04:00
Lucas Galfaso 600a41a7b6 fix($parse): Handle one-time to null
Handles when a one-time binding stabilizes to `null`

Closes #7743
Closes #7787
2014-06-13 09:05:19 -07:00
Peter Bacon Darwin 398053c563 fix($compile): ensure transclude works at root of templateUrl
If a "replace" directive has an async template, which contains a transclusion
directive at its root node, then outer transclusions were failing to be
passed to this directive.  An example would be uses of `ngIf` inside and
outside the template.

Collaborated with @caitp

Closes #7183
Closes #7772
2014-06-13 14:28:47 +01:00
Matias Niemelä 0ebab08e66 docs(forms): additional documentation for $touched vs $pristine states 2014-06-12 21:46:39 -04:00
Matias Niemelä 1be9bb9d35 fix(NgModel): ensure pattern and ngPattern use the same validator
When the pattern and ng-pattern attributes are used with an input element
containing a ngModel directive then they should both use the same validator
and the validation errors of the model should be placed on model.$error.pattern.

BREAKING CHANGE:

If an expression is used on ng-pattern (such as `ng-pattern="exp"`) or on the
pattern attribute (something like on `pattern="{{ exp }}"`) and the expression
itself evaluates to a string then the validator will not parse the string as a
literal regular expression object (a value like `/abc/i`).  Instead, the entire
string will be created as the regular expression to test against. This means
that any expression flags will not be placed on the RegExp object. To get around
this limitation, use a regular expression object as the value for the expression.

    //before
    $scope.exp = '/abc/i';

    //after
    $scope.exp = /abc/i;
2014-06-12 21:18:36 -04:00
Matias Niemelä 26d91b653a fix(NgModel): make ngMinlength and ngMaxlength as standalone directives
Fixes #6750
2014-06-12 21:18:24 -04:00
Matias Niemelä 5b8e7ecfeb fix(NgModel): make sure the ngMinlength and ngMaxlength validators use the $validators pipeline
Fixes #6304
2014-06-12 21:17:03 -04:00
Matias Niemelä e63d4253d0 fix(NgModel): make sure the pattern validator uses the $validators pipeline 2014-06-12 21:16:36 -04:00
Matias Niemelä e53554a0e2 fix(NgModel): make sure the required validator uses the $validators pipeline
Fixes #5164
2014-06-12 21:16:16 -04:00
Matias Niemelä a8c7cb81c9 feat(NgModel): introduce the $validators pipeline 2014-06-12 21:16:02 -04:00
Tero Parviainen 545d22b470 fix($injector): report circularity in circular dependency error message
Change the error message for a circular dependency to display the full
circle back to the first service being instantiated, so that the problem
is obvious. The previous message stopped one dependency short of the full
circle.

Changes the content of the cdep error message, which may be considered
a breaking change.

Closes #7500
2014-06-12 17:23:09 -07:00
Brian Ford 3de07aa2eb docs(TRIAGING.md): clarification for issues that are not reproducable 2014-06-12 14:38:52 -07:00
Aiden N abf31ae624 docs(guide/bootstrap): fix link to angular.js script in example
code.angularjs.org/angular.js is 404, updated it

Updated http://code.angularjs.org/angular.js in the example to http://code.angularjs.org/snapshot/angular.js
It works fine now.

Closes #7807
2014-06-12 14:24:03 -04:00
Michal Kawalec dd1d189ee7 perf($http): move xsrf cookie check to after cache check in $http
$http was previously checking cookies to find an xsrf-token prior to checking
the cache. This caused a performance penalty of about 2ms, which can be very
significant when loading hundreds of template instances on a page.

Fixes #7717
2014-06-12 10:28:17 -07:00
fuqcool b32d0f8649 docs(guide/services): fix link to services api
Closes #7795
2014-06-12 00:09:56 -04:00
Dave Wells 2d5a84963e docs(errors/$compile/nonassing): fix reversed attribute and scope property names 2014-06-11 14:51:37 -07:00
Arturo Guzman adcc5a00bf feat(input): add $touched and $untouched states
Sets the ngModel controller property $touched to True and $untouched to False whenever a 'blur' event is triggered over a control with the ngModel directive.
Also adds the $setTouched and $setUntouched methods to the NgModelController.

References #583
2014-06-10 23:06:31 -04:00
Alexander Karpan 94bcc03f3e docs(guide/services): fix link to wikipedia article containing parentheses
Fix in a mis-parsed link with ')' symbol in it

Closess #7775
2014-06-10 16:10:21 -04:00
fvanderwielen d8e4093b5a docs(loader): improve explanation of modules 2014-06-10 11:47:56 -07:00
Buu Nguyen 2cde927e58 fix($compile): always error if two directives add isolate-scope and new-scope
Previously, the compiler would throw an error if a directive requested new non-isolate scope
after a directive had requested isolate scope. But it would not error if a directive
requested an isolate scope after a directive had requested a new non-isolate scope.

Since it is invalid to have more than one directive request any kind of scope if one of
them has requested isolate scope, then the compiler should error whatever order the
directives are applied.

This fix addresses this situation by throwing error regardless of order of directives.

BREAKING CHANGE:

Requesting isolate scope and any other scope on a single element is an error.
Before this change, the compiler let two directives request a child scope
and an isolate scope if the compiler applied them in the order of non-isolate
scope directive followed by isolate scope directive.

Now the compiler will error regardless of the order.

If you find that your code is now throwing a `$compile:multidir` error,
check that you do not have directives on the same element that are trying
to request both an isolate and a non-isolate scope and fix your code.

Closes #4402
Closes #4421
2014-06-10 12:08:55 +01:00
Rocky Assad 73e3e8551c chore(jshint): dedupe jshint option 2014-06-10 01:46:12 -07:00
Brian Ford 0675938931 docs(tutorial/step_09): fix formatting 2014-06-10 01:36:46 -07:00
Brian Ford b21122002a docs(tutorial/step_09): fix link to filter guide 2014-06-10 01:34:41 -07:00
Kevin Western b6cb045627 docs($animate): $animate.enabled's param is optional
The docs show that param 'element' (of type DOMElement) is required when it is optional.
2014-06-10 01:07:19 -07:00
Amar Patel ac3f0d0b58 docs(api/index): add Oxford comma
Verified grammar at https://owl.english.purdue.edu/owl/resource/607/01/
2014-06-10 00:57:21 -07:00
Dylan Semler 650f14eb28 docs(error/$compile/tplrt): note that html comments can cause this 2014-06-10 00:52:15 -07:00
Carl Sutherland 56084b8718 docs(directive/input): document ngTrim for textarea 2014-06-10 00:36:42 -07:00
Matt Johansen c6088da00f docs(tutorial/step_09): note about 'Filter' suffix
Reminder that 'Filter' is appended to filter names when injected.
Link to Filter guide where this is mentioned.
2014-06-09 21:44:48 -07:00
Caitlin Potter e4419daf70 feat(ngInclude): emit $includeContentError when HTTP request fails
This adds a scope event notification when a template fails to load.

This can have performance implications, and unfortunately cannot at this moment
be terminated with preventDefault(). But it's nice to be notified when problems
occur!

Closes #5803
2014-06-09 21:20:01 -07:00
Stephen Nancekivell 63ea0c1aac docs(error/$sce/unsafe): suggest including ngSanatize 2014-06-09 19:53:05 -07:00
Matias Niemelä d9b90d7c10 feat(attrs): trigger observers for specific ng-attributes
When an observer is set to listen on the pattern, minlength or maxlength attributes
via $attrs then the observer will also listen on the ngPattern, ngMinlength and the
ngMaxlength attributes as well.

Closes #7758
2014-06-09 21:48:07 -04:00
Arjunkumar 8fddaa23c8 docs(guide/index): add codeschool link 2014-06-09 15:50:18 -07:00
Uri Goldshtein a958bd88a4 docs(guide): add UI-Map to Complementary Libraries 2014-06-09 14:30:36 -07:00
Uri Goldshtein e2f339e044 docs(guide): add ngTagsInput to UI Widgets 2014-06-09 14:01:13 -07:00
Joseph Orbegoso Pea 63b3060808 docs(ngController): improve wording 2014-06-09 13:39:55 -07:00
Caitlin Potter 3df2ccae0f docs(CHANGELOG.md): remove mention of strict-DI from v1.2.17
Strict-DI feature was originally merged into v1.2.x, but was reverted by https://github.com/angular/angular.js/commit/373078a94cf3d525b9ae11a2f2876acb6e26f6a3
2014-06-09 00:00:18 -04:00
Nikita Vasilyev 1064686599 docs(guide/databinding): add line breaks after images
This looks much better, thanks a bunch @NV

Closes #7748
2014-06-08 22:12:42 -04:00
Peter Bacon Darwin 4124a653d9 docs(error/$rootScope/inprog): improve understanding and diagnosis of the error
See #5549
2014-06-08 19:33:57 +01:00
Sebastian Müller 751ebc17f7 perf(isArray): use native Array.isArray
see benchmark: http://jsperf.com/isarray-performance

Closes #7735
2014-06-06 20:06:38 -04:00
Peter Bacon Darwin 560f00860d docs(tutorial): mention additional Debian install step
Thanks to GSC Leticia (gsc-leticia) for identifying this problem.

Closes #7665
2014-06-06 21:27:23 +01:00
47 changed files with 1281 additions and 296 deletions
-3
View File
@@ -151,9 +151,6 @@
## Features
- **injector:** "strict-DI" mode which disables "automatic" function annotation
([f5a04f59](https://github.com/angular/angular.js/commit/f5a04f59cf8e8dd6d1806059e3d7fe440aa1613e),
[#6719](https://github.com/angular/angular.js/issues/6719), [#6717](https://github.com/angular/angular.js/issues/6717), [#4504](https://github.com/angular/angular.js/issues/4504), [#6069](https://github.com/angular/angular.js/issues/6069), [#3611](https://github.com/angular/angular.js/issues/3611))
- **ngMock:** add support of mocha tdd interface
([6d1c6772](https://github.com/angular/angular.js/commit/6d1c67727ab872c44addc783ef1406952142d89e),
[#7489](https://github.com/angular/angular.js/issues/7489))
+1 -1
View File
@@ -271,6 +271,6 @@ You can find out more detailed information about contributing in the
[ngDocs]: https://github.com/angular/angular.js/wiki/Writing-AngularJS-Documentation
[plunker]: http://plnkr.co/edit
[stackoverflow]: http://stackoverflow.com/questions/tagged/angularjs
[unit-testing]: http://docs.angularjs.org/guide/dev_guide.unit-testing
[unit-testing]: https://docs.angularjs.org/guide/unit-testing
[![Analytics](https://ga-beacon.appspot.com/UA-8594346-11/angular.js/CONTRIBUTING.md?pixel)](https://github.com/igrigorik/ga-beacon)
+1 -1
View File
@@ -234,7 +234,7 @@ module.exports = function(grunt) {
'src/**/*.js',
'test/**/*.js',
'!test/ngScenario/DescribeSpec.js',
'!src/ng/directive/booleanAttrs.js', // legitimate xit here
'!src/ng/directive/attrs.js', // legitimate xit here
'!src/ngScenario/**/*.js'
]
},
+1 -1
View File
@@ -34,7 +34,7 @@ This process based on the idea of minimizing user pain
* Check if there are comments that link to a dupe. If so verify that this is indeed a dupe, [close it][], and go to the last step.
1. Bugs:
* Label `Type: Bug`
* Reproducible? - Steps to reproduce the bug are clear. If they are not,
* Reproducible? - Steps to reproduce the bug are clear. If they are not, ask for a clarification. If there's no reply after a week, [close it][].
* Reproducible on master? - <http://code.angularjs.org/snapshot/>
1. Non bugs:
+1 -1
View File
@@ -44,7 +44,7 @@ angularFiles = {
'src/ng/directive/directives.js',
'src/ng/directive/a.js',
'src/ng/directive/booleanAttrs.js',
'src/ng/directive/attrs.js',
'src/ng/directive/form.js',
'src/ng/directive/input.js',
'src/ng/directive/ngBind.js',
+3 -3
View File
@@ -6,7 +6,7 @@
Welcome to the AngularJS API docs page. These pages contain the AngularJS reference materials for version <strong ng-bind="version"></strong>.
The documentation is organized into **{@link guide/module modules}** which contain various components of an AngularJS application.
These components are {@link guide/directive directives}, {@link guide/services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates templates}, global APIs and testing mocks.
These components are {@link guide/directive directives}, {@link guide/services services}, {@link guide/filter filters}, {@link guide/providers providers}, {@link guide/templates templates}, global APIs, and testing mocks.
<div class="alert alert-info">
**Angular Namespaces `$` and `$$`**
@@ -212,7 +212,7 @@ Use ngTouch when developing for mobile browsers/devices.
{@link ngTouch#service Services / Factories}
</td>
<td>
The {@link ngTouch.$swipe $swipe} service is used to register and manage mobile DOM events.
The {@link ngTouch.$swipe $swipe} service is used to register and manage mobile DOM events.
</td>
</tr>
<tr>
@@ -252,7 +252,7 @@ Use ngSanitize to securely parse and manipulate HTML data in your application.
## {@link ngMock ngMock}
Use ngMock to inject and mock modules, factories, services and providers within your unit tests
Use ngMock to inject and mock modules, factories, services and providers within your unit tests
<div class="alert alert-info">Include the **angular-mocks.js** file into your test runner for this to work.</div>
+1 -1
View File
@@ -16,7 +16,7 @@ myModule.directive('myDirective', function factory() {
return {
...
scope: {
'bind': '=localValue'
localValue: '=bind'
}
...
}
+14
View File
@@ -37,3 +37,17 @@ elements. For example:
```
<b>Hello</b> World!
```
Watch out for html comments at the beginning or end of templates, as these can cause this error as
well. Consider the following template:
```
<div class='container'>
<div class='wrapper>
...
</div> <!-- wrapper -->
</div> <!-- container -->
```
The `<!-- container -->` comment is interpreted as a second root element and causes the template to
be invalid.
+280 -42
View File
@@ -3,72 +3,310 @@
@fullName Action Already In Progress
@description
At any point in time there can be only one `$digest` or $apply operation in progress.
The stack trace of this error allows you to trace the origin of the currently executing $apply or $digest call.
At any point in time there can be only one `$digest` or `$apply` operation in progress. This is to
prevent very hard to detect bugs from entering your application. The stack trace of this error
allows you to trace the origin of the currently executing `$apply` or `$digest` call, which caused
the error.
`$digest` or `$apply` are processing operational states of the Scope - data-structure in Angular that provides context for models and enables model mutation observation.
## Background
Trying to reenter a `$digest` or `$apply` while one of them is already in progress is typically a sign of programming error that needs to be fixed.
Angular uses a dirty-checking digest mechanism to monitor and update values of the scope during
the processing of your application. The digest works by checking all the values that are being
watched against their previous value and running any watch handlers that have been defined for those
values that have changed.
This digest mechanism is triggered by calling `$digest` on a scope object. Normally you do not need
to trigger a digest manually, because every external action that can trigger changes in your
application, such as mouse events, timeouts or server responses, wrap the Angular application code
in a block of code that will run `$digest` when the code completes.
You wrap Angular code in a block that will be followed by a `$digest` by calling `$apply` on a scope
object. So, in pseudo-code, the process looks like this:
```
element.on('mouseup', function() {
scope.$apply(function() {
$scope.doStuff();
});
});
```
where `$apply()` looks something like:
```
$apply = function(fn) {
try {
fn();
} finally() {
$digest();
}
}
```
## Digest Phases
Angular keeps track of what phase of processing we are in, the relevant ones being `$apply` and
`$digest`. Trying to reenter a `$digest` or `$apply` while one of them is already in progress is
typically a sign of programming error that needs to be fixed. So Angular will throw this error when
that occurs.
In most situations it should be well defined whether a piece of code will be run inside an `$apply`,
in which case you should not be calling `$apply` or `$digest`, or it will be run outside, in which
case you should wrap any code that will be interacting with Angular scope or services, in a call to
`$apply`.
As an example, all Controller code should expect to be run within Angular, so it should have no need
to call `$apply` or `$digest`. Conversely, code that is being trigger directly as a call back to
some external event, from the DOM or 3rd party library, should expect that it is never called from
within Angular, and so any Angular application code that it calls should first be wrapped in a call
to $apply.
## Common Causes
Apart from simply incorrect calls to `$apply` or `$digest` there are some cases when you may get
this error through no fault of your own.
### Inconsistent API (Sync/Async)
This error is often seen when interacting with an API that is sometimes sync and sometimes async.
For example:
For example, imagine a 3rd party library that has a method which will retrieve data for us. Since it
may be making an asynchronous call to a server, it accepts a callback function, which will be called
when the data arrives.
```
function MyController() {
function MyController($scope, thirdPartyComponent) {
thirdPartyComponent.getData(function(someData) {
scope.$apply(function() {
scope.someData = someData;
$scope.$apply(function() {
$scope.someData = someData;
});
});
}
```
The controller constructor is always instantiated from within an $apply cycle, so if the third-party component called our callback synchronously, we'd be trying to enter the $apply again.
We expect that our callback will be called asynchronously, and so from outside Angular. Therefore, we
correctly wrap our application code that interacts with Angular in a call to `$apply`.
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or wrap the call to the api with setTimeout call to make it always asynchronous.
The problem comes if `getData()` decides to call the callback handler synchronously; perhaps it has
the data already cached in memory and so it immediately calls the callback to return the data,
synchronously.
Since, the `MyController` constructor is always instantiated from within an `$apply` call, our
handler is trying to enter a new `$apply` block from within one.
Other situation that leads to this error is when you are trying to reuse a function to by using it as a callback for code that is called by various apis inside and outside of $apply.
This is not an ideal design choice on the part of the 3rd party library.
For example:
To resolve this type of issue, either fix the api to be always synchronous or asynchronous or force
your callback handler to always run asynchronously by using the `$timeout` service.
```
myApp.directive('myDirective', function() {
function MyController($scope, thirdPartyComponent) {
thirdPartyComponent.getData(function(someData) {
$timeout(function() {
$scope.someData = someData;
}, 0);
});
}
```
Here we have used `$timeout` to schedule the changes to the scope in a future call stack.
By providing a timeout period of 0ms, this will occur as soon as possible and `$timeout` will ensure
that the code will be called in a single `$apply` block.
### Triggering Events Programmatically
The other situation that often leads to this error is when you trigger code (such as a DOM event)
programmatically (from within Angular), which is normally called by an external trigger.
For example, consider a directive that will set focus on an input control when a value in the scope
is true:
```
myApp.directive('setFocusIf', function() {
return {
link: function($scope, $element) {
function doSomeWork() {
$scope.$apply(function() {
// do work here, and update the model
};
}
$element.on('click', doSomeWork);
doSomeWork(); // << this will throw an exception because templates are compiled within $apply
}
}
});
```
The fix for the example above looks like this:
```
myApp.directive('myDirective', function() {
return {
link: function($scope, $element) {
function doSomeWork() {
// do work here, and update the model
}
$element.on('click', function() {
$scope.$apply(doSomeWork); // <<< the $apply call was moved to the callsite that doesn't execute in $apply call already
link: function($scope, $element, $attr) {
$scope.$watch($attr.setFocusIf, function(value) {
if ( value ) { $element[0].focus(); }
});
}
};
});
```
doSomeWork();
If we applied this directive to an input which also used the `ngFocus` directive to trigger some
work when the element receives focus we will have a problem:
```
<input set-focus-if="hasFocus" ng-focus="msg='has focus'">
<button ng-click="hasFocus = true">Focus</button>
```
In this setup, there are two ways to trigger ngFocus. First from a user interaction:
* Click on the input control
* The input control gets focus
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
`$apply()`
Second programmatically:
* Click the button
* The `ngClick` directive sets the value of `$scope.hasFocus` to true inside a call to `$apply`
* The `$digest` runs, which triggers the watch inside the `setFocusIf` directive
* The watch's handle runs, which gives the focus to the input
* The `ngFocus` directive is triggered, setting `$scope.msg='has focus'` from within a new call to
`$apply()`
In this second scenario, we are already inside a `$digest` when the ngFocus directive makes another
call to `$apply()`, causing this error to be thrown.
It is possible to workaround this problem by moving the call to set the focus outside of the digest,
by using `$timeOut(fn, 0, false)`, where the `false` value tells Angular not to wrap this `fn` in a
`$apply` block:
```
myApp.directive('setFocusIf', function($timeout) {
return {
link: function($scope, $element, $attr) {
$scope.$watch($attr.setFocusIf, function(value) {
if ( value ) {
$timeout(function() {
// We must reevaluate the value in case it was changed by a subsequent
// watch handler in the digest.
if ( $scope.$eval($attr.setFocusIf) ) {
$element[0].focus();
}
}, 0, false);
}
});
}
}
});
```
To learn more about Angular processing model please check out the {@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
## Diagnosing This Error
When you get this error it can be rather daunting to diagnose the cause of the issue. The best
course of action is to investigate the stack trace from the error. You need to look for places
where `$apply` or `$digest` have been called and find the context in which this occurred.
There should be two calls:
* The first call is the good `$apply`/`$digest` and would normally be triggered by some event near
the top of the call stack.
* The second call is the bad `$apply`/`$digest` and this is the one to investigate.
Once you have identified this call you work your way up the stack to see what the problem is.
* If the second call was made in your application code then you should look at why this code has been
called from within a `$apply`/`$digest`. It may be a simple oversight or maybe it fits with the
sync/async scenario described earlier.
* If the second call was made inside an Angular directive then it is likely that it matches the second
programmatic event trigger scenario described earlier. In this case you may need to look further up
the tree to what triggered the event in the first place.
### Example Problem
Let's look at how to investigate this error using the `setFocusIf` example from above. This example
defines a new `setFocusIf` directive that sets the focus on the element where it is defined when the
value of its attribute becomes true.
<example name="error-$rootScope-inprog" module="app">
<file name="index.html">
<button ng-click="focusInput = true">Focus</button>
<input ng-focus="count = count + 1" set-focus-if="focusInput" />
</file>
<file name="app.js">
angular.module('app', []).directive('setFocusIf', function() {
return function link($scope, $element, $attr) {
$scope.$watch($attr.setFocusIf, function(value) {
if ( value ) { $element[0].focus(); }
});
};
});
</file>
</example>
When you click on the button to cause the focus to occur we get our `$rootScope:inprog` error. The
stacktrace looks like this:
```
Error: [$rootScope:inprog]
at Error (native)
at angular.min.js:6:467
at n (angular.min.js:105:60)
at g.$get.g.$apply (angular.min.js:113:195)
at HTMLInputElement.<anonymous> (angular.min.js:198:401)
at angular.min.js:32:32
at Array.forEach (native)
at q (angular.min.js:7:295)
at HTMLInputElement.c (angular.min.js:32:14)
at Object.fn (app.js:12:38) angular.js:10111
(anonymous function) angular.js:10111
$get angular.js:7412
$get.g.$apply angular.js:12738 <--- $apply
(anonymous function) angular.js:19833 <--- called here
(anonymous function) angular.js:2890
q angular.js:320
c angular.js:2889
(anonymous function) app.js:12
$get.g.$digest angular.js:12469
$get.g.$apply angular.js:12742 <--- $apply
(anonymous function) angular.js:19833 <--- called here
(anonymous function) angular.js:2890
q angular.js:320
```
We can see (even though the Angular code is minified) that there were two calls to `$apply`, first
on line `19833`, then on line `12738` of `angular.js`.
It is this second call that caused the error. If we look at the angular.js code, we can see that
this call is made by an Angular directive.
```
var ngEventDirectives = {};
forEach(
'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
function(name) {
var directiveName = directiveNormalize('ng-' + name);
ngEventDirectives[directiveName] = ['$parse', function($parse) {
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
});
});
};
}
};
}];
}
);
```
It is not possible to tell which from the stack trace, but we happen to know in this case that it is
the `ngFocus` directive.
Now look up the stack to see that our application code is only entered once in `app.js` at line `12`.
This is where our problem is:
```
10: link: function($scope, $element, $attr) {
11: $scope.$watch($attr.setFocusIf, function(value) {
12: if ( value ) { $element[0].focus(); } <---- This is the source of the problem
13: });
14: }
```
We can now see that the second `$apply` was caused by us programmatically triggering a DOM event
(i.e. focus) to occur. We must fix this by moving the code outside of the $apply block using
`$timeout` as described above.
## Further Reading
To learn more about Angular processing model please check out the
{@link guide/concepts concepts doc} as well as the {@link ng.$rootScope.Scope api} doc.
+1
View File
@@ -13,3 +13,4 @@ Angular template from a URL requires that the URL is one considered safe for loa
This helps prevent XSS and other security issues. Read more at {@link
api/ng.$sce Strict Contextual Escaping (SCE)}
You may want to include the ngSanitize module to use the automatic sanitizing.
+1 -1
View File
@@ -91,7 +91,7 @@ Here is an example of manually initializing Angular:
<html>
<body>
Hello {{'World'}}!
<script src="http://code.angularjs.org/angular.js"></script>
<script src="http://code.angularjs.org/snapshot/angular.js"></script>
<script>
angular.module('myApp', [])
+2 -2
View File
@@ -9,7 +9,7 @@ When the model changes, the view reflects the change, and vice versa.
## Data Binding in Classical Template Systems
<img class="right" src="img/One_Way_Data_Binding.png"/>
<img class="right" src="img/One_Way_Data_Binding.png"/><br />
Most templating systems bind data in only one direction: they merge template and model components
together into a view. After the merge occurs, changes to the model
or related sections of the view are NOT automatically reflected in the view. Worse, any changes
@@ -18,7 +18,7 @@ to write code that constantly syncs the view with the model and the model with t
## Data Binding in Angular Templates
<img class="right" src="img/Two_Way_Data_Binding.png"/>
<img class="right" src="img/Two_Way_Data_Binding.png"/><br />
Angular templates work differently. First the template (which is the uncompiled HTML along with
any additional markup or directives) is compiled on the browser. The compilation step produces a
live view. Any changes to the view are immediately reflected in the model, and any changes in
+1 -1
View File
@@ -5,7 +5,7 @@
A filter formats the value of an expression for display to the user. They can be used in view templates,
controllers or services and it is easy to define your own filter.
The underlying API is the {@link ng.$filterProvider filterProvider}.
The underlying API is the {@link ng.$filterProvider `filterProvider`}.
## Using filters in view templates
+2
View File
@@ -62,6 +62,8 @@ To allow styling of form as well as controls, `ngModel` adds these CSS classes:
- `ng-invalid`
- `ng-pristine`
- `ng-dirty`
- `ng-touched`
- `ng-untouched`
The following example uses the CSS to display validity of each form control.
In the example both `user.name` and `user.email` are required, but are rendered with red background only when they are dirty.
+3 -1
View File
@@ -73,8 +73,9 @@ This is a short list of libraries with specific support and documentation for wo
* **Internationalization:** [angular-translate](http://angular-translate.github.io), [angular-gettext](http://angular-gettext.rocketeer.be/)
* **RESTful services:** [Restangular](https://github.com/mgonto/restangular)
* **SQL and NoSQL backends:** [BreezeJS](http://www.breezejs.com/), [AngularFire](http://angularfire.com/)
* **UI Widgets: **[KendoUI](http://kendo-labs.github.io/angular-kendo/#/), [UI Bootstrap](http://angular-ui.github.io/bootstrap/), [Wijmo](http://wijmo.com/tag/angularjs-2/)
* **UI Widgets: **[KendoUI](http://kendo-labs.github.io/angular-kendo/#/), [UI Bootstrap](http://angular-ui.github.io/bootstrap/), [Wijmo](http://wijmo.com/tag/angularjs-2/), [ngTagsInput](https://github.com/mbenford/ngTagsInput)
* **Advanced Routing:** [UI-Router](https://github.com/angular-ui/ui-router)
* **Maps:** [UI-Map (Google Maps)](https://github.com/angular-ui/ui-map)
## Deployment
@@ -112,6 +113,7 @@ This is a short list of libraries with specific support and documentation for wo
* **Free online:**
[thinkster.io](http://thinkster.io),
[CodeAcademy](http://www.codecademy.com/courses/javascript-advanced-en-2hJ3J/0/1)
[CodeSchool](https://www.codeschool.com/courses/shaping-up-with-angular-js)
* **Paid online:**
[Pluralsite (3 courses)](http://www.pluralsight.com/training/Courses/Find?highlight=true&searchTerm=angularjs),
[Tuts+](https://tutsplus.com/course/easier-js-apps-with-angular/),
+2 -2
View File
@@ -130,7 +130,7 @@ injection of `$window`, `$scope`, and our `notify` service:
</example>
<div class="alert alert-danger">
**Careful:** If you plan to [minify](http://en.wikipedia.org/wiki/Minification_(programming)) your
**Careful:** If you plan to [minify](http://en.wikipedia.org/wiki/Minification_(programming&#41;) your
code, your variable names will get renamed unless you use one of the annotation techniques above.
</div>
@@ -299,5 +299,5 @@ it('should clear messages after alert', function() {
## Related API
* {@link ./ng Angular Service API}
* {@link ./api/ng/service Angular Service API}
* {@link angular.injector Injector API}
+4 -1
View File
@@ -105,9 +105,12 @@ Check the version of Node.js that you have installed by running the following co
node --version
```
Or in Debian based distributions:
In Debian based distributions, there is a name clash with another utility called `node`. The
suggested solution is to also install the `nodejs-legacy` apt package, which renames `node` to
`nodejs`.
```
apt-get install nodejs-legacy
nodejs --version
```
+4
View File
@@ -109,6 +109,10 @@ for this test run.
Note that we call the helper function, `inject(function(checkmarkFilter) { ... })`, to get
access to the filter that we want to test. See {@link angular.mock.inject angular.mock.inject()}.
Notice that the suffix 'Filter' is appended to your filter name when injected.
See the {@link guide/filter#using-filters-in-controllers-services-and-directives Filter Guide}
section where this is outlined.
You should now see the following output in the Karma tab:
<pre>Chrome 22.0: Executed 4 of 4 SUCCESS (0.034 secs / 0.012 secs)</pre>
+3 -1
View File
@@ -34,11 +34,11 @@
"nodeName_": false,
"uid": false,
"REGEX_STRING_REGEXP" : false,
"lowercase": false,
"uppercase": false,
"manualLowercase": false,
"manualUppercase": false,
"nodeName_": false,
"isArrayLike": false,
"forEach": false,
"sortedKeys": false,
@@ -117,6 +117,7 @@
/* jqLite.js */
"BOOLEAN_ATTR": false,
"ALIASED_ATTR": false,
"jqNextId": false,
"camelCase": false,
"jqLitePatchJQueryRemove": false,
@@ -134,6 +135,7 @@
"jqLiteController": false,
"jqLiteInheritedData": false,
"getBooleanAttrName": false,
"getAliasedAttrName": false,
"createEventHandler": false,
"JQLitePrototype": false,
"addEventListenerFn": false,
+33 -35
View File
@@ -13,6 +13,7 @@
-angularModule,
-nodeName_,
-uid,
-REGEX_STRING_REGEXP,
-lowercase,
-uppercase,
@@ -102,6 +103,8 @@
* <div doc-module-components="ng"></div>
*/
var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
/**
* @ngdoc function
* @name angular.lowercase
@@ -164,7 +167,7 @@ var /** holds major version number for IE or NaN for real browsers */
angular = window.angular || (window.angular = {}),
angularModule,
nodeName_,
uid = ['0', '0', '0'];
uid = 0;
/**
* IE 11 changed the format of the UserAgent string.
@@ -226,8 +229,9 @@ function isArrayLike(obj) {
* @param {Object=} context Object to become context (`this`) for the iterator function.
* @returns {Object|Array} Reference to `obj`.
*/
function forEach(obj, iterator, context) {
var key;
var key, length;
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
@@ -240,8 +244,9 @@ function forEach(obj, iterator, context) {
} else if (obj.forEach && obj.forEach !== forEach) {
obj.forEach(iterator, context);
} else if (isArrayLike(obj)) {
for (key = 0; key < obj.length; key++)
for (key = 0, length = obj.length; key < length; key++) {
iterator.call(context, obj[key], key);
}
} else {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
@@ -282,33 +287,17 @@ function reverseParams(iteratorFn) {
}
/**
* A consistent way of creating unique IDs in angular. The ID is a sequence of alpha numeric
* characters such as '012ABC'. The reason why we are not using simply a number counter is that
* the number string gets longer over time, and it can also overflow, where as the nextId
* will grow much slower, it is a string, and it will never overflow.
* A consistent way of creating unique IDs in angular.
*
* @returns {string} an unique alpha-numeric string
* Using simple numbers allows us to generate 28.6 million unique ids per second for 10 years before
* we hit number precision issues in JavaScript.
*
* Math.pow(2,53) / 60 / 60 / 24 / 365 / 10 = 28.6M
*
* @returns {number} an unique alpha-numeric string
*/
function nextUid() {
var index = uid.length;
var digit;
while(index) {
index--;
digit = uid[index].charCodeAt(0);
if (digit == 57 /*'9'*/) {
uid[index] = 'A';
return uid.join('');
}
if (digit == 90 /*'Z'*/) {
uid[index] = '0';
} else {
uid[index] = String.fromCharCode(digit + 1);
return uid.join('');
}
}
uid.unshift('0');
return uid.join('');
return ++uid;
}
@@ -510,10 +499,14 @@ function isDate(value) {
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
*/
function isArray(value) {
return toString.call(value) === '[object Array]';
}
var isArray = (function() {
if (!isFunction(Array.isArray)) {
return function(value) {
return toString.call(value) === '[object Array]';
};
}
return Array.isArray;
})();
/**
* @ngdoc function
@@ -550,7 +543,7 @@ function isRegExp(value) {
* @returns {boolean} True if `obj` is a window obj.
*/
function isWindow(obj) {
return obj && obj.document && obj.location && obj.alert && obj.setInterval;
return obj && obj.window === obj;
}
@@ -826,17 +819,22 @@ function copy(source, destination, stackSource, stackDest) {
* Creates a shallow copy of an object, an array or a primitive
*/
function shallowCopy(src, dst) {
var i = 0;
if (isArray(src)) {
dst = dst || [];
for ( var i = 0; i < src.length; i++) {
for (; i < src.length; i++) {
dst[i] = src[i];
}
} else if (isObject(src)) {
dst = dst || {};
for (var key in src) {
if (hasOwnProperty.call(src, key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) {
var keys = Object.keys(src);
for (var l = keys.length; i < l; i++) {
var key = keys[i];
if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
dst[key] = src[key];
}
}
+12
View File
@@ -43,8 +43,14 @@
ngModelDirective,
ngListDirective,
ngChangeDirective,
patternDirective,
patternDirective,
requiredDirective,
requiredDirective,
minlengthDirective,
minlengthDirective,
maxlengthDirective,
maxlengthDirective,
ngValueDirective,
ngModelOptionsDirective,
ngAttributeAliasDirectives,
@@ -182,8 +188,14 @@ function publishExternalAPI(angular){
ngModel: ngModelDirective,
ngList: ngListDirective,
ngChange: ngChangeDirective,
pattern: patternDirective,
ngPattern: patternDirective,
required: requiredDirective,
ngRequired: requiredDirective,
minlength: minlengthDirective,
ngMinlength: minlengthDirective,
maxlength: maxlengthDirective,
ngMaxlength: maxlengthDirective,
ngValue: ngValueDirective,
ngModelOptions: ngModelOptionsDirective
}).
+2 -1
View File
@@ -749,7 +749,8 @@ function createInjector(modulesToLoad, strictDi) {
function getService(serviceName) {
if (cache.hasOwnProperty(serviceName)) {
if (cache[serviceName] === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- '));
throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
serviceName + ' <- ' + path.join(' <- '));
}
return cache[serviceName];
} else {
+53 -28
View File
@@ -5,7 +5,8 @@
-JQLitePrototype,
-addEventListenerFn,
-removeEventListenerFn,
-BOOLEAN_ATTR
-BOOLEAN_ATTR,
-ALIASED_ATTR
*/
//////////////////////////////////
@@ -98,8 +99,9 @@
* @returns {Object} jQuery object.
*/
JQLite.expando = 'ng';
var jqCache = JQLite.cache = {},
jqName = JQLite.expando = 'ng' + new Date().getTime(),
jqId = 1,
addEventListenerFn = (window.document.addEventListener
? function(element, type, fn) {element.addEventListener(type, fn, false);}
@@ -239,8 +241,10 @@ function jqLiteClone(element) {
function jqLiteDealoc(element){
jqLiteRemoveData(element);
for ( var i = 0, children = element.childNodes || []; i < children.length; i++) {
jqLiteDealoc(children[i]);
var childElement;
for ( var i = 0, children = element.children, l = (children && children.length) || 0; i < l; i++) {
childElement = children[i];
jqLiteDealoc(childElement);
}
}
@@ -270,7 +274,7 @@ function jqLiteOff(element, type, fn, unsupported) {
}
function jqLiteRemoveData(element, name) {
var expandoId = element[jqName],
var expandoId = element.ng,
expandoStore = jqCache[expandoId];
if (expandoStore) {
@@ -284,17 +288,17 @@ function jqLiteRemoveData(element, name) {
jqLiteOff(element);
}
delete jqCache[expandoId];
element[jqName] = undefined; // ie does not allow deletion of attributes on elements.
element.ng = undefined; // don't delete DOM expandos. IE and Chrome don't like it
}
}
function jqLiteExpandoStore(element, key, value) {
var expandoId = element[jqName],
var expandoId = element.ng,
expandoStore = jqCache[expandoId || -1];
if (isDefined(value)) {
if (!expandoStore) {
element[jqName] = expandoId = jqNextId();
element.ng = expandoId = jqNextId();
expandoStore = jqCache[expandoId] = {};
}
expandoStore[key] = value;
@@ -314,7 +318,10 @@ function jqLiteData(element, key, value) {
}
if (isSetter) {
data[key] = value;
// set data only on Elements and Documents
if (element.nodeType === 1 || element.nodeType === 9) {
data[key] = value;
}
} else {
if (keyDefined) {
if (isSimpleGetter) {
@@ -363,17 +370,31 @@ function jqLiteAddClass(element, cssClasses) {
}
}
function jqLiteAddNodes(root, elements) {
// THIS CODE IS VERY HOT. Don't make changes without benchmarking.
if (elements) {
elements = (!elements.nodeName && isDefined(elements.length) && !isWindow(elements))
? elements
: [ elements ];
for(var i=0; i < elements.length; i++) {
root.push(elements[i]);
// if a Node (the most common case)
if (elements.nodeType) {
root[root.length++] = elements;
} else {
var length = elements.length;
// if an Array or NodeList and not a Window
if (typeof length === 'number' && elements.window !== elements) {
if (length) {
push.apply(root, elements);
}
} else {
root[root.length++] = elements;
}
}
}
}
function jqLiteController(element, name) {
return jqLiteInheritedData(element, '$' + (name || 'ngController' ) + 'Controller');
}
@@ -463,6 +484,11 @@ var BOOLEAN_ELEMENTS = {};
forEach('input,select,option,textarea,button,form,details'.split(','), function(value) {
BOOLEAN_ELEMENTS[uppercase(value)] = true;
});
var ALIASED_ATTR = {
'ngMinlength' : 'minlength',
'ngMaxlength' : 'maxlength',
'ngPattern' : 'pattern'
};
function getBooleanAttrName(element, name) {
// check dom last since we will most likely fail on name
@@ -472,6 +498,11 @@ function getBooleanAttrName(element, name) {
return booleanAttr && BOOLEAN_ELEMENTS[element.nodeName] && booleanAttr;
}
function getAliasedAttrName(element, name) {
var nodeName = element.nodeName;
return (nodeName === 'INPUT' || nodeName === 'TEXTAREA') && ALIASED_ATTR[name];
}
forEach({
data: jqLiteData,
inheritedData: jqLiteInheritedData,
@@ -560,23 +591,15 @@ forEach({
},
text: (function() {
var NODE_TYPE_TEXT_PROPERTY = [];
if (msie < 9) {
NODE_TYPE_TEXT_PROPERTY[1] = 'innerText'; /** Element **/
NODE_TYPE_TEXT_PROPERTY[3] = 'nodeValue'; /** Text **/
} else {
NODE_TYPE_TEXT_PROPERTY[1] = /** Element **/
NODE_TYPE_TEXT_PROPERTY[3] = 'textContent'; /** Text **/
}
getText.$dv = '';
return getText;
function getText(element, value) {
var textProp = NODE_TYPE_TEXT_PROPERTY[element.nodeType];
if (isUndefined(value)) {
return textProp ? element[textProp] : '';
var nodeType = element.nodeType;
return (nodeType === 1 || nodeType === 3) ? element.textContent : '';
}
element[textProp] = value;
element.textContent = value;
}
})(),
@@ -613,6 +636,7 @@ forEach({
*/
JQLite.prototype[name] = function(arg1, arg2) {
var i, key;
var nodeCount = this.length;
// jqLiteHasClass has only two arguments, but is a getter-only fn, so we need to special-case it
// in a way that survives minification.
@@ -622,7 +646,7 @@ forEach({
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
for (i = 0; i < this.length; i++) {
for (i = 0; i < nodeCount; i++) {
if (fn === jqLiteData) {
// data() takes the whole object in jQuery
fn(this[i], arg1);
@@ -636,9 +660,10 @@ forEach({
return this;
} else {
// we are a read, so read the first child.
// TODO: do we still need this?
var value = fn.$dv;
// Only if we have $dv do we iterate over all, otherwise it is just the first element.
var jj = (value === undefined) ? Math.min(this.length, 1) : this.length;
var jj = (value === undefined) ? Math.min(nodeCount, 1) : nodeCount;
for (var j = 0; j < jj; j++) {
var nodeValue = fn(this[j], arg1, arg2);
value = value ? value + nodeValue : nodeValue;
@@ -647,7 +672,7 @@ forEach({
}
} else {
// we are a write, so apply to all children
for (i = 0; i < this.length; i++) {
for (i = 0; i < nodeCount; i++) {
fn(this[i], arg1, arg2);
}
// return self for chaining
+1 -1
View File
@@ -44,7 +44,7 @@ function setupModuleLoader(window) {
*
* # Module
*
* A module is a collection of services, directives, filters, and configuration information.
* A module is a collection of services, directives, controllers, filters, and configuration information.
* `angular.module` is used to configure the {@link auto.$injector $injector}.
*
* ```js
+45 -20
View File
@@ -729,13 +729,19 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
//is set through this function since it may cause $updateClass to
//become unstable.
var booleanKey = getBooleanAttrName(this.$$element[0], key),
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
aliasedKey = getAliasedAttrName(node, key),
observer = key,
normalizedVal,
nodeName;
if (booleanKey) {
this.$$element.prop(key, value);
attrName = booleanKey;
} else if(aliasedKey) {
this[aliasedKey] = value;
observer = aliasedKey;
}
this[key] = value;
@@ -768,7 +774,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// fire observers
var $$observers = this.$$observers;
$$observers && forEach($$observers[key], function(fn) {
$$observers && forEach($$observers[observer], function(fn) {
try {
fn(value);
} catch (e) {
@@ -1207,17 +1213,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (directiveValue = directive.scope) {
newScopeDirective = newScopeDirective || directive;
// skip the check for directives with async templates, we'll check the derived sync
// directive when the template arrives
if (!directive.templateUrl) {
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
$compileNode);
if (isObject(directiveValue)) {
// This directive is trying to add an isolated scope.
// Check that there is no scope of any kind already
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective || newScopeDirective,
directive, $compileNode);
newIsolateScopeDirective = directive;
} else {
// This directive is trying to add a child scope.
// Check that there is no isolated scope already
assertNoDuplicate('new/isolated scope', newIsolateScopeDirective, directive,
$compileNode);
}
}
newScopeDirective = newScopeDirective || directive;
}
directiveName = directive.name;
@@ -1757,7 +1771,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
afterTemplateChildLinkFn = compileNodes($compileNode[0].childNodes, childTranscludeFn);
while(linkQueue.length) {
var scope = linkQueue.shift(),
beforeTemplateLinkNode = linkQueue.shift(),
@@ -1794,13 +1807,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
});
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
var childBoundTranscludeFn = boundTranscludeFn;
if (linkQueue) {
linkQueue.push(scope);
linkQueue.push(node);
linkQueue.push(rootElement);
linkQueue.push(boundTranscludeFn);
linkQueue.push(childBoundTranscludeFn);
} else {
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, boundTranscludeFn);
if (afterTemplateNodeLinkFn.transcludeOnThisElement) {
childBoundTranscludeFn = createBoundTranscludeFn(scope, afterTemplateNodeLinkFn.transclude, boundTranscludeFn);
}
afterTemplateNodeLinkFn(afterTemplateChildLinkFn, scope, node, rootElement, childBoundTranscludeFn);
}
};
}
@@ -1830,18 +1847,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (interpolateFn) {
directives.push({
priority: 0,
compile: valueFn(function textInterpolateLinkFn(scope, node) {
var parent = node.parent(),
bindings = parent.data('$binding') || [];
// Need to interpolate again in case this is using one-time bindings in multiple clones
// of transcluded templates.
interpolateFn = $interpolate(text);
bindings.push(interpolateFn);
safeAddClass(parent.data('$binding', bindings), 'ng-binding');
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
});
})
compile: function textInterpolateCompileFn(templateNode) {
// when transcluding a template that has bindings in the root
// then we don't have a parent and should do this in the linkFn
var parent = templateNode.parent(), hasCompileParent = parent.length;
if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding');
return function textInterpolateLinkFn(scope, node) {
var parent = node.parent(),
bindings = parent.data('$binding') || [];
// Need to interpolate again in case this is using one-time bindings in multiple clones
// of transcluded templates.
interpolateFn = $interpolate(text);
bindings.push(interpolateFn);
parent.data('$binding', bindings);
if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
node[0].nodeValue = value;
});
};
}
});
}
}
@@ -361,6 +361,29 @@ forEach(BOOLEAN_ATTR, function(propName, attrName) {
};
});
// aliased input attrs are evaluated
forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
ngAttributeAliasDirectives[ngAttr] = function() {
return {
priority: 100,
link: function(scope, element, attr) {
//special case ngPattern when a literal regular expression value
//is used as the expression (this way we don't have to watch anything).
if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
if (match) {
attr.$set("ngPattern", new RegExp(match[1], match[2]));
return;
}
}
scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
attr.$set(ngAttr, value);
});
}
};
};
});
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
+182 -87
View File
@@ -5,7 +5,9 @@
-VALID_CLASS,
-INVALID_CLASS,
-PRISTINE_CLASS,
-DIRTY_CLASS
-DIRTY_CLASS,
-UNTOUCHED_CLASS,
-TOUCHED_CLASS
*/
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
@@ -973,60 +975,6 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
};
// pattern validator
var pattern = attr.ngPattern,
patternValidator,
match;
if (pattern) {
var validateRegex = function(regexp, value) {
return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value);
};
match = pattern.match(/^\/(.*)\/([gim]*)$/);
if (match) {
pattern = new RegExp(match[1], match[2]);
patternValidator = function(value) {
return validateRegex(pattern, value);
};
} else {
patternValidator = function(value) {
var patternObj = scope.$eval(pattern);
if (!patternObj || !patternObj.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern,
patternObj, startingTag(element));
}
return validateRegex(patternObj, value);
};
}
ctrl.$formatters.push(patternValidator);
ctrl.$parsers.push(patternValidator);
}
// min length validator
if (attr.ngMinlength) {
var minlength = int(attr.ngMinlength);
var minLengthValidator = function(value) {
return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value);
};
ctrl.$parsers.push(minLengthValidator);
ctrl.$formatters.push(minLengthValidator);
}
// max length validator
if (attr.ngMaxlength) {
var maxlength = int(attr.ngMaxlength);
var maxLengthValidator = function(value) {
return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value);
};
ctrl.$parsers.push(maxLengthValidator);
ctrl.$formatters.push(maxLengthValidator);
}
}
function weekParser(isoWeek) {
@@ -1281,6 +1229,7 @@ function checkboxInputType(scope, element, attr, ctrl) {
* patterns defined as scope expressions.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
*/
@@ -1410,7 +1359,9 @@ var inputDirective = ['$browser', '$sniffer', '$filter', function($browser, $sni
var VALID_CLASS = 'ng-valid',
INVALID_CLASS = 'ng-invalid',
PRISTINE_CLASS = 'ng-pristine',
DIRTY_CLASS = 'ng-dirty';
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched';
/**
* @ngdoc type
@@ -1439,12 +1390,20 @@ var VALID_CLASS = 'ng-valid',
* ngModel.$formatters.push(formatter);
* ```
*
* @property {Object.<string, function>} $validators A collection of validators that are applied
* whenever the model value changes. The key value within the object refers to the name of the
* validator while the function refers to the validation operation. The validation operation is
* provided with the model value as an argument and must return a true or false value depending
* on the response of that validation.
*
* @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
* view value has changed. It is called with no arguments, and its return value is ignored.
* This can be used in place of additional $watches against the model value.
*
* @property {Object} $error An object hash with all errors as keys.
*
* @property {boolean} $untouched True if control has not lost focus yet.
* @property {boolean} $touched True if control has lost focus.
* @property {boolean} $pristine True if user has not interacted with the control yet.
* @property {boolean} $dirty True if user has already interacted with the control.
* @property {boolean} $valid True if there is no error.
@@ -1555,9 +1514,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$validators = {};
this.$parsers = [];
this.$formatters = [];
this.$viewChangeListeners = [];
this.$untouched = true;
this.$touched = false;
this.$pristine = true;
this.$dirty = false;
this.$valid = true;
@@ -1612,7 +1574,9 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// Setup initial state of the control
$element.addClass(PRISTINE_CLASS);
$element
.addClass(PRISTINE_CLASS)
.addClass(UNTOUCHED_CLASS);
toggleValidCss(true);
// convenience method for easy toggling of classes
@@ -1630,7 +1594,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* Change the validity state, and notifies the form when the control changes validity. (i.e. it
* does not notify form if given validator is already marked as invalid).
*
* This method should be called by validators - i.e. the parser or formatter functions.
* This method can be called within $parsers/$formatters. However, if possible, please use the
* `ngModel.$validators` pipeline which is designed to handle validations with true/false values.
*
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
* to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
@@ -1673,7 +1638,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* Sets the control to its pristine state.
*
* This method can be called to remove the 'ng-dirty' class and set the control to its pristine
* state (ng-pristine class).
* state (ng-pristine class). A model is considered to be pristine when the model has not been changed
* from when first compiled within then form.
*/
this.$setPristine = function () {
ctrl.$dirty = false;
@@ -1682,6 +1648,42 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
$animate.addClass($element, PRISTINE_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setUntouched
*
* @description
* Sets the control to its untouched state.
*
* This method can be called to remove the 'ng-touched' class and set the control to its
* untouched state (ng-untouched class). Upon compilation, a model is set as untouched
* by default, however this function can be used to restore that state if the model has
* already been touched by the user.
*/
this.$setUntouched = function() {
ctrl.$touched = false;
ctrl.$untouched = true;
$animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setTouched
*
* @description
* Sets the control to its touched state.
*
* This method can be called to remove the 'ng-untouched' class and set the control to its
* touched state (ng-touched class). A model is considered to be touched when the user has
* first interacted (focussed) on the model input element and then shifted focus away (blurred)
* from the input element.
*/
this.$setTouched = function() {
ctrl.$touched = true;
ctrl.$untouched = false;
$animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$rollbackViewValue
@@ -1747,6 +1749,23 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$render();
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$validate
*
* @description
* Runs each of the registered validations set on the $validators object.
*/
this.$validate = function() {
this.$$runValidators(ctrl.$modelValue, ctrl.$viewValue);
};
this.$$runValidators = function(modelValue, viewValue) {
forEach(ctrl.$validators, function(fn, name) {
ctrl.$setValidity(name, fn(modelValue, viewValue));
});
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$commitViewValue
@@ -1759,12 +1778,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* usually handles calling this in response to input events.
*/
this.$commitViewValue = function() {
var value = ctrl.$viewValue;
var viewValue = ctrl.$viewValue;
$timeout.cancel(pendingDebounce);
if (ctrl.$$lastCommittedViewValue === value) {
if (ctrl.$$lastCommittedViewValue === viewValue) {
return;
}
ctrl.$$lastCommittedViewValue = value;
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
if (ctrl.$pristine) {
@@ -1775,13 +1795,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
parentForm.$setDirty();
}
var modelValue = viewValue;
forEach(ctrl.$parsers, function(fn) {
value = fn(value);
modelValue = fn(modelValue);
});
if (ctrl.$modelValue !== value) {
ctrl.$modelValue = value;
ngModelSet($scope, value);
if (ctrl.$modelValue !== modelValue &&
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
ctrl.$$runValidators(modelValue, viewValue);
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;
ngModelSet($scope, ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
@@ -1855,26 +1881,31 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// model -> value
$scope.$watch(function ngModelWatch() {
var value = ngModelGet($scope);
var modelValue = ngModelGet($scope);
// if scope model value and ngModel value are out of sync
if (ctrl.$modelValue !== value) {
if (ctrl.$modelValue !== modelValue &&
(isUndefined(ctrl.$$invalidModelValue) || ctrl.$$invalidModelValue != modelValue)) {
var formatters = ctrl.$formatters,
idx = formatters.length;
ctrl.$modelValue = value;
var viewValue = modelValue;
while(idx--) {
value = formatters[idx](value);
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== value) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = value;
ctrl.$$runValidators(modelValue, viewValue);
ctrl.$modelValue = ctrl.$valid ? modelValue : undefined;
ctrl.$$invalidModelValue = ctrl.$valid ? undefined : modelValue;
if (ctrl.$viewValue !== viewValue) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
}
}
return value;
return modelValue;
});
}];
@@ -1895,8 +1926,8 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
* require.
* - Providing validation behavior (i.e. required, number, email, url).
* - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors).
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations.
* - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
* - Registering the control with its parent {@link ng.directive:form form}.
*
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
@@ -2017,6 +2048,12 @@ var ngModelDirective = function() {
});
});
}
element.on('blur', function(ev) {
scope.$apply(function() {
modelCtrl.$setTouched();
});
});
}
}
};
@@ -2098,27 +2135,85 @@ var requiredDirective = function() {
if (!ctrl) return;
attr.required = true; // force truthy in case we are on non input element
var validator = function(value) {
if (attr.required && ctrl.$isEmpty(value)) {
ctrl.$setValidity('required', false);
return;
} else {
ctrl.$setValidity('required', true);
return value;
}
ctrl.$validators.required = function(modelValue, viewValue) {
return !attr.required || !ctrl.$isEmpty(viewValue);
};
ctrl.$formatters.push(validator);
ctrl.$parsers.unshift(validator);
attr.$observe('required', function() {
validator(ctrl.$viewValue);
ctrl.$validate();
});
}
};
};
var patternDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var regexp, patternExp = attr.ngPattern || attr.pattern;
attr.$observe('pattern', function(regex) {
if(isString(regex) && regex.length > 0) {
regex = new RegExp(regex);
}
if (regex && !regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(elm));
}
regexp = regex || undefined;
ctrl.$validate();
});
ctrl.$validators.pattern = function(value) {
return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value);
};
}
};
};
var maxlengthDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = 0;
attr.$observe('maxlength', function(value) {
maxlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(value) {
return ctrl.$isEmpty(value) || value.length <= maxlength;
};
}
};
};
var minlengthDirective = function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var minlength = 0;
attr.$observe('minlength', function(value) {
minlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.minlength = function(value) {
return ctrl.$isEmpty(value) || value.length >= minlength;
};
}
};
};
/**
* @ngdoc directive
* @name ngList
+13 -8
View File
@@ -50,14 +50,19 @@
</file>
</example>
*/
var ngBindDirective = ngDirective(function(scope, element, attr) {
element.addClass('ng-binding').data('$binding', attr.ngBind);
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
// We are purposefully using == here rather than === because we want to
// catch when value is "null or undefined"
// jshint -W041
element.text(value == undefined ? '' : value);
});
var ngBindDirective = ngDirective({
compile: function(templateElement) {
templateElement.addClass('ng-binding');
return function (scope, element, attr) {
element.data('$binding', attr.ngBind);
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
// We are purposefully using == here rather than === because we want to
// catch when value is "null or undefined"
// jshint -W041
element.text(value == undefined ? '' : value);
});
};
}
});
+1 -1
View File
@@ -10,7 +10,7 @@
*
* MVC components in angular:
*
* * Model — The Model is scope properties; scopes are attached to the DOM where scope properties
* * Model — Models are the properties of a scope; scopes are attached to the DOM where scope properties
* are accessed through bindings.
* * View — The template (HTML with data bindings) that is rendered into the View.
* * Controller — The `ngController` directive specifies a Controller class; the class contains business
+1 -1
View File
@@ -45,7 +45,7 @@ forEach(
return {
compile: function($element, attr) {
var fn = $parse(attr[directiveName]);
return function(scope, element, attr) {
return function ngEventHandler(scope, element) {
element.on(lowercase(name), function(event) {
scope.$apply(function() {
fn(scope, {$event:event});
+14 -1
View File
@@ -159,6 +159,16 @@
* @description
* Emitted every time the ngInclude content is reloaded.
*/
/**
* @ngdoc event
* @name ng.directive:ngInclude#$includeContentError
* @eventOf ng.directive:ngInclude
* @eventType emit on the scope ngInclude was declared in
* @description
* Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299)
*/
var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce',
function($http, $templateCache, $anchorScroll, $animate, $sce) {
return {
@@ -227,7 +237,10 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'
currentScope.$emit('$includeContentLoaded');
scope.$eval(onloadExp);
}).error(function() {
if (thisChangeId === changeCounter) cleanupLastIncludeContent();
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError');
}
});
scope.$emit('$includeContentRequested');
} else {
+10 -9
View File
@@ -602,14 +602,6 @@ function $HttpProvider() {
config.headers = headers;
config.method = uppercase(config.method);
var xsrfValue = urlIsSameOrigin(config.url)
? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
: undefined;
if (xsrfValue) {
headers[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
}
var serverRequest = function(config) {
headers = config.headers;
var reqData = transformData(config.data, headersGetter(headers), config.transformRequest);
@@ -885,8 +877,17 @@ function $HttpProvider() {
}
}
// if we won't have the response in cache, send the request to the backend
// if we won't have the response in cache, set the xsrf headers and
// send the request to the backend
if (isUndefined(cachedResp)) {
var xsrfValue = urlIsSameOrigin(config.url)
? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
: undefined;
if (xsrfValue) {
reqHeaders[(config.xsrfHeaderName || defaults.xsrfHeaderName)] = xsrfValue;
}
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
config.withCredentials, config.responseType);
}
+2 -1
View File
@@ -1055,7 +1055,8 @@ function $ParseProvider() {
if (oneTimeParseFn.$$unwatch && self && self.$$postDigestQueue) {
self.$$postDigestQueue.push(function () {
// create a copy if the value is defined and it is not a $sce value
if ((stable = isDefined(lastValue)) && !lastValue.$$unwrapTrustedValue) {
if ((stable = isDefined(lastValue)) &&
(lastValue === null || !lastValue.$$unwrapTrustedValue)) {
lastValue = copy(lastValue, null);
}
});
+1 -1
View File
@@ -831,7 +831,7 @@ angular.module('ngAnimate', ['ng'])
* @kind function
*
* @param {boolean=} value If provided then set the animation on or off.
* @param {DOMElement} element If provided then the element will be used to represent the enable/disable operation
* @param {DOMElement=} element If provided then the element will be used to represent the enable/disable operation
* @return {boolean} Current animation state.
*
* @description
+8 -1
View File
@@ -580,7 +580,14 @@ angular.module('ngResource', ['ng']).
if (action.isArray) {
value.length = 0;
forEach(data, function (item) {
value.push(new Resource(item));
if (typeof item === "object") {
value.push(new Resource(item));
} else {
// Valid JSON values may be string literals, and these should not be converted
// into objects. These items will not have access to the Resource prototype
// methods, but unfortunately there
value.push(item);
}
});
} else {
shallowClearAndCopy(data, value);
+17 -1
View File
@@ -886,6 +886,22 @@ describe('angular', function() {
});
describe('isWindow', function () {
it('should return true for the Window object', function() {
expect(isWindow(window)).toBe(true);
});
it('should return false for any object that is not a Window', function() {
expect(isWindow([])).toBe(false);
expect(isWindow('')).toBeFalsy();
expect(isWindow(23)).toBe(false);
expect(isWindow({})).toBe(false);
expect(isWindow(new Date())).toBe(false);
expect(isWindow(document)).toBe(false);
});
});
describe('compile', function() {
it('should link to existing node and create scope', inject(function($rootScope, $compile) {
var template = angular.element('<div>{{greeting = "hello world"}}</div>');
@@ -958,7 +974,7 @@ describe('angular', function() {
while(count--) {
var current = nextUid();
expect(current.match(/[\d\w]+/)).toBeTruthy();
expect(typeof current).toBe('number');
expect(seen[current]).toBeFalsy();
seen[current] = true;
}
+2 -2
View File
@@ -656,7 +656,7 @@ describe('injector', function() {
$provide.factory('service', function(service){});
return function(service) {};
}]);
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: service');
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: service <- service');
});
@@ -667,7 +667,7 @@ describe('injector', function() {
$provide.factory('b', function(a){});
return function(a) {};
}]);
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: b <- a');
}).toThrowMinErr('$injector', 'cdep', 'Circular dependency found: a <- b <- a');
});
});
+2
View File
@@ -48,6 +48,8 @@ beforeEach(function() {
toBeValid: cssMatcher('ng-valid', 'ng-invalid'),
toBeDirty: cssMatcher('ng-dirty', 'ng-pristine'),
toBePristine: cssMatcher('ng-pristine', 'ng-dirty'),
toBeUntouched: cssMatcher('ng-untouched', 'ng-touched'),
toBeTouched: cssMatcher('ng-touched', 'ng-untouched'),
toBeShown: function() {
this.message = valueFn(
"Expected element " + (this.isNot ? "": "not ") + "to have 'ng-hide' class");
+1 -1
View File
@@ -24,7 +24,7 @@ beforeEach(function() {
}
// This resets global id counter;
uid = ['0', '0', '0'];
uid = 0;
// reset to jQuery or default to us.
bindJQuery();
+8
View File
@@ -881,6 +881,12 @@ describe('jqLite', function() {
expect(element.text('xyz') == element).toBeTruthy();
expect(element.text()).toEqual('xyzxyz');
});
it('should return text only for element or text nodes', function() {
expect(jqLite('<div>foo</div>').text()).toBe('foo');
expect(jqLite('<div>foo</div>').contents().eq(0).text()).toBe('foo');
expect(jqLite(document.createComment('foo')).text()).toBe('');
});
});
@@ -963,6 +969,8 @@ describe('jqLite', function() {
},
detachEvent: noop
};
window.window = window;
var log;
var jWindow = jqLite(window).on('hashchange', function() {
log = 'works!';
+91 -21
View File
@@ -1885,7 +1885,7 @@ describe('$compile', function() {
it('should allow creation of new scopes', inject(function($rootScope, $compile, log) {
element = $compile('<div><span scope><a log></a></span></div>')($rootScope);
expect(log).toEqual('002; log-002-001; LOG');
expect(log).toEqual('2; log-2-1; LOG');
expect(element.find('span').hasClass('ng-scope')).toBe(true);
}));
@@ -1893,7 +1893,7 @@ describe('$compile', function() {
it('should allow creation of new isolated scopes for directives', inject(
function($rootScope, $compile, log) {
element = $compile('<div><span iscope><a log></a></span></div>')($rootScope);
expect(log).toEqual('log-001-no-parent; LOG; 002');
expect(log).toEqual('log-1-no-parent; LOG; 2');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
@@ -1905,11 +1905,11 @@ describe('$compile', function() {
$httpBackend.expect('GET', 'tscope.html').respond('<a log>{{name}}; scopeId: {{$id}}</a>');
element = $compile('<div><span tscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
expect(log).toEqual('log-2-1; LOG; 2');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
expect(element.find('span').scope().$id).toBe('002');
expect(element.text()).toBe('Jozo; scopeId: 2');
expect(element.find('span').scope().$id).toBe(2);
}));
@@ -1919,11 +1919,11 @@ describe('$compile', function() {
respond('<p><a log>{{name}}; scopeId: {{$id}}</a></p>');
element = $compile('<div><span trscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
expect(log).toEqual('log-2-1; LOG; 2');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 002');
expect(element.find('a').scope().$id).toBe('002');
expect(element.text()).toBe('Jozo; scopeId: 2');
expect(element.find('a').scope().$id).toBe(2);
}));
@@ -1933,12 +1933,12 @@ describe('$compile', function() {
respond('<p><a log>{{name}}; scopeId: {{$id}} |</a></p>');
element = $compile('<div><span ng-repeat="i in [1,2,3]" trscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-003-002; LOG; 003; log-005-004; LOG; 005; log-007-006; LOG; 007');
expect(log).toEqual('log-3-2; LOG; 3; log-5-4; LOG; 5; log-7-6; LOG; 7');
$rootScope.name = 'Jozo';
$rootScope.$apply();
expect(element.text()).toBe('Jozo; scopeId: 003 |Jozo; scopeId: 005 |Jozo; scopeId: 007 |');
expect(element.find('p').scope().$id).toBe('003');
expect(element.find('a').scope().$id).toBe('003');
expect(element.text()).toBe('Jozo; scopeId: 3 |Jozo; scopeId: 5 |Jozo; scopeId: 7 |');
expect(element.find('p').scope().$id).toBe(3);
expect(element.find('a').scope().$id).toBe(3);
}));
@@ -1947,7 +1947,7 @@ describe('$compile', function() {
$httpBackend.expect('GET', 'tiscope.html').respond('<a log></a>');
element = $compile('<div><span tiscope></span></div>')($rootScope);
$httpBackend.flush();
expect(log).toEqual('log-002-001; LOG; 002');
expect(log).toEqual('log-2-1; LOG; 2');
$rootScope.name = 'abc';
expect(iscope.$parent).toBe($rootScope);
expect(iscope.name).toBeUndefined();
@@ -1967,7 +1967,7 @@ describe('$compile', function() {
'</b>' +
'</div>'
)($rootScope);
expect(log).toEqual('002; 003; log-003-002; LOG; log-002-001; LOG; 004; log-004-001; LOG');
expect(log).toEqual('2; 3; log-3-2; LOG; log-2-1; LOG; 4; log-4-1; LOG');
})
);
@@ -1976,7 +1976,7 @@ describe('$compile', function() {
'the scope', inject(
function($rootScope, $compile, log) {
element = $compile('<div class="scope-a; scope-b"></div>')($rootScope);
expect(log).toEqual('002; 002');
expect(log).toEqual('2; 2');
})
);
@@ -1989,11 +1989,30 @@ describe('$compile', function() {
})
);
it('should not allow more than one isolate scope creation per element regardless of directive priority', function() {
module(function($compileProvider) {
$compileProvider.directive('highPriorityScope', function() {
return {
restrict: 'C',
priority: 1,
scope: true,
link: function() {}
};
});
});
inject(function($compile) {
expect(function(){
$compile('<div class="iscope-a; high-priority-scope"></div>');
}).toThrowMinErr('$compile', 'multidir', 'Multiple directives [highPriorityScope, iscopeA] asking for new/isolated scope on: ' +
'<div class="iscope-a; high-priority-scope">');
});
});
it('should create new scope even at the root of the template', inject(
function($rootScope, $compile, log) {
element = $compile('<div scope-a></div>')($rootScope);
expect(log).toEqual('002');
expect(log).toEqual('2');
})
);
@@ -2001,7 +2020,7 @@ describe('$compile', function() {
it('should create isolate scope even at the root of the template', inject(
function($rootScope, $compile, log) {
element = $compile('<div iscope></div>')($rootScope);
expect(log).toEqual('002');
expect(log).toEqual('2');
})
);
@@ -3852,8 +3871,8 @@ describe('$compile', function() {
element = $compile('<div><div trans>T:{{$parent.$id}}-{{$id}}<span>;</span></div></div>')
($rootScope);
$rootScope.$apply();
expect(element.text()).toEqual('W:001-002;T:001-003;');
expect(jqLite(element.find('span')[0]).text()).toEqual('T:001-003');
expect(element.text()).toEqual('W:1-2;T:1-3;');
expect(jqLite(element.find('span')[0]).text()).toEqual('T:1-3');
expect(jqLite(element.find('span')[1]).text()).toEqual(';');
});
});
@@ -4289,7 +4308,7 @@ describe('$compile', function() {
inject(function($compile) {
element = $compile('<div transclude>{{$id}}</div>')($rootScope);
$rootScope.$apply();
expect(element.text()).toBe($rootScope.$id);
expect(element.text()).toBe('' + $rootScope.$id);
});
});
@@ -4525,7 +4544,7 @@ describe('$compile', function() {
($rootScope);
$rootScope.$apply();
expect(log).toEqual('compile: <!-- trans: text -->; link; LOG; LOG; HIGH');
expect(element.text()).toEqual('001-002;001-003;');
expect(element.text()).toEqual('1-2;1-3;');
});
});
@@ -4833,6 +4852,57 @@ describe('$compile', function() {
expect(element.text()).toBe('-->|x|');
}));
// See https://github.com/angular/angular.js/issues/7183
it("should pass transclusion through to template of a 'replace' directive", function() {
module(function() {
directive('transSync', function() {
return {
transclude: true,
link: function(scope, element, attr, ctrl, transclude) {
expect(transclude).toEqual(jasmine.any(Function));
transclude(function(child) { element.append(child); });
}
};
});
directive('trans', function($timeout) {
return {
transclude: true,
link: function(scope, element, attrs, ctrl, transclude) {
// We use timeout here to simulate how ng-if works
$timeout(function() {
transclude(function(child) { element.append(child); });
});
}
};
});
directive('replaceWithTemplate', function() {
return {
templateUrl: "template.html",
replace: true
};
});
});
inject(function($compile, $rootScope, $templateCache, $timeout) {
$templateCache.put('template.html', '<div trans-sync>Content To Be Transcluded</div>');
expect(function() {
element = $compile('<div><div trans><div replace-with-template></div></div></div>')($rootScope);
$timeout.flush();
}).not.toThrow();
expect(element.text()).toEqual('Content To Be Transcluded');
});
});
});
+358 -11
View File
@@ -51,6 +51,8 @@ describe('NgModelController', function() {
it('should init the properties', function() {
expect(ctrl.$untouched).toBe(true);
expect(ctrl.$touched).toBe(false);
expect(ctrl.$dirty).toBe(false);
expect(ctrl.$pristine).toBe(true);
expect(ctrl.$valid).toBe(true);
@@ -133,6 +135,28 @@ describe('NgModelController', function() {
});
});
describe('setUntouched', function() {
it('should set control to its untouched state', function() {
ctrl.$setTouched();
ctrl.$setUntouched();
expect(ctrl.$touched).toBe(false);
expect(ctrl.$untouched).toBe(true);
});
});
describe('setTouched', function() {
it('should set control to its touched state', function() {
ctrl.$setUntouched();
ctrl.$setTouched();
expect(ctrl.$touched).toBe(true);
expect(ctrl.$untouched).toBe(false);
});
});
describe('view -> model', function() {
it('should set the value to $viewValue', function() {
@@ -261,17 +285,167 @@ describe('NgModelController', function() {
expect(ctrl.$render).toHaveBeenCalledOnce();
});
});
describe('$validators', function() {
it('should perform validations when $validate() is called', function() {
ctrl.$validators.uppercase = function(value) {
return (/^[A-Z]+$/).test(value);
};
ctrl.$modelValue = 'test';
ctrl.$validate();
expect(ctrl.$valid).toBe(false);
ctrl.$modelValue = 'TEST';
ctrl.$validate();
expect(ctrl.$valid).toBe(true);
});
it('should perform validations when $validate() is called', function() {
ctrl.$validators.uppercase = function(value) {
return (/^[A-Z]+$/).test(value);
};
ctrl.$modelValue = 'test';
ctrl.$validate();
expect(ctrl.$valid).toBe(false);
ctrl.$modelValue = 'TEST';
ctrl.$validate();
expect(ctrl.$valid).toBe(true);
});
it('should always perform validations using the parsed model value', function() {
var captures;
ctrl.$validators.raw = function() {
captures = arguments;
return captures[0];
};
ctrl.$parsers.push(function(value) {
return value.toUpperCase();
});
ctrl.$setViewValue('my-value');
expect(captures).toEqual(['MY-VALUE', 'my-value']);
});
it('should always perform validations using the formatted view value', function() {
var captures;
ctrl.$validators.raw = function() {
captures = arguments;
return captures[0];
};
ctrl.$formatters.push(function(value) {
return value + '...';
});
scope.$apply(function() {
scope.value = 'matias';
});
expect(captures).toEqual(['matias', 'matias...']);
});
it('should only perform validations if the view value is different', function() {
var count = 0;
ctrl.$validators.countMe = function() {
count++;
};
ctrl.$setViewValue('my-value');
expect(count).toBe(1);
ctrl.$setViewValue('my-value');
expect(count).toBe(1);
ctrl.$setViewValue('your-value');
expect(count).toBe(2);
});
it('should perform validations twice each time the model value changes within a digest', function() {
var count = 0;
ctrl.$validators.number = function(value) {
count++;
return (/^\d+$/).test(value);
};
function val(v) {
scope.$apply(function() {
scope.value = v;
});
}
val('');
expect(count).toBe(1);
val(1);
expect(count).toBe(2);
val(1);
expect(count).toBe(2);
val('');
expect(count).toBe(3);
});
it('should only validate to true if all validations are true', function() {
var curry = function(v) {
return function() {
return v;
};
};
ctrl.$validators.a = curry(true);
ctrl.$validators.b = curry(true);
ctrl.$validators.c = curry(false);
ctrl.$validate();
expect(ctrl.$valid).toBe(false);
ctrl.$validators.c = curry(true);
ctrl.$validate();
expect(ctrl.$valid).toBe(true);
});
it('should register invalid validations on the $error object', function() {
var curry = function(v) {
return function() {
return v;
};
};
ctrl.$validators.unique = curry(false);
ctrl.$validators.tooLong = curry(false);
ctrl.$validators.notNumeric = curry(true);
ctrl.$validate();
expect(ctrl.$error.unique).toBe(true);
expect(ctrl.$error.tooLong).toBe(true);
expect(ctrl.$error.notNumeric).not.toBe(true);
});
});
});
describe('ngModel', function() {
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty)',
it('should set css classes (ng-valid, ng-invalid, ng-pristine, ng-dirty, ng-untouched, ng-touched)',
inject(function($compile, $rootScope, $sniffer) {
var element = $compile('<input type="email" ng-model="value" />')($rootScope);
$rootScope.$digest();
expect(element).toBeValid();
expect(element).toBePristine();
expect(element).toBeUntouched();
expect(element.hasClass('ng-valid-email')).toBe(true);
expect(element.hasClass('ng-invalid-email')).toBe(false);
@@ -297,6 +471,9 @@ describe('ngModel', function() {
expect(element.hasClass('ng-valid-email')).toBe(true);
expect(element.hasClass('ng-invalid-email')).toBe(false);
browserTrigger(element, 'blur');
expect(element).toBeTouched();
dealoc(element);
}));
@@ -309,6 +486,23 @@ describe('ngModel', function() {
expect(element).toHaveClass('ng-invalid-required');
}));
it('should set the control touched state on "blur" event', inject(function($compile, $rootScope) {
var element = $compile('<form name="myForm">' +
'<input name="myControl" ng-model="value" >' +
'</form>')($rootScope);
var inputElm = element.find('input');
var control = $rootScope.myForm.myControl;
expect(control.$touched).toBe(false);
expect(control.$untouched).toBe(true);
browserTrigger(inputElm, 'blur');
expect(control.$touched).toBe(true);
expect(control.$untouched).toBe(false);
dealoc(element);
}));
it('should register/deregister a nested ngModel with parent form when entering or leaving DOM',
inject(function($compile, $rootScope) {
@@ -423,6 +617,15 @@ describe('input', function() {
scope.$digest();
}
var attrs;
beforeEach(module(function($compileProvider) {
$compileProvider.directive('attrCapture', function() {
return function(scope, element, $attrs) {
attrs = $attrs;
};
});
}));
beforeEach(inject(function($injector, _$sniffer_, _$browser_) {
$sniffer = _$sniffer_;
$browser = _$browser_;
@@ -1073,6 +1276,19 @@ describe('input', function() {
expect(inputElm).toBeInvalid();
});
it('should listen on ng-pattern when pattern is observed', function() {
var value, patternVal = /^\w+$/;
compileInput('<input type="text" ng-model="value" ng-pattern="pat" attr-capture />');
attrs.$observe('pattern', function(v) {
value = attrs.pattern;
});
scope.$apply(function() {
scope.pat = patternVal;
});
expect(value).toBe(patternVal);
});
it('should validate in-lined pattern with modifiers', function() {
compileInput('<input type="text" ng-model="value" ng-pattern="/^abc?$/i" />');
@@ -1104,7 +1320,9 @@ describe('input', function() {
changeInputValueTo('x');
expect(inputElm).toBeInvalid();
scope.regexp = /abc?/;
scope.$apply(function() {
scope.regexp = /abc?/;
});
changeInputValueTo('ab');
expect(inputElm).toBeValid();
@@ -1113,11 +1331,60 @@ describe('input', function() {
expect(inputElm).toBeInvalid();
});
it('should perform validations when the ngPattern scope value changes', function() {
scope.regexp = /^[a-z]+$/;
compileInput('<input type="text" ng-model="value" ng-pattern="regexp" />');
it('should throw an error when scope pattern can\'t be found', function() {
changeInputValueTo('abcdef');
expect(inputElm).toBeValid();
changeInputValueTo('123');
expect(inputElm).toBeInvalid();
scope.$apply(function() {
scope.regexp = /^\d+$/;
});
expect(inputElm).toBeValid();
changeInputValueTo('abcdef');
expect(inputElm).toBeInvalid();
scope.$apply(function() {
scope.regexp = '';
});
expect(inputElm).toBeValid();
});
it('should register "pattern" with the model validations when the pattern attribute is used', function() {
compileInput('<input type="text" name="input" ng-model="value" pattern="^\\d+$" />');
changeInputValueTo('abcd');
expect(inputElm).toBeInvalid();
expect(scope.form.input.$error.pattern).toBe(true);
changeInputValueTo('12345');
expect(inputElm).toBeValid();
expect(scope.form.input.$error.pattern).not.toBe(true);
});
it('should not throw an error when scope pattern can\'t be found', function() {
expect(function() {
compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
scope.$apply();
scope.$apply(function() {
scope.foo = 'bar';
});
}).not.toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/);
});
it('should throw an error when the scope pattern is not a regular expression', function() {
expect(function() {
compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
scope.$apply(function() {
scope.fooRegexp = {};
scope.foo = 'bar';
});
}).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/);
});
});
@@ -1125,28 +1392,92 @@ describe('input', function() {
describe('minlength', function() {
it('should invalid shorter than given minlength', function() {
it('should invalidate values that are shorter than the given minlength', function() {
compileInput('<input type="text" ng-model="value" ng-minlength="3" />');
changeInputValueTo('aa');
expect(scope.value).toBeUndefined();
expect(inputElm).toBeInvalid();
changeInputValueTo('aaa');
expect(scope.value).toBe('aaa');
expect(inputElm).toBeValid();
});
it('should listen on ng-minlength when minlength is observed', function() {
var value = 0;
compileInput('<input type="text" ng-model="value" ng-minlength="min" attr-capture />');
attrs.$observe('minlength', function(v) {
value = int(attrs.minlength);
});
scope.$apply(function() {
scope.min = 5;
});
expect(value).toBe(5);
});
it('should observe the standard minlength attribute and register it as a validator on the model', function() {
compileInput('<input type="text" name="input" ng-model="value" minlength="{{ min }}" />');
scope.$apply(function() {
scope.min = 10;
});
changeInputValueTo('12345');
expect(inputElm).toBeInvalid();
expect(scope.form.input.$error.minlength).toBe(true);
scope.$apply(function() {
scope.min = 5;
});
expect(inputElm).toBeValid();
expect(scope.form.input.$error.minlength).not.toBe(true);
});
});
describe('maxlength', function() {
it('should invalid shorter than given maxlength', function() {
it('should invalidate values that are longer than the given maxlength', function() {
compileInput('<input type="text" ng-model="value" ng-maxlength="5" />');
changeInputValueTo('aaaaaaaa');
expect(scope.value).toBeUndefined();
expect(inputElm).toBeInvalid();
changeInputValueTo('aaa');
expect(scope.value).toBe('aaa');
expect(inputElm).toBeValid();
});
it('should listen on ng-maxlength when maxlength is observed', function() {
var value = 0;
compileInput('<input type="text" ng-model="value" ng-maxlength="max" attr-capture />');
attrs.$observe('maxlength', function(v) {
value = int(attrs.maxlength);
});
scope.$apply(function() {
scope.max = 10;
});
expect(value).toBe(10);
});
it('should observe the standard maxlength attribute and register it as a validator on the model', function() {
compileInput('<input type="text" name="input" ng-model="value" maxlength="{{ max }}" />');
scope.$apply(function() {
scope.max = 1;
});
changeInputValueTo('12345');
expect(inputElm).toBeInvalid();
expect(scope.form.input.$error.maxlength).toBe(true);
scope.$apply(function() {
scope.max = 6;
});
expect(inputElm).toBeValid();
expect(scope.form.input.$error.maxlength).not.toBe(true);
});
});
@@ -2361,7 +2692,7 @@ describe('input', function() {
compileInput('<input type="text" ng-model="name" name="alias" required />');
scope.$apply(function() {
scope.name = '';
scope.name = null;
});
expect(inputElm).toBeInvalid();
@@ -2633,6 +2964,22 @@ describe('NgModel animations', function() {
assertValidAnimation(animations[1], 'addClass', 'ng-pristine');
}));
it('should trigger an animation when untouched', inject(function($animate) {
model.$setUntouched();
var animations = findElementAnimations(input, $animate.queue);
assertValidAnimation(animations[0], 'setClass', 'ng-untouched');
expect(animations[0].args[2]).toBe('ng-touched');
}));
it('should trigger an animation when touched', inject(function($animate) {
model.$setTouched();
var animations = findElementAnimations(input, $animate.queue);
assertValidAnimation(animations[0], 'setClass', 'ng-touched', 'ng-untouched');
expect(animations[0].args[2]).toBe('ng-untouched');
}));
it('should trigger custom errors as addClass/removeClass when invalid/valid', inject(function($animate) {
model.$setValidity('custom-error', false);
+23
View File
@@ -143,6 +143,29 @@ describe('ngInclude', function() {
}));
it('should fire $includeContentError event when content request fails', inject(
function($rootScope, $compile, $httpBackend, $templateCache) {
var contentLoadedSpy = jasmine.createSpy('content loaded'),
contentErrorSpy = jasmine.createSpy('content error');
$rootScope.$on('$includeContentLoaded', contentLoadedSpy);
$rootScope.$on('$includeContentError', contentErrorSpy);
$httpBackend.expect('GET', 'tpl.html').respond(400, 'nope');
element = $compile('<div><div ng-include="template"></div></div>')($rootScope);
$rootScope.$apply(function() {
$rootScope.template = 'tpl.html';
});
$httpBackend.flush();
expect(contentLoadedSpy).not.toHaveBeenCalled();
expect(contentErrorSpy).toHaveBeenCalledOnce();
expect(element.children('div').contents().length).toBe(0);
}));
it('should evaluate onload expression when a partial is loaded', inject(
putIntoCache('myUrl', 'my partial'),
function($rootScope, $compile) {
+2 -2
View File
@@ -976,7 +976,7 @@ describe('ngRepeat', function() {
scope.items = [a, a, a];
scope.$digest();
expect($exceptionHandler.errors.shift().message).
toMatch(/^\[ngRepeat:dupes\] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:003/);
toMatch(/^\[ngRepeat:dupes\] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:3/);
// recover
scope.items = [a];
@@ -996,7 +996,7 @@ describe('ngRepeat', function() {
scope.items = [d, d, d];
scope.$digest();
expect($exceptionHandler.errors.shift().message).
toMatch(/^\[ngRepeat:dupes\] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:009/);
toMatch(/^\[ngRepeat:dupes\] Duplicates in a repeater are not allowed\. Use 'track by' expression to specify unique keys\. Repeater: item in items, Duplicate key: object:9/);
// recover
scope.items = [a];
+19
View File
@@ -741,6 +741,25 @@ describe('$http', function() {
$httpBackend.flush();
}));
it('should check the cache before checking the XSRF cookie', inject(function($browser, $cacheFactory) {
var testCache = $cacheFactory('testCache'),
executionOrder = [];
spyOn($browser, 'cookies').andCallFake(function() {
executionOrder.push('cookies');
return {'XSRF-TOKEN':'foo'};
});
spyOn(testCache, 'get').andCallFake(function() {
executionOrder.push('cache');
});
$httpBackend.expect('GET', '/url', undefined).respond('');
$http({url: '/url', method: 'GET', cache: testCache});
$httpBackend.flush();
expect(executionOrder).toEqual(['cache', 'cookies']);
}));
});
+11 -1
View File
@@ -1012,8 +1012,18 @@ describe('parser', function() {
value.baz = 'baz';
expect(fn()).toEqual({bar: 'bar'});
}));
it('should not throw if the stable value is `null`', inject(function($parse, $rootScope) {
var fn = $parse('::foo');
$rootScope.$watch(fn);
$rootScope.foo = null;
$rootScope.$digest();
$rootScope.foo = 'foo';
$rootScope.$digest();
expect(fn()).toEqual(null);
}));
});
+21
View File
@@ -1113,6 +1113,27 @@ describe("resource", function() {
$httpBackend.flush();
expect(user).toEqualData([ {id: 1, name: 'user1'} ]);
});
it('should not convert string literals in array into Resource objects', function() {
$httpBackend.expect('GET', '/names.json').respond(["mary", "jane"]);
var strings = $resource('/names.json').query();
$httpBackend.flush();
expect(strings).toEqualData(["mary", "jane"]);
});
it('should not convert number literals in array into Resource objects', function() {
$httpBackend.expect('GET', '/names.json').respond([213, 456]);
var numbers = $resource('/names.json').query();
$httpBackend.flush();
expect(numbers).toEqualData([213, 456]);
});
it('should not convert boolean literals in array into Resource objects', function() {
$httpBackend.expect('GET', '/names.json').respond([true, false]);
var bools = $resource('/names.json').query();
$httpBackend.flush();
expect(bools).toEqualData([true, false]);
});
});
describe('get', function(){