}));
/**
- * @license AngularJS v1.4.8
- * (c) 2010-2015 Google, Inc. http://angularjs.org
+ * @license AngularJS v1.5.5
+ * (c) 2010-2016 Google, Inc. http://angularjs.org
* License: MIT
*/
-(function(window, document){
+(function(window){
var _jQuery = window.jQuery.noConflict(true);
/**
return match;
});
- message += '\nhttp://errors.angularjs.org/1.4.8/' +
+ message += '\nhttp://errors.angularjs.org/1.5.5/' +
(module ? module + '/' : '') + code;
for (i = SKIP_INDEXES, paramPrefix = '?'; i < templateArgs.length; i++, paramPrefix = '&') {
* @ngdoc module
* @name ng
* @module ng
+ * @installation
* @description
*
* # ng (core module)
// This is used so that it's possible for internal tests to create mock ValidityStates.
var VALIDITY_STATE_PROPERTY = 'validity';
-/**
- * @ngdoc function
- * @name angular.lowercase
- * @module ng
- * @kind function
- *
- * @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
- * @name angular.uppercase
- * @module ng
- * @kind function
- *
- * @description Converts the specified string to uppercase.
- * @param {string} string String to be converted to uppercase.
- * @returns {string} Uppercased string.
- */
+var lowercase = function(string) {return isString(string) ? string.toLowerCase() : string;};
var uppercase = function(string) {return isString(string) ? string.toUpperCase() : string;};
// 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;
* 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;
/**
// 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;
// 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');
+
}
/**
*
* 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
* @returns {function(*, string)}
*/
function reverseParams(iteratorFn) {
- return function(value, key) { iteratorFn(key, value); };
+ return function(value, key) {iteratorFn(key, value);};
}
/**
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;
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;
* @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;
}
var stackDest = [];
if (destination) {
- if (isTypedArray(destination)) {
+ if (isTypedArray(destination) || isArrayBuffer(destination)) {
throw ngMinErr('cpta', "Can't copy! TypedArray destination cannot be mutated.");
}
if (source === destination) {
function copyRecurse(source, destination) {
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]));
}
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;
}
? copyRecurse(source, destination)
: destination;
}
+
+ 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));
+
+ case '[object ArrayBuffer]':
+ //Support: IE10
+ if (!source.slice) {
+ var copied = new ArrayBuffer(source.byteLength);
+ new Uint8Array(copied).set(new Uint8Array(source));
+ return copied;
+ }
+ return source.slice(0);
+
+ 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});
+ }
+
+ if (isFunction(source.cloneNode)) {
+ return source.cloneNode(true);
+ }
+ }
}
/**
* @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.result;
+ $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;
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;
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') ||
var i, ii = ngAttrPrefixes.length, prefix, name;
for (i = 0; i < ii; ++i) {
prefix = ngAttrPrefixes[i];
- if (el = document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
+ if (el = window.document.querySelector('[' + prefix.replace(':', '\\:') + 'jq]')) {
name = el.getAttribute(prefix + 'jq');
break;
}
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';
* @returns {string|undefined} JSON-ified string representing `obj`.
*/
function toJson(obj, pretty) {
- if (typeof obj === 'undefined') return undefined;
+ if (isUndefined(obj)) return undefined;
if (!isNumber(pretty)) {
pretty = pretty ? 2 : null;
}
}
+var ALL_COLONS = /:/g;
function timezoneToOffset(timezone, fallback) {
+ // 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;
}
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));
}
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);
}
* 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
* @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>
element = jqLite(element);
if (element.injector()) {
- var tag = (element[0] === document) ? 'document' : startingTag(element);
+ 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(/</,'<').replace(/>/,'>'));
}
}
var bindJQueryFired = false;
-var skipDestroyOnNextJQueryCleanData;
function bindJQuery() {
var originalCleanData;
originalCleanData = jQuery.cleanData;
jQuery.cleanData = function(elems) {
var events;
- if (!skipDestroyOnNextJQueryCleanData) {
- for (var i = 0, elem; (elem = elems[i]) != null; i++) {
- events = jQuery._data(elem, "events");
- if (events && events.$destroy) {
- jQuery(elem).triggerHandler('$destroy');
- }
+ for (var i = 0, elem; (elem = elems[i]) != null; i++) {
+ events = jQuery._data(elem, "events");
+ if (events && events.$destroy) {
+ jQuery(elem).triggerHandler('$destroy');
}
- } else {
- skipDestroyOnNextJQueryCleanData = false;
}
originalCleanData(elems);
};
* 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 assertNotHasOwnProperty = function(name, context) {
* @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()}.
*/
/**
* @ngdoc method
+ * @name angular.Module#component
+ * @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})
+ *
+ * @description
+ * See {@link ng.$compileProvider#component $compileProvider.component()}.
+ */
+ component: invokeLaterAndSetModuleName('$compileProvider', 'component'),
+
+ /**
+ * @ngdoc method
* @name angular.Module#config
* @module ng
* @param {Function} configFn Execute this function on module load. Useful for service
$AnchorScrollProvider,
$AnimateProvider,
$CoreAnimateCssProvider,
+ $$CoreAnimateJsProvider,
$$CoreAnimateQueueProvider,
- $$CoreAnimateRunnerProvider,
+ $$AnimateRunnerFactoryProvider,
+ $$AnimateAsyncRunFactoryProvider,
$BrowserProvider,
$CacheFactoryProvider,
$ControllerProvider,
+ $DateProvider,
$DocumentProvider,
$ExceptionHandlerProvider,
$FilterProvider,
* - `codeName` – `{string}` – Code name of the release, such as "jiggling-armfat".
*/
var version = {
- full: '1.4.8', // all of these placeholder strings will be replaced by grunt's
+ full: '1.5.5', // all of these placeholder strings will be replaced by grunt's
major: 1, // package task
- minor: 4,
- dot: 8,
- codeName: 'ice-manipulation'
+ minor: 5,
+ dot: 5,
+ codeName: 'material-conspiration'
};
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
$animateCss: $CoreAnimateCssProvider,
+ $$animateJs: $$CoreAnimateJsProvider,
$$animateQueue: $$CoreAnimateQueueProvider,
- $$AnimateRunner: $$CoreAnimateRunnerProvider,
+ $$AnimateRunner: $$AnimateRunnerFactoryProvider,
+ $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider,
$browser: $BrowserProvider,
$cacheFactory: $CacheFactoryProvider,
$controller: $ControllerProvider,
*
* 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**.
*
- * <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>
+ * 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.
*
- * To use `jQuery`, simply ensure it is loaded before the `angular.js` file.
+ * 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.
*
- * <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-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 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:
* - [`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/)
* - `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.
*/
return false;
}
+function jqLiteCleanData(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ jqLiteRemoveData(nodes[i]);
+ }
+}
+
function jqLiteBuildFragment(html, context) {
var tmp, tag, wrap,
fragment = context.createDocumentFragment(),
}
function jqLiteParseHTML(html, context) {
- context = context || document;
+ context = context || window.document;
var parsed;
if ((parsed = SINGLE_TAG_REGEXP.exec(html))) {
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) {
+var jqLiteContains = window.Node.prototype.contains || function(arg) {
// jshint bitwise: false
return !!(this.compareDocumentPosition(arg) & 16);
// jshint bitwise: true
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);
}
// check if document is already loaded
- if (document.readyState === 'complete') {
- setTimeout(trigger);
+ if (window.document.readyState === 'complete') {
+ window.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.
forEach({
data: jqLiteData,
removeData: jqLiteRemoveData,
- hasData: jqLiteHasData
+ hasData: jqLiteHasData,
+ cleanData: jqLiteCleanData
}, function(fn, name) {
JQLite[name] = fn;
});
},
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,
/**
* @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 FN_ARG_SPLIT = /,/;
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
var $injectorMinErr = minErr('$injector');
+function extractArgs(fn) {
+ var fnText = Function.prototype.toString.call(fn).replace(STRIP_COMMENTS, ''),
+ args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
+ return args;
+}
+
function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
- var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
- args = fnText.match(FN_ARGS);
+ var args = extractArgs(fn);
if (args) {
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
}
function annotate(fn, strictDi, name) {
var $inject,
- fnText,
argDecl,
last;
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
- fnText = fn.toString().replace(STRIP_COMMENTS, '');
- argDecl = fnText.match(FN_ARGS);
+ argDecl = extractArgs(fn);
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(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.
* @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.
* @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}.
*
* @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
+ * intercepts the creation of a service, allowing it to override or modify the behavior 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.
*
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),
instanceCache = {},
- instanceInjector = (instanceCache.$injector =
+ protoInstanceInjector =
createInternalInjector(instanceCache, function(serviceName, caller) {
var provider = providerInjector.get(serviceName + providerSuffix, caller);
- return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
- }));
-
+ return instanceInjector.invoke(
+ provider.$get, provider, undefined, serviceName);
+ }),
+ instanceInjector = protoInstanceInjector;
- forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
+ providerCache['$injector' + providerSuffix] = { $get: valueFn(protoInstanceInjector) };
+ var runBlocks = loadModules(modulesToLoad);
+ instanceInjector = protoInstanceInjector.get('$injector');
+ instanceInjector.strictDi = strictDi;
+ forEach(runBlocks, function(fn) { if (fn) instanceInjector.invoke(fn); });
return instanceInjector;
}
}
- 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) {
+ // IE 9-11 do not support classes and IE9 leaks with the code below.
+ if (msie <= 11) {
+ return false;
+ }
+ // Workaround for MS Edge.
+ // Check https://connect.microsoft.com/IE/Feedback/Details/2211653
+ return typeof func === 'function'
+ && /^(?:class\s|constructor\()/.test(Function.prototype.toString.call(func));
+ }
+
+ 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,
: {};
}
-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 = function() {
+ this.$get = noop;
};
// this is prefixed with Core since it conflicts with
addRemoveClassesPostDigest(element, options.addClass, options.removeClass);
}
- return new $$AnimateRunner(); // jshint ignore:line
+ var runner = new $$AnimateRunner(); // jshint ignore:line
+
+ // since there are no animations to run the runner needs to be
+ // notified that the animation call is complete.
+ runner.complete();
+ return runner;
}
};
* // 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
*/
* @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 classNamem, 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.)
+ * (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
*
* @return {Promise} the animation callback promise
}];
}];
+var $$AnimateAsyncRunFactoryProvider = 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) {
+ passed ? callback() : waitForTick(callback);
+ };
+ };
+ }];
+};
+
+var $$AnimateRunnerFactoryProvider = function() {
+ this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout',
+ function($q, $sniffer, $$animateAsyncRun, $document, $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) {
+ var doc = $document[0];
+
+ // the document may not be ready or attached
+ // to the module for some internal tests
+ if (doc && doc.hidden) {
+ 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) {
+ status === false ? reject() : 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;
+ }];
+};
+
/**
* @ngdoc service
* @name $animateCss
* 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.
options.from = null;
}
- var closed, runner = new RAFPromise();
+ /* jshint newcap: false */
+ var closed, runner = new $$AnimateRunner();
return {
start: run,
end: run
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;
*/
function Browser(window, document, $log, $sniffer) {
var self = this,
- rawDocument = document[0],
location = window.location,
history = window.history,
setTimeout = window.setTimeout,
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;
fireUrlChange();
}
- function getCurrentState() {
- try {
- return history.state;
- } catch (e) {
- // MSIE can reportedly throw when there is no state (UNCONFIRMED).
- }
- }
-
// This variable should be used *only* inside the cacheState function.
var lastCachedState = null;
function cacheState() {
* When this property is set to true, 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`
* 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.
*
*
* #### `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. Additionally, a controller
+ * alias must be set, either by using `controllerAs: 'myAlias'` or by specifying the alias in the controller
+ * definition: `controller: 'myCtrl as myAlias'`.
+ *
+ * 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
* * `$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)
* 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 translusion 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).
+ *
+ * The controller can provide the following methods that act as life-cycle hooks:
+ * * `$onInit()` - Called on each controller after all the controllers on an element have been constructed and
+ * had their bindings initialized (and before the pre & 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.
+ * * `$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.
*
*
* #### `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.
+ * 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.
* 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`
*
* <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
* 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
* content and the `scope` is the newly created transclusion scope, to which the clone is bound.
*
* <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>
*
* </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.
*
*
*
* 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
* 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
* 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.
var $compileMinErr = minErr('$compile');
+function UNINITIALIZED_VALUE() {}
+var _UNINITIALIZED_VALUE = new UNINITIALIZED_VALUE();
+
/**
* @ngdoc provider
* @name $compileProvider
// 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) {
optional: match[3] === '?',
attrName: match[4] || scopeName
};
+ if (match[4]) {
+ bindingCache[definition] = bindings[scopeName];
+ }
});
return bindings;
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);
}
}
* @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) {
assertNotHasOwnProperty(name, 'directive');
if (isString(name)) {
assertValidDirectiveName(name);
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.$$moduleName = directiveFactory.$$moduleName;
directives.push(directive);
} catch (e) {
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 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
return debugInfoEnabled;
};
+
+ 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;
+ };
+
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 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;
- var Attributes = function(element, attributesToCopy) {
+ // 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() {
+ for (var i = 0, ii = onChangesQueue.length; i < ii; ++i) {
+ onChangesQueue[i]();
+ }
+ // Reset the queue to trigger a new schedule next time there is a change
+ onChangesQueue = undefined;
+ });
+ } finally {
+ onChangesTtl++;
+ }
+ }
+
+
+ function Attributes(element, attributesToCopy) {
if (attributesToCopy) {
var keys = Object.keys(attributesToCopy);
var i, l, key;
}
this.$$element = element;
- };
+ }
Attributes.prototype = {
/**
nodeName = nodeName_(this.$$element);
- if ((nodeName === 'a' && key === 'href') ||
+ if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
(nodeName === 'img' && key === 'src')) {
// sanitize a[href] and img[src] values
this[key] = value = $$sanitizeUri(value, key === 'src');
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);
+ }
}
}
* @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) {
}
};
+ 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 {
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);
safeAddClass($element, isolated ? 'ng-isolate-scope' : 'ng-scope');
} : noop;
+ compile.$$createComment = function(directiveName, comment) {
+ var content = '';
+ if (debugInfoEnabled) {
+ content = ' ' + (directiveName || '') + ': ' + (comment || '') + ' ';
+ }
+ return window.document.createComment(content);
+ };
+
return compile;
//================================
// modify it.
$compileNodes = jqLite($compileNodes);
}
+
+ var NOT_EMPTY = /\S+/;
+
// 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];
+ for (var i = 0, len = $compileNodes.length; i < len; i++) {
+ var domNode = $compileNodes[i];
+
+ if (domNode.nodeType === NODE_TYPE_TEXT && domNode.nodeValue.match(NOT_EMPTY) /* non-empty */) {
+ jqLiteWrapNode(domNode, $compileNodes[i] = window.document.createElement('span'));
}
- });
+ }
+
var compositeLinkFn =
compileNodes($compileNodes, transcludeFn, $compileNodes,
maxPriority, ignoreDirective, previousCompileContext);
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';
}
}
}
function createBoundTranscludeFn(scope, transcludeFn, previousBoundTranscludeFn) {
-
- var boundTranscludeFn = function(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
+ function boundTranscludeFn(transcludedScope, cloneFn, controllers, futureParentElement, containingScope) {
if (!transcludedScope) {
transcludedScope = scope.$new(false, containingScope);
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) {
+ if (transcludeFn.$$slots[slotName]) {
+ boundSlots[slotName] = createBoundTranscludeFn(scope, transcludeFn.$$slots[slotName], previousBoundTranscludeFn);
+ } else {
+ boundSlots[slotName] = null;
+ }
+ }
return boundTranscludeFn;
}
* @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);
};
}
/**
+ * A function generator that is used to support both eager and lazy compilation
+ * linking function.
+ * @param eager
+ * @param $compileNodes
+ * @param transcludeFn
+ * @param maxPriority
+ * @param ignoreDirective
+ * @param previousCompileContext
+ * @returns {Function}
+ */
+ function compilationGenerator(eager, $compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext) {
+ var compiled;
+
+ if (eager) {
+ return compile($compileNodes, transcludeFn, maxPriority, ignoreDirective, previousCompileContext);
+ }
+ return function lazyCompilation() {
+ 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;
+ }
+ return compiled.apply(this, arguments);
+ };
+ }
+
+ /**
* Once the directives have been collected, their compile functions are executed. This method
* is responsible for inlining directive templates as well as terminating the application
* of the directives if the terminal directive has been reached.
replaceDirective = originalReplaceDirective,
childTranscludeFn = transcludeFn,
linkFn,
+ didScanForMultipleTransclusion = false,
+ mightHaveMultipleTransclusionError = false,
directiveValue;
// executes all directives on the current element
directiveName = directive.name;
+ // If we encounter a condition that can result in transclusion on the directive,
+ // then scan ahead in the remaining directives for others that may cause a multiple
+ // transclusion error to be thrown during the compilation process. If a matching directive
+ // is found, then we know that when we encounter a transcluded directive, we need to eagerly
+ // compile the `transclude` function rather than doing it lazily in order to throw
+ // exceptions at the correct time
+ if (!didScanForMultipleTransclusion && ((directive.replace && (directive.templateUrl || directive.template))
+ || (directive.transclude && !directive.$$tlb))) {
+ var candidateDirective;
+
+ for (var scanningIndex = i + 1; candidateDirective = directives[scanningIndex++];) {
+ if ((candidateDirective.transclude && !candidateDirective.$$tlb)
+ || (candidateDirective.replace && (candidateDirective.templateUrl || candidateDirective.template))) {
+ mightHaveMultipleTransclusionError = true;
+ break;
+ }
+ }
+
+ didScanForMultipleTransclusion = true;
+ }
+
if (!directive.templateUrl && directive.controller) {
directiveValue = directive.controller;
controllerDirectives = controllerDirectives || createMap();
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);
- childTranscludeFn = compile($template, transcludeFn, terminalPriority,
+ // 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:
// - controllerDirectives - otherwise we'll create duplicates controllers
nonTlbTranscludeDirective: nonTlbTranscludeDirective
});
} else {
+
+ var slots = createMap();
+
$template = jqLite(jqLiteClone(compileNode)).contents();
+
+ if (isObject(directiveValue)) {
+
+ // We have transclusion slots,
+ // collect them up, compile them and store their transclusion functions
+ $template = [];
+
+ var slotMap = createMap();
+ var filledSlots = createMap();
+
+ // 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.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);
+ }
+ });
+
+ // Check for required slots that were not filled
+ forEach(filledSlots, function(filled, slotName) {
+ if (!filled) {
+ throw $compileMinErr('reqslot', 'Required transclusion slot `{0}` was not filled.', slotName);
+ }
+ });
+
+ 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
- childTranscludeFn = compile($template, transcludeFn, undefined,
+ childTranscludeFn = compilationGenerator(mightHaveMultipleTransclusionError, $template, transcludeFn, undefined,
undefined, { needsNewScope: directive.$$isolateScope || directive.$$newScope});
+ childTranscludeFn.$$slots = slots;
}
}
replaceDirective = directive;
}
+ /* jshint -W021 */
nodeLinkFn = compileTemplateUrl(directives.splice(i, directives.length - i), $compileNode,
+ /* jshint +W021 */
templateAttrs, jqCollection, hasTranscludeDirective && childTranscludeFn, preLinkFns, postLinkFns, {
controllerDirectives: controllerDirectives,
newScopeDirective: (newScopeDirective !== directive) && newScopeDirective,
}
}
-
- 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;
// 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) {
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);
}
}
var bindings = controllerDirective.$$bindings.bindToController;
if (controller.identifier && bindings) {
- removeControllerBindingWatches =
+ controller.bindingInfo =
initializeDirectiveBindings(controllerScope, attrs, controller.instance, bindings, controllerDirective);
+ } else {
+ controller.bindingInfo = {};
}
var controllerResult = controller();
// from setupControllers
controller.instance = controllerResult;
$element.data('$' + controllerDirective.name + 'Controller', controllerResult);
- removeControllerBindingWatches && removeControllerBindingWatches();
- removeControllerBindingWatches =
+ controller.bindingInfo.removeWatches && controller.bindingInfo.removeWatches();
+ 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)) {
+ controllerInstance.$onChanges(controller.bindingInfo.initialChanges);
+ }
+ if (isFunction(controllerInstance.$onInit)) {
+ controllerInstance.$onInit();
+ }
+ if (isFunction(controllerInstance.$onDestroy)) {
+ controllerScope.$on('$destroy', function callOnDestroyHook() {
+ controllerInstance.$onDestroy();
+ });
+ }
+ });
+
// PRELINKING
for (i = 0, ii = preLinkFns.length; i < ii; i++) {
linkFn = preLinkFns[i];
);
}
+ // 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) {
+ function controllersBoundTransclude(scope, cloneAttachFn, futureParentElement, slotName) {
var transcludeControllers;
-
// No scope passed in:
if (!isScope(scope)) {
+ slotName = futureParentElement;
futureParentElement = cloneAttachFn;
cloneAttachFn = scope;
scope = undefined;
if (!futureParentElement) {
futureParentElement = hasElementTranscludeDirective ? $element.parent() : $element;
}
- return boundTranscludeFn(scope, cloneAttachFn, transcludeControllers, futureParentElement, scopeToChild);
+ 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) {
+ 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));
+ }
+ } else {
+ 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
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;
}
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:
parent.replaceChild(newNode, firstElementToRemove);
}
- // TODO(perf): what's this document fragment for? is it needed? can we at least reuse it?
- var fragment = document.createDocumentFragment();
- fragment.appendChild(firstElementToRemove);
+ // Append all the `elementsToRemove` to a fragment. This will...
+ // - 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 = window.document.createDocumentFragment();
+ for (i = 0; i < removeCount; i++) {
+ fragment.appendChild(elementsToRemove[i]);
+ }
if (jqLite.hasData(firstElementToRemove)) {
// Copy over user data (that includes Angular's $scope etc.). Don't copy private
// event listeners (which is the main use of private data) wouldn't work anyway.
jqLite.data(newNode, jqLite.data(firstElementToRemove));
- // Remove data of the replaced element. We cannot just call .remove()
- // on the element it since that would deallocate scope that is needed
- // for the new node. Instead, remove the data "manually".
- if (!jQuery) {
- delete jqLite.cache[firstElementToRemove[jqLite.expando]];
- } else {
- // jQuery 2.x doesn't expose the data storage. Use jQuery.cleanData to clean up after
- // the replaced element. The cleanData version monkey-patched by Angular would cause
- // the scope to be trashed and we do need the very same scope to work with the new
- // element. However, we cannot just cache the non-patched version and use it here as
- // that would break if another library patches the method after Angular does (one
- // example is jQuery UI). Instead, set a flag indicating scope destroying should be
- // skipped this one time.
- skipDestroyOnNextJQueryCleanData = true;
- jQuery.cleanData([firstElementToRemove]);
- }
+ // Remove $destroy event listeners from `firstElementToRemove`
+ jqLite(firstElementToRemove).off('$destroy');
}
- for (var k = 1, kk = elementsToRemove.length; k < kk; k++) {
- var element = elementsToRemove[k];
- jqLite(element).remove(); // must do this way to clean up expando
- fragment.appendChild(element);
- delete elementsToRemove[k];
- }
+ // Cleanup any data/listeners on the elements and children.
+ // This includes invoking the $destroy event on any elements with listeners.
+ jqLite.cleanData(fragment.querySelectorAll('*'));
+ // Update the jqLite collection to only contain the `newNode`
+ for (i = 1; i < removeCount; i++) {
+ delete elementsToRemove[i];
+ }
elementsToRemove[0] = newNode;
elementsToRemove.length = 1;
}
// only occurs for isolate scopes and new scopes with controllerAs.
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 &
lastValue,
- parentGet, parentSet, compare;
+ parentGet, parentSet, compare, removeWatch;
switch (mode) {
destination[scopeName] = attrs[attrName] = void 0;
}
attrs.$observe(attrName, function(value) {
- if (isString(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]);
break;
case '=':
if (parentGet.literal) {
compare = equals;
} else {
- compare = function(a, b) { return a === b || (a !== a && b !== b); };
+ 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) {
return lastValue = parentValue;
};
parentValueWatch.$stateful = true;
- var removeWatch;
if (definition.collection) {
removeWatch = scope.$watchCollection(attrs[attrName], parentValueWatch);
} else {
removeWatchCollection.push(removeWatch);
break;
+ case '<':
+ if (!hasOwnProperty.call(attrs, attrName)) {
+ if (optional) break;
+ attrs[attrName] = void 0;
+ }
+ if (optional && !attrs[attrName]) break;
+
+ parentGet = $parse(attrs[attrName]);
+
+ destination[scopeName] = parentGet(scope);
+ initialChanges[scopeName] = new SimpleChange(_UNINITIALIZED_VALUE, destination[scopeName]);
+
+ removeWatch = scope.$watch(parentGet, function parentValueWatchAction(newValue, oldValue) {
+ if (newValue === oldValue) {
+ // If the new and old values are identical then this is the first time the watch has been triggered
+ // So instead we use the current value on the destination as the old value
+ oldValue = destination[scopeName];
+ }
+ recordChanges(scopeName, newValue, oldValue);
+ destination[scopeName] = newValue;
+ }, parentGet.literal);
+
+ removeWatchCollection.push(removeWatch);
+ break;
+
case '&':
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
}
});
- 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) {
+ // 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]();
+ }
}
};
}
}];
}
+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;
/**
* Converts all accepted directives format into proper directive name.
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)) {
/**
* @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.
* 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
}
var instantiate;
- return instantiate = extend(function() {
+ return instantiate = extend(function $controllerInit() {
var result = $injector.invoke(expression, instance, locals, constructor);
if (result !== instance && (isObject(result) || isFunction(result))) {
instance = result;
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 {
*
* 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'`.
*
* ```
* module.run(function($http) {
- * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w'
+ * $http.defaults.headers.common.Authorization = 'Basic YmVlcDpib29w';
* });
* ```
*
* 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
*
* ## 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
+ *
+ * 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.
*
- * Note that even if the response is served from cache, delivery of the data is asynchronous in
- * the same way that real requests are.
+ * 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.
*
- * 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.
+ * 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.
*
- * 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.
+ * 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
*
*
* ### 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
* - **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** –
* 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
* 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
*/
function $http(requestConfig) {
- if (!angular.isObject(requestConfig)) {
+ if (!isObject(requestConfig)) {
throw minErr('$http')('badreq', 'Http request configuration must be an object. Received: {0}', requestConfig);
}
+ if (!isString(requestConfig.url)) {
+ throw minErr('$http')('badreq', 'Http request configuration url must be a string. Received: {0}', requestConfig.url);
+ }
+
var config = extend({
method: 'get',
transformRequest: defaults.transformRequest,
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);
}
$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():
function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDocument) {
// TODO(vojta): fix the signature
- return function(method, url, post, callback, headers, timeout, withCredentials, responseType) {
+ return function(method, url, post, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers) {
$browser.$$incOutstandingRequestCount();
url = url || $browser.url();
xhr.onerror = requestError;
xhr.onabort = 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;
}
*
* Used for configuring the interpolation markup. Defaults to `{{` and `}}`.
*
+ * <div class="alert alert-danger">
+ * This feature is sometimes used to mix different markup languages, e.g. to wrap an Angular
+ * template within a Python Jinja template (or any other template language). Mixing templating
+ * languages is **very dangerous**. The embedding template language will not safely escape Angular
+ * expressions, so any user-controlled values in the template will cause Cross Site Scripting (XSS)
+ * security bugs!
+ * </div>
+ *
* @example
-<example module="customInterpolationApp">
+<example name="custom-interpolation-markup" module="customInterpolationApp">
<file name="index.html">
<script>
var customInterpolationApp = angular.module('customInterpolationApp', []);
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>
return value;
}
+ //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) {
+ unwatch();
+ return constantInterp(scope);
+ }, listener, objectEquality);
+ }
+
/**
* @ngdoc service
* @name $interpolate
* - `context`: evaluation context for all expressions embedded in the interpolated text
*/
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
+ // Provide a quick exit and simplified result function for text with no interpolation
+ if (!text.length || text.indexOf(startSymbol) === -1) {
+ var constantInterp;
+ if (!mustHaveExpression) {
+ var unescapedText = unescapeText(text);
+ constantInterp = valueFn(unescapedText);
+ constantInterp.exp = text;
+ constantInterp.expressions = [];
+ constantInterp.$$watchDelegate = constantWatchDelegate;
+ }
+ return constantInterp;
+ }
+
allOrNothing = !!allOrNothing;
var startIndex,
endIndex,
}
function $IntervalProvider() {
- this.$get = ['$rootScope', '$window', '$q', '$$q',
- function($rootScope, $window, $q, $$q) {
+ this.$get = ['$rootScope', '$window', '$q', '$$q', '$browser',
+ function($rootScope, $window, $q, $$q, $browser) {
var intervals = {};
count = isDefined(count) ? count : 0;
- promise.then(null, null, (!hasParams) ? fn : function() {
- fn.apply(null, args);
- });
-
promise.$$intervalId = setInterval(function tick() {
+ if (skipApply) {
+ $browser.defer(callback);
+ } else {
+ $rootScope.$evalAsync(callback);
+ }
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
intervals[promise.$$intervalId] = deferred;
return promise;
+
+ function callback() {
+ if (!hasParams) {
+ fn(iteration);
+ } else {
+ fn.apply(null, args);
+ }
+ }
}
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;
+ // 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 ensureSafeObject(obj, fullExpression) {
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});
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);
+ /*jshint bitwise: false*/
+ return (ch.charCodeAt(0) << 10) + ch.charCodeAt(1) - 0x35FDC00;
+ /*jshint bitwise: true*/
+ },
+
+ 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));
},
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,
AST.Property = 'Property';
AST.ObjectExpression = 'ObjectExpression';
AST.ThisExpression = 'ThisExpression';
+AST.LocalsExpression = 'LocalsExpression';
// Internal use only
AST.NGValueParameter = 'NGValueParameter';
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) {
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 }
}
};
ast.constant = false;
ast.toWatch = [];
break;
+ case AST.LocalsExpression:
+ ast.constant = false;
+ ast.toWatch = [];
+ break;
}
}
intoId = intoId || this.nextId();
self.recurse(ast.object, left, undefined, function() {
self.if_(self.notNull(left), function() {
+ if (create && create !== 1) {
+ self.addEnsureSafeAssignContext(left);
+ }
if (ast.computed) {
right = self.nextId();
self.recurse(ast.property, right);
right = this.nextId();
left = {};
if (!isAssignable(ast.left)) {
- throw $parseMinErr('lval', 'Trying to assing a value to a non l-value');
+ 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() {
this.assign(intoId, 's');
recursionFn('s');
break;
+ case AST.LocalsExpression:
+ this.assign(intoId, 'l');
+ recursionFn('l');
+ break;
case AST.NGValueParameter:
this.assign(intoId, 'v');
recursionFn('v');
},
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) {
},
getStringValue: function(item) {
- this.assign(item, 'getStringValue(' + item + ',text)');
+ this.assign(item, 'getStringValue(' + item + ')');
},
ensureSafeAssignContext: function(item) {
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;
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;
};
}
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) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[rhs])) {
+ lhs[rhs] = {};
+ }
}
value = lhs[rhs];
ensureSafeObject(value, expression);
nonComputedMember: function(left, right, expensiveChecks, context, create, expression) {
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) {
+ ensureSafeAssignContext(lhs);
+ if (lhs && !(lhs[right])) {
+ lhs[right] = {};
+ }
}
var value = lhs != null ? lhs[right] : undefined;
if (expensiveChecks || isPossiblyDangerousMemberName(right)) {
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);
};
}
};
-var getterFnCacheDefault = createMap();
-var getterFnCacheExpensive = createMap();
-
function isPossiblyDangerousMemberName(name) {
return name == 'constructor';
}
function $ParseProvider() {
var cacheDefault = createMap();
var cacheExpensive = 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 extensivelly, 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
+ expensiveChecks: false,
+ literals: copy(literals),
+ isIdentifierStart: isFunction(identStart) && identStart,
+ isIdentifierContinue: isFunction(identContinue) && identContinue
},
$parseOptionsExpensive = {
csp: noUnsafeEval,
- expensiveChecks: true
+ expensiveChecks: true,
+ literals: copy(literals),
+ isIdentifierStart: isFunction(identStart) && identStart,
+ isIdentifierContinue: isFunction(identContinue) && identContinue
};
+ var runningChecksEnabled = false;
+
+ $parse.$$runningExpensiveChecks = function() {
+ return runningChecksEnabled;
+ };
+
+ return $parse;
- return function $parse(exp, interceptorFn, expensiveChecks) {
+ function $parse(exp, interceptorFn, expensiveChecks) {
var parsedExpression, oneTime, cacheKey;
+ expensiveChecks = expensiveChecks || runningChecksEnabled;
+
switch (typeof exp) {
case 'string':
exp = exp.trim();
} else if (parsedExpression.inputs) {
parsedExpression.$$watchDelegate = inputsWatchDelegate;
}
+ if (expensiveChecks) {
+ parsedExpression = expensiveChecksInterceptor(parsedExpression);
+ }
cache[cacheKey] = parsedExpression;
}
return addInterceptor(parsedExpression, interceptorFn);
return addInterceptor(exp, interceptorFn);
default:
- return noop;
+ return addInterceptor(noop, interceptorFn);
}
- };
+ }
+
+ function expensiveChecksInterceptor(fn) {
+ if (!fn) return fn;
+ expensiveCheckFn.$$watchDelegate = fn.$$watchDelegate;
+ expensiveCheckFn.assign = expensiveChecksInterceptor(fn.assign);
+ expensiveCheckFn.constant = fn.constant;
+ expensiveCheckFn.literal = fn.literal;
+ for (var i = 0; fn.inputs && i < fn.inputs.length; ++i) {
+ fn.inputs[i] = expensiveChecksInterceptor(fn.inputs[i]);
+ }
+ expensiveCheckFn.inputs = fn.inputs;
+
+ return expensiveCheckFn;
+
+ function expensiveCheckFn(scope, locals, assign, inputs) {
+ var expensiveCheckOldValue = runningChecksEnabled;
+ runningChecksEnabled = true;
+ try {
+ return fn(scope, locals, assign, inputs);
+ } finally {
+ runningChecksEnabled = expensiveCheckOldValue;
+ }
+ }
+ }
function expressionInputDirtyCheck(newValue, oldValueOfValue) {
function constantWatchDelegate(scope, listener, objectEquality, parsedExpression) {
var unwatch;
return unwatch = scope.$watch(function constantWatch(scope) {
- return parsedExpression(scope);
- }, function constantListener(value, old, scope) {
- if (isFunction(listener)) {
- listener.apply(this, arguments);
- }
unwatch();
- }, objectEquality);
+ return parsedExpression(scope);
+ }, listener, objectEquality);
}
function addInterceptor(parsedExpression, interceptorFn) {
* [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:
*
* 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.
*
* - 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) {
*/
function qFactory(nextTick, exceptionHandler) {
var $qMinErr = minErr('$q', TypeError);
- function callOnce(self, resolveFn, rejectFn) {
- var called = false;
- function wrap(fn) {
- return function(value) {
- if (called) return;
- called = true;
- fn.call(self, value);
- };
- }
-
- return [wrap(resolveFn), wrap(rejectFn)];
- }
/**
* @ngdoc method
* @returns {Deferred} Returns a new instance of deferred.
*/
var defer = function() {
- return new Deferred();
+ 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 Promise() {
function Deferred() {
this.promise = new Promise();
- //Necessary to support unbound execution :/
- this.resolve = simpleBind(this, this.resolve);
- this.reject = simpleBind(this, this.reject);
- this.notify = simpleBind(this, this.notify);
}
extend(Deferred.prototype, {
},
$$resolve: function(val) {
- var then, fns;
-
- fns = callOnce(this, this.$$resolve, this.$$reject);
+ 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, fns[0], fns[1], this.notify);
+ 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) {
- fns[1](e);
+ rejectPromise(e);
exceptionHandler(e);
}
+
+ function resolvePromise(val) {
+ if (done) return;
+ done = true;
+ that.$$resolve(val);
+ }
+ function rejectPromise(val) {
+ if (done) return;
+ done = true;
+ that.$$reject(val);
+ }
},
reject: function(reason) {
throw $qMinErr('norslvr', "Expected resolverFn, got '{0}'", resolver);
}
- if (!(this instanceof Q)) {
- // More useful when $Q is the Promise itself.
- return new Q(resolver);
- }
-
var deferred = new Deferred();
function resolveFn(value) {
return deferred.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;
return ChildScope;
}
- this.$get = ['$injector', '$exceptionHandler', '$parse', '$browser',
- function($injector, $exceptionHandler, $parse, $browser) {
+ this.$get = ['$exceptionHandler', '$parse', '$browser',
+ function($exceptionHandler, $parse, $browser) {
function destroyChildScope($event) {
$event.currentScope.$$destroyed = true;
* - `newVal` contains the current value of the `watchExpression`
* - `oldVal` contains the previous value of the `watchExpression`
* - `scope` refers to the current scope
- * @param {boolean=} objectEquality Compare for object equality using {@link angular.equals} instead of
+ * @param {boolean=} [objectEquality=false] Compare for object equality using {@link angular.equals} instead of
* comparing for reference equality.
* @returns {function()} Returns a deregistration function for this listener.
*/
*
*/
$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
// 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'
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] = [];
});
}
- asyncQueue.push({scope: this, expression: expr, locals: locals});
+ asyncQueue.push({scope: this, expression: $parse(expr), locals: locals});
},
$$postDigest: function(fn) {
$applyAsync: function(expr) {
var scope = this;
expr && applyAsyncQueue.push($applyAsyncExpression);
+ expr = $parse(expr);
scheduleApplyAsync();
function $applyAsyncExpression() {
}
/**
+ * @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
+
+/**
* @description
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
* @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.
*
* @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.
*
* returns the originally supplied value if the queried context type is a supertype of the
* created type. If this condition isn't satisfied, throws an exception.
*
+ * <div class="alert alert-danger">
+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ * </div>
+ *
* @param {string} type The kind of context in which this value is to be used.
* @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
* `$sceDelegate.trustAs`} call.
function $SnifferProvider() {
this.$get = ['$window', '$document', function($window, $document) {
var eventSupport = {},
+ // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by
+ // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index)
+ isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime,
+ hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
android =
toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
// 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),
+ history: !!(hasHistoryPushState && !(android < 4) && !boxee),
// jshint +W018
hasEvent: function(event) {
// IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
}];
}
-var $compileMinErr = minErr('$compile');
+var $templateRequestMinErr = minErr('$compile');
/**
- * @ngdoc service
- * @name $templateRequest
- *
+ * @ngdoc provider
+ * @name $templateRequestProvider
* @description
- * The `$templateRequest` service runs security checks then downloads the provided template using
- * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
- * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
- * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
- * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
- * when `tpl` is of type string and `$templateCache` has the matching entry.
- *
- * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
- * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
+ * Used to configure the options passed to the {@link $http} service when making a template request.
*
- * @return {Promise} a promise for the HTTP response data of the given URL.
- *
- * @property {number} totalPendingRequests total amount of pending template requests being downloaded.
+ * For example, it can be used for specifying the "Accept" header that is sent to the server, when
+ * requesting a template.
*/
function $TemplateRequestProvider() {
+
+ var httpOptions;
+
+ /**
+ * @ngdoc method
+ * @name $templateRequestProvider#httpOptions
+ * @description
+ * The options to be passed to the {@link $http} service when making the request.
+ * You can use this to override options such as the "Accept" header for template requests.
+ *
+ * The {@link $templateRequest} will set the `cache` and the `transformResponse` properties of the
+ * options if not overridden here.
+ *
+ * @param {string=} value new value for the {@link $http} options.
+ * @returns {string|self} Returns the {@link $http} options when used as getter and self if used as setter.
+ */
+ this.httpOptions = function(val) {
+ if (val) {
+ httpOptions = val;
+ return this;
+ }
+ return httpOptions;
+ };
+
+ /**
+ * @ngdoc service
+ * @name $templateRequest
+ *
+ * @description
+ * The `$templateRequest` service runs security checks then downloads the provided template using
+ * `$http` and, upon success, stores the contents inside of `$templateCache`. If the HTTP request
+ * fails or the response data of the HTTP request is empty, a `$compile` error will be thrown (the
+ * exception can be thwarted by setting the 2nd parameter of the function to true). Note that the
+ * contents of `$templateCache` are trusted, so the call to `$sce.getTrustedUrl(tpl)` is omitted
+ * when `tpl` is of type string and `$templateCache` has the matching entry.
+ *
+ * If you want to pass custom options to the `$http` service, such as setting the Accept header you
+ * can configure this via {@link $templateRequestProvider#httpOptions}.
+ *
+ * @param {string|TrustedResourceUrl} tpl The HTTP request template URL
+ * @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
+ *
+ * @return {Promise} a promise for the HTTP response data of the given URL.
+ *
+ * @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++;
transformResponse = null;
}
- var httpOptions = {
- cache: $templateCache,
- transformResponse: transformResponse
- };
-
- return $http.get(tpl, httpOptions)
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
['finally'](function() {
handleRequestFn.totalPendingRequests--;
})
function handleError(resp) {
if (!ignoreRequestError) {
- throw $compileMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
+ throw $templateRequestMinErr('tpload', 'Failed to load template: {0} (HTTP status: {1} {2})',
tpl, resp.status, resp.statusText);
}
return $q.reject(resp);
* @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) {
// 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);
return (val === null) ? 'null' : typeof val;
}
+var MAX_DIGITS = 22;
+var DECIMAL_SEP = '.';
+var ZERO_CHAR = '0';
+
/**
* @ngdoc filter
* @name currency
* 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.
*
*
* @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">
</file>
</example>
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
};
}
-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++) {/* jshint noempty: false */}
- 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 };
+}
- // 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);
+/**
+ * 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;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
+ // 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 i, pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[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;
- }
- formatedText += whole.charAt(i);
- }
- }
+ if (roundAt > 0) {
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i) % lgroup === 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++;
+ }
+ digits.unshift(1);
+ parsedNumber.i++;
+ } else {
+ digits[roundAt - 1]++;
}
- formatedText += whole.charAt(i);
}
- // format fraction part.
- while (fraction.length < fractionSize) {
- fraction += '0';
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(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 '';
+
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ 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);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
- parts.push(isNegative ? pattern.negPre : pattern.posPre,
- formatedText,
- isNegative ? pattern.negSuf : pattern.posSuf);
- return parts.join('');
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.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);
}
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
value += offset;
}
if (value === 0 && offset == -12) value = 12;
- return padNumber(value, size, trim);
+ 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];
};
}
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),
GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))(.*)/,
NUMBER_STRING = /^\-?\d+$/;
/**
* * `'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)
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;
* 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.
*
- * @param {Array} array The array to sort.
+ * @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.
*
* `reverse` is not set, which means it defaults to `false`.
<example module="orderByExample">
<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">
<tr>
</table>
</div>
</file>
+ <file name="script.js">
+ 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}];
+ }]);
+ </file>
</example>
*
* The predicate and reverse parameters can be controlled dynamically through scope properties,
* @example
<example module="orderByExample">
<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>
<hr/>
- [ <a href="" ng-click="predicate=''">unsorted</a> ]
+ <button ng-click="predicate=''">Set to unsorted</button>
<table class="friend">
<tr>
- <th>
- <a href="" ng-click="order('name')">Name</a>
- <span class="sortorder" ng-show="predicate === '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>
- </th>
- <th>
- <a href="" ng-click="order('age')">Age</a>
- <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
- </th>
+ <th>
+ <button ng-click="order('name')">Name</button>
+ <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('age')">Age</button>
+ <span class="sortorder" ng-show="predicate === 'age'" ng-class="{reverse:reverse}"></span>
+ </th>
</tr>
<tr ng-repeat="friend in friends | orderBy:predicate:reverse">
<td>{{friend.name}}</td>
</table>
</div>
</file>
+ <file name="script.js">
+ 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;
+ };
+ }]);
+ </file>
+ <file name="style.css">
+ .sortorder:after {
+ content: '\25b2';
+ }
+ .sortorder.reverse:after {
+ content: '\25bc';
+ }
+ </file>
</example>
*
* It's also possible to call the orderBy filter manually, by injecting `$filter`, retrieving the
* @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>
+ <div ng-controller="ExampleController">
+ <pre>Sorting predicate = {{predicate}}; reverse = {{reverse}}</pre>
+ <table class="friend">
+ <tr>
+ <th>
+ <button ng-click="order('name')">Name</button>
+ <span class="sortorder" ng-show="predicate === 'name'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('phone')">Phone Number</button>
+ <span class="sortorder" ng-show="predicate === 'phone'" ng-class="{reverse:reverse}"></span>
+ </th>
+ <th>
+ <button ng-click="order('age')">Age</button>
+ <span class="sortorder" ng-show="predicate === '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">
{ 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 = function(predicate) {
+ $scope.predicate = predicate;
+ $scope.reverse = ($scope.predicate === predicate) ? !$scope.reverse : false;
+ $scope.friends = orderBy($scope.friends, predicate, $scope.reverse);
};
- $scope.order('-age',false);
+ $scope.order('age', true);
}]);
</file>
+
+ <file name="style.css">
+ .sortorder:after {
+ content: '\25b2';
+ }
+ .sortorder.reverse:after {
+ content: '\25bc';
+ }
+ </file>
</example>
*/
orderByFilter.$inject = ['$parse'];
function orderByFilter($parse) {
return function(array, sortPredicate, reverseOrder) {
- if (!(isArrayLike(array))) return array;
+ if (array == null) return array;
+ if (!isArrayLike(array)) {
+ throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array);
+ }
if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
if (sortPredicate.length === 0) { sortPredicate = ['+']; }
* {@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>
* 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>
<file name="index.html">
* @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.
+ *
+ * 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>
<file name="index.html">
* @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.
*
* @example
<example>
* @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.
+ *
* @example
<example>
<file name="index.html">
*
* 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.
*
* 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.
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#!:.?+=&%@\-/]*)?$/;
+// 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 555555555555555555555555 666 77777777 8888888 999
+var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+\])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
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)$/;
+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 = {
/**
* @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
*
* @description
* Input with time validation and transformation. In browsers that do not yet support
- * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
*
}]);
</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">
* @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
* @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
* @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
if (!$sniffer.android) {
var composing = false;
- element.on('compositionstart', function(data) {
+ element.on('compositionstart', function() {
composing = true;
});
});
}
+ var timeout;
+
var listener = function(ev) {
if (timeout) {
$browser.defer.cancel(timeout);
if ($sniffer.hasEvent('input')) {
element.on('input', listener);
} else {
- var timeout;
-
var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
// 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, 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;
if (nativeValidation) {
ctrl.$parsers.push(function(value) {
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
- // Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430):
- // - also sets validity.badInput (should only be validity.typeMismatch).
- // - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email)
- // - can ignore this case as we can still read out the erroneous email...
- return validity.badInput && !validity.typeMismatch ? undefined : value;
+ return validity.badInput || validity.typeMismatch ? undefined : value;
});
}
}
* @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
* @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
updateClasses(oldClasses, newClasses);
}
}
- oldVal = shallowCopy(newVal);
+ if (isArray(newVal)) {
+ oldVal = newVal.map(function(v) { return shallowCopy(v); });
+ } else {
+ oldVal = shallowCopy(newVal);
+ }
}
}
};
* new classes added.
*
* @animations
- * **add** - happens just before the class is applied to the elements
- *
- * **remove** - happens just before the class is removed from the element
+ * | 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
*
* * 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:
*
* 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.
* 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
</file>
</example>
*/
-var ngIfDirective = ['$animate', function($animate) {
+var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
multiElement: true,
transclude: 'element',
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.
* 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.
*
//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;
currentScope.$emit('$includeContentLoaded', src);
scope.$eval(onloadExp);
}, function() {
+ if (scope.$$destroyed) return;
+
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError', src);
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});
DIRTY_CLASS = 'ng-dirty',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
- PENDING_CLASS = 'ng-pending';
+ PENDING_CLASS = 'ng-pending',
+ EMPTY_CLASS = 'ng-empty',
+ NOT_EMPTY_CLASS = 'ng-not-empty';
var ngModelMinErr = minErr('ngModel');
};
ngModelSet = function($scope, newValue) {
if (isFunction(parsedNgModel($scope))) {
- invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
+ invokeModelSetter($scope, {$$$p: newValue});
} else {
- parsedNgModelAssign($scope, ctrl.$modelValue);
+ parsedNgModelAssign($scope, newValue);
}
};
} else if (!parsedNgModel.assign) {
* 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.
*/
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);
+ } else {
+ $animate.removeClass($element, EMPTY_CLASS);
+ $animate.addClass($element, NOT_EMPTY_CLASS);
+ }
+ };
+
+
var currentValidationRunId = 0;
/**
* which may be caused by a pending debounced event or because the input is waiting for a 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, you can have a situation where there is 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.
*
* 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) {
+ * $scope.model = {};
+ *
+ * $scope.setEmpty = function(e, value, rollback) {
* if (e.keyCode == 27) {
- * $scope.myValue = '';
+ * 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() {
forEach(ctrl.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
- throw ngModelMinErr("$asyncValidators",
+ 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);
}));
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
* 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.
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
+ ctrl.$$updateEmptyClasses(viewValue);
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
* require.
* - Providing validation behavior (i.e. required, number, email, url).
* - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`, `ng-untouched`) including animations.
+ * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
+ * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
* - Registering the control with its parent {@link ng.directive:form form}.
*
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
* - {@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.
* - `ng-touched`: the control has been blurred
* - `ng-untouched`: the control hasn't been blurred
* - `ng-pending`: any `$asyncValidators` are unfulfilled
+ * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
+ * by the {@link ngModel.NgModelController#$isEmpty} method
+ * - `ng-not-empty`: the view contains a non-empty value
*
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
* ## Animation Hooks
*
* Animations within models are triggered when any of the associated CSS classes are added and removed
- * on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
+ * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
* `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
* The animations that are triggered within ngModel are similar to how they work in ngClass and
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
});
}
- element.on('blur', function(ev) {
+ element.on('blur', function() {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
// jshint maxlen: 100
-var ngOptionsDirective = ['$compile', '$parse', function($compile, $parse) {
+var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
function parseOptionsExpression(optionsExp, selectElement, scope) {
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
// 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) {
- // if ngModel is not defined, we don't need to do anything
- var ngModelCtrl = ctrls[1];
- if (!ngModelCtrl) return;
-
var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
var multiple = attr.multiple;
// The emptyOption allows the application developer to provide their own custom "empty"
var options;
var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
-
+ // 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();
var renderEmptyOption = function() {
if (!providedEmptyOption) {
selectCtrl.writeValue = function writeNgOptionsValue(value) {
var option = options.getOptionFromViewValue(value);
- if (option && !option.disabled) {
+ 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();
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();
if (value) {
value.forEach(function(item) {
var option = options.getOptionFromViewValue(item);
- if (option && !option.disabled) option.element.selected = true;
+ if (option) option.element.selected = true;
});
}
};
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();
// ------------------------------------------------------------------ //
+ function addOptionElement(option, parent) {
+ var optionElement = optionTemplate.cloneNode(false);
+ parent.appendChild(optionElement);
+ updateOptionElement(option, optionElement);
+ }
+
function updateOptionElement(option, element) {
option.element = element;
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;
- }
- }
-
-
- function skipEmptyAndUnknownOptions(current) {
- var emptyOption_ = emptyOption && emptyOption[0];
- var unknownOption_ = unknownOption && unknownOption[0];
+ function updateOptions() {
+ var previousValue = options && selectCtrl.readValue();
- // 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 (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);
}
- currentElement = skipEmptyAndUnknownOptions(currentElement);
-
- options.items.forEach(function updateOption(option) {
- var group;
+ options.items.forEach(function addOption(option) {
var groupElement;
- var optionElement;
- if (option.group) {
+ 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;
// 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();
}
return {
restrict: 'A',
terminal: true,
- require: ['select', '?ngModel'],
+ require: ['select', 'ngModel'],
link: {
pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
// Deactivate the SelectController.register method to prevent
}
// 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.
+ // In JS `NaN !== NaN`, so we have to explicitly check.
if ((count !== lastCount) && !(countIsNaN && isNumber(lastCount) && isNaN(lastCount))) {
watchRemover();
var whenExpFn = whensExpFns[count];
* <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 limitations compared to array iteration:
+ *
+ * - 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).
*
- * 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).
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by Angular for public (`$`) and private (`$$`) properties.
*
- * 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
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw 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.
*
* by the identifier instead of the whole object. 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
* 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
* 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. New (entering) and removed (leaving) items are animated.
+ <example module="ngRepeat" name="ngRepeat" deps="angular-animate.js" animations="true">
<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">
</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;
}
.animate-repeat {
- line-height:40px;
+ line-height:30px;
list-style:none;
box-sizing:border-box;
}
.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">
</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');
$$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 (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);
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
* .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
+ * 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 handle the style toggling automatically 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
* .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
+ * 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 handle the style toggling automatically 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
* </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
*
</file>
</example>
*/
-var ngSwitchDirective = ['$animate', function($animate) {
+var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
return {
require: 'ngSwitch',
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);
* @description
* Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
*
- * Any existing content of the element that this directive is placed on will be removed before the transcluded content is inserted.
+ * 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.
+ *
+ * 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, 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, is empty
+ * or its value is the same as the name of the attribute then the default slot is used.
+ *
* @example
- <example 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}}">{{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>
+ *
+ * @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">
+ * <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
+ * 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) {
+
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ // If the attribute is of the form: `ng-transclude="ng-transclude"`
+ // then treat it like the default
+ $attrs.ngTransclude = '';
+ }
+
+ function ngTranscludeCloneAttachFn(clone) {
+ if (clone.length) {
+ $element.empty();
+ $element.append(clone);
+ }
+ }
+
if (!$transclude) {
- throw minErr('ngTransclude')('orphan',
+ throw ngTranscludeMinErr('orphan',
'Illegal use of ngTransclude directive in the template! ' +
'No parent directive that requires a transclusion found. ' +
'Element: {0}',
startingTag($element));
}
- $transclude(function(clone) {
- $element.empty();
- $element.append(clone);
- });
+ // If there is no slot name defined or the slot name is not optional
+ // then transclude the slot
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
}
});
* added `<option>` elements, perhaps by an `ngRepeat` directive.
*/
var SelectController =
- ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
+ ['$element', '$scope', function($element, $scope) {
var self = this,
optionsMap = new HashMap();
//
// 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'));
self.renderUnknownOption = function(val) {
var unknownVal = '? ' + hashKey(val) + ' ?';
self.unknownOption.val(unknownVal);
// 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.emptyOption = element;
*
* 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
*
* <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
+ * When the model needs to be bound to a non-string value, you must either explicitly 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>
controller: SelectController,
priority: 1,
link: {
- pre: selectPreLink
+ pre: selectPreLink,
+ post: selectPostLink
}
};
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);
- };
-
// 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`
}
}
+
+ 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);
+ };
+ }
};
restrict: 'E',
priority: 100,
compile: function(element, attr) {
-
if (isDefined(attr.value)) {
// If the value attribute is defined, check if it contains an interpolation
var interpolateValueFn = $interpolate(attr.value, true);
}
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',
terminal: false
});
+/**
+ * @ngdoc directive
+ * @name ngRequired
+ *
+ * @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',
};
};
+/**
+ * @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',
};
};
+/**
+ * @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',
};
};
+/**
+ * @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',
if (window.angular.bootstrap) {
//AngularJS is already loaded, so we can return here...
- console.log('WARNING: Tried to load angular more than once.');
+ if (window.console) {
+ console.log('WARNING: Tried to load angular more than once.');
+ }
return;
}
"Nov",
"Dec"
],
+ "STANDALONEMONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
"WEEKENDRANGE": [
5,
6
]
},
"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;}
});
}]);
*/
angular.scenario.setUpAndRun = function(config) {
var href = window.location.href;
- var body = _jQuery(document.body);
+ var body = _jQuery(window.document.body);
var output = [];
var objModel = new angular.scenario.ObjectModel($runner);
evnt = new TransitionEvent(eventType, eventData);
}
catch (e) {
- evnt = document.createEvent('TransitionEvent');
+ evnt = window.document.createEvent('TransitionEvent');
evnt.initTransitionEvent(eventType, null, null, null, eventData.elapsedTime || 0);
}
}
evnt = new AnimationEvent(eventType, eventData);
}
catch (e) {
- evnt = document.createEvent('AnimationEvent');
+ evnt = window.document.createEvent('AnimationEvent');
evnt.initAnimationEvent(eventType, null, null, null, eventData.elapsedTime || 0);
}
}
} else if (/touch/.test(eventType) && supportsTouchEvents()) {
evnt = createTouchEvent(element, eventType, x, y);
} else {
- evnt = document.createEvent('MouseEvents');
+ evnt = window.document.createEvent('MouseEvents');
x = x || 0;
y = y || 0;
evnt.initMouseEvent(eventType, true, true, window, 0, x, y, x, y, pressed('ctrl'),
if ('_cached' in supportsTouchEvents) {
return supportsTouchEvents._cached;
}
- if (!document.createTouch || !document.createTouchList) {
+ if (!window.document.createTouch || !window.document.createTouchList) {
supportsTouchEvents._cached = false;
return false;
}
try {
- document.createEvent('TouchEvent');
+ window.document.createEvent('TouchEvent');
} catch (e) {
supportsTouchEvents._cached = false;
return false;
}
function createTouchEvent(element, eventType, x, y) {
- var evnt = new Event(eventType);
+ var evnt = new window.Event(eventType);
x = x || 0;
y = y || 0;
- var touch = document.createTouch(window, element, Date.now(), x, y, x, y);
- var touches = document.createTouchList(touch);
+ var touch = window.document.createTouch(window, element, Date.now(), x, y, x, y);
+ var touches = window.document.createTouchList(touch);
evnt.touches = touches;
self.context.find('#test-frames').append('<iframe>');
frame = self.getFrame_();
- frame.load(function() {
+ frame.on('load', function() {
frame.off();
try {
var $window = self.getWindow_();
*/
angular.scenario.dsl('input', function() {
var chain = {};
- var supportInputEvent = 'oninput' in document.createElement('div') && !(msie && msie <= 11);
+ var supportInputEvent = 'oninput' in window.document.createElement('div') && !msie;
chain.enter = function(value, event) {
return this.addFutureAction("input '" + this.name + "' enter '" + value + "'",
publishExternalAPI(angular);
var $runner = new angular.scenario.Runner(window),
- scripts = document.getElementsByTagName('script'),
+ scripts = window.document.getElementsByTagName('script'),
script = scripts[scripts.length - 1],
config = {};
});
if (config.autotest) {
- JQLite(document).ready(function() {
+ JQLite(window.document).ready(function() {
angular.scenario.setUpAndRun(config);
});
}
-})(window, document);
+})(window);
!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend('<style type="text/css">@charset "UTF-8";\n\n[ng\\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],\n.ng-cloak, .x-ng-cloak,\n.ng-hide:not(.ng-hide-animate) {\n display: none !important;\n}\n\nng\\:form {\n display: block;\n}\n\n.ng-animate-shim {\n visibility:hidden;\n}\n\n.ng-anchor {\n position:absolute;\n}\n</style>');