summaryrefslogtreecommitdiffstats
path: root/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js
diff options
context:
space:
mode:
Diffstat (limited to 'ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js')
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js11827
1 files changed, 7886 insertions, 3941 deletions
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js
index 9e6a4f4f..db30da8f 100644
--- a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/notebook-integration/scripts/dependency/angular.js
@@ -1,9 +1,9 @@
/**
- * @license AngularJS v1.5.0-beta.2
- * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.6.3
+ * (c) 2010-2017 Google, Inc. http://angularjs.org
* License: MIT
*/
-(function(window, document, undefined) {'use strict';
+(function(window) {'use strict';
/**
* @description
@@ -38,131 +38,134 @@
function minErr(module, ErrorConstructor) {
ErrorConstructor = ErrorConstructor || Error;
return function() {
- var SKIP_INDEXES = 2;
-
- var templateArgs = arguments,
- code = templateArgs[0],
+ var code = arguments[0],
+ template = arguments[1],
message = '[' + (module ? module + ':' : '') + code + '] ',
- template = templateArgs[1],
+ templateArgs = sliceArgs(arguments, 2).map(function(arg) {
+ return toDebugString(arg, minErrConfig.objectMaxDepth);
+ }),
paramPrefix, i;
message += template.replace(/\{\d+\}/g, function(match) {
- var index = +match.slice(1, -1),
- shiftedIndex = index + SKIP_INDEXES;
+ var index = +match.slice(1, -1);
- if (shiftedIndex < templateArgs.length) {
- return toDebugString(templateArgs[shiftedIndex]);
+ if (index < templateArgs.length) {
+ return templateArgs[index];
}
return match;
});
- message += '\nhttp://errors.angularjs.org/1.5.0-beta.2/' +
+ message += '\nhttp://errors.angularjs.org/1.6.3/' +
(module ? module + '/' : '') + code;
- for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
- message += paramPrefix + 'p' + (i - SKIP_INDEXES) + '=' +
- encodeURIComponent(toDebugString(templateArgs[i]));
+ for (i = 0, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
+ message += paramPrefix + 'p' + i + '=' + encodeURIComponent(templateArgs[i]);
}
return new ErrorConstructor(message);
};
}
-/* We need to tell jshint what variables are being exported */
-/* global angular: true,
- msie: true,
- jqLite: true,
- jQuery: true,
- slice: true,
- splice: true,
- push: true,
- toString: true,
- ngMinErr: true,
- angularModule: true,
- uid: true,
- REGEX_STRING_REGEXP: true,
- VALIDITY_STATE_PROPERTY: true,
-
- lowercase: true,
- uppercase: true,
- manualLowercase: true,
- manualUppercase: true,
- nodeName_: true,
- isArrayLike: true,
- forEach: true,
- forEachSorted: true,
- reverseParams: true,
- nextUid: true,
- setHashKey: true,
- extend: true,
- toInt: true,
- inherit: true,
- merge: true,
- noop: true,
- identity: true,
- valueFn: true,
- isUndefined: true,
- isDefined: true,
- isObject: true,
- isBlankObject: true,
- isString: true,
- isNumber: true,
- isDate: true,
- isArray: true,
- isFunction: true,
- isRegExp: true,
- isWindow: true,
- isScope: true,
- isFile: true,
- isFormData: true,
- isBlob: true,
- isBoolean: true,
- isPromiseLike: true,
- trim: true,
- escapeForRegexp: true,
- isElement: true,
- makeMap: true,
- includes: true,
- arrayRemove: true,
- copy: true,
- shallowCopy: true,
- equals: true,
- csp: true,
- jq: true,
- concat: true,
- sliceArgs: true,
- bind: true,
- toJsonReplacer: true,
- toJson: true,
- fromJson: true,
- convertTimezoneToLocal: true,
- timezoneToOffset: true,
- startingTag: true,
- tryDecodeURIComponent: true,
- parseKeyValue: true,
- toKeyValue: true,
- encodeUriSegment: true,
- encodeUriQuery: true,
- angularInit: true,
- bootstrap: true,
- getTestability: true,
- snake_case: true,
- bindJQuery: true,
- assertArg: true,
- assertArgFn: true,
- assertNotHasOwnProperty: true,
- getter: true,
- getBlockNodes: true,
- hasOwnProperty: true,
- createMap: true,
-
- NODE_TYPE_ELEMENT: true,
- NODE_TYPE_ATTRIBUTE: true,
- NODE_TYPE_TEXT: true,
- NODE_TYPE_COMMENT: true,
- NODE_TYPE_DOCUMENT: true,
- NODE_TYPE_DOCUMENT_FRAGMENT: true,
+/* We need to tell ESLint what variables are being exported */
+/* exported
+ angular,
+ msie,
+ jqLite,
+ jQuery,
+ slice,
+ splice,
+ push,
+ toString,
+ minErrConfig,
+ errorHandlingConfig,
+ isValidObjectMaxDepth,
+ ngMinErr,
+ angularModule,
+ uid,
+ REGEX_STRING_REGEXP,
+ VALIDITY_STATE_PROPERTY,
+
+ lowercase,
+ uppercase,
+ manualLowercase,
+ manualUppercase,
+ nodeName_,
+ isArrayLike,
+ forEach,
+ forEachSorted,
+ reverseParams,
+ nextUid,
+ setHashKey,
+ extend,
+ toInt,
+ inherit,
+ merge,
+ noop,
+ identity,
+ valueFn,
+ isUndefined,
+ isDefined,
+ isObject,
+ isBlankObject,
+ isString,
+ isNumber,
+ isNumberNaN,
+ isDate,
+ isArray,
+ isFunction,
+ isRegExp,
+ isWindow,
+ isScope,
+ isFile,
+ isFormData,
+ isBlob,
+ isBoolean,
+ isPromiseLike,
+ trim,
+ escapeForRegexp,
+ isElement,
+ makeMap,
+ includes,
+ arrayRemove,
+ copy,
+ equals,
+ csp,
+ jq,
+ concat,
+ sliceArgs,
+ bind,
+ toJsonReplacer,
+ toJson,
+ fromJson,
+ convertTimezoneToLocal,
+ timezoneToOffset,
+ startingTag,
+ tryDecodeURIComponent,
+ parseKeyValue,
+ toKeyValue,
+ encodeUriSegment,
+ encodeUriQuery,
+ angularInit,
+ bootstrap,
+ getTestability,
+ snake_case,
+ bindJQuery,
+ assertArg,
+ assertArgFn,
+ assertNotHasOwnProperty,
+ getter,
+ getBlockNodes,
+ hasOwnProperty,
+ createMap,
+ stringify,
+
+ NODE_TYPE_ELEMENT,
+ NODE_TYPE_ATTRIBUTE,
+ NODE_TYPE_TEXT,
+ NODE_TYPE_COMMENT,
+ NODE_TYPE_DOCUMENT,
+ NODE_TYPE_DOCUMENT_FRAGMENT
*/
////////////////////////////////////
@@ -171,6 +174,7 @@ function minErr(module, ErrorConstructor) {
* @ngdoc module
* @name ng
* @module ng
+ * @installation
* @description
*
* # ng (core module)
@@ -188,18 +192,69 @@ var REGEX_STRING_REGEXP = /^\/(.+)\/([a-z]*)$/;
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
+
+var hasOwnProperty = Object.prototype.hasOwnProperty;
+
+var minErrConfig = {
+ objectMaxDepth: 5
+};
+
+ /**
+ * @ngdoc function
+ * @name angular.errorHandlingConfig
+ * @module ng
+ * @kind function
+ *
+ * @description
+ * Configure several aspects of error handling in AngularJS if used as a setter or return the
+ * current configuration if used as a getter. The following options are supported:
+ *
+ * - **objectMaxDepth**: The maximum depth to which objects are traversed when stringified for error messages.
+ *
+ * Omitted or undefined options will leave the corresponding configuration values unchanged.
+ *
+ * @param {Object=} config - The configuration object. May only contain the options that need to be
+ * updated. Supported keys:
+ *
+ * * `objectMaxDepth` **{Number}** - The max depth for stringifying objects. Setting to a
+ * non-positive or non-numeric value, removes the max depth limit.
+ * Default: 5
+ */
+function errorHandlingConfig(config) {
+ if (isObject(config)) {
+ if (isDefined(config.objectMaxDepth)) {
+ minErrConfig.objectMaxDepth = isValidObjectMaxDepth(config.objectMaxDepth) ? config.objectMaxDepth : NaN;
+ }
+ } else {
+ return minErrConfig;
+ }
+}
+
+/**
+ * @private
+ * @param {Number} maxDepth
+ * @return {boolean}
+ */
+function isValidObjectMaxDepth(maxDepth) {
+ return isNumber(maxDepth) && maxDepth > 0;
+}
+
/**
* @ngdoc function
* @name angular.lowercase
* @module ng
* @kind function
*
+ * @deprecated
+ * sinceVersion="1.5.0"
+ * removeVersion="1.7.0"
+ * Use [String.prototype.toLowerCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase) instead.
+ *
* @description Converts the specified string to lowercase.
* @param {string} string String to be converted to lowercase.
* @returns {string} Lowercased string.
*/
var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
-var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* @ngdoc function
@@ -207,6 +262,11 @@ var hasOwnProperty = Object.prototype.hasOwnProperty;
* @module ng
* @kind function
*
+ * @deprecated
+ * sinceVersion="1.5.0"
+ * removeVersion="1.7.0"
+ * Use [String.prototype.toUpperCase](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase) instead.
+ *
* @description Converts the specified string to uppercase.
* @param {string} string String to be converted to uppercase.
* @returns {string} Uppercased string.
@@ -215,22 +275,24 @@ var uppercase = function(string) {return isString(string) ? string.toUpperCase()
var manualLowercase = function(s) {
- /* jshint bitwise: false */
+ /* eslint-disable no-bitwise */
return isString(s)
? s.replace(/[A-Z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) | 32);})
: s;
+ /* eslint-enable */
};
var manualUppercase = function(s) {
- /* jshint bitwise: false */
+ /* eslint-disable no-bitwise */
return isString(s)
? s.replace(/[a-z]/g, function(ch) {return String.fromCharCode(ch.charCodeAt(0) & ~32);})
: s;
+ /* eslint-enable */
};
// String#toLowerCase and String#toUpperCase don't produce correct results in browsers with Turkish
// locale, for this reason we need to detect this case and redefine lowercase/uppercase methods
-// with correct but slower alternatives.
+// with correct but slower alternatives. See https://github.com/angular/angular.js/issues/11387
if ('i' !== 'I'.toLowerCase()) {
lowercase = manualLowercase;
uppercase = manualUppercase;
@@ -253,11 +315,12 @@ var
angularModule,
uid = 0;
+// Support: IE 9-11 only
/**
* documentMode is an IE-only property
* http://msdn.microsoft.com/en-us/library/ie/cc196988(v=vs.85).aspx
*/
-msie = document.documentMode;
+msie = window.document.documentMode;
/**
@@ -273,18 +336,19 @@ function isArrayLike(obj) {
// arrays, strings and jQuery/jqLite objects are array like
// * jqLite is either the jQuery or jqLite constructor function
- // * we have to check the existance of jqLite first as this method is called
+ // * we have to check the existence of jqLite first as this method is called
// via the forEach method when constructing the jqLite object in the first place
if (isArray(obj) || isString(obj) || (jqLite && obj instanceof jqLite)) return true;
// Support: iOS 8.2 (not reproducible in simulator)
// "length" in obj used to prevent JIT error (gh-11508)
- var length = "length" in Object(obj) && obj.length;
+ var length = 'length' in Object(obj) && obj.length;
// NodeList objects (with `item` method) and
// other objects with suitable length characteristics are array-like
return isNumber(length) &&
- (length >= 0 && (length - 1) in obj || typeof obj.item == 'function');
+ (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function');
+
}
/**
@@ -304,7 +368,7 @@ function isArrayLike(obj) {
*
* Unlike ES262's
* [Array.prototype.forEach](http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.18),
- * Providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just
+ * providing 'undefined' or 'null' values for `obj` will not throw a TypeError, but rather just
* return the value provided.
*
```js
@@ -327,9 +391,7 @@ function forEach(obj, iterator, context) {
if (obj) {
if (isFunction(obj)) {
for (key in obj) {
- // Need to check if hasOwnProperty exists,
- // as on IE8 the result of querySelectorAll is an object without a hasOwnProperty function
- if (key != 'prototype' && key != 'length' && key != 'name' && (!obj.hasOwnProperty || obj.hasOwnProperty(key))) {
+ if (key !== 'prototype' && key !== 'length' && key !== 'name' && obj.hasOwnProperty(key)) {
iterator.call(context, obj[key], key, obj);
}
}
@@ -381,7 +443,7 @@ function forEachSorted(obj, iterator, context) {
* @returns {function(*, string)}
*/
function reverseParams(iteratorFn) {
- return function(value, key) { iteratorFn(key, value); };
+ return function(value, key) {iteratorFn(key, value);};
}
/**
@@ -498,6 +560,11 @@ function toInt(str) {
return parseInt(str, 10);
}
+var isNumberNaN = Number.isNaN || function isNumberNaN(num) {
+ // eslint-disable-next-line no-self-compare
+ return num !== num;
+};
+
function inherit(parent, extra) {
return extend(Object.create(parent), extra);
@@ -534,18 +601,28 @@ noop.$inject = [];
* functional style.
*
```js
- function transformer(transformationFn, value) {
- return (transformationFn || angular.identity)(value);
- };
+ function transformer(transformationFn, value) {
+ return (transformationFn || angular.identity)(value);
+ };
+
+ // E.g.
+ function getResult(fn, input) {
+ return (fn || angular.identity)(input);
+ };
+
+ getResult(function(n) { return n * 2; }, 21); // returns 42
+ getResult(null, 21); // returns 21
+ getResult(undefined, 21); // returns 21
```
- * @param {*} value to be returned.
- * @returns {*} the value passed in.
+ *
+ * @param {*} value to be returned.
+ * @returns {*} the value passed in.
*/
function identity($) {return $;}
identity.$inject = [];
-function valueFn(value) {return function() {return value;};}
+function valueFn(value) {return function valueRef() {return value;};}
function hasCustomToString(obj) {
return isFunction(obj.toString) && obj.toString !== toString;
@@ -671,7 +748,7 @@ function isDate(value) {
* @kind function
*
* @description
- * Determines if a reference is an `Array`.
+ * Determines if a reference is an `Array`. Alias of Array.isArray.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
@@ -747,11 +824,15 @@ function isPromiseLike(obj) {
}
-var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array\]$/;
+var TYPED_ARRAY_REGEXP = /^\[object (?:Uint8|Uint8Clamped|Uint16|Uint32|Int8|Int16|Int32|Float32|Float64)Array]$/;
function isTypedArray(value) {
return value && isNumber(value.length) && TYPED_ARRAY_REGEXP.test(toString.call(value));
}
+function isArrayBuffer(obj) {
+ return toString.call(obj) === '[object ArrayBuffer]';
+}
+
var trim = function(value) {
return isString(value) ? value.trim() : value;
@@ -761,8 +842,10 @@ var trim = function(value) {
// http://docs.closure-library.googlecode.com/git/local_closure_goog_string_string.js.source.html#line1021
// Prereq: s is a string.
var escapeForRegexp = function(s) {
- return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
- replace(/\x08/g, '\\x08');
+ return s
+ .replace(/([-()[\]{}+?*.$^|,:#<!\\])/g, '\\$1')
+ // eslint-disable-next-line no-control-regex
+ .replace(/\x08/g, '\\x08');
};
@@ -780,8 +863,8 @@ var escapeForRegexp = function(s) {
*/
function isElement(node) {
return !!(node &&
- (node.nodeName // we are a direct element
- || (node.prop && node.attr && node.find))); // we have an on and find method part of jQuery API
+ (node.nodeName // We are a direct element.
+ || (node.prop && node.attr && node.find))); // We have an on and find method part of jQuery API.
}
/**
@@ -789,7 +872,7 @@ function isElement(node) {
* @returns {object} in the form of {key1:true, key2:true, ...}
*/
function makeMap(str) {
- var obj = {}, items = str.split(","), i;
+ var obj = {}, items = str.split(','), i;
for (i = 0; i < items.length; i++) {
obj[items[i]] = true;
}
@@ -802,7 +885,7 @@ function nodeName_(element) {
}
function includes(array, obj) {
- return Array.prototype.indexOf.call(array, obj) != -1;
+ return Array.prototype.indexOf.call(array, obj) !== -1;
}
function arrayRemove(array, value) {
@@ -826,7 +909,13 @@ function arrayRemove(array, value) {
* * If a destination is provided, all of its elements (for arrays) or properties (for objects)
* are deleted and then all elements/properties from the source are copied to it.
* * If `source` is not an object or array (inc. `null` and `undefined`), `source` is returned.
- * * If `source` is identical to 'destination' an exception will be thrown.
+ * * 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`.
@@ -835,52 +924,54 @@ function arrayRemove(array, value) {
* @returns {*} The copy or updated `destination`, if `destination` was specified.
*
* @example
- <example module="copyExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form novalidate class="simple-form">
- Name: <input type="text" ng-model="user.name" /><br />
- E-mail: <input type="email" ng-model="user.email" /><br />
- Gender: <input type="radio" ng-model="user.gender" value="male" />male
- <input type="radio" ng-model="user.gender" value="female" />female<br />
- <button ng-click="reset()">RESET</button>
- <button ng-click="update(user)">SAVE</button>
- </form>
- <pre>form = {{user | json}}</pre>
- <pre>master = {{master | json}}</pre>
- </div>
-
- <script>
- angular.module('copyExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.master= {};
-
- $scope.update = function(user) {
- // Example with 1 argument
- $scope.master= angular.copy(user);
- };
+ <example module="copyExample" name="angular-copy">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <form novalidate class="simple-form">
+ <label>Name: <input type="text" ng-model="user.name" /></label><br />
+ <label>Age: <input type="number" ng-model="user.age" /></label><br />
+ Gender: <label><input type="radio" ng-model="user.gender" value="male" />male</label>
+ <label><input type="radio" ng-model="user.gender" value="female" />female</label><br />
+ <button ng-click="reset()">RESET</button>
+ <button ng-click="update(user)">SAVE</button>
+ </form>
+ <pre>form = {{user | json}}</pre>
+ <pre>master = {{master | json}}</pre>
+ </div>
+ </file>
+ <file name="script.js">
+ // Module: copyExample
+ angular.
+ module('copyExample', []).
+ controller('ExampleController', ['$scope', function($scope) {
+ $scope.master = {};
+
+ $scope.reset = function() {
+ // Example with 1 argument
+ $scope.user = angular.copy($scope.master);
+ };
- $scope.reset = function() {
- // Example with 2 arguments
- angular.copy($scope.master, $scope.user);
- };
+ $scope.update = function(user) {
+ // Example with 2 arguments
+ angular.copy(user, $scope.master);
+ };
- $scope.reset();
- }]);
- </script>
- </file>
- </example>
+ $scope.reset();
+ }]);
+ </file>
+ </example>
*/
-function copy(source, destination) {
+function copy(source, destination, maxDepth) {
var stackSource = [];
var stackDest = [];
+ maxDepth = isValidObjectMaxDepth(maxDepth) ? maxDepth : NaN;
if (destination) {
- if (isTypedArray(destination)) {
- throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
+ if (isTypedArray(destination) || isArrayBuffer(destination)) {
+ throw ngMinErr('cpta', 'Can\'t copy! TypedArray destination cannot be mutated.');
}
if (source === destination) {
- throw ngMinErr('cpi', "Can't copy! Source and destination are identical.");
+ throw ngMinErr('cpi', 'Can\'t copy! Source and destination are identical.');
}
// Empty the destination object
@@ -896,35 +987,39 @@ function copy(source, destination) {
stackSource.push(source);
stackDest.push(destination);
- return copyRecurse(source, destination);
+ return copyRecurse(source, destination, maxDepth);
}
- return copyElement(source);
+ return copyElement(source, maxDepth);
- function copyRecurse(source, destination) {
+ function copyRecurse(source, destination, maxDepth) {
+ maxDepth--;
+ if (maxDepth < 0) {
+ return '...';
+ }
var h = destination.$$hashKey;
- var result, key;
+ var key;
if (isArray(source)) {
for (var i = 0, ii = source.length; i < ii; i++) {
- destination.push(copyElement(source[i]));
+ destination.push(copyElement(source[i], maxDepth));
}
} else if (isBlankObject(source)) {
// createMap() fast path --- Safe to avoid hasOwnProperty check because prototype chain is empty
for (key in source) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
} else if (source && typeof source.hasOwnProperty === 'function') {
// Slow path, which must rely on hasOwnProperty
for (key in source) {
if (source.hasOwnProperty(key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
} else {
// Slowest path --- hasOwnProperty can't be called as a method
for (key in source) {
if (hasOwnProperty.call(source, key)) {
- destination[key] = copyElement(source[key]);
+ destination[key] = copyElement(source[key], maxDepth);
}
}
}
@@ -932,7 +1027,7 @@ function copy(source, destination) {
return destination;
}
- function copyElement(source) {
+ function copyElement(source, maxDepth) {
// Simple values
if (!isObject(source)) {
return source;
@@ -946,26 +1041,14 @@ function copy(source, destination) {
if (isWindow(source) || isScope(source)) {
throw ngMinErr('cpws',
- "Can't copy! Making copies of Window or Scope instances is not supported.");
+ 'Can\'t copy! Making copies of Window or Scope instances is not supported.');
}
var needsRecurse = false;
- var destination;
+ var destination = copyType(source);
- if (isArray(source)) {
- destination = [];
- needsRecurse = true;
- } else if (isTypedArray(source)) {
- destination = new source.constructor(source);
- } else if (isDate(source)) {
- destination = new Date(source.getTime());
- } else if (isRegExp(source)) {
- destination = new RegExp(source.source, source.toString().match(/[^\/]*$/)[0]);
- destination.lastIndex = source.lastIndex;
- } else if (isFunction(source.cloneNode)) {
- destination = source.cloneNode(true);
- } else {
- destination = Object.create(getPrototypeOf(source));
+ if (destination === undefined) {
+ destination = isArray(source) ? [] : Object.create(getPrototypeOf(source));
needsRecurse = true;
}
@@ -973,34 +1056,54 @@ function copy(source, destination) {
stackDest.push(destination);
return needsRecurse
- ? copyRecurse(source, destination)
+ ? copyRecurse(source, destination, maxDepth)
: destination;
}
-}
-/**
- * Creates a shallow copy of an object, an array or a primitive.
- *
- * Assumes that there are no proto properties for objects.
- */
-function shallowCopy(src, dst) {
- if (isArray(src)) {
- dst = dst || [];
+ function copyType(source) {
+ switch (toString.call(source)) {
+ case '[object Int8Array]':
+ case '[object Int16Array]':
+ case '[object Int32Array]':
+ case '[object Float32Array]':
+ case '[object Float64Array]':
+ case '[object Uint8Array]':
+ case '[object Uint8ClampedArray]':
+ case '[object Uint16Array]':
+ case '[object Uint32Array]':
+ return new source.constructor(copyElement(source.buffer), source.byteOffset, source.length);
+
+ case '[object ArrayBuffer]':
+ // Support: IE10
+ if (!source.slice) {
+ // If we're in this case we know the environment supports ArrayBuffer
+ /* eslint-disable no-undef */
+ var copied = new ArrayBuffer(source.byteLength);
+ new Uint8Array(copied).set(new Uint8Array(source));
+ /* eslint-enable */
+ return copied;
+ }
+ return source.slice(0);
- for (var i = 0, ii = src.length; i < ii; i++) {
- dst[i] = src[i];
+ case '[object Boolean]':
+ case '[object Number]':
+ case '[object String]':
+ case '[object Date]':
+ return new source.constructor(source.valueOf());
+
+ case '[object RegExp]':
+ var re = new RegExp(source.source, source.toString().match(/[^/]*$/)[0]);
+ re.lastIndex = source.lastIndex;
+ return re;
+
+ case '[object Blob]':
+ return new source.constructor([source], {type: source.type});
}
- } else if (isObject(src)) {
- dst = dst || {};
- for (var key in src) {
- if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
- dst[key] = src[key];
- }
+ if (isFunction(source.cloneNode)) {
+ return source.cloneNode(true);
}
}
-
- return dst || src;
}
@@ -1032,44 +1135,78 @@ function shallowCopy(src, dst) {
* @param {*} o1 Object or value to compare.
* @param {*} o2 Object or value to compare.
* @returns {boolean} True if arguments are equal.
+ *
+ * @example
+ <example module="equalsExample" name="equalsExample">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <form novalidate>
+ <h3>User 1</h3>
+ Name: <input type="text" ng-model="user1.name">
+ Age: <input type="number" ng-model="user1.age">
+
+ <h3>User 2</h3>
+ Name: <input type="text" ng-model="user2.name">
+ Age: <input type="number" ng-model="user2.age">
+
+ <div>
+ <br/>
+ <input type="button" value="Compare" ng-click="compare()">
+ </div>
+ User 1: <pre>{{user1 | json}}</pre>
+ User 2: <pre>{{user2 | json}}</pre>
+ Equal: <pre>{{result}}</pre>
+ </form>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('equalsExample', []).controller('ExampleController', ['$scope', function($scope) {
+ $scope.user1 = {};
+ $scope.user2 = {};
+ $scope.compare = function() {
+ $scope.result = angular.equals($scope.user1, $scope.user2);
+ };
+ }]);
+ </file>
+ </example>
*/
function equals(o1, o2) {
if (o1 === o2) return true;
if (o1 === null || o2 === null) return false;
+ // eslint-disable-next-line no-self-compare
if (o1 !== o1 && o2 !== o2) return true; // NaN === NaN
var t1 = typeof o1, t2 = typeof o2, length, key, keySet;
- if (t1 == t2) {
- if (t1 == 'object') {
- if (isArray(o1)) {
- if (!isArray(o2)) return false;
- if ((length = o1.length) == o2.length) {
- for (key = 0; key < length; key++) {
- if (!equals(o1[key], o2[key])) return false;
- }
- return true;
- }
- } else if (isDate(o1)) {
- if (!isDate(o2)) return false;
- return equals(o1.getTime(), o2.getTime());
- } else if (isRegExp(o1)) {
- return isRegExp(o2) ? o1.toString() == o2.toString() : false;
- } else {
- if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
- isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
- keySet = createMap();
- for (key in o1) {
- if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (t1 === t2 && t1 === 'object') {
+ if (isArray(o1)) {
+ if (!isArray(o2)) return false;
+ if ((length = o1.length) === o2.length) {
+ for (key = 0; key < length; key++) {
if (!equals(o1[key], o2[key])) return false;
- keySet[key] = true;
- }
- for (key in o2) {
- if (!(key in keySet) &&
- key.charAt(0) !== '$' &&
- isDefined(o2[key]) &&
- !isFunction(o2[key])) return false;
}
return true;
}
+ } else if (isDate(o1)) {
+ if (!isDate(o2)) return false;
+ return equals(o1.getTime(), o2.getTime());
+ } else if (isRegExp(o1)) {
+ if (!isRegExp(o2)) return false;
+ return o1.toString() === o2.toString();
+ } else {
+ if (isScope(o1) || isScope(o2) || isWindow(o1) || isWindow(o2) ||
+ isArray(o2) || isDate(o2) || isRegExp(o2)) return false;
+ keySet = createMap();
+ for (key in o1) {
+ if (key.charAt(0) === '$' || isFunction(o1[key])) continue;
+ if (!equals(o1[key], o2[key])) return false;
+ keySet[key] = true;
+ }
+ for (key in o2) {
+ if (!(key in keySet) &&
+ key.charAt(0) !== '$' &&
+ isDefined(o2[key]) &&
+ !isFunction(o2[key])) return false;
+ }
+ return true;
}
}
return false;
@@ -1079,8 +1216,8 @@ var csp = function() {
if (!isDefined(csp.rules)) {
- var ngCspElement = (document.querySelector('[ng-csp]') ||
- document.querySelector('[data-ng-csp]'));
+ var ngCspElement = (window.document.querySelector('[ng-csp]') ||
+ window.document.querySelector('[data-ng-csp]'));
if (ngCspElement) {
var ngCspAttribute = ngCspElement.getAttribute('ng-csp') ||
@@ -1101,9 +1238,8 @@ var csp = function() {
function noUnsafeEval() {
try {
- /* jshint -W031, -W054 */
+ // eslint-disable-next-line no-new, no-new-func
new Function('');
- /* jshint +W031, +W054 */
return false;
} catch (e) {
return true;
@@ -1155,7 +1291,8 @@ var jq = function() {
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
- if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]');
+ if (el) {
name = el.getAttribute(prefix + 'jq');
break;
}
@@ -1173,7 +1310,6 @@ function sliceArgs(args, startIndex) {
}
-/* jshint -W101 */
/**
* @ngdoc function
* @name angular.bind
@@ -1191,7 +1327,6 @@ function sliceArgs(args, startIndex) {
* @param {...*} args Optional arguments to be prebound to the `fn` function call.
* @returns {function()} Function that wraps the `fn` with all the specified bindings.
*/
-/* jshint +W101 */
function bind(self, fn) {
var curryArgs = arguments.length > 2 ? sliceArgs(arguments, 2) : [];
if (isFunction(fn) && !(fn instanceof RegExp)) {
@@ -1207,7 +1342,7 @@ function bind(self, fn) {
: fn.call(self);
};
} else {
- // in IE, native methods are not functions so they cannot be bound (note: they don't need to be)
+ // In IE, native methods are not functions so they cannot be bound (note: they don't need to be).
return fn;
}
}
@@ -1220,7 +1355,7 @@ function toJsonReplacer(key, value) {
val = undefined;
} else if (isWindow(value)) {
val = '$WINDOW';
- } else if (value && document === value) {
+ } else if (value && window.document === value) {
val = '$DOCUMENT';
} else if (isScope(value)) {
val = '$SCOPE';
@@ -1240,13 +1375,34 @@ function toJsonReplacer(key, value) {
* Serializes input into a JSON-formatted string. Properties with leading $$ characters will be
* stripped since angular uses this notation internally.
*
- * @param {Object|Array|Date|string|number} obj Input to be serialized into JSON.
+ * @param {Object|Array|Date|string|number|boolean} obj Input to be serialized into JSON.
* @param {boolean|number} [pretty=2] If set to true, the JSON output will contain newlines and whitespace.
* If set to an integer, the JSON output will contain that many spaces per indentation.
* @returns {string|undefined} JSON-ified string representing `obj`.
+ * @knownIssue
+ *
+ * The Safari browser throws a `RangeError` instead of returning `null` when it tries to stringify a `Date`
+ * object with an invalid date value. The only reliable way to prevent this is to monkeypatch the
+ * `Date.prototype.toJSON` method as follows:
+ *
+ * ```
+ * var _DatetoJSON = Date.prototype.toJSON;
+ * Date.prototype.toJSON = function() {
+ * try {
+ * return _DatetoJSON.call(this);
+ * } catch(e) {
+ * if (e instanceof RangeError) {
+ * return null;
+ * }
+ * throw e;
+ * }
+ * };
+ * ```
+ *
+ * See https://github.com/angular/angular.js/pull/14221 for more information.
*/
function toJson(obj, pretty) {
- if (typeof obj === 'undefined') return undefined;
+ if (isUndefined(obj)) return undefined;
if (!isNumber(pretty)) {
pretty = pretty ? 2 : null;
}
@@ -1273,9 +1429,13 @@ function fromJson(json) {
}
+var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
+ // Support: IE 9-11 only, Edge 13-14+
+ // IE/Edge do not "understand" colon (`:`) in timezone
+ timezone = timezone.replace(ALL_COLONS, '');
var requestedTimezoneOffset = Date.parse('Jan 01, 1970 00:00:00 ' + timezone) / 60000;
- return isNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
+ return isNumberNaN(requestedTimezoneOffset) ? fallback : requestedTimezoneOffset;
}
@@ -1288,8 +1448,9 @@ function addDateMinutes(date, minutes) {
function convertTimezoneToLocal(date, timezone, reverse) {
reverse = reverse ? -1 : 1;
- var timezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
- return addDateMinutes(date, reverse * (timezoneOffset - date.getTimezoneOffset()));
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ var timezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ return addDateMinutes(date, reverse * (timezoneOffset - dateTimezoneOffset));
}
@@ -1302,13 +1463,13 @@ function startingTag(element) {
// turns out IE does not let you set .html() on elements which
// are not allowed to have children. So we just ignore it.
element.empty();
- } catch (e) {}
+ } catch (e) { /* empty */ }
var elemHtml = jqLite('<div>').append(element).html();
try {
return element[0].nodeType === NODE_TYPE_TEXT ? lowercase(elemHtml) :
elemHtml.
match(/^(<[^>]+>)/)[1].
- replace(/^<([\w\-]+)/, function(match, nodeName) { return '<' + lowercase(nodeName); });
+ replace(/^<([\w-]+)/, function(match, nodeName) {return '<' + lowercase(nodeName);});
} catch (e) {
return lowercase(elemHtml);
}
@@ -1330,7 +1491,7 @@ function tryDecodeURIComponent(value) {
try {
return decodeURIComponent(value);
} catch (e) {
- // Ignore any invalid uri component
+ // Ignore any invalid uri component.
}
}
@@ -1341,7 +1502,7 @@ function tryDecodeURIComponent(value) {
*/
function parseKeyValue(/**string*/keyValue) {
var obj = {};
- forEach((keyValue || "").split('&'), function(keyValue) {
+ forEach((keyValue || '').split('&'), function(keyValue) {
var splitPoint, key, val;
if (keyValue) {
key = keyValue = keyValue.replace(/\+/g,'%20');
@@ -1406,7 +1567,7 @@ function encodeUriSegment(val) {
* This method is intended for encoding *key* or *value* parts of query component. We need a custom
* method because encodeURIComponent is too aggressive and encodes stuff that doesn't have to be
* encoded per http://tools.ietf.org/html/rfc3986:
- * query = *( pchar / "/" / "?" )
+ * query = *( pchar / "/" / "?" )
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* pct-encoded = "%" HEXDIG HEXDIG
@@ -1436,6 +1597,57 @@ function getNgAttribute(element, ngAttr) {
return null;
}
+function allowAutoBootstrap(document) {
+ var script = document.currentScript;
+
+ if (!script) {
+ // IE does not have `document.currentScript`
+ return true;
+ }
+
+ // If the `currentScript` property has been clobbered just return false, since this indicates a probable attack
+ if (!(script instanceof window.HTMLScriptElement || script instanceof window.SVGScriptElement)) {
+ return false;
+ }
+
+ var attributes = script.attributes;
+ var srcs = [attributes.getNamedItem('src'), attributes.getNamedItem('href'), attributes.getNamedItem('xlink:href')];
+
+ return srcs.every(function(src) {
+ if (!src) {
+ return true;
+ }
+ if (!src.value) {
+ return false;
+ }
+
+ var link = document.createElement('a');
+ link.href = src.value;
+
+ if (document.location.origin === link.origin) {
+ // Same-origin resources are always allowed, even for non-whitelisted schemes.
+ return true;
+ }
+ // Disabled bootstrapping unless angular.js was loaded from a known scheme used on the web.
+ // This is to prevent angular.js bundled with browser extensions from being used to bypass the
+ // content security policy in web pages and other browser extensions.
+ switch (link.protocol) {
+ case 'http:':
+ case 'https:':
+ case 'ftp:':
+ case 'blob:':
+ case 'file:':
+ case 'data:':
+ return true;
+ default:
+ return false;
+ }
+ });
+}
+
+// Cached as it has to run during loading so that document.currentScript is available.
+var isAutoBootstrapAllowed = allowAutoBootstrap(window.document);
+
/**
* @ngdoc directive
* @name ngApp
@@ -1456,10 +1668,17 @@ function getNgAttribute(element, ngAttr) {
* designates the **root element** of the application and is typically placed near the root element
* of the page - e.g. on the `<body>` or `<html>` tags.
*
- * Only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
- * found in the document will be used to define the root element to auto-bootstrap as an
- * application. To run multiple applications in an HTML document you must manually bootstrap them using
- * {@link angular.bootstrap} instead. AngularJS applications cannot be nested within each other.
+ * There are a few things to keep in mind when using `ngApp`:
+ * - only one AngularJS application can be auto-bootstrapped per HTML document. The first `ngApp`
+ * found in the document will be used to define the root element to auto-bootstrap as an
+ * application. To run multiple applications in an HTML document you must manually bootstrap them using
+ * {@link angular.bootstrap} instead.
+ * - AngularJS applications cannot be nested within each other.
+ * - Do not use a directive that uses {@link ng.$compile#transclusion transclusion} on the same element as `ngApp`.
+ * This includes directives such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and
+ * {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
*
* You can specify an **AngularJS module** to be used as the root module for the application. This
* module will be loaded into the {@link auto.$injector} when the application is bootstrapped. It
@@ -1472,7 +1691,7 @@ function getNgAttribute(element, ngAttr) {
*
* `ngApp` is the easiest, and most common way to bootstrap an application.
*
- <example module="ngAppDemo">
+ <example module="ngAppDemo" name="ng-app">
<file name="index.html">
<div ng-controller="ngAppDemoController">
I can add: {{a}} + {{b}} = {{ a+b }}
@@ -1488,7 +1707,7 @@ function getNgAttribute(element, ngAttr) {
*
* Using `ngStrictDi`, you would see something like this:
*
- <example ng-app-included="true">
+ <example ng-app-included="true" name="strict-di">
<file name="index.html">
<div ng-app="ngAppStrictDemo" ng-strict-di>
<div ng-controller="GoodController1">
@@ -1537,7 +1756,7 @@ function getNgAttribute(element, ngAttr) {
}])
.controller('GoodController2', GoodController2);
function GoodController2($scope) {
- $scope.name = "World";
+ $scope.name = 'World';
}
GoodController2.$inject = ['$scope'];
</file>
@@ -1568,7 +1787,7 @@ function angularInit(element, bootstrap) {
module,
config = {};
- // The element `element` has priority over any other element
+ // The element `element` has priority over any other element.
forEach(ngAttrPrefixes, function(prefix) {
var name = prefix + 'app';
@@ -1587,7 +1806,12 @@ function angularInit(element, bootstrap) {
}
});
if (appElement) {
- config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
+ if (!isAutoBootstrapAllowed) {
+ window.console.error('Angular: disabling automatic bootstrap. <script> protocol indicates ' +
+ 'an extension, document.location.href does not match.');
+ return;
+ }
+ config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
bootstrap(appElement, module ? [module] : [], config);
}
}
@@ -1599,16 +1823,25 @@ function angularInit(element, bootstrap) {
* @description
* Use this function to manually start up angular application.
*
- * See: {@link guide/bootstrap Bootstrap}
- *
- * Note that Protractor based end-to-end tests cannot use this function to bootstrap manually.
- * They must use {@link ng.directive:ngApp ngApp}.
+ * For more information, see the {@link guide/bootstrap Bootstrap guide}.
*
* Angular will detect if it has been loaded into the browser more than once and only allow the
* first loaded script to be bootstrapped and will report a warning to the browser console for
* each of the subsequent scripts. This prevents strange results in applications, where otherwise
* multiple instances of Angular try to work on the DOM.
*
+ * <div class="alert alert-warning">
+ * **Note:** Protractor based end-to-end tests cannot use this function to bootstrap manually.
+ * They must use {@link ng.directive:ngApp ngApp}.
+ * </div>
+ *
+ * <div class="alert alert-warning">
+ * **Note:** Do not bootstrap the app on an element with a directive that uses {@link ng.$compile#transclusion transclusion},
+ * such as {@link ng.ngIf `ngIf`}, {@link ng.ngInclude `ngInclude`} and {@link ngRoute.ngView `ngView`}.
+ * Doing this misplaces the app {@link ng.$rootElement `$rootElement`} and the app's {@link auto.$injector injector},
+ * causing animations to stop working and making the injector inaccessible from outside the app.
+ * </div>
+ *
* ```html
* <!doctype html>
* <html>
@@ -1652,11 +1885,11 @@ function bootstrap(element, modules, config) {
element = jqLite(element);
if (element.injector()) {
- var tag = (element[0] === document) ? 'document' : startingTag(element);
- //Encode angle brackets to prevent input from being sanitized to empty string #8683
+ var tag = (element[0] === window.document) ? 'document' : startingTag(element);
+ // Encode angle brackets to prevent input from being sanitized to empty string #8683.
throw ngMinErr(
'btstrpd',
- "App Already Bootstrapped with this Element '{0}'",
+ 'App already bootstrapped with this element \'{0}\'',
tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
}
@@ -1773,7 +2006,7 @@ function bindJQuery() {
extend(jQuery.fn, {
scope: JQLitePrototype.scope,
isolateScope: JQLitePrototype.isolateScope,
- controller: JQLitePrototype.controller,
+ controller: /** @type {?} */ (JQLitePrototype).controller,
injector: JQLitePrototype.injector,
inheritedData: JQLitePrototype.inheritedData
});
@@ -1785,7 +2018,7 @@ function bindJQuery() {
jQuery.cleanData = function(elems) {
var events;
for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
+ events = jQuery._data(elem, 'events');
if (events && events.$destroy) {
jQuery(elem).triggerHandler('$destroy');
}
@@ -1807,7 +2040,7 @@ function bindJQuery() {
*/
function assertArg(arg, name, reason) {
if (!arg) {
- throw ngMinErr('areq', "Argument '{0}' is {1}", (name || '?'), (reason || "required"));
+ throw ngMinErr('areq', 'Argument \'{0}\' is {1}', (name || '?'), (reason || 'required'));
}
return arg;
}
@@ -1829,7 +2062,7 @@ function assertArgFn(arg, name, acceptArrayAnnotation) {
*/
function assertNotHasOwnProperty(name, context) {
if (name === 'hasOwnProperty') {
- throw ngMinErr('badname', "hasOwnProperty is not a valid {0} name", context);
+ throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
}
}
@@ -1899,6 +2132,27 @@ function createMap() {
return Object.create(null);
}
+function stringify(value) {
+ if (value == null) { // null || undefined
+ return '';
+ }
+ switch (typeof value) {
+ case 'string':
+ break;
+ case 'number':
+ value = '' + value;
+ break;
+ default:
+ if (hasCustomToString(value) && !isArray(value) && !isDate(value)) {
+ value = value.toString();
+ } else {
+ value = toJson(value);
+ }
+ }
+
+ return value;
+}
+
var NODE_TYPE_ELEMENT = 1;
var NODE_TYPE_ATTRIBUTE = 2;
var NODE_TYPE_TEXT = 3;
@@ -1982,9 +2236,12 @@ function setupModuleLoader(window) {
* unspecified then the module is being retrieved for further configuration.
* @param {Function=} configFn Optional configuration function for the module. Same as
* {@link angular.Module#config Module#config()}.
- * @returns {module} new module with the {@link angular.Module} api.
+ * @returns {angular.Module} new module with the {@link angular.Module} api.
*/
return function module(name, requires, configFn) {
+
+ var info = {};
+
var assertNotHasOwnProperty = function(name, context) {
if (name === 'hasOwnProperty') {
throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
@@ -1997,9 +2254,9 @@ function setupModuleLoader(window) {
}
return ensure(modules, name, function() {
if (!requires) {
- throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
- "the module name or forgot to load it. If registering a module ensure that you " +
- "specify the dependencies as the second argument.", name);
+ throw $injectorMinErr('nomod', 'Module \'{0}\' is not available! You either misspelled ' +
+ 'the module name or forgot to load it. If registering a module ensure that you ' +
+ 'specify the dependencies as the second argument.', name);
}
/** @type {!Array.<Array.<*>>} */
@@ -2021,6 +2278,45 @@ function setupModuleLoader(window) {
_runBlocks: runBlocks,
/**
+ * @ngdoc method
+ * @name angular.Module#info
+ * @module ng
+ *
+ * @param {Object=} info Information about the module
+ * @returns {Object|Module} The current info object for this module if called as a getter,
+ * or `this` if called as a setter.
+ *
+ * @description
+ * Read and write custom information about this module.
+ * For example you could put the version of the module in here.
+ *
+ * ```js
+ * angular.module('myModule', []).info({ version: '1.0.0' });
+ * ```
+ *
+ * The version could then be read back out by accessing the module elsewhere:
+ *
+ * ```
+ * var version = angular.module('myModule').info().version;
+ * ```
+ *
+ * You can also retrieve this information during runtime via the
+ * {@link $injector#modules `$injector.modules`} property:
+ *
+ * ```js
+ * var version = $injector.modules['myModule'].info().version;
+ * ```
+ */
+ info: function(value) {
+ if (isDefined(value)) {
+ if (!isObject(value)) throw ngMinErr('aobj', 'Argument \'{0}\' must be an object', 'value');
+ info = value;
+ return this;
+ }
+ return info;
+ },
+
+ /**
* @ngdoc property
* @name angular.Module#requires
* @module ng
@@ -2103,13 +2399,13 @@ function setupModuleLoader(window) {
* @ngdoc method
* @name angular.Module#decorator
* @module ng
- * @param {string} The name of the service to decorate.
- * @param {Function} This function will be invoked when the service needs to be
- * instantiated and should return the decorated service instance.
+ * @param {string} name The name of the service to decorate.
+ * @param {Function} decorFn This function will be invoked when the service needs to be
+ * instantiated and should return the decorated service instance.
* @description
* See {@link auto.$provide#decorator $provide.decorator()}.
*/
- decorator: invokeLaterAndSetModuleName('$provide', 'decorator'),
+ decorator: invokeLaterAndSetModuleName('$provide', 'decorator', configBlocks),
/**
* @ngdoc method
@@ -2194,112 +2490,12 @@ function setupModuleLoader(window) {
* @module ng
* @param {string} name Name of the component in camel-case (i.e. myComp which will match as my-comp)
* @param {Object} options Component definition object (a simplified
- * {@link ng.$compile#directive-definition-object directive definition object}),
- * has the following properties (all optional):
- *
- * - `controller` – `{(string|function()=}` – Controller constructor function that should be
- * associated with newly created scope or the name of a {@link ng.$compile#-controller-
- * registered controller} if passed as a string. Empty function by default.
- * - `controllerAs` – `{string=}` – An identifier name for a reference to the controller.
- * If present, the controller will be published to scope under the `controllerAs` name.
- * If not present, this will default to be the same as the component name.
- * - `template` – `{string=|function()=}` – html template as a string or a function that
- * returns an html template as a string which should be used as the contents of this component.
- * Empty string by default.
- *
- * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- *
- * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
- * template that should be used as the contents of this component.
- *
- * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
- * the following locals:
- *
- * - `$element` - Current element
- * - `$attrs` - Current attributes object for the element
- * - `bindings` – `{object=}` – Define DOM attribute binding to component properties.
- * Component properties are always bound to the component controller and not to the scope.
- * - `transclude` – `{boolean=}` – Whether {@link $compile#transclusion transclusion} is enabled.
- * Enabled by default.
- * - `isolate` – `{boolean=}` – Whether the new scope is isolated. Isolated by default.
- * - `restrict` - `{string=}` - String of subset of {@link ng.$compile#-restrict- EACM} which
- * restricts the component to specific directive declaration style. If omitted, this defaults to 'E'.
- * - `$canActivate` – `{function()=}` – TBD.
- * - `$routeConfig` – `{object=}` – TBD.
+ * {@link ng.$compile#directive-definition-object directive definition object})
*
* @description
- * Register a component definition with the compiler. This is short for registering a specific
- * subset of directives which represents actual UI components in your application. Component
- * definitions are very simple and do not require the complexity behind defining directives.
- * Component definitions usually consist only of the template and the controller backing it.
- * In order to make the definition easier, components enforce best practices like controllerAs
- * and default behaviors like scope isolation, restrict to elements and allow transclusion.
- *
- * Here are a few examples of how you would usually define components:
- *
- * ```js
- * var myMod = angular.module(...);
- * myMod.component('myComp', {
- * template: '<div>My name is {{myComp.name}}</div>',
- * controller: function() {
- * this.name = 'shahar';
- * }
- * });
- *
- * myMod.component('myComp', {
- * template: '<div>My name is {{myComp.name}}</div>',
- * bindings: {name: '@'}
- * });
- *
- * myMod.component('myComp', {
- * templateUrl: 'views/my-comp.html',
- * controller: 'MyCtrl as ctrl',
- * bindings: {name: '@'}
- * });
- *
- * ```
- *
- * See {@link ng.$compileProvider#directive $compileProvider.directive()}.
+ * See {@link ng.$compileProvider#component $compileProvider.component()}.
*/
- component: function(name, options) {
- function factory($injector) {
- function makeInjectable(fn) {
- if (angular.isFunction(fn)) {
- return function(tElement, tAttrs) {
- return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
- };
- } else {
- return fn;
- }
- }
-
- var template = (!options.template && !options.templateUrl ? '' : options.template);
- return {
- controller: options.controller || function() {},
- controllerAs: identifierForController(options.controller) || options.controllerAs || name,
- template: makeInjectable(template),
- templateUrl: makeInjectable(options.templateUrl),
- transclude: options.transclude === undefined ? true : options.transclude,
- scope: options.isolate === false ? true : {},
- bindToController: options.bindings || {},
- restrict: options.restrict || 'E'
- };
- }
-
- if (options.$canActivate) {
- factory.$canActivate = options.$canActivate;
- }
- if (options.$routeConfig) {
- factory.$routeConfig = options.$routeConfig;
- }
- factory.$inject = ['$injector'];
-
- return moduleInstance.directive(name, factory);
- },
+ component: invokeLaterAndSetModuleName('$compileProvider', 'component'),
/**
* @ngdoc method
@@ -2355,10 +2551,11 @@ function setupModuleLoader(window) {
* @param {string} method
* @returns {angular.Module}
*/
- function invokeLaterAndSetModuleName(provider, method) {
+ function invokeLaterAndSetModuleName(provider, method, queue) {
+ if (!queue) queue = invokeQueue;
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
- invokeQueue.push([provider, method, arguments]);
+ queue.push([provider, method, arguments]);
return moduleInstance;
};
}
@@ -2368,11 +2565,44 @@ function setupModuleLoader(window) {
}
-/* global: toDebugString: true */
+/* global shallowCopy: true */
+
+/**
+ * Creates a shallow copy of an object, an array or a primitive.
+ *
+ * Assumes that there are no proto properties for objects.
+ */
+function shallowCopy(src, dst) {
+ if (isArray(src)) {
+ dst = dst || [];
+
+ for (var i = 0, ii = src.length; i < ii; i++) {
+ dst[i] = src[i];
+ }
+ } else if (isObject(src)) {
+ dst = dst || {};
+
+ for (var key in src) {
+ if (!(key.charAt(0) === '$' && key.charAt(1) === '$')) {
+ dst[key] = src[key];
+ }
+ }
+ }
-function serializeObject(obj) {
+ return dst || src;
+}
+
+/* global toDebugString: true */
+
+function serializeObject(obj, maxDepth) {
var seen = [];
+ // There is no direct way to stringify object until reaching a specific depth
+ // and a very deep object can cause a performance issue, so we copy the object
+ // based on this specific depth and then stringify it.
+ if (isValidObjectMaxDepth(maxDepth)) {
+ obj = copy(obj, null, maxDepth);
+ }
return JSON.stringify(obj, function(key, val) {
val = toJsonReplacer(key, val);
if (isObject(val)) {
@@ -2385,13 +2615,13 @@ function serializeObject(obj) {
});
}
-function toDebugString(obj) {
+function toDebugString(obj, maxDepth) {
if (typeof obj === 'function') {
return obj.toString().replace(/ \{[\s\S]*$/, '');
} else if (isUndefined(obj)) {
return 'undefined';
} else if (typeof obj !== 'string') {
- return serializeObject(obj);
+ return serializeObject(obj, maxDepth);
}
return obj;
}
@@ -2407,7 +2637,6 @@ function toDebugString(obj) {
formDirective,
scriptDirective,
selectDirective,
- styleDirective,
optionDirective,
ngBindDirective,
ngBindHtmlDirective,
@@ -2452,26 +2681,30 @@ function toDebugString(obj) {
$AnchorScrollProvider,
$AnimateProvider,
$CoreAnimateCssProvider,
+ $$CoreAnimateJsProvider,
$$CoreAnimateQueueProvider,
- $$CoreAnimateRunnerProvider,
+ $$AnimateRunnerFactoryProvider,
+ $$AnimateAsyncRunFactoryProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
$DateProvider,
$DocumentProvider,
+ $$IsDocumentHiddenProvider,
$ExceptionHandlerProvider,
$FilterProvider,
$$ForceReflowProvider,
$InterpolateProvider,
$IntervalProvider,
- $$HashMapProvider,
$HttpProvider,
$HttpParamSerializerProvider,
$HttpParamSerializerJQLikeProvider,
$HttpBackendProvider,
$xhrFactoryProvider,
+ $jsonpCallbacksProvider,
$LocationProvider,
$LogProvider,
+ $$MapProvider,
$ParseProvider,
$RootScopeProvider,
$QProvider,
@@ -2507,16 +2740,19 @@ function toDebugString(obj) {
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.5.0-beta.2', // all of these placeholder strings will be replaced by grunt's
- major: 1, // package task
- minor: 5,
- dot: 0,
- codeName: 'effective-delegation'
+ // These placeholder strings will be replaced by grunt's `build` task.
+ // They need to be double- or single-quoted.
+ full: '1.6.3',
+ major: 1,
+ minor: 6,
+ dot: 3,
+ codeName: 'scriptalicious-bootstrapping'
};
function publishExternalAPI(angular) {
extend(angular, {
+ 'errorHandlingConfig': errorHandlingConfig,
'bootstrap': bootstrap,
'copy': copy,
'extend': extend,
@@ -2542,11 +2778,14 @@ function publishExternalAPI(angular) {
'isDate': isDate,
'lowercase': lowercase,
'uppercase': uppercase,
- 'callbacks': {counter: 0},
+ 'callbacks': {$$counter: 0},
'getTestability': getTestability,
+ 'reloadWithDebugInfo': reloadWithDebugInfo,
'$$minErr': minErr,
'$$csp': csp,
- 'reloadWithDebugInfo': reloadWithDebugInfo
+ '$$encodeUriSegment': encodeUriSegment,
+ '$$encodeUriQuery': encodeUriQuery,
+ '$$stringify': stringify
});
angularModule = setupModuleLoader(window);
@@ -2565,7 +2804,6 @@ function publishExternalAPI(angular) {
form: formDirective,
script: scriptDirective,
select: selectDirective,
- style: styleDirective,
option: optionDirective,
ngBind: ngBindDirective,
ngBindHtml: ngBindHtmlDirective,
@@ -2613,12 +2851,15 @@ function publishExternalAPI(angular) {
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
$animateCss: $CoreAnimateCssProvider,
+ $$animateJs: $$CoreAnimateJsProvider,
$$animateQueue: $$CoreAnimateQueueProvider,
- $$AnimateRunner: $$CoreAnimateRunnerProvider,
+ $$AnimateRunner: $$AnimateRunnerFactoryProvider,
+ $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
$document: $DocumentProvider,
+ $$isDocumentHidden: $$IsDocumentHiddenProvider,
$exceptionHandler: $ExceptionHandlerProvider,
$filter: $FilterProvider,
$$forceReflow: $$ForceReflowProvider,
@@ -2629,6 +2870,7 @@ function publishExternalAPI(angular) {
$httpParamSerializerJQLike: $HttpParamSerializerJQLikeProvider,
$httpBackend: $HttpBackendProvider,
$xhrFactory: $xhrFactoryProvider,
+ $jsonpCallbacks: $jsonpCallbacksProvider,
$location: $LocationProvider,
$log: $LogProvider,
$parse: $ParseProvider,
@@ -2645,11 +2887,12 @@ function publishExternalAPI(angular) {
$window: $WindowProvider,
$$rAF: $$RAFProvider,
$$jqLite: $$jqLiteProvider,
- $$HashMap: $$HashMapProvider,
+ $$Map: $$MapProvider,
$$cookieReader: $$CookieReaderProvider
});
}
- ]);
+ ])
+ .info({ angularVersion: '1.6.3' });
}
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
@@ -2663,11 +2906,10 @@ function publishExternalAPI(angular) {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
-/* global JQLitePrototype: true,
- addEventListenerFn: true,
- removeEventListenerFn: true,
+/* global
+ JQLitePrototype: true,
BOOLEAN_ATTR: true,
- ALIASED_ATTR: true,
+ ALIASED_ATTR: true
*/
//////////////////////////////////
@@ -2685,29 +2927,36 @@ function publishExternalAPI(angular) {
*
* If jQuery is available, `angular.element` is an alias for the
* [jQuery](http://api.jquery.com/jQuery/) function. If jQuery is not available, `angular.element`
- * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or "jqLite."
+ * delegates to Angular's built-in subset of jQuery, called "jQuery lite" or **jqLite**.
+ *
+ * jqLite is a tiny, API-compatible subset of jQuery that allows
+ * Angular to manipulate the DOM in a cross-browser compatible way. jqLite implements only the most
+ * commonly needed functionality with the goal of having a very small footprint.
*
- * <div class="alert alert-success">jqLite is a tiny, API-compatible subset of jQuery that allows
- * Angular to manipulate the DOM in a cross-browser compatible way. **jqLite** implements only the most
- * commonly needed functionality with the goal of having a very small footprint.</div>
+ * To use `jQuery`, simply ensure it is loaded before the `angular.js` file. You can also use the
+ * {@link ngJq `ngJq`} directive to specify that jqlite should be used over jQuery, or to use a
+ * specific version of jQuery if multiple versions exist on the page.
*
- * To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
+ * <div class="alert alert-info">**Note:** All element references in Angular are always wrapped with jQuery or
+ * jqLite (such as the element argument in a directive's compile / link function). They are never raw DOM references.</div>
*
- * <div class="alert">**Note:** all element references in Angular are always wrapped with jQuery or
- * jqLite; they are never raw DOM references.</div>
+ * <div class="alert alert-warning">**Note:** Keep in mind that this function will not find elements
+ * by tag name / CSS selector. For lookups by tag name, try instead `angular.element(document).find(...)`
+ * or `$document.find()`, or use the standard DOM APIs, e.g. `document.querySelectorAll()`.</div>
*
* ## Angular's jqLite
* jqLite provides only the following jQuery methods:
*
- * - [`addClass()`](http://api.jquery.com/addClass/)
+ * - [`addClass()`](http://api.jquery.com/addClass/) - Does not support a function as first argument
* - [`after()`](http://api.jquery.com/after/)
* - [`append()`](http://api.jquery.com/append/)
* - [`attr()`](http://api.jquery.com/attr/) - Does not support functions as parameters
- * - [`bind()`](http://api.jquery.com/bind/) - Does not support namespaces, selectors or eventData
+ * - [`bind()`](http://api.jquery.com/bind/) (_deprecated_, use [`on()`](http://api.jquery.com/on/)) - Does not support namespaces, selectors or eventData
* - [`children()`](http://api.jquery.com/children/) - Does not support selectors
* - [`clone()`](http://api.jquery.com/clone/)
* - [`contents()`](http://api.jquery.com/contents/)
- * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`. As a setter, does not convert numbers to strings or append 'px'.
+ * - [`css()`](http://api.jquery.com/css/) - Only retrieves inline-styles, does not call `getComputedStyle()`.
+ * As a setter, does not convert numbers to strings or append 'px', and also does not have automatic property prefixing.
* - [`data()`](http://api.jquery.com/data/)
* - [`detach()`](http://api.jquery.com/detach/)
* - [`empty()`](http://api.jquery.com/empty/)
@@ -2722,16 +2971,16 @@ function publishExternalAPI(angular) {
* - [`parent()`](http://api.jquery.com/parent/) - Does not support selectors
* - [`prepend()`](http://api.jquery.com/prepend/)
* - [`prop()`](http://api.jquery.com/prop/)
- * - [`ready()`](http://api.jquery.com/ready/)
+ * - [`ready()`](http://api.jquery.com/ready/) (_deprecated_, use `angular.element(callback)` instead of `angular.element(document).ready(callback)`)
* - [`remove()`](http://api.jquery.com/remove/)
- * - [`removeAttr()`](http://api.jquery.com/removeAttr/)
- * - [`removeClass()`](http://api.jquery.com/removeClass/)
+ * - [`removeAttr()`](http://api.jquery.com/removeAttr/) - Does not support multiple attributes
+ * - [`removeClass()`](http://api.jquery.com/removeClass/) - Does not support a function as first argument
* - [`removeData()`](http://api.jquery.com/removeData/)
* - [`replaceWith()`](http://api.jquery.com/replaceWith/)
* - [`text()`](http://api.jquery.com/text/)
- * - [`toggleClass()`](http://api.jquery.com/toggleClass/)
- * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers.
- * - [`unbind()`](http://api.jquery.com/unbind/) - Does not support namespaces or event object as parameter
+ * - [`toggleClass()`](http://api.jquery.com/toggleClass/) - Does not support a function as first argument
+ * - [`triggerHandler()`](http://api.jquery.com/triggerHandler/) - Passes a dummy event object to handlers
+ * - [`unbind()`](http://api.jquery.com/unbind/) (_deprecated_, use [`off()`](http://api.jquery.com/off/)) - Does not support namespaces or event object as parameter
* - [`val()`](http://api.jquery.com/val/)
* - [`wrap()`](http://api.jquery.com/wrap/)
*
@@ -2759,6 +3008,9 @@ function publishExternalAPI(angular) {
* - `inheritedData()` - same as `data()`, but walks up the DOM until a value is found or the top
* parent element is reached.
*
+ * @knownIssue You cannot spy on `angular.element` if you are using Jasmine version 1.x. See
+ * https://github.com/angular/angular.js/issues/14251 for more information.
+ *
* @param {string|DOMElement} element HTML string or DOMElement to be wrapped into jQuery.
* @returns {Object} jQuery object.
*/
@@ -2766,13 +3018,7 @@ function publishExternalAPI(angular) {
JQLite.expando = 'ng339';
var jqCache = JQLite.cache = {},
- jqId = 1,
- addEventListenerFn = function(element, type, fn) {
- element.addEventListener(type, fn, false);
- },
- removeEventListenerFn = function(element, type, fn) {
- element.removeEventListener(type, fn, false);
- };
+ jqId = 1;
/*
* !!! This is an undocumented "private" function !!!
@@ -2785,22 +3031,31 @@ JQLite._data = function(node) {
function jqNextId() { return ++jqId; }
-var SPECIAL_CHARS_REGEXP = /([\:\-\_]+(.))/g;
-var MOZ_HACK_REGEXP = /^moz([A-Z])/;
-var MOUSE_EVENT_MAP= { mouseleave: "mouseout", mouseenter: "mouseover"};
+var DASH_LOWERCASE_REGEXP = /-([a-z])/g;
+var MS_HACK_REGEXP = /^-ms-/;
+var MOUSE_EVENT_MAP = { mouseleave: 'mouseout', mouseenter: 'mouseover' };
var jqLiteMinErr = minErr('jqLite');
/**
- * Converts snake_case to camelCase.
- * Also there is special case for Moz prefix starting with upper case letter.
+ * Converts kebab-case to camelCase.
+ * There is also a special case for the ms prefix starting with a lowercase letter.
* @param name Name to normalize
*/
-function camelCase(name) {
- return name.
- replace(SPECIAL_CHARS_REGEXP, function(_, separator, letter, offset) {
- return offset ? letter.toUpperCase() : letter;
- }).
- replace(MOZ_HACK_REGEXP, 'Moz$1');
+function cssKebabToCamel(name) {
+ return kebabToCamel(name.replace(MS_HACK_REGEXP, 'ms-'));
+}
+
+function fnCamelCaseReplace(all, letter) {
+ return letter.toUpperCase();
+}
+
+/**
+ * Converts kebab-case to camelCase.
+ * @param name Name to normalize
+ */
+function kebabToCamel(name) {
+ return name
+ .replace(DASH_LOWERCASE_REGEXP, fnCamelCaseReplace);
}
var SINGLE_TAG_REGEXP = /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/;
@@ -2815,7 +3070,7 @@ var wrapMap = {
'col': [2, '<table><colgroup>', '</colgroup></table>'],
'tr': [2, '<table><tbody>', '</tbody></table>'],
'td': [3, '<table><tbody><tr>', '</tr></tbody></table>'],
- '_default': [0, "", ""]
+ '_default': [0, '', '']
};
wrapMap.optgroup = wrapMap.option;
@@ -2857,10 +3112,10 @@ function jqLiteBuildFragment(html, context) {
nodes.push(context.createTextNode(html));
} else {
// Convert html into DOM nodes
- tmp = tmp || fragment.appendChild(context.createElement("div"));
- tag = (TAG_NAME_REGEXP.exec(html) || ["", ""])[1].toLowerCase();
+ tmp = fragment.appendChild(context.createElement('div'));
+ tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase();
wrap = wrapMap[tag] || wrapMap._default;
- tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, "<$1></$2>") + wrap[2];
+ tmp.innerHTML = wrap[1] + html.replace(XHTML_TAG_REGEXP, '<$1></$2>') + wrap[2];
// Descend through wrappers to the right content
i = wrap[0];
@@ -2871,12 +3126,12 @@ function jqLiteBuildFragment(html, context) {
nodes = concat(nodes, tmp.childNodes);
tmp = fragment.firstChild;
- tmp.textContent = "";
+ tmp.textContent = '';
}
// Remove wrapper from fragment
- fragment.textContent = "";
- fragment.innerHTML = ""; // Clear inner HTML
+ fragment.textContent = '';
+ fragment.innerHTML = ''; // Clear inner HTML
forEach(nodes, function(node) {
fragment.appendChild(node);
});
@@ -2885,7 +3140,7 @@ function jqLiteBuildFragment(html, context) {
}
function jqLiteParseHTML(html, context) {
- context = context || document;
+ context = context || window.document;
var parsed;
if ((parsed = SINGLE_TAG_REGEXP.exec(html))) {
@@ -2899,12 +3154,21 @@ function jqLiteParseHTML(html, context) {
return [];
}
+function jqLiteWrapNode(node, wrapper) {
+ var parent = node.parentNode;
+
+ if (parent) {
+ parent.replaceChild(wrapper, node);
+ }
+
+ wrapper.appendChild(node);
+}
+
// IE9-11 has no method "contains" in SVG element and in Node.prototype. Bug #10259.
-var jqLiteContains = Node.prototype.contains || function(arg) {
- // jshint bitwise: false
+var jqLiteContains = window.Node.prototype.contains || /** @this */ function(arg) {
+ // eslint-disable-next-line no-bitwise
return !!(this.compareDocumentPosition(arg) & 16);
- // jshint bitwise: true
};
/////////////////////////////////////////////
@@ -2920,7 +3184,7 @@ function JQLite(element) {
argIsString = true;
}
if (!(this instanceof JQLite)) {
- if (argIsString && element.charAt(0) != '<') {
+ if (argIsString && element.charAt(0) !== '<') {
throw jqLiteMinErr('nosel', 'Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element');
}
return new JQLite(element);
@@ -2928,6 +3192,8 @@ function JQLite(element) {
if (argIsString) {
jqLiteAddNodes(this, jqLiteParseHTML(element));
+ } else if (isFunction(element)) {
+ jqLiteReady(element);
} else {
jqLiteAddNodes(this, element);
}
@@ -2960,7 +3226,7 @@ function jqLiteOff(element, type, fn, unsupported) {
if (!type) {
for (type in events) {
if (type !== '$destroy') {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
}
delete events[type];
}
@@ -2972,7 +3238,7 @@ function jqLiteOff(element, type, fn, unsupported) {
arrayRemove(listenerFns || [], fn);
}
if (!(isDefined(fn) && listenerFns && listenerFns.length > 0)) {
- removeEventListenerFn(element, type, handle);
+ element.removeEventListener(type, handle);
delete events[type];
}
};
@@ -3023,6 +3289,7 @@ function jqLiteExpandoStore(element, createIfNecessary) {
function jqLiteData(element, key, value) {
if (jqLiteAcceptsData(element)) {
+ var prop;
var isSimpleSetter = isDefined(value);
var isSimpleGetter = !isSimpleSetter && key && !isObject(key);
@@ -3031,16 +3298,18 @@ function jqLiteData(element, key, value) {
var data = expandoStore && expandoStore.data;
if (isSimpleSetter) { // data('key', value)
- data[key] = value;
+ data[kebabToCamel(key)] = value;
} else {
if (massGetter) { // data()
return data;
} else {
if (isSimpleGetter) { // data('key')
// don't force creation of expandoStore if it doesn't exist yet
- return data && data[key];
+ return data && data[kebabToCamel(key)];
} else { // mass-setter: data({key1: val1, key2: val2})
- extend(data, key);
+ for (prop in key) {
+ data[kebabToCamel(prop)] = key[prop];
+ }
}
}
}
@@ -3049,17 +3318,17 @@ function jqLiteData(element, key, value) {
function jqLiteHasClass(element, selector) {
if (!element.getAttribute) return false;
- return ((" " + (element.getAttribute('class') || '') + " ").replace(/[\n\t]/g, " ").
- indexOf(" " + selector + " ") > -1);
+ return ((' ' + (element.getAttribute('class') || '') + ' ').replace(/[\n\t]/g, ' ').
+ indexOf(' ' + selector + ' ') > -1);
}
function jqLiteRemoveClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
forEach(cssClasses.split(' '), function(cssClass) {
element.setAttribute('class', trim(
- (" " + (element.getAttribute('class') || '') + " ")
- .replace(/[\n\t]/g, " ")
- .replace(" " + trim(cssClass) + " ", " "))
+ (' ' + (element.getAttribute('class') || '') + ' ')
+ .replace(/[\n\t]/g, ' ')
+ .replace(' ' + trim(cssClass) + ' ', ' '))
);
});
}
@@ -3068,7 +3337,7 @@ function jqLiteRemoveClass(element, cssClasses) {
function jqLiteAddClass(element, cssClasses) {
if (cssClasses && element.setAttribute) {
var existingClasses = (' ' + (element.getAttribute('class') || '') + ' ')
- .replace(/[\n\t]/g, " ");
+ .replace(/[\n\t]/g, ' ');
forEach(cssClasses.split(' '), function(cssClass) {
cssClass = trim(cssClass);
@@ -3115,7 +3384,7 @@ function jqLiteController(element, name) {
function jqLiteInheritedData(element, name, value) {
// if element is the document object work with the html element instead
// this makes $(document).scope() possible
- if (element.nodeType == NODE_TYPE_DOCUMENT) {
+ if (element.nodeType === NODE_TYPE_DOCUMENT) {
element = element.documentElement;
}
var names = isArray(name) ? name : [name];
@@ -3149,7 +3418,7 @@ function jqLiteRemove(element, keepData) {
function jqLiteDocumentLoaded(action, win) {
win = win || window;
if (win.document.readyState === 'complete') {
- // Force the action to be run async for consistent behaviour
+ // Force the action to be run async for consistent behavior
// from the action's point of view
// i.e. it will definitely not be in a $apply
win.setTimeout(action);
@@ -3159,30 +3428,32 @@ function jqLiteDocumentLoaded(action, win) {
}
}
+function jqLiteReady(fn) {
+ function trigger() {
+ window.document.removeEventListener('DOMContentLoaded', trigger);
+ window.removeEventListener('load', trigger);
+ fn();
+ }
+
+ // check if document is already loaded
+ if (window.document.readyState === 'complete') {
+ window.setTimeout(fn);
+ } else {
+ // We can not use jqLite since we are not done loading and jQuery could be loaded later.
+
+ // Works for modern browsers and IE9
+ window.document.addEventListener('DOMContentLoaded', trigger);
+
+ // Fallback to window.onload for others
+ window.addEventListener('load', trigger);
+ }
+}
+
//////////////////////////////////////////
// Functions which are declared directly.
//////////////////////////////////////////
var JQLitePrototype = JQLite.prototype = {
- ready: function(fn) {
- var fired = false;
-
- function trigger() {
- if (fired) return;
- fired = true;
- fn();
- }
-
- // check if document is already loaded
- if (document.readyState === 'complete') {
- setTimeout(trigger);
- } else {
- this.on('DOMContentLoaded', trigger); // works for modern browsers and IE9
- // we can not use jqLite since we are not done loading and jQuery could be loaded later.
- // jshint -W064
- JQLite(window).on('load', trigger); // fallback to window.onload for others
- // jshint +W064
- }
- },
+ ready: jqLiteReady,
toString: function() {
var value = [];
forEach(this, function(e) { value.push('' + e);});
@@ -3217,7 +3488,8 @@ var ALIASED_ATTR = {
'ngMaxlength': 'maxlength',
'ngMin': 'min',
'ngMax': 'max',
- 'ngPattern': 'pattern'
+ 'ngPattern': 'pattern',
+ 'ngStep': 'step'
};
function getBooleanAttrName(element, name) {
@@ -3268,7 +3540,7 @@ forEach({
hasClass: jqLiteHasClass,
css: function(element, name, value) {
- name = camelCase(name);
+ name = cssKebabToCamel(name);
if (isDefined(value)) {
element.style[name] = value;
@@ -3278,33 +3550,33 @@ forEach({
},
attr: function(element, name, value) {
+ var ret;
var nodeType = element.nodeType;
- if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT) {
+ if (nodeType === NODE_TYPE_TEXT || nodeType === NODE_TYPE_ATTRIBUTE || nodeType === NODE_TYPE_COMMENT ||
+ !element.getAttribute) {
return;
}
+
var lowercasedName = lowercase(name);
- if (BOOLEAN_ATTR[lowercasedName]) {
- if (isDefined(value)) {
- if (!!value) {
- element[name] = true;
- element.setAttribute(name, lowercasedName);
- } else {
- element[name] = false;
- element.removeAttribute(lowercasedName);
- }
+ var isBooleanAttr = BOOLEAN_ATTR[lowercasedName];
+
+ if (isDefined(value)) {
+ // setter
+
+ if (value === null || (value === false && isBooleanAttr)) {
+ element.removeAttribute(name);
} else {
- return (element[name] ||
- (element.attributes.getNamedItem(name) || noop).specified)
- ? lowercasedName
- : undefined;
- }
- } else if (isDefined(value)) {
- element.setAttribute(name, value);
- } else if (element.getAttribute) {
- // the extra argument "2" is to get the right thing for a.href in IE, see jQuery code
- // some elements (e.g. Document) don't have get attribute, so return undefined
- var ret = element.getAttribute(name, 2);
- // normalize non-existing attributes to undefined (as jQuery)
+ element.setAttribute(name, isBooleanAttr ? lowercasedName : value);
+ }
+ } else {
+ // getter
+
+ ret = element.getAttribute(name);
+
+ if (isBooleanAttr && ret !== null) {
+ ret = lowercasedName;
+ }
+ // Normalize non-existing attributes to undefined (as jQuery).
return ret === null ? undefined : ret;
}
},
@@ -3339,7 +3611,7 @@ forEach({
result.push(option.value || option.text);
}
});
- return result.length === 0 ? null : result;
+ return result;
}
return element.value;
}
@@ -3367,7 +3639,7 @@ forEach({
// in a way that survives minification.
// jqLiteEmpty takes no arguments but is a setter.
if (fn !== jqLiteEmpty &&
- (isUndefined((fn.length == 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
+ (isUndefined((fn.length === 2 && (fn !== jqLiteHasClass && fn !== jqLiteController)) ? arg1 : arg2))) {
if (isObject(arg1)) {
// we are a write, but the object properties are the key/values
@@ -3509,7 +3781,7 @@ forEach({
eventFns = events[type] = [];
eventFns.specialHandlerWrapper = specialHandlerWrapper;
if (type !== '$destroy' && !noEventListener) {
- addEventListenerFn(element, type, handle);
+ element.addEventListener(type, handle);
}
}
@@ -3591,12 +3863,7 @@ forEach({
},
wrap: function(element, wrapNode) {
- wrapNode = jqLite(wrapNode).eq(0).clone()[0];
- var parent = element.parentNode;
- if (parent) {
- parent.replaceChild(wrapNode, element);
- }
- wrapNode.appendChild(element);
+ jqLiteWrapNode(element, jqLite(wrapNode).eq(0).clone()[0]);
},
remove: jqLiteRemove,
@@ -3607,12 +3874,15 @@ forEach({
after: function(element, newElement) {
var index = element, parent = element.parentNode;
- newElement = new JQLite(newElement);
- for (var i = 0, ii = newElement.length; i < ii; i++) {
- var node = newElement[i];
- parent.insertBefore(node, index.nextSibling);
- index = node;
+ if (parent) {
+ newElement = new JQLite(newElement);
+
+ for (var i = 0, ii = newElement.length; i < ii; i++) {
+ var node = newElement[i];
+ parent.insertBefore(node, index.nextSibling);
+ index = node;
+ }
}
},
@@ -3706,14 +3976,15 @@ forEach({
}
return isDefined(value) ? value : this;
};
-
- // bind legacy bind/unbind to on/off
- JQLite.prototype.bind = JQLite.prototype.on;
- JQLite.prototype.unbind = JQLite.prototype.off;
});
+// bind legacy bind/unbind to on/off
+JQLite.prototype.bind = JQLite.prototype.on;
+JQLite.prototype.unbind = JQLite.prototype.off;
+
// Provider for private $$jqLite service
+/** @this */
function $$jqLiteProvider() {
this.$get = function $$jqLite() {
return extend(JQLite, {
@@ -3756,7 +4027,7 @@ function hashKey(obj, nextUidFn) {
}
var objType = typeof obj;
- if (objType == 'function' || (objType == 'object' && obj !== null)) {
+ if (objType === 'function' || (objType === 'object' && obj !== null)) {
key = obj.$$hashKey = objType + ':' + (nextUidFn || nextUid)();
} else {
key = objType + ':' + obj;
@@ -3765,50 +4036,70 @@ function hashKey(obj, nextUidFn) {
return key;
}
-/**
- * HashMap which can use objects as keys
- */
-function HashMap(array, isolatedUid) {
- if (isolatedUid) {
- var uid = 0;
- this.nextUid = function() {
- return ++uid;
- };
- }
- forEach(array, this.put, this);
+// A minimal ES2015 Map implementation.
+// Should be bug/feature equivalent to the native implementations of supported browsers
+// (for the features required in Angular).
+// See https://kangax.github.io/compat-table/es6/#test-Map
+var nanKey = Object.create(null);
+function NgMapShim() {
+ this._keys = [];
+ this._values = [];
+ this._lastKey = NaN;
+ this._lastIndex = -1;
}
-HashMap.prototype = {
- /**
- * Store key value pair
- * @param key key to store can be any type
- * @param value value to store can be any type
- */
- put: function(key, value) {
- this[hashKey(key, this.nextUid)] = value;
+NgMapShim.prototype = {
+ _idx: function(key) {
+ if (key === this._lastKey) {
+ return this._lastIndex;
+ }
+ this._lastKey = key;
+ this._lastIndex = this._keys.indexOf(key);
+ return this._lastIndex;
+ },
+ _transformKey: function(key) {
+ return isNumberNaN(key) ? nanKey : key;
},
-
- /**
- * @param key
- * @returns {Object} the value for the key
- */
get: function(key) {
- return this[hashKey(key, this.nextUid)];
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx !== -1) {
+ return this._values[idx];
+ }
},
+ set: function(key, value) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ idx = this._lastIndex = this._keys.length;
+ }
+ this._keys[idx] = key;
+ this._values[idx] = value;
- /**
- * Remove the key/value pair
- * @param key
- */
- remove: function(key) {
- var value = this[key = hashKey(key, this.nextUid)];
- delete this[key];
- return value;
+ // Support: IE11
+ // Do not `return this` to simulate the partial IE11 implementation
+ },
+ delete: function(key) {
+ key = this._transformKey(key);
+ var idx = this._idx(key);
+ if (idx === -1) {
+ return false;
+ }
+ this._keys.splice(idx, 1);
+ this._values.splice(idx, 1);
+ this._lastKey = NaN;
+ this._lastIndex = -1;
+ return true;
}
};
-var $$HashMapProvider = [function() {
+// For now, always use `NgMapShim`, even if `window.Map` is available. Some native implementations
+// are still buggy (often in subtle ways) and can cause hard-to-debug failures. When native `Map`
+// implementations get more stable, we can reconsider switching to `window.Map` (when available).
+var NgMap = NgMapShim;
+
+var $$MapProvider = [/** @this */function() {
this.$get = [function() {
- return HashMap;
+ return NgMap;
}];
}];
@@ -3869,20 +4160,25 @@ var $$HashMapProvider = [function() {
/**
* @ngdoc module
* @name auto
+ * @installation
* @description
*
* Implicit module which gets automatically added to each {@link auto.$injector $injector}.
*/
-var ARROW_ARG = /^([^\(]+?)=>/;
-var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
+var ARROW_ARG = /^([^(]+?)=>/;
+var FN_ARGS = /^[^(]*\(\s*([^)]*)\)/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
+function stringifyFn(fn) {
+ return Function.prototype.toString.call(fn);
+}
+
function extractArgs(fn) {
- var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
+ var fnText = stringifyFn(fn).replace(STRIP_COMMENTS, ''),
args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
return args;
}
@@ -3988,6 +4284,28 @@ function annotate(fn, strictDi, name) {
*/
/**
+ * @ngdoc property
+ * @name $injector#modules
+ * @type {Object}
+ * @description
+ * A hash containing all the modules that have been loaded into the
+ * $injector.
+ *
+ * You can use this property to find out information about a module via the
+ * {@link angular.Module#info `myModule.info(...)`} method.
+ *
+ * For example:
+ *
+ * ```
+ * var info = $injector.modules['ngAnimate'].info();
+ * ```
+ *
+ * **Do not use this property to attempt to modify the modules after the application
+ * has been bootstrapped.**
+ */
+
+
+/**
* @ngdoc method
* @name $injector#get
*
@@ -4125,7 +4443,6 @@ function annotate(fn, strictDi, name) {
-
/**
* @ngdoc service
* @name $provide
@@ -4150,18 +4467,20 @@ function annotate(fn, strictDi, name) {
* these cases the {@link auto.$provide $provide} service has additional helper methods to register
* services without specifying a provider.
*
- * * {@link auto.$provide#provider provider(provider)} - registers a **service provider** with the
+ * * {@link auto.$provide#provider provider(name, provider)} - registers a **service provider** with the
* {@link auto.$injector $injector}
- * * {@link auto.$provide#constant constant(obj)} - registers a value/object that can be accessed by
+ * * {@link auto.$provide#constant constant(name, obj)} - registers a value/object that can be accessed by
* providers and services.
- * * {@link auto.$provide#value value(obj)} - registers a value/object that can only be accessed by
+ * * {@link auto.$provide#value value(name, obj)} - registers a value/object that can only be accessed by
* services, not providers.
- * * {@link auto.$provide#factory factory(fn)} - registers a service **factory function**, `fn`,
+ * * {@link auto.$provide#factory factory(name, fn)} - registers a service **factory function**
* that will be wrapped in a **service provider** object, whose `$get` property will contain the
* given factory function.
- * * {@link auto.$provide#service service(class)} - registers a **constructor function**, `class`
+ * * {@link auto.$provide#service service(name, Fn)} - registers a **constructor function**
* that will be wrapped in a **service provider** object, whose `$get` property will instantiate
* a new object using the given constructor function.
+ * * {@link auto.$provide#decorator decorator(name, decorFn)} - registers a **decorator function** that
+ * will be able to modify or replace the implementation of another service.
*
* See the individual methods for more information and examples.
*/
@@ -4303,8 +4622,20 @@ function annotate(fn, strictDi, name) {
*
* Register a **service constructor**, which will be invoked with `new` to create the service
* instance.
- * This is short for registering a service where its provider's `$get` property is the service
- * constructor function that will be used to instantiate the service instance.
+ * This is short for registering a service where its provider's `$get` property is a factory
+ * function that returns an instance instantiated by the injector from the service constructor
+ * function.
+ *
+ * Internally it looks a bit like this:
+ *
+ * ```
+ * {
+ * $get: function() {
+ * return $injector.instantiate(constructor);
+ * }
+ * }
+ * ```
+ *
*
* You should use {@link auto.$provide#service $provide.service(class)} if you define your service
* as a type/class.
@@ -4344,14 +4675,13 @@ function annotate(fn, strictDi, name) {
* @description
*
* Register a **value service** with the {@link auto.$injector $injector}, such as a string, a
- * number, an array, an object or a function. This is short for registering a service where its
+ * number, an array, an object or a function. This is short for registering a service where its
* provider's `$get` property is a factory function that takes no arguments and returns the **value
- * service**.
+ * service**. That also means it is not possible to inject other services into a value service.
*
* Value services are similar to constant services, except that they cannot be injected into a
* module configuration function (see {@link angular.Module#config}) but they can be overridden by
- * an Angular
- * {@link auto.$provide#decorator decorator}.
+ * an Angular {@link auto.$provide#decorator decorator}.
*
* @param {string} name The name of the instance.
* @param {*} value The value.
@@ -4376,8 +4706,11 @@ function annotate(fn, strictDi, name) {
* @name $provide#constant
* @description
*
- * Register a **constant service**, such as a string, a number, an array, an object or a function,
- * with the {@link auto.$injector $injector}. Unlike {@link auto.$provide#value value} it can be
+ * Register a **constant service** with the {@link auto.$injector $injector}, such as a string,
+ * a number, an array, an object or a function. Like the {@link auto.$provide#value value}, it is not
+ * possible to inject other services into a constant.
+ *
+ * But unlike {@link auto.$provide#value value}, a constant can be
* injected into a module configuration function (see {@link angular.Module#config}) and it cannot
* be overridden by an Angular {@link auto.$provide#decorator decorator}.
*
@@ -4404,18 +4737,20 @@ function annotate(fn, strictDi, name) {
* @name $provide#decorator
* @description
*
- * Register a **service decorator** with the {@link auto.$injector $injector}. A service decorator
- * intercepts the creation of a service, allowing it to override or modify the behaviour of the
- * service. The object returned by the decorator may be the original service, or a new service
- * object which replaces or wraps and delegates to the original service.
+ * Register a **decorator function** with the {@link auto.$injector $injector}. A decorator function
+ * intercepts the creation of a service, allowing it to override or modify the behavior of the
+ * service. The return value of the decorator function may be the original service, or a new service
+ * that replaces (or wraps and delegates to) the original service.
+ *
+ * You can find out more about using decorators in the {@link guide/decorators} guide.
*
* @param {string} name The name of the service to decorate.
* @param {Function|Array.<string|Function>} decorator This function will be invoked when the service needs to be
- * instantiated and should return the decorated service instance. The function is called using
+ * provided and should return the decorated service instance. The function is called using
* the {@link auto.$injector#invoke injector.invoke} method and is therefore fully injectable.
* Local injection arguments:
*
- * * `$delegate` - The original service instance, which can be monkey patched, configured,
+ * * `$delegate` - The original service instance, which can be replaced, monkey patched, configured,
* decorated or delegated to.
*
* @example
@@ -4435,7 +4770,7 @@ function createInjector(modulesToLoad, strictDi) {
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
- loadedModules = new HashMap([], true),
+ loadedModules = new NgMap(),
providerCache = {
$provide: {
provider: supportObject(provider),
@@ -4451,7 +4786,7 @@ function createInjector(modulesToLoad, strictDi) {
if (angular.isString(caller)) {
path.push(caller);
}
- throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
+ throw $injectorMinErr('unpr', 'Unknown provider: {0}', path.join(' <- '));
})),
instanceCache = {},
protoInstanceInjector =
@@ -4463,6 +4798,7 @@ function createInjector(modulesToLoad, strictDi) {
instanceInjector = protoInstanceInjector;
providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ instanceInjector.modules = providerInjector.modules = createMap();
var runBlocks = loadModules(modulesToLoad);
instanceInjector = protoInstanceInjector.get('$injector');
instanceInjector.strictDi = strictDi;
@@ -4490,16 +4826,16 @@ function createInjector(modulesToLoad, strictDi) {
provider_ = providerInjector.instantiate(provider_);
}
if (!provider_.$get) {
- throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
+ throw $injectorMinErr('pget', 'Provider \'{0}\' must define $get factory method.', name);
}
- return providerCache[name + providerSuffix] = provider_;
+ return (providerCache[name + providerSuffix] = provider_);
}
function enforceReturnValue(name, factory) {
- return function enforcedReturnValue() {
+ return /** @this */ function enforcedReturnValue() {
var result = instanceInjector.invoke(factory, this);
if (isUndefined(result)) {
- throw $injectorMinErr('undef', "Provider '{0}' must return a value from $get factory method.", name);
+ throw $injectorMinErr('undef', 'Provider \'{0}\' must return a value from $get factory method.', name);
}
return result;
};
@@ -4543,7 +4879,7 @@ function createInjector(modulesToLoad, strictDi) {
var runBlocks = [], moduleFn;
forEach(modulesToLoad, function(module) {
if (loadedModules.get(module)) return;
- loadedModules.put(module, true);
+ loadedModules.set(module, true);
function runInvokeQueue(queue) {
var i, ii;
@@ -4558,6 +4894,7 @@ function createInjector(modulesToLoad, strictDi) {
try {
if (isString(module)) {
moduleFn = angularModule(module);
+ instanceInjector.modules[module] = moduleFn;
runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
runInvokeQueue(moduleFn._invokeQueue);
runInvokeQueue(moduleFn._configBlocks);
@@ -4572,15 +4909,15 @@ function createInjector(modulesToLoad, strictDi) {
if (isArray(module)) {
module = module[module.length - 1];
}
- if (e.message && e.stack && e.stack.indexOf(e.message) == -1) {
+ if (e.message && e.stack && e.stack.indexOf(e.message) === -1) {
// Safari & FF's stack traces don't contain error.message content
// unlike those of Chrome and IE
// So if stack doesn't contain message, we create a new string that contains both.
// Since error.stack is read-only in Safari, I'm overriding e and not e.stack here.
- /* jshint -W022 */
+ // eslint-disable-next-line no-ex-assign
e = e.message + '\n' + e.stack;
}
- throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}",
+ throw $injectorMinErr('modulerr', 'Failed to instantiate module {0} due to:\n{1}',
module, e.stack || e.message || e);
}
});
@@ -4604,7 +4941,8 @@ function createInjector(modulesToLoad, strictDi) {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
- return cache[serviceName] = factory(serviceName, caller);
+ cache[serviceName] = factory(serviceName, caller);
+ return cache[serviceName];
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
@@ -4616,48 +4954,71 @@ function createInjector(modulesToLoad, strictDi) {
}
}
- function invoke(fn, self, locals, serviceName) {
- if (typeof locals === 'string') {
- serviceName = locals;
- locals = null;
- }
+ function injectionArgs(fn, locals, serviceName) {
var args = [],
- $inject = createInjector.$$annotate(fn, strictDi, serviceName),
- length, i,
- key;
+ $inject = createInjector.$$annotate(fn, strictDi, serviceName);
- for (i = 0, length = $inject.length; i < length; i++) {
- key = $inject[i];
+ for (var i = 0, length = $inject.length; i < length; i++) {
+ var key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
- args.push(
- locals && locals.hasOwnProperty(key)
- ? locals[key]
- : getService(key, serviceName)
- );
+ args.push(locals && locals.hasOwnProperty(key) ? locals[key] :
+ getService(key, serviceName));
+ }
+ return args;
+ }
+
+ function isClass(func) {
+ // Support: IE 9-11 only
+ // IE 9-11 do not support classes and IE9 leaks with the code below.
+ if (msie || typeof func !== 'function') {
+ return false;
+ }
+ var result = func.$$ngIsClass;
+ if (!isBoolean(result)) {
+ // Support: Edge 12-13 only
+ // See: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/6156135/
+ result = func.$$ngIsClass = /^(?:class\b|constructor\()/.test(stringifyFn(func));
+ }
+ return result;
+ }
+
+ function invoke(fn, self, locals, serviceName) {
+ if (typeof locals === 'string') {
+ serviceName = locals;
+ locals = null;
}
+
+ var args = injectionArgs(fn, locals, serviceName);
if (isArray(fn)) {
- fn = fn[length];
+ fn = fn[fn.length - 1];
}
- // http://jsperf.com/angularjs-invoke-apply-vs-switch
- // #5388
- return fn.apply(self, args);
+ if (!isClass(fn)) {
+ // http://jsperf.com/angularjs-invoke-apply-vs-switch
+ // #5388
+ return fn.apply(self, args);
+ } else {
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(fn, args))();
+ }
}
+
function instantiate(Type, locals, serviceName) {
// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
- // Object creation: http://jsperf.com/create-constructor/2
- var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
- var returnedValue = invoke(Type, instance, locals, serviceName);
-
- return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
+ var ctor = (isArray(Type) ? Type[Type.length - 1] : Type);
+ var args = injectionArgs(Type, locals, serviceName);
+ // Empty object at position 0 is ignored for invocation with `new`, but required.
+ args.unshift(null);
+ return new (Function.prototype.bind.apply(ctor, args))();
}
+
return {
invoke: invoke,
instantiate: instantiate,
@@ -4675,6 +5036,7 @@ createInjector.$$annotate = annotate;
/**
* @ngdoc provider
* @name $anchorScrollProvider
+ * @this
*
* @description
* Use `$anchorScrollProvider` to disable automatic scrolling whenever
@@ -4713,7 +5075,7 @@ function $AnchorScrollProvider() {
* When called, it scrolls to the element related to the specified `hash` or (if omitted) to the
* current value of {@link ng.$location#hash $location.hash()}, according to the rules specified
* in the
- * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#the-indicated-part-of-the-document).
+ * [HTML5 spec](http://www.w3.org/html/wg/drafts/html/master/browsers.html#an-indicated-part-of-the-document).
*
* It also watches the {@link ng.$location#hash $location.hash()} and automatically scrolls to
* match any anchor whenever it changes. This can be disabled by calling
@@ -4746,7 +5108,7 @@ function $AnchorScrollProvider() {
* </div>
*
* @example
- <example module="anchorScrollExample">
+ <example module="anchorScrollExample" name="anchor-scroll">
<file name="index.html">
<div id="scrollArea" ng-controller="ScrollController">
<a ng-click="gotoBottom()">Go to bottom</a>
@@ -4756,7 +5118,7 @@ function $AnchorScrollProvider() {
<file name="script.js">
angular.module('anchorScrollExample', [])
.controller('ScrollController', ['$scope', '$location', '$anchorScroll',
- function ($scope, $location, $anchorScroll) {
+ function($scope, $location, $anchorScroll) {
$scope.gotoBottom = function() {
// set the location.hash to the id of
// the element you wish to scroll to.
@@ -4785,7 +5147,7 @@ function $AnchorScrollProvider() {
* See {@link ng.$anchorScroll#yOffset $anchorScroll.yOffset} for more details.
*
* @example
- <example module="anchorScrollOffsetExample">
+ <example module="anchorScrollOffsetExample" name="anchor-scroll-offset">
<file name="index.html">
<div class="fixed-header" ng-controller="headerCtrl">
<a href="" ng-click="gotoAnchor(x)" ng-repeat="x in [1,2,3,4,5]">
@@ -4802,7 +5164,7 @@ function $AnchorScrollProvider() {
$anchorScroll.yOffset = 50; // always scroll by 50 extra pixels
}])
.controller('headerCtrl', ['$anchorScroll', '$location', '$scope',
- function ($anchorScroll, $location, $scope) {
+ function($anchorScroll, $location, $scope) {
$scope.gotoAnchor = function(x) {
var newHash = 'anchor' + x;
if ($location.hash() !== newHash) {
@@ -4909,7 +5271,8 @@ function $AnchorScrollProvider() {
}
function scroll(hash) {
- hash = isString(hash) ? hash : $location.hash();
+ // Allow numeric hashes
+ hash = isString(hash) ? hash : isNumber(hash) ? hash.toString() : $location.hash();
var elm;
// empty hash, scroll to the top of the page
@@ -4921,7 +5284,7 @@ function $AnchorScrollProvider() {
// first anchor with given name :-D
else if ((elm = getFirstAnchor(document.getElementsByName(hash)))) scrollTo(elm);
- // no element and hash == 'top', scroll to the top of the page
+ // no element and hash === 'top', scroll to the top of the page
else if (hash === 'top') scrollTo(null);
}
@@ -4996,33 +5359,14 @@ function prepareAnimateOptions(options) {
: {};
}
-var $$CoreAnimateRunnerProvider = function() {
- this.$get = ['$q', '$$rAF', function($q, $$rAF) {
- function AnimateRunner() {}
- AnimateRunner.all = noop;
- AnimateRunner.chain = noop;
- AnimateRunner.prototype = {
- end: noop,
- cancel: noop,
- resume: noop,
- pause: noop,
- complete: noop,
- then: function(pass, fail) {
- return $q(function(resolve) {
- $$rAF(function() {
- resolve();
- });
- }).then(pass, fail);
- }
- };
- return AnimateRunner;
- }];
+var $$CoreAnimateJsProvider = /** @this */ function() {
+ this.$get = noop;
};
// this is prefixed with Core since it conflicts with
// the animateQueueProvider defined in ngAnimate/animateQueue.js
-var $$CoreAnimateQueueProvider = function() {
- var postDigestQueue = new HashMap();
+var $$CoreAnimateQueueProvider = /** @this */ function() {
+ var postDigestQueue = new NgMap();
var postDigestElements = [];
this.$get = ['$$AnimateRunner', '$rootScope',
@@ -5034,17 +5378,28 @@ var $$CoreAnimateQueueProvider = function() {
pin: noop,
push: function(element, event, options, domOperation) {
- domOperation && domOperation();
+ if (domOperation) {
+ domOperation();
+ }
options = options || {};
- options.from && element.css(options.from);
- options.to && element.css(options.to);
+ if (options.from) {
+ element.css(options.from);
+ }
+ if (options.to) {
+ element.css(options.to);
+ }
if (options.addClass || options.removeClass) {
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- return new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner();
+
+ // since there are no animations to run the runner needs to be
+ // notified that the animation call is complete.
+ runner.complete();
+ return runner;
}
};
@@ -5083,10 +5438,14 @@ var $$CoreAnimateQueueProvider = function() {
});
forEach(element, function(elm) {
- toAdd && jqLiteAddClass(elm, toAdd);
- toRemove && jqLiteRemoveClass(elm, toRemove);
+ if (toAdd) {
+ jqLiteAddClass(elm, toAdd);
+ }
+ if (toRemove) {
+ jqLiteRemoveClass(elm, toRemove);
+ }
});
- postDigestQueue.remove(element);
+ postDigestQueue.delete(element);
}
});
postDigestElements.length = 0;
@@ -5101,7 +5460,7 @@ var $$CoreAnimateQueueProvider = function() {
if (classesAdded || classesRemoved) {
- postDigestQueue.put(element, data);
+ postDigestQueue.set(element, data);
postDigestElements.push(element);
if (postDigestElements.length === 1) {
@@ -5124,8 +5483,9 @@ var $$CoreAnimateQueueProvider = function() {
*
* To see the functional implementation check out `src/ngAnimate/animate.js`.
*/
-var $AnimateProvider = ['$provide', function($provide) {
+var $AnimateProvider = ['$provide', /** @this */ function($provide) {
var provider = this;
+ var classNameFilter = null;
this.$$registeredAnimations = Object.create(null);
@@ -5170,7 +5530,7 @@ var $AnimateProvider = ['$provide', function($provide) {
*/
this.register = function(name, factory) {
if (name && name.charAt(0) !== '.') {
- throw $animateMinErr('notcsel', "Expecting class selector starting with '.' got '{0}'.", name);
+ throw $animateMinErr('notcsel', 'Expecting class selector starting with \'.\' got \'{0}\'.', name);
}
var key = name + '-animation';
@@ -5194,16 +5554,16 @@ var $AnimateProvider = ['$provide', function($provide) {
*/
this.classNameFilter = function(expression) {
if (arguments.length === 1) {
- this.$$classNameFilter = (expression instanceof RegExp) ? expression : null;
- if (this.$$classNameFilter) {
- var reservedRegex = new RegExp("(\\s+|\\/)" + NG_ANIMATE_CLASSNAME + "(\\s+|\\/)");
- if (reservedRegex.test(this.$$classNameFilter.toString())) {
- throw $animateMinErr('nongcls','$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
-
+ classNameFilter = (expression instanceof RegExp) ? expression : null;
+ if (classNameFilter) {
+ var reservedRegex = new RegExp('[(\\s|\\/)]' + NG_ANIMATE_CLASSNAME + '[(\\s|\\/)]');
+ if (reservedRegex.test(classNameFilter.toString())) {
+ classNameFilter = null;
+ throw $animateMinErr('nongcls', '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "{0}" CSS class.', NG_ANIMATE_CLASSNAME);
}
}
}
- return this.$$classNameFilter;
+ return classNameFilter;
};
this.$get = ['$$animateQueue', function($$animateQueue) {
@@ -5217,7 +5577,11 @@ var $AnimateProvider = ['$provide', function($provide) {
afterElement = null;
}
}
- afterElement ? afterElement.after(element) : parentElement.prepend(element);
+ if (afterElement) {
+ afterElement.after(element);
+ } else {
+ parentElement.prepend(element);
+ }
}
/**
@@ -5283,15 +5647,20 @@ var $AnimateProvider = ['$provide', function($provide) {
* // remove all the animation event listeners listening for `enter`
* $animate.off('enter');
*
+ * // remove listeners for all animation events from the container element
+ * $animate.off(container);
+ *
* // remove all the animation event listeners listening for `enter` on the given element and its children
* $animate.off('enter', container);
*
- * // remove the event listener function provided by `listenerFn` that is set
- * // to listen for `enter` on the given `element` as well as its children
+ * // remove the event listener function provided by `callback` that is set
+ * // to listen for `enter` on the given `container` as well as its children
* $animate.off('enter', container, callback);
* ```
*
- * @param {string} event the animation event (e.g. enter, leave, move, addClass, removeClass, etc...)
+ * @param {string|DOMElement} event|container the animation event (e.g. enter, leave, move,
+ * addClass, removeClass, etc...), or the container element. If it is the element, all other
+ * arguments are ignored.
* @param {DOMElement=} container the container element the event listener was placed on
* @param {Function=} callback the callback function that was registered as the listener
*/
@@ -5355,7 +5724,9 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {Promise} animationPromise The animation promise that is returned when an animation is started.
*/
cancel: function(runner) {
- runner.end && runner.end();
+ if (runner.end) {
+ runner.end();
+ }
},
/**
@@ -5372,7 +5743,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} parent the parent element which will append the element as
* a child (so long as the after element is not present)
* @param {DOMElement=} after the sibling element after which the element will be appended
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5398,7 +5775,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} parent the parent element which will append the element as
* a child (so long as the after element is not present)
* @param {DOMElement=} after the sibling element after which the element will be appended
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5419,7 +5802,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* digest once the animation has completed.
*
* @param {DOMElement} element the element which will be removed from the DOM
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5443,7 +5832,13 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} className the CSS class(es) that will be added (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5467,7 +5862,13 @@ var $AnimateProvider = ['$provide', function($provide) {
*
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} className the CSS class(es) that will be removed (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5492,7 +5893,13 @@ var $AnimateProvider = ['$provide', function($provide) {
* @param {DOMElement} element the element which the CSS classes will be applied to
* @param {string} add the CSS class(es) that will be added (multiple classes are separated via spaces)
* @param {string} remove the CSS class(es) that will be removed (multiple classes are separated via spaces)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5509,18 +5916,37 @@ var $AnimateProvider = ['$provide', function($provide) {
* @kind function
*
* @description Performs an inline animation on the element which applies the provided to and from CSS styles to the element.
- * If any detected CSS transition, keyframe or JavaScript matches the provided className value then the animation will take
- * on the provided styles. For example, if a transition animation is set for the given className then the provided from and
- * to styles will be applied alongside the given transition. If a JavaScript animation is detected then the provided styles
- * will be given in as function paramters into the `animate` method (or as apart of the `options` parameter).
+ * If any detected CSS transition, keyframe or JavaScript matches the provided className value, then the animation will take
+ * on the provided styles. For example, if a transition animation is set for the given className, then the provided `from` and
+ * `to` styles will be applied alongside the given transition. If the CSS style provided in `from` does not have a corresponding
+ * style in `to`, the style in `from` is applied immediately, and no animation is run.
+ * If a JavaScript animation is detected then the provided styles will be given in as function parameters into the `animate`
+ * method (or as part of the `options` parameter):
+ *
+ * ```js
+ * ngModule.animation('.my-inline-animation', function() {
+ * return {
+ * animate : function(element, from, to, done, options) {
+ * //animation
+ * done();
+ * }
+ * }
+ * });
+ * ```
*
* @param {DOMElement} element the element which the CSS styles will be applied to
* @param {object} from the from (starting) CSS styles that will be applied to the element and across the animation.
* @param {object} to the to (destination) CSS styles that will be applied to the element and across the animation.
* @param {string=} className an optional CSS class that will be applied to the element for the duration of the animation. If
* this value is left as empty then a CSS class of `ng-inline-animate` will be applied to the element.
- * (Note that if no animation is detected then this value will not be appplied to the element.)
- * @param {object=} options an optional collection of options/styles that will be applied to the element
+ * (Note that if no animation is detected then this value will not be applied to the element.)
+ * @param {object=} options an optional collection of options/styles that will be applied to the element.
+ * The object can have the following properties:
+ *
+ * - **addClass** - `{string}` - space-separated CSS classes to add to element
+ * - **from** - `{Object}` - CSS properties & values at the beginning of animation. Must have matching `to`
+ * - **removeClass** - `{string}` - space-separated CSS classes to remove from element
+ * - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
* @return {Promise} the animation callback promise
*/
@@ -5537,10 +5963,201 @@ var $AnimateProvider = ['$provide', function($provide) {
}];
}];
+var $$AnimateAsyncRunFactoryProvider = /** @this */ function() {
+ this.$get = ['$$rAF', function($$rAF) {
+ var waitQueue = [];
+
+ function waitForTick(fn) {
+ waitQueue.push(fn);
+ if (waitQueue.length > 1) return;
+ $$rAF(function() {
+ for (var i = 0; i < waitQueue.length; i++) {
+ waitQueue[i]();
+ }
+ waitQueue = [];
+ });
+ }
+
+ return function() {
+ var passed = false;
+ waitForTick(function() {
+ passed = true;
+ });
+ return function(callback) {
+ if (passed) {
+ callback();
+ } else {
+ waitForTick(callback);
+ }
+ };
+ };
+ }];
+};
+
+var $$AnimateRunnerFactoryProvider = /** @this */ function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout',
+ function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) {
+
+ var INITIAL_STATE = 0;
+ var DONE_PENDING_STATE = 1;
+ var DONE_COMPLETE_STATE = 2;
+
+ AnimateRunner.chain = function(chain, callback) {
+ var index = 0;
+
+ next();
+ function next() {
+ if (index === chain.length) {
+ callback(true);
+ return;
+ }
+
+ chain[index](function(response) {
+ if (response === false) {
+ callback(false);
+ return;
+ }
+ index++;
+ next();
+ });
+ }
+ };
+
+ AnimateRunner.all = function(runners, callback) {
+ var count = 0;
+ var status = true;
+ forEach(runners, function(runner) {
+ runner.done(onProgress);
+ });
+
+ function onProgress(response) {
+ status = status && response;
+ if (++count === runners.length) {
+ callback(status);
+ }
+ }
+ };
+
+ function AnimateRunner(host) {
+ this.setHost(host);
+
+ var rafTick = $$animateAsyncRun();
+ var timeoutTick = function(fn) {
+ $timeout(fn, 0, false);
+ };
+
+ this._doneCallbacks = [];
+ this._tick = function(fn) {
+ if ($$isDocumentHidden()) {
+ timeoutTick(fn);
+ } else {
+ rafTick(fn);
+ }
+ };
+ this._state = 0;
+ }
+
+ AnimateRunner.prototype = {
+ setHost: function(host) {
+ this.host = host || {};
+ },
+
+ done: function(fn) {
+ if (this._state === DONE_COMPLETE_STATE) {
+ fn();
+ } else {
+ this._doneCallbacks.push(fn);
+ }
+ },
+
+ progress: noop,
+
+ getPromise: function() {
+ if (!this.promise) {
+ var self = this;
+ this.promise = $q(function(resolve, reject) {
+ self.done(function(status) {
+ if (status === false) {
+ reject();
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+ return this.promise;
+ },
+
+ then: function(resolveHandler, rejectHandler) {
+ return this.getPromise().then(resolveHandler, rejectHandler);
+ },
+
+ 'catch': function(handler) {
+ return this.getPromise()['catch'](handler);
+ },
+
+ 'finally': function(handler) {
+ return this.getPromise()['finally'](handler);
+ },
+
+ pause: function() {
+ if (this.host.pause) {
+ this.host.pause();
+ }
+ },
+
+ resume: function() {
+ if (this.host.resume) {
+ this.host.resume();
+ }
+ },
+
+ end: function() {
+ if (this.host.end) {
+ this.host.end();
+ }
+ this._resolve(true);
+ },
+
+ cancel: function() {
+ if (this.host.cancel) {
+ this.host.cancel();
+ }
+ this._resolve(false);
+ },
+
+ complete: function(response) {
+ var self = this;
+ if (self._state === INITIAL_STATE) {
+ self._state = DONE_PENDING_STATE;
+ self._tick(function() {
+ self._resolve(response);
+ });
+ }
+ },
+
+ _resolve: function(response) {
+ if (this._state !== DONE_COMPLETE_STATE) {
+ forEach(this._doneCallbacks, function(fn) {
+ fn(response);
+ });
+ this._doneCallbacks.length = 0;
+ this._state = DONE_COMPLETE_STATE;
+ }
+ }
+ };
+
+ return AnimateRunner;
+ }];
+};
+
+/* exported $CoreAnimateCssProvider */
+
/**
* @ngdoc service
* @name $animateCss
* @kind object
+ * @this
*
* @description
* This is the core version of `$animateCss`. By default, only when the `ngAnimate` is included,
@@ -5549,37 +6166,18 @@ var $AnimateProvider = ['$provide', function($provide) {
* Click here {@link ngAnimate.$animateCss to read the documentation for $animateCss}.
*/
var $CoreAnimateCssProvider = function() {
- this.$get = ['$$rAF', '$q', function($$rAF, $q) {
+ this.$get = ['$$rAF', '$q', '$$AnimateRunner', function($$rAF, $q, $$AnimateRunner) {
- var RAFPromise = function() {};
- RAFPromise.prototype = {
- done: function(cancel) {
- this.defer && this.defer[cancel === true ? 'reject' : 'resolve']();
- },
- end: function() {
- this.done();
- },
- cancel: function() {
- this.done(true);
- },
- getPromise: function() {
- if (!this.defer) {
- this.defer = $q.defer();
- }
- return this.defer.promise;
- },
- then: function(f1,f2) {
- return this.getPromise().then(f1,f2);
- },
- 'catch': function(f1) {
- return this.getPromise()['catch'](f1);
- },
- 'finally': function(f1) {
- return this.getPromise()['finally'](f1);
+ return function(element, initialOptions) {
+ // all of the animation functions should create
+ // a copy of the options data, however, if a
+ // parent service has already created a copy then
+ // we should stick to using that
+ var options = initialOptions || {};
+ if (!options.$$prepared) {
+ options = copy(options);
}
- };
- return function(element, options) {
// there is no point in applying the styles since
// there is no animation that goes on at all in
// this version of $animateCss.
@@ -5592,7 +6190,7 @@ var $CoreAnimateCssProvider = function() {
options.from = null;
}
- var closed, runner = new RAFPromise();
+ var closed, runner = new $$AnimateRunner();
return {
start: run,
end: run
@@ -5600,16 +6198,16 @@ var $CoreAnimateCssProvider = function() {
function run() {
$$rAF(function() {
- close();
+ applyAnimationContents();
if (!closed) {
- runner.done();
+ runner.complete();
}
closed = true;
});
return runner;
}
- function close() {
+ function applyAnimationContents() {
if (options.addClass) {
element.addClass(options.addClass);
options.addClass = null;
@@ -5652,7 +6250,6 @@ var $CoreAnimateCssProvider = function() {
*/
function Browser(window, document, $log, $sniffer) {
var self = this,
- rawDocument = document[0],
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
@@ -5715,10 +6312,16 @@ function Browser(window, document, $log, $sniffer) {
var cachedState, lastHistoryState,
lastBrowserUrl = location.href,
baseElement = document.find('base'),
- pendingLocation = null;
+ pendingLocation = null,
+ getCurrentState = !$sniffer.history ? noop : function getCurrentState() {
+ try {
+ return history.state;
+ } catch (e) {
+ // MSIE can reportedly throw when there is no state (UNCONFIRMED).
+ }
+ };
cacheState();
- lastHistoryState = cachedState;
/**
* @name $browser#url
@@ -5772,10 +6375,8 @@ function Browser(window, document, $log, $sniffer) {
if ($sniffer.history && (!sameBase || !sameState)) {
history[replace ? 'replaceState' : 'pushState'](state, '', url);
cacheState();
- // Do the assignment again so that those two variables are referentially identical.
- lastHistoryState = cachedState;
} else {
- if (!sameBase || pendingLocation) {
+ if (!sameBase) {
pendingLocation = url;
}
if (replace) {
@@ -5789,6 +6390,9 @@ function Browser(window, document, $log, $sniffer) {
pendingLocation = url;
}
}
+ if (pendingLocation) {
+ pendingLocation = url;
+ }
return self;
// getter
} else {
@@ -5796,7 +6400,7 @@ function Browser(window, document, $log, $sniffer) {
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
// https://openradar.appspot.com/22186109).
// - the replacement is a workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=407172
- return pendingLocation || location.href.replace(/%27/g,"'");
+ return pendingLocation || location.href.replace(/%27/g,'\'');
}
};
@@ -5819,16 +6423,7 @@ function Browser(window, document, $log, $sniffer) {
function cacheStateAndFireUrlChange() {
pendingLocation = null;
- cacheState();
- fireUrlChange();
- }
-
- function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
+ fireStateOrUrlChange();
}
// This variable should be used *only* inside the cacheState function.
@@ -5842,11 +6437,16 @@ function Browser(window, document, $log, $sniffer) {
if (equals(cachedState, lastCachedState)) {
cachedState = lastCachedState;
}
+
lastCachedState = cachedState;
+ lastHistoryState = cachedState;
}
- function fireUrlChange() {
- if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
+ function fireStateOrUrlChange() {
+ var prevLastHistoryState = lastHistoryState;
+ cacheState();
+
+ if (lastBrowserUrl === self.url() && prevLastHistoryState === cachedState) {
return;
}
@@ -5881,8 +6481,8 @@ function Browser(window, document, $log, $sniffer) {
self.onUrlChange = function(callback) {
// TODO(vojta): refactor to use node's syntax for events
if (!urlChangeInit) {
- // We listen on both (hashchange/popstate) when available, as some browsers (e.g. Opera)
- // don't fire popstate when user change the address bar and don't fire hashchange when url
+ // We listen on both (hashchange/popstate) when available, as some browsers don't
+ // fire popstate when user changes the address bar and don't fire hashchange when url
// changed by push/replaceState
// html5 history api - popstate event
@@ -5912,7 +6512,7 @@ function Browser(window, document, $log, $sniffer) {
* Needs to be exported to be able to check for changes that have been done in sync,
* as hashchange/popstate events fire in async.
*/
- self.$$checkUrlChange = fireUrlChange;
+ self.$$checkUrlChange = fireStateOrUrlChange;
//////////////////////////////////////////////////////////////
// Misc API
@@ -5929,7 +6529,7 @@ function Browser(window, document, $log, $sniffer) {
*/
self.baseHref = function() {
var href = baseElement.attr('href');
- return href ? href.replace(/^(https?\:)?\/\/[^\/]*/, '') : '';
+ return href ? href.replace(/^(https?:)?\/\/[^/]*/, '') : '';
};
/**
@@ -5980,6 +6580,7 @@ function Browser(window, document, $log, $sniffer) {
}
+/** @this */
function $BrowserProvider() {
this.$get = ['$window', '$log', '$sniffer', '$document',
function($window, $log, $sniffer, $document) {
@@ -5990,6 +6591,7 @@ function $BrowserProvider() {
/**
* @ngdoc service
* @name $cacheFactory
+ * @this
*
* @description
* Factory that constructs {@link $cacheFactory.Cache Cache} objects and gives access to
@@ -6026,7 +6628,7 @@ function $BrowserProvider() {
* - `{void}` `destroy()` — Removes references to this cache from $cacheFactory.
*
* @example
- <example module="cacheExampleApp">
+ <example module="cacheExampleApp" name="cache-factory">
<file name="index.html">
<div ng-controller="CacheController">
<input ng-model="newCacheKey" placeholder="Key">
@@ -6075,7 +6677,7 @@ function $CacheFactoryProvider() {
function cacheFactory(cacheId, options) {
if (cacheId in caches) {
- throw minErr('$cacheFactory')('iid', "CacheId '{0}' is already taken!", cacheId);
+ throw minErr('$cacheFactory')('iid', 'CacheId \'{0}\' is already taken!', cacheId);
}
var size = 0,
@@ -6125,7 +6727,7 @@ function $CacheFactoryProvider() {
* }));
* ```
*/
- return caches[cacheId] = {
+ return (caches[cacheId] = {
/**
* @ngdoc method
@@ -6203,8 +6805,8 @@ function $CacheFactoryProvider() {
if (!lruEntry) return;
- if (lruEntry == freshEnd) freshEnd = lruEntry.p;
- if (lruEntry == staleEnd) staleEnd = lruEntry.n;
+ if (lruEntry === freshEnd) freshEnd = lruEntry.p;
+ if (lruEntry === staleEnd) staleEnd = lruEntry.n;
link(lruEntry.n,lruEntry.p);
delete lruHash[key];
@@ -6269,17 +6871,17 @@ function $CacheFactoryProvider() {
info: function() {
return extend({}, stats, {size: size});
}
- };
+ });
/**
* makes the `entry` the freshEnd of the LRU linked list
*/
function refresh(entry) {
- if (entry != freshEnd) {
+ if (entry !== freshEnd) {
if (!staleEnd) {
staleEnd = entry;
- } else if (staleEnd == entry) {
+ } else if (staleEnd === entry) {
staleEnd = entry.n;
}
@@ -6295,7 +6897,7 @@ function $CacheFactoryProvider() {
* bidirectionally links two entries of the LRU linked list
*/
function link(nextEntry, prevEntry) {
- if (nextEntry != prevEntry) {
+ if (nextEntry !== prevEntry) {
if (nextEntry) nextEntry.p = prevEntry; //p stands for previous, 'prev' didn't minify
if (prevEntry) prevEntry.n = nextEntry; //n stands for next, 'next' didn't minify
}
@@ -6343,6 +6945,7 @@ function $CacheFactoryProvider() {
/**
* @ngdoc service
* @name $templateCache
+ * @this
*
* @description
* The first time a template is used, it is loaded in the template cache for quick retrieval. You
@@ -6370,12 +6973,14 @@ function $CacheFactoryProvider() {
* });
* ```
*
- * To retrieve the template later, simply use it in your HTML:
- * ```html
- * <div ng-include=" 'templateId.html' "></div>
+ * To retrieve the template later, simply use it in your component:
+ * ```js
+ * myApp.component('myComponent', {
+ * templateUrl: 'templateId.html'
+ * });
* ```
*
- * or get it via Javascript:
+ * or get it via the `$templateCache` service:
* ```js
* $templateCache.get('templateId.html')
* ```
@@ -6397,7 +7002,7 @@ function $TemplateCacheProvider() {
* *
* Does the change somehow allow for arbitrary javascript to be executed? *
* Or allows for someone to change the prototype of built-in objects? *
- * Or gives undesired access to variables likes document or window? *
+ * Or gives undesired access to variables like document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* ! VARIABLE/FUNCTION NAMING CONVENTIONS THAT APPLY TO THIS FILE!
@@ -6441,8 +7046,9 @@ function $TemplateCacheProvider() {
* There are many different options for a directive.
*
* The difference resides in the return value of the factory function.
- * You can either return a "Directive Definition Object" (see below) that defines the directive properties,
- * or just the `postLink` function (all other properties will have the default values).
+ * You can either return a {@link $compile#directive-definition-object Directive Definition Object (see below)}
+ * that defines the directive properties, or just the `postLink` function (all other properties will have
+ * the default values).
*
* <div class="alert alert-success">
* **Best Practice:** It's recommended to use the "directive definition object" form.
@@ -6455,33 +7061,34 @@ function $TemplateCacheProvider() {
*
* myModule.directive('directiveName', function factory(injectables) {
* var directiveDefinitionObject = {
- * priority: 0,
- * template: '<div></div>', // or // function(tElement, tAttrs) { ... },
+ * {@link $compile#-priority- priority}: 0,
+ * {@link $compile#-template- template}: '<div></div>', // or // function(tElement, tAttrs) { ... },
* // or
- * // templateUrl: 'directive.html', // or // function(tElement, tAttrs) { ... },
- * transclude: false,
- * restrict: 'A',
- * templateNamespace: 'html',
- * scope: false,
- * controller: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
- * controllerAs: 'stringIdentifier',
- * bindToController: false,
- * require: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
- * compile: function compile(tElement, tAttrs, transclude) {
+ * // {@link $compile#-templateurl- templateUrl}: 'directive.html', // or // function(tElement, tAttrs) { ... },
+ * {@link $compile#-transclude- transclude}: false,
+ * {@link $compile#-restrict- restrict}: 'A',
+ * {@link $compile#-templatenamespace- templateNamespace}: 'html',
+ * {@link $compile#-scope- scope}: false,
+ * {@link $compile#-controller- controller}: function($scope, $element, $attrs, $transclude, otherInjectables) { ... },
+ * {@link $compile#-controlleras- controllerAs}: 'stringIdentifier',
+ * {@link $compile#-bindtocontroller- bindToController}: false,
+ * {@link $compile#-require- require}: 'siblingDirectiveName', // or // ['^parentDirectiveName', '?optionalDirectiveName', '?^optionalParent'],
+ * {@link $compile#-multielement- multiElement}: false,
+ * {@link $compile#-compile- compile}: function compile(tElement, tAttrs, transclude) {
* return {
- * pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * post: function postLink(scope, iElement, iAttrs, controller) { ... }
+ * {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... },
+ * {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... }
* }
* // or
* // return function postLink( ... ) { ... }
* },
* // or
- * // link: {
- * // pre: function preLink(scope, iElement, iAttrs, controller) { ... },
- * // post: function postLink(scope, iElement, iAttrs, controller) { ... }
+ * // {@link $compile#-link- link}: {
+ * // {@link $compile#pre-linking-function pre}: function preLink(scope, iElement, iAttrs, controller) { ... },
+ * // {@link $compile#post-linking-function post}: function postLink(scope, iElement, iAttrs, controller) { ... }
* // }
* // or
- * // link: function postLink( ... ) { ... }
+ * // {@link $compile#-link- link}: function postLink( ... ) { ... }
* };
* return directiveDefinitionObject;
* });
@@ -6506,6 +7113,126 @@ function $TemplateCacheProvider() {
* });
* ```
*
+ * ### Life-cycle hooks
+ * Directive controllers can provide the following methods that are called by Angular at points in the life-cycle of the
+ * directive:
+ * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
+ * had their bindings initialized (and before the pre &amp; post linking functions for the directives on
+ * this element). This is a good place to put initialization code for your controller.
+ * * `$onChanges(changesObj)` - Called whenever one-way (`<`) or interpolation (`@`) bindings are updated. The
+ * `changesObj` is a hash whose keys are the names of the bound properties that have changed, and the values are an
+ * object of the form `{ currentValue, previousValue, isFirstChange() }`. Use this hook to trigger updates within a
+ * component such as cloning the bound value to prevent accidental mutation of the outer value. Note that this will
+ * also be called when your bindings are initialized.
+ * * `$doCheck()` - Called on each turn of the digest cycle. Provides an opportunity to detect and act on
+ * changes. Any actions that you wish to take in response to the changes that you detect must be
+ * invoked from this hook; implementing this has no effect on when `$onChanges` is called. For example, this hook
+ * could be useful if you wish to perform a deep equality check, or to check a Date object, changes to which would not
+ * be detected by Angular's change detector and thus not trigger `$onChanges`. This hook is invoked with no arguments;
+ * if detecting changes, you must store the previous value(s) for comparison to the current values.
+ * * `$onDestroy()` - Called on a controller when its containing scope is destroyed. Use this hook for releasing
+ * external resources, watches and event handlers. Note that components have their `$onDestroy()` hooks called in
+ * the same order as the `$scope.$broadcast` events are triggered, which is top down. This means that parent
+ * components will have their `$onDestroy()` hook called before child components.
+ * * `$postLink()` - Called after this controller's element and its children have been linked. Similar to the post-link
+ * function this hook can be used to set up DOM event handlers and do direct DOM manipulation.
+ * Note that child elements that contain `templateUrl` directives will not have been compiled and linked since
+ * they are waiting for their template to load asynchronously and their own compilation and linking has been
+ * suspended until that occurs.
+ *
+ * #### Comparison with Angular 2 life-cycle hooks
+ * Angular 2 also uses life-cycle hooks for its components. While the Angular 1 life-cycle hooks are similar there are
+ * some differences that you should be aware of, especially when it comes to moving your code from Angular 1 to Angular 2:
+ *
+ * * Angular 1 hooks are prefixed with `$`, such as `$onInit`. Angular 2 hooks are prefixed with `ng`, such as `ngOnInit`.
+ * * Angular 1 hooks can be defined on the controller prototype or added to the controller inside its constructor.
+ * In Angular 2 you can only define hooks on the prototype of the Component class.
+ * * Due to the differences in change-detection, you may get many more calls to `$doCheck` in Angular 1 than you would to
+ * `ngDoCheck` in Angular 2
+ * * Changes to the model inside `$doCheck` will trigger new turns of the digest loop, which will cause the changes to be
+ * propagated throughout the application.
+ * Angular 2 does not allow the `ngDoCheck` hook to trigger a change outside of the component. It will either throw an
+ * error or do nothing depending upon the state of `enableProdMode()`.
+ *
+ * #### Life-cycle hook examples
+ *
+ * This example shows how you can check for mutations to a Date object even though the identity of the object
+ * has not changed.
+ *
+ * <example name="doCheckDateExample" module="do-check-module">
+ * <file name="app.js">
+ * angular.module('do-check-module', [])
+ * .component('app', {
+ * template:
+ * 'Month: <input ng-model="$ctrl.month" ng-change="$ctrl.updateDate()">' +
+ * 'Date: {{ $ctrl.date }}' +
+ * '<test date="$ctrl.date"></test>',
+ * controller: function() {
+ * this.date = new Date();
+ * this.month = this.date.getMonth();
+ * this.updateDate = function() {
+ * this.date.setMonth(this.month);
+ * };
+ * }
+ * })
+ * .component('test', {
+ * bindings: { date: '<' },
+ * template:
+ * '<pre>{{ $ctrl.log | json }}</pre>',
+ * controller: function() {
+ * var previousValue;
+ * this.log = [];
+ * this.$doCheck = function() {
+ * var currentValue = this.date && this.date.valueOf();
+ * if (previousValue !== currentValue) {
+ * this.log.push('doCheck: date mutated: ' + this.date);
+ * previousValue = currentValue;
+ * }
+ * };
+ * }
+ * });
+ * </file>
+ * <file name="index.html">
+ * <app></app>
+ * </file>
+ * </example>
+ *
+ * This example show how you might use `$doCheck` to trigger changes in your component's inputs even if the
+ * actual identity of the component doesn't change. (Be aware that cloning and deep equality checks on large
+ * arrays or objects can have a negative impact on your application performance)
+ *
+ * <example name="doCheckArrayExample" module="do-check-module">
+ * <file name="index.html">
+ * <div ng-init="items = []">
+ * <button ng-click="items.push(items.length)">Add Item</button>
+ * <button ng-click="items = []">Reset Items</button>
+ * <pre>{{ items }}</pre>
+ * <test items="items"></test>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('do-check-module', [])
+ * .component('test', {
+ * bindings: { items: '<' },
+ * template:
+ * '<pre>{{ $ctrl.log | json }}</pre>',
+ * controller: function() {
+ * this.log = [];
+ *
+ * this.$doCheck = function() {
+ * if (this.items_ref !== this.items) {
+ * this.log.push('doCheck: items changed');
+ * this.items_ref = this.items;
+ * }
+ * if (!angular.equals(this.items_clone, this.items)) {
+ * this.log.push('doCheck: items mutated');
+ * this.items_clone = angular.copy(this.items);
+ * }
+ * };
+ * }
+ * });
+ * </file>
+ * </example>
*
*
* ### Directive Definition Object
@@ -6514,10 +7241,10 @@ function $TemplateCacheProvider() {
* compiler}. The attributes are:
*
* #### `multiElement`
- * When this property is set to true, the HTML compiler will collect DOM nodes between
+ * When this property is set to true (default is `false`), the HTML compiler will collect DOM nodes between
* nodes with the attributes `directive-name-start` and `directive-name-end`, and group them
* together as the directive elements. It is recommended that this feature be used on directives
- * which are not strictly behavioural (such as {@link ngClick}), and which
+ * which are not strictly behavioral (such as {@link ngClick}), and which
* do not manipulate or replace child nodes (such as {@link ngInclude}).
*
* #### `priority`
@@ -6535,19 +7262,21 @@ function $TemplateCacheProvider() {
* and other directives used in the directive's template will also be excluded from execution.
*
* #### `scope`
- * The scope property can be `true`, an object or a falsy value:
+ * The scope property can be `false`, `true`, or an object:
*
- * * **falsy:** No scope will be created for the directive. The directive will use its parent's scope.
+ * * **`false` (default):** No scope will be created for the directive. The directive will use its
+ * parent's scope.
*
* * **`true`:** A new child scope that prototypically inherits from its parent will be created for
* the directive's element. If multiple directives on the same element request a new scope,
- * only one new scope is created. The new scope rule does not apply for the root of the template
- * since the root of the template always gets a new scope.
+ * only one new scope is created.
*
- * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's element. The
- * 'isolate' scope differs from normal scope in that it does not prototypically inherit from its parent
- * scope. This is useful when creating reusable components, which should not accidentally read or modify
- * data in the parent scope.
+ * * **`{...}` (an object hash):** A new "isolate" scope is created for the directive's template.
+ * The 'isolate' scope differs from normal scope in that it does not prototypically
+ * inherit from its parent scope. This is useful when creating reusable components, which should not
+ * accidentally read or modify data in the parent scope. Note that an isolate scope
+ * directive without a `template` or `templateUrl` will not apply the isolate scope
+ * to its children elements.
*
* The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the
* directive's element. These local properties are useful for aliasing values for templates. The keys in
@@ -6555,35 +7284,62 @@ function $TemplateCacheProvider() {
* is bound to the parent scope, via matching attributes on the directive's element:
*
* * `@` or `@attr` - bind a local scope property to the value of DOM attribute. The result is
- * always a string since DOM attributes are strings. If no `attr` name is specified then the
- * attribute name is assumed to be the same as the local name.
- * Given `<widget my-attr="hello {{name}}">` and widget definition
- * of `scope: { localName:'@myAttr' }`, then widget scope property `localName` will reflect
- * the interpolated value of `hello {{name}}`. As the `name` attribute changes so will the
- * `localName` property on the widget scope. The `name` is read from the parent scope (not
- * component scope).
- *
- * * `=` or `=attr` - set up bi-directional binding between a local scope property and the
- * parent scope property of name defined via the value of the `attr` attribute. If no `attr`
- * name is specified then the attribute name is assumed to be the same as the local name.
- * Given `<widget my-attr="parentModel">` and widget definition of
- * `scope: { localModel:'=myAttr' }`, then widget scope property `localModel` will reflect the
+ * always a string since DOM attributes are strings. If no `attr` name is specified then the
+ * attribute name is assumed to be the same as the local name. Given `<my-component
+ * my-attr="hello {{name}}">` and the isolate scope definition `scope: { localName:'@myAttr' }`,
+ * the directive's scope property `localName` will reflect the interpolated value of `hello
+ * {{name}}`. As the `name` attribute changes so will the `localName` property on the directive's
+ * scope. The `name` is read from the parent scope (not the directive's scope).
+ *
+ * * `=` or `=attr` - set up a bidirectional binding between a local scope property and an expression
+ * passed via the attribute `attr`. The expression is evaluated in the context of the parent scope.
+ * If no `attr` name is specified then the attribute name is assumed to be the same as the local
+ * name. Given `<my-component my-attr="parentModel">` and the isolate scope definition `scope: {
+ * localModel: '=myAttr' }`, the property `localModel` on the directive's scope will reflect the
+ * value of `parentModel` on the parent scope. Changes to `parentModel` will be reflected in
+ * `localModel` and vice versa. Optional attributes should be marked as such with a question mark:
+ * `=?` or `=?attr`. If the binding expression is non-assignable, or if the attribute isn't
+ * optional and doesn't exist, an exception ({@link error/$compile/nonassign `$compile:nonassign`})
+ * will be thrown upon discovering changes to the local value, since it will be impossible to sync
+ * them back to the parent scope. By default, the {@link ng.$rootScope.Scope#$watch `$watch`}
+ * method is used for tracking changes, and the equality check is based on object identity.
+ * However, if an object literal or an array literal is passed as the binding expression, the
+ * equality check is done by value (using the {@link angular.equals} function). It's also possible
+ * to watch the evaluated value shallowly with {@link ng.$rootScope.Scope#$watchCollection
+ * `$watchCollection`}: use `=*` or `=*attr` (`=*?` or `=*?attr` if the attribute is optional).
+ *
+ * * `<` or `<attr` - set up a one-way (one-directional) binding between a local scope property and an
+ * expression passed via the attribute `attr`. The expression is evaluated in the context of the
+ * parent scope. If no `attr` name is specified then the attribute name is assumed to be the same as the
+ * local name. You can also make the binding optional by adding `?`: `<?` or `<?attr`.
+ *
+ * For example, given `<my-component my-attr="parentModel">` and directive definition of
+ * `scope: { localModel:'<myAttr' }`, then the isolated scope property `localModel` will reflect the
* value of `parentModel` on the parent scope. Any changes to `parentModel` will be reflected
- * in `localModel` and any changes in `localModel` will reflect in `parentModel`. If the parent
- * scope property doesn't exist, it will throw a NON_ASSIGNABLE_MODEL_EXPRESSION exception. You
- * can avoid this behavior using `=?` or `=?attr` in order to flag the property as optional. If
- * you want to shallow watch for changes (i.e. $watchCollection instead of $watch) you can use
- * `=*` or `=*attr` (`=*?` or `=*?attr` if the property is optional).
- *
- * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope.
- * If no `attr` name is specified then the attribute name is assumed to be the same as the
- * local name. Given `<widget my-attr="count = count + value">` and widget definition of
- * `scope: { localFn:'&myAttr' }`, then isolate scope property `localFn` will point to
- * a function wrapper for the `count = count + value` expression. Often it's desirable to
- * pass data from the isolated scope via an expression to the parent scope, this can be
- * done by passing a map of local variable names and values into the expression wrapper fn.
- * For example, if the expression is `increment(amount)` then we can specify the amount value
- * by calling the `localFn` as `localFn({amount: 22})`.
+ * in `localModel`, but changes in `localModel` will not reflect in `parentModel`. There are however
+ * two caveats:
+ * 1. one-way binding does not copy the value from the parent to the isolate scope, it simply
+ * sets the same value. That means if your bound value is an object, changes to its properties
+ * in the isolated scope will be reflected in the parent scope (because both reference the same object).
+ * 2. one-way binding watches changes to the **identity** of the parent value. That means the
+ * {@link ng.$rootScope.Scope#$watch `$watch`} on the parent value only fires if the reference
+ * to the value has changed. In most cases, this should not be of concern, but can be important
+ * to know if you one-way bind to an object, and then replace that object in the isolated scope.
+ * If you now change a property of the object in your parent scope, the change will not be
+ * propagated to the isolated scope, because the identity of the object on the parent scope
+ * has not changed. Instead you must assign a new object.
+ *
+ * One-way binding is useful if you do not plan to propagate changes to your isolated scope bindings
+ * back to the parent. However, it does not make this completely impossible.
+ *
+ * * `&` or `&attr` - provides a way to execute an expression in the context of the parent scope. If
+ * no `attr` name is specified then the attribute name is assumed to be the same as the local name.
+ * Given `<my-component my-attr="count = count + value">` and the isolate scope definition `scope: {
+ * localFn:'&myAttr' }`, the isolate scope property `localFn` will point to a function wrapper for
+ * the `count = count + value` expression. Often it's desirable to pass data from the isolated scope
+ * via an expression to the parent scope. This can be done by passing a map of local variable names
+ * and values into the expression wrapper fn. For example, if the expression is `increment(amount)`
+ * then we can specify the amount value by calling the `localFn` as `localFn({amount: 22})`.
*
* In general it's possible to apply more than one directive to one element, but there might be limitations
* depending on the type of scope required by the directives. The following points will help explain these limitations.
@@ -6601,9 +7357,30 @@ function $TemplateCacheProvider() {
*
*
* #### `bindToController`
- * When an isolate scope is used for a component (see above), and `controllerAs` is used, `bindToController: true` will
- * allow a component to have its properties bound to the controller, rather than to scope. When the controller
- * is instantiated, the initial values of the isolate scope bindings are already available.
+ * This property is used to bind scope properties directly to the controller. It can be either
+ * `true` or an object hash with the same format as the `scope` property.
+ *
+ * When an isolate scope is used for a directive (see above), `bindToController: true` will
+ * allow a component to have its properties bound to the controller, rather than to scope.
+ *
+ * After the controller is instantiated, the initial values of the isolate scope bindings will be bound to the controller
+ * properties. You can access these bindings once they have been initialized by providing a controller method called
+ * `$onInit`, which is called after all the controllers on an element have been constructed and had their bindings
+ * initialized.
+ *
+ * <div class="alert alert-warning">
+ * **Deprecation warning:** although bindings for non-ES6 class controllers are currently
+ * bound to `this` before the controller constructor is called, this use is now deprecated. Please place initialization
+ * code that relies upon bindings inside a `$onInit` method on the controller, instead.
+ * </div>
+ *
+ * It is also possible to set `bindToController` to an object hash with the same format as the `scope` property.
+ * This will set up the scope bindings to the controller directly. Note that `scope` can still be used
+ * to define which kind of scope is created. By default, no scope is created. Use `scope: {}` to create an isolate
+ * scope (useful for component directives).
+ *
+ * If both `bindToController` and `scope` are defined and have object hashes, `bindToController` overrides `scope`.
+ *
*
* #### `controller`
* Controller constructor function. The controller is instantiated before the
@@ -6615,25 +7392,41 @@ function $TemplateCacheProvider() {
* * `$element` - Current element
* * `$attrs` - Current attributes object for the element
* * `$transclude` - A transclude linking function pre-bound to the correct transclusion scope:
- * `function([scope], cloneLinkingFn, futureParentElement)`.
- * * `scope`: optional argument to override the scope.
- * * `cloneLinkingFn`: optional argument to create clones of the original transcluded content.
- * * `futureParentElement`:
+ * `function([scope], cloneLinkingFn, futureParentElement, slotName)`:
+ * * `scope`: (optional) override the scope.
+ * * `cloneLinkingFn`: (optional) argument to create clones of the original transcluded content.
+ * * `futureParentElement` (optional):
* * defines the parent to which the `cloneLinkingFn` will add the cloned elements.
* * default: `$element.parent()` resp. `$element` for `transclude:'element'` resp. `transclude:true`.
* * only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements)
- * and when the `cloneLinkinFn` is passed,
+ * and when the `cloneLinkingFn` is passed,
* as those elements need to created and cloned in a special way when they are defined outside their
* usual containers (e.g. like `<svg>`).
* * See also the `directive.templateNamespace` property.
- *
+ * * `slotName`: (optional) the name of the slot to transclude. If falsy (e.g. `null`, `undefined` or `''`)
+ * then the default transclusion is provided.
+ * The `$transclude` function also has a method on it, `$transclude.isSlotFilled(slotName)`, which returns
+ * `true` if the specified slot contains content (i.e. one or more DOM nodes).
*
* #### `require`
* Require another directive and inject its controller as the fourth argument to the linking function. The
- * `require` takes a string name (or array of strings) of the directive(s) to pass in. If an array is used, the
- * injected argument will be an array in corresponding order. If no such directive can be
- * found, or if the directive does not have a controller, then an error is raised (unless no link function
- * is specified, in which case error checking is skipped). The name can be prefixed with:
+ * `require` property can be a string, an array or an object:
+ * * a **string** containing the name of the directive to pass to the linking function
+ * * an **array** containing the names of directives to pass to the linking function. The argument passed to the
+ * linking function will be an array of controllers in the same order as the names in the `require` property
+ * * an **object** whose property values are the names of the directives to pass to the linking function. The argument
+ * passed to the linking function will also be an object with matching keys, whose values will hold the corresponding
+ * controllers.
+ *
+ * If the `require` property is an object and `bindToController` is truthy, then the required controllers are
+ * bound to the controller using the keys of the `require` property. This binding occurs after all the controllers
+ * have been constructed but before `$onInit` is called.
+ * If the name of the required controller is the same as the local name (the key), the name can be
+ * omitted. For example, `{parentDir: '^^'}` is equivalent to `{parentDir: '^^parentDir'}`.
+ * See the {@link $compileProvider#component} helper for an example of how this can be used.
+ * If no such required directive(s) can be found, or if the directive does not have a controller, then an error is
+ * raised (unless no link function is specified and the required controllers are not being bound to the directive
+ * controller, in which case error checking is skipped). The name can be prefixed with:
*
* * (no prefix) - Locate the required controller on the current element. Throw an error if not found.
* * `?` - Attempt to locate the required controller or pass `null` to the `link` fn if not found.
@@ -6726,14 +7519,6 @@ function $TemplateCacheProvider() {
* The contents are compiled and provided to the directive as a **transclusion function**. See the
* {@link $compile#transclusion Transclusion} section below.
*
- * There are two kinds of transclusion depending upon whether you want to transclude just the contents of the
- * directive's element or the entire element:
- *
- * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
- * * `'element'` - transclude the whole of the directive's element including any directives on this
- * element that defined at a lower priority than this directive. When used, the `template`
- * property is ignored.
- *
*
* #### `compile`
*
@@ -6761,7 +7546,7 @@ function $TemplateCacheProvider() {
* <div class="alert alert-warning">
* **Note:** The compile function cannot handle directives that recursively use themselves in their
- * own templates or compile functions. Compiling these directives results in an infinite loop and a
+ * own templates or compile functions. Compiling these directives results in an infinite loop and
* stack overflow errors.
*
* This can be avoided by manually using $compile in the postLink function to imperatively compile
@@ -6820,8 +7605,8 @@ function $TemplateCacheProvider() {
* any other controller.
*
* * `transcludeFn` - A transclude linking function pre-bound to the correct transclusion scope.
- * This is the same as the `$transclude`
- * parameter of directive controllers, see there for details.
+ * This is the same as the `$transclude` parameter of directive controllers,
+ * see {@link ng.$compile#-controller- the controller section for details}.
* `function([scope], cloneLinkingFn, futureParentElement)`.
*
* #### Pre-linking function
@@ -6863,6 +7648,34 @@ function $TemplateCacheProvider() {
* Testing Transclusion Directives}.
* </div>
*
+ * There are three kinds of transclusion depending upon whether you want to transclude just the contents of the
+ * directive's element, the entire element or multiple parts of the element contents:
+ *
+ * * `true` - transclude the content (i.e. the child nodes) of the directive's element.
+ * * `'element'` - transclude the whole of the directive's element including any directives on this
+ * element that defined at a lower priority than this directive. When used, the `template`
+ * property is ignored.
+ * * **`{...}` (an object hash):** - map elements of the content onto transclusion "slots" in the template.
+ *
+ * **Mult-slot transclusion** is declared by providing an object for the `transclude` property.
+ *
+ * This object is a map where the keys are the name of the slot to fill and the value is an element selector
+ * used to match the HTML to the slot. The element selector should be in normalized form (e.g. `myElement`)
+ * and will match the standard element variants (e.g. `my-element`, `my:element`, `data-my-element`, etc).
+ *
+ * For further information check out the guide on {@link guide/directive#matching-directives Matching Directives}
+ *
+ * If the element selector is prefixed with a `?` then that slot is optional.
+ *
+ * For example, the transclude object `{ slotA: '?myCustomElement' }` maps `<my-custom-element>` elements to
+ * the `slotA` slot, which can be accessed via the `$transclude` function or via the {@link ngTransclude} directive.
+ *
+ * Slots that are not marked as optional (`?`) will trigger a compile time error if there are no matching elements
+ * in the transclude content. If you wish to know if an optional slot was filled with content, then you can call
+ * `$transclude.isSlotFilled(slotName)` on the transclude function passed to the directive's link function and
+ * injectable into the directive's controller.
+ *
+ *
* #### Transclusion Functions
*
* When a directive requests transclusion, the compiler extracts its contents and provides a **transclusion
@@ -6880,10 +7693,10 @@ function $TemplateCacheProvider() {
*
* When you call a transclusion function you can pass in a **clone attach function**. This function accepts
* two parameters, `function(clone, scope) { ... }`, where the `clone` is a fresh compiled copy of your transcluded
- * content and the `scope` is the newly created transclusion scope, to which the clone is bound.
+ * content and the `scope` is the newly created transclusion scope, which the clone will be linked to.
*
* <div class="alert alert-info">
- * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a translude function
+ * **Best Practice**: Always provide a `cloneFn` (clone attach function) when you call a transclude function
* since you then get a fresh clone of the original DOM and also have access to the new transclusion scope.
* </div>
*
@@ -6915,7 +7728,7 @@ function $TemplateCacheProvider() {
* </div>
*
* The built-in DOM manipulation directives, such as {@link ngIf}, {@link ngSwitch} and {@link ngRepeat}
- * automatically destroy their transluded clones as necessary so you do not need to worry about this if
+ * automatically destroy their transcluded clones as necessary so you do not need to worry about this if
* you are simply using {@link ngTransclude} to inject the transclusion into your directive.
*
*
@@ -6940,19 +7753,19 @@ function $TemplateCacheProvider() {
*
* The `$parent` scope hierarchy will look like this:
*
- * ```
- * - $rootScope
- * - isolate
- * - transclusion
- * ```
+ ```
+ - $rootScope
+ - isolate
+ - transclusion
+ ```
*
* but the scopes will inherit prototypically from different scopes to their `$parent`.
*
- * ```
- * - $rootScope
- * - transclusion
- * - isolate
- * ```
+ ```
+ - $rootScope
+ - transclusion
+ - isolate
+ ```
*
*
* ### Attributes
@@ -6960,10 +7773,9 @@ function $TemplateCacheProvider() {
* The {@link ng.$compile.directive.Attributes Attributes} object - passed as a parameter in the
* `link()` or `compile()` functions. It has a variety of uses.
*
- * accessing *Normalized attribute names:*
- * Directives like 'ngBind' can be expressed in many ways: 'ng:bind', `data-ng-bind`, or 'x-ng-bind'.
- * the attributes object allows for normalized access to
- * the attributes.
+ * * *Accessing normalized attribute names:* Directives like 'ngBind' can be expressed in many ways:
+ * 'ng:bind', `data-ng-bind`, or 'x-ng-bind'. The attributes object allows for normalized access
+ * to the attributes.
*
* * *Directive inter-communication:* All directives share the same instance of the attributes
* object which allows the directives to use the attributes object as inter directive
@@ -6999,7 +7811,7 @@ function $TemplateCacheProvider() {
* to illustrate how `$compile` works.
* </div>
*
- <example module="compileExample">
+ <example module="compileExample" name="compile">
<file name="index.html">
<script>
angular.module('compileExample', [], function($compileProvider) {
@@ -7084,8 +7896,15 @@ function $TemplateCacheProvider() {
* directives; if given, it will be passed through to the link functions of
* directives found in `element` during compilation.
* * `transcludeControllers` - an object hash with keys that map controller names
- * to controller instances; if given, it will make the controllers
- * available to directives.
+ * to a hash with the key `instance`, which maps to the controller instance;
+ * if given, it will make the controllers available to directives on the compileNode:
+ * ```
+ * {
+ * parent: {
+ * instance: parentControllerInstance
+ * }
+ * }
+ * ```
* * `futureParentElement` - defines the parent to which the `cloneAttachFn` will add
* the cloned elements; only needed for transcludes that are allowed to contain non html
* elements (e.g. SVG elements). See also the directive.controller property.
@@ -7121,10 +7940,23 @@ function $TemplateCacheProvider() {
*
* For information on how the compiler works, see the
* {@link guide/compiler Angular HTML Compiler} section of the Developer Guide.
+ *
+ * @knownIssue
+ *
+ * ### Double Compilation
+ *
+ Double compilation occurs when an already compiled part of the DOM gets
+ compiled again. This is an undesired effect and can lead to misbehaving directives, performance issues,
+ and memory leaks. Refer to the Compiler Guide {@link guide/compiler#double-compilation-and-how-to-avoid-it
+ section on double compilation} for an in-depth explanation and ways to avoid it.
+ *
*/
var $compileMinErr = minErr('$compile');
+function UNINITIALIZED_VALUE() {}
+var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
+
/**
* @ngdoc provider
* @name $compileProvider
@@ -7132,11 +7964,12 @@ var $compileMinErr = minErr('$compile');
* @description
*/
$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider'];
+/** @this */
function $CompileProvider($provide, $$sanitizeUriProvider) {
var hasDirectives = {},
Suffix = 'Directive',
- COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\w\-]+)\s+(.*)$/,
- CLASS_DIRECTIVE_REGEXP = /(([\w\-]+)(?:\:([^;]+))?;?)/,
+ COMMENT_DIRECTIVE_REGEXP = /^\s*directive:\s*([\w-]+)\s+(.*)$/,
+ CLASS_DIRECTIVE_REGEXP = /(([\w-]+)(?::([^;]+))?;?)/,
ALL_OR_NOTHING_ATTRS = makeMap('ngSrc,ngSrcset,src,srcset'),
REQUIRE_PREFIX_REGEXP = /^(?:(\^\^?)?(\?)?(\^\^?)?)?/;
@@ -7144,22 +7977,27 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// The assumption is that future DOM event attribute names will begin with
// 'on' and be composed of only English letters.
var EVENT_HANDLER_ATTR_REGEXP = /^(on[a-z]+|formaction)$/;
+ var bindingCache = createMap();
function parseIsolateBindings(scope, directiveName, isController) {
- var LOCAL_REGEXP = /^\s*([@&]|=(\*?))(\??)\s*(\w*)\s*$/;
+ var LOCAL_REGEXP = /^\s*([@&<]|=(\*?))(\??)\s*([\w$]*)\s*$/;
- var bindings = {};
+ var bindings = createMap();
forEach(scope, function(definition, scopeName) {
+ if (definition in bindingCache) {
+ bindings[scopeName] = bindingCache[definition];
+ return;
+ }
var match = definition.match(LOCAL_REGEXP);
if (!match) {
throw $compileMinErr('iscp',
- "Invalid {3} for directive '{0}'." +
- " Definition: {... {1}: '{2}' ...}",
+ 'Invalid {3} for directive \'{0}\'.' +
+ ' Definition: {... {1}: \'{2}\' ...}',
directiveName, scopeName, definition,
- (isController ? "controller bindings definition" :
- "isolate scope definition"));
+ (isController ? 'controller bindings definition' :
+ 'isolate scope definition'));
}
bindings[scopeName] = {
@@ -7168,6 +8006,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
optional: match[3] === '?',
attrName: match[4] || scopeName
};
+ if (match[4]) {
+ bindingCache[definition] = bindings[scopeName];
+ }
});
return bindings;
@@ -7192,20 +8033,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
bindings.bindToController =
parseIsolateBindings(directive.bindToController, directiveName, true);
}
- if (isObject(bindings.bindToController)) {
- var controller = directive.controller;
- var controllerAs = directive.controllerAs;
- if (!controller) {
- // There is no controller, there may or may not be a controllerAs property
- throw $compileMinErr('noctrl',
- "Cannot bind to controller without directive '{0}'s controller.",
- directiveName);
- } else if (!identifierForController(controller, controllerAs)) {
- // There is a controller, but no identifier or controllerAs property
- throw $compileMinErr('noident',
- "Cannot bind to controller without identifier for directive '{0}'.",
- directiveName);
- }
+ if (bindings.bindToController && !directive.controller) {
+ // There is no controller
+ throw $compileMinErr('noctrl',
+ 'Cannot bind to controller without directive \'{0}\'s controller.',
+ directiveName);
}
return bindings;
}
@@ -7213,15 +8045,40 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function assertValidDirectiveName(name) {
var letter = name.charAt(0);
if (!letter || letter !== lowercase(letter)) {
- throw $compileMinErr('baddir', "Directive name '{0}' is invalid. The first character must be a lowercase letter", name);
+ throw $compileMinErr('baddir', 'Directive/Component name \'{0}\' is invalid. The first character must be a lowercase letter', name);
}
if (name !== name.trim()) {
throw $compileMinErr('baddir',
- "Directive name '{0}' is invalid. The name should not contain leading or trailing whitespaces",
+ 'Directive/Component name \'{0}\' is invalid. The name should not contain leading or trailing whitespaces',
name);
}
}
+ function getDirectiveRequire(directive) {
+ var require = directive.require || (directive.controller && directive.name);
+
+ if (!isArray(require) && isObject(require)) {
+ forEach(require, function(value, key) {
+ var match = value.match(REQUIRE_PREFIX_REGEXP);
+ var name = value.substring(match[0].length);
+ if (!name) require[key] = match[0] + key;
+ });
+ }
+
+ return require;
+ }
+
+ function getDirectiveRestrict(restrict, name) {
+ if (restrict && !(isString(restrict) && /[EACM]/.test(restrict))) {
+ throw $compileMinErr('badrestrict',
+ 'Restrict property \'{0}\' of directive \'{1}\' is invalid',
+ restrict,
+ name);
+ }
+
+ return restrict || 'EA';
+ }
+
/**
* @ngdoc method
* @name $compileProvider#directive
@@ -7233,11 +8090,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string|Object} name Name of the directive in camel-case (i.e. <code>ngBind</code> which
* will match as <code>ng-bind</code>), or an object map of directives where the keys are the
* names and the values are the factories.
- * @param {Function|Array} directiveFactory An injectable directive factory function. See
- * {@link guide/directive} for more info.
+ * @param {Function|Array} directiveFactory An injectable directive factory function. See the
+ * {@link guide/directive directive guide} and the {@link $compile compile API} for more info.
* @returns {ng.$compileProvider} Self for chaining.
*/
- this.directive = function registerDirective(name, directiveFactory) {
+ this.directive = function registerDirective(name, directiveFactory) {
+ assertArg(name, 'name');
assertNotHasOwnProperty(name, 'directive');
if (isString(name)) {
assertValidDirectiveName(name);
@@ -7258,13 +8116,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
directive.priority = directive.priority || 0;
directive.index = index;
directive.name = directive.name || name;
- directive.require = directive.require || (directive.controller && directive.name);
- directive.restrict = directive.restrict || 'EA';
- var bindings = directive.$$bindings =
- parseDirectiveBindings(directive, directive.name);
- if (isObject(bindings.isolateScope)) {
- directive.$$isolateBindings = bindings.isolateScope;
- }
+ directive.require = getDirectiveRequire(directive);
+ directive.restrict = getDirectiveRestrict(directive.restrict, name);
directive.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
} catch (e) {
@@ -7281,6 +8134,147 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return this;
};
+ /**
+ * @ngdoc method
+ * @name $compileProvider#component
+ * @module ng
+ * @param {string} name Name of the component in camelCase (i.e. `myComp` which will match `<my-comp>`)
+ * @param {Object} options Component definition object (a simplified
+ * {@link ng.$compile#directive-definition-object directive definition object}),
+ * with the following properties (all optional):
+ *
+ * - `controller` – `{(string|function()=}` – controller constructor function that should be
+ * associated with newly created scope or the name of a {@link ng.$compile#-controller-
+ * registered controller} if passed as a string. An empty `noop` function by default.
+ * - `controllerAs` – `{string=}` – identifier name for to reference the controller in the component's scope.
+ * If present, the controller will be published to scope under the `controllerAs` name.
+ * If not present, this will default to be `$ctrl`.
+ * - `template` – `{string=|function()=}` – html template as a string or a function that
+ * returns an html template as a string which should be used as the contents of this component.
+ * Empty string by default.
+ *
+ * If `template` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `templateUrl` – `{string=|function()=}` – path or function that returns a path to an html
+ * template that should be used as the contents of this component.
+ *
+ * If `templateUrl` is a function, then it is {@link auto.$injector#invoke injected} with
+ * the following locals:
+ *
+ * - `$element` - Current element
+ * - `$attrs` - Current attributes object for the element
+ *
+ * - `bindings` – `{object=}` – defines bindings between DOM attributes and component properties.
+ * Component properties are always bound to the component controller and not to the scope.
+ * See {@link ng.$compile#-bindtocontroller- `bindToController`}.
+ * - `transclude` – `{boolean=}` – whether {@link $compile#transclusion content transclusion} is enabled.
+ * Disabled by default.
+ * - `require` - `{Object<string, string>=}` - requires the controllers of other directives and binds them to
+ * this component's controller. The object keys specify the property names under which the required
+ * controllers (object values) will be bound. See {@link ng.$compile#-require- `require`}.
+ * - `$...` – additional properties to attach to the directive factory function and the controller
+ * constructor function. (This is used by the component router to annotate)
+ *
+ * @returns {ng.$compileProvider} the compile provider itself, for chaining of function calls.
+ * @description
+ * Register a **component definition** with the compiler. This is a shorthand for registering a special
+ * type of directive, which represents a self-contained UI component in your application. Such components
+ * are always isolated (i.e. `scope: {}`) and are always restricted to elements (i.e. `restrict: 'E'`).
+ *
+ * Component definitions are very simple and do not require as much configuration as defining general
+ * directives. Component definitions usually consist only of a template and a controller backing it.
+ *
+ * In order to make the definition easier, components enforce best practices like use of `controllerAs`,
+ * `bindToController`. They always have **isolate scope** and are restricted to elements.
+ *
+ * Here are a few examples of how you would usually define components:
+ *
+ * ```js
+ * var myMod = angular.module(...);
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * controller: function() {
+ * this.name = 'shahar';
+ * }
+ * });
+ *
+ * myMod.component('myComp', {
+ * template: '<div>My name is {{$ctrl.name}}</div>',
+ * bindings: {name: '@'}
+ * });
+ *
+ * myMod.component('myComp', {
+ * templateUrl: 'views/my-comp.html',
+ * controller: 'MyCtrl',
+ * controllerAs: 'ctrl',
+ * bindings: {name: '@'}
+ * });
+ *
+ * ```
+ * For more examples, and an in-depth guide, see the {@link guide/component component guide}.
+ *
+ * <br />
+ * See also {@link ng.$compileProvider#directive $compileProvider.directive()}.
+ */
+ this.component = function registerComponent(name, options) {
+ var controller = options.controller || function() {};
+
+ function factory($injector) {
+ function makeInjectable(fn) {
+ if (isFunction(fn) || isArray(fn)) {
+ return /** @this */ function(tElement, tAttrs) {
+ return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
+ };
+ } else {
+ return fn;
+ }
+ }
+
+ var template = (!options.template && !options.templateUrl ? '' : options.template);
+ var ddo = {
+ controller: controller,
+ controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
+ template: makeInjectable(template),
+ templateUrl: makeInjectable(options.templateUrl),
+ transclude: options.transclude,
+ scope: {},
+ bindToController: options.bindings || {},
+ restrict: 'E',
+ require: options.require
+ };
+
+ // Copy annotations (starting with $) over to the DDO
+ forEach(options, function(val, key) {
+ if (key.charAt(0) === '$') ddo[key] = val;
+ });
+
+ return ddo;
+ }
+
+ // TODO(pete) remove the following `forEach` before we release 1.6.0
+ // The component-router@0.2.0 looks for the annotations on the controller constructor
+ // Nothing in Angular looks for annotations on the factory function but we can't remove
+ // it from 1.5.x yet.
+
+ // Copy any annotation properties (starting with $) over to the factory and controller constructor functions
+ // These could be used by libraries such as the new component router
+ forEach(options, function(val, key) {
+ if (key.charAt(0) === '$') {
+ factory[key] = val;
+ // Don't try to copy over annotations to named controller
+ if (isFunction(controller)) controller[key] = val;
+ }
+ });
+
+ factory.$inject = ['$injector'];
+
+ return this.directive(name, factory);
+ };
+
/**
* @ngdoc method
@@ -7372,13 +8366,172 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
+ /**
+ * @ngdoc method
+ * @name $compileProvider#preAssignBindingsEnabled
+ *
+ * @param {boolean=} enabled update the preAssignBindingsEnabled state if provided, otherwise just return the
+ * current preAssignBindingsEnabled state
+ * @returns {*} current value if used as getter or itself (chaining) if used as setter
+ *
+ * @kind function
+ *
+ * @description
+ * Call this method to enable/disable whether directive controllers are assigned bindings before
+ * calling the controller's constructor.
+ * If enabled (true), the compiler assigns the value of each of the bindings to the
+ * properties of the controller object before the constructor of this object is called.
+ *
+ * If disabled (false), the compiler calls the constructor first before assigning bindings.
+ *
+ * The default value is true in Angular 1.5.x but will switch to false in Angular 1.6.x.
+ */
+ var preAssignBindingsEnabled = false;
+ this.preAssignBindingsEnabled = function(enabled) {
+ if (isDefined(enabled)) {
+ preAssignBindingsEnabled = enabled;
+ return this;
+ }
+ return preAssignBindingsEnabled;
+ };
+
+
+ var TTL = 10;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#onChangesTtl
+ * @description
+ *
+ * Sets the number of times `$onChanges` hooks can trigger new changes before giving up and
+ * assuming that the model is unstable.
+ *
+ * The current default is 10 iterations.
+ *
+ * In complex applications it's possible that dependencies between `$onChanges` hooks and bindings will result
+ * in several iterations of calls to these hooks. However if an application needs more than the default 10
+ * iterations to stabilize then you should investigate what is causing the model to continuously change during
+ * the `$onChanges` hook execution.
+ *
+ * Increasing the TTL could have performance implications, so you should not change it without proper justification.
+ *
+ * @param {number} limit The number of `$onChanges` hook iterations.
+ * @returns {number|object} the current limit (or `this` if called as a setter for chaining)
+ */
+ this.onChangesTtl = function(value) {
+ if (arguments.length) {
+ TTL = value;
+ return this;
+ }
+ return TTL;
+ };
+
+ var commentDirectivesEnabledConfig = true;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#commentDirectivesEnabled
+ * @description
+ *
+ * It indicates to the compiler
+ * whether or not directives on comments should be compiled.
+ * Defaults to `true`.
+ *
+ * Calling this function with false disables the compilation of directives
+ * on comments for the whole application.
+ * This results in a compilation performance gain,
+ * as the compiler doesn't have to check comments when looking for directives.
+ * This should however only be used if you are sure that no comment directives are used in
+ * the application (including any 3rd party directives).
+ *
+ * @param {boolean} enabled `false` if the compiler may ignore directives on comments
+ * @returns {boolean|object} the current value (or `this` if called as a setter for chaining)
+ */
+ this.commentDirectivesEnabled = function(value) {
+ if (arguments.length) {
+ commentDirectivesEnabledConfig = value;
+ return this;
+ }
+ return commentDirectivesEnabledConfig;
+ };
+
+
+ var cssClassDirectivesEnabledConfig = true;
+ /**
+ * @ngdoc method
+ * @name $compileProvider#cssClassDirectivesEnabled
+ * @description
+ *
+ * It indicates to the compiler
+ * whether or not directives on element classes should be compiled.
+ * Defaults to `true`.
+ *
+ * Calling this function with false disables the compilation of directives
+ * on element classes for the whole application.
+ * This results in a compilation performance gain,
+ * as the compiler doesn't have to check element classes when looking for directives.
+ * This should however only be used if you are sure that no class directives are used in
+ * the application (including any 3rd party directives).
+ *
+ * @param {boolean} enabled `false` if the compiler may ignore directives on element classes
+ * @returns {boolean|object} the current value (or `this` if called as a setter for chaining)
+ */
+ this.cssClassDirectivesEnabled = function(value) {
+ if (arguments.length) {
+ cssClassDirectivesEnabledConfig = value;
+ return this;
+ }
+ return cssClassDirectivesEnabledConfig;
+ };
+
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
+ '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {
+ $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
+
+ var SIMPLE_ATTR_NAME = /^\w/;
+ var specialAttrHolder = window.document.createElement('div');
+
+
+ var commentDirectivesEnabled = commentDirectivesEnabledConfig;
+ var cssClassDirectivesEnabled = cssClassDirectivesEnabledConfig;
+
+
+ var onChangesTtl = TTL;
+ // The onChanges hooks should all be run together in a single digest
+ // When changes occur, the call to trigger their hooks will be added to this queue
+ var onChangesQueue;
+
+ // This function is called in a $$postDigest to trigger all the onChanges hooks in a single digest
+ function flushOnChangesQueue() {
+ try {
+ if (!(--onChangesTtl)) {
+ // We have hit the TTL limit so reset everything
+ onChangesQueue = undefined;
+ throw $compileMinErr('infchng', '{0} $onChanges() iterations reached. Aborting!\n', TTL);
+ }
+ // We must run this hook in an apply since the $$postDigest runs outside apply
+ $rootScope.$apply(function() {
+ var errors = [];
+ for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
+ try {
+ onChangesQueue[i]();
+ } catch (e) {
+ errors.push(e);
+ }
+ }
+ // Reset the queue to trigger a new schedule next time there is a change
+ onChangesQueue = undefined;
+ if (errors.length) {
+ throw errors;
+ }
+ });
+ } finally {
+ onChangesTtl++;
+ }
+ }
+
- var Attributes = function(element, attributesToCopy) {
+ function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
var i, l, key;
@@ -7392,7 +8545,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
this.$$element = element;
- };
+ }
Attributes.prototype = {
/**
@@ -7517,9 +8670,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
(nodeName === 'img' && key === 'src')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
- } else if (nodeName === 'img' && key === 'srcset') {
+ } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
// sanitize img[srcset] values
- var result = "";
+ var result = '';
// first check if there are spaces because it's not the same pattern
var trimmedSrcset = trim(value);
@@ -7537,7 +8690,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// sanitize the uri
result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
// add the descriptor
- result += (" " + trim(rawUris[innerIdx + 1]));
+ result += (' ' + trim(rawUris[innerIdx + 1]));
}
// split the last item into uri and descriptor
@@ -7548,7 +8701,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// and add the last descriptor if any
if (lastTuple.length === 2) {
- result += (" " + trim(lastTuple[1]));
+ result += (' ' + trim(lastTuple[1]));
}
this[key] = value = result;
}
@@ -7557,19 +8710,25 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (value === null || isUndefined(value)) {
this.$$element.removeAttr(attrName);
} else {
- this.$$element.attr(attrName, value);
+ if (SIMPLE_ATTR_NAME.test(attrName)) {
+ this.$$element.attr(attrName, value);
+ } else {
+ setSpecialAttr(this.$$element[0], attrName, value);
+ }
}
}
// fire observers
var $$observers = this.$$observers;
- $$observers && forEach($$observers[observer], function(fn) {
- try {
- fn(value);
- } catch (e) {
- $exceptionHandler(e);
- }
- });
+ if ($$observers) {
+ forEach($$observers[observer], function(fn) {
+ try {
+ fn(value);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ });
+ }
},
@@ -7588,7 +8747,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @param {string} key Normalized key. (ie ngAttribute) .
* @param {function(interpolatedValue)} fn Function that will be called whenever
the interpolated value of the attribute changes.
- * See the {@link guide/directive#text-and-attribute-bindings Directives} guide for more info.
+ * See the {@link guide/interpolation#how-text-and-attribute-bindings-work Interpolation
+ * guide} for more info.
* @returns {function()} Returns a deregistration function for this observer.
*/
$observe: function(key, fn) {
@@ -7610,6 +8770,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
};
+ function setSpecialAttr(element, attrName, value) {
+ // Attributes names that do not start with letters (such as `(click)`) cannot be set using `setAttribute`
+ // so we have to jump through some hoops to get such an attribute
+ // https://github.com/angular/angular.js/pull/13318
+ specialAttrHolder.innerHTML = '<span ' + attrName + '>';
+ var attributes = specialAttrHolder.firstChild.attributes;
+ var attribute = attributes[0];
+ // We have to remove the attribute from its container element before we can add it to the destination element
+ attributes.removeNamedItem(attribute.name);
+ attribute.value = value;
+ element.attributes.setNamedItem(attribute);
+ }
function safeAddClass($element, className) {
try {
@@ -7623,7 +8795,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
- denormalizeTemplate = (startSymbol == '{{' || endSymbol == '}}')
+ denormalizeTemplate = (startSymbol === '{{' && endSymbol === '}}')
? identity
: function denormalizeTemplate(template) {
return template.replace(/\{\{/g, startSymbol).replace(/}}/g, endSymbol);
@@ -7656,6 +8828,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
} : noop;
+ compile.$$createComment = function(directiveName, comment) {
+ var content = '';
+ if (debugInfoEnabled) {
+ content = ' ' + (directiveName || '') + ': ';
+ if (comment) content += comment + ' ';
+ }
+ return window.document.createComment(content);
+ };
+
return compile;
//================================
@@ -7667,19 +8848,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// modify it.
$compileNodes = jqLite($compileNodes);
}
- // We can not compile top level text elements since text nodes can be merged and we will
- // not be able to attach scope data to them, so we will wrap them in <span>
- forEach($compileNodes, function(node, index) {
- if (node.nodeType == NODE_TYPE_TEXT && node.nodeValue.match(/\S+/) /* non-empty */) {
- $compileNodes[index] = jqLite(node).wrap('<span></span>').parent()[0];
- }
- });
var compositeLinkFn =
compileNodes($compileNodes, transcludeFn, $compileNodes,
maxPriority, ignoreDirective, previousCompileContext);
compile.$$addScopeClass($compileNodes);
var namespace = null;
return function publicLinkFn(scope, cloneConnectFn, options) {
+ if (!$compileNodes) {
+ throw $compileMinErr('multilink', 'This element has already been linked.');
+ }
assertArg(scope, 'scope');
if (previousCompileContext && previousCompileContext.needsNewScope) {
@@ -7734,6 +8911,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (cloneConnectFn) cloneConnectFn($linkNode, scope);
if (compositeLinkFn) compositeLinkFn(scope, $linkNode, $linkNode, parentBoundTranscludeFn);
+
+ if (!cloneConnectFn) {
+ $compileNodes = compositeLinkFn = null;
+ }
return $linkNode;
};
}
@@ -7744,7 +8925,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (!node) {
return 'html';
} else {
- return nodeName_(node) !== 'foreignobject' && node.toString().match(/SVG/) ? 'svg' : 'html';
+ return nodeName_(node) !== 'foreignobject' && toString.call(node).match(/SVG/) ? 'svg' : 'html';
}
}
@@ -7766,12 +8947,23 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function compileNodes(nodeList, transcludeFn, $rootElement, maxPriority, ignoreDirective,
previousCompileContext) {
var linkFns = [],
+ // `nodeList` can be either an element's `.childNodes` (live NodeList)
+ // or a jqLite/jQuery collection or an array
+ notLiveList = isArray(nodeList) || (nodeList instanceof jqLite),
attrs, directives, nodeLinkFn, childNodes, childLinkFn, linkFnFound, nodeLinkFnFound;
+
for (var i = 0; i < nodeList.length; i++) {
attrs = new Attributes();
- // we must always refer to nodeList[i] since the nodes can be replaced underneath us.
+ // Support: IE 11 only
+ // Workaround for #11781 and #14924
+ if (msie === 11) {
+ mergeConsecutiveTextNodes(nodeList, i, notLiveList);
+ }
+
+ // We must always refer to `nodeList[i]` hereafter,
+ // since the nodes can be replaced underneath us.
directives = collectDirectives(nodeList[i], [], attrs, i === 0 ? maxPriority : undefined,
ignoreDirective);
@@ -7818,7 +9010,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
stableNodeList = new Array(nodeListLength);
// create a sparse array by only copying the elements which have a linkFn
- for (i = 0; i < linkFns.length; i+=3) {
+ for (i = 0; i < linkFns.length; i += 3) {
idx = linkFns[i];
stableNodeList[idx] = nodeList[idx];
}
@@ -7862,9 +9054,34 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
- function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
+ function mergeConsecutiveTextNodes(nodeList, idx, notLiveList) {
+ var node = nodeList[idx];
+ var parent = node.parentNode;
+ var sibling;
+
+ if (node.nodeType !== NODE_TYPE_TEXT) {
+ return;
+ }
+
+ while (true) {
+ sibling = parent ? node.nextSibling : nodeList[idx + 1];
+ if (!sibling || sibling.nodeType !== NODE_TYPE_TEXT) {
+ break;
+ }
+
+ node.nodeValue = node.nodeValue + sibling.nodeValue;
+
+ if (sibling.parentNode) {
+ sibling.parentNode.removeChild(sibling);
+ }
+ if (notLiveList && sibling === nodeList[idx + 1]) {
+ nodeList.splice(idx + 1, 1);
+ }
+ }
+ }
- var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
+ function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
+ function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
transcludedScope = scope.$new(false, containingScope);
@@ -7876,13 +9093,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
transcludeControllers: controllers,
futureParentElement: futureParentElement
});
- };
+ }
// We need to attach the transclusion slots onto the `boundTranscludeFn`
// so that they are available inside the `controllersBoundTransclude` function
var boundSlots = boundTranscludeFn.$$slots = createMap();
for (var slotName in transcludeFn.$$slots) {
- boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
+ if (transcludeFn.$$slots[slotName]) {
+ boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
+ } else {
+ boundSlots[slotName] = null;
+ }
}
return boundTranscludeFn;
@@ -7902,13 +9123,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var nodeType = node.nodeType,
attrsMap = attrs.$attr,
match,
+ nodeName,
className;
switch (nodeType) {
case NODE_TYPE_ELEMENT: /* Element */
+
+ nodeName = nodeName_(node);
+
// use the node name: <directive>
addDirective(directives,
- directiveNormalize(nodeName_(node)), 'E', maxPriority, ignoreDirective);
+ directiveNormalize(nodeName), 'E', maxPriority, ignoreDirective);
// iterate over the attributes
for (var attr, name, nName, ngAttrName, value, isNgAttr, nAttrs = node.attributes,
@@ -7918,11 +9143,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attr = nAttrs[j];
name = attr.name;
- value = trim(attr.value);
+ value = attr.value;
// support ngAttr attribute binding
ngAttrName = directiveNormalize(name);
- if (isNgAttr = NG_ATTR_BINDING.test(ngAttrName)) {
+ isNgAttr = NG_ATTR_BINDING.test(ngAttrName);
+ if (isNgAttr) {
name = name.replace(PREFIX_REGEXP, '')
.substr(8).replace(/_(.)/g, function(match, letter) {
return letter.toUpperCase();
@@ -7949,14 +9175,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
attrEndName);
}
+ if (nodeName === 'input' && node.getAttribute('type') === 'hidden') {
+ // Hidden input elements can have strange behaviour when navigating back to the page
+ // This tells the browser not to try to cache and reinstate previous values
+ node.setAttribute('autocomplete', 'off');
+ }
+
// use class as directive
+ if (!cssClassDirectivesEnabled) break;
className = node.className;
if (isObject(className)) {
// Maybe SVGAnimatedString
className = className.animVal;
}
if (isString(className) && className !== '') {
- while (match = CLASS_DIRECTIVE_REGEXP.exec(className)) {
+ while ((match = CLASS_DIRECTIVE_REGEXP.exec(className))) {
nName = directiveNormalize(match[2]);
if (addDirective(directives, nName, 'C', maxPriority, ignoreDirective)) {
attrs[nName] = trim(match[3]);
@@ -7966,29 +9199,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
break;
case NODE_TYPE_TEXT: /* Text Node */
- if (msie === 11) {
- // Workaround for #11781
- while (node.parentNode && node.nextSibling && node.nextSibling.nodeType === NODE_TYPE_TEXT) {
- node.nodeValue = node.nodeValue + node.nextSibling.nodeValue;
- node.parentNode.removeChild(node.nextSibling);
- }
- }
addTextInterpolateDirective(directives, node.nodeValue);
break;
case NODE_TYPE_COMMENT: /* Comment */
- try {
- match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
- if (match) {
- nName = directiveNormalize(match[1]);
- if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
- attrs[nName] = trim(match[2]);
- }
- }
- } catch (e) {
- // turns out that under some circumstances IE9 throws errors when one attempts to read
- // comment's node value.
- // Just ignore it and continue. (Can't seem to reproduce in test case.)
- }
+ if (!commentDirectivesEnabled) break;
+ collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective);
break;
}
@@ -7996,8 +9211,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return directives;
}
+ function collectCommentDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
+ // function created because of performance, try/catch disables
+ // the optimization of the whole function #14848
+ try {
+ var match = COMMENT_DIRECTIVE_REGEXP.exec(node.nodeValue);
+ if (match) {
+ var nName = directiveNormalize(match[1]);
+ if (addDirective(directives, nName, 'M', maxPriority, ignoreDirective)) {
+ attrs[nName] = trim(match[2]);
+ }
+ }
+ } catch (e) {
+ // turns out that under some circumstances IE9 throws errors when one attempts to read
+ // comment's node value.
+ // Just ignore it and continue. (Can't seem to reproduce in test case.)
+ }
+ }
+
/**
- * Given a node with an directive-start it collects all of the siblings until it finds
+ * Given a node with a directive-start it collects all of the siblings until it finds
* directive-end.
* @param node
* @param attrStart
@@ -8011,10 +9244,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
do {
if (!node) {
throw $compileMinErr('uterdir',
- "Unterminated attribute, found '{0}' but no matching '{1}' found.",
+ 'Unterminated attribute, found \'{0}\' but no matching \'{1}\' found.',
attrStart, attrEnd);
}
- if (node.nodeType == NODE_TYPE_ELEMENT) {
+ if (node.nodeType === NODE_TYPE_ELEMENT) {
if (node.hasAttribute(attrStart)) depth++;
if (node.hasAttribute(attrEnd)) depth--;
}
@@ -8037,7 +9270,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @returns {Function}
*/
function groupElementsLinkFnWrapper(linkFn, attrStart, attrEnd) {
- return function(scope, element, attrs, controllers, transcludeFn) {
+ return function groupedElementsLink(scope, element, attrs, controllers, transcludeFn) {
element = groupScan(element[0], attrStart, attrEnd);
return linkFn(scope, element, attrs, controllers, transcludeFn);
};
@@ -8055,23 +9288,21 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
* @returns {Function}
*/
function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
- if (eager) {
- return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
- }
-
- var compiled;
+ var compiled;
- return function() {
- if (!compiled) {
- compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
-
- // Null out all of these references in order to make them eligible for garbage collection
- // since this is a potentially long lived closure
- $compileNodes = transcludeFn = previousCompileContext = null;
- }
+ if (eager) {
+ return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+ }
+ return /** @this */ function lazyCompilation() {
+ if (!compiled) {
+ compiled = compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
- return compiled.apply(this, arguments);
- };
+ // Null out all of these references in order to make them eligible for garbage collection
+ // since this is a potentially long lived closure
+ $compileNodes = transcludeFn = previousCompileContext = null;
+ }
+ return compiled.apply(this, arguments);
+ };
}
/**
@@ -8138,7 +9369,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break; // prevent further processing of directives
}
- if (directiveValue = directive.scope) {
+ directiveValue = directive.scope;
+
+ if (directiveValue) {
// skip the check for directives with async templates, we'll check the derived sync
// directive when the template arrives
@@ -8172,7 +9405,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
|| (directive.transclude && !directive.$$tlb))) {
var candidateDirective;
- for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
+ for (var scanningIndex = i + 1; (candidateDirective = directives[scanningIndex++]);) {
if ((candidateDirective.transclude && !candidateDirective.$$tlb)
|| (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
mightHaveMultipleTransclusionError = true;
@@ -8184,14 +9417,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
if (!directive.templateUrl && directive.controller) {
- directiveValue = directive.controller;
controllerDirectives = controllerDirectives || createMap();
- assertNoDuplicate("'" + directiveName + "' controller",
+ assertNoDuplicate('\'' + directiveName + '\' controller',
controllerDirectives[directiveName], directive, $compileNode);
controllerDirectives[directiveName] = directive;
}
- if (directiveValue = directive.transclude) {
+ directiveValue = directive.transclude;
+
+ if (directiveValue) {
hasTranscludeDirective = true;
// Special case ngIf and ngRepeat so that we don't complain about duplicate transclusion.
@@ -8202,16 +9436,26 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nonTlbTranscludeDirective = directive;
}
- if (directiveValue == 'element') {
+ if (directiveValue === 'element') {
hasElementTranscludeDirective = true;
terminalPriority = directive.priority;
$template = $compileNode;
$compileNode = templateAttrs.$$element =
- jqLite(document.createComment(' ' + directiveName + ': ' +
- templateAttrs[directiveName] + ' '));
+ jqLite(compile.$$createComment(directiveName, templateAttrs[directiveName]));
compileNode = $compileNode[0];
replaceWith(jqCollection, sliceArgs($template), compileNode);
+ // Support: Chrome < 50
+ // https://github.com/angular/angular.js/issues/14041
+
+ // In the versions of V8 prior to Chrome 50, the document fragment that is created
+ // in the `replaceWith` function is improperly garbage collected despite still
+ // being referenced by the `parentNode` property of all of the child nodes. By adding
+ // a reference to the fragment via a different property, we can avoid that incorrect
+ // behavior.
+ // TODO: remove this line after Chrome 50 has been released
+ $template[0].$$parentNode = $template[0].parentNode;
+
childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, terminalPriority,
replaceDirective && replaceDirective.name, {
// Don't pass in:
@@ -8226,32 +9470,42 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else {
var slots = createMap();
- $template = jqLite(jqLiteClone(compileNode)).contents();
- if (isObject(directiveValue)) {
+ if (!isObject(directiveValue)) {
+ $template = jqLite(jqLiteClone(compileNode)).contents();
+ } else {
- // We have transclusion slots - collect them up and compile them and store their
- // transclusion functions
+ // We have transclusion slots,
+ // collect them up, compile them and store their transclusion functions
$template = [];
- var slotNames = createMap();
+
+ var slotMap = createMap();
var filledSlots = createMap();
- // Parse the slot names: if they start with a ? then they are optional
- forEach(directiveValue, function(slotName, key) {
- var optional = (slotName.charAt(0) === '?');
- slotName = optional ? slotName.substring(1) : slotName;
- slotNames[key] = slotName;
- slots[slotName] = [];
+ // Parse the element selectors
+ forEach(directiveValue, function(elementSelector, slotName) {
+ // If an element selector starts with a ? then it is optional
+ var optional = (elementSelector.charAt(0) === '?');
+ elementSelector = optional ? elementSelector.substring(1) : elementSelector;
+
+ slotMap[elementSelector] = slotName;
+
+ // We explicitly assign `null` since this implies that a slot was defined but not filled.
+ // Later when calling boundTransclusion functions with a slot name we only error if the
+ // slot is `undefined`
+ slots[slotName] = null;
+
// filledSlots contains `true` for all slots that are either optional or have been
// filled. This is used to check that we have not missed any required slots
filledSlots[slotName] = optional;
});
// Add the matching elements into their slot
- forEach($compileNode.children(), function(node) {
- var slotName = slotNames[directiveNormalize(nodeName_(node))];
+ forEach($compileNode.contents(), function(node) {
+ var slotName = slotMap[directiveNormalize(nodeName_(node))];
if (slotName) {
filledSlots[slotName] = true;
+ slots[slotName] = slots[slotName] || [];
slots[slotName].push(node);
} else {
$template.push(node);
@@ -8265,9 +9519,12 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
- forEach(Object.keys(slots), function(slotName) {
- slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
- });
+ for (var slotName in slots) {
+ if (slots[slotName]) {
+ // Only define a transclusion function if the slot was filled
+ slots[slotName] = compilationGenerator(mightHaveMultipleTransclusionError, slots[slotName], transcludeFn);
+ }
+ }
}
$compileNode.empty(); // clear contents
@@ -8297,9 +9554,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
compileNode = $template[0];
- if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
+ if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
throw $compileMinErr('tplrt',
- "Template for directive '{0}' must have exactly one root element. {1}",
+ 'Template for directive \'{0}\' must have exactly one root element. {1}',
directiveName, '');
}
@@ -8339,6 +9596,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
replaceDirective = directive;
}
+ // eslint-disable-next-line no-func-assign
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
@@ -8351,10 +9609,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
} else if (directive.compile) {
try {
linkFn = directive.compile($compileNode, templateAttrs, childTranscludeFn);
+ var context = directive.$$originalDirective || directive;
if (isFunction(linkFn)) {
- addLinkFns(null, linkFn, attrStart, attrEnd);
+ addLinkFns(null, bind(context, linkFn), attrStart, attrEnd);
} else if (linkFn) {
- addLinkFns(linkFn.pre, linkFn.post, attrStart, attrEnd);
+ addLinkFns(bind(context, linkFn.pre), bind(context, linkFn.post), attrStart, attrEnd);
}
} catch (e) {
$exceptionHandler(e, startingTag($compileNode));
@@ -8401,80 +9660,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
-
- function getControllers(directiveName, require, $element, elementControllers) {
- var value;
-
- if (isString(require)) {
- var match = require.match(REQUIRE_PREFIX_REGEXP);
- var name = require.substring(match[0].length);
- var inheritType = match[1] || match[3];
- var optional = match[2] === '?';
-
- //If only parents then start at the parent element
- if (inheritType === '^^') {
- $element = $element.parent();
- //Otherwise attempt getting the controller from elementControllers in case
- //the element is transcluded (and has no data) and to avoid .data if possible
- } else {
- value = elementControllers && elementControllers[name];
- value = value && value.instance;
- }
-
- if (!value) {
- var dataName = '$' + name + 'Controller';
- value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
- }
-
- if (!value && !optional) {
- throw $compileMinErr('ctreq',
- "Controller '{0}', required by directive '{1}', can't be found!",
- name, directiveName);
- }
- } else if (isArray(require)) {
- value = [];
- for (var i = 0, ii = require.length; i < ii; i++) {
- value[i] = getControllers(directiveName, require[i], $element, elementControllers);
- }
- }
-
- return value || null;
- }
-
- function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope) {
- var elementControllers = createMap();
- for (var controllerKey in controllerDirectives) {
- var directive = controllerDirectives[controllerKey];
- var locals = {
- $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
- $element: $element,
- $attrs: attrs,
- $transclude: transcludeFn
- };
-
- var controller = directive.controller;
- if (controller == '@') {
- controller = attrs[directive.name];
- }
-
- var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
-
- // For directives with element transclusion the element is a comment,
- // but jQuery .data doesn't support attaching data to comment nodes as it's hard to
- // clean up (http://bugs.jquery.com/ticket/8335).
- // Instead, we save the controllers for the element in a local hash and attach to .data
- // later, once we have the actual element.
- elementControllers[directive.name] = controllerInstance;
- if (!hasElementTranscludeDirective) {
- $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
- }
- }
- return elementControllers;
- }
-
function nodeLinkFn(childLinkFn, scope, linkNode, $rootElement, boundTranscludeFn) {
- var linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
- attrs, removeScopeBindingWatches, removeControllerBindingWatches;
+ var i, ii, linkFn, isolateScope, controllerScope, elementControllers, transcludeFn, $element,
+ attrs, scopeBindingInfo;
if (compileNode === linkNode) {
attrs = templateAttrs;
@@ -8496,10 +9684,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// is later passed as `parentBoundTranscludeFn` to `publicLinkFn`
transcludeFn = controllersBoundTransclude;
transcludeFn.$$boundTransclude = boundTranscludeFn;
+ // expose the slots on the `$transclude` function
+ transcludeFn.isSlotFilled = function(slotName) {
+ return !!boundTranscludeFn.$$slots[slotName];
+ };
}
if (controllerDirectives) {
- elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope);
+ elementControllers = setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective);
}
if (newIsolateScopeDirective) {
@@ -8509,11 +9701,11 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
compile.$$addScopeClass($element, true);
isolateScope.$$isolateBindings =
newIsolateScopeDirective.$$isolateBindings;
- removeScopeBindingWatches = initializeDirectiveBindings(scope, attrs, isolateScope,
+ scopeBindingInfo = initializeDirectiveBindings(scope, attrs, isolateScope,
isolateScope.$$isolateBindings,
newIsolateScopeDirective);
- if (removeScopeBindingWatches) {
- isolateScope.$on('$destroy', removeScopeBindingWatches);
+ if (scopeBindingInfo.removeWatches) {
+ isolateScope.$on('$destroy', scopeBindingInfo.removeWatches);
}
}
@@ -8523,23 +9715,70 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
var controller = elementControllers[name];
var bindings = controllerDirective.$$bindings.bindToController;
- if (controller.identifier && bindings) {
- removeControllerBindingWatches =
- initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
- }
+ if (preAssignBindingsEnabled) {
+ if (bindings) {
+ controller.bindingInfo =
+ initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ } else {
+ controller.bindingInfo = {};
+ }
- var controllerResult = controller();
- if (controllerResult !== controller.instance) {
- // If the controller constructor has a return value, overwrite the instance
- // from setupControllers
- controller.instance = controllerResult;
- $element.data('$' + controllerDirective.name + 'Controller', controllerResult);
- removeControllerBindingWatches && removeControllerBindingWatches();
- removeControllerBindingWatches =
+ var controllerResult = controller();
+ if (controllerResult !== controller.instance) {
+ // If the controller constructor has a return value, overwrite the instance
+ // from setupControllers
+ controller.instance = controllerResult;
+ $element.data('$' + controllerDirective.name + 'Controller', controllerResult);
+ if (controller.bindingInfo.removeWatches) {
+ controller.bindingInfo.removeWatches();
+ }
+ controller.bindingInfo =
+ initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ }
+ } else {
+ controller.instance = controller();
+ $element.data('$' + controllerDirective.name + 'Controller', controller.instance);
+ controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
}
}
+ // Bind the required controllers to the controller, if `require` is an object and `bindToController` is truthy
+ forEach(controllerDirectives, function(controllerDirective, name) {
+ var require = controllerDirective.require;
+ if (controllerDirective.bindToController && !isArray(require) && isObject(require)) {
+ extend(elementControllers[name].instance, getControllers(name, require, $element, elementControllers));
+ }
+ });
+
+ // Handle the init and destroy lifecycle hooks on all controllers that have them
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$onChanges)) {
+ try {
+ controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ if (isFunction(controllerInstance.$onInit)) {
+ try {
+ controllerInstance.$onInit();
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ if (isFunction(controllerInstance.$doCheck)) {
+ controllerScope.$watch(function() { controllerInstance.$doCheck(); });
+ controllerInstance.$doCheck();
+ }
+ if (isFunction(controllerInstance.$onDestroy)) {
+ controllerScope.$on('$destroy', function callOnDestroyHook() {
+ controllerInstance.$onDestroy();
+ });
+ }
+ });
+
// PRELINKING
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[i];
@@ -8559,7 +9798,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (newIsolateScopeDirective && (newIsolateScopeDirective.template || newIsolateScopeDirective.templateUrl === null)) {
scopeToChild = isolateScope;
}
- childLinkFn && childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
+ if (childLinkFn) {
+ childLinkFn(scopeToChild, linkNode.childNodes, undefined, boundTranscludeFn);
+ }
// POSTLINKING
for (i = postLinkFns.length - 1; i >= 0; i--) {
@@ -8573,6 +9814,14 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
);
}
+ // Trigger $postLink lifecycle hooks
+ forEach(elementControllers, function(controller) {
+ var controllerInstance = controller.instance;
+ if (isFunction(controllerInstance.$postLink)) {
+ controllerInstance.$postLink();
+ }
+ });
+
// This is the function that is injected as `$transclude`.
// Note: all arguments are optional!
function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
@@ -8592,20 +9841,98 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
if (slotName) {
+ // slotTranscludeFn can be one of three things:
+ // * a transclude function - a filled slot
+ // * `null` - an optional slot that was not filled
+ // * `undefined` - a slot that was not declared (i.e. invalid)
var slotTranscludeFn = boundTranscludeFn.$$slots[slotName];
- if (!slotTranscludeFn) {
+ if (slotTranscludeFn) {
+ return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ } else if (isUndefined(slotTranscludeFn)) {
throw $compileMinErr('noslot',
'No parent directive that requires a transclusion with slot name "{0}". ' +
'Element: {1}',
slotName, startingTag($element));
}
- return slotTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ } else {
+ return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
}
}
}
+ function getControllers(directiveName, require, $element, elementControllers) {
+ var value;
+
+ if (isString(require)) {
+ var match = require.match(REQUIRE_PREFIX_REGEXP);
+ var name = require.substring(match[0].length);
+ var inheritType = match[1] || match[3];
+ var optional = match[2] === '?';
+
+ //If only parents then start at the parent element
+ if (inheritType === '^^') {
+ $element = $element.parent();
+ //Otherwise attempt getting the controller from elementControllers in case
+ //the element is transcluded (and has no data) and to avoid .data if possible
+ } else {
+ value = elementControllers && elementControllers[name];
+ value = value && value.instance;
+ }
+
+ if (!value) {
+ var dataName = '$' + name + 'Controller';
+ value = inheritType ? $element.inheritedData(dataName) : $element.data(dataName);
+ }
+
+ if (!value && !optional) {
+ throw $compileMinErr('ctreq',
+ 'Controller \'{0}\', required by directive \'{1}\', can\'t be found!',
+ name, directiveName);
+ }
+ } else if (isArray(require)) {
+ value = [];
+ for (var i = 0, ii = require.length; i < ii; i++) {
+ value[i] = getControllers(directiveName, require[i], $element, elementControllers);
+ }
+ } else if (isObject(require)) {
+ value = {};
+ forEach(require, function(controller, property) {
+ value[property] = getControllers(directiveName, controller, $element, elementControllers);
+ });
+ }
+
+ return value || null;
+ }
+
+ function setupControllers($element, attrs, transcludeFn, controllerDirectives, isolateScope, scope, newIsolateScopeDirective) {
+ var elementControllers = createMap();
+ for (var controllerKey in controllerDirectives) {
+ var directive = controllerDirectives[controllerKey];
+ var locals = {
+ $scope: directive === newIsolateScopeDirective || directive.$$isolateScope ? isolateScope : scope,
+ $element: $element,
+ $attrs: attrs,
+ $transclude: transcludeFn
+ };
+
+ var controller = directive.controller;
+ if (controller === '@') {
+ controller = attrs[directive.name];
+ }
+
+ var controllerInstance = $controller(controller, locals, true, directive.controllerAs);
+
+ // For directives with element transclusion the element is a comment.
+ // In this case .data will not attach any data.
+ // Instead, we save the controllers for the element in a local hash and attach to .data
+ // later, once we have the actual element.
+ elementControllers[directive.name] = controllerInstance;
+ $element.data('$' + directive.name + 'Controller', controllerInstance.instance);
+ }
+ return elementControllers;
+ }
+
// Depending upon the context in which a directive finds itself it might need to have a new isolated
// or child scope created. For instance:
// * if the directive has been pulled into a template because another directive with a higher priority
@@ -8639,17 +9966,22 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (hasDirectives.hasOwnProperty(name)) {
for (var directive, directives = $injector.get(name + Suffix),
i = 0, ii = directives.length; i < ii; i++) {
- try {
- directive = directives[i];
- if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
- directive.restrict.indexOf(location) != -1) {
- if (startAttrName) {
- directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
+ directive = directives[i];
+ if ((isUndefined(maxPriority) || maxPriority > directive.priority) &&
+ directive.restrict.indexOf(location) !== -1) {
+ if (startAttrName) {
+ directive = inherit(directive, {$$start: startAttrName, $$end: endAttrName});
+ }
+ if (!directive.$$bindings) {
+ var bindings = directive.$$bindings =
+ parseDirectiveBindings(directive, directive.name);
+ if (isObject(bindings.isolateScope)) {
+ directive.$$isolateBindings = bindings.isolateScope;
}
- tDirectives.push(directive);
- match = directive;
}
- } catch (e) { $exceptionHandler(e); }
+ tDirectives.push(directive);
+ match = directive;
+ }
}
}
return match;
@@ -8687,14 +10019,17 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
function mergeTemplateAttributes(dst, src) {
var srcAttr = src.$attr,
- dstAttr = dst.$attr,
- $element = dst.$$element;
+ dstAttr = dst.$attr;
// reapply the old attributes to the new element
forEach(dst, function(value, key) {
- if (key.charAt(0) != '$') {
+ if (key.charAt(0) !== '$') {
if (src[key] && src[key] !== value) {
- value += (key === 'style' ? ';' : ' ') + src[key];
+ if (value.length) {
+ value += (key === 'style' ? ';' : ' ') + src[key];
+ } else {
+ value = src[key];
+ }
}
dst.$set(key, value, true, srcAttr[key]);
}
@@ -8702,18 +10037,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// copy the new attributes on the old attrs object
forEach(src, function(value, key) {
- if (key == 'class') {
- safeAddClass($element, value);
- dst['class'] = (dst['class'] ? dst['class'] + ' ' : '') + value;
- } else if (key == 'style') {
- $element.attr('style', $element.attr('style') + ';' + value);
- dst['style'] = (dst['style'] ? dst['style'] + ';' : '') + value;
- // `dst` will never contain hasOwnProperty as DOM parser won't let it.
- // You will get an "InvalidCharacterError: DOM Exception 5" error if you
- // have an attribute like "has-own-property" or "data-has-own-property", etc.
- } else if (key.charAt(0) != '$' && !dst.hasOwnProperty(key)) {
+ // Check if we already set this attribute in the loop above.
+ // `dst` will never contain hasOwnProperty as DOM parser won't let it.
+ // You will get an "InvalidCharacterError: DOM Exception 5" error if you
+ // have an attribute like "has-own-property" or "data-has-own-property", etc.
+ if (!dst.hasOwnProperty(key) && key.charAt(0) !== '$') {
dst[key] = value;
- dstAttr[key] = srcAttr[key];
+
+ if (key !== 'class' && key !== 'style') {
+ dstAttr[key] = srcAttr[key];
+ }
}
});
}
@@ -8750,9 +10083,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
compileNode = $template[0];
- if ($template.length != 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
+ if ($template.length !== 1 || compileNode.nodeType !== NODE_TYPE_ELEMENT) {
throw $compileMinErr('tplrt',
- "Template for directive '{0}' must have exactly one root element. {1}",
+ 'Template for directive \'{0}\' must have exactly one root element. {1}',
origAsyncDirective.name, templateUrl);
}
@@ -8778,7 +10111,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childTranscludeFn, $compileNode, origAsyncDirective, preLinkFns, postLinkFns,
previousCompileContext);
forEach($rootElement, function(node, i) {
- if (node == compileNode) {
+ if (node === compileNode) {
$rootElement[i] = $compileNode[0];
}
});
@@ -8815,6 +10148,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn);
}
linkQueue = null;
+ }).catch(function(error) {
+ if (error instanceof Error) {
+ $exceptionHandler(error);
+ }
});
return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
@@ -8893,7 +10230,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
switch (type) {
case 'svg':
case 'math':
- var wrapper = document.createElement('div');
+ var wrapper = window.document.createElement('div');
wrapper.innerHTML = '<' + type + '>' + template + '</' + type + '>';
return wrapper.childNodes[0].childNodes;
default:
@@ -8903,36 +10240,49 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
function getTrustedContext(node, attrNormalizedName) {
- if (attrNormalizedName == "srcdoc") {
+ if (attrNormalizedName === 'srcdoc') {
return $sce.HTML;
}
var tag = nodeName_(node);
+ // All tags with src attributes require a RESOURCE_URL value, except for
+ // img and various html5 media tags.
+ if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
+ if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
+ return $sce.RESOURCE_URL;
+ }
// maction[xlink:href] can source SVG. It's not limited to <maction>.
- if (attrNormalizedName == "xlinkHref" ||
- (tag == "form" && attrNormalizedName == "action") ||
- (tag != "img" && (attrNormalizedName == "src" ||
- attrNormalizedName == "ngSrc"))) {
+ } else if (attrNormalizedName === 'xlinkHref' ||
+ (tag === 'form' && attrNormalizedName === 'action') ||
+ // links can be stylesheets or imports, which can run script in the current origin
+ (tag === 'link' && attrNormalizedName === 'href')
+ ) {
return $sce.RESOURCE_URL;
}
}
- function addAttrInterpolateDirective(node, directives, value, name, allOrNothing) {
+ function addAttrInterpolateDirective(node, directives, value, name, isNgAttr) {
var trustedContext = getTrustedContext(node, name);
- allOrNothing = ALL_OR_NOTHING_ATTRS[name] || allOrNothing;
+ var mustHaveExpression = !isNgAttr;
+ var allOrNothing = ALL_OR_NOTHING_ATTRS[name] || isNgAttr;
- var interpolateFn = $interpolate(value, true, trustedContext, allOrNothing);
+ var interpolateFn = $interpolate(value, mustHaveExpression, trustedContext, allOrNothing);
// no interpolation found -> ignore
if (!interpolateFn) return;
-
- if (name === "multiple" && nodeName_(node) === "select") {
- throw $compileMinErr("selmulti",
- "Binding to the 'multiple' attribute is not supported. Element: {0}",
+ if (name === 'multiple' && nodeName_(node) === 'select') {
+ throw $compileMinErr('selmulti',
+ 'Binding to the \'multiple\' attribute is not supported. Element: {0}',
startingTag(node));
}
+ if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
+ throw $compileMinErr('nodomevents',
+ 'Interpolations for HTML DOM event attributes are disallowed. Please use the ' +
+ 'ng- versions (such as ng-click instead of onclick) instead.');
+ }
+
directives.push({
priority: 100,
compile: function() {
@@ -8940,12 +10290,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
pre: function attrInterpolatePreLinkFn(scope, element, attr) {
var $$observers = (attr.$$observers || (attr.$$observers = createMap()));
- if (EVENT_HANDLER_ATTR_REGEXP.test(name)) {
- throw $compileMinErr('nodomevents',
- "Interpolations for HTML DOM event attributes are disallowed. Please use the " +
- "ng- versions (such as ng-click instead of onclick) instead.");
- }
-
// If the attribute has changed since last $interpolate()ed
var newValue = attr[name];
if (newValue !== value) {
@@ -8974,7 +10318,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
//skip animations when the first digest occurs (when
//both the new and the old values are the same) since
//the CSS classes are the non-interpolated values
- if (name === 'class' && newValue != oldValue) {
+ if (name === 'class' && newValue !== oldValue) {
attr.$updateClass(newValue, oldValue);
} else {
attr.$set(name, newValue);
@@ -9005,7 +10349,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if ($rootElement) {
for (i = 0, ii = $rootElement.length; i < ii; i++) {
- if ($rootElement[i] == firstElementToRemove) {
+ if ($rootElement[i] === firstElementToRemove) {
$rootElement[i++] = newNode;
for (var j = i, j2 = j + removeCount - 1,
jj = $rootElement.length;
@@ -9037,7 +10381,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
// - remove them from the DOM
// - allow them to still be traversed with .nextSibling
// - allow a single fragment.qSA to fetch all elements being removed
- var fragment = document.createDocumentFragment();
+ var fragment = window.document.createDocumentFragment();
for (i = 0; i < removeCount; i++) {
fragment.appendChild(elementsToRemove[i]);
}
@@ -9079,40 +10423,50 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
- // Set up $watches for isolate scope and controller bindings. This process
- // only occurs for isolate scopes and new scopes with controllerAs.
+ // Set up $watches for isolate scope and controller bindings.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
- forEach(bindings, function(definition, scopeName) {
+ var initialChanges = {};
+ var changes;
+ forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
- mode = definition.mode, // @, =, or &
+ mode = definition.mode, // @, =, <, or &
lastValue,
- parentGet, parentSet, compare;
+ parentGet, parentSet, compare, removeWatch;
switch (mode) {
case '@':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
- destination[scopeName] = attrs[attrName] = void 0;
+ destination[scopeName] = attrs[attrName] = undefined;
}
- attrs.$observe(attrName, function(value) {
- if (isString(value)) {
+ removeWatch = attrs.$observe(attrName, function(value) {
+ if (isString(value) || isBoolean(value)) {
+ var oldValue = destination[scopeName];
+ recordChanges(scopeName, value, oldValue);
destination[scopeName] = value;
}
});
attrs.$$observers[attrName].$$scope = scope;
- if (isString(attrs[attrName])) {
+ lastValue = attrs[attrName];
+ if (isString(lastValue)) {
// If the attribute has been provided then we trigger an interpolation to ensure
// the value is there for use in the link fn
- destination[scopeName] = $interpolate(attrs[attrName])(scope);
+ destination[scopeName] = $interpolate(lastValue)(scope);
+ } else if (isBoolean(lastValue)) {
+ // If the attributes is one of the BOOLEAN_ATTR then Angular will have converted
+ // the value to boolean rather than a string, so we special case this situation
+ destination[scopeName] = lastValue;
}
+ initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
+ removeWatchCollection.push(removeWatch);
break;
case '=':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
- attrs[attrName] = void 0;
+ attrs[attrName] = undefined;
}
if (optional && !attrs[attrName]) break;
@@ -9120,14 +10474,15 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
if (parentGet.literal) {
compare = equals;
} else {
- compare = function(a, b) { return a === b || (a !== a && b !== b); };
+ // eslint-disable-next-line no-self-compare
+ compare = function simpleCompare(a, b) { return a === b || (a !== a && b !== b); };
}
parentSet = parentGet.assign || function() {
// reset the change, or we will throw this exception on every $digest
lastValue = destination[scopeName] = parentGet(scope);
throw $compileMinErr('nonassign',
- "Expression '{0}' used with directive '{1}' is non-assignable!",
- attrs[attrName], directive.name);
+ 'Expression \'{0}\' in attribute \'{1}\' used with directive \'{2}\' is non-assignable!',
+ attrs[attrName], attrName, directive.name);
};
lastValue = destination[scopeName] = parentGet(scope);
var parentValueWatch = function parentValueWatch(parentValue) {
@@ -9141,10 +10496,10 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
parentSet(scope, parentValue = destination[scopeName]);
}
}
- return lastValue = parentValue;
+ lastValue = parentValue;
+ return lastValue;
};
parentValueWatch.$stateful = true;
- var removeWatch;
if (definition.collection) {
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
@@ -9153,6 +10508,33 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
removeWatchCollection.push(removeWatch);
break;
+ case '<':
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = undefined;
+ }
+ if (optional && !attrs[attrName]) break;
+
+ parentGet = $parse(attrs[attrName]);
+ var deepWatch = parentGet.literal;
+
+ var initialValue = destination[scopeName] = parentGet(scope);
+ initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
+
+ removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
+ if (oldValue === newValue) {
+ if (oldValue === initialValue || (deepWatch && equals(oldValue, initialValue))) {
+ return;
+ }
+ oldValue = initialValue;
+ }
+ recordChanges(scopeName, newValue, oldValue);
+ destination[scopeName] = newValue;
+ }, deepWatch);
+
+ removeWatchCollection.push(removeWatch);
+ break;
+
case '&':
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
@@ -9167,22 +10549,65 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
});
- return removeWatchCollection.length && function removeWatches() {
- for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
- removeWatchCollection[i]();
+ function recordChanges(key, currentValue, previousValue) {
+ if (isFunction(destination.$onChanges) && currentValue !== previousValue &&
+ // eslint-disable-next-line no-self-compare
+ (currentValue === currentValue || previousValue === previousValue)) {
+ // If we have not already scheduled the top level onChangesQueue handler then do so now
+ if (!onChangesQueue) {
+ scope.$$postDigest(flushOnChangesQueue);
+ onChangesQueue = [];
+ }
+ // If we have not already queued a trigger of onChanges for this controller then do so now
+ if (!changes) {
+ changes = {};
+ onChangesQueue.push(triggerOnChangesHook);
+ }
+ // If the has been a change on this property already then we need to reuse the previous value
+ if (changes[key]) {
+ previousValue = changes[key].previousValue;
+ }
+ // Store this change
+ changes[key] = new SimpleChange(previousValue, currentValue);
+ }
+ }
+
+ function triggerOnChangesHook() {
+ destination.$onChanges(changes);
+ // Now clear the changes so that we schedule onChanges when more changes arrive
+ changes = undefined;
+ }
+
+ return {
+ initialChanges: initialChanges,
+ removeWatches: removeWatchCollection.length && function removeWatches() {
+ for (var i = 0, ii = removeWatchCollection.length; i < ii; ++i) {
+ removeWatchCollection[i]();
+ }
}
};
}
}];
}
-var PREFIX_REGEXP = /^((?:x|data)[\:\-_])/i;
+function SimpleChange(previous, current) {
+ this.previousValue = previous;
+ this.currentValue = current;
+}
+SimpleChange.prototype.isFirstChange = function() { return this.previousValue === _UNINITIALIZED_VALUE; };
+
+
+var PREFIX_REGEXP = /^((?:x|data)[:\-_])/i;
+var SPECIAL_CHARS_REGEXP = /[:\-_]+(.)/g;
+
/**
* Converts all accepted directives format into proper directive name.
* @param name Name to normalize
*/
function directiveNormalize(name) {
- return camelCase(name.replace(PREFIX_REGEXP, ''));
+ return name
+ .replace(PREFIX_REGEXP, '')
+ .replace(SPECIAL_CHARS_REGEXP, fnCamelCaseReplace);
}
/**
@@ -9254,7 +10679,7 @@ function tokenDifference(str1, str2) {
for (var i = 0; i < tokens1.length; i++) {
var token = tokens1[i];
for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
+ if (token === tokens2[j]) continue outer;
}
values += (values.length > 0 ? ' ' : '') + token;
}
@@ -9271,8 +10696,9 @@ function removeComments(jqNodes) {
while (i--) {
var node = jqNodes[i];
- if (node.nodeType === NODE_TYPE_COMMENT) {
- splice.call(jqNodes, i, 1);
+ if (node.nodeType === NODE_TYPE_COMMENT ||
+ (node.nodeType === NODE_TYPE_TEXT && node.nodeValue.trim() === '')) {
+ splice.call(jqNodes, i, 1);
}
}
return jqNodes;
@@ -9281,7 +10707,7 @@ function removeComments(jqNodes) {
var $controllerMinErr = minErr('$controller');
-var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
+var CNTRL_REG = /^(\S+)(\s+as\s+([\w$]+))?$/;
function identifierForController(controller, ident) {
if (ident && isString(ident)) return ident;
if (isString(controller)) {
@@ -9294,6 +10720,8 @@ function identifierForController(controller, ident) {
/**
* @ngdoc provider
* @name $controllerProvider
+ * @this
+ *
* @description
* The {@link ng.$controller $controller service} is used by Angular to create new
* controllers.
@@ -9307,6 +10735,15 @@ function $ControllerProvider() {
/**
* @ngdoc method
+ * @name $controllerProvider#has
+ * @param {string} name Controller name to check.
+ */
+ this.has = function(name) {
+ return controllers.hasOwnProperty(name);
+ };
+
+ /**
+ * @ngdoc method
* @name $controllerProvider#register
* @param {string|Object} name Controller name, or an object map of controllers where the keys are
* the names and the values are the constructors.
@@ -9326,6 +10763,11 @@ function $ControllerProvider() {
* @ngdoc method
* @name $controllerProvider#allowGlobals
* @description If called, allows `$controller` to find controller constructors on `window`
+ *
+ * @deprecated
+ * sinceVersion="v1.3.0"
+ * removeVersion="v1.7.0"
+ * This method of finding controllers has been deprecated.
*/
this.allowGlobals = function() {
globals = true;
@@ -9346,7 +10788,7 @@ function $ControllerProvider() {
* * check if a controller with given name is registered via `$controllerProvider`
* * check if evaluating the string on the current scope returns a constructor
* * if $controllerProvider#allowGlobals, check `window[constructor]` on the global
- * `window` object (not recommended)
+ * `window` object (deprecated, not recommended)
*
* The string can use the `controller as property` syntax, where the controller instance is published
* as the specified property on the `scope`; the `scope` must be injected into `locals` param for this
@@ -9361,7 +10803,7 @@ function $ControllerProvider() {
* It's just a simple call to {@link auto.$injector $injector}, but extracted into
* a service, so that one can override this service with [BC version](https://gist.github.com/1649788).
*/
- return function(expression, locals, later, ident) {
+ return function $controller(expression, locals, later, ident) {
// PRIVATE API:
// param `later` --- indicates that the controller's constructor is invoked at a later time.
// If true, $controller will allocate the object with the correct
@@ -9379,16 +10821,21 @@ function $ControllerProvider() {
match = expression.match(CNTRL_REG);
if (!match) {
throw $controllerMinErr('ctrlfmt',
- "Badly formed controller string '{0}'. " +
- "Must match `__name__ as __id__` or `__name__`.", expression);
+ 'Badly formed controller string \'{0}\'. ' +
+ 'Must match `__name__ as __id__` or `__name__`.', expression);
}
- constructor = match[1],
+ constructor = match[1];
identifier = identifier || match[3];
expression = controllers.hasOwnProperty(constructor)
? controllers[constructor]
: getter(locals.$scope, constructor, true) ||
(globals ? getter($window, constructor, true) : undefined);
+ if (!expression) {
+ throw $controllerMinErr('ctrlreg',
+ 'The controller with the name \'{0}\' is not registered.', constructor);
+ }
+
assertArgFn(expression, constructor, true);
}
@@ -9411,8 +10858,7 @@ function $ControllerProvider() {
addIdentifier(locals, identifier, instance, constructor || expression.name);
}
- var instantiate;
- return instantiate = extend(function() {
+ return extend(function $controllerInit() {
var result = $injector.invoke(expression, instance, locals, constructor);
if (result !== instance && (isObject(result) || isFunction(result))) {
instance = result;
@@ -9440,7 +10886,7 @@ function $ControllerProvider() {
function addIdentifier(locals, identifier, instance, name) {
if (!(locals && isObject(locals.$scope))) {
throw minErr('$controller')('noscp',
- "Cannot export controller '{0}' as '{1}'! No $scope object provided via `locals`.",
+ 'Cannot export controller \'{0}\' as \'{1}\'! No $scope object provided via `locals`.',
name, identifier);
}
@@ -9453,12 +10899,13 @@ function $ControllerProvider() {
* @ngdoc service
* @name $document
* @requires $window
+ * @this
*
* @description
* A {@link angular.element jQuery or jqLite} wrapper for the browser's `window.document` object.
*
* @example
- <example module="documentExample">
+ <example module="documentExample" name="document">
<file name="index.html">
<div ng-controller="ExampleController">
<p>$document title: <b ng-bind="title"></b></p>
@@ -9480,10 +10927,38 @@ function $DocumentProvider() {
}];
}
+
+/**
+ * @private
+ * @this
+ * Listens for document visibility change and makes the current status accessible.
+ */
+function $$IsDocumentHiddenProvider() {
+ this.$get = ['$document', '$rootScope', function($document, $rootScope) {
+ var doc = $document[0];
+ var hidden = doc && doc.hidden;
+
+ $document.on('visibilitychange', changeListener);
+
+ $rootScope.$on('$destroy', function() {
+ $document.off('visibilitychange', changeListener);
+ });
+
+ function changeListener() {
+ hidden = doc.hidden;
+ }
+
+ return function() {
+ return hidden;
+ };
+ }];
+}
+
/**
* @ngdoc service
* @name $exceptionHandler
* @requires ng.$log
+ * @this
*
* @description
* Any uncaught exception in angular expressions is delegated to this service.
@@ -9495,18 +10970,21 @@ function $DocumentProvider() {
*
* ## Example:
*
+ * The example below will overwrite the default `$exceptionHandler` in order to (a) log uncaught
+ * errors to the backend for later inspection by the developers and (b) to use `$log.warn()` instead
+ * of `$log.error()`.
+ *
* ```js
- * angular.module('exceptionOverride', []).factory('$exceptionHandler', function() {
- * return function(exception, cause) {
- * exception.message += ' (caused by "' + cause + '")';
- * throw exception;
- * };
- * });
+ * angular.
+ * module('exceptionOverwrite', []).
+ * factory('$exceptionHandler', ['$log', 'logErrorsToBackend', function($log, logErrorsToBackend) {
+ * return function myExceptionHandler(exception, cause) {
+ * logErrorsToBackend(exception, cause);
+ * $log.warn(exception, cause);
+ * };
+ * }]);
* ```
*
- * This example will override the normal action of `$exceptionHandler`, to make angular
- * exceptions fail hard when they happen, instead of just logging to the console.
- *
* <hr />
* Note, that code executed in event-listeners (even those registered using jqLite's `on`/`bind`
* methods) does not delegate exceptions to the {@link ng.$exceptionHandler $exceptionHandler}
@@ -9516,7 +10994,7 @@ function $DocumentProvider() {
* `try { ... } catch(e) { $exceptionHandler(e); }`
*
* @param {Error} exception Exception associated with the error.
- * @param {string=} cause optional information about the context in which
+ * @param {string=} cause Optional information about the context in which
* the error was thrown.
*
*/
@@ -9528,7 +11006,7 @@ function $ExceptionHandlerProvider() {
}];
}
-var $$ForceReflowProvider = function() {
+var $$ForceReflowProvider = /** @this */ function() {
this.$get = ['$document', function($document) {
return function(domNode) {
//the line below will force the browser to perform a repaint so
@@ -9558,13 +11036,8 @@ var JSON_ENDS = {
'[': /]$/,
'{': /}$/
};
-var JSON_PROTECTION_PREFIX = /^\)\]\}',?\n/;
+var JSON_PROTECTION_PREFIX = /^\)]\}',?\n/;
var $httpMinErr = minErr('$http');
-var $httpMinErrLegacyFn = function(method) {
- return function() {
- throw $httpMinErr('legacy', 'The method `{0}` on the promise returned from `$http` has been disabled.', method);
- };
-};
function serializeValue(v) {
if (isObject(v)) {
@@ -9574,6 +11047,7 @@ function serializeValue(v) {
}
+/** @this */
function $HttpParamSerializerProvider() {
/**
* @ngdoc service
@@ -9586,7 +11060,7 @@ function $HttpParamSerializerProvider() {
* * `{'foo': 'bar'}` results in `foo=bar`
* * `{'foo': Date.now()}` results in `foo=2015-04-01T09%3A50%3A49.262Z` (`toISOString()` and encoded representation of a Date object)
* * `{'foo': ['bar', 'baz']}` results in `foo=bar&foo=baz` (repeated key for each array element)
- * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D"` (stringified and encoded representation of an object)
+ * * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
*
* Note that serializer will sort the request parameters alphabetically.
* */
@@ -9598,7 +11072,7 @@ function $HttpParamSerializerProvider() {
forEachSorted(params, function(value, key) {
if (value === null || isUndefined(value)) return;
if (isArray(value)) {
- forEach(value, function(v, k) {
+ forEach(value, function(v) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
});
} else {
@@ -9611,10 +11085,12 @@ function $HttpParamSerializerProvider() {
};
}
+/** @this */
function $HttpParamSerializerJQLikeProvider() {
/**
* @ngdoc service
* @name $httpParamSerializerJQLike
+ *
* @description
*
* Alternative {@link $http `$http`} params serializer that follows
@@ -9743,7 +11219,7 @@ function parseHeaders(headers) {
* @param {(string|Object)} headers Headers to provide access to.
* @returns {function(string=)} Returns a getter function which if called with:
*
- * - if called with single an argument returns a single header value or null
+ * - if called with an argument returns a single header value or null
* - if called with no arguments returns an object containing all headers.
*/
function headersGetter(headers) {
@@ -9754,7 +11230,7 @@ function headersGetter(headers) {
if (name) {
var value = headersObj[lowercase(name)];
- if (value === void 0) {
+ if (value === undefined) {
value = null;
}
return value;
@@ -9797,6 +11273,8 @@ function isSuccess(status) {
/**
* @ngdoc provider
* @name $httpProvider
+ * @this
+ *
* @description
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
* */
@@ -9808,10 +11286,9 @@ function $HttpProvider() {
*
* Object containing default values for all {@link ng.$http $http} requests.
*
- * - **`defaults.cache`** - {Object} - an object built with {@link ng.$cacheFactory `$cacheFactory`}
- * that will provide the cache for all requests who set their `cache` property to `true`.
- * If you set the `defaults.cache = false` then only requests that specify their own custom
- * cache object will be cached. See {@link $http#caching $http Caching} for more information.
+ * - **`defaults.cache`** - {boolean|Object} - A boolean value or object created with
+ * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of HTTP responses
+ * by default. See {@link $http#caching $http Caching} for more information.
*
* - **`defaults.xsrfCookieName`** - {string} - Name of cookie containing the XSRF token.
* Defaults value is `'XSRF-TOKEN'`.
@@ -9833,6 +11310,10 @@ function $HttpProvider() {
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
*
+ * - **`defaults.jsonpCallbackParam`** - `{string}` - the name of the query parameter that passes the name of the
+ * callback in a JSONP request. The value of this parameter will be replaced with the expression generated by the
+ * {@link $jsonpCallbacks} service. Defaults to `'callback'`.
+ *
**/
var defaults = this.defaults = {
// transform incoming response data
@@ -9856,7 +11337,9 @@ function $HttpProvider() {
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
- paramSerializer: '$httpParamSerializer'
+ paramSerializer: '$httpParamSerializer',
+
+ jsonpCallbackParam: 'callback'
};
var useApplyAsync = false;
@@ -9887,30 +11370,6 @@ function $HttpProvider() {
return useApplyAsync;
};
- var useLegacyPromise = true;
- /**
- * @ngdoc method
- * @name $httpProvider#useLegacyPromiseExtensions
- * @description
- *
- * Configure `$http` service to return promises without the shorthand methods `success` and `error`.
- * This should be used to make sure that applications work without these methods.
- *
- * Defaults to true. If no value is specified, returns the current configured value.
- *
- * @param {boolean=} value If true, `$http` will return a promise with the deprecated legacy `success` and `error` methods.
- *
- * @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
- * otherwise, returns the current configured value.
- **/
- this.useLegacyPromiseExtensions = function(value) {
- if (isDefined(value)) {
- useLegacyPromise = !!value;
- return this;
- }
- return useLegacyPromise;
- };
-
/**
* @ngdoc property
* @name $httpProvider#interceptors
@@ -9926,8 +11385,8 @@ function $HttpProvider() {
**/
var interceptorFactories = this.interceptors = [];
- this.$get = ['$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
- function($httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
+ this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector', '$sce',
+ function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector, $sce) {
var defaultCache = $cacheFactory('$http');
@@ -10002,10 +11461,13 @@ function $HttpProvider() {
* - **config** – `{Object}` – The configuration object that was used to generate the request.
* - **statusText** – `{string}` – HTTP status text of the response.
*
- * A response status code between 200 and 299 is considered a success status and
- * will result in the success callback being called. Note that if the response is a redirect,
- * XMLHttpRequest will transparently follow it, meaning that the error callback will not be
- * called for such responses.
+ * A response status code between 200 and 299 is considered a success status and will result in
+ * the success callback being called. Any response status code outside of that range is
+ * considered an error status and will result in the error callback being called.
+ * Also, status codes less than -1 are normalized to zero. -1 usually means the request was
+ * aborted, e.g. using a `config.timeout`.
+ * Note that if the response is a redirect, XMLHttpRequest will transparently follow it, meaning
+ * that the outcome (success or error) will be determined by the final response status code.
*
*
* ## Shortcut methods
@@ -10041,14 +11503,6 @@ function $HttpProvider() {
* $httpBackend.flush();
* ```
*
- * ## Deprecation Notice
- * <div class="alert alert-danger">
- * The `$http` legacy promise methods `success` and `error` have been deprecated.
- * Use the standard `then` method instead.
- * If {@link $httpProvider#useLegacyPromiseExtensions `$httpProvider.useLegacyPromiseExtensions`} is set to
- * `false` then these methods will throw {@link $http:legacy `$http/legacy`} error.
- * </div>
- *
* ## Setting HTTP Headers
*
* The $http service will automatically add certain HTTP headers to all requests. These defaults
@@ -10056,7 +11510,7 @@ function $HttpProvider() {
* object, which currently contains this default configuration:
*
* - `$httpProvider.defaults.headers.common` (headers that are common for all requests):
- * - `Accept: application/json, text/plain, * / *`
+ * - <code>Accept: application/json, text/plain, \*&#65279;/&#65279;\*</code>
* - `$httpProvider.defaults.headers.post`: (header defaults for POST requests)
* - `Content-Type: application/json`
* - `$httpProvider.defaults.headers.put` (header defaults for PUT requests)
@@ -10102,6 +11556,15 @@ function $HttpProvider() {
* the transformed value (`function(data, headersGetter, status)`) or an array of such transformation functions,
* which allows you to `push` or `unshift` a new transformation function into the transformation chain.
*
+ * <div class="alert alert-warning">
+ * **Note:** Angular does not make a copy of the `data` parameter before it is passed into the `transformRequest` pipeline.
+ * That means changes to the properties of `data` are not local to the transform function (since Javascript passes objects by reference).
+ * For example, when calling `$http.get(url, $scope.myObject)`, modifications to the object's properties in a transformRequest
+ * function will be reflected on the scope and in any templates where the object is data-bound.
+ * To prevent this, transform functions should have no side-effects.
+ * If you need to modify properties, it is recommended to make a copy of the data, or create new object to return.
+ * </div>
+ *
* ### Default Transformations
*
* The `$httpProvider` provider and `$http` service expose `defaults.transformRequest` and
@@ -10126,7 +11589,7 @@ function $HttpProvider() {
*
* ### Overriding the Default Transformations Per Request
*
- * If you wish override the request/response transformations only for a single request then provide
+ * If you wish to override the request/response transformations only for a single request then provide
* `transformRequest` and/or `transformResponse` properties on the configuration object passed
* into `$http`.
*
@@ -10159,26 +11622,35 @@ function $HttpProvider() {
*
* ## Caching
*
- * To enable caching, set the request configuration `cache` property to `true` (to use default
- * cache) or to a custom cache object (built with {@link ng.$cacheFactory `$cacheFactory`}).
- * When the cache is enabled, `$http` stores the response from the server in the specified
- * cache. The next time the same request is made, the response is served from the cache without
- * sending a request to the server.
+ * {@link ng.$http `$http`} responses are not cached by default. To enable caching, you must
+ * set the config.cache value or the default cache value to TRUE or to a cache object (created
+ * with {@link ng.$cacheFactory `$cacheFactory`}). If defined, the value of config.cache takes
+ * precedence over the default cache value.
+ *
+ * In order to:
+ * * cache all responses - set the default cache value to TRUE or to a cache object
+ * * cache a specific response - set config.cache value to TRUE or to a cache object
*
- * Note that even if the response is served from cache, delivery of the data is asynchronous in
- * the same way that real requests are.
+ * If caching is enabled, but neither the default cache nor config.cache are set to a cache object,
+ * then the default `$cacheFactory("$http")` object is used.
*
- * If there are multiple GET requests for the same URL that should be cached using the same
- * cache, but the cache is not populated yet, only one request to the server will be made and
- * the remaining requests will be fulfilled using the response from the first request.
+ * The default cache value can be set by updating the
+ * {@link ng.$http#defaults `$http.defaults.cache`} property or the
+ * {@link $httpProvider#defaults `$httpProvider.defaults.cache`} property.
*
- * You can change the default cache to a new object (built with
- * {@link ng.$cacheFactory `$cacheFactory`}) by updating the
- * {@link ng.$http#defaults `$http.defaults.cache`} property. All requests who set
- * their `cache` property to `true` will now use this cache object.
+ * When caching is enabled, {@link ng.$http `$http`} stores the response from the server using
+ * the relevant cache object. The next time the same request is made, the response is returned
+ * from the cache without sending a request to the server.
+ *
+ * Take note that:
+ *
+ * * Only GET and JSONP requests are cached.
+ * * The cache key is the request URL including search parameters; headers are not considered.
+ * * Cached responses are returned asynchronously, in the same way as responses from the server.
+ * * If multiple identical requests are made using the same cache, which is not yet populated,
+ * one request will be made to the server and remaining requests will return the same response.
+ * * A cache-control header on the response does not affect if or how responses are cached.
*
- * If you set the default cache to `false` then only requests that specify their own custom
- * cache object will be cached.
*
* ## Interceptors
*
@@ -10300,13 +11772,13 @@ function $HttpProvider() {
*
* ### Cross Site Request Forgery (XSRF) Protection
*
- * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is a technique by which
- * an unauthorized site can gain your user's private data. Angular provides a mechanism
- * to counter XSRF. When performing XHR requests, the $http service reads a token from a cookie
- * (by default, `XSRF-TOKEN`) and sets it as an HTTP header (`X-XSRF-TOKEN`). Since only
- * JavaScript that runs on your domain could read the cookie, your server can be assured that
- * the XHR came from JavaScript running on your domain. The header will not be set for
- * cross-domain requests.
+ * [XSRF](http://en.wikipedia.org/wiki/Cross-site_request_forgery) is an attack technique by
+ * which the attacker can trick an authenticated user into unknowingly executing actions on your
+ * website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
+ * $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
+ * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
+ * cookie, your server can be assured that the XHR came from JavaScript running on your domain.
+ * The header will not be set for cross-domain requests.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
@@ -10328,13 +11800,20 @@ function $HttpProvider() {
* processed. The object has following properties:
*
* - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc)
- * - **url** – `{string}` – Absolute or relative URL of the resource that is being requested.
+ * - **url** – `{string|TrustedObject}` – Absolute or relative URL of the resource that is being requested;
+ * or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* - **params** – `{Object.<string|Object>}` – Map of strings or objects which will be serialized
* with the `paramSerializer` and appended as GET parameters.
* - **data** – `{string|Object}` – Data to be sent as the request message data.
* - **headers** – `{Object}` – Map of strings or functions which return strings representing
* HTTP headers to send to the server. If the return value of a function is null, the
* header will not be sent. Functions accept a config object as an argument.
+ * - **eventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest object.
+ * To bind events to the XMLHttpRequest upload object, use `uploadEventHandlers`.
+ * The handler will be called in the context of a `$apply` block.
+ * - **uploadEventHandlers** - `{Object}` - Event listeners to be bound to the XMLHttpRequest upload
+ * object. To bind events to the XMLHttpRequest object, use `eventHandlers`.
+ * The handler will be called in the context of a `$apply` block.
* - **xsrfHeaderName** – `{string}` – Name of HTTP header to populate with the XSRF token.
* - **xsrfCookieName** – `{string}` – Name of cookie containing the XSRF token.
* - **transformRequest** –
@@ -10348,7 +11827,7 @@ function $HttpProvider() {
* transform function or an array of such functions. The transform function takes the http
* response body, headers and status and returns its transformed (typically deserialized) version.
* See {@link ng.$http#overriding-the-default-transformations-per-request
- * Overriding the Default TransformationjqLiks}
+ * Overriding the Default Transformations}
* - **paramSerializer** - `{string|function(Object<string,string>):string}` - A function used to
* prepare the string representation of request parameters (specified as an object).
* If specified as string, it is interpreted as function registered with the
@@ -10356,10 +11835,9 @@ function $HttpProvider() {
* by registering it as a {@link auto.$provide#service service}.
* The default serializer is the {@link $httpParamSerializer $httpParamSerializer};
* alternatively, you can use the {@link $httpParamSerializerJQLike $httpParamSerializerJQLike}
- * - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the
- * GET request, otherwise if a cache instance built with
- * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for
- * caching.
+ * - **cache** – `{boolean|Object}` – A boolean value or object created with
+ * {@link ng.$cacheFactory `$cacheFactory`} to enable or disable caching of the HTTP response.
+ * See {@link $http#caching $http Caching} for more information.
* - **timeout** – `{number|Promise}` – timeout in milliseconds, or {@link ng.$q promise}
* that should abort the request when resolved.
* - **withCredentials** - `{boolean}` - whether to set the `withCredentials` flag on the
@@ -10377,7 +11855,7 @@ function $HttpProvider() {
*
*
* @example
-<example module="httpExample">
+<example module="httpExample" name="http-service">
<file name="index.html">
<div ng-controller="FetchController">
<select ng-model="method" aria-label="Request method">
@@ -10389,11 +11867,11 @@ function $HttpProvider() {
<button id="samplegetbtn" ng-click="updateModel('GET', 'http-hello.html')">Sample GET</button>
<button id="samplejsonpbtn"
ng-click="updateModel('JSONP',
- 'https://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">
+ 'https://angularjs.org/greet.php?name=Super%20Hero')">
Sample JSONP
</button>
<button id="invalidjsonpbtn"
- ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist&callback=JSON_CALLBACK')">
+ ng-click="updateModel('JSONP', 'https://angularjs.org/doesntexist')">
Invalid JSONP
</button>
<pre>http status code: {{status}}</pre>
@@ -10402,6 +11880,13 @@ function $HttpProvider() {
</file>
<file name="script.js">
angular.module('httpExample', [])
+ .config(['$sceDelegateProvider', function($sceDelegateProvider) {
+ // We must whitelist the JSONP endpoint that we are using to show that we trust it
+ $sceDelegateProvider.resourceUrlWhitelist([
+ 'self',
+ 'https://angularjs.org/**'
+ ]);
+ }])
.controller('FetchController', ['$scope', '$http', '$templateCache',
function($scope, $http, $templateCache) {
$scope.method = 'GET';
@@ -10416,7 +11901,7 @@ function $HttpProvider() {
$scope.status = response.status;
$scope.data = response.data;
}, function(response) {
- $scope.data = response.data || "Request failed";
+ $scope.data = response.data || 'Request failed';
$scope.status = response.status;
});
};
@@ -10435,7 +11920,6 @@ function $HttpProvider() {
var data = element(by.binding('data'));
var fetchBtn = element(by.id('fetchbtn'));
var sampleGetBtn = element(by.id('samplegetbtn'));
- var sampleJsonpBtn = element(by.id('samplejsonpbtn'));
var invalidJsonpBtn = element(by.id('invalidjsonpbtn'));
it('should make an xhr GET request', function() {
@@ -10447,6 +11931,7 @@ function $HttpProvider() {
// Commented out due to flakes. See https://github.com/angular/angular.js/issues/9185
// it('should make a JSONP request to angularjs.org', function() {
+// var sampleJsonpBtn = element(by.id('samplejsonpbtn'));
// sampleJsonpBtn.click();
// fetchBtn.click();
// expect(status.getText()).toMatch('200');
@@ -10469,92 +11954,62 @@ function $HttpProvider() {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}
+ if (!isString($sce.valueOf(requestConfig.url))) {
+ throw minErr('$http')('badreq', 'Http request configuration url must be a string or a $sce trusted object. Received: {0}', requestConfig.url);
+ }
+
var config = extend({
method: 'get',
transformRequest: defaults.transformRequest,
transformResponse: defaults.transformResponse,
- paramSerializer: defaults.paramSerializer
+ paramSerializer: defaults.paramSerializer,
+ jsonpCallbackParam: defaults.jsonpCallbackParam
}, requestConfig);
config.headers = mergeHeaders(requestConfig);
config.method = uppercase(config.method);
config.paramSerializer = isString(config.paramSerializer) ?
- $injector.get(config.paramSerializer) : config.paramSerializer;
+ $injector.get(config.paramSerializer) : config.paramSerializer;
- var serverRequest = function(config) {
- var headers = config.headers;
- var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
+ $browser.$$incOutstandingRequestCount();
- // strip content-type if data is undefined
- if (isUndefined(reqData)) {
- forEach(headers, function(value, header) {
- if (lowercase(header) === 'content-type') {
- delete headers[header];
- }
- });
- }
-
- if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
- config.withCredentials = defaults.withCredentials;
- }
-
- // send request
- return sendReq(config, reqData).then(transformResponse, transformResponse);
- };
-
- var chain = [serverRequest, undefined];
- var promise = $q.when(config);
+ var requestInterceptors = [];
+ var responseInterceptors = [];
+ var promise = $q.resolve(config);
// apply interceptors
forEach(reversedInterceptors, function(interceptor) {
if (interceptor.request || interceptor.requestError) {
- chain.unshift(interceptor.request, interceptor.requestError);
+ requestInterceptors.unshift(interceptor.request, interceptor.requestError);
}
if (interceptor.response || interceptor.responseError) {
- chain.push(interceptor.response, interceptor.responseError);
+ responseInterceptors.push(interceptor.response, interceptor.responseError);
}
});
- while (chain.length) {
- var thenFn = chain.shift();
- var rejectFn = chain.shift();
+ promise = chainInterceptors(promise, requestInterceptors);
+ promise = promise.then(serverRequest);
+ promise = chainInterceptors(promise, responseInterceptors);
+ promise = promise.finally(completeOutstandingRequest);
- promise = promise.then(thenFn, rejectFn);
- }
+ return promise;
- if (useLegacyPromise) {
- promise.success = function(fn) {
- assertArgFn(fn, 'fn');
- promise.then(function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
+ function chainInterceptors(promise, interceptors) {
+ for (var i = 0, ii = interceptors.length; i < ii;) {
+ var thenFn = interceptors[i++];
+ var rejectFn = interceptors[i++];
- promise.error = function(fn) {
- assertArgFn(fn, 'fn');
+ promise = promise.then(thenFn, rejectFn);
+ }
- promise.then(null, function(response) {
- fn(response.data, response.status, response.headers, config);
- });
- return promise;
- };
- } else {
- promise.success = $httpMinErrLegacyFn('success');
- promise.error = $httpMinErrLegacyFn('error');
- }
+ interceptors.length = 0;
- return promise;
+ return promise;
+ }
- function transformResponse(response) {
- // make a copy since the response must be cacheable
- var resp = extend({}, response);
- resp.data = transformData(response.data, response.headers, response.status,
- config.transformResponse);
- return (isSuccess(response.status))
- ? resp
- : $q.reject(resp);
+ function completeOutstandingRequest() {
+ $browser.$$completeOutstandingRequest(noop);
}
function executeHeaderFns(headers, config) {
@@ -10581,7 +12036,7 @@ function $HttpProvider() {
defHeaders = extend({}, defHeaders.common, defHeaders[lowercase(config.method)]);
- // using for-in instead of forEach to avoid unecessary iteration after header has been found
+ // using for-in instead of forEach to avoid unnecessary iteration after header has been found
defaultHeadersIteration:
for (defHeaderName in defHeaders) {
lowercaseDefHeaderName = lowercase(defHeaderName);
@@ -10598,6 +12053,37 @@ function $HttpProvider() {
// execute if header value is a function for merged headers
return executeHeaderFns(reqHeaders, shallowCopy(config));
}
+
+ function serverRequest(config) {
+ var headers = config.headers;
+ var reqData = transformData(config.data, headersGetter(headers), undefined, config.transformRequest);
+
+ // strip content-type if data is undefined
+ if (isUndefined(reqData)) {
+ forEach(headers, function(value, header) {
+ if (lowercase(header) === 'content-type') {
+ delete headers[header];
+ }
+ });
+ }
+
+ if (isUndefined(config.withCredentials) && !isUndefined(defaults.withCredentials)) {
+ config.withCredentials = defaults.withCredentials;
+ }
+
+ // send request
+ return sendReq(config, reqData).then(transformResponse, transformResponse);
+ }
+
+ function transformResponse(response) {
+ // make a copy since the response must be cacheable
+ var resp = extend({}, response);
+ resp.data = transformData(response.data, response.headers, response.status,
+ config.transformResponse);
+ return (isSuccess(response.status))
+ ? resp
+ : $q.reject(resp);
+ }
}
$http.pendingRequests = [];
@@ -10609,7 +12095,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `GET` request.
*
- * @param {string} url Relative or absolute URL specifying the destination of the request
+ * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
+ * or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
@@ -10621,7 +12108,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `DELETE` request.
*
- * @param {string} url Relative or absolute URL specifying the destination of the request
+ * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
+ * or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
@@ -10633,7 +12121,8 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `HEAD` request.
*
- * @param {string} url Relative or absolute URL specifying the destination of the request
+ * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
+ * or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
@@ -10645,8 +12134,33 @@ function $HttpProvider() {
* @description
* Shortcut method to perform `JSONP` request.
*
- * @param {string} url Relative or absolute URL specifying the destination of the request.
- * The name of the callback should be the string `JSON_CALLBACK`.
+ * Note that, since JSONP requests are sensitive because the response is given full access to the browser,
+ * the url must be declared, via {@link $sce} as a trusted resource URL.
+ * You can trust a URL by adding it to the whitelist via
+ * {@link $sceDelegateProvider#resourceUrlWhitelist `$sceDelegateProvider.resourceUrlWhitelist`} or
+ * by explicitly trusting the URL via {@link $sce#trustAsResourceUrl `$sce.trustAsResourceUrl(url)`}.
+ *
+ * JSONP requests must specify a callback to be used in the response from the server. This callback
+ * is passed as a query parameter in the request. You must specify the name of this parameter by
+ * setting the `jsonpCallbackParam` property on the request config object.
+ *
+ * ```
+ * $http.jsonp('some/trusted/url', {jsonpCallbackParam: 'callback'})
+ * ```
+ *
+ * You can also specify a default callback parameter name in `$http.defaults.jsonpCallbackParam`.
+ * Initially this is set to `'callback'`.
+ *
+ * <div class="alert alert-danger">
+ * You can no longer use the `JSON_CALLBACK` string as a placeholder for specifying where the callback
+ * parameter value should go.
+ * </div>
+ *
+ * If you would like to customise where and how the callbacks are stored then try overriding
+ * or decorating the {@link $jsonpCallbacks} service.
+ *
+ * @param {string|TrustedObject} url Absolute or relative URL of the resource that is being requested;
+ * or an object created by a call to `$sce.trustAsResourceUrl(url)`.
* @param {Object=} config Optional configuration object
* @returns {HttpPromise} Future object
*/
@@ -10745,16 +12259,33 @@ function $HttpProvider() {
cache,
cachedResp,
reqHeaders = config.headers,
- url = buildUrl(config.url, config.paramSerializer(config.params));
+ isJsonp = lowercase(config.method) === 'jsonp',
+ url = config.url;
+
+ if (isJsonp) {
+ // JSONP is a pretty sensitive operation where we're allowing a script to have full access to
+ // our DOM and JS space. So we require that the URL satisfies SCE.RESOURCE_URL.
+ url = $sce.getTrustedResourceUrl(url);
+ } else if (!isString(url)) {
+ // If it is not a string then the URL must be a $sce trusted object
+ url = $sce.valueOf(url);
+ }
+
+ url = buildUrl(url, config.paramSerializer(config.params));
+
+ if (isJsonp) {
+ // Check the url and add the JSONP callback placeholder
+ url = sanitizeJsonpCallbackParam(url, config.jsonpCallbackParam);
+ }
$http.pendingRequests.push(config);
promise.then(removePendingReq, removePendingReq);
-
if ((config.cache || defaults.cache) && config.cache !== false &&
(config.method === 'GET' || config.method === 'JSONP')) {
cache = isObject(config.cache) ? config.cache
- : isObject(defaults.cache) ? defaults.cache
+ : isObject(/** @type {?} */ (defaults).cache)
+ ? /** @type {?} */ (defaults).cache
: defaultCache;
}
@@ -10790,11 +12321,35 @@ function $HttpProvider() {
}
$httpBackend(config.method, url, reqData, done, reqHeaders, config.timeout,
- config.withCredentials, config.responseType);
+ config.withCredentials, config.responseType,
+ createApplyHandlers(config.eventHandlers),
+ createApplyHandlers(config.uploadEventHandlers));
}
return promise;
+ function createApplyHandlers(eventHandlers) {
+ if (eventHandlers) {
+ var applyHandlers = {};
+ forEach(eventHandlers, function(eventHandler, key) {
+ applyHandlers[key] = function(event) {
+ if (useApplyAsync) {
+ $rootScope.$applyAsync(callEventHandler);
+ } else if ($rootScope.$$phase) {
+ callEventHandler();
+ } else {
+ $rootScope.$apply(callEventHandler);
+ }
+
+ function callEventHandler() {
+ eventHandler(event);
+ }
+ };
+ });
+ return applyHandlers;
+ }
+ }
+
/**
* Callback registered to $httpBackend():
@@ -10854,8 +12409,26 @@ function $HttpProvider() {
function buildUrl(url, serializedParams) {
if (serializedParams.length > 0) {
- url += ((url.indexOf('?') == -1) ? '?' : '&') + serializedParams;
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + serializedParams;
+ }
+ return url;
+ }
+
+ function sanitizeJsonpCallbackParam(url, key) {
+ if (/[&?][^=]+=JSON_CALLBACK/.test(url)) {
+ // Throw if the url already contains a reference to JSON_CALLBACK
+ throw $httpMinErr('badjsonp', 'Illegal use of JSON_CALLBACK in url, "{0}"', url);
}
+
+ var callbackParamRegex = new RegExp('[&?]' + key + '=');
+ if (callbackParamRegex.test(url)) {
+ // Throw if the callback param was already provided
+ throw $httpMinErr('badjsonp', 'Illegal use of callback param, "{0}", in url, "{1}"', key, url);
+ }
+
+ // Add in the JSON_CALLBACK callback param value
+ url += ((url.indexOf('?') === -1) ? '?' : '&') + key + '=JSON_CALLBACK';
+
return url;
}
}];
@@ -10864,6 +12437,7 @@ function $HttpProvider() {
/**
* @ngdoc service
* @name $xhrFactory
+ * @this
*
* @description
* Factory function used to create XMLHttpRequest objects.
@@ -10893,9 +12467,10 @@ function $xhrFactoryProvider() {
/**
* @ngdoc service
* @name $httpBackend
- * @requires $window
+ * @requires $jsonpCallbacks
* @requires $document
* @requires $xhrFactory
+ * @this
*
* @description
* HTTP backend used by the {@link ng.$http service} that delegates to
@@ -10908,28 +12483,23 @@ function $xhrFactoryProvider() {
* $httpBackend} which can be trained with responses.
*/
function $HttpBackendProvider() {
- this.$get = ['$browser', '$window', '$document', '$xhrFactory', function($browser, $window, $document, $xhrFactory) {
- return createHttpBackend($browser, $xhrFactory, $browser.defer, $window.angular.callbacks, $document[0]);
+ this.$get = ['$browser', '$jsonpCallbacks', '$document', '$xhrFactory', function($browser, $jsonpCallbacks, $document, $xhrFactory) {
+ return createHttpBackend($browser, $xhrFactory, $browser.defer, $jsonpCallbacks, $document[0]);
}];
}
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
// TODO(vojta): fix the signature
- return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
- $browser.$$incOutstandingRequestCount();
+ return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
url = url || $browser.url();
- if (lowercase(method) == 'jsonp') {
- var callbackId = '_' + (callbacks.counter++).toString(36);
- callbacks[callbackId] = function(data) {
- callbacks[callbackId].data = data;
- callbacks[callbackId].called = true;
- };
-
- var jsonpDone = jsonpReq(url.replace('JSON_CALLBACK', 'angular.callbacks.' + callbackId),
- callbackId, function(status, text) {
- completeRequest(callback, status, callbacks[callbackId].data, "", text);
- callbacks[callbackId] = noop;
+ if (lowercase(method) === 'jsonp') {
+ var callbackPath = callbacks.createCallback(url);
+ var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
+ // jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
+ var response = (status === 200) && callbacks.getResponse(callbackPath);
+ completeRequest(callback, status, response, '', text);
+ callbacks.removeCallback(callbackPath);
});
} else {
@@ -10956,7 +12526,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
// Occurs when accessing file resources or on Android 4.1 stock browser
// while retrieving files from application cache.
if (status === 0) {
- status = response ? 200 : urlResolve(url).protocol == 'file' ? 404 : 0;
+ status = response ? 200 : urlResolve(url).protocol === 'file' ? 404 : 0;
}
completeRequest(callback,
@@ -10974,6 +12544,15 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
xhr.onerror = requestError;
xhr.onabort = requestError;
+ xhr.ontimeout = requestError;
+
+ forEach(eventHandlers, function(value, key) {
+ xhr.addEventListener(key, value);
+ });
+
+ forEach(uploadEventHandlers, function(value, key) {
+ xhr.upload.addEventListener(key, value);
+ });
if (withCredentials) {
xhr.withCredentials = true;
@@ -11007,8 +12586,12 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
function timeoutRequest() {
- jsonpDone && jsonpDone();
- xhr && xhr.abort();
+ if (jsonpDone) {
+ jsonpDone();
+ }
+ if (xhr) {
+ xhr.abort();
+ }
}
function completeRequest(callback, status, response, headersString, statusText) {
@@ -11019,33 +12602,33 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
jsonpDone = xhr = null;
callback(status, response, headersString, statusText);
- $browser.$$completeOutstandingRequest(noop);
}
};
- function jsonpReq(url, callbackId, done) {
+ function jsonpReq(url, callbackPath, done) {
+ url = url.replace('JSON_CALLBACK', callbackPath);
// we can't use jQuery/jqLite here because jQuery does crazy stuff with script elements, e.g.:
// - fetches local scripts via XHR and evals them
// - adds and immediately removes script elements from the document
var script = rawDocument.createElement('script'), callback = null;
- script.type = "text/javascript";
+ script.type = 'text/javascript';
script.src = url;
script.async = true;
callback = function(event) {
- removeEventListenerFn(script, "load", callback);
- removeEventListenerFn(script, "error", callback);
+ script.removeEventListener('load', callback);
+ script.removeEventListener('error', callback);
rawDocument.body.removeChild(script);
script = null;
var status = -1;
- var text = "unknown";
+ var text = 'unknown';
if (event) {
- if (event.type === "load" && !callbacks[callbackId].called) {
- event = { type: "error" };
+ if (event.type === 'load' && !callbacks.wasCalled(callbackPath)) {
+ event = { type: 'error' };
}
text = event.type;
- status = event.type === "error" ? 404 : 200;
+ status = event.type === 'error' ? 404 : 200;
}
if (done) {
@@ -11053,8 +12636,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
};
- addEventListenerFn(script, "load", callback);
- addEventListenerFn(script, "error", callback);
+ script.addEventListener('load', callback);
+ script.addEventListener('error', callback);
rawDocument.body.appendChild(script);
return callback;
}
@@ -11063,18 +12646,19 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
var $interpolateMinErr = angular.$interpolateMinErr = minErr('$interpolate');
$interpolateMinErr.throwNoconcat = function(text) {
throw $interpolateMinErr('noconcat',
- "Error while interpolating: {0}\nStrict Contextual Escaping disallows " +
- "interpolations that concatenate multiple expressions when a trusted value is " +
- "required. See http://docs.angularjs.org/api/ng.$sce", text);
+ 'Error while interpolating: {0}\nStrict Contextual Escaping disallows ' +
+ 'interpolations that concatenate multiple expressions when a trusted value is ' +
+ 'required. See http://docs.angularjs.org/api/ng.$sce', text);
};
$interpolateMinErr.interr = function(text, err) {
- return $interpolateMinErr('interr', "Can't interpolate: {0}\n{1}", text, err.toString());
+ return $interpolateMinErr('interr', 'Can\'t interpolate: {0}\n{1}', text, err.toString());
};
/**
* @ngdoc provider
* @name $interpolateProvider
+ * @this
*
* @description
*
@@ -11089,7 +12673,7 @@ $interpolateMinErr.interr = function(text, err) {
* </div>
*
* @example
-<example module="customInterpolationApp">
+<example name="custom-interpolation-markup" module="customInterpolationApp">
<file name="index.html">
<script>
var customInterpolationApp = angular.module('customInterpolationApp', []);
@@ -11104,7 +12688,7 @@ $interpolateMinErr.interr = function(text, err) {
this.label = "This binding is brought you by // interpolation symbols.";
});
</script>
-<div ng-app="App" ng-controller="DemoController as demo">
+<div ng-controller="DemoController as demo">
//demo.label//
</div>
</file>
@@ -11171,30 +12755,13 @@ function $InterpolateProvider() {
replace(escapedEndRegexp, endSymbol);
}
- function stringify(value) {
- if (value == null) { // null || undefined
- return '';
- }
- switch (typeof value) {
- case 'string':
- break;
- case 'number':
- value = '' + value;
- break;
- default:
- value = toJson(value);
- }
-
- return value;
- }
-
- //TODO: this is the same as the constantWatchDelegate in parse.js
+ // TODO: this is the same as the constantWatchDelegate in parse.js
function constantWatchDelegate(scope, listener, objectEquality, constantInterp) {
- var unwatch;
- return unwatch = scope.$watch(function constantInterpolateWatch(scope) {
+ var unwatch = scope.$watch(function constantInterpolateWatch(scope) {
unwatch();
return constantInterp(scope);
}, listener, objectEquality);
+ return unwatch;
}
/**
@@ -11240,7 +12807,7 @@ function $InterpolateProvider() {
*
* `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior.
*
- * ####Escaped Interpolation
+ * #### Escaped Interpolation
* $interpolate provides a mechanism for escaping interpolation markers. Start and end markers
* can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash).
* It will be rendered as a regular start/end marker, and will not be interpreted as an expression
@@ -11261,7 +12828,7 @@ function $InterpolateProvider() {
* this is typically useful only when user-data is used in rendering a template from the server, or
* when otherwise untrusted data is used by a directive.
*
- * <example>
+ * <example name="interpolation">
* <file name="index.html">
* <div ng-init="username='A user'">
* <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "defaced value"; \}\}
@@ -11276,6 +12843,30 @@ function $InterpolateProvider() {
* </file>
* </example>
*
+ * @knownIssue
+ * It is currently not possible for an interpolated expression to contain the interpolation end
+ * symbol. For example, `{{ '}}' }}` will be incorrectly interpreted as `{{ ' }}` + `' }}`, i.e.
+ * an interpolated expression consisting of a single-quote (`'`) and the `' }}` string.
+ *
+ * @knownIssue
+ * All directives and components must use the standard `{{` `}}` interpolation symbols
+ * in their templates. If you change the application interpolation symbols the {@link $compile}
+ * service will attempt to denormalize the standard symbols to the custom symbols.
+ * The denormalization process is not clever enough to know not to replace instances of the standard
+ * symbols where they would not normally be treated as interpolation symbols. For example in the following
+ * code snippet the closing braces of the literal object will get incorrectly denormalized:
+ *
+ * ```
+ * <div data-context='{"context":{"id":3,"type":"page"}}">
+ * ```
+ *
+ * The workaround is to ensure that such instances are separated by whitespace:
+ * ```
+ * <div data-context='{"context":{"id":3,"type":"page"} }">
+ * ```
+ *
+ * See https://github.com/angular/angular.js/pull/14610#issuecomment-219401099 for more information.
+ *
* @param {string} text The text with markup to interpolate.
* @param {boolean=} mustHaveExpression if set to true then the interpolation string must have
* embedded expression in order to return an interpolation function. Strings with no
@@ -11317,8 +12908,8 @@ function $InterpolateProvider() {
expressionPositions = [];
while (index < textLength) {
- if (((startIndex = text.indexOf(startSymbol, index)) != -1) &&
- ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1)) {
+ if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
+ ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) !== -1)) {
if (index !== startIndex) {
concat.push(unescapeText(text.substring(index, startIndex)));
}
@@ -11383,7 +12974,7 @@ function $InterpolateProvider() {
expressions: expressions,
$$watchDelegate: function(scope, listener) {
var lastValue;
- return scope.$watchGroup(parseFns, function interpolateFnWatcher(values, oldValues) {
+ return scope.$watchGroup(parseFns, /** @this */ function interpolateFnWatcher(values, oldValues) {
var currValue = compute(values);
if (isFunction(listener)) {
listener.call(this, currValue, values !== oldValues ? lastValue : currValue, scope);
@@ -11440,6 +13031,7 @@ function $InterpolateProvider() {
}];
}
+/** @this */
function $IntervalProvider() {
this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
function($rootScope, $window, $q, $$q, $browser) {
@@ -11472,7 +13064,8 @@ function $IntervalProvider() {
* appropriate moment. See the example below for more details on how and when to do this.
* </div>
*
- * @param {function()} fn A function that should be called repeatedly.
+ * @param {function()} fn A function that should be called repeatedly. If no additional arguments
+ * are passed (see below), the function is called with the current iteration count.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
@@ -11482,7 +13075,7 @@ function $IntervalProvider() {
* @returns {promise} A promise which will be notified on each iteration.
*
* @example
- * <example module="intervalExample">
+ * <example module="intervalExample" name="interval-service">
* <file name="index.html">
* <script>
* angular.module('intervalExample', [])
@@ -11627,6 +13220,8 @@ function $IntervalProvider() {
*/
interval.cancel = function(promise) {
if (promise && promise.$$intervalId in intervals) {
+ // Interval cancels should not report as unhandled promise.
+ intervals[promise.$$intervalId].promise.catch(noop);
intervals[promise.$$intervalId].reject('canceled');
$window.clearInterval(promise.$$intervalId);
delete intervals[promise.$$intervalId];
@@ -11641,6 +13236,87 @@ function $IntervalProvider() {
/**
* @ngdoc service
+ * @name $jsonpCallbacks
+ * @requires $window
+ * @description
+ * This service handles the lifecycle of callbacks to handle JSONP requests.
+ * Override this service if you wish to customise where the callbacks are stored and
+ * how they vary compared to the requested url.
+ */
+var $jsonpCallbacksProvider = /** @this */ function() {
+ this.$get = function() {
+ var callbacks = angular.callbacks;
+ var callbackMap = {};
+
+ function createCallback(callbackId) {
+ var callback = function(data) {
+ callback.data = data;
+ callback.called = true;
+ };
+ callback.id = callbackId;
+ return callback;
+ }
+
+ return {
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#createCallback
+ * @param {string} url the url of the JSONP request
+ * @returns {string} the callback path to send to the server as part of the JSONP request
+ * @description
+ * {@link $httpBackend} calls this method to create a callback and get hold of the path to the callback
+ * to pass to the server, which will be used to call the callback with its payload in the JSONP response.
+ */
+ createCallback: function(url) {
+ var callbackId = '_' + (callbacks.$$counter++).toString(36);
+ var callbackPath = 'angular.callbacks.' + callbackId;
+ var callback = createCallback(callbackId);
+ callbackMap[callbackPath] = callbacks[callbackId] = callback;
+ return callbackPath;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#wasCalled
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @returns {boolean} whether the callback has been called, as a result of the JSONP response
+ * @description
+ * {@link $httpBackend} calls this method to find out whether the JSONP response actually called the
+ * callback that was passed in the request.
+ */
+ wasCalled: function(callbackPath) {
+ return callbackMap[callbackPath].called;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#getResponse
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @returns {*} the data received from the response via the registered callback
+ * @description
+ * {@link $httpBackend} calls this method to get hold of the data that was provided to the callback
+ * in the JSONP response.
+ */
+ getResponse: function(callbackPath) {
+ return callbackMap[callbackPath].data;
+ },
+ /**
+ * @ngdoc method
+ * @name $jsonpCallbacks#removeCallback
+ * @param {string} callbackPath the path to the callback that was sent in the JSONP request
+ * @description
+ * {@link $httpBackend} calls this method to remove the callback after the JSONP request has
+ * completed or timed-out.
+ */
+ removeCallback: function(callbackPath) {
+ var callback = callbackMap[callbackPath];
+ delete callbacks[callback.id];
+ delete callbackMap[callbackPath];
+ }
+ };
+ };
+};
+
+/**
+ * @ngdoc service
* @name $locale
*
* @description
@@ -11650,7 +13326,7 @@ function $IntervalProvider() {
* * `id` – `{string}` – locale id formatted as `languageId-countryId` (e.g. `en-us`)
*/
-var PATH_MATCH = /^([^\?#]*)(\?([^#]*))?(#(.*))?$/,
+var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
var $locationMinErr = minErr('$location');
@@ -11680,42 +13356,50 @@ function parseAbsoluteUrl(absoluteUrl, locationObj) {
locationObj.$$port = toInt(parsedUrl.port) || DEFAULT_PORTS[parsedUrl.protocol] || null;
}
+var DOUBLE_SLASH_REGEX = /^\s*[\\/]{2,}/;
+function parseAppUrl(url, locationObj) {
+
+ if (DOUBLE_SLASH_REGEX.test(url)) {
+ throw $locationMinErr('badpath', 'Invalid url "{0}".', url);
+ }
-function parseAppUrl(relativeUrl, locationObj) {
- var prefixed = (relativeUrl.charAt(0) !== '/');
+ var prefixed = (url.charAt(0) !== '/');
if (prefixed) {
- relativeUrl = '/' + relativeUrl;
+ url = '/' + url;
}
- var match = urlResolve(relativeUrl);
+ var match = urlResolve(url);
locationObj.$$path = decodeURIComponent(prefixed && match.pathname.charAt(0) === '/' ?
match.pathname.substring(1) : match.pathname);
locationObj.$$search = parseKeyValue(match.search);
locationObj.$$hash = decodeURIComponent(match.hash);
// make sure path starts with '/';
- if (locationObj.$$path && locationObj.$$path.charAt(0) != '/') {
+ if (locationObj.$$path && locationObj.$$path.charAt(0) !== '/') {
locationObj.$$path = '/' + locationObj.$$path;
}
}
+function startsWith(str, search) {
+ return str.slice(0, search.length) === search;
+}
/**
*
- * @param {string} begin
- * @param {string} whole
- * @returns {string} returns text from whole after begin or undefined if it does not begin with
- * expected string.
+ * @param {string} base
+ * @param {string} url
+ * @returns {string} returns text from `url` after `base` or `undefined` if it does not begin with
+ * the expected string.
*/
-function beginsWith(begin, whole) {
- if (whole.indexOf(begin) === 0) {
- return whole.substr(begin.length);
+function stripBaseUrl(base, url) {
+ if (startsWith(url, base)) {
+ return url.substr(base.length);
}
}
function stripHash(url) {
var index = url.indexOf('#');
- return index == -1 ? url : url.substr(0, index);
+ return index === -1 ? url : url.substr(0, index);
}
function trimEmptyHash(url) {
@@ -11734,13 +13418,13 @@ function serverBase(url) {
/**
- * LocationHtml5Url represents an url
+ * LocationHtml5Url represents a URL
* This object is exposed as $location service when HTML5 mode is enabled and supported
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
- * @param {string} basePrefix url path prefix
+ * @param {string} basePrefix URL path prefix
*/
function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
@@ -11749,12 +13433,12 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * Parse given html5 (regular) url string into properties
- * @param {string} url HTML5 url
+ * Parse given HTML5 (regular) URL string into properties
+ * @param {string} url HTML5 URL
* @private
*/
this.$$parse = function(url) {
- var pathUrl = beginsWith(appBaseNoFile, url);
+ var pathUrl = stripBaseUrl(appBaseNoFile, url);
if (!isString(pathUrl)) {
throw $locationMinErr('ipthprfx', 'Invalid url "{0}", missing path prefix "{1}".', url,
appBaseNoFile);
@@ -11779,6 +13463,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
+
+ this.$$urlUpdatedByLocation = true;
};
this.$$parseLinkUrl = function(url, relHref) {
@@ -11791,16 +13477,17 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
var appUrl, prevAppUrl;
var rewrittenUrl;
- if (isDefined(appUrl = beginsWith(appBase, url))) {
+
+ if (isDefined(appUrl = stripBaseUrl(appBase, url))) {
prevAppUrl = appUrl;
- if (isDefined(appUrl = beginsWith(basePrefix, appUrl))) {
- rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
+ if (basePrefix && isDefined(appUrl = stripBaseUrl(basePrefix, appUrl))) {
+ rewrittenUrl = appBaseNoFile + (stripBaseUrl('/', appUrl) || appUrl);
} else {
rewrittenUrl = appBase + prevAppUrl;
}
- } else if (isDefined(appUrl = beginsWith(appBaseNoFile, url))) {
+ } else if (isDefined(appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBaseNoFile + appUrl;
- } else if (appBaseNoFile == url + '/') {
+ } else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
}
if (rewrittenUrl) {
@@ -11812,7 +13499,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when developer doesn't opt into html5 mode.
* It also serves as the base class for html5 mode fallback on legacy browsers.
*
@@ -11827,19 +13514,19 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * Parse given hashbang url into properties
- * @param {string} url Hashbang url
+ * Parse given hashbang URL into properties
+ * @param {string} url Hashbang URL
* @private
*/
this.$$parse = function(url) {
- var withoutBaseUrl = beginsWith(appBase, url) || beginsWith(appBaseNoFile, url);
+ var withoutBaseUrl = stripBaseUrl(appBase, url) || stripBaseUrl(appBaseNoFile, url);
var withoutHashUrl;
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
- // The rest of the url starts with a hash so we have
+ // The rest of the URL starts with a hash so we have
// got either a hashbang path or a plain hash fragment
- withoutHashUrl = beginsWith(hashPrefix, withoutBaseUrl);
+ withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
// There was no hashbang prefix so we just have a hash fragment
withoutHashUrl = withoutBaseUrl;
@@ -11855,7 +13542,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
withoutHashUrl = '';
if (isUndefined(withoutBaseUrl)) {
appBase = url;
- this.replace();
+ /** @type {?} */ (this).replace();
}
}
}
@@ -11887,7 +13574,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
var firstPathSegmentMatch;
//Get the relative path from the input URL.
- if (url.indexOf(base) === 0) {
+ if (startsWith(url, base)) {
url = url.replace(base, '');
}
@@ -11902,7 +13589,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
};
/**
- * Compose hashbang url and update `absUrl` property
+ * Compose hashbang URL and update `absUrl` property
* @private
*/
this.$$compose = function() {
@@ -11911,10 +13598,12 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
+
+ this.$$urlUpdatedByLocation = true;
};
this.$$parseLinkUrl = function(url, relHref) {
- if (stripHash(appBase) == stripHash(url)) {
+ if (stripHash(appBase) === stripHash(url)) {
this.$$parse(url);
return true;
}
@@ -11924,7 +13613,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
- * LocationHashbangUrl represents url
+ * LocationHashbangUrl represents URL
* This object is exposed as $location service when html5 history api is enabled but the browser
* does not support it.
*
@@ -11948,9 +13637,9 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var rewrittenUrl;
var appUrl;
- if (appBase == stripHash(url)) {
+ if (appBase === stripHash(url)) {
rewrittenUrl = url;
- } else if ((appUrl = beginsWith(appBaseNoFile, url))) {
+ } else if ((appUrl = stripBaseUrl(appBaseNoFile, url))) {
rewrittenUrl = appBase + hashPrefix + appUrl;
} else if (appBaseNoFile === url + '/') {
rewrittenUrl = appBaseNoFile;
@@ -11968,6 +13657,8 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
this.$$url = encodePath(this.$$path) + (search ? '?' + search : '') + hash;
// include hashPrefix in $$absUrl when $$url is empty so IE9 does not reload page because of removal of '#'
this.$$absUrl = appBase + hashPrefix + this.$$url;
+
+ this.$$urlUpdatedByLocation = true;
};
}
@@ -11976,6 +13667,12 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var locationPrototype = {
/**
+ * Ensure absolute URL is initialized.
+ * @private
+ */
+ $$absUrl:'',
+
+ /**
* Are we in html5 mode?
* @private
*/
@@ -11994,17 +13691,17 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return full url representation with all segments encoded according to rules specified in
+ * Return full URL representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var absUrl = $location.absUrl();
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
* ```
*
- * @return {string} full url
+ * @return {string} full URL
*/
absUrl: locationGetter('$$absUrl'),
@@ -12015,18 +13712,18 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return url (e.g. `/path?a=b#hash`) when called without any parameter.
+ * Return URL (e.g. `/path?a=b#hash`) when called without any parameter.
*
* Change path, search and hash, when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var url = $location.url();
* // => "/some/path?foo=bar&baz=xoxo"
* ```
*
- * @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
+ * @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
url: function(url) {
@@ -12049,16 +13746,16 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return protocol of current url.
+ * Return protocol of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var protocol = $location.protocol();
* // => "http"
* ```
*
- * @return {string} protocol of current url
+ * @return {string} protocol of current URL
*/
protocol: locationGetter('$$protocol'),
@@ -12069,24 +13766,24 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return host of current url.
+ * Return host of current URL.
*
* Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
- * // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
- * @return {string} host of current url.
+ * @return {string} host of current URL.
*/
host: locationGetter('$$host'),
@@ -12097,11 +13794,11 @@ var locationPrototype = {
* @description
* This method is getter only.
*
- * Return port of current url.
+ * Return port of current URL.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var port = $location.port();
* // => 80
* ```
@@ -12117,7 +13814,7 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return path of current url when called without any parameter.
+ * Return path of current URL when called without any parameter.
*
* Change path when called with parameter and return `$location`.
*
@@ -12126,17 +13823,17 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var path = $location.path();
* // => "/some/path"
* ```
*
* @param {(string|number)=} path New path
- * @return {string} path
+ * @return {(string|object)} path if called with no parameters, or `$location` if called with a parameter
*/
path: locationGetterSetter('$$path', function(path) {
path = path !== null ? path.toString() : '';
- return path.charAt(0) == '/' ? path : '/' + path;
+ return path.charAt(0) === '/' ? path : '/' + path;
}),
/**
@@ -12146,13 +13843,13 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
- * Return search part (as object) of current url when called without any parameter.
+ * Return search part (as object) of current URL when called without any parameter.
*
* Change search part when called with parameter and return `$location`.
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
@@ -12168,7 +13865,7 @@ var locationPrototype = {
* of `$location` to the specified value.
*
* If the argument is a hash object containing an array of values, these values will be encoded
- * as duplicate search parameters in the url.
+ * as duplicate search parameters in the URL.
*
* @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
* will override only a single search property.
@@ -12230,7 +13927,7 @@ var locationPrototype = {
*
*
* ```js
- * // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
+ * // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* var hash = $location.hash();
* // => "hashValue"
* ```
@@ -12291,6 +13988,7 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
// but we're changing the $$state reference to $browser.state() during the $digest
// so the modification window is narrow.
this.$$state = isUndefined(state) ? null : state;
+ this.$$urlUpdatedByLocation = true;
return this;
};
@@ -12298,14 +13996,14 @@ forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], fun
function locationGetter(property) {
- return function() {
+ return /** @this */ function() {
return this[property];
};
}
function locationGetterSetter(property, preprocess) {
- return function(value) {
+ return /** @this */ function(value) {
if (isUndefined(value)) {
return this[property];
}
@@ -12347,11 +14045,13 @@ function locationGetterSetter(property, preprocess) {
/**
* @ngdoc provider
* @name $locationProvider
+ * @this
+ *
* @description
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
function $LocationProvider() {
- var hashPrefix = '',
+ var hashPrefix = '!',
html5Mode = {
enabled: false,
requireBase: true,
@@ -12362,6 +14062,7 @@ function $LocationProvider() {
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
+ * The default value for the prefix is `'!'`.
* @param {string=} prefix Prefix for hash part (containing path and search)
* @returns {*} current value if used as getter or itself (chaining) if used as setter
*/
@@ -12388,8 +14089,12 @@ function $LocationProvider() {
* whether or not a <base> tag is required to be present. If `enabled` and `requireBase` are
* true, and a base tag is not present, an error will be thrown when `$location` is injected.
* See the {@link guide/$location $location guide for more information}
- * - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled,
- * enables/disables url rewriting for relative links.
+ * - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
+ * enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
+ * only happen on links with an attribute that matches the given string. For example, if set
+ * to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links.
+ * Note that [attribute name normalization](guide/directive#normalization) does not apply
+ * here, so `'internalLink'` will **not** match `'internal-link'`.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
@@ -12407,7 +14112,7 @@ function $LocationProvider() {
html5Mode.requireBase = mode.requireBase;
}
- if (isBoolean(mode.rewriteLinks)) {
+ if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
html5Mode.rewriteLinks = mode.rewriteLinks;
}
@@ -12467,7 +14172,7 @@ function $LocationProvider() {
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
- "$location in HTML5 mode requires a <base> tag to be present!");
+ '$location in HTML5 mode requires a <base> tag to be present!');
}
appBase = serverBase(initialUrl) + (baseHref || '/');
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
@@ -12504,10 +14209,11 @@ function $LocationProvider() {
}
$rootElement.on('click', function(event) {
+ var rewriteLinks = html5Mode.rewriteLinks;
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
- if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which == 2 || event.button == 2) return;
+ if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
var elm = jqLite(event.target);
@@ -12517,6 +14223,8 @@ function $LocationProvider() {
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
}
+ if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return;
+
var absHref = elm.prop('href');
// get the actual href attribute - see
// http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
@@ -12538,7 +14246,7 @@ function $LocationProvider() {
// getting double entries in the location history.
event.preventDefault();
// update location manually
- if ($location.absUrl() != $browser.url()) {
+ if ($location.absUrl() !== $browser.url()) {
$rootScope.$apply();
// hack to work around FF6 bug 684208 when scenario runner clicks on links
$window.angular['ff-684208-preventDefault'] = true;
@@ -12549,7 +14257,7 @@ function $LocationProvider() {
// rewrite hashbang url <> html5 url
- if (trimEmptyHash($location.absUrl()) != trimEmptyHash(initialUrl)) {
+ if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) {
$browser.url($location.absUrl(), true);
}
@@ -12558,7 +14266,7 @@ function $LocationProvider() {
// update $location when $browser url changes
$browser.onUrlChange(function(newUrl, newState) {
- if (isUndefined(beginsWith(appBaseNoFile, newUrl))) {
+ if (!startsWith(newUrl, appBaseNoFile)) {
// If we are navigating outside of the app then force a reload
$window.location.href = newUrl;
return;
@@ -12593,36 +14301,40 @@ function $LocationProvider() {
// update browser
$rootScope.$watch(function $locationWatch() {
- var oldUrl = trimEmptyHash($browser.url());
- var newUrl = trimEmptyHash($location.absUrl());
- var oldState = $browser.state();
- var currentReplace = $location.$$replace;
- var urlOrStateChanged = oldUrl !== newUrl ||
- ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
+ if (initializing || $location.$$urlUpdatedByLocation) {
+ $location.$$urlUpdatedByLocation = false;
- if (initializing || urlOrStateChanged) {
- initializing = false;
+ var oldUrl = trimEmptyHash($browser.url());
+ var newUrl = trimEmptyHash($location.absUrl());
+ var oldState = $browser.state();
+ var currentReplace = $location.$$replace;
+ var urlOrStateChanged = oldUrl !== newUrl ||
+ ($location.$$html5 && $sniffer.history && oldState !== $location.$$state);
- $rootScope.$evalAsync(function() {
- var newUrl = $location.absUrl();
- var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
- $location.$$state, oldState).defaultPrevented;
+ if (initializing || urlOrStateChanged) {
+ initializing = false;
- // if the location was changed by a `$locationChangeStart` handler then stop
- // processing this location change
- if ($location.absUrl() !== newUrl) return;
+ $rootScope.$evalAsync(function() {
+ var newUrl = $location.absUrl();
+ var defaultPrevented = $rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
+ $location.$$state, oldState).defaultPrevented;
- if (defaultPrevented) {
- $location.$$parse(oldUrl);
- $location.$$state = oldState;
- } else {
- if (urlOrStateChanged) {
- setBrowserUrlWithFallback(newUrl, currentReplace,
- oldState === $location.$$state ? null : $location.$$state);
+ // if the location was changed by a `$locationChangeStart` handler then stop
+ // processing this location change
+ if ($location.absUrl() !== newUrl) return;
+
+ if (defaultPrevented) {
+ $location.$$parse(oldUrl);
+ $location.$$state = oldState;
+ } else {
+ if (urlOrStateChanged) {
+ setBrowserUrlWithFallback(newUrl, currentReplace,
+ oldState === $location.$$state ? null : $location.$$state);
+ }
+ afterLocationChange(oldUrl, oldState);
}
- afterLocationChange(oldUrl, oldState);
- }
- });
+ });
+ }
}
$location.$$replace = false;
@@ -12655,7 +14367,7 @@ function $LocationProvider() {
* {@link ng.$logProvider ng.$logProvider#debugEnabled} to change this.
*
* @example
- <example module="logExample">
+ <example module="logExample" name="log-service">
<file name="script.js">
angular.module('logExample', [])
.controller('LogController', ['$scope', '$log', function($scope, $log) {
@@ -12681,6 +14393,8 @@ function $LocationProvider() {
/**
* @ngdoc provider
* @name $logProvider
+ * @this
+ *
* @description
* Use the `$logProvider` to configure how the application logs messages
*/
@@ -12698,13 +14412,22 @@ function $LogProvider() {
this.debugEnabled = function(flag) {
if (isDefined(flag)) {
debug = flag;
- return this;
+ return this;
} else {
return debug;
}
};
this.$get = ['$window', function($window) {
+ // Support: IE 9-11, Edge 12-14+
+ // IE/Edge display errors in such a way that it requires the user to click in 4 places
+ // to see the stack trace. There is no way to feature-detect it so there's a chance
+ // of the user agent sniffing to go wrong but since it's only about logging, this shouldn't
+ // break apps. Other browsers display errors in a sensible way and some of them map stack
+ // traces along source maps if available so it makes sense to let browsers display it
+ // as they want.
+ var formatStackTrace = msie || /\bEdge\//.test($window.navigator && $window.navigator.userAgent);
+
return {
/**
* @ngdoc method
@@ -12757,12 +14480,12 @@ function $LogProvider() {
fn.apply(self, arguments);
}
};
- }())
+ })()
};
function formatError(arg) {
if (arg instanceof Error) {
- if (arg.stack) {
+ if (arg.stack && formatStackTrace) {
arg = (arg.message && arg.stack.indexOf(arg.message) === -1)
? 'Error: ' + arg.message + '\n' + arg.stack
: arg.stack;
@@ -12782,7 +14505,7 @@ function $LogProvider() {
// The reason behind this is that console.log has type "object" in IE8...
try {
hasApply = !!logFn.apply;
- } catch (e) {}
+ } catch (e) { /* empty */ }
if (hasApply) {
return function() {
@@ -12816,118 +14539,45 @@ function $LogProvider() {
var $parseMinErr = minErr('$parse');
+var objectValueOf = {}.constructor.prototype.valueOf;
+
// Sandboxing Angular Expressions
// ------------------------------
-// Angular expressions are generally considered safe because these expressions only have direct
-// access to `$scope` and locals. However, one can obtain the ability to execute arbitrary JS code by
-// obtaining a reference to native JS functions such as the Function constructor.
+// Angular expressions are no longer sandboxed. So it is now even easier to access arbitrary JS code by
+// various means such as obtaining a reference to native JS functions like the Function constructor.
//
// As an example, consider the following Angular expression:
//
// {}.toString.constructor('alert("evil JS code")')
//
-// This sandboxing technique is not perfect and doesn't aim to be. The goal is to prevent exploits
-// against the expression language, but not to prevent exploits that were enabled by exposing
-// sensitive JavaScript or browser APIs on Scope. Exposing such objects on a Scope is never a good
-// practice and therefore we are not even trying to protect against interaction with an object
-// explicitly exposed in this way.
-//
-// In general, it is not possible to access a Window object from an angular expression unless a
-// window or some DOM object that has a reference to window is published onto a Scope.
-// Similarly we prevent invocations of function known to be dangerous, as well as assignments to
-// native objects.
+// It is important to realize that if you create an expression from a string that contains user provided
+// content then it is possible that your application contains a security vulnerability to an XSS style attack.
//
// See https://docs.angularjs.org/guide/security
-function ensureSafeMemberName(name, fullExpression) {
- if (name === "__defineGetter__" || name === "__defineSetter__"
- || name === "__lookupGetter__" || name === "__lookupSetter__"
- || name === "__proto__") {
- throw $parseMinErr('isecfld',
- 'Attempting to access a disallowed field in Angular expressions! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
-}
-
-function getStringValue(name, fullExpression) {
- // From the JavaScript docs:
+function getStringValue(name) {
// Property names must be strings. This means that non-string objects cannot be used
// as keys in an object. Any non-string object, including a number, is typecasted
// into a string via the toString method.
+ // -- MDN, https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Property_accessors#Property_names
//
- // So, to ensure that we are checking the same `name` that JavaScript would use,
- // we cast it to a string, if possible.
- // Doing `name + ''` can cause a repl error if the result to `toString` is not a string,
- // this is, this will handle objects that misbehave.
- name = name + '';
- if (!isString(name)) {
- throw $parseMinErr('iseccst',
- 'Cannot convert object to primitive value! '
- + 'Expression: {0}', fullExpression);
- }
- return name;
-}
-
-function ensureSafeObject(obj, fullExpression) {
- // nifty check if obj is Function that is fast and works across iframes and other contexts
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isWindow(obj)
- obj.window === obj) {
- throw $parseMinErr('isecwindow',
- 'Referencing the Window in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// isElement(obj)
- obj.children && (obj.nodeName || (obj.prop && obj.attr && obj.find))) {
- throw $parseMinErr('isecdom',
- 'Referencing DOM nodes in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (// block Object so that we can't get hold of dangerous Object.* methods
- obj === Object) {
- throw $parseMinErr('isecobj',
- 'Referencing Object in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
- return obj;
-}
-
-var CALL = Function.prototype.call;
-var APPLY = Function.prototype.apply;
-var BIND = Function.prototype.bind;
-
-function ensureSafeFunction(obj, fullExpression) {
- if (obj) {
- if (obj.constructor === obj) {
- throw $parseMinErr('isecfn',
- 'Referencing Function in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- } else if (obj === CALL || obj === APPLY || obj === BIND) {
- throw $parseMinErr('isecff',
- 'Referencing call, apply or bind in Angular expressions is disallowed! Expression: {0}',
- fullExpression);
- }
- }
+ // So, to ensure that we are checking the same `name` that JavaScript would use, we cast it
+ // to a string. It's not always possible. If `name` is an object and its `toString` method is
+ // 'broken' (doesn't return a string, isn't a function, etc.), an error will be thrown:
+ //
+ // TypeError: Cannot convert object to primitive value
+ //
+ // For performance reasons, we don't catch this error here and allow it to propagate up the call
+ // stack. Note that you'll get the same error in JavaScript if you try to access a property using
+ // such a 'broken' object as a key.
+ return name + '';
}
-function ensureSafeAssignContext(obj, fullExpression) {
- if (obj) {
- if (obj === (0).constructor || obj === (false).constructor || obj === ''.constructor ||
- obj === {}.constructor || obj === [].constructor || obj === Function.constructor) {
- throw $parseMinErr('isecaf',
- 'Assigning to a constructor is disallowed! Expression: {0}', fullExpression);
- }
- }
-}
var OPERATORS = createMap();
forEach('+ - * / % === !== == != < > <= >= && || ! = |'.split(' '), function(operator) { OPERATORS[operator] = true; });
-var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'};
+var ESCAPE = {'n':'\n', 'f':'\f', 'r':'\r', 't':'\t', 'v':'\v', '\'':'\'', '"':'"'};
/////////////////////////////////////////
@@ -12936,7 +14586,7 @@ var ESCAPE = {"n":"\n", "f":"\f", "r":"\r", "t":"\t", "v":"\v", "'":"'", '"':'"'
/**
* @constructor
*/
-var Lexer = function(options) {
+var Lexer = function Lexer(options) {
this.options = options;
};
@@ -12950,11 +14600,11 @@ Lexer.prototype = {
while (this.index < this.text.length) {
var ch = this.text.charAt(this.index);
- if (ch === '"' || ch === "'") {
+ if (ch === '"' || ch === '\'') {
this.readString(ch);
} else if (this.isNumber(ch) || ch === '.' && this.isNumber(this.peek())) {
this.readNumber();
- } else if (this.isIdent(ch)) {
+ } else if (this.isIdentifierStart(this.peekMultichar())) {
this.readIdent();
} else if (this.is(ch, '(){}[].,;:?')) {
this.tokens.push({index: this.index, text: ch});
@@ -12989,7 +14639,7 @@ Lexer.prototype = {
},
isNumber: function(ch) {
- return ('0' <= ch && ch <= '9') && typeof ch === "string";
+ return ('0' <= ch && ch <= '9') && typeof ch === 'string';
},
isWhitespace: function(ch) {
@@ -12998,12 +14648,48 @@ Lexer.prototype = {
ch === '\n' || ch === '\v' || ch === '\u00A0');
},
- isIdent: function(ch) {
+ isIdentifierStart: function(ch) {
+ return this.options.isIdentifierStart ?
+ this.options.isIdentifierStart(ch, this.codePointAt(ch)) :
+ this.isValidIdentifierStart(ch);
+ },
+
+ isValidIdentifierStart: function(ch) {
return ('a' <= ch && ch <= 'z' ||
'A' <= ch && ch <= 'Z' ||
'_' === ch || ch === '$');
},
+ isIdentifierContinue: function(ch) {
+ return this.options.isIdentifierContinue ?
+ this.options.isIdentifierContinue(ch, this.codePointAt(ch)) :
+ this.isValidIdentifierContinue(ch);
+ },
+
+ isValidIdentifierContinue: function(ch, cp) {
+ return this.isValidIdentifierStart(ch, cp) || this.isNumber(ch);
+ },
+
+ codePointAt: function(ch) {
+ if (ch.length === 1) return ch.charCodeAt(0);
+ // eslint-disable-next-line no-bitwise
+ return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
+ },
+
+ peekMultichar: function() {
+ var ch = this.text.charAt(this.index);
+ var peek = this.peek();
+ if (!peek) {
+ return ch;
+ }
+ var cp1 = ch.charCodeAt(0);
+ var cp2 = peek.charCodeAt(0);
+ if (cp1 >= 0xD800 && cp1 <= 0xDBFF && cp2 >= 0xDC00 && cp2 <= 0xDFFF) {
+ return ch + peek;
+ }
+ return ch;
+ },
+
isExpOperator: function(ch) {
return (ch === '-' || ch === '+' || this.isNumber(ch));
},
@@ -13022,19 +14708,19 @@ Lexer.prototype = {
var start = this.index;
while (this.index < this.text.length) {
var ch = lowercase(this.text.charAt(this.index));
- if (ch == '.' || this.isNumber(ch)) {
+ if (ch === '.' || this.isNumber(ch)) {
number += ch;
} else {
var peekCh = this.peek();
- if (ch == 'e' && this.isExpOperator(peekCh)) {
+ if (ch === 'e' && this.isExpOperator(peekCh)) {
number += ch;
} else if (this.isExpOperator(ch) &&
peekCh && this.isNumber(peekCh) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
number += ch;
} else if (this.isExpOperator(ch) &&
(!peekCh || !this.isNumber(peekCh)) &&
- number.charAt(number.length - 1) == 'e') {
+ number.charAt(number.length - 1) === 'e') {
this.throwError('Invalid exponent');
} else {
break;
@@ -13052,12 +14738,13 @@ Lexer.prototype = {
readIdent: function() {
var start = this.index;
+ this.index += this.peekMultichar().length;
while (this.index < this.text.length) {
- var ch = this.text.charAt(this.index);
- if (!(this.isIdent(ch) || this.isNumber(ch))) {
+ var ch = this.peekMultichar();
+ if (!this.isIdentifierContinue(ch)) {
break;
}
- this.index++;
+ this.index += ch.length;
}
this.tokens.push({
index: start,
@@ -13108,7 +14795,7 @@ Lexer.prototype = {
}
};
-var AST = function(lexer, options) {
+var AST = function AST(lexer, options) {
this.lexer = lexer;
this.options = options;
};
@@ -13128,6 +14815,7 @@ AST.ArrayExpression = 'ArrayExpression';
AST.Property = 'Property';
AST.ObjectExpression = 'ObjectExpression';
AST.ThisExpression = 'ThisExpression';
+AST.LocalsExpression = 'LocalsExpression';
// Internal use only
AST.NGValueParameter = 'NGValueParameter';
@@ -13163,8 +14851,7 @@ AST.prototype = {
filterChain: function() {
var left = this.expression();
- var token;
- while ((token = this.expect('|'))) {
+ while (this.expect('|')) {
left = this.filter(left);
}
return left;
@@ -13177,6 +14864,10 @@ AST.prototype = {
assignment: function() {
var result = this.ternary();
if (this.expect('=')) {
+ if (!isAssignable(result)) {
+ throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
+ }
+
result = { type: AST.AssignmentExpression, left: result, right: this.assignment(), operator: '='};
}
return result;
@@ -13266,8 +14957,10 @@ AST.prototype = {
primary = this.arrayDeclaration();
} else if (this.expect('{')) {
primary = this.object();
- } else if (this.constants.hasOwnProperty(this.peek().text)) {
- primary = copy(this.constants[this.consume().text]);
+ } else if (this.selfReferential.hasOwnProperty(this.peek().text)) {
+ primary = copy(this.selfReferential[this.consume().text]);
+ } else if (this.options.literals.hasOwnProperty(this.peek().text)) {
+ primary = { type: AST.Literal, value: this.options.literals[this.consume().text]};
} else if (this.peek().identifier) {
primary = this.identifier();
} else if (this.peek().constant) {
@@ -13308,7 +15001,7 @@ AST.prototype = {
var args = [];
if (this.peekToken().text !== ')') {
do {
- args.push(this.expression());
+ args.push(this.filterChain());
} while (this.expect(','));
}
return args;
@@ -13354,13 +15047,28 @@ AST.prototype = {
property = {type: AST.Property, kind: 'init'};
if (this.peek().constant) {
property.key = this.constant();
+ property.computed = false;
+ this.consume(':');
+ property.value = this.expression();
} else if (this.peek().identifier) {
property.key = this.identifier();
+ property.computed = false;
+ if (this.peek(':')) {
+ this.consume(':');
+ property.value = this.expression();
+ } else {
+ property.value = property.key;
+ }
+ } else if (this.peek('[')) {
+ this.consume('[');
+ property.key = this.expression();
+ this.consume(']');
+ property.computed = true;
+ this.consume(':');
+ property.value = this.expression();
} else {
- this.throwError("invalid key", this.peek());
+ this.throwError('invalid key', this.peek());
}
- this.consume(':');
- property.value = this.expression();
properties.push(property);
} while (this.expect(','));
}
@@ -13419,16 +15127,9 @@ AST.prototype = {
return false;
},
-
- /* `undefined` is not a constant, it is an identifier,
- * but using it as an identifier is not supported
- */
- constants: {
- 'true': { type: AST.Literal, value: true },
- 'false': { type: AST.Literal, value: false },
- 'null': { type: AST.Literal, value: null },
- 'undefined': {type: AST.Literal, value: undefined },
- 'this': {type: AST.ThisExpression }
+ selfReferential: {
+ 'this': {type: AST.ThisExpression },
+ '$locals': {type: AST.LocalsExpression }
}
};
@@ -13450,6 +15151,7 @@ function isStateless($filter, filterName) {
function findConstantAndWatchExpressions(ast, $filter) {
var allConstants;
var argsToWatch;
+ var isStatelessFilter;
switch (ast.type) {
case AST.Program:
allConstants = true;
@@ -13500,7 +15202,8 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.toWatch = [ast];
break;
case AST.CallExpression:
- allConstants = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false;
+ allConstants = isStatelessFilter;
argsToWatch = [];
forEach(ast.arguments, function(expr) {
findConstantAndWatchExpressions(expr, $filter);
@@ -13510,7 +15213,7 @@ function findConstantAndWatchExpressions(ast, $filter) {
}
});
ast.constant = allConstants;
- ast.toWatch = ast.filter && isStateless($filter, ast.callee.name) ? argsToWatch : [ast];
+ ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
break;
case AST.AssignmentExpression:
findConstantAndWatchExpressions(ast.left, $filter);
@@ -13536,10 +15239,17 @@ function findConstantAndWatchExpressions(ast, $filter) {
argsToWatch = [];
forEach(ast.properties, function(property) {
findConstantAndWatchExpressions(property.value, $filter);
- allConstants = allConstants && property.value.constant;
+ allConstants = allConstants && property.value.constant && !property.computed;
if (!property.value.constant) {
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
}
+ if (property.computed) {
+ findConstantAndWatchExpressions(property.key, $filter);
+ if (!property.key.constant) {
+ argsToWatch.push.apply(argsToWatch, property.key.toWatch);
+ }
+ }
+
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
@@ -13548,11 +15258,15 @@ function findConstantAndWatchExpressions(ast, $filter) {
ast.constant = false;
ast.toWatch = [];
break;
+ case AST.LocalsExpression:
+ ast.constant = false;
+ ast.toWatch = [];
+ break;
}
}
function getInputs(body) {
- if (body.length != 1) return;
+ if (body.length !== 1) return;
var lastExpression = body[0].expression;
var candidate = lastExpression.toWatch;
if (candidate.length !== 1) return candidate;
@@ -13587,13 +15301,12 @@ function ASTCompiler(astBuilder, $filter) {
}
ASTCompiler.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(expression) {
var self = this;
var ast = this.astBuilder.ast(expression);
this.state = {
nextId: 0,
filters: {},
- expensiveChecks: expensiveChecks,
fn: {vars: [], body: [], own: {}},
assign: {vars: [], body: [], own: {}},
inputs: []
@@ -13634,27 +15347,16 @@ ASTCompiler.prototype = {
this.watchFns() +
'return fn;';
- /* jshint -W054 */
+ // eslint-disable-next-line no-new-func
var fn = (new Function('$filter',
- 'ensureSafeMemberName',
- 'ensureSafeObject',
- 'ensureSafeFunction',
'getStringValue',
- 'ensureSafeAssignContext',
'ifDefined',
'plus',
- 'text',
fnString))(
this.$filter,
- ensureSafeMemberName,
- ensureSafeObject,
- ensureSafeFunction,
getStringValue,
- ensureSafeAssignContext,
ifDefined,
- plusFn,
- expression);
- /* jshint +W054 */
+ plusFn);
this.state = this.stage = undefined;
fn.literal = isLiteral(ast);
fn.constant = isConstant(ast);
@@ -13704,7 +15406,7 @@ ASTCompiler.prototype = {
},
recurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
- var left, right, self = this, args, expression;
+ var left, right, self = this, args, expression, computed;
recursionFn = recursionFn || noop;
if (!skipWatchIdCheck && isDefined(ast.watchId)) {
intoId = intoId || this.nextId();
@@ -13728,7 +15430,7 @@ ASTCompiler.prototype = {
case AST.Literal:
expression = this.escape(ast.value);
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.UnaryExpression:
this.recurse(ast.argument, undefined, undefined, function(expr) { right = expr; });
@@ -13768,22 +15470,18 @@ ASTCompiler.prototype = {
nameId.computed = false;
nameId.name = ast.name;
}
- ensureSafeMemberName(ast.name);
self.if_(self.stage === 'inputs' || self.not(self.getHasOwnProperty('l', ast.name)),
function() {
self.if_(self.stage === 'inputs' || 's', function() {
if (create && create !== 1) {
self.if_(
- self.not(self.nonComputedMember('s', ast.name)),
+ self.isNull(self.nonComputedMember('s', ast.name)),
self.lazyAssign(self.nonComputedMember('s', ast.name), '{}'));
}
self.assign(intoId, self.nonComputedMember('s', ast.name));
});
}, intoId && self.lazyAssign(intoId, self.nonComputedMember('l', ast.name))
);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.name)) {
- self.addEnsureSafeObject(intoId);
- }
recursionFn(intoId);
break;
case AST.MemberExpression:
@@ -13795,25 +15493,20 @@ ASTCompiler.prototype = {
right = self.nextId();
self.recurse(ast.property, right);
self.getStringValue(right);
- self.addEnsureSafeMemberName(right);
if (create && create !== 1) {
self.if_(self.not(self.computedMember(left, right)), self.lazyAssign(self.computedMember(left, right), '{}'));
}
- expression = self.ensureSafeObject(self.computedMember(left, right));
+ expression = self.computedMember(left, right);
self.assign(intoId, expression);
if (nameId) {
nameId.computed = true;
nameId.name = right;
}
} else {
- ensureSafeMemberName(ast.property.name);
if (create && create !== 1) {
- self.if_(self.not(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
+ self.if_(self.isNull(self.nonComputedMember(left, ast.property.name)), self.lazyAssign(self.nonComputedMember(left, ast.property.name), '{}'));
}
expression = self.nonComputedMember(left, ast.property.name);
- if (self.state.expensiveChecks || isPossiblyDangerousMemberName(ast.property.name)) {
- expression = self.ensureSafeObject(expression);
- }
self.assign(intoId, expression);
if (nameId) {
nameId.computed = false;
@@ -13845,21 +15538,16 @@ ASTCompiler.prototype = {
args = [];
self.recurse(ast.callee, right, left, function() {
self.if_(self.notNull(right), function() {
- self.addEnsureSafeFunction(right);
forEach(ast.arguments, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
- args.push(self.ensureSafeObject(argument));
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
+ args.push(argument);
});
});
if (left.name) {
- if (!self.state.expensiveChecks) {
- self.addEnsureSafeObject(left.context);
- }
expression = self.member(left.context, left.name, left.computed) + '(' + args.join(',') + ')';
} else {
expression = right + '(' + args.join(',') + ')';
}
- expression = self.ensureSafeObject(expression);
self.assign(intoId, expression);
}, function() {
self.assign(intoId, 'undefined');
@@ -13871,14 +15559,9 @@ ASTCompiler.prototype = {
case AST.AssignmentExpression:
right = this.nextId();
left = {};
- if (!isAssignable(ast.left)) {
- throw $parseMinErr('lval', 'Trying to assign a value to a non l-value');
- }
this.recurse(ast.left, undefined, left, function() {
self.if_(self.notNull(left.context), function() {
self.recurse(ast.right, right);
- self.addEnsureSafeObject(self.member(left.context, left.name, left.computed));
- self.addEnsureSafeAssignContext(left.context);
expression = self.member(left.context, left.name, left.computed) + ast.operator + right;
self.assign(intoId, expression);
recursionFn(intoId || expression);
@@ -13888,35 +15571,63 @@ ASTCompiler.prototype = {
case AST.ArrayExpression:
args = [];
forEach(ast.elements, function(expr) {
- self.recurse(expr, self.nextId(), undefined, function(argument) {
+ self.recurse(expr, ast.constant ? undefined : self.nextId(), undefined, function(argument) {
args.push(argument);
});
});
expression = '[' + args.join(',') + ']';
this.assign(intoId, expression);
- recursionFn(expression);
+ recursionFn(intoId || expression);
break;
case AST.ObjectExpression:
args = [];
+ computed = false;
forEach(ast.properties, function(property) {
- self.recurse(property.value, self.nextId(), undefined, function(expr) {
- args.push(self.escape(
- property.key.type === AST.Identifier ? property.key.name :
- ('' + property.key.value)) +
- ':' + expr);
- });
+ if (property.computed) {
+ computed = true;
+ }
});
- expression = '{' + args.join(',') + '}';
- this.assign(intoId, expression);
- recursionFn(expression);
+ if (computed) {
+ intoId = intoId || this.nextId();
+ this.assign(intoId, '{}');
+ forEach(ast.properties, function(property) {
+ if (property.computed) {
+ left = self.nextId();
+ self.recurse(property.key, left);
+ } else {
+ left = property.key.type === AST.Identifier ?
+ property.key.name :
+ ('' + property.key.value);
+ }
+ right = self.nextId();
+ self.recurse(property.value, right);
+ self.assign(self.member(intoId, left, property.computed), right);
+ });
+ } else {
+ forEach(ast.properties, function(property) {
+ self.recurse(property.value, ast.constant ? undefined : self.nextId(), undefined, function(expr) {
+ args.push(self.escape(
+ property.key.type === AST.Identifier ? property.key.name :
+ ('' + property.key.value)) +
+ ':' + expr);
+ });
+ });
+ expression = '{' + args.join(',') + '}';
+ this.assign(intoId, expression);
+ }
+ recursionFn(intoId || expression);
break;
case AST.ThisExpression:
this.assign(intoId, 's');
- recursionFn('s');
+ recursionFn(intoId || 's');
+ break;
+ case AST.LocalsExpression:
+ this.assign(intoId, 'l');
+ recursionFn(intoId || 'l');
break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
- recursionFn('v');
+ recursionFn(intoId || 'v');
break;
}
},
@@ -13975,12 +15686,22 @@ ASTCompiler.prototype = {
return '!(' + expression + ')';
},
+ isNull: function(expression) {
+ return expression + '==null';
+ },
+
notNull: function(expression) {
return expression + '!=null';
},
nonComputedMember: function(left, right) {
- return left + '.' + right;
+ var SAFE_IDENTIFIER = /^[$_a-zA-Z][$_a-zA-Z0-9]*$/;
+ var UNSAFE_CHARACTERS = /[^$_a-zA-Z0-9]/g;
+ if (SAFE_IDENTIFIER.test(right)) {
+ return left + '.' + right;
+ } else {
+ return left + '["' + right.replace(UNSAFE_CHARACTERS, this.stringEscapeFn) + '"]';
+ }
},
computedMember: function(left, right) {
@@ -13992,40 +15713,8 @@ ASTCompiler.prototype = {
return this.nonComputedMember(left, right);
},
- addEnsureSafeObject: function(item) {
- this.current().body.push(this.ensureSafeObject(item), ';');
- },
-
- addEnsureSafeMemberName: function(item) {
- this.current().body.push(this.ensureSafeMemberName(item), ';');
- },
-
- addEnsureSafeFunction: function(item) {
- this.current().body.push(this.ensureSafeFunction(item), ';');
- },
-
- addEnsureSafeAssignContext: function(item) {
- this.current().body.push(this.ensureSafeAssignContext(item), ';');
- },
-
- ensureSafeObject: function(item) {
- return 'ensureSafeObject(' + item + ',text)';
- },
-
- ensureSafeMemberName: function(item) {
- return 'ensureSafeMemberName(' + item + ',text)';
- },
-
- ensureSafeFunction: function(item) {
- return 'ensureSafeFunction(' + item + ',text)';
- },
-
getStringValue: function(item) {
- this.assign(item, 'getStringValue(' + item + ',text)');
- },
-
- ensureSafeAssignContext: function(item) {
- return 'ensureSafeAssignContext(' + item + ',text)';
+ this.assign(item, 'getStringValue(' + item + ')');
},
lazyRecurse: function(ast, intoId, nameId, recursionFn, create, skipWatchIdCheck) {
@@ -14049,7 +15738,7 @@ ASTCompiler.prototype = {
},
escape: function(value) {
- if (isString(value)) return "'" + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + "'";
+ if (isString(value)) return '\'' + value.replace(this.stringEscapeRegex, this.stringEscapeFn) + '\'';
if (isNumber(value)) return value.toString();
if (value === true) return 'true';
if (value === false) return 'false';
@@ -14079,11 +15768,9 @@ function ASTInterpreter(astBuilder, $filter) {
}
ASTInterpreter.prototype = {
- compile: function(expression, expensiveChecks) {
+ compile: function(expression) {
var self = this;
var ast = this.astBuilder.ast(expression);
- this.expression = expression;
- this.expensiveChecks = expensiveChecks;
findConstantAndWatchExpressions(ast, self.$filter);
var assignable;
var assign;
@@ -14105,7 +15792,7 @@ ASTInterpreter.prototype = {
forEach(ast.body, function(expression) {
expressions.push(self.recurse(expression.expression));
});
- var fn = ast.body.length === 0 ? function() {} :
+ var fn = ast.body.length === 0 ? noop :
ast.body.length === 1 ? expressions[0] :
function(scope, locals) {
var lastValue;
@@ -14128,7 +15815,7 @@ ASTInterpreter.prototype = {
},
recurse: function(ast, context, create) {
- var left, right, self = this, args, expression;
+ var left, right, self = this, args;
if (ast.input) {
return this.inputs(ast.input, ast.watchId);
}
@@ -14154,20 +15841,16 @@ ASTInterpreter.prototype = {
context
);
case AST.Identifier:
- ensureSafeMemberName(ast.name, self.expression);
- return self.identifier(ast.name,
- self.expensiveChecks || isPossiblyDangerousMemberName(ast.name),
- context, create, self.expression);
+ return self.identifier(ast.name, context, create);
case AST.MemberExpression:
left = this.recurse(ast.object, false, !!create);
if (!ast.computed) {
- ensureSafeMemberName(ast.property.name, self.expression);
right = ast.property.name;
}
if (ast.computed) right = this.recurse(ast.property);
return ast.computed ?
- this.computedMember(left, right, context, create, self.expression) :
- this.nonComputedMember(left, right, self.expensiveChecks, context, create, self.expression);
+ this.computedMember(left, right, context, create) :
+ this.nonComputedMember(left, right, context, create);
case AST.CallExpression:
args = [];
forEach(ast.arguments, function(expr) {
@@ -14188,13 +15871,11 @@ ASTInterpreter.prototype = {
var rhs = right(scope, locals, assign, inputs);
var value;
if (rhs.value != null) {
- ensureSafeObject(rhs.context, self.expression);
- ensureSafeFunction(rhs.value, self.expression);
var values = [];
for (var i = 0; i < args.length; ++i) {
- values.push(ensureSafeObject(args[i](scope, locals, assign, inputs), self.expression));
+ values.push(args[i](scope, locals, assign, inputs));
}
- value = ensureSafeObject(rhs.value.apply(rhs.context, values), self.expression);
+ value = rhs.value.apply(rhs.context, values);
}
return context ? {value: value} : value;
};
@@ -14204,8 +15885,6 @@ ASTInterpreter.prototype = {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs = right(scope, locals, assign, inputs);
- ensureSafeObject(lhs.value, self.expression);
- ensureSafeAssignContext(lhs.context);
lhs.context[lhs.name] = rhs;
return context ? {value: rhs} : rhs;
};
@@ -14224,16 +15903,28 @@ ASTInterpreter.prototype = {
case AST.ObjectExpression:
args = [];
forEach(ast.properties, function(property) {
- args.push({key: property.key.type === AST.Identifier ?
- property.key.name :
- ('' + property.key.value),
- value: self.recurse(property.value)
- });
+ if (property.computed) {
+ args.push({key: self.recurse(property.key),
+ computed: true,
+ value: self.recurse(property.value)
+ });
+ } else {
+ args.push({key: property.key.type === AST.Identifier ?
+ property.key.name :
+ ('' + property.key.value),
+ computed: false,
+ value: self.recurse(property.value)
+ });
+ }
});
return function(scope, locals, assign, inputs) {
var value = {};
for (var i = 0; i < args.length; ++i) {
- value[args[i].key] = args[i].value(scope, locals, assign, inputs);
+ if (args[i].computed) {
+ value[args[i].key(scope, locals, assign, inputs)] = args[i].value(scope, locals, assign, inputs);
+ } else {
+ value[args[i].key] = args[i].value(scope, locals, assign, inputs);
+ }
}
return context ? {value: value} : value;
};
@@ -14241,8 +15932,12 @@ ASTInterpreter.prototype = {
return function(scope) {
return context ? {value: scope} : scope;
};
+ case AST.LocalsExpression:
+ return function(scope, locals) {
+ return context ? {value: locals} : locals;
+ };
case AST.NGValueParameter:
- return function(scope, locals, assign, inputs) {
+ return function(scope, locals, assign) {
return context ? {value: assign} : assign;
};
}
@@ -14265,7 +15960,7 @@ ASTInterpreter.prototype = {
if (isDefined(arg)) {
arg = -arg;
} else {
- arg = 0;
+ arg = -0;
}
return context ? {value: arg} : arg;
};
@@ -14324,12 +16019,14 @@ ASTInterpreter.prototype = {
},
'binary==': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) == right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
},
'binary!=': function(left, right, context) {
return function(scope, locals, assign, inputs) {
+ // eslint-disable-next-line eqeqeq
var arg = left(scope, locals, assign, inputs) != right(scope, locals, assign, inputs);
return context ? {value: arg} : arg;
};
@@ -14379,16 +16076,13 @@ ASTInterpreter.prototype = {
value: function(value, context) {
return function() { return context ? {context: undefined, name: undefined, value: value} : value; };
},
- identifier: function(name, expensiveChecks, context, create, expression) {
+ identifier: function(name, context, create) {
return function(scope, locals, assign, inputs) {
var base = locals && (name in locals) ? locals : scope;
- if (create && create !== 1 && base && !(base[name])) {
+ if (create && create !== 1 && base && base[name] == null) {
base[name] = {};
}
var value = base ? base[name] : undefined;
- if (expensiveChecks) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: base, name: name, value: value};
} else {
@@ -14396,7 +16090,7 @@ ASTInterpreter.prototype = {
}
};
},
- computedMember: function(left, right, context, create, expression) {
+ computedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
var rhs;
@@ -14404,12 +16098,12 @@ ASTInterpreter.prototype = {
if (lhs != null) {
rhs = right(scope, locals, assign, inputs);
rhs = getStringValue(rhs);
- ensureSafeMemberName(rhs, expression);
- if (create && create !== 1 && lhs && !(lhs[rhs])) {
- lhs[rhs] = {};
+ if (create && create !== 1) {
+ if (lhs && !(lhs[rhs])) {
+ lhs[rhs] = {};
+ }
}
value = lhs[rhs];
- ensureSafeObject(value, expression);
}
if (context) {
return {context: lhs, name: rhs, value: value};
@@ -14418,16 +16112,15 @@ ASTInterpreter.prototype = {
}
};
},
- nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
+ nonComputedMember: function(left, right, context, create) {
return function(scope, locals, assign, inputs) {
var lhs = left(scope, locals, assign, inputs);
- if (create && create !== 1 && lhs && !(lhs[right])) {
- lhs[right] = {};
+ if (create && create !== 1) {
+ if (lhs && lhs[right] == null) {
+ lhs[right] = {};
+ }
}
var value = lhs != null ? lhs[right] : undefined;
- if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
- ensureSafeObject(value, expression);
- }
if (context) {
return {context: lhs, name: right, value: value};
} else {
@@ -14446,11 +16139,11 @@ ASTInterpreter.prototype = {
/**
* @constructor
*/
-var Parser = function(lexer, $filter, options) {
+var Parser = function Parser(lexer, $filter, options) {
this.lexer = lexer;
this.$filter = $filter;
this.options = options;
- this.ast = new AST(this.lexer);
+ this.ast = new AST(lexer, options);
this.astCompiler = options.csp ? new ASTInterpreter(this.ast, $filter) :
new ASTCompiler(this.ast, $filter);
};
@@ -14459,19 +16152,10 @@ Parser.prototype = {
constructor: Parser,
parse: function(text) {
- return this.astCompiler.compile(text, this.options.expensiveChecks);
+ return this.astCompiler.compile(text);
}
};
-var getterFnCacheDefault = createMap();
-var getterFnCacheExpensive = createMap();
-
-function isPossiblyDangerousMemberName(name) {
- return name == 'constructor';
-}
-
-var objectValueOf = Object.prototype.valueOf;
-
function getValueOf(value) {
return isFunction(value.valueOf) ? value.valueOf() : objectValueOf.call(value);
}
@@ -14522,27 +16206,78 @@ function getValueOf(value) {
/**
* @ngdoc provider
* @name $parseProvider
+ * @this
*
* @description
* `$parseProvider` can be used for configuring the default behavior of the {@link ng.$parse $parse}
* service.
*/
function $ParseProvider() {
- var cacheDefault = createMap();
- var cacheExpensive = createMap();
+ var cache = createMap();
+ var literals = {
+ 'true': true,
+ 'false': false,
+ 'null': null,
+ 'undefined': undefined
+ };
+ var identStart, identContinue;
+
+ /**
+ * @ngdoc method
+ * @name $parseProvider#addLiteral
+ * @description
+ *
+ * Configure $parse service to add literal values that will be present as literal at expressions.
+ *
+ * @param {string} literalName Token for the literal value. The literal name value must be a valid literal name.
+ * @param {*} literalValue Value for this literal. All literal values must be primitives or `undefined`.
+ *
+ **/
+ this.addLiteral = function(literalName, literalValue) {
+ literals[literalName] = literalValue;
+ };
+
+ /**
+ * @ngdoc method
+ * @name $parseProvider#setIdentifierFns
+ *
+ * @description
+ *
+ * Allows defining the set of characters that are allowed in Angular expressions. The function
+ * `identifierStart` will get called to know if a given character is a valid character to be the
+ * first character for an identifier. The function `identifierContinue` will get called to know if
+ * a given character is a valid character to be a follow-up identifier character. The functions
+ * `identifierStart` and `identifierContinue` will receive as arguments the single character to be
+ * identifier and the character code point. These arguments will be `string` and `numeric`. Keep in
+ * mind that the `string` parameter can be two characters long depending on the character
+ * representation. It is expected for the function to return `true` or `false`, whether that
+ * character is allowed or not.
+ *
+ * Since this function will be called extensively, keep the implementation of these functions fast,
+ * as the performance of these functions have a direct impact on the expressions parsing speed.
+ *
+ * @param {function=} identifierStart The function that will decide whether the given character is
+ * a valid identifier start character.
+ * @param {function=} identifierContinue The function that will decide whether the given character is
+ * a valid identifier continue character.
+ */
+ this.setIdentifierFns = function(identifierStart, identifierContinue) {
+ identStart = identifierStart;
+ identContinue = identifierContinue;
+ return this;
+ };
this.$get = ['$filter', function($filter) {
var noUnsafeEval = csp().noUnsafeEval;
var $parseOptions = {
csp: noUnsafeEval,
- expensiveChecks: false
- },
- $parseOptionsExpensive = {
- csp: noUnsafeEval,
- expensiveChecks: true
+ literals: copy(literals),
+ isIdentifierStart: isFunction(identStart) && identStart,
+ isIdentifierContinue: isFunction(identContinue) && identContinue
};
+ return $parse;
- return function $parse(exp, interceptorFn, expensiveChecks) {
+ function $parse(exp, interceptorFn) {
var parsedExpression, oneTime, cacheKey;
switch (typeof exp) {
@@ -14550,7 +16285,6 @@ function $ParseProvider() {
exp = exp.trim();
cacheKey = exp;
- var cache = (expensiveChecks ? cacheExpensive : cacheDefault);
parsedExpression = cache[cacheKey];
if (!parsedExpression) {
@@ -14558,9 +16292,8 @@ function $ParseProvider() {
oneTime = true;
exp = exp.substring(2);
}
- var parseOptions = expensiveChecks ? $parseOptionsExpensive : $parseOptions;
- var lexer = new Lexer(parseOptions);
- var parser = new Parser(lexer, $filter, parseOptions);
+ var lexer = new Lexer($parseOptions);
+ var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
if (parsedExpression.constant) {
parsedExpression.$$watchDelegate = constantWatchDelegate;
@@ -14578,17 +16311,17 @@ function $ParseProvider() {
return addInterceptor(exp, interceptorFn);
default:
- return noop;
+ return addInterceptor(noop, interceptorFn);
}
- };
+ }
- function expressionInputDirtyCheck(newValue, oldValueOfValue) {
+ function expressionInputDirtyCheck(newValue, oldValueOfValue, compareObjectIdentity) {
if (newValue == null || oldValueOfValue == null) { // null/undefined
return newValue === oldValueOfValue;
}
- if (typeof newValue === 'object') {
+ if (typeof newValue === 'object' && !compareObjectIdentity) {
// attempt to convert the value to a primitive type
// TODO(docs): add a note to docs that by implementing valueOf even objects and arrays can
@@ -14604,6 +16337,7 @@ function $ParseProvider() {
}
//Primitive or NaN
+ // eslint-disable-next-line no-self-compare
return newValue === oldValueOfValue || (newValue !== newValue && oldValueOfValue !== oldValueOfValue);
}
@@ -14616,7 +16350,7 @@ function $ParseProvider() {
inputExpressions = inputExpressions[0];
return scope.$watch(function expressionInputWatch(scope) {
var newInputValue = inputExpressions(scope);
- if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf)) {
+ if (!expressionInputDirtyCheck(newInputValue, oldInputValueOf, parsedExpression.literal)) {
lastResult = parsedExpression(scope, undefined, undefined, [newInputValue]);
oldInputValueOf = newInputValue && getValueOf(newInputValue);
}
@@ -14636,7 +16370,7 @@ function $ParseProvider() {
for (var i = 0, ii = inputExpressions.length; i < ii; i++) {
var newInputValue = inputExpressions[i](scope);
- if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i]))) {
+ if (changed || (changed = !expressionInputDirtyCheck(newInputValue, oldInputValueOfValues[i], parsedExpression.literal))) {
oldInputValues[i] = newInputValue;
oldInputValueOfValues[i] = newInputValue && getValueOf(newInputValue);
}
@@ -14650,14 +16384,22 @@ function $ParseProvider() {
}, listener, objectEquality, prettyPrintExpression);
}
- function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression) {
+ function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
+ if (parsedExpression.inputs) {
+ unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
+ } else {
+ unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality);
+ }
+ return unwatch;
+
+ function oneTimeWatch(scope) {
return parsedExpression(scope);
- }, function oneTimeListener(value, old, scope) {
+ }
+ function oneTimeListener(value, old, scope) {
lastValue = value;
if (isFunction(listener)) {
- listener.apply(this, arguments);
+ listener(value, old, scope);
}
if (isDefined(value)) {
scope.$$postDigest(function() {
@@ -14666,17 +16408,17 @@ function $ParseProvider() {
}
});
}
- }, objectEquality);
+ }
}
function oneTimeLiteralWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch, lastValue;
- return unwatch = scope.$watch(function oneTimeWatch(scope) {
+ unwatch = scope.$watch(function oneTimeWatch(scope) {
return parsedExpression(scope);
}, function oneTimeListener(value, old, scope) {
lastValue = value;
if (isFunction(listener)) {
- listener.call(this, value, old, scope);
+ listener(value, old, scope);
}
if (isAllDefined(value)) {
scope.$$postDigest(function() {
@@ -14685,6 +16427,8 @@ function $ParseProvider() {
}
}, objectEquality);
+ return unwatch;
+
function isAllDefined(value) {
var allDefined = true;
forEach(value, function(val) {
@@ -14695,11 +16439,11 @@ function $ParseProvider() {
}
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
- var unwatch;
- return unwatch = scope.$watch(function constantWatch(scope) {
+ var unwatch = scope.$watch(function constantWatch(scope) {
unwatch();
return parsedExpression(scope);
}, listener, objectEquality);
+ return unwatch;
}
function addInterceptor(parsedExpression, interceptorFn) {
@@ -14723,14 +16467,15 @@ function $ParseProvider() {
};
// Propagate $$watchDelegates other then inputsWatchDelegate
+ useInputs = !parsedExpression.inputs;
if (parsedExpression.$$watchDelegate &&
parsedExpression.$$watchDelegate !== inputsWatchDelegate) {
fn.$$watchDelegate = parsedExpression.$$watchDelegate;
+ fn.inputs = parsedExpression.inputs;
} else if (!interceptorFn.$stateful) {
// If there is an interceptor, but no watchDelegate then treat the interceptor like
// we treat filters - it is assumed to be a pure function unless flagged with $stateful
fn.$$watchDelegate = inputsWatchDelegate;
- useInputs = !parsedExpression.inputs;
fn.inputs = parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression];
}
@@ -14748,19 +16493,19 @@ function $ParseProvider() {
* A service that helps you run functions asynchronously, and use their return values (or exceptions)
* when they are done processing.
*
- * This is an implementation of promises/deferred objects inspired by
- * [Kris Kowal's Q](https://github.com/kriskowal/q).
+ * This is a [Promises/A+](https://promisesaplus.com/)-compliant implementation of promises/deferred
+ * objects inspired by [Kris Kowal's Q](https://github.com/kriskowal/q).
*
* $q can be used in two fashions --- one which is more similar to Kris Kowal's Q or jQuery's Deferred
- * implementations, and the other which resembles ES6 promises to some degree.
+ * implementations, and the other which resembles ES6 (ES2015) promises to some degree.
*
* # $q constructor
*
* The streamlined ES6 style promise is essentially just using $q as a constructor which takes a `resolver`
- * function as the first argument. This is similar to the native Promise implementation from ES6 Harmony,
+ * function as the first argument. This is similar to the native Promise implementation from ES6,
* see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).
*
- * While the constructor-style use is supported, not all of the supporting methods from ES6 Harmony promises are
+ * While the constructor-style use is supported, not all of the supporting methods from ES6 promises are
* available yet.
*
* It can be used like so:
@@ -14792,7 +16537,7 @@ function $ParseProvider() {
*
* Note: progress/notify callbacks are not currently supported via the ES6-style interface.
*
- * Note: unlike ES6 behaviour, an exception thrown in the constructor function will NOT implicitly reject the promise.
+ * Note: unlike ES6 behavior, an exception thrown in the constructor function will NOT implicitly reject the promise.
*
* However, the more traditional CommonJS-style usage is still available, and documented below.
*
@@ -14874,7 +16619,7 @@ function $ParseProvider() {
*
* **Methods**
*
- * - `then(successCallback, errorCallback, notifyCallback)` – regardless of when the promise was or
+ * - `then(successCallback, [errorCallback], [notifyCallback])` – regardless of when the promise was or
* will be resolved or rejected, `then` calls one of the success or error callbacks asynchronously
* as soon as the result is available. The callbacks are called with a single argument: the result
* or rejection reason. Additionally, the notify callback may be called zero or more times to
@@ -14885,7 +16630,8 @@ function $ParseProvider() {
* with the value which is resolved in that promise using
* [promise chaining](http://www.html5rocks.com/en/tutorials/es6/promises/#toc-promises-queues)).
* It also notifies via the return value of the `notifyCallback` method. The promise cannot be
- * resolved or rejected from the notifyCallback method.
+ * resolved or rejected from the notifyCallback method. The errorCallback and notifyCallback
+ * arguments are optional.
*
* - `catch(errorCallback)` – shorthand for `promise.then(null, errorCallback)`
*
@@ -14925,7 +16671,7 @@ function $ParseProvider() {
* - Q has many more features than $q, but that comes at a cost of bytes. $q is tiny, but contains
* all the important functionality needed for common async tasks.
*
- * # Testing
+ * # Testing
*
* ```js
* it('should simulate promise', inject(function($q, $rootScope) {
@@ -14955,21 +16701,61 @@ function $ParseProvider() {
*
* @returns {Promise} The newly created promise.
*/
+/**
+ * @ngdoc provider
+ * @name $qProvider
+ * @this
+ *
+ * @description
+ */
function $QProvider() {
-
+ var errorOnUnhandledRejections = true;
this.$get = ['$rootScope', '$exceptionHandler', function($rootScope, $exceptionHandler) {
return qFactory(function(callback) {
$rootScope.$evalAsync(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ /**
+ * @ngdoc method
+ * @name $qProvider#errorOnUnhandledRejections
+ * @kind function
+ *
+ * @description
+ * Retrieves or overrides whether to generate an error when a rejected promise is not handled.
+ * This feature is enabled by default.
+ *
+ * @param {boolean=} value Whether to generate an error when a rejected promise is not handled.
+ * @returns {boolean|ng.$qProvider} Current value when called without a new value or self for
+ * chaining otherwise.
+ */
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
+/** @this */
function $$QProvider() {
+ var errorOnUnhandledRejections = true;
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) {
return qFactory(function(callback) {
$browser.defer(callback);
- }, $exceptionHandler);
+ }, $exceptionHandler, errorOnUnhandledRejections);
}];
+
+ this.errorOnUnhandledRejections = function(value) {
+ if (isDefined(value)) {
+ errorOnUnhandledRejections = value;
+ return this;
+ } else {
+ return errorOnUnhandledRejections;
+ }
+ };
}
/**
@@ -14978,10 +16764,14 @@ function $$QProvider() {
* @param {function(function)} nextTick Function for executing functions in the next turn.
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for
* debugging purposes.
+ @ param {=boolean} errorOnUnhandledRejections Whether an error should be generated on unhandled
+ * promises rejections.
* @returns {object} Promise manager.
*/
-function qFactory(nextTick, exceptionHandler) {
+function qFactory(nextTick, exceptionHandler, errorOnUnhandledRejections) {
var $qMinErr = minErr('$q', TypeError);
+ var queueSize = 0;
+ var checkQueue = [];
/**
* @ngdoc method
@@ -14993,14 +16783,18 @@ function qFactory(nextTick, exceptionHandler) {
*
* @returns {Deferred} Returns a new instance of deferred.
*/
- var defer = function() {
- var d = new Deferred();
- //Necessary to support unbound execution :/
- d.resolve = simpleBind(d, d.resolve);
- d.reject = simpleBind(d, d.reject);
- d.notify = simpleBind(d, d.notify);
- return d;
- };
+ function defer() {
+ return new Deferred();
+ }
+
+ function Deferred() {
+ var promise = this.promise = new Promise();
+ //Non prototype methods necessary to support unbound execution :/
+ this.resolve = function(val) { resolvePromise(promise, val); };
+ this.reject = function(reason) { rejectPromise(promise, reason); };
+ this.notify = function(progress) { notifyPromise(promise, progress); };
+ }
+
function Promise() {
this.$$state = { status: 0 };
@@ -15011,144 +16805,162 @@ function qFactory(nextTick, exceptionHandler) {
if (isUndefined(onFulfilled) && isUndefined(onRejected) && isUndefined(progressBack)) {
return this;
}
- var result = new Deferred();
+ var result = new Promise();
this.$$state.pending = this.$$state.pending || [];
this.$$state.pending.push([result, onFulfilled, onRejected, progressBack]);
if (this.$$state.status > 0) scheduleProcessQueue(this.$$state);
- return result.promise;
+ return result;
},
- "catch": function(callback) {
+ 'catch': function(callback) {
return this.then(null, callback);
},
- "finally": function(callback, progressBack) {
+ 'finally': function(callback, progressBack) {
return this.then(function(value) {
- return handleCallback(value, true, callback);
+ return handleCallback(value, resolve, callback);
}, function(error) {
- return handleCallback(error, false, callback);
+ return handleCallback(error, reject, callback);
}, progressBack);
}
});
- //Faster, more basic than angular.bind http://jsperf.com/angular-bind-vs-custom-vs-native
- function simpleBind(context, fn) {
- return function(value) {
- fn.call(context, value);
- };
- }
-
function processQueue(state) {
- var fn, deferred, pending;
+ var fn, promise, pending;
pending = state.pending;
state.processScheduled = false;
state.pending = undefined;
- for (var i = 0, ii = pending.length; i < ii; ++i) {
- deferred = pending[i][0];
- fn = pending[i][state.status];
- try {
- if (isFunction(fn)) {
- deferred.resolve(fn(state.value));
- } else if (state.status === 1) {
- deferred.resolve(state.value);
+ try {
+ for (var i = 0, ii = pending.length; i < ii; ++i) {
+ state.pur = true;
+ promise = pending[i][0];
+ fn = pending[i][state.status];
+ try {
+ if (isFunction(fn)) {
+ resolvePromise(promise, fn(state.value));
+ } else if (state.status === 1) {
+ resolvePromise(promise, state.value);
+ } else {
+ rejectPromise(promise, state.value);
+ }
+ } catch (e) {
+ rejectPromise(promise, e);
+ }
+ }
+ } finally {
+ --queueSize;
+ if (errorOnUnhandledRejections && queueSize === 0) {
+ nextTick(processChecks);
+ }
+ }
+ }
+
+ function processChecks() {
+ // eslint-disable-next-line no-unmodified-loop-condition
+ while (!queueSize && checkQueue.length) {
+ var toCheck = checkQueue.shift();
+ if (!toCheck.pur) {
+ toCheck.pur = true;
+ var errorMessage = 'Possibly unhandled rejection: ' + toDebugString(toCheck.value);
+ if (toCheck.value instanceof Error) {
+ exceptionHandler(toCheck.value, errorMessage);
} else {
- deferred.reject(state.value);
+ exceptionHandler(errorMessage);
}
- } catch (e) {
- deferred.reject(e);
- exceptionHandler(e);
}
}
}
function scheduleProcessQueue(state) {
+ if (errorOnUnhandledRejections && !state.pending && state.status === 2 && !state.pur) {
+ if (queueSize === 0 && checkQueue.length === 0) {
+ nextTick(processChecks);
+ }
+ checkQueue.push(state);
+ }
if (state.processScheduled || !state.pending) return;
state.processScheduled = true;
+ ++queueSize;
nextTick(function() { processQueue(state); });
}
- function Deferred() {
- this.promise = new Promise();
+ function resolvePromise(promise, val) {
+ if (promise.$$state.status) return;
+ if (val === promise) {
+ $$reject(promise, $qMinErr(
+ 'qcycle',
+ 'Expected promise to be resolved with value other than itself \'{0}\'',
+ val));
+ } else {
+ $$resolve(promise, val);
+ }
+
}
- extend(Deferred.prototype, {
- resolve: function(val) {
- if (this.promise.$$state.status) return;
- if (val === this.promise) {
- this.$$reject($qMinErr(
- 'qcycle',
- "Expected promise to be resolved with value other than itself '{0}'",
- val));
+ function $$resolve(promise, val) {
+ var then;
+ var done = false;
+ try {
+ if (isObject(val) || isFunction(val)) then = val.then;
+ if (isFunction(then)) {
+ promise.$$state.status = -1;
+ then.call(val, doResolve, doReject, doNotify);
} else {
- this.$$resolve(val);
- }
-
- },
-
- $$resolve: function(val) {
- var then;
- var that = this;
- var done = false;
- try {
- if ((isObject(val) || isFunction(val))) then = val && val.then;
- if (isFunction(then)) {
- this.promise.$$state.status = -1;
- then.call(val, resolvePromise, rejectPromise, simpleBind(this, this.notify));
- } else {
- this.promise.$$state.value = val;
- this.promise.$$state.status = 1;
- scheduleProcessQueue(this.promise.$$state);
- }
- } catch (e) {
- rejectPromise(e);
- exceptionHandler(e);
+ promise.$$state.value = val;
+ promise.$$state.status = 1;
+ scheduleProcessQueue(promise.$$state);
}
+ } catch (e) {
+ doReject(e);
+ }
- function resolvePromise(val) {
- if (done) return;
- done = true;
- that.$$resolve(val);
- }
- function rejectPromise(val) {
- if (done) return;
- done = true;
- that.$$reject(val);
- }
- },
+ function doResolve(val) {
+ if (done) return;
+ done = true;
+ $$resolve(promise, val);
+ }
+ function doReject(val) {
+ if (done) return;
+ done = true;
+ $$reject(promise, val);
+ }
+ function doNotify(progress) {
+ notifyPromise(promise, progress);
+ }
+ }
- reject: function(reason) {
- if (this.promise.$$state.status) return;
- this.$$reject(reason);
- },
+ function rejectPromise(promise, reason) {
+ if (promise.$$state.status) return;
+ $$reject(promise, reason);
+ }
- $$reject: function(reason) {
- this.promise.$$state.value = reason;
- this.promise.$$state.status = 2;
- scheduleProcessQueue(this.promise.$$state);
- },
+ function $$reject(promise, reason) {
+ promise.$$state.value = reason;
+ promise.$$state.status = 2;
+ scheduleProcessQueue(promise.$$state);
+ }
- notify: function(progress) {
- var callbacks = this.promise.$$state.pending;
+ function notifyPromise(promise, progress) {
+ var callbacks = promise.$$state.pending;
- if ((this.promise.$$state.status <= 0) && callbacks && callbacks.length) {
- nextTick(function() {
- var callback, result;
- for (var i = 0, ii = callbacks.length; i < ii; i++) {
- result = callbacks[i][0];
- callback = callbacks[i][3];
- try {
- result.notify(isFunction(callback) ? callback(progress) : progress);
- } catch (e) {
- exceptionHandler(e);
- }
+ if ((promise.$$state.status <= 0) && callbacks && callbacks.length) {
+ nextTick(function() {
+ var callback, result;
+ for (var i = 0, ii = callbacks.length; i < ii; i++) {
+ result = callbacks[i][0];
+ callback = callbacks[i][3];
+ try {
+ notifyPromise(result, isFunction(callback) ? callback(progress) : progress);
+ } catch (e) {
+ exceptionHandler(e);
}
- });
- }
+ }
+ });
}
- });
+ }
/**
* @ngdoc method
@@ -15186,39 +16998,27 @@ function qFactory(nextTick, exceptionHandler) {
* @param {*} reason Constant, message, exception or an object representing the rejection reason.
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`.
*/
- var reject = function(reason) {
- var result = new Deferred();
- result.reject(reason);
- return result.promise;
- };
-
- var makePromise = function makePromise(value, resolved) {
- var result = new Deferred();
- if (resolved) {
- result.resolve(value);
- } else {
- result.reject(value);
- }
- return result.promise;
- };
+ function reject(reason) {
+ var result = new Promise();
+ rejectPromise(result, reason);
+ return result;
+ }
- var handleCallback = function handleCallback(value, isResolved, callback) {
+ function handleCallback(value, resolver, callback) {
var callbackOutput = null;
try {
if (isFunction(callback)) callbackOutput = callback();
} catch (e) {
- return makePromise(e, false);
+ return reject(e);
}
if (isPromiseLike(callbackOutput)) {
return callbackOutput.then(function() {
- return makePromise(value, isResolved);
- }, function(error) {
- return makePromise(error, false);
- });
+ return resolver(value);
+ }, reject);
} else {
- return makePromise(value, isResolved);
+ return resolver(value);
}
- };
+ }
/**
* @ngdoc method
@@ -15238,11 +17038,11 @@ function qFactory(nextTick, exceptionHandler) {
*/
- var when = function(value, callback, errback, progressBack) {
- var result = new Deferred();
- result.resolve(value);
- return result.promise.then(callback, errback, progressBack);
- };
+ function when(value, callback, errback, progressBack) {
+ var result = new Promise();
+ resolvePromise(result, value);
+ return result.then(callback, errback, progressBack);
+ }
/**
* @ngdoc method
@@ -15277,63 +17077,86 @@ function qFactory(nextTick, exceptionHandler) {
*/
function all(promises) {
- var deferred = new Deferred(),
+ var result = new Promise(),
counter = 0,
results = isArray(promises) ? [] : {};
forEach(promises, function(promise, key) {
counter++;
when(promise).then(function(value) {
- if (results.hasOwnProperty(key)) return;
results[key] = value;
- if (!(--counter)) deferred.resolve(results);
+ if (!(--counter)) resolvePromise(result, results);
}, function(reason) {
- if (results.hasOwnProperty(key)) return;
- deferred.reject(reason);
+ rejectPromise(result, reason);
});
});
if (counter === 0) {
- deferred.resolve(results);
+ resolvePromise(result, results);
}
+ return result;
+ }
+
+ /**
+ * @ngdoc method
+ * @name $q#race
+ * @kind function
+ *
+ * @description
+ * Returns a promise that resolves or rejects as soon as one of those promises
+ * resolves or rejects, with the value or reason from that promise.
+ *
+ * @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises.
+ * @returns {Promise} a promise that resolves or rejects as soon as one of the `promises`
+ * resolves or rejects, with the value or reason from that promise.
+ */
+
+ function race(promises) {
+ var deferred = defer();
+
+ forEach(promises, function(promise) {
+ when(promise).then(deferred.resolve, deferred.reject);
+ });
+
return deferred.promise;
}
- var $Q = function Q(resolver) {
+ function $Q(resolver) {
if (!isFunction(resolver)) {
- throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
- }
-
- if (!(this instanceof Q)) {
- // More useful when $Q is the Promise itself.
- return new Q(resolver);
+ throw $qMinErr('norslvr', 'Expected resolverFn, got \'{0}\'', resolver);
}
- var deferred = new Deferred();
+ var promise = new Promise();
function resolveFn(value) {
- deferred.resolve(value);
+ resolvePromise(promise, value);
}
function rejectFn(reason) {
- deferred.reject(reason);
+ rejectPromise(promise, reason);
}
resolver(resolveFn, rejectFn);
- return deferred.promise;
- };
+ return promise;
+ }
+
+ // Let's make the instanceof operator work for promises, so that
+ // `new $q(fn) instanceof $q` would evaluate to true.
+ $Q.prototype = Promise.prototype;
$Q.defer = defer;
$Q.reject = reject;
$Q.when = when;
$Q.resolve = resolve;
$Q.all = all;
+ $Q.race = race;
return $Q;
}
+/** @this */
function $$RAFProvider() { //rAF
this.$get = ['$window', '$timeout', function($window, $timeout) {
var requestAnimationFrame = $window.requestAnimationFrame ||
@@ -15423,6 +17246,8 @@ function $$RAFProvider() { //rAF
/**
* @ngdoc service
* @name $rootScope
+ * @this
+ *
* @description
*
* Every application has a single root {@link ng.$rootScope.Scope scope}.
@@ -15467,14 +17292,19 @@ function $RootScopeProvider() {
function cleanUpScope($scope) {
+ // Support: IE 9 only
if (msie === 9) {
// There is a memory leak in IE9 if all child scopes are not disconnected
// completely when a scope is destroyed. So this code will recurse up through
// all this scopes children
//
// See issue https://github.com/angular/angular.js/issues/10706
- $scope.$$childHead && cleanUpScope($scope.$$childHead);
- $scope.$$nextSibling && cleanUpScope($scope.$$nextSibling);
+ if ($scope.$$childHead) {
+ cleanUpScope($scope.$$childHead);
+ }
+ if ($scope.$$nextSibling) {
+ cleanUpScope($scope.$$nextSibling);
+ }
}
// The code below works around IE9 and V8's memory leaks
@@ -15626,7 +17456,7 @@ function $RootScopeProvider() {
// prototypically. In all other cases, this property needs to be set
// when the parent scope is destroyed.
// The listener needs to be added after the parent is set
- if (isolate || parent != this) child.$on('$destroy', destroyChildScope);
+ if (isolate || parent !== this) child.$on('$destroy', destroyChildScope);
return child;
},
@@ -15643,7 +17473,7 @@ function $RootScopeProvider() {
* $digest()} and should return the value that will be watched. (`watchExpression` should not change
* its value when executed multiple times with the same input because it may be executed multiple
* times by {@link ng.$rootScope.Scope#$digest $digest()}. That is, `watchExpression` should be
- * [idempotent](http://en.wikipedia.org/wiki/Idempotence).
+ * [idempotent](http://en.wikipedia.org/wiki/Idempotence).)
* - The `listener` is called only when the value from the current `watchExpression` and the
* previous call to `watchExpression` are not equal (with the exception of the initial run,
* see below). Inequality is determined according to reference inequality,
@@ -15654,6 +17484,8 @@ function $RootScopeProvider() {
* according to the {@link angular.equals} function. To save the value of the object for
* later comparison, the {@link angular.copy} function is used. This therefore means that
* watching complex objects will have adverse memory and performance implications.
+ * - This should not be used to watch for changes in objects that are
+ * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
* This is achieved by rerunning the watchers until no changes are detected. The rerun
* iteration limit is 10 to prevent an infinite loop deadlock.
@@ -15771,15 +17603,21 @@ function $RootScopeProvider() {
if (!array) {
array = scope.$$watchers = [];
+ array.$$digestWatchIndex = -1;
}
// we use unshift since we use a while loop in $digest for speed.
// the while loop reads in reverse order.
array.unshift(watcher);
+ array.$$digestWatchIndex++;
incrementWatchersCount(this, 1);
return function deregisterWatch() {
- if (arrayRemove(array, watcher) >= 0) {
+ var index = arrayRemove(array, watcher);
+ if (index >= 0) {
incrementWatchersCount(scope, -1);
+ if (index < array.$$digestWatchIndex) {
+ array.$$digestWatchIndex--;
+ }
}
lastDirtyWatch = null;
};
@@ -15794,8 +17632,8 @@ function $RootScopeProvider() {
* A variant of {@link ng.$rootScope.Scope#$watch $watch()} where it watches an array of `watchExpressions`.
* If any one expression in the collection changes the `listener` is executed.
*
- * - The items in the `watchExpressions` array are observed via standard $watch operation and are examined on every
- * call to $digest() to see if any items changes.
+ * - The items in the `watchExpressions` array are observed via the standard `$watch` operation. Their return
+ * values are examined for changes on every call to `$digest`.
* - The `listener` is called whenever any expression in the `watchExpressions` array changes.
*
* @param {Array.<string|Function(scope)>} watchExpressions Array of expressions that will be individually
@@ -15976,6 +17814,7 @@ function $RootScopeProvider() {
oldItem = oldValue[i];
newItem = newValue[i];
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -15998,6 +17837,7 @@ function $RootScopeProvider() {
oldItem = oldValue[key];
if (key in oldValue) {
+ // eslint-disable-next-line no-self-compare
bothNaN = (oldItem !== oldItem) && (newItem !== newItem);
if (!bothNaN && (oldItem !== newItem)) {
changeDetected++;
@@ -16108,13 +17948,12 @@ function $RootScopeProvider() {
*
*/
$digest: function() {
- var watch, value, last,
+ var watch, value, last, fn, get,
watchers,
- length,
dirty, ttl = TTL,
next, current, target = this,
watchLog = [],
- logIdx, logMsg, asyncTask;
+ logIdx, asyncTask;
beginPhase('$digest');
// Check for changes to browser url that happened in sync before the call to $digest
@@ -16133,36 +17972,42 @@ function $RootScopeProvider() {
dirty = false;
current = target;
- while (asyncQueue.length) {
+ // It's safe for asyncQueuePosition to be a local variable here because this loop can't
+ // be reentered recursively. Calling $digest from a function passed to $evalAsync would
+ // lead to a '$digest already in progress' error.
+ for (var asyncQueuePosition = 0; asyncQueuePosition < asyncQueue.length; asyncQueuePosition++) {
try {
- asyncTask = asyncQueue.shift();
- asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
+ asyncTask = asyncQueue[asyncQueuePosition];
+ fn = asyncTask.fn;
+ fn(asyncTask.scope, asyncTask.locals);
} catch (e) {
$exceptionHandler(e);
}
lastDirtyWatch = null;
}
+ asyncQueue.length = 0;
traverseScopesLoop:
do { // "traverse the scopes" loop
if ((watchers = current.$$watchers)) {
// process our watches
- length = watchers.length;
- while (length--) {
+ watchers.$$digestWatchIndex = watchers.length;
+ while (watchers.$$digestWatchIndex--) {
try {
- watch = watchers[length];
+ watch = watchers[watchers.$$digestWatchIndex];
// Most common watches are on primitives, in which case we can short
// circuit it with === operator, only when === fails do we use .equals
if (watch) {
- if ((value = watch.get(current)) !== (last = watch.last) &&
+ get = watch.get;
+ if ((value = get(current)) !== (last = watch.last) &&
!(watch.eq
? equals(value, last)
- : (typeof value === 'number' && typeof last === 'number'
- && isNaN(value) && isNaN(last)))) {
+ : (isNumberNaN(value) && isNumberNaN(last)))) {
dirty = true;
lastDirtyWatch = watch;
watch.last = watch.eq ? copy(value, null) : value;
- watch.fn(value, ((last === initWatchVal) ? value : last), current);
+ fn = watch.fn;
+ fn(value, ((last === initWatchVal) ? value : last), current);
if (ttl < 5) {
logIdx = 4 - ttl;
if (!watchLog[logIdx]) watchLog[logIdx] = [];
@@ -16210,13 +18055,19 @@ function $RootScopeProvider() {
clearPhase();
- while (postDigestQueue.length) {
+ // postDigestQueuePosition isn't local here because this loop can be reentered recursively.
+ while (postDigestQueuePosition < postDigestQueue.length) {
try {
- postDigestQueue.shift()();
+ postDigestQueue[postDigestQueuePosition++]();
} catch (e) {
$exceptionHandler(e);
}
}
+ postDigestQueue.length = postDigestQueuePosition = 0;
+
+ // Check for changes to browser url that happened during the $digest
+ // (for which no event is fired; e.g. via `history.pushState()`)
+ $browser.$$checkUrlChange();
},
@@ -16274,8 +18125,8 @@ function $RootScopeProvider() {
// sever all the references to parent scopes (after this cleanup, the current scope should
// not be retained by any of our references and should be eligible for garbage collection)
- if (parent && parent.$$childHead == this) parent.$$childHead = this.$$nextSibling;
- if (parent && parent.$$childTail == this) parent.$$childTail = this.$$prevSibling;
+ if (parent && parent.$$childHead === this) parent.$$childHead = this.$$nextSibling;
+ if (parent && parent.$$childTail === this) parent.$$childTail = this.$$prevSibling;
if (this.$$prevSibling) this.$$prevSibling.$$nextSibling = this.$$nextSibling;
if (this.$$nextSibling) this.$$nextSibling.$$prevSibling = this.$$prevSibling;
@@ -16362,7 +18213,7 @@ function $RootScopeProvider() {
});
}
- asyncQueue.push({scope: this, expression: expr, locals: locals});
+ asyncQueue.push({scope: this, fn: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
@@ -16429,6 +18280,7 @@ function $RootScopeProvider() {
$rootScope.$digest();
} catch (e) {
$exceptionHandler(e);
+ // eslint-disable-next-line no-unsafe-finally
throw e;
}
}
@@ -16453,7 +18305,10 @@ function $RootScopeProvider() {
*/
$applyAsync: function(expr) {
var scope = this;
- expr && applyAsyncQueue.push($applyAsyncExpression);
+ if (expr) {
+ applyAsyncQueue.push($applyAsyncExpression);
+ }
+ expr = $parse(expr);
scheduleApplyAsync();
function $applyAsyncExpression() {
@@ -16670,6 +18525,8 @@ function $RootScopeProvider() {
var postDigestQueue = $rootScope.$$postDigestQueue = [];
var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];
+ var postDigestQueuePosition = 0;
+
return $rootScope;
@@ -16729,6 +18586,22 @@ function $RootScopeProvider() {
}
/**
+ * @ngdoc service
+ * @name $rootElement
+ *
+ * @description
+ * The root element of Angular application. This is either the element where {@link
+ * ng.directive:ngApp ngApp} was declared or the element passed into
+ * {@link angular.bootstrap}. The element represents the root element of application. It is also the
+ * location where the application's {@link auto.$injector $injector} service gets
+ * published, and can be retrieved using `$rootElement.injector()`.
+ */
+
+
+// the implementation is in angular.bootstrap
+
+/**
+ * @this
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
@@ -16809,6 +18682,8 @@ function $$SanitizeUriProvider() {
* Or gives undesired access to variables likes document or window? *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+/* exported $SceProvider, $SceDelegateProvider */
+
var $sceMinErr = minErr('$sce');
var SCE_CONTEXTS = {
@@ -16823,6 +18698,13 @@ var SCE_CONTEXTS = {
// Helper functions follow.
+var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;
+
+function snakeToCamel(name) {
+ return name
+ .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
+}
+
function adjustMatcher(matcher) {
if (matcher === 'self') {
return matcher;
@@ -16836,8 +18718,8 @@ function adjustMatcher(matcher) {
'Illegal sequence *** in string matcher. String: {0}', matcher);
}
matcher = escapeForRegexp(matcher).
- replace('\\*\\*', '.*').
- replace('\\*', '[^:/.?&;]*');
+ replace(/\\\*\\\*/g, '.*').
+ replace(/\\\*/g, '[^:/.?&;]*');
return new RegExp('^' + matcher + '$');
} else if (isRegExp(matcher)) {
// The only other type of matcher allowed is a Regexp.
@@ -16892,6 +18774,8 @@ function adjustMatchers(matchers) {
/**
* @ngdoc provider
* @name $sceDelegateProvider
+ * @this
+ *
* @description
*
* The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
@@ -16907,7 +18791,7 @@ function adjustMatchers(matchers) {
*
* - your app is hosted at url `http://myapp.example.com/`
* - but some of your templates are hosted on other domains you control such as
- * `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc.
+ * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
* - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
*
* Here is what a secure configuration for this scenario might look like:
@@ -16942,13 +18826,15 @@ function $SceDelegateProvider() {
* @kind function
*
* @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * provided. This must be an array or null. A snapshot of this array is used so further
+ * changes to the array are ignored.
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * Note: **an empty whitelist array will block all URLs**!
+ * <div class="alert alert-warning">
+ * **Note:** an empty whitelist array will block all URLs!
+ * </div>
*
* @return {Array} the currently set whitelist array.
*
@@ -16971,17 +18857,17 @@ function $SceDelegateProvider() {
* @kind function
*
* @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value
- * provided. This must be an array or null. A snapshot of this array is used so further
- * changes to the array are ignored.
+ * provided. This must be an array or null. A snapshot of this array is used so further
+ * changes to the array are ignored.
*
- * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
- * allowed in this array.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
*
- * The typical usage for the blacklist is to **block
- * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
- * these would otherwise be trusted but actually return content from the redirected domain.
+ * The typical usage for the blacklist is to **block
+ * [open redirects](http://cwe.mitre.org/data/definitions/601.html)** served by your domain as
+ * these would otherwise be trusted but actually return content from the redirected domain.
*
- * Finally, **the blacklist overrides the whitelist** and has the final say.
+ * Finally, **the blacklist overrides the whitelist** and has the final say.
*
* @return {Array} the currently set blacklist array.
*
@@ -17186,6 +19072,8 @@ function $SceDelegateProvider() {
/**
* @ngdoc provider
* @name $sceProvider
+ * @this
+ *
* @description
*
* The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service.
@@ -17195,8 +19083,6 @@ function $SceDelegateProvider() {
* Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
*/
-/* jshint maxlen: false*/
-
/**
* @ngdoc service
* @name $sce
@@ -17221,7 +19107,7 @@ function $SceDelegateProvider() {
* You can ensure your document is in standards mode and not quirks mode by adding `<!doctype html>`
* to the top of your HTML document.
*
- * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for
+ * SCE assists in writing code in a way that (a) is secure by default and (b) makes auditing for
* security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
*
* Here's an example of a binding in a privileged context:
@@ -17329,7 +19215,7 @@ function $SceDelegateProvider() {
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered and the {@link ngSanitize $sanitize} module is present this will sanitize the value instead of throwing an error. |
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
* | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't constitute an SCE context. |
- * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
+ * | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contents are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG`, `VIDEO`, `AUDIO`, `SOURCE`, and `TRACK` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
*
* ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a>
@@ -17381,7 +19267,7 @@ function $SceDelegateProvider() {
*
* ## Show me an example using SCE.
*
- * <example module="mySceApp" deps="angular-sanitize.js">
+ * <example module="mySceApp" deps="angular-sanitize.js" name="sce-service">
* <file name="index.html">
* <div ng-controller="AppController as myCtrl">
* <i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br>
@@ -17402,10 +19288,10 @@ function $SceDelegateProvider() {
* <file name="script.js">
* angular.module('mySceApp', ['ngSanitize'])
* .controller('AppController', ['$http', '$templateCache', '$sce',
- * function($http, $templateCache, $sce) {
+ * function AppController($http, $templateCache, $sce) {
* var self = this;
- * $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
- * self.userComments = userComments;
+ * $http.get('test_data.json', {cache: $templateCache}).then(function(response) {
+ * self.userComments = response.data;
* });
* self.explicitlyTrustedHtml = $sce.trustAsHtml(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
@@ -17428,12 +19314,12 @@ function $SceDelegateProvider() {
* <file name="protractor.js" type="protractor">
* describe('SCE doc demo', function() {
* it('should sanitize untrusted values', function() {
- * expect(element.all(by.css('.htmlComment')).first().getInnerHtml())
+ * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML'))
* .toBe('<span>Is <i>anyone</i> reading this?</span>');
* });
*
* it('should NOT sanitize explicitly trusted values', function() {
- * expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe(
+ * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe(
* '<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
* 'sanitization.&quot;">Hover over this text.</span>');
* });
@@ -17462,7 +19348,6 @@ function $SceDelegateProvider() {
* ```
*
*/
-/* jshint maxlen: 100 */
function $SceProvider() {
var enabled = true;
@@ -17534,6 +19419,7 @@ function $SceProvider() {
this.$get = ['$parse', '$sceDelegate', function(
$parse, $sceDelegate) {
+ // Support: IE 9-11 only
// Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
// the "expression(javascript expression)" syntax which is insecure.
if (enabled && msie < 8) {
@@ -17847,13 +19733,13 @@ function $SceProvider() {
forEach(SCE_CONTEXTS, function(enumValue, name) {
var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function(expr) {
+ sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
return parse(enumValue, expr);
};
- sce[camelCase("get_trusted_" + lName)] = function(value) {
+ sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
return getTrusted(enumValue, value);
};
- sce[camelCase("trust_as_" + lName)] = function(value) {
+ sce[snakeToCamel('trust_as_' + lName)] = function(value) {
return trustAs(enumValue, value);
};
});
@@ -17862,12 +19748,15 @@ function $SceProvider() {
}];
}
+/* exported $SnifferProvider */
+
/**
* !!! This is an undocumented "private" service !!!
*
* @name $sniffer
* @requires $window
* @requires $document
+ * @this
*
* @property {boolean} history Does the browser support html5 history api ?
* @property {boolean} transitions Does the browser support CSS transition events ?
@@ -17879,37 +19768,32 @@ function $SceProvider() {
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
+ // Chrome Packaged Apps are not allowed to access `history.pushState`.
+ // If not sandboxed, they can be detected by the presence of `chrome.app.runtime`
+ // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by
+ // the presence of an extension runtime ID and the absence of other Chrome runtime APIs
+ // (see https://developer.chrome.com/apps/manifest/sandbox).
+ // (NW.js apps have access to Chrome APIs, but do support `history`.)
+ isNw = $window.nw && $window.nw.process,
+ isChromePackagedApp =
+ !isNw &&
+ $window.chrome &&
+ ($window.chrome.app && $window.chrome.app.runtime ||
+ !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id),
+ hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
document = $document[0] || {},
- vendorPrefix,
- vendorRegex = /^(Moz|webkit|ms)(?=[A-Z])/,
bodyStyle = document.body && document.body.style,
transitions = false,
- animations = false,
- match;
+ animations = false;
if (bodyStyle) {
- for (var prop in bodyStyle) {
- if (match = vendorRegex.exec(prop)) {
- vendorPrefix = match[0];
- vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
- break;
- }
- }
-
- if (!vendorPrefix) {
- vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
- }
-
- transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
- animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
-
- if (android && (!transitions || !animations)) {
- transitions = isString(bodyStyle.webkitTransition);
- animations = isString(bodyStyle.webkitAnimation);
- }
+ // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x
+ // Mentioned browsers need a -webkit- prefix for transitions & animations.
+ transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle);
+ animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle);
}
@@ -17922,16 +19806,15 @@ function $SnifferProvider() {
// older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
// so let's not use the history API also
// We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
- // jshint -W018
- history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
- // jshint +W018
+ history: !!(hasHistoryPushState && !(android < 4) && !boxee),
hasEvent: function(event) {
+ // Support: IE 9-11 only
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
// it. In particular the event is not fired when backspace or delete key are pressed or
// when cut operation is performed.
// IE10+ implements 'input' event but it erroneously fires under various situations,
// e.g. when placeholder changes, or a form is focused.
- if (event === 'input' && msie <= 11) return false;
+ if (event === 'input' && msie) return false;
if (isUndefined(eventSupport[event])) {
var divElm = document.createElement('div');
@@ -17941,7 +19824,6 @@ function $SnifferProvider() {
return eventSupport[event];
},
csp: csp(),
- vendorPrefix: vendorPrefix,
transitions: transitions,
animations: animations,
android: android
@@ -17949,11 +19831,13 @@ function $SnifferProvider() {
}];
}
-var $compileMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$compile');
/**
* @ngdoc provider
* @name $templateRequestProvider
+ * @this
+ *
* @description
* Used to configure the options passed to the {@link $http} service when making a template request.
*
@@ -18007,57 +19891,64 @@ function $TemplateRequestProvider() {
*
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
*/
- this.$get = ['$templateCache', '$http', '$q', '$sce', function($templateCache, $http, $q, $sce) {
-
- function handleRequestFn(tpl, ignoreRequestError) {
- handleRequestFn.totalPendingRequests++;
-
- // We consider the template cache holds only trusted templates, so
- // there's no need to go through whitelisting again for keys that already
- // are included in there. This also makes Angular accept any script
- // directive, no matter its name. However, we still need to unwrap trusted
- // types.
- if (!isString(tpl) || !$templateCache.get(tpl)) {
- tpl = $sce.getTrustedResourceUrl(tpl);
- }
+ this.$get = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce',
+ function($exceptionHandler, $templateCache, $http, $q, $sce) {
+
+ function handleRequestFn(tpl, ignoreRequestError) {
+ handleRequestFn.totalPendingRequests++;
+
+ // We consider the template cache holds only trusted templates, so
+ // there's no need to go through whitelisting again for keys that already
+ // are included in there. This also makes Angular accept any script
+ // directive, no matter its name. However, we still need to unwrap trusted
+ // types.
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
+ tpl = $sce.getTrustedResourceUrl(tpl);
+ }
- var transformResponse = $http.defaults && $http.defaults.transformResponse;
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
- if (isArray(transformResponse)) {
- transformResponse = transformResponse.filter(function(transformer) {
- return transformer !== defaultHttpResponseTransform;
- });
- } else if (transformResponse === defaultHttpResponseTransform) {
- transformResponse = null;
- }
+ if (isArray(transformResponse)) {
+ transformResponse = transformResponse.filter(function(transformer) {
+ return transformer !== defaultHttpResponseTransform;
+ });
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
- return $http.get(tpl, extend({
- cache: $templateCache,
- transformResponse: transformResponse
- }, httpOptions))
- ['finally'](function() {
- handleRequestFn.totalPendingRequests--;
- })
- .then(function(response) {
- $templateCache.put(tpl, response.data);
- return response.data;
- }, handleError);
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
+ .finally(function() {
+ handleRequestFn.totalPendingRequests--;
+ })
+ .then(function(response) {
+ $templateCache.put(tpl, response.data);
+ return response.data;
+ }, handleError);
+
+ function handleError(resp) {
+ if (!ignoreRequestError) {
+ resp = $templateRequestMinErr('tpload',
+ 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
+
+ $exceptionHandler(resp);
+ }
- function handleError(resp) {
- if (!ignoreRequestError) {
- throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
- tpl, resp.status, resp.statusText);
+ return $q.reject(resp);
}
- return $q.reject(resp);
}
- }
- handleRequestFn.totalPendingRequests = 0;
+ handleRequestFn.totalPendingRequests = 0;
- return handleRequestFn;
- }];
+ return handleRequestFn;
+ }
+ ];
}
+/** @this */
function $$TestabilityProvider() {
this.$get = ['$rootScope', '$browser', '$location',
function($rootScope, $browser, $location) {
@@ -18096,7 +19987,7 @@ function $$TestabilityProvider() {
matches.push(binding);
}
} else {
- if (bindingName.indexOf(expression) != -1) {
+ if (bindingName.indexOf(expression) !== -1) {
matches.push(binding);
}
}
@@ -18173,6 +20064,7 @@ function $$TestabilityProvider() {
}];
}
+/** @this */
function $TimeoutProvider() {
this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
function($rootScope, $browser, $q, $$q, $exceptionHandler) {
@@ -18205,8 +20097,8 @@ function $TimeoutProvider() {
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @param {...*=} Pass additional parameters to the executed function.
- * @returns {Promise} Promise that will be resolved when the timeout is reached. The value this
- * promise will be resolved with is the return value of the `fn` function.
+ * @returns {Promise} Promise that will be resolved when the timeout is reached. The promise
+ * will be resolved with the return value of the `fn` function.
*
*/
function timeout(fn, delay, invokeApply) {
@@ -18228,8 +20120,7 @@ function $TimeoutProvider() {
} catch (e) {
deferred.reject(e);
$exceptionHandler(e);
- }
- finally {
+ } finally {
delete deferreds[promise.$$timeoutId];
}
@@ -18257,6 +20148,8 @@ function $TimeoutProvider() {
*/
timeout.cancel = function(promise) {
if (promise && promise.$$timeoutId in deferreds) {
+ // Timeout cancels should not report an unhandled promise.
+ deferreds[promise.$$timeoutId].promise.catch(noop);
deferreds[promise.$$timeoutId].reject('canceled');
delete deferreds[promise.$$timeoutId];
return $browser.defer.cancel(promise.$$timeoutId);
@@ -18275,7 +20168,7 @@ function $TimeoutProvider() {
// doesn't know about mocked locations and resolves URLs to the real document - which is
// exactly the behavior needed here. There is little value is mocking these out for this
// service.
-var urlParsingNode = document.createElement("a");
+var urlParsingNode = window.document.createElement('a');
var originUrl = urlResolve(window.location.href);
@@ -18288,7 +20181,7 @@ var originUrl = urlResolve(window.location.href);
* URL will be resolved into an absolute URL in the context of the application document.
* Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
* properties are all populated to reflect the normalized URL. This approach has wide
- * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
+ * compatibility - Safari 1+, Mozilla 1+ etc. See
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
*
* Implementation Notes for IE
@@ -18327,10 +20220,11 @@ var originUrl = urlResolve(window.location.href);
function urlResolve(url) {
var href = url;
+ // Support: IE 9-11 only
if (msie) {
// Normalize before parse. Refer Implementation Notes on why this is
// done in two steps on IE.
- urlParsingNode.setAttribute("href", href);
+ urlParsingNode.setAttribute('href', href);
href = urlParsingNode.href;
}
@@ -18367,6 +20261,7 @@ function urlIsSameOrigin(requestUrl) {
/**
* @ngdoc service
* @name $window
+ * @this
*
* @description
* A reference to the browser's `window` object. While `window`
@@ -18380,7 +20275,7 @@ function urlIsSameOrigin(requestUrl) {
* expression.
*
* @example
- <example module="windowExample">
+ <example module="windowExample" name="window-service">
<file name="index.html">
<script>
angular.module('windowExample', [])
@@ -18423,6 +20318,14 @@ function $$CookieReader($document) {
var lastCookies = {};
var lastCookieString = '';
+ function safeGetCookie(rawDocument) {
+ try {
+ return rawDocument.cookie || '';
+ } catch (e) {
+ return '';
+ }
+ }
+
function safeDecodeURIComponent(str) {
try {
return decodeURIComponent(str);
@@ -18433,7 +20336,7 @@ function $$CookieReader($document) {
return function() {
var cookieArray, cookie, i, index, name;
- var currentCookieString = rawDocument.cookie || '';
+ var currentCookieString = safeGetCookie(rawDocument);
if (currentCookieString !== lastCookieString) {
lastCookieString = currentCookieString;
@@ -18460,6 +20363,7 @@ function $$CookieReader($document) {
$$CookieReader.$inject = ['$document'];
+/** @this */
function $$CookieReaderProvider() {
this.$get = $$CookieReader;
}
@@ -18539,9 +20443,15 @@ function $$CookieReaderProvider() {
* @description
* Filters are used for formatting data displayed to the user.
*
+ * They can be used in view templates, controllers or services.Angular comes
+ * with a collection of [built-in filters](api/ng/filter), but it is easy to
+ * define your own as well.
+ *
* The general syntax in templates is as follows:
*
- * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```html
+ * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```
*
* @param {String} name Name of the filter function to retrieve
* @return {Function} the filter function
@@ -18564,6 +20474,7 @@ function $$CookieReaderProvider() {
</example>
*/
$FilterProvider.$inject = ['$provide'];
+/** @this */
function $FilterProvider($provide) {
var suffix = 'Filter';
@@ -18613,7 +20524,7 @@ function $FilterProvider($provide) {
lowercaseFilter: false,
numberFilter: false,
orderByFilter: false,
- uppercaseFilter: false,
+ uppercaseFilter: false
*/
register('currency', currencyFilter);
@@ -18636,6 +20547,9 @@ function $FilterProvider($provide) {
* Selects a subset of items from `array` and returns it as a new array.
*
* @param {Array} array The source array.
+ * <div class="alert alert-info">
+ * **Note**: If the array contains objects that reference themselves, filtering is not possible.
+ * </div>
* @param {string|Object|function()} expression The predicate to be used for selecting items from
* `array`.
*
@@ -18649,10 +20563,11 @@ function $FilterProvider($provide) {
* - `Object`: A pattern object can be used to filter specific properties on objects contained
* by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
* which have property `name` containing "M" and property `phone` containing "1". A special
- * property name `$` can be used (as in `{$:"text"}`) to accept a match against any
- * property of the object or its nested object properties. That's equivalent to the simple
- * substring match with a `string` as described above. The predicate can be negated by prefixing
- * the string with `!`.
+ * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
+ * against any property of the object or its nested object properties. That's equivalent to the
+ * simple substring match with a `string` as described above. The special property name can be
+ * overwritten, using the `anyPropertyKey` parameter.
+ * The predicate can be negated by prefixing the string with `!`.
* For example `{name: "!M"}` predicate will return an array of items which have property `name`
* not containing "M".
*
@@ -18667,7 +20582,7 @@ function $FilterProvider($provide) {
*
* The final result is an array of those elements that the predicate returned true for.
*
- * @param {function(actual, expected)|true|undefined} comparator Comparator which is used in
+ * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in
* determining if the expected value (from the filter expression) and actual value (from
* the object in the array) should be considered a match.
*
@@ -18680,14 +20595,18 @@ function $FilterProvider($provide) {
* - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
* This is essentially strict comparison of expected and actual.
*
- * - `false|undefined`: A short hand for a function which will look for a substring match in case
- * insensitive way.
+ * - `false`: A short hand for a function which will look for a substring match in a case
+ * insensitive way. Primitive values are converted to strings. Objects are not compared against
+ * primitives, unless they have a custom `toString` method (e.g. `Date` objects).
+ *
+ *
+ * Defaults to `false`.
*
- * Primitive values are converted to strings. Objects are not compared against primitives,
- * unless they have a custom `toString` method (e.g. `Date` objects).
+ * @param {string} [anyPropertyKey] The special property name that matches against any property.
+ * By default `$`.
*
* @example
- <example>
+ <example name="filter-filter">
<file name="index.html">
<div ng-init="friends = [{name:'John', phone:'555-1276'},
{name:'Mary', phone:'800-BIG-MARY'},
@@ -18754,8 +20673,9 @@ function $FilterProvider($provide) {
</file>
</example>
*/
+
function filterFilter() {
- return function(array, expression, comparator) {
+ return function(array, expression, comparator, anyPropertyKey) {
if (!isArrayLike(array)) {
if (array == null) {
return array;
@@ -18764,6 +20684,7 @@ function filterFilter() {
}
}
+ anyPropertyKey = anyPropertyKey || '$';
var expressionType = getTypeForFilter(expression);
var predicateFn;
var matchAgainstAnyProp;
@@ -18777,10 +20698,9 @@ function filterFilter() {
case 'number':
case 'string':
matchAgainstAnyProp = true;
- //jshint -W086
+ // falls through
case 'object':
- //jshint +W086
- predicateFn = createPredicateFn(expression, comparator, matchAgainstAnyProp);
+ predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
break;
default:
return array;
@@ -18791,8 +20711,8 @@ function filterFilter() {
}
// Helper functions for `filterFilter`
-function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
- var shouldMatchPrimitives = isObject(expression) && ('$' in expression);
+function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) {
+ var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression);
var predicateFn;
if (comparator === true) {
@@ -18820,25 +20740,25 @@ function createPredicateFn(expression, comparator, matchAgainstAnyProp) {
predicateFn = function(item) {
if (shouldMatchPrimitives && !isObject(item)) {
- return deepCompare(item, expression.$, comparator, false);
+ return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false);
}
- return deepCompare(item, expression, comparator, matchAgainstAnyProp);
+ return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp);
};
return predicateFn;
}
-function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatchWholeObject) {
+function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) {
var actualType = getTypeForFilter(actual);
var expectedType = getTypeForFilter(expected);
if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
- return !deepCompare(actual, expected.substring(1), comparator, matchAgainstAnyProp);
+ return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp);
} else if (isArray(actual)) {
// In case `actual` is an array, consider it a match
// if ANY of it's items matches `expected`
return actual.some(function(item) {
- return deepCompare(item, expected, comparator, matchAgainstAnyProp);
+ return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp);
});
}
@@ -18847,11 +20767,14 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
var key;
if (matchAgainstAnyProp) {
for (key in actual) {
- if ((key.charAt(0) !== '$') && deepCompare(actual[key], expected, comparator, true)) {
+ // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined
+ // See: https://github.com/angular/angular.js/issues/15644
+ if (key.charAt && (key.charAt(0) !== '$') &&
+ deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
return true;
}
}
- return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, false);
+ return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false);
} else if (expectedType === 'object') {
for (key in expected) {
var expectedVal = expected[key];
@@ -18859,9 +20782,9 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
continue;
}
- var matchAnyProperty = key === '$';
+ var matchAnyProperty = key === anyPropertyKey;
var actualVal = matchAnyProperty ? actual : actual[key];
- if (!deepCompare(actualVal, expectedVal, comparator, matchAnyProperty, matchAnyProperty)) {
+ if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) {
return false;
}
}
@@ -18869,7 +20792,6 @@ function deepCompare(actual, expected, comparator, matchAgainstAnyProp, dontMatc
} else {
return comparator(actual, expected);
}
- break;
case 'function':
return false;
default:
@@ -18882,6 +20804,10 @@ function getTypeForFilter(val) {
return (val === null) ? 'null' : typeof val;
}
+var MAX_DIGITS = 22;
+var DECIMAL_SEP = '.';
+var ZERO_CHAR = '0';
+
/**
* @ngdoc filter
* @name currency
@@ -18898,7 +20824,7 @@ function getTypeForFilter(val) {
*
*
* @example
- <example module="currencyExample">
+ <example module="currencyExample" name="currency-filter">
<file name="index.html">
<script>
angular.module('currencyExample', [])
@@ -18909,7 +20835,7 @@ function getTypeForFilter(val) {
<div ng-controller="ExampleController">
<input type="number" ng-model="amount" aria-label="amount"> <br>
default currency symbol ($): <span id="currency-default">{{amount | currency}}</span><br>
- custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span>
+ custom currency identifier (USD$): <span id="currency-custom">{{amount | currency:"USD$"}}</span><br>
no fractions (0): <span id="currency-no-fractions">{{amount | currency:"USD$":0}}</span>
</div>
</file>
@@ -18920,7 +20846,7 @@ function getTypeForFilter(val) {
expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
});
it('should update', function() {
- if (browser.params.browser == 'safari') {
+ if (browser.params.browser === 'safari') {
// Safari does not understand the minus key. See
// https://github.com/angular/protractor/issues/481
return;
@@ -18963,7 +20889,7 @@ function currencyFilter($locale) {
* Formats a number as text.
*
* If the input is null or undefined, it will just be returned.
- * If the input is infinite (Infinity/-Infinity) the Infinity symbol '∞' is returned.
+ * If the input is infinite (Infinity or -Infinity), the Infinity symbol '∞' or '-∞' is returned, respectively.
* If the input is not a number an empty string is returned.
*
*
@@ -18971,10 +20897,12 @@ function currencyFilter($locale) {
* @param {(number|string)=} fractionSize Number of decimal places to round the number to.
* If this is not provided then the fraction size is computed from the current locale's number
* formatting pattern. In the case of the default locale, it will be 3.
- * @returns {string} Number rounded to decimalPlaces and places a “,” after each third digit.
+ * @returns {string} Number rounded to `fractionSize` appropriately formatted based on the current
+ * locale (e.g., in the en_US locale it will have "." as the decimal separator and
+ * include "," group separators after each third digit).
*
* @example
- <example module="numberFilterExample">
+ <example module="numberFilterExample" name="number-filter">
<file name="index.html">
<script>
angular.module('numberFilterExample', [])
@@ -19006,8 +20934,6 @@ function currencyFilter($locale) {
</file>
</example>
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
@@ -19021,103 +20947,227 @@ function numberFilter($locale) {
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (isObject(number)) return '';
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
- var isNegative = number < 0;
- number = Math.abs(number);
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
+ }
- var isInfinity = number === Infinity;
- if (!isInfinity && !isFinite(number)) return '';
+ // Exponential form?
+ if ((i = numStr.search(/e/i)) > 0) {
+ // Work out the exponent.
+ if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
+ numberOfIntegerDigits += +numStr.slice(i + 1);
+ numStr = numStr.substring(0, i);
+ } else if (numberOfIntegerDigits < 0) {
+ // There was no decimal point or exponent so it is an integer.
+ numberOfIntegerDigits = numStr.length;
+ }
- var numStr = number + '',
- formatedText = '',
- hasExponent = false,
- parts = [];
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
- if (isInfinity) formatedText = '\u221e';
+ if (i === (zeros = numStr.length)) {
+ // The digits are all zero.
+ digits = [0];
+ numberOfIntegerDigits = 1;
+ } else {
+ // Count the number of trailing zeros
+ zeros--;
+ while (numStr.charAt(zeros) === ZERO_CHAR) zeros--;
- if (!isInfinity && numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- number = 0;
- } else {
- formatedText = numStr;
- hasExponent = true;
+ // Trailing zeros are insignificant so ignore them
+ numberOfIntegerDigits -= i;
+ digits = [];
+ // Convert string to array of digits without leading/trailing zeros.
+ for (j = 0; i <= zeros; i++, j++) {
+ digits[j] = +numStr.charAt(i);
}
}
- if (!isInfinity && !hasExponent) {
- var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
+ // If the number overflows the maximum allowed digits then use an exponent.
+ if (numberOfIntegerDigits > MAX_DIGITS) {
+ digits = digits.splice(0, MAX_DIGITS - 1);
+ exponent = numberOfIntegerDigits - 1;
+ numberOfIntegerDigits = 1;
+ }
- // determine fractionSize if it is not specified
- if (isUndefined(fractionSize)) {
- fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
- }
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
+}
+
+/**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
- // safely round numbers in JS without hitting imprecisions of floating-point arithmetics
- // inspired by:
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
- number = +(Math.round(+(number.toString() + 'e' + fractionSize)).toString() + 'e' + -fractionSize);
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
- var i, pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
+ if (roundAt > 0) {
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (i = 0; i < pos; i++) {
- if ((pos - i) % group === 0 && i !== 0) {
- formatedText += groupSep;
+ // Set non-fractional digits beyond `roundAt` to 0
+ for (var j = roundAt; j < digits.length; j++) {
+ digits[j] = 0;
+ }
+ } else {
+ // We rounded to zero so reset the parsedNumber
+ fractionLen = Math.max(0, fractionLen);
+ parsedNumber.i = 1;
+ digits.length = Math.max(1, roundAt = fractionSize + 1);
+ digits[0] = 0;
+ for (var i = 1; i < roundAt; i++) digits[i] = 0;
+ }
+
+ if (digit >= 5) {
+ if (roundAt - 1 < 0) {
+ for (var k = 0; k > roundAt; k--) {
+ digits.unshift(0);
+ parsedNumber.i++;
}
- formatedText += whole.charAt(i);
+ digits.unshift(1);
+ parsedNumber.i++;
+ } else {
+ digits[roundAt - 1]++;
}
}
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i) % lgroup === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
+
- // format fraction part.
- while (fraction.length < fractionSize) {
- fraction += '0';
+ // Do any carrying, e.g. a digit was rounded up to 10
+ var carry = digits.reduceRight(function(carry, d, i, digits) {
+ d = d + carry;
+ digits[i] = d % 10;
+ return Math.floor(d / 10);
+ }, 0);
+ if (carry) {
+ digits.unshift(carry);
+ parsedNumber.i++;
}
+}
+
+/**
+ * Format a number into a string
+ * @param {number} number The number to format
+ * @param {{
+ * minFrac, // the minimum number of digits required in the fraction part of the number
+ * maxFrac, // the maximum number of digits required in the fraction part of the number
+ * gSize, // number of digits in each group of separated digits
+ * lgSize, // number of digits in the last group of digits before the decimal separator
+ * negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
+ * posPre, // the string to go in front of a positive number
+ * negSuf, // the string to go after a negative number (e.g. `)`)
+ * posSuf // the string to go after a positive number
+ * }} pattern
+ * @param {string} groupSep The string to separate groups of number (e.g. `,`)
+ * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
+ * @param {[type]} fractionSize The size of the fractional part of the number
+ * @return {string} The number formatted as a string
+ */
+function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
+
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
} else {
- if (fractionSize > 0 && number < 1) {
- formatedText = number.toFixed(fractionSize);
- number = parseFloat(formatedText);
- formatedText = formatedText.replace(DECIMAL_SEP, decimalSep);
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
+
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
}
- }
- if (number === 0) {
- isNegative = false;
- }
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen, digits.length);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
+ }
- parts.push(isNegative ? pattern.negPre : pattern.posPre,
- formatedText,
- isNegative ? pattern.negSuf : pattern.posSuf);
- return parts.join('');
+ if (exponent) {
+ formattedText += 'e+' + exponent;
+ }
+ }
+ if (number < 0 && !isZero) {
+ return pattern.negPre + formattedText + pattern.negSuf;
+ } else {
+ return pattern.posPre + formattedText + pattern.posSuf;
+ }
}
-function padNumber(num, digits, trim) {
+function padNumber(num, digits, trim, negWrap) {
var neg = '';
- if (num < 0) {
- neg = '-';
- num = -num;
+ if (num < 0 || (negWrap && num <= 0)) {
+ if (negWrap) {
+ num = -num + 1;
+ } else {
+ num = -num;
+ neg = '-';
+ }
}
num = '' + num;
- while (num.length < digits) num = '0' + num;
+ while (num.length < digits) num = ZERO_CHAR + num;
if (trim) {
num = num.substr(num.length - digits);
}
@@ -19125,22 +21175,23 @@ function padNumber(num, digits, trim) {
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
if (offset > 0 || value > -offset) {
value += offset;
}
- if (value === 0 && offset == -12) value = 12;
- return padNumber(value, size, trim);
+ if (value === 0 && offset === -12) value = 12;
+ return padNumber(value, size, trim, negWrap);
};
}
-function dateStrGetter(name, shortForm) {
+function dateStrGetter(name, shortForm, standAlone) {
return function(date, formats) {
var value = date['get' + name]();
- var get = uppercase(shortForm ? ('SHORT' + name) : name);
+ var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
+ var get = uppercase(propPrefix + name);
return formats[get][value];
};
@@ -19148,7 +21199,7 @@ function dateStrGetter(name, shortForm) {
function timeZoneGetter(date, formats, offset) {
var zone = -1 * offset;
- var paddedZone = (zone >= 0) ? "+" : "";
+ var paddedZone = (zone >= 0) ? '+' : '';
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
padNumber(Math.abs(zone % 60), 2);
@@ -19195,13 +21246,14 @@ function longEraGetter(date, formats) {
}
var DATE_FORMATS = {
- yyyy: dateGetter('FullYear', 4),
- yy: dateGetter('FullYear', 2, 0, true),
- y: dateGetter('FullYear', 1),
+ yyyy: dateGetter('FullYear', 4, 0, false, true),
+ yy: dateGetter('FullYear', 2, 0, true, true),
+ y: dateGetter('FullYear', 1, 0, false, true),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
+ LLLL: dateStrGetter('Month', false, true),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
@@ -19227,8 +21279,8 @@ var DATE_FORMATS = {
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
- NUMBER_STRING = /^\-?\d+$/;
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
+ NUMBER_STRING = /^-?\d+$/;
/**
* @ngdoc filter
@@ -19247,6 +21299,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
* * `'MMM'`: Month in year (Jan-Dec)
* * `'MM'`: Month in year, padded (01-12)
* * `'M'`: Month in year (1-12)
+ * * `'LLLL'`: Stand-alone month in year (January-December)
* * `'dd'`: Day in month, padded (01-31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
@@ -19298,7 +21351,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
- <example>
+ <example name="filter-date">
<file name="index.html">
<span ng-non-bindable>{{1288323623006 | date:'medium'}}</span>:
<span>{{1288323623006 | date:'medium'}}</span><br>
@@ -19314,7 +21367,7 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|
expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
- toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
+ toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/);
expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
@@ -19331,7 +21384,7 @@ function dateFilter($locale) {
// 1 2 3 4 5 6 7 8 9 10 11
function jsonStringToDate(string) {
var match;
- if (match = string.match(R_ISO8601_STR)) {
+ if ((match = string.match(R_ISO8601_STR))) {
var date = new Date(0),
tzHour = 0,
tzMin = 0,
@@ -19386,13 +21439,13 @@ function dateFilter($locale) {
var dateTimezoneOffset = date.getTimezoneOffset();
if (timezone) {
- dateTimezoneOffset = timezoneToOffset(timezone, date.getTimezoneOffset());
+ dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
date = convertTimezoneToLocal(date, timezone, true);
}
forEach(parts, function(value) {
fn = DATE_FORMATS[value];
text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
- : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
});
return text;
@@ -19417,15 +21470,15 @@ function dateFilter($locale) {
*
*
* @example
- <example>
+ <example name="filter-json">
<file name="index.html">
<pre id="default-spacing">{{ {'name':'value'} | json }}</pre>
<pre id="custom-spacing">{{ {'name':'value'} | json:4 }}</pre>
</file>
<file name="protractor.js" type="protractor">
it('should jsonify filtered objects', function() {
- expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
- expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/);
});
</file>
</example>
@@ -19468,24 +21521,25 @@ var uppercaseFilter = valueFn(uppercase);
* @kind function
*
* @description
- * Creates a new array or string containing only a specified number of elements. The elements
- * are taken from either the beginning or the end of the source array, string or number, as specified by
- * the value and sign (positive or negative) of `limit`. If a number is used as input, it is
- * converted to a string.
- *
- * @param {Array|string|number} input Source array, string or number to be limited.
- * @param {string|number} limit The length of the returned array or string. If the `limit` number
+ * Creates a new array or string containing only a specified number of elements. The elements are
+ * taken from either the beginning or the end of the source array, string or number, as specified by
+ * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported
+ * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input,
+ * it is converted to a string.
+ *
+ * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited.
+ * @param {string|number} limit - The length of the returned array or string. If the `limit` number
* is positive, `limit` number of items from the beginning of the source array/string are copied.
* If the number is negative, `limit` number of items from the end of the source array/string
* are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
* the input will be returned unchanged.
- * @param {(string|number)=} begin Index at which to begin limitation. As a negative index, `begin`
- * indicates an offset from the end of `input`. Defaults to `0`.
- * @returns {Array|string} A new sub-array or substring of length `limit` or less if input array
- * had less than `limit` elements.
+ * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index,
+ * `begin` indicates an offset from the end of `input`. Defaults to `0`.
+ * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had
+ * less than `limit` elements.
*
* @example
- <example module="limitToExample">
+ <example module="limitToExample" name="limit-to-filter">
<file name="index.html">
<script>
angular.module('limitToExample', [])
@@ -19567,81 +21621,163 @@ function limitToFilter() {
} else {
limit = toInt(limit);
}
- if (isNaN(limit)) return input;
+ if (isNumberNaN(limit)) return input;
if (isNumber(input)) input = input.toString();
- if (!isArray(input) && !isString(input)) return input;
+ if (!isArrayLike(input)) return input;
begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
if (limit >= 0) {
- return input.slice(begin, begin + limit);
+ return sliceFn(input, begin, begin + limit);
} else {
if (begin === 0) {
- return input.slice(limit, input.length);
+ return sliceFn(input, limit, input.length);
} else {
- return input.slice(Math.max(0, begin + limit), begin);
+ return sliceFn(input, Math.max(0, begin + limit), begin);
}
}
};
}
+function sliceFn(input, begin, end) {
+ if (isString(input)) return input.slice(begin, end);
+
+ return slice.call(input, begin, end);
+}
+
/**
* @ngdoc filter
* @name orderBy
* @kind function
*
* @description
- * Orders a specified `array` by the `expression` predicate. It is ordered alphabetically
- * for strings and numerically for numbers. Note: if you notice numbers are not being sorted
- * as expected, make sure they are actually being saved as numbers and not strings.
- * Array-like values (e.g. NodeLists, jQuery objects, TypedArrays, Strings, etc) are also supported.
+ * Returns an array containing the items from the specified `collection`, ordered by a `comparator`
+ * function based on the values computed using the `expression` predicate.
+ *
+ * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in
+ * `[{id: 'bar'}, {id: 'foo'}]`.
+ *
+ * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray,
+ * String, etc).
+ *
+ * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
+ * for the preceding one. The `expression` is evaluated against each item and the output is used
+ * for comparing with other items.
+ *
+ * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
+ * ascending order.
*
- * @param {Array} array The array (or array-like object) to sort.
- * @param {function(*)|string|Array.<(function(*)|string)>=} expression A predicate to be
- * used by the comparator to determine the order of elements.
+ * The comparison is done using the `comparator` function. If none is specified, a default, built-in
+ * comparator is used (see below for details - in a nutshell, it compares numbers numerically and
+ * strings alphabetically).
+ *
+ * ### Under the hood
+ *
+ * Ordering the specified `collection` happens in two phases:
+ *
+ * 1. All items are passed through the predicate (or predicates), and the returned values are saved
+ * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed
+ * through a predicate that extracts the value of the `label` property, would be transformed to:
+ * ```
+ * {
+ * value: 'foo',
+ * type: 'string',
+ * index: ...
+ * }
+ * ```
+ * 2. The comparator function is used to sort the items, based on the derived values, types and
+ * indices.
+ *
+ * If you use a custom comparator, it will be called with pairs of objects of the form
+ * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal
+ * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the
+ * second, or `1` otherwise.
+ *
+ * In order to ensure that the sorting will be deterministic across platforms, if none of the
+ * specified predicates can distinguish between two items, `orderBy` will automatically introduce a
+ * dummy predicate that returns the item's index as `value`.
+ * (If you are using a custom comparator, make sure it can handle this predicate as well.)
+ *
+ * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
+ * value for an item, `orderBy` will try to convert that object to a primitive value, before passing
+ * it to the comparator. The following rules govern the conversion:
+ *
+ * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be
+ * used instead.<br />
+ * (If the object has a `valueOf()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that
+ * returns a primitive, its return value will be used instead.<br />
+ * (If the object has a `toString()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 3. No conversion; the object itself is used.
+ *
+ * ### The default comparator
+ *
+ * The default, built-in comparator should be sufficient for most usecases. In short, it compares
+ * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
+ * using their index in the original collection, and sorts values of different types by type.
+ *
+ * More specifically, it follows these steps to determine the relative order of items:
+ *
+ * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 2. If both values are of type `string`, compare them alphabetically in a case- and
+ * locale-insensitive way.
+ * 3. If both values are objects, compare their indices instead.
+ * 4. Otherwise, return:
+ * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`).
+ * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator).
+ * - `1`, otherwise.
+ *
+ * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
+ * saved as numbers and not strings.
+ * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e.
+ * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to
+ * other values.
+ *
+ * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
+ * @param {(Function|string|Array.<Function|string>)=} expression - A predicate (or list of
+ * predicates) to be used by the comparator to determine the order of elements.
*
* Can be one of:
*
- * - `function`: Getter function. The result of this function will be sorted using the
- * `<`, `===`, `>` operator.
- * - `string`: An Angular expression. The result of this expression is used to compare elements
- * (for example `name` to sort by a property called `name` or `name.substr(0, 3)` to sort by
- * 3 first characters of a property called `name`). The result of a constant expression
- * is interpreted as a property name to be used in comparisons (for example `"special name"`
- * to sort object by the value of their `special name` property). An expression can be
- * optionally prefixed with `+` or `-` to control ascending or descending sort order
- * (for example, `+name` or `-name`). If no property is provided, (e.g. `'+'`) then the array
- * element itself is used to compare where sorting.
- * - `Array`: An array of function or string predicates. The first predicate in the array
- * is used for sorting, but when two items are equivalent, the next predicate is used.
+ * - `Function`: A getter function. This function will be called with each item as argument and
+ * the return value will be used for sorting.
+ * - `string`: An Angular expression. This expression will be evaluated against each item and the
+ * result will be used for sorting. For example, use `'label'` to sort by a property called
+ * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
+ * property.<br />
+ * (The result of a constant expression is interpreted as a property name to be used for
+ * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a
+ * property called `special name`.)<br />
+ * An expression can be optionally prefixed with `+` or `-` to control the sorting direction,
+ * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided,
+ * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons.
+ * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the
+ * relative order of two items, the next predicate is used as a tie-breaker.
+ *
+ * **Note:** If the predicate is missing or empty then it defaults to `'+'`.
*
- * If the predicate is missing or empty then it defaults to `'+'`.
+ * @param {boolean=} reverse - If `true`, reverse the sorting order.
+ * @param {(Function)=} comparator - The comparator function used to determine the relative order of
+ * value pairs. If omitted, the built-in comparator will be used.
*
- * @param {boolean=} reverse Reverse the order of the array.
- * @returns {Array} Sorted copy of the source array.
+ * @returns {Array} - The sorted array.
*
*
* @example
- * The example below demonstrates a simple ngRepeat, where the data is sorted
- * by age in descending order (predicate is set to `'-age'`).
- * `reverse` is not set, which means it defaults to `false`.
- <example module="orderByExample">
+ * ### Ordering a table with `ngRepeat`
+ *
+ * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by
+ * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means
+ * it defaults to the built-in comparator.
+ *
+ <example name="orderBy-static" module="orderByExample1">
<file name="index.html">
- <script>
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
- }]);
- </script>
<div ng-controller="ExampleController">
- <table class="friend">
+ <table class="friends">
<tr>
<th>Name</th>
<th>Phone Number</th>
@@ -19655,58 +21791,78 @@ function limitToFilter() {
</table>
</div>
</file>
+ <file name="script.js">
+ angular.module('orderByExample1', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+ }]);
+ </file>
+ <file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var names = element.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by age in reverse order', function() {
+ expect(names.get(0).getText()).toBe('Adam');
+ expect(names.get(1).getText()).toBe('Julie');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('John');
+ });
+ </file>
</example>
+ * <hr />
*
- * The predicate and reverse parameters can be controlled dynamically through scope properties,
- * as shown in the next example.
* @example
- <example module="orderByExample">
+ * ### Changing parameters dynamically
+ *
+ * All parameters can be changed dynamically. The next example shows how you can make the columns of
+ * a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
+ *
+ <example name="orderBy-dynamic" module="orderByExample2">
<file name="index.html">
- <script>
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.friends =
- [{name:'John', phone:'555-1212', age:10},
- {name:'Mary', phone:'555-9876', age:19},
- {name:'Mike', phone:'555-4321', age:21},
- {name:'Adam', phone:'555-5678', age:35},
- {name:'Julie', phone:'555-8765', age:29}];
- $scope.predicate = 'age';
- $scope.reverse = true;
- $scope.order = function(predicate) {
- $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
- $scope.predicate = predicate;
- };
- }]);
- </script>
- <style type="text/css">
- .sortorder:after {
- content: '\25b2';
- }
- .sortorder.reverse:after {
- content: '\25bc';
- }
- </style>
<div ng-controller="ExampleController">
- <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
+ <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
+ <hr/>
+ <button ng-click="propertyName = null; reverse = false">Set to unsorted</button>
<hr/>
- [ <a href="" ng-click="predicate=''">unsorted</a> ]
- <table class="friend">
+ <table class="friends">
<tr>
<th>
- <a href="" ng-click="order('name')">Name</a>
- <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
+ <button ng-click="sortBy('name')">Name</button>
+ <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
</th>
<th>
- <a href="" ng-click="order('phone')">Phone Number</a>
- <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
+ <button ng-click="sortBy('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
</th>
<th>
- <a href="" ng-click="order('age')">Age</a>
- <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
+ <button ng-click="sortBy('age')">Age</button>
+ <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
</th>
</tr>
- <tr ng-repeat="friend in friends | orderBy:predicate:reverse">
+ <tr ng-repeat="friend in friends | orderBy:propertyName:reverse">
<td>{{friend.name}}</td>
<td>{{friend.phone}}</td>
<td>{{friend.age}}</td>
@@ -19714,56 +21870,336 @@ function limitToFilter() {
</table>
</div>
</file>
+ <file name="script.js">
+ angular.module('orderByExample2', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = friends;
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+ </file>
</example>
+ * <hr />
*
- * It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the
- * filter routine with `$filter('orderBy')`, and calling the returned filter routine with the
- * desired parameters.
+ * @example
+ * ### Using `orderBy` inside a controller
*
- * Example:
+ * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and
+ * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory
+ * and retrieve the `orderBy` filter with `$filter('orderBy')`.)
+ *
+ <example name="orderBy-call-manually" module="orderByExample3">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <pre>Sort by = {{propertyName}}; reverse = {{reverse}}</pre>
+ <hr/>
+ <button ng-click="sortBy(null)">Set to unsorted</button>
+ <hr/>
+ <table class="friends">
+ <tr>
+ <th>
+ <button ng-click="sortBy('name')">Name</button>
+ <span class="sortorder" ng-show="propertyName === 'name'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="propertyName === 'phone'" ng-class="{reverse: reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="sortBy('age')">Age</button>
+ <span class="sortorder" ng-show="propertyName === 'age'" ng-class="{reverse: reverse}"></span>
+ </th>
+ </tr>
+ <tr ng-repeat="friend in friends">
+ <td>{{friend.name}}</td>
+ <td>{{friend.phone}}</td>
+ <td>{{friend.age}}</td>
+ </tr>
+ </table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('orderByExample3', [])
+ .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
+ ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+ </file>
+ </example>
+ * <hr />
*
* @example
- <example module="orderByExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <table class="friend">
- <tr>
- <th><a href="" ng-click="reverse=false;order('name', false)">Name</a>
- (<a href="" ng-click="order('-name',false)">^</a>)</th>
- <th><a href="" ng-click="reverse=!reverse;order('phone', reverse)">Phone Number</a></th>
- <th><a href="" ng-click="reverse=!reverse;order('age',reverse)">Age</a></th>
- </tr>
- <tr ng-repeat="friend in friends">
- <td>{{friend.name}}</td>
- <td>{{friend.phone}}</td>
- <td>{{friend.age}}</td>
- </tr>
- </table>
- </div>
- </file>
+ * ### Using a custom comparator
+ *
+ * If you have very specific requirements about the way items are sorted, you can pass your own
+ * comparator function. For example, you might need to compare some strings in a locale-sensitive
+ * way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
+ * argument - passing `false` retains the default sorting order, i.e. ascending.)
+ *
+ <example name="orderBy-custom-comparator" module="orderByExample4">
+ <file name="index.html">
+ <div ng-controller="ExampleController">
+ <div class="friends-container custom-comparator">
+ <h3>Locale-sensitive Comparator</h3>
+ <table class="friends">
+ <tr>
+ <th>Name</th>
+ <th>Favorite Letter</th>
+ </tr>
+ <tr ng-repeat="friend in friends | orderBy:'favoriteLetter':false:localeSensitiveComparator">
+ <td>{{friend.name}}</td>
+ <td>{{friend.favoriteLetter}}</td>
+ </tr>
+ </table>
+ </div>
+ <div class="friends-container default-comparator">
+ <h3>Default Comparator</h3>
+ <table class="friends">
+ <tr>
+ <th>Name</th>
+ <th>Favorite Letter</th>
+ </tr>
+ <tr ng-repeat="friend in friends | orderBy:'favoriteLetter'">
+ <td>{{friend.name}}</td>
+ <td>{{friend.favoriteLetter}}</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('orderByExample4', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', favoriteLetter: 'Ä'},
+ {name: 'Mary', favoriteLetter: 'Ü'},
+ {name: 'Mike', favoriteLetter: 'Ö'},
+ {name: 'Adam', favoriteLetter: 'H'},
+ {name: 'Julie', favoriteLetter: 'Z'}
+ ];
+
+ $scope.localeSensitiveComparator = function(v1, v2) {
+ // If we don't get strings, just compare by index
+ if (v1.type !== 'string' || v2.type !== 'string') {
+ return (v1.index < v2.index) ? -1 : 1;
+ }
- <file name="script.js">
- angular.module('orderByExample', [])
- .controller('ExampleController', ['$scope', '$filter', function($scope, $filter) {
- var orderBy = $filter('orderBy');
- $scope.friends = [
- { name: 'John', phone: '555-1212', age: 10 },
- { name: 'Mary', phone: '555-9876', age: 19 },
- { name: 'Mike', phone: '555-4321', age: 21 },
- { name: 'Adam', phone: '555-5678', age: 35 },
- { name: 'Julie', phone: '555-8765', age: 29 }
- ];
- $scope.order = function(predicate, reverse) {
- $scope.friends = orderBy($scope.friends, predicate, reverse);
- };
- $scope.order('-age',false);
- }]);
- </file>
-</example>
+ // Compare strings alphabetically, taking locale into account
+ return v1.value.localeCompare(v2.value);
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .friends-container {
+ display: inline-block;
+ margin: 0 30px;
+ }
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+ </file>
+ <file name="protractor.js" type="protractor">
+ // Element locators
+ var container = element(by.css('.custom-comparator'));
+ var names = container.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by favorite letter (in correct alphabetical order)', function() {
+ expect(names.get(0).getText()).toBe('John');
+ expect(names.get(1).getText()).toBe('Adam');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('Julie');
+ });
+ </file>
+ </example>
+ *
*/
orderByFilter.$inject = ['$parse'];
function orderByFilter($parse) {
- return function(array, sortPredicate, reverseOrder) {
+ return function(array, sortPredicate, reverseOrder, compareFn) {
if (array == null) return array;
if (!isArrayLike(array)) {
@@ -19773,11 +22209,12 @@ function orderByFilter($parse) {
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
- var predicates = processPredicates(sortPredicate, reverseOrder);
- // Add a predicate at the end that evaluates to the element index. This makes the
- // sort stable as it works as a tie-breaker when all the input predicates cannot
- // distinguish between two elements.
- predicates.push({ get: function() { return {}; }, descending: reverseOrder ? -1 : 1});
+ var predicates = processPredicates(sortPredicate);
+
+ var descending = reverseOrder ? -1 : 1;
+
+ // Define the `compare()` function. Use a default comparator if none is specified.
+ var compare = isFunction(compareFn) ? compareFn : defaultCompare;
// The next three lines are a version of a Swartzian Transform idiom from Perl
// (sometimes called the Decorate-Sort-Undecorate idiom)
@@ -19789,8 +22226,12 @@ function orderByFilter($parse) {
return array;
function getComparisonObject(value, index) {
+ // NOTE: We are adding an extra `tieBreaker` value based on the element's index.
+ // This will be used to keep the sort stable when none of the input predicates can
+ // distinguish between two elements.
return {
value: value,
+ tieBreaker: {value: index, type: 'number', index: index},
predicateValues: predicates.map(function(predicate) {
return getPredicateValue(predicate.get(value), index);
})
@@ -19798,25 +22239,26 @@ function orderByFilter($parse) {
}
function doComparison(v1, v2) {
- var result = 0;
- for (var index=0, length = predicates.length; index < length; ++index) {
- result = compare(v1.predicateValues[index], v2.predicateValues[index]) * predicates[index].descending;
- if (result) break;
+ for (var i = 0, ii = predicates.length; i < ii; i++) {
+ var result = compare(v1.predicateValues[i], v2.predicateValues[i]);
+ if (result) {
+ return result * predicates[i].descending * descending;
+ }
}
- return result;
+
+ return compare(v1.tieBreaker, v2.tieBreaker) * descending;
}
};
- function processPredicates(sortPredicate, reverseOrder) {
- reverseOrder = reverseOrder ? -1 : 1;
- return sortPredicate.map(function(predicate) {
+ function processPredicates(sortPredicates) {
+ return sortPredicates.map(function(predicate) {
var descending = 1, get = identity;
if (isFunction(predicate)) {
get = predicate;
} else if (isString(predicate)) {
- if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-' ? -1 : 1;
+ if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) {
+ descending = predicate.charAt(0) === '-' ? -1 : 1;
predicate = predicate.substring(1);
}
if (predicate !== '') {
@@ -19827,7 +22269,7 @@ function orderByFilter($parse) {
}
}
}
- return { get: get, descending: descending * reverseOrder };
+ return {get: get, descending: descending};
});
}
@@ -19842,9 +22284,9 @@ function orderByFilter($parse) {
}
}
- function objectValue(value, index) {
+ function objectValue(value) {
// If `valueOf` is a valid function use that
- if (typeof value.valueOf === 'function') {
+ if (isFunction(value.valueOf)) {
value = value.valueOf();
if (isPrimitive(value)) return value;
}
@@ -19853,8 +22295,8 @@ function orderByFilter($parse) {
value = value.toString();
if (isPrimitive(value)) return value;
}
- // We have a basic object so we use the position of the object in the collection
- return index;
+
+ return value;
}
function getPredicateValue(value, index) {
@@ -19862,23 +22304,39 @@ function orderByFilter($parse) {
if (value === null) {
type = 'string';
value = 'null';
- } else if (type === 'string') {
- value = value.toLowerCase();
} else if (type === 'object') {
- value = objectValue(value, index);
+ value = objectValue(value);
}
- return { value: value, type: type };
+ return {value: value, type: type, index: index};
}
- function compare(v1, v2) {
+ function defaultCompare(v1, v2) {
var result = 0;
- if (v1.type === v2.type) {
- if (v1.value !== v2.value) {
- result = v1.value < v2.value ? -1 : 1;
+ var type1 = v1.type;
+ var type2 = v2.type;
+
+ if (type1 === type2) {
+ var value1 = v1.value;
+ var value2 = v2.value;
+
+ if (type1 === 'string') {
+ // Compare strings case-insensitively
+ value1 = value1.toLowerCase();
+ value2 = value2.toLowerCase();
+ } else if (type1 === 'object') {
+ // For basic objects, use the position of the object
+ // in the collection instead of the value
+ if (isObject(value1)) value1 = v1.index;
+ if (isObject(value2)) value2 = v2.index;
+ }
+
+ if (value1 !== value2) {
+ result = value1 < value2 ? -1 : 1;
}
} else {
- result = v1.type < v2.type ? -1 : 1;
+ result = type1 < type2 ? -1 : 1;
}
+
return result;
}
}
@@ -19899,12 +22357,10 @@ function ngDirective(directive) {
* @restrict E
*
* @description
- * Modifies the default behavior of the html A tag so that the default action is prevented when
+ * Modifies the default behavior of the html a tag so that the default action is prevented when
* the href attribute is empty.
*
- * This change permits the easy creation of action links with the `ngClick` directive
- * without changing the location or causing page reloads, e.g.:
- * `<a href="" ng-click="list.addItem()">Add Item</a>`
+ * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive.
*/
var htmlAnchorDirective = valueFn({
restrict: 'E',
@@ -19958,7 +22414,7 @@ var htmlAnchorDirective = valueFn({
* @example
* This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
* in links and their different behaviors:
- <example>
+ <example name="ng-href">
<file name="index.html">
<input ng-model="value" /><br />
<a id="link-1" href ng-click="value = 1">link 1</a> (link, don't reload)<br />
@@ -20087,27 +22543,15 @@ var htmlAnchorDirective = valueFn({
*
* @description
*
- * This directive sets the `disabled` attribute on the element if the
+ * This directive sets the `disabled` attribute on the element (typically a form control,
+ * e.g. `input`, `button`, `select` etc.) if the
* {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
* A special directive is necessary because we cannot use interpolation inside the `disabled`
- * attribute. The following example would make the button enabled on Chrome/Firefox
- * but not on older IEs:
- *
- * ```html
- * <!-- See below for an example of ng-disabled being used correctly -->
- * <div ng-init="isDisabled = false">
- * <button disabled="{{isDisabled}}">Disabled</button>
- * </div>
- * ```
- *
- * This is because the HTML specification does not require browsers to preserve the values of
- * boolean attributes such as `disabled` (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
- <example>
+ <example name="ng-disabled">
<file name="index.html">
<label>Click me to toggle: <input type="checkbox" ng-model="checked"></label><br/>
<button ng-model="button" ng-disabled="checked">Button</button>
@@ -20139,17 +22583,11 @@ var htmlAnchorDirective = valueFn({
* Note that this directive should not be used together with {@link ngModel `ngModel`},
* as this can lead to unexpected behavior.
*
- * ### Why do we need `ngChecked`?
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as checked. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngChecked` directive solves this problem for the `checked` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
* @example
- <example>
+ <example name="ng-checked">
<file name="index.html">
<label>Check me to check both: <input type="checkbox" ng-model="master"></label><br/>
<input id="checkSlave" type="checkbox" ng-checked="master" aria-label="Slave input">
@@ -20176,15 +22614,16 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as readonly. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngReadonly` directive solves this problem for the `readonly` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on
+ * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `readonly`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
* @example
- <example>
+ <example name="ng-readonly">
<file name="index.html">
<label>Check me to make text readonly: <input type="checkbox" ng-model="checked"></label><br/>
<input type="text" ng-readonly="checked" value="I'm Angular" aria-label="Readonly field" />
@@ -20211,16 +22650,21 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as selected. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngSelected` directive solves this problem for the `selected` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `selected`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ * <div class="alert alert-warning">
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only
+ * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you
+ * should not use `ngSelected` on the options, as `ngModel` will set the select value and
+ * selected options.
+ * </div>
*
* @example
- <example>
+ <example name="ng-selected">
<file name="index.html">
<label>Check me to select: <input type="checkbox" ng-model="selected"></label><br/>
<select aria-label="ngSelected demo">
@@ -20249,15 +22693,19 @@ var htmlAnchorDirective = valueFn({
* @priority 100
*
* @description
- * The HTML specification does not require browsers to preserve the values of boolean attributes
- * such as open. (Their presence means true and their absence means false.)
- * If we put an Angular interpolation expression into such an attribute then the
- * binding information would be lost when the browser removes the attribute.
- * The `ngOpen` directive solves this problem for the `open` attribute.
- * This complementary directive is not removed by the browser and so provides
- * a permanent reliable place to store the binding information.
+ *
+ * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `open`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ * ## A note about browser compatibility
+ *
+ * Edge, Firefox, and Internet Explorer do not support the `details` element, it is
+ * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
+ *
* @example
- <example>
+ <example name="ng-open">
<file name="index.html">
<label>Check me check multiple: <input type="checkbox" ng-model="open"></label><br/>
<details id="details" ng-open="open">
@@ -20283,7 +22731,7 @@ var ngAttributeAliasDirectives = {};
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
// binding to multiple is not supported
- if (propName == "multiple") return;
+ if (propName === 'multiple') return;
function defaultLinkFn(scope, element, attr) {
scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
@@ -20320,10 +22768,10 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
link: function(scope, element, attr) {
//special case ngPattern when a literal regular expression value
//is used as the expression (this way we don't have to watch anything).
- if (ngAttr === "ngPattern" && attr.ngPattern.charAt(0) == "/") {
+ if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') {
var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
if (match) {
- attr.$set("ngPattern", new RegExp(match[1], match[2]));
+ attr.$set('ngPattern', new RegExp(match[1], match[2]));
return;
}
}
@@ -20363,10 +22811,11 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
attr.$set(name, value);
- // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
+ // Support: IE 9-11 only
+ // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
- // we use attr[attrName] value since $set can sanitize the url.
+ // We use attr[attrName] value since $set can sanitize the url.
if (msie && propName) element.prop(propName, attr[name]);
});
}
@@ -20374,7 +22823,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
};
});
-/* global -nullFormCtrl, -SUBMITTED_CLASS, addSetValidityMethod: true
+/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
*/
var nullFormCtrl = {
$addControl: noop,
@@ -20385,6 +22834,7 @@ var nullFormCtrl = {
$setPristine: noop,
$setSubmitted: noop
},
+PENDING_CLASS = 'ng-pending',
SUBMITTED_CLASS = 'ng-submitted';
function nullFormRenameControl(control, name) {
@@ -20435,22 +22885,28 @@ function nullFormRenameControl(control, name) {
*/
//asks for $scope to fool the BC controller module
FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
-function FormController(element, attrs, $scope, $animate, $interpolate) {
- var form = this,
- controls = [];
+function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
// init state
- form.$error = {};
- form.$$success = {};
- form.$pending = undefined;
- form.$name = $interpolate(attrs.name || attrs.ngForm || '')($scope);
- form.$dirty = false;
- form.$pristine = true;
- form.$valid = true;
- form.$invalid = false;
- form.$submitted = false;
- form.$$parentForm = nullFormCtrl;
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
+
+ this.$$element = $element;
+ this.$$animate = $animate;
+ setupValidity(this);
+}
+
+FormController.prototype = {
/**
* @ngdoc method
* @name form.FormController#$rollbackViewValue
@@ -20462,11 +22918,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is typically needed by the reset button of
* a form that uses `ng-model-options` to pend updates.
*/
- form.$rollbackViewValue = function() {
- forEach(controls, function(control) {
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$rollbackViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -20479,11 +22935,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- form.$commitViewValue = function() {
- forEach(controls, function(control) {
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
control.$commitViewValue();
});
- };
+ },
/**
* @ngdoc method
@@ -20501,34 +22957,34 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
*
* However, if the method is used programmatically, for example by adding dynamically created controls,
* or controls that have been previously removed without destroying their corresponding DOM element,
- * it's the developers responsiblity to make sure the current state propagates to the parent form.
+ * it's the developers responsibility to make sure the current state propagates to the parent form.
*
* For example, if an input control is added that is already `$dirty` and has `$error` properties,
* calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
*/
- form.$addControl = function(control) {
+ $addControl: function(control) {
// Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
// and not added to the scope. Now we throw an error.
assertNotHasOwnProperty(control.$name, 'input');
- controls.push(control);
+ this.$$controls.push(control);
if (control.$name) {
- form[control.$name] = control;
+ this[control.$name] = control;
}
- control.$$parentForm = form;
- };
+ control.$$parentForm = this;
+ },
// Private API: rename a form control
- form.$$renameControl = function(control, newName) {
+ $$renameControl: function(control, newName) {
var oldName = control.$name;
- if (form[oldName] === control) {
- delete form[oldName];
+ if (this[oldName] === control) {
+ delete this[oldName];
}
- form[newName] = control;
+ this[newName] = control;
control.$name = newName;
- };
+ },
/**
* @ngdoc method
@@ -20546,60 +23002,26 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* different from case to case. For example, removing the only `$dirty` control from a form may or
* may not mean that the form is still `$dirty`.
*/
- form.$removeControl = function(control) {
- if (control.$name && form[control.$name] === control) {
- delete form[control.$name];
- }
- forEach(form.$pending, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$error, function(value, name) {
- form.$setValidity(name, null, control);
- });
- forEach(form.$$success, function(value, name) {
- form.$setValidity(name, null, control);
- });
-
- arrayRemove(controls, control);
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
+ }
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
control.$$parentForm = nullFormCtrl;
- };
-
-
- /**
- * @ngdoc method
- * @name form.FormController#$setValidity
- *
- * @description
- * Sets the validity of a form control.
- *
- * This method will also propagate to parent forms.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: element,
- set: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- object[property] = [controller];
- } else {
- var index = list.indexOf(controller);
- if (index === -1) {
- list.push(controller);
- }
- }
- },
- unset: function(object, property, controller) {
- var list = object[property];
- if (!list) {
- return;
- }
- arrayRemove(list, controller);
- if (list.length === 0) {
- delete object[property];
- }
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -20611,13 +23033,13 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* This method can be called to add the 'ng-dirty' class and set the form to a dirty
* state (ng-dirty class). This method will also propagate to parent forms.
*/
- form.$setDirty = function() {
- $animate.removeClass(element, PRISTINE_CLASS);
- $animate.addClass(element, DIRTY_CLASS);
- form.$dirty = true;
- form.$pristine = false;
- form.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -20626,22 +23048,24 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @description
* Sets the form to its pristine state.
*
- * This method can be called to remove the 'ng-dirty' class and set the form to its pristine
- * state (ng-pristine class). This method will also propagate to all the controls contained
- * in this form.
+ * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes
+ * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted`
+ * state to false.
+ *
+ * This method will also propagate to all the controls contained in this form.
*
* Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
* saving or resetting it.
*/
- form.$setPristine = function() {
- $animate.setClass(element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
- form.$dirty = false;
- form.$pristine = true;
- form.$submitted = false;
- forEach(controls, function(control) {
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
control.$setPristine();
});
- };
+ },
/**
* @ngdoc method
@@ -20656,11 +23080,11 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* Setting a form controls back to their untouched state is often useful when setting the form
* back to its pristine state.
*/
- form.$setUntouched = function() {
- forEach(controls, function(control) {
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
control.$setUntouched();
});
- };
+ },
/**
* @ngdoc method
@@ -20669,12 +23093,46 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* @description
* Sets the form to its submitted state.
*/
- form.$setSubmitted = function() {
- $animate.addClass(element, SUBMITTED_CLASS);
- form.$submitted = true;
- form.$$parentForm.$setSubmitted();
- };
-}
+ $setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ this.$$parentForm.$setSubmitted();
+ }
+};
+
+/**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Sets the validity of a form control.
+ *
+ * This method will also propagate to parent forms.
+ */
+addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
+ }
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+});
/**
* @ngdoc directive
@@ -20711,13 +23169,9 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
*
* In Angular, forms can be nested. This means that the outer form is valid when all of the child
* forms are valid as well. However, browsers do not allow nesting of `<form>` elements, so
- * Angular provides the {@link ng.directive:ngForm `ngForm`} directive which behaves identically to
- * `<form>` but can be nested. This allows you to have nested forms, which is very useful when
- * using Angular validation directives in forms that are dynamically generated using the
- * {@link ng.directive:ngRepeat `ngRepeat`} directive. Since you cannot dynamically generate the `name`
- * attribute of input elements using interpolation, you have to wrap each set of repeated inputs in an
- * `ngForm` directive and nest these in an outer `form` element.
- *
+ * Angular provides the {@link ng.directive:ngForm `ngForm`} directive, which behaves identically to
+ * `form` but can be nested. Nested forms can be useful, for example, if the validity of a sub-group
+ * of controls needs to be determined.
*
* # CSS classes
* - `ng-valid` is set if the form is valid.
@@ -20788,7 +23242,7 @@ function FormController(element, attrs, $scope, $animate, $interpolate) {
* </pre>
*
* @example
- <example deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
+ <example name="ng-form" deps="angular-animate.js" animations="true" fixBase="true" module="formExample">
<file name="index.html">
<script>
angular.module('formExample', [])
@@ -20875,13 +23329,13 @@ var formDirectiveFactory = function(isNgForm) {
event.preventDefault();
};
- addEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].addEventListener('submit', handleFormSubmission);
// unregister the preventDefault listener so that we don't not leak memory but in a
// way that will achieve the prevention of the default action.
formElement.on('$destroy', function() {
$timeout(function() {
- removeEventListenerFn(formElement[0], 'submit', handleFormSubmission);
+ formElement[0].removeEventListener('submit', handleFormSubmission);
}, 0, false);
});
}
@@ -20926,27 +23380,149 @@ var formDirectiveFactory = function(isNgForm) {
var formDirective = formDirectiveFactory();
var ngFormDirective = formDirectiveFactory(true);
-/* global VALID_CLASS: false,
+
+
+// helper methods
+function setupValidity(instance) {
+ instance.$$classCache = {};
+ instance.$$classCache[INVALID_CLASS] = !(instance.$$classCache[VALID_CLASS] = instance.$$element.hasClass(VALID_CLASS));
+}
+function addSetValidityMethod(context) {
+ var clazz = context.clazz,
+ set = context.set,
+ unset = context.unset;
+
+ clazz.prototype.$setValidity = function(validationErrorKey, state, controller) {
+ if (isUndefined(state)) {
+ createAndSet(this, '$pending', validationErrorKey, controller);
+ } else {
+ unsetAndCleanup(this, '$pending', validationErrorKey, controller);
+ }
+ if (!isBoolean(state)) {
+ unset(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
+ } else {
+ if (state) {
+ unset(this.$error, validationErrorKey, controller);
+ set(this.$$success, validationErrorKey, controller);
+ } else {
+ set(this.$error, validationErrorKey, controller);
+ unset(this.$$success, validationErrorKey, controller);
+ }
+ }
+ if (this.$pending) {
+ cachedToggleClass(this, PENDING_CLASS, true);
+ this.$valid = this.$invalid = undefined;
+ toggleValidationCss(this, '', null);
+ } else {
+ cachedToggleClass(this, PENDING_CLASS, false);
+ this.$valid = isObjectEmpty(this.$error);
+ this.$invalid = !this.$valid;
+ toggleValidationCss(this, '', this.$valid);
+ }
+
+ // re-read the state as the set/unset methods could have
+ // combined state in this.$error[validationError] (used for forms),
+ // where setting/unsetting only increments/decrements the value,
+ // and does not replace it.
+ var combinedState;
+ if (this.$pending && this.$pending[validationErrorKey]) {
+ combinedState = undefined;
+ } else if (this.$error[validationErrorKey]) {
+ combinedState = false;
+ } else if (this.$$success[validationErrorKey]) {
+ combinedState = true;
+ } else {
+ combinedState = null;
+ }
+
+ toggleValidationCss(this, validationErrorKey, combinedState);
+ this.$$parentForm.$setValidity(validationErrorKey, combinedState, this);
+ };
+
+ function createAndSet(ctrl, name, value, controller) {
+ if (!ctrl[name]) {
+ ctrl[name] = {};
+ }
+ set(ctrl[name], value, controller);
+ }
+
+ function unsetAndCleanup(ctrl, name, value, controller) {
+ if (ctrl[name]) {
+ unset(ctrl[name], value, controller);
+ }
+ if (isObjectEmpty(ctrl[name])) {
+ ctrl[name] = undefined;
+ }
+ }
+
+ function cachedToggleClass(ctrl, className, switchValue) {
+ if (switchValue && !ctrl.$$classCache[className]) {
+ ctrl.$$animate.addClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = true;
+ } else if (!switchValue && ctrl.$$classCache[className]) {
+ ctrl.$$animate.removeClass(ctrl.$$element, className);
+ ctrl.$$classCache[className] = false;
+ }
+ }
+
+ function toggleValidationCss(ctrl, validationErrorKey, isValid) {
+ validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
+
+ cachedToggleClass(ctrl, VALID_CLASS + validationErrorKey, isValid === true);
+ cachedToggleClass(ctrl, INVALID_CLASS + validationErrorKey, isValid === false);
+ }
+}
+
+function isObjectEmpty(obj) {
+ if (obj) {
+ for (var prop in obj) {
+ if (obj.hasOwnProperty(prop)) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+/* global
+ VALID_CLASS: false,
INVALID_CLASS: false,
PRISTINE_CLASS: false,
DIRTY_CLASS: false,
- UNTOUCHED_CLASS: false,
- TOUCHED_CLASS: false,
- ngModelMinErr: false,
+ ngModelMinErr: false
*/
-// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
-var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
+// Regex code was initially obtained from SO prior to modification: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
+var ISO_DATE_REGEXP = /^\d{4,}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+(?:[+-][0-2]\d:[0-5]\d|Z)$/;
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
-var URL_REGEXP = /^[A-Za-z][A-Za-z\d.+-]*:\/*(?:\w+(?::\w+)?@)?[^\s/]+(?::\d+)?(?:\/[\w#!:.?+=&%@\-/]*)?$/;
-var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
-var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
-var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
-var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
-var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
-var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
+// Note: We are being more lenient, because browsers are too.
+// 1. Scheme
+// 2. Slashes
+// 3. Username
+// 4. Password
+// 5. Hostname
+// 6. Port
+// 7. Path
+// 8. Query
+// 9. Fragment
+// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999
+var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
+// eslint-disable-next-line max-len
+var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
+var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\d+)?\s*$/;
+var DATE_REGEXP = /^(\d{4,})-(\d{2})-(\d{2})$/;
+var DATETIMELOCAL_REGEXP = /^(\d{4,})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
+var WEEK_REGEXP = /^(\d{4,})-W(\d\d)$/;
+var MONTH_REGEXP = /^(\d{4,})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
+var PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
+var PARTIAL_VALIDATION_TYPES = createMap();
+forEach('date,datetime-local,month,time,week'.split(','), function(type) {
+ PARTIAL_VALIDATION_TYPES[type] = true;
+});
+
var inputType = {
/**
@@ -20971,8 +23547,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21009,11 +23585,11 @@ var inputType = {
<span class="error" ng-show="myForm.input.$error.pattern">
Single word only!</span>
</div>
- <tt>text = {{example.text}}</tt><br/>
- <tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
- <tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
- <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
- <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
+ <code>text = {{example.text}}</code><br/>
+ <code>myForm.input.$valid = {{myForm.input.$valid}}</code><br/>
+ <code>myForm.input.$error = {{myForm.input.$error}}</code><br/>
+ <code>myForm.$valid = {{myForm.$valid}}</code><br/>
+ <code>myForm.$error.required = {{!!myForm.$error.required}}</code><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
@@ -21114,7 +23690,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-dd"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -21217,7 +23792,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -21302,7 +23876,7 @@ var inputType = {
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
- <label for="exampleInput">Pick a between 8am and 5pm:</label>
+ <label for="exampleInput">Pick a time between 8am and 5pm:</label>
<input type="time" id="exampleInput" name="input" ng-model="example.value"
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
<div role="alert">
@@ -21321,7 +23895,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "HH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -21426,7 +23999,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-Www"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -21530,7 +24102,6 @@ var inputType = {
<file name="protractor.js" type="protractor">
var value = element(by.binding('example.value | date: "yyyy-MM"'));
var valid = element(by.binding('myForm.input.$valid'));
- var input = element(by.model('example.value'));
// currently protractor/webdriver does not support
// sending keys to all known HTML5 input controls
@@ -21593,7 +24164,17 @@ var inputType = {
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * Can be interpolated.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * Can be interpolated.
+ * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
+ * Can be interpolated.
+ * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint,
+ * but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -21606,8 +24187,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21704,8 +24285,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21803,8 +24384,8 @@ var inputType = {
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -21925,19 +24506,140 @@ var inputType = {
</file>
<file name="protractor.js" type="protractor">
it('should change state', function() {
+ var inputs = element.all(by.model('color.name'));
var color = element(by.binding('color.name'));
expect(color.getText()).toContain('blue');
- element.all(by.model('color.name')).get(0).click();
-
+ inputs.get(0).click();
expect(color.getText()).toContain('red');
+
+ inputs.get(1).click();
+ expect(color.getText()).toContain('green');
});
</file>
</example>
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ * The model for the range input must always be a `Number`.
+ *
+ * IE9 and other browsers that do not support the `range` type fall back
+ * to a text input without any default values for `min`, `max` and `step`. Model binding,
+ * validation and number parsing are nevertheless supported.
+ *
+ * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
+ * in a way that never allows the input to hold an invalid value. That means:
+ * - any non-numerical value is set to `(max + min) / 2`.
+ * - any numerical value that is less than the current min val, or greater than the current max val
+ * is set to the min / max val respectively.
+ * - additionally, the current `step` is respected, so the nearest value that satisfies a step
+ * is used.
+ *
+ * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
+ * for more info.
+ *
+ * This has the following consequences for Angular:
+ *
+ * Since the element value should always reflect the current model value, a range input
+ * will set the bound ngModel expression to the value that the browser has set for the
+ * input element. For example, in the following input `<input type="range" ng-model="model.value">`,
+ * if the application sets `model.value = null`, the browser will set the input to `'50'`.
+ * Angular will then set the model to `50`, to prevent input and model value being out of sync.
+ *
+ * That means the model for range will immediately be set to `50` after `ngModel` has been
+ * initialized. It also means a range input can never have the required error.
+ *
+ * This does not only affect changes to the model value, but also to the values of the `min`,
+ * `max`, and `step` attributes. When these change in a way that will cause the browser to modify
+ * the input value, Angular will also update the model value.
+ *
+ * Automatic value adjustment also means that a range input element can never have the `required`,
+ * `min`, or `max` errors.
+ *
+ * However, `step` is currently only fully implemented by Firefox. Other browsers have problems
+ * when the step value changes dynamically - they do not adjust the element value correctly, but
+ * instead may set the `stepMismatch` error. If that's the case, the Angular will set the `step`
+ * error on the input, and set the model to `undefined`.
+ *
+ * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
+ * not set the `min` and `max` attributes, which means that the browser won't automatically adjust
+ * the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
+ *
+ * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation to ensure that the value entered is greater
+ * than `min`. Can be interpolated.
+ * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
+ * Can be interpolated.
+ * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
+ * Can be interpolated.
+ * @param {string=} ngChange Angular expression to be executed when the ngModel value changes due
+ * to user interaction with the input element.
+ * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
+ * element. **Note** : `ngChecked` should not be used alongside `ngModel`.
+ * Checkout {@link ng.directive:ngChecked ngChecked} for usage.
+ *
+ * @example
+ <example name="range-input-directive" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+
+ Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @example
+ <example name="range-input-directive-ng" module="rangeExample">
+ <file name="index.html">
+ <script>
+ angular.module('rangeExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.value = 75;
+ $scope.min = 10;
+ $scope.max = 90;
+ }]);
+ </script>
+ <form name="myForm" ng-controller="ExampleController">
+ Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max">
+ <hr>
+ Model as number: <input type="number" ng-model="value"><br>
+ Min: <input type="number" ng-model="min"><br>
+ Max: <input type="number" ng-model="max"><br>
+ value = <code>{{value}}</code><br/>
+ myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
+ myForm.range.$error = <code>{{myForm.range.$error}}</code>
+ </form>
+ </file>
+ </example>
+
+ */
+ 'range': rangeInputType,
/**
* @ngdoc input
@@ -22017,13 +24719,13 @@ function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var type = lowercase(element[0].type);
- // In composition mode, users are still inputing intermediate text buffer,
+ // In composition mode, users are still inputting intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
if (!$sniffer.android) {
var composing = false;
- element.on('compositionstart', function(data) {
+ element.on('compositionstart', function() {
composing = true;
});
@@ -22033,6 +24735,8 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
});
}
+ var timeout;
+
var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
@@ -22062,8 +24766,6 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
if ($sniffer.hasEvent('input')) {
element.on('input', listener);
} else {
- var timeout;
-
var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
@@ -22075,7 +24777,7 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
};
- element.on('keydown', function(event) {
+ element.on('keydown', /** @this */ function(event) {
var key = event.keyCode;
// ignore
@@ -22095,6 +24797,26 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// or form autocomplete on newer browser, we need "change" event to catch it
element.on('change', listener);
+ // Some native input types (date-family) have the ability to change validity without
+ // firing any input/change events.
+ // For these event types, when native validators are present and the browser supports the type,
+ // check for validity changes on various DOM events.
+ if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
+ element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) {
+ if (!timeout) {
+ var validity = this[VALIDITY_STATE_PROPERTY];
+ var origBadInput = validity.badInput;
+ var origTypeMismatch = validity.typeMismatch;
+ timeout = $browser.defer(function() {
+ timeout = null;
+ if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
+ listener(ev);
+ }
+ });
+ }
+ });
+ }
+
ctrl.$render = function() {
// Workaround for Firefox validation #12102.
var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;
@@ -22148,7 +24870,7 @@ function createDateParser(regexp, mapping) {
// When a date is JSON'ified to wraps itself inside of an extra
// set of double quotes. This makes the date parsing code unable
// to match the date string and parse it as a date.
- if (iso.charAt(0) == '"' && iso.charAt(iso.length - 1) == '"') {
+ if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') {
iso = iso.substring(1, iso.length - 1);
}
if (ISO_DATE_REGEXP.test(iso)) {
@@ -22190,7 +24912,7 @@ function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
- var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
+ var timezone = ctrl && ctrl.$options.getOption('timezone');
var previousDate;
ctrl.$$parserName = type;
@@ -22269,10 +24991,7 @@ function badInputChecker(scope, element, attr, ctrl) {
}
}
-function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- badInputChecker(scope, element, attr, ctrl);
- baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
-
+function numberFormatterParser(ctrl) {
ctrl.$$parserName = 'number';
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
@@ -22289,40 +25008,255 @@ function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
return value;
});
+}
+
+function parseNumberAttrVal(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ return !isNumberNaN(val) ? val : undefined;
+}
+
+function isNumberInteger(num) {
+ // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
+ // (minus the assumption that `num` is a number)
+
+ // eslint-disable-next-line no-bitwise
+ return (num | 0) === num;
+}
+
+function countDecimals(num) {
+ var numString = num.toString();
+ var decimalSymbolIndex = numString.indexOf('.');
+
+ if (decimalSymbolIndex === -1) {
+ if (-1 < num && num < 1) {
+ // It may be in the exponential notation format (`1e-X`)
+ var match = /e-(\d+)$/.exec(numString);
+
+ if (match) {
+ return Number(match[1]);
+ }
+ }
+
+ return 0;
+ }
+
+ return numString.length - decimalSymbolIndex - 1;
+}
+
+function isValidForStep(viewValue, stepBase, step) {
+ // At this point `stepBase` and `step` are expected to be non-NaN values
+ // and `viewValue` is expected to be a valid stringified number.
+ var value = Number(viewValue);
+
+ var isNonIntegerValue = !isNumberInteger(value);
+ var isNonIntegerStepBase = !isNumberInteger(stepBase);
+ var isNonIntegerStep = !isNumberInteger(step);
+
+ // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
+ // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
+ if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) {
+ var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0;
+ var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0;
+ var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0;
+
+ var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals);
+ var multiplier = Math.pow(10, decimalCount);
+
+ value = value * multiplier;
+ stepBase = stepBase * multiplier;
+ step = step * multiplier;
+
+ if (isNonIntegerValue) value = Math.round(value);
+ if (isNonIntegerStepBase) stepBase = Math.round(stepBase);
+ if (isNonIntegerStep) step = Math.round(step);
+ }
+
+ return (value - stepBase) % step === 0;
+}
+
+function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl);
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var minVal;
+ var maxVal;
if (isDefined(attr.min) || attr.ngMin) {
- var minVal;
ctrl.$validators.min = function(value) {
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
};
attr.$observe('min', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val, 10);
- }
- minVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ minVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
});
}
if (isDefined(attr.max) || attr.ngMax) {
- var maxVal;
ctrl.$validators.max = function(value) {
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
};
attr.$observe('max', function(val) {
- if (isDefined(val) && !isNumber(val)) {
- val = parseFloat(val, 10);
- }
- maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
+ maxVal = parseNumberAttrVal(val);
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ });
+ }
+
+ if (isDefined(attr.step) || attr.ngStep) {
+ var stepVal;
+ ctrl.$validators.step = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
+ isValidForStep(viewValue, minVal || 0, stepVal);
+ };
+
+ attr.$observe('step', function(val) {
+ stepVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
});
}
}
+function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl);
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
+ minVal = supportsRange ? 0 : undefined,
+ maxVal = supportsRange ? 100 : undefined,
+ stepVal = supportsRange ? 1 : undefined,
+ validity = element[0].validity,
+ hasMinAttr = isDefined(attr.min),
+ hasMaxAttr = isDefined(attr.max),
+ hasStepAttr = isDefined(attr.step);
+
+ var originalRender = ctrl.$render;
+
+ ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
+ //Browsers that implement range will set these values automatically, but reading the adjusted values after
+ //$render would cause the min / max validators to be applied with the wrong value
+ function rangeRender() {
+ originalRender();
+ ctrl.$setViewValue(element.val());
+ } :
+ originalRender;
+
+ if (hasMinAttr) {
+ 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; } :
+ // non-support browsers validate the min val
+ function minValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
+ };
+
+ setInitialValueAndObserver('min', minChange);
+ }
+
+ if (hasMaxAttr) {
+ 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; } :
+ // non-support browsers validate the max val
+ function maxValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
+ };
+
+ setInitialValueAndObserver('max', maxChange);
+ }
+
+ if (hasStepAttr) {
+ ctrl.$validators.step = supportsRange ?
+ function nativeStepValidator() {
+ // Currently, only FF implements the spec on step change correctly (i.e. adjusting the
+ // input element value to a valid value). It's possible that other browsers set the stepMismatch
+ // validity error instead, so we can at least report an error in that case.
+ return !validity.stepMismatch;
+ } :
+ // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
+ function stepValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
+ isValidForStep(viewValue, minVal || 0, stepVal);
+ };
+
+ setInitialValueAndObserver('step', stepChange);
+ }
+
+ function setInitialValueAndObserver(htmlAttrName, changeFn) {
+ // interpolated attributes set the attribute value only after a digest, but we need the
+ // 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);
+ }
+
+ function minChange(val) {
+ minVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the minVal is greater than the element value
+ if (minVal > elVal) {
+ elVal = minVal;
+ element.val(elVal);
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function maxChange(val) {
+ maxVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the maxVal is less than the element value
+ if (maxVal < elVal) {
+ element.val(maxVal);
+ // IE11 and Chrome don't set the value to the minVal when max < min
+ elVal = maxVal < minVal ? minVal : maxVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function stepChange(val) {
+ stepVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ // 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 {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+}
+
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// Note: no badInputChecker here by purpose as `url` is only a validation
// in browsers, i.e. we can always read out input.value even if it is not valid!
@@ -22350,14 +25284,20 @@ function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
function radioInputType(scope, element, attr, ctrl) {
+ var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false';
// make the name unique, if not defined
if (isUndefined(attr.name)) {
element.attr('name', nextUid());
}
var listener = function(ev) {
+ var value;
if (element[0].checked) {
- ctrl.$setViewValue(attr.value, ev && ev.type);
+ value = attr.value;
+ if (doTrim) {
+ value = trim(value);
+ }
+ ctrl.$setViewValue(value, ev && ev.type);
}
};
@@ -22365,7 +25305,10 @@ function radioInputType(scope, element, attr, ctrl) {
ctrl.$render = function() {
var value = attr.value;
- element[0].checked = (value == ctrl.$viewValue);
+ if (doTrim) {
+ value = trim(value);
+ }
+ element[0].checked = (value === ctrl.$viewValue);
};
attr.$observe('value', ctrl.$render);
@@ -22436,8 +25379,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22448,6 +25391,20 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
+ *
+ * @knownIssue
+ *
+ * When specifying the `placeholder` attribute of `<textarea>`, Internet Explorer will temporarily
+ * insert the placeholder value as the textarea's content. If the placeholder value contains
+ * interpolation (`{{ ... }}`), an error will be logged in the console when Angular tries to update
+ * the value of the by-then-removed text node. This doesn't affect the functionality of the
+ * textarea, but can be undesirable.
+ *
+ * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of
+ * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can
+ * find more details on `ngAttr` in the
+ * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the
+ * Developer Guide.
*/
@@ -22475,8 +25432,8 @@ function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filt
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
* length.
- * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
- * a RegExp found by evaluating the Angular expression given in the attribute value.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * value does not match a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object, then this is used directly.
* If the expression evaluates to a string, then it will be converted to a RegExp
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
@@ -22612,21 +25569,19 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
* @name ngValue
*
* @description
- * Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`},
- * so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to
- * the bound value.
+ * Binds the given expression to the value of the element.
*
- * `ngValue` is useful when dynamically generating lists of radio buttons using
- * {@link ngRepeat `ngRepeat`}, as shown below.
+ * It is mainly used on {@link input[radio] `input[radio]`} and option elements,
+ * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its
+ * {@link select `select`} parent element) is set to the bound value. It is especially useful
+ * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below.
*
- * Likewise, `ngValue` can be used to generate `<option>` elements for
- * the {@link select `select`} element. In that case however, only strings are supported
- * for the `value `attribute, so the resulting `ngModel` will always be a string.
- * Support for `select` models with non-string values is available via `ngOptions`.
+ * It can also be used to achieve one-way binding of a given expression to an input element
+ * such as an `input[text]` or a `textarea`, when that element does not use ngModel.
*
* @element input
* @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
- * of the `input` element
+ * and `value` property of the element.
*
* @example
<example name="ngValue-directive" module="valueExample">
@@ -22665,18 +25620,33 @@ var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
</example>
*/
var ngValueDirective = function() {
+ /**
+ * inputs use the value attribute as their default value if the value property is not set.
+ * Once the value property has been set (by adding input), it will not react to changes to
+ * the value attribute anymore. Setting both attribute and property fixes this behavior, and
+ * makes it possible to use ngValue as a sort of one-way bind.
+ */
+ function updateElementValue(element, attr, value) {
+ // Support: IE9 only
+ // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`).
+ var propValue = isDefined(value) ? value : (msie === 9) ? '' : null;
+ element.prop('value', propValue);
+ attr.$set('value', value);
+ }
+
return {
restrict: 'A',
priority: 100,
compile: function(tpl, tplAttr) {
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
return function ngValueConstantLink(scope, elm, attr) {
- attr.$set('value', scope.$eval(attr.ngValue));
+ var value = scope.$eval(attr.ngValue);
+ updateElementValue(elm, attr, value);
};
} else {
return function ngValueLink(scope, elm, attr) {
scope.$watch(attr.ngValue, function valueWatchAction(value) {
- attr.$set('value', value);
+ updateElementValue(elm, attr, value);
});
};
}
@@ -22710,7 +25680,7 @@ var ngValueDirective = function() {
*
* @example
* Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
- <example module="bindExample">
+ <example module="bindExample" name="ng-bind">
<file name="index.html">
<script>
angular.module('bindExample', [])
@@ -22744,7 +25714,7 @@ var ngBindDirective = ['$compile', function($compile) {
$compile.$$addBindingInfo(element, attr.ngBind);
element = element[0];
scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
- element.textContent = isUndefined(value) ? '' : value;
+ element.textContent = stringify(value);
});
};
}
@@ -22770,7 +25740,7 @@ var ngBindDirective = ['$compile', function($compile) {
*
* @example
* Try it here: enter text in text box and watch the greeting change.
- <example module="bindExample">
+ <example module="bindExample" name="ng-bind-template">
<file name="index.html">
<script>
angular.module('bindExample', [])
@@ -22843,7 +25813,7 @@ var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate
*
* @example
- <example module="bindHtmlExample" deps="angular-sanitize.js">
+ <example module="bindHtmlExample" deps="angular-sanitize.js" name="ng-bind-html">
<file name="index.html">
<div ng-controller="ExampleController">
<p ng-bind-html="myHTML"></p>
@@ -22872,8 +25842,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
restrict: 'A',
compile: function ngBindHtmlCompile(tElement, tAttrs) {
var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
- var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function getStringValue(value) {
- return (value || '').toString();
+ var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
+ // Unwrap the value to compare the actual inner safe value, not the wrapper object.
+ return $sce.valueOf(val);
});
$compile.$$addBindingClass(tElement);
@@ -22881,9 +25852,9 @@ var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse,
$compile.$$addBindingInfo(element, attr.ngBindHtml);
scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
- // we re-evaluate the expr because we want a TrustedValueHolderType
- // for $sce, not a string
- element.html($sce.getTrustedHtml(ngBindHtmlGetter(scope)) || '');
+ // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
+ var value = ngBindHtmlGetter(scope);
+ element.html($sce.getTrustedHtml(value) || '');
});
};
}
@@ -22967,50 +25938,79 @@ var ngChangeDirective = valueFn({
}
});
+/* exported
+ ngClassDirective,
+ ngClassEvenDirective,
+ ngClassOddDirective
+*/
+
function classDirective(name, selector) {
name = 'ngClass' + name;
- return ['$animate', function($animate) {
+ var indexWatchExpression;
+
+ return ['$parse', function($parse) {
return {
restrict: 'AC',
link: function(scope, element, attr) {
- var oldVal;
+ var expression = attr[name].trim();
+ var isOneTime = (expression.charAt(0) === ':') && (expression.charAt(1) === ':');
- scope.$watch(attr[name], ngClassWatchAction, true);
+ var watchInterceptor = isOneTime ? toFlatValue : toClassString;
+ var watchExpression = $parse(expression, watchInterceptor);
+ var watchAction = isOneTime ? ngClassOneTimeWatchAction : ngClassWatchAction;
- attr.$observe('class', function(value) {
- ngClassWatchAction(scope.$eval(attr[name]));
- });
+ var classCounts = element.data('$classCounts');
+ var oldModulo = true;
+ var oldClassString;
+ if (!classCounts) {
+ // Use createMap() to prevent class assumptions involving property
+ // names in Object.prototype
+ classCounts = createMap();
+ element.data('$classCounts', classCounts);
+ }
if (name !== 'ngClass') {
- scope.$watch('$index', function($index, old$index) {
- // jshint bitwise: false
- var mod = $index & 1;
- if (mod !== (old$index & 1)) {
- var classes = arrayClasses(scope.$eval(attr[name]));
- mod === selector ?
- addClasses(classes) :
- removeClasses(classes);
- }
- });
+ if (!indexWatchExpression) {
+ indexWatchExpression = $parse('$index', function moduloTwo($index) {
+ // eslint-disable-next-line no-bitwise
+ return $index & 1;
+ });
+ }
+
+ scope.$watch(indexWatchExpression, ngClassIndexWatchAction);
}
- function addClasses(classes) {
- var newClasses = digestClassCounts(classes, 1);
- attr.$addClass(newClasses);
+ scope.$watch(watchExpression, watchAction, isOneTime);
+
+ function addClasses(classString) {
+ classString = digestClassCounts(split(classString), 1);
+ attr.$addClass(classString);
}
- function removeClasses(classes) {
- var newClasses = digestClassCounts(classes, -1);
- attr.$removeClass(newClasses);
+ function removeClasses(classString) {
+ classString = digestClassCounts(split(classString), -1);
+ attr.$removeClass(classString);
}
- function digestClassCounts(classes, count) {
- // Use createMap() to prevent class assumptions involving property
- // names in Object.prototype
- var classCounts = element.data('$classCounts') || createMap();
+ function updateClasses(oldClassString, newClassString) {
+ var oldClassArray = split(oldClassString);
+ var newClassArray = split(newClassString);
+
+ var toRemoveArray = arrayDifference(oldClassArray, newClassArray);
+ var toAddArray = arrayDifference(newClassArray, oldClassArray);
+
+ var toRemoveString = digestClassCounts(toRemoveArray, -1);
+ var toAddString = digestClassCounts(toAddArray, 1);
+
+ attr.$addClass(toAddString);
+ attr.$removeClass(toRemoveString);
+ }
+
+ function digestClassCounts(classArray, count) {
var classesToUpdate = [];
- forEach(classes, function(className) {
+
+ forEach(classArray, function(className) {
if (count > 0 || classCounts[className]) {
classCounts[className] = (classCounts[className] || 0) + count;
if (classCounts[className] === +(count > 0)) {
@@ -23018,72 +26018,106 @@ function classDirective(name, selector) {
}
}
});
- element.data('$classCounts', classCounts);
+
return classesToUpdate.join(' ');
}
- function updateClasses(oldClasses, newClasses) {
- var toAdd = arrayDifference(newClasses, oldClasses);
- var toRemove = arrayDifference(oldClasses, newClasses);
- toAdd = digestClassCounts(toAdd, 1);
- toRemove = digestClassCounts(toRemove, -1);
- if (toAdd && toAdd.length) {
- $animate.addClass(element, toAdd);
+ function ngClassIndexWatchAction(newModulo) {
+ // This watch-action should run before the `ngClass[OneTime]WatchAction()`, thus it
+ // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the
+ // `ngClass[OneTime]WatchAction()` will update the classes.
+ if (newModulo === selector) {
+ addClasses(oldClassString);
+ } else {
+ removeClasses(oldClassString);
}
- if (toRemove && toRemove.length) {
- $animate.removeClass(element, toRemove);
+
+ oldModulo = newModulo;
+ }
+
+ function ngClassOneTimeWatchAction(newClassValue) {
+ var newClassString = toClassString(newClassValue);
+
+ if (newClassString !== oldClassString) {
+ ngClassWatchAction(newClassString);
}
}
- function ngClassWatchAction(newVal) {
- if (selector === true || scope.$index % 2 === selector) {
- var newClasses = arrayClasses(newVal || []);
- if (!oldVal) {
- addClasses(newClasses);
- } else if (!equals(newVal,oldVal)) {
- var oldClasses = arrayClasses(oldVal);
- updateClasses(oldClasses, newClasses);
- }
+ function ngClassWatchAction(newClassString) {
+ if (oldModulo === selector) {
+ updateClasses(oldClassString, newClassString);
}
- oldVal = shallowCopy(newVal);
+
+ oldClassString = newClassString;
}
}
};
+ }];
- function arrayDifference(tokens1, tokens2) {
- var values = [];
+ // Helpers
+ function arrayDifference(tokens1, tokens2) {
+ if (!tokens1 || !tokens1.length) return [];
+ if (!tokens2 || !tokens2.length) return tokens1;
- outer:
- for (var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for (var j = 0; j < tokens2.length; j++) {
- if (token == tokens2[j]) continue outer;
- }
- values.push(token);
+ var values = [];
+
+ outer:
+ for (var i = 0; i < tokens1.length; i++) {
+ var token = tokens1[i];
+ for (var j = 0; j < tokens2.length; j++) {
+ if (token === tokens2[j]) continue outer;
}
- return values;
+ values.push(token);
}
- function arrayClasses(classVal) {
- var classes = [];
- if (isArray(classVal)) {
- forEach(classVal, function(v) {
- classes = classes.concat(arrayClasses(v));
- });
- return classes;
- } else if (isString(classVal)) {
- return classVal.split(' ');
- } else if (isObject(classVal)) {
- forEach(classVal, function(v, k) {
- if (v) {
- classes = classes.concat(k.split(' '));
- }
- });
- return classes;
+ return values;
+ }
+
+ function split(classString) {
+ return classString && classString.split(' ');
+ }
+
+ function toClassString(classValue) {
+ var classString = classValue;
+
+ if (isArray(classValue)) {
+ classString = classValue.map(toClassString).join(' ');
+ } else if (isObject(classValue)) {
+ classString = Object.keys(classValue).
+ filter(function(key) { return classValue[key]; }).
+ join(' ');
+ }
+
+ return classString;
+ }
+
+ function toFlatValue(classValue) {
+ var flatValue = classValue;
+
+ if (isArray(classValue)) {
+ flatValue = classValue.map(toFlatValue);
+ } else if (isObject(classValue)) {
+ var hasUndefined = false;
+
+ flatValue = Object.keys(classValue).filter(function(key) {
+ var value = classValue[key];
+
+ if (!hasUndefined && isUndefined(value)) {
+ hasUndefined = true;
+ }
+
+ return value;
+ });
+
+ if (hasUndefined) {
+ // Prevent the `oneTimeLiteralWatchInterceptor` from unregistering
+ // the watcher, by including at least one `undefined` value.
+ flatValue.push(undefined);
}
- return classVal;
}
- }];
+
+ return flatValue;
+ }
}
/**
@@ -23114,10 +26148,16 @@ function classDirective(name, selector) {
* When the expression changes, the previously added classes are removed and only then are the
* new classes added.
*
- * @animations
- * **add** - happens just before the class is applied to the elements
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `class`
+ * attribute, when using the `ngClass` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
*
- * **remove** - happens just before the class is removed from the element
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
*
* @element ANY
* @param {expression} ngClass {@link guide/expression Expression} to eval. The result
@@ -23127,7 +26167,7 @@ function classDirective(name, selector) {
* element.
*
* @example Example that demonstrates basic bindings via ngClass directive.
- <example>
+ <example name="ng-class">
<file name="index.html">
<p ng-class="{strike: deleted, bold: important, 'has-error': error}">Map Syntax Example</p>
<label>
@@ -23220,7 +26260,7 @@ function classDirective(name, selector) {
The example below demonstrates how to perform animations using ngClass.
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-class">
<file name="index.html">
<input id="setbtn" type="button" value="set" ng-click="myVar='my-class'">
<input id="clearbtn" type="button" value="clear" ng-click="myVar=''">
@@ -23283,7 +26323,7 @@ var ngClassDirective = classDirective('', true);
* of the evaluation can be a string representing space delimited class names or an array.
*
* @example
- <example>
+ <example name="ng-class-odd">
<file name="index.html">
<ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
<li ng-repeat="name in names">
@@ -23331,7 +26371,7 @@ var ngClassOddDirective = classDirective('Odd', 0);
* result of the evaluation can be a string representing space delimited class names or an array.
*
* @example
- <example>
+ <example name="ng-class-even">
<file name="index.html">
<ol ng-init="names=['John', 'Mary', 'Cate', 'Suz']">
<li ng-repeat="name in names">
@@ -23397,7 +26437,7 @@ var ngClassEvenDirective = classDirective('Even', 1);
* @element ANY
*
* @example
- <example>
+ <example name="ng-cloak">
<file name="index.html">
<div id="template1" ng-cloak>{{ 'hello' }}</div>
<div id="template2" class="ng-cloak">{{ 'world' }}</div>
@@ -23453,7 +26493,7 @@ var ngCloakDirective = ngDirective({
*
* If the current `$controllerProvider` is configured to use globals (via
* {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
- * also be the name of a globally accessible constructor function (not recommended).
+ * also be the name of a globally accessible constructor function (deprecated, not recommended).
*
* @example
* Here is a simple form for editing user contact information. Adding, removing, clearing, and
@@ -23506,10 +26546,11 @@ var ngCloakDirective = ngDirective({
* .controller('SettingsController1', SettingsController1);
*
* function SettingsController1() {
- * this.name = "John Smith";
+ * this.name = 'John Smith';
* this.contacts = [
* {type: 'phone', value: '408 555 1212'},
- * {type: 'email', value: 'john.smith@example.org'} ];
+ * {type: 'email', value: 'john.smith@example.org'}
+ * ];
* }
*
* SettingsController1.prototype.greet = function() {
@@ -23589,10 +26630,11 @@ var ngCloakDirective = ngDirective({
* .controller('SettingsController2', ['$scope', SettingsController2]);
*
* function SettingsController2($scope) {
- * $scope.name = "John Smith";
+ * $scope.name = 'John Smith';
* $scope.contacts = [
* {type:'phone', value:'408 555 1212'},
- * {type:'email', value:'john.smith@example.org'} ];
+ * {type:'email', value:'john.smith@example.org'}
+ * ];
*
* $scope.greet = function() {
* alert($scope.name);
@@ -23659,31 +26701,38 @@ var ngControllerDirective = [function() {
* @ngdoc directive
* @name ngCsp
*
- * @element html
+ * @restrict A
+ * @element ANY
* @description
*
- * Angular has some features that can break certain
+ * Angular has some features that can conflict with certain restrictions that are applied when using
* [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
*
- * If you intend to implement these rules then you must tell Angular not to use these features.
+ * If you intend to implement CSP with these rules then you must tell Angular not to use these
+ * features.
*
* This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
*
*
- * The following rules affect Angular:
+ * The following default rules in CSP affect Angular:
*
- * * `unsafe-eval`: this rule forbids apps to use `eval` or `Function(string)` generated functions
- * (among other things). Angular makes use of this in the {@link $parse} service to provide a 30%
- * increase in the speed of evaluating Angular expressions.
+ * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute
+ * code from strings is forbidden. Angular makes use of this in the {@link $parse} service to
+ * provide a 30% increase in the speed of evaluating Angular expressions. (This CSP rule can be
+ * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would
+ * weaken the protections offered by CSP.)
*
- * * `unsafe-inline`: this rule forbids apps from inject custom styles into the document. Angular
- * makes use of this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}).
- * To make these directives work when a CSP rule is blocking inline styles, you must link to the
- * `angular-csp.css` in your HTML manually.
+ * * The use of inline resources, such as inline `<script>` and `<style>` elements, are forbidden.
+ * This prevents apps from injecting custom styles directly into the document. Angular makes use of
+ * this to include some CSS rules (e.g. {@link ngCloak} and {@link ngHide}). To make these
+ * directives work when a CSP rule is blocking inline styles, you must link to the `angular-csp.css`
+ * in your HTML manually. (This CSP rule can be disabled with the CSP keyword `unsafe-inline`, but
+ * it is generally not recommended as it would weaken the protections offered by CSP.)
*
- * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking unsafe-eval
- * and automatically deactivates this feature in the {@link $parse} service. This autodetection,
- * however, triggers a CSP error to be logged in the console:
+ * If you do not provide `ngCsp` then Angular tries to autodetect if CSP is blocking dynamic code
+ * creation from strings (e.g., `unsafe-eval` not specified in CSP header) and automatically
+ * deactivates this feature in the {@link $parse} service. This autodetection, however, triggers a
+ * CSP error to be logged in the console:
*
* ```
* Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
@@ -23702,24 +26751,24 @@ var ngControllerDirective = [function() {
*
* * no-inline-style: this stops Angular from injecting CSS styles into the DOM
*
- * * no-unsafe-eval: this stops Angular from optimising $parse with unsafe eval of strings
+ * * no-unsafe-eval: this stops Angular from optimizing $parse with unsafe eval of strings
*
* You can use these values in the following combinations:
*
*
* * No declaration means that Angular will assume that you can do inline styles, but it will do
- * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous versions
- * of Angular.
+ * a runtime check for unsafe-eval. E.g. `<body>`. This is backwardly compatible with previous
+ * versions of Angular.
*
* * A simple `ng-csp` (or `data-ng-csp`) attribute will tell Angular to deactivate both inline
- * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous versions
- * of Angular.
+ * styles and unsafe eval. E.g. `<body ng-csp>`. This is backwardly compatible with previous
+ * versions of Angular.
*
- * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can inject
- * inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
+ * * Specifying only `no-unsafe-eval` tells Angular that we must not use eval, but that we can
+ * inject inline styles. E.g. `<body ng-csp="no-unsafe-eval">`.
*
* * Specifying only `no-inline-style` tells Angular that we must not inject styles, but that we can
- * run eval - no automcatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
+ * run eval - no automatic check for unsafe eval will occur. E.g. `<body ng-csp="no-inline-style">`
*
* * Specifying both `no-unsafe-eval` and `no-inline-style` tells Angular that we must not inject
* styles nor use eval, which is the same as an empty: ng-csp.
@@ -23735,8 +26784,7 @@ var ngControllerDirective = [function() {
</html>
```
* @example
- // Note: the suffix `.csp` in the example name triggers
- // csp mode in our http server!
+ <!-- Note: the `.csp` suffix in the example name triggers CSP mode in our http server! -->
<example name="example.csp" module="cspExample" ng-csp="true">
<file name="index.html">
<div ng-controller="MainController as ctrl">
@@ -23757,15 +26805,14 @@ var ngControllerDirective = [function() {
</file>
<file name="script.js">
angular.module('cspExample', [])
- .controller('MainController', function() {
+ .controller('MainController', function MainController() {
this.counter = 0;
this.inc = function() {
this.counter++;
};
this.evil = function() {
- // jshint evil:true
try {
- eval('1+2');
+ eval('1+2'); // eslint-disable-line no-eval
} catch (e) {
this.evilError = e.message;
}
@@ -23817,7 +26864,7 @@ var ngControllerDirective = [function() {
beforeEach(function() {
util = require('util');
- webdriver = require('protractor/node_modules/selenium-webdriver');
+ webdriver = require('selenium-webdriver');
});
// For now, we only test on Chrome,
@@ -23854,9 +26901,9 @@ var ngControllerDirective = [function() {
</example>
*/
-// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
-// bootstrap the system (before $parse is instantiated), for this reason we just have
-// the csp() fn that looks for the `ng-csp` attribute anywhere in the current doc
+// `ngCsp` is not implemented as a proper directive any more, because we need it be processed while
+// we bootstrap the app (before `$parse` is instantiated). For this reason, we just have the `csp()`
+// fn that looks for the `ng-csp` attribute anywhere in the current doc.
/**
* @ngdoc directive
@@ -23872,7 +26919,7 @@ var ngControllerDirective = [function() {
* click. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-click">
<file name="index.html">
<button ng-click="count = count + 1" ng-init="count=0">
Increment
@@ -23911,15 +26958,15 @@ forEach(
return {
restrict: 'A',
compile: function($element, attr) {
- // We expose the powerful $event object on the scope that provides access to the Window,
- // etc. that isn't protected by the fast paths in $parse. We explicitly request better
- // checks at the cost of speed since event handler expressions are not executed as
- // frequently as regular change detection.
- var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true);
+ // NOTE:
+ // We expose the powerful `$event` object on the scope that provides access to the Window,
+ // etc. This is OK, because expressions are not sandboxed any more (and the expression
+ // sandbox was never meant to be a security feature anyway).
+ var fn = $parse(attr[directiveName]);
return function ngEventHandler(scope, element) {
element.on(eventName, function(event) {
var callback = function() {
- fn(scope, {$event:event});
+ fn(scope, {$event: event});
};
if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
scope.$evalAsync(callback);
@@ -23947,7 +26994,7 @@ forEach(
* a dblclick. (The Event object is available as `$event`)
*
* @example
- <example>
+ <example name="ng-dblclick">
<file name="index.html">
<button ng-dblclick="count = count + 1" ng-init="count=0">
Increment (on double click)
@@ -23971,7 +27018,7 @@ forEach(
* mousedown. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mousedown">
<file name="index.html">
<button ng-mousedown="count = count + 1" ng-init="count=0">
Increment (on mouse down)
@@ -23995,7 +27042,7 @@ forEach(
* mouseup. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mouseup">
<file name="index.html">
<button ng-mouseup="count = count + 1" ng-init="count=0">
Increment (on mouse up)
@@ -24018,7 +27065,7 @@ forEach(
* mouseover. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mouseover">
<file name="index.html">
<button ng-mouseover="count = count + 1" ng-init="count=0">
Increment (when mouse is over)
@@ -24042,7 +27089,7 @@ forEach(
* mouseenter. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mouseenter">
<file name="index.html">
<button ng-mouseenter="count = count + 1" ng-init="count=0">
Increment (when mouse enters)
@@ -24066,7 +27113,7 @@ forEach(
* mouseleave. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mouseleave">
<file name="index.html">
<button ng-mouseleave="count = count + 1" ng-init="count=0">
Increment (when mouse leaves)
@@ -24090,7 +27137,7 @@ forEach(
* mousemove. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-mousemove">
<file name="index.html">
<button ng-mousemove="count = count + 1" ng-init="count=0">
Increment (when mouse moves)
@@ -24114,7 +27161,7 @@ forEach(
* keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
*
* @example
- <example>
+ <example name="ng-keydown">
<file name="index.html">
<input ng-keydown="count = count + 1" ng-init="count=0">
key down count: {{count}}
@@ -24136,7 +27183,7 @@ forEach(
* keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
*
* @example
- <example>
+ <example name="ng-keyup">
<file name="index.html">
<p>Typing in the input box below updates the key count</p>
<input ng-keyup="count = count + 1" ng-init="count=0"> key up count: {{count}}
@@ -24163,7 +27210,7 @@ forEach(
* and can be interrogated for keyCode, altKey, etc.)
*
* @example
- <example>
+ <example name="ng-keypress">
<file name="index.html">
<input ng-keypress="count = count + 1" ng-init="count=0">
key press count: {{count}}
@@ -24196,7 +27243,7 @@ forEach(
* ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example module="submitExample">
+ <example module="submitExample" name="ng-submit">
<file name="index.html">
<script>
angular.module('submitExample', [])
@@ -24292,7 +27339,7 @@ forEach(
* copy. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-copy">
<file name="index.html">
<input ng-copy="copied=true" ng-init="copied=false; value='copy me'" ng-model="value">
copied: {{copied}}
@@ -24313,7 +27360,7 @@ forEach(
* cut. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-cut">
<file name="index.html">
<input ng-cut="cut=true" ng-init="cut=false; value='cut me'" ng-model="value">
cut: {{cut}}
@@ -24334,7 +27381,7 @@ forEach(
* paste. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- <example>
+ <example name="ng-paste">
<file name="index.html">
<input ng-paste="paste=true" ng-init="paste=false" placeholder='paste here'>
pasted: {{paste}}
@@ -24376,8 +27423,10 @@ forEach(
* and `leave` effects.
*
* @animations
- * enter - happens just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container
- * leave - happens just before the `ngIf` contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
+ * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM |
*
* @element ANY
* @scope
@@ -24387,7 +27436,7 @@ forEach(
* element is added to the DOM tree.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-if">
<file name="index.html">
<label>Click me: <input type="checkbox" ng-model="checked" ng-init="checked=true" /></label><br/>
Show when checked:
@@ -24418,7 +27467,7 @@ forEach(
</file>
</example>
*/
-var ngIfDirective = ['$animate', function($animate) {
+var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
multiElement: true,
transclude: 'element',
@@ -24434,7 +27483,7 @@ var ngIfDirective = ['$animate', function($animate) {
if (!childScope) {
$transclude(function(clone, newScope) {
childScope = newScope;
- clone[clone.length++] = document.createComment(' end ngIf: ' + $attr.ngIf + ' ');
+ clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
// by a directive with templateUrl when its template arrives.
@@ -24455,8 +27504,8 @@ var ngIfDirective = ['$animate', function($animate) {
}
if (block) {
previousElements = getBlockNodes(block.clone);
- $animate.leave(previousElements).then(function() {
- previousElements = null;
+ $animate.leave(previousElements).done(function(response) {
+ if (response !== false) previousElements = null;
});
block = null;
}
@@ -24489,8 +27538,10 @@ var ngIfDirective = ['$animate', function($animate) {
* access on some browsers.
*
* @animations
- * enter - animation is used to bring new content into the browser.
- * leave - animation is used to animate existing content away.
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
+ * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
*
* The enter and leave animation occur concurrently.
*
@@ -24515,7 +27566,7 @@ var ngIfDirective = ['$animate', function($animate) {
* - Otherwise enable scrolling only if the expression evaluates to truthy value.
*
* @example
- <example module="includeExample" deps="angular-animate.js" animations="true">
+ <example module="includeExample" deps="angular-animate.js" animations="true" name="ng-include">
<file name="index.html">
<div ng-controller="ExampleController">
<select ng-model="template" ng-options="t.name for t in templates">
@@ -24532,8 +27583,8 @@ var ngIfDirective = ['$animate', function($animate) {
angular.module('includeExample', ['ngAnimate'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.templates =
- [ { name: 'template1.html', url: 'template1.html'},
- { name: 'template2.html', url: 'template2.html'} ];
+ [{ name: 'template1.html', url: 'template1.html'},
+ { name: 'template2.html', url: 'template2.html'}];
$scope.template = $scope.templates[0];
}]);
</file>
@@ -24591,7 +27642,7 @@ var ngIfDirective = ['$animate', function($animate) {
});
it('should load template2.html', function() {
- if (browser.params.browser == 'firefox') {
+ if (browser.params.browser === 'firefox') {
// Firefox can't handle using selects
// See https://github.com/angular/protractor/issues/480
return;
@@ -24602,7 +27653,7 @@ var ngIfDirective = ['$animate', function($animate) {
});
it('should change to blank', function() {
- if (browser.params.browser == 'firefox') {
+ if (browser.params.browser === 'firefox') {
// Firefox can't handle using selects
return;
}
@@ -24678,8 +27729,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
currentScope = null;
}
if (currentElement) {
- $animate.leave(currentElement).then(function() {
- previousElement = null;
+ $animate.leave(currentElement).done(function(response) {
+ if (response !== false) previousElement = null;
});
previousElement = currentElement;
currentElement = null;
@@ -24687,9 +27738,10 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
};
scope.$watch(srcExp, function ngIncludeWatchAction(src) {
- var afterAnimation = function() {
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
+ var afterAnimation = function(response) {
+ if (response !== false && isDefined(autoScrollExp) &&
+ (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
}
};
var thisChangeId = ++changeCounter;
@@ -24698,6 +27750,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
+ if (scope.$$destroyed) return;
+
if (thisChangeId !== changeCounter) return;
var newScope = scope.$new();
ctrl.template = response;
@@ -24710,7 +27764,7 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
// directives to non existing elements.
var clone = $transclude(newScope, function(clone) {
cleanupLastIncludeContent();
- $animate.enter(clone, null, $element).then(afterAnimation);
+ $animate.enter(clone, null, $element).done(afterAnimation);
});
currentScope = newScope;
@@ -24719,6 +27773,8 @@ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
currentScope.$emit('$includeContentLoaded', src);
scope.$eval(onloadExp);
}, function() {
+ if (scope.$$destroyed) return;
+
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
@@ -24747,12 +27803,12 @@ var ngIncludeFillContentDirective = ['$compile',
priority: -400,
require: 'ngInclude',
link: function(scope, $element, $attr, ctrl) {
- if (/SVG/.test($element[0].toString())) {
+ if (toString.call($element[0]).match(/SVG/)) {
// WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
// support innerHTML, so detect this here and try to generate the contents
// specially.
$element.empty();
- $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
+ $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
function namespaceAdaptedClone(clone) {
$element.append(clone);
}, {futureParentElement: $element});
@@ -24796,7 +27852,7 @@ var ngIncludeFillContentDirective = ['$compile',
* @param {expression} ngInit {@link guide/expression Expression} to eval.
*
* @example
- <example module="initExample">
+ <example module="initExample" name="ng-init">
<file name="index.html">
<script>
angular.module('initExample', [])
@@ -24924,9 +27980,7 @@ var ngListDirective = function() {
priority: 100,
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
- // We want to control whitespace trimming so we use this convoluted approach
- // to access the ngList attribute, which doesn't pre-trim the attribute
- var ngList = element.attr(attr.$attr.ngList) || ', ';
+ var ngList = attr.ngList || ', ';
var trimValues = attr.ngTrim !== 'false';
var separator = trimValues ? trim(ngList) : ngList;
@@ -24968,15 +28022,19 @@ var ngListDirective = function() {
DIRTY_CLASS: true,
UNTOUCHED_CLASS: true,
TOUCHED_CLASS: true,
+ PENDING_CLASS: true,
+ addSetValidityMethod: true,
+ setupValidity: true,
+ defaultModelOptions: false
*/
+
var VALID_CLASS = 'ng-valid',
INVALID_CLASS = 'ng-invalid',
PRISTINE_CLASS = 'ng-pristine',
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending',
EMPTY_CLASS = 'ng-empty',
NOT_EMPTY_CLASS = 'ng-not-empty';
@@ -24989,32 +28047,57 @@ var ngModelMinErr = minErr('ngModel');
* @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
* String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
* is set.
+ *
* @property {*} $modelValue The value in the model that the control is bound to.
+ *
* @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
- the control reads value from the DOM. The functions are called in array order, each passing
- its return value through to the next. The last return value is forwarded to the
- {@link ngModel.NgModelController#$validators `$validators`} collection.
+ * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue
+ `$viewValue`} from the DOM, usually via user input.
+ See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation.
+ Note that the `$parsers` are not called when the bound ngModel expression changes programmatically.
-Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
-`$viewValue`}.
+ The functions are called in array order, each passing
+ its return value through to the next. The last return value is forwarded to the
+ {@link ngModel.NgModelController#$validators `$validators`} collection.
-Returning `undefined` from a parser means a parse error occurred. In that case,
-no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
-will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
-is set to `true`. The parse error is stored in `ngModel.$error.parse`.
+ Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
+ `$viewValue`}.
+
+ Returning `undefined` from a parser means a parse error occurred. In that case,
+ no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
+ will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
+ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
+
+ This simple example shows a parser that would convert text input value to lowercase:
+ * ```js
+ * function parse(value) {
+ * if (value) {
+ * return value.toLowerCase();
+ * }
+ * }
+ * ngModelController.$parsers.push(parse);
+ * ```
*
* @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
- the model value changes. The functions are called in reverse array order, each passing the value through to the
- next. The last return value is used as the actual DOM value.
- Used to format / convert values for display in the control.
+ the bound ngModel expression changes programmatically. The `$formatters` are not called when the
+ value of the control is changed by user interaction.
+
+ Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue
+ `$modelValue`} for display in the control.
+
+ The functions are called in reverse array order, each passing the value through to the
+ next. The last return value is used as the actual DOM value.
+
+ This simple example shows a formatter that would convert the model value to uppercase:
+
* ```js
- * function formatter(value) {
+ * function format(value) {
* if (value) {
* return value.toUpperCase();
* }
* }
- * ngModel.$formatters.push(formatter);
+ * ngModel.$formatters.push(format);
* ```
*
* @property {Object.<string, function>} $validators A collection of validators that are applied
@@ -25140,7 +28223,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
var html = element.html();
// When we clear the content editable the browser leaves a <br> behind
// If strip-br attribute is provided then we strip this out
- if ( attrs.stripBr && html == '<br>' ) {
+ if (attrs.stripBr && html === '<br>') {
html = '';
}
ngModel.$setViewValue(html);
@@ -25162,7 +28245,7 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
</file>
<file name="protractor.js" type="protractor">
it('should data-bind and become invalid', function() {
- if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
+ if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') {
// SafariDriver can't handle contenteditable
// and Firefox driver can't clear contenteditables very well
return;
@@ -25182,8 +28265,8 @@ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
*
*
*/
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
- function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
+NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate'];
+function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
@@ -25203,40 +28286,56 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
this.$$parentForm = nullFormCtrl;
+ this.$options = defaultModelOptions;
+
+ this.$$parsedNgModel = $parse($attr.ngModel);
+ this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
+ this.$$ngModelGet = this.$$parsedNgModel;
+ this.$$ngModelSet = this.$$parsedNgModelAssign;
+ this.$$pendingDebounce = null;
+ this.$$parserValid = undefined;
+
+ this.$$currentValidationRunId = 0;
+
+ this.$$scope = $scope;
+ this.$$attr = $attr;
+ this.$$element = $element;
+ this.$$animate = $animate;
+ this.$$timeout = $timeout;
+ this.$$parse = $parse;
+ this.$$q = $q;
+ this.$$exceptionHandler = $exceptionHandler;
+
+ setupValidity(this);
+ setupModelWatcher(this);
+}
- var parsedNgModel = $parse($attr.ngModel),
- parsedNgModelAssign = parsedNgModel.assign,
- ngModelGet = parsedNgModel,
- ngModelSet = parsedNgModelAssign,
- pendingDebounce = null,
- parserValid,
- ctrl = this;
-
- this.$$setOptions = function(options) {
- ctrl.$options = options;
- if (options && options.getterSetter) {
- var invokeModelGetter = $parse($attr.ngModel + '()'),
- invokeModelSetter = $parse($attr.ngModel + '($$$p)');
-
- ngModelGet = function($scope) {
- var modelValue = parsedNgModel($scope);
+NgModelController.prototype = {
+ $$initGetterSetters: function() {
+ if (this.$options.getOption('getterSetter')) {
+ var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
+ invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
+
+ this.$$ngModelGet = function($scope) {
+ var modelValue = this.$$parsedNgModel($scope);
if (isFunction(modelValue)) {
modelValue = invokeModelGetter($scope);
}
return modelValue;
};
- ngModelSet = function($scope, newValue) {
- if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
+ this.$$ngModelSet = function($scope, newValue) {
+ if (isFunction(this.$$parsedNgModel($scope))) {
+ invokeModelSetter($scope, {$$$p: newValue});
} else {
- parsedNgModelAssign($scope, ctrl.$modelValue);
+ this.$$parsedNgModelAssign($scope, newValue);
}
};
- } else if (!parsedNgModel.assign) {
- throw ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
- $attr.ngModel, startingTag($element));
+ } else if (!this.$$parsedNgModel.assign) {
+ throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}',
+ this.$$attr.ngModel, startingTag(this.$$element));
}
- };
+ },
+
/**
* @ngdoc method
@@ -25254,11 +28353,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* the `$viewValue` are different from last time.
*
* Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
- * `$modelValue` and `$viewValue` are actually different from their previous value. If `$modelValue`
+ * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
* or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
* invoked if you only change a property on the objects.
*/
- this.$render = noop;
+ $render: noop,
/**
* @ngdoc method
@@ -25278,56 +28377,20 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @param {*} value The value of the input to check for emptiness.
* @returns {boolean} True if `value` is "empty".
*/
- this.$isEmpty = function(value) {
+ $isEmpty: function(value) {
+ // eslint-disable-next-line no-self-compare
return isUndefined(value) || value === '' || value === null || value !== value;
- };
+ },
- this.$$updateEmptyClasses = function(value) {
- if (ctrl.$isEmpty(value)) {
- $animate.removeClass($element, NOT_EMPTY_CLASS);
- $animate.addClass($element, EMPTY_CLASS);
+ $$updateEmptyClasses: function(value) {
+ if (this.$isEmpty(value)) {
+ this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, EMPTY_CLASS);
} else {
- $animate.removeClass($element, EMPTY_CLASS);
- $animate.addClass($element, NOT_EMPTY_CLASS);
+ this.$$animate.removeClass(this.$$element, EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS);
}
- };
-
-
- var currentValidationRunId = 0;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setValidity
- *
- * @description
- * Change the validity state, and notify the form.
- *
- * This method can be called within $parsers/$formatters or a custom validation implementation.
- * However, in most cases it should be sufficient to use the `ngModel.$validators` and
- * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
- *
- * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
- * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
- * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
- * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
- * Skipped is used by Angular when validators do not run because of parse errors and
- * when `$asyncValidators` do not run because any of the `$validators` failed.
- */
- addSetValidityMethod({
- ctrl: this,
- $element: $element,
- set: function(object, property) {
- object[property] = true;
- },
- unset: function(object, property) {
- delete object[property];
- },
- $animate: $animate
- });
+ },
/**
* @ngdoc method
@@ -25340,12 +28403,12 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* state (`ng-pristine` class). A model is considered to be pristine when the control
* has not been changed from when first compiled.
*/
- this.$setPristine = function() {
- ctrl.$dirty = false;
- ctrl.$pristine = true;
- $animate.removeClass($element, DIRTY_CLASS);
- $animate.addClass($element, PRISTINE_CLASS);
- };
+ $setPristine: function() {
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$$animate.removeClass(this.$$element, DIRTY_CLASS);
+ this.$$animate.addClass(this.$$element, PRISTINE_CLASS);
+ },
/**
* @ngdoc method
@@ -25358,13 +28421,13 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
* from when first compiled.
*/
- this.$setDirty = function() {
- ctrl.$dirty = true;
- ctrl.$pristine = false;
- $animate.removeClass($element, PRISTINE_CLASS);
- $animate.addClass($element, DIRTY_CLASS);
- ctrl.$$parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$$parentForm.$setDirty();
+ },
/**
* @ngdoc method
@@ -25378,11 +28441,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* by default, however this function can be used to restore that state if the model has
* already been touched by the user.
*/
- this.$setUntouched = function() {
- ctrl.$touched = false;
- ctrl.$untouched = true;
- $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
- };
+ $setUntouched: function() {
+ this.$touched = false;
+ this.$untouched = true;
+ this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS);
+ },
/**
* @ngdoc method
@@ -25395,11 +28458,11 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* touched state (`ng-touched` class). A model is considered to be touched when the user has
* first focused the control element and then shifted focus away from the control (blur event).
*/
- this.$setTouched = function() {
- ctrl.$touched = true;
- ctrl.$untouched = false;
- $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
- };
+ $setTouched: function() {
+ this.$touched = true;
+ this.$untouched = false;
+ this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS);
+ },
/**
* @ngdoc method
@@ -25407,14 +28470,17 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*
* @description
* Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
- * which may be caused by a pending debounced event or because the input is waiting for a some
+ * which may be caused by a pending debounced event or because the input is waiting for some
* future event.
*
- * If you have an input that uses `ng-model-options` to set up debounced events or events such
- * as blur you can have a situation where there is a period when the `$viewValue`
- * is out of synch with the ngModel's `$modelValue`.
+ * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
+ * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of
+ * sync with the ngModel's `$modelValue`.
+ *
+ * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
+ * and reset the input to the last committed view value.
*
- * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
+ * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue`
* programmatically before these debounced/future events have resolved/occurred, because Angular's
* dirty checking mechanism is not able to tell whether the model has actually changed or not.
*
@@ -25427,46 +28493,70 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* angular.module('cancel-update-example', [])
*
* .controller('CancelUpdateController', ['$scope', function($scope) {
- * $scope.resetWithCancel = function(e) {
- * if (e.keyCode == 27) {
- * $scope.myForm.myInput1.$rollbackViewValue();
- * $scope.myValue = '';
- * }
- * };
- * $scope.resetWithoutCancel = function(e) {
- * if (e.keyCode == 27) {
- * $scope.myValue = '';
+ * $scope.model = {value1: '', value2: ''};
+ *
+ * $scope.setEmpty = function(e, value, rollback) {
+ * if (e.keyCode === 27) {
+ * e.preventDefault();
+ * if (rollback) {
+ * $scope.myForm[value].$rollbackViewValue();
+ * }
+ * $scope.model[value] = '';
* }
* };
* }]);
* </file>
* <file name="index.html">
* <div ng-controller="CancelUpdateController">
- * <p>Try typing something in each input. See that the model only updates when you
- * blur off the input.
- * </p>
- * <p>Now see what happens if you start typing then press the Escape key</p>
+ * <p>Both of these inputs are only updated if they are blurred. Hitting escape should
+ * empty them. Follow these steps and observe the difference:</p>
+ * <ol>
+ * <li>Type something in the input. You will see that the model is not yet updated</li>
+ * <li>Press the Escape key.
+ * <ol>
+ * <li> In the first example, nothing happens, because the model is already '', and no
+ * update is detected. If you blur the input, the model will be set to the current view.
+ * </li>
+ * <li> In the second example, the pending update is cancelled, and the input is set back
+ * to the last committed view value (''). Blurring the input does nothing.
+ * </li>
+ * </ol>
+ * </li>
+ * </ol>
*
* <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
- * <p id="inputDescription1">With $rollbackViewValue()</p>
- * <input name="myInput1" aria-describedby="inputDescription1" ng-model="myValue"
- * ng-keydown="resetWithCancel($event)"><br/>
- * myValue: "{{ myValue }}"
- *
- * <p id="inputDescription2">Without $rollbackViewValue()</p>
- * <input name="myInput2" aria-describedby="inputDescription2" ng-model="myValue"
- * ng-keydown="resetWithoutCancel($event)"><br/>
- * myValue: "{{ myValue }}"
+ * <div>
+ * <p id="inputDescription1">Without $rollbackViewValue():</p>
+ * <input name="value1" aria-describedby="inputDescription1" ng-model="model.value1"
+ * ng-keydown="setEmpty($event, 'value1')">
+ * value1: "{{ model.value1 }}"
+ * </div>
+ *
+ * <div>
+ * <p id="inputDescription2">With $rollbackViewValue():</p>
+ * <input name="value2" aria-describedby="inputDescription2" ng-model="model.value2"
+ * ng-keydown="setEmpty($event, 'value2', true)">
+ * value2: "{{ model.value2 }}"
+ * </div>
* </form>
* </div>
* </file>
+ <file name="style.css">
+ div {
+ display: table-cell;
+ }
+ div:nth-child(1) {
+ padding-right: 30px;
+ }
+
+ </file>
* </example>
*/
- this.$rollbackViewValue = function() {
- $timeout.cancel(pendingDebounce);
- ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
- ctrl.$render();
- };
+ $rollbackViewValue: function() {
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ this.$viewValue = this.$$lastCommittedViewValue;
+ this.$render();
+ },
/**
* @ngdoc method
@@ -25480,45 +28570,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* If the validity changes to valid, it will set the model to the last available valid
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
*/
- this.$validate = function() {
+ $validate: function() {
// ignore $validate before model is initialized
- if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
+ if (isNumberNaN(this.$modelValue)) {
return;
}
- var viewValue = ctrl.$$lastCommittedViewValue;
+ var viewValue = this.$$lastCommittedViewValue;
// Note: we use the $$rawModelValue as $modelValue might have been
// set to undefined during a view -> model update that found validation
// errors. We can't parse the view here, since that could change
// the model although neither viewValue nor the model on the scope changed
- var modelValue = ctrl.$$rawModelValue;
+ var modelValue = this.$$rawModelValue;
- var prevValid = ctrl.$valid;
- var prevModelValue = ctrl.$modelValue;
+ var prevValid = this.$valid;
+ var prevModelValue = this.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+ var allowInvalid = this.$options.getOption('allowInvalid');
- ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
+ var that = this;
+ this.$$runValidators(modelValue, viewValue, function(allValid) {
// If there was no change in validity, don't update the model
// This prevents changing an invalid modelValue to undefined
if (!allowInvalid && prevValid !== allValid) {
- // Note: Don't check ctrl.$valid here, as we could have
+ // Note: Don't check this.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
+ that.$modelValue = allValid ? modelValue : undefined;
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
}
}
});
+ },
- };
-
- this.$$runValidators = function(modelValue, viewValue, doneCallback) {
- currentValidationRunId++;
- var localValidationRunId = currentValidationRunId;
+ $$runValidators: function(modelValue, viewValue, doneCallback) {
+ this.$$currentValidationRunId++;
+ var localValidationRunId = this.$$currentValidationRunId;
+ var that = this;
// check parser error
if (!processParseErrors()) {
@@ -25532,34 +28623,34 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
processAsyncValidators();
function processParseErrors() {
- var errorKey = ctrl.$$parserName || 'parse';
- if (isUndefined(parserValid)) {
+ var errorKey = that.$$parserName || 'parse';
+ if (isUndefined(that.$$parserValid)) {
setValidity(errorKey, null);
} else {
- if (!parserValid) {
- forEach(ctrl.$validators, function(v, name) {
+ if (!that.$$parserValid) {
+ forEach(that.$validators, function(v, name) {
setValidity(name, null);
});
- forEach(ctrl.$asyncValidators, function(v, name) {
+ forEach(that.$asyncValidators, function(v, name) {
setValidity(name, null);
});
}
// Set the parse error last, to prevent unsetting it, should a $validators key == parserName
- setValidity(errorKey, parserValid);
- return parserValid;
+ setValidity(errorKey, that.$$parserValid);
+ return that.$$parserValid;
}
return true;
}
function processSyncValidators() {
var syncValidatorsValid = true;
- forEach(ctrl.$validators, function(validator, name) {
- var result = validator(modelValue, viewValue);
+ forEach(that.$validators, function(validator, name) {
+ var result = Boolean(validator(modelValue, viewValue));
syncValidatorsValid = syncValidatorsValid && result;
setValidity(name, result);
});
if (!syncValidatorsValid) {
- forEach(ctrl.$asyncValidators, function(v, name) {
+ forEach(that.$asyncValidators, function(v, name) {
setValidity(name, null);
});
return false;
@@ -25570,16 +28661,16 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
function processAsyncValidators() {
var validatorPromises = [];
var allValid = true;
- forEach(ctrl.$asyncValidators, function(validator, name) {
+ forEach(that.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
- throw ngModelMinErr("$asyncValidators",
- "Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
+ throw ngModelMinErr('nopromise',
+ 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise);
}
setValidity(name, undefined);
validatorPromises.push(promise.then(function() {
setValidity(name, true);
- }, function(error) {
+ }, function() {
allValid = false;
setValidity(name, false);
}));
@@ -25587,25 +28678,25 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
if (!validatorPromises.length) {
validationDone(true);
} else {
- $q.all(validatorPromises).then(function() {
+ that.$$q.all(validatorPromises).then(function() {
validationDone(allValid);
}, noop);
}
}
function setValidity(name, isValid) {
- if (localValidationRunId === currentValidationRunId) {
- ctrl.$setValidity(name, isValid);
+ if (localValidationRunId === that.$$currentValidationRunId) {
+ that.$setValidity(name, isValid);
}
}
function validationDone(allValid) {
- if (localValidationRunId === currentValidationRunId) {
+ if (localValidationRunId === that.$$currentValidationRunId) {
doneCallback(allValid);
}
}
- };
+ },
/**
* @ngdoc method
@@ -25618,84 +28709,87 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
- this.$commitViewValue = function() {
- var viewValue = ctrl.$viewValue;
+ $commitViewValue: function() {
+ var viewValue = this.$viewValue;
- $timeout.cancel(pendingDebounce);
+ this.$$timeout.cancel(this.$$pendingDebounce);
// If the view value has not changed then we should just exit, except in the case where there is
// a native validator on the element. In this case the validation state may have changed even though
// the viewValue has stayed empty.
- if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
+ if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) {
return;
}
- ctrl.$$updateEmptyClasses(viewValue);
- ctrl.$$lastCommittedViewValue = viewValue;
+ this.$$updateEmptyClasses(viewValue);
+ this.$$lastCommittedViewValue = viewValue;
// change to dirty
- if (ctrl.$pristine) {
+ if (this.$pristine) {
this.$setDirty();
}
this.$$parseAndValidate();
- };
+ },
- this.$$parseAndValidate = function() {
- var viewValue = ctrl.$$lastCommittedViewValue;
+ $$parseAndValidate: function() {
+ var viewValue = this.$$lastCommittedViewValue;
var modelValue = viewValue;
- parserValid = isUndefined(modelValue) ? undefined : true;
+ var that = this;
- if (parserValid) {
- for (var i = 0; i < ctrl.$parsers.length; i++) {
- modelValue = ctrl.$parsers[i](modelValue);
+ this.$$parserValid = isUndefined(modelValue) ? undefined : true;
+
+ if (this.$$parserValid) {
+ for (var i = 0; i < this.$parsers.length; i++) {
+ modelValue = this.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
- parserValid = false;
+ this.$$parserValid = false;
break;
}
}
}
- if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
- // ctrl.$modelValue has not been touched yet...
- ctrl.$modelValue = ngModelGet($scope);
+ if (isNumberNaN(this.$modelValue)) {
+ // this.$modelValue has not been touched yet...
+ this.$modelValue = this.$$ngModelGet(this.$$scope);
}
- var prevModelValue = ctrl.$modelValue;
- var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
- ctrl.$$rawModelValue = modelValue;
+ var prevModelValue = this.$modelValue;
+ var allowInvalid = this.$options.getOption('allowInvalid');
+ this.$$rawModelValue = modelValue;
if (allowInvalid) {
- ctrl.$modelValue = modelValue;
+ this.$modelValue = modelValue;
writeToModelIfNeeded();
}
// Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
// This can happen if e.g. $setViewValue is called from inside a parser
- ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
+ this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) {
if (!allowInvalid) {
- // Note: Don't check ctrl.$valid here, as we could have
+ // Note: Don't check this.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
- ctrl.$modelValue = allValid ? modelValue : undefined;
+ that.$modelValue = allValid ? modelValue : undefined;
writeToModelIfNeeded();
}
});
function writeToModelIfNeeded() {
- if (ctrl.$modelValue !== prevModelValue) {
- ctrl.$$writeModelToScope();
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
}
}
- };
+ },
- this.$$writeModelToScope = function() {
- ngModelSet($scope, ctrl.$modelValue);
- forEach(ctrl.$viewChangeListeners, function(listener) {
+ $$writeModelToScope: function() {
+ this.$$ngModelSet(this.$$scope, this.$modelValue);
+ forEach(this.$viewChangeListeners, function(listener) {
try {
listener();
} catch (e) {
- $exceptionHandler(e);
+ // eslint-disable-next-line no-invalid-this
+ this.$$exceptionHandler(e);
}
- });
- };
+ }, this);
+ },
/**
* @ngdoc method
@@ -25711,9 +28805,10 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
*
* When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
* and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
- * value sent directly for processing, finally to be applied to `$modelValue` and then the
- * **expression** specified in the `ng-model` attribute. Lastly, all the registered change listeners,
- * in the `$viewChangeListeners` list, are called.
+ * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and
+ * `$asyncValidators` are called and the value is applied to `$modelValue`.
+ * Finally, the value is set to the **expression** specified in the `ng-model` attribute and
+ * all the registered change listeners, in the `$viewChangeListeners` list are called.
*
* In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
* and the `default` trigger is not listed, all those actions will remain pending until one of the
@@ -25728,7 +28823,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* However, custom controls might also pass objects to this method. In this case, we should make
* a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
* perform a deep watch of objects, it only looks for a change of identity. If you only change
- * the property of the object then ngModel will not realise that the object has changed and
+ * the property of the object then ngModel will not realize that the object has changed and
* will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
* not change properties of the copy once it has been passed to `$setViewValue`.
* Otherwise you may cause the model value on the scope to change incorrectly.
@@ -25747,43 +28842,62 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* @param {*} value value from the view.
* @param {string} trigger Event that triggered the update.
*/
- this.$setViewValue = function(value, trigger) {
- ctrl.$viewValue = value;
- if (!ctrl.$options || ctrl.$options.updateOnDefault) {
- ctrl.$$debounceViewValueCommit(trigger);
+ $setViewValue: function(value, trigger) {
+ this.$viewValue = value;
+ if (this.$options.getOption('updateOnDefault')) {
+ this.$$debounceViewValueCommit(trigger);
}
- };
+ },
- this.$$debounceViewValueCommit = function(trigger) {
- var debounceDelay = 0,
- options = ctrl.$options,
- debounce;
+ $$debounceViewValueCommit: function(trigger) {
+ var debounceDelay = this.$options.getOption('debounce');
- if (options && isDefined(options.debounce)) {
- debounce = options.debounce;
- if (isNumber(debounce)) {
- debounceDelay = debounce;
- } else if (isNumber(debounce[trigger])) {
- debounceDelay = debounce[trigger];
- } else if (isNumber(debounce['default'])) {
- debounceDelay = debounce['default'];
- }
+ if (isNumber(debounceDelay[trigger])) {
+ debounceDelay = debounceDelay[trigger];
+ } else if (isNumber(debounceDelay['default'])) {
+ debounceDelay = debounceDelay['default'];
}
- $timeout.cancel(pendingDebounce);
- if (debounceDelay) {
- pendingDebounce = $timeout(function() {
- ctrl.$commitViewValue();
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ var that = this;
+ if (debounceDelay > 0) { // this fails if debounceDelay is an object
+ this.$$pendingDebounce = this.$$timeout(function() {
+ that.$commitViewValue();
}, debounceDelay);
- } else if ($rootScope.$$phase) {
- ctrl.$commitViewValue();
+ } else if (this.$$scope.$root.$$phase) {
+ this.$commitViewValue();
} else {
- $scope.$apply(function() {
- ctrl.$commitViewValue();
+ this.$$scope.$apply(function() {
+ that.$commitViewValue();
});
}
- };
+ },
+ /**
+ * @ngdoc method
+ *
+ * @name ngModel.NgModelController#$overrideModelOptions
+ *
+ * @description
+ *
+ * Override the current model options settings programmatically.
+ *
+ * The previous `ModelOptions` value will not be modified. Instead, a
+ * new `ModelOptions` object will inherit from the previous one overriding
+ * or inheriting settings that are defined in the given parameter.
+ *
+ * See {@link ngModelOptions} for information about what options can be specified
+ * and how model option inheritance works.
+ *
+ * @param {Object} options a hash of settings to override the previous options
+ *
+ */
+ $overrideModelOptions: function(options) {
+ this.$options = this.$options.createChild(options);
+ }
+};
+
+function setupModelWatcher(ctrl) {
// model -> value
// Note: we cannot use a normal scope.$watch as we want to detect the following:
// 1. scope value is 'a'
@@ -25792,17 +28906,18 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
// -> scope value did not change since the last digest as
// ng-change executes in apply phase
// 4. view should be changed back to 'a'
- $scope.$watch(function ngModelWatch() {
- var modelValue = ngModelGet($scope);
+ ctrl.$$scope.$watch(function ngModelWatch() {
+ var modelValue = ctrl.$$ngModelGet(ctrl.$$scope);
// if scope model value and ngModel value are out of sync
// TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue &&
// checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
+ // eslint-disable-next-line no-self-compare
(ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
) {
ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
- parserValid = undefined;
+ ctrl.$$parserValid = undefined;
var formatters = ctrl.$formatters,
idx = formatters.length;
@@ -25816,13 +28931,46 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
- ctrl.$$runValidators(modelValue, viewValue, noop);
+ // It is possible that model and view value have been updated during render
+ ctrl.$$runValidators(ctrl.$modelValue, ctrl.$viewValue, noop);
}
}
return modelValue;
});
-}];
+}
+
+/**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setValidity
+ *
+ * @description
+ * Change the validity state, and notify the form.
+ *
+ * This method can be called within $parsers/$formatters or a custom validation implementation.
+ * However, in most cases it should be sufficient to use the `ngModel.$validators` and
+ * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
+ * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
+ * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
+ * The `validationErrorKey` should be in camelCase and will get converted into dash-case
+ * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
+ * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
+ * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by Angular when validators do not run because of parse errors and
+ * when `$asyncValidators` do not run because any of the `$validators` failed.
+ */
+addSetValidityMethod({
+ clazz: NgModelController,
+ set: function(object, property) {
+ object[property] = true;
+ },
+ unset: function(object, property) {
+ delete object[property];
+ }
+});
/**
@@ -25872,6 +29020,22 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* - {@link ng.directive:select select}
* - {@link ng.directive:textarea textarea}
*
+ * # Complex Models (objects or collections)
+ *
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the
+ * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered.
+ *
+ * The model must be assigned an entirely new object or collection before a re-rendering will occur.
+ *
+ * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression
+ * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or
+ * if the select is given the `multiple` attribute.
+ *
+ * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the
+ * first level of the object (or only changing the properties of an item in the collection if it's an array) will still
+ * not trigger a re-rendering of the model.
+ *
* # CSS classes
* The following CSS classes are added and removed on the associated input/select/textarea element
* depending on the validity of the model.
@@ -25916,7 +29080,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$
* </pre>
*
* @example
- * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
+ * <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample" name="ng-model">
<file name="index.html">
<script>
angular.module('inputExample', [])
@@ -26012,9 +29176,14 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
return {
pre: function ngModelPreLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || modelCtrl.$$parentForm;
+ formCtrl = ctrls[1] || modelCtrl.$$parentForm,
+ optionsCtrl = ctrls[2];
+
+ if (optionsCtrl) {
+ modelCtrl.$options = optionsCtrl.$options;
+ }
- modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
+ modelCtrl.$$initGetterSetters();
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
@@ -26031,19 +29200,23 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
- if (modelCtrl.$options && modelCtrl.$options.updateOn) {
- element.on(modelCtrl.$options.updateOn, function(ev) {
+ if (modelCtrl.$options.getOption('updateOn')) {
+ element.on(modelCtrl.$options.getOption('updateOn'), function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
- element.on('blur', function(ev) {
+ function setTouched() {
+ modelCtrl.$setTouched();
+ }
+
+ element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
- scope.$evalAsync(modelCtrl.$setTouched);
+ scope.$evalAsync(setTouched);
} else {
- scope.$apply(modelCtrl.$setTouched);
+ scope.$apply(setTouched);
}
});
}
@@ -26052,23 +29225,173 @@ var ngModelDirective = ['$rootScope', function($rootScope) {
};
}];
+/* exported defaultModelOptions */
+var defaultModelOptions;
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
/**
+ * @ngdoc type
+ * @name ModelOptions
+ * @description
+ * A container for the options set by the {@link ngModelOptions} directive
+ */
+function ModelOptions(options) {
+ this.$$options = options;
+}
+
+ModelOptions.prototype = {
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#getOption
+ * @param {string} name the name of the option to retrieve
+ * @returns {*} the value of the option
+ * @description
+ * Returns the value of the given option
+ */
+ getOption: function(name) {
+ return this.$$options[name];
+ },
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#createChild
+ * @param {Object} options a hash of options for the new child that will override the parent's options
+ * @return {ModelOptions} a new `ModelOptions` object initialized with the given options.
+ */
+ createChild: function(options) {
+ var inheritAll = false;
+
+ // make a shallow copy
+ options = extend({}, options);
+
+ // Inherit options from the parent if specified by the value `"$inherit"`
+ forEach(options, /* @this */ function(option, key) {
+ if (option === '$inherit') {
+ if (key === '*') {
+ inheritAll = true;
+ } else {
+ options[key] = this.$$options[key];
+ // `updateOn` is special so we must also inherit the `updateOnDefault` option
+ if (key === 'updateOn') {
+ options.updateOnDefault = this.$$options.updateOnDefault;
+ }
+ }
+ } else {
+ if (key === 'updateOn') {
+ // If the `updateOn` property contains the `default` event then we have to remove
+ // it from the event list and set the `updateOnDefault` flag.
+ options.updateOnDefault = false;
+ options[key] = trim(option.replace(DEFAULT_REGEXP, function() {
+ options.updateOnDefault = true;
+ return ' ';
+ }));
+ }
+ }
+ }, this);
+
+ if (inheritAll) {
+ // We have a property of the form: `"*": "$inherit"`
+ delete options['*'];
+ defaults(options, this.$$options);
+ }
+
+ // Finally add in any missing defaults
+ defaults(options, defaultModelOptions.$$options);
+
+ return new ModelOptions(options);
+ }
+};
+
+
+defaultModelOptions = new ModelOptions({
+ updateOn: '',
+ updateOnDefault: true,
+ debounce: 0,
+ getterSetter: false,
+ allowInvalid: false,
+ timezone: null
+});
+
+
+/**
* @ngdoc directive
* @name ngModelOptions
*
* @description
- * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
- * events that will trigger a model update and/or a debouncing delay so that the actual update only
- * takes place when a timer expires; this timer will be reset after another change takes place.
+ * This directive allows you to modify the behaviour of {@link ngModel} directives within your
+ * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel}
+ * directives will use the options of their nearest `ngModelOptions` ancestor.
+ *
+ * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as
+ * an Angular expression. This expression should evaluate to an object, whose properties contain
+ * the settings. For example: `<div "ng-model-options"="{ debounce: 100 }"`.
+ *
+ * ## Inheriting Options
+ *
+ * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions`
+ * directive by giving it the value of `"$inherit"`.
+ * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the
+ * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings
+ * will be used.
+ *
+ * For example given the following fragment of HTML
+ *
+ *
+ * ```html
+ * <div ng-model-options="{ allowInvalid: true, debounce: 200 }">
+ * <form ng-model-options="{ updateOn: 'blur', allowInvalid: '$inherit' }">
+ * <input ng-model-options="{ updateOn: 'default', allowInvalid: '$inherit' }" />
+ * </form>
+ * </div>
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 0 }
+ * ```
+ *
+ * Notice that the `debounce` setting was not inherited and used the default value instead.
+ *
+ * You can specify that all undefined settings are automatically inherited from an ancestor by
+ * including a property with key of `"*"` and value of `"$inherit"`.
+ *
+ * For example given the following fragment of HTML
+ *
+ *
+ * ```html
+ * <div ng-model-options="{ allowInvalid: true, debounce: 200 }">
+ * <form ng-model-options="{ updateOn: 'blur', "*": '$inherit' }">
+ * <input ng-model-options="{ updateOn: 'default', "*": '$inherit' }" />
+ * </form>
+ * </div>
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 200 }
+ * ```
+ *
+ * Notice that the `debounce` setting now inherits the value from the outer `<div>` element.
+ *
+ * If you are creating a reusable component then you should be careful when using `"*": "$inherit"`
+ * since you may inadvertently inherit a setting in the future that changes the behavior of your component.
+ *
+ *
+ * ## Triggering and debouncing model updates
+ *
+ * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
+ * trigger a model update and/or a debouncing delay so that the actual update only takes place when
+ * a timer expires; this timer will be reset after another change takes place.
*
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
* be different from the value in the actual model. This means that if you update the model you
- * should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
+ * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in
* order to make sure it is synchronized with the model and that any debounced action is canceled.
*
- * The easiest way to reference the control's {@link ngModel.NgModelController `$rollbackViewValue`}
+ * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue}
* method is by making sure the input is placed inside a form that has a `name` attribute. This is
* important because `form` controllers are published to the related scope under the name in their
* `name` attribute.
@@ -26077,271 +29400,194 @@ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
* `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
* to have access to the updated model.
*
- * `ngModelOptions` has an effect on the element it's declared on and its descendants.
+ * The following example shows how to override immediate updates. Changes on the inputs within the
+ * form will update the model only when the control loses focus (blur event). If `escape` key is
+ * pressed while the input field is focused, the value is reset to the value in the current model.
+ *
+ * <example name="ngModelOptions-directive-blur" module="optionsExample">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="userForm">
+ * <label>
+ * Name:
+ * <input type="text" name="userName"
+ * ng-model="user.name"
+ * ng-model-options="{ updateOn: 'blur' }"
+ * ng-keyup="cancel($event)" />
+ * </label><br />
+ * <label>
+ * Other data:
+ * <input type="text" ng-model="user.data" />
+ * </label><br />
+ * </form>
+ * <pre>user.name = <span ng-bind="user.name"></span></pre>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say', data: '' };
+ *
+ * $scope.cancel = function(e) {
+ * if (e.keyCode === 27) {
+ * $scope.userForm.userName.$rollbackViewValue();
+ * }
+ * };
+ * }]);
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * var model = element(by.binding('user.name'));
+ * var input = element(by.model('user.name'));
+ * var other = element(by.model('user.data'));
+ *
+ * it('should allow custom events', function() {
+ * input.sendKeys(' hello');
+ * input.click();
+ * expect(model.getText()).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say hello');
+ * });
*
- * @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
+ * it('should $rollbackViewValue when model changes', function() {
+ * input.sendKeys(' hello');
+ * expect(input.getAttribute('value')).toEqual('say hello');
+ * input.sendKeys(protractor.Key.ESCAPE);
+ * expect(input.getAttribute('value')).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say');
+ * });
+ * </file>
+ * </example>
+ *
+ * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
+ * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
+ *
+ * <example name="ngModelOptions-directive-debounce" module="optionsExample">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="userForm">
+ * Name:
+ * <input type="text" name="userName"
+ * ng-model="user.name"
+ * ng-model-options="{ debounce: 1000 }" />
+ * <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
+ * </form>
+ * <pre>user.name = <span ng-bind="user.name"></span></pre>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say' };
+ * }]);
+ * </file>
+ * </example>
+ *
+ * ## Model updates and validation
+ *
+ * The default behaviour in `ngModel` is that the model value is set to `undefined` when the
+ * validation determines that the value is invalid. By setting the `allowInvalid` property to true,
+ * the model will still be updated even if the value is invalid.
+ *
+ *
+ * ## Connecting to the scope
+ *
+ * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
+ * on the scope refers to a "getter/setter" function rather than the value itself.
+ *
+ * The following example shows how to bind to getter/setters:
+ *
+ * <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="userForm">
+ * <label>
+ * Name:
+ * <input type="text" name="userName"
+ * ng-model="user.name"
+ * ng-model-options="{ getterSetter: true }" />
+ * </label>
+ * </form>
+ * <pre>user.name = <span ng-bind="user.name()"></span></pre>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('getterSetterExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * var _name = 'Brian';
+ * $scope.user = {
+ * name: function(newName) {
+ * return angular.isDefined(newName) ? (_name = newName) : _name;
+ * }
+ * };
+ * }]);
+ * </file>
+ * </example>
+ *
+ *
+ * ## Specifying timezones
+ *
+ * You can specify the timezone that date/time input directives expect by providing its name in the
+ * `timezone` property.
+ *
+ * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
+ * and its descendents. Valid keys are:
* - `updateOn`: string specifying which event should the input be bound to. You can set several
* events using an space delimited list. There is a special event called `default` that
- * matches the default events belonging of the control.
+ * matches the default events belonging to the control.
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
* custom value for each event. For example:
- * `ng-model-options="{ updateOn: 'default blur', debounce: { 'default': 500, 'blur': 0 } }"`
+ * ```
+ * ng-model-options="{
+ * updateOn: 'default blur',
+ * debounce: { 'default': 500, 'blur': 0 }
+ * }"
+ * ```
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
- `ngModel` as getters/setters.
+ * `ngModel` as getters/setters.
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
- * `<input type="date">`, `<input type="time">`, ... . It understands UTC/GMT and the
+ * `<input type="date" />`, `<input type="time" />`, ... . It understands UTC/GMT and the
* continental US time zone abbreviations, but for general use, use a time zone offset, for
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
* If not specified, the timezone of the browser will be used.
*
- * @example
-
- The following example shows how to override immediate updates. Changes on the inputs within the
- form will update the model only when the control loses focus (blur event). If `escape` key is
- pressed while the input field is focused, the value is reset to the value in the current model.
-
- <example name="ngModelOptions-directive-blur" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ updateOn: 'blur' }"
- ng-keyup="cancel($event)" />
- </label><br />
- <label>Other data:
- <input type="text" ng-model="user.data" />
- </label><br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- <pre>user.data = <span ng-bind="user.data"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'John', data: '' };
-
- $scope.cancel = function(e) {
- if (e.keyCode == 27) {
- $scope.userForm.userName.$rollbackViewValue();
- }
- };
- }]);
- </file>
- <file name="protractor.js" type="protractor">
- var model = element(by.binding('user.name'));
- var input = element(by.model('user.name'));
- var other = element(by.model('user.data'));
-
- it('should allow custom events', function() {
- input.sendKeys(' Doe');
- input.click();
- expect(model.getText()).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John Doe');
- });
-
- it('should $rollbackViewValue when model changes', function() {
- input.sendKeys(' Doe');
- expect(input.getAttribute('value')).toEqual('John Doe');
- input.sendKeys(protractor.Key.ESCAPE);
- expect(input.getAttribute('value')).toEqual('John');
- other.click();
- expect(model.getText()).toEqual('John');
- });
- </file>
- </example>
-
- This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
- If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
-
- <example name="ngModelOptions-directive-debounce" module="optionsExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ debounce: 1000 }" />
- </label>
- <button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button>
- <br />
- </form>
- <pre>user.name = <span ng-bind="user.name"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('optionsExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.user = { name: 'Igor' };
- }]);
- </file>
- </example>
-
- This one shows how to bind to getter/setters:
-
- <example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <form name="userForm">
- <label>Name:
- <input type="text" name="userName"
- ng-model="user.name"
- ng-model-options="{ getterSetter: true }" />
- </label>
- </form>
- <pre>user.name = <span ng-bind="user.name()"></span></pre>
- </div>
- </file>
- <file name="app.js">
- angular.module('getterSetterExample', [])
- .controller('ExampleController', ['$scope', function($scope) {
- var _name = 'Brian';
- $scope.user = {
- name: function(newName) {
- // Note that newName can be undefined for two reasons:
- // 1. Because it is called as a getter and thus called with no arguments
- // 2. Because the property should actually be set to undefined. This happens e.g. if the
- // input is invalid
- return arguments.length ? (_name = newName) : _name;
- }
- };
- }]);
- </file>
- </example>
*/
var ngModelOptionsDirective = function() {
- return {
- restrict: 'A',
- controller: ['$scope', '$attrs', function($scope, $attrs) {
- var that = this;
- this.$options = copy($scope.$eval($attrs.ngModelOptions));
- // Allow adding/overriding bound events
- if (isDefined(this.$options.updateOn)) {
- this.$options.updateOnDefault = false;
- // extract "default" pseudo-event from list of events that can trigger a model update
- this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
- that.$options.updateOnDefault = true;
- return ' ';
- }));
- } else {
- this.$options.updateOnDefault = true;
- }
- }]
- };
-};
-
-
-
-// helper methods
-function addSetValidityMethod(context) {
- var ctrl = context.ctrl,
- $element = context.$element,
- classCache = {},
- set = context.set,
- unset = context.unset,
- $animate = context.$animate;
-
- classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
-
- ctrl.$setValidity = setValidity;
-
- function setValidity(validationErrorKey, state, controller) {
- if (isUndefined(state)) {
- createAndSet('$pending', validationErrorKey, controller);
- } else {
- unsetAndCleanup('$pending', validationErrorKey, controller);
- }
- if (!isBoolean(state)) {
- unset(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
- } else {
- if (state) {
- unset(ctrl.$error, validationErrorKey, controller);
- set(ctrl.$$success, validationErrorKey, controller);
- } else {
- set(ctrl.$error, validationErrorKey, controller);
- unset(ctrl.$$success, validationErrorKey, controller);
- }
- }
- if (ctrl.$pending) {
- cachedToggleClass(PENDING_CLASS, true);
- ctrl.$valid = ctrl.$invalid = undefined;
- toggleValidationCss('', null);
- } else {
- cachedToggleClass(PENDING_CLASS, false);
- ctrl.$valid = isObjectEmpty(ctrl.$error);
- ctrl.$invalid = !ctrl.$valid;
- toggleValidationCss('', ctrl.$valid);
- }
-
- // re-read the state as the set/unset methods could have
- // combined state in ctrl.$error[validationError] (used for forms),
- // where setting/unsetting only increments/decrements the value,
- // and does not replace it.
- var combinedState;
- if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
- combinedState = undefined;
- } else if (ctrl.$error[validationErrorKey]) {
- combinedState = false;
- } else if (ctrl.$$success[validationErrorKey]) {
- combinedState = true;
- } else {
- combinedState = null;
- }
-
- toggleValidationCss(validationErrorKey, combinedState);
- ctrl.$$parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
+ NgModelOptionsController.$inject = ['$attrs', '$scope'];
+ function NgModelOptionsController($attrs, $scope) {
+ this.$$attrs = $attrs;
+ this.$$scope = $scope;
}
+ NgModelOptionsController.prototype = {
+ $onInit: function() {
+ var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions;
+ var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions);
- function createAndSet(name, value, controller) {
- if (!ctrl[name]) {
- ctrl[name] = {};
+ this.$options = parentOptions.createChild(modelOptionsDefinition);
}
- set(ctrl[name], value, controller);
- }
-
- function unsetAndCleanup(name, value, controller) {
- if (ctrl[name]) {
- unset(ctrl[name], value, controller);
- }
- if (isObjectEmpty(ctrl[name])) {
- ctrl[name] = undefined;
- }
- }
-
- function cachedToggleClass(className, switchValue) {
- if (switchValue && !classCache[className]) {
- $animate.addClass($element, className);
- classCache[className] = true;
- } else if (!switchValue && classCache[className]) {
- $animate.removeClass($element, className);
- classCache[className] = false;
- }
- }
+ };
- function toggleValidationCss(validationErrorKey, isValid) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
+ return {
+ restrict: 'A',
+ // ngModelOptions needs to run before ngModel and input directives
+ priority: 10,
+ require: {parentCtrl: '?^^ngModelOptions'},
+ bindToController: true,
+ controller: NgModelOptionsController
+ };
+};
- cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
- cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
- }
-}
-function isObjectEmpty(obj) {
- if (obj) {
- for (var prop in obj) {
- if (obj.hasOwnProperty(prop)) {
- return false;
- }
+// shallow copy over values from `src` that are not already specified on `dst`
+function defaults(dst, src) {
+ forEach(src, function(value, key) {
+ if (!isDefined(dst[key])) {
+ dst[key] = value;
}
- }
- return true;
+ });
}
/**
@@ -26363,7 +29609,7 @@ function isObjectEmpty(obj) {
* but the one wrapped in `ngNonBindable` is left alone.
*
* @example
- <example>
+ <example name="ng-non-bindable">
<file name="index.html">
<div>Normal: {{1 + 2}}</div>
<div ng-non-bindable>Ignored: {{1 + 2}}</div>
@@ -26378,6 +29624,8 @@ function isObjectEmpty(obj) {
*/
var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+/* exported ngOptionsDirective */
+
/* global jqLiteRemove */
var ngOptionsMinErr = minErr('ngOptions');
@@ -26393,13 +29641,12 @@ var ngOptionsMinErr = minErr('ngOptions');
* elements for the `<select>` element using the array or object obtained by evaluating the
* `ngOptions` comprehension expression.
*
- * In many cases, `ngRepeat` can be used on `<option>` elements instead of `ngOptions` to achieve a
- * similar result. However, `ngOptions` provides some benefits such as reducing memory and
- * increasing speed by not creating a new scope for each repeated instance, as well as providing
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression. `ngOptions` should be used when the `<select>` model needs to be bound
- * to a non-string value. This is because an option element can only be bound to string values at
- * present.
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `<option>` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
* When an item in the `<select>` menu is selected, the array element or object property
* represented by the selected option will be bound to the model identified by the `ngModel`
@@ -26488,13 +29735,8 @@ var ngOptionsMinErr = minErr('ngOptions');
* is not matched against any `<option>` and the `<select>` appears as having no selected value.
*
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
- * @param {string=} name Property name of the form under which the control is published.
- * @param {string=} required The control is considered valid only if value is entered.
- * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
- * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
- * `required` when you want to data-bind to the `required` attribute.
- * @param {comprehension_expression=} ngOptions in one of the following forms:
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {comprehension_expression} ngOptions in one of the following forms:
*
* * for array data sources:
* * `label` **`for`** `value` **`in`** `array`
@@ -26533,9 +29775,16 @@ var ngOptionsMinErr = minErr('ngOptions');
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
* even when the options are recreated (e.g. reloaded from the server).
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} required The control is considered valid only if value is entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
* @example
- <example module="selectExample">
+ <example module="selectExample" name="select">
<file name="index.html">
<script>
angular.module('selectExample', [])
@@ -26608,9 +29857,9 @@ var ngOptionsMinErr = minErr('ngOptions');
</example>
*/
-// jshint maxlen: false
-// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555550000000006666666666666660000000777777777777777000000000000000888888888800000000000000000009999999999
-var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+/* eslint-disable max-len */
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
// 1: value expression (valueFn)
// 2: label expression (displayFn)
// 3: group by expression (groupByFn)
@@ -26620,19 +29869,19 @@ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s
// 7: object item value variable name
// 8: collection expression
// 9: track by expression
-// jshint maxlen: 100
+/* eslint-enable */
-var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
+var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
function parseOptionsExpression(optionsExp, selectElement, scope) {
var match = optionsExp.match(NG_OPTIONS_REGEXP);
if (!(match)) {
throw ngOptionsMinErr('iexp',
- "Expected expression in form of " +
- "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '{0}'. Element: {1}",
+ 'Expected expression in form of ' +
+ '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' +
+ ' but got \'{0}\'. Element: {1}',
optionsExp, startingTag(selectElement));
}
@@ -26720,8 +29969,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
var value = optionValues[key];
- var locals = getLocals(optionValues[key], key);
- var selectValue = getTrackByValueFn(optionValues[key], locals);
+ var locals = getLocals(value, key);
+ var selectValue = getTrackByValueFn(value, locals);
watchedArray.push(selectValue);
// Only need to watch the displayFn if there is a specific label expression
@@ -26774,7 +30023,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
getViewValueFromOption: function(option) {
// If the viewValue could be an object that may be mutated by the application,
// we need to make a copy and not return the reference to the value on the option.
- return trackBy ? angular.copy(option.viewValue) : option.viewValue;
+ return trackBy ? copy(option.viewValue) : option.viewValue;
}
};
}
@@ -26784,8 +30033,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// we can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
- var optionTemplate = document.createElement('option'),
- optGroupTemplate = document.createElement('optgroup');
+ var optionTemplate = window.document.createElement('option'),
+ optGroupTemplate = window.document.createElement('optgroup');
function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
@@ -26795,72 +30044,65 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// The emptyOption allows the application developer to provide their own custom "empty"
// option when the viewValue does not match any of the option values.
- var emptyOption;
for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
if (children[i].value === '') {
- emptyOption = children.eq(i);
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = children.eq(i);
break;
}
}
- var providedEmptyOption = !!emptyOption;
+ var providedEmptyOption = !!selectCtrl.emptyOption;
var unknownOption = jqLite(optionTemplate.cloneNode(false));
unknownOption.val('?');
var options;
var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
-
-
- var renderEmptyOption = function() {
- if (!providedEmptyOption) {
- selectElement.prepend(emptyOption);
- }
- selectElement.val('');
- emptyOption.prop('selected', true); // needed for IE
- emptyOption.attr('selected', true);
- };
-
- var removeEmptyOption = function() {
- if (!providedEmptyOption) {
- emptyOption.remove();
- }
- };
-
-
- var renderUnknownOption = function() {
- selectElement.prepend(unknownOption);
- selectElement.val('?');
- unknownOption.prop('selected', true); // needed for IE
- unknownOption.attr('selected', true);
- };
-
- var removeUnknownOption = function() {
- unknownOption.remove();
+ // This stores the newly created options before they are appended to the select.
+ // Since the contents are removed from the fragment when it is appended,
+ // we only need to create it once.
+ var listFragment = $document[0].createDocumentFragment();
+
+ // Overwrite the implementation. ngOptions doesn't use hashes
+ selectCtrl.generateUnknownOptionValue = function(val) {
+ return '?';
};
// Update the controller methods for multiple selectable options
if (!multiple) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ var selectedOption = options.selectValueMap[selectElement.val()];
var option = options.getOptionFromViewValue(value);
- if (option && !option.disabled) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ if (selectedOption) selectedOption.element.removeAttribute('selected');
+
+ if (option) {
+ // Don't update the option when it is already selected.
+ // For example, the browser will select the first option by default. In that case,
+ // most properties are set automatically - except the `selected` attribute, which we
+ // set always
+
if (selectElement[0].value !== option.selectValue) {
- removeUnknownOption();
- removeEmptyOption();
+ selectCtrl.removeUnknownOption();
+ selectCtrl.unselectEmptyOption();
selectElement[0].value = option.selectValue;
option.element.selected = true;
- option.element.setAttribute('selected', 'selected');
}
+
+ option.element.setAttribute('selected', 'selected');
} else {
- if (value === null || providedEmptyOption) {
- removeUnknownOption();
- renderEmptyOption();
+
+ if (providedEmptyOption) {
+ selectCtrl.selectEmptyOption();
+ } else if (selectCtrl.unknownOption.parent().length) {
+ selectCtrl.updateUnknownOption(value);
} else {
- removeEmptyOption();
- renderUnknownOption();
+ selectCtrl.renderUnknownOption(value);
}
}
};
@@ -26870,8 +30112,8 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
var selectedOption = options.selectValueMap[selectElement.val()];
if (selectedOption && !selectedOption.disabled) {
- removeEmptyOption();
- removeUnknownOption();
+ selectCtrl.unselectEmptyOption();
+ selectCtrl.removeUnknownOption();
return options.getViewValueFromOption(selectedOption);
}
return null;
@@ -26879,6 +30121,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// If we are using `track by` then we must watch the tracked value on the model
// since ngModel only watches for object identity change
+ // FIXME: When a user selects an option, this watch will fire needlessly
if (ngOptions.trackBy) {
scope.$watch(
function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
@@ -26888,22 +30131,17 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
} else {
- ngModelCtrl.$isEmpty = function(value) {
- return !value || value.length === 0;
- };
+ selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
+ // Only set `<option>.selected` if necessary, in order to prevent some browsers from
+ // scrolling to `<option>` elements that are outside the `<select>` element's viewport.
+ var selectedOptions = values && values.map(getAndUpdateSelectedOption) || [];
- selectCtrl.writeValue = function writeNgOptionsMultiple(value) {
options.items.forEach(function(option) {
- option.element.selected = false;
+ if (option.element.selected && !includes(selectedOptions, option)) {
+ option.element.selected = false;
+ }
});
-
- if (value) {
- value.forEach(function(item) {
- var option = options.getOptionFromViewValue(item);
- if (option && !option.disabled) option.element.selected = true;
- });
- }
};
@@ -26936,23 +30174,48 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
}
}
-
if (providedEmptyOption) {
// we need to remove it before calling selectElement.empty() because otherwise IE will
// remove the label from the element. wtf?
- emptyOption.remove();
+ selectCtrl.emptyOption.remove();
// compile the element since there might be bindings in it
- $compile(emptyOption)(scope);
+ $compile(selectCtrl.emptyOption)(scope);
+
+ if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
+ // This means the empty option has currently no actual DOM node, probably because
+ // it has been modified by a transclusion directive.
+ selectCtrl.hasEmptyOption = false;
+
+ // Redefine the registerOption function, which will catch
+ // options that are added by ngIf etc. (rendering of the node is async because of
+ // lazy transclusion)
+ selectCtrl.registerOption = function(optionScope, optionEl) {
+ if (optionEl.val() === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = optionEl;
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ // This ensures the new empty option is selected if previously no option was selected
+ ngModelCtrl.$render();
+
+ optionEl.on('$destroy', function() {
+ selectCtrl.hasEmptyOption = false;
+ selectCtrl.emptyOption = undefined;
+ });
+ }
+ };
+
+ } else {
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ }
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- emptyOption.removeClass('ng-scope');
- } else {
- emptyOption = jqLite(optionTemplate.cloneNode(false));
}
+ selectElement.empty();
+
// We need to do this here to ensure that the options object is defined
// when we first hit it in writeNgOptionsValue
updateOptions();
@@ -26962,6 +30225,20 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
// ------------------------------------------------------------------ //
+ function addOptionElement(option, parent) {
+ var optionElement = optionTemplate.cloneNode(false);
+ parent.appendChild(optionElement);
+ updateOptionElement(option, optionElement);
+ }
+
+ function getAndUpdateSelectedOption(viewValue) {
+ var option = options.getOptionFromViewValue(viewValue);
+ var element = option && option.element;
+
+ if (element && !element.selected) element.selected = true;
+
+ return option;
+ }
function updateOptionElement(option, element) {
option.element = element;
@@ -26975,143 +30252,78 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
element.label = option.label;
element.textContent = option.label;
}
- if (option.value !== element.value) element.value = option.selectValue;
- }
-
- function addOrReuseElement(parent, current, type, templateElement) {
- var element;
- // Check whether we can reuse the next element
- if (current && lowercase(current.nodeName) === type) {
- // The next element is the right type so reuse it
- element = current;
- } else {
- // The next element is not the right type so create a new one
- element = templateElement.cloneNode(false);
- if (!current) {
- // There are no more elements so just append it to the select
- parent.appendChild(element);
- } else {
- // The next element is not a group so insert the new one
- parent.insertBefore(element, current);
- }
- }
- return element;
- }
-
-
- function removeExcessElements(current) {
- var next;
- while (current) {
- next = current.nextSibling;
- jqLiteRemove(current);
- current = next;
- }
+ element.value = option.selectValue;
}
+ function updateOptions() {
+ var previousValue = options && selectCtrl.readValue();
- function skipEmptyAndUnknownOptions(current) {
- var emptyOption_ = emptyOption && emptyOption[0];
- var unknownOption_ = unknownOption && unknownOption[0];
-
- // We cannot rely on the extracted empty option being the same as the compiled empty option,
- // because the compiled empty option might have been replaced by a comment because
- // it had an "element" transclusion directive on it (such as ngIf)
- if (emptyOption_ || unknownOption_) {
- while (current &&
- (current === emptyOption_ ||
- current === unknownOption_ ||
- current.nodeType === NODE_TYPE_COMMENT ||
- current.value === '')) {
- current = current.nextSibling;
+ // We must remove all current options, but cannot simply set innerHTML = null
+ // since the providedEmptyOption might have an ngIf on it that inserts comments which we
+ // must preserve.
+ // Instead, iterate over the current option elements and remove them or their optgroup
+ // parents
+ if (options) {
+
+ for (var i = options.items.length - 1; i >= 0; i--) {
+ var option = options.items[i];
+ if (isDefined(option.group)) {
+ jqLiteRemove(option.element.parentNode);
+ } else {
+ jqLiteRemove(option.element);
+ }
}
}
- return current;
- }
-
-
- function updateOptions() {
-
- var previousValue = options && selectCtrl.readValue();
options = ngOptions.getOptions();
- var groupMap = {};
- var currentElement = selectElement[0].firstChild;
+ var groupElementMap = {};
// Ensure that the empty option is always there if it was explicitly provided
if (providedEmptyOption) {
- selectElement.prepend(emptyOption);
+ selectElement.prepend(selectCtrl.emptyOption);
}
- currentElement = skipEmptyAndUnknownOptions(currentElement);
-
- options.items.forEach(function updateOption(option) {
- var group;
+ options.items.forEach(function addOption(option) {
var groupElement;
- var optionElement;
if (isDefined(option.group)) {
// This option is to live in a group
// See if we have already created this group
- group = groupMap[option.group];
+ groupElement = groupElementMap[option.group];
- if (!group) {
+ if (!groupElement) {
- // We have not already created this group
- groupElement = addOrReuseElement(selectElement[0],
- currentElement,
- 'optgroup',
- optGroupTemplate);
- // Move to the next element
- currentElement = groupElement.nextSibling;
+ groupElement = optGroupTemplate.cloneNode(false);
+ listFragment.appendChild(groupElement);
// Update the label on the group element
- groupElement.label = option.group;
+ // "null" is special cased because of Safari
+ groupElement.label = option.group === null ? 'null' : option.group;
// Store it for use later
- group = groupMap[option.group] = {
- groupElement: groupElement,
- currentOptionElement: groupElement.firstChild
- };
-
+ groupElementMap[option.group] = groupElement;
}
- // So now we have a group for this option we add the option to the group
- optionElement = addOrReuseElement(group.groupElement,
- group.currentOptionElement,
- 'option',
- optionTemplate);
- updateOptionElement(option, optionElement);
- // Move to the next element
- group.currentOptionElement = optionElement.nextSibling;
+ addOptionElement(option, groupElement);
} else {
// This option is not in a group
- optionElement = addOrReuseElement(selectElement[0],
- currentElement,
- 'option',
- optionTemplate);
- updateOptionElement(option, optionElement);
- // Move to the next element
- currentElement = optionElement.nextSibling;
+ addOptionElement(option, listFragment);
}
});
-
- // Now remove all excess options and group
- Object.keys(groupMap).forEach(function(key) {
- removeExcessElements(groupMap[key].currentOptionElement);
- });
- removeExcessElements(currentElement);
+ selectElement[0].appendChild(listFragment);
ngModelCtrl.$render();
// Check to see if the value has changed due to the update to the options
if (!ngModelCtrl.$isEmpty(previousValue)) {
var nextValue = selectCtrl.readValue();
- if (ngOptions.trackBy ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
+ var isNotPrimitive = ngOptions.trackBy || multiple;
+ if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
ngModelCtrl.$setViewValue(nextValue);
ngModelCtrl.$render();
}
@@ -27228,7 +30440,7 @@ var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
* @param {number=} offset Offset to deduct from the total number.
*
* @example
- <example module="pluralizeExample">
+ <example module="pluralizeExample" name="ng-pluralize">
<file name="index.html">
<script>
angular.module('pluralizeExample', [])
@@ -27342,7 +30554,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
var count = parseFloat(newVal);
- var countIsNaN = isNaN(count);
+ var countIsNaN = isNumberNaN(count);
if (!countIsNaN && !(count in whens)) {
// If an explicit number rule such as 1, 2, 3... is defined, just use it.
@@ -27351,13 +30563,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
// If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
- // In JS `NaN !== NaN`, so we have to exlicitly check.
- if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
+ // In JS `NaN !== NaN`, so we have to explicitly check.
+ if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
if (isUndefined(whenExpFn)) {
if (newVal != null) {
- $log.debug("ngPluralize: no rule defined for '" + count + "' in " + whenExp);
+ $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp);
}
watchRemover = noop;
updateElementText();
@@ -27375,10 +30587,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
};
}];
+/* exported ngRepeatDirective */
+
/**
* @ngdoc directive
* @name ngRepeat
* @multiElement
+ * @restrict A
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -27411,17 +30626,23 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* <div ng-repeat="(key, value) in myObj"> ... </div>
* ```
*
- * You need to be aware that the JavaScript specification does not define the order of keys
- * returned for an object. (To mitigate this in Angular 1.3 the `ngRepeat` directive
- * used to sort the keys alphabetically.)
+ * However, there are a few limitations compared to array iteration:
*
- * Version 1.4 removed the alphabetic sorting. We now rely on the order returned by the browser
- * when running `for key in myObj`. It seems that browsers generally follow the strategy of providing
- * keys in the order in which they were defined, although there are exceptions when keys are deleted
- * and reinstated. See the [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
+ * - The JavaScript specification does not define the order of keys
+ * returned for an object, so Angular relies on the order returned by the browser
+ * when running `for key in myObj`. Browsers generally follow the strategy of providing
+ * keys in the order in which they were defined, although there are exceptions when keys are deleted
+ * and reinstated. See the
+ * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
*
- * If this is not desired, the recommended workaround is to convert your object into an array
- * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
+ *
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw an error if used with one.
+ *
+ * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
* do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
* or implement a `$watch` on the object yourself.
*
@@ -27429,7 +30650,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* # Tracking and Duplicates
*
* `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
- * the collection. When a change happens, ngRepeat then makes the corresponding changes to the DOM:
+ * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM:
*
* * When an item is added, a new instance of the template is added to the DOM.
* * When an item is removed, its template instance is removed from the DOM.
@@ -27437,7 +30658,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
*
* To minimize creation of DOM elements, `ngRepeat` uses a function
* to "keep track" of all items in the collection and their corresponding DOM elements.
- * For example, if an item is added to the collection, ngRepeat will know that all other items
+ * 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.
*
* The default tracking function (which tracks items by their identity) does not allow
@@ -27464,19 +30685,29 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* ```
*
* <div class="alert alert-success">
- * If you are working with objects that have an identifier property, you should track
- * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
+ * If you are working with objects that have a unique identifier property, you should track
+ * by this identifier instead of the object instance. Should you reload your data later, `ngRepeat`
* will not have to rebuild the DOM elements for items it has already rendered, even if the
* JavaScript objects in the collection have been substituted for new ones. For large collections,
- * this signifincantly improves rendering performance. If you don't have a unique identifier,
+ * this significantly improves rendering performance. If you don't have a unique identifier,
* `track by $index` can also provide a performance boost.
* </div>
+ *
* ```html
* <div ng-repeat="model in collection track by model.id">
* {{model.name}}
* </div>
* ```
*
+ * <br />
+ * <div class="alert alert-warning">
+ * Avoid using `track by $index` when the repeated template contains
+ * {@link guide/expression#one-time-binding one-time bindings}. In such cases, the `nth` DOM
+ * element will always be matched with the `nth` item of the array, so the bindings on that element
+ * will not be updated even when the corresponding item changes, essentially causing the view to get
+ * out-of-sync with the underlying data.
+ * </div>
+ *
* When no `track by` expression is provided, it is equivalent to tracking by the built-in
* `$id` function, which tracks items by their identity:
* ```html
@@ -27485,15 +30716,17 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* </div>
* ```
*
+ * <br />
* <div class="alert alert-warning">
* **Note:** `track by` must always be the last expression:
* </div>
* ```
- * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
- * {{model.name}}
- * </div>
+ * <div ng-repeat="model in collection | orderBy: 'id' as filtered_result track by model.id">
+ * {{model.name}}
+ * </div>
* ```
*
+ *
* # Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
@@ -27539,11 +30772,13 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
*
* @animations
- * **.enter** - when a new item is added to the list or when an item is revealed after a filter
- *
- * **.leave** - when an item is removed from the list or when an item is filtered out
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter |
+ * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out |
+ * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
*
- * **.move** - when an adjacent item is filtered out causing a reorder or when the item contents are reordered
+ * See the example below for defining CSS animations with ngRepeat.
*
* @element ANY
* @scope
@@ -27597,34 +30832,39 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
* For example: `item in items | filter : x | orderBy : order | limitTo : limit as results` .
*
* @example
- * This example initializes the scope to a list of names and
- * then uses `ngRepeat` to display every person:
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
+ * results by name or by age. New (entering) and removed (leaving) items are animated.
+ <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true" name="ng-repeat">
<file name="index.html">
- <div ng-init="friends = [
- {name:'John', age:25, gender:'boy'},
- {name:'Jessie', age:30, gender:'girl'},
- {name:'Johanna', age:28, gender:'girl'},
- {name:'Joy', age:15, gender:'girl'},
- {name:'Mary', age:28, gender:'girl'},
- {name:'Peter', age:95, gender:'boy'},
- {name:'Sebastian', age:50, gender:'boy'},
- {name:'Erika', age:27, gender:'girl'},
- {name:'Patrick', age:40, gender:'boy'},
- {name:'Samantha', age:60, gender:'girl'}
- ]">
+ <div ng-controller="repeatController">
I have {{friends.length}} friends. They are:
<input type="search" ng-model="q" placeholder="filter friends..." aria-label="filter friends" />
<ul class="example-animate-container">
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
</li>
- <li class="animate-repeat" ng-if="results.length == 0">
+ <li class="animate-repeat" ng-if="results.length === 0">
<strong>No results found...</strong>
</li>
</ul>
</div>
</file>
+ <file name="script.js">
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ $scope.friends = [
+ {name:'John', age:25, gender:'boy'},
+ {name:'Jessie', age:30, gender:'girl'},
+ {name:'Johanna', age:28, gender:'girl'},
+ {name:'Joy', age:15, gender:'girl'},
+ {name:'Mary', age:28, gender:'girl'},
+ {name:'Peter', age:95, gender:'boy'},
+ {name:'Sebastian', age:50, gender:'boy'},
+ {name:'Erika', age:27, gender:'girl'},
+ {name:'Patrick', age:40, gender:'boy'},
+ {name:'Samantha', age:60, gender:'girl'}
+ ];
+ });
+ </file>
<file name="animations.css">
.example-animate-container {
background:white;
@@ -27635,7 +30875,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
}
.animate-repeat {
- line-height:40px;
+ line-height:30px;
list-style:none;
box-sizing:border-box;
}
@@ -27657,7 +30897,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
- max-height:40px;
+ max-height:30px;
}
</file>
<file name="protractor.js" type="protractor">
@@ -27684,7 +30924,7 @@ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale,
</file>
</example>
*/
-var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
+var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) {
var NG_REMOVED = '$$NG_REMOVED';
var ngRepeatMinErr = minErr('ngRepeat');
@@ -27696,9 +30936,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
scope.$first = (index === 0);
scope.$last = (index === (arrayLength - 1));
scope.$middle = !(scope.$first || scope.$last);
- // jshint bitwise: false
- scope.$odd = !(scope.$even = (index&1) === 0);
- // jshint bitwise: true
+ // eslint-disable-next-line no-bitwise
+ scope.$odd = !(scope.$even = (index & 1) === 0);
};
var getBlockStart = function(block) {
@@ -27719,12 +30958,12 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
$$tlb: true,
compile: function ngRepeatCompile($element, $attr) {
var expression = $attr.ngRepeat;
- var ngRepeatEndComment = document.createComment(' end ngRepeat: ' + expression + ' ');
+ var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
- throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
expression);
}
@@ -27733,10 +30972,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var aliasAs = match[3];
var trackByExp = match[4];
- match = lhs.match(/^(?:(\s*[\$\w]+)|\(\s*([\$\w]+)\s*,\s*([\$\w]+)\s*\))$/);
+ match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);
if (!match) {
- throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
+ throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
lhs);
}
var valueIdentifier = match[3] || match[1];
@@ -27744,7 +30983,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
/^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
- throw ngRepeatMinErr('badident', "alias '{0}' is invalid --- must be a valid JS identifier which is not a reserved name.",
+ throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
aliasAs);
}
@@ -27840,7 +31079,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
if (block && block.scope) lastBlockMap[block.id] = block;
});
throw ngRepeatMinErr('dupes',
- "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}",
+ 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
expression, trackById, value);
} else {
// new never before seen block
@@ -27881,9 +31120,9 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
nextNode = nextNode.nextSibling;
} while (nextNode && nextNode[NG_REMOVED]);
- if (getBlockStart(block) != nextNode) {
+ if (getBlockStart(block) !== nextNode) {
// existing item which got moved
- $animate.move(getBlockNodes(block.clone), null, jqLite(previousNode));
+ $animate.move(getBlockNodes(block.clone), null, previousNode);
}
previousNode = getBlockEnd(block);
updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
@@ -27895,8 +31134,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var endNode = ngRepeatEndComment.cloneNode(false);
clone[clone.length++] = endNode;
- // TODO(perf): support naked previousNode in `enter` to avoid creation of jqLite wrapper?
- $animate.enter(clone, null, jqLite(previousNode));
+ $animate.enter(clone, null, previousNode);
previousNode = endNode;
// Note: We only need the first/last node of the cloned nodes.
// However, we need to keep the reference to the jqlite wrapper as it might be changed later
@@ -27922,11 +31160,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* @multiElement
*
* @description
- * The `ngShow` directive shows or hides the given HTML element based on the expression
- * provided to the `ngShow` attribute. The element is shown or hidden by removing or adding
- * the `.ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngShow` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngShow` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is visible) -->
@@ -27936,31 +31176,32 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* <div ng-show="myValue" class="ng-hide"></div>
* ```
*
- * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When truthy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope
- * with extra animation classes that can be added.
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
* .ng-hide:not(.ng-hide-animate) {
- * /&#42; this is just another form of hiding an element &#42;/
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -27968,29 +31209,20 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override anything in CSS and the animations will work around the
+ * display style.
*
* ## A note about animations with `ngShow`
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass except that
- * you must also include the !important flag to override the display property
- * so that you can perform an animation when the element is hidden during the time of the animation.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * /&#42; this is required as of 1.3x to properly
- * apply all styling in a show/hide animation &#42;/
- * transition: 0s linear all;
- * }
- *
- * .my-element.ng-hide-add-active,
- * .my-element.ng-hide-remove-active {
- * /&#42; the transition is defined in the active class &#42;/
- * transition: 1s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -27999,74 +31231,108 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @animations
- * addClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a truthy value and the just before contents are set to visible
- * removeClass: `.ng-hide` - happens after the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngShow` expression evaluates to a truthy value and just before contents are set to visible. |
*
* @element ANY
- * @param {expression} ngShow If the {@link guide/expression expression} is truthy
- * then the element is shown or hidden respectively.
+ * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the
+ * element is shown/hidden respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br/>
- <div>
- Show:
- <div class="check-element animate-show" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element animate-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
</div>
- <div>
- Hide:
- <div class="check-element animate-show" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
- </div>
- </file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
</file>
<file name="animations.css">
- .animate-show {
- line-height: 20px;
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-show.ng-hide-add, .animate-show.ng-hide-remove {
- transition: all linear 0.5s;
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-show-complex">
+ <file name="index.html">
+ Show: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br />
+ <div class="check-element funky-show-hide" ng-show="checked">
+ I show up when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
}
- .animate-show.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
-
- element(by.model('checked')).click();
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
});
</file>
</example>
@@ -28096,11 +31362,13 @@ var ngShowDirective = ['$animate', function($animate) {
* @multiElement
*
* @description
- * The `ngHide` directive shows or hides the given HTML element based on the expression
- * provided to the `ngHide` attribute. The element is shown or hidden by removing or adding
- * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngHide` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngHide` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
* <!-- when $scope.myValue is truthy (element is hidden) -->
@@ -28110,30 +31378,32 @@ var ngShowDirective = ['$animate', function($animate) {
* <div ng-hide="myValue"></div>
* ```
*
- * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added to the class
- * attribute on the element causing it to become hidden. When falsy, the `.ng-hide` CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the `.ng-hide` CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
- * the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class in CSS:
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
* ```css
- * .ng-hide {
- * /&#42; this is just another form of hiding an element &#42;/
+ * .ng-hide:not(.ng-hide-animate) {
+ * /&#42; These are just alternative ways of hiding an element &#42;/
* display: block!important;
* position: absolute;
* top: -9999px;
@@ -28141,20 +31411,20 @@ var ngShowDirective = ['$animate', function($animate) {
* }
* ```
*
- * By default you don't need to override in CSS anything and the animations will work around the display style.
+ * By default you don't need to override in CSS anything and the animations will work around the
+ * display style.
*
* ## A note about animations with `ngHide`
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass, except that the `.ng-hide`
- * CSS class is added and removed for you instead of your own CSS class.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /&#42; A working example can be found at the bottom of this page. &#42;/
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * transition: 0.5s linear all;
+ * transition: all 0.5s linear;
* }
*
* .my-element.ng-hide-add { ... }
@@ -28163,71 +31433,109 @@ var ngShowDirective = ['$animate', function($animate) {
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * Keep in mind that, as of AngularJS version 1.3.0-beta.11, there is no need to change the display
- * property to block during animation states--ngAnimate will handle the style toggling automatically for you.
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @animations
- * removeClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden
- * addClass: `.ng-hide` - happens after the `ngHide` expression evaluates to a non truthy value and just before the contents are set to visible
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible. |
+ *
*
* @element ANY
- * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
- * the element is shown or hidden respectively.
+ * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the
+ * element is hidden/shown respectively.
*
* @example
- <example module="ngAnimate" deps="angular-animate.js" animations="true">
+ * A simple example, animating the element's opacity:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-simple">
<file name="index.html">
- Click me: <input type="checkbox" ng-model="checked" aria-label="Toggle ngShow"><br/>
- <div>
- Show:
- <div class="check-element animate-hide" ng-show="checked">
- <span class="glyphicon glyphicon-thumbs-up"></span> I show up when your checkbox is checked.
- </div>
- </div>
- <div>
- Hide:
- <div class="check-element animate-hide" ng-hide="checked">
- <span class="glyphicon glyphicon-thumbs-down"></span> I hide when your checkbox is checked.
- </div>
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element animate-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
</div>
</file>
- <file name="glyphicons.css">
- @import url(../../components/bootstrap-3.1.1/css/bootstrap.css);
- </file>
<file name="animations.css">
- .animate-hide {
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
transition: all linear 0.5s;
- line-height: 20px;
+ }
+
+ .check-element {
+ border: 1px solid black;
opacity: 1;
padding: 10px;
- border: 1px solid black;
- background: white;
}
+ </file>
+ <file name="protractor.js" type="protractor">
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- .animate-hide.ng-hide {
- line-height: 0;
- opacity: 0;
- padding: 0 10px;
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+ </file>
+ </example>
+ *
+ * <hr />
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+ <example module="ngAnimate" deps="angular-animate.js" animations="true" name="ng-hide-complex">
+ <file name="index.html">
+ Hide: <input type="checkbox" ng-model="checked" aria-label="Toggle ngHide"><br />
+ <div class="check-element funky-show-hide" ng-hide="checked">
+ I hide when your checkbox is checked.
+ </div>
+ </file>
+ <file name="animations.css">
+ body {
+ overflow: hidden;
+ perspective: 1000px;
+ }
+
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding: 10px;
border: 1px solid black;
- background: white;
+ opacity: 1;
+ padding: 10px;
}
</file>
<file name="protractor.js" type="protractor">
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
});
</file>
</example>
@@ -28256,6 +31564,11 @@ var ngHideDirective = ['$animate', function($animate) {
* @description
* The `ngStyle` directive allows you to set CSS style on an HTML element conditionally.
*
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `style`
+ * attribute, when using the `ngStyle` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
+ *
* @element ANY
* @param {expression} ngStyle
*
@@ -28267,7 +31580,7 @@ var ngHideDirective = ['$animate', function($animate) {
* See the 'background-color' style in the example below.
*
* @example
- <example>
+ <example name="ng-style">
<file name="index.html">
<input type="button" value="set color" ng-click="myStyle={color:'red'}">
<input type="button" value="set background" ng-click="myStyle={'background-color':'blue'}">
@@ -28330,8 +31643,10 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
* </div>
* @animations
- * enter - happens after the ngSwitch contents change and the matched child element is placed inside the container
- * leave - happens just after the ngSwitch contents change and just before the former contents are removed from the DOM
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container |
+ * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM |
*
* @usage
*
@@ -28351,14 +31666,18 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
* case will be displayed. If the same match appears multiple times, all the
- * elements will be displayed.
+ * elements will be displayed. It is possible to associate multiple values to
+ * the same `ngSwitchWhen` by defining the optional attribute
+ * `ngSwitchWhenSeparator`. The separator will be used to split the value of
+ * the `ngSwitchWhen` attribute into multiple tokens, and the element will show
+ * if any of the `ngSwitch` evaluates to any of these tokens.
* * `ngSwitchDefault`: the default case when no other case match. If there
* are multiple default cases, all of them will be displayed when no other
* case match.
*
*
* @example
- <example module="switchExample" deps="angular-animate.js" animations="true">
+ <example module="switchExample" deps="angular-animate.js" animations="true" name="ng-switch">
<file name="index.html">
<div ng-controller="ExampleController">
<select ng-model="selection" ng-options="item for item in items">
@@ -28367,7 +31686,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<hr/>
<div class="animate-switch-container"
ng-switch on="selection">
- <div class="animate-switch" ng-switch-when="settings">Settings Div</div>
+ <div class="animate-switch" ng-switch-when="settings|options" ng-switch-when-separator="|">Settings Div</div>
<div class="animate-switch" ng-switch-when="home">Home Span</div>
<div class="animate-switch" ng-switch-default>default</div>
</div>
@@ -28376,7 +31695,7 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
<file name="script.js">
angular.module('switchExample', ['ngAnimate'])
.controller('ExampleController', ['$scope', function($scope) {
- $scope.items = ['settings', 'home', 'other'];
+ $scope.items = ['settings', 'home', 'options', 'other'];
$scope.selection = $scope.items[0];
}]);
</file>
@@ -28423,19 +31742,23 @@ var ngStyleDirective = ngDirective(function(scope, element, attr) {
select.all(by.css('option')).get(1).click();
expect(switchElem.getText()).toMatch(/Home Span/);
});
- it('should select default', function() {
+ it('should change to settings via "options"', function() {
select.all(by.css('option')).get(2).click();
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
+ it('should select default', function() {
+ select.all(by.css('option')).get(3).click();
expect(switchElem.getText()).toMatch(/default/);
});
</file>
</example>
*/
-var ngSwitchDirective = ['$animate', function($animate) {
+var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
return {
require: 'ngSwitch',
// asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
+ controller: ['$scope', function NgSwitchController() {
this.cases = {};
}],
link: function(scope, element, attr, ngSwitchController) {
@@ -28446,21 +31769,24 @@ var ngSwitchDirective = ['$animate', function($animate) {
selectedScopes = [];
var spliceFactory = function(array, index) {
- return function() { array.splice(index, 1); };
+ return function(response) {
+ if (response !== false) array.splice(index, 1);
+ };
};
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
var i, ii;
- for (i = 0, ii = previousLeaveAnimations.length; i < ii; ++i) {
- $animate.cancel(previousLeaveAnimations[i]);
+
+ // Start with the last, in case the array is modified during the loop
+ while (previousLeaveAnimations.length) {
+ $animate.cancel(previousLeaveAnimations.pop());
}
- previousLeaveAnimations.length = 0;
for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
var selected = getBlockNodes(selectedElements[i].clone);
selectedScopes[i].$destroy();
- var promise = previousLeaveAnimations[i] = $animate.leave(selected);
- promise.then(spliceFactory(previousLeaveAnimations, i));
+ var runner = previousLeaveAnimations[i] = $animate.leave(selected);
+ runner.done(spliceFactory(previousLeaveAnimations, i));
}
selectedElements.length = 0;
@@ -28471,7 +31797,7 @@ var ngSwitchDirective = ['$animate', function($animate) {
selectedTransclude.transclude(function(caseElement, selectedScope) {
selectedScopes.push(selectedScope);
var anchor = selectedTransclude.element;
- caseElement[caseElement.length++] = document.createComment(' end ngSwitchWhen: ');
+ caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
var block = { clone: caseElement };
selectedElements.push(block);
@@ -28490,8 +31816,16 @@ var ngSwitchWhenDirective = ngDirective({
require: '^ngSwitch',
multiElement: true,
link: function(scope, element, attrs, ctrl, $transclude) {
- ctrl.cases['!' + attrs.ngSwitchWhen] = (ctrl.cases['!' + attrs.ngSwitchWhen] || []);
- ctrl.cases['!' + attrs.ngSwitchWhen].push({ transclude: $transclude, element: element });
+
+ var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
+ // Filter duplicate cases
+ function(element, index, array) { return array[index - 1] !== element; }
+ );
+
+ forEach(cases, function(whenCase) {
+ ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
+ ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
+ });
}
});
@@ -28517,124 +31851,220 @@ var ngSwitchDefaultDirective = ngDirective({
* You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
* as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
*
- * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
+ * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
+ * content of this element will be removed before the transcluded content is inserted.
+ * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback
+ * content in the case that no transcluded content is provided.
*
* @element ANY
*
- * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided or empty then
- * the default slot is used.
+ * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty
+ * or its value is the same as the name of the attribute then the default slot is used.
*
* @example
- * ### Default transclusion
- * This example demonstrates simple transclusion.
- <example name="simpleTranscludeExample" module="transcludeExample">
- <file name="index.html">
- <script>
- angular.module('transcludeExample', [])
- .directive('pane', function(){
- return {
- restrict: 'E',
- transclude: true,
- scope: { title:'@' },
- template: '<div style="border: 1px solid black;">' +
- '<div style="background-color: gray">{{title}}</div>' +
- '<ng-transclude></ng-transclude>' +
- '</div>'
- };
- })
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.title = 'Lorem Ipsum';
- $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
- }]);
- </script>
- <div ng-controller="ExampleController">
- <input ng-model="title" aria-label="title"> <br/>
- <textarea ng-model="text" aria-label="text"></textarea> <br/>
- <pane title="{{title}}">{{text}}</pane>
- </div>
- </file>
- <file name="protractor.js" type="protractor">
- it('should have transcluded', function() {
- var titleElement = element(by.model('title'));
- titleElement.clear();
- titleElement.sendKeys('TITLE');
- var textElement = element(by.model('text'));
- textElement.clear();
- textElement.sendKeys('TEXT');
- expect(element(by.binding('title')).getText()).toEqual('TITLE');
- expect(element(by.binding('text')).getText()).toEqual('TEXT');
- });
- </file>
- </example>
+ * ### Basic transclusion
+ * This example demonstrates basic transclusion of content into a component directive.
+ * <example name="simpleTranscludeExample" module="transcludeExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeExample', [])
+ * .directive('pane', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: { title:'@' },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div style="background-color: gray">{{title}}</div>' +
+ * '<ng-transclude></ng-transclude>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane title="{{title}}"><span>{{text}}</span></pane>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.binding('title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * });
+ * </file>
+ * </example>
+ *
+ * @example
+ * ### Transclude fallback content
+ * This example shows how to use `NgTransclude` with fallback content, that
+ * is displayed if no transcluded content is provided.
+ *
+ * <example module="transcludeFallbackContentExample" name="ng-transclude">
+ * <file name="index.html">
+ * <script>
+ * angular.module('transcludeFallbackContentExample', [])
+ * .directive('myButton', function(){
+ * return {
+ * restrict: 'E',
+ * transclude: true,
+ * scope: true,
+ * template: '<button style="cursor: pointer;">' +
+ * '<ng-transclude>' +
+ * '<b style="color: red;">Button1</b>' +
+ * '</ng-transclude>' +
+ * '</button>'
+ * };
+ * });
+ * </script>
+ * <!-- fallback button content -->
+ * <my-button id="fallback"></my-button>
+ * <!-- modified button content -->
+ * <my-button id="modified">
+ * <i style="color: green;">Button2</i>
+ * </my-button>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have different transclude element content', function() {
+ * expect(element(by.id('fallback')).getText()).toBe('Button1');
+ * expect(element(by.id('modified')).getText()).toBe('Button2');
+ * });
+ * </file>
+ * </example>
*
* @example
* ### Multi-slot transclusion
- <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
- <file name="index.html">
- <div ng-controller="ExampleController">
- <input ng-model="title" aria-label="title"> <br/>
- <textarea ng-model="text" aria-label="text"></textarea> <br/>
- <pane>
- <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
- <pane-body><p>{{text}}</p></pane-body>
- </pane>
- </div>
- </file>
- <file name="app.js">
- angular.module('multiSlotTranscludeExample', [])
- .directive('pane', function(){
- return {
- restrict: 'E',
- transclude: {
- 'paneTitle': '?title',
- 'paneBody': 'body'
- },
- template: '<div style="border: 1px solid black;">' +
- '<div ng-transclude="title" style="background-color: gray"></div>' +
- '<div ng-transclude="body"></div>' +
- '</div>'
- };
- })
- .controller('ExampleController', ['$scope', function($scope) {
- $scope.title = 'Lorem Ipsum';
- $scope.link = "https://google.com";
- $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
- }]);
- </file>
- <file name="protractor.js" type="protractor">
- it('should have transcluded the title and the body', function() {
- var titleElement = element(by.model('title'));
- titleElement.clear();
- titleElement.sendKeys('TITLE');
- var textElement = element(by.model('text'));
- textElement.clear();
- textElement.sendKeys('TEXT');
- expect(element(by.binding('title')).getText()).toEqual('TITLE');
- expect(element(by.binding('text')).getText()).toEqual('TEXT');
- });
- </file>
- </example> */
+ * This example demonstrates using multi-slot transclusion in a component directive.
+ * <example name="multiSlotTranscludeExample" module="multiSlotTranscludeExample">
+ * <file name="index.html">
+ * <style>
+ * .title, .footer {
+ * background-color: gray
+ * }
+ * </style>
+ * <div ng-controller="ExampleController">
+ * <input ng-model="title" aria-label="title"> <br/>
+ * <textarea ng-model="text" aria-label="text"></textarea> <br/>
+ * <pane>
+ * <pane-title><a ng-href="{{link}}">{{title}}</a></pane-title>
+ * <pane-body><p>{{text}}</p></pane-body>
+ * </pane>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('multiSlotTranscludeExample', [])
+ * .directive('pane', function() {
+ * return {
+ * restrict: 'E',
+ * transclude: {
+ * 'title': '?paneTitle',
+ * 'body': 'paneBody',
+ * 'footer': '?paneFooter'
+ * },
+ * template: '<div style="border: 1px solid black;">' +
+ * '<div class="title" ng-transclude="title">Fallback Title</div>' +
+ * '<div ng-transclude="body"></div>' +
+ * '<div class="footer" ng-transclude="footer">Fallback Footer</div>' +
+ * '</div>'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.link = 'https://google.com';
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ * it('should have transcluded the title and the body', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.css('.title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer');
+ * });
+ * </file>
+ * </example>
+ */
var ngTranscludeMinErr = minErr('ngTransclude');
-var ngTranscludeDirective = ngDirective({
- restrict: 'EAC',
- link: function($scope, $element, $attrs, controller, $transclude) {
+var ngTranscludeDirective = ['$compile', function($compile) {
+ return {
+ restrict: 'EAC',
+ terminal: true,
+ compile: function ngTranscludeCompile(tElement) {
- function ngTranscludeCloneAttachFn(clone) {
- $element.empty();
- $element.append(clone);
- }
+ // Remove and cache any original content to act as a fallback
+ var fallbackLinkFn = $compile(tElement.contents());
+ tElement.empty();
- if (!$transclude) {
- throw ngTranscludeMinErr('orphan',
- 'Illegal use of ngTransclude directive in the template! ' +
- 'No parent directive that requires a transclusion found. ' +
- 'Element: {0}',
- startingTag($element));
- }
+ return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
- $transclude(ngTranscludeCloneAttachFn, null, $attrs.ngTransclude || $attrs.ngTranscludeSlot);
- }
-});
+ if (!$transclude) {
+ throw ngTranscludeMinErr('orphan',
+ 'Illegal use of ngTransclude directive in the template! ' +
+ 'No parent directive that requires a transclusion found. ' +
+ 'Element: {0}',
+ startingTag($element));
+ }
+
+
+ // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ $attrs.ngTransclude = '';
+ }
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
+
+ // If the slot is required and no transclusion content is provided then this call will throw an error
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
+
+ // If the slot is optional and no transclusion content is provided then use the fallback content
+ if (slotName && !$transclude.isSlotFilled(slotName)) {
+ useFallbackContent();
+ }
+
+ function ngTranscludeCloneAttachFn(clone, transcludedScope) {
+ if (clone.length && notWhitespace(clone)) {
+ $element.append(clone);
+ } else {
+ useFallbackContent();
+ // There is nothing linked against the transcluded scope since no content was available,
+ // so it should be safe to clean up the generated scope.
+ transcludedScope.$destroy();
+ }
+ }
+
+ function useFallbackContent() {
+ // Since this is the fallback content rather than the transcluded content,
+ // we link against the scope of this directive rather than the transcluded scope
+ fallbackLinkFn($scope, function(clone) {
+ $element.append(clone);
+ });
+ }
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
+ };
+ }
+ };
+}];
/**
* @ngdoc directive
@@ -28652,7 +32082,7 @@ var ngTranscludeDirective = ngDirective({
* @param {string} id Cache name of the template.
*
* @example
- <example>
+ <example name="script-tag">
<file name="index.html">
<script type="text/ng-template" id="/tpl.html">
Content of the template.
@@ -28674,7 +32104,7 @@ var scriptDirective = ['$templateCache', function($templateCache) {
restrict: 'E',
terminal: true,
compile: function(element, attr) {
- if (attr.type == 'text/ng-template') {
+ if (attr.type === 'text/ng-template') {
var templateUrl = attr.id,
text = element[0].text;
@@ -28684,15 +32114,20 @@ var scriptDirective = ['$templateCache', function($templateCache) {
};
}];
+/* exported selectDirective, optionDirective */
+
var noopNgModelController = { $setViewValue: noop, $render: noop };
-function chromeHack(optionElement) {
- // Workaround for https://code.google.com/p/chromium/issues/detail?id=381459
- // Adding an <option selected="selected"> element to a <select required="required"> should
- // automatically select the new element
- if (optionElement[0].hasAttribute('selected')) {
- optionElement[0].selected = true;
- }
+function setOptionSelectedStatus(optionEl, value) {
+ optionEl.prop('selected', value); // needed for IE
+ /**
+ * When unselecting an option, setting the property to null / false should be enough
+ * However, screenreaders might react to the selected attribute instead, see
+ * https://github.com/angular/angular.js/issues/14419
+ * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false
+ * or null
+ */
+ optionEl.attr('selected', value);
}
/**
@@ -28704,13 +32139,16 @@ function chromeHack(optionElement) {
* added `<option>` elements, perhaps by an `ngRepeat` directive.
*/
var SelectController =
- ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+ ['$element', '$scope', /** @this */ function($element, $scope) {
var self = this,
- optionsMap = new HashMap();
+ optionsMap = new NgMap();
+
+ self.selectValueMap = {}; // Keys are the hashed values, values the original values
// If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
self.ngModelCtrl = noopNgModelController;
+ self.multiple = false;
// The "unknown" option is one that is prepended to the list if the viewValue
// does not match any of the options. When it is rendered the value of the unknown
@@ -28718,43 +32156,95 @@ var SelectController =
//
// We can't just jqLite('<option>') since jqLite is not smart enough
// to create it in <select> and IE barfs otherwise.
- self.unknownOption = jqLite(document.createElement('option'));
+ self.unknownOption = jqLite(window.document.createElement('option'));
+
+ // The empty option is an option with the value '' that te application developer can
+ // provide inside the select. When the model changes to a value that doesn't match an option,
+ // it is selected - so if an empty option is provided, no unknown option is generated.
+ // However, the empty option is not removed when the model matches an option. It is always selectable
+ // and indicates that a "null" selection has been made.
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+
self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
+ var unknownVal = self.generateUnknownOptionValue(val);
self.unknownOption.val(unknownVal);
$element.prepend(self.unknownOption);
+ setOptionSelectedStatus(self.unknownOption, true);
$element.val(unknownVal);
};
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
- });
+ self.updateUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
+
+ self.generateUnknownOptionValue = function(val) {
+ return '? ' + hashKey(val) + ' ?';
+ };
self.removeUnknownOption = function() {
if (self.unknownOption.parent()) self.unknownOption.remove();
};
+ self.selectEmptyOption = function() {
+ if (self.emptyOption) {
+ $element.val('');
+ setOptionSelectedStatus(self.emptyOption, true);
+ }
+ };
+
+ self.unselectEmptyOption = function() {
+ if (self.hasEmptyOption) {
+ self.emptyOption.removeAttr('selected');
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
// Read the value of the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.readValue = function readSingleValue() {
- self.removeUnknownOption();
- return $element.val();
+ var val = $element.val();
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
+ var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
+
+ if (self.hasOption(realVal)) {
+ return realVal;
+ }
+
+ return null;
};
// Write the value to the select control, the implementation of this changes depending
// upon whether the select can have multiple values and whether ngOptions is at work.
self.writeValue = function writeSingleValue(value) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
+ if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
+
if (self.hasOption(value)) {
self.removeUnknownOption();
- $element.val(value);
- if (value === '') self.emptyOption.prop('selected', true); // to make IE9 happy
+
+ var hashedVal = hashKey(value);
+ $element.val(hashedVal in self.selectValueMap ? hashedVal : value);
+
+ // Set selected attribute and property on selected option for screen readers
+ var selectedOption = $element[0].options[$element[0].selectedIndex];
+ setOptionSelectedStatus(jqLite(selectedOption), true);
} else {
if (value == null && self.emptyOption) {
self.removeUnknownOption();
- $element.val('');
+ self.selectEmptyOption();
+ } else if (self.unknownOption.parent().length) {
+ self.updateUnknownOption(value);
} else {
self.renderUnknownOption(value);
}
@@ -28764,14 +32254,19 @@ var SelectController =
// Tell the select control that an option, with the given value, has been added
self.addOption = function(value, element) {
+ // Skip comment nodes, as they only pollute the `optionsMap`
+ if (element[0].nodeType === NODE_TYPE_COMMENT) return;
+
assertNotHasOwnProperty(value, '"option value"');
if (value === '') {
+ self.hasEmptyOption = true;
self.emptyOption = element;
}
var count = optionsMap.get(value) || 0;
- optionsMap.put(value, count + 1);
- self.ngModelCtrl.$render();
- chromeHack(element);
+ optionsMap.set(value, count + 1);
+ // Only render at the end of a digest. This improves render performance when many options
+ // are added during a digest and ensures all relevant options are correctly marked as selected
+ scheduleRender();
};
// Tell the select control that an option, with the given value, has been removed
@@ -28779,12 +32274,13 @@ var SelectController =
var count = optionsMap.get(value);
if (count) {
if (count === 1) {
- optionsMap.remove(value);
+ optionsMap.delete(value);
if (value === '') {
+ self.hasEmptyOption = false;
self.emptyOption = undefined;
}
} else {
- optionsMap.put(value, count - 1);
+ optionsMap.set(value, count - 1);
}
}
};
@@ -28795,35 +32291,131 @@ var SelectController =
};
+ var renderScheduled = false;
+ function scheduleRender() {
+ if (renderScheduled) return;
+ renderScheduled = true;
+ $scope.$$postDigest(function() {
+ renderScheduled = false;
+ self.ngModelCtrl.$render();
+ });
+ }
+
+ var updateScheduled = false;
+ function scheduleViewValueUpdate(renderAfter) {
+ if (updateScheduled) return;
+
+ updateScheduled = true;
+
+ $scope.$$postDigest(function() {
+ if ($scope.$$destroyed) return;
+
+ updateScheduled = false;
+ self.ngModelCtrl.$setViewValue(self.readValue());
+ if (renderAfter) self.ngModelCtrl.$render();
+ });
+ }
+
+
self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
- if (interpolateValueFn) {
+ if (optionAttrs.$attr.ngValue) {
+ // The value attribute is set by ngValue
+ var oldVal, hashedVal = NaN;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(hashedVal)) {
+ self.removeOption(oldVal);
+ delete self.selectValueMap[hashedVal];
+ removal = true;
+ }
+
+ hashedVal = hashKey(newVal);
+ oldVal = newVal;
+ self.selectValueMap[hashedVal] = newVal;
+ self.addOption(newVal, optionElement);
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
+ // from firing a second time. Other $observers on value will also get the result of the
+ // ngValue expression, not the hashed value
+ optionElement.attr('value', hashedVal);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+
+ });
+ } else if (interpolateValueFn) {
// The value attribute is interpolated
- var oldVal;
optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ // This method is overwritten in ngOptions and has side-effects!
+ self.readValue();
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
if (isDefined(oldVal)) {
self.removeOption(oldVal);
+ removal = true;
}
oldVal = newVal;
self.addOption(newVal, optionElement);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else if (interpolateTextFn) {
// The text content is interpolated
optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
optionAttrs.$set('value', newVal);
+ var previouslySelected = optionElement.prop('selected');
if (oldVal !== newVal) {
self.removeOption(oldVal);
}
self.addOption(newVal, optionElement);
+
+ if (oldVal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
} else {
// The value attribute is static
self.addOption(optionAttrs.value, optionElement);
}
+
+ optionAttrs.$observe('disabled', function(newVal) {
+
+ // Since model updates will also select disabled options (like ngOptions),
+ // we only have to handle options becoming disabled, not enabled
+
+ if (newVal === 'true' || newVal && optionElement.prop('selected')) {
+ if (self.multiple) {
+ scheduleViewValueUpdate(true);
+ } else {
+ self.ngModelCtrl.$setViewValue(null);
+ self.ngModelCtrl.$render();
+ }
+ }
+ });
+
optionElement.on('$destroy', function() {
- self.removeOption(optionAttrs.value);
- self.ngModelCtrl.$render();
+ var currentValue = self.readValue();
+ var removeValue = optionAttrs.value;
+
+ self.removeOption(removeValue);
+ scheduleRender();
+
+ if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
+ currentValue === removeValue
+ ) {
+ // When multiple (selected) options are destroyed at the same time, we don't want
+ // to run a model update for each of them. Instead, run a single update in the $$postDigest
+ scheduleViewValueUpdate(true);
+ }
});
};
}];
@@ -28834,24 +32426,34 @@ var SelectController =
* @restrict E
*
* @description
- * HTML `SELECT` element with angular data-binding.
+ * HTML `select` element with angular data-binding.
*
* The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
* between the scope and the `<select>` control (including setting default values).
- * Ìt also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or
+ * It also handles dynamic `<option>` elements, which can be added using the {@link ngRepeat `ngRepeat}` or
* {@link ngOptions `ngOptions`} directives.
*
* When an item in the `<select>` menu is selected, the value of the selected option will be bound
* to the model identified by the `ngModel` directive. With static or repeated options, this is
* the content of the `value` attribute or the textContent of the `<option>`, if the value attribute is missing.
- * If you want dynamic value attributes, you can use interpolation inside the value attribute.
+ * Value and textContent can be interpolated.
*
- * <div class="alert alert-warning">
- * Note that the value of a `select` directive used without `ngOptions` is always a string.
- * When the model needs to be bound to a non-string value, you must either explictly convert it
- * using a directive (see example below) or use `ngOptions` to specify the set of options.
- * This is because an option element can only be bound to string values at present.
- * </div>
+ * ## Matching model and option values
+ *
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
+ * value against the value of the available options.
+ *
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
+ * value will always be a `string` which means that the model value must also be a string.
+ * Otherwise the `select` directive cannot match them correctly.
+ *
+ * To bind the model to a non-string value, you can use one of the following strategies:
+ * - the {@link ng.ngOptions `ngOptions`} directive
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
+ * - model $parsers / $formatters to convert the string value
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
*
* If the viewValue of `ngModel` does not match any of the options, then the control
* will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
@@ -28860,13 +32462,17 @@ var SelectController =
* be nested into the `<select>` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
- * <div class="alert alert-info">
+ * ## Choosing between `ngRepeat` and `ngOptions`
+ *
* In many cases, `ngRepeat` can be used on `<option>` elements instead of {@link ng.directive:ngOptions
- * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits, such as
- * more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
- * comprehension expression, and additionally in reducing memory and increasing speed by not creating
- * a new scope for each repeated instance.
- * </div>
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the `<select>`'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
+ *
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
+ * Chrome and Internet Explorer / Edge.
*
*
* @param {string} ngModel Assignable angular expression to data-bind to.
@@ -28881,6 +32487,8 @@ var SelectController =
* interaction with the select element.
* @param {string=} ngOptions sets the options that the select is populated with and defines what is
* set on the model on selection. See {@link ngOptions `ngOptions`}.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
* @example
* ### Simple `select` elements with static options
@@ -28921,7 +32529,7 @@ var SelectController =
* $scope.data = {
* singleSelect: null,
* multipleSelect: [],
- * option1: 'option-1',
+ * option1: 'option-1'
* };
*
* $scope.forceUnknownOption = function() {
@@ -28932,34 +32540,65 @@ var SelectController =
*</example>
*
* ### Using `ngRepeat` to generate `select` options
- * <example name="ngrepeat-select" module="ngrepeatSelect">
+ * <example name="select-ngrepeat" module="ngrepeatSelect">
* <file name="index.html">
* <div ng-controller="ExampleController">
* <form name="myForm">
* <label for="repeatSelect"> Repeat select: </label>
- * <select name="repeatSelect" id="repeatSelect" ng-model="data.repeatSelect">
+ * <select name="repeatSelect" id="repeatSelect" ng-model="data.model">
* <option ng-repeat="option in data.availableOptions" value="{{option.id}}">{{option.name}}</option>
* </select>
* </form>
* <hr>
- * <tt>repeatSelect = {{data.repeatSelect}}</tt><br/>
+ * <tt>model = {{data.model}}</tt><br/>
* </div>
* </file>
* <file name="app.js">
* angular.module('ngrepeatSelect', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.data = {
- * repeatSelect: null,
+ * model: null,
* availableOptions: [
* {id: '1', name: 'Option A'},
* {id: '2', name: 'Option B'},
* {id: '3', name: 'Option C'}
- * ],
+ * ]
* };
* }]);
* </file>
*</example>
*
+ * ### Using `ngValue` to bind the model to an array of objects
+ * <example name="select-ngvalue" module="ngvalueSelect">
+ * <file name="index.html">
+ * <div ng-controller="ExampleController">
+ * <form name="myForm">
+ * <label for="ngvalueselect"> ngvalue select: </label>
+ * <select size="6" name="ngvalueselect" ng-model="data.model" multiple>
+ * <option ng-repeat="option in data.availableOptions" ng-value="option.value">{{option.name}}</option>
+ * </select>
+ * </form>
+ * <hr>
+ * <pre>model = {{data.model | json}}</pre><br/>
+ * </div>
+ * </file>
+ * <file name="app.js">
+ * angular.module('ngvalueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * model: null,
+ * availableOptions: [
+ {value: 'myString', name: 'string'},
+ {value: 1, name: 'integer'},
+ {value: true, name: 'boolean'},
+ {value: null, name: 'null'},
+ {value: {prop: 'value'}, name: 'object'},
+ {value: ['a'], name: 'array'}
+ * ]
+ * };
+ * }]);
+ * </file>
+ *</example>
*
* ### Using `select` with `ngOptions` and setting a default value
* See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
@@ -29025,7 +32664,6 @@ var SelectController =
* </file>
* <file name="protractor.js" type="protractor">
* it('should initialize to model', function() {
- * var select = element(by.css('select'));
* expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
* });
* </file>
@@ -29040,31 +32678,31 @@ var selectDirective = function() {
controller: SelectController,
priority: 1,
link: {
- pre: selectPreLink
+ pre: selectPreLink,
+ post: selectPostLink
}
};
function selectPreLink(scope, element, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
+ var selectCtrl = ctrls[0];
var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
- var selectCtrl = ctrls[0];
+ // if ngModel is not defined, we don't need to do anything but set the registerOption
+ // function to noop, so options don't get added internally
+ if (!ngModelCtrl) {
+ selectCtrl.registerOption = noop;
+ return;
+ }
- selectCtrl.ngModelCtrl = ngModelCtrl;
- // We delegate rendering to the `writeValue` method, which can be changed
- // if the select can have multiple selected values or if the options are being
- // generated by `ngOptions`
- ngModelCtrl.$render = function() {
- selectCtrl.writeValue(ngModelCtrl.$viewValue);
- };
+ selectCtrl.ngModelCtrl = ngModelCtrl;
// When the selected item(s) changes we delegate getting the value of the select control
// to the `readValue` method, which can be changed if the select can have multiple
// selected values or if the options are being generated by `ngOptions`
element.on('change', function() {
+ selectCtrl.removeUnknownOption();
scope.$apply(function() {
ngModelCtrl.$setViewValue(selectCtrl.readValue());
});
@@ -29075,13 +32713,15 @@ var selectDirective = function() {
// we have to add an extra watch since ngModel doesn't work well with arrays - it
// doesn't trigger rendering if only an item in the array changes.
if (attr.multiple) {
+ selectCtrl.multiple = true;
// Read value now needs to check each option to see if it is selected
selectCtrl.readValue = function readMultipleValue() {
var array = [];
forEach(element.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
+ if (option.selected && !option.disabled) {
+ var val = option.value;
+ array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
}
});
return array;
@@ -29089,9 +32729,21 @@ var selectDirective = function() {
// Write value now needs to set the selected property of each matching option
selectCtrl.writeValue = function writeMultipleValue(value) {
- var items = new HashMap(value);
forEach(element.find('option'), function(option) {
- option.selected = isDefined(items.get(option.value));
+ var shouldBeSelected = !!value && (includes(value, option.value) ||
+ includes(value, selectCtrl.selectValueMap[option.value]));
+ var currentlySelected = option.selected;
+
+ // IE and Edge, adding options to the selection via shift+click/UP/DOWN,
+ // will de-select already selected options if "selected" on those options was set
+ // more than once (i.e. when the options were already selected)
+ // So we only modify the selected property if neccessary.
+ // Note: this behavior cannot be replicated via unit tests because it only shows in the
+ // actual user interface.
+ if (shouldBeSelected !== currentlySelected) {
+ setOptionSelectedStatus(jqLite(option), shouldBeSelected);
+ }
+
});
};
@@ -29114,6 +32766,23 @@ var selectDirective = function() {
}
}
+
+ function selectPostLink(scope, element, attrs, ctrls) {
+ // if ngModel is not defined, we don't need to do anything
+ var ngModelCtrl = ctrls[1];
+ if (!ngModelCtrl) return;
+
+ var selectCtrl = ctrls[0];
+
+ // We delegate rendering to the `writeValue` method, which can be changed
+ // if the select can have multiple selected values or if the options are being
+ // generated by `ngOptions`.
+ // This must be done in the postLink fn to prevent $render to be called before
+ // all nodes have been linked correctly.
+ ngModelCtrl.$render = function() {
+ selectCtrl.writeValue(ngModelCtrl.$viewValue);
+ };
+ }
};
@@ -29125,21 +32794,23 @@ var optionDirective = ['$interpolate', function($interpolate) {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
+ var interpolateValueFn, interpolateTextFn;
- if (isDefined(attr.value)) {
+ if (isDefined(attr.ngValue)) {
+ // Will be handled by registerOption
+ } else if (isDefined(attr.value)) {
// If the value attribute is defined, check if it contains an interpolation
- var interpolateValueFn = $interpolate(attr.value, true);
+ interpolateValueFn = $interpolate(attr.value, true);
} else {
// If the value attribute is not defined then we fall back to the
// text content of the option element, which may be interpolated
- var interpolateTextFn = $interpolate(element.text(), true);
+ interpolateTextFn = $interpolate(element.text(), true);
if (!interpolateTextFn) {
attr.$set('value', element.text());
}
}
return function(scope, element, attr) {
-
// This is an optimization over using ^^ since we don't want to have to search
// all the way to the root of the DOM for every single option element
var selectCtrlName = '$selectController',
@@ -29155,11 +32826,65 @@ var optionDirective = ['$interpolate', function($interpolate) {
};
}];
-var styleDirective = valueFn({
- restrict: 'E',
- terminal: false
-});
+/**
+ * @ngdoc directive
+ * @name ngRequired
+ * @restrict A
+ *
+ * @description
+ *
+ * ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be
+ * applied to custom controls.
+ *
+ * The directive sets the `required` attribute on the element if the Angular expression inside
+ * `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we
+ * cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide}
+ * for more info.
+ *
+ * The validator will set the `required` error key to true if the `required` attribute is set and
+ * calling {@link ngModel.NgModelController#$isEmpty `NgModelController.$isEmpty`} with the
+ * {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} returns `true`. For example, the
+ * `$isEmpty()` implementation for `input[text]` checks the length of the `$viewValue`. When developing
+ * custom controls, `$isEmpty()` can be overwritten to account for a $viewValue that is not string-based.
+ *
+ * @example
+ * <example name="ngRequiredDirective" module="ngRequiredExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngRequiredExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.required = true;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="required">Toggle required: </label>
+ * <input type="checkbox" ng-model="required" id="required" />
+ * <br>
+ * <label for="input">This input must be filled if `required` is true: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-required="required" /><br>
+ * <hr>
+ * required error set? = <code>{{form.input.$error.required}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var required = element(by.binding('form.input.$error.required'));
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+ it('should set the required error', function() {
+ expect(required.getText()).toContain('true');
+
+ input.sendKeys('123');
+ expect(required.getText()).not.toContain('true');
+ expect(model.getText()).toContain('123');
+ });
+ * </file>
+ * </example>
+ */
var requiredDirective = function() {
return {
restrict: 'A',
@@ -29179,7 +32904,81 @@ var requiredDirective = function() {
};
};
+/**
+ * @ngdoc directive
+ * @name ngPattern
+ *
+ * @description
+ *
+ * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * does not match a RegExp which is obtained by evaluating the Angular expression given in the
+ * `ngPattern` attribute value:
+ * * If the expression evaluates to a RegExp object, then this is used directly.
+ * * If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
+ * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ *
+ * <div class="alert alert-info">
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * </div>
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `pattern` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is
+ * not available.
+ * </li>
+ * <li>
+ * The `ngPattern` attribute must be an expression, while the `pattern` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngPatternDirective" module="ngPatternExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngPatternExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.regex = '\\d+';
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="regex">Set a pattern (regex string): </label>
+ * <input type="text" ng-model="regex" id="regex" />
+ * <br>
+ * <label for="input">This input is restricted by the current pattern: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-pattern="regex" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+ it('should validate the input with the default pattern', function() {
+ input.sendKeys('aaa');
+ expect(model.getText()).not.toContain('aaa');
+
+ input.clear().then(function() {
+ input.sendKeys('123');
+ expect(model.getText()).toContain('123');
+ });
+ });
+ * </file>
+ * </example>
+ */
var patternDirective = function() {
return {
restrict: 'A',
@@ -29211,7 +33010,72 @@ var patternDirective = function() {
};
};
+/**
+ * @ngdoc directive
+ * @name ngMaxlength
+ *
+ * @description
+ *
+ * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is longer than the integer obtained by evaluating the Angular expression given in the
+ * `ngMaxlength` attribute value.
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ * </li>
+ * <li>
+ * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngMaxlengthDirective" module="ngMaxlengthExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngMaxlengthExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.maxlength = 5;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="maxlength">Set a maxlength: </label>
+ * <input type="number" ng-model="maxlength" id="maxlength" />
+ * <br>
+ * <label for="input">This input is restricted by the current maxlength: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-maxlength="maxlength" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default maxlength', function() {
+ input.sendKeys('abcdef');
+ expect(model.getText()).not.toContain('abcdef');
+ input.clear().then(function() {
+ input.sendKeys('abcde');
+ expect(model.getText()).toContain('abcde');
+ });
+ });
+ * </file>
+ * </example>
+ */
var maxlengthDirective = function() {
return {
restrict: 'A',
@@ -29222,7 +33086,7 @@ var maxlengthDirective = function() {
var maxlength = -1;
attr.$observe('maxlength', function(value) {
var intVal = toInt(value);
- maxlength = isNaN(intVal) ? -1 : intVal;
+ maxlength = isNumberNaN(intVal) ? -1 : intVal;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
@@ -29232,6 +33096,70 @@ var maxlengthDirective = function() {
};
};
+/**
+ * @ngdoc directive
+ * @name ngMinlength
+ *
+ * @description
+ *
+ * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is shorter than the integer obtained by evaluating the Angular expression given in the
+ * `ngMinlength` attribute value.
+ *
+ * <div class="alert alert-info">
+ * **Note:** This directive is also added when the plain `minlength` attribute is used, with two
+ * differences:
+ * <ol>
+ * <li>
+ * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ * </li>
+ * <li>
+ * The `ngMinlength` value must be an expression, while the `minlength` value must be
+ * interpolated.
+ * </li>
+ * </ol>
+ * </div>
+ *
+ * @example
+ * <example name="ngMinlengthDirective" module="ngMinlengthExample">
+ * <file name="index.html">
+ * <script>
+ * angular.module('ngMinlengthExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.minlength = 3;
+ * }]);
+ * </script>
+ * <div ng-controller="ExampleController">
+ * <form name="form">
+ * <label for="minlength">Set a minlength: </label>
+ * <input type="number" ng-model="minlength" id="minlength" />
+ * <br>
+ * <label for="input">This input is restricted by the current minlength: </label>
+ * <input type="text" ng-model="model" id="input" name="input" ng-minlength="minlength" /><br>
+ * <hr>
+ * input valid? = <code>{{form.input.$valid}}</code><br>
+ * model = <code>{{model}}</code>
+ * </form>
+ * </div>
+ * </file>
+ * <file name="protractor.js" type="protractor">
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default minlength', function() {
+ input.sendKeys('ab');
+ expect(model.getText()).not.toContain('ab');
+
+ input.sendKeys('abc');
+ expect(model.getText()).toContain('abc');
+ });
+ * </file>
+ * </example>
+ */
var minlengthDirective = function() {
return {
restrict: 'A',
@@ -29252,13 +33180,15 @@ var minlengthDirective = function() {
};
if (window.angular.bootstrap) {
- //AngularJS is already loaded, so we can return here...
- console.log('WARNING: Tried to load angular more than once.');
+ // AngularJS is already loaded, so we can return here...
+ if (window.console) {
+ console.log('WARNING: Tried to load angular more than once.');
+ }
return;
}
-//try to bind to jquery now so that one can write jqLite(document).ready()
-//but we will rebind on bootstrap again.
+// try to bind to jquery now so that one can write jqLite(fn)
+// but we will rebind on bootstrap again.
bindJQuery();
publishExternalAPI(angular);
@@ -29344,6 +33274,20 @@ $provide.value("$locale", {
"Nov",
"Dec"
],
+ "STANDALONEMONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
"WEEKENDRANGE": [
5,
6
@@ -29387,14 +33331,15 @@ $provide.value("$locale", {
]
},
"id": "en-us",
+ "localeID": "en_US",
"pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
}]);
- jqLite(document).ready(function() {
- angularInit(document, bootstrap);
+ jqLite(function() {
+ angularInit(window.document, bootstrap);
});
-})(window, document);
+})(window);
!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";[ng\\:cloak],[ng-cloak],[data-ng-cloak],[x-ng-cloak],.ng-cloak,.x-ng-cloak,.ng-hide:not(.ng-hide-animate){display:none !important;}ng\\:form{display:block;}.ng-animate-shim{visibility:hidden;}.ng-anchor{position:absolute;}</style>'); \ No newline at end of file