Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16dfcb61ae | |||
| 65a44dd49c | |||
| 35358fddc1 | |||
| fd5f389676 | |||
| 41d2eba5f8 | |||
| 3cbc8e5563 | |||
| 92489886dc | |||
| ea230ea91d | |||
| b68ac4cb4c | |||
| 31faeaa729 | |||
| e35abc9d2f | |||
| a196c8bca8 | |||
| ea9a130a43 | |||
| 04468db441 | |||
| 55991e33af | |||
| 8c6a8171f9 | |||
| 9971fbb3e0 | |||
| 600a41a7b6 | |||
| 398053c563 | |||
| 0ebab08e66 | |||
| 1be9bb9d35 | |||
| 26d91b653a | |||
| 5b8e7ecfeb | |||
| e63d4253d0 | |||
| e53554a0e2 | |||
| a8c7cb81c9 | |||
| 545d22b470 | |||
| 3de07aa2eb | |||
| abf31ae624 | |||
| dd1d189ee7 | |||
| b32d0f8649 | |||
| 2d5a84963e | |||
| adcc5a00bf | |||
| 94bcc03f3e | |||
| d8e4093b5a | |||
| 2cde927e58 | |||
| 73e3e8551c | |||
| 0675938931 | |||
| b21122002a | |||
| b6cb045627 | |||
| ac3f0d0b58 | |||
| 650f14eb28 | |||
| 56084b8718 | |||
| c6088da00f | |||
| e4419daf70 | |||
| 63ea0c1aac | |||
| d9b90d7c10 | |||
| 8fddaa23c8 | |||
| a958bd88a4 | |||
| e2f339e044 | |||
| 63b3060808 | |||
| 3df2ccae0f | |||
| 1064686599 | |||
| 4124a653d9 | |||
| 751ebc17f7 | |||
| 560f00860d |
@@ -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
@@ -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
|
||||
|
||||
[](https://github.com/igrigorik/ga-beacon)
|
||||
|
||||
+1
-1
@@ -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
@@ -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:
|
||||
|
||||
Vendored
+1
-1
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ myModule.directive('myDirective', function factory() {
|
||||
return {
|
||||
...
|
||||
scope: {
|
||||
'bind': '=localValue'
|
||||
localValue: '=bind'
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/),
|
||||
|
||||
@@ -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)) 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}
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}).
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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});
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
Reference in New Issue
Block a user