Compare commits
67 Commits
v1.3.20
...
v1.4.0-beta.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a7e9de8d8 | |||
| 299b3e7e01 | |||
| 54cae0f1d0 | |||
| 4af7cdaf4d | |||
| 593b18c66a | |||
| f2e1a930aa | |||
| babc20b43d | |||
| ba90261b75 | |||
| fc21db8a15 | |||
| b4bdec35cb | |||
| 933591d69c | |||
| cf9331ac66 | |||
| 02977c5bab | |||
| 9f5ac048d7 | |||
| 408f89d8e6 | |||
| 7fda214c4f | |||
| eb6cb785df | |||
| aa798f1236 | |||
| 5a60302389 | |||
| 034fade3e8 | |||
| e24f22bdb1 | |||
| 371c1e19d8 | |||
| b5e00cf615 | |||
| 5765061652 | |||
| b146cae02c | |||
| 3353afbb59 | |||
| 40cb57c8f6 | |||
| f06f28e018 | |||
| f3b088a4e4 | |||
| ef1a9d2cda | |||
| 9c9c6b3fe4 | |||
| 51d6774286 | |||
| e079111b33 | |||
| e1132f53b0 | |||
| ab4b632dbf | |||
| 1b704071c8 | |||
| 647d93338f | |||
| 1334b8c832 | |||
| d2a9a163fb | |||
| e24d968276 | |||
| a01ce6b81c | |||
| c66b4b6a13 | |||
| 66ceecc295 | |||
| 349742b3f0 | |||
| 2ff7edfdd1 | |||
| 1e5e527c84 | |||
| 1c76bf7e94 | |||
| 6018f5da3f | |||
| 3616b9b07c | |||
| d224fe8172 | |||
| e9bf93d510 | |||
| 2e721a7914 | |||
| 1eb6036d29 | |||
| 4836dacae6 | |||
| 0e2ac3cd70 | |||
| 0f9fd2f642 | |||
| 3e42b22b0e | |||
| deb3cb4dae | |||
| b43fa3bb30 | |||
| 521c12c265 | |||
| 7f5051bb2a | |||
| 25623b709f | |||
| e4f23c4d25 | |||
| 8928d02345 | |||
| 5bb2636aac | |||
| c95e38c603 | |||
| a3c3bf3332 |
@@ -0,0 +1,5 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# JS files must always use LF for tools to work
|
||||
*.js eol=lf
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License
|
||||
|
||||
Copyright (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
Copyright (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
Vendored
+6
-1
@@ -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,7 +62,10 @@ 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/ngOptions.js',
|
||||
'src/ng/directive/ngPluralize.js',
|
||||
'src/ng/directive/ngRepeat.js',
|
||||
'src/ng/directive/ngShowHide.js',
|
||||
@@ -70,7 +74,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': [
|
||||
|
||||
Executable
+95
@@ -0,0 +1,95 @@
|
||||
"use strict";
|
||||
|
||||
/* globals angular, benchmarkSteps */
|
||||
|
||||
var app = angular.module('ngOptionsBenchmark', []);
|
||||
|
||||
app.config(function($compileProvider) {
|
||||
if ($compileProvider.debugInfoEnabled) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
app.controller('DataController', function($scope, $element) {
|
||||
$scope.items = [];
|
||||
$scope.count = 10000;
|
||||
|
||||
function changeOptions() {
|
||||
$scope.items = [];
|
||||
for (var i = 0; i < $scope.count; ++i) {
|
||||
$scope.items.push({
|
||||
id: i,
|
||||
label: 'item-' + i,
|
||||
group: 'group-' + i % 100
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var selectElement = $element.find('select');
|
||||
console.log(selectElement);
|
||||
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'add-options',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.count = 10000;
|
||||
changeOptions();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'set-model-1',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.x = $scope.items[1000];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'set-model-2',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.x = $scope.items[10];
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'remove-options',
|
||||
fn: function() {
|
||||
$scope.count = 100;
|
||||
changeOptions();
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'add-options',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
$scope.count = 10000;
|
||||
changeOptions();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'set-view-1',
|
||||
fn: function() {
|
||||
selectElement.val('2000');
|
||||
selectElement.triggerHandler('change');
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'set-view-2',
|
||||
fn: function() {
|
||||
selectElement.val('1000');
|
||||
selectElement.triggerHandler('change');
|
||||
}
|
||||
});
|
||||
});
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
scripts: [ {
|
||||
id: 'angular',
|
||||
src: '/build/angular.js'
|
||||
},
|
||||
{
|
||||
src: 'app.js',
|
||||
}]
|
||||
});
|
||||
};
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<div ng-app="ngOptionsBenchmark" ng-cloak>
|
||||
<div ng-controller="DataController">
|
||||
<div class="container-fluid">
|
||||
<p>
|
||||
Tests the execution of ng-options for rendering during model and option updates.
|
||||
</p>
|
||||
<select ng-model="x" ng-options="a as a.label group by a.group for a in items track by a.id"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,7 +220,7 @@
|
||||
<p class="pull-right"><a back-to-top>Back to top</a></p>
|
||||
|
||||
<p>
|
||||
Super-powered by Google ©2010-2014
|
||||
Super-powered by Google ©2010-2015
|
||||
( <a id="version"
|
||||
ng-href="https://github.com/angular/angular.js/blob/master/CHANGELOG.md#{{versionNumber}}"
|
||||
ng-bind-template="v{{version}}">
|
||||
|
||||
@@ -35,7 +35,7 @@ URL of the subcontext:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<base href="/subapp">
|
||||
<base href="/subapp/">
|
||||
...
|
||||
</head>
|
||||
```
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -197,14 +197,14 @@ Then Angular applies configuration blocks in the same order they were registered
|
||||
## Run Blocks
|
||||
|
||||
Run blocks are the closest thing in Angular to the main method. A run block is the code which
|
||||
needs to run to kickstart the application. It is executed after all of the service have been
|
||||
needs to run to kickstart the application. It is executed after all of the services have been
|
||||
configured and the injector has been created. Run blocks typically contain code which is hard
|
||||
to unit-test, and for this reason should be declared in isolated modules, so that they can be
|
||||
ignored in the unit-tests.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Modules can list other modules as their dependencies. Depending on a module implies that required
|
||||
Modules can list other modules as their dependencies. Depending on a module implies that the required
|
||||
module needs to be loaded before the requiring module is loaded. In other words the configuration
|
||||
blocks of the required modules execute before the configuration blocks of the requiring module.
|
||||
The same is true for the run blocks. Each module can only be loaded once, even if multiple other
|
||||
|
||||
@@ -33,7 +33,7 @@ templating systems.
|
||||
### Do I need to worry about security holes in AngularJS?
|
||||
|
||||
Like any other technology, AngularJS is not impervious to attack. Angular does, however, provide
|
||||
built-in protection from basic security holes including cross-site scripting and HTML injection
|
||||
built-in protection from basic security holes, including cross-site scripting and HTML injection
|
||||
attacks. AngularJS does round-trip escaping on all strings for you and even offers XSRF protection
|
||||
for server-side communication.
|
||||
|
||||
@@ -52,7 +52,7 @@ Yes. See instructions in {@link downloading}.
|
||||
|
||||
We run our extensive test suite against the following browsers: Safari, Chrome, Firefox, Opera 15,
|
||||
IE9 and mobile browsers (Android, Chrome Mobile, iOS Safari). See {@link guide/ie Internet
|
||||
Explorer Compatibility} for more details in supporting legacy IE browsers.
|
||||
Explorer Compatibility} for more details on supporting legacy IE browsers.
|
||||
|
||||
|
||||
### What's Angular's performance like?
|
||||
@@ -61,8 +61,8 @@ The startup time heavily depends on your network connection, state of the cache,
|
||||
available hardware, but typically we measure bootstrap time in tens or hundreds of milliseconds.
|
||||
|
||||
The runtime performance will vary depending on the number and complexity of bindings on the page
|
||||
as well as the speed of your backend (for apps that fetch data from the backend). Just for an
|
||||
illustration we typically build snappy apps with hundreds or thousands of active bindings.
|
||||
as well as the speed of your backend (for apps that fetch data from the backend). For an
|
||||
illustration, we typically build snappy apps with hundreds or thousands of active bindings.
|
||||
|
||||
|
||||
### How big is the angular.js file that I need to include?
|
||||
@@ -88,7 +88,7 @@ but we don't guarantee that.
|
||||
|
||||
### What is testability like in Angular?
|
||||
|
||||
Very testable and designed this way from ground up. It has an integrated dependency injection
|
||||
Very testable and designed this way from the ground up. It has an integrated dependency injection
|
||||
framework, provides mocks for many heavy dependencies (server-side communication). See
|
||||
{@link ngMock} for details.
|
||||
|
||||
@@ -189,7 +189,7 @@ Then whenever a value on a scope changes, all `$watch`es observing that element
|
||||
|
||||
Sometimes, usually when you're writing a custom directive, you will have to define your own `$watch` on a scope value to make the directive react to changes.
|
||||
|
||||
On the flip side, sometimes you change a scope value in some code but the app doesn't react to it.
|
||||
On the flip side, sometimes you change a scope value in some code, but the app doesn't react to it.
|
||||
Angular checks for scope variable changes after pieces of your code have finished running; for example, when `ng-click` calls a function on your scope, Angular will check for changes and react.
|
||||
However, some code is outside of Angular and you'll have to call `scope.$apply()` yourself to trigger the update.
|
||||
This is most commonly seen in event handlers in custom directives.
|
||||
|
||||
@@ -11,7 +11,7 @@ multiple views by adding routing, using an Angular module called 'ngRoute'.
|
||||
|
||||
* When you now navigate to `app/index.html`, you are redirected to `app/index.html/#/phones`
|
||||
and the phone list appears in the browser.
|
||||
* When you click on a phone link the url changes to one specific to that phone and the stub of a
|
||||
* When you click on a phone link, the url changes to that specific phone and the stub of a
|
||||
phone detail page is displayed.
|
||||
|
||||
<div doc-tutorial-reset="7"></div>
|
||||
|
||||
@@ -31,7 +31,7 @@ phonecatControllers.controller('PhoneDetailCtrl', ['$scope', '$routeParams', '$h
|
||||
|
||||
$scope.setImage = function(imageUrl) {
|
||||
$scope.mainImageUrl = imageUrl;
|
||||
}
|
||||
};
|
||||
}]);
|
||||
```
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
...
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ var getTaggedVersion = function() {
|
||||
if ( version && semver.satisfies(version, currentPackage.branchVersion)) {
|
||||
version.codeName = getCodeName(tag);
|
||||
version.full = version.version;
|
||||
version.branch = 'v' + currentPackage.branchVersion.replace('*', 'x');
|
||||
version.branch = 'v' + currentPackage.branchPattern.replace('*', 'x');
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -130,13 +130,17 @@ var getCdnVersion = function() {
|
||||
return semver.satisfies(tag, currentPackage.branchVersion);
|
||||
})
|
||||
.reverse()
|
||||
.tap(function(versions) {
|
||||
console.log(versions);
|
||||
})
|
||||
.reduce(function(cdnVersion, version) {
|
||||
if (!cdnVersion) {
|
||||
// Note: need to use shell.exec and curl here
|
||||
// as version-infos returns its result synchronously...
|
||||
var cdnResult = shell.exec('curl http://ajax.googleapis.com/ajax/libs/angularjs/'+version+'/angular.min.js '+
|
||||
'--head --write-out "%{http_code}" -o /dev/null -silent',
|
||||
{silent: true});
|
||||
{silent: false});
|
||||
console.log('http://ajax.googleapis.com/ajax/libs/angularjs/'+version+'/angular.min.js');
|
||||
if ( cdnResult.code === 0 ) {
|
||||
var statusCode = cdnResult.output.trim();
|
||||
if (statusCode === '200') {
|
||||
@@ -161,7 +165,7 @@ var getSnapshotVersion = function() {
|
||||
|
||||
if ( !version ) {
|
||||
// a snapshot version before the first tag on the branch
|
||||
version = semver(currentPackage.branchVersion.replace('*','0-beta.1'));
|
||||
version = semver(currentPackage.branchPattern.replace('*','0-beta.1'));
|
||||
}
|
||||
|
||||
// We need to clone to ensure that we are not modifying another version
|
||||
|
||||
Generated
+18
-18
@@ -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",
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "angularjs",
|
||||
"branchVersion": "1.3.*",
|
||||
"branchVersion": "^1.4.0-beta.0",
|
||||
"branchPattern": "1.4.*",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/angular/angular.js.git"
|
||||
@@ -11,6 +12,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 +53,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 +61,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": [
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ ARG_DEFS=(
|
||||
)
|
||||
|
||||
function checkVersionNumber() {
|
||||
BRANCH_PATTERN=$(readJsonProp "package.json" "branchVersion")
|
||||
BRANCH_PATTERN=$(readJsonProp "package.json" "branchPattern")
|
||||
if [[ $VERSION_NUMBER != $BRANCH_PATTERN ]]; then
|
||||
echo "version-number needs to match $BRANCH_PATTERN on this branch"
|
||||
usage
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@
|
||||
"nextUid": false,
|
||||
"setHashKey": false,
|
||||
"extend": false,
|
||||
"int": false,
|
||||
"toInt": false,
|
||||
"inherit": false,
|
||||
"noop": false,
|
||||
"identity": false,
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@
|
||||
nextUid: true,
|
||||
setHashKey: true,
|
||||
extend: true,
|
||||
int: true,
|
||||
toInt: true,
|
||||
inherit: true,
|
||||
noop: true,
|
||||
identity: true,
|
||||
@@ -357,7 +357,7 @@ function extend(dst) {
|
||||
return dst;
|
||||
}
|
||||
|
||||
function int(str) {
|
||||
function toInt(str) {
|
||||
return parseInt(str, 10);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license AngularJS v"NG_VERSION_FULL"
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, document, undefined) {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license AngularJS v"NG_VERSION_FULL"
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
+18
-13
@@ -33,28 +33,33 @@
|
||||
function minErr(module, ErrorConstructor) {
|
||||
ErrorConstructor = ErrorConstructor || Error;
|
||||
return function() {
|
||||
var code = arguments[0],
|
||||
prefix = '[' + (module ? module + ':' : '') + code + '] ',
|
||||
template = arguments[1],
|
||||
templateArgs = arguments,
|
||||
var SKIP_INDEXES = 2;
|
||||
|
||||
message, i;
|
||||
var templateArgs = arguments,
|
||||
code = templateArgs[0],
|
||||
message = '[' + (module ? module + ':' : '') + code + '] ',
|
||||
template = templateArgs[1],
|
||||
paramPrefix, i;
|
||||
|
||||
message = prefix + template.replace(/\{\d+\}/g, function(match) {
|
||||
var index = +match.slice(1, -1), arg;
|
||||
message += template.replace(/\{\d+\}/g, function(match) {
|
||||
var index = +match.slice(1, -1),
|
||||
shiftedIndex = index + SKIP_INDEXES;
|
||||
|
||||
if (index + 2 < templateArgs.length) {
|
||||
return toDebugString(templateArgs[index + 2]);
|
||||
if (shiftedIndex < templateArgs.length) {
|
||||
return toDebugString(templateArgs[shiftedIndex]);
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
|
||||
message = message + '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
|
||||
message += '\nhttp://errors.angularjs.org/"NG_VERSION_FULL"/' +
|
||||
(module ? module + '/' : '') + code;
|
||||
for (i = 2; i < arguments.length; i++) {
|
||||
message = message + (i == 2 ? '?' : '&') + 'p' + (i - 2) + '=' +
|
||||
encodeURIComponent(toDebugString(arguments[i]));
|
||||
|
||||
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
|
||||
message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
|
||||
encodeURIComponent(toDebugString(templateArgs[i]));
|
||||
}
|
||||
|
||||
return new ErrorConstructor(message);
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license AngularJS v"NG_VERSION_FULL"
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {
|
||||
|
||||
@@ -159,13 +159,13 @@ function $CacheFactoryProvider() {
|
||||
* @returns {*} the value stored.
|
||||
*/
|
||||
put: function(key, value) {
|
||||
if (isUndefined(value)) return;
|
||||
if (capacity < Number.MAX_VALUE) {
|
||||
var lruEntry = lruHash[key] || (lruHash[key] = {key: key});
|
||||
|
||||
refresh(lruEntry);
|
||||
}
|
||||
|
||||
if (isUndefined(value)) return;
|
||||
if (!(key in data)) size++;
|
||||
data[key] = value;
|
||||
|
||||
|
||||
+1
-1
@@ -477,7 +477,7 @@
|
||||
*
|
||||
* <div class="alert alert-info">
|
||||
* **Best Practice**: if you intend to add and remove transcluded content manually in your directive
|
||||
* (by calling the transclude function to get the DOM and and calling `element.remove()` to remove it),
|
||||
* (by calling the transclude function to get the DOM and calling `element.remove()` to remove it),
|
||||
* then you are also responsible for calling `$destroy` on the transclusion scope.
|
||||
* </div>
|
||||
*
|
||||
|
||||
@@ -341,22 +341,34 @@
|
||||
|
||||
var ngAttributeAliasDirectives = {};
|
||||
|
||||
|
||||
// boolean attrs are evaluated
|
||||
forEach(BOOLEAN_ATTR, function(propName, attrName) {
|
||||
// binding to multiple is not supported
|
||||
if (propName == "multiple") return;
|
||||
|
||||
function defaultLinkFn(scope, element, attr) {
|
||||
scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
|
||||
attr.$set(attrName, !!value);
|
||||
});
|
||||
}
|
||||
|
||||
var normalized = directiveNormalize('ng-' + attrName);
|
||||
var linkFn = defaultLinkFn;
|
||||
|
||||
if (propName === 'checked') {
|
||||
linkFn = function(scope, element, attr) {
|
||||
// ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
|
||||
if (attr.ngModel !== attr[normalized]) {
|
||||
defaultLinkFn(scope, element, attr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ngAttributeAliasDirectives[normalized] = function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
priority: 100,
|
||||
link: function(scope, element, attr) {
|
||||
scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
|
||||
attr.$set(attrName, !!value);
|
||||
});
|
||||
}
|
||||
link: linkFn
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
+8
-1620
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -97,7 +97,7 @@ function classDirective(name, selector) {
|
||||
|
||||
function arrayClasses(classVal) {
|
||||
if (isArray(classVal)) {
|
||||
return classVal;
|
||||
return classVal.join(' ').split(' ');
|
||||
} else if (isString(classVal)) {
|
||||
return classVal.split(' ');
|
||||
} else if (isObject(classVal)) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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=" " 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
@@ -0,0 +1,612 @@
|
||||
'use strict';
|
||||
|
||||
/* global jqLiteRemove */
|
||||
|
||||
var ngOptionsMinErr = minErr('ngOptions');
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name ngOptions
|
||||
* @restrict A
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
|
||||
* elements for the `<select>` element using the array or object obtained by evaluating the
|
||||
* `ngOptions` comprehension expression.
|
||||
*
|
||||
* In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
|
||||
* similar result. However, `ngOptions` provides some benefits such as reducing memory and
|
||||
* increasing speed by not creating a new scope for each repeated instance, as well as providing
|
||||
* more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
|
||||
* comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
|
||||
* to a non-string value. This is because an option element can only be bound to string values at
|
||||
* present.
|
||||
*
|
||||
* When an item in the `<select>` menu is selected, the array element or object property
|
||||
* represented by the selected option will be bound to the model identified by the `ngModel`
|
||||
* directive.
|
||||
*
|
||||
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
|
||||
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
|
||||
* option. See example below for demonstration.
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* **Note:** `ngModel` compares by reference, not value. This is important when binding to an
|
||||
* array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
|
||||
* </div>
|
||||
*
|
||||
* ## `select` **`as`**
|
||||
*
|
||||
* Using `select` **`as`** will bind the result of the `select` expression to the model, but
|
||||
* the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
|
||||
* or property name (for object data sources) of the value within the collection. If a **`track by`** expression
|
||||
* is used, the result of that expression will be set as the value of the `option` and `select` elements.
|
||||
*
|
||||
*
|
||||
* ### `select` **`as`** and **`track by`**
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
|
||||
* </div>
|
||||
*
|
||||
* Consider the following example:
|
||||
*
|
||||
* ```html
|
||||
* <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* $scope.values = [{
|
||||
* id: 1,
|
||||
* label: 'aLabel',
|
||||
* subItem: { name: 'aSubItem' }
|
||||
* }, {
|
||||
* id: 2,
|
||||
* label: 'bLabel',
|
||||
* subItem: { name: 'bSubItem' }
|
||||
* }];
|
||||
*
|
||||
* $scope.selected = { name: 'aSubItem' };
|
||||
* ```
|
||||
*
|
||||
* With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
|
||||
* of the data source (to `item` in this example). To calculate whether an element is selected, we do the
|
||||
* following:
|
||||
*
|
||||
* 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
|
||||
* 2. Apply **`track by`** to the already selected value in `ngModel`.
|
||||
* In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
|
||||
* value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
|
||||
* a wrong object, the selected element can't be found, `<select>` is always reset to the "not
|
||||
* selected" option.
|
||||
*
|
||||
*
|
||||
* @param {string} ngModel Assignable angular expression to data-bind to.
|
||||
* @param {string=} name Property name of the form under which the control is published.
|
||||
* @param {string=} required The control is considered valid only if value is entered.
|
||||
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
||||
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
||||
* `required` when you want to data-bind to the `required` attribute.
|
||||
* @param {comprehension_expression=} ngOptions in one of the following forms:
|
||||
*
|
||||
* * for array data sources:
|
||||
* * `label` **`for`** `value` **`in`** `array`
|
||||
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
|
||||
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
|
||||
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
|
||||
* * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
|
||||
* (for including a filter with `track by`)
|
||||
* * for object data sources:
|
||||
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
* * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
|
||||
* * `select` **`as`** `label` **`group by`** `group`
|
||||
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
|
||||
*
|
||||
* Where:
|
||||
*
|
||||
* * `array` / `object`: an expression which evaluates to an array / object to iterate over.
|
||||
* * `value`: local variable which will refer to each item in the `array` or each property value
|
||||
* of `object` during iteration.
|
||||
* * `key`: local variable which will refer to a property name in `object` during iteration.
|
||||
* * `label`: The result of this expression will be the label for `<option>` element. The
|
||||
* `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
|
||||
* * `select`: The result of this expression will be bound to the model of the parent `<select>`
|
||||
* element. If not specified, `select` expression will default to `value`.
|
||||
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
|
||||
* DOM element.
|
||||
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
|
||||
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
|
||||
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
|
||||
* even when the options are recreated (e.g. reloaded from the server).
|
||||
*
|
||||
* @example
|
||||
<example module="selectExample">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('selectExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.colors = [
|
||||
{name:'black', shade:'dark'},
|
||||
{name:'white', shade:'light'},
|
||||
{name:'red', shade:'dark'},
|
||||
{name:'blue', shade:'dark'},
|
||||
{name:'yellow', shade:'light'}
|
||||
];
|
||||
$scope.myColor = $scope.colors[2]; // red
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
<ul>
|
||||
<li ng-repeat="color in colors">
|
||||
Name: <input ng-model="color.name">
|
||||
[<a href ng-click="colors.splice($index, 1)">X</a>]
|
||||
</li>
|
||||
<li>
|
||||
[<a href ng-click="colors.push({})">add</a>]
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
Color (null not allowed):
|
||||
<select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
|
||||
|
||||
Color (null allowed):
|
||||
<span class="nullable">
|
||||
<select ng-model="myColor" ng-options="color.name for color in colors">
|
||||
<option value="">-- choose color --</option>
|
||||
</select>
|
||||
</span><br/>
|
||||
|
||||
Color grouped by shade:
|
||||
<select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
|
||||
</select><br/>
|
||||
|
||||
|
||||
Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
|
||||
<hr/>
|
||||
Currently selected: {{ {selected_color:myColor} }}
|
||||
<div style="border:solid 1px black; height:20px"
|
||||
ng-style="{'background-color':myColor.name}">
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should check ng-options', function() {
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
|
||||
element.all(by.model('myColor')).first().click();
|
||||
element.all(by.css('select[ng-model="myColor"] option')).first().click();
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
|
||||
element(by.css('.nullable select[ng-model="myColor"]')).click();
|
||||
element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
// jshint maxlen: false
|
||||
//000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
|
||||
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
|
||||
// 1: value expression (valueFn)
|
||||
// 2: label expression (displayFn)
|
||||
// 3: group by expression (groupByFn)
|
||||
// 4: array item variable name
|
||||
// 5: object item key variable name
|
||||
// 6: object item value variable name
|
||||
// 7: collection expression
|
||||
// 8: track by expression
|
||||
// jshint maxlen: 100
|
||||
|
||||
|
||||
var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
|
||||
function parseOptionsExpression(optionsExp, selectElement, scope) {
|
||||
|
||||
var match = optionsExp.match(NG_OPTIONS_REGEXP);
|
||||
if (!(match)) {
|
||||
throw ngOptionsMinErr('iexp',
|
||||
"Expected expression in form of " +
|
||||
"'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
|
||||
" but got '{0}'. Element: {1}",
|
||||
optionsExp, startingTag(selectElement));
|
||||
}
|
||||
|
||||
// Extract the parts from the ngOptions expression
|
||||
|
||||
// The variable name for the value of the item in the collection
|
||||
var valueName = match[4] || match[6];
|
||||
// The variable name for the key of the item in the collection
|
||||
var keyName = match[5];
|
||||
|
||||
// An expression that generates the viewValue for an option if there is a label expression
|
||||
var selectAs = / as /.test(match[0]) && match[1];
|
||||
// An expression that is used to track the id of each object in the options collection
|
||||
var trackBy = match[8];
|
||||
// An expression that generates the viewValue for an option if there is no label expression
|
||||
var valueFn = $parse(match[2] ? match[1] : valueName);
|
||||
var selectAsFn = selectAs && $parse(selectAs);
|
||||
var viewValueFn = selectAsFn || valueFn;
|
||||
var trackByFn = trackBy && $parse(trackBy);
|
||||
|
||||
// Get the value by which we are going to track the option
|
||||
// if we have a trackFn then use that (passing scope and locals)
|
||||
// otherwise just hash the given viewValue
|
||||
var getTrackByValue = trackBy ?
|
||||
function(viewValue, locals) { return trackByFn(scope, locals); } :
|
||||
function getHashOfValue(viewValue) { return hashKey(viewValue); };
|
||||
var displayFn = $parse(match[2] || match[1]);
|
||||
var groupByFn = $parse(match[3] || '');
|
||||
var valuesFn = $parse(match[7]);
|
||||
|
||||
var locals = {};
|
||||
var getLocals = keyName ? function(value, key) {
|
||||
locals[keyName] = key;
|
||||
locals[valueName] = value;
|
||||
return locals;
|
||||
} : function(value) {
|
||||
locals[valueName] = value;
|
||||
return locals;
|
||||
};
|
||||
|
||||
|
||||
function Option(selectValue, viewValue, label, group) {
|
||||
this.selectValue = selectValue;
|
||||
this.viewValue = viewValue;
|
||||
this.label = label;
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
return {
|
||||
getWatchables: $parse(valuesFn, function(values) {
|
||||
// Create a collection of things that we would like to watch (watchedArray)
|
||||
// so that they can all be watched using a single $watchCollection
|
||||
// that only runs the handler once if anything changes
|
||||
var watchedArray = [];
|
||||
values = values || [];
|
||||
|
||||
Object.keys(values).forEach(function getWatchable(key) {
|
||||
var locals = getLocals(values[key], key);
|
||||
var label = displayFn(scope, locals);
|
||||
var selectValue = getTrackByValue(values[key], locals);
|
||||
watchedArray.push(selectValue);
|
||||
watchedArray.push(label);
|
||||
});
|
||||
return watchedArray;
|
||||
}),
|
||||
|
||||
getOptions: function() {
|
||||
|
||||
var optionItems = [];
|
||||
var selectValueMap = {};
|
||||
|
||||
// The option values were already computed in the `getWatchables` fn,
|
||||
// which must have been called to trigger `getOptions`
|
||||
var optionValues = valuesFn(scope) || [];
|
||||
|
||||
var keys = Object.keys(optionValues);
|
||||
keys.forEach(function getOption(key) {
|
||||
|
||||
// Ignore "angular" properties that start with $ or $$
|
||||
if (key.charAt(0) === '$') return;
|
||||
|
||||
var value = optionValues[key];
|
||||
var locals = getLocals(value, key);
|
||||
var viewValue = viewValueFn(scope, locals);
|
||||
var selectValue = getTrackByValue(viewValue, locals);
|
||||
var label = displayFn(scope, locals);
|
||||
var group = groupByFn(scope, locals);
|
||||
var optionItem = new Option(selectValue, viewValue, label, group);
|
||||
|
||||
optionItems.push(optionItem);
|
||||
selectValueMap[selectValue] = optionItem;
|
||||
});
|
||||
|
||||
return {
|
||||
items: optionItems,
|
||||
selectValueMap: selectValueMap,
|
||||
getOptionFromViewValue: function(value) {
|
||||
return selectValueMap[getTrackByValue(value, getLocals(value))];
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// we can't just jqLite('<option>') since jqLite is not smart enough
|
||||
// to create it in <select> and IE barfs otherwise.
|
||||
var optionTemplate = document.createElement('option'),
|
||||
optGroupTemplate = document.createElement('optgroup');
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
terminal: true,
|
||||
require: ['select', '?ngModel'],
|
||||
link: function(scope, selectElement, attr, ctrls) {
|
||||
|
||||
// if ngModel is not defined, we don't need to do anything
|
||||
var ngModelCtrl = ctrls[1];
|
||||
if (!ngModelCtrl) return;
|
||||
|
||||
var selectCtrl = ctrls[0];
|
||||
var multiple = attr.multiple;
|
||||
|
||||
var emptyOption = selectCtrl.emptyOption;
|
||||
var providedEmptyOption = !!emptyOption;
|
||||
|
||||
var unknownOption = jqLite(optionTemplate.cloneNode(false));
|
||||
unknownOption.val('?');
|
||||
|
||||
var options;
|
||||
var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
|
||||
|
||||
|
||||
var renderEmptyOption = function() {
|
||||
if (!providedEmptyOption) {
|
||||
selectElement.prepend(emptyOption);
|
||||
}
|
||||
selectElement.val('');
|
||||
emptyOption.prop('selected', true); // needed for IE
|
||||
emptyOption.attr('selected', true);
|
||||
};
|
||||
|
||||
var removeEmptyOption = function() {
|
||||
if (!providedEmptyOption) {
|
||||
emptyOption.remove();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var renderUnknownOption = function() {
|
||||
selectElement.prepend(unknownOption);
|
||||
selectElement.val('?');
|
||||
unknownOption.prop('selected', true); // needed for IE
|
||||
unknownOption.attr('selected', true);
|
||||
};
|
||||
|
||||
var removeUnknownOption = function() {
|
||||
unknownOption.remove();
|
||||
};
|
||||
|
||||
|
||||
selectCtrl.writeValue = function writeNgOptionsValue(value) {
|
||||
var option = options.getOptionFromViewValue(value);
|
||||
|
||||
if (option) {
|
||||
if (selectElement[0].value !== option.selectValue) {
|
||||
removeUnknownOption();
|
||||
removeEmptyOption();
|
||||
|
||||
selectElement[0].value = option.selectValue;
|
||||
option.element.selected = true;
|
||||
option.element.setAttribute('selected', 'selected');
|
||||
}
|
||||
} else {
|
||||
if (value === null || providedEmptyOption) {
|
||||
removeUnknownOption();
|
||||
renderEmptyOption();
|
||||
} else {
|
||||
removeEmptyOption();
|
||||
renderUnknownOption();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
selectCtrl.readValue = function readNgOptionsValue() {
|
||||
|
||||
var selectedOption = options.selectValueMap[selectElement.val()];
|
||||
|
||||
if (selectedOption) {
|
||||
removeEmptyOption();
|
||||
removeUnknownOption();
|
||||
return selectedOption.viewValue;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
// Update the controller methods for multiple selectable options
|
||||
if (multiple) {
|
||||
|
||||
ngModelCtrl.$isEmpty = function(value) {
|
||||
return !value || value.length === 0;
|
||||
};
|
||||
|
||||
|
||||
selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
|
||||
options.items.forEach(function(option) {
|
||||
option.element.selected = false;
|
||||
});
|
||||
|
||||
if (value) {
|
||||
value.forEach(function(item) {
|
||||
var option = options.getOptionFromViewValue(item);
|
||||
if (option) option.element.selected = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
selectCtrl.readValue = function readNgOptionsMultiple() {
|
||||
var selectedValues = selectElement.val() || [];
|
||||
return selectedValues.map(function(selectedKey) {
|
||||
var option = options.selectValueMap[selectedKey];
|
||||
return option.viewValue;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (providedEmptyOption) {
|
||||
|
||||
// we need to remove it before calling selectElement.empty() because otherwise IE will
|
||||
// remove the label from the element. wtf?
|
||||
emptyOption.remove();
|
||||
|
||||
// compile the element since there might be bindings in it
|
||||
$compile(emptyOption)(scope);
|
||||
|
||||
// remove the class, which is added automatically because we recompile the element and it
|
||||
// becomes the compilation root
|
||||
emptyOption.removeClass('ng-scope');
|
||||
} else {
|
||||
emptyOption = jqLite(optionTemplate.cloneNode(false));
|
||||
}
|
||||
|
||||
// We need to do this here to ensure that the options object is defined
|
||||
// when we first hit it in writeNgOptionsValue
|
||||
updateOptions();
|
||||
|
||||
// We will re-render the option elements if the option values or labels change
|
||||
scope.$watchCollection(ngOptions.getWatchables, updateOptions);
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
|
||||
function updateOptionElement(option, element) {
|
||||
option.element = element;
|
||||
if (option.value !== element.value) element.value = option.selectValue;
|
||||
if (option.label !== element.label) {
|
||||
element.label = option.label;
|
||||
element.textContent = option.label;
|
||||
}
|
||||
}
|
||||
|
||||
function addOrReuseElement(parent, current, type, templateElement) {
|
||||
var element;
|
||||
// Check whether we can reuse the next element
|
||||
if (current && lowercase(current.nodeName) === type) {
|
||||
// The next element is the right type so reuse it
|
||||
element = current;
|
||||
} else {
|
||||
// The next element is not the right type so create a new one
|
||||
element = templateElement.cloneNode(false);
|
||||
if (!current) {
|
||||
// There are no more elements so just append it to the select
|
||||
parent.appendChild(element);
|
||||
} else {
|
||||
// The next element is not a group so insert the new one
|
||||
parent.insertBefore(element, current);
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
function removeExcessElements(current) {
|
||||
var next;
|
||||
while (current) {
|
||||
next = current.nextSibling;
|
||||
jqLiteRemove(current);
|
||||
current = next;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function skipEmptyAndUnknownOptions(current) {
|
||||
var emptyOption_ = emptyOption && emptyOption[0];
|
||||
var unknownOption_ = unknownOption && unknownOption[0];
|
||||
|
||||
if (emptyOption_ || unknownOption_) {
|
||||
while (current &&
|
||||
(current === emptyOption_ ||
|
||||
current === unknownOption_)) {
|
||||
current = current.nextSibling;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
|
||||
function updateOptions() {
|
||||
|
||||
var previousValue = options && selectCtrl.readValue();
|
||||
|
||||
options = ngOptions.getOptions();
|
||||
|
||||
var groupMap = {};
|
||||
var currentElement = selectElement[0].firstChild;
|
||||
|
||||
// Ensure that the empty option is always there if it was explicitly provided
|
||||
if (providedEmptyOption) {
|
||||
selectElement.prepend(emptyOption);
|
||||
}
|
||||
|
||||
currentElement = skipEmptyAndUnknownOptions(currentElement);
|
||||
|
||||
options.items.forEach(function updateOption(option) {
|
||||
var group;
|
||||
var groupElement;
|
||||
var optionElement;
|
||||
|
||||
if (option.group) {
|
||||
|
||||
// This option is to live in a group
|
||||
// See if we have already created this group
|
||||
group = groupMap[option.group];
|
||||
|
||||
if (!group) {
|
||||
|
||||
// We have not already created this group
|
||||
groupElement = addOrReuseElement(selectElement[0],
|
||||
currentElement,
|
||||
'optgroup',
|
||||
optGroupTemplate);
|
||||
// Move to the next element
|
||||
currentElement = groupElement.nextSibling;
|
||||
|
||||
// Update the label on the group element
|
||||
groupElement.label = option.group;
|
||||
|
||||
// Store it for use later
|
||||
group = groupMap[option.group] = {
|
||||
groupElement: groupElement,
|
||||
currentOptionElement: groupElement.firstChild
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// So now we have a group for this option we add the option to the group
|
||||
optionElement = addOrReuseElement(group.groupElement,
|
||||
group.currentOptionElement,
|
||||
'option',
|
||||
optionTemplate);
|
||||
updateOptionElement(option, optionElement);
|
||||
// Move to the next element
|
||||
group.currentOptionElement = optionElement.nextSibling;
|
||||
|
||||
} else {
|
||||
|
||||
// This option is not in a group
|
||||
optionElement = addOrReuseElement(selectElement[0],
|
||||
currentElement,
|
||||
'option',
|
||||
optionTemplate);
|
||||
updateOptionElement(option, optionElement);
|
||||
// Move to the next element
|
||||
currentElement = optionElement.nextSibling;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Now remove all excess options and group
|
||||
Object.keys(groupMap).forEach(function(key) {
|
||||
removeExcessElements(groupMap[key].currentOptionElement);
|
||||
});
|
||||
removeExcessElements(currentElement);
|
||||
|
||||
ngModelCtrl.$render();
|
||||
|
||||
// Check to see if the value has changed due to the update to the options
|
||||
if (!ngModelCtrl.$isEmpty(previousValue)) {
|
||||
var nextValue = selectCtrl.readValue();
|
||||
if (!equals(previousValue, nextValue)) {
|
||||
ngModelCtrl.$setViewValue(nextValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
}];
|
||||
@@ -54,6 +54,9 @@
|
||||
* <span ng-non-bindable>`{{personCount}}`</span>. The closed braces `{}` is a placeholder
|
||||
* for <span ng-non-bindable>{{numberExpression}}</span>.
|
||||
*
|
||||
* If no rule is defined for a category, then an empty string is displayed and a warning is generated.
|
||||
* Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
|
||||
*
|
||||
* # Configuring ngPluralize with offset
|
||||
* The `offset` attribute allows further customization of pluralized text, which can result in
|
||||
* a better user experience. For example, instead of the message "4 people are viewing this document",
|
||||
@@ -172,7 +175,7 @@
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
|
||||
var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) {
|
||||
var BRACE = /{}/g,
|
||||
IS_WHEN = /^when(Minus)?(.+)$/;
|
||||
|
||||
@@ -216,7 +219,14 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
|
||||
// In JS `NaN !== NaN`, so we have to exlicitly check.
|
||||
if ((count !== lastCount) && !(countIsNaN && isNaN(lastCount))) {
|
||||
watchRemover();
|
||||
watchRemover = scope.$watch(whensExpFns[count], updateElementText);
|
||||
var whenExpFn = whensExpFns[count];
|
||||
if (isUndefined(whenExpFn)) {
|
||||
$log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
|
||||
watchRemover = noop;
|
||||
updateElementText();
|
||||
} else {
|
||||
watchRemover = scope.$watch(whenExpFn, updateElementText);
|
||||
}
|
||||
lastCount = count;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+201
-691
@@ -1,6 +1,111 @@
|
||||
'use strict';
|
||||
|
||||
var ngOptionsMinErr = minErr('ngOptions');
|
||||
var noopNgModelController = { $setViewValue: noop, $render: noop };
|
||||
|
||||
/**
|
||||
* @ngdoc type
|
||||
* @name select.SelectController
|
||||
* @description
|
||||
* The controller for the `<select>` directive. This provides support for reading
|
||||
* and writing the selected value(s) of the control and also coordinates dynamically
|
||||
* added `<option>` elements, perhaps by an `ngRepeat` directive.
|
||||
*/
|
||||
var SelectController =
|
||||
['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
|
||||
|
||||
var self = this,
|
||||
optionsMap = new HashMap();
|
||||
|
||||
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
|
||||
self.ngModelCtrl = noopNgModelController;
|
||||
|
||||
// The "unknown" option is one that is prepended to the list if the viewValue
|
||||
// does not match any of the options. When it is rendered the value of the unknown
|
||||
// option is '? XXX ?' where XXX is the hashKey of the value that is not known.
|
||||
//
|
||||
// We can't just jqLite('<option>') since jqLite is not smart enough
|
||||
// to create it in <select> and IE barfs otherwise.
|
||||
self.unknownOption = jqLite(document.createElement('option'));
|
||||
self.renderUnknownOption = function(val) {
|
||||
var unknownVal = '? ' + hashKey(val) + ' ?';
|
||||
self.unknownOption.val(unknownVal);
|
||||
$element.prepend(self.unknownOption);
|
||||
$element.val(unknownVal);
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
// disable unknown option so that we don't do work when the whole select is being destroyed
|
||||
self.renderUnknownOption = noop;
|
||||
});
|
||||
|
||||
self.removeUnknownOption = function() {
|
||||
if (self.unknownOption.parent()) self.unknownOption.remove();
|
||||
};
|
||||
|
||||
// Here we find the option that represents the "empty" value, i.e. the option with a value
|
||||
// of `""`. This option needs to be accessed (to select it directly) when setting the value
|
||||
// of the select to `""` because IE9 will not automatically select the option.
|
||||
//
|
||||
// Additionally, the `ngOptions` directive uses this option to allow the application developer
|
||||
// to provide their own custom "empty" option when the viewValue does not match any of the
|
||||
// option values.
|
||||
for (var i = 0, children = $element.children(), ii = children.length; i < ii; i++) {
|
||||
if (children[i].value === '') {
|
||||
self.emptyOption = children.eq(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the value of the select control, the implementation of this changes depending
|
||||
// upon whether the select can have multiple values and whether ngOptions is at work.
|
||||
self.readValue = function readSingleValue() {
|
||||
self.removeUnknownOption();
|
||||
return $element.val();
|
||||
};
|
||||
|
||||
|
||||
// Write the value to the select control, the implementation of this changes depending
|
||||
// upon whether the select can have multiple values and whether ngOptions is at work.
|
||||
self.writeValue = function writeSingleValue(value) {
|
||||
if (self.hasOption(value)) {
|
||||
self.removeUnknownOption();
|
||||
$element.val(value);
|
||||
if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
|
||||
} else {
|
||||
if (isUndefined(value) && self.emptyOption) {
|
||||
$element.val('');
|
||||
} else {
|
||||
self.renderUnknownOption(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Tell the select control that an option, with the given value, has been added
|
||||
self.addOption = function(value) {
|
||||
assertNotHasOwnProperty(value, '"option value"');
|
||||
var count = optionsMap.get(value) || 0;
|
||||
optionsMap.put(value, count + 1);
|
||||
};
|
||||
|
||||
// Tell the select control that an option, with the given value, has been removed
|
||||
self.removeOption = function(value) {
|
||||
var count = optionsMap.get(value);
|
||||
if (count) {
|
||||
if (count === 1) {
|
||||
optionsMap.remove(value);
|
||||
} else {
|
||||
optionsMap.put(value, count - 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check whether the select control has an option matching the given value
|
||||
self.hasOption = function(value) {
|
||||
return !!optionsMap.get(value);
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
* @name select
|
||||
@@ -9,12 +114,6 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* @description
|
||||
* HTML `SELECT` element with angular data-binding.
|
||||
*
|
||||
* # `ngOptions`
|
||||
*
|
||||
* The `ngOptions` attribute can be used to dynamically generate a list of `<option>`
|
||||
* elements for the `<select>` element using the array or object obtained by evaluating the
|
||||
* `ngOptions` comprehension expression.
|
||||
*
|
||||
* In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
|
||||
* similar result. However, `ngOptions` provides some benefits such as reducing memory and
|
||||
* increasing speed by not creating a new scope for each repeated instance, as well as providing
|
||||
@@ -27,6 +126,9 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* represented by the selected option will be bound to the model identified by the `ngModel`
|
||||
* directive.
|
||||
*
|
||||
* If the viewValue contains a value that doesn't match any of the options then the control
|
||||
* will automatically add an "unknown" option, which it then removes when this is resolved.
|
||||
*
|
||||
* Optionally, a single hard-coded `<option>` element, with the value set to an empty string, can
|
||||
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
|
||||
* option. See example below for demonstration.
|
||||
@@ -36,307 +138,61 @@ var ngOptionsMinErr = minErr('ngOptions');
|
||||
* array of objects. See an example [in this jsfiddle](http://jsfiddle.net/qWzTb/).
|
||||
* </div>
|
||||
*
|
||||
* ## `select` **`as`**
|
||||
*
|
||||
* Using `select` **`as`** will bind the result of the `select` expression to the model, but
|
||||
* the value of the `<select>` and `<option>` html elements will be either the index (for array data sources)
|
||||
* or property name (for object data sources) of the value within the collection. If a **`track by`** expression
|
||||
* is used, the result of that expression will be set as the value of the `option` and `select` elements.
|
||||
*
|
||||
*
|
||||
* ### `select` **`as`** and **`track by`**
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.
|
||||
* </div>
|
||||
*
|
||||
* Consider the following example:
|
||||
*
|
||||
* ```html
|
||||
* <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
|
||||
* ```
|
||||
*
|
||||
* ```js
|
||||
* $scope.values = [{
|
||||
* id: 1,
|
||||
* label: 'aLabel',
|
||||
* subItem: { name: 'aSubItem' }
|
||||
* }, {
|
||||
* id: 2,
|
||||
* label: 'bLabel',
|
||||
* subItem: { name: 'bSubItem' }
|
||||
* }];
|
||||
*
|
||||
* $scope.selected = { name: 'aSubItem' };
|
||||
* ```
|
||||
*
|
||||
* With the purpose of preserving the selection, the **`track by`** expression is always applied to the element
|
||||
* of the data source (to `item` in this example). To calculate whether an element is selected, we do the
|
||||
* following:
|
||||
*
|
||||
* 1. Apply **`track by`** to the elements in the array. In the example: `[1, 2]`
|
||||
* 2. Apply **`track by`** to the already selected value in `ngModel`.
|
||||
* In the example: this is not possible as **`track by`** refers to `item.id`, but the selected
|
||||
* value from `ngModel` is `{name: 'aSubItem'}`, so the **`track by`** expression is applied to
|
||||
* a wrong object, the selected element can't be found, `<select>` is always reset to the "not
|
||||
* selected" option.
|
||||
*
|
||||
*
|
||||
* @param {string} ngModel Assignable angular expression to data-bind to.
|
||||
* @param {string=} name Property name of the form under which the control is published.
|
||||
* @param {string=} required The control is considered valid only if value is entered.
|
||||
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
||||
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
||||
* `required` when you want to data-bind to the `required` attribute.
|
||||
* @param {comprehension_expression=} ngOptions in one of the following forms:
|
||||
*
|
||||
* * for array data sources:
|
||||
* * `label` **`for`** `value` **`in`** `array`
|
||||
* * `select` **`as`** `label` **`for`** `value` **`in`** `array`
|
||||
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
|
||||
* * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
|
||||
* * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
|
||||
* (for including a filter with `track by`)
|
||||
* * for object data sources:
|
||||
* * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
* * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
|
||||
* * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
|
||||
* * `select` **`as`** `label` **`group by`** `group`
|
||||
* **`for` `(`**`key`**`,`** `value`**`) in`** `object`
|
||||
*
|
||||
* Where:
|
||||
*
|
||||
* * `array` / `object`: an expression which evaluates to an array / object to iterate over.
|
||||
* * `value`: local variable which will refer to each item in the `array` or each property value
|
||||
* of `object` during iteration.
|
||||
* * `key`: local variable which will refer to a property name in `object` during iteration.
|
||||
* * `label`: The result of this expression will be the label for `<option>` element. The
|
||||
* `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
|
||||
* * `select`: The result of this expression will be bound to the model of the parent `<select>`
|
||||
* element. If not specified, `select` expression will default to `value`.
|
||||
* * `group`: The result of this expression will be used to group options using the `<optgroup>`
|
||||
* DOM element.
|
||||
* * `trackexpr`: Used when working with an array of objects. The result of this expression will be
|
||||
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
|
||||
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
|
||||
* even when the options are recreated (e.g. reloaded from the server).
|
||||
*
|
||||
* @example
|
||||
<example module="selectExample">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('selectExample', [])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.colors = [
|
||||
{name:'black', shade:'dark'},
|
||||
{name:'white', shade:'light'},
|
||||
{name:'red', shade:'dark'},
|
||||
{name:'blue', shade:'dark'},
|
||||
{name:'yellow', shade:'light'}
|
||||
];
|
||||
$scope.myColor = $scope.colors[2]; // red
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
<ul>
|
||||
<li ng-repeat="color in colors">
|
||||
Name: <input ng-model="color.name">
|
||||
[<a href ng-click="colors.splice($index, 1)">X</a>]
|
||||
</li>
|
||||
<li>
|
||||
[<a href ng-click="colors.push({})">add</a>]
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
Color (null not allowed):
|
||||
<select ng-model="myColor" ng-options="color.name for color in colors"></select><br>
|
||||
|
||||
Color (null allowed):
|
||||
<span class="nullable">
|
||||
<select ng-model="myColor" ng-options="color.name for color in colors">
|
||||
<option value="">-- choose color --</option>
|
||||
</select>
|
||||
</span><br/>
|
||||
|
||||
Color grouped by shade:
|
||||
<select ng-model="myColor" ng-options="color.name group by color.shade for color in colors">
|
||||
</select><br/>
|
||||
|
||||
|
||||
Select <a href ng-click="myColor = { name:'not in list', shade: 'other' }">bogus</a>.<br>
|
||||
<hr/>
|
||||
Currently selected: {{ {selected_color:myColor} }}
|
||||
<div style="border:solid 1px black; height:20px"
|
||||
ng-style="{'background-color':myColor.name}">
|
||||
</div>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should check ng-options', function() {
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
|
||||
element.all(by.model('myColor')).first().click();
|
||||
element.all(by.css('select[ng-model="myColor"] option')).first().click();
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
|
||||
element(by.css('.nullable select[ng-model="myColor"]')).click();
|
||||
element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
|
||||
expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
|
||||
var ngOptionsDirective = valueFn({
|
||||
restrict: 'A',
|
||||
terminal: true
|
||||
});
|
||||
|
||||
// jshint maxlen: false
|
||||
var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
//000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
|
||||
var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
|
||||
nullModelCtrl = {$setViewValue: noop};
|
||||
// jshint maxlen: 100
|
||||
var selectDirective = function() {
|
||||
var lastView;
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
require: ['select', '?ngModel'],
|
||||
controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
|
||||
var self = this,
|
||||
optionsMap = {},
|
||||
ngModelCtrl = nullModelCtrl,
|
||||
nullOption,
|
||||
unknownOption;
|
||||
|
||||
|
||||
self.databound = $attrs.ngModel;
|
||||
|
||||
|
||||
self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
|
||||
ngModelCtrl = ngModelCtrl_;
|
||||
nullOption = nullOption_;
|
||||
unknownOption = unknownOption_;
|
||||
};
|
||||
|
||||
|
||||
self.addOption = function(value, element) {
|
||||
assertNotHasOwnProperty(value, '"option value"');
|
||||
optionsMap[value] = true;
|
||||
|
||||
if (ngModelCtrl.$viewValue == value) {
|
||||
$element.val(value);
|
||||
if (unknownOption.parent()) unknownOption.remove();
|
||||
}
|
||||
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
|
||||
// Adding an <option selected="selected"> element to a <select required="required"> should
|
||||
// automatically select the new element
|
||||
if (element && element[0].hasAttribute('selected')) {
|
||||
element[0].selected = true;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
self.removeOption = function(value) {
|
||||
if (this.hasOption(value)) {
|
||||
delete optionsMap[value];
|
||||
if (ngModelCtrl.$viewValue === value) {
|
||||
this.renderUnknownOption(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
self.renderUnknownOption = function(val) {
|
||||
var unknownVal = '? ' + hashKey(val) + ' ?';
|
||||
unknownOption.val(unknownVal);
|
||||
$element.prepend(unknownOption);
|
||||
$element.val(unknownVal);
|
||||
unknownOption.prop('selected', true); // needed for IE
|
||||
};
|
||||
|
||||
|
||||
self.hasOption = function(value) {
|
||||
return optionsMap.hasOwnProperty(value);
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
// disable unknown option so that we don't do work when the whole select is being destroyed
|
||||
self.renderUnknownOption = noop;
|
||||
});
|
||||
}],
|
||||
|
||||
controller: SelectController,
|
||||
link: function(scope, element, attr, ctrls) {
|
||||
|
||||
// if ngModel is not defined, we don't need to do anything
|
||||
if (!ctrls[1]) return;
|
||||
var ngModelCtrl = ctrls[1];
|
||||
if (!ngModelCtrl) return;
|
||||
|
||||
var selectCtrl = ctrls[0],
|
||||
ngModelCtrl = ctrls[1],
|
||||
multiple = attr.multiple,
|
||||
optionsExp = attr.ngOptions,
|
||||
nullOption = false, // if false, user will not be able to select it (used by ngOptions)
|
||||
emptyOption,
|
||||
renderScheduled = false,
|
||||
// we can't just jqLite('<option>') since jqLite is not smart enough
|
||||
// to create it in <select> and IE barfs otherwise.
|
||||
optionTemplate = jqLite(document.createElement('option')),
|
||||
optGroupTemplate =jqLite(document.createElement('optgroup')),
|
||||
unknownOption = optionTemplate.clone();
|
||||
var selectCtrl = ctrls[0];
|
||||
|
||||
// find "null" option
|
||||
for (var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
|
||||
if (children[i].value === '') {
|
||||
emptyOption = nullOption = children.eq(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectCtrl.ngModelCtrl = ngModelCtrl;
|
||||
|
||||
selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
|
||||
// We delegate rendering to the `writeValue` method, which can be changed
|
||||
// if the select can have multiple selected values or if the options are being
|
||||
// generated by `ngOptions`
|
||||
ngModelCtrl.$render = function() {
|
||||
selectCtrl.writeValue(ngModelCtrl.$viewValue);
|
||||
};
|
||||
|
||||
// required validator
|
||||
if (multiple) {
|
||||
ngModelCtrl.$isEmpty = function(value) {
|
||||
return !value || value.length === 0;
|
||||
};
|
||||
}
|
||||
|
||||
if (optionsExp) setupAsOptions(scope, element, ngModelCtrl);
|
||||
else if (multiple) setupAsMultiple(scope, element, ngModelCtrl);
|
||||
else setupAsSingle(scope, element, ngModelCtrl, selectCtrl);
|
||||
|
||||
|
||||
////////////////////////////
|
||||
|
||||
|
||||
|
||||
function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) {
|
||||
ngModelCtrl.$render = function() {
|
||||
var viewValue = ngModelCtrl.$viewValue;
|
||||
|
||||
if (selectCtrl.hasOption(viewValue)) {
|
||||
if (unknownOption.parent()) unknownOption.remove();
|
||||
selectElement.val(viewValue);
|
||||
if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
|
||||
} else {
|
||||
if (isUndefined(viewValue) && emptyOption) {
|
||||
selectElement.val('');
|
||||
} else {
|
||||
selectCtrl.renderUnknownOption(viewValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
selectElement.on('change', function() {
|
||||
scope.$apply(function() {
|
||||
if (unknownOption.parent()) unknownOption.remove();
|
||||
ngModelCtrl.$setViewValue(selectElement.val());
|
||||
});
|
||||
// When the selected item(s) changes we delegate getting the value of the select control
|
||||
// to the `readValue` method, which can be changed if the select can have multiple
|
||||
// selected values or if the options are being generated by `ngOptions`
|
||||
element.on('change', function() {
|
||||
scope.$apply(function() {
|
||||
ngModelCtrl.$setViewValue(selectCtrl.readValue());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function setupAsMultiple(scope, selectElement, ctrl) {
|
||||
var lastView;
|
||||
ctrl.$render = function() {
|
||||
var items = new HashMap(ctrl.$viewValue);
|
||||
forEach(selectElement.find('option'), function(option) {
|
||||
// If the select allows multiple values then we need to modify how we read and write
|
||||
// values from and to the control; also what it means for the value to be empty and
|
||||
// we have to add an extra watch since ngModel doesn't work well with arrays - it
|
||||
// doesn't trigger rendering if only an item in the array changes.
|
||||
if (attr.multiple) {
|
||||
|
||||
// Read value now needs to check each option to see if it is selected
|
||||
selectCtrl.readValue = function readMultipleValue() {
|
||||
var array = [];
|
||||
forEach(element.find('option'), function(option) {
|
||||
if (option.selected) {
|
||||
array.push(option.value);
|
||||
}
|
||||
});
|
||||
return array;
|
||||
};
|
||||
|
||||
// Write value now needs to set the selected property of each matching option
|
||||
selectCtrl.writeValue = function writeMultipleValue(value) {
|
||||
var items = new HashMap(value);
|
||||
forEach(element.find('option'), function(option) {
|
||||
option.selected = isDefined(items.get(option.value));
|
||||
});
|
||||
};
|
||||
@@ -344,400 +200,45 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
|
||||
// we have to do it on each watch since ngModel watches reference, but
|
||||
// we need to work of an array, so we need to see if anything was inserted/removed
|
||||
scope.$watch(function selectMultipleWatch() {
|
||||
if (!equals(lastView, ctrl.$viewValue)) {
|
||||
lastView = shallowCopy(ctrl.$viewValue);
|
||||
ctrl.$render();
|
||||
if (!equals(lastView, ngModelCtrl.$viewValue)) {
|
||||
lastView = shallowCopy(ngModelCtrl.$viewValue);
|
||||
ngModelCtrl.$render();
|
||||
}
|
||||
});
|
||||
|
||||
selectElement.on('change', function() {
|
||||
scope.$apply(function() {
|
||||
var array = [];
|
||||
forEach(selectElement.find('option'), function(option) {
|
||||
if (option.selected) {
|
||||
array.push(option.value);
|
||||
}
|
||||
});
|
||||
ctrl.$setViewValue(array);
|
||||
});
|
||||
});
|
||||
}
|
||||
// If we are a multiple select then value is now a collection
|
||||
// so the meaning of $isEmpty changes
|
||||
ngModelCtrl.$isEmpty = function(value) {
|
||||
return !value || value.length === 0;
|
||||
};
|
||||
|
||||
function setupAsOptions(scope, selectElement, ctrl) {
|
||||
var match;
|
||||
|
||||
if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) {
|
||||
throw ngOptionsMinErr('iexp',
|
||||
"Expected expression in form of " +
|
||||
"'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
|
||||
" but got '{0}'. Element: {1}",
|
||||
optionsExp, startingTag(selectElement));
|
||||
}
|
||||
|
||||
var displayFn = $parse(match[2] || match[1]),
|
||||
valueName = match[4] || match[6],
|
||||
selectAs = / as /.test(match[0]) && match[1],
|
||||
selectAsFn = selectAs ? $parse(selectAs) : null,
|
||||
keyName = match[5],
|
||||
groupByFn = $parse(match[3] || ''),
|
||||
valueFn = $parse(match[2] ? match[1] : valueName),
|
||||
valuesFn = $parse(match[7]),
|
||||
track = match[8],
|
||||
trackFn = track ? $parse(match[8]) : null,
|
||||
trackKeysCache = {},
|
||||
// This is an array of array of existing option groups in DOM.
|
||||
// We try to reuse these if possible
|
||||
// - optionGroupsCache[0] is the options with no option group
|
||||
// - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
|
||||
optionGroupsCache = [[{element: selectElement, label:''}]],
|
||||
//re-usable object to represent option's locals
|
||||
locals = {};
|
||||
|
||||
if (nullOption) {
|
||||
// compile the element since there might be bindings in it
|
||||
$compile(nullOption)(scope);
|
||||
|
||||
// remove the class, which is added automatically because we recompile the element and it
|
||||
// becomes the compilation root
|
||||
nullOption.removeClass('ng-scope');
|
||||
|
||||
// we need to remove it before calling selectElement.empty() because otherwise IE will
|
||||
// remove the label from the element. wtf?
|
||||
nullOption.remove();
|
||||
}
|
||||
|
||||
// clear contents, we'll add what's needed based on the model
|
||||
selectElement.empty();
|
||||
|
||||
selectElement.on('change', selectionChanged);
|
||||
|
||||
ctrl.$render = render;
|
||||
|
||||
scope.$watchCollection(valuesFn, scheduleRendering);
|
||||
scope.$watchCollection(getLabels, scheduleRendering);
|
||||
|
||||
if (multiple) {
|
||||
scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
function callExpression(exprFn, key, value) {
|
||||
locals[valueName] = value;
|
||||
if (keyName) locals[keyName] = key;
|
||||
return exprFn(scope, locals);
|
||||
}
|
||||
|
||||
function selectionChanged() {
|
||||
scope.$apply(function() {
|
||||
var collection = valuesFn(scope) || [];
|
||||
var viewValue;
|
||||
if (multiple) {
|
||||
viewValue = [];
|
||||
forEach(selectElement.val(), function(selectedKey) {
|
||||
selectedKey = trackFn ? trackKeysCache[selectedKey] : selectedKey;
|
||||
viewValue.push(getViewValue(selectedKey, collection[selectedKey]));
|
||||
});
|
||||
} else {
|
||||
var selectedKey = trackFn ? trackKeysCache[selectElement.val()] : selectElement.val();
|
||||
viewValue = getViewValue(selectedKey, collection[selectedKey]);
|
||||
}
|
||||
ctrl.$setViewValue(viewValue);
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function getViewValue(key, value) {
|
||||
if (key === '?') {
|
||||
return undefined;
|
||||
} else if (key === '') {
|
||||
return null;
|
||||
} else {
|
||||
var viewValueFn = selectAsFn ? selectAsFn : valueFn;
|
||||
return callExpression(viewValueFn, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function getLabels() {
|
||||
var values = valuesFn(scope);
|
||||
var toDisplay;
|
||||
if (values && isArray(values)) {
|
||||
toDisplay = new Array(values.length);
|
||||
for (var i = 0, ii = values.length; i < ii; i++) {
|
||||
toDisplay[i] = callExpression(displayFn, i, values[i]);
|
||||
}
|
||||
return toDisplay;
|
||||
} else if (values) {
|
||||
// TODO: Add a test for this case
|
||||
toDisplay = {};
|
||||
for (var prop in values) {
|
||||
if (values.hasOwnProperty(prop)) {
|
||||
toDisplay[prop] = callExpression(displayFn, prop, values[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return toDisplay;
|
||||
}
|
||||
|
||||
function createIsSelectedFn(viewValue) {
|
||||
var selectedSet;
|
||||
if (multiple) {
|
||||
if (trackFn && isArray(viewValue)) {
|
||||
|
||||
selectedSet = new HashMap([]);
|
||||
for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) {
|
||||
// tracking by key
|
||||
selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true);
|
||||
}
|
||||
} else {
|
||||
selectedSet = new HashMap(viewValue);
|
||||
}
|
||||
} else if (trackFn) {
|
||||
viewValue = callExpression(trackFn, null, viewValue);
|
||||
}
|
||||
|
||||
return function isSelected(key, value) {
|
||||
var compareValueFn;
|
||||
if (trackFn) {
|
||||
compareValueFn = trackFn;
|
||||
} else if (selectAsFn) {
|
||||
compareValueFn = selectAsFn;
|
||||
} else {
|
||||
compareValueFn = valueFn;
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value)));
|
||||
} else {
|
||||
return viewValue === callExpression(compareValueFn, key, value);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleRendering() {
|
||||
if (!renderScheduled) {
|
||||
scope.$$postDigest(render);
|
||||
renderScheduled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A new labelMap is created with each render.
|
||||
* This function is called for each existing option with added=false,
|
||||
* and each new option with added=true.
|
||||
* - Labels that are passed to this method twice,
|
||||
* (once with added=true and once with added=false) will end up with a value of 0, and
|
||||
* will cause no change to happen to the corresponding option.
|
||||
* - Labels that are passed to this method only once with added=false will end up with a
|
||||
* value of -1 and will eventually be passed to selectCtrl.removeOption()
|
||||
* - Labels that are passed to this method only once with added=true will end up with a
|
||||
* value of 1 and will eventually be passed to selectCtrl.addOption()
|
||||
*/
|
||||
function updateLabelMap(labelMap, label, added) {
|
||||
labelMap[label] = labelMap[label] || 0;
|
||||
labelMap[label] += (added ? 1 : -1);
|
||||
}
|
||||
|
||||
function render() {
|
||||
renderScheduled = false;
|
||||
|
||||
// Temporary location for the option groups before we render them
|
||||
var optionGroups = {'':[]},
|
||||
optionGroupNames = [''],
|
||||
optionGroupName,
|
||||
optionGroup,
|
||||
option,
|
||||
existingParent, existingOptions, existingOption,
|
||||
viewValue = ctrl.$viewValue,
|
||||
values = valuesFn(scope) || [],
|
||||
keys = keyName ? sortedKeys(values) : values,
|
||||
key,
|
||||
value,
|
||||
groupLength, length,
|
||||
groupIndex, index,
|
||||
labelMap = {},
|
||||
selected,
|
||||
isSelected = createIsSelectedFn(viewValue),
|
||||
anySelected = false,
|
||||
lastElement,
|
||||
element,
|
||||
label,
|
||||
optionId;
|
||||
|
||||
trackKeysCache = {};
|
||||
|
||||
// We now build up the list of options we need (we merge later)
|
||||
for (index = 0; length = keys.length, index < length; index++) {
|
||||
key = index;
|
||||
if (keyName) {
|
||||
key = keys[index];
|
||||
if (key.charAt(0) === '$') continue;
|
||||
}
|
||||
value = values[key];
|
||||
|
||||
optionGroupName = callExpression(groupByFn, key, value) || '';
|
||||
if (!(optionGroup = optionGroups[optionGroupName])) {
|
||||
optionGroup = optionGroups[optionGroupName] = [];
|
||||
optionGroupNames.push(optionGroupName);
|
||||
}
|
||||
|
||||
selected = isSelected(key, value);
|
||||
anySelected = anySelected || selected;
|
||||
|
||||
label = callExpression(displayFn, key, value); // what will be seen by the user
|
||||
|
||||
// doing displayFn(scope, locals) || '' overwrites zero values
|
||||
label = isDefined(label) ? label : '';
|
||||
optionId = trackFn ? trackFn(scope, locals) : (keyName ? keys[index] : index);
|
||||
if (trackFn) {
|
||||
trackKeysCache[optionId] = key;
|
||||
}
|
||||
|
||||
optionGroup.push({
|
||||
// either the index into array or key from object
|
||||
id: optionId,
|
||||
label: label,
|
||||
selected: selected // determine if we should be selected
|
||||
});
|
||||
}
|
||||
if (!multiple) {
|
||||
if (nullOption || viewValue === null) {
|
||||
// insert null option if we have a placeholder, or the model is null
|
||||
optionGroups[''].unshift({id:'', label:'', selected:!anySelected});
|
||||
} else if (!anySelected) {
|
||||
// option could not be found, we have to insert the undefined item
|
||||
optionGroups[''].unshift({id:'?', label:'', selected:true});
|
||||
}
|
||||
}
|
||||
|
||||
// Now we need to update the list of DOM nodes to match the optionGroups we computed above
|
||||
for (groupIndex = 0, groupLength = optionGroupNames.length;
|
||||
groupIndex < groupLength;
|
||||
groupIndex++) {
|
||||
// current option group name or '' if no group
|
||||
optionGroupName = optionGroupNames[groupIndex];
|
||||
|
||||
// list of options for that group. (first item has the parent)
|
||||
optionGroup = optionGroups[optionGroupName];
|
||||
|
||||
if (optionGroupsCache.length <= groupIndex) {
|
||||
// we need to grow the optionGroups
|
||||
existingParent = {
|
||||
element: optGroupTemplate.clone().attr('label', optionGroupName),
|
||||
label: optionGroup.label
|
||||
};
|
||||
existingOptions = [existingParent];
|
||||
optionGroupsCache.push(existingOptions);
|
||||
selectElement.append(existingParent.element);
|
||||
} else {
|
||||
existingOptions = optionGroupsCache[groupIndex];
|
||||
existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
|
||||
|
||||
// update the OPTGROUP label if not the same.
|
||||
if (existingParent.label != optionGroupName) {
|
||||
existingParent.element.attr('label', existingParent.label = optionGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
lastElement = null; // start at the beginning
|
||||
for (index = 0, length = optionGroup.length; index < length; index++) {
|
||||
option = optionGroup[index];
|
||||
if ((existingOption = existingOptions[index + 1])) {
|
||||
// reuse elements
|
||||
lastElement = existingOption.element;
|
||||
if (existingOption.label !== option.label) {
|
||||
updateLabelMap(labelMap, existingOption.label, false);
|
||||
updateLabelMap(labelMap, option.label, true);
|
||||
lastElement.text(existingOption.label = option.label);
|
||||
lastElement.prop('label', existingOption.label);
|
||||
}
|
||||
if (existingOption.id !== option.id) {
|
||||
lastElement.val(existingOption.id = option.id);
|
||||
}
|
||||
// lastElement.prop('selected') provided by jQuery has side-effects
|
||||
if (lastElement[0].selected !== option.selected) {
|
||||
lastElement.prop('selected', (existingOption.selected = option.selected));
|
||||
if (msie) {
|
||||
// See #7692
|
||||
// The selected item wouldn't visually update on IE without this.
|
||||
// Tested on Win7: IE9, IE10 and IE11. Future IEs should be tested as well
|
||||
lastElement.prop('selected', existingOption.selected);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// grow elements
|
||||
|
||||
// if it's a null option
|
||||
if (option.id === '' && nullOption) {
|
||||
// put back the pre-compiled element
|
||||
element = nullOption;
|
||||
} else {
|
||||
// jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
|
||||
// in this version of jQuery on some browser the .text() returns a string
|
||||
// rather then the element.
|
||||
(element = optionTemplate.clone())
|
||||
.val(option.id)
|
||||
.prop('selected', option.selected)
|
||||
.attr('selected', option.selected)
|
||||
.prop('label', option.label)
|
||||
.text(option.label);
|
||||
}
|
||||
|
||||
existingOptions.push(existingOption = {
|
||||
element: element,
|
||||
label: option.label,
|
||||
id: option.id,
|
||||
selected: option.selected
|
||||
});
|
||||
updateLabelMap(labelMap, option.label, true);
|
||||
if (lastElement) {
|
||||
lastElement.after(element);
|
||||
} else {
|
||||
existingParent.element.append(element);
|
||||
}
|
||||
lastElement = element;
|
||||
}
|
||||
}
|
||||
// remove any excessive OPTIONs in a group
|
||||
index++; // increment since the existingOptions[0] is parent element not OPTION
|
||||
while (existingOptions.length > index) {
|
||||
option = existingOptions.pop();
|
||||
updateLabelMap(labelMap, option.label, false);
|
||||
option.element.remove();
|
||||
}
|
||||
}
|
||||
// remove any excessive OPTGROUPs from select
|
||||
while (optionGroupsCache.length > groupIndex) {
|
||||
// remove all the labels in the option group
|
||||
optionGroup = optionGroupsCache.pop();
|
||||
for (index = 1; index < optionGroup.length; ++index) {
|
||||
updateLabelMap(labelMap, optionGroup[index].label, false);
|
||||
}
|
||||
optionGroup[0].element.remove();
|
||||
}
|
||||
forEach(labelMap, function(count, label) {
|
||||
if (count > 0) {
|
||||
selectCtrl.addOption(label);
|
||||
} else if (count < 0) {
|
||||
selectCtrl.removeOption(label);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}];
|
||||
};
|
||||
|
||||
|
||||
// The option directive is purely designed to communicate the existence (or lack of)
|
||||
// of dynamically created (and destroyed) option elements to their containing select
|
||||
// directive via its controller.
|
||||
var optionDirective = ['$interpolate', function($interpolate) {
|
||||
var nullSelectCtrl = {
|
||||
addOption: noop,
|
||||
removeOption: noop
|
||||
};
|
||||
|
||||
function chromeHack(optionElement) {
|
||||
// Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
|
||||
// Adding an <option selected="selected"> element to a <select required="required"> should
|
||||
// automatically select the new element
|
||||
if (optionElement[0].hasAttribute('selected')) {
|
||||
optionElement[0].selected = true;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
priority: 100,
|
||||
compile: function(element, attr) {
|
||||
|
||||
// If the value attribute is not defined then we fall back to the
|
||||
// text content of the option element, which may be interpolated
|
||||
if (isUndefined(attr.value)) {
|
||||
var interpolateFn = $interpolate(element.text(), true);
|
||||
if (!interpolateFn) {
|
||||
@@ -746,30 +247,39 @@ var optionDirective = ['$interpolate', function($interpolate) {
|
||||
}
|
||||
|
||||
return function(scope, element, attr) {
|
||||
|
||||
// This is an optimization over using ^^ since we don't want to have to search
|
||||
// all the way to the root of the DOM for every single option element
|
||||
var selectCtrlName = '$selectController',
|
||||
parent = element.parent(),
|
||||
selectCtrl = parent.data(selectCtrlName) ||
|
||||
parent.parent().data(selectCtrlName); // in case we are in optgroup
|
||||
|
||||
if (!selectCtrl || !selectCtrl.databound) {
|
||||
selectCtrl = nullSelectCtrl;
|
||||
}
|
||||
// Only update trigger option updates if this is an option within a `select`
|
||||
// that also has `ngModel` attached
|
||||
if (selectCtrl && selectCtrl.ngModelCtrl) {
|
||||
|
||||
if (interpolateFn) {
|
||||
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
|
||||
attr.$set('value', newVal);
|
||||
if (oldVal !== newVal) {
|
||||
selectCtrl.removeOption(oldVal);
|
||||
}
|
||||
selectCtrl.addOption(newVal, element);
|
||||
if (interpolateFn) {
|
||||
scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
|
||||
attr.$set('value', newVal);
|
||||
if (oldVal !== newVal) {
|
||||
selectCtrl.removeOption(oldVal);
|
||||
}
|
||||
selectCtrl.addOption(newVal, element);
|
||||
selectCtrl.ngModelCtrl.$render();
|
||||
chromeHack(element);
|
||||
});
|
||||
} else {
|
||||
selectCtrl.addOption(attr.value, element);
|
||||
selectCtrl.ngModelCtrl.$render();
|
||||
chromeHack(element);
|
||||
}
|
||||
|
||||
element.on('$destroy', function() {
|
||||
selectCtrl.removeOption(attr.value);
|
||||
selectCtrl.ngModelCtrl.$render();
|
||||
});
|
||||
} else {
|
||||
selectCtrl.addOption(attr.value, element);
|
||||
}
|
||||
|
||||
element.on('$destroy', function() {
|
||||
selectCtrl.removeOption(attr.value);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 = toInt(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 = toInt(value) || 0;
|
||||
ctrl.$validate();
|
||||
});
|
||||
ctrl.$validators.minlength = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
+20
-12
@@ -82,6 +82,8 @@ function currencyFilter($locale) {
|
||||
*
|
||||
* If the input is not a number an empty string is returned.
|
||||
*
|
||||
* If the input is an infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned.
|
||||
*
|
||||
* @param {number|string} number Number to format.
|
||||
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
|
||||
* If this is not provided then the fraction size is computed from the current locale's number
|
||||
@@ -138,16 +140,22 @@ function numberFilter($locale) {
|
||||
|
||||
var DECIMAL_SEP = '.';
|
||||
function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
|
||||
if (!isFinite(number) || isObject(number)) return '';
|
||||
if (isObject(number)) return '';
|
||||
|
||||
var isNegative = number < 0;
|
||||
number = Math.abs(number);
|
||||
|
||||
var isInfinity = number === Infinity;
|
||||
if (!isInfinity && !isFinite(number)) return '';
|
||||
|
||||
var numStr = number + '',
|
||||
formatedText = '',
|
||||
hasExponent = false,
|
||||
parts = [];
|
||||
|
||||
var hasExponent = false;
|
||||
if (numStr.indexOf('e') !== -1) {
|
||||
if (isInfinity) formatedText = '\u221e';
|
||||
|
||||
if (!isInfinity && numStr.indexOf('e') !== -1) {
|
||||
var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
|
||||
if (match && match[2] == '-' && match[3] > fractionSize + 1) {
|
||||
number = 0;
|
||||
@@ -157,7 +165,7 @@ function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasExponent) {
|
||||
if (!isInfinity && !hasExponent) {
|
||||
var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
|
||||
|
||||
// determine fractionSize if it is not specified
|
||||
@@ -429,13 +437,13 @@ function dateFilter($locale) {
|
||||
timeSetter = match[8] ? date.setUTCHours : date.setHours;
|
||||
|
||||
if (match[9]) {
|
||||
tzHour = int(match[9] + match[10]);
|
||||
tzMin = int(match[9] + match[11]);
|
||||
tzHour = toInt(match[9] + match[10]);
|
||||
tzMin = toInt(match[9] + match[11]);
|
||||
}
|
||||
dateSetter.call(date, int(match[1]), int(match[2]) - 1, int(match[3]));
|
||||
var h = int(match[4] || 0) - tzHour;
|
||||
var m = int(match[5] || 0) - tzMin;
|
||||
var s = int(match[6] || 0);
|
||||
dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
|
||||
var h = toInt(match[4] || 0) - tzHour;
|
||||
var m = toInt(match[5] || 0) - tzMin;
|
||||
var s = toInt(match[6] || 0);
|
||||
var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
|
||||
timeSetter.call(date, h, m, s, ms);
|
||||
return date;
|
||||
@@ -452,14 +460,14 @@ function dateFilter($locale) {
|
||||
format = format || 'mediumDate';
|
||||
format = $locale.DATETIME_FORMATS[format] || format;
|
||||
if (isString(date)) {
|
||||
date = NUMBER_STRING.test(date) ? int(date) : jsonStringToDate(date);
|
||||
date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
|
||||
}
|
||||
|
||||
if (isNumber(date)) {
|
||||
date = new Date(date);
|
||||
}
|
||||
|
||||
if (!isDate(date)) {
|
||||
if (!isDate(date) || !isFinite(date.getTime())) {
|
||||
return date;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
* @param {string|number} limit The length of the returned array or string. If the `limit` number
|
||||
* is positive, `limit` number of items from the beginning of the source array/string are copied.
|
||||
* If the number is negative, `limit` number of items from the end of the source array/string
|
||||
* are copied. The `limit` will be trimmed if it exceeds `array.length`
|
||||
* are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
|
||||
* the input will be returned unchanged.
|
||||
* @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
|
||||
* had less than `limit` elements.
|
||||
*
|
||||
@@ -88,20 +89,16 @@
|
||||
*/
|
||||
function limitToFilter() {
|
||||
return function(input, limit) {
|
||||
if (isNumber(input)) input = input.toString();
|
||||
if (!isArray(input) && !isString(input)) return input;
|
||||
|
||||
if (Math.abs(Number(limit)) === Infinity) {
|
||||
limit = Number(limit);
|
||||
} else {
|
||||
limit = int(limit);
|
||||
limit = toInt(limit);
|
||||
}
|
||||
if (isNaN(limit)) return input;
|
||||
|
||||
//NaN check on limit
|
||||
if (limit) {
|
||||
return limit > 0 ? input.slice(0, limit) : input.slice(limit);
|
||||
} else {
|
||||
return isString(input) ? "" : [];
|
||||
}
|
||||
if (isNumber(input)) input = input.toString();
|
||||
if (!isArray(input) && !isString(input)) return input;
|
||||
|
||||
return limit >= 0 ? input.slice(0, limit) : input.slice(limit);
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -347,7 +347,7 @@ function $HttpProvider() {
|
||||
* To add or overwrite these defaults, simply add or remove a property from these configuration
|
||||
* objects. To add headers for an HTTP method other than POST or PUT, simply add a new object
|
||||
* with the lowercased HTTP method name as the key, e.g.
|
||||
* `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }.
|
||||
* `$httpProvider.defaults.headers.get = { 'My-Header' : 'value' }`.
|
||||
*
|
||||
* The defaults can also be set at runtime via the `$http.defaults` object in the same
|
||||
* fashion. For example:
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) {
|
||||
|
||||
locationObj.$$protocol = parsedUrl.protocol;
|
||||
locationObj.$$host = parsedUrl.hostname;
|
||||
locationObj.$$port = int(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
|
||||
locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -837,7 +837,7 @@ function $LocationProvider() {
|
||||
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
|
||||
// currently we open nice url link and redirect then
|
||||
|
||||
if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2) return;
|
||||
if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.which == 2 || event.button == 2) return;
|
||||
|
||||
var elm = jqLite(event.target);
|
||||
|
||||
|
||||
+3
-3
@@ -18,7 +18,7 @@ function $SnifferProvider() {
|
||||
this.$get = ['$window', '$document', function($window, $document) {
|
||||
var eventSupport = {},
|
||||
android =
|
||||
int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
|
||||
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
|
||||
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
|
||||
document = $document[0] || {},
|
||||
vendorPrefix,
|
||||
@@ -45,8 +45,8 @@ function $SnifferProvider() {
|
||||
animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
|
||||
|
||||
if (android && (!transitions || !animations)) {
|
||||
transitions = isString(document.body.style.webkitTransition);
|
||||
animations = isString(document.body.style.webkitAnimation);
|
||||
transitions = isString(bodyStyle.webkitTransition);
|
||||
animations = isString(bodyStyle.webkitAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ var $compileMinErr = minErr('$compile');
|
||||
* @param {string} tpl The HTTP request template URL
|
||||
* @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
|
||||
*
|
||||
* @return {Promise} the HTTP Promise for the given.
|
||||
* @return {Promise} a promise for the the HTTP response data of the given URL.
|
||||
*
|
||||
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
|
||||
*/
|
||||
@@ -49,7 +49,8 @@ function $TemplateRequestProvider() {
|
||||
function handleError(resp) {
|
||||
self.totalPendingRequests--;
|
||||
if (!ignoreRequestError) {
|
||||
throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
|
||||
throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
|
||||
tpl, resp.status, resp.statusText);
|
||||
}
|
||||
return $q.reject(resp);
|
||||
}
|
||||
|
||||
+13
-3
@@ -4,6 +4,7 @@
|
||||
function $TimeoutProvider() {
|
||||
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
|
||||
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
|
||||
|
||||
var deferreds = {};
|
||||
|
||||
|
||||
@@ -16,15 +17,18 @@ function $TimeoutProvider() {
|
||||
* block and delegates any exceptions to
|
||||
* {@link ng.$exceptionHandler $exceptionHandler} service.
|
||||
*
|
||||
* The return value of registering a timeout function is a promise, which will be resolved when
|
||||
* the timeout is reached and the timeout function is executed.
|
||||
* The return value of calling `$timeout` is a promise, which will be resolved when
|
||||
* the delay has passed and the timeout function, if provided, is executed.
|
||||
*
|
||||
* To cancel a timeout request, call `$timeout.cancel(promise)`.
|
||||
*
|
||||
* In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
|
||||
* synchronously flush the queue of deferred functions.
|
||||
*
|
||||
* @param {function()} fn A function, whose execution should be delayed.
|
||||
* If you only want a promise that will be resolved after some specified delay
|
||||
* then you can call `$timeout` without the `fn` function.
|
||||
*
|
||||
* @param {function()=} fn A function, whose execution should be delayed.
|
||||
* @param {number=} [delay=0] Delay in milliseconds.
|
||||
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
|
||||
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
|
||||
@@ -33,6 +37,12 @@ function $TimeoutProvider() {
|
||||
*
|
||||
*/
|
||||
function timeout(fn, delay, invokeApply) {
|
||||
if (!isFunction(fn)) {
|
||||
invokeApply = delay;
|
||||
delay = fn;
|
||||
fn = noop;
|
||||
}
|
||||
|
||||
var skipApply = (isDefined(invokeApply) && !invokeApply),
|
||||
deferred = (skipApply ? $$q : $q).defer(),
|
||||
promise = deferred.promise,
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
*
|
||||
* Whenever the `ngMessages` directive contains one or more visible messages then the `.ng-active` CSS
|
||||
* class will be added to the element. The `.ng-inactive` CSS class will be applied when there are no
|
||||
* animations present. Therefore, CSS transitions and keyframes as well as JavaScript animations can
|
||||
* messages present. Therefore, CSS transitions and keyframes as well as JavaScript animations can
|
||||
* hook into the animations whenever these classes are added/removed.
|
||||
*
|
||||
* Let's say that our HTML code for our messages container looks like so:
|
||||
@@ -380,7 +380,7 @@ angular.module('ngMessages', [])
|
||||
});
|
||||
}
|
||||
},
|
||||
detach: function(now) {
|
||||
detach: function() {
|
||||
if (element) {
|
||||
$animate.leave(element);
|
||||
element = null;
|
||||
|
||||
Vendored
+19
-19
@@ -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:
|
||||
@@ -574,20 +574,20 @@ function jsonStringToDate(string) {
|
||||
tzHour = 0,
|
||||
tzMin = 0;
|
||||
if (match[9]) {
|
||||
tzHour = int(match[9] + match[10]);
|
||||
tzMin = int(match[9] + match[11]);
|
||||
tzHour = toInt(match[9] + match[10]);
|
||||
tzMin = toInt(match[9] + match[11]);
|
||||
}
|
||||
date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
|
||||
date.setUTCHours(int(match[4] || 0) - tzHour,
|
||||
int(match[5] || 0) - tzMin,
|
||||
int(match[6] || 0),
|
||||
int(match[7] || 0));
|
||||
date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
|
||||
date.setUTCHours(toInt(match[4] || 0) - tzHour,
|
||||
toInt(match[5] || 0) - tzMin,
|
||||
toInt(match[6] || 0),
|
||||
toInt(match[7] || 0));
|
||||
return date;
|
||||
}
|
||||
return string;
|
||||
}
|
||||
|
||||
function int(str) {
|
||||
function toInt(str) {
|
||||
return parseInt(str, 10);
|
||||
}
|
||||
|
||||
|
||||
@@ -607,8 +607,8 @@ function $RouteProvider() {
|
||||
return $q.all(locals);
|
||||
}
|
||||
}).
|
||||
// after route change
|
||||
then(function(locals) {
|
||||
// after route change
|
||||
if (nextRoute == $route.current) {
|
||||
if (nextRoute) {
|
||||
nextRoute.locals = locals;
|
||||
|
||||
@@ -114,7 +114,7 @@ angular.scenario.ObjectModel = function(runner) {
|
||||
});
|
||||
|
||||
function complete(item) {
|
||||
item.endTime = new Date().getTime();
|
||||
item.endTime = Date.now();
|
||||
item.duration = item.endTime - item.startTime;
|
||||
item.status = item.status || 'success';
|
||||
}
|
||||
@@ -188,7 +188,7 @@ angular.scenario.ObjectModel.prototype.getSpec = function(id) {
|
||||
angular.scenario.ObjectModel.Spec = function(id, name, definitionNames) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.startTime = new Date().getTime();
|
||||
this.startTime = Date.now();
|
||||
this.steps = [];
|
||||
this.fullDefinitionName = (definitionNames || []).join(' ');
|
||||
};
|
||||
@@ -234,7 +234,7 @@ angular.scenario.ObjectModel.Spec.prototype.setStatusFromStep = function(step) {
|
||||
*/
|
||||
angular.scenario.ObjectModel.Step = function(name) {
|
||||
this.name = name;
|
||||
this.startTime = new Date().getTime();
|
||||
this.startTime = Date.now();
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @license AngularJS v"NG_VERSION_FULL"
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* (c) 2010-2015 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, document){
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@
|
||||
"nextUid": false,
|
||||
"setHashKey": false,
|
||||
"extend": false,
|
||||
"int": false,
|
||||
"toInt": false,
|
||||
"inherit": false,
|
||||
"noop": false,
|
||||
"identity": false,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -97,4 +97,11 @@ describe('minErr', function() {
|
||||
var typeMinErr = minErr('type', TypeError);
|
||||
expect(typeMinErr('acode', 'aproblem') instanceof TypeError).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
it('should include a properly formatted error reference URL in the message', function() {
|
||||
// to avoid maintaining the root URL in two locations, we only validate the parameters
|
||||
expect(testError('acode', 'aproblem', 'a', 'b', 'value with space').message)
|
||||
.toMatch(/^[\s\S]*\?p0=a&p1=b&p2=value%20with%20space$/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,22 @@ describe('boolean attr directives', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should not bind checked when ngModel is present', inject(function($rootScope, $compile) {
|
||||
// test for https://github.com/angular/angular.js/issues/10662
|
||||
element = $compile('<input type="checkbox" ng-model="value" ng-false-value="\'false\'" ' +
|
||||
'ng-true-value="\'true\'" ng-checked="value" />')($rootScope);
|
||||
$rootScope.value = 'true';
|
||||
$rootScope.$digest();
|
||||
expect(element[0].checked).toBe(true);
|
||||
browserTrigger(element, 'click');
|
||||
expect(element[0].checked).toBe(false);
|
||||
expect($rootScope.value).toBe('false');
|
||||
browserTrigger(element, 'click');
|
||||
expect(element[0].checked).toBe(true);
|
||||
expect($rootScope.value).toBe('true');
|
||||
}));
|
||||
|
||||
|
||||
it('should bind selected', inject(function($rootScope, $compile) {
|
||||
element = $compile('<select><option value=""></option><option ng-selected="isSelected">Greetings!</option></select>')($rootScope);
|
||||
jqLite(document.body).append(element);
|
||||
|
||||
+989
-3699
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -88,6 +88,16 @@ describe('ngClass', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should support adding multiple classes via a space delimited string inside an array', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div class="existing" ng-class="[\'A B\', \'C\']"></div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('existing')).toBeTruthy();
|
||||
expect(element.hasClass('A')).toBeTruthy();
|
||||
expect(element.hasClass('B')).toBeTruthy();
|
||||
expect(element.hasClass('C')).toBeTruthy();
|
||||
}));
|
||||
|
||||
|
||||
it('should preserve class added post compilation with pre-existing classes', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div class="existing" ng-class="dynClass"></div>')($rootScope);
|
||||
$rootScope.dynClass = 'A';
|
||||
|
||||
@@ -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=" "></textarea');
|
||||
helper.changeInputValueTo('a\nb');
|
||||
expect($rootScope.list).toEqual(['a','b']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -146,6 +146,57 @@ describe('ngPluralize', function() {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('undefined rule cases', function() {
|
||||
var $locale, $log;
|
||||
beforeEach(inject(function(_$locale_, _$log_) {
|
||||
$locale = _$locale_;
|
||||
$log = _$log_;
|
||||
}));
|
||||
afterEach(inject(function($log) {
|
||||
$log.reset();
|
||||
}));
|
||||
|
||||
it('should generate a warning when being asked to use a rule that is not defined',
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile(
|
||||
'<ng:pluralize count="email"' +
|
||||
"when=\"{'0': 'Zero'," +
|
||||
"'one': 'Some text'," +
|
||||
"'other': 'Some text'}\">" +
|
||||
'</ng:pluralize>')($rootScope);
|
||||
$locale.pluralCat = function() {return "few";};
|
||||
|
||||
$rootScope.email = '3';
|
||||
expect($log.debug.logs).toEqual([]);
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toBe('');
|
||||
expect($log.debug.logs.shift())
|
||||
.toEqual(["ngPluralize: no rule defined for 'few' in {'0': 'Zero','one': 'Some text','other': 'Some text'}"]);
|
||||
}));
|
||||
|
||||
it('should empty the element content when using a rule that is not defined',
|
||||
inject(function($rootScope, $compile) {
|
||||
element = $compile(
|
||||
'<ng:pluralize count="email"' +
|
||||
"when=\"{'0': 'Zero'," +
|
||||
"'one': 'Some text'," +
|
||||
"'other': 'Some text'}\">" +
|
||||
'</ng:pluralize>')($rootScope);
|
||||
$locale.pluralCat = function(count) {return count === 1 ? "one" : "few";};
|
||||
|
||||
$rootScope.email = '0';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toBe('Zero');
|
||||
|
||||
$rootScope.email = '3';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toBe('');
|
||||
|
||||
$rootScope.email = '1';
|
||||
$rootScope.$digest();
|
||||
expect(element.text()).toBe('Some text');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('deal with pluralized strings with offset', function() {
|
||||
it('should show single/plural strings with offset', inject(function($rootScope, $compile) {
|
||||
|
||||
+326
-1901
File diff suppressed because it is too large
Load Diff
@@ -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 = toInt(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 = toInt(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'}];
|
||||
|
||||
@@ -150,10 +150,10 @@ describe('filters', function() {
|
||||
expect(number(Number.NaN)).toEqual('');
|
||||
expect(number({})).toEqual('');
|
||||
expect(number([])).toEqual('');
|
||||
expect(number(+Infinity)).toEqual('');
|
||||
expect(number(-Infinity)).toEqual('');
|
||||
expect(number(+Infinity)).toEqual('∞');
|
||||
expect(number(-Infinity)).toEqual('-∞');
|
||||
expect(number("1234.5678")).toEqual('1,234.568');
|
||||
expect(number(1 / 0)).toEqual("");
|
||||
expect(number(1 / 0)).toEqual('∞');
|
||||
expect(number(1, 2)).toEqual("1.00");
|
||||
expect(number(.1, 2)).toEqual("0.10");
|
||||
expect(number(.01, 2)).toEqual("0.01");
|
||||
@@ -247,6 +247,11 @@ describe('filters', function() {
|
||||
expect(date('')).toEqual('');
|
||||
});
|
||||
|
||||
it('should ignore invalid dates', function() {
|
||||
var invalidDate = new Date('abc');
|
||||
expect(date(invalidDate)).toBe(invalidDate);
|
||||
});
|
||||
|
||||
it('should do basic filter', function() {
|
||||
expect(date(noon)).toEqual(date(noon, 'mediumDate'));
|
||||
expect(date(noon, '')).toEqual(date(noon, 'mediumDate'));
|
||||
|
||||
@@ -34,20 +34,30 @@ describe('Filter: limitTo', function() {
|
||||
});
|
||||
|
||||
|
||||
it('should return an empty array when X cannot be parsed', function() {
|
||||
expect(limitTo(items, 'bogus')).toEqual([]);
|
||||
expect(limitTo(items, 'null')).toEqual([]);
|
||||
expect(limitTo(items, 'undefined')).toEqual([]);
|
||||
expect(limitTo(items, null)).toEqual([]);
|
||||
expect(limitTo(items, undefined)).toEqual([]);
|
||||
it('should return an empty array when X = 0', function() {
|
||||
expect(limitTo(items, 0)).toEqual([]);
|
||||
expect(limitTo(items, '0')).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty string when X cannot be parsed', function() {
|
||||
expect(limitTo(str, 'bogus')).toEqual("");
|
||||
expect(limitTo(str, 'null')).toEqual("");
|
||||
expect(limitTo(str, 'undefined')).toEqual("");
|
||||
expect(limitTo(str, null)).toEqual("");
|
||||
expect(limitTo(str, undefined)).toEqual("");
|
||||
it('should return entire array when X cannot be parsed', function() {
|
||||
expect(limitTo(items, 'bogus')).toEqual(items);
|
||||
expect(limitTo(items, 'null')).toEqual(items);
|
||||
expect(limitTo(items, 'undefined')).toEqual(items);
|
||||
expect(limitTo(items, null)).toEqual(items);
|
||||
expect(limitTo(items, undefined)).toEqual(items);
|
||||
});
|
||||
|
||||
it('should return an empty string when X = 0', function() {
|
||||
expect(limitTo(str, 0)).toEqual("");
|
||||
expect(limitTo(str, '0')).toEqual("");
|
||||
});
|
||||
|
||||
it('should return entire string when X cannot be parsed', function() {
|
||||
expect(limitTo(str, 'bogus')).toEqual(str);
|
||||
expect(limitTo(str, 'null')).toEqual(str);
|
||||
expect(limitTo(str, 'undefined')).toEqual(str);
|
||||
expect(limitTo(str, null)).toEqual(str);
|
||||
expect(limitTo(str, undefined)).toEqual(str);
|
||||
});
|
||||
|
||||
|
||||
|
||||
+21
-25
@@ -274,63 +274,59 @@ describe('$http', function() {
|
||||
describe('the instance', function() {
|
||||
var $httpBackend, $http, $rootScope;
|
||||
|
||||
beforeEach(inject(['$rootScope', function($rs) {
|
||||
beforeEach(inject(['$httpBackend', '$http', '$rootScope', function($hb, $h, $rs) {
|
||||
$httpBackend = $hb;
|
||||
$http = $h;
|
||||
$rootScope = $rs;
|
||||
|
||||
spyOn($rootScope, '$apply').andCallThrough();
|
||||
}]));
|
||||
|
||||
beforeEach(inject(['$httpBackend', '$http', function($hb, $h) {
|
||||
$httpBackend = $hb;
|
||||
$http = $h;
|
||||
}]));
|
||||
|
||||
it('should throw error if the request configuration is not an object', inject(function($httpBackend, $http) {
|
||||
it('should throw error if the request configuration is not an object', function() {
|
||||
expect(function() {
|
||||
$http('/url');
|
||||
}).toThrowMinErr('$http','badreq', 'Http request configuration must be an object. Received: /url');
|
||||
}));
|
||||
});
|
||||
|
||||
it('should send GET requests if no method specified', inject(function($httpBackend, $http) {
|
||||
it('should send GET requests if no method specified', function() {
|
||||
$httpBackend.expect('GET', '/url').respond('');
|
||||
$http({url: '/url'});
|
||||
}));
|
||||
});
|
||||
|
||||
it('should do basic request', inject(function($httpBackend, $http) {
|
||||
it('should do basic request', function() {
|
||||
$httpBackend.expect('GET', '/url').respond('');
|
||||
$http({url: '/url', method: 'GET'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should pass data if specified', inject(function($httpBackend, $http) {
|
||||
it('should pass data if specified', function() {
|
||||
$httpBackend.expect('POST', '/url', 'some-data').respond('');
|
||||
$http({url: '/url', method: 'POST', data: 'some-data'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
describe('params', function() {
|
||||
it('should do basic request with params and encode', inject(function($httpBackend, $http) {
|
||||
it('should do basic request with params and encode', function() {
|
||||
$httpBackend.expect('GET', '/url?a%3D=%3F%26&b=2').respond('');
|
||||
$http({url: '/url', params: {'a=':'?&', b:2}, method: 'GET'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should merge params if url contains some already', inject(function($httpBackend, $http) {
|
||||
it('should merge params if url contains some already', function() {
|
||||
$httpBackend.expect('GET', '/url?c=3&a=1&b=2').respond('');
|
||||
$http({url: '/url?c=3', params: {a:1, b:2}, method: 'GET'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should jsonify objects in params map', inject(function($httpBackend, $http) {
|
||||
it('should jsonify objects in params map', function() {
|
||||
$httpBackend.expect('GET', '/url?a=1&b=%7B%22c%22:3%7D').respond('');
|
||||
$http({url: '/url', params: {a:1, b:{c:3}}, method: 'GET'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should expand arrays in params map', inject(function($httpBackend, $http) {
|
||||
it('should expand arrays in params map', function() {
|
||||
$httpBackend.expect('GET', '/url?a=1&a=2&a=3').respond('');
|
||||
$http({url: '/url', params: {a: [1,2,3]}, method: 'GET'});
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should not encode @ in url params', function() {
|
||||
@@ -763,7 +759,7 @@ describe('$http', function() {
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
|
||||
it('should send execute result if header value is function', inject(function() {
|
||||
it('should send execute result if header value is function', function() {
|
||||
var headerConfig = {'Accept': function() { return 'Rewritten'; }};
|
||||
|
||||
function checkHeaders(headers) {
|
||||
@@ -783,7 +779,7 @@ describe('$http', function() {
|
||||
$http({url: '/url', method: 'DELETE', headers: headerConfig});
|
||||
|
||||
$httpBackend.flush();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should check the cache before checking the XSRF cookie', inject(function($browser, $cacheFactory) {
|
||||
var testCache = $cacheFactory('testCache'),
|
||||
|
||||
@@ -1504,6 +1504,40 @@ describe('$location', function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('should not rewrite when right click pressed', function() {
|
||||
configureService({linkHref: '/a?b=c', html5Mode: true, supportHist: true});
|
||||
inject(
|
||||
initBrowser(),
|
||||
initLocation(),
|
||||
function($browser) {
|
||||
var rightClick;
|
||||
if (document.createEvent) {
|
||||
rightClick = document.createEvent('MouseEvents');
|
||||
rightClick.initMouseEvent('click', true, true, window, 1, 10, 10, 10, 10, false,
|
||||
false, false, false, 2, null);
|
||||
|
||||
link.dispatchEvent(rightClick);
|
||||
} else if (document.createEventObject) { // for IE
|
||||
rightClick = document.createEventObject();
|
||||
rightClick.type = 'click';
|
||||
rightClick.cancelBubble = true;
|
||||
rightClick.detail = 1;
|
||||
rightClick.screenX = 10;
|
||||
rightClick.screenY = 10;
|
||||
rightClick.clientX = 10;
|
||||
rightClick.clientY = 10;
|
||||
rightClick.ctrlKey = false;
|
||||
rightClick.altKey = false;
|
||||
rightClick.shiftKey = false;
|
||||
rightClick.metaKey = false;
|
||||
rightClick.button = 2;
|
||||
link.fireEvent('onclick', rightClick);
|
||||
}
|
||||
expectNoRewrite($browser);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should not mess up hash urls when clicking on links in hashbang mode', function() {
|
||||
var base;
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('$templateRequest', function() {
|
||||
it('should throw an error when the template is not found',
|
||||
inject(function($rootScope, $templateRequest, $httpBackend) {
|
||||
|
||||
$httpBackend.expectGET('tpl.html').respond(404);
|
||||
$httpBackend.expectGET('tpl.html').respond(404, '', {}, 'Not found');
|
||||
|
||||
$templateRequest('tpl.html');
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('$templateRequest', function() {
|
||||
expect(function() {
|
||||
$rootScope.$digest();
|
||||
$httpBackend.flush();
|
||||
}).toThrowMinErr('$compile', 'tpload', 'Failed to load template: tpl.html');
|
||||
}).toThrowMinErr('$compile', 'tpload', 'Failed to load template: tpl.html (HTTP status: 404 Not found)');
|
||||
}));
|
||||
|
||||
it('should not throw when the template is not found and ignoreRequestError is true',
|
||||
|
||||
+26
-2
@@ -105,6 +105,25 @@ describe('$timeout', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should allow the `fn` parameter to be optional', inject(function($timeout, log) {
|
||||
|
||||
$timeout().then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
|
||||
expect(log).toEqual([]);
|
||||
|
||||
$timeout.flush();
|
||||
expect(log).toEqual(['promise success: undefined']);
|
||||
|
||||
log.reset();
|
||||
$timeout(1000).then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
|
||||
expect(log).toEqual([]);
|
||||
|
||||
$timeout.flush(500);
|
||||
expect(log).toEqual([]);
|
||||
$timeout.flush(500);
|
||||
expect(log).toEqual(['promise success: undefined']);
|
||||
}));
|
||||
|
||||
|
||||
describe('exception handling', function() {
|
||||
|
||||
beforeEach(module(function($exceptionHandlerProvider) {
|
||||
@@ -165,19 +184,24 @@ describe('$timeout', function() {
|
||||
var task1 = jasmine.createSpy('task1'),
|
||||
task2 = jasmine.createSpy('task2'),
|
||||
task3 = jasmine.createSpy('task3'),
|
||||
promise1, promise3;
|
||||
task4 = jasmine.createSpy('task4'),
|
||||
promise1, promise3, promise4;
|
||||
|
||||
promise1 = $timeout(task1);
|
||||
$timeout(task2);
|
||||
promise3 = $timeout(task3, 333);
|
||||
promise4 = $timeout(333);
|
||||
promise3.then(task4);
|
||||
|
||||
$timeout.cancel(promise3);
|
||||
$timeout.cancel(promise1);
|
||||
$timeout.cancel(promise3);
|
||||
$timeout.cancel(promise4);
|
||||
$timeout.flush();
|
||||
|
||||
expect(task1).not.toHaveBeenCalled();
|
||||
expect(task2).toHaveBeenCalledOnce();
|
||||
expect(task3).not.toHaveBeenCalled();
|
||||
expect(task4).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
|
||||
|
||||
Vendored
+33
-13
@@ -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!");
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user