Compare commits

...

21 Commits

Author SHA1 Message Date
Caitlin Potter e4adebd07a revert: chore(npm): Make require()-able as part of publish script
This reverts commit c5686c5271.

(We wanted to get some feedback before doin this)
2015-01-13 14:29:29 -05:00
Peter Bacon Darwin ac94f6125f docs(CHANGELOG): add changes for 1.3.9 and 1.4.0-beta.0 2015-01-13 00:58:04 +00:00
Ben Clinkinbeard c5686c5271 chore(npm): Make require()-able as part of publish script
(This has not been tested locally with browserify --- but it should work!
If it doesn't, please file a bug rather than just leaving a comment on this
commit :)

Closes #10731
2015-01-12 19:09:46 -05:00
Julie Ralph 7fdb54d12b chore(testing): bump protractor to version 1.6.0 2015-01-12 11:47:47 -08:00
Jason Bedard 869008140a fix($parse): allow use of locals in assignments Fixes #4664 2015-01-12 13:38:34 +00:00
Julie Ralph 26ee32ec79 chore(travis): split out the docs e2e tests into their own travis job
Previously, they were in the 'unit' job to save travis VMs, but this
was confusing and made it more difficult to track down errors easily.
2015-01-09 14:29:26 -08:00
Julie Ralph a06193f97b chore(travis): make browserstack unit tests allowed failures 2015-01-09 10:45:23 -08:00
Uri Goldshtein ba9dee170c docs(guide/index): add angular-easyfb with Facebook login to login libraries
Merci~

Closes #5792
2015-01-06 13:41:45 -05:00
Andrey Pushkarev d4b60ada1e fix(filterFilter): use isArray() to determine array type
In JavaScript, an array is a special type of object, therefore typeof [] returns object.
Added corresponding unit tests.

Changed condition for array type to isArray.

Closes #10621
2015-01-02 13:26:51 -05:00
Rus1 b839f73ad0 docs(ngInclude): replace <tt> with <code>
Using obsolete <tt> HTML tag may not be good for Angular examples

Closes #10594
2014-12-30 15:48:19 -05:00
Peter Bacon Darwin aee32931fd test(input): split tests into smaller files
This is complement to the previous commit.
It also refactors the input compile helpers to make it cleaner and more
consistent.
2014-12-24 23:26:34 +00:00
Peter Bacon Darwin 7ee5f46bbc refact(input): split input.js into smaller files
The input.js file is unnecessarily large, containing many directives including the
vast `ngModel`. This change moves ngModel and a few other directives into their
own files, which will make maintenance easier.
2014-12-24 13:01:03 +00:00
David Souther 2b97854bf4 feat(ngMock/$exceptionHandler): log errors when rethrowing
Now the `rethrow` mode will also record a log of the error in the same
way as the `log` mode.

Closes #10540
Closes #10564
2014-12-23 18:35:49 +00:00
David Souther 316ee8f7ca test($exceptionHandlerProvider): call inject() to run tests
In the current angular-mocksSpec, the tests for $exceptionHandlerProvider
call `module` to run tests on `$exceptionHandlerProvider.mode()`, but do
not call `inject()` to pump the module definitions.

Closes #10563
2014-12-23 18:11:21 +00:00
gokulkrishh 2e18f44fcd docs(guide/*): spelling/grammar improvements
Closes #10552
2014-12-22 10:44:54 -05:00
Caitlin Potter c85d064ecf docs($rootScope): remove erroneous closing parenthesis
Closes #10549
2014-12-22 08:34:29 -05:00
Kevin Primat 7b9b82281a docs(guide/location): add missing definite article
The sentence was missing a definite article so was unclear. Added one to clarify.

Closes #10547
2014-12-22 08:25:24 -05:00
Olivier Giulieri aab632b330 docs(guide): fix spaces
Closes #10539
2014-12-22 01:08:06 +00:00
gdi2290 4c8d8ad508 perf(ngStyleDirective): use $watchCollection
Since we are simply watching a flat object collection it is more performant
to use $watchCollection than a deepWatch...

Closes #10535
2014-12-22 00:58:45 +00:00
Dan Cancro 3a8f3dc9ea docs(guide/index): Link to starter options
Add a link to a comparison spreadsheet of alternative generators, examples,
tutorials and seeds that one can use to get started on a new Angular project.

Closes #10526
2014-12-22 00:30:30 +00:00
Robert Haritonov c139e68d99 docs(tutorial/12): fix path to jquery in bower
Closes #10504
2014-12-22 00:21:27 +00:00
33 changed files with 6002 additions and 5279 deletions
+6
View File
@@ -9,9 +9,11 @@ branches:
env:
matrix:
- JOB=unit BROWSER_PROVIDER=saucelabs
- JOB=docs-e2e BROWSER_PROVIDER=saucelabs
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
- JOB=unit BROWSER_PROVIDER=browserstack
- JOB=docs-e2e BROWSER_PROVIDER=browserstack
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=browserstack
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=browserstack
global:
@@ -22,6 +24,10 @@ env:
- LOGS_DIR=/tmp/angular-build/logs
- BROWSER_PROVIDER_READY_FILE=/tmp/browsersprovider-tunnel-ready
matrix:
allow_failures:
- env: "JOB=unit BROWSER_PROVIDER=browserstack"
install:
# - npm config set registry http://23.251.144.68
# Disable the spinner, it looks bad on Travis
+150
View File
@@ -1,3 +1,153 @@
<a name="1.4.0-beta.0"></a>
# 1.4.0-beta.0 photonic-umbrakinesis (2015-01-13)
## Bug Fixes
- **$location:** support right button click on anchors in firefox
([aa798f12](https://github.com/angular/angular.js/commit/aa798f123658cb78b5581513d26577016195cafe),
[#7984](https://github.com/angular/angular.js/issues/7984))
- **$templateRequest:** propagate HTTP status on failed requests
([e24f22bd](https://github.com/angular/angular.js/commit/e24f22bdb1740388938d58778aa24d307a79a796),
[#10514](https://github.com/angular/angular.js/issues/10514), [#10628](https://github.com/angular/angular.js/issues/10628))
- **dateFilter:** ignore invalid dates
([1334b8c8](https://github.com/angular/angular.js/commit/1334b8c8326b93e0ca016c85516627900c7a9fd3),
[#10640](https://github.com/angular/angular.js/issues/10640))
- **filterFilter:** use isArray() to determine array type
([a01ce6b8](https://github.com/angular/angular.js/commit/a01ce6b81c197b0a4a1057981e8e9c1b74f37587),
[#10621](https://github.com/angular/angular.js/issues/10621))
- **ngChecked:** ensure that ngChecked doesn't interfere with ngModel
([e079111b](https://github.com/angular/angular.js/commit/e079111b33bf36be21c0941718b41cc9ca67bea0),
[#10662](https://github.com/angular/angular.js/issues/10662), [#10664](https://github.com/angular/angular.js/issues/10664))
- **ngClass:** handle multi-class definitions as an element of an array
([e1132f53](https://github.com/angular/angular.js/commit/e1132f53b03a5a71aa9b6eded24d64e3bc83929b),
[#8578](https://github.com/angular/angular.js/issues/8578), [#10651](https://github.com/angular/angular.js/issues/10651))
- **ngModelOptions:** allow sharing options between multiple inputs
([9c9c6b3f](https://github.com/angular/angular.js/commit/9c9c6b3fe4edfe78ae275c413ee3eefb81f1ebf6),
[#10667](https://github.com/angular/angular.js/issues/10667))
- **ngOptions:**
- support one-time binding on the option values
([ba90261b](https://github.com/angular/angular.js/commit/ba90261b7586b519483883800ea876510faf5c21),
[#10687](https://github.com/angular/angular.js/issues/10687), [#10694](https://github.com/angular/angular.js/issues/10694))
- prevent infinite digest if track by expression is stable
([fc21db8a](https://github.com/angular/angular.js/commit/fc21db8a15545fad53124fc941b3c911a8d57067),
[#9464](https://github.com/angular/angular.js/issues/9464))
- update model if selected option is removed
([933591d6](https://github.com/angular/angular.js/commit/933591d69cee2c5580da1d8522ba90a7d924da0e),
[#7736](https://github.com/angular/angular.js/issues/7736))
- ensure that the correct option is selected when options are loaded async
([7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
[#8019](https://github.com/angular/angular.js/issues/8019), [#9714](https://github.com/angular/angular.js/issues/9714), [#10639](https://github.com/angular/angular.js/issues/10639))
- **ngPluralize:** generate a warning when using a not defined rule
([c66b4b6a](https://github.com/angular/angular.js/commit/c66b4b6a133f7215d50c23db516986cfc1f0a985))
## Features
- **$filter:** display Infinity symbol when number is Infinity
([51d67742](https://github.com/angular/angular.js/commit/51d6774286202b55ade402ca097e417e70fd546b),
[#10421](https://github.com/angular/angular.js/issues/10421))
- **$timeout:** allow `fn` to be an optional parameter
([5a603023](https://github.com/angular/angular.js/commit/5a60302389162c6ef45f311c1aaa65a00d538c66),
[#9176](https://github.com/angular/angular.js/issues/9176))
- **limitTo:** ignore limit when invalid
([a3c3bf33](https://github.com/angular/angular.js/commit/a3c3bf3332e5685dc319c46faef882cb6ac246e1),
[#10510](https://github.com/angular/angular.js/issues/10510))
- **ngMock/$exceptionHandler:** log errors when rethrowing
([deb3cb4d](https://github.com/angular/angular.js/commit/deb3cb4daef0054457bd9fb8995829fff0e8f1e4),
[#10540](https://github.com/angular/angular.js/issues/10540), [#10564](https://github.com/angular/angular.js/issues/10564))
## Performance Improvements
- **ngStyleDirective:** use $watchCollection
([8928d023](https://github.com/angular/angular.js/commit/8928d0234551a272992d0eccef73b3ad6cb8bfd1),
[#10535](https://github.com/angular/angular.js/issues/10535))
## Breaking Changes
- **limitTo:** due to [a3c3bf33](https://github.com/angular/angular.js/commit/a3c3bf3332e5685dc319c46faef882cb6ac246e1),
limitTo changed behavior when limit value is invalid.
Instead of returning empty object/array it returns unchanged input.
- **ngOptions:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
When using `ngOptions`: the directive applies a surrogate key as the value of the `<option>` element.
This commit changes the actual string used as the surrogate key. We now store a string that is computed
by calling `hashKey` on the item in the options collection; previously it was the index or key of the
item in the collection.
(This is in keeping with the way that the unknown option value is represented in the select directive.)
Before you might have seen:
```
<select ng-model="x" ng-option="i in items">
<option value="1">a</option>
<option value="2">b</option>
<option value="3">c</option>
<option value="4">d</option>
</select>
```
Now it will be something like:
```
<select ng-model="x" ng-option="i in items">
<option value="string:a">a</option>
<option value="string:b">b</option>
<option value="string:c">c</option>
<option value="string:d">d</option>
</select>
```
If your application code relied on this value, which it shouldn't, then you will need to modify your
application to accommodate this. You may find that you can use the `track by` feaure of `ngOptions`
as this provides the ability to specify the key that is stored.
- **ngOptions:** due to [7fda214c](https://github.com/angular/angular.js/commit/7fda214c4f65a6a06b25cf5d5aff013a364e9cef),
When iterating over an object's properties using the `(key, value) in obj` syntax
the order of the elements used to be sorted alphabetically. This was an artificial
attempt to create a deterministic ordering since browsers don't guarantee the order.
But in practice this is not what people want and so this change iterates over properties
in the order they are returned by Object.keys(obj), which is almost always the order
in which the properties were defined.
<a name="1.3.9"></a>
# 1.3.9 multidimensional-awareness (2015-01-13)
## Bug Fixes
- **$parse:** allow use of locals in assignments
([86900814](https://github.com/angular/angular.js/commit/869008140a96e0e9e0d9774cc2e5fdd66ada7ba9))
- **filterFilter:** use isArray() to determine array type
([d4b60ada](https://github.com/angular/angular.js/commit/d4b60ada1ecff5afdb3210caa44e149e9f3d4c1b),
[#10621](https://github.com/angular/angular.js/issues/10621))
## Features
- **ngMock/$exceptionHandler:** log errors when rethrowing
([2b97854b](https://github.com/angular/angular.js/commit/2b97854bf4786fe8579974e2b9d6b4adee8a3dc3),
[#10540](https://github.com/angular/angular.js/issues/10540), [#10564](https://github.com/angular/angular.js/issues/10564))
## Performance Improvements
- **ngStyleDirective:** use $watchCollection
([4c8d8ad5](https://github.com/angular/angular.js/commit/4c8d8ad5083d9dd17c0b8480339d5f95943f1b71),
[#10535](https://github.com/angular/angular.js/issues/10535))
<a name="1.3.8"></a>
# 1.3.8 prophetic-narwhal (2014-12-19)
+5 -1
View File
@@ -53,6 +53,7 @@ var angularFiles = {
'src/ng/directive/form.js',
'src/ng/directive/input.js',
'src/ng/directive/ngBind.js',
'src/ng/directive/ngChange.js',
'src/ng/directive/ngClass.js',
'src/ng/directive/ngCloak.js',
'src/ng/directive/ngController.js',
@@ -61,6 +62,8 @@ var angularFiles = {
'src/ng/directive/ngIf.js',
'src/ng/directive/ngInclude.js',
'src/ng/directive/ngInit.js',
'src/ng/directive/ngList.js',
'src/ng/directive/ngModel.js',
'src/ng/directive/ngNonBindable.js',
'src/ng/directive/ngPluralize.js',
'src/ng/directive/ngRepeat.js',
@@ -70,7 +73,8 @@ var angularFiles = {
'src/ng/directive/ngTransclude.js',
'src/ng/directive/script.js',
'src/ng/directive/select.js',
'src/ng/directive/style.js'
'src/ng/directive/style.js',
'src/ng/directive/validators.js'
],
'angularLoader': [
+1 -1
View File
@@ -35,7 +35,7 @@ var users = [ { name: 'Hank' }, { name: 'Francisco' } ];
$scope.getUsers = function() {
return users;
});
};
```
The maximum number of allowed iterations of the `$digest` cycle is controlled via TTL setting which can be configured via {@link ng.$rootScopeProvider $rootScopeProvider}.
+1 -1
View File
@@ -358,7 +358,7 @@ legacy browsers and hashbang links in modern browser:
Here you can see two `$location` instances, both in **Html5 mode**, but on different browsers, so
that you can see the differences. These `$location` services are connected to a fake browsers. Each
input represents address bar of the browser.
input represents the address bar of the browser.
Note that when you type hashbang url into first browser (or vice versa) it doesn't rewrite /
redirect to regular / hashbang url, as this conversion happens only during parsing the initial URL
+2 -2
View File
@@ -88,7 +88,7 @@ As a best practice, consider adding an `ng-strict-di` directive on the same elem
```
This will ensure that all services in your application are properly annotated.
See the {@link guide/di#using-strict-dependency-injection dependancy injection strict mode} docs
See the {@link guide/di#using-strict-dependency-injection dependency injection strict mode} docs
for more.
@@ -156,4 +156,4 @@ until `angular.resumeBootstrap()` is called.
`angular.resumeBootstrap()` takes an optional array of modules that
should be added to the original list of modules that the app was
about to be bootstrapped with.
about to be bootstrapped with.
+2 -2
View File
@@ -131,7 +131,7 @@ A form is an instance of {@link form.FormController FormController}.
The form instance can optionally be published into the scope using the `name` attribute.
Similarly, an input control that has the {@link ng.directive:ngModel ngModel} directive holds an
instance of {@link ngModel.NgModelController NgModelController}.Such a control instance
instance of {@link ngModel.NgModelController NgModelController}. Such a control instance
can be published as a property of the form instance using the `name` attribute on the input control.
The name attribute specifies the name of the property on the form instance.
@@ -339,7 +339,7 @@ In the following example we create two directives:
<div>
Username:
<input type="text" ng-model="name" name="name" username />{{name}}<br />
<span ng-show="form.name.$pending.username">Checking if this name is available ...</span>
<span ng-show="form.name.$pending.username">Checking if this name is available...</span>
<span ng-show="form.name.$error.username">This username is already taken!</span>
</div>
+2 -1
View File
@@ -53,7 +53,7 @@ In Angular applications, you move the job of filling page templates with data fr
## Specific Topics
* **Login: **[Google example](https://developers.google.com/+/photohunt/python), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
* **Login: **[Google example](https://developers.google.com/+/photohunt/python), [AngularJS Faceb0ok library](https://github.com/pc035860/angular-easyfb), [Facebook example](http://blog.brunoscopelliti.com/facebook-authentication-in-your-angularjs-web-app), [authentication strategy](http://blog.brunoscopelliti.com/deal-with-users-authentication-in-an-angularjs-web-app), [unix-style authorization](http://frederiknakstad.com/authentication-in-single-page-applications-with-angular-js/)
* **Mobile:** [Angular on Mobile Guide](http://www.ng-newsletter.com/posts/angular-on-mobile.html), [PhoneGap](http://devgirl.org/2013/06/10/quick-start-guide-phonegap-and-angularjs/)
* **Other Languages:** [CoffeeScript](http://www.coffeescriptlove.com/2013/08/angularjs-and-coffeescript-tutorials.html), [Dart](https://github.com/angular/angular.dart.tutorial/wiki)
* **Realtime: **[Socket.io](http://www.creativebloq.com/javascript/angularjs-collaboration-board-socketio-2132885), [OmniBinder](https://github.com/jeffbcross/omnibinder)
@@ -62,6 +62,7 @@ In Angular applications, you move the job of filling page templates with data fr
## Tools
* **Getting Started:** [Comparison of the options for starting a new project](http://www.dancancro.com/comparison-of-angularjs-application-starters/)
* **Debugging:** [Batarang](https://chrome.google.com/webstore/detail/angularjs-batarang/ighdmehidhipcmcojjgiloacoafjmpfk?hl=en)
* **Testing:** [Karma](http://karma-runner.github.io), [Protractor](https://github.com/angular/protractor)
* **Editor support:** [Webstorm](http://plugins.jetbrains.com/plugin/6971) (and [video](http://www.youtube.com/watch?v=LJOyrSh1kDU)), [Sublime Text](https://github.com/angular-ui/AngularJS-sublime-package), [Visual Studio](http://madskristensen.net/post/angularjs-intellisense-in-visual-studio-2012)
+5 -5
View File
@@ -21,7 +21,7 @@ which drives many of these changes.
You can no longer invoke .bind, .call or .apply on a function in angular expressions.
This is to disallow changing the behaviour of existing functions
in an unforseen fashion.
in an unforeseen fashion.
- due to [6081f207](https://github.com/angular/angular.js/commit/6081f20769e64a800ee8075c168412b21f026d99),
@@ -877,7 +877,7 @@ of `$sce.trustAsHtml(string)`. When bound to a plain string, the string is sanit
module is not loaded) and the bound expression evaluates to a value that is not trusted an
exception is thrown.
When using this directive you can either include `ngSanitize` in your module's dependencis (See the
When using this directive you can either include `ngSanitize` in your module's dependencies (See the
example at the {@link ngBindHtml} reference) or use the {@link $sce} service to set the value as
trusted.
@@ -1134,10 +1134,10 @@ freely available to JavaScript code (as before).
Angular expressions execute in a limited context. They do not have
direct access to the global scope, `window`, `document` or the Function
constructor. However, they have direct access to names/properties on
the scope chain. It has been a long standing best practice to keep
constructor. However, they have direct access to names/properties on
the scope chain. It has been a long standing best practice to keep
sensitive APIs outside of the scope chain (in a closure or your
controller.) That's easier said that done for two reasons:
controller.) That's easier said than done for two reasons:
1. JavaScript does not have a notion of private properties so if you need
someone on the scope chain for JavaScript use, you also expose it to
+1 -1
View File
@@ -97,7 +97,7 @@ __`app/index.html`.__
...
<!-- jQuery is used for JavaScript animations (include this before angular.js) -->
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/jquery/dist/jquery.js"></script>
...
+18 -18
View File
@@ -2453,17 +2453,6 @@
"grunt-merge-conflict": {
"version": "0.0.2"
},
"grunt-parallel": {
"version": "0.3.1",
"dependencies": {
"q": {
"version": "0.8.12"
},
"lpad": {
"version": "0.1.0"
}
}
},
"grunt-shell": {
"version": "1.1.1",
"dependencies": {
@@ -4810,7 +4799,7 @@
}
},
"protractor": {
"version": "1.4.0",
"version": "1.6.0",
"dependencies": {
"request": {
"version": "2.36.0",
@@ -4828,7 +4817,7 @@
"version": "0.5.2"
},
"node-uuid": {
"version": "1.4.1"
"version": "1.4.2"
},
"tough-cookie": {
"version": "0.12.1",
@@ -4858,16 +4847,16 @@
"version": "0.4.0"
},
"http-signature": {
"version": "0.10.0",
"version": "0.10.1",
"dependencies": {
"assert-plus": {
"version": "0.1.2"
"version": "0.1.5"
},
"asn1": {
"version": "0.1.11"
},
"ctype": {
"version": "0.5.2"
"version": "0.5.3"
}
}
},
@@ -4909,7 +4898,7 @@
"version": "0.6.1"
},
"xmlbuilder": {
"version": "2.4.4",
"version": "2.4.5",
"dependencies": {
"lodash-node": {
"version": "2.4.1"
@@ -4926,6 +4915,17 @@
"jasminewd": {
"version": "1.1.0"
},
"jasminewd2": {
"version": "0.0.2"
},
"jasmine": {
"version": "2.1.1",
"dependencies": {
"jasmine-core": {
"version": "2.1.3"
}
}
},
"saucelabs": {
"version": "0.1.1"
},
@@ -4966,7 +4966,7 @@
"version": "1.0.0"
},
"source-map-support": {
"version": "0.2.8",
"version": "0.2.9",
"dependencies": {
"source-map": {
"version": "0.1.32",
+3 -3
View File
@@ -11,6 +11,7 @@
"bower": "~1.3.9",
"browserstacktunnel-wrapper": "~1.3.1",
"canonical-path": "0.0.2",
"cheerio": "^0.17.0",
"dgeni": "^0.4.0",
"dgeni-packages": "^0.10.0",
"event-stream": "~3.1.0",
@@ -51,7 +52,7 @@
"marked": "~0.3.0",
"node-html-encoder": "0.0.2",
"promises-aplus-tests": "~2.1.0",
"protractor": "1.4.0",
"protractor": "^1.6.0",
"q": "~1.0.0",
"q-io": "^1.10.9",
"qq": "^0.3.5",
@@ -59,8 +60,7 @@
"semver": "~4.0.3",
"shelljs": "~0.3.0",
"sorted-object": "^1.0.0",
"stringmap": "^0.2.2",
"cheerio": "^0.17.0"
"stringmap": "^0.2.2"
},
"licenses": [
{
+1
View File
@@ -16,6 +16,7 @@ if [ $JOB = "unit" ]; then
grunt test:unit --browsers $BROWSERS --reporters dots
grunt ci-checks
grunt tests:docs --browsers $BROWSERS --reporters dots
elif [ $JOB = "docs-e2e" ]; then
grunt test:travis-protractor --specs "docs/app/e2e/**/*.scenario.js"
elif [ $JOB = "e2e" ]; then
if [ $TEST_TARGET = "jquery" ]; then
+4 -4
View File
@@ -489,19 +489,19 @@ var formDirectiveFactory = function(isNgForm) {
alias = controller.$name;
if (alias) {
setter(scope, alias, controller, alias);
setter(scope, null, alias, controller, alias);
attr.$observe(attr.name ? 'name' : 'ngForm', function(newValue) {
if (alias === newValue) return;
setter(scope, alias, undefined, alias);
setter(scope, null, alias, undefined, alias);
alias = newValue;
setter(scope, alias, controller, alias);
setter(scope, null, alias, controller, alias);
parentFormCtrl.$$renameControl(controller, alias);
});
}
formElement.on('$destroy', function() {
parentFormCtrl.$removeControl(controller);
if (alias) {
setter(scope, alias, undefined, alias);
setter(scope, null, alias, undefined, alias);
}
extend(controller, nullFormCtrl); //stop propagating child destruction handlers upwards
});
+7 -1619
View File
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
'use strict';
/**
* @ngdoc directive
* @name ngChange
*
* @description
* Evaluate the given expression when the user changes the input.
* The expression is evaluated immediately, unlike the JavaScript onchange event
* which only triggers at the end of a change (usually, when the user leaves the
* form element or presses the return key).
*
* The `ngChange` expression is only evaluated when a change in the input value causes
* a new value to be committed to the model.
*
* It will not be evaluated:
* * if the value returned from the `$parsers` transformation pipeline has not changed
* * if the input has continued to be invalid since the model will stay `null`
* * if the model is changed programmatically and not by a change to the input value
*
*
* Note, this directive requires `ngModel` to be present.
*
* @element input
* @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
* in input value.
*
* @example
* <example name="ngChange-directive" module="changeExample">
* <file name="index.html">
* <script>
* angular.module('changeExample', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.counter = 0;
* $scope.change = function() {
* $scope.counter++;
* };
* }]);
* </script>
* <div ng-controller="ExampleController">
* <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
* <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
* <label for="ng-change-example2">Confirmed</label><br />
* <tt>debug = {{confirmed}}</tt><br/>
* <tt>counter = {{counter}}</tt><br/>
* </div>
* </file>
* <file name="protractor.js" type="protractor">
* var counter = element(by.binding('counter'));
* var debug = element(by.binding('confirmed'));
*
* it('should evaluate the expression if changing from view', function() {
* expect(counter.getText()).toContain('0');
*
* element(by.id('ng-change-example1')).click();
*
* expect(counter.getText()).toContain('1');
* expect(debug.getText()).toContain('true');
* });
*
* it('should not evaluate the expression if changing from model', function() {
* element(by.id('ng-change-example2')).click();
* expect(counter.getText()).toContain('0');
* expect(debug.getText()).toContain('true');
* });
* </file>
* </example>
*/
var ngChangeDirective = valueFn({
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attr.ngChange);
});
}
});
+1 -1
View File
@@ -49,7 +49,7 @@
<select ng-model="template" ng-options="t.name for t in templates">
<option value="">(blank)</option>
</select>
url of the template: <tt>{{template.url}}</tt>
url of the template: <code>{{template.url}}</code>
<hr/>
<div class="slide-animate-container">
<div class="slide-animate" ng-include="template.url"></div>
+128
View File
@@ -0,0 +1,128 @@
'use strict';
/**
* @ngdoc directive
* @name ngList
*
* @description
* Text input that converts between a delimited string and an array of strings. The default
* delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
* delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
*
* The behaviour of the directive is affected by the use of the `ngTrim` attribute.
* * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
* list item is respected. This implies that the user of the directive is responsible for
* dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
* tab or newline character.
* * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
* when joining the list items back together) and whitespace around each list item is stripped
* before it is added to the model.
*
* ### Example with Validation
*
* <example name="ngList-directive" module="listExample">
* <file name="app.js">
* angular.module('listExample', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.names = ['morpheus', 'neo', 'trinity'];
* }]);
* </file>
* <file name="index.html">
* <form name="myForm" ng-controller="ExampleController">
* List: <input name="namesInput" ng-model="names" ng-list required>
* <span class="error" ng-show="myForm.namesInput.$error.required">
* Required!</span>
* <br>
* <tt>names = {{names}}</tt><br/>
* <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
* <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
* <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
* <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
* </form>
* </file>
* <file name="protractor.js" type="protractor">
* var listInput = element(by.model('names'));
* var names = element(by.exactBinding('names'));
* var valid = element(by.binding('myForm.namesInput.$valid'));
* var error = element(by.css('span.error'));
*
* it('should initialize to model', function() {
* expect(names.getText()).toContain('["morpheus","neo","trinity"]');
* expect(valid.getText()).toContain('true');
* expect(error.getCssValue('display')).toBe('none');
* });
*
* it('should be invalid if empty', function() {
* listInput.clear();
* listInput.sendKeys('');
*
* expect(names.getText()).toContain('');
* expect(valid.getText()).toContain('false');
* expect(error.getCssValue('display')).not.toBe('none');
* });
* </file>
* </example>
*
* ### Example - splitting on whitespace
* <example name="ngList-directive-newlines">
* <file name="index.html">
* <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
* <pre>{{ list | json }}</pre>
* </file>
* <file name="protractor.js" type="protractor">
* it("should split the text by newlines", function() {
* var listInput = element(by.model('list'));
* var output = element(by.binding('list | json'));
* listInput.sendKeys('abc\ndef\nghi');
* expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
* });
* </file>
* </example>
*
* @element input
* @param {string=} ngList optional delimiter that should be used to split the value.
*/
var ngListDirective = function() {
return {
restrict: 'A',
priority: 100,
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
// We want to control whitespace trimming so we use this convoluted approach
// to access the ngList attribute, which doesn't pre-trim the attribute
var ngList = element.attr(attr.$attr.ngList) || ', ';
var trimValues = attr.ngTrim !== 'false';
var separator = trimValues ? trim(ngList) : ngList;
var parse = function(viewValue) {
// If the viewValue is invalid (say required but empty) it will be `undefined`
if (isUndefined(viewValue)) return;
var list = [];
if (viewValue) {
forEach(viewValue.split(separator), function(value) {
if (value) list.push(trimValues ? trim(value) : value);
});
}
return list;
};
ctrl.$parsers.push(parse);
ctrl.$formatters.push(function(value) {
if (isArray(value)) {
return value.join(ngList);
}
return undefined;
});
// Override the standard $isEmpty because an empty array means the input is empty.
ctrl.$isEmpty = function(value) {
return !value || !value.length;
};
}
};
};
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -47,10 +47,10 @@
</example>
*/
var ngStyleDirective = ngDirective(function(scope, element, attr) {
scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) { element.css(style, '');});
}
if (newStyles) element.css(newStyles);
}, true);
});
});
+91
View File
@@ -0,0 +1,91 @@
'use strict';
var requiredDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
attr.required = true; // force truthy in case we are on non input element
ctrl.$validators.required = function(modelValue, viewValue) {
return !attr.required || !ctrl.$isEmpty(viewValue);
};
attr.$observe('required', function() {
ctrl.$validate();
});
}
};
};
var patternDirective = function() {
return {
restrict: 'A',
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 {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = -1;
attr.$observe('maxlength', function(value) {
var intVal = int(value);
maxlength = isNaN(intVal) ? -1 : intVal;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (viewValue.length <= maxlength);
};
}
};
};
var minlengthDirective = function() {
return {
restrict: 'A',
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(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
};
}
};
};
+1 -1
View File
@@ -186,7 +186,7 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
} else if (actualType === 'array') {
} else if (isArray(actual)) {
// In case `actual` is an array, consider it a match
// if ANY of it's items matches `expected`
return actual.some(function(item) {
+8 -7
View File
@@ -664,7 +664,7 @@ Parser.prototype = {
}, {
assign: function(scope, value, locals) {
var o = object(scope, locals);
if (!o) object.assign(scope, o = {});
if (!o) object.assign(scope, o = {}, locals);
return getter.assign(o, value);
}
});
@@ -690,7 +690,7 @@ Parser.prototype = {
var key = ensureSafeMemberName(indexFn(self, locals), expression);
// prevent overwriting of Function.constructor which would break ensureSafeObject check
var o = ensureSafeObject(obj(self, locals), expression);
if (!o) obj.assign(self, o = {});
if (!o) obj.assign(self, o = {}, locals);
return o[key] = value;
}
});
@@ -800,18 +800,19 @@ Parser.prototype = {
// Parser helper functions
//////////////////////////////////////////////////
function setter(obj, path, setValue, fullExp) {
function setter(obj, locals, path, setValue, fullExp) {
ensureSafeObject(obj, fullExp);
ensureSafeObject(locals, fullExp);
var element = path.split('.'), key;
for (var i = 0; element.length > 1; i++) {
key = ensureSafeMemberName(element.shift(), fullExp);
var propertyObj = ensureSafeObject(obj[key], fullExp);
var propertyObj = (i === 0 && locals && locals[key]) || obj[key];
if (!propertyObj) {
propertyObj = {};
obj[key] = propertyObj;
}
obj = propertyObj;
obj = ensureSafeObject(propertyObj, fullExp);
}
key = ensureSafeMemberName(element.shift(), fullExp);
ensureSafeObject(obj[key], fullExp);
@@ -938,8 +939,8 @@ function getterFn(path, options, fullExp) {
}
fn.sharedGetter = true;
fn.assign = function(self, value) {
return setter(self, path, value, path);
fn.assign = function(self, value, locals) {
return setter(self, locals, path, value, path);
};
getterFnCache[path] = fn;
return fn;
+11 -11
View File
@@ -243,31 +243,31 @@ angular.mock.$ExceptionHandlerProvider = function() {
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
* is a bug in the application or test, so this mock will make these tests fail.
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
* is a bug in the application or test, so this mock will make these tests fail.
* For any implementations that expect exceptions to be thrown, the `rethrow` mode
* will also maintain a log of thrown errors.
*/
this.mode = function(mode) {
switch (mode) {
case 'rethrow':
handler = function(e) {
throw e;
};
break;
case 'log':
var errors = [];
switch (mode) {
case 'log':
case 'rethrow':
var errors = [];
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
if (mode === "rethrow") {
throw e;
}
};
handler.errors = errors;
break;
default:
+64
View File
@@ -339,3 +339,67 @@ window.dump = function() {
return angular.mock.dump(arg);
}));
};
function getInputCompileHelper(currentSpec) {
var helper = {};
module(function($compileProvider) {
$compileProvider.directive('attrCapture', function() {
return function(scope, element, $attrs) {
helper.attrs = $attrs;
};
});
});
inject(function($compile, $rootScope, $sniffer) {
helper.compileInput = function(inputHtml, mockValidity, scope) {
scope = helper.scope = scope || $rootScope;
// Create the input element and dealoc when done
helper.inputElm = jqLite(inputHtml);
// Set up mock validation if necessary
if (isObject(mockValidity)) {
VALIDITY_STATE_PROPERTY = 'ngMockValidity';
helper.inputElm.prop(VALIDITY_STATE_PROPERTY, mockValidity);
currentSpec.after(function() {
VALIDITY_STATE_PROPERTY = 'validity';
});
}
// Create the form element and dealoc when done
helper.formElm = jqLite('<form name="form"></form>');
helper.formElm.append(helper.inputElm);
// Compile the lot and return the input element
$compile(helper.formElm)(scope);
spyOn(scope.form, '$addControl').andCallThrough();
spyOn(scope.form, '$$renameControl').andCallThrough();
scope.$digest();
return helper.inputElm;
};
helper.changeInputValueTo = function(value) {
helper.inputElm.val(value);
browserTrigger(helper.inputElm, $sniffer.hasEvent('input') ? 'input' : 'change');
};
helper.changeGivenInputTo = function(inputElm, value) {
inputElm.val(value);
browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change');
};
helper.dealoc = function() {
dealoc(helper.inputElm);
dealoc(helper.formElm);
};
});
return helper;
}
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
'use strict';
/* globals getInputCompileHelper: false */
describe('ngChange', function() {
var helper, $rootScope;
beforeEach(function() {
helper = getInputCompileHelper(this);
});
afterEach(function() {
helper.dealoc();
});
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
it('should $eval expression after new value is set in the model', function() {
helper.compileInput('<input type="text" ng-model="value" ng-change="change()" />');
$rootScope.change = jasmine.createSpy('change').andCallFake(function() {
expect($rootScope.value).toBe('new value');
});
helper.changeInputValueTo('new value');
expect($rootScope.change).toHaveBeenCalledOnce();
});
it('should not $eval the expression if changed from model', function() {
helper.compileInput('<input type="text" ng-model="value" ng-change="change()" />');
$rootScope.change = jasmine.createSpy('change');
$rootScope.$apply('value = true');
expect($rootScope.change).not.toHaveBeenCalled();
});
it('should $eval ngChange expression on checkbox', function() {
var inputElm = helper.compileInput('<input type="checkbox" ng-model="foo" ng-change="changeFn()">');
$rootScope.changeFn = jasmine.createSpy('changeFn');
expect($rootScope.changeFn).not.toHaveBeenCalled();
browserTrigger(inputElm, 'click');
expect($rootScope.changeFn).toHaveBeenCalledOnce();
});
it('should be able to change the model and via that also update the view', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-change="value=\'b\'" />');
helper.changeInputValueTo('a');
expect(inputElm.val()).toBe('b');
});
});
+142
View File
@@ -0,0 +1,142 @@
'use strict';
/* globals getInputCompileHelper: false */
describe('ngList', function() {
var helper, $rootScope;
beforeEach(function() {
helper = getInputCompileHelper(this);
});
afterEach(function() {
helper.dealoc();
});
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
it('should parse text into an array', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="list" ng-list />');
// model -> view
$rootScope.$apply("list = ['x', 'y', 'z']");
expect(inputElm.val()).toBe('x, y, z');
// view -> model
helper.changeInputValueTo('1, 2, 3');
expect($rootScope.list).toEqual(['1', '2', '3']);
});
it("should not clobber text if model changes due to itself", function() {
// When the user types 'a,b' the 'a,' stage parses to ['a'] but if the
// $parseModel function runs it will change to 'a', in essence preventing
// the user from ever typing ','.
var inputElm = helper.compileInput('<input type="text" ng-model="list" ng-list />');
helper.changeInputValueTo('a ');
expect(inputElm.val()).toEqual('a ');
expect($rootScope.list).toEqual(['a']);
helper.changeInputValueTo('a ,');
expect(inputElm.val()).toEqual('a ,');
expect($rootScope.list).toEqual(['a']);
helper.changeInputValueTo('a , ');
expect(inputElm.val()).toEqual('a , ');
expect($rootScope.list).toEqual(['a']);
helper.changeInputValueTo('a , b');
expect(inputElm.val()).toEqual('a , b');
expect($rootScope.list).toEqual(['a', 'b']);
});
it('should convert empty string to an empty array', function() {
helper.compileInput('<input type="text" ng-model="list" ng-list />');
helper.changeInputValueTo('');
expect($rootScope.list).toEqual([]);
});
it('should be invalid if required and empty', function() {
var inputElm = helper.compileInput('<input type="text" ng-list ng-model="list" required>');
helper.changeInputValueTo('');
expect($rootScope.list).toBeUndefined();
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('a,b');
expect($rootScope.list).toEqual(['a','b']);
expect(inputElm).toBeValid();
});
describe('with a custom separator', function() {
it('should split on the custom separator', function() {
helper.compileInput('<input type="text" ng-model="list" ng-list=":" />');
helper.changeInputValueTo('a,a');
expect($rootScope.list).toEqual(['a,a']);
helper.changeInputValueTo('a:b');
expect($rootScope.list).toEqual(['a', 'b']);
});
it("should join the list back together with the custom separator", function() {
var inputElm = helper.compileInput('<input type="text" ng-model="list" ng-list=" : " />');
$rootScope.$apply(function() {
$rootScope.list = ['x', 'y', 'z'];
});
expect(inputElm.val()).toBe('x : y : z');
});
});
describe('(with ngTrim undefined or true)', function() {
it('should ignore separator whitespace when splitting', function() {
helper.compileInput('<input type="text" ng-model="list" ng-list=" | " />');
helper.changeInputValueTo('a|b');
expect($rootScope.list).toEqual(['a', 'b']);
});
it('should trim whitespace from each list item', function() {
helper.compileInput('<input type="text" ng-model="list" ng-list="|" />');
helper.changeInputValueTo('a | b');
expect($rootScope.list).toEqual(['a', 'b']);
});
});
describe('(with ngTrim set to false)', function() {
it('should use separator whitespace when splitting', function() {
helper.compileInput('<input type="text" ng-model="list" ng-trim="false" ng-list=" | " />');
helper.changeInputValueTo('a|b');
expect($rootScope.list).toEqual(['a|b']);
helper.changeInputValueTo('a | b');
expect($rootScope.list).toEqual(['a','b']);
});
it("should not trim whitespace from each list item", function() {
helper.compileInput('<input type="text" ng-model="list" ng-trim="false" ng-list="|" />');
helper.changeInputValueTo('a | b');
expect($rootScope.list).toEqual(['a ',' b']);
});
it("should support splitting on newlines", function() {
helper.compileInput('<textarea type="text" ng-model="list" ng-trim="false" ng-list="&#10;"></textarea');
helper.changeInputValueTo('a\nb');
expect($rootScope.list).toEqual(['a','b']);
});
});
});
File diff suppressed because it is too large Load Diff
+507
View File
@@ -0,0 +1,507 @@
'use strict';
/* globals getInputCompileHelper: false */
describe('validators', function() {
var helper, $rootScope;
beforeEach(function() {
helper = getInputCompileHelper(this);
});
afterEach(function() {
helper.dealoc();
});
beforeEach(inject(function(_$rootScope_) {
$rootScope = _$rootScope_;
}));
describe('pattern', function() {
it('should validate in-lined pattern', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-pattern="/^\\d\\d\\d-\\d\\d-\\d\\d\\d\\d$/" />');
helper.changeInputValueTo('x000-00-0000x');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('000-00-0000');
expect(inputElm).toBeValid();
helper.changeInputValueTo('000-00-0000x');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('123-45-6789');
expect(inputElm).toBeValid();
helper.changeInputValueTo('x');
expect(inputElm).toBeInvalid();
});
it('should listen on ng-pattern when pattern is observed', function() {
var value, patternVal = /^\w+$/;
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-pattern="pat" attr-capture />');
helper.attrs.$observe('pattern', function(v) {
value = helper.attrs.pattern;
});
$rootScope.$apply(function() {
$rootScope.pat = patternVal;
});
expect(value).toBe(patternVal);
});
it('should validate in-lined pattern with modifiers', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-pattern="/^abc?$/i" />');
helper.changeInputValueTo('aB');
expect(inputElm).toBeValid();
helper.changeInputValueTo('xx');
expect(inputElm).toBeInvalid();
});
it('should validate pattern from scope', function() {
$rootScope.regexp = /^\d\d\d-\d\d-\d\d\d\d$/;
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-pattern="regexp" />');
helper.changeInputValueTo('x000-00-0000x');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('000-00-0000');
expect(inputElm).toBeValid();
helper.changeInputValueTo('000-00-0000x');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('123-45-6789');
expect(inputElm).toBeValid();
helper.changeInputValueTo('x');
expect(inputElm).toBeInvalid();
$rootScope.$apply(function() {
$rootScope.regexp = /abc?/;
});
helper.changeInputValueTo('ab');
expect(inputElm).toBeValid();
helper.changeInputValueTo('xx');
expect(inputElm).toBeInvalid();
});
it('should perform validations when the ngPattern scope value changes', function() {
$rootScope.regexp = /^[a-z]+$/;
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-pattern="regexp" />');
helper.changeInputValueTo('abcdef');
expect(inputElm).toBeValid();
helper.changeInputValueTo('123');
expect(inputElm).toBeInvalid();
$rootScope.$apply(function() {
$rootScope.regexp = /^\d+$/;
});
expect(inputElm).toBeValid();
helper.changeInputValueTo('abcdef');
expect(inputElm).toBeInvalid();
$rootScope.$apply(function() {
$rootScope.regexp = '';
});
expect(inputElm).toBeValid();
});
it('should register "pattern" with the model validations when the pattern attribute is used', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" pattern="^\\d+$" />');
helper.changeInputValueTo('abcd');
expect(inputElm).toBeInvalid();
expect($rootScope.form.input.$error.pattern).toBe(true);
helper.changeInputValueTo('12345');
expect(inputElm).toBeValid();
expect($rootScope.form.input.$error.pattern).not.toBe(true);
});
it('should not throw an error when scope pattern can\'t be found', function() {
expect(function() {
var inputElm = helper.compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
$rootScope.$apply("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() {
var inputElm = helper.compileInput('<input type="text" ng-model="foo" ng-pattern="fooRegexp" />');
$rootScope.$apply(function() {
$rootScope.fooRegexp = {};
$rootScope.foo = 'bar';
});
}).toThrowMatching(/^\[ngPattern:noregexp\] Expected fooRegexp to be a RegExp but was/);
});
it('should be invalid if entire string does not match pattern', function() {
var inputElm = helper.compileInput('<input type="text" name="test" ng-model="value" pattern="\\d{4}">');
helper.changeInputValueTo('1234');
expect($rootScope.form.test.$error.pattern).not.toBe(true);
expect(inputElm).toBeValid();
helper.changeInputValueTo('123');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
helper.changeInputValueTo('12345');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
});
it('should be cope with patterns that start with ^', function() {
var inputElm = helper.compileInput('<input type="text" name="test" ng-model="value" pattern="^\\d{4}">');
helper.changeInputValueTo('1234');
expect($rootScope.form.test.$error.pattern).not.toBe(true);
expect(inputElm).toBeValid();
helper.changeInputValueTo('123');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
helper.changeInputValueTo('12345');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
});
it('should be cope with patterns that end with $', function() {
var inputElm = helper.compileInput('<input type="text" name="test" ng-model="value" pattern="\\d{4}$">');
helper.changeInputValueTo('1234');
expect($rootScope.form.test.$error.pattern).not.toBe(true);
expect(inputElm).toBeValid();
helper.changeInputValueTo('123');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
helper.changeInputValueTo('12345');
expect($rootScope.form.test.$error.pattern).toBe(true);
expect(inputElm).not.toBeValid();
});
});
describe('minlength', function() {
it('should invalidate values that are shorter than the given minlength', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-minlength="3" />');
helper.changeInputValueTo('aa');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('aaa');
expect(inputElm).toBeValid();
});
it('should listen on ng-minlength when minlength is observed', function() {
var value = 0;
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-minlength="min" attr-capture />');
helper.attrs.$observe('minlength', function(v) {
value = int(helper.attrs.minlength);
});
$rootScope.$apply('min = 5');
expect(value).toBe(5);
});
it('should observe the standard minlength attribute and register it as a validator on the model', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" minlength="{{ min }}" />');
$rootScope.$apply('min = 10');
helper.changeInputValueTo('12345');
expect(inputElm).toBeInvalid();
expect($rootScope.form.input.$error.minlength).toBe(true);
$rootScope.$apply('min = 5');
expect(inputElm).toBeValid();
expect($rootScope.form.input.$error.minlength).not.toBe(true);
});
it('should validate when the model is initalized as a number', function() {
$rootScope.value = 12345;
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" minlength="3" />');
expect($rootScope.value).toBe(12345);
expect($rootScope.form.input.$error.minlength).toBeUndefined();
});
});
describe('maxlength', function() {
it('should invalidate values that are longer than the given maxlength', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-maxlength="5" />');
helper.changeInputValueTo('aaaaaaaa');
expect(inputElm).toBeInvalid();
helper.changeInputValueTo('aaa');
expect(inputElm).toBeValid();
});
it('should only accept empty values when maxlength is 0', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-maxlength="0" />');
helper.changeInputValueTo('');
expect(inputElm).toBeValid();
helper.changeInputValueTo('a');
expect(inputElm).toBeInvalid();
});
it('should accept values of any length when maxlength is negative', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-maxlength="-1" />');
helper.changeInputValueTo('');
expect(inputElm).toBeValid();
helper.changeInputValueTo('aaaaaaaaaa');
expect(inputElm).toBeValid();
});
it('should accept values of any length when maxlength is non-numeric', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-maxlength="{{maxlength}}" />');
helper.changeInputValueTo('aaaaaaaaaa');
$rootScope.$apply('maxlength = "5"');
expect(inputElm).toBeInvalid();
$rootScope.$apply('maxlength = "abc"');
expect(inputElm).toBeValid();
$rootScope.$apply('maxlength = ""');
expect(inputElm).toBeValid();
$rootScope.$apply('maxlength = null');
expect(inputElm).toBeValid();
$rootScope.someObj = {};
$rootScope.$apply('maxlength = someObj');
expect(inputElm).toBeValid();
});
it('should listen on ng-maxlength when maxlength is observed', function() {
var value = 0;
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-maxlength="max" attr-capture />');
helper.attrs.$observe('maxlength', function(v) {
value = int(helper.attrs.maxlength);
});
$rootScope.$apply('max = 10');
expect(value).toBe(10);
});
it('should observe the standard maxlength attribute and register it as a validator on the model', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="{{ max }}" />');
$rootScope.$apply('max = 1');
helper.changeInputValueTo('12345');
expect(inputElm).toBeInvalid();
expect($rootScope.form.input.$error.maxlength).toBe(true);
$rootScope.$apply('max = 6');
expect(inputElm).toBeValid();
expect($rootScope.form.input.$error.maxlength).not.toBe(true);
});
it('should assign the correct model after an observed validator became valid', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="{{ max }}" />');
$rootScope.$apply('max = 1');
helper.changeInputValueTo('12345');
expect($rootScope.value).toBeUndefined();
$rootScope.$apply('max = 6');
expect($rootScope.value).toBe('12345');
});
it('should assign the correct model after an observed validator became invalid', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="{{ max }}" />');
$rootScope.$apply('max = 6');
helper.changeInputValueTo('12345');
expect($rootScope.value).toBe('12345');
$rootScope.$apply('max = 1');
expect($rootScope.value).toBeUndefined();
});
it('should leave the value as invalid if observed maxlength changed, but is still invalid', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="{{ max }}" />');
$rootScope.$apply('max = 1');
helper.changeInputValueTo('12345');
expect(inputElm).toBeInvalid();
expect($rootScope.form.input.$error.maxlength).toBe(true);
expect($rootScope.value).toBeUndefined();
$rootScope.$apply('max = 3');
expect(inputElm).toBeInvalid();
expect($rootScope.form.input.$error.maxlength).toBe(true);
expect($rootScope.value).toBeUndefined();
});
it('should not notify if observed maxlength changed, but is still invalid', function() {
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" ng-change="ngChangeSpy()" ' +
'maxlength="{{ max }}" />');
$rootScope.$apply('max = 1');
helper.changeInputValueTo('12345');
$rootScope.ngChangeSpy = jasmine.createSpy();
$rootScope.$apply('max = 3');
expect($rootScope.ngChangeSpy).not.toHaveBeenCalled();
});
it('should leave the model untouched when validating before model initialization', function() {
$rootScope.value = '12345';
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" minlength="3" />');
expect($rootScope.value).toBe('12345');
});
it('should validate when the model is initalized as a number', function() {
$rootScope.value = 12345;
var inputElm = helper.compileInput('<input type="text" name="input" ng-model="value" maxlength="10" />');
expect($rootScope.value).toBe(12345);
expect($rootScope.form.input.$error.maxlength).toBeUndefined();
});
});
describe('required', function() {
it('should allow bindings via ngRequired', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" ng-required="required" />');
$rootScope.$apply("required = false");
helper.changeInputValueTo('');
expect(inputElm).toBeValid();
$rootScope.$apply("required = true");
expect(inputElm).toBeInvalid();
$rootScope.$apply("value = 'some'");
expect(inputElm).toBeValid();
helper.changeInputValueTo('');
expect(inputElm).toBeInvalid();
$rootScope.$apply("required = false");
expect(inputElm).toBeValid();
});
it('should invalid initial value with bound required', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" required="{{required}}" />');
$rootScope.$apply('required = true');
expect(inputElm).toBeInvalid();
});
it('should be $invalid but $pristine if not touched', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="name" name="alias" required />');
$rootScope.$apply("name = null");
expect(inputElm).toBeInvalid();
expect(inputElm).toBePristine();
helper.changeInputValueTo('');
expect(inputElm).toBeInvalid();
expect(inputElm).toBeDirty();
});
it('should allow empty string if not required', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="foo" />');
helper.changeInputValueTo('a');
helper.changeInputValueTo('');
expect($rootScope.foo).toBe('');
});
it('should set $invalid when model undefined', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="notDefined" required />');
expect(inputElm).toBeInvalid();
});
it('should consider bad input as an error before any other errors are considered', function() {
var inputElm = helper.compileInput('<input type="text" ng-model="value" required />', { badInput: true });
var ctrl = inputElm.controller('ngModel');
ctrl.$parsers.push(function() {
return undefined;
});
helper.changeInputValueTo('abc123');
expect(ctrl.$error.parse).toBe(true);
expect(inputElm).toHaveClass('ng-invalid-parse');
expect(inputElm).toBeInvalid(); // invalid because of the number validator
});
it('should allow `false` as a valid value when the input type is not "checkbox"', function() {
var inputElm = helper.compileInput('<input type="radio" ng-value="true" ng-model="answer" required />' +
'<input type="radio" ng-value="false" ng-model="answer" required />');
$rootScope.$apply();
expect(inputElm).toBeInvalid();
$rootScope.$apply("answer = true");
expect(inputElm).toBeValid();
$rootScope.$apply("answer = false");
expect(inputElm).toBeValid();
});
});
});
+33
View File
@@ -92,6 +92,39 @@ describe('Filter: filter', function() {
);
it('should match items with array properties containing one or more matching items', function() {
var items, expr;
items = [
{tags: ['web', 'html', 'css', 'js']},
{tags: ['hybrid', 'html', 'css', 'js', 'ios', 'android']},
{tags: ['mobile', 'ios', 'android']}
];
expr = {tags: 'html'};
expect(filter(items, expr).length).toBe(2);
expect(filter(items, expr)).toEqual([items[0], items[1]]);
items = [
{nums: [1, 345, 12]},
{nums: [0, 46, 78]},
{nums: [123, 4, 67]}
];
expr = {nums: 12};
expect(filter(items, expr).length).toBe(2);
expect(filter(items, expr)).toEqual([items[0], items[2]]);
items = [
{customers: [{name: 'John'}, {name: 'Elena'}, {name: 'Bill'}]},
{customers: [{name: 'Sam'}, {name: 'Klara'}, {name: 'Bill'}]},
{customers: [{name: 'Molli'}, {name: 'Elena'}, {name: 'Lora'}]}
];
expr = {customers: {name: 'Bill'}};
expect(filter(items, expr).length).toBe(2);
expect(filter(items, expr)).toEqual([items[0], items[1]]);
}
);
it('should take object as predicate', function() {
var items = [{first: 'misko', last: 'hevery'},
{first: 'adam', last: 'abrons'}];
+70
View File
@@ -488,6 +488,62 @@ describe('parser', function() {
expect(scope.b).toEqual(234);
});
it('should allow use of locals in the left side of an assignment', inject(function($rootScope) {
$rootScope.a = {};
$rootScope.key = "value";
var localA = {};
//getterFn
$rootScope.$eval('a.value = 1', {a: localA});
expect(localA.value).toBe(1);
$rootScope.$eval('w.a.value = 2', {w: {a: localA}});
expect(localA.value).toBe(2);
//field access
$rootScope.$eval('(a).value = 3', {a: localA});
expect(localA.value).toBe(3);
$rootScope.$eval('{c: {b: a}}.c.b.value = 4', {a: localA});
expect(localA.value).toBe(4);
//object index
$rootScope.$eval('a[key] = 5', {a: localA});
expect(localA.value).toBe(5);
$rootScope.$eval('w.a[key] = 6', {w: {a: localA}});
expect(localA.value).toBe(6);
$rootScope.$eval('{c: {b: a}}.c.b[key] = 7', {a: localA});
expect(localA.value).toBe(7);
//Nothing should have touched the $rootScope.a
expect($rootScope.a.value).toBeUndefined();
}));
it('should allow use of locals in sub expressions of the left side of an assignment', inject(function($rootScope, $parse) {
delete $rootScope.x;
$rootScope.$eval('x[a][b] = true', {a: 'foo', b: 'bar'});
expect($rootScope.x.foo.bar).toBe(true);
delete $rootScope.x;
$rootScope.$eval('x.foo[b] = true', {b: 'bar'});
expect($rootScope.x.foo.bar).toBe(true);
delete $rootScope.x;
$rootScope.$eval('x[a].bar = true', {a: 'foo'});
expect($rootScope.x.foo.bar).toBe(true);
}));
it('should ignore locals beyond the root object of an assignment expression', inject(function($rootScope) {
var a = {};
var locals = {a: a};
$rootScope.b = {a: {value: 123}};
$rootScope.$eval('b.a.value = 1', locals);
expect(a.value).toBeUndefined();
expect($rootScope.b.a.value).toBe(1);
}));
it('should evaluate assignments in ternary operator', function() {
scope.$eval('a = 1 ? 2 : 3');
expect(scope.a).toBe(2);
@@ -799,6 +855,12 @@ describe('parser', function() {
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: a.toString.constructor');
expect(function() {
scope.$eval("c.a = 1", {c: Function.prototype.constructor});
}).toThrowMinErr(
'$parse', 'isecfn', 'Referencing Function in Angular expressions is disallowed! ' +
'Expression: c.a');
});
it('should disallow traversing the Function object in a setter: E02', function() {
@@ -933,6 +995,14 @@ describe('parser', function() {
'$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
'Expression: foo["bar"]["keys"](foo)');
});
it('should NOT allow access to Object constructor in assignment locals', function() {
expect(function() {
scope.$eval("O.constructor.a = 1", {O: Object});
}).toThrowMinErr(
'$parse', 'isecobj', 'Referencing Object in Angular expressions is disallowed! ' +
'Expression: O.constructor.a');
});
});
describe('Window and $element/node', function() {
+33 -13
View File
@@ -592,22 +592,42 @@ describe('ngMock', function() {
}));
it('should log exceptions', module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
var $exceptionHandler = $exceptionHandlerProvider.$get();
$exceptionHandler('MyError');
expect($exceptionHandler.errors).toEqual(['MyError']);
it('should log exceptions', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
});
inject(function($exceptionHandler) {
$exceptionHandler('MyError');
expect($exceptionHandler.errors).toEqual(['MyError']);
$exceptionHandler('MyError', 'comment');
expect($exceptionHandler.errors[1]).toEqual(['MyError', 'comment']);
}));
$exceptionHandler('MyError', 'comment');
expect($exceptionHandler.errors[1]).toEqual(['MyError', 'comment']);
});
});
it('should log and rethrow exceptions', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('rethrow');
});
inject(function($exceptionHandler) {
expect(function() { $exceptionHandler('MyError'); }).toThrow('MyError');
expect($exceptionHandler.errors).toEqual(['MyError']);
expect(function() { $exceptionHandler('MyError', 'comment'); }).toThrow('MyError');
expect($exceptionHandler.errors[1]).toEqual(['MyError', 'comment']);
});
});
it('should throw on wrong argument', function() {
module(function($exceptionHandlerProvider) {
expect(function() {
$exceptionHandlerProvider.mode('XXX');
}).toThrow("Unknown mode 'XXX', only 'log'/'rethrow' modes are allowed!");
});
inject(); // Trigger the tests in `module`
});
it('should throw on wrong argument', module(function($exceptionHandlerProvider) {
expect(function() {
$exceptionHandlerProvider.mode('XXX');
}).toThrow("Unknown mode 'XXX', only 'log'/'rethrow' modes are allowed!");
}));
});