Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8822a4ff69 | |||
| 772440cdaf | |||
| 1fbddf950f | |||
| 369ca3e13e | |||
| 00d38e3358 | |||
| a0ed1b73ff | |||
| e44c3ac327 | |||
| c6551231b2 | |||
| bf073be69d | |||
| aed34c75be | |||
| 78e17f5729 | |||
| ba6f477ac8 | |||
| 6482297185 | |||
| 41deaf1d21 | |||
| 23e4138c07 | |||
| 0ffb30f585 | |||
| 6f52013668 | |||
| 07e1ba29f0 | |||
| 04c64b3d10 | |||
| 4d9a95ffdb | |||
| 27486bd15e | |||
| cf919a6fb7 | |||
| d4d1031bcd | |||
| 798114075f | |||
| 838dd12ee1 | |||
| 6926223963 | |||
| 5f8372ee2d | |||
| 289374a43c | |||
| 3b333f3b9f | |||
| 7cbb1044fc | |||
| a8bfeff5d8 | |||
| fc0e100c45 | |||
| eb49f6b755 | |||
| f9e651d113 | |||
| 2f72a69ded | |||
| 8a67ac57fc | |||
| 90a41d415c | |||
| eefaa76a90 | |||
| bc3432365c | |||
| f4ac5c8487 | |||
| 9f2f567167 | |||
| 3e6f49a227 | |||
| 795398fab0 | |||
| f1a34bad20 | |||
| 84a6ea9eb4 | |||
| ff0ef2aeaf | |||
| c81e3556e8 | |||
| 8cc69b9edb | |||
| ae8ce3ec60 | |||
| df0d5d245e | |||
| 67ec5e8ceb | |||
| 4df2188e96 | |||
| 816a587578 | |||
| 0e1bd7822e | |||
| b27080d525 | |||
| 1a509873bf | |||
| fead62e958 | |||
| e2011bd0b3 | |||
| 3cdffcecba | |||
| f87d6648d7 | |||
| b1d0a83d01 | |||
| d6098eeb1c | |||
| c903b76f6c | |||
| f3a565872d | |||
| 582b03b983 | |||
| be240b1176 | |||
| 61b33543ff | |||
| 1dcba9cd88 | |||
| 8cd54d7794 | |||
| 3105b2c26a | |||
| bc5a48d4a4 | |||
| 2bbc7c464f | |||
| e85f91d582 | |||
| 862a78dfd2 | |||
| bd772abf34 | |||
| b074d719ae | |||
| a43a40b778 | |||
| 2ceeb739f3 | |||
| fa715abf45 | |||
| f943e377e8 | |||
| 30084c1369 | |||
| 668a33da34 | |||
| 19e2347759 | |||
| f17292e1b5 | |||
| 77b4330011 |
@@ -30,7 +30,7 @@ https://plnkr.co or similar (you can use this template as a starting point: http
|
||||
<!-- Check whether this is still an issue in the most recent stable or in the snapshot AngularJS
|
||||
version (https://code.angularjs.org/snapshot/) -->
|
||||
|
||||
**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
|
||||
**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView | Opera XX ]
|
||||
<!-- All browsers where this could be reproduced (and Operating System if relevant) -->
|
||||
|
||||
**Anything else:**
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ env:
|
||||
- JOB=ci-checks
|
||||
- JOB=unit-core BROWSER_PROVIDER=saucelabs
|
||||
- JOB=unit-jquery BROWSER_PROVIDER=saucelabs
|
||||
- JOB=unit-modules BROWSER_PROVIDER=saucelabs
|
||||
- JOB=docs-app BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
|
||||
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
|
||||
@@ -26,7 +27,7 @@ env:
|
||||
- secure: oTBjhnOKhs0qDSKTf7fE4f6DYiNDPycvB7qfSF5QRIbJK/LK/J4UtFwetXuXj79HhUZG9qnoT+5e7lPaiaMlpsIKn9ann7ffqFWN1E8TMtpJF+AGigx3djYElwfgf5nEnFUFhwjFzvbfpZNnxVGgX5YbIZpe/WUbHkP4ffU0Wks=
|
||||
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.10.1
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
before_script:
|
||||
|
||||
+95
-2
@@ -1,3 +1,96 @@
|
||||
<a name="1.7.6"></a>
|
||||
# 1.7.6 gravity-manipulation (2019-01-17)
|
||||
|
||||
## Bug Fixes
|
||||
- **$compile:** fix ng-prop-* with undefined values
|
||||
([772440](https://github.com/angular/angular.js/commit/772440cdaf9a9bfa40de1675e20a5f0e356089ed),
|
||||
[#16797](https://github.com/angular/angular.js/issues/16797),
|
||||
[#16798](https://github.com/angular/angular.js/issues/16798))
|
||||
- **compile:** properly handle false value for boolean attrs with jQuery
|
||||
([27486b](https://github.com/angular/angular.js/commit/27486bd15e70946ece2ba713e4e8654b7f9bddad),
|
||||
[#16778](https://github.com/angular/angular.js/issues/16778),
|
||||
[#16779](https://github.com/angular/angular.js/issues/16779))
|
||||
- **ngRepeat:**
|
||||
- fix reference to last collection value remaining across linkages
|
||||
([cf919a](https://github.com/angular/angular.js/commit/cf919a6fb7fc655f3fa37a74899a797ea5b8073e))
|
||||
- fix trackBy function being invoked with incorrect scope
|
||||
([d4d103](https://github.com/angular/angular.js/commit/d4d1031bcd9b30ae6a58bd60a79bcc9d20f0f2b7),
|
||||
[#16776](https://github.com/angular/angular.js/issues/16776),
|
||||
[#16777](https://github.com/angular/angular.js/issues/16777))
|
||||
- **aria/ngClick:** check if element is `contenteditable` before blocking spacebar
|
||||
([289374](https://github.com/angular/angular.js/commit/289374a43c1b2fd715ddf7455db225b17afebbaf),
|
||||
[#16762](https://github.com/angular/angular.js/issues/16762))
|
||||
- **input:** prevent browsers from autofilling hidden inputs
|
||||
([7cbb10](https://github.com/angular/angular.js/commit/7cbb1044fcb3576cdad791bd22ebea3dfd533ff8))
|
||||
- **Angular:** add workaround for Safari / Webdriver problem
|
||||
([eb49f6](https://github.com/angular/angular.js/commit/eb49f6b7555cfd7ab03fd35581adb6b4bd49044e))
|
||||
- **$browser:** normalize inputted URLs
|
||||
([2f72a6](https://github.com/angular/angular.js/commit/2f72a69ded53a122afad3ec28d91f9bd2f41eb4f),
|
||||
[#16606](https://github.com/angular/angular.js/issues/16606))
|
||||
- **interpolate:** do not create directives for constant media URL attributes
|
||||
([90a41d](https://github.com/angular/angular.js/commit/90a41d415c83abdbf28317f49df0fd0a7e07db86),
|
||||
[#16734](https://github.com/angular/angular.js/issues/16734))
|
||||
- **$q:** allow third-party promise libraries
|
||||
([eefaa7](https://github.com/angular/angular.js/commit/eefaa76a90dbef08fdc7d734a205cc2de50d9f91),
|
||||
[#16164](https://github.com/angular/angular.js/issues/16164),
|
||||
[#16471](https://github.com/angular/angular.js/issues/16471))
|
||||
- **urlUtils:** make IPv6 URL's hostname wrapped in square brackets in IE/Edge
|
||||
([0e1bd7](https://github.com/angular/angular.js/commit/0e1bd7822e61822a48b8fd7ba5913a8702e6dabf),
|
||||
[#16692](https://github.com/angular/angular.js/issues/16692),
|
||||
[#16715](https://github.com/angular/angular.js/issues/16715))
|
||||
- **ngAnimateSwap:** make it compatible with `ngIf` on the same element
|
||||
([b27080](https://github.com/angular/angular.js/commit/b27080d52546409fb4e483f212f03616e2ca8037),
|
||||
[#16616](https://github.com/angular/angular.js/issues/16616),
|
||||
[#16729](https://github.com/angular/angular.js/issues/16729))
|
||||
- **ngMock:** make matchLatestDefinitionEnabled work
|
||||
([3cdffc](https://github.com/angular/angular.js/commit/3cdffcecbae71189b4db69b57fadda6608a23b61),
|
||||
[#16702](https://github.com/angular/angular.js/issues/16702))
|
||||
- **ngStyle:** skip setting empty value when new style has the property
|
||||
([d6098e](https://github.com/angular/angular.js/commit/d6098eeb1c9510d599e9bd3cfdba7dd21e7a55a5),
|
||||
[#16709](https://github.com/angular/angular.js/issues/16709))
|
||||
|
||||
## Performance Improvements
|
||||
- **input:** prevent multiple validations on initialization
|
||||
([692622](https://github.com/angular/angular.js/commit/69262239632027b373258e75c670b89132ad9edb),
|
||||
[#14691](https://github.com/angular/angular.js/issues/14691),
|
||||
[#16760](https://github.com/angular/angular.js/issues/16760))
|
||||
|
||||
|
||||
|
||||
<a name="1.7.5"></a>
|
||||
# 1.7.5 anti-prettification (2018-10-04)
|
||||
|
||||
## Bug Fixes
|
||||
- **ngClass:** do not break on invalid values
|
||||
([f3a565](https://github.com/angular/angular.js/commit/f3a565872d802c94bb213944791b11b483d52f73),
|
||||
[#16697](https://github.com/angular/angular.js/issues/16697),
|
||||
[#16699](https://github.com/angular/angular.js/issues/16699))
|
||||
|
||||
|
||||
<a name="1.7.4"></a>
|
||||
# 1.7.4 interstellar-exploration (2018-09-07)
|
||||
|
||||
## Bug Fixes
|
||||
- **ngAria.ngClick:** prevent default event on space/enter only for non-interactive elements
|
||||
([61b335](https://github.com/angular/angular.js/commit/61b33543ff8e7f32464dec98a46bf0a35e9b03a4),
|
||||
[#16664](https://github.com/angular/angular.js/issues/16664),
|
||||
[#16680](https://github.com/angular/angular.js/issues/16680))
|
||||
- **ngAnimate:** remove the "prepare" classes with multiple structural animations
|
||||
([3105b2](https://github.com/angular/angular.js/commit/3105b2c26a71594c4e7904efc18f4b2e9da25b1b),
|
||||
[#16681](https://github.com/angular/angular.js/issues/16681),
|
||||
[#16677](https://github.com/angular/angular.js/issues/16677))
|
||||
- **$route:** correctly extract path params if the path contains a question mark or a hash
|
||||
([2ceeb7](https://github.com/angular/angular.js/commit/2ceeb739f35e01fcebcabac4beeeb7684ae9f86d))
|
||||
- **ngHref:** allow numbers and other objects in interpolation
|
||||
([30084c](https://github.com/angular/angular.js/commit/30084c13699c814ff6703d7aa2d3947a9b2f7067),
|
||||
[#16652](https://github.com/angular/angular.js/issues/16652),
|
||||
[#16626](https://github.com/angular/angular.js/issues/16626))
|
||||
- **select:** allow to select first option with value `undefined`
|
||||
([668a33](https://github.com/angular/angular.js/commit/668a33da3439f17e61dfa8f6d9b114ebde8c9d87),
|
||||
[#16653](https://github.com/angular/angular.js/issues/16653),
|
||||
[#16656](https://github.com/angular/angular.js/issues/16656))
|
||||
|
||||
|
||||
<a name="1.7.3"></a>
|
||||
# 1.7.3 eventful-proposal (2018-08-03)
|
||||
|
||||
@@ -712,7 +805,7 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var maxValidator = ctrl.$validators.max;
|
||||
|
||||
ctrk.$validators.max = function(modelValue, viewValue) {
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return maxValidator(modelValue, modelValue);
|
||||
};
|
||||
}
|
||||
@@ -1545,7 +1638,7 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var maxValidator = ctrl.$validators.max;
|
||||
|
||||
ctrk.$validators.max = function(modelValue, viewValue) {
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return maxValidator(modelValue, modelValue);
|
||||
};
|
||||
}
|
||||
|
||||
+56
-3
@@ -4,6 +4,7 @@ var serveFavicon = require('serve-favicon');
|
||||
var serveStatic = require('serve-static');
|
||||
var serveIndex = require('serve-index');
|
||||
var files = require('./angularFiles').files;
|
||||
var mergeFilesFor = require('./angularFiles').mergeFilesFor;
|
||||
var util = require('./lib/grunt/utils.js');
|
||||
var versionInfo = require('./lib/versions/version-info');
|
||||
var path = require('path');
|
||||
@@ -30,7 +31,7 @@ if (!semver.satisfies(currentYarnVersion, expectedYarnVersion)) {
|
||||
}
|
||||
|
||||
// Grunt CLI version checks
|
||||
var expectedGruntVersion = pkg.engines.grunt;
|
||||
var expectedGruntVersion = pkg.engines['grunt-cli'];
|
||||
var currentGruntVersions = exec('grunt --version', {silent: true}).stdout;
|
||||
var match = /^grunt-cli v(.+)$/m.exec(currentGruntVersions);
|
||||
if (!match) {
|
||||
@@ -141,7 +142,9 @@ module.exports = function(grunt) {
|
||||
'jquery-2.2': 'karma-jquery-2.2.conf.js',
|
||||
'jquery-2.1': 'karma-jquery-2.1.conf.js',
|
||||
docs: 'karma-docs.conf.js',
|
||||
modules: 'karma-modules.conf.js'
|
||||
modules: 'karma-modules.conf.js',
|
||||
'modules-ngAnimate': 'karma-modules-ngAnimate.conf.js',
|
||||
'modules-ngMock': 'karma-modules-ngMock.conf.js'
|
||||
},
|
||||
|
||||
|
||||
@@ -211,6 +214,12 @@ module.exports = function(grunt) {
|
||||
dest: 'build/angular-touch.js',
|
||||
src: util.wrap(files['angularModules']['ngTouch'], 'module')
|
||||
},
|
||||
touchModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-touch.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngTouch'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
mocks: {
|
||||
dest: 'build/angular-mocks.js',
|
||||
src: util.wrap(files['angularModules']['ngMock'], 'module'),
|
||||
@@ -220,18 +229,42 @@ module.exports = function(grunt) {
|
||||
dest: 'build/angular-sanitize.js',
|
||||
src: util.wrap(files['angularModules']['ngSanitize'], 'module')
|
||||
},
|
||||
sanitizeModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-sanitize.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngSanitize'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
resource: {
|
||||
dest: 'build/angular-resource.js',
|
||||
src: util.wrap(files['angularModules']['ngResource'], 'module')
|
||||
},
|
||||
resourceModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-resource.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngResource'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
messageformat: {
|
||||
dest: 'build/angular-message-format.js',
|
||||
src: util.wrap(files['angularModules']['ngMessageFormat'], 'module')
|
||||
},
|
||||
messageformatModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-message-format.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngMessageFormat'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
messages: {
|
||||
dest: 'build/angular-messages.js',
|
||||
src: util.wrap(files['angularModules']['ngMessages'], 'module')
|
||||
},
|
||||
messagesModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-messages.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngMessages'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
animate: {
|
||||
dest: 'build/angular-animate.js',
|
||||
src: util.wrap(files['angularModules']['ngAnimate'], 'module')
|
||||
@@ -240,14 +273,32 @@ module.exports = function(grunt) {
|
||||
dest: 'build/angular-route.js',
|
||||
src: util.wrap(files['angularModules']['ngRoute'], 'module')
|
||||
},
|
||||
routeModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-route.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngRoute'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
cookies: {
|
||||
dest: 'build/angular-cookies.js',
|
||||
src: util.wrap(files['angularModules']['ngCookies'], 'module')
|
||||
},
|
||||
cookiesModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-cookies.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngCookies'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
aria: {
|
||||
dest: 'build/angular-aria.js',
|
||||
src: util.wrap(files['angularModules']['ngAria'], 'module')
|
||||
},
|
||||
ariaModuleTestBundle: {
|
||||
dest: 'build/test-bundles/angular-aria.js',
|
||||
prefix: 'src/module.prefix',
|
||||
src: mergeFilesFor('karmaModules-ngAria'),
|
||||
suffix: 'src/module.suffix'
|
||||
},
|
||||
parseext: {
|
||||
dest: 'build/angular-parse-ext.js',
|
||||
src: util.wrap(files['angularModules']['ngParseExt'], 'module')
|
||||
@@ -430,7 +481,9 @@ module.exports = function(grunt) {
|
||||
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
|
||||
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', [
|
||||
'build',
|
||||
'tests:modules'
|
||||
'tests:modules',
|
||||
'tests:modules-ngAnimate',
|
||||
'tests:modules-ngMock'
|
||||
]);
|
||||
grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']);
|
||||
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', [
|
||||
|
||||
@@ -14,9 +14,9 @@ piece of cake. Best of all? It makes development fun!
|
||||
|
||||
--------------------
|
||||
|
||||
##### AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018: [Find out more](https://docs.angularjs.org/misc/version-support-status)
|
||||
**On July 1, 2018 AngularJS entered a 3 year Long Term Support period:** [Find out more](https://docs.angularjs.org/misc/version-support-status)
|
||||
|
||||
##### Looking for the new Angular? Go here: https://github.com/angular/angular
|
||||
**Looking for the new Angular? Go here:** https://github.com/angular/angular
|
||||
|
||||
--------------------
|
||||
|
||||
|
||||
Vendored
+74
-13
@@ -189,21 +189,72 @@ var angularFiles = {
|
||||
'src/angular.bind.js'
|
||||
],
|
||||
|
||||
'karmaModules': [
|
||||
'karmaModules-ngAnimate': [
|
||||
'build/angular.js',
|
||||
'@angularSrcModules',
|
||||
'build/angular-mocks.js',
|
||||
'test/modules/no_bootstrap.js',
|
||||
'test/helpers/*.js',
|
||||
'test/ngAnimate/*.js',
|
||||
'test/ngMessageFormat/*.js',
|
||||
'test/ngMessages/*.js',
|
||||
'test/ngMock/*.js',
|
||||
'test/ngCookies/*.js',
|
||||
'test/ngRoute/**/*.js',
|
||||
'test/ngResource/*.js',
|
||||
'test/ngSanitize/**/*.js',
|
||||
'test/ngTouch/**/*.js',
|
||||
'test/ngAria/*.js'
|
||||
'test/helpers/matchers.js',
|
||||
'test/helpers/privateMocks.js',
|
||||
'test/helpers/support.js',
|
||||
'test/helpers/testabilityPatch.js',
|
||||
'@angularSrcModuleNgAnimate',
|
||||
'test/ngAnimate/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngAria': [
|
||||
'@angularSrcModuleNgAria',
|
||||
'test/ngAria/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngCookies': [
|
||||
'@angularSrcModuleNgCookies',
|
||||
'test/ngCookies/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngMessageFormat': [
|
||||
'@angularSrcModuleNgMessageFormat',
|
||||
'test/ngMessageFormat/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngMessages': [
|
||||
'build/angular-animate.js',
|
||||
'@angularSrcModuleNgMessages',
|
||||
'test/ngMessages/**/*.js'
|
||||
],
|
||||
|
||||
// ngMock doesn't include the base because it must use the ngMock src files
|
||||
'karmaModules-ngMock': [
|
||||
'build/angular.js',
|
||||
'src/ngMock/*.js',
|
||||
'test/modules/no_bootstrap.js',
|
||||
'test/helpers/matchers.js',
|
||||
'test/helpers/privateMocks.js',
|
||||
'test/helpers/support.js',
|
||||
'test/helpers/testabilityPatch.js',
|
||||
'src/routeToRegExp.js',
|
||||
'build/angular-animate.js',
|
||||
'test/ngMock/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngResource': [
|
||||
'@angularSrcModuleNgResource',
|
||||
'test/ngResource/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngRoute': [
|
||||
'build/angular-animate.js',
|
||||
'@angularSrcModuleNgRoute',
|
||||
'test/ngRoute/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngSanitize': [
|
||||
'@angularSrcModuleNgSanitize',
|
||||
'test/ngSanitize/**/*.js'
|
||||
],
|
||||
|
||||
'karmaModules-ngTouch': [
|
||||
'@angularSrcModuleNgTouch',
|
||||
'test/ngTouch/**/*.js'
|
||||
],
|
||||
|
||||
'karmaJquery': [
|
||||
@@ -232,6 +283,16 @@ var angularFiles = {
|
||||
});
|
||||
});
|
||||
|
||||
angularFiles['angularSrcModuleNgAnimate'] = angularFiles['angularModules']['ngAnimate'];
|
||||
angularFiles['angularSrcModuleNgAria'] = angularFiles['angularModules']['ngAria'];
|
||||
angularFiles['angularSrcModuleNgCookies'] = angularFiles['angularModules']['ngCookies'];
|
||||
angularFiles['angularSrcModuleNgMessageFormat'] = angularFiles['angularModules']['ngMessageFormat'];
|
||||
angularFiles['angularSrcModuleNgMessages'] = angularFiles['angularModules']['ngMessages'];
|
||||
angularFiles['angularSrcModuleNgResource'] = angularFiles['angularModules']['ngResource'];
|
||||
angularFiles['angularSrcModuleNgRoute'] = angularFiles['angularModules']['ngRoute'];
|
||||
angularFiles['angularSrcModuleNgSanitize'] = angularFiles['angularModules']['ngSanitize'];
|
||||
angularFiles['angularSrcModuleNgTouch'] = angularFiles['angularModules']['ngTouch'];
|
||||
|
||||
angularFiles['angularSrcModules'] = [].concat(
|
||||
angularFiles['angularModules']['ngAnimate'],
|
||||
angularFiles['angularModules']['ngMessageFormat'],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
|
||||
.config(function($animateProvider) {
|
||||
$animateProvider.classNameFilter(/animate-/);
|
||||
})
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'classfilter';
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', [])
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'noanimate';
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('repeatAnimateBenchmark', ['ngAnimate'])
|
||||
.run(function($rootScope) {
|
||||
$rootScope.fileType = 'default';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* eslint-env node */
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
scripts: [
|
||||
{
|
||||
id: 'angular',
|
||||
src: '/build/angular.js'
|
||||
},
|
||||
{
|
||||
id: 'angular-animate',
|
||||
src: '/build/angular-animate.js'
|
||||
},
|
||||
{
|
||||
id: 'app',
|
||||
src: 'app.js'
|
||||
},
|
||||
{
|
||||
src: 'common.js'
|
||||
}]
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
var app = angular.module('repeatAnimateBenchmark');
|
||||
|
||||
app.config(function($compileProvider, $animateProvider) {
|
||||
if ($compileProvider.debugInfoEnabled) {
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
app.run(function($animate) {
|
||||
if ($animate.enabled) {
|
||||
$animate.enabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
app.controller('DataController', function($scope, $rootScope, $animate) {
|
||||
var totalRows = 500;
|
||||
var totalColumns = 20;
|
||||
|
||||
var data = $scope.data = [];
|
||||
|
||||
function fillData() {
|
||||
if ($animate.enabled) {
|
||||
$animate.enabled($scope.benchmarkType !== 'globallyDisabled');
|
||||
}
|
||||
|
||||
for (var i = 0; i < totalRows; i++) {
|
||||
data[i] = [];
|
||||
for (var j = 0; j < totalColumns; j++) {
|
||||
data[i][j] = {
|
||||
i: i
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'enter',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
fillData();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
benchmarkSteps.push({
|
||||
name: 'leave',
|
||||
fn: function() {
|
||||
$scope.$apply(function() {
|
||||
data = $scope.data = [];
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.directive('disableAnimations', function($animate) {
|
||||
return {
|
||||
link: {
|
||||
pre: function(s, e) {
|
||||
$animate.enabled(e, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('noop', function($animate) {
|
||||
return {
|
||||
link: {
|
||||
pre: angular.noop
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
app.directive('baseline', function($document) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, $element) {
|
||||
var document = $document[0];
|
||||
|
||||
var i, j, row, cell, comment;
|
||||
var template = document.createElement('span');
|
||||
template.setAttribute('ng-repeat', 'foo in foos');
|
||||
template.classList.add('ng-scope');
|
||||
template.appendChild(document.createElement('span'));
|
||||
template.appendChild(document.createTextNode(':'));
|
||||
|
||||
function createList() {
|
||||
for (i = 0; i < $scope.data.length; i++) {
|
||||
row = document.createElement('div');
|
||||
$element[0].appendChild(row);
|
||||
for (j = 0; j < $scope.data[i].length; j++) {
|
||||
cell = template.cloneNode(true);
|
||||
row.appendChild(cell);
|
||||
cell.childNodes[0].textContent = i;
|
||||
cell.ng339 = 'xxx';
|
||||
comment = document.createComment('ngRepeat end: bar in foo');
|
||||
row.appendChild(comment);
|
||||
}
|
||||
|
||||
comment = document.createComment('ngRepeat end: foo in foos');
|
||||
$element[0].appendChild(comment);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.$watch('data.length', function(newVal) {
|
||||
if (newVal === 0) {
|
||||
while ($element[0].firstChild) {
|
||||
$element[0].removeChild($element[0].firstChild);
|
||||
}
|
||||
} else {
|
||||
createList();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,70 @@
|
||||
<div ng-app="repeatAnimateBenchmark" ng-cloak>
|
||||
<div ng-controller="DataController">
|
||||
<div class="container-fluid">
|
||||
<p>
|
||||
Tests rendering of an ngRepeat with 500 elements.<br>
|
||||
Animations can be enabled / disabled in different ways.<br>
|
||||
Two tests require reloading the app with different module / app configurations.
|
||||
</p>
|
||||
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="none">none: </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" value="baseline">baseline (vanilla Javascript): </label></div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="enabled">enabled : </label> (requires <a href="./">app.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default' && fileType !== 'classfilter'" value="globallyDisabled">globally disabled:</label> (requires <a href="./">app.js</a> or <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'default'" value="disabledParentElement">disabled by $animate.enabled() on parent element: </label> (requires <a href="./">app.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'noanimate'" value="noanimate">Without ngAnimate:</label> (requires <a href="?app=app-noanimate.js">app-noanimate.js</a>)</div>
|
||||
<div><label><input type="radio" ng-model="benchmarkType" ng-disabled="fileType !== 'classfilter'" value="disabledClassFilter">disabled by classNameFilter on element:</label> (requires <a href="?app=app-classfilter.js">app-classfilter.js</a>)</div>
|
||||
|
||||
<ng-switch on="benchmarkType">
|
||||
<baseline ng-switch-when="baseline">
|
||||
</baseline>
|
||||
<div ng-switch-when="noanimate">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="enabled">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="globallyDisabled">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="disabledClassFilter">
|
||||
<div noop>
|
||||
<div ng-repeat="row in data">
|
||||
<span class="disable-animations" ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="disabledParentElement">
|
||||
<div disable-animations>
|
||||
<div ng-repeat="row in data">
|
||||
<span ng-repeat="column in row">
|
||||
<span>{{column.i}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-switch>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,7 +15,7 @@ var cdnUrl = googleCdnUrl + versionInfo.cdnVersion;
|
||||
// docs.angularjs.org and code.angularjs.org need them.
|
||||
var versionPath = versionInfo.currentVersion.isSnapshot ?
|
||||
'snapshot' :
|
||||
(versionInfo.currentVersion.version || versionInfo.currentVersion.version);
|
||||
versionInfo.currentVersion.version;
|
||||
var examplesDependencyPath = angularCodeUrl + versionPath + '/';
|
||||
|
||||
module.exports = function productionDeployment(getVersion) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if method.this %}
|
||||
<h4>Method's {% code %}this{% endcode %}</h4>
|
||||
<h4>Method's `this`</h4>
|
||||
{$ method.this | marked $}
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% if doc.this %}
|
||||
<h3>Method's {% code %}this{% endcode %}</h3>
|
||||
<h3>Method's `this`</h3>
|
||||
{$ doc.this | marked $}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# AngularJS API Docs
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**AngularJS will be moving to Long Term Support (LTS) mode on July 1st 2018.**: [Find out more](misc/version-support-status).
|
||||
**On July 1, 2018 AngularJS entered a 3 year Long Term Support period:** [Find out more](misc/version-support-status).
|
||||
</div>
|
||||
|
||||
## Welcome to the AngularJS API docs page.
|
||||
|
||||
@@ -186,7 +186,7 @@ Right now, the `InvoiceController` contains all logic of our example. When the a
|
||||
is a good practice to move view-independent logic from the controller into a
|
||||
<a name="service">{@link services service}</a>, so it can be reused by other parts
|
||||
of the application as well. Later on, we could also change that service to load the exchange rates
|
||||
from the web, e.g. by calling the [Fixer.io](http://fixer.io) exchange rate API, without changing the controller.
|
||||
from the web, e.g. by calling the [exchangeratesapi.io](https://exchangeratesapi.io) exchange rate API, without changing the controller.
|
||||
|
||||
Let's refactor our example and move the currency conversion into a service in another file:
|
||||
|
||||
@@ -300,7 +300,7 @@ to something shorter like `a`.
|
||||
|
||||
## Accessing the backend
|
||||
|
||||
Let's finish our example by fetching the exchange rates from the [Fixer.io](http://fixer.io) exchange rate API.
|
||||
Let's finish our example by fetching the exchange rates from the [exchangeratesapi.io](https://exchangeratesapi.io) exchange rate API.
|
||||
The following example shows how this is done with AngularJS:
|
||||
|
||||
<example name="guide-concepts-3" ng-app-included="true">
|
||||
@@ -331,7 +331,7 @@ The following example shows how this is done with AngularJS:
|
||||
};
|
||||
|
||||
var refresh = function() {
|
||||
var url = 'https://api.fixer.io/latest?base=USD&symbols=' + currencies.join(",");
|
||||
var url = 'https://api.exchangeratesapi.io/latest?base=USD&symbols=' + currencies.join(",");
|
||||
return $http.get(url).then(function(response) {
|
||||
usdToForeignRates = response.data.rates;
|
||||
usdToForeignRates['USD'] = 1;
|
||||
|
||||
@@ -43,7 +43,7 @@ In AngularJS applications, you move the job of filling page templates with data
|
||||
|
||||
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}
|
||||
* **Security:** {@link guide/security Security Docs}, {@link ng.$sce Strict Contextual Escaping}, {@link ng.directive:ngCsp Content Security Policy}, {@link ngSanitize.$sanitize $sanitize}, [video](https://www.youtube.com/watch?v=18ifoT-Id54)
|
||||
* **Internationalization and Localization:** {@link guide/i18n AngularJS Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
|
||||
* **Internationalization and Localization:** {@link guide/i18n AngularJS Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](https://blog.novanet.no/creating-multilingual-support-using-angularjs/)
|
||||
* **Touch events:** {@link ngTouch Touch events}
|
||||
* **Accessibility:** {@link guide/accessibility ngAria}
|
||||
|
||||
|
||||
+128
-112
@@ -25,7 +25,7 @@ Additionally, we have removed some long-deprecated modules and APIs.
|
||||
|
||||
The most notable changes are:
|
||||
|
||||
- $resource has now support for request and requestError interceptors
|
||||
- `$resource` has now support for request and requestError interceptors
|
||||
|
||||
- Several deprecated features have been removed:
|
||||
- the `$controllerProvider.allowGlobals()` flag
|
||||
@@ -36,8 +36,8 @@ The most notable changes are:
|
||||
- the complete `ngScenario` module
|
||||
|
||||
Please note that feature development (without breaking changes) has happened in parallel on the
|
||||
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those features
|
||||
that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
|
||||
1.6.x branch, so 1.7 doesn't contain many new features, but you may still benefit from those
|
||||
features that were added (with possible BCs), bugfixes, and a few smaller performance improvements.
|
||||
|
||||
|
||||
<br />
|
||||
@@ -48,11 +48,11 @@ Below is the full list of breaking changes:
|
||||
<a name="migrate1.6to1.7-ng-directives"></a>
|
||||
### Core: _Directives_
|
||||
|
||||
<a name="migrate1.6to1.7-ng-directives-form"></a>
|
||||
|
||||
#### **form**
|
||||
|
||||
**Due to [223de5](https://github.com/angular/angular.js/commit/223de59e988dc0cc8b4ec3a045b7c0735eba1c77)**,
|
||||
forms will now set $submitted on child forms when they are submitted.
|
||||
forms will now set `$submitted` on child forms when they are submitted.
|
||||
For example:
|
||||
```
|
||||
<form name="parentform" ng-submit="$ctrl.submit()">
|
||||
@@ -63,15 +63,16 @@ For example:
|
||||
</form>
|
||||
```
|
||||
|
||||
Submitting this form will set $submitted on "parentform" and "childform".
|
||||
Submitting this form will set `$submitted` on "parentform" and "childform".
|
||||
Previously, it was only set on "parentform".
|
||||
|
||||
This change was introduced because mixing form and ngForm does not create
|
||||
This change was introduced because mixing `form` and `ngForm` does not create
|
||||
logically separate forms, but rather something like input groups.
|
||||
Therefore, child forms should inherit the submission state from their parent form.
|
||||
|
||||
|
||||
#### **input[radio]** and **input[checkbox]**
|
||||
|
||||
**Due to [656c8f](https://github.com/angular/angular.js/commit/656c8fa8f23b1277cc5c214c4d0237f3393afa1e)**,
|
||||
`input[radio]` and `input[checkbox]` now listen to the "change" event instead of the "click" event.
|
||||
Most apps should not be affected, as "change" is automatically fired by browsers after "click"
|
||||
@@ -84,10 +85,10 @@ Two scenarios might need migration:
|
||||
Before this change, custom click event listeners on radio / checkbox would be called after the
|
||||
input element and `ngModel` had been updated, unless they were specifically registered before
|
||||
the built-in click handlers.
|
||||
After this change, they are called before the input is updated, and can call event.preventDefault()
|
||||
to prevent the input from updating.
|
||||
After this change, they are called before the input is updated, and can call
|
||||
`event.preventDefault()` to prevent the input from updating.
|
||||
|
||||
If an app uses a click event listener that expects ngModel to be updated when it is called, it now
|
||||
If an app uses a click event listener that expects `ngModel` to be updated when it is called, it now
|
||||
needs to register a change event listener instead.
|
||||
|
||||
- Triggering click events:
|
||||
@@ -95,57 +96,59 @@ needs to register a change event listener instead.
|
||||
Conventional trigger functions:
|
||||
|
||||
The change event might not be fired when the input element is not attached to the document. This
|
||||
can happen in **tests** that compile input elements and
|
||||
trigger click events on them. Depending on the browser (Chrome and Safari) and the trigger method,
|
||||
the change event will not be fired when the input isn't attached to the document.
|
||||
can happen in **tests** that compile input elements and trigger click events on them. Depending on
|
||||
the browser (Chrome and Safari) and the trigger method, the change event will not be fired when the
|
||||
input isn't attached to the document.
|
||||
|
||||
Before:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
it('should update the model', inject(function($compile, $rootScope) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
With this patch, `$rootScope.checkbox` might not be true, because the click event
|
||||
hasn't triggered the change event. To make the test, work append the inputElm to the app's
|
||||
`$rootElement`, and the `$rootElement` to the `$document`.
|
||||
With this patch, `$rootScope.checkbox` might not be true, because the click event hasn't triggered
|
||||
the change event. To make the test, work append `inputElm` to the app's `$rootElement`, and the
|
||||
`$rootElement` to the `$document`.
|
||||
|
||||
After:
|
||||
|
||||
```js
|
||||
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
it('should update the model', inject(function($compile, $rootScope, $rootElement, $document) {
|
||||
var inputElm = $compile('<input type="checkbox" ng-model="checkbox" />')($rootScope);
|
||||
|
||||
$rootElement.append(inputElm);
|
||||
$document.append($rootElement);
|
||||
$rootElement.append(inputElm);
|
||||
$document.append($rootElement);
|
||||
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
inputElm[0].click(); // Or different trigger mechanisms, such as jQuery.trigger()
|
||||
expect($rootScope.checkbox).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
#### **input\[number\]**
|
||||
|
||||
**Due to [aa3f95](https://github.com/angular/angular.js/commit/aa3f951330ec7b10b43ea884d9b5754e296770ec)**,
|
||||
`input[type=number]` with `ngModel` now validates the input for the `max`/`min` restriction against
|
||||
the `ngModelController.$viewValue` instead of against the `ngModelController.$modelValue`.
|
||||
|
||||
This affects apps that use `$parsers` or `$formatters` to transform the input / model value.
|
||||
|
||||
If you rely on the $modelValue validation, you can overwrite the `min`/`max` validator from a custom directive, as seen in the following example directive definition object:
|
||||
If you rely on the `$modelValue` validation, you can overwrite the `min`/`max` validator from a
|
||||
custom directive, as seen in the following example directive definition object:
|
||||
|
||||
```
|
||||
```js
|
||||
{
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var maxValidator = ctrl.$validators.max;
|
||||
|
||||
ctrk.$validators.max = function(modelValue, viewValue) {
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return maxValidator(modelValue, modelValue);
|
||||
};
|
||||
}
|
||||
@@ -154,11 +157,11 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
|
||||
|
||||
|
||||
#### **ngModel, input**
|
||||
|
||||
**Due to [74b04c](https://github.com/angular/angular.js/commit/74b04c9403af4fc7df5b6420f22c9f45a3e84140)**,
|
||||
*Custom* parsers that fail to parse on input types "email", "url", "number", "date", "month",
|
||||
"time", "datetime-local", "week", no longer set `ngModelController.$error[inputType]`, and
|
||||
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" do no
|
||||
longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
|
||||
the `ng-invalid-[inputType]` class. Also, custom parsers on input type "range" no longer set `ngModelController.$error.number` and the `ng-invalid-number` class.
|
||||
|
||||
Instead, any custom parsers on these inputs set `ngModelController.$error.parse` and
|
||||
`ng-invalid-parse`. This change was made to make distinguishing errors from built-in parsers
|
||||
@@ -166,6 +169,7 @@ and custom parsers easier.
|
||||
|
||||
|
||||
#### **ngModelOptions**
|
||||
|
||||
**Due to [55ba44](https://github.com/angular/angular.js/commit/55ba44913e02650b56410aa9ab5eeea5d3492b68)**,
|
||||
the 'default' key in 'debounce' now only debounces the default event, i.e. the event that is added
|
||||
as an update trigger by the different input directives automatically.
|
||||
@@ -179,24 +183,24 @@ See the following example:
|
||||
|
||||
Pre-1.7:
|
||||
'mouseup' is also debounced by 500 milliseconds because 'default' is applied:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500, 'blur': 0 }
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
1.7:
|
||||
The pre-1.7 behavior can be re-created by setting '*' as a catch-all debounce value:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { '*': 500, 'blur': 0 }
|
||||
}
|
||||
}"
|
||||
```
|
||||
|
||||
In contrast, when only 'default' is used, 'blur' and 'mouseup' are not debounced:
|
||||
```
|
||||
```html
|
||||
ng-model-options="{
|
||||
updateOn: 'default blur mouseup',
|
||||
debounce: { 'default': 500 }
|
||||
@@ -207,14 +211,15 @@ ng-model-options="{
|
||||
#### **ngStyle**
|
||||
|
||||
**Due to [15bbd3](https://github.com/angular/angular.js/commit/15bbd3e18cd89b91f7206a06c73d40e54a8a48a0)**,
|
||||
previously the use of deep watch by ng-style would trigger styles to be
|
||||
re-applied when nested state changed. Now only changes to direct
|
||||
properties of the watched object will trigger changes.
|
||||
the use of deep-watching in `ngStyle` has changed. Previously, `ngStyle` would trigger styles to be
|
||||
re-applied whenever nested state changed. Now, only changes to direct properties of the watched
|
||||
object will trigger changes.
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-services"></a>
|
||||
### Core: _Services_
|
||||
|
||||
|
||||
#### **$compile**
|
||||
|
||||
**Due to [38f8c9](https://github.com/angular/angular.js/commit/38f8c97af74649ce224b6dd45f433cc665acfbfb)**,
|
||||
@@ -238,16 +243,16 @@ migrating to AngularJS 1.7.0 shouldn't require any further action.
|
||||
3. If you specified `$compileProvider.preAssignBindingsEnabled(true)` you need
|
||||
to first migrate your code so that the flag can be flipped to `false`. The
|
||||
instructions on how to do that are available in the "Migrating from 1.5 to 1.6"
|
||||
guide:
|
||||
https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
|
||||
guide: https://docs.angularjs.org/guide/migration#migrating-from-1-5-to-1-6
|
||||
Afterwards, remove the `$compileProvider.preAssignBindingsEnabled(true)`
|
||||
statement.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [6ccbfa](https://github.com/angular/angular.js/commit/6ccbfa65d60a3dc396d0cf6da21b993ad74653fd)**,
|
||||
the `xlink:href` security context for SVG's `a` and `image` elements has been lowered.
|
||||
|
||||
In the unlikely case that an app relied on RESOURCE_URL whitelisting for the
|
||||
In the unlikely case that an app relied on `RESOURCE_URL` whitelisting for the
|
||||
purpose of binding to the `xlink:href` property of SVG's `<a>` or `<image>`
|
||||
elements and if the values do not pass the regular URL sanitization, they will
|
||||
break.
|
||||
@@ -258,37 +263,39 @@ To fix this you need to ensure that the values used for binding to the affected
|
||||
`imgSrcSanitizationWhitelist` (for `<image>` elements).
|
||||
|
||||
<hr />
|
||||
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
|
||||
deepWatch is no longer used in in literal one-way bindings.
|
||||
|
||||
Previously when a literal value was passed into a directive/component via
|
||||
**Due to [fd4f01](https://github.com/angular/angular.js/commit/fd4f0111188b62773b99ab6eab38b4d2b5d8d727)**,
|
||||
deep-watching is no longer used in literal one-way bindings.
|
||||
|
||||
Previously, when a literal value was passed into a directive/component via
|
||||
one-way binding it would be watched with a deep watcher.
|
||||
|
||||
For example, for `<my-component input="[a]">`, a new instance of the array
|
||||
would be passed into the directive/component (and trigger $onChanges) not
|
||||
would be passed into the directive/component (and trigger `$onChanges`) not
|
||||
only if `a` changed but also if any sub property of `a` changed such as
|
||||
`a.b` or `a.b.c.d.e` etc.
|
||||
|
||||
This also means a new but equal value for `a` would NOT trigger such a
|
||||
change.
|
||||
|
||||
Now literal values use an input-based watch similar to other directive/component
|
||||
Now, literal values use an input-based watch similar to other directive/component
|
||||
one-way bindings. In this context inputs are the non-constant parts of the
|
||||
literal. In the example above the input would be `a`. Changes are only
|
||||
triggered when the inputs to the literal change.
|
||||
literal. In the example above, the input would be `a`. Changes are only
|
||||
triggered, when the inputs to the literal change.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [1cf728](https://github.com/angular/angular.js/commit/1cf728e209a9e0016068fac2769827e8f747760e)**,
|
||||
`base[href]` was added to the list of RESOURCE_URL context attributes.
|
||||
`base[href]` was added to the list of `RESOURCE_URL` context attributes.
|
||||
|
||||
Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to
|
||||
be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s
|
||||
RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same
|
||||
be trusted as a `RESOURCE_URL`. Now, `baseUrl` will be sent to `$sce`'s
|
||||
`RESOURCE_URL` checks. By default, it will break unless `baseUrl` is of the same
|
||||
origin as the application document.
|
||||
|
||||
Refer to the
|
||||
[`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce)
|
||||
for more info on how to trust a value in a RESOURCE_URL context.
|
||||
for more info on how to trust a value in a `RESOURCE_URL` context.
|
||||
|
||||
Also, concatenation in trusted contexts is not allowed, which means that the
|
||||
following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`.
|
||||
@@ -315,10 +322,10 @@ except for the simplest of cases):
|
||||
**Due to ([c2b8fa](https://github.com/angular/angular.js/commit/c2b8fab0a480204374d561d6b9b3d47347ac5570))**,
|
||||
the arguments of `$watchGroup` callbacks have changed.
|
||||
|
||||
Previously when using `$watchGroup` the entries in `newValues` and
|
||||
Previously, when using `$watchGroup`, the entries in `newValues` and
|
||||
`oldValues` represented the *most recent change of each entry*.
|
||||
|
||||
Now the entries in `oldValues` will always equal the `newValues` of the previous
|
||||
Now, the entries in `oldValues` will always equal the `newValues` of the previous
|
||||
call of the listener. This means comparing the entries in `newValues` and
|
||||
`oldValues` can be used to determine which individual expressions changed.
|
||||
|
||||
@@ -343,7 +350,7 @@ Now the `oldValue` will always equal the previous `newValue`:
|
||||
|
||||
Note the last call now shows `a === 2` in the `oldValues` array.
|
||||
|
||||
This also makes the `oldValue` of one-time watchers more clear. Previously
|
||||
This also makes the `oldValue` of one-time watchers more clear. Previously,
|
||||
the `oldValue` of a one-time watcher would remain `undefined` forever. For
|
||||
example `$scope.$watchGroup(['a', '::b'], fn)` would previously:
|
||||
|
||||
@@ -367,7 +374,7 @@ Where now the `oldValue` will always equal the previous `newValue`:
|
||||
#### **$interval**
|
||||
|
||||
**Due to [a8bef9](https://github.com/angular/angular.js/commit/a8bef95127775d83d80daa4617c33227c4b443d4)**,
|
||||
`$interval.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$interval.cancel()` will throw an error if called with a promise that was not generated by
|
||||
`$interval()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
@@ -393,7 +400,7 @@ $interval.cancel(promise); // Interval canceled.
|
||||
#### **$timeout**
|
||||
|
||||
**Due to [336525](https://github.com/angular/angular.js/commit/3365256502344970f86355d3ace1cb4251ae9828)**,
|
||||
`$timeout.cancel() will throw an error if called with a promise that was not generated by
|
||||
`$timeout.cancel()` will throw an error if called with a promise that was not generated by
|
||||
`$timeout()`. Previously, it would silently do nothing.
|
||||
|
||||
Before:
|
||||
@@ -417,10 +424,11 @@ $timeout.cancel(promise); // Timeout canceled.
|
||||
|
||||
|
||||
#### **$cookies**
|
||||
|
||||
**Due to [73c646](https://github.com/angular/angular.js/commit/73c6467f1468353215dc689c019ed83aa4993c77)**,
|
||||
the `$cookieStore`service has been removed. Migrate to the $cookies service. Note that
|
||||
for object values you need to use the `putObject` & `getObject` methods as
|
||||
`get`/`put` will not correctly save/retrieve them.
|
||||
the `$cookieStore`service has been removed. Migrate to the `$cookies` service. Note that
|
||||
for object values you need to use the `putObject` & `getObject` methods, as
|
||||
`get`/`put` will not correctly save/retrieve the object values.
|
||||
|
||||
Before:
|
||||
```js
|
||||
@@ -433,29 +441,31 @@ $cookieStore.remove('name');
|
||||
#### **$templateRequest**
|
||||
|
||||
**Due to [c617d6](https://github.com/angular/angular.js/commit/c617d6dceee5b000bfceda44ced22fc16b48b18b)**,
|
||||
give tpload error namespace has changed. Previously the `tpload` error was namespaced to `$compile`.
|
||||
If you have code that matches errors of the form `[$compile:tpload]` it will no
|
||||
longer run. You should change the code to match
|
||||
`[$templateRequest:tpload]`.
|
||||
the `tpload` error namespace has changed. Previously, the `tpload` error was namespaced to
|
||||
`$compile`. If you have code that matches errors of the form `[$compile:tpload]` it will no longer
|
||||
run. You should change the code to match `[$templateRequest:tpload]`.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to ([fb0099](https://github.com/angular/angular.js/commit/fb00991460cf69ae8bc7f1f826363d09c73c0d5e)**,
|
||||
the service now returns the result of `$templateCache.put()` when making a server request to the
|
||||
template. Previously it would return the content of the response directly.
|
||||
This now means if you are decorating `$templateCache.put()` to manipulate the template, you will
|
||||
now get this manipulated result also on the first `$templateRequest` rather than only on subsequent
|
||||
calls (when the template is retrived from the cache).
|
||||
In practice this should not affect any apps, as it is unlikely that they rely on the template being
|
||||
`$templateRequest()` now returns the result of `$templateCache.put()` when making a server request
|
||||
for a template. Previously, it would return the content of the response directly.
|
||||
|
||||
This means that if you are decorating `$templateCache.put()` to manipulate the template, you will
|
||||
now get this manipulated result also on the first `$templateRequest()` call rather than only on
|
||||
subsequent calls (when the template is retrieved from the cache).
|
||||
|
||||
In practice, this should not affect any apps, as it is unlikely that they rely on the template being
|
||||
different in the first and subsequent calls.
|
||||
|
||||
|
||||
#### **$animate**
|
||||
|
||||
**Due to [16b82c](https://github.com/angular/angular.js/commit/16b82c6afe0ab916fef1d6ca78053b00bf5ada83)**,
|
||||
$animate.cancel(runner) now rejects the underlying
|
||||
promise and calls the catch() handler on the runner
|
||||
returned by $animate functions (enter, leave, move,
|
||||
addClass, removeClass, setClass, animate).
|
||||
Previously it would resolve the promise as if the animation
|
||||
had ended successfully.
|
||||
`$animate.cancel(runner)` now rejects the underlying promise and calls the `catch()` handler on the
|
||||
runner returned by `$animate` functions (`enter`, `leave`, `move`, `addClass`, `removeClass`,
|
||||
`setClass`, `animate`).
|
||||
Previously, it would resolve the promise as if the animation had ended successfully.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -468,7 +478,7 @@ runner.cancel();
|
||||
```
|
||||
|
||||
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
|
||||
To migrate, add a catch() handler to your animation runners.
|
||||
To migrate, add a `catch()` handler to your animation runners.
|
||||
|
||||
|
||||
#### **$controller**
|
||||
@@ -479,26 +489,29 @@ has been removed. Likewise, the deprecated `$controllerProvider.allowGlobals()`
|
||||
method that could enable this behavior, has been removed.
|
||||
|
||||
This behavior had been deprecated since AngularJS v1.3.0, because polluting the global scope
|
||||
is bad. To migrate, remove the call to $controllerProvider.allowGlobals() in the config, and
|
||||
register your controller via the Module API or the $controllerProvider, e.g.
|
||||
is considered bad practice. To migrate, remove the call to `$controllerProvider.allowGlobals()` in
|
||||
the config, and register your controller via the Module API or the `$controllerProvider`, e.g.:
|
||||
|
||||
```
|
||||
```js
|
||||
angular.module('myModule', []).controller('myController', function() {...});
|
||||
|
||||
// or
|
||||
|
||||
angular.module('myModule', []).config(function($controllerProvider) {
|
||||
$controllerProvider.register('myController', function() {...});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### **$sce**
|
||||
|
||||
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
if you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
|
||||
if you use `attrs.$set` for URL attributes (`a[href]` and `img[src]`) there will no
|
||||
longer be any automated sanitization of the value. This is in line with other
|
||||
programmatic operations, such as writing to the innerHTML of an element.
|
||||
programmatic operations, such as writing to the `innerHTML` of an element.
|
||||
|
||||
If you are programmatically writing URL values to attributes from untrusted
|
||||
input then you must sanitize it yourself. You could write your own sanitizer or copy
|
||||
input, then you must sanitize it yourself. You could write your own sanitizer or copy
|
||||
the private `$$sanitizeUri` service.
|
||||
|
||||
Note that values that have been passed through the `$interpolate` service within the
|
||||
@@ -507,40 +520,43 @@ these values again.
|
||||
|
||||
<hr/>
|
||||
|
||||
Due to **[1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
**Due to [1e9ead](https://github.com/angular/angular.js/commit/1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9)**,
|
||||
binding {@link ng.$sce#trustAs trustAs()} and the short versions
|
||||
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to
|
||||
{@link ng.ngSrc}, {@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
|
||||
({@link ng.$sce#trustAsResourceUrl trustAsResourceUrl()} et al.) to {@link ng.ngSrc},
|
||||
{@link ng.ngSrcset}, and {@link ng.ngHref} will now raise an infinite digest error:
|
||||
|
||||
```js
|
||||
$scope.imgThumbFn = function(id) {
|
||||
return $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
};
|
||||
$scope.imgThumbFn = function(id) {
|
||||
return $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
};
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumbFn(imgId)}}">
|
||||
<img ng-src="{{ imgThumbFn(imgId) }}" />
|
||||
```
|
||||
|
||||
This is because {@link ng.$interpolate} is now responsible for sanitizing
|
||||
the attribute value, and its watcher receives a new object from `trustAs()`
|
||||
on every digest.
|
||||
To migrate, compute the trusted value only when the input value changes:
|
||||
|
||||
```js
|
||||
$scope.$watch('imgId', function(id) {
|
||||
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
});
|
||||
$scope.$watch('imgId', function(id) {
|
||||
$scope.imgThumb = $sce.trustAsResourceUrl(someService.someUrl(id));
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<img ng-src="{{imgThumb}}">
|
||||
<img ng-src="{{ imgThumb }}" />
|
||||
```
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ng-filters"></a>
|
||||
### Core: _Filters_
|
||||
|
||||
|
||||
#### **orderBy**
|
||||
|
||||
**Due to [1d8046](https://github.com/angular/angular.js/commit/1d804645f7656d592c90216a0355b4948807f6b8)**,
|
||||
when using `orderBy` to sort arrays containing `null` values, the `null` values
|
||||
will be considered "greater than" all other values, except for `undefined`.
|
||||
@@ -565,8 +581,9 @@ orderByFilter(['a', undefined, 'o', null, 'z']);
|
||||
|
||||
|
||||
#### **jqLite**
|
||||
|
||||
**Due to [b7d396](https://github.com/angular/angular.js/commit/b7d396b8b6e8f27a1f4556d58fc903321e8d532a)**,
|
||||
removeData() no longer removes event handlers.
|
||||
`removeData()` no longer removes event handlers.
|
||||
|
||||
Before this commit `removeData()` invoked on an element removed its event
|
||||
handlers as well. If you want to trigger a full cleanup of an element, change:
|
||||
@@ -591,22 +608,20 @@ elem.remove();
|
||||
will remove event handlers as well.
|
||||
|
||||
|
||||
|
||||
#### **Angular**
|
||||
#### **Helpers**
|
||||
|
||||
**Due to [1daa4f](https://github.com/angular/angular.js/commit/1daa4f2231a89ee88345689f001805ffffa9e7de)**,
|
||||
the helper functions `angular.lowercase` `and angular.uppercase` have been removed.
|
||||
the helper functions `angular.lowercase` and `angular.uppercase` have been removed.
|
||||
|
||||
These functions have been deprecated since 1.5.0. They are internally
|
||||
used, but should not be exposed as they contain special locale handling
|
||||
(for Turkish) to maintain internal consistency regardless of user-set locale.
|
||||
|
||||
Developers should generally use the built-ins `toLowerCase` and `toUpperCase`
|
||||
Developers should generally use the built-in methods `toLowerCase` and `toUpperCase`
|
||||
or `toLocaleLowerCase` and `toLocaleUpperCase` for special cases.
|
||||
|
||||
Further, we generally discourage using the angular.x helpers in application code.
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [e3ece2](https://github.com/angular/angular.js/commit/e3ece2fad9e1e6d47b5f06815ff186d7e6f44948)**,
|
||||
`angular.isArray()` now supports Array subclasses.
|
||||
|
||||
@@ -627,18 +642,19 @@ be able to handle these objects better when copying or watching.
|
||||
### ngAria
|
||||
|
||||
**Due to [6d5ef3](https://github.com/angular/angular.js/commit/6d5ef34fc6a974cde73157ba94f9706723dd8f5b)**,
|
||||
the ngAria directive no longer sets aria-* attributes on input[type="hidden"] with ngModel.
|
||||
This can affect apps that test for the presence of aria attributes on hidden inputs.
|
||||
`ngAria` no longer sets `aria-*` attributes on `input[type="hidden"]` with `ngModel`.
|
||||
This can affect apps that test for the presence of ARIA attributes on hidden inputs.
|
||||
To migrate, remove these assertions.
|
||||
In actual apps, this should not have a user-facing effect, as the previous behavior
|
||||
was incorrect, and the new behavior is correct for accessibility.
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngResource"></a>
|
||||
### ngResource
|
||||
|
||||
|
||||
#### **$resource**
|
||||
|
||||
**Due to [ea0585](https://github.com/angular/angular.js/commit/ea0585773bb93fd891576e2271254a17e15f1ddd)**,
|
||||
the behavior of interceptors and success/error callbacks has changed.
|
||||
|
||||
@@ -720,6 +736,7 @@ User.get({id: 2}, onSuccess, onError);
|
||||
```
|
||||
|
||||
<hr />
|
||||
|
||||
**Due to [240a3d](https://github.com/angular/angular.js/commit/240a3ddbf12a9bb79754031be95dae4b6bd2dded)**,
|
||||
`$http` will be called asynchronously from `$resource` methods
|
||||
(regardless if a `request`/`requestError` interceptor has been defined).
|
||||
@@ -758,7 +775,6 @@ it('...', function() {
|
||||
```
|
||||
|
||||
|
||||
|
||||
<a name="migrate1.6to1.7-ngScenario"></a>
|
||||
### ngScenario
|
||||
|
||||
@@ -766,7 +782,7 @@ it('...', function() {
|
||||
the angular scenario runner end-to-end test framework has been
|
||||
removed from the project and will no longer be available on npm
|
||||
or bower starting with 1.7.0.
|
||||
It was deprecated and removed from the documentation in 2014.
|
||||
It has been deprecated and removed from the documentation since 2014.
|
||||
Applications that still use it should migrate to
|
||||
[Protractor](http://www.protractortest.org).
|
||||
Technically, it should also be possible to continue using an
|
||||
@@ -778,10 +794,10 @@ not changed. However, we do not guarantee future compatibility.
|
||||
### ngTouch
|
||||
|
||||
**Due to [11d9ad](https://github.com/angular/angular.js/commit/11d9ad1eb25eaf5967195e424108207427835d50)**,
|
||||
the `ngClick` directive from the ngTouch module has been removed, and with it the
|
||||
the `ngClick` directive of the `ngTouch` module has been removed, and with it the
|
||||
corresponding `$touchProvider` and `$touch` service.
|
||||
|
||||
If you have included ngTouch v1.5.0 or higher in your application, and have not
|
||||
If you have included `ngTouch` v1.5.0 or higher in your application, and have not
|
||||
changed the value of `$touchProvider.ngClickOverrideEnabled()`, or injected and used the `$touch`
|
||||
service, then there are no migration steps for your code. Otherwise you must remove references to
|
||||
the provider and service.
|
||||
|
||||
@@ -102,7 +102,7 @@ For more information please visit {@link $http#json-vulnerability-protection JSO
|
||||
|
||||
Bear in mind that calling `$http.jsonp` gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
|
||||
instant remote code execution in your application: the result of these requests is handed off
|
||||
to the browser as regular `<script>` tag.
|
||||
to the browser as a regular `<script>` tag.
|
||||
|
||||
## Strict Contextual Escaping
|
||||
|
||||
|
||||
@@ -7,26 +7,9 @@
|
||||
This page describes the support status of the significant versions of AngularJS.
|
||||
|
||||
<div class="alert alert-info">
|
||||
AngularJS is planning one more significant release, version 1.7, and on July 1, 2018 it will enter a 3 year Long Term Support period.
|
||||
On July 1, 2018 AngularJS entered a 3 year Long Term Support period.
|
||||
</div>
|
||||
|
||||
### Until July 1st 2018
|
||||
|
||||
Any version branch not shown in the following table (e.g. 1.5.x) is no longer being developed.
|
||||
|
||||
<table class="dev-status table table-bordered">
|
||||
<thead>
|
||||
<tr><th>Version</th><th>Status</th><th>Comments</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="security"><td><span>1.2.x</span></td><td>Security patches only</td><td>Last version to provide IE 8 support</td></tr>
|
||||
<tr class="stable"><td><span>1.6.x</span></td><td>Patch Releases</td><td>Minor features, bug fixes, security patches - no breaking changes</td></tr>
|
||||
<tr class="current"><td><span>1.7.x</span></td><td>Active Development</td><td>1.7.0 (not yet released) will be the last release of AngularJS to contain breaking changes</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
### After July 1st 2018
|
||||
|
||||
Any version branch not shown in the following table (e.g. 1.6.x) is no longer being developed.
|
||||
|
||||
<table class="dev-status table table-bordered">
|
||||
@@ -36,7 +19,7 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
|
||||
<tbody>
|
||||
<tr class="security">
|
||||
<td><span>1.2.x</span></td>
|
||||
<td>Long Term Support</td>
|
||||
<td>Security patches only</td>
|
||||
<td>Last version to provide IE 8 support</td>
|
||||
</tr>
|
||||
<tr class="stable">
|
||||
@@ -49,14 +32,16 @@ Any version branch not shown in the following table (e.g. 1.6.x) is no longer be
|
||||
|
||||
### Long Term Support
|
||||
|
||||
On July 1st 2018, we will enter a Long Term Support period for AngularJS.
|
||||
On July 1st 2018, AngularJS entered a Long Term Support period for AngularJS.
|
||||
|
||||
At this time we will focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
|
||||
We now focus exclusively on providing fixes to bugs that satisfy at least one of the following criteria:
|
||||
|
||||
* A security flaw is detected in the 1.7.x branch of the framework
|
||||
* One of the major browsers releases a version that will cause current production applications using AngularJS 1.7.x to stop working
|
||||
* The jQuery library releases a version that will cause current production applications using AngularJS 1.7.x to stop working.
|
||||
|
||||
AngularJS 1.2.x will get a new version if and only if a new severe security issue is discovered.
|
||||
|
||||
### Blog Post
|
||||
|
||||
You can read more about these plans in our [blog post announcement](https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c).
|
||||
|
||||
@@ -64,7 +64,7 @@ a few git commands.
|
||||
|
||||
### Install Git
|
||||
|
||||
You can download and install Git from http://git-scm.com/download. Once installed, you should have
|
||||
You can download and install Git from https://git-scm.com/download. Once installed, you should have
|
||||
access to the `git` command line tool. The main commands that you will need to use are:
|
||||
|
||||
* `git clone ...`: Clone a remote repository onto your local machine.
|
||||
@@ -99,8 +99,8 @@ The tutorial instructions, from now on, assume you are running all commands from
|
||||
|
||||
### Install Node.js
|
||||
|
||||
If you want to run the preconfigured local web server and the test tools then you will also need
|
||||
[Node.js v4+][node].
|
||||
In order to install dependencies (such as the test tools and AngularJS itself) and run the
|
||||
preconfigured local web server, you will also need [Node.js v6+][node].
|
||||
|
||||
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
|
||||
|
||||
@@ -125,22 +125,25 @@ npm --version
|
||||
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
|
||||
</div>
|
||||
|
||||
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
|
||||
By installing Node.js, you also get [npm][npm], which is a command line executable for downloading
|
||||
and managing Node.js packages. We use it to download the AngularJS framework as well as development
|
||||
and testing tools.
|
||||
|
||||
Once you have Node.js installed on your machine, you can download these dependencies by running:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
|
||||
`node_modules` directory:
|
||||
This command reads angular-phonecat's `package.json` file and downloads the following dependencies
|
||||
into the `node_modules` directory:
|
||||
|
||||
* [Bower][bower] - client-side code package manager
|
||||
* [Http-Server][http-server] - simple local static web server
|
||||
* [Karma][karma] - unit test runner
|
||||
* [Protractor][protractor] - end-to-end (E2E) test runner
|
||||
|
||||
Running `npm install` will also automatically use bower to download the AngularJS framework into the
|
||||
`app/bower_components` directory.
|
||||
Running `npm install` will also automatically copy the AngularJS framework and other dependencies
|
||||
necessary for our app to work into the `app/lib/` directory.
|
||||
|
||||
<div class="alert alert-info">
|
||||
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
|
||||
@@ -160,23 +163,23 @@ tasks that you will need while developing:
|
||||
|
||||
### Install Helper Tools (optional)
|
||||
|
||||
The Bower, Http-Server, Karma and Protractor modules are also executables, which can be installed
|
||||
globally and run directly from a terminal/command prompt. You don't need to do this to follow the
|
||||
tutorial, but if you decide you do want to run them directly, you can install these modules globally
|
||||
using, `sudo npm install -g ...`.
|
||||
The Http-Server, Karma and Protractor modules are also executables, which can be installed globally
|
||||
and run directly from a terminal/command prompt. You don't need to do this to follow the tutorial,
|
||||
but if you decide you do want to run them directly, you can install these modules globally using,
|
||||
`sudo npm install --global ...`.
|
||||
|
||||
For instance, to install the Bower command line executable you would do:
|
||||
For instance, to install the `http-server` command line executable you would do:
|
||||
|
||||
```
|
||||
sudo npm install -g bower
|
||||
sudo npm install --global http-server
|
||||
```
|
||||
|
||||
_(Omit the sudo if running on Windows)_
|
||||
_(Omit the sudo if running on Windows.)_
|
||||
|
||||
Then you can run the bower tool directly, such as:
|
||||
Then you can run the `http-server` tool directly, such as:
|
||||
|
||||
```
|
||||
bower install
|
||||
http-server ./app
|
||||
```
|
||||
|
||||
|
||||
@@ -278,6 +281,45 @@ It is good to run the E2E tests whenever you make changes to the HTML views or w
|
||||
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
|
||||
a new commit of changes to a remote repository.
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<p>
|
||||
Each version of Protractor is compatible with specific browser versions. If you are reading this
|
||||
some time in the future, it is possible that the specified Protractor version is no longer
|
||||
compatible with the latest version of Chrome that you are using.
|
||||
</p>
|
||||
<p>
|
||||
If that is the case, you can try upgrading Protractor to newer version. For instructions on how
|
||||
to upgrade dependencies see [Updating dependencies](tutorial/#updating-dependencies).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
### Updating dependencies
|
||||
|
||||
In order to avoid surprises, all dependencies listed in `package.json` are pinned to specific
|
||||
versions (this is what the [package-lock.json][package-lock] file is about). This ensures that the
|
||||
same version of a dependency is installed every time.
|
||||
|
||||
Since all dependencies are acquired via npm, you can use the same tool to easily update them as
|
||||
well (although you probably don't need to for the purpose of this tutorial). Simply run the
|
||||
preconfigured script:
|
||||
|
||||
```
|
||||
npm run update-deps
|
||||
```
|
||||
|
||||
This will update all packages to the latest version that satisfy their version ranges (as specified
|
||||
in `package.json`) and also copy the necessary files into `app/lib/`. For example, if `package.json`
|
||||
contains `"some-package": "1.2.x"`, it will be updated to the latest 1.2.x version (e.g. 1.2.99),
|
||||
but not to 1.3.x (e.g. 1.3.0).
|
||||
|
||||
If you want to update a dependency to a version newer than what the specificed range would permit,
|
||||
you can change the version range in `package.json` and then run `npm run update-deps` as usual.
|
||||
|
||||
<div class="alert alert-info">
|
||||
See [here][semver-ranges] for more info on the various version range formats.
|
||||
</div>
|
||||
|
||||
|
||||
### Common Issues
|
||||
|
||||
@@ -324,14 +366,16 @@ Now that you have set up your local machine, let's get started with the tutorial
|
||||
|
||||
|
||||
[angular-phonecat]: https://github.com/angular/angular-phonecat
|
||||
[bower]: http://bower.io/
|
||||
[git]: http://git-scm.com/
|
||||
[git]: https://git-scm.com/
|
||||
[http-server]: https://github.com/nodeapps/http-server
|
||||
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
|
||||
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[jdk-download]: https://www.oracle.com/technetwork/java/javase/downloads/index.html
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[node]: http://nodejs.org/
|
||||
[node]: https://nodejs.org/
|
||||
[npm]: https://www.npmjs.com/
|
||||
[nvm]: https://github.com/creationix/nvm
|
||||
[nvm-windows]: https://github.com/coreybutler/nvm-windows
|
||||
[package-lock]: https://docs.npmjs.com/files/package-lock.json
|
||||
[protractor]: https://github.com/angular/protractor
|
||||
[selenium]: http://docs.seleniumhq.org/
|
||||
[selenium]: https://docs.seleniumhq.org/
|
||||
[semver-ranges]: https://docs.npmjs.com/misc/semver#ranges
|
||||
|
||||
@@ -51,8 +51,8 @@ The code contains some key AngularJS elements that we will need as we progress.
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>My HTML File</title>
|
||||
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -84,7 +84,7 @@ For more info on `ngApp`, check out the {@link ngApp API Reference}.
|
||||
**`angular.js` script tag:**
|
||||
|
||||
```html
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
```
|
||||
|
||||
This code downloads the `angular.js` script which registers a callback that will be executed by the
|
||||
@@ -154,8 +154,8 @@ and one static binding, and our model is empty. That will soon change!
|
||||
|
||||
Most of the files in your working directory come from the [angular-seed project][angular-seed],
|
||||
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
|
||||
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
|
||||
for developing and testing a typical web application (via `npm`).
|
||||
install the AngularJS framework (via `npm` into the `app/lib/` directory) and tools for developing
|
||||
and testing a typical web application (via `npm`).
|
||||
|
||||
For the purposes of this tutorial, we modified the angular-seed with the following changes:
|
||||
|
||||
@@ -163,7 +163,7 @@ For the purposes of this tutorial, we modified the angular-seed with the followi
|
||||
* Removed unused dependencies.
|
||||
* Added phone images to `app/img/phones/`.
|
||||
* Added phone data files (JSON) to `app/phones/`.
|
||||
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
|
||||
* Added a dependency on [Bootstrap][bootstrap-3.3] in the `package.json` file.
|
||||
|
||||
|
||||
## Experiments
|
||||
@@ -186,3 +186,4 @@ Now let's go to {@link step_01 step 1} and add some content to the web app.
|
||||
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[bootstrap-3.3]: https://getbootstrap.com/docs/3.3
|
||||
|
||||
@@ -33,7 +33,7 @@ The view is constructed by AngularJS from this template.
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
<body ng-controller="PhoneListController">
|
||||
@@ -317,7 +317,7 @@ by utilizing components.
|
||||
<ul doc-tutorial-nav="2"></ul>
|
||||
|
||||
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[jasmine-docs]: https://jasmine.github.io/api/3.3/global
|
||||
[jasmine-home]: https://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
[mvc-pattern]: https://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -88,6 +88,13 @@ Let's see an example:
|
||||
});
|
||||
```
|
||||
|
||||
```html
|
||||
<body>
|
||||
<!-- The following line is how to use the `greetUser` component above in your html doc. -->
|
||||
<greet-user></greet-user>
|
||||
</body>
|
||||
```
|
||||
|
||||
Now, every time we include `<greet-user></greet-user>` in our view, AngularJS will expand it into a
|
||||
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
|
||||
controller.
|
||||
@@ -120,14 +127,14 @@ acquired skill.
|
||||
<html ng-app="phonecatApp">
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
<script src="app.js"></script>
|
||||
<script src="phone-list.component.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Use a custom component to render a list of phones -->
|
||||
<phone-list></phone-list>
|
||||
<phone-list></phone-list> <!-- This tells AngularJS to instantiate a `phoneList` component here. -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -148,7 +155,7 @@ angular.module('phonecatApp', []);
|
||||
// Register `phoneList` component, along with its associated controller and template
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
component('phoneList', {
|
||||
component('phoneList', { // This name is what AngularJS uses to match to the `<phone-list>` element.
|
||||
template:
|
||||
'<ul>' +
|
||||
'<li ng-repeat="phone in $ctrl.phones">' +
|
||||
@@ -277,7 +284,7 @@ files, so it remains easy to locate as our application grows.
|
||||
|
||||
|
||||
[case-styles]: https://en.wikipedia.org/wiki/Letter_case#Special_case_styles
|
||||
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
|
||||
[jasmine-home]: http://jasmine.github.io/
|
||||
[jasmine-docs]: https://jasmine.github.io/api/3.3/global
|
||||
[jasmine-home]: https://jasmine.github.io/
|
||||
[karma]: https://karma-runner.github.io/
|
||||
[mvc-pattern]: http://en.wikipedia.org/wiki/Model–View–Controller
|
||||
[mvc-pattern]: https://en.wikipedia.org/wiki/Model–View–Controller
|
||||
|
||||
@@ -230,6 +230,8 @@ You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
* Reverse the sort order by adding a `-` symbol before the sorting value:
|
||||
`<option value="-age">Oldest</option>`
|
||||
After making this change, you'll notice that the drop-down list has a blank option selected and does not default to age anymore.
|
||||
Fix this by updating the `orderProp` value in `phone-list.component.js` to match the new value on the `<option>` element.
|
||||
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
|
||||
from our server using one of AngularJS's built-in {@link guide/services services} called
|
||||
{@link ng.$http $http}. We will use AngularJS's {@link guide/di dependency injection (DI)} to provide
|
||||
the service to the `phoneList` component's controller.
|
||||
{@link ng.$http $http}. We will use AngularJS's {@link guide/di dependency injection (DI)} to
|
||||
provide the service to the `phoneList` component's controller.
|
||||
|
||||
* There is now a list of 20 phones, loaded from the server.
|
||||
|
||||
|
||||
@@ -47,10 +47,10 @@ URLs point to the `app/img/phones/` directory.
|
||||
...
|
||||
<ul class="phones">
|
||||
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
|
||||
<a href="#/phones/{{phone.id}}" class="thumb">
|
||||
<a href="#!/phones/{{phone.id}}" class="thumb">
|
||||
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
|
||||
</a>
|
||||
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
|
||||
<p>{{phone.snippet}}</p>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -83,7 +83,7 @@ HTTP request to an invalid location.
|
||||
query.sendKeys('nexus');
|
||||
|
||||
element.all(by.css('.phones li a')).first().click();
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
|
||||
expect(browser.getCurrentUrl()).toContain('index.html#!/phones/nexus-s');
|
||||
});
|
||||
|
||||
...
|
||||
@@ -110,8 +110,9 @@ You can now rerun `npm run protractor` to see the tests run.
|
||||
|
||||
## Summary
|
||||
|
||||
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about AngularJS
|
||||
layout templates and how AngularJS makes it easy to create applications that have multiple views.
|
||||
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about
|
||||
AngularJS layout templates and how AngularJS makes it easy to create applications that have
|
||||
multiple views.
|
||||
|
||||
|
||||
<ul doc-tutorial-nav="8"></ul>
|
||||
|
||||
@@ -23,49 +23,33 @@ has multiple views by adding routing, using an AngularJS module called {@link ng
|
||||
The routing functionality added in this step is provided by AngularJS in the `ngRoute` module, which
|
||||
is distributed separately from the core AngularJS framework.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
Since we are using [npm][npm] to install client-side dependencies, this step updates the
|
||||
`package.json` configuration file to include the new dependency:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
**`package.json`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
...
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"angular": "1.7.x",
|
||||
"angular-route": "1.7.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
|
||||
module that is compatible with version 1.5.x of AngularJS. We must tell bower to download and install
|
||||
The new dependency `"angular-route": "1.7.x"` tells npm to install a version of the angular-route
|
||||
module that is compatible with version 1.7.x of AngularJS. We must tell npm to download and install
|
||||
this dependency.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Multiple Views, Routing and Layout Templates
|
||||
|
||||
@@ -127,8 +111,8 @@ service, the `$routeProvider` exposes APIs that allow you to define routes for y
|
||||
</div>
|
||||
|
||||
AngularJS modules solve the problem of removing global variables from the application and provide a
|
||||
way of configuring the injector. As opposed to AMD or require.js modules, AngularJS modules don't try
|
||||
to solve the problem of script load ordering or lazy script fetching. These goals are totally
|
||||
way of configuring the injector. As opposed to AMD or require.js modules, AngularJS modules don't
|
||||
try to solve the problem of script load ordering or lazy script fetching. These goals are totally
|
||||
independent and both module systems can live side-by-side and fulfill their goals.
|
||||
|
||||
To deepen your understanding on AngularJS's DI, see [Understanding Dependency Injection][wiki-di].
|
||||
@@ -146,8 +130,8 @@ into the layout template. This makes it a perfect fit for our `index.html` templ
|
||||
```html
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular/angular.js"></script>
|
||||
<script src="bower_components/angular-route/angular-route.js"></script>
|
||||
<script src="lib/angular/angular.js"></script>
|
||||
<script src="lib/angular-route/angular-route.js"></script>
|
||||
<script src="app.module.js"></script>
|
||||
<script src="app.config.js"></script>
|
||||
...
|
||||
@@ -203,10 +187,8 @@ code, we put it into a separate file and used the `.config` suffix.
|
||||
```js
|
||||
angular.
|
||||
module('phonecatApp').
|
||||
config(['$locationProvider', '$routeProvider',
|
||||
function config($locationProvider, $routeProvider) {
|
||||
$locationProvider.hashPrefix('!');
|
||||
|
||||
config(['$routeProvider',
|
||||
function config($routeProvider) {
|
||||
$routeProvider.
|
||||
when('/phones', {
|
||||
template: '<phone-list></phone-list>'
|
||||
@@ -226,18 +208,6 @@ the corresponding services. Here, we use the
|
||||
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
|
||||
application routes.
|
||||
|
||||
<div class="alert alert-success">
|
||||
<p>
|
||||
We also used {@link $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
|
||||
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
|
||||
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
|
||||
</p>
|
||||
<p>
|
||||
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
|
||||
outside the scope of this tutorial). `!` is the most commonly used prefix.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Our routes are defined as follows:
|
||||
|
||||
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
|
||||
@@ -261,6 +231,25 @@ the route declaration — `'/phones/:phoneId'` — as a template that is matched
|
||||
URL. All variables defined with the `:` prefix are extracted into the (injectable)
|
||||
{@link ngRoute.$routeParams $routeParams} object.
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
You may have noticed, that — while the configured route paths start with `/` (e.g.
|
||||
`/phones`) — the URLs used in templates start with `#!/` (e.g. `#!/phones`).
|
||||
</p>
|
||||
<p>
|
||||
Without getting into much detail, AngularJS (by default) uses the hash part of the URL (i.e.
|
||||
what comes after the hash (`#`) symbol) to determine the current route. In addition to that, you
|
||||
can also specify a {@link $locationProvider#hashPrefix hash-prefix} (`!` by default) that needs
|
||||
to appear after the hash symbol in order for AngularJS to consider the value an "AngularJS path"
|
||||
and process it (for example, try to match it to a route).
|
||||
</p>
|
||||
<p>
|
||||
You can find out more about how all this works in the [Using $location](guide/$location) section
|
||||
of the Developer Guide. But all you need to know for now, is that the URLs to our various routes
|
||||
should be prefixed with `#!`.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
## The `phoneDetail` Component
|
||||
|
||||
@@ -345,8 +334,8 @@ any modification.
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-route/angular-route.js',
|
||||
'lib/angular/angular.js',
|
||||
'lib/angular-route/angular-route.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
@@ -363,7 +352,7 @@ various URLs and verifying that the correct view was rendered.
|
||||
|
||||
it('should redirect `index.html` to `index.html#!/phones', function() {
|
||||
browser.get('index.html');
|
||||
expect(browser.getLocationAbsUrl()).toBe('/phones');
|
||||
expect(browser.getCurrentUrl()).toContain('index.html#!/phones');
|
||||
});
|
||||
|
||||
...
|
||||
@@ -424,6 +413,6 @@ With the routing set up and the phone list view implemented, we are ready to go
|
||||
<ul doc-tutorial-nav="9"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io
|
||||
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
|
||||
[npm]: https://www.npmjs.com/
|
||||
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
|
||||
|
||||
@@ -21,50 +21,34 @@ In this step, we will change the way our application fetches data.
|
||||
The RESTful functionality is provided by AngularJS in the {@link ngResource ngResource} module, which
|
||||
is distributed separately from the core AngularJS framework.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependency:
|
||||
Since we are using [npm][npm] to install client-side dependencies, this step updates the
|
||||
`package.json` configuration file to include the new dependency:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
**`package.json`:**
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
...
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"angular": "1.7.x",
|
||||
"angular-resource": "1.7.x",
|
||||
"angular-route": "1.7.x",
|
||||
"bootstrap": "3.3.x"
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the
|
||||
angular-resource module that is compatible with version 1.5.x of AngularJS. We must tell bower to
|
||||
The new dependency `"angular-resource": "1.7.x"` tells npm to install a version of the
|
||||
angular-resource module that is compatible with version 1.7.x of AngularJS. We must tell npm to
|
||||
download and install this dependency.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## Service
|
||||
|
||||
@@ -129,7 +113,7 @@ need to load the `angular-resource.js` file, which contains the `ngResource` mod
|
||||
```html
|
||||
<head>
|
||||
...
|
||||
<script src="bower_components/angular-resource/angular-resource.js"></script>
|
||||
<script src="lib/angular-resource/angular-resource.js"></script>
|
||||
...
|
||||
<script src="core/phone/phone.module.js"></script>
|
||||
<script src="core/phone/phone.service.js"></script>
|
||||
@@ -141,9 +125,10 @@ need to load the `angular-resource.js` file, which contains the `ngResource` mod
|
||||
## Component Controllers
|
||||
|
||||
We can now simplify our component controllers (`PhoneListController` and `PhoneDetailController`) by
|
||||
factoring out the lower-level `$http` service, replacing it with the new `Phone` service. AngularJS's
|
||||
`$resource` service is easier to use than `$http` for interacting with data sources exposed as
|
||||
RESTful resources. It is also easier now to understand what the code in our controllers is doing.
|
||||
factoring out the lower-level `$http` service, replacing it with the new `Phone` service.
|
||||
AngularJS's `$resource` service is easier to use than `$http` for interacting with data sources
|
||||
exposed as RESTful resources. It is also easier now to understand what the code in our controllers
|
||||
is doing.
|
||||
|
||||
<br />
|
||||
**`app/phone-list/phone-list.module.js`:**
|
||||
@@ -240,8 +225,8 @@ Karma configuration file with angular-resource.
|
||||
|
||||
```js
|
||||
files: [
|
||||
'bower_components/angular/angular.js',
|
||||
'bower_components/angular-resource/angular-resource.js',
|
||||
'lib/angular/angular.js',
|
||||
'lib/angular-resource/angular-resource.js',
|
||||
...
|
||||
],
|
||||
```
|
||||
@@ -319,6 +304,6 @@ Now that we have seen how to build a custom service as a RESTful client, we are
|
||||
<ul doc-tutorial-nav="13"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[jasmine-equality]: https://jasmine.github.io/2.4/custom_equality.html
|
||||
[npm]: https://www.npmjs.com/
|
||||
[restful]: https://en.wikipedia.org/wiki/Representational_State_Transfer
|
||||
|
||||
@@ -22,59 +22,43 @@ the template code we created earlier.
|
||||
## Dependencies
|
||||
|
||||
The animation functionality is provided by AngularJS in the `ngAnimate` module, which is distributed
|
||||
separately from the core AngularJS framework. In addition we will use [jQuery][jquery] in this project
|
||||
to do extra JavaScript animations.
|
||||
separately from the core AngularJS framework. In addition we will use [jQuery][jquery] in this
|
||||
project to do extra JavaScript animations.
|
||||
|
||||
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
|
||||
`bower.json` configuration file to include the new dependencies:
|
||||
Since we are using [npm][npm] to install client-side dependencies, this step updates the
|
||||
`package.json` configuration file to include the new dependencies:
|
||||
|
||||
<br />
|
||||
**`bower.json`:**
|
||||
**`package.json`:**
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"name": "angular-phonecat",
|
||||
"description": "A starter project for AngularJS",
|
||||
"version": "0.0.0",
|
||||
"homepage": "https://github.com/angular/angular-phonecat",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
...
|
||||
"dependencies": {
|
||||
"angular": "1.5.x",
|
||||
"angular-animate": "1.5.x",
|
||||
"angular-mocks": "1.5.x",
|
||||
"angular-resource": "1.5.x",
|
||||
"angular-route": "1.5.x",
|
||||
"angular": "1.7.x",
|
||||
"angular-animate": "1.7.x",
|
||||
"angular-resource": "1.7.x",
|
||||
"angular-route": "1.7.x",
|
||||
"bootstrap": "3.3.x",
|
||||
"jquery": "3.2.x"
|
||||
}
|
||||
"jquery": "3.3.x"
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
* `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that
|
||||
is compatible with version 1.5.x of AngularJS.
|
||||
* `"jquery": "3.2.x"` tells bower to install the latest patch release of the 3.2 version of jQuery.
|
||||
Note that this is not an AngularJS library; it is the standard jQuery library. We can use bower to
|
||||
* `"angular-animate": "1.7.x"` tells npm to install a version of the angular-animate module that
|
||||
is compatible with version 1.7.x of AngularJS.
|
||||
* `"jquery": "3.3.x"` tells npm to install the latest patch release of the 3.3 version of jQuery.
|
||||
Note that this is not an AngularJS library; it is the standard jQuery library. We can use npm to
|
||||
install a wide range of 3rd party libraries.
|
||||
|
||||
Now, we must tell bower to download and install these dependencies.
|
||||
Now, we must tell npm to download and install these dependencies.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
<div class="alert alert-info">
|
||||
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
|
||||
we have preconfigured `npm install` to run bower for us.
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
|
||||
you may have a problem with the `bower install` due to a conflict between the versions of
|
||||
angular.js that need to be installed. If you run into this issue, simply delete your
|
||||
`app/bower_components` directory and then run `npm install`.
|
||||
</div>
|
||||
|
||||
|
||||
## How Animations work with `ngAnimate`
|
||||
|
||||
@@ -101,12 +85,12 @@ code necessary to make your application "animation aware".
|
||||
...
|
||||
|
||||
<!-- Used for JavaScript animations (include this before angular.js) -->
|
||||
<script src="bower_components/jquery/dist/jquery.js"></script>
|
||||
<script src="lib/jquery/dist/jquery.js"></script>
|
||||
|
||||
...
|
||||
|
||||
<!-- Adds animation support in AngularJS -->
|
||||
<script src="bower_components/angular-animate/angular-animate.js"></script>
|
||||
<script src="lib/angular-animate/angular-animate.js"></script>
|
||||
|
||||
<!-- Defines JavaScript animations -->
|
||||
<script src="app.animations.js"></script>
|
||||
@@ -115,8 +99,8 @@ code necessary to make your application "animation aware".
|
||||
```
|
||||
|
||||
<div class="alert alert-error">
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer, when using AngularJS 1.5 or newer; jQuery 1.x is
|
||||
not officially supported.
|
||||
**Important:** Be sure to use jQuery version 2.1 or newer, when using AngularJS 1.5 or newer;
|
||||
jQuery 1.x is not officially supported.
|
||||
In order for AngularJS to detect jQuery and take advantage of it, make sure to include `jquery.js`
|
||||
before `angular.js`.
|
||||
</div>
|
||||
@@ -556,9 +540,9 @@ There you have it! We have created a web application in a relatively short amoun
|
||||
<ul doc-tutorial-nav="14"></ul>
|
||||
|
||||
|
||||
[bower]: http://bower.io/
|
||||
[caniuse-css-animation]: http://caniuse.com/#feat=css-animation
|
||||
[caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions
|
||||
[caniuse-css-animation]: https://caniuse.com/#feat=css-animation
|
||||
[caniuse-css-transitions]: https://caniuse.com/#feat=css-transitions
|
||||
[jquery]: https://jquery.com/
|
||||
[jquery-animate]: https://api.jquery.com/animate/
|
||||
[jquery-animate]: https://api.jquery.com/animate
|
||||
[mdn-animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
|
||||
[npm]: https://www.npmjs.com/
|
||||
|
||||
@@ -22,5 +22,5 @@ If you have questions or feedback or just want to say "hi", please post a messag
|
||||
|
||||
[angular-seed]: https://github.com/angular/angular-seed
|
||||
[gitter]: https://gitter.im/angular/angular.js
|
||||
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[irc]: https://webchat.freenode.net/?channels=angularjs&uio=d4
|
||||
[mailing-list]: https://groups.google.com/forum/#!forum/angular
|
||||
|
||||
@@ -20,7 +20,7 @@ function main() {
|
||||
} catch (e) {
|
||||
fs.mkdirSync(__dirname + '/../../../src/ngParseExt');
|
||||
}
|
||||
fs.writeFile(__dirname + '/../../../src/ngParseExt/ucd.js', code);
|
||||
fs.writeFileSync(__dirname + '/../../../src/ngParseExt/ucd.js', code);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
var angularFiles = require('./angularFiles');
|
||||
var sharedConfig = require('./karma-shared.conf');
|
||||
|
||||
module.exports = function(config) {
|
||||
sharedConfig(config, {testName: 'AngularJS: isolated module tests (ngAnimate)', logFile: 'karma-ngAnimate-isolated.log'});
|
||||
|
||||
config.set({
|
||||
files: angularFiles.mergeFilesFor('karmaModules-ngAnimate')
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
var angularFiles = require('./angularFiles');
|
||||
var sharedConfig = require('./karma-shared.conf');
|
||||
|
||||
module.exports = function(config) {
|
||||
sharedConfig(config, {testName: 'AngularJS: isolated module tests (ngMock)', logFile: 'karma-ngMock-isolated.log'});
|
||||
|
||||
config.set({
|
||||
files: angularFiles.mergeFilesFor('karmaModules-ngMock')
|
||||
});
|
||||
};
|
||||
+11
-8
@@ -1,17 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
var angularFiles = require('./angularFiles');
|
||||
var sharedConfig = require('./karma-shared.conf');
|
||||
|
||||
module.exports = function(config) {
|
||||
sharedConfig(config, {testName: 'AngularJS: modules', logFile: 'karma-modules.log'});
|
||||
sharedConfig(config, {testName: 'AngularJS: isolated module tests', logFile: 'karma-modules-isolated.log'});
|
||||
|
||||
config.set({
|
||||
files: angularFiles.mergeFilesFor('karmaModules'),
|
||||
|
||||
junitReporter: {
|
||||
outputFile: 'test_out/modules.xml',
|
||||
suite: 'modules'
|
||||
}
|
||||
files: [
|
||||
'build/angular.js',
|
||||
'build/angular-mocks.js',
|
||||
'test/modules/no_bootstrap.js',
|
||||
'test/helpers/matchers.js',
|
||||
'test/helpers/privateMocks.js',
|
||||
'test/helpers/support.js',
|
||||
'test/helpers/testabilityPatch.js',
|
||||
'build/test-bundles/angular-*.js'
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
+8
-14
@@ -23,12 +23,7 @@ module.exports = function(config, specificOptions) {
|
||||
// SauceLabs config for local development.
|
||||
sauceLabs: {
|
||||
testName: specificOptions.testName || 'AngularJS',
|
||||
startConnect: true,
|
||||
options: {
|
||||
// We need selenium version +2.46 for Firefox 39 and the last selenium version for OS X is 2.45.
|
||||
// TODO: Uncomment when there is a selenium 2.46 available for OS X.
|
||||
// 'selenium-version': '2.46.0'
|
||||
}
|
||||
startConnect: true
|
||||
},
|
||||
|
||||
// BrowserStack config for local development.
|
||||
@@ -65,13 +60,11 @@ module.exports = function(config, specificOptions) {
|
||||
'SL_Safari-1': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'safari',
|
||||
platform: 'OS X 10.12',
|
||||
version: 'latest-1'
|
||||
},
|
||||
'SL_Safari': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'safari',
|
||||
platform: 'OS X 10.12',
|
||||
version: 'latest'
|
||||
},
|
||||
'SL_IE_9': {
|
||||
@@ -104,17 +97,15 @@ module.exports = function(config, specificOptions) {
|
||||
platform: 'Windows 10',
|
||||
version: 'latest-1'
|
||||
},
|
||||
'SL_iOS_10': {
|
||||
'SL_iOS': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'iphone',
|
||||
platform: 'OS X 10.12',
|
||||
version: '10.3'
|
||||
version: 'latest'
|
||||
},
|
||||
'SL_iOS_11': {
|
||||
'SL_iOS-1': {
|
||||
base: 'SauceLabs',
|
||||
browserName: 'iphone',
|
||||
platform: 'OS X 10.12',
|
||||
version: '11.2'
|
||||
version: 'latest-1'
|
||||
},
|
||||
|
||||
'BS_Chrome': {
|
||||
@@ -193,6 +184,9 @@ module.exports = function(config, specificOptions) {
|
||||
config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
|
||||
config.sauceLabs.recordScreenshots = true;
|
||||
|
||||
// Try 'websocket' for a faster transmission first. Fallback to 'polling' if necessary.
|
||||
config.transports = ['websocket', 'polling'];
|
||||
|
||||
// Debug logging into a file, that we print out at the end of the build.
|
||||
config.loggers.push({
|
||||
type: 'file',
|
||||
|
||||
+12
-4
@@ -75,10 +75,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
|
||||
wrap: function(src, name) {
|
||||
src.unshift('src/' + name + '.prefix');
|
||||
src.push('src/' + name + '.suffix');
|
||||
return src;
|
||||
wrap(src, name) {
|
||||
return [`src/${name}.prefix`, ...src, `src/${name}.suffix`];
|
||||
},
|
||||
|
||||
|
||||
@@ -135,6 +133,16 @@ module.exports = {
|
||||
|
||||
build: function(config, fn) {
|
||||
var files = grunt.file.expand(config.src);
|
||||
// grunt.file.expand might reorder the list of files
|
||||
// when it is expanding globs, so we use prefix and suffix
|
||||
// fields to ensure that files are at the start of end of
|
||||
// the list (primarily for wrapping in an IIFE).
|
||||
if (config.prefix) {
|
||||
files = grunt.file.expand(config.prefix).concat(files);
|
||||
}
|
||||
if (config.suffix) {
|
||||
files = files.concat(grunt.file.expand(config.suffix));
|
||||
}
|
||||
var styles = config.styles;
|
||||
var processedStyles;
|
||||
//concat
|
||||
|
||||
@@ -11,14 +11,14 @@ set -e
|
||||
# Curl and run this script as part of your .travis.yml before_script section:
|
||||
# before_script:
|
||||
# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash
|
||||
SC_VERSION="4.4.12"
|
||||
SC_VERSION="4.5.2"
|
||||
CONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-linux.tar.gz"
|
||||
CONNECT_DIR="/tmp/sauce-connect-$RANDOM"
|
||||
CONNECT_DOWNLOAD="sc-$SC_VERSION-linux.tar.gz"
|
||||
|
||||
CONNECT_LOG="$LOGS_DIR/sauce-connect"
|
||||
CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout"
|
||||
CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr"
|
||||
# We don't want to create a log file because sauceconnect always logs in verbose mode. This seems
|
||||
# to be overwhelming Travis and causing flakes when we are cat-ing the log in "print_logs.sh"
|
||||
CONNECT_LOG="/dev/null"
|
||||
|
||||
# Get Connect and start it
|
||||
mkdir -p $CONNECT_DIR
|
||||
@@ -42,9 +42,6 @@ if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then
|
||||
fi
|
||||
|
||||
|
||||
echo "Starting Sauce Connect in the background, logging into:"
|
||||
echo " $CONNECT_LOG"
|
||||
echo " $CONNECT_STDOUT"
|
||||
echo " $CONNECT_STDERR"
|
||||
echo "Starting Sauce Connect in the background"
|
||||
sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \
|
||||
--logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT &
|
||||
--logfile $CONNECT_LOG &
|
||||
|
||||
+15
-11
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "angularjs",
|
||||
"name": "angular",
|
||||
"license": "MIT",
|
||||
"branchVersion": "^1.7.0",
|
||||
"branchPattern": "1.7.*",
|
||||
@@ -9,15 +9,14 @@
|
||||
"url": "https://github.com/angular/angular.js.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.9.1",
|
||||
"yarn": ">=1.3.2",
|
||||
"grunt": "^1.2.0"
|
||||
"node": ">=8.12.0",
|
||||
"yarn": ">=1.10.1",
|
||||
"grunt-cli": "^1.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"commit": "git-cz",
|
||||
"test-i18n": "jasmine-node i18n/spec",
|
||||
"test-i18n-ucd": "jasmine-node i18n/ucd/spec",
|
||||
"grunt": "grunt"
|
||||
"test-i18n-ucd": "jasmine-node i18n/ucd/spec"
|
||||
},
|
||||
"devDependencies": {
|
||||
"angular-benchpress": "0.x.x",
|
||||
@@ -33,7 +32,7 @@
|
||||
"cross-spawn": "^4.0.0",
|
||||
"cz-conventional-changelog": "1.1.4",
|
||||
"dgeni": "^0.4.9",
|
||||
"dgeni-packages": "^0.26.2",
|
||||
"dgeni-packages": "^0.26.5",
|
||||
"eslint-plugin-promise": "^3.6.0",
|
||||
"event-stream": "~3.1.0",
|
||||
"glob": "^6.0.1",
|
||||
@@ -63,7 +62,7 @@
|
||||
"jquery": "3.2.1",
|
||||
"jquery-2.1": "npm:jquery@2.1.4",
|
||||
"jquery-2.2": "npm:jquery@2.2.4",
|
||||
"karma": "^2.0.4",
|
||||
"karma": "^3.1.4",
|
||||
"karma-browserstack-launcher": "^1.3.0",
|
||||
"karma-chrome-launcher": "^2.2.0",
|
||||
"karma-edge-launcher": "^0.4.2",
|
||||
@@ -72,7 +71,7 @@
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-junit-reporter": "^1.2.0",
|
||||
"karma-safari-launcher": "^1.0.0",
|
||||
"karma-sauce-launcher": "^1.2.0",
|
||||
"karma-sauce-launcher": "^2.0.2",
|
||||
"karma-script-launcher": "^1.0.0",
|
||||
"karma-spec-reporter": "^0.0.32",
|
||||
"load-grunt-tasks": "^3.5.0",
|
||||
@@ -84,13 +83,13 @@
|
||||
"npm-run": "^4.1.0",
|
||||
"open-sans-fontface": "^1.4.0",
|
||||
"promises-aplus-tests": "~2.1.0",
|
||||
"protractor": "^5.1.2",
|
||||
"protractor": "^5.4.1",
|
||||
"q": "~1.0.0",
|
||||
"q-io": "^1.10.9",
|
||||
"qq": "^0.3.5",
|
||||
"rewire": "~2.1.0",
|
||||
"sax": "^1.1.1",
|
||||
"selenium-webdriver": "^2.53.1",
|
||||
"selenium-webdriver": "^4.0.0-alpha.1",
|
||||
"semver": "^5.4.1",
|
||||
"serve-favicon": "^2.3.0",
|
||||
"serve-index": "^1.8.0",
|
||||
@@ -100,6 +99,11 @@
|
||||
"stringmap": "^0.2.2"
|
||||
},
|
||||
"dependencies": {},
|
||||
"resolutions": {
|
||||
"//1": "`natives@1.1.0` does not work with Node.js 10.x on Windows 10",
|
||||
"//2": "(E.g. see https://github.com/gulpjs/gulp/issues/2162.)",
|
||||
"natives": "1.1.3"
|
||||
},
|
||||
"commitplease": {
|
||||
"style": "angular",
|
||||
"nohook": true
|
||||
|
||||
@@ -79,6 +79,8 @@ function capabilitiesForSauceLabs(capabilities) {
|
||||
'browserName': capabilities.browserName,
|
||||
'platform': capabilities.platform,
|
||||
'version': capabilities.version,
|
||||
'elementScrollBehavior': 1
|
||||
'elementScrollBehavior': 1,
|
||||
// Allow e2e test sessions to run for a maximum of 35 minutes, instead of the default 30 minutes.
|
||||
'maxDuration': 2100
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ const BROWSER_CACHE_DURATION = 60 * 10;
|
||||
const CDN_CACHE_DURATION = 60 * 60 * 12;
|
||||
|
||||
function sendStoredFile(request, response) {
|
||||
let filePathSegments = request.path.split('/').filter((segment) => {
|
||||
const requestPath = request.path || '/';
|
||||
let filePathSegments = requestPath.split('/').filter((segment) => {
|
||||
// Remove empty leading or trailing path parts
|
||||
return segment !== '';
|
||||
});
|
||||
@@ -159,7 +160,11 @@ function sendStoredFile(request, response) {
|
||||
const nextQuery = data[1];
|
||||
const apiResponse = data[2];
|
||||
|
||||
if (!files.length && (!apiResponse || !apiResponse.prefixes)) {
|
||||
if (
|
||||
// we got no files or directories from previous query pages
|
||||
!fileList.length && !directoryList.length &&
|
||||
// this query page has no file or directories
|
||||
!files.length && (!apiResponse || !apiResponse.prefixes)) {
|
||||
return Promise.reject({
|
||||
code: 404
|
||||
});
|
||||
@@ -190,22 +195,16 @@ const snapshotRegex = /^snapshot(-stable)?\//;
|
||||
* When a new zip file is uploaded into snapshot or snapshot-stable,
|
||||
* delete the previous zip file.
|
||||
*/
|
||||
function deleteOldSnapshotZip(event) {
|
||||
const object = event.data;
|
||||
|
||||
function deleteOldSnapshotZip(object, context) {
|
||||
const bucketId = object.bucket;
|
||||
const filePath = object.name;
|
||||
const contentType = object.contentType;
|
||||
const resourceState = object.resourceState;
|
||||
|
||||
const bucket = gcs.bucket(bucketId);
|
||||
|
||||
const snapshotFolderMatch = filePath.match(snapshotRegex);
|
||||
|
||||
if (!snapshotFolderMatch ||
|
||||
contentType !== 'application/zip' ||
|
||||
resourceState === 'not_exists' // Deletion event
|
||||
) {
|
||||
if (!snapshotFolderMatch || contentType !== 'application/zip') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -230,4 +229,4 @@ function deleteOldSnapshotZip(event) {
|
||||
}
|
||||
|
||||
exports.sendStoredFile = functions.https.onRequest(sendStoredFile);
|
||||
exports.deleteOldSnapshotZip = functions.storage.object().onChange(deleteOldSnapshotZip);
|
||||
exports.deleteOldSnapshotZip = functions.storage.object().onFinalize(deleteOldSnapshotZip);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,8 @@
|
||||
"description": "Cloud Functions to serve files from gcs to code.angularjs.org",
|
||||
"dependencies": {
|
||||
"@google-cloud/storage": "^1.1.1",
|
||||
"firebase-admin": "^4.2.1",
|
||||
"firebase-functions": "^0.5.9"
|
||||
"firebase-admin": "^5.11.0",
|
||||
"firebase-functions": "^1.0.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -21,21 +21,21 @@ rm -f angular.js.size
|
||||
|
||||
|
||||
# BUILD #
|
||||
yarn run grunt -- ci-checks package --no-color
|
||||
yarn grunt ci-checks package --no-color
|
||||
|
||||
mkdir -p test_out
|
||||
|
||||
# UNIT TESTS #
|
||||
yarn run grunt -- test:unit --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
|
||||
yarn grunt test:unit --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
|
||||
|
||||
# END TO END TESTS #
|
||||
yarn run grunt -- test:ci-protractor
|
||||
yarn grunt test:ci-protractor
|
||||
|
||||
# DOCS APP TESTS #
|
||||
yarn run grunt -- test:docs --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
|
||||
yarn grunt test:docs --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
|
||||
|
||||
# Promises/A+ TESTS #
|
||||
yarn run grunt -- test:promises-aplus --no-color
|
||||
yarn grunt test:promises-aplus --no-color
|
||||
|
||||
|
||||
# CHECK SIZE #
|
||||
|
||||
@@ -8,11 +8,11 @@ nvm install
|
||||
|
||||
# clean out and install yarn
|
||||
rm -rf ~/.yarn
|
||||
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
|
||||
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.10.1
|
||||
export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
# Ensure that we have the local dependencies installed
|
||||
yarn install
|
||||
|
||||
echo testing grunt version
|
||||
yarn run grunt -- --version
|
||||
yarn grunt --version
|
||||
|
||||
@@ -37,7 +37,7 @@ function init {
|
||||
function build {
|
||||
cd ../..
|
||||
source scripts/jenkins/init-node.sh
|
||||
yarn run grunt -- ci-checks package --no-color
|
||||
yarn grunt ci-checks package --no-color
|
||||
|
||||
cd $SCRIPT_DIR
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
set -e
|
||||
|
||||
yarn global add grunt-cli@1.2.0
|
||||
|
||||
mkdir -p "$LOGS_DIR"
|
||||
|
||||
if [ "$JOB" != "ci-checks" ]; then
|
||||
@@ -13,13 +11,13 @@ fi
|
||||
|
||||
# ci-checks and unit tests do not run against the packaged code
|
||||
if [[ "$JOB" != "ci-checks" ]] && [[ "$JOB" != unit-* ]]; then
|
||||
grunt package
|
||||
yarn grunt package
|
||||
fi
|
||||
|
||||
# unit runs the docs tests too which need a built version of the code
|
||||
if [[ "$JOB" = unit-* ]]; then
|
||||
grunt validate-angular-files
|
||||
grunt build
|
||||
yarn grunt validate-angular-files
|
||||
yarn grunt build
|
||||
fi
|
||||
|
||||
# check this after the package, because at this point the browser_provider
|
||||
|
||||
+10
-7
@@ -11,14 +11,14 @@ export SAUCE_ACCESS_KEY
|
||||
BROWSER_STACK_ACCESS_KEY=$(echo "$BROWSER_STACK_ACCESS_KEY" | rev)
|
||||
SAUCE_ACCESS_KEY=$(echo "$SAUCE_ACCESS_KEY" | rev)
|
||||
|
||||
# TODO: restore "SL_EDGE-1" once Sauce Labs adds Edge 17 and "SL_EDGE-1" refers
|
||||
# to version 16. Edge 15 disconnects from Karma frequently causing extreme build instability.
|
||||
# The currently latest version of Safari on Saucelabs (v12) is unstable and disconnects frequently.
|
||||
# TODO: Add `SL_Safari` back, once/if it becomes more stable again.
|
||||
BROWSERS="SL_Chrome,SL_Chrome-1,\
|
||||
SL_Firefox,SL_Firefox-1,\
|
||||
SL_Safari,SL_Safari-1,\
|
||||
SL_iOS_10,SL_iOS_11,\
|
||||
SL_Safari-1,\
|
||||
SL_iOS,SL_iOS-1,\
|
||||
SL_IE_9,SL_IE_10,SL_IE_11,\
|
||||
SL_EDGE"
|
||||
SL_EDGE,SL_EDGE-1"
|
||||
|
||||
case "$JOB" in
|
||||
"ci-checks")
|
||||
@@ -29,19 +29,21 @@ case "$JOB" in
|
||||
# convert commit range to 2 dots, as commitplease uses `git log`.
|
||||
# See https://github.com/travis-ci/travis-ci/issues/4596 for more info
|
||||
echo "Validate commit messages in PR:"
|
||||
yarn run commitplease -- "${TRAVIS_COMMIT_RANGE/.../..}"
|
||||
yarn run commitplease "${TRAVIS_COMMIT_RANGE/.../..}"
|
||||
fi
|
||||
;;
|
||||
"unit-core")
|
||||
grunt test:promises-aplus
|
||||
grunt test:jqlite --browsers="$BROWSERS" --reporters=spec
|
||||
grunt test:modules --browsers="$BROWSERS" --reporters=spec
|
||||
;;
|
||||
"unit-jquery")
|
||||
grunt test:jquery --browsers="$BROWSERS" --reporters=spec
|
||||
grunt test:jquery-2.2 --browsers="$BROWSERS" --reporters=spec
|
||||
grunt test:jquery-2.1 --browsers="$BROWSERS" --reporters=spec
|
||||
;;
|
||||
"unit-modules")
|
||||
grunt test:modules --browsers="$BROWSERS" --reporters=spec
|
||||
;;
|
||||
"docs-app")
|
||||
grunt tests:docs --browsers="$BROWSERS" --reporters=spec
|
||||
grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js"
|
||||
@@ -102,6 +104,7 @@ case "$JOB" in
|
||||
'ci-checks',\
|
||||
'unit-core',\
|
||||
'unit-jquery',\
|
||||
'unit-modules',\
|
||||
'docs-app',\
|
||||
'e2e',\
|
||||
or\
|
||||
|
||||
@@ -7,5 +7,5 @@ for FILE in $LOG_FILES; do
|
||||
echo "================================================================================"
|
||||
echo " $FILE"
|
||||
echo "================================================================================"
|
||||
cat $FILE
|
||||
cat $FILE || true
|
||||
done
|
||||
|
||||
+6
-10
@@ -792,15 +792,16 @@ function arrayRemove(array, value) {
|
||||
* * If `source` is identical to `destination` an exception will be thrown.
|
||||
*
|
||||
* <br />
|
||||
*
|
||||
* <div class="alert alert-warning">
|
||||
* Only enumerable properties are taken into account. Non-enumerable properties (both on `source`
|
||||
* and on `destination`) will be ignored.
|
||||
* </div>
|
||||
*
|
||||
* @param {*} source The source that will be used to make a copy.
|
||||
* Can be any type, including primitives, `null`, and `undefined`.
|
||||
* @param {(Object|Array)=} destination Destination into which the source is copied. If
|
||||
* provided, must be of the same type as `source`.
|
||||
* @param {*} source The source that will be used to make a copy. Can be any type, including
|
||||
* primitives, `null`, and `undefined`.
|
||||
* @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
|
||||
* must be of the same type as `source`.
|
||||
* @returns {*} The copy or updated `destination`, if `destination` was specified.
|
||||
*
|
||||
* @example
|
||||
@@ -1695,13 +1696,8 @@ function angularInit(element, bootstrap) {
|
||||
});
|
||||
if (appElement) {
|
||||
if (!isAutoBootstrapAllowed) {
|
||||
try {
|
||||
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
|
||||
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
|
||||
'an extension, document.location.href does not match.');
|
||||
} catch (e) {
|
||||
// Support: Safari 11 w/ Webdriver
|
||||
// The console.error will throw and make the test fail
|
||||
}
|
||||
return;
|
||||
}
|
||||
config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
htmlAnchorDirective,
|
||||
inputDirective,
|
||||
inputDirective,
|
||||
hiddenInputBrowserCacheDirective,
|
||||
formDirective,
|
||||
scriptDirective,
|
||||
selectDirective,
|
||||
@@ -221,7 +221,8 @@ function publishExternalAPI(angular) {
|
||||
ngModelOptions: ngModelOptionsDirective
|
||||
}).
|
||||
directive({
|
||||
ngInclude: ngIncludeFillContentDirective
|
||||
ngInclude: ngIncludeFillContentDirective,
|
||||
input: hiddenInputBrowserCacheDirective
|
||||
}).
|
||||
directive(ngAttributeAliasDirectives).
|
||||
directive(ngEventDirectives);
|
||||
|
||||
@@ -108,6 +108,9 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
|
||||
if (url) {
|
||||
var sameState = lastHistoryState === state;
|
||||
|
||||
// Normalize the inputted URL
|
||||
url = urlResolve(url).href;
|
||||
|
||||
// Don't change anything if previous and current URLs and states match. This also prevents
|
||||
// IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
|
||||
// See https://github.com/angular/angular.js/commit/ffb2701
|
||||
|
||||
+11
-2
@@ -2231,7 +2231,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
this.$$element.removeAttr(attrName);
|
||||
} else {
|
||||
if (SIMPLE_ATTR_NAME.test(attrName)) {
|
||||
this.$$element.attr(attrName, value);
|
||||
// jQuery skips special boolean attrs treatment in XML nodes for
|
||||
// historical reasons and hence AngularJS cannot freely call
|
||||
// `.attr(attrName, false) with such attributes. To avoid issues
|
||||
// in XHTML, call `removeAttr` in such cases instead.
|
||||
// See https://github.com/jquery/jquery/issues/4249
|
||||
if (booleanKey && value === false) {
|
||||
this.$$element.removeAttr(attrName);
|
||||
} else {
|
||||
this.$$element.attr(attrName, value);
|
||||
}
|
||||
} else {
|
||||
setSpecialAttr(this.$$element[0], attrName, value);
|
||||
}
|
||||
@@ -3828,7 +3837,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|
||||
pre: function ngPropPreLinkFn(scope, $element) {
|
||||
function applyPropValue() {
|
||||
var propValue = ngPropGetter(scope);
|
||||
$element.prop(propName, sanitizer(propValue));
|
||||
$element[0][propName] = sanitizer(propValue);
|
||||
}
|
||||
|
||||
applyPropValue();
|
||||
|
||||
@@ -408,7 +408,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
|
||||
// ng-src, ng-srcset, ng-href are interpolated
|
||||
forEach(['src', 'srcset', 'href'], function(attrName) {
|
||||
var normalized = directiveNormalize('ng-' + attrName);
|
||||
ngAttributeAliasDirectives[normalized] = function() {
|
||||
ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) {
|
||||
return {
|
||||
priority: 99, // it needs to run after the attributes are interpolated
|
||||
link: function(scope, element, attr) {
|
||||
@@ -422,6 +422,10 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
|
||||
propName = null;
|
||||
}
|
||||
|
||||
// We need to sanitize the url at least once, in case it is a constant
|
||||
// non-interpolated attribute.
|
||||
attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized]));
|
||||
|
||||
attr.$observe(normalized, function(value) {
|
||||
if (!value) {
|
||||
if (attrName === 'href') {
|
||||
@@ -441,5 +445,5 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
});
|
||||
|
||||
+111
-29
@@ -1497,7 +1497,7 @@ function createDateParser(regexp, mapping) {
|
||||
}
|
||||
|
||||
function createDateInputType(type, regexp, parseDate, format) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
|
||||
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
||||
badInputChecker(scope, element, attr, ctrl, type);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
@@ -1540,24 +1540,34 @@ function createDateInputType(type, regexp, parseDate, format) {
|
||||
});
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
var minVal;
|
||||
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
||||
var parsedMinVal = parseObservedDateValue(minVal);
|
||||
|
||||
ctrl.$validators.min = function(value) {
|
||||
return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
|
||||
return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
|
||||
};
|
||||
attr.$observe('min', function(val) {
|
||||
minVal = parseObservedDateValue(val);
|
||||
ctrl.$validate();
|
||||
if (val !== minVal) {
|
||||
parsedMinVal = parseObservedDateValue(val);
|
||||
minVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
var maxVal;
|
||||
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
||||
var parsedMaxVal = parseObservedDateValue(maxVal);
|
||||
|
||||
ctrl.$validators.max = function(value) {
|
||||
return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
|
||||
return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
|
||||
};
|
||||
attr.$observe('max', function(val) {
|
||||
maxVal = parseObservedDateValue(val);
|
||||
ctrl.$validate();
|
||||
if (val !== maxVal) {
|
||||
parsedMaxVal = parseObservedDateValue(val);
|
||||
maxVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1709,50 +1719,68 @@ function isValidForStep(viewValue, stepBase, step) {
|
||||
return (value - stepBase) % step === 0;
|
||||
}
|
||||
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
||||
badInputChecker(scope, element, attr, ctrl, 'number');
|
||||
numberFormatterParser(ctrl);
|
||||
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
||||
|
||||
var minVal;
|
||||
var maxVal;
|
||||
var parsedMinVal;
|
||||
|
||||
if (isDefined(attr.min) || attr.ngMin) {
|
||||
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
||||
parsedMinVal = parseNumberAttrVal(minVal);
|
||||
|
||||
ctrl.$validators.min = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
|
||||
};
|
||||
|
||||
attr.$observe('min', function(val) {
|
||||
minVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== minVal) {
|
||||
parsedMinVal = parseNumberAttrVal(val);
|
||||
minVal = val;
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.max) || attr.ngMax) {
|
||||
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
||||
var parsedMaxVal = parseNumberAttrVal(maxVal);
|
||||
|
||||
ctrl.$validators.max = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
|
||||
};
|
||||
|
||||
attr.$observe('max', function(val) {
|
||||
maxVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== maxVal) {
|
||||
parsedMaxVal = parseNumberAttrVal(val);
|
||||
maxVal = val;
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(attr.step) || attr.ngStep) {
|
||||
var stepVal;
|
||||
var stepVal = attr.step || $parse(attr.ngStep)(scope);
|
||||
var parsedStepVal = parseNumberAttrVal(stepVal);
|
||||
|
||||
ctrl.$validators.step = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
|
||||
isValidForStep(viewValue, minVal || 0, stepVal);
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
|
||||
isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
|
||||
};
|
||||
|
||||
attr.$observe('step', function(val) {
|
||||
stepVal = parseNumberAttrVal(val);
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
if (val !== stepVal) {
|
||||
parsedStepVal = parseNumberAttrVal(val);
|
||||
stepVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,6 +1810,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
originalRender;
|
||||
|
||||
if (hasMinAttr) {
|
||||
minVal = parseNumberAttrVal(attr.min);
|
||||
|
||||
ctrl.$validators.min = supportsRange ?
|
||||
// Since all browsers set the input to a valid value, we don't need to check validity
|
||||
function noopMinValidator() { return true; } :
|
||||
@@ -1794,6 +1824,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (hasMaxAttr) {
|
||||
maxVal = parseNumberAttrVal(attr.max);
|
||||
|
||||
ctrl.$validators.max = supportsRange ?
|
||||
// Since all browsers set the input to a valid value, we don't need to check validity
|
||||
function noopMaxValidator() { return true; } :
|
||||
@@ -1806,6 +1838,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
if (hasStepAttr) {
|
||||
stepVal = parseNumberAttrVal(attr.step);
|
||||
|
||||
ctrl.$validators.step = supportsRange ?
|
||||
function nativeStepValidator() {
|
||||
// Currently, only FF implements the spec on step change correctly (i.e. adjusting the
|
||||
@@ -1827,7 +1861,13 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
// attribute value when the input is first rendered, so that the browser can adjust the
|
||||
// input value based on the min/max value
|
||||
element.attr(htmlAttrName, attr[htmlAttrName]);
|
||||
attr.$observe(htmlAttrName, changeFn);
|
||||
var oldVal = attr[htmlAttrName];
|
||||
attr.$observe(htmlAttrName, function wrappedObserver(val) {
|
||||
if (val !== oldVal) {
|
||||
oldVal = val;
|
||||
changeFn(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function minChange(val) {
|
||||
@@ -1881,11 +1921,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
||||
}
|
||||
|
||||
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
|
||||
if (supportsRange && ctrl.$viewValue !== element.val()) {
|
||||
ctrl.$setViewValue(element.val());
|
||||
} else {
|
||||
if (!supportsRange) {
|
||||
// TODO(matsko): implement validateLater to reduce number of validations
|
||||
ctrl.$validate();
|
||||
} else if (ctrl.$viewValue !== element.val()) {
|
||||
ctrl.$setViewValue(element.val());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2193,6 +2233,48 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
|
||||
}];
|
||||
|
||||
|
||||
var hiddenInputBrowserCacheDirective = function() {
|
||||
var valueProperty = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
get: function() {
|
||||
return this.getAttribute('value') || '';
|
||||
},
|
||||
set: function(val) {
|
||||
this.setAttribute('value', val);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
priority: 200,
|
||||
compile: function(_, attr) {
|
||||
if (lowercase(attr.type) !== 'hidden') {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
pre: function(scope, element, attr, ctrls) {
|
||||
var node = element[0];
|
||||
|
||||
// Support: Edge
|
||||
// Moving the DOM around prevents autofillling
|
||||
if (node.parentNode) {
|
||||
node.parentNode.insertBefore(node, node.nextSibling);
|
||||
}
|
||||
|
||||
// Support: FF, IE
|
||||
// Avoiding direct assignment to .value prevents autofillling
|
||||
if (Object.defineProperty) {
|
||||
Object.defineProperty(node, 'value', valueProperty);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
||||
/**
|
||||
|
||||
@@ -125,6 +125,8 @@ function classDirective(name, selector) {
|
||||
}
|
||||
|
||||
function toClassString(classValue) {
|
||||
if (!classValue) return classValue;
|
||||
|
||||
var classString = classValue;
|
||||
|
||||
if (isArray(classValue)) {
|
||||
@@ -133,6 +135,8 @@ function classDirective(name, selector) {
|
||||
classString = Object.keys(classValue).
|
||||
filter(function(key) { return classValue[key]; }).
|
||||
join(' ');
|
||||
} else if (!isString(classValue)) {
|
||||
classString = classValue + '';
|
||||
}
|
||||
|
||||
return classString;
|
||||
|
||||
@@ -562,6 +562,7 @@ NgModelController.prototype = {
|
||||
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
|
||||
*/
|
||||
$validate: function() {
|
||||
|
||||
// ignore $validate before model is initialized
|
||||
if (isNumberNaN(this.$modelValue)) {
|
||||
return;
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
* For example, if an item is added to the collection, `ngRepeat` will know that all other items
|
||||
* already have DOM elements, and will not re-render them.
|
||||
*
|
||||
* All different types of tracking functions, their syntax, and and their support for duplicate
|
||||
* All different types of tracking functions, their syntax, and their support for duplicate
|
||||
* items in collections can be found in the
|
||||
* {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
|
||||
*
|
||||
@@ -454,6 +454,13 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
|
||||
return block.clone[block.clone.length - 1];
|
||||
};
|
||||
|
||||
var trackByIdArrayFn = function($scope, key, value) {
|
||||
return hashKey(value);
|
||||
};
|
||||
|
||||
var trackByIdObjFn = function($scope, key) {
|
||||
return key;
|
||||
};
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
@@ -493,32 +500,23 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
|
||||
aliasAs);
|
||||
}
|
||||
|
||||
var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
|
||||
var hashFnLocals = {$id: hashKey};
|
||||
var trackByIdExpFn;
|
||||
|
||||
if (trackByExp) {
|
||||
trackByExpGetter = $parse(trackByExp);
|
||||
} else {
|
||||
trackByIdArrayFn = function(key, value) {
|
||||
return hashKey(value);
|
||||
};
|
||||
trackByIdObjFn = function(key) {
|
||||
return key;
|
||||
var hashFnLocals = {$id: hashKey};
|
||||
var trackByExpGetter = $parse(trackByExp);
|
||||
|
||||
trackByIdExpFn = function($scope, key, value, index) {
|
||||
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||||
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||||
hashFnLocals[valueIdentifier] = value;
|
||||
hashFnLocals.$index = index;
|
||||
return trackByExpGetter($scope, hashFnLocals);
|
||||
};
|
||||
}
|
||||
|
||||
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
|
||||
|
||||
if (trackByExpGetter) {
|
||||
trackByIdExpFn = function(key, value, index) {
|
||||
// assign key, value, and $index to the locals so that they can be used in hash functions
|
||||
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
|
||||
hashFnLocals[valueIdentifier] = value;
|
||||
hashFnLocals.$index = index;
|
||||
return trackByExpGetter($scope, hashFnLocals);
|
||||
};
|
||||
}
|
||||
|
||||
// Store a list of elements from previous run. This is a hash where key is the item from the
|
||||
// iterator, and the value is objects with following properties.
|
||||
// - scope: bound scope
|
||||
@@ -572,7 +570,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
|
||||
for (index = 0; index < collectionLength; index++) {
|
||||
key = (collection === collectionKeys) ? index : collectionKeys[index];
|
||||
value = collection[key];
|
||||
trackById = trackByIdFn(key, value, index);
|
||||
trackById = trackByIdFn($scope, key, value, index);
|
||||
if (lastBlockMap[trackById]) {
|
||||
// found previously seen block
|
||||
block = lastBlockMap[trackById];
|
||||
@@ -594,6 +592,12 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the value property from the hashFnLocals object to prevent a reference to the last value
|
||||
// being leaked into the ngRepeatCompile function scope
|
||||
if (hashFnLocals) {
|
||||
hashFnLocals[valueIdentifier] = undefined;
|
||||
}
|
||||
|
||||
// remove leftover items
|
||||
for (var blockKey in lastBlockMap) {
|
||||
block = lastBlockMap[blockKey];
|
||||
|
||||
@@ -54,7 +54,14 @@
|
||||
var ngStyleDirective = ngDirective(function(scope, element, attr) {
|
||||
scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
|
||||
if (oldStyles && (newStyles !== oldStyles)) {
|
||||
forEach(oldStyles, function(val, style) { element.css(style, '');});
|
||||
if (!newStyles) {
|
||||
newStyles = {};
|
||||
}
|
||||
forEach(oldStyles, function(val, style) {
|
||||
if (newStyles[style] == null) {
|
||||
newStyles[style] = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (newStyles) element.css(newStyles);
|
||||
});
|
||||
|
||||
@@ -383,7 +383,7 @@ var SelectController =
|
||||
|
||||
if (optionAttrs.$attr.ngValue) {
|
||||
// The value attribute is set by ngValue
|
||||
var oldVal, hashedVal = NaN;
|
||||
var oldVal, hashedVal;
|
||||
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
|
||||
|
||||
var removal;
|
||||
@@ -556,18 +556,6 @@ var SelectController =
|
||||
* {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
|
||||
*
|
||||
*
|
||||
* @knownIssue
|
||||
*
|
||||
* In Firefox, the select model is only updated when the select element is blurred. For example,
|
||||
* when switching between options with the keyboard, the select model is only set to the
|
||||
* currently selected option when the select is blurred, e.g via tab key or clicking the mouse
|
||||
* outside the select.
|
||||
*
|
||||
* This is due to an ambiguity in the select element specification. See the
|
||||
* [issue on the Firefox bug tracker](https://bugzilla.mozilla.org/show_bug.cgi?id=126379)
|
||||
* for more information, and this
|
||||
* [Github comment for a workaround](https://github.com/angular/angular.js/issues/9134#issuecomment-130800488)
|
||||
*
|
||||
* @example
|
||||
* ### Simple `select` elements with static options
|
||||
*
|
||||
|
||||
@@ -62,24 +62,29 @@
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var requiredDirective = function() {
|
||||
var requiredDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
var oldVal = attr.required || $parse(attr.ngRequired)(scope);
|
||||
|
||||
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();
|
||||
attr.$observe('required', function(val) {
|
||||
if (oldVal !== val) {
|
||||
oldVal = val;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -162,36 +167,59 @@ var requiredDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var patternDirective = function() {
|
||||
var patternDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
compile: function(tElm, tAttr) {
|
||||
var patternExp;
|
||||
var parseFn;
|
||||
|
||||
var regexp, patternExp = attr.ngPattern || attr.pattern;
|
||||
attr.$observe('pattern', function(regex) {
|
||||
if (isString(regex) && regex.length > 0) {
|
||||
regex = new RegExp('^' + regex + '$');
|
||||
if (tAttr.ngPattern) {
|
||||
patternExp = tAttr.ngPattern;
|
||||
|
||||
// ngPattern might be a scope expression, or an inlined regex, which is not parsable.
|
||||
// We get value of the attribute here, so we can compare the old and the new value
|
||||
// in the observer to avoid unnecessary validations
|
||||
if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
|
||||
parseFn = function() { return tAttr.ngPattern; };
|
||||
} else {
|
||||
parseFn = $parse(tAttr.ngPattern);
|
||||
}
|
||||
}
|
||||
|
||||
return function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var attrVal = attr.pattern;
|
||||
|
||||
if (attr.ngPattern) {
|
||||
attrVal = parseFn(scope);
|
||||
} else {
|
||||
patternExp = attr.pattern;
|
||||
}
|
||||
|
||||
if (regex && !regex.test) {
|
||||
throw minErr('ngPattern')('noregexp',
|
||||
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
|
||||
regex, startingTag(elm));
|
||||
}
|
||||
var regexp = parsePatternAttr(attrVal, patternExp, elm);
|
||||
|
||||
regexp = regex || undefined;
|
||||
ctrl.$validate();
|
||||
});
|
||||
attr.$observe('pattern', function(newVal) {
|
||||
var oldRegexp = regexp;
|
||||
|
||||
ctrl.$validators.pattern = function(modelValue, viewValue) {
|
||||
// HTML5 pattern constraint validates the input value, so we validate the viewValue
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
|
||||
regexp = parsePatternAttr(newVal, patternExp, elm);
|
||||
|
||||
if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
|
||||
ctrl.$validators.pattern = function(modelValue, viewValue) {
|
||||
// HTML5 pattern constraint validates the input value, so we validate the viewValue
|
||||
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -264,25 +292,29 @@ var patternDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var maxlengthDirective = function() {
|
||||
var maxlengthDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var maxlength = -1;
|
||||
var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
|
||||
var maxlengthParsed = parseLength(maxlength);
|
||||
|
||||
attr.$observe('maxlength', function(value) {
|
||||
var intVal = toInt(value);
|
||||
maxlength = isNumberNaN(intVal) ? -1 : intVal;
|
||||
ctrl.$validate();
|
||||
if (maxlength !== value) {
|
||||
maxlengthParsed = parseLength(value);
|
||||
maxlength = value;
|
||||
ctrl.$validate();
|
||||
}
|
||||
});
|
||||
ctrl.$validators.maxlength = function(modelValue, viewValue) {
|
||||
return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
|
||||
return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
/**
|
||||
* @ngdoc directive
|
||||
@@ -353,21 +385,49 @@ var maxlengthDirective = function() {
|
||||
* </file>
|
||||
* </example>
|
||||
*/
|
||||
var minlengthDirective = function() {
|
||||
var minlengthDirective = ['$parse', function($parse) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: '?ngModel',
|
||||
link: function(scope, elm, attr, ctrl) {
|
||||
if (!ctrl) return;
|
||||
|
||||
var minlength = 0;
|
||||
var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
|
||||
var minlengthParsed = parseLength(minlength) || -1;
|
||||
|
||||
attr.$observe('minlength', function(value) {
|
||||
minlength = toInt(value) || 0;
|
||||
ctrl.$validate();
|
||||
if (minlength !== value) {
|
||||
minlengthParsed = parseLength(value) || -1;
|
||||
minlength = value;
|
||||
ctrl.$validate();
|
||||
}
|
||||
|
||||
});
|
||||
ctrl.$validators.minlength = function(modelValue, viewValue) {
|
||||
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
|
||||
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
}];
|
||||
|
||||
|
||||
function parsePatternAttr(regex, patternExp, elm) {
|
||||
if (!regex) return undefined;
|
||||
|
||||
if (isString(regex)) {
|
||||
regex = new RegExp('^' + regex + '$');
|
||||
}
|
||||
|
||||
if (!regex.test) {
|
||||
throw minErr('ngPattern')('noregexp',
|
||||
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
|
||||
regex, startingTag(elm));
|
||||
}
|
||||
|
||||
return regex;
|
||||
}
|
||||
|
||||
function parseLength(val) {
|
||||
var intVal = toInt(val);
|
||||
return isNumberNaN(intVal) ? -1 : intVal;
|
||||
}
|
||||
|
||||
@@ -242,7 +242,7 @@ function $InterpolateProvider() {
|
||||
|
||||
// Provide a quick exit and simplified result function for text with no interpolation
|
||||
if (!text.length || text.indexOf(startSymbol) === -1) {
|
||||
if (mustHaveExpression && !contextAllowsConcatenation) return;
|
||||
if (mustHaveExpression) return;
|
||||
|
||||
var unescapedText = unescapeText(text);
|
||||
if (contextAllowsConcatenation) {
|
||||
|
||||
+7
-1
@@ -683,5 +683,11 @@ function markQStateExceptionHandled(state) {
|
||||
state.pur = true;
|
||||
}
|
||||
function markQExceptionHandled(q) {
|
||||
markQStateExceptionHandled(q.$$state);
|
||||
// Built-in `$q` promises will always have a `$$state` property. This check is to allow
|
||||
// overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises).
|
||||
// (Currently, this is the only method that might be called with a promise, even if it is not
|
||||
// created by the built-in `$q`.)
|
||||
if (q.$$state) {
|
||||
markQStateExceptionHandled(q.$$state);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -440,7 +440,7 @@ function $SceDelegateProvider() {
|
||||
// If we get here, then we will either sanitize the value or throw an exception.
|
||||
if (type === SCE_CONTEXTS.MEDIA_URL || type === SCE_CONTEXTS.URL) {
|
||||
// we attempt to sanitize non-resource URLs
|
||||
return $$sanitizeUri(maybeTrusted, type === SCE_CONTEXTS.MEDIA_URL);
|
||||
return $$sanitizeUri(maybeTrusted.toString(), type === SCE_CONTEXTS.MEDIA_URL);
|
||||
} else if (type === SCE_CONTEXTS.RESOURCE_URL) {
|
||||
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
|
||||
return maybeTrusted;
|
||||
|
||||
+13
-1
@@ -10,6 +10,12 @@ var urlParsingNode = window.document.createElement('a');
|
||||
var originUrl = urlResolve(window.location.href);
|
||||
var baseUrlParsingNode;
|
||||
|
||||
urlParsingNode.href = 'http://[::1]';
|
||||
|
||||
// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
|
||||
// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
|
||||
// when parsed out of an anchor element.
|
||||
var ipv6InBrackets = urlParsingNode.hostname === '[::1]';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -72,13 +78,19 @@ function urlResolve(url) {
|
||||
|
||||
urlParsingNode.setAttribute('href', href);
|
||||
|
||||
var hostname = urlParsingNode.hostname;
|
||||
|
||||
if (!ipv6InBrackets && hostname.indexOf(':') > -1) {
|
||||
hostname = '[' + hostname + ']';
|
||||
}
|
||||
|
||||
return {
|
||||
href: urlParsingNode.href,
|
||||
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
|
||||
host: urlParsingNode.host,
|
||||
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
|
||||
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
|
||||
hostname: urlParsingNode.hostname,
|
||||
hostname: hostname,
|
||||
port: urlParsingNode.port,
|
||||
pathname: (urlParsingNode.pathname.charAt(0) === '/')
|
||||
? urlParsingNode.pathname
|
||||
|
||||
@@ -57,11 +57,11 @@
|
||||
"applyInlineStyle": false,
|
||||
"assertArg": false,
|
||||
"blockKeyframeAnimations": false,
|
||||
"blockTransitions": false,
|
||||
"clearGeneratedClasses": false,
|
||||
"concatWithSpace": false,
|
||||
"extractElementNode": false,
|
||||
"getDomNode": false,
|
||||
"helpers": false,
|
||||
"mergeAnimationDetails": false,
|
||||
"mergeClasses": false,
|
||||
"packageStyles": false,
|
||||
|
||||
@@ -560,7 +560,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
// that if there is no transition defined then nothing will happen and this will also allow
|
||||
// other transitions to be stacked on top of each other without any chopping them out.
|
||||
if (isFirst && !options.skipBlocking) {
|
||||
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
|
||||
helpers.blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
|
||||
}
|
||||
|
||||
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
|
||||
@@ -646,7 +646,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
if (flags.blockTransition || flags.blockKeyframeAnimation) {
|
||||
applyBlocking(maxDuration);
|
||||
} else if (!options.skipBlocking) {
|
||||
blockTransitions(node, false);
|
||||
helpers.blockTransitions(node, false);
|
||||
}
|
||||
|
||||
// TODO(matsko): for 1.5 change this code to have an animator object for better debugging
|
||||
@@ -699,7 +699,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
}
|
||||
|
||||
blockKeyframeAnimations(node, false);
|
||||
blockTransitions(node, false);
|
||||
helpers.blockTransitions(node, false);
|
||||
|
||||
forEach(temporaryStyles, function(entry) {
|
||||
// There is only one way to remove inline style properties entirely from elements.
|
||||
@@ -750,7 +750,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
function applyBlocking(duration) {
|
||||
if (flags.blockTransition) {
|
||||
blockTransitions(node, duration);
|
||||
helpers.blockTransitions(node, duration);
|
||||
}
|
||||
|
||||
if (flags.blockKeyframeAnimation) {
|
||||
|
||||
@@ -113,8 +113,6 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
|
||||
// TODO(matsko): document the signature in a better way
|
||||
return function(element, event, options) {
|
||||
var node = getDomNode(element);
|
||||
|
||||
options = prepareAnimationOptions(options);
|
||||
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
|
||||
|
||||
@@ -186,8 +184,9 @@ var $$AnimationProvider = ['$animateProvider', /** @this */ function($animatePro
|
||||
forEach(groupedAnimations, function(animationEntry) {
|
||||
var element = animationEntry.from ? animationEntry.from.element : animationEntry.element;
|
||||
var extraClasses = options.addClass;
|
||||
|
||||
extraClasses = (extraClasses ? (extraClasses + ' ') : '') + NG_ANIMATE_CLASSNAME;
|
||||
var cacheKey = $$animateCache.cacheKey(node, event, extraClasses, options.removeClass);
|
||||
var cacheKey = $$animateCache.cacheKey(element[0], animationEntry.event, extraClasses, options.removeClass);
|
||||
|
||||
toBeSortedAnimations.push({
|
||||
element: element,
|
||||
|
||||
@@ -92,7 +92,8 @@ var ngAnimateSwapDirective = ['$animate', function($animate) {
|
||||
restrict: 'A',
|
||||
transclude: 'element',
|
||||
terminal: true,
|
||||
priority: 600, // we use 600 here to ensure that the directive is caught before others
|
||||
priority: 550, // We use 550 here to ensure that the directive is caught before others,
|
||||
// but after `ngIf` (at priority 600).
|
||||
link: function(scope, $element, attrs, ctrl, $transclude) {
|
||||
var previousElement, previousScope;
|
||||
scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) {
|
||||
|
||||
+11
-9
@@ -329,15 +329,6 @@ function clearGeneratedClasses(element, options) {
|
||||
}
|
||||
}
|
||||
|
||||
function blockTransitions(node, duration) {
|
||||
// we use a negative delay value since it performs blocking
|
||||
// yet it doesn't kill any existing transitions running on the
|
||||
// same element which makes this safe for class-based animations
|
||||
var value = duration ? '-' + duration + 's' : '';
|
||||
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
|
||||
return [TRANSITION_DELAY_PROP, value];
|
||||
}
|
||||
|
||||
function blockKeyframeAnimations(node, applyBlock) {
|
||||
var value = applyBlock ? 'paused' : '';
|
||||
var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
|
||||
@@ -356,3 +347,14 @@ function concatWithSpace(a,b) {
|
||||
if (!b) return a;
|
||||
return a + ' ' + b;
|
||||
}
|
||||
|
||||
var helpers = {
|
||||
blockTransitions: function(node, duration) {
|
||||
// we use a negative delay value since it performs blocking
|
||||
// yet it doesn't kill any existing transitions running on the
|
||||
// same element which makes this safe for class-based animations
|
||||
var value = duration ? '-' + duration + 's' : '';
|
||||
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
|
||||
return [TRANSITION_DELAY_PROP, value];
|
||||
}
|
||||
};
|
||||
|
||||
+6
-2
@@ -389,8 +389,12 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
|
||||
var keyCode = event.which || event.keyCode;
|
||||
|
||||
if (keyCode === 13 || keyCode === 32) {
|
||||
// Prevent the default browser behavior (e.g. scrolling when pressing spacebar).
|
||||
event.preventDefault();
|
||||
// If the event is triggered on a non-interactive element ...
|
||||
if (nodeBlackList.indexOf(event.target.nodeName) === -1 && !event.target.isContentEditable) {
|
||||
// ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
|
||||
// See https://github.com/angular/angular.js/issues/16664
|
||||
event.preventDefault();
|
||||
}
|
||||
scope.$apply(callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -215,10 +215,10 @@ var noop;
|
||||
var toJson;
|
||||
var $$stringify;
|
||||
|
||||
var module = window['angular']['module']('ngMessageFormat', ['ng']);
|
||||
module['info']({ 'angularVersion': '"NG_VERSION_FULL"' });
|
||||
module['factory']('$$messageFormat', $$MessageFormatFactory);
|
||||
module['config'](['$provide', function($provide) {
|
||||
var ngModule = window['angular']['module']('ngMessageFormat', ['ng']);
|
||||
ngModule['info']({ 'angularVersion': '"NG_VERSION_FULL"' });
|
||||
ngModule['factory']('$$messageFormat', $$MessageFormatFactory);
|
||||
ngModule['config'](['$provide', function($provide) {
|
||||
$interpolateMinErr = window['angular']['$interpolateMinErr'];
|
||||
isFunction = window['angular']['isFunction'];
|
||||
noop = window['angular']['noop'];
|
||||
|
||||
Vendored
+118
-98
@@ -1619,12 +1619,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $httpBackend#matchLatestDefinition
|
||||
* @name $httpBackend#matchLatestDefinitionEnabled
|
||||
* @description
|
||||
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
|
||||
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
|
||||
* By default, `$httpBackend` returns the first definition that matches. When setting
|
||||
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
|
||||
* `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
|
||||
* one that was added last.
|
||||
*
|
||||
* ```js
|
||||
@@ -1632,7 +1632,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* hb.when('GET', '/url1').respond(201, 'another', {});
|
||||
* hb('GET', '/url1'); // receives "content"
|
||||
*
|
||||
* $http.matchLatestDefinition(true)
|
||||
* $http.matchLatestDefinitionEnabled(true)
|
||||
* hb('GET', '/url1'); // receives "another"
|
||||
*
|
||||
* hb.when('GET', '/url1').respond(201, 'onemore', {});
|
||||
@@ -1641,7 +1641,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
*
|
||||
* This is useful if a you have a default response that is overriden inside specific tests.
|
||||
*
|
||||
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
|
||||
* Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
|
||||
* even when the application is already running.
|
||||
*
|
||||
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
|
||||
@@ -1650,7 +1650,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* as a getter
|
||||
*/
|
||||
$httpBackend.matchLatestDefinitionEnabled = function(value) {
|
||||
if (isDefined(value)) {
|
||||
if (angular.isDefined(value)) {
|
||||
matchLatestDefinition = value;
|
||||
return this;
|
||||
} else {
|
||||
@@ -1771,8 +1771,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* See {@link ngMock.$httpBackend#when `when`} for more info.
|
||||
*/
|
||||
$httpBackend.whenRoute = function(method, url) {
|
||||
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
|
||||
return $httpBackend.when(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
var parsed = parseRouteUrl(url);
|
||||
return $httpBackend.when(method, parsed.regexp, undefined, undefined, parsed.keys);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1955,8 +1955,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
* See {@link ngMock.$httpBackend#expect `expect`} for more info.
|
||||
*/
|
||||
$httpBackend.expectRoute = function(method, url) {
|
||||
var pathObj = routeToRegExp(url, {caseInsensitiveMatch: true, ignoreTrailingSlashes: true});
|
||||
return $httpBackend.expect(method, pathObj.regexp, undefined, undefined, pathObj.keys);
|
||||
var parsed = parseRouteUrl(url);
|
||||
return $httpBackend.expect(method, parsed.regexp, undefined, undefined, parsed.keys);
|
||||
};
|
||||
|
||||
|
||||
@@ -2084,6 +2084,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function parseRouteUrl(url) {
|
||||
var strippedUrl = stripQueryAndHash(url);
|
||||
var parseOptions = {caseInsensitiveMatch: true, ignoreTrailingSlashes: true};
|
||||
return routeToRegExp(strippedUrl, parseOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function assertArgDefined(args, index, name) {
|
||||
@@ -2092,110 +2098,124 @@ function assertArgDefined(args, index, name) {
|
||||
}
|
||||
}
|
||||
|
||||
function stripQueryAndHash(url) {
|
||||
return url.replace(/[?#].*$/, '');
|
||||
}
|
||||
|
||||
function MockHttpExpectation(method, url, data, headers, keys) {
|
||||
function MockHttpExpectation(expectedMethod, expectedUrl, expectedData, expectedHeaders,
|
||||
expectedKeys) {
|
||||
|
||||
function getUrlParams(u) {
|
||||
var params = u.slice(u.indexOf('?') + 1).split('&');
|
||||
return params.sort();
|
||||
}
|
||||
this.data = expectedData;
|
||||
this.headers = expectedHeaders;
|
||||
|
||||
function compareUrl(u) {
|
||||
return (url.slice(0, url.indexOf('?')) === u.slice(0, u.indexOf('?')) &&
|
||||
getUrlParams(url).join() === getUrlParams(u).join());
|
||||
}
|
||||
|
||||
this.data = data;
|
||||
this.headers = headers;
|
||||
|
||||
this.match = function(m, u, d, h) {
|
||||
if (method !== m) return false;
|
||||
if (!this.matchUrl(u)) return false;
|
||||
if (angular.isDefined(d) && !this.matchData(d)) return false;
|
||||
if (angular.isDefined(h) && !this.matchHeaders(h)) return false;
|
||||
this.match = function(method, url, data, headers) {
|
||||
if (expectedMethod !== method) return false;
|
||||
if (!this.matchUrl(url)) return false;
|
||||
if (angular.isDefined(data) && !this.matchData(data)) return false;
|
||||
if (angular.isDefined(headers) && !this.matchHeaders(headers)) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
this.matchUrl = function(u) {
|
||||
if (!url) return true;
|
||||
if (angular.isFunction(url.test)) return url.test(u);
|
||||
if (angular.isFunction(url)) return url(u);
|
||||
return (url === u || compareUrl(u));
|
||||
this.matchUrl = function(url) {
|
||||
if (!expectedUrl) return true;
|
||||
if (angular.isFunction(expectedUrl.test)) return expectedUrl.test(url);
|
||||
if (angular.isFunction(expectedUrl)) return expectedUrl(url);
|
||||
return (expectedUrl === url || compareUrlWithQuery(url));
|
||||
};
|
||||
|
||||
this.matchHeaders = function(h) {
|
||||
if (angular.isUndefined(headers)) return true;
|
||||
if (angular.isFunction(headers)) return headers(h);
|
||||
return angular.equals(headers, h);
|
||||
this.matchHeaders = function(headers) {
|
||||
if (angular.isUndefined(expectedHeaders)) return true;
|
||||
if (angular.isFunction(expectedHeaders)) return expectedHeaders(headers);
|
||||
return angular.equals(expectedHeaders, headers);
|
||||
};
|
||||
|
||||
this.matchData = function(d) {
|
||||
if (angular.isUndefined(data)) return true;
|
||||
if (data && angular.isFunction(data.test)) return data.test(d);
|
||||
if (data && angular.isFunction(data)) return data(d);
|
||||
if (data && !angular.isString(data)) {
|
||||
return angular.equals(angular.fromJson(angular.toJson(data)), angular.fromJson(d));
|
||||
this.matchData = function(data) {
|
||||
if (angular.isUndefined(expectedData)) return true;
|
||||
if (expectedData && angular.isFunction(expectedData.test)) return expectedData.test(data);
|
||||
if (expectedData && angular.isFunction(expectedData)) return expectedData(data);
|
||||
if (expectedData && !angular.isString(expectedData)) {
|
||||
return angular.equals(angular.fromJson(angular.toJson(expectedData)), angular.fromJson(data));
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return data == d;
|
||||
return expectedData == data;
|
||||
};
|
||||
|
||||
this.toString = function() {
|
||||
return method + ' ' + url;
|
||||
return expectedMethod + ' ' + expectedUrl;
|
||||
};
|
||||
|
||||
this.params = function(u) {
|
||||
return angular.extend(parseQuery(), pathParams());
|
||||
this.params = function(url) {
|
||||
var queryStr = url.indexOf('?') === -1 ? '' : url.substring(url.indexOf('?') + 1);
|
||||
var strippedUrl = stripQueryAndHash(url);
|
||||
|
||||
function pathParams() {
|
||||
var keyObj = {};
|
||||
if (!url || !angular.isFunction(url.test) || !keys || keys.length === 0) return keyObj;
|
||||
|
||||
var m = url.exec(u);
|
||||
if (!m) return keyObj;
|
||||
for (var i = 1, len = m.length; i < len; ++i) {
|
||||
var key = keys[i - 1];
|
||||
var val = m[i];
|
||||
if (key && val) {
|
||||
keyObj[key.name || key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return keyObj;
|
||||
}
|
||||
|
||||
function parseQuery() {
|
||||
var obj = {}, key_value, key,
|
||||
queryStr = u.indexOf('?') > -1
|
||||
? u.substring(u.indexOf('?') + 1)
|
||||
: '';
|
||||
|
||||
angular.forEach(queryStr.split('&'), function(keyValue) {
|
||||
if (keyValue) {
|
||||
key_value = keyValue.replace(/\+/g,'%20').split('=');
|
||||
key = tryDecodeURIComponent(key_value[0]);
|
||||
if (angular.isDefined(key)) {
|
||||
var val = angular.isDefined(key_value[1]) ? tryDecodeURIComponent(key_value[1]) : true;
|
||||
if (!hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = val;
|
||||
} else if (angular.isArray(obj[key])) {
|
||||
obj[key].push(val);
|
||||
} else {
|
||||
obj[key] = [obj[key],val];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
function tryDecodeURIComponent(value) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
// Ignore any invalid uri component
|
||||
}
|
||||
}
|
||||
return angular.extend(extractParamsFromQuery(queryStr), extractParamsFromPath(strippedUrl));
|
||||
};
|
||||
|
||||
function compareUrlWithQuery(url) {
|
||||
var urlWithQueryRe = /^([^?]*)\?(.*)$/;
|
||||
|
||||
var expectedMatch = urlWithQueryRe.exec(expectedUrl);
|
||||
var actualMatch = urlWithQueryRe.exec(url);
|
||||
|
||||
return !!(expectedMatch && actualMatch) &&
|
||||
(expectedMatch[1] === actualMatch[1]) &&
|
||||
(normalizeQuery(expectedMatch[2]) === normalizeQuery(actualMatch[2]));
|
||||
}
|
||||
|
||||
function normalizeQuery(queryStr) {
|
||||
return queryStr.split('&').sort().join('&');
|
||||
}
|
||||
|
||||
function extractParamsFromPath(strippedUrl) {
|
||||
var keyObj = {};
|
||||
|
||||
if (!expectedUrl || !angular.isFunction(expectedUrl.test) ||
|
||||
!expectedKeys || !expectedKeys.length) return keyObj;
|
||||
|
||||
var match = expectedUrl.exec(strippedUrl);
|
||||
if (!match) return keyObj;
|
||||
|
||||
for (var i = 1, len = match.length; i < len; ++i) {
|
||||
var key = expectedKeys[i - 1];
|
||||
var val = match[i];
|
||||
if (key && val) {
|
||||
keyObj[key.name || key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
return keyObj;
|
||||
}
|
||||
|
||||
function extractParamsFromQuery(queryStr) {
|
||||
var obj = {},
|
||||
keyValuePairs = queryStr.split('&').
|
||||
filter(angular.identity). // Ignore empty segments.
|
||||
map(function(keyValue) { return keyValue.replace(/\+/g, '%20').split('='); });
|
||||
|
||||
angular.forEach(keyValuePairs, function(pair) {
|
||||
var key = tryDecodeURIComponent(pair[0]);
|
||||
if (angular.isDefined(key)) {
|
||||
var val = angular.isDefined(pair[1]) ? tryDecodeURIComponent(pair[1]) : true;
|
||||
if (!hasOwnProperty.call(obj, key)) {
|
||||
obj[key] = val;
|
||||
} else if (angular.isArray(obj[key])) {
|
||||
obj[key].push(val);
|
||||
} else {
|
||||
obj[key] = [obj[key], val];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function tryDecodeURIComponent(value) {
|
||||
try {
|
||||
return decodeURIComponent(value);
|
||||
} catch (e) {
|
||||
// Ignore any invalid uri component
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createMockXhr() {
|
||||
@@ -2899,13 +2919,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
|
||||
*/
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $httpBackend#matchLatestDefinition
|
||||
* @name $httpBackend#matchLatestDefinitionEnabled
|
||||
* @module ngMockE2E
|
||||
* @description
|
||||
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
|
||||
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
|
||||
* By default, `$httpBackend` returns the first definition that matches. When setting
|
||||
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
|
||||
* `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
|
||||
* one that was added last.
|
||||
*
|
||||
* ```js
|
||||
@@ -2913,7 +2933,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
|
||||
* hb.when('GET', '/url1').respond(201, 'another', {});
|
||||
* hb('GET', '/url1'); // receives "content"
|
||||
*
|
||||
* $http.matchLatestDefinition(true)
|
||||
* $http.matchLatestDefinitionEnabled(true)
|
||||
* hb('GET', '/url1'); // receives "another"
|
||||
*
|
||||
* hb.when('GET', '/url1').respond(201, 'onemore', {});
|
||||
@@ -2922,7 +2942,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
|
||||
*
|
||||
* This is useful if a you have a default response that is overriden inside specific tests.
|
||||
*
|
||||
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
|
||||
* Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
|
||||
* even when the application is already running.
|
||||
*
|
||||
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
|
||||
|
||||
@@ -225,6 +225,7 @@ function $RouteProvider() {
|
||||
}
|
||||
routes[path] = angular.extend(
|
||||
routeCopy,
|
||||
{originalPath: path},
|
||||
path && routeToRegExp(path, routeCopy)
|
||||
);
|
||||
|
||||
@@ -235,7 +236,7 @@ function $RouteProvider() {
|
||||
: path + '/';
|
||||
|
||||
routes[redirectPath] = angular.extend(
|
||||
{redirectTo: path},
|
||||
{originalPath: path, redirectTo: path},
|
||||
routeToRegExp(redirectPath, routeCopy)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
/* global routeToRegExp: true */
|
||||
|
||||
/**
|
||||
* @param path {string} path
|
||||
* @param opts {Object} options
|
||||
* @return {?Object}
|
||||
* @param {string} path - The path to parse. (It is assumed to have query and hash stripped off.)
|
||||
* @param {Object} opts - Options.
|
||||
* @return {Object} - An object containing an array of path parameter names (`keys`) and a regular
|
||||
* expression (`regexp`) that can be used to identify a matching URL and extract the path
|
||||
* parameter values.
|
||||
*
|
||||
* @description
|
||||
* Normalizes the given path, returning a regular expression
|
||||
* and the original path.
|
||||
* Parses the given path, extracting path parameter names and a regular expression to match URLs.
|
||||
*
|
||||
* Inspired by pathRexp in visionmedia/express/lib/utils.js.
|
||||
* Originally inspired by `pathRexp` in `visionmedia/express/lib/utils.js`.
|
||||
*/
|
||||
function routeToRegExp(path, opts) {
|
||||
var keys = [];
|
||||
@@ -21,11 +22,11 @@ function routeToRegExp(path, opts) {
|
||||
.replace(/(\/)?:(\w+)(\*\?|[?*])?/g, function(_, slash, key, option) {
|
||||
var optional = option === '?' || option === '*?';
|
||||
var star = option === '*' || option === '*?';
|
||||
keys.push({ name: key, optional: optional });
|
||||
keys.push({name: key, optional: optional});
|
||||
slash = slash || '';
|
||||
return (
|
||||
(optional ? '(?:' + slash : slash + '(?:') +
|
||||
(star ? '([^?#]+?)' : '([^/?#]+)') +
|
||||
(star ? '(.+?)' : '([^/]+)') +
|
||||
(optional ? '?)?' : ')')
|
||||
);
|
||||
})
|
||||
@@ -36,7 +37,6 @@ function routeToRegExp(path, opts) {
|
||||
}
|
||||
|
||||
return {
|
||||
originalPath: path,
|
||||
keys: keys,
|
||||
regexp: new RegExp(
|
||||
'^' + pattern + '(?:[?#]|$)',
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="test">
|
||||
<body ng-class="{hacked: internalFnCalled}">
|
||||
<form>
|
||||
<input id="input1" type="hidden" value="{{value}}" />
|
||||
<input id="input2" type="hidden" ng-value="value" />
|
||||
|
||||
<textarea ng-model="value"></textarea>
|
||||
</form>
|
||||
<script src="angular.js"></script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('test', [])
|
||||
.run(function($rootScope) {
|
||||
$rootScope.internalFnCalled = false;
|
||||
|
||||
$rootScope.internalFn = function() {
|
||||
$rootScope.internalFnCalled = true;
|
||||
};
|
||||
});
|
||||
@@ -7,7 +7,7 @@ describe('SCE URL policy when base tags are present', function() {
|
||||
|
||||
|
||||
it('allows the page URL (location.href)', function() {
|
||||
expectToBeTrusted(browser.getLocationAbsUrl(), true);
|
||||
expectToBeTrusted(browser.getCurrentUrl(), true);
|
||||
});
|
||||
|
||||
it('blocks off-origin URLs', function() {
|
||||
|
||||
@@ -14,4 +14,72 @@ describe('hidden thingy', function() {
|
||||
var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : '';
|
||||
expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue);
|
||||
});
|
||||
|
||||
it('should prevent browser autofill on browser.refresh', function() {
|
||||
|
||||
loadFixture('back2dom');
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
|
||||
|
||||
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
|
||||
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
|
||||
browser.refresh();
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
});
|
||||
|
||||
it('should prevent browser autofill on location.reload', function() {
|
||||
|
||||
loadFixture('back2dom');
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
|
||||
|
||||
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
|
||||
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
|
||||
browser.driver.executeScript('location.reload()');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
});
|
||||
|
||||
it('should prevent browser autofill on history.back', function() {
|
||||
|
||||
loadFixture('back2dom');
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
|
||||
|
||||
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
|
||||
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
|
||||
loadFixture('sample');
|
||||
|
||||
browser.driver.executeScript('history.back()');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
});
|
||||
|
||||
it('should prevent browser autofill on history.forward', function() {
|
||||
|
||||
loadFixture('sample');
|
||||
loadFixture('back2dom');
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
|
||||
|
||||
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
|
||||
|
||||
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
|
||||
browser.driver.executeScript('history.back()');
|
||||
browser.driver.executeScript('history.forward()');
|
||||
expect(element(by.css('body')).getAttribute('class')).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,7 @@ afterEach(function() {
|
||||
} else {
|
||||
dump('LEAK', key, angular.toJson(value));
|
||||
}
|
||||
delete expando.data[key];
|
||||
});
|
||||
});
|
||||
if (count) {
|
||||
@@ -312,7 +313,28 @@ window.dump = function() {
|
||||
|
||||
function generateInputCompilerHelper(helper) {
|
||||
beforeEach(function() {
|
||||
helper.validationCounter = {};
|
||||
|
||||
module(function($compileProvider) {
|
||||
$compileProvider.directive('validationSpy', function() {
|
||||
return {
|
||||
priority: 1,
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attrs, ctrl) {
|
||||
var validationName = attrs.validationSpy;
|
||||
|
||||
var originalValidator = ctrl.$validators[validationName];
|
||||
helper.validationCounter[validationName] = 0;
|
||||
|
||||
ctrl.$validators[validationName] = function(modelValue, viewValue) {
|
||||
helper.validationCounter[validationName]++;
|
||||
|
||||
return originalValidator(modelValue, viewValue);
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
$compileProvider.directive('attrCapture', function() {
|
||||
return function(scope, element, $attrs) {
|
||||
helper.attrs = $attrs;
|
||||
|
||||
+93
-25
@@ -11,8 +11,9 @@ function MockWindow(options) {
|
||||
}
|
||||
var events = {};
|
||||
var timeouts = this.timeouts = [];
|
||||
var locationHref = 'http://server/';
|
||||
var committedHref = 'http://server/';
|
||||
var locationHref = window.document.createElement('a');
|
||||
var committedHref = window.document.createElement('a');
|
||||
locationHref.href = committedHref.href = 'http://server/';
|
||||
var mockWindow = this;
|
||||
var msie = options.msie;
|
||||
var ieState;
|
||||
@@ -60,28 +61,28 @@ function MockWindow(options) {
|
||||
|
||||
this.location = {
|
||||
get href() {
|
||||
return committedHref;
|
||||
return committedHref.href;
|
||||
},
|
||||
set href(value) {
|
||||
locationHref = value;
|
||||
locationHref.href = value;
|
||||
mockWindow.history.state = null;
|
||||
historyEntriesLength++;
|
||||
if (!options.updateAsync) this.flushHref();
|
||||
},
|
||||
get hash() {
|
||||
return getHash(committedHref);
|
||||
return getHash(committedHref.href);
|
||||
},
|
||||
set hash(value) {
|
||||
locationHref = replaceHash(locationHref, value);
|
||||
locationHref.href = replaceHash(locationHref.href, value);
|
||||
if (!options.updateAsync) this.flushHref();
|
||||
},
|
||||
replace: function(url) {
|
||||
locationHref = url;
|
||||
locationHref.href = url;
|
||||
mockWindow.history.state = null;
|
||||
if (!options.updateAsync) this.flushHref();
|
||||
},
|
||||
flushHref: function() {
|
||||
committedHref = locationHref;
|
||||
committedHref.href = locationHref.href;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -91,13 +92,13 @@ function MockWindow(options) {
|
||||
historyEntriesLength++;
|
||||
},
|
||||
replaceState: function(state, title, url) {
|
||||
locationHref = url;
|
||||
if (!options.updateAsync) committedHref = locationHref;
|
||||
locationHref.href = url;
|
||||
if (!options.updateAsync) committedHref.href = locationHref.href;
|
||||
mockWindow.history.state = copy(state);
|
||||
if (!options.updateAsync) this.flushHref();
|
||||
},
|
||||
flushHref: function() {
|
||||
committedHref = locationHref;
|
||||
committedHref.href = locationHref.href;
|
||||
}
|
||||
};
|
||||
// IE 10-11 deserialize history.state on each read making subsequent reads
|
||||
@@ -398,18 +399,18 @@ describe('browser', function() {
|
||||
|
||||
it('should return current location.href', function() {
|
||||
fakeWindow.location.href = 'http://test.com';
|
||||
expect(browser.url()).toEqual('http://test.com');
|
||||
expect(browser.url()).toEqual('http://test.com/');
|
||||
|
||||
fakeWindow.location.href = 'https://another.com';
|
||||
expect(browser.url()).toEqual('https://another.com');
|
||||
expect(browser.url()).toEqual('https://another.com/');
|
||||
});
|
||||
|
||||
it('should strip an empty hash fragment', function() {
|
||||
fakeWindow.location.href = 'http://test.com#';
|
||||
expect(browser.url()).toEqual('http://test.com');
|
||||
fakeWindow.location.href = 'http://test.com/#';
|
||||
expect(browser.url()).toEqual('http://test.com/');
|
||||
|
||||
fakeWindow.location.href = 'https://another.com#foo';
|
||||
expect(browser.url()).toEqual('https://another.com#foo');
|
||||
fakeWindow.location.href = 'https://another.com/#foo';
|
||||
expect(browser.url()).toEqual('https://another.com/#foo');
|
||||
});
|
||||
|
||||
it('should use history.pushState when available', function() {
|
||||
@@ -417,7 +418,7 @@ describe('browser', function() {
|
||||
browser.url('http://new.org');
|
||||
|
||||
expect(pushState).toHaveBeenCalledOnce();
|
||||
expect(pushState.calls.argsFor(0)[2]).toEqual('http://new.org');
|
||||
expect(pushState.calls.argsFor(0)[2]).toEqual('http://new.org/');
|
||||
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
@@ -429,7 +430,7 @@ describe('browser', function() {
|
||||
browser.url('http://new.org', true);
|
||||
|
||||
expect(replaceState).toHaveBeenCalledOnce();
|
||||
expect(replaceState.calls.argsFor(0)[2]).toEqual('http://new.org');
|
||||
expect(replaceState.calls.argsFor(0)[2]).toEqual('http://new.org/');
|
||||
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
@@ -440,7 +441,7 @@ describe('browser', function() {
|
||||
sniffer.history = false;
|
||||
browser.url('http://new.org');
|
||||
|
||||
expect(fakeWindow.location.href).toEqual('http://new.org');
|
||||
expect(fakeWindow.location.href).toEqual('http://new.org/');
|
||||
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
@@ -473,7 +474,7 @@ describe('browser', function() {
|
||||
sniffer.history = false;
|
||||
browser.url('http://new.org', true);
|
||||
|
||||
expect(locationReplace).toHaveBeenCalledWith('http://new.org');
|
||||
expect(locationReplace).toHaveBeenCalledWith('http://new.org/');
|
||||
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
@@ -507,9 +508,9 @@ describe('browser', function() {
|
||||
it('should not set URL when the URL is already set', function() {
|
||||
var current = fakeWindow.location.href;
|
||||
sniffer.history = false;
|
||||
fakeWindow.location.href = 'dontchange';
|
||||
fakeWindow.location.href = 'http://dontchange/';
|
||||
browser.url(current);
|
||||
expect(fakeWindow.location.href).toBe('dontchange');
|
||||
expect(fakeWindow.location.href).toBe('http://dontchange/');
|
||||
});
|
||||
|
||||
it('should not read out location.href if a reload was triggered but still allow to change the url', function() {
|
||||
@@ -688,6 +689,73 @@ describe('browser', function() {
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not do pushState with a URL using relative protocol', function() {
|
||||
browser.url('http://server/');
|
||||
|
||||
pushState.calls.reset();
|
||||
replaceState.calls.reset();
|
||||
locationReplace.calls.reset();
|
||||
|
||||
browser.url('//server');
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not do pushState with a URL only adding a trailing slash after domain', function() {
|
||||
// A domain without a trailing /
|
||||
browser.url('http://server');
|
||||
|
||||
pushState.calls.reset();
|
||||
replaceState.calls.reset();
|
||||
locationReplace.calls.reset();
|
||||
|
||||
// A domain from something such as window.location.href with a trailing slash
|
||||
browser.url('http://server/');
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not do pushState with a URL only removing a trailing slash after domain', function() {
|
||||
// A domain from something such as window.location.href with a trailing slash
|
||||
browser.url('http://server/');
|
||||
|
||||
pushState.calls.reset();
|
||||
replaceState.calls.reset();
|
||||
locationReplace.calls.reset();
|
||||
|
||||
// A domain without a trailing /
|
||||
browser.url('http://server');
|
||||
expect(pushState).not.toHaveBeenCalled();
|
||||
expect(replaceState).not.toHaveBeenCalled();
|
||||
expect(locationReplace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should do pushState with a URL only adding a trailing slash after the path', function() {
|
||||
browser.url('http://server/foo');
|
||||
|
||||
pushState.calls.reset();
|
||||
replaceState.calls.reset();
|
||||
locationReplace.calls.reset();
|
||||
|
||||
browser.url('http://server/foo/');
|
||||
expect(pushState).toHaveBeenCalledOnce();
|
||||
expect(fakeWindow.location.href).toEqual('http://server/foo/');
|
||||
});
|
||||
|
||||
it('should do pushState with a URL only removing a trailing slash after the path', function() {
|
||||
browser.url('http://server/foo/');
|
||||
|
||||
pushState.calls.reset();
|
||||
replaceState.calls.reset();
|
||||
locationReplace.calls.reset();
|
||||
|
||||
browser.url('http://server/foo');
|
||||
expect(pushState).toHaveBeenCalledOnce();
|
||||
expect(fakeWindow.location.href).toEqual('http://server/foo');
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -812,7 +880,7 @@ describe('browser', function() {
|
||||
it('should not fire urlChange if changed by browser.url method', function() {
|
||||
sniffer.history = false;
|
||||
browser.onUrlChange(callback);
|
||||
browser.url('http://new.com');
|
||||
browser.url('http://new.com/');
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
@@ -1092,7 +1160,7 @@ describe('browser', function() {
|
||||
it('should not interfere with legacy browser url replace behavior', function() {
|
||||
inject(function($rootScope) {
|
||||
var current = fakeWindow.location.href;
|
||||
var newUrl = 'notyet';
|
||||
var newUrl = 'http://notyet/';
|
||||
sniffer.history = false;
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
browser.url(newUrl, true);
|
||||
|
||||
+93
-7
@@ -3577,6 +3577,15 @@ describe('$compile', function() {
|
||||
})
|
||||
);
|
||||
|
||||
it('should support non-interpolated `src` and `data-src` on the same element',
|
||||
inject(function($rootScope, $compile) {
|
||||
var element = $compile('<img src="abc" data-src="123">')($rootScope);
|
||||
expect(element.attr('src')).toEqual('abc');
|
||||
expect(element.attr('data-src')).toEqual('123');
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toEqual('abc');
|
||||
expect(element.attr('data-src')).toEqual('123');
|
||||
}));
|
||||
|
||||
it('should call observer only when the attribute value changes', function() {
|
||||
module(function() {
|
||||
@@ -4213,6 +4222,40 @@ describe('$compile', function() {
|
||||
expect(element.attr('ng-my-attr')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should set the value to lowercased keys for boolean attrs', function() {
|
||||
attr.$set('disabled', 'value');
|
||||
expect(element.attr('disabled')).toEqual('disabled');
|
||||
|
||||
element.removeAttr('disabled');
|
||||
|
||||
attr.$set('dISaBlEd', 'VaLuE');
|
||||
expect(element.attr('disabled')).toEqual('disabled');
|
||||
});
|
||||
|
||||
it('should call removeAttr for boolean attrs when value is `false`', function() {
|
||||
attr.$set('disabled', 'value');
|
||||
|
||||
spyOn(jqLite.prototype, 'attr').and.callThrough();
|
||||
spyOn(jqLite.prototype, 'removeAttr').and.callThrough();
|
||||
|
||||
attr.$set('disabled', false);
|
||||
|
||||
expect(element.attr).not.toHaveBeenCalled();
|
||||
expect(element.removeAttr).toHaveBeenCalledWith('disabled');
|
||||
expect(element.attr('disabled')).toEqual(undefined);
|
||||
|
||||
attr.$set('disabled', 'value');
|
||||
|
||||
element.attr.calls.reset();
|
||||
element.removeAttr.calls.reset();
|
||||
|
||||
attr.$set('dISaBlEd', false);
|
||||
|
||||
expect(element.attr).not.toHaveBeenCalled();
|
||||
expect(element.removeAttr).toHaveBeenCalledWith('disabled');
|
||||
expect(element.attr('disabled')).toEqual(undefined);
|
||||
});
|
||||
|
||||
|
||||
it('should not set DOM element attr if writeAttr false', function() {
|
||||
attr.$set('test', 'value', false);
|
||||
@@ -11703,37 +11746,37 @@ describe('$compile', function() {
|
||||
// All interpolations are disallowed.
|
||||
$rootScope.onClickJs = '';
|
||||
expect(function() {
|
||||
$compile('<button onclick="{{onClickJs}}"></script>');
|
||||
$compile('<button onclick="{{onClickJs}}"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ONCLICK="{{onClickJs}}"></script>');
|
||||
$compile('<button ONCLICK="{{onClickJs}}"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
|
||||
$compile('<button ng-attr-onclick="{{onClickJs}}"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
|
||||
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
|
||||
}));
|
||||
|
||||
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
|
||||
element = $compile('<button on-click="{{onClickJs}}"></script>')($rootScope);
|
||||
element = $compile('<button on-click="{{onClickJs}}"></button>')($rootScope);
|
||||
$rootScope.onClickJs = 'javascript:doSomething()';
|
||||
$rootScope.$apply();
|
||||
expect(element.attr('on-click')).toEqual('javascript:doSomething()');
|
||||
}));
|
||||
|
||||
it('should pass through arbitrary values on "on" and "data-on" attributes', inject(function($compile, $rootScope) {
|
||||
element = $compile('<button data-on="{{dataOnVar}}"></script>')($rootScope);
|
||||
element = $compile('<button data-on="{{dataOnVar}}"></button>')($rootScope);
|
||||
$rootScope.dataOnVar = 'data-on text';
|
||||
$rootScope.$apply();
|
||||
expect(element.attr('data-on')).toEqual('data-on text');
|
||||
|
||||
element = $compile('<button on="{{onVar}}"></script>')($rootScope);
|
||||
element = $compile('<button on="{{onVar}}"></button>')($rootScope);
|
||||
$rootScope.onVar = 'on text';
|
||||
$rootScope.$apply();
|
||||
expect(element.attr('on')).toEqual('on text');
|
||||
@@ -12084,6 +12127,49 @@ describe('$compile', function() {
|
||||
expect(element.attr('test6')).toBe('Misko');
|
||||
}));
|
||||
|
||||
describe('with media url attributes', function() {
|
||||
it('should work with interpolated ng-attr-src', inject(function() {
|
||||
$rootScope.name = 'some-image.png';
|
||||
element = $compile('<img ng-attr-src="{{name}}">')($rootScope);
|
||||
expect(element.attr('src')).toBeUndefined();
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('some-image.png');
|
||||
|
||||
$rootScope.name = 'other-image.png';
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('other-image.png');
|
||||
}));
|
||||
|
||||
it('should work with interpolated ng-attr-data-src', inject(function() {
|
||||
$rootScope.name = 'some-image.png';
|
||||
element = $compile('<img ng-attr-data-src="{{name}}">')($rootScope);
|
||||
expect(element.attr('data-src')).toBeUndefined();
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('data-src')).toBe('some-image.png');
|
||||
|
||||
$rootScope.name = 'other-image.png';
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('data-src')).toBe('other-image.png');
|
||||
}));
|
||||
|
||||
it('should work alongside constant [src]-attribute and [ng-attr-data-src] attributes', inject(function() {
|
||||
$rootScope.name = 'some-image.png';
|
||||
element = $compile('<img src="constant.png" ng-attr-data-src="{{name}}">')($rootScope);
|
||||
expect(element.attr('data-src')).toBeUndefined();
|
||||
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('constant.png');
|
||||
expect(element.attr('data-src')).toBe('some-image.png');
|
||||
|
||||
$rootScope.name = 'other-image.png';
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('constant.png');
|
||||
expect(element.attr('data-src')).toBe('other-image.png');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when an attribute has a dash-separated name', function() {
|
||||
it('should work with different prefixes', inject(function() {
|
||||
$rootScope.name = 'JamieMason';
|
||||
|
||||
@@ -839,6 +839,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -898,6 +914,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="month" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1114,6 +1146,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -1176,6 +1224,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="week" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1506,6 +1570,23 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -1565,6 +1646,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="datetime-local" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1972,6 +2069,22 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -2019,6 +2132,22 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="time" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2361,6 +2490,26 @@ describe('input', function() {
|
||||
|
||||
expect($rootScope.form.alias.$error.min).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = '2000-01-01';
|
||||
$rootScope.value = new Date(2010, 1, 1, 0, 0, 0);
|
||||
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('max', function() {
|
||||
@@ -2428,6 +2577,25 @@ describe('input', function() {
|
||||
expect($rootScope.form.alias.$error.max).toBeFalsy();
|
||||
expect($rootScope.form.alias.$valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxVal = '2000-01-01';
|
||||
$rootScope.value = new Date(2020, 1, 1, 0, 0, 0);
|
||||
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
|
||||
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="date" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3063,6 +3231,18 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.minVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ngMin', function() {
|
||||
@@ -3131,6 +3311,17 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.minVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="min" ng-min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3200,6 +3391,18 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.maxVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="max" name="alias" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('ngMax', function() {
|
||||
@@ -3268,6 +3471,17 @@ describe('input', function() {
|
||||
$rootScope.$digest();
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 5;
|
||||
$rootScope.maxVal = 3;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3364,7 +3578,7 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.value).toBe(10); // an initially invalid value should not be changed
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
@@ -3444,6 +3658,17 @@ describe('input', function() {
|
||||
expect($rootScope.value).toBe(1.16);
|
||||
}
|
||||
);
|
||||
|
||||
it('should validate only once after compilation inside ngRepeat', function() {
|
||||
$rootScope.step = 10;
|
||||
$rootScope.value = 20;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="number" ng-model="value" name="alias" ' + attrHtml + ' validation-spy="step" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.step).toBe(1);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3485,6 +3710,16 @@ describe('input', function() {
|
||||
|
||||
expect(inputElm).toBeValid();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 'text';
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input ng-model="value" validation-spy="required" required />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ngRequired', function() {
|
||||
@@ -3534,6 +3769,17 @@ describe('input', function() {
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.form.numberInput.$error.required).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.value = 'text';
|
||||
$rootScope.isRequired = true;
|
||||
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input ng-model="value" validation-spy="required" ng-required="isRequired" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the ngRequired expression initially evaluates to false', function() {
|
||||
@@ -3848,6 +4094,17 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('20');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
// input[type=range] will become type=text in browsers that don't support it
|
||||
|
||||
@@ -3926,6 +4183,16 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('15');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.min).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4006,6 +4273,17 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('0');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat and the value is valid', function() {
|
||||
$rootScope.maxVal = 5;
|
||||
$rootScope.value = 5;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
it('should validate if "range" is not implemented', function() {
|
||||
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
|
||||
@@ -4081,6 +4359,17 @@ describe('input', function() {
|
||||
expect(scope.value).toBe(5);
|
||||
expect(inputElm.val()).toBe('5');
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.max).toBe(1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4183,6 +4472,18 @@ describe('input', function() {
|
||||
expect(scope.value).toBe(10);
|
||||
expect(scope.form.alias.$error.step).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.stepVal = 5;
|
||||
$rootScope.value = 10;
|
||||
helper.compileInput('<div ng-repeat="input in [0]">' +
|
||||
'<input type="range" ng-model="value" validation-spy="step" step="{{ stepVal }}" />' +
|
||||
'</div>');
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.step).toBe(1);
|
||||
});
|
||||
|
||||
} else {
|
||||
|
||||
it('should validate if "range" is not implemented', function() {
|
||||
@@ -4269,7 +4570,7 @@ describe('input', function() {
|
||||
expect(inputElm.val()).toBe('10');
|
||||
expect(inputElm).toBeInvalid();
|
||||
expect(ngModel.$error.step).toBe(true);
|
||||
expect($rootScope.value).toBeUndefined();
|
||||
expect($rootScope.value).toBe(10);
|
||||
|
||||
helper.changeInputValueTo('15');
|
||||
expect(inputElm).toBeValid();
|
||||
|
||||
@@ -88,6 +88,12 @@ describe('ngClass', function() {
|
||||
expect(element.hasClass('AnotB')).toBeFalsy();
|
||||
}));
|
||||
|
||||
it('should not break when passed non-string/array/object, truthy values', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div ng-class="42"></div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.hasClass('42')).toBeTruthy();
|
||||
}));
|
||||
|
||||
it('should support adding multiple classes via an array mixed with conditionally via a map', inject(function($rootScope, $compile) {
|
||||
element = $compile('<div class="existing" ng-class="[\'A\', {\'B\': condition}]"></div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('ngHref', function() {
|
||||
}));
|
||||
|
||||
|
||||
// Support: IE 9-11 only, Edge 12-15+
|
||||
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
|
||||
// Support: IE 9-11 only, Edge 12-17
|
||||
if (msie || /\bEdge\/1[2-7]\.[\d.]+\b/.test(window.navigator.userAgent)) {
|
||||
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
|
||||
// See https://github.com/angular/angular.js/issues/13388
|
||||
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
|
||||
@@ -79,6 +79,42 @@ describe('ngHref', function() {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
it('should bind numbers', inject(function($rootScope, $compile) {
|
||||
element = $compile('<a ng-href="{{1234}}"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('1234');
|
||||
}));
|
||||
|
||||
|
||||
it('should bind and sanitize the result of a (custom) toString() function', inject(function($rootScope, $compile) {
|
||||
$rootScope.value = {};
|
||||
element = $compile('<a ng-href="{{value}}"></a>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('[object Object]');
|
||||
|
||||
function SafeClass() {}
|
||||
|
||||
SafeClass.prototype.toString = function() {
|
||||
return 'custom value';
|
||||
};
|
||||
|
||||
$rootScope.value = new SafeClass();
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('custom value');
|
||||
|
||||
function UnsafeClass() {}
|
||||
|
||||
UnsafeClass.prototype.toString = function() {
|
||||
return 'javascript:alert(1);';
|
||||
};
|
||||
|
||||
$rootScope.value = new UnsafeClass();
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('href')).toEqual('unsafe:javascript:alert(1);');
|
||||
}));
|
||||
|
||||
|
||||
if (isDefined(window.SVGElement)) {
|
||||
describe('SVGAElement', function() {
|
||||
it('should interpolate the expression and bind to xlink:href', inject(function($compile, $rootScope) {
|
||||
|
||||
@@ -863,7 +863,6 @@ describe('ngModel', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('view -> model update', function() {
|
||||
|
||||
it('should always perform validations using the parsed model value', function() {
|
||||
|
||||
@@ -400,6 +400,43 @@ describe('ngRepeat', function() {
|
||||
expect(element.find('input')[1].checked).toBe(true);
|
||||
expect(element.find('input')[2].checked).toBe(true);
|
||||
}));
|
||||
|
||||
it('should invoke track by with correct locals', function() {
|
||||
scope.trackBy = jasmine.createSpy().and.callFake(function(k, v) {
|
||||
return [k, v].join('');
|
||||
});
|
||||
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="(k, v) in [1, 2] track by trackBy(k, v)"></li>' +
|
||||
'</ul>')(scope);
|
||||
scope.$digest();
|
||||
|
||||
expect(scope.trackBy).toHaveBeenCalledTimes(2);
|
||||
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1]);
|
||||
expect(scope.trackBy.calls.argsFor(1)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
// https://github.com/angular/angular.js/issues/16776
|
||||
it('should invoke nested track by with correct locals', function() {
|
||||
scope.trackBy = jasmine.createSpy().and.callFake(function(k1, v1, k2, v2) {
|
||||
return [k1, v1, k2, v2].join('');
|
||||
});
|
||||
|
||||
element = $compile(
|
||||
'<ul>' +
|
||||
'<li ng-repeat="(k1, v1) in [1, 2]">' +
|
||||
'<div ng-repeat="(k2, v2) in [3, 4] track by trackBy(k1, v1, k2, v2)"></div>' +
|
||||
'</li>' +
|
||||
'</ul>')(scope);
|
||||
scope.$digest();
|
||||
|
||||
expect(scope.trackBy).toHaveBeenCalledTimes(4);
|
||||
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1, 0, 3]);
|
||||
expect(scope.trackBy.calls.argsFor(1)).toEqual([0, 1, 1, 4]);
|
||||
expect(scope.trackBy.calls.argsFor(2)).toEqual([1, 2, 0, 3]);
|
||||
expect(scope.trackBy.calls.argsFor(3)).toEqual([1, 2, 1, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alias as', function() {
|
||||
|
||||
@@ -78,6 +78,20 @@ describe('ngSrc', function() {
|
||||
expect(element.prop('src')).toEqual('http://somewhere/abc');
|
||||
}));
|
||||
}
|
||||
|
||||
it('should work with `src` attribute on the same element', inject(function($rootScope, $compile) {
|
||||
$rootScope.imageUrl = 'dynamic';
|
||||
element = $compile('<img ng-src="{{imageUrl}}" src="static">')($rootScope);
|
||||
expect(element.attr('src')).toBe('static');
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('dynamic');
|
||||
dealoc(element);
|
||||
|
||||
element = $compile('<img src="static" ng-src="{{imageUrl}}">')($rootScope);
|
||||
expect(element.attr('src')).toBe('static');
|
||||
$rootScope.$digest();
|
||||
expect(element.attr('src')).toBe('dynamic');
|
||||
}));
|
||||
});
|
||||
|
||||
describe('iframe[ng-src]', function() {
|
||||
|
||||
@@ -120,5 +120,38 @@ describe('ngStyle', function() {
|
||||
expect(element.css(preCompStyle)).not.toBe('88px');
|
||||
expect(element.css(postCompStyle)).not.toBe('99px');
|
||||
});
|
||||
|
||||
it('should clear style when the new model is null', function() {
|
||||
scope.styleObj = {'height': '99px', 'width': '88px'};
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).toBe('88px');
|
||||
expect(element.css(postCompStyle)).toBe('99px');
|
||||
scope.styleObj = null;
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).not.toBe('88px');
|
||||
expect(element.css(postCompStyle)).not.toBe('99px');
|
||||
});
|
||||
|
||||
it('should clear style when the value is undefined or null', function() {
|
||||
scope.styleObj = {'height': '99px', 'width': '88px'};
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).toBe('88px');
|
||||
expect(element.css(postCompStyle)).toBe('99px');
|
||||
scope.styleObj = {'height': undefined, 'width': null};
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).not.toBe('88px');
|
||||
expect(element.css(postCompStyle)).not.toBe('99px');
|
||||
});
|
||||
|
||||
it('should set style when the value is zero', function() {
|
||||
scope.styleObj = {'height': '99px', 'width': '88px'};
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).toBe('88px');
|
||||
expect(element.css(postCompStyle)).toBe('99px');
|
||||
scope.styleObj = {'height': 0, 'width': 0};
|
||||
scope.$apply();
|
||||
expect(element.css(preCompStyle)).toBe('0px');
|
||||
expect(element.css(postCompStyle)).toBe('0px');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1530,11 +1530,14 @@ describe('select', function() {
|
||||
['a'],
|
||||
NaN
|
||||
], function(prop) {
|
||||
|
||||
scope.option1 = prop;
|
||||
scope.option2 = 'red';
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option1">{{option1}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
scope.$digest();
|
||||
@@ -1571,10 +1574,12 @@ describe('select', function() {
|
||||
NaN
|
||||
], function(prop) {
|
||||
scope.option = prop;
|
||||
scope.option2 = 'red';
|
||||
scope.selected = 'NOMATCH';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option">{{option}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
var selectController = element.controller('select');
|
||||
@@ -1604,7 +1609,7 @@ describe('select', function() {
|
||||
|
||||
expect(scope.selected).toBe(null);
|
||||
expect(element[0].selectedIndex).toBe(0);
|
||||
expect(element.find('option').length).toBe(2);
|
||||
expect(element.find('option').length).toBe(3);
|
||||
expect(element.find('option').eq(0).prop('selected')).toBe(true);
|
||||
expect(element.find('option').eq(0).val()).toBe(unknownValue(prop));
|
||||
expect(element.find('option').eq(1).prop('selected')).toBe(false);
|
||||
@@ -1617,6 +1622,7 @@ describe('select', function() {
|
||||
expect(element.find('option').eq(0).val()).toBe('string:UPDATEDVALUE');
|
||||
});
|
||||
|
||||
|
||||
it('should interact with custom attribute $observe and $set calls', function() {
|
||||
var log = [], optionAttr;
|
||||
|
||||
@@ -1638,26 +1644,43 @@ describe('select', function() {
|
||||
optionAttr.$set('value', 'update');
|
||||
expect(log[1]).toBe('update');
|
||||
expect(element.find('option').eq(1).val()).toBe('string:update');
|
||||
|
||||
});
|
||||
|
||||
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.value = 'def';
|
||||
scope.textvalue = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
it('should ignore the option text / value attribute if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.value = 'def';
|
||||
scope.textvalue = 'ghi';
|
||||
|
||||
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.textvalue = 'def';
|
||||
scope.textvalue2 = 'ghi';
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue" value="{{value}}">{{textvalue}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
|
||||
it('should ignore option text with multiple interpolations if the ngValue attribute exists', function() {
|
||||
scope.ngvalue = 'abc';
|
||||
scope.textvalue = 'def';
|
||||
scope.textvalue2 = 'ghi';
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
|
||||
it('should select the first option if it is `undefined`', function() {
|
||||
scope.selected = undefined;
|
||||
|
||||
scope.option1 = undefined;
|
||||
scope.option2 = 'red';
|
||||
|
||||
compile('<select ng-model="selected">' +
|
||||
'<option ng-value="option1">{{option1}}</option>' +
|
||||
'<option ng-value="option2">{{option2}}</option>' +
|
||||
'</select>');
|
||||
|
||||
expect(element).toEqualSelect(['undefined:undefined'], 'string:red');
|
||||
});
|
||||
|
||||
compile('<select ng-model="x"><option ng-value="ngvalue">{{textvalue}} {{textvalue2}}</option></select>');
|
||||
expect(element).toEqualSelect([unknownValue(undefined)], 'string:abc');
|
||||
});
|
||||
|
||||
describe('and select[multiple]', function() {
|
||||
|
||||
|
||||
@@ -230,6 +230,29 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.pattern).toBe(true);
|
||||
expect(ctrlNg.$error.pattern).toBe(true);
|
||||
}));
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
|
||||
$rootScope.pattern = /\d{4}/;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" pattern="\\d{4}" validation-spy="pattern" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.pattern).toBe(1);
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-pattern="pattern" validation-spy="pattern" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.pattern).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -312,9 +335,31 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.minlength).toBe(true);
|
||||
expect(ctrlNg.$error.minlength).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.minlength = 5;
|
||||
|
||||
var element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" minlength="{{minlength}}" validation-spy="minlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.minlength).toBe(1);
|
||||
|
||||
element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-minlength="minlength" validation-spy="minlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.minlength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('maxlength', function() {
|
||||
|
||||
it('should invalidate values that are longer than the given maxlength', function() {
|
||||
@@ -500,6 +545,29 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.maxlength).toBe(true);
|
||||
expect(ctrlNg.$error.maxlength).toBe(true);
|
||||
}));
|
||||
|
||||
|
||||
it('should only validate once after compilation when inside ngRepeat', function() {
|
||||
$rootScope.maxlength = 5;
|
||||
|
||||
var element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" maxlength="{{maxlength}}" validation-spy="maxlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.maxlength).toBe(1);
|
||||
|
||||
element = helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-maxlength="maxlength" validation-spy="maxlength" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.maxlength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -626,5 +694,41 @@ describe('validators', function() {
|
||||
expect(ctrl.$error.required).toBe(true);
|
||||
expect(ctrlNg.$error.required).toBe(true);
|
||||
}));
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat', function() {
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" required validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
$rootScope.$digest();
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat and ngRequired is true', function() {
|
||||
$rootScope.isRequired = true;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
it('should validate only once after compilation when inside ngRepeat and ngRequired is false', function() {
|
||||
$rootScope.isRequired = false;
|
||||
|
||||
helper.compileInput(
|
||||
'<div ng-repeat="input in [0]">' +
|
||||
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
|
||||
'</div>');
|
||||
|
||||
expect(helper.validationCounter.required).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,4 +155,34 @@ describe('ngOn* event binding', function() {
|
||||
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
|
||||
});
|
||||
});
|
||||
|
||||
it('should correctly bind to kebab-cased event names', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo-bar="cb()"></span>')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
|
||||
element.triggerHandler('foobar');
|
||||
element.triggerHandler('fooBar');
|
||||
element.triggerHandler('foo_bar');
|
||||
element.triggerHandler('foo:bar');
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
element.triggerHandler('foo-bar');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should correctly bind to camelCased event names', inject(function($compile, $rootScope) {
|
||||
var element = $compile('<span ng-on-foo_bar="cb()"></span>')($rootScope);
|
||||
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
|
||||
$rootScope.$digest();
|
||||
|
||||
element.triggerHandler('foobar');
|
||||
element.triggerHandler('foo-bar');
|
||||
element.triggerHandler('foo_bar');
|
||||
element.triggerHandler('foo:bar');
|
||||
expect(cb).not.toHaveBeenCalled();
|
||||
|
||||
element.triggerHandler('fooBar');
|
||||
expect(cb).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
+44
-2
@@ -49,6 +49,48 @@ describe('ngProp*', function() {
|
||||
expect(element.prop('asdf')).toBe(true);
|
||||
}));
|
||||
|
||||
// https://github.com/angular/angular.js/issues/16797
|
||||
it('should support falsy property values', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-text="myText" />')($rootScope);
|
||||
// Initialize to truthy value
|
||||
$rootScope.myText = 'abc';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBe('abc');
|
||||
|
||||
// Assert various falsey values get assigned to the property
|
||||
$rootScope.myText = '';
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBe('');
|
||||
$rootScope.myText = 0;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBe(0);
|
||||
$rootScope.myText = false;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBe(false);
|
||||
$rootScope.myText = undefined;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBeUndefined();
|
||||
$rootScope.myText = null;
|
||||
$rootScope.$digest();
|
||||
expect(element.prop('text')).toBe(null);
|
||||
}));
|
||||
|
||||
it('should directly map special properties (class)', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-class="myText" />')($rootScope);
|
||||
$rootScope.myText = 'abc';
|
||||
$rootScope.$digest();
|
||||
expect(element[0].class).toBe('abc');
|
||||
expect(element).not.toHaveClass('abc');
|
||||
}));
|
||||
|
||||
it('should not use jQuery .prop() to avoid jQuery propFix/hooks', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-class="myText" />')($rootScope);
|
||||
spyOn(jqLite.prototype, 'prop');
|
||||
$rootScope.myText = 'abc';
|
||||
$rootScope.$digest();
|
||||
expect(jqLite.prototype.prop).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
|
||||
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
|
||||
$rootScope.value = 123;
|
||||
@@ -147,11 +189,11 @@ describe('ngProp*', function() {
|
||||
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
|
||||
// All event prop bindings are disallowed.
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-onclick="onClickJs"></script>');
|
||||
$compile('<button ng-prop-onclick="onClickJs"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
expect(function() {
|
||||
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
|
||||
$compile('<button ng-prop-ONCLICK="onClickJs"></button>');
|
||||
}).toThrowMinErr(
|
||||
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
|
||||
}));
|
||||
|
||||
@@ -31,6 +31,19 @@ describe('urlUtils', function() {
|
||||
var parsed = urlResolve('/');
|
||||
expect(parsed.pathname).toBe('/');
|
||||
});
|
||||
|
||||
it('should return an IPv6 hostname wrapped in brackets', function() {
|
||||
// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
|
||||
// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
|
||||
// when parsed out of an anchor element.
|
||||
var parsed = urlResolve('http://[::1]/');
|
||||
expect(parsed.hostname).toBe('[::1]');
|
||||
});
|
||||
|
||||
it('should not put the domain in brackets for the hostname field', function() {
|
||||
var parsed = urlResolve('https://google.com/');
|
||||
expect(parsed.hostname).toBe('google.com');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
},
|
||||
"globals": {
|
||||
"getDomNode": false,
|
||||
"helpers": false,
|
||||
"mergeAnimationDetails": false,
|
||||
"prepareAnimationOptions": false,
|
||||
"applyAnimationStyles": false,
|
||||
|
||||
@@ -1822,7 +1822,7 @@ describe('ngAnimate $animateCss', function() {
|
||||
they('should not place a CSS transition block if options.skipBlocking is provided',
|
||||
['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
|
||||
|
||||
inject(function($animateCss, $rootElement, $document, $window) {
|
||||
inject(function($animateCss, $rootElement, $document) {
|
||||
var element = angular.element('<div></div>');
|
||||
$rootElement.append(element);
|
||||
angular.element($document[0].body).append($rootElement);
|
||||
@@ -1840,7 +1840,7 @@ describe('ngAnimate $animateCss', function() {
|
||||
data.event = event;
|
||||
}
|
||||
|
||||
var blockSpy = spyOn($window, 'blockTransitions').and.callThrough();
|
||||
var blockSpy = spyOn(helpers, 'blockTransitions').and.callThrough();
|
||||
|
||||
data.skipBlocking = true;
|
||||
var animator = $animateCss(element, data);
|
||||
|
||||
@@ -25,6 +25,7 @@ describe('ngAnimate integration tests', function() {
|
||||
ss.destroy();
|
||||
});
|
||||
|
||||
|
||||
it('should cancel a running and started removeClass animation when a follow-up addClass animation adds the same class',
|
||||
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
|
||||
|
||||
@@ -362,6 +363,7 @@ describe('ngAnimate integration tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
|
||||
@@ -397,6 +399,7 @@ describe('ngAnimate integration tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
|
||||
@@ -468,6 +471,84 @@ describe('ngAnimate integration tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should remove the prepare classes when different structural animations happen in the same digest', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
|
||||
element = jqLite(
|
||||
// Class animation on parent element is neeeded so the child elements get the prepare class
|
||||
'<div id="outer" ng-class="{blue: cond}" ng-switch="cond">' +
|
||||
'<div id="default" ng-switch-default></div>' +
|
||||
'<div id="truthy" ng-switch-when="true"></div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.cond = false;
|
||||
$rootScope.$digest();
|
||||
|
||||
$rootScope.cond = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = element;
|
||||
var truthySwitch = jqLite(parent[0].querySelector('#truthy'));
|
||||
var defaultSwitch = jqLite(parent[0].querySelector('#default'));
|
||||
|
||||
expect(parent).not.toHaveClass('blue');
|
||||
expect(parent).toHaveClass('blue-add');
|
||||
expect(truthySwitch).toHaveClass('ng-enter-prepare');
|
||||
expect(defaultSwitch).toHaveClass('ng-leave-prepare');
|
||||
|
||||
$animate.flush();
|
||||
|
||||
expect(parent).toHaveClass('blue');
|
||||
expect(parent).not.toHaveClass('blue-add');
|
||||
expect(truthySwitch).not.toHaveClass('ng-enter-prepare');
|
||||
expect(defaultSwitch).not.toHaveClass('ng-leave-prepare');
|
||||
});
|
||||
});
|
||||
|
||||
it('should respect the element node for caching when animations with the same type happen in the same digest', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
|
||||
ss.addRule('.animate.ng-enter', 'transition:2s linear all;');
|
||||
|
||||
element = jqLite(
|
||||
'<div>' +
|
||||
'<div>' +
|
||||
'<div id="noanimate" ng-if="cond"></div>' +
|
||||
'</div>' +
|
||||
'<div>' +
|
||||
'<div id="animate" class="animate" ng-if="cond"></div>' +
|
||||
'</div>' +
|
||||
'</div>'
|
||||
);
|
||||
|
||||
$rootElement.append(element);
|
||||
jqLite($document[0].body).append($rootElement);
|
||||
|
||||
$compile(element)($rootScope);
|
||||
$rootScope.cond = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var parent = element;
|
||||
var noanimate = jqLite(parent[0].querySelector('#noanimate'));
|
||||
var animate = jqLite(parent[0].querySelector('#animate'));
|
||||
|
||||
expect(noanimate).not.toHaveClass('ng-enter');
|
||||
expect(animate).toHaveClass('ng-enter');
|
||||
|
||||
$animate.closeAndFlush();
|
||||
|
||||
expect(noanimate).not.toHaveClass('ng-enter');
|
||||
expect(animate).not.toHaveClass('ng-enter');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should pack level elements into their own RAF flush', function() {
|
||||
module('ngAnimateMock');
|
||||
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
|
||||
|
||||
@@ -20,7 +20,7 @@ describe('ngAnimateSwap', function() {
|
||||
}));
|
||||
|
||||
|
||||
it('should render a new container when the expression changes', inject(function() {
|
||||
it('should render a new container when the expression changes', function() {
|
||||
element = $compile('<div><div ng-animate-swap="exp">{{ exp }}</div></div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -40,9 +40,9 @@ describe('ngAnimateSwap', function() {
|
||||
expect(third.textContent).toBe('super');
|
||||
expect(third).not.toEqual(second);
|
||||
expect(second.parentNode).toBeFalsy();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should render a new container only when the expression property changes', inject(function() {
|
||||
it('should render a new container only when the expression property changes', function() {
|
||||
element = $compile('<div><div ng-animate-swap="exp.prop">{{ exp.value }}</div></div>')($rootScope);
|
||||
$rootScope.exp = {
|
||||
prop: 'hello',
|
||||
@@ -66,9 +66,9 @@ describe('ngAnimateSwap', function() {
|
||||
var three = element.find('div')[0];
|
||||
expect(three.textContent).toBe('planet');
|
||||
expect(three).not.toBe(two);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should watch the expression as a collection', inject(function() {
|
||||
it('should watch the expression as a collection', function() {
|
||||
element = $compile('<div><div ng-animate-swap="exp">{{ exp.a }} {{ exp.b }} {{ exp.c }}</div></div>')($rootScope);
|
||||
$rootScope.exp = {
|
||||
a: 1,
|
||||
@@ -99,26 +99,24 @@ describe('ngAnimateSwap', function() {
|
||||
var four = element.find('div')[0];
|
||||
expect(four.textContent.trim()).toBe('4');
|
||||
expect(four).not.toEqual(three);
|
||||
}));
|
||||
|
||||
they('should consider $prop as a falsy value', [false, undefined, null], function(value) {
|
||||
inject(function() {
|
||||
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
|
||||
$rootScope.value = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var one = element.find('div')[0];
|
||||
expect(one).toBeTruthy();
|
||||
|
||||
$rootScope.value = value;
|
||||
$rootScope.$digest();
|
||||
|
||||
var two = element.find('div')[0];
|
||||
expect(two).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should consider "0" as a truthy value', inject(function() {
|
||||
they('should consider $prop as a falsy value', [false, undefined, null], function(value) {
|
||||
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
|
||||
$rootScope.value = true;
|
||||
$rootScope.$digest();
|
||||
|
||||
var one = element.find('div')[0];
|
||||
expect(one).toBeTruthy();
|
||||
|
||||
$rootScope.value = value;
|
||||
$rootScope.$digest();
|
||||
|
||||
var two = element.find('div')[0];
|
||||
expect(two).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should consider "0" as a truthy value', function() {
|
||||
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
@@ -130,9 +128,9 @@ describe('ngAnimateSwap', function() {
|
||||
|
||||
var two = element.find('div')[0];
|
||||
expect(two).toBeTruthy();
|
||||
}));
|
||||
});
|
||||
|
||||
it('should create a new (non-isolate) scope for each inserted clone', inject(function() {
|
||||
it('should create a new (non-isolate) scope for each inserted clone', function() {
|
||||
var parentScope = $rootScope.$new();
|
||||
parentScope.foo = 'bar';
|
||||
|
||||
@@ -147,9 +145,9 @@ describe('ngAnimateSwap', function() {
|
||||
expect(scopeTwo.foo).toBe('bar');
|
||||
|
||||
expect(scopeOne).not.toBe(scopeTwo);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should destroy the previous scope when removing the element', inject(function() {
|
||||
it('should destroy the previous scope when removing the element', function() {
|
||||
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
|
||||
|
||||
$rootScope.$apply('value = 1');
|
||||
@@ -166,9 +164,9 @@ describe('ngAnimateSwap', function() {
|
||||
// Removing the old element (without inserting a new one).
|
||||
$rootScope.$apply('value = null');
|
||||
expect(scopeTwo.$$destroyed).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should destroy the previous scope when swapping elements', inject(function() {
|
||||
it('should destroy the previous scope when swapping elements', function() {
|
||||
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
|
||||
|
||||
$rootScope.$apply('value = 1');
|
||||
@@ -177,13 +175,34 @@ describe('ngAnimateSwap', function() {
|
||||
|
||||
$rootScope.$apply('value = 2');
|
||||
expect(scopeOne.$$destroyed).toBe(true);
|
||||
}));
|
||||
});
|
||||
|
||||
it('should work with `ngIf` on the same element', function() {
|
||||
var tmpl = '<div><div ng-animate-swap="exp" ng-if="true">{{ exp }}</div></div>';
|
||||
element = $compile(tmpl)($rootScope);
|
||||
$rootScope.$digest();
|
||||
|
||||
var first = element.find('div')[0];
|
||||
expect(first).toBeFalsy();
|
||||
|
||||
$rootScope.exp = 'yes';
|
||||
$rootScope.$digest();
|
||||
|
||||
var second = element.find('div')[0];
|
||||
expect(second.textContent).toBe('yes');
|
||||
|
||||
$rootScope.exp = 'super';
|
||||
$rootScope.$digest();
|
||||
|
||||
var third = element.find('div')[0];
|
||||
expect(third.textContent).toBe('super');
|
||||
expect(third).not.toEqual(second);
|
||||
expect(second.parentNode).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
describe('animations', function() {
|
||||
it('should trigger a leave animation followed by an enter animation upon swap',
|
||||
inject(function() {
|
||||
|
||||
it('should trigger a leave animation followed by an enter animation upon swap',function() {
|
||||
element = $compile('<div><div ng-animate-swap="exp">{{ exp }}</div></div>')($rootScope);
|
||||
$rootScope.exp = 1;
|
||||
$rootScope.$digest();
|
||||
@@ -208,6 +227,6 @@ describe('ngAnimateSwap', function() {
|
||||
var forth = $animate.queue.shift();
|
||||
expect(forth.event).toBe('leave');
|
||||
expect($animate.queue.length).toBe(0);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
/* globals nodeBlackList false */
|
||||
|
||||
describe('$aria', function() {
|
||||
var scope, $compile, element;
|
||||
|
||||
@@ -972,6 +974,122 @@ describe('$aria', function() {
|
||||
}
|
||||
);
|
||||
|
||||
it('should not prevent default keyboard action if the target element has editable content',
|
||||
inject(function($document) {
|
||||
// Note:
|
||||
// `contenteditable` is an enumarated (not a boolean) attribute (see
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
|
||||
// We need to check the following conditions:
|
||||
// - No attribute.
|
||||
// - Value: ""
|
||||
// - Value: "true"
|
||||
// - Value: "false"
|
||||
|
||||
function eventFor(keyCode) {
|
||||
return {bubbles: true, cancelable: true, keyCode: keyCode};
|
||||
}
|
||||
|
||||
compileElement(
|
||||
'<section>' +
|
||||
// No attribute.
|
||||
'<div id="no-attribute">' +
|
||||
'<div ng-click="onClick($event)"></div>' +
|
||||
'<ul ng-click="onClick($event)"><li></li></ul>' +
|
||||
'</div>' +
|
||||
|
||||
// Value: ""
|
||||
'<div id="value-empty">' +
|
||||
'<div ng-click="onClick($event)" contenteditable=""></div>' +
|
||||
'<ul ng-click="onClick($event)"><li contenteditable=""></li></ul>' +
|
||||
'</div>' +
|
||||
|
||||
// Value: "true"
|
||||
'<div id="value-true">' +
|
||||
'<div ng-click="onClick($event)" contenteditable="true"></div>' +
|
||||
'<ul ng-click="onClick($event)"><li contenteditable="true"></li></ul>' +
|
||||
'</div>' +
|
||||
|
||||
// Value: "false"
|
||||
'<div id="value-false">' +
|
||||
'<div ng-click="onClick($event)" contenteditable="false"></div>' +
|
||||
'<ul ng-click="onClick($event)"><li contenteditable="false"></li></ul>' +
|
||||
'</div>' +
|
||||
'</section>');
|
||||
|
||||
// Support: Safari 11-12+
|
||||
// Attach to DOM, because otherwise Safari will not update the `isContentEditable` property
|
||||
// based on the `contenteditable` attribute.
|
||||
$document.find('body').append(element);
|
||||
|
||||
var containers = element.children();
|
||||
var container;
|
||||
|
||||
// Using `browserTrigger()`, because it supports event bubbling.
|
||||
|
||||
// No attribute | Elements are not editable.
|
||||
container = containers.eq(0);
|
||||
browserTrigger(container.find('div'), 'keydown', eventFor(13));
|
||||
browserTrigger(container.find('ul'), 'keydown', eventFor(32));
|
||||
browserTrigger(container.find('li'), 'keydown', eventFor(13));
|
||||
|
||||
expect(clickEvents).toEqual(['div(true)', 'ul(true)', 'li(true)']);
|
||||
|
||||
// Value: "" | Elements are editable.
|
||||
clickEvents = [];
|
||||
container = containers.eq(1);
|
||||
browserTrigger(container.find('div'), 'keydown', eventFor(32));
|
||||
browserTrigger(container.find('ul'), 'keydown', eventFor(13));
|
||||
browserTrigger(container.find('li'), 'keydown', eventFor(32));
|
||||
|
||||
expect(clickEvents).toEqual(['div(false)', 'ul(true)', 'li(false)']);
|
||||
|
||||
// Value: "true" | Elements are editable.
|
||||
clickEvents = [];
|
||||
container = containers.eq(2);
|
||||
browserTrigger(container.find('div'), 'keydown', eventFor(13));
|
||||
browserTrigger(container.find('ul'), 'keydown', eventFor(32));
|
||||
browserTrigger(container.find('li'), 'keydown', eventFor(13));
|
||||
|
||||
expect(clickEvents).toEqual(['div(false)', 'ul(true)', 'li(false)']);
|
||||
|
||||
// Value: "false" | Elements are not editable.
|
||||
clickEvents = [];
|
||||
container = containers.eq(3);
|
||||
browserTrigger(container.find('div'), 'keydown', eventFor(32));
|
||||
browserTrigger(container.find('ul'), 'keydown', eventFor(13));
|
||||
browserTrigger(container.find('li'), 'keydown', eventFor(32));
|
||||
|
||||
expect(clickEvents).toEqual(['div(true)', 'ul(true)', 'li(true)']);
|
||||
})
|
||||
);
|
||||
|
||||
they('should not prevent default keyboard action if an interactive $type element' +
|
||||
'is nested inside ng-click', nodeBlackList, function(elementType) {
|
||||
function createHTML(type) {
|
||||
return '<' + type + '></' + type + '>';
|
||||
}
|
||||
|
||||
compileElement(
|
||||
'<section>' +
|
||||
'<div ng-click="onClick($event)">' + createHTML(elementType) + '</div>' +
|
||||
'</section>');
|
||||
|
||||
var divElement = element.find('div');
|
||||
var interactiveElement = element.find(elementType);
|
||||
|
||||
// Use browserTrigger because it supports event bubbling
|
||||
// 13 Enter
|
||||
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 13});
|
||||
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
|
||||
|
||||
clickEvents = [];
|
||||
|
||||
// 32 Space
|
||||
browserTrigger(interactiveElement, 'keydown', {cancelable: true, bubbles: true, keyCode: 32});
|
||||
expect(clickEvents).toEqual([elementType.toLowerCase() + '(false)']);
|
||||
}
|
||||
);
|
||||
|
||||
they('should not bind to key events if there is existing `ng-$prop`',
|
||||
['keydown', 'keypress', 'keyup'], function(eventName) {
|
||||
scope.onKeyEvent = jasmine.createSpy('onKeyEvent');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user