to learn more about them.
- * You can ensure your document is in standards mode and not quirks mode by adding ``
- * to the top of your HTML document.
- *
- * SCE assists in writing code in way that (a) is secure by default and (b) makes auditing for
- * security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
- *
- * Here's an example of a binding in a privileged context:
- *
- *
- *
- *
- *
- *
- * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE
- * disabled, this application allows the user to render arbitrary HTML into the DIV.
- * In a more realistic example, one may be rendering user comments, blog articles, etc. via
- * bindings. (HTML is just one example of a context where rendering user controlled input creates
- * security vulnerabilities.)
- *
- * For the case of HTML, you might use a library, either on the client side, or on the server side,
- * to sanitize unsafe HTML before binding to the value and rendering it in the document.
- *
- * How would you ensure that every place that used these types of bindings was bound to a value that
- * was sanitized by your library (or returned as safe for rendering by your server?) How can you
- * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some
- * properties/fields and forgot to update the binding to the sanitized value?
+ $$postDigest: function(fn) {
+ postDigestQueue.push(fn);
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$apply
+ * @kind function
+ *
+ * @description
+ * `$apply()` is used to execute an expression in AngularJS from outside of the AngularJS
+ * framework. (For example from browser DOM events, setTimeout, XHR or third party libraries).
+ * Because we are calling into the AngularJS framework we need to perform proper scope life
+ * cycle of {@link ng.$exceptionHandler exception handling},
+ * {@link ng.$rootScope.Scope#$digest executing watches}.
+ *
+ * **Life cycle: Pseudo-Code of `$apply()`**
+ *
+ * ```js
+ function $apply(expr) {
+ try {
+ return $eval(expr);
+ } catch (e) {
+ $exceptionHandler(e);
+ } finally {
+ $root.$digest();
+ }
+ }
+ * ```
+ *
+ *
+ * Scope's `$apply()` method transitions through the following stages:
+ *
+ * 1. The {@link guide/expression expression} is executed using the
+ * {@link ng.$rootScope.Scope#$eval $eval()} method.
+ * 2. Any exceptions from the execution of the expression are forwarded to the
+ * {@link ng.$exceptionHandler $exceptionHandler} service.
+ * 3. The {@link ng.$rootScope.Scope#$watch watch} listeners are fired immediately after the
+ * expression was executed using the {@link ng.$rootScope.Scope#$digest $digest()} method.
+ *
+ *
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
+ *
+ * - `string`: execute using the rules as defined in {@link guide/expression expression}.
+ * - `function(scope)`: execute the function with current `scope` parameter.
+ *
+ * @returns {*} The result of evaluating the expression.
+ */
+ $apply: function(expr) {
+ try {
+ beginPhase('$apply');
+ try {
+ return this.$eval(expr);
+ } finally {
+ clearPhase();
+ }
+ } catch (e) {
+ $exceptionHandler(e);
+ } finally {
+ try {
+ $rootScope.$digest();
+ } catch (e) {
+ $exceptionHandler(e);
+ // eslint-disable-next-line no-unsafe-finally
+ throw e;
+ }
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$applyAsync
+ * @kind function
+ *
+ * @description
+ * Schedule the invocation of $apply to occur at a later time. The actual time difference
+ * varies across browsers, but is typically around ~10 milliseconds.
+ *
+ * This can be used to queue up multiple expressions which need to be evaluated in the same
+ * digest.
+ *
+ * @param {(string|function())=} exp An AngularJS expression to be executed.
+ *
+ * - `string`: execute using the rules as defined in {@link guide/expression expression}.
+ * - `function(scope)`: execute the function with current `scope` parameter.
+ */
+ $applyAsync: function(expr) {
+ var scope = this;
+ if (expr) {
+ applyAsyncQueue.push($applyAsyncExpression);
+ }
+ expr = $parse(expr);
+ scheduleApplyAsync();
+
+ function $applyAsyncExpression() {
+ scope.$eval(expr);
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$on
+ * @kind function
+ *
+ * @description
+ * Listens on events of a given type. See {@link ng.$rootScope.Scope#$emit $emit} for
+ * discussion of event life cycle.
+ *
+ * The event listener function format is: `function(event, args...)`. The `event` object
+ * passed into the listener has the following attributes:
+ *
+ * - `targetScope` - `{Scope}`: the scope on which the event was `$emit`-ed or
+ * `$broadcast`-ed.
+ * - `currentScope` - `{Scope}`: the scope that is currently handling the event. Once the
+ * event propagates through the scope hierarchy, this property is set to null.
+ * - `name` - `{string}`: name of the event.
+ * - `stopPropagation` - `{function=}`: calling `stopPropagation` function will cancel
+ * further event propagation (available only for events that were `$emit`-ed).
+ * - `preventDefault` - `{function}`: calling `preventDefault` sets `defaultPrevented` flag
+ * to true.
+ * - `defaultPrevented` - `{boolean}`: true if `preventDefault` was called.
+ *
+ * @param {string} name Event name to listen on.
+ * @param {function(event, ...args)} listener Function to call when the event is emitted.
+ * @returns {function()} Returns a deregistration function for this listener.
+ */
+ $on: function(name, listener) {
+ var namedListeners = this.$$listeners[name];
+ if (!namedListeners) {
+ this.$$listeners[name] = namedListeners = [];
+ }
+ namedListeners.push(listener);
+
+ var current = this;
+ do {
+ if (!current.$$listenerCount[name]) {
+ current.$$listenerCount[name] = 0;
+ }
+ current.$$listenerCount[name]++;
+ } while ((current = current.$parent));
+
+ var self = this;
+ return function() {
+ var indexOfListener = namedListeners.indexOf(listener);
+ if (indexOfListener !== -1) {
+ // Use delete in the hope of the browser deallocating the memory for the array entry,
+ // while not shifting the array indexes of other listeners.
+ // See issue https://github.com/angular/angular.js/issues/16135
+ delete namedListeners[indexOfListener];
+ decrementListenerCount(self, 1, name);
+ }
+ };
+ },
+
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$emit
+ * @kind function
+ *
+ * @description
+ * Dispatches an event `name` upwards through the scope hierarchy notifying the
+ * registered {@link ng.$rootScope.Scope#$on} listeners.
+ *
+ * The event life cycle starts at the scope on which `$emit` was called. All
+ * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get
+ * notified. Afterwards, the event traverses upwards toward the root scope and calls all
+ * registered listeners along the way. The event will stop propagating if one of the listeners
+ * cancels it.
+ *
+ * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed
+ * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * @param {string} name Event name to emit.
+ * @param {...*} args Optional one or more arguments which will be passed onto the event listeners.
+ * @return {Object} Event object (see {@link ng.$rootScope.Scope#$on}).
+ */
+ $emit: function(name, args) {
+ var empty = [],
+ namedListeners,
+ scope = this,
+ stopPropagation = false,
+ event = {
+ name: name,
+ targetScope: scope,
+ stopPropagation: function() {stopPropagation = true;},
+ preventDefault: function() {
+ event.defaultPrevented = true;
+ },
+ defaultPrevented: false
+ },
+ listenerArgs = concat([event], arguments, 1),
+ i, length;
+
+ do {
+ namedListeners = scope.$$listeners[name] || empty;
+ event.currentScope = scope;
+ for (i = 0, length = namedListeners.length; i < length; i++) {
+
+ // if listeners were deregistered, defragment the array
+ if (!namedListeners[i]) {
+ namedListeners.splice(i, 1);
+ i--;
+ length--;
+ continue;
+ }
+ try {
+ //allow all listeners attached to the current scope to run
+ namedListeners[i].apply(null, listenerArgs);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ //if any listener on the current scope stops propagation, prevent bubbling
+ if (stopPropagation) {
+ break;
+ }
+ //traverse upwards
+ scope = scope.$parent;
+ } while (scope);
+
+ event.currentScope = null;
+
+ return event;
+ },
+
+
+ /**
+ * @ngdoc method
+ * @name $rootScope.Scope#$broadcast
+ * @kind function
+ *
+ * @description
+ * Dispatches an event `name` downwards to all child scopes (and their children) notifying the
+ * registered {@link ng.$rootScope.Scope#$on} listeners.
+ *
+ * The event life cycle starts at the scope on which `$broadcast` was called. All
+ * {@link ng.$rootScope.Scope#$on listeners} listening for `name` event on this scope get
+ * notified. Afterwards, the event propagates to all direct and indirect scopes of the current
+ * scope and calls all registered listeners along the way. The event cannot be canceled.
+ *
+ * Any exception emitted from the {@link ng.$rootScope.Scope#$on listeners} will be passed
+ * onto the {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * @param {string} name Event name to broadcast.
+ * @param {...*} args Optional one or more arguments which will be passed onto the event listeners.
+ * @return {Object} Event object, see {@link ng.$rootScope.Scope#$on}
+ */
+ $broadcast: function(name, args) {
+ var target = this,
+ current = target,
+ next = target,
+ event = {
+ name: name,
+ targetScope: target,
+ preventDefault: function() {
+ event.defaultPrevented = true;
+ },
+ defaultPrevented: false
+ };
+
+ if (!target.$$listenerCount[name]) return event;
+
+ var listenerArgs = concat([event], arguments, 1),
+ listeners, i, length;
+
+ //down while you can, then up and next sibling or up and next sibling until back at root
+ while ((current = next)) {
+ event.currentScope = current;
+ listeners = current.$$listeners[name] || [];
+ for (i = 0, length = listeners.length; i < length; i++) {
+ // if listeners were deregistered, defragment the array
+ if (!listeners[i]) {
+ listeners.splice(i, 1);
+ i--;
+ length--;
+ continue;
+ }
+
+ try {
+ listeners[i].apply(null, listenerArgs);
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+
+ // Insanity Warning: scope depth-first traversal
+ // yes, this code is a bit crazy, but it works and we have tests to prove it!
+ // this piece should be kept in sync with the traversal in $digest
+ // (though it differs due to having the extra check for $$listenerCount)
+ if (!(next = ((current.$$listenerCount[name] && current.$$childHead) ||
+ (current !== target && current.$$nextSibling)))) {
+ while (current !== target && !(next = current.$$nextSibling)) {
+ current = current.$parent;
+ }
+ }
+ }
+
+ event.currentScope = null;
+ return event;
+ }
+ };
+
+ var $rootScope = new Scope();
+
+ //The internal queues. Expose them on the $rootScope for debugging/testing purposes.
+ var asyncQueue = $rootScope.$$asyncQueue = [];
+ var postDigestQueue = $rootScope.$$postDigestQueue = [];
+ var applyAsyncQueue = $rootScope.$$applyAsyncQueue = [];
+
+ var postDigestQueuePosition = 0;
+
+ return $rootScope;
+
+
+ function beginPhase(phase) {
+ if ($rootScope.$$phase) {
+ throw $rootScopeMinErr('inprog', '{0} already in progress', $rootScope.$$phase);
+ }
+
+ $rootScope.$$phase = phase;
+ }
+
+ function clearPhase() {
+ $rootScope.$$phase = null;
+ }
+
+ function incrementWatchersCount(current, count) {
+ do {
+ current.$$watchersCount += count;
+ } while ((current = current.$parent));
+ }
+
+ function decrementListenerCount(current, count, name) {
+ do {
+ current.$$listenerCount[name] -= count;
+
+ if (current.$$listenerCount[name] === 0) {
+ delete current.$$listenerCount[name];
+ }
+ } while ((current = current.$parent));
+ }
+
+ /**
+ * function used as an initial value for watchers.
+ * because it's unique we can easily tell it apart from other values
+ */
+ function initWatchVal() {}
+
+ function flushApplyAsync() {
+ while (applyAsyncQueue.length) {
+ try {
+ applyAsyncQueue.shift()();
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ }
+ applyAsyncId = null;
+ }
+
+ function scheduleApplyAsync() {
+ if (applyAsyncId === null) {
+ applyAsyncId = $browser.defer(function() {
+ $rootScope.$apply(flushApplyAsync);
+ });
+ }
+ }
+ }];
+ }
+
+ /**
+ * @ngdoc service
+ * @name $rootElement
+ *
+ * @description
+ * The root element of AngularJS application. This is either the element where {@link
+ * ng.directive:ngApp ngApp} was declared or the element passed into
+ * {@link angular.bootstrap}. The element represents the root element of application. It is also the
+ * location where the application's {@link auto.$injector $injector} service gets
+ * published, and can be retrieved using `$rootElement.injector()`.
+ */
+
+
+// the implementation is in angular.bootstrap
+
+ /**
+ * @this
+ * @description
+ * Private service to sanitize uris for links and images. Used by $compile and $sanitize.
+ */
+ function $$SanitizeUriProvider() {
+ var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/,
+ imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
+
+ /**
+ * @description
+ * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * urls during a[href] sanitization.
+ *
+ * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ *
+ * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
+ * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
+ * regular expression. If a match is found, the original url is written into the dom. Otherwise,
+ * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ *
+ * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
+ * chaining otherwise.
+ */
+ this.aHrefSanitizationWhitelist = function(regexp) {
+ if (isDefined(regexp)) {
+ aHrefSanitizationWhitelist = regexp;
+ return this;
+ }
+ return aHrefSanitizationWhitelist;
+ };
+
+
+ /**
+ * @description
+ * Retrieves or overrides the default regular expression that is used for whitelisting of safe
+ * urls during img[src] sanitization.
+ *
+ * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ *
+ * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
+ * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
+ * regular expression. If a match is found, the original url is written into the dom. Otherwise,
+ * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ *
+ * @param {RegExp=} regexp New regexp to whitelist urls with.
+ * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
+ * chaining otherwise.
+ */
+ this.imgSrcSanitizationWhitelist = function(regexp) {
+ if (isDefined(regexp)) {
+ imgSrcSanitizationWhitelist = regexp;
+ return this;
+ }
+ return imgSrcSanitizationWhitelist;
+ };
+
+ this.$get = function() {
+ return function sanitizeUri(uri, isImage) {
+ var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
+ var normalizedVal;
+ normalizedVal = urlResolve(uri && uri.trim()).href;
+ if (normalizedVal !== '' && !normalizedVal.match(regex)) {
+ return 'unsafe:' + normalizedVal;
+ }
+ return uri;
+ };
+ };
+ }
+
+ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ * Any commits to this file should be reviewed with security in mind. *
+ * Changes to this file can potentially create security vulnerabilities. *
+ * An approval from 2 Core members with history of modifying *
+ * this file is required. *
+ * *
+ * Does the change somehow allow for arbitrary javascript to be executed? *
+ * Or allows for someone to change the prototype of built-in objects? *
+ * Or gives undesired access to variables likes document or window? *
+ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
+
+ /* exported $SceProvider, $SceDelegateProvider */
+
+ var $sceMinErr = minErr('$sce');
+
+ var SCE_CONTEXTS = {
+ // HTML is used when there's HTML rendered (e.g. ng-bind-html, iframe srcdoc binding).
+ HTML: 'html',
+
+ // Style statements or stylesheets. Currently unused in AngularJS.
+ CSS: 'css',
+
+ // An URL used in a context where it does not refer to a resource that loads code. Currently
+ // unused in AngularJS.
+ URL: 'url',
+
+ // RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
+ // code. (e.g. ng-include, script src binding, templateUrl)
+ RESOURCE_URL: 'resourceUrl',
+
+ // Script. Currently unused in AngularJS.
+ JS: 'js'
+ };
+
+// Helper functions follow.
+
+ var UNDERSCORE_LOWERCASE_REGEXP = /_([a-z])/g;
+
+ function snakeToCamel(name) {
+ return name
+ .replace(UNDERSCORE_LOWERCASE_REGEXP, fnCamelCaseReplace);
+ }
+
+ function adjustMatcher(matcher) {
+ if (matcher === 'self') {
+ return matcher;
+ } else if (isString(matcher)) {
+ // Strings match exactly except for 2 wildcards - '*' and '**'.
+ // '*' matches any character except those from the set ':/.?&'.
+ // '**' matches any character (like .* in a RegExp).
+ // More than 2 *'s raises an error as it's ill defined.
+ if (matcher.indexOf('***') > -1) {
+ throw $sceMinErr('iwcard',
+ 'Illegal sequence *** in string matcher. String: {0}', matcher);
+ }
+ matcher = escapeForRegexp(matcher).
+ replace(/\\\*\\\*/g, '.*').
+ replace(/\\\*/g, '[^:/.?&;]*');
+ return new RegExp('^' + matcher + '$');
+ } else if (isRegExp(matcher)) {
+ // The only other type of matcher allowed is a Regexp.
+ // Match entire URL / disallow partial matches.
+ // Flags are reset (i.e. no global, ignoreCase or multiline)
+ return new RegExp('^' + matcher.source + '$');
+ } else {
+ throw $sceMinErr('imatcher',
+ 'Matchers may only be "self", string patterns or RegExp objects');
+ }
+ }
+
+
+ function adjustMatchers(matchers) {
+ var adjustedMatchers = [];
+ if (isDefined(matchers)) {
+ forEach(matchers, function(matcher) {
+ adjustedMatchers.push(adjustMatcher(matcher));
+ });
+ }
+ return adjustedMatchers;
+ }
+
+
+ /**
+ * @ngdoc service
+ * @name $sceDelegate
+ * @kind function
+ *
+ * @description
+ *
+ * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
+ * Contextual Escaping (SCE)} services to AngularJS.
+ *
+ * For an overview of this service and the functionnality it provides in AngularJS, see the main
+ * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
+ * SCE works in their application, which shouldn't be needed in most cases.
+ *
+ *
+ * AngularJS strongly relies on contextual escaping for the security of bindings: disabling or
+ * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners,
+ * changes to this service will also influence users, so be extra careful and document your changes.
+ *
+ *
+ * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of
+ * the `$sce` service to customize the way Strict Contextual Escaping works in AngularJS. This is
+ * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to
+ * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things
+ * work because `$sce` delegates to `$sceDelegate` for these operations.
+ *
+ * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service.
+ *
+ * The default instance of `$sceDelegate` should work out of the box with little pain. While you
+ * can override it completely to change the behavior of `$sce`, the common case would
+ * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting
+ * your own whitelists and blacklists for trusting URLs used for loading AngularJS resources such as
+ * templates. Refer {@link ng.$sceDelegateProvider#resourceUrlWhitelist
+ * $sceDelegateProvider.resourceUrlWhitelist} and {@link
+ * ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
+ */
+
+ /**
+ * @ngdoc provider
+ * @name $sceDelegateProvider
+ * @this
+ *
+ * @description
+ *
+ * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
+ * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}.
+ *
+ * The `$sceDelegateProvider` allows one to get/set the whitelists and blacklists used to ensure
+ * that the URLs used for sourcing AngularJS templates and other script-running URLs are safe (all
+ * places that use the `$sce.RESOURCE_URL` context). See
+ * {@link ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist}
+ * and
+ * {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist},
+ *
+ * For the general details about this service in AngularJS, read the main page for {@link ng.$sce
+ * Strict Contextual Escaping (SCE)}.
+ *
+ * **Example**: Consider the following case.
+ *
+ * - your app is hosted at url `http://myapp.example.com/`
+ * - but some of your templates are hosted on other domains you control such as
+ * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
+ * - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
+ *
+ * Here is what a secure configuration for this scenario might look like:
+ *
+ * ```
+ * angular.module('myApp', []).config(function($sceDelegateProvider) {
+ * $sceDelegateProvider.resourceUrlWhitelist([
+ * // Allow same origin resource loads.
+ * 'self',
+ * // Allow loading from our assets domain. Notice the difference between * and **.
+ * 'http://srv*.assets.example.com/**'
+ * ]);
+ *
+ * // The blacklist overrides the whitelist so the open redirect here is blocked.
+ * $sceDelegateProvider.resourceUrlBlacklist([
+ * 'http://myapp.example.com/clickThru**'
+ * ]);
+ * });
+ * ```
+ * Note that an empty whitelist will block every resource URL from being loaded, and will require
+ * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates
+ * requested by {@link ng.$templateRequest $templateRequest} that are present in
+ * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism
+ * to populate your templates in that cache at config time, then it is a good idea to remove 'self'
+ * from that whitelist. This helps to mitigate the security impact of certain types of issues, like
+ * for instance attacker-controlled `ng-includes`.
+ */
+
+ function $SceDelegateProvider() {
+ this.SCE_CONTEXTS = SCE_CONTEXTS;
+
+ // Resource URLs can also be trusted by policy.
+ var resourceUrlWhitelist = ['self'],
+ resourceUrlBlacklist = [];
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#resourceUrlWhitelist
+ * @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.
+ * Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
+ * allowed in this array.
+ *
+ * @return {Array} The currently set whitelist array.
+ *
+ * @description
+ * Sets/Gets the whitelist of trusted resource URLs.
+ *
+ * The **default value** when no whitelist has been explicitly set is `['self']` allowing only
+ * same origin resource requests.
+ *
+ *
+ * **Note:** the default whitelist of 'self' is not recommended if your app shares its origin
+ * with other apps! It is a good idea to limit it to only your application's directory.
+ *
+ */
+ this.resourceUrlWhitelist = function(value) {
+ if (arguments.length) {
+ resourceUrlWhitelist = adjustMatchers(value);
+ }
+ return resourceUrlWhitelist;
+ };
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegateProvider#resourceUrlBlacklist
+ * @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.
+ * 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.
+ *
+ * Finally, **the blacklist overrides the whitelist** and has the final say.
+ *
+ * @return {Array} The currently set blacklist array.
+ *
+ * @description
+ * Sets/Gets the blacklist of trusted resource URLs.
+ *
+ * The **default value** when no whitelist has been explicitly set is the empty array (i.e. there
+ * is no blacklist.)
+ */
+
+ this.resourceUrlBlacklist = function(value) {
+ if (arguments.length) {
+ resourceUrlBlacklist = adjustMatchers(value);
+ }
+ return resourceUrlBlacklist;
+ };
+
+ this.$get = ['$injector', function($injector) {
+
+ var htmlSanitizer = function htmlSanitizer(html) {
+ throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
+ };
+
+ if ($injector.has('$sanitize')) {
+ htmlSanitizer = $injector.get('$sanitize');
+ }
+
+
+ function matchUrl(matcher, parsedUrl) {
+ if (matcher === 'self') {
+ return urlIsSameOrigin(parsedUrl);
+ } else {
+ // definitely a regex. See adjustMatchers()
+ return !!matcher.exec(parsedUrl.href);
+ }
+ }
+
+ function isResourceUrlAllowedByPolicy(url) {
+ var parsedUrl = urlResolve(url.toString());
+ var i, n, allowed = false;
+ // Ensure that at least one item from the whitelist allows this url.
+ for (i = 0, n = resourceUrlWhitelist.length; i < n; i++) {
+ if (matchUrl(resourceUrlWhitelist[i], parsedUrl)) {
+ allowed = true;
+ break;
+ }
+ }
+ if (allowed) {
+ // Ensure that no item from the blacklist blocked this url.
+ for (i = 0, n = resourceUrlBlacklist.length; i < n; i++) {
+ if (matchUrl(resourceUrlBlacklist[i], parsedUrl)) {
+ allowed = false;
+ break;
+ }
+ }
+ }
+ return allowed;
+ }
+
+ function generateHolderType(Base) {
+ var holderType = function TrustedValueHolderType(trustedValue) {
+ this.$$unwrapTrustedValue = function() {
+ return trustedValue;
+ };
+ };
+ if (Base) {
+ holderType.prototype = new Base();
+ }
+ holderType.prototype.valueOf = function sceValueOf() {
+ return this.$$unwrapTrustedValue();
+ };
+ holderType.prototype.toString = function sceToString() {
+ return this.$$unwrapTrustedValue().toString();
+ };
+ return holderType;
+ }
+
+ var trustedValueHolderBase = generateHolderType(),
+ byType = {};
+
+ byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegate#trustAs
+ *
+ * @description
+ * Returns a trusted representation of the parameter for the specified context. This trusted
+ * object will later on be used as-is, without any security check, by bindings or directives
+ * that require this security context.
+ * For instance, marking a string as trusted for the `$sce.HTML` context will entirely bypass
+ * the potential `$sanitize` call in corresponding `$sce.HTML` bindings or directives, such as
+ * `ng-bind-html`. Note that in most cases you won't need to call this function: if you have the
+ * sanitizer loaded, passing the value itself will render all the HTML that does not pose a
+ * security risk.
+ *
+ * See {@link ng.$sceDelegate#getTrusted getTrusted} for the function that will consume those
+ * trusted values, and {@link ng.$sce $sce} for general documentation about strict contextual
+ * escaping.
+ *
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
+ *
+ * @param {*} value The value that should be considered trusted.
+ * @return {*} A trusted representation of value, that can be used in the given context.
+ */
+ function trustAs(type, trustedValue) {
+ var Constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
+ if (!Constructor) {
+ throw $sceMinErr('icontext',
+ 'Attempted to trust a value in invalid context. Context: {0}; Value: {1}',
+ type, trustedValue);
+ }
+ if (trustedValue === null || isUndefined(trustedValue) || trustedValue === '') {
+ return trustedValue;
+ }
+ // All the current contexts in SCE_CONTEXTS happen to be strings. In order to avoid trusting
+ // mutable objects, we ensure here that the value passed in is actually a string.
+ if (typeof trustedValue !== 'string') {
+ throw $sceMinErr('itype',
+ 'Attempted to trust a non-string value in a content requiring a string: Context: {0}',
+ type);
+ }
+ return new Constructor(trustedValue);
+ }
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegate#valueOf
+ *
+ * @description
+ * If the passed parameter had been returned by a prior call to {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`}, returns the value that had been passed to {@link
+ * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}.
+ *
+ * If the passed parameter is not a value that had been returned by {@link
+ * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}, it must be returned as-is.
+ *
+ * @param {*} value The result of a prior {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}
+ * call or anything else.
+ * @return {*} The `value` that was originally provided to {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} if `value` is the result of such a call. Otherwise, returns
+ * `value` unchanged.
+ */
+ function valueOf(maybeTrusted) {
+ if (maybeTrusted instanceof trustedValueHolderBase) {
+ return maybeTrusted.$$unwrapTrustedValue();
+ } else {
+ return maybeTrusted;
+ }
+ }
+
+ /**
+ * @ngdoc method
+ * @name $sceDelegate#getTrusted
+ *
+ * @description
+ * Takes any input, and either returns a value that's safe to use in the specified context, or
+ * throws an exception.
+ *
+ * In practice, there are several cases. When given a string, this function runs checks
+ * and sanitization to make it safe without prior assumptions. When given the result of a {@link
+ * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied
+ * value if that value's context is valid for this call's context. Finally, this function can
+ * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization
+ * is available or possible.)
+ *
+ * @param {string} type The context in which this value is to be used (such as `$sce.HTML`).
+ * @param {*} maybeTrusted The result of a prior {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
+ */
+ function getTrusted(type, maybeTrusted) {
+ if (maybeTrusted === null || isUndefined(maybeTrusted) || maybeTrusted === '') {
+ return maybeTrusted;
+ }
+ var constructor = (byType.hasOwnProperty(type) ? byType[type] : null);
+ // If maybeTrusted is a trusted class instance or subclass instance, then unwrap and return
+ // as-is.
+ if (constructor && maybeTrusted instanceof constructor) {
+ return maybeTrusted.$$unwrapTrustedValue();
+ }
+ // Otherwise, if we get here, then we may either make it safe, or throw an exception. This
+ // depends on the context: some are sanitizatible (HTML), some use whitelists (RESOURCE_URL),
+ // some are impossible to do (JS). This step isn't implemented for CSS and URL, as AngularJS
+ // has no corresponding sinks.
+ if (type === SCE_CONTEXTS.RESOURCE_URL) {
+ // RESOURCE_URL uses a whitelist.
+ if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
+ return maybeTrusted;
+ } else {
+ throw $sceMinErr('insecurl',
+ 'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}',
+ maybeTrusted.toString());
+ }
+ } else if (type === SCE_CONTEXTS.HTML) {
+ // htmlSanitizer throws its own error when no sanitizer is available.
+ return htmlSanitizer(maybeTrusted);
+ }
+ // Default error when the $sce service has no way to make the input safe.
+ throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
+ }
+
+ return { trustAs: trustAs,
+ getTrusted: getTrusted,
+ valueOf: valueOf };
+ }];
+ }
+
+
+ /**
+ * @ngdoc provider
+ * @name $sceProvider
+ * @this
+ *
+ * @description
+ *
+ * The $sceProvider provider allows developers to configure the {@link ng.$sce $sce} service.
+ * - enable/disable Strict Contextual Escaping (SCE) in a module
+ * - override the default implementation with a custom delegate
+ *
+ * Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
+ */
+
+ /**
+ * @ngdoc service
+ * @name $sce
+ * @kind function
+ *
+ * @description
+ *
+ * `$sce` is a service that provides Strict Contextual Escaping services to AngularJS.
+ *
+ * ## Strict Contextual Escaping
+ *
+ * Strict Contextual Escaping (SCE) is a mode in which AngularJS constrains bindings to only render
+ * trusted values. Its goal is to assist in writing code in a way that (a) is secure by default, and
+ * (b) makes auditing for security vulnerabilities such as XSS, clickjacking, etc. a lot easier.
+ *
+ * ### Overview
+ *
+ * To systematically block XSS security bugs, AngularJS treats all values as untrusted by default in
+ * HTML or sensitive URL bindings. When binding untrusted values, AngularJS will automatically
+ * run security checks on them (sanitizations, whitelists, depending on context), or throw when it
+ * cannot guarantee the security of the result. That behavior depends strongly on contexts: HTML
+ * can be sanitized, but template URLs cannot, for instance.
+ *
+ * To illustrate this, consider the `ng-bind-html` directive. It renders its value directly as HTML:
+ * we call that the *context*. When given an untrusted input, AngularJS will attempt to sanitize it
+ * before rendering if a sanitizer is available, and throw otherwise. To bypass sanitization and
+ * render the input as-is, you will need to mark it as trusted for that context before attempting
+ * to bind it.
+ *
+ * As of version 1.2, AngularJS ships with SCE enabled by default.
+ *
+ * ### In practice
+ *
+ * Here's an example of a binding in a privileged context:
+ *
+ * ```
+ *
+ *
+ * ```
+ *
+ * Notice that `ng-bind-html` is bound to `userHtml` controlled by the user. With SCE
+ * disabled, this application allows the user to render arbitrary HTML into the DIV, which would
+ * be an XSS security bug. In a more realistic example, one may be rendering user comments, blog
+ * articles, etc. via bindings. (HTML is just one example of a context where rendering user
+ * controlled input creates security vulnerabilities.)
+ *
+ * For the case of HTML, you might use a library, either on the client side, or on the server side,
+ * to sanitize unsafe HTML before binding to the value and rendering it in the document.
+ *
+ * How would you ensure that every place that used these types of bindings was bound to a value that
+ * was sanitized by your library (or returned as safe for rendering by your server?) How can you
+ * ensure that you didn't accidentally delete the line that sanitized the value, or renamed some
+ * properties/fields and forgot to update the binding to the sanitized value?
+ *
+ * To be secure by default, AngularJS makes sure bindings go through that sanitization, or
+ * any similar validation process, unless there's a good reason to trust the given value in this
+ * context. That trust is formalized with a function call. This means that as a developer, you
+ * can assume all untrusted bindings are safe. Then, to audit your code for binding security issues,
+ * you just need to ensure the values you mark as trusted indeed are safe - because they were
+ * received from your server, sanitized by your library, etc. You can organize your codebase to
+ * help with this - perhaps allowing only the files in a specific directory to do this.
+ * Ensuring that the internal API exposed by that code doesn't markup arbitrary values as safe then
+ * becomes a more manageable task.
+ *
+ * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs}
+ * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to
+ * build the trusted versions of your values.
+ *
+ * ### How does it work?
+ *
+ * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
+ * $sce.getTrusted(context, value)} rather than to the value directly. Think of this function as
+ * a way to enforce the required security context in your data sink. Directives use {@link
+ * ng.$sce#parseAs $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs
+ * the {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals. Also,
+ * when binding without directives, AngularJS will understand the context of your bindings
+ * automatically.
+ *
+ * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
+ * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
+ * simplified):
+ *
+ * ```
+ * var ngBindHtmlDirective = ['$sce', function($sce) {
+ * return function(scope, element, attr) {
+ * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
+ * element.html(value || '');
+ * });
+ * };
+ * }];
+ * ```
+ *
+ * ### Impact on loading templates
+ *
+ * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as
+ * `templateUrl`'s specified by {@link guide/directive directives}.
+ *
+ * By default, AngularJS only loads templates from the same domain and protocol as the application
+ * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
+ * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
+ * protocols, you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
+ * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
+ *
+ * *Please note*:
+ * The browser's
+ * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
+ * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
+ * policy apply in addition to this and may further restrict whether the template is successfully
+ * loaded. This means that without the right CORS policy, loading templates from a different domain
+ * won't work on all browsers. Also, loading templates from `file://` URL does not work on some
+ * browsers.
+ *
+ * ### This feels like too much overhead
+ *
+ * It's important to remember that SCE only applies to interpolation expressions.
+ *
+ * If your expressions are constant literals, they're automatically trusted and you don't need to
+ * call `$sce.trustAs` on them (e.g.
+ * `
`) just works. The `$sceDelegate` will
+ * also use the `$sanitize` service if it is available when binding untrusted values to
+ * `$sce.HTML` context. AngularJS provides an implementation in `angular-sanitize.js`, and if you
+ * wish to use it, you will also need to depend on the {@link ngSanitize `ngSanitize`} module in
+ * your application.
+ *
+ * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load
+ * templates in `ng-include` from your application's domain without having to even know about SCE.
+ * It blocks loading templates from other domains or loading templates over http from an https
+ * served document. You can change these by setting your own custom {@link
+ * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link
+ * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs.
+ *
+ * This significantly reduces the overhead. It is far easier to pay the small overhead and have an
+ * application that's secure and can be audited to verify that with much more ease than bolting
+ * security onto an application later.
+ *
+ *
+ * ### What trusted context types are supported?
+ *
+ * | Context | Notes |
+ * |---------------------|----------------|
+ * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. If an unsafe value is encountered, and the {@link ngSanitize.$sanitize $sanitize} service is available (implemented by the {@link ngSanitize ngSanitize} module) this will sanitize the value instead of throwing an error. |
+ * | `$sce.CSS` | For CSS that's safe to source into the application. Currently, no bindings require this context. Feel free to use it in your own directives. |
+ * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`
Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does (it's not just the URL that matters, but also what is at the end of it), and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
+ * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently, no bindings require this context. Feel free to use it in your own directives. |
+ *
+ *
+ * Be aware that `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
+ * through {@link ng.$sce#getTrusted $sce.getTrusted}. There's no CSS-, URL-, or JS-context bindings
+ * in AngularJS currently, so their corresponding `$sce.trustAs` functions aren't useful yet. This
+ * might evolve.
+ *
+ * ### Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ *
+ * Each element in these arrays must be one of the following:
+ *
+ * - **'self'**
+ * - The special **string**, `'self'`, can be used to match against all URLs of the **same
+ * domain** as the application document using the **same protocol**.
+ * - **String** (except the special value `'self'`)
+ * - The string is matched against the full *normalized / absolute URL* of the resource
+ * being tested (substring matches are not good enough.)
+ * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters
+ * match themselves.
+ * - `*`: matches zero or more occurrences of any character other than one of the following 6
+ * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and '`;`'. It's a useful wildcard for use
+ * in a whitelist.
+ * - `**`: matches zero or more occurrences of *any* character. As such, it's not
+ * appropriate for use in a scheme, domain, etc. as it would match too much. (e.g.
+ * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
+ * not have been the intention.) Its usage at the very end of the path is ok. (e.g.
+ * http://foo.example.com/templates/**).
+ * - **RegExp** (*see caveat below*)
+ * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax
+ * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to
+ * accidentally introduce a bug when one updates a complex expression (imho, all regexes should
+ * have good test coverage). For instance, the use of `.` in the regex is correct only in a
+ * small number of cases. A `.` character in the regex used when matching the scheme or a
+ * subdomain could be matched against a `:` or literal `.` that was likely not intended. It
+ * is highly recommended to use the string patterns and only fall back to regular expressions
+ * as a last resort.
+ * - The regular expression must be an instance of RegExp (i.e. not a string.) It is
+ * matched against the **entire** *normalized / absolute URL* of the resource being tested
+ * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags
+ * present on the RegExp (such as multiline, global, ignoreCase) are ignored.
+ * - If you are generating your JavaScript from some other templating engine (not
+ * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)),
+ * remember to escape your regular expression (and be aware that you might need more than
+ * one level of escaping depending on your templating engine and the way you interpolated
+ * the value.) Do make use of your platform's escaping mechanism as it might be good
+ * enough before coding your own. E.g. Ruby has
+ * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape)
+ * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape).
+ * Javascript lacks a similar built in function for escaping. Take a look at Google
+ * Closure library's [goog.string.regExpEscape(s)](
+ * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962).
+ *
+ * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example.
+ *
+ * ### Show me an example using SCE.
+ *
+ *
+ *
+ *
+ *
+ *
User comments
+ * By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when
+ * $sanitize is available. If $sanitize isn't available, this results in an error instead of an
+ * exploit.
+ *
+ *
+ * {{userComment.name}} :
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * angular.module('mySceApp', ['ngSanitize'])
+ * .controller('AppController', ['$http', '$templateCache', '$sce',
+ * function AppController($http, $templateCache, $sce) {
+ * var self = this;
+ * $http.get('test_data.json', {cache: $templateCache}).then(function(response) {
+ * self.userComments = response.data;
+ * });
+ * self.explicitlyTrustedHtml = $sce.trustAsHtml(
+ * 'Hover over this text. ');
+ * }]);
+ *
+ *
+ *
+ * [
+ * { "name": "Alice",
+ * "htmlComment":
+ * "Is anyone reading this? "
+ * },
+ * { "name": "Bob",
+ * "htmlComment": "Yes! Am I the only other one?"
+ * }
+ * ]
+ *
+ *
+ *
+ * describe('SCE doc demo', function() {
+ * it('should sanitize untrusted values', function() {
+ * expect(element.all(by.css('.htmlComment')).first().getAttribute('innerHTML'))
+ * .toBe('Is anyone reading this? ');
+ * });
+ *
+ * it('should NOT sanitize explicitly trusted values', function() {
+ * expect(element(by.id('explicitlyTrustedHtml')).getAttribute('innerHTML')).toBe(
+ * 'Hover over this text. ');
+ * });
+ * });
+ *
+ *
+ *
+ *
+ *
+ * ## Can I disable SCE completely?
+ *
+ * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits
+ * for little coding overhead. It will be much harder to take an SCE disabled application and
+ * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE
+ * for cases where you have a lot of existing code that was written before SCE was introduced and
+ * you're migrating them a module at a time. Also do note that this is an app-wide setting, so if
+ * you are writing a library, you will cause security bugs applications using it.
+ *
+ * That said, here's how you can completely disable SCE:
+ *
+ * ```
+ * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
+ * // Completely disable SCE. For demonstration purposes only!
+ * // Do not use in new projects or libraries.
+ * $sceProvider.enabled(false);
+ * });
+ * ```
+ *
+ */
+
+ function $SceProvider() {
+ var enabled = true;
+
+ /**
+ * @ngdoc method
+ * @name $sceProvider#enabled
+ * @kind function
+ *
+ * @param {boolean=} value If provided, then enables/disables SCE application-wide.
+ * @return {boolean} True if SCE is enabled, false otherwise.
+ *
+ * @description
+ * Enables/disables SCE and returns the current value.
+ */
+ this.enabled = function(value) {
+ if (arguments.length) {
+ enabled = !!value;
+ }
+ return enabled;
+ };
+
+
+ /* Design notes on the default implementation for SCE.
+ *
+ * The API contract for the SCE delegate
+ * -------------------------------------
+ * The SCE delegate object must provide the following 3 methods:
+ *
+ * - trustAs(contextEnum, value)
+ * This method is used to tell the SCE service that the provided value is OK to use in the
+ * contexts specified by contextEnum. It must return an object that will be accepted by
+ * getTrusted() for a compatible contextEnum and return this value.
+ *
+ * - valueOf(value)
+ * For values that were not produced by trustAs(), return them as is. For values that were
+ * produced by trustAs(), return the corresponding input value to trustAs. Basically, if
+ * trustAs is wrapping the given values into some type, this operation unwraps it when given
+ * such a value.
+ *
+ * - getTrusted(contextEnum, value)
+ * This function should return the a value that is safe to use in the context specified by
+ * contextEnum or throw and exception otherwise.
+ *
+ * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
+ * opaque or wrapped in some holder object. That happens to be an implementation detail. For
+ * instance, an implementation could maintain a registry of all trusted objects by context. In
+ * such a case, trustAs() would return the same object that was passed in. getTrusted() would
+ * return the same object passed in if it was found in the registry under a compatible context or
+ * throw an exception otherwise. An implementation might only wrap values some of the time based
+ * on some criteria. getTrusted() might return a value and not throw an exception for special
+ * constants or objects even if not wrapped. All such implementations fulfill this contract.
+ *
+ *
+ * A note on the inheritance model for SCE contexts
+ * ------------------------------------------------
+ * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This
+ * is purely an implementation details.
+ *
+ * The contract is simply this:
+ *
+ * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value)
+ * will also succeed.
+ *
+ * Inheritance happens to capture this in a natural way. In some future, we may not use
+ * inheritance anymore. That is OK because no code outside of sce.js and sceSpecs.js would need to
+ * be aware of this detail.
+ */
+
+ this.$get = ['$parse', '$sceDelegate', function(
+ $parse, $sceDelegate) {
+ // Support: IE 9-11 only
+ // Prereq: Ensure that we're not running in IE<11 quirks mode. In that mode, IE < 11 allow
+ // the "expression(javascript expression)" syntax which is insecure.
+ if (enabled && msie < 8) {
+ throw $sceMinErr('iequirks',
+ 'Strict Contextual Escaping does not support Internet Explorer version < 11 in quirks ' +
+ 'mode. You can fix this by adding the text to the top of your HTML ' +
+ 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
+ }
+
+ var sce = shallowCopy(SCE_CONTEXTS);
+
+ /**
+ * @ngdoc method
+ * @name $sce#isEnabled
+ * @kind function
+ *
+ * @return {Boolean} True if SCE is enabled, false otherwise. If you want to set the value, you
+ * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
+ *
+ * @description
+ * Returns a boolean indicating if SCE is enabled.
+ */
+ sce.isEnabled = function() {
+ return enabled;
+ };
+ sce.trustAs = $sceDelegate.trustAs;
+ sce.getTrusted = $sceDelegate.getTrusted;
+ sce.valueOf = $sceDelegate.valueOf;
+
+ if (!enabled) {
+ sce.trustAs = sce.getTrusted = function(type, value) { return value; };
+ sce.valueOf = identity;
+ }
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAs
+ *
+ * @description
+ * Converts AngularJS {@link guide/expression expression} into a function. This is like {@link
+ * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it
+ * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*,
+ * *result*)}
+ *
+ * @param {string} type The SCE context in which this result will be used.
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+ sce.parseAs = function sceParseAs(type, expr) {
+ var parsed = $parse(expr);
+ if (parsed.literal && parsed.constant) {
+ return parsed;
+ } else {
+ return $parse(expr, function(value) {
+ return sce.getTrusted(type, value);
+ });
+ }
+ };
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAs
+ *
+ * @description
+ * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such, returns a
+ * wrapped object that represents your value, and the trust you have in its safety for the given
+ * context. AngularJS can then use that value as-is in bindings of the specified secure context.
+ * This is used in bindings for `ng-bind-html`, `ng-include`, and most `src` attribute
+ * interpolations. See {@link ng.$sce $sce} for strict contextual escaping.
+ *
+ * @param {string} type The context in which this value is safe for use, e.g. `$sce.URL`,
+ * `$sce.RESOURCE_URL`, `$sce.HTML`, `$sce.JS` or `$sce.CSS`.
+ *
+ * @param {*} value The value that that should be considered trusted.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in the context you specified.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsHtml
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsHtml(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.HTML` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.HTML` context (like `ng-bind-html`).
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsCss
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsCss(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.CSS, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.CSS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant
+ * of your `value` in `$sce.CSS` context. This context is currently unused, so there are
+ * almost no reasons to use this function so far.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsUrl
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsUrl(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.URL` context. That context is currently unused, so there are almost no reasons
+ * to use this function so far.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsResourceUrl
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsResourceUrl(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.RESOURCE_URL` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.RESOURCE_URL` context (template URLs in `ng-include`, most `src` attribute
+ * bindings, ...)
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#trustAsJs
+ *
+ * @description
+ * Shorthand method. `$sce.trustAsJs(value)` →
+ * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
+ *
+ * @param {*} value The value to mark as trusted for `$sce.JS` context.
+ * @return {*} A wrapped version of value that can be used as a trusted variant of your `value`
+ * in `$sce.JS` context. That context is currently unused, so there are almost no reasons to
+ * use this function so far.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrusted
+ *
+ * @description
+ * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such,
+ * takes any input, and either returns a value that's safe to use in the specified context,
+ * or throws an exception. This function is aware of trusted values created by the `trustAs`
+ * function and its shorthands, and when contexts are appropriate, returns the unwrapped value
+ * as-is. Finally, this function can also throw when there is no way to turn `maybeTrusted` in a
+ * safe value (e.g., no sanitization is available or possible.)
+ *
+ * @param {string} type The context in which this value is to be used.
+ * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs
+ * `$sce.trustAs`} call, or anything else (which will not be considered trusted.)
+ * @return {*} A version of the value that's safe to use in the given context, or throws an
+ * exception if this is impossible.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrustedHtml
+ *
+ * @description
+ * Shorthand method. `$sce.getTrustedHtml(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
+ *
+ * @param {*} value The value to pass to `$sce.getTrusted`.
+ * @return {*} The return value of `$sce.getTrusted($sce.HTML, value)`
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrustedCss
+ *
+ * @description
+ * Shorthand method. `$sce.getTrustedCss(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
+ *
+ * @param {*} value The value to pass to `$sce.getTrusted`.
+ * @return {*} The return value of `$sce.getTrusted($sce.CSS, value)`
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrustedUrl
+ *
+ * @description
+ * Shorthand method. `$sce.getTrustedUrl(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
+ *
+ * @param {*} value The value to pass to `$sce.getTrusted`.
+ * @return {*} The return value of `$sce.getTrusted($sce.URL, value)`
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrustedResourceUrl
+ *
+ * @description
+ * Shorthand method. `$sce.getTrustedResourceUrl(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
+ *
+ * @param {*} value The value to pass to `$sceDelegate.getTrusted`.
+ * @return {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#getTrustedJs
+ *
+ * @description
+ * Shorthand method. `$sce.getTrustedJs(value)` →
+ * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
+ *
+ * @param {*} value The value to pass to `$sce.getTrusted`.
+ * @return {*} The return value of `$sce.getTrusted($sce.JS, value)`
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAsHtml
+ *
+ * @description
+ * Shorthand method. `$sce.parseAsHtml(expression string)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.HTML, value)`}
+ *
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAsCss
+ *
+ * @description
+ * Shorthand method. `$sce.parseAsCss(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.CSS, value)`}
+ *
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAsUrl
+ *
+ * @description
+ * Shorthand method. `$sce.parseAsUrl(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.URL, value)`}
+ *
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAsResourceUrl
+ *
+ * @description
+ * Shorthand method. `$sce.parseAsResourceUrl(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.RESOURCE_URL, value)`}
+ *
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+
+ /**
+ * @ngdoc method
+ * @name $sce#parseAsJs
+ *
+ * @description
+ * Shorthand method. `$sce.parseAsJs(value)` →
+ * {@link ng.$sce#parseAs `$sce.parseAs($sce.JS, value)`}
+ *
+ * @param {string} expression String expression to compile.
+ * @return {function(context, locals)} A function which represents the compiled expression:
+ *
+ * * `context` – `{object}` – an object against which any expressions embedded in the
+ * strings are evaluated against (typically a scope object).
+ * * `locals` – `{object=}` – local variables context object, useful for overriding values
+ * in `context`.
+ */
+
+ // Shorthand delegations.
+ var parse = sce.parseAs,
+ getTrusted = sce.getTrusted,
+ trustAs = sce.trustAs;
+
+ forEach(SCE_CONTEXTS, function(enumValue, name) {
+ var lName = lowercase(name);
+ sce[snakeToCamel('parse_as_' + lName)] = function(expr) {
+ return parse(enumValue, expr);
+ };
+ sce[snakeToCamel('get_trusted_' + lName)] = function(value) {
+ return getTrusted(enumValue, value);
+ };
+ sce[snakeToCamel('trust_as_' + lName)] = function(value) {
+ return trustAs(enumValue, value);
+ };
+ });
+
+ return sce;
+ }];
+ }
+
+ /* exported $SnifferProvider */
+
+ /**
+ * !!! This is an undocumented "private" service !!!
+ *
+ * @name $sniffer
+ * @requires $window
+ * @requires $document
+ * @this
+ *
+ * @property {boolean} history Does the browser support html5 history api ?
+ * @property {boolean} transitions Does the browser support CSS transition events ?
+ * @property {boolean} animations Does the browser support CSS animation events ?
+ *
+ * @description
+ * This is very simple implementation of testing browser's features.
+ */
+ function $SnifferProvider() {
+ this.$get = ['$window', '$document', function($window, $document) {
+ var eventSupport = {},
+ // Chrome Packaged Apps are not allowed to access `history.pushState`.
+ // If not sandboxed, they can be detected by the presence of `chrome.app.runtime`
+ // (see https://developer.chrome.com/apps/api_index). If sandboxed, they can be detected by
+ // the presence of an extension runtime ID and the absence of other Chrome runtime APIs
+ // (see https://developer.chrome.com/apps/manifest/sandbox).
+ // (NW.js apps have access to Chrome APIs, but do support `history`.)
+ isNw = $window.nw && $window.nw.process,
+ isChromePackagedApp =
+ !isNw &&
+ $window.chrome &&
+ ($window.chrome.app && $window.chrome.app.runtime ||
+ !$window.chrome.app && $window.chrome.runtime && $window.chrome.runtime.id),
+ hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState,
+ android =
+ toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
+ boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
+ document = $document[0] || {},
+ bodyStyle = document.body && document.body.style,
+ transitions = false,
+ animations = false;
+
+ if (bodyStyle) {
+ // Support: Android <5, Blackberry Browser 10, default Chrome in Android 4.4.x
+ // Mentioned browsers need a -webkit- prefix for transitions & animations.
+ transitions = !!('transition' in bodyStyle || 'webkitTransition' in bodyStyle);
+ animations = !!('animation' in bodyStyle || 'webkitAnimation' in bodyStyle);
+ }
+
+
+ return {
+ // Android has history.pushState, but it does not update location correctly
+ // so let's not use the history API at all.
+ // http://code.google.com/p/android/issues/detail?id=17471
+ // https://github.com/angular/angular.js/issues/904
+
+ // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
+ // so let's not use the history API also
+ // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
+ history: !!(hasHistoryPushState && !(android < 4) && !boxee),
+ hasEvent: function(event) {
+ // Support: IE 9-11 only
+ // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
+ // it. In particular the event is not fired when backspace or delete key are pressed or
+ // when cut operation is performed.
+ // IE10+ implements 'input' event but it erroneously fires under various situations,
+ // e.g. when placeholder changes, or a form is focused.
+ if (event === 'input' && msie) return false;
+
+ if (isUndefined(eventSupport[event])) {
+ var divElm = document.createElement('div');
+ eventSupport[event] = 'on' + event in divElm;
+ }
+
+ return eventSupport[event];
+ },
+ csp: csp(),
+ transitions: transitions,
+ animations: animations,
+ android: android
+ };
+ }];
+ }
+
+ var $templateRequestMinErr = minErr('$compile');
+
+ /**
+ * @ngdoc provider
+ * @name $templateRequestProvider
+ * @this
+ *
+ * @description
+ * Used to configure the options passed to the {@link $http} service when making a template request.
+ *
+ * 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}.
+ *
+ * `$templateRequest` is used internally by {@link $compile}, {@link ngRoute.$route}, and directives such
+ * as {@link ngInclude} to download and cache templates.
+ *
+ * 3rd party modules should use `$templateRequest` if their services or directives are loading
+ * templates.
+ *
+ * @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 = ['$exceptionHandler', '$templateCache', '$http', '$q', '$sce',
+ function($exceptionHandler, $templateCache, $http, $q, $sce) {
+
+ function handleRequestFn(tpl, ignoreRequestError) {
+ handleRequestFn.totalPendingRequests++;
+
+ // We consider the template cache holds only trusted templates, so
+ // there's no need to go through whitelisting again for keys that already
+ // are included in there. This also makes AngularJS accept any script
+ // directive, no matter its name. However, we still need to unwrap trusted
+ // types.
+ if (!isString(tpl) || isUndefined($templateCache.get(tpl))) {
+ tpl = $sce.getTrustedResourceUrl(tpl);
+ }
+
+ var transformResponse = $http.defaults && $http.defaults.transformResponse;
+
+ if (isArray(transformResponse)) {
+ transformResponse = transformResponse.filter(function(transformer) {
+ return transformer !== defaultHttpResponseTransform;
+ });
+ } else if (transformResponse === defaultHttpResponseTransform) {
+ transformResponse = null;
+ }
+
+ return $http.get(tpl, extend({
+ cache: $templateCache,
+ transformResponse: transformResponse
+ }, httpOptions))
+ .finally(function() {
+ handleRequestFn.totalPendingRequests--;
+ })
+ .then(function(response) {
+ $templateCache.put(tpl, response.data);
+ return response.data;
+ }, handleError);
+
+ function handleError(resp) {
+ if (!ignoreRequestError) {
+ resp = $templateRequestMinErr('tpload',
+ 'Failed to load template: {0} (HTTP status: {1} {2})',
+ tpl, resp.status, resp.statusText);
+
+ $exceptionHandler(resp);
+ }
+
+ return $q.reject(resp);
+ }
+ }
+
+ handleRequestFn.totalPendingRequests = 0;
+
+ return handleRequestFn;
+ }
+ ];
+ }
+
+ /** @this */
+ function $$TestabilityProvider() {
+ this.$get = ['$rootScope', '$browser', '$location',
+ function($rootScope, $browser, $location) {
+
+ /**
+ * @name $testability
+ *
+ * @description
+ * The private $$testability service provides a collection of methods for use when debugging
+ * or by automated test and debugging tools.
+ */
+ var testability = {};
+
+ /**
+ * @name $$testability#findBindings
+ *
+ * @description
+ * Returns an array of elements that are bound (via ng-bind or {{}})
+ * to expressions matching the input.
+ *
+ * @param {Element} element The element root to search from.
+ * @param {string} expression The binding expression to match.
+ * @param {boolean} opt_exactMatch If true, only returns exact matches
+ * for the expression. Filters and whitespace are ignored.
+ */
+ testability.findBindings = function(element, expression, opt_exactMatch) {
+ var bindings = element.getElementsByClassName('ng-binding');
+ var matches = [];
+ forEach(bindings, function(binding) {
+ var dataBinding = angular.element(binding).data('$binding');
+ if (dataBinding) {
+ forEach(dataBinding, function(bindingName) {
+ if (opt_exactMatch) {
+ var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
+ if (matcher.test(bindingName)) {
+ matches.push(binding);
+ }
+ } else {
+ if (bindingName.indexOf(expression) !== -1) {
+ matches.push(binding);
+ }
+ }
+ });
+ }
+ });
+ return matches;
+ };
+
+ /**
+ * @name $$testability#findModels
+ *
+ * @description
+ * Returns an array of elements that are two-way found via ng-model to
+ * expressions matching the input.
+ *
+ * @param {Element} element The element root to search from.
+ * @param {string} expression The model expression to match.
+ * @param {boolean} opt_exactMatch If true, only returns exact matches
+ * for the expression.
+ */
+ testability.findModels = function(element, expression, opt_exactMatch) {
+ var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
+ for (var p = 0; p < prefixes.length; ++p) {
+ var attributeEquals = opt_exactMatch ? '=' : '*=';
+ var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
+ var elements = element.querySelectorAll(selector);
+ if (elements.length) {
+ return elements;
+ }
+ }
+ };
+
+ /**
+ * @name $$testability#getLocation
+ *
+ * @description
+ * Shortcut for getting the location in a browser agnostic way. Returns
+ * the path, search, and hash. (e.g. /path?a=b#hash)
+ */
+ testability.getLocation = function() {
+ return $location.url();
+ };
+
+ /**
+ * @name $$testability#setLocation
+ *
+ * @description
+ * Shortcut for navigating to a location without doing a full page reload.
+ *
+ * @param {string} url The location url (path, search and hash,
+ * e.g. /path?a=b#hash) to go to.
+ */
+ testability.setLocation = function(url) {
+ if (url !== $location.url()) {
+ $location.url(url);
+ $rootScope.$digest();
+ }
+ };
+
+ /**
+ * @name $$testability#whenStable
+ *
+ * @description
+ * Calls the callback when $timeout and $http requests are completed.
+ *
+ * @param {function} callback
+ */
+ testability.whenStable = function(callback) {
+ $browser.notifyWhenNoOutstandingRequests(callback);
+ };
+
+ return testability;
+ }];
+ }
+
+ /** @this */
+ function $TimeoutProvider() {
+ this.$get = ['$rootScope', '$browser', '$q', '$$q', '$exceptionHandler',
+ function($rootScope, $browser, $q, $$q, $exceptionHandler) {
+
+ var deferreds = {};
+
+
+ /**
+ * @ngdoc service
+ * @name $timeout
+ *
+ * @description
+ * AngularJS's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
+ * block and delegates any exceptions to
+ * {@link ng.$exceptionHandler $exceptionHandler} service.
+ *
+ * The return value of calling `$timeout` is a promise, which will be resolved when
+ * the delay has passed and the timeout function, if provided, is executed.
+ *
+ * To cancel a timeout request, call `$timeout.cancel(promise)`.
+ *
+ * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
+ * synchronously flush the queue of deferred functions.
+ *
+ * If you only want a promise that will be resolved after some specified delay
+ * then you can call `$timeout` without the `fn` function.
+ *
+ * @param {function()=} fn A function, whose execution should be delayed.
+ * @param {number=} [delay=0] Delay in milliseconds.
+ * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
+ * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
+ * @param {...*=} Pass additional parameters to the executed 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) {
+ if (!isFunction(fn)) {
+ invokeApply = delay;
+ delay = fn;
+ fn = noop;
+ }
+
+ var args = sliceArgs(arguments, 3),
+ skipApply = (isDefined(invokeApply) && !invokeApply),
+ deferred = (skipApply ? $$q : $q).defer(),
+ promise = deferred.promise,
+ timeoutId;
+
+ timeoutId = $browser.defer(function() {
+ try {
+ deferred.resolve(fn.apply(null, args));
+ } catch (e) {
+ deferred.reject(e);
+ $exceptionHandler(e);
+ } finally {
+ delete deferreds[promise.$$timeoutId];
+ }
+
+ if (!skipApply) $rootScope.$apply();
+ }, delay);
+
+ promise.$$timeoutId = timeoutId;
+ deferreds[timeoutId] = deferred;
+
+ return promise;
+ }
+
+
+ /**
+ * @ngdoc method
+ * @name $timeout#cancel
+ *
+ * @description
+ * Cancels a task associated with the `promise`. As a result of this, the promise will be
+ * resolved with a rejection.
+ *
+ * @param {Promise=} promise Promise returned by the `$timeout` function.
+ * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
+ * canceled.
+ */
+ timeout.cancel = function(promise) {
+ if (promise && promise.$$timeoutId in deferreds) {
+ // Timeout cancels should not report an unhandled promise.
+ markQExceptionHandled(deferreds[promise.$$timeoutId].promise);
+ deferreds[promise.$$timeoutId].reject('canceled');
+ delete deferreds[promise.$$timeoutId];
+ return $browser.defer.cancel(promise.$$timeoutId);
+ }
+ return false;
+ };
+
+ return timeout;
+ }];
+ }
+
+// NOTE: The usage of window and document instead of $window and $document here is
+// deliberate. This service depends on the specific behavior of anchor nodes created by the
+// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and
+// cause us to break tests. In addition, when the browser resolves a URL for XHR, it
+// 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 = window.document.createElement('a');
+ var originUrl = urlResolve(window.location.href);
+
+
+ /**
+ *
+ * Implementation Notes for non-IE browsers
+ * ----------------------------------------
+ * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM,
+ * results both in the normalizing and parsing of the URL. Normalizing means that a relative
+ * URL will be resolved into an absolute URL in the context of the application document.
+ * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
+ * properties are all populated to reflect the normalized URL. This approach has wide
+ * compatibility - Safari 1+, Mozilla 1+ etc. See
+ * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
+ *
+ * Implementation Notes for IE
+ * ---------------------------
+ * IE <= 10 normalizes the URL when assigned to the anchor node similar to the other
+ * browsers. However, the parsed components will not be set if the URL assigned did not specify
+ * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We
+ * work around that by performing the parsing in a 2nd step by taking a previously normalized
+ * URL (e.g. by assigning to a.href) and assigning it a.href again. This correctly populates the
+ * properties such as protocol, hostname, port, etc.
+ *
+ * References:
+ * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement
+ * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
+ * http://url.spec.whatwg.org/#urlutils
+ * https://github.com/angular/angular.js/pull/2902
+ * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
+ *
+ * @kind function
+ * @param {string} url The URL to be parsed.
+ * @description Normalizes and parses a URL.
+ * @returns {object} Returns the normalized URL as a dictionary.
+ *
+ * | member name | Description |
+ * |---------------|----------------|
+ * | href | A normalized version of the provided URL if it was not an absolute URL |
+ * | protocol | The protocol including the trailing colon |
+ * | host | The host and port (if the port is non-default) of the normalizedUrl |
+ * | search | The search params, minus the question mark |
+ * | hash | The hash string, minus the hash symbol
+ * | hostname | The hostname
+ * | port | The port, without ":"
+ * | pathname | The pathname, beginning with "/"
+ *
+ */
+ function urlResolve(url) {
+ var href = url;
+
+ // Support: IE 9-11 only
+ if (msie) {
+ // Normalize before parse. Refer Implementation Notes on why this is
+ // done in two steps on IE.
+ urlParsingNode.setAttribute('href', href);
+ href = urlParsingNode.href;
+ }
+
+ urlParsingNode.setAttribute('href', href);
+
+ // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
+ return {
+ href: urlParsingNode.href,
+ protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
+ host: urlParsingNode.host,
+ search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
+ hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
+ hostname: urlParsingNode.hostname,
+ port: urlParsingNode.port,
+ pathname: (urlParsingNode.pathname.charAt(0) === '/')
+ ? urlParsingNode.pathname
+ : '/' + urlParsingNode.pathname
+ };
+ }
+
+ /**
+ * Parse a request URL and determine whether this is a same-origin request as the application document.
+ *
+ * @param {string|object} requestUrl The url of the request as a string that will be resolved
+ * or a parsed URL object.
+ * @returns {boolean} Whether the request is for the same origin as the application document.
+ */
+ function urlIsSameOrigin(requestUrl) {
+ var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
+ return (parsed.protocol === originUrl.protocol &&
+ parsed.host === originUrl.host);
+ }
+
+ /**
+ * @ngdoc service
+ * @name $window
+ * @this
+ *
+ * @description
+ * A reference to the browser's `window` object. While `window`
+ * is globally available in JavaScript, it causes testability problems, because
+ * it is a global variable. In AngularJS we always refer to it through the
+ * `$window` service, so it may be overridden, removed or mocked for testing.
+ *
+ * Expressions, like the one defined for the `ngClick` directive in the example
+ * below, are evaluated with respect to the current scope. Therefore, there is
+ * no risk of inadvertently coding in a dependency on a global value in such an
+ * expression.
+ *
+ * @example
+
+
+
+
+
+ ALERT
+
+
+
+ it('should display the greeting in the input box', function() {
+ element(by.model('greeting')).sendKeys('Hello, E2E Tests');
+ // If we click the button it will block the test runner
+ // element(':button').click();
+ });
+
+
+ */
+ function $WindowProvider() {
+ this.$get = valueFn(window);
+ }
+
+ /**
+ * @name $$cookieReader
+ * @requires $document
+ *
+ * @description
+ * This is a private service for reading cookies used by $http and ngCookies
+ *
+ * @return {Object} a key/value map of the current cookies
+ */
+ function $$CookieReader($document) {
+ var rawDocument = $document[0] || {};
+ var lastCookies = {};
+ var lastCookieString = '';
+
+ function safeGetCookie(rawDocument) {
+ try {
+ return rawDocument.cookie || '';
+ } catch (e) {
+ return '';
+ }
+ }
+
+ function safeDecodeURIComponent(str) {
+ try {
+ return decodeURIComponent(str);
+ } catch (e) {
+ return str;
+ }
+ }
+
+ return function() {
+ var cookieArray, cookie, i, index, name;
+ var currentCookieString = safeGetCookie(rawDocument);
+
+ if (currentCookieString !== lastCookieString) {
+ lastCookieString = currentCookieString;
+ cookieArray = lastCookieString.split('; ');
+ lastCookies = {};
+
+ for (i = 0; i < cookieArray.length; i++) {
+ cookie = cookieArray[i];
+ index = cookie.indexOf('=');
+ if (index > 0) { //ignore nameless cookies
+ name = safeDecodeURIComponent(cookie.substring(0, index));
+ // the first value that is seen for a cookie is the most
+ // specific one. values for the same cookie name that
+ // follow are for less specific paths.
+ if (isUndefined(lastCookies[name])) {
+ lastCookies[name] = safeDecodeURIComponent(cookie.substring(index + 1));
+ }
+ }
+ }
+ }
+ return lastCookies;
+ };
+ }
+
+ $$CookieReader.$inject = ['$document'];
+
+ /** @this */
+ function $$CookieReaderProvider() {
+ this.$get = $$CookieReader;
+ }
+
+ /* global currencyFilter: true,
+ dateFilter: true,
+ filterFilter: true,
+ jsonFilter: true,
+ limitToFilter: true,
+ lowercaseFilter: true,
+ numberFilter: true,
+ orderByFilter: true,
+ uppercaseFilter: true,
+ */
+
+ /**
+ * @ngdoc provider
+ * @name $filterProvider
+ * @description
+ *
+ * Filters are just functions which transform input to an output. However filters need to be
+ * Dependency Injected. To achieve this a filter definition consists of a factory function which is
+ * annotated with dependencies and is responsible for creating a filter function.
+ *
+ *
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
+ * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
+ * (`myapp_subsection_filterx`).
+ *
+ *
+ * ```js
+ * // Filter registration
+ * function MyModule($provide, $filterProvider) {
+ * // create a service to demonstrate injection (not always needed)
+ * $provide.value('greet', function(name){
+ * return 'Hello ' + name + '!';
+ * });
+ *
+ * // register a filter factory which uses the
+ * // greet service to demonstrate DI.
+ * $filterProvider.register('greet', function(greet){
+ * // return the filter function which uses the greet service
+ * // to generate salutation
+ * return function(text) {
+ * // filters need to be forgiving so check input validity
+ * return text && greet(text) || text;
+ * };
+ * });
+ * }
+ * ```
+ *
+ * The filter function is registered with the `$injector` under the filter name suffix with
+ * `Filter`.
+ *
+ * ```js
+ * it('should be the same instance', inject(
+ * function($filterProvider) {
+ * $filterProvider.register('reverse', function(){
+ * return ...;
+ * });
+ * },
+ * function($filter, reverseFilter) {
+ * expect($filter('reverse')).toBe(reverseFilter);
+ * });
+ * ```
+ *
+ *
+ * For more information about how AngularJS filters work, and how to create your own filters, see
+ * {@link guide/filter Filters} in the AngularJS Developer Guide.
+ */
+
+ /**
+ * @ngdoc service
+ * @name $filter
+ * @kind function
+ * @description
+ * Filters are used for formatting data displayed to the user.
+ *
+ * They can be used in view templates, controllers or services. AngularJS comes
+ * with a collection of [built-in filters](api/ng/filter), but it is easy to
+ * define your own as well.
+ *
+ * The general syntax in templates is as follows:
+ *
+ * ```html
+ * {{ expression [| filter_name[:parameter_value] ... ] }}
+ * ```
+ *
+ * @param {String} name Name of the filter function to retrieve
+ * @return {Function} the filter function
+ * @example
+
+
+
+
{{ originalText }}
+ {{ filteredText }}
+
+
+
+
+ angular.module('filterExample', [])
+ .controller('MainCtrl', function($scope, $filter) {
+ $scope.originalText = 'hello';
+ $scope.filteredText = $filter('uppercase')($scope.originalText);
+ });
+
+
+ */
+ $FilterProvider.$inject = ['$provide'];
+ /** @this */
+ function $FilterProvider($provide) {
+ var suffix = 'Filter';
+
+ /**
+ * @ngdoc method
+ * @name $filterProvider#register
+ * @param {string|Object} name Name of the filter function, or an object map of filters where
+ * the keys are the filter names and the values are the filter factories.
+ *
+ *
+ * **Note:** Filter names must be valid AngularJS {@link expression} identifiers, such as `uppercase` or `orderBy`.
+ * Names with special characters, such as hyphens and dots, are not allowed. If you wish to namespace
+ * your filters, then you can use capitalization (`myappSubsectionFilterx`) or underscores
+ * (`myapp_subsection_filterx`).
+ *
+ * @param {Function} factory If the first argument was a string, a factory function for the filter to be registered.
+ * @returns {Object} Registered filter instance, or if a map of filters was provided then a map
+ * of the registered filter instances.
+ */
+ function register(name, factory) {
+ if (isObject(name)) {
+ var filters = {};
+ forEach(name, function(filter, key) {
+ filters[key] = register(key, filter);
+ });
+ return filters;
+ } else {
+ return $provide.factory(name + suffix, factory);
+ }
+ }
+ this.register = register;
+
+ this.$get = ['$injector', function($injector) {
+ return function(name) {
+ return $injector.get(name + suffix);
+ };
+ }];
+
+ ////////////////////////////////////////
+
+ /* global
+ currencyFilter: false,
+ dateFilter: false,
+ filterFilter: false,
+ jsonFilter: false,
+ limitToFilter: false,
+ lowercaseFilter: false,
+ numberFilter: false,
+ orderByFilter: false,
+ uppercaseFilter: false
+ */
+
+ register('currency', currencyFilter);
+ register('date', dateFilter);
+ register('filter', filterFilter);
+ register('json', jsonFilter);
+ register('limitTo', limitToFilter);
+ register('lowercase', lowercaseFilter);
+ register('number', numberFilter);
+ register('orderBy', orderByFilter);
+ register('uppercase', uppercaseFilter);
+ }
+
+ /**
+ * @ngdoc filter
+ * @name filter
+ * @kind function
+ *
+ * @description
+ * Selects a subset of items from `array` and returns it as a new array.
+ *
+ * @param {Array} array The source array.
+ *
+ * **Note**: If the array contains objects that reference themselves, filtering is not possible.
+ *
+ * @param {string|Object|function()} expression The predicate to be used for selecting items from
+ * `array`.
+ *
+ * Can be one of:
+ *
+ * - `string`: The string is used for matching against the contents of the `array`. All strings or
+ * objects with string properties in `array` that match this string will be returned. This also
+ * applies to nested object properties.
+ * The predicate can be negated by prefixing the string with `!`.
+ *
+ * - `Object`: A pattern object can be used to filter specific properties on objects contained
+ * by `array`. For example `{name:"M", phone:"1"}` predicate will return an array of items
+ * which have property `name` containing "M" and property `phone` containing "1". A special
+ * property name (`$` by default) can be used (e.g. as in `{$: "text"}`) to accept a match
+ * against any property of the object or its nested object properties. That's equivalent to the
+ * simple substring match with a `string` as described above. The special property name can be
+ * overwritten, using the `anyPropertyKey` parameter.
+ * The predicate can be negated by prefixing the string with `!`.
+ * For example `{name: "!M"}` predicate will return an array of items which have property `name`
+ * not containing "M".
+ *
+ * Note that a named property will match properties on the same level only, while the special
+ * `$` property will match properties on the same level or deeper. E.g. an array item like
+ * `{name: {first: 'John', last: 'Doe'}}` will **not** be matched by `{name: 'John'}`, but
+ * **will** be matched by `{$: 'John'}`.
+ *
+ * - `function(value, index, array)`: A predicate function can be used to write arbitrary filters.
+ * The function is called for each element of the array, with the element, its index, and
+ * the entire array itself as arguments.
+ *
+ * The final result is an array of those elements that the predicate returned true for.
+ *
+ * @param {function(actual, expected)|true|false} [comparator] Comparator which is used in
+ * determining if values retrieved using `expression` (when it is not a function) should be
+ * considered a match based on the expected value (from the filter expression) and actual
+ * value (from the object in the array).
+ *
+ * Can be one of:
+ *
+ * - `function(actual, expected)`:
+ * The function will be given the object value and the predicate value to compare and
+ * should return true if both values should be considered equal.
+ *
+ * - `true`: A shorthand for `function(actual, expected) { return angular.equals(actual, expected)}`.
+ * This is essentially strict comparison of expected and actual.
+ *
+ * - `false`: A short hand for a function which will look for a substring match in a case
+ * insensitive way. Primitive values are converted to strings. Objects are not compared against
+ * primitives, unless they have a custom `toString` method (e.g. `Date` objects).
+ *
+ *
+ * Defaults to `false`.
+ *
+ * @param {string} [anyPropertyKey] The special property name that matches against any property.
+ * By default `$`.
+ *
+ * @example
+
+
+
+
+ Search:
+
+ Name Phone
+
+ {{friend.name}}
+ {{friend.phone}}
+
+
+
+ Any:
+ Name only
+ Phone only
+ Equality
+
+ Name Phone
+
+ {{friendObj.name}}
+ {{friendObj.phone}}
+
+
+
+
+ var expectFriendNames = function(expectedNames, key) {
+ element.all(by.repeater(key + ' in friends').column(key + '.name')).then(function(arr) {
+ arr.forEach(function(wd, i) {
+ expect(wd.getText()).toMatch(expectedNames[i]);
+ });
+ });
+ };
+
+ it('should search across all fields when filtering with a string', function() {
+ var searchText = element(by.model('searchText'));
+ searchText.clear();
+ searchText.sendKeys('m');
+ expectFriendNames(['Mary', 'Mike', 'Adam'], 'friend');
+
+ searchText.clear();
+ searchText.sendKeys('76');
+ expectFriendNames(['John', 'Julie'], 'friend');
+ });
+
+ it('should search in specific fields when filtering with a predicate object', function() {
+ var searchAny = element(by.model('search.$'));
+ searchAny.clear();
+ searchAny.sendKeys('i');
+ expectFriendNames(['Mary', 'Mike', 'Julie', 'Juliette'], 'friendObj');
+ });
+ it('should use a equal comparison when comparator is true', function() {
+ var searchName = element(by.model('search.name'));
+ var strict = element(by.model('strict'));
+ searchName.clear();
+ searchName.sendKeys('Julie');
+ strict.click();
+ expectFriendNames(['Julie'], 'friendObj');
+ });
+
+
+ */
+
+ function filterFilter() {
+ return function(array, expression, comparator, anyPropertyKey) {
+ if (!isArrayLike(array)) {
+ if (array == null) {
+ return array;
+ } else {
+ throw minErr('filter')('notarray', 'Expected array but received: {0}', array);
+ }
+ }
+
+ anyPropertyKey = anyPropertyKey || '$';
+ var expressionType = getTypeForFilter(expression);
+ var predicateFn;
+ var matchAgainstAnyProp;
+
+ switch (expressionType) {
+ case 'function':
+ predicateFn = expression;
+ break;
+ case 'boolean':
+ case 'null':
+ case 'number':
+ case 'string':
+ matchAgainstAnyProp = true;
+ // falls through
+ case 'object':
+ predicateFn = createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp);
+ break;
+ default:
+ return array;
+ }
+
+ return Array.prototype.filter.call(array, predicateFn);
+ };
+ }
+
+// Helper functions for `filterFilter`
+ function createPredicateFn(expression, comparator, anyPropertyKey, matchAgainstAnyProp) {
+ var shouldMatchPrimitives = isObject(expression) && (anyPropertyKey in expression);
+ var predicateFn;
+
+ if (comparator === true) {
+ comparator = equals;
+ } else if (!isFunction(comparator)) {
+ comparator = function(actual, expected) {
+ if (isUndefined(actual)) {
+ // No substring matching against `undefined`
+ return false;
+ }
+ if ((actual === null) || (expected === null)) {
+ // No substring matching against `null`; only match against `null`
+ return actual === expected;
+ }
+ if (isObject(expected) || (isObject(actual) && !hasCustomToString(actual))) {
+ // Should not compare primitives against objects, unless they have custom `toString` method
+ return false;
+ }
+
+ actual = lowercase('' + actual);
+ expected = lowercase('' + expected);
+ return actual.indexOf(expected) !== -1;
+ };
+ }
+
+ predicateFn = function(item) {
+ if (shouldMatchPrimitives && !isObject(item)) {
+ return deepCompare(item, expression[anyPropertyKey], comparator, anyPropertyKey, false);
+ }
+ return deepCompare(item, expression, comparator, anyPropertyKey, matchAgainstAnyProp);
+ };
+
+ return predicateFn;
+ }
+
+ function deepCompare(actual, expected, comparator, anyPropertyKey, matchAgainstAnyProp, dontMatchWholeObject) {
+ var actualType = getTypeForFilter(actual);
+ var expectedType = getTypeForFilter(expected);
+
+ if ((expectedType === 'string') && (expected.charAt(0) === '!')) {
+ return !deepCompare(actual, expected.substring(1), comparator, anyPropertyKey, matchAgainstAnyProp);
+ } else if (isArray(actual)) {
+ // In case `actual` is an array, consider it a match
+ // if ANY of it's items matches `expected`
+ return actual.some(function(item) {
+ return deepCompare(item, expected, comparator, anyPropertyKey, matchAgainstAnyProp);
+ });
+ }
+
+ switch (actualType) {
+ case 'object':
+ var key;
+ if (matchAgainstAnyProp) {
+ for (key in actual) {
+ // Under certain, rare, circumstances, key may not be a string and `charAt` will be undefined
+ // See: https://github.com/angular/angular.js/issues/15644
+ if (key.charAt && (key.charAt(0) !== '$') &&
+ deepCompare(actual[key], expected, comparator, anyPropertyKey, true)) {
+ return true;
+ }
+ }
+ return dontMatchWholeObject ? false : deepCompare(actual, expected, comparator, anyPropertyKey, false);
+ } else if (expectedType === 'object') {
+ for (key in expected) {
+ var expectedVal = expected[key];
+ if (isFunction(expectedVal) || isUndefined(expectedVal)) {
+ continue;
+ }
+
+ var matchAnyProperty = key === anyPropertyKey;
+ var actualVal = matchAnyProperty ? actual : actual[key];
+ if (!deepCompare(actualVal, expectedVal, comparator, anyPropertyKey, matchAnyProperty, matchAnyProperty)) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return comparator(actual, expected);
+ }
+ case 'function':
+ return false;
+ default:
+ return comparator(actual, expected);
+ }
+ }
+
+// Used for easily differentiating between `null` and actual `object`
+ function getTypeForFilter(val) {
+ return (val === null) ? 'null' : typeof val;
+ }
+
+ var MAX_DIGITS = 22;
+ var DECIMAL_SEP = '.';
+ var ZERO_CHAR = '0';
+
+ /**
+ * @ngdoc filter
+ * @name currency
+ * @kind function
+ *
+ * @description
+ * Formats a number as a currency (ie $1,234.56). When no currency symbol is provided, default
+ * symbol for current locale is used.
+ *
+ * @param {number} amount Input to filter.
+ * @param {string=} symbol Currency symbol or identifier to be displayed.
+ * @param {number=} fractionSize Number of decimal places to round the amount to, defaults to default max fraction size for current locale
+ * @returns {string} Formatted number.
+ *
+ *
+ * @example
+
+
+
+
+
+ default currency symbol ($): {{amount | currency}}
+ custom currency identifier (USD$): {{amount | currency:"USD$"}}
+ no fractions (0): {{amount | currency:"USD$":0}}
+
+
+
+ it('should init with 1234.56', function() {
+ expect(element(by.id('currency-default')).getText()).toBe('$1,234.56');
+ expect(element(by.id('currency-custom')).getText()).toBe('USD$1,234.56');
+ expect(element(by.id('currency-no-fractions')).getText()).toBe('USD$1,235');
+ });
+ it('should update', function() {
+ if (browser.params.browser === 'safari') {
+ // Safari does not understand the minus key. See
+ // https://github.com/angular/protractor/issues/481
+ return;
+ }
+ element(by.model('amount')).clear();
+ element(by.model('amount')).sendKeys('-1234');
+ expect(element(by.id('currency-default')).getText()).toBe('-$1,234.00');
+ expect(element(by.id('currency-custom')).getText()).toBe('-USD$1,234.00');
+ expect(element(by.id('currency-no-fractions')).getText()).toBe('-USD$1,234');
+ });
+
+
+ */
+ currencyFilter.$inject = ['$locale'];
+ function currencyFilter($locale) {
+ var formats = $locale.NUMBER_FORMATS;
+ return function(amount, currencySymbol, fractionSize) {
+ if (isUndefined(currencySymbol)) {
+ currencySymbol = formats.CURRENCY_SYM;
+ }
+
+ if (isUndefined(fractionSize)) {
+ fractionSize = formats.PATTERNS[1].maxFrac;
+ }
+
+ // If the currency symbol is empty, trim whitespace around the symbol
+ var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g;
+
+ // if null or undefined pass it through
+ return (amount == null)
+ ? amount
+ : formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
+ replace(currencySymbolRe, currencySymbol);
+ };
+ }
+
+ /**
+ * @ngdoc filter
+ * @name number
+ * @kind function
+ *
+ * @description
+ * Formats a number as text.
+ *
+ * If the input is null or undefined, it will just be 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} number Number to format.
+ * @param {(number|string)=} fractionSize Number of decimal places to round the number to.
+ * If this is not provided then the fraction size is computed from the current locale's number
+ * formatting pattern. In the case of the default locale, it will be 3.
+ * @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
+
+
+
+
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}}
+
+
+
+ it('should format numbers', function() {
+ expect(element(by.id('number-default')).getText()).toBe('1,234.568');
+ expect(element(by.binding('val | number:0')).getText()).toBe('1,235');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679');
+ });
+
+ it('should update', function() {
+ element(by.model('val')).clear();
+ element(by.model('val')).sendKeys('3374.333');
+ expect(element(by.id('number-default')).getText()).toBe('3,374.333');
+ expect(element(by.binding('val | number:0')).getText()).toBe('3,374');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330');
+ });
+
+
+ */
+ numberFilter.$inject = ['$locale'];
+ function numberFilter($locale) {
+ var formats = $locale.NUMBER_FORMATS;
+ return function(number, fractionSize) {
+
+ // if null or undefined pass it through
+ return (number == null)
+ ? number
+ : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
+ fractionSize);
+ };
+ }
+
+ /**
+ * 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;
+
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
+ }
+
+ // 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;
+ }
+
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
+
+ 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--;
+
+ // 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 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;
+ }
+
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
+ }
+
+ /**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+ function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
+
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
+
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
+
+ if (roundAt > 0) {
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
+
+ // 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]++;
+ }
+ }
+
+ // 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 (isInfinity) {
+ formattedText = '\u221e';
+ } else {
+ 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++;
+ }
+
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen, digits.length);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
+ }
+
+ 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, negWrap) {
+ var neg = '';
+ if (num < 0 || (negWrap && num <= 0)) {
+ if (negWrap) {
+ num = -num + 1;
+ } else {
+ num = -num;
+ neg = '-';
+ }
+ }
+ num = '' + num;
+ while (num.length < digits) num = ZERO_CHAR + num;
+ if (trim) {
+ num = num.substr(num.length - digits);
+ }
+ return neg + num;
+ }
+
+
+ function dateGetter(name, size, offset, trim, negWrap) {
+ offset = offset || 0;
+ return function(date) {
+ var value = date['get' + name]();
+ if (offset > 0 || value > -offset) {
+ value += offset;
+ }
+ if (value === 0 && offset === -12) value = 12;
+ return padNumber(value, size, trim, negWrap);
+ };
+ }
+
+ function dateStrGetter(name, shortForm, standAlone) {
+ return function(date, formats) {
+ var value = date['get' + name]();
+ var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
+ var get = uppercase(propPrefix + name);
+
+ return formats[get][value];
+ };
+ }
+
+ function timeZoneGetter(date, formats, offset) {
+ var zone = -1 * offset;
+ var paddedZone = (zone >= 0) ? '+' : '';
+
+ paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
+ padNumber(Math.abs(zone % 60), 2);
+
+ return paddedZone;
+ }
+
+ function getFirstThursdayOfYear(year) {
+ // 0 = index of January
+ var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay();
+ // 4 = index of Thursday (+1 to account for 1st = 5)
+ // 11 = index of *next* Thursday (+1 account for 1st = 12)
+ return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst);
+ }
+
+ function getThursdayThisWeek(datetime) {
+ return new Date(datetime.getFullYear(), datetime.getMonth(),
+ // 4 = index of Thursday
+ datetime.getDate() + (4 - datetime.getDay()));
+ }
+
+ function weekGetter(size) {
+ return function(date) {
+ var firstThurs = getFirstThursdayOfYear(date.getFullYear()),
+ thisThurs = getThursdayThisWeek(date);
+
+ var diff = +thisThurs - +firstThurs,
+ result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
+
+ return padNumber(result, size);
+ };
+ }
+
+ function ampmGetter(date, formats) {
+ return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
+ }
+
+ function eraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1];
+ }
+
+ function longEraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1];
+ }
+
+ var DATE_FORMATS = {
+ 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),
+ H: dateGetter('Hours', 1),
+ hh: dateGetter('Hours', 2, -12),
+ h: dateGetter('Hours', 1, -12),
+ mm: dateGetter('Minutes', 2),
+ m: dateGetter('Minutes', 1),
+ ss: dateGetter('Seconds', 2),
+ s: dateGetter('Seconds', 1),
+ // while ISO 8601 requires fractions to be prefixed with `.` or `,`
+ // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions
+ sss: dateGetter('Milliseconds', 3),
+ EEEE: dateStrGetter('Day'),
+ EEE: dateStrGetter('Day', true),
+ a: ampmGetter,
+ Z: timeZoneGetter,
+ ww: weekGetter(2),
+ w: weekGetter(1),
+ G: eraGetter,
+ GG: eraGetter,
+ GGG: eraGetter,
+ GGGG: longEraGetter
+ };
+
+ var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,
+ NUMBER_STRING = /^-?\d+$/;
+
+ /**
+ * @ngdoc filter
+ * @name date
+ * @kind function
+ *
+ * @description
+ * Formats `date` to a string based on the requested `format`.
+ *
+ * `format` string can be composed of the following elements:
+ *
+ * * `'yyyy'`: 4 digit representation of year (e.g. AD 1 => 0001, AD 2010 => 2010)
+ * * `'yy'`: 2 digit representation of year, padded (00-99). (e.g. AD 2001 => 01, AD 2010 => 10)
+ * * `'y'`: 1 digit representation of year, e.g. (AD 1 => 1, AD 199 => 199)
+ * * `'MMMM'`: Month in year (January-December)
+ * * `'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)
+ * * `'EEE'`: Day in Week, (Sun-Sat)
+ * * `'HH'`: Hour in day, padded (00-23)
+ * * `'H'`: Hour in day (0-23)
+ * * `'hh'`: Hour in AM/PM, padded (01-12)
+ * * `'h'`: Hour in AM/PM, (1-12)
+ * * `'mm'`: Minute in hour, padded (00-59)
+ * * `'m'`: Minute in hour (0-59)
+ * * `'ss'`: Second in minute, padded (00-59)
+ * * `'s'`: Second in minute (0-59)
+ * * `'sss'`: Millisecond in second, padded (000-999)
+ * * `'a'`: AM/PM marker
+ * * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
+ * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year
+ * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year
+ * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD')
+ * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini')
+ *
+ * `format` string can also be one of the following predefined
+ * {@link guide/i18n localizable formats}:
+ *
+ * * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
+ * (e.g. Sep 3, 2010 12:05:08 PM)
+ * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM)
+ * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale
+ * (e.g. Friday, September 3, 2010)
+ * * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010)
+ * * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
+ * * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
+ * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM)
+ * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM)
+ *
+ * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g.
+ * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
+ * (e.g. `"h 'o''clock'"`).
+ *
+ * Any other characters in the `format` string will be output as-is.
+ *
+ * @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
+ * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
+ * shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
+ * specified in the string input, the time is considered to be in the local timezone.
+ * @param {string=} format Formatting rules (see Description). If not specified,
+ * `mediumDate` is used.
+ * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
+ * @returns {string} Formatted string or the input if input is not recognized as date/millis.
+ *
+ * @example
+
+
+ {{1288323623006 | date:'medium'}} :
+ {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}} :
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'MM/dd/yyyy @ h:mma'}} :
+ {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+ {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}} :
+ {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
+
+
+ it('should format date', function() {
+ expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
+ toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
+ expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
+ toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/);
+ expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
+ toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
+ expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
+ toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/);
+ });
+
+
+ */
+ dateFilter.$inject = ['$locale'];
+ function dateFilter($locale) {
+
+
+ var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
+ // 1 2 3 4 5 6 7 8 9 10 11
+ function jsonStringToDate(string) {
+ var match;
+ if ((match = string.match(R_ISO8601_STR))) {
+ var date = new Date(0),
+ tzHour = 0,
+ tzMin = 0,
+ dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear,
+ timeSetter = match[8] ? date.setUTCHours : date.setHours;
+
+ if (match[9]) {
+ tzHour = toInt(match[9] + match[10]);
+ tzMin = toInt(match[9] + match[11]);
+ }
+ dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
+ var h = toInt(match[4] || 0) - tzHour;
+ var m = toInt(match[5] || 0) - tzMin;
+ var s = toInt(match[6] || 0);
+ var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
+ timeSetter.call(date, h, m, s, ms);
+ return date;
+ }
+ return string;
+ }
+
+
+ return function(date, format, timezone) {
+ var text = '',
+ parts = [],
+ fn, match;
+
+ format = format || 'mediumDate';
+ format = $locale.DATETIME_FORMATS[format] || format;
+ if (isString(date)) {
+ date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
+ }
+
+ if (isNumber(date)) {
+ date = new Date(date);
+ }
+
+ if (!isDate(date) || !isFinite(date.getTime())) {
+ return date;
+ }
+
+ while (format) {
+ match = DATE_FORMATS_SPLIT.exec(format);
+ if (match) {
+ parts = concat(parts, match, 1);
+ format = parts.pop();
+ } else {
+ parts.push(format);
+ format = null;
+ }
+ }
+
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ if (timezone) {
+ 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 === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
+ });
+
+ return text;
+ };
+ }
+
+
+ /**
+ * @ngdoc filter
+ * @name json
+ * @kind function
+ *
+ * @description
+ * Allows you to convert a JavaScript object into JSON string.
+ *
+ * This filter is mostly useful for debugging. When using the double curly {{value}} notation
+ * the binding is automatically converted to JSON.
+ *
+ * @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
+ * @param {number=} spacing The number of spaces to use per indentation, defaults to 2.
+ * @returns {string} JSON string.
+ *
+ *
+ * @example
+
+
+ {{ {'name':'value'} | json }}
+ {{ {'name':'value'} | json:4 }}
+
+
+ it('should jsonify filtered objects', function() {
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/);
+ });
+
+
+ *
+ */
+ function jsonFilter() {
+ return function(object, spacing) {
+ if (isUndefined(spacing)) {
+ spacing = 2;
+ }
+ return toJson(object, spacing);
+ };
+ }
+
+
+ /**
+ * @ngdoc filter
+ * @name lowercase
+ * @kind function
+ * @description
+ * Converts string to lowercase.
+ *
+ * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example.
+ *
+ * @see angular.lowercase
+ */
+ var lowercaseFilter = valueFn(lowercase);
+
+
+ /**
+ * @ngdoc filter
+ * @name uppercase
+ * @kind function
+ * @description
+ * Converts string to uppercase.
+ * @example
+
+
+
+
+
+
{{title}}
+
+ {{title | uppercase}}
+
+
+
+ */
+ var uppercaseFilter = valueFn(uppercase);
+
+ /**
+ * @ngdoc filter
+ * @name limitTo
+ * @kind function
+ *
+ * @description
+ * Creates a new array or string containing only a specified number of elements. The elements are
+ * taken from either the beginning or the end of the source array, string or number, as specified by
+ * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported
+ * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input,
+ * it is converted to a string.
+ *
+ * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited.
+ * @param {string|number} limit - The length of the returned array or string. If the `limit` number
+ * is positive, `limit` number of items from the beginning of the source array/string are copied.
+ * If the number is negative, `limit` number of items from the end of the source array/string
+ * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
+ * the input will be returned unchanged.
+ * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index,
+ * `begin` indicates an offset from the end of `input`. Defaults to `0`.
+ * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had
+ * less than `limit` elements.
+ *
+ * @example
+
+
+
+
+
+ Limit {{numbers}} to:
+
+
+
Output numbers: {{ numbers | limitTo:numLimit }}
+
+ Limit {{letters}} to:
+
+
+
Output letters: {{ letters | limitTo:letterLimit }}
+
+ Limit {{longNumber}} to:
+
+
+
Output long number: {{ longNumber | limitTo:longNumberLimit }}
+
+
+
+ var numLimitInput = element(by.model('numLimit'));
+ var letterLimitInput = element(by.model('letterLimit'));
+ var longNumberLimitInput = element(by.model('longNumberLimit'));
+ var limitedNumbers = element(by.binding('numbers | limitTo:numLimit'));
+ var limitedLetters = element(by.binding('letters | limitTo:letterLimit'));
+ var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit'));
+
+ it('should limit the number array to first three items', function() {
+ expect(numLimitInput.getAttribute('value')).toBe('3');
+ expect(letterLimitInput.getAttribute('value')).toBe('3');
+ expect(longNumberLimitInput.getAttribute('value')).toBe('3');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abc');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 234');
+ });
+
+ // There is a bug in safari and protractor that doesn't like the minus key
+ // it('should update the output when -3 is entered', function() {
+ // numLimitInput.clear();
+ // numLimitInput.sendKeys('-3');
+ // letterLimitInput.clear();
+ // letterLimitInput.sendKeys('-3');
+ // longNumberLimitInput.clear();
+ // longNumberLimitInput.sendKeys('-3');
+ // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
+ // expect(limitedLetters.getText()).toEqual('Output letters: ghi');
+ // expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
+ // });
+
+ it('should not exceed the maximum size of input array', function() {
+ numLimitInput.clear();
+ numLimitInput.sendKeys('100');
+ letterLimitInput.clear();
+ letterLimitInput.sendKeys('100');
+ longNumberLimitInput.clear();
+ longNumberLimitInput.sendKeys('100');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342');
+ });
+
+
+ */
+ function limitToFilter() {
+ return function(input, limit, begin) {
+ if (Math.abs(Number(limit)) === Infinity) {
+ limit = Number(limit);
+ } else {
+ limit = toInt(limit);
+ }
+ if (isNumberNaN(limit)) return input;
+
+ if (isNumber(input)) input = input.toString();
+ if (!isArrayLike(input)) return input;
+
+ begin = (!begin || isNaN(begin)) ? 0 : toInt(begin);
+ begin = (begin < 0) ? Math.max(0, input.length + begin) : begin;
+
+ if (limit >= 0) {
+ return sliceFn(input, begin, begin + limit);
+ } else {
+ if (begin === 0) {
+ return sliceFn(input, limit, input.length);
+ } else {
+ return sliceFn(input, Math.max(0, begin + limit), begin);
+ }
+ }
+ };
+ }
+
+ function sliceFn(input, begin, end) {
+ if (isString(input)) return input.slice(begin, end);
+
+ return slice.call(input, begin, end);
+ }
+
+ /**
+ * @ngdoc filter
+ * @name orderBy
+ * @kind function
+ *
+ * @description
+ * Returns an array containing the items from the specified `collection`, ordered by a `comparator`
+ * function based on the values computed using the `expression` predicate.
+ *
+ * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in
+ * `[{id: 'bar'}, {id: 'foo'}]`.
+ *
+ * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray,
+ * String, etc).
+ *
+ * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
+ * for the preceding one. The `expression` is evaluated against each item and the output is used
+ * for comparing with other items.
+ *
+ * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
+ * ascending order.
+ *
+ * The comparison is done using the `comparator` function. If none is specified, a default, built-in
+ * comparator is used (see below for details - in a nutshell, it compares numbers numerically and
+ * strings alphabetically).
+ *
+ * ### Under the hood
+ *
+ * Ordering the specified `collection` happens in two phases:
+ *
+ * 1. All items are passed through the predicate (or predicates), and the returned values are saved
+ * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed
+ * through a predicate that extracts the value of the `label` property, would be transformed to:
+ * ```
+ * {
+ * value: 'foo',
+ * type: 'string',
+ * index: ...
+ * }
+ * ```
+ * 2. The comparator function is used to sort the items, based on the derived values, types and
+ * indices.
+ *
+ * If you use a custom comparator, it will be called with pairs of objects of the form
+ * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal
+ * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the
+ * second, or `1` otherwise.
+ *
+ * In order to ensure that the sorting will be deterministic across platforms, if none of the
+ * specified predicates can distinguish between two items, `orderBy` will automatically introduce a
+ * dummy predicate that returns the item's index as `value`.
+ * (If you are using a custom comparator, make sure it can handle this predicate as well.)
+ *
+ * If a custom comparator still can't distinguish between two items, then they will be sorted based
+ * on their index using the built-in comparator.
+ *
+ * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
+ * value for an item, `orderBy` will try to convert that object to a primitive value, before passing
+ * it to the comparator. The following rules govern the conversion:
+ *
+ * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be
+ * used instead.
+ * (If the object has a `valueOf()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that
+ * returns a primitive, its return value will be used instead.
+ * (If the object has a `toString()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 3. No conversion; the object itself is used.
+ *
+ * ### The default comparator
+ *
+ * The default, built-in comparator should be sufficient for most usecases. In short, it compares
+ * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
+ * using their index in the original collection, and sorts values of different types by type.
+ *
+ * More specifically, it follows these steps to determine the relative order of items:
+ *
+ * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 2. If both values are of type `string`, compare them alphabetically in a case- and
+ * locale-insensitive way.
+ * 3. If both values are objects, compare their indices instead.
+ * 4. Otherwise, return:
+ * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`).
+ * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator).
+ * - `1`, otherwise.
+ *
+ * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
+ * saved as numbers and not strings.
+ * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e.
+ * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to
+ * other values.
+ *
+ * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
+ * @param {(Function|string|Array.
)=} expression - A predicate (or list of
+ * predicates) to be used by the comparator to determine the order of elements.
+ *
+ * Can be one of:
+ *
+ * - `Function`: A getter function. This function will be called with each item as argument and
+ * the return value will be used for sorting.
+ * - `string`: An AngularJS expression. This expression will be evaluated against each item and the
+ * result will be used for sorting. For example, use `'label'` to sort by a property called
+ * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
+ * property.
+ * (The result of a constant expression is interpreted as a property name to be used for
+ * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a
+ * property called `special name`.)
+ * An expression can be optionally prefixed with `+` or `-` to control the sorting direction,
+ * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided,
+ * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons.
+ * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the
+ * relative order of two items, the next predicate is used as a tie-breaker.
+ *
+ * **Note:** If the predicate is missing or empty then it defaults to `'+'`.
+ *
+ * @param {boolean=} reverse - If `true`, reverse the sorting order.
+ * @param {(Function)=} comparator - The comparator function used to determine the relative order of
+ * value pairs. If omitted, the built-in comparator will be used.
+ *
+ * @returns {Array} - The sorted array.
+ *
+ *
+ * @example
+ * ### Ordering a table with `ngRepeat`
+ *
+ * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by
+ * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means
+ * it defaults to the built-in comparator.
+ *
+
+
+
+
+
+ Name
+ Phone Number
+ Age
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample1', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var names = element.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by age in reverse order', function() {
+ expect(names.get(0).getText()).toBe('Adam');
+ expect(names.get(1).getText()).toBe('Julie');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('John');
+ });
+
+
+ *
*
- * To be secure by default, you want to ensure that any such bindings are disallowed unless you can
- * determine that something explicitly says it's safe to use a value for binding in that
- * context. You can then audit your code (a simple grep would do) to ensure that this is only done
- * for those values that you can easily tell are safe - because they were received from your server,
- * sanitized by your library, etc. You can organize your codebase to help with this - perhaps
- * allowing only the files in a specific directory to do this. Ensuring that the internal API
- * exposed by that code doesn't markup arbitrary values as safe then becomes a more manageable task.
+ * @example
+ * ### Changing parameters dynamically
*
- * In the case of AngularJS' SCE service, one uses {@link ng.$sce#trustAs $sce.trustAs}
- * (and shorthand methods such as {@link ng.$sce#trustAsHtml $sce.trustAsHtml}, etc.) to
- * obtain values that will be accepted by SCE / privileged contexts.
+ * All parameters can be changed dynamically. The next example shows how you can make the columns of
+ * a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
+
+
+
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample2', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = friends;
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Using `orderBy` inside a controller
+ *
+ * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and
+ * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory
+ * and retrieve the `orderBy` filter with `$filter('orderBy')`.)
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
+
+
+
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample3', [])
+ .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
+ ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Using a custom comparator
+ *
+ * If you have very specific requirements about the way items are sorted, you can pass your own
+ * comparator function. For example, you might need to compare some strings in a locale-sensitive
+ * way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
+ * argument - passing `false` retains the default sorting order, i.e. ascending.)
+ *
+
+
+
+
+
Locale-sensitive Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
Default Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
+
+ angular.module('orderByExample4', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', favoriteLetter: 'Ä'},
+ {name: 'Mary', favoriteLetter: 'Ü'},
+ {name: 'Mike', favoriteLetter: 'Ö'},
+ {name: 'Adam', favoriteLetter: 'H'},
+ {name: 'Julie', favoriteLetter: 'Z'}
+ ];
+
+ $scope.localeSensitiveComparator = function(v1, v2) {
+ // If we don't get strings, just compare by index
+ if (v1.type !== 'string' || v2.type !== 'string') {
+ return (v1.index < v2.index) ? -1 : 1;
+ }
+
+ // Compare strings alphabetically, taking locale into account
+ return v1.value.localeCompare(v2.value);
+ };
+ }]);
+
+
+ .friends-container {
+ display: inline-block;
+ margin: 0 30px;
+ }
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var container = element(by.css('.custom-comparator'));
+ var names = container.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by favorite letter (in correct alphabetical order)', function() {
+ expect(names.get(0).getText()).toBe('John');
+ expect(names.get(1).getText()).toBe('Adam');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('Julie');
+ });
+
+
*
+ */
+ orderByFilter.$inject = ['$parse'];
+ function orderByFilter($parse) {
+ return function(array, sortPredicate, reverseOrder, compareFn) {
+
+ 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 = ['+']; }
+
+ var predicates = processPredicates(sortPredicate);
+
+ var descending = reverseOrder ? -1 : 1;
+
+ // Define the `compare()` function. Use a default comparator if none is specified.
+ var compare = isFunction(compareFn) ? compareFn : defaultCompare;
+
+ // The next three lines are a version of a Swartzian Transform idiom from Perl
+ // (sometimes called the Decorate-Sort-Undecorate idiom)
+ // See https://en.wikipedia.org/wiki/Schwartzian_transform
+ var compareValues = Array.prototype.map.call(array, getComparisonObject);
+ compareValues.sort(doComparison);
+ array = compareValues.map(function(item) { return item.value; });
+
+ return array;
+
+ function getComparisonObject(value, index) {
+ // NOTE: We are adding an extra `tieBreaker` value based on the element's index.
+ // This will be used to keep the sort stable when none of the input predicates can
+ // distinguish between two elements.
+ return {
+ value: value,
+ tieBreaker: {value: index, type: 'number', index: index},
+ predicateValues: predicates.map(function(predicate) {
+ return getPredicateValue(predicate.get(value), index);
+ })
+ };
+ }
+
+ function doComparison(v1, v2) {
+ for (var i = 0, ii = predicates.length; i < ii; i++) {
+ var result = compare(v1.predicateValues[i], v2.predicateValues[i]);
+ if (result) {
+ return result * predicates[i].descending * descending;
+ }
+ }
+
+ return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending;
+ }
+ };
+
+ function processPredicates(sortPredicates) {
+ return sortPredicates.map(function(predicate) {
+ var descending = 1, get = identity;
+
+ if (isFunction(predicate)) {
+ get = predicate;
+ } else if (isString(predicate)) {
+ if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) {
+ descending = predicate.charAt(0) === '-' ? -1 : 1;
+ predicate = predicate.substring(1);
+ }
+ if (predicate !== '') {
+ get = $parse(predicate);
+ if (get.constant) {
+ var key = get();
+ get = function(value) { return value[key]; };
+ }
+ }
+ }
+ return {get: get, descending: descending};
+ });
+ }
+
+ function isPrimitive(value) {
+ switch (typeof value) {
+ case 'number': /* falls through */
+ case 'boolean': /* falls through */
+ case 'string':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ function objectValue(value) {
+ // If `valueOf` is a valid function use that
+ if (isFunction(value.valueOf)) {
+ value = value.valueOf();
+ if (isPrimitive(value)) return value;
+ }
+ // If `toString` is a valid function and not the one from `Object.prototype` use that
+ if (hasCustomToString(value)) {
+ value = value.toString();
+ if (isPrimitive(value)) return value;
+ }
+
+ return value;
+ }
+
+ function getPredicateValue(value, index) {
+ var type = typeof value;
+ if (value === null) {
+ type = 'string';
+ value = 'null';
+ } else if (type === 'object') {
+ value = objectValue(value);
+ }
+ return {value: value, type: type, index: index};
+ }
+
+ function defaultCompare(v1, v2) {
+ var result = 0;
+ var type1 = v1.type;
+ var type2 = v2.type;
+
+ if (type1 === type2) {
+ var value1 = v1.value;
+ var value2 = v2.value;
+
+ if (type1 === 'string') {
+ // Compare strings case-insensitively
+ value1 = value1.toLowerCase();
+ value2 = value2.toLowerCase();
+ } else if (type1 === 'object') {
+ // For basic objects, use the position of the object
+ // in the collection instead of the value
+ if (isObject(value1)) value1 = v1.index;
+ if (isObject(value2)) value2 = v2.index;
+ }
+
+ if (value1 !== value2) {
+ result = value1 < value2 ? -1 : 1;
+ }
+ } else {
+ result = type1 < type2 ? -1 : 1;
+ }
+
+ return result;
+ }
+ }
+
+ function ngDirective(directive) {
+ if (isFunction(directive)) {
+ directive = {
+ link: directive
+ };
+ }
+ directive.restrict = directive.restrict || 'AC';
+ return valueFn(directive);
+ }
+
+ /**
+ * @ngdoc directive
+ * @name a
+ * @restrict E
+ *
+ * @description
+ * Modifies the default behavior of the html a tag so that the default action is prevented when
+ * the href attribute is empty.
+ *
+ * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive.
+ */
+ var htmlAnchorDirective = valueFn({
+ restrict: 'E',
+ compile: function(element, attr) {
+ if (!attr.href && !attr.xlinkHref) {
+ return function(scope, element) {
+ // If the linked element is not an anchor tag anymore, do nothing
+ if (element[0].nodeName.toLowerCase() !== 'a') return;
+
+ // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
+ var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
+ 'xlink:href' : 'href';
+ element.on('click', function(event) {
+ // if we have no href url, then don't navigate anywhere.
+ if (!element.attr(href)) {
+ event.preventDefault();
+ }
+ });
+ };
+ }
+ }
+ });
+
+ /**
+ * @ngdoc directive
+ * @name ngHref
+ * @restrict A
+ * @priority 99
+ *
+ * @description
+ * Using AngularJS markup like `{{hash}}` in an href attribute will
+ * make the link go to the wrong URL if the user clicks it before
+ * AngularJS has a chance to replace the `{{hash}}` markup with its
+ * value. Until AngularJS replaces the markup the link will be broken
+ * and will most likely return a 404 error. The `ngHref` directive
+ * solves this problem.
+ *
+ * The wrong way to write it:
+ * ```html
+ * link1
+ * ```
+ *
+ * The correct way to write it:
+ * ```html
+ * link1
+ * ```
+ *
+ * @element A
+ * @param {template} ngHref any string which can contain `{{}}` markup.
+ *
+ * @example
+ * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
+ * in links and their different behaviors:
+
+
+
+ link 1 (link, don't reload)
+ link 2 (link, don't reload)
+ link 3 (link, reload!)
+ anchor (link, don't reload)
+ anchor (no link)
+ link (link, change location)
+
+
+ it('should execute ng-click but not reload when href without value', function() {
+ element(by.id('link-1')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('1');
+ expect(element(by.id('link-1')).getAttribute('href')).toBe('');
+ });
+
+ it('should execute ng-click but not reload when href empty string', function() {
+ element(by.id('link-2')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('2');
+ expect(element(by.id('link-2')).getAttribute('href')).toBe('');
+ });
+
+ it('should execute ng-click and change url when ng-href specified', function() {
+ expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/);
+
+ element(by.id('link-3')).click();
+
+ // At this point, we navigate away from an AngularJS page, so we need
+ // to use browser.driver to get the base webdriver.
+
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/123$/);
+ });
+ }, 5000, 'page should navigate to /123');
+ });
+
+ it('should execute ng-click but not reload when href empty string and name specified', function() {
+ element(by.id('link-4')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('4');
+ expect(element(by.id('link-4')).getAttribute('href')).toBe('');
+ });
+
+ it('should execute ng-click but not reload when no href but name specified', function() {
+ element(by.id('link-5')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('5');
+ expect(element(by.id('link-5')).getAttribute('href')).toBe(null);
+ });
+
+ it('should only change url when only ng-href', function() {
+ element(by.model('value')).clear();
+ element(by.model('value')).sendKeys('6');
+ expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/);
+
+ element(by.id('link-6')).click();
+
+ // At this point, we navigate away from an AngularJS page, so we need
+ // to use browser.driver to get the base webdriver.
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/6$/);
+ });
+ }, 5000, 'page should navigate to /6');
+ });
+
+
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngSrc
+ * @restrict A
+ * @priority 99
*
- * ## How does it work?
+ * @description
+ * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't
+ * work right: The browser will fetch from the URL with the literal
+ * text `{{hash}}` until AngularJS replaces the expression inside
+ * `{{hash}}`. The `ngSrc` directive solves this problem.
*
- * In privileged contexts, directives and code will bind to the result of {@link ng.$sce#getTrusted
- * $sce.getTrusted(context, value)} rather than to the value directly. Directives use {@link
- * ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
- * {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
+ * The buggy way to write it:
+ * ```html
+ *
+ * ```
*
- * As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
- * ng.$sce#parseAsHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
- * simplified):
+ * The correct way to write it:
+ * ```html
+ *
+ * ```
*
- *
- * var ngBindHtmlDirective = ['$sce', function($sce) {
- * return function(scope, element, attr) {
- * scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
- * element.html(value || '');
- * });
- * };
- * }];
- *
+ * @element IMG
+ * @param {template} ngSrc any string which can contain `{{}}` markup.
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngSrcset
+ * @restrict A
+ * @priority 99
*
- * ## Impact on loading templates
+ * @description
+ * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't
+ * work right: The browser will fetch from the URL with the literal
+ * text `{{hash}}` until AngularJS replaces the expression inside
+ * `{{hash}}`. The `ngSrcset` directive solves this problem.
*
- * This applies both to the {@link ng.directive:ngInclude `ng-include`} directive as well as
- * `templateUrl`'s specified by {@link guide/directive directives}.
+ * The buggy way to write it:
+ * ```html
+ *
+ * ```
*
- * By default, Angular only loads templates from the same domain and protocol as the application
- * document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl} on the template URL. To load templates from other domains and/or
- * protocols, you may either either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist
- * them} or {@link ng.$sce#trustAsResourceUrl wrap it} into a trusted value.
+ * The correct way to write it:
+ * ```html
+ *
+ * ```
*
- * *Please note*:
- * The browser's
- * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
- * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
- * policy apply in addition to this and may further restrict whether the template is successfully
- * loaded. This means that without the right CORS policy, loading templates from a different domain
- * won't work on all browsers. Also, loading templates from `file://` URL does not work on some
- * browsers.
+ * @element IMG
+ * @param {template} ngSrcset any string which can contain `{{}}` markup.
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngDisabled
+ * @restrict A
+ * @priority 100
*
- * ## This feels like too much overhead for the developer?
+ * @description
*
- * It's important to remember that SCE only applies to interpolation expressions.
+ * This directive sets the `disabled` attribute on the element (typically a form control,
+ * e.g. `input`, `button`, `select` etc.) if the
+ * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
- * If your expressions are constant literals, they're automatically trusted and you don't need to
- * call `$sce.trustAs` on them (remember to include the `ngSanitize` module) (e.g.
- * `
`) just works.
+ * A special directive is necessary because we cannot use interpolation inside the `disabled`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
- * Additionally, `a[href]` and `img[src]` automatically sanitize their URLs and do not pass them
- * through {@link ng.$sce#getTrusted $sce.getTrusted}. SCE doesn't play a role here.
+ * @example
+
+
+ Click me to toggle:
+ Button
+
+
+ it('should toggle button', function() {
+ expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy();
+ });
+
+
*
- * The included {@link ng.$sceDelegate $sceDelegate} comes with sane defaults to allow you to load
- * templates in `ng-include` from your application's domain without having to even know about SCE.
- * It blocks loading templates from other domains or loading templates over http from an https
- * served document. You can change these by setting your own custom {@link
- * ng.$sceDelegateProvider#resourceUrlWhitelist whitelists} and {@link
- * ng.$sceDelegateProvider#resourceUrlBlacklist blacklists} for matching such URLs.
+ * @element INPUT
+ * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
+ * then the `disabled` attribute will be set on the element
+ */
+
+
+ /**
+ * @ngdoc directive
+ * @name ngChecked
+ * @restrict A
+ * @priority 100
*
- * This significantly reduces the overhead. It is far easier to pay the small overhead and have an
- * application that's secure and can be audited to verify that with much more ease than bolting
- * security onto an application later.
+ * @description
+ * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy.
*
- *
- * ## What trusted context types are supported?
+ * Note that this directive should not be used together with {@link ngModel `ngModel`},
+ * as this can lead to unexpected behavior.
*
- * | Context | Notes |
- * |---------------------|----------------|
- * | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. |
- * | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
- * | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (` Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
- * | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
- * ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist}
+ * @example
+
+
+ Check me to check both:
+
+
+
+ it('should check both checkBoxes', function() {
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy();
+ element(by.model('leader')).click();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy();
+ });
+
+
*
- * Each element in these arrays must be one of the following:
+ * @element INPUT
+ * @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
+ * then the `checked` attribute will be set on the element
+ */
+
+
+ /**
+ * @ngdoc directive
+ * @name ngReadonly
+ * @restrict A
+ * @priority 100
*
- * - **'self'**
- * - The special **string**, `'self'`, can be used to match against all URLs of the **same
- * domain** as the application document using the **same protocol**.
- * - **String** (except the special value `'self'`)
- * - The string is matched against the full *normalized / absolute URL* of the resource
- * being tested (substring matches are not good enough.)
- * - There are exactly **two wildcard sequences** - `*` and `**`. All other characters
- * match themselves.
- * - `*`: matches zero or more occurrences of any character other than one of the following 6
- * characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use
- * in a whitelist.
- * - `**`: matches zero or more occurrences of *any* character. As such, it's not
- * not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g.
- * http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
- * not have been the intention.) It's usage at the very end of the path is ok. (e.g.
- * http://foo.example.com/templates/**).
- * - **RegExp** (*see caveat below*)
- * - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax
- * (and all the inevitable escaping) makes them *harder to maintain*. It's easy to
- * accidentally introduce a bug when one updates a complex expression (imho, all regexes should
- * have good test coverage.). For instance, the use of `.` in the regex is correct only in a
- * small number of cases. A `.` character in the regex used when matching the scheme or a
- * subdomain could be matched against a `:` or literal `.` that was likely not intended. It
- * is highly recommended to use the string patterns and only fall back to regular expressions
- * if they as a last resort.
- * - The regular expression must be an instance of RegExp (i.e. not a string.) It is
- * matched against the **entire** *normalized / absolute URL* of the resource being tested
- * (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags
- * present on the RegExp (such as multiline, global, ignoreCase) are ignored.
- * - If you are generating your JavaScript from some other templating engine (not
- * recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)),
- * remember to escape your regular expression (and be aware that you might need more than
- * one level of escaping depending on your templating engine and the way you interpolated
- * the value.) Do make use of your platform's escaping mechanism as it might be good
- * enough before coding your own. e.g. Ruby has
- * [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape)
- * and Python has [re.escape](http://docs.python.org/library/re.html#re.escape).
- * Javascript lacks a similar built in function for escaping. Take a look at Google
- * Closure library's [goog.string.regExpEscape(s)](
- * http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962).
+ * @description
*
- * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} for an example.
+ * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on
+ * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information.
*
- * ## Show me an example using SCE.
+ * 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
-
+
-
-
-
User comments
- By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when
- $sanitize is available. If $sanitize isn't available, this results in an error instead of an
- exploit.
-
-
- {{userComment.name}} :
-
-
-
-
-
+ Check me to make text readonly:
+
-
-
- var mySceApp = angular.module('mySceApp', ['ngSanitize']);
-
- mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) {
- var self = this;
- $http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
- self.userComments = userComments;
- });
- self.explicitlyTrustedHtml = $sce.trustAsHtml(
- 'Hover over this text. ');
- });
+
+ it('should toggle readonly attr', function() {
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy();
+ });
+
+ *
+ * @element INPUT
+ * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
+ * then special attribute "readonly" will be set on the element
+ */
-
- [
- { "name": "Alice",
- "htmlComment":
- "Is anyone reading this? "
- },
- { "name": "Bob",
- "htmlComment": "Yes! Am I the only other one?"
- }
- ]
-
+ /**
+ * @ngdoc directive
+ * @name ngSelected
+ * @restrict A
+ * @priority 100
+ *
+ * @description
+ *
+ * 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.
+ *
+ *
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only
+ * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you
+ * should not use `ngSelected` on the options, as `ngModel` will set the select value and
+ * selected options.
+ *
+ *
+ * @example
+
+
+ Check me to select:
+
+ Hello!
+ Greetings!
+
+
- describe('SCE doc demo', function() {
- it('should sanitize untrusted values', function() {
- expect(element(by.css('.htmlComment')).getInnerHtml())
- .toBe('Is anyone reading this? ');
- });
-
- it('should NOT sanitize explicitly trusted values', function() {
- expect(element(by.id('explicitlyTrustedHtml')).getInnerHtml()).toBe(
- 'Hover over this text. ');
- });
- });
+ it('should select Greetings!', function() {
+ expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy();
+ element(by.model('selected')).click();
+ expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy();
+ });
*
+ * @element OPTION
+ * @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
+ * then special attribute "selected" will be set on the element
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngOpen
+ * @restrict A
+ * @priority 100
+ *
+ * @description
*
+ * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy.
*
- * ## Can I disable SCE completely?
+ * A special directive is necessary because we cannot use interpolation inside the `open`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
- * Yes, you can. However, this is strongly discouraged. SCE gives you a lot of security benefits
- * for little coding overhead. It will be much harder to take an SCE disabled application and
- * either secure it on your own or enable SCE at a later stage. It might make sense to disable SCE
- * for cases where you have a lot of existing code that was written before SCE was introduced and
- * you're migrating them a module at a time.
+ * ## A note about browser compatibility
*
- * That said, here's how you can completely disable SCE:
+ * Internet Explorer and Edge do not support the `details` element, it is
+ * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
*
- *
- * angular.module('myAppWithSceDisabledmyApp', []).config(function($sceProvider) {
- * // Completely disable SCE. For demonstration purposes only!
- * // Do not use in new projects.
- * $sceProvider.enabled(false);
- * });
- *
+ * @example
+
+
+ Toggle details:
+
+ List
+
+ Apple
+ Orange
+ Durian
+
+
+
+
+ it('should toggle open', function() {
+ expect(element(by.id('details')).getAttribute('open')).toBeFalsy();
+ element(by.model('open')).click();
+ expect(element(by.id('details')).getAttribute('open')).toBeTruthy();
+ });
+
+
*
+ * @element DETAILS
+ * @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
+ * then special attribute "open" will be set on the element
*/
- /* jshint maxlen: 100 */
-
- function $SceProvider() {
- var enabled = true;
-
- /**
- * @ngdoc method
- * @name $sceProvider#enabled
- * @function
- *
- * @param {boolean=} value If provided, then enables/disables SCE.
- * @return {boolean} true if SCE is enabled, false otherwise.
- *
- * @description
- * Enables/disables SCE and returns the current value.
- */
- this.enabled = function (value) {
- if (arguments.length) {
- enabled = !!value;
- }
- return enabled;
- };
-
-
- /* Design notes on the default implementation for SCE.
- *
- * The API contract for the SCE delegate
- * -------------------------------------
- * The SCE delegate object must provide the following 3 methods:
- *
- * - trustAs(contextEnum, value)
- * This method is used to tell the SCE service that the provided value is OK to use in the
- * contexts specified by contextEnum. It must return an object that will be accepted by
- * getTrusted() for a compatible contextEnum and return this value.
- *
- * - valueOf(value)
- * For values that were not produced by trustAs(), return them as is. For values that were
- * produced by trustAs(), return the corresponding input value to trustAs. Basically, if
- * trustAs is wrapping the given values into some type, this operation unwraps it when given
- * such a value.
- *
- * - getTrusted(contextEnum, value)
- * This function should return the a value that is safe to use in the context specified by
- * contextEnum or throw and exception otherwise.
- *
- * NOTE: This contract deliberately does NOT state that values returned by trustAs() must be
- * opaque or wrapped in some holder object. That happens to be an implementation detail. For
- * instance, an implementation could maintain a registry of all trusted objects by context. In
- * such a case, trustAs() would return the same object that was passed in. getTrusted() would
- * return the same object passed in if it was found in the registry under a compatible context or
- * throw an exception otherwise. An implementation might only wrap values some of the time based
- * on some criteria. getTrusted() might return a value and not throw an exception for special
- * constants or objects even if not wrapped. All such implementations fulfill this contract.
- *
- *
- * A note on the inheritance model for SCE contexts
- * ------------------------------------------------
- * I've used inheritance and made RESOURCE_URL wrapped types a subtype of URL wrapped types. This
- * is purely an implementation details.
- *
- * The contract is simply this:
- *
- * getTrusted($sce.RESOURCE_URL, value) succeeding implies that getTrusted($sce.URL, value)
- * will also succeed.
- *
- * Inheritance happens to capture this in a natural way. In some future, we
- * may not use inheritance anymore. That is OK because no code outside of
- * sce.js and sceSpecs.js would need to be aware of this detail.
- */
- this.$get = ['$parse', '$sniffer', '$sceDelegate', function(
- $parse, $sniffer, $sceDelegate) {
- // Prereq: Ensure that we're not running in IE8 quirks mode. In that mode, IE allows
- // the "expression(javascript expression)" syntax which is insecure.
- if (enabled && $sniffer.msie && $sniffer.msieDocumentMode < 8) {
- throw $sceMinErr('iequirks',
- 'Strict Contextual Escaping does not support Internet Explorer version < 9 in quirks ' +
- 'mode. You can fix this by adding the text to the top of your HTML ' +
- 'document. See http://docs.angularjs.org/api/ng.$sce for more information.');
- }
+ var ngAttributeAliasDirectives = {};
- var sce = copy(SCE_CONTEXTS);
+// boolean attrs are evaluated
+ forEach(BOOLEAN_ATTR, function(propName, attrName) {
+ // binding to multiple is not supported
+ if (propName === 'multiple') return;
- /**
- * @ngdoc method
- * @name $sce#isEnabled
- * @function
- *
- * @return {Boolean} true if SCE is enabled, false otherwise. If you want to set the value, you
- * have to do it at module config time on {@link ng.$sceProvider $sceProvider}.
- *
- * @description
- * Returns a boolean indicating if SCE is enabled.
- */
- sce.isEnabled = function () {
- return enabled;
- };
- sce.trustAs = $sceDelegate.trustAs;
- sce.getTrusted = $sceDelegate.getTrusted;
- sce.valueOf = $sceDelegate.valueOf;
+ function defaultLinkFn(scope, element, attr) {
+ scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
+ attr.$set(attrName, !!value);
+ });
+ }
- if (!enabled) {
- sce.trustAs = sce.getTrusted = function(type, value) { return value; };
- sce.valueOf = identity;
- }
+ var normalized = directiveNormalize('ng-' + attrName);
+ var linkFn = defaultLinkFn;
- /**
- * @ngdoc method
- * @name $sce#parse
- *
- * @description
- * Converts Angular {@link guide/expression expression} into a function. This is like {@link
- * ng.$parse $parse} and is identical when the expression is a literal constant. Otherwise, it
- * wraps the expression in a call to {@link ng.$sce#getTrusted $sce.getTrusted(*type*,
- * *result*)}
- *
- * @param {string} type The kind of SCE context in which this result will be used.
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
- sce.parseAs = function sceParseAs(type, expr) {
- var parsed = $parse(expr);
- if (parsed.literal && parsed.constant) {
- return parsed;
- } else {
- return function sceParseAsTrusted(self, locals) {
- return sce.getTrusted(type, parsed(self, locals));
- };
+ if (propName === 'checked') {
+ linkFn = function(scope, element, attr) {
+ // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
+ if (attr.ngModel !== attr[normalized]) {
+ defaultLinkFn(scope, element, attr);
}
};
+ }
- /**
- * @ngdoc method
- * @name $sce#trustAs
- *
- * @description
- * Delegates to {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs`}. As such,
- * returns an object that is trusted by angular for use in specified strict contextual
- * escaping contexts (such as ng-bind-html, ng-include, any src attribute
- * interpolation, any dom event binding attribute interpolation such as for onclick, etc.)
- * that uses the provided value. See * {@link ng.$sce $sce} for enabling strict contextual
- * escaping.
- *
- * @param {string} type The kind of context in which this value is safe for use. e.g. url,
- * resource_url, html, js and css.
- * @param {*} value The value that that should be considered trusted/safe.
- * @returns {*} A value that can be used to stand in for the provided `value` in places
- * where Angular expects a $sce.trustAs() return value.
- */
-
- /**
- * @ngdoc method
- * @name $sce#trustAsHtml
- *
- * @description
- * Shorthand method. `$sce.trustAsHtml(value)` →
- * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.HTML, value)`}
- *
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedHtml
- * $sce.getTrustedHtml(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
- */
-
- /**
- * @ngdoc method
- * @name $sce#trustAsUrl
- *
- * @description
- * Shorthand method. `$sce.trustAsUrl(value)` →
- * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.URL, value)`}
- *
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedUrl
- * $sce.getTrustedUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
- */
-
- /**
- * @ngdoc method
- * @name $sce#trustAsResourceUrl
- *
- * @description
- * Shorthand method. `$sce.trustAsResourceUrl(value)` →
- * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.RESOURCE_URL, value)`}
- *
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the return
- * value of {@link ng.$sce#trustAs $sce.trustAs}.)
- */
-
- /**
- * @ngdoc method
- * @name $sce#trustAsJs
- *
- * @description
- * Shorthand method. `$sce.trustAsJs(value)` →
- * {@link ng.$sceDelegate#trustAs `$sceDelegate.trustAs($sce.JS, value)`}
- *
- * @param {*} value The value to trustAs.
- * @returns {*} An object that can be passed to {@link ng.$sce#getTrustedJs
- * $sce.getTrustedJs(value)} to obtain the original value. (privileged directives
- * only accept expressions that are either literal constants or are the
- * return value of {@link ng.$sce#trustAs $sce.trustAs}.)
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrusted
- *
- * @description
- * Delegates to {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted`}. As such,
- * takes the result of a {@link ng.$sce#trustAs `$sce.trustAs`}() call and 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.
- *
- * @param {string} type The kind of context in which this value is to be used.
- * @param {*} maybeTrusted The result of a prior {@link ng.$sce#trustAs `$sce.trustAs`}
- * call.
- * @returns {*} The value the was originally provided to
- * {@link ng.$sce#trustAs `$sce.trustAs`} if valid in this context.
- * Otherwise, throws an exception.
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrustedHtml
- *
- * @description
- * Shorthand method. `$sce.getTrustedHtml(value)` →
- * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.HTML, value)`}
- *
- * @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.HTML, value)`
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrustedCss
- *
- * @description
- * Shorthand method. `$sce.getTrustedCss(value)` →
- * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.CSS, value)`}
- *
- * @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.CSS, value)`
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrustedUrl
- *
- * @description
- * Shorthand method. `$sce.getTrustedUrl(value)` →
- * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.URL, value)`}
- *
- * @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.URL, value)`
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrustedResourceUrl
- *
- * @description
- * Shorthand method. `$sce.getTrustedResourceUrl(value)` →
- * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.RESOURCE_URL, value)`}
- *
- * @param {*} value The value to pass to `$sceDelegate.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.RESOURCE_URL, value)`
- */
-
- /**
- * @ngdoc method
- * @name $sce#getTrustedJs
- *
- * @description
- * Shorthand method. `$sce.getTrustedJs(value)` →
- * {@link ng.$sceDelegate#getTrusted `$sceDelegate.getTrusted($sce.JS, value)`}
- *
- * @param {*} value The value to pass to `$sce.getTrusted`.
- * @returns {*} The return value of `$sce.getTrusted($sce.JS, value)`
- */
+ ngAttributeAliasDirectives[normalized] = function() {
+ return {
+ restrict: 'A',
+ priority: 100,
+ link: linkFn
+ };
+ };
+ });
- /**
- * @ngdoc method
- * @name $sce#parseAsHtml
- *
- * @description
- * Shorthand method. `$sce.parseAsHtml(expression string)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.HTML, value)`}
- *
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
+// aliased input attrs are evaluated
+ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
+ ngAttributeAliasDirectives[ngAttr] = function() {
+ return {
+ priority: 100,
+ link: function(scope, element, attr) {
+ //special case ngPattern when a literal regular expression value
+ //is used as the expression (this way we don't have to watch anything).
+ if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') {
+ var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
+ if (match) {
+ attr.$set('ngPattern', new RegExp(match[1], match[2]));
+ return;
+ }
+ }
- /**
- * @ngdoc method
- * @name $sce#parseAsCss
- *
- * @description
- * Shorthand method. `$sce.parseAsCss(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.CSS, value)`}
- *
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
+ scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
+ attr.$set(ngAttr, value);
+ });
+ }
+ };
+ };
+ });
- /**
- * @ngdoc method
- * @name $sce#parseAsUrl
- *
- * @description
- * Shorthand method. `$sce.parseAsUrl(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.URL, value)`}
- *
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
+// ng-src, ng-srcset, ng-href are interpolated
+ forEach(['src', 'srcset', 'href'], function(attrName) {
+ var normalized = directiveNormalize('ng-' + attrName);
+ ngAttributeAliasDirectives[normalized] = function() {
+ return {
+ priority: 99, // it needs to run after the attributes are interpolated
+ link: function(scope, element, attr) {
+ var propName = attrName,
+ name = attrName;
- /**
- * @ngdoc method
- * @name $sce#parseAsResourceUrl
- *
- * @description
- * Shorthand method. `$sce.parseAsResourceUrl(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.RESOURCE_URL, value)`}
- *
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
+ if (attrName === 'href' &&
+ toString.call(element.prop('href')) === '[object SVGAnimatedString]') {
+ name = 'xlinkHref';
+ attr.$attr[name] = 'xlink:href';
+ propName = null;
+ }
- /**
- * @ngdoc method
- * @name $sce#parseAsJs
- *
- * @description
- * Shorthand method. `$sce.parseAsJs(value)` →
- * {@link ng.$sce#parse `$sce.parseAs($sce.JS, value)`}
- *
- * @param {string} expression String expression to compile.
- * @returns {function(context, locals)} a function which represents the compiled expression:
- *
- * * `context` – `{object}` – an object against which any expressions embedded in the strings
- * are evaluated against (typically a scope object).
- * * `locals` – `{object=}` – local variables context object, useful for overriding values in
- * `context`.
- */
+ attr.$observe(normalized, function(value) {
+ if (!value) {
+ if (attrName === 'href') {
+ attr.$set(name, null);
+ }
+ return;
+ }
- // Shorthand delegations.
- var parse = sce.parseAs,
- getTrusted = sce.getTrusted,
- trustAs = sce.trustAs;
+ attr.$set(name, value);
- forEach(SCE_CONTEXTS, function (enumValue, name) {
- var lName = lowercase(name);
- sce[camelCase("parse_as_" + lName)] = function (expr) {
- return parse(enumValue, expr);
- };
- sce[camelCase("get_trusted_" + lName)] = function (value) {
- return getTrusted(enumValue, value);
- };
- sce[camelCase("trust_as_" + lName)] = function (value) {
- return trustAs(enumValue, value);
- };
- });
+ // Support: IE 9-11 only
+ // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
+ // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
+ // to set the property as well to achieve the desired effect.
+ // We use attr[attrName] value since $set can sanitize the url.
+ if (msie && propName) element.prop(propName, attr[name]);
+ });
+ }
+ };
+ };
+ });
- return sce;
- }];
+ /* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
+ */
+ var nullFormCtrl = {
+ $addControl: noop,
+ $$renameControl: nullFormRenameControl,
+ $removeControl: noop,
+ $setValidity: noop,
+ $setDirty: noop,
+ $setPristine: noop,
+ $setSubmitted: noop
+ },
+ PENDING_CLASS = 'ng-pending',
+ SUBMITTED_CLASS = 'ng-submitted';
+
+ function nullFormRenameControl(control, name) {
+ control.$name = name;
}
/**
- * !!! This is an undocumented "private" service !!!
+ * @ngdoc type
+ * @name form.FormController
*
- * @name $sniffer
- * @requires $window
- * @requires $document
+ * @property {boolean} $pristine True if user has not interacted with the form yet.
+ * @property {boolean} $dirty True if user has already interacted with the form.
+ * @property {boolean} $valid True if all of the containing forms and controls are valid.
+ * @property {boolean} $invalid True if at least one containing control or form is invalid.
+ * @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
- * @property {boolean} history Does the browser support html5 history api ?
- * @property {boolean} hashchange Does the browser support hashchange event ?
- * @property {boolean} transitions Does the browser support CSS transition events ?
- * @property {boolean} animations Does the browser support CSS animation events ?
+ * @property {Object} $pending An object hash, containing references to controls or forms with
+ * pending validators, where:
+ *
+ * - keys are validations tokens (error names).
+ * - values are arrays of controls or forms that have a pending validator for the given error name.
+ *
+ * See {@link form.FormController#$error $error} for a list of built-in validation tokens.
+ *
+ * @property {Object} $error An object hash, containing references to controls or forms with failing
+ * validators, where:
+ *
+ * - keys are validation tokens (error names),
+ * - values are arrays of controls or forms that have a failing validator for the given error name.
+ *
+ * Built-in validation tokens:
+ * - `email`
+ * - `max`
+ * - `maxlength`
+ * - `min`
+ * - `minlength`
+ * - `number`
+ * - `pattern`
+ * - `required`
+ * - `url`
+ * - `date`
+ * - `datetimelocal`
+ * - `time`
+ * - `week`
+ * - `month`
*
* @description
- * This is very simple implementation of testing browser's features.
+ * `FormController` keeps track of all its controls and nested forms as well as the state of them,
+ * such as being valid/invalid or dirty/pristine.
+ *
+ * Each {@link ng.directive:form form} directive creates an instance
+ * of `FormController`.
+ *
*/
- function $SnifferProvider() {
- this.$get = ['$window', '$document', function($window, $document) {
- var eventSupport = {},
- android =
- int((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]),
- boxee = /Boxee/i.test(($window.navigator || {}).userAgent),
- document = $document[0] || {},
- documentMode = document.documentMode,
- vendorPrefix,
- vendorRegex = /^(Moz|webkit|O|ms)(?=[A-Z])/,
- bodyStyle = document.body && document.body.style,
- transitions = false,
- animations = false,
- match;
-
- if (bodyStyle) {
- for(var prop in bodyStyle) {
- if(match = vendorRegex.exec(prop)) {
- vendorPrefix = match[0];
- vendorPrefix = vendorPrefix.substr(0, 1).toUpperCase() + vendorPrefix.substr(1);
- break;
- }
- }
-
- if(!vendorPrefix) {
- vendorPrefix = ('WebkitOpacity' in bodyStyle) && 'webkit';
- }
+//asks for $scope to fool the BC controller module
+ FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
+ function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
- transitions = !!(('transition' in bodyStyle) || (vendorPrefix + 'Transition' in bodyStyle));
- animations = !!(('animation' in bodyStyle) || (vendorPrefix + 'Animation' in bodyStyle));
+ // init state
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
+
+ this.$$element = $element;
+ this.$$animate = $animate;
+
+ setupValidity(this);
+ }
- if (android && (!transitions||!animations)) {
- transitions = isString(document.body.style.webkitTransition);
- animations = isString(document.body.style.webkitAnimation);
- }
- }
+ FormController.prototype = {
+ /**
+ * @ngdoc method
+ * @name form.FormController#$rollbackViewValue
+ *
+ * @description
+ * Rollback all form controls pending updates to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is typically needed by the reset button of
+ * a form that uses `ng-model-options` to pend updates.
+ */
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
+ control.$rollbackViewValue();
+ });
+ },
+ /**
+ * @ngdoc method
+ * @name form.FormController#$commitViewValue
+ *
+ * @description
+ * Commit all form controls pending updates to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
+ * usually handles calling this in response to input events.
+ */
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
+ control.$commitViewValue();
+ });
+ },
- return {
- // Android has history.pushState, but it does not update location correctly
- // so let's not use the history API at all.
- // http://code.google.com/p/android/issues/detail?id=17471
- // https://github.com/angular/angular.js/issues/904
+ /**
+ * @ngdoc method
+ * @name form.FormController#$addControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
+ *
+ * @description
+ * Register a control with the form. Input elements using ngModelController do this automatically
+ * when they are linked.
+ *
+ * Note that the current state of the control will not be reflected on the new parent form. This
+ * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine`
+ * state.
+ *
+ * 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 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.
+ */
+ $addControl: function(control) {
+ // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
+ // and not added to the scope. Now we throw an error.
+ assertNotHasOwnProperty(control.$name, 'input');
+ this.$$controls.push(control);
- // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has
- // so let's not use the history API also
- // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined
- // jshint -W018
- history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee),
- // jshint +W018
- hashchange: 'onhashchange' in $window &&
- // IE8 compatible mode lies
- (!documentMode || documentMode > 7),
- hasEvent: function(event) {
- // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
- // it. In particular the event is not fired when backspace or delete key are pressed or
- // when cut operation is performed.
- if (event == 'input' && msie == 9) return false;
+ if (control.$name) {
+ this[control.$name] = control;
+ }
- if (isUndefined(eventSupport[event])) {
- var divElm = document.createElement('div');
- eventSupport[event] = 'on' + event in divElm;
- }
+ control.$$parentForm = this;
+ },
- return eventSupport[event];
- },
- csp: csp(),
- vendorPrefix: vendorPrefix,
- transitions : transitions,
- animations : animations,
- android: android,
- msie : msie,
- msieDocumentMode: documentMode
- };
- }];
- }
+ // Private API: rename a form control
+ $$renameControl: function(control, newName) {
+ var oldName = control.$name;
- function $TimeoutProvider() {
- this.$get = ['$rootScope', '$browser', '$q', '$exceptionHandler',
- function($rootScope, $browser, $q, $exceptionHandler) {
- var deferreds = {};
+ if (this[oldName] === control) {
+ delete this[oldName];
+ }
+ this[newName] = control;
+ control.$name = newName;
+ },
+ /**
+ * @ngdoc method
+ * @name form.FormController#$removeControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
+ *
+ * @description
+ * Deregister a control from the form.
+ *
+ * Input elements using ngModelController do this automatically when they are destroyed.
+ *
+ * Note that only the removed control's validation state (`$errors`etc.) will be removed from the
+ * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be
+ * different from case to case. For example, removing the only `$dirty` control from a form may or
+ * may not mean that the form is still `$dirty`.
+ */
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
+ }
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
+ control.$$parentForm = nullFormCtrl;
+ },
- /**
- * @ngdoc service
- * @name $timeout
- *
- * @description
- * Angular's wrapper for `window.setTimeout`. The `fn` function is wrapped into a try/catch
- * block and delegates any exceptions to
- * {@link ng.$exceptionHandler $exceptionHandler} service.
- *
- * The return value of registering a timeout function is a promise, which will be resolved when
- * the timeout is reached and the timeout function is executed.
- *
- * To cancel a timeout request, call `$timeout.cancel(promise)`.
- *
- * In tests you can use {@link ngMock.$timeout `$timeout.flush()`} to
- * synchronously flush the queue of deferred functions.
- *
- * @param {function()} fn A function, whose execution should be delayed.
- * @param {number=} [delay=0] Delay in milliseconds.
- * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
- * will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
- * @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.
- *
- */
- function timeout(fn, delay, invokeApply) {
- var deferred = $q.defer(),
- promise = deferred.promise,
- skipApply = (isDefined(invokeApply) && !invokeApply),
- timeoutId;
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setDirty
+ *
+ * @description
+ * Sets the form to a dirty state.
+ *
+ * This method can be called to add the 'ng-dirty' class and set the form to a dirty
+ * state (ng-dirty class). This method will also propagate to parent forms.
+ */
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
- timeoutId = $browser.defer(function() {
- try {
- deferred.resolve(fn());
- } catch(e) {
- deferred.reject(e);
- $exceptionHandler(e);
- }
- finally {
- delete deferreds[promise.$$timeoutId];
- }
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setPristine
+ *
+ * @description
+ * Sets the form to its pristine state.
+ *
+ * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes
+ * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted`
+ * state to false.
+ *
+ * This method will also propagate to all the controls contained in this form.
+ *
+ * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
+ * saving or resetting it.
+ */
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
+ control.$setPristine();
+ });
+ },
- if (!skipApply) $rootScope.$apply();
- }, delay);
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setUntouched
+ *
+ * @description
+ * Sets the form to its untouched state.
+ *
+ * This method can be called to remove the 'ng-touched' class and set the form controls to their
+ * untouched state (ng-untouched class).
+ *
+ * Setting a form controls back to their untouched state is often useful when setting the form
+ * back to its pristine state.
+ */
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
+ control.$setUntouched();
+ });
+ },
- promise.$$timeoutId = timeoutId;
- deferreds[timeoutId] = deferred;
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setSubmitted
+ *
+ * @description
+ * Sets the form to its submitted state.
+ */
+ $setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ this.$$parentForm.$setSubmitted();
+ }
+ };
- return promise;
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Change the validity state of the form, and notify the parent form (if any).
+ *
+ * Application developers will rarely need to call this method directly. It is used internally, by
+ * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a
+ * control's validity state to the parent `FormController`.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be
+ * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for
+ * unfulfilled `$asyncValidators`), so that it is available for data-binding. The
+ * `validationErrorKey` should be in camelCase and will get converted into dash-case for
+ * class name. Example: `myError` will result in `ng-valid-my-error` and
+ * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending
+ * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and when
+ * `$asyncValidators` do not run because any of the `$validators` failed.
+ * @param {NgModelController | FormController} controller - The controller whose validity state is
+ * triggering the change.
+ */
+ addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
}
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+ });
-
- /**
- * @ngdoc method
- * @name $timeout#cancel
- *
- * @description
- * Cancels a task associated with the `promise`. As a result of this, the promise will be
- * resolved with a rejection.
- *
- * @param {Promise=} promise Promise returned by the `$timeout` function.
- * @returns {boolean} Returns `true` if the task hasn't executed yet and was successfully
- * canceled.
- */
- timeout.cancel = function(promise) {
- if (promise && promise.$$timeoutId in deferreds) {
- deferreds[promise.$$timeoutId].reject('canceled');
- delete deferreds[promise.$$timeoutId];
- return $browser.defer.cancel(promise.$$timeoutId);
- }
- return false;
- };
-
- return timeout;
- }];
- }
-
-// NOTE: The usage of window and document instead of $window and $document here is
-// deliberate. This service depends on the specific behavior of anchor nodes created by the
-// browser (resolving and parsing URLs) that is unlikely to be provided by mock objects and
-// cause us to break tests. In addition, when the browser resolves a URL for XHR, it
-// 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 originUrl = urlResolve(window.location.href, true);
-
+ /**
+ * @ngdoc directive
+ * @name ngForm
+ * @restrict EAC
+ *
+ * @description
+ * Nestable alias of {@link ng.directive:form `form`} directive. HTML
+ * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
+ * sub-group of controls needs to be determined.
+ *
+ * Note: the purpose of `ngForm` is to group controls,
+ * but not to be a replacement for the `
+ */
+
+
+ /**
+ * @ngdoc directive
+ * @name ngMousedown
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * The ngMousedown directive allows you to specify custom behavior on mousedown event.
+ *
+ * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon
+ * mousedown. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ Increment (on mouse down)
+
+ count: {{count}}
*/
- var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
- return {
- restrict: 'E',
- require: '?ngModel',
- link: function(scope, element, attr, ctrl) {
- if (ctrl) {
- (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
- $browser);
- }
- }
- };
- }];
- var VALID_CLASS = 'ng-valid',
- INVALID_CLASS = 'ng-invalid',
- PRISTINE_CLASS = 'ng-pristine',
- DIRTY_CLASS = 'ng-dirty';
/**
- * @ngdoc type
- * @name ngModel.NgModelController
- *
- * @property {string} $viewValue Actual string value in the view.
- * @property {*} $modelValue The value in the model, that the control is bound to.
- * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever
- the control reads value from the DOM. Each function is called, in turn, passing the value
- through to the next. The last return value is used to populate the model.
- Used to sanitize / convert the value as well as validation. For validation,
- the parsers should update the validity state using
- {@link ngModel.NgModelController#$setValidity $setValidity()},
- and return `undefined` for invalid values.
-
+ * @ngdoc directive
+ * @name ngMouseup
+ * @restrict A
+ * @element ANY
+ * @priority 0
*
- * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
- the model value changes. Each function is called, in turn, passing the value through to the
- next. Used to format / convert values for display in the control and validation.
- * ```js
- * function formatter(value) {
- * if (value) {
- * return value.toUpperCase();
- * }
- * }
- * ngModel.$formatters.push(formatter);
- * ```
- *
- * @property {Array.} $viewChangeListeners Array of functions to execute whenever the
- * view value has changed. It is called with no arguments, and its return value is ignored.
- * This can be used in place of additional $watches against the model value.
+ * @description
+ * Specify custom behavior on mouseup event.
*
- * @property {Object} $error An object hash with all errors as keys.
+ * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon
+ * mouseup. ({@link guide/expression#-event- Event object is available as `$event`})
*
- * @property {boolean} $pristine True if user has not interacted with the control yet.
- * @property {boolean} $dirty True if user has already interacted with the control.
- * @property {boolean} $valid True if there is no error.
- * @property {boolean} $invalid True if at least one error on the control.
+ * @example
+
+
+
+ Increment (on mouse up)
+
+ count: {{count}}
+
+
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngMouseover
+ * @restrict A
+ * @element ANY
+ * @priority 0
*
* @description
+ * Specify custom behavior on mouseover event.
*
- * `NgModelController` provides API for the `ng-model` directive. The controller contains
- * services for data-binding, validation, CSS updates, and value formatting and parsing. It
- * purposefully does not contain any logic which deals with DOM rendering or listening to
- * DOM events. Such DOM related logic should be provided by other directives which make use of
- * `NgModelController` for data-binding.
- *
- * ## Custom Control Example
- * This example shows how to use `NgModelController` with a custom control to achieve
- * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
- * collaborate together to achieve the desired result.
- *
- * Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element
- * contents be edited in place by the user. This will not work on older browsers.
+ * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon
+ * mouseover. ({@link guide/expression#-event- Event object is available as `$event`})
*
- *
-
- [contenteditable] {
- border: 1px solid black;
- background-color: white;
- min-height: 20px;
- }
-
- .ng-invalid {
- border: 1px solid red;
- }
-
+ * @example
+
+
+
+ Increment (when mouse is over)
+
+ count: {{count}}
-
- angular.module('customControl', []).
- directive('contenteditable', function() {
- return {
- restrict: 'A', // only activate on element attribute
- require: '?ngModel', // get a hold of NgModelController
- link: function(scope, element, attrs, ngModel) {
- if(!ngModel) return; // do nothing if no ng-model
-
- // Specify how UI should be updated
- ngModel.$render = function() {
- element.html(ngModel.$viewValue || '');
- };
+
+ */
- // Listen for change events to enable binding
- element.on('blur keyup change', function() {
- scope.$apply(read);
- });
- read(); // initialize
- // Write data to the model
- function read() {
- var html = element.html();
- // When we clear the content editable the browser leaves a behind
- // If strip-br attribute is provided then we strip this out
- if( attrs.stripBr && html == ' ' ) {
- html = '';
- }
- ngModel.$setViewValue(html);
- }
- }
- };
- });
-
+ /**
+ * @ngdoc directive
+ * @name ngMouseenter
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on mouseenter event.
+ *
+ * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon
+ * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
-
- Change me!
- Required!
-
-
-
+
+ Increment (when mouse enters)
+
+ count: {{count}}
-
- it('should data-bind and become invalid', function() {
- if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
- // SafariDriver can't handle contenteditable
- // and Firefox driver can't clear contenteditables very well
- return;
- }
- var contentEditable = element(by.css('[contenteditable]'));
- var content = 'Change me!';
+
+ */
- expect(contentEditable.getText()).toEqual(content);
- contentEditable.clear();
- contentEditable.sendKeys(protractor.Key.BACK_SPACE);
- expect(contentEditable.getText()).toEqual('');
- expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
- });
-
- *
+ /**
+ * @ngdoc directive
+ * @name ngMouseleave
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on mouseleave event.
*
+ * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon
+ * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`})
*
+ * @example
+
+
+
+ Increment (when mouse leaves)
+
+ count: {{count}}
+
+
*/
- var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate',
- function($scope, $exceptionHandler, $attr, $element, $parse, $animate) {
- this.$viewValue = Number.NaN;
- this.$modelValue = Number.NaN;
- this.$parsers = [];
- this.$formatters = [];
- this.$viewChangeListeners = [];
- this.$pristine = true;
- this.$dirty = false;
- this.$valid = true;
- this.$invalid = false;
- this.$name = $attr.name;
-
- var ngModelGet = $parse($attr.ngModel),
- ngModelSet = ngModelGet.assign;
-
- if (!ngModelSet) {
- throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
- $attr.ngModel, startingTag($element));
- }
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$render
- *
- * @description
- * Called when the view needs to be updated. It is expected that the user of the ng-model
- * directive will implement this method.
- */
- this.$render = noop;
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$isEmpty
- *
- * @description
- * This is called when we need to determine if the value of the input is empty.
- *
- * For instance, the required directive does this to work out if the input has data or not.
- * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
- *
- * You can override this for input directives whose concept of being empty is different to the
- * default. The `checkboxInputType` directive does this because in its case a value of `false`
- * implies empty.
- *
- * @param {*} value Reference to check.
- * @returns {boolean} True if `value` is empty.
- */
- this.$isEmpty = function(value) {
- return isUndefined(value) || value === '' || value === null || value !== value;
- };
-
- var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
- invalidCount = 0, // used to easily determine if we are valid
- $error = this.$error = {}; // keep invalid keys here
-
-
- // Setup initial state of the control
- $element.addClass(PRISTINE_CLASS);
- toggleValidCss(true);
-
- // convenience method for easy toggling of classes
- function toggleValidCss(isValid, validationErrorKey) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- $animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
- $animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
- }
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setValidity
- *
- * @description
- * Change the validity state, and notifies the form when the control changes validity. (i.e. it
- * does not notify form if given validator is already marked as invalid).
- *
- * This method should be called by validators - i.e. the parser or formatter functions.
- *
- * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
- * to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
- */
- this.$setValidity = function(validationErrorKey, isValid) {
- // Purposeful use of ! here to cast isValid to boolean in case it is undefined
- // jshint -W018
- if ($error[validationErrorKey] === !isValid) return;
- // jshint +W018
-
- if (isValid) {
- if ($error[validationErrorKey]) invalidCount--;
- if (!invalidCount) {
- toggleValidCss(true);
- this.$valid = true;
- this.$invalid = false;
- }
- } else {
- toggleValidCss(false);
- this.$invalid = true;
- this.$valid = false;
- invalidCount++;
- }
-
- $error[validationErrorKey] = !isValid;
- toggleValidCss(isValid, validationErrorKey);
-
- parentForm.$setValidity(validationErrorKey, isValid, this);
- };
-
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setPristine
- *
- * @description
- * Sets the control to its pristine state.
- *
- * This method can be called to remove the 'ng-dirty' class and set the control to its pristine
- * state (ng-pristine class).
- */
- this.$setPristine = function () {
- this.$dirty = false;
- this.$pristine = true;
- $animate.removeClass($element, DIRTY_CLASS);
- $animate.addClass($element, PRISTINE_CLASS);
- };
- /**
- * @ngdoc method
- * @name ngModel.NgModelController#$setViewValue
- *
- * @description
- * Update the view value.
- *
- * This method should be called when the view value changes, typically from within a DOM event handler.
- * For example {@link ng.directive:input input} and
- * {@link ng.directive:select select} directives call it.
- *
- * It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
- * which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
- * `$modelValue` and the **expression** specified in the `ng-model` attribute.
- *
- * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
- *
- * Note that calling this function does not trigger a `$digest`.
- *
- * @param {string} value Value from the view.
- */
- this.$setViewValue = function(value) {
- this.$viewValue = value;
-
- // change to dirty
- if (this.$pristine) {
- this.$dirty = true;
- this.$pristine = false;
- $animate.removeClass($element, PRISTINE_CLASS);
- $animate.addClass($element, DIRTY_CLASS);
- parentForm.$setDirty();
- }
- forEach(this.$parsers, function(fn) {
- value = fn(value);
- });
-
- if (this.$modelValue !== value) {
- this.$modelValue = value;
- ngModelSet($scope, value);
- forEach(this.$viewChangeListeners, function(listener) {
- try {
- listener();
- } catch(e) {
- $exceptionHandler(e);
- }
- });
- }
- };
+ /**
+ * @ngdoc directive
+ * @name ngMousemove
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on mousemove event.
+ *
+ * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon
+ * mousemove. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ Increment (when mouse moves)
+
+ count: {{count}}
+
+
+ */
- // model -> value
- var ctrl = this;
- $scope.$watch(function ngModelWatch() {
- var value = ngModelGet($scope);
+ /**
+ * @ngdoc directive
+ * @name ngKeydown
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on keydown event.
+ *
+ * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon
+ * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
+ *
+ * @example
+
+
+
+ key down count: {{count}}
+
+
+ */
- // if scope model value and ngModel value are out of sync
- if (ctrl.$modelValue !== value) {
- var formatters = ctrl.$formatters,
- idx = formatters.length;
+ /**
+ * @ngdoc directive
+ * @name ngKeyup
+ * @restrict A
+ * @element ANY
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on keyup event.
+ *
+ * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon
+ * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
+ *
+ * @example
+
+
+ Typing in the input box below updates the key count
+ key up count: {{count}}
- ctrl.$modelValue = value;
- while(idx--) {
- value = formatters[idx](value);
- }
+ Typing in the input box below updates the keycode
+
+ event keyCode: {{ event.keyCode }}
+ event altKey: {{ event.altKey }}
+
+
+ */
- if (ctrl.$viewValue !== value) {
- ctrl.$viewValue = value;
- ctrl.$render();
- }
- }
- return value;
- });
- }];
+ /**
+ * @ngdoc directive
+ * @name ngKeypress
+ * @restrict A
+ * @element ANY
+ *
+ * @description
+ * Specify custom behavior on keypress event.
+ *
+ * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon
+ * keypress. ({@link guide/expression#-event- Event object is available as `$event`}
+ * and can be interrogated for keyCode, altKey, etc.)
+ *
+ * @example
+
+
+
+ key press count: {{count}}
+
+
+ */
/**
* @ngdoc directive
- * @name ngModel
- *
- * @element input
+ * @name ngSubmit
+ * @restrict A
+ * @element form
+ * @priority 0
*
* @description
- * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
- * property on the scope using {@link ngModel.NgModelController NgModelController},
- * which is created and exposed by this directive.
+ * Enables binding AngularJS expressions to onsubmit events.
*
- * `ngModel` is responsible for:
+ * Additionally it prevents the default action (which for form means sending the request to the
+ * server and reloading the current page), but only if the form does not contain `action`,
+ * `data-action`, or `x-action` attributes.
*
- * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
- * require.
- * - Providing validation behavior (i.e. required, number, email, url).
- * - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors).
- * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations.
- * - Registering the control with its parent {@link ng.directive:form form}.
+ *
+ * **Warning:** Be careful not to cause "double-submission" by using both the `ngClick` and
+ * `ngSubmit` handlers together. See the
+ * {@link form#submitting-a-form-and-preventing-the-default-action `form` directive documentation}
+ * for a detailed discussion of when `ngSubmit` may be triggered.
+ *
*
- * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
- * current scope. If the property doesn't already exist on this scope, it will be created
- * implicitly and added to the scope.
+ * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
+ * ({@link guide/expression#-event- Event object is available as `$event`})
*
- * For best practices on using `ngModel`, see:
+ * @example
+
+
+
+
+ Enter text and hit enter:
+
+
+ list={{list}}
+
+
+
+ it('should check ng-submit', function() {
+ expect(element(by.binding('list')).getText()).toBe('list=[]');
+ element(by.css('#submit')).click();
+ expect(element(by.binding('list')).getText()).toContain('hello');
+ expect(element(by.model('text')).getAttribute('value')).toBe('');
+ });
+ it('should ignore empty strings', function() {
+ expect(element(by.binding('list')).getText()).toBe('list=[]');
+ element(by.css('#submit')).click();
+ element(by.css('#submit')).click();
+ expect(element(by.binding('list')).getText()).toContain('hello');
+ });
+
+
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngFocus
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
*
- * - [https://github.com/angular/angular.js/wiki/Understanding-Scopes]
+ * @description
+ * Specify custom behavior on focus event.
*
- * For basic examples, how to use `ngModel`, see:
+ * Note: As the `focus` event is executed synchronously when calling `input.focus()`
+ * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
+ * during an `$apply` to ensure a consistent state.
*
- * - {@link ng.directive:input input}
- * - {@link input[text] text}
- * - {@link input[checkbox] checkbox}
- * - {@link input[radio] radio}
- * - {@link input[number] number}
- * - {@link input[email] email}
- * - {@link input[url] url}
- * - {@link ng.directive:select select}
- * - {@link ng.directive:textarea textarea}
+ * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
+ * focus. ({@link guide/expression#-event- Event object is available as `$event`})
*
- * # CSS classes
- * The following CSS classes are added and removed on the associated input/select/textarea element
- * depending on the validity of the model.
+ * @example
+ * See {@link ng.directive:ngClick ngClick}
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngBlur
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
*
- * - `ng-valid` is set if the model is valid.
- * - `ng-invalid` is set if the model is invalid.
- * - `ng-pristine` is set if the model is pristine.
- * - `ng-dirty` is set if the model is dirty.
+ * @description
+ * Specify custom behavior on blur event.
*
- * Keep in mind that ngAnimate can detect each of these classes when added and removed.
+ * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
+ * an element has lost focus.
*
- * ## Animation Hooks
+ * Note: As the `blur` event is executed synchronously also during DOM manipulations
+ * (e.g. removing a focussed input),
+ * AngularJS executes the expression using `scope.$evalAsync` if the event is fired
+ * during an `$apply` to ensure a consistent state.
*
- * 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`,
- * `.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.
+ * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
+ * blur. ({@link guide/expression#-event- Event object is available as `$event`})
*
- * The following example shows a simple way to utilize CSS transitions to style an input element
- * that has been rendered as invalid after it has been validated:
+ * @example
+ * See {@link ng.directive:ngClick ngClick}
+ */
+
+ /**
+ * @ngdoc directive
+ * @name ngCopy
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
*
- *
- * //be sure to include ngAnimate as a module to hook into more
- * //advanced animations
- * .my-input {
- * transition:0.5s linear all;
- * background: white;
- * }
- * .my-input.ng-invalid {
- * background: red;
- * color:white;
- * }
- *
+ * @description
+ * Specify custom behavior on copy event.
+ *
+ * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon
+ * copy. ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
- *
+
-
-
- Update input to see transitions when valid/invalid.
- Integer is a valid value.
-
-
-
+
+ copied: {{copied}}
- *
+
*/
- var ngModelDirective = function() {
- return {
- require: ['ngModel', '^?form'],
- controller: NgModelController,
- link: function(scope, element, attr, ctrls) {
- // notify others, especially parent forms
-
- var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || nullFormCtrl;
- formCtrl.$addControl(modelCtrl);
-
- scope.$on('$destroy', function() {
- formCtrl.$removeControl(modelCtrl);
- });
- }
- };
- };
+ /**
+ * @ngdoc directive
+ * @name ngCut
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on cut event.
+ *
+ * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon
+ * cut. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ cut: {{cut}}
+
+
+ */
+ /**
+ * @ngdoc directive
+ * @name ngPaste
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on paste event.
+ *
+ * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon
+ * paste. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ pasted: {{paste}}
+
+
+ */
/**
* @ngdoc directive
- * @name ngChange
+ * @name ngIf
+ * @restrict A
+ * @multiElement
*
* @description
- * Evaluate the given expression when the user changes the input.
- * The expression is evaluated immediately, unlike the JavaScript onchange event
- * which only triggers at the end of a change (usually, when the user leaves the
- * form element or presses the return key).
- * The expression is not evaluated when the value change is coming from the model.
+ * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
+ * {expression}. If the expression assigned to `ngIf` evaluates to a false
+ * value then the element is removed from the DOM, otherwise a clone of the
+ * element is reinserted into the DOM.
+ *
+ * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the
+ * element in the DOM rather than changing its visibility via the `display` css property. A common
+ * case when this difference is significant is when using css selectors that rely on an element's
+ * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes.
*
- * Note, this directive requires `ngModel` to be present.
+ * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope
+ * is created when the element is restored. The scope created within `ngIf` inherits from
+ * its parent scope using
+ * [prototypal inheritance](https://github.com/angular/angular.js/wiki/Understanding-Scopes#javascript-prototypal-inheritance).
+ * An important implication of this is if `ngModel` is used within `ngIf` to bind to
+ * a javascript primitive defined in the parent scope. In this case any modifications made to the
+ * variable within the child scope will override (hide) the value in the parent scope.
*
- * @element input
- * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
- * in input value.
+ * Also, `ngIf` recreates elements using their compiled state. An example of this behavior
+ * is if an element's class attribute is directly modified after it's compiled, using something like
+ * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
+ * the added class will be lost because the original compiled state is used to regenerate the element.
*
- * @example
- *
- *
- *
- *
- *
- *
- * Confirmed
- * debug = {{confirmed}}
- * counter = {{counter}}
- *
- *
- *
- * var counter = element(by.binding('counter'));
- * var debug = element(by.binding('confirmed'));
+ * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
+ * and `leave` effects.
*
- * it('should evaluate the expression if changing from view', function() {
- * expect(counter.getText()).toContain('0');
- *
- * element(by.id('ng-change-example1')).click();
- *
- * expect(counter.getText()).toContain('1');
- * expect(debug.getText()).toContain('true');
- * });
+ * @animations
+ * | 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 |
*
- * it('should not evaluate the expression if changing from model', function() {
- * element(by.id('ng-change-example2')).click();
+ * @element ANY
+ * @scope
+ * @priority 600
+ * @param {expression} ngIf If the {@link guide/expression expression} is falsy then
+ * the element is removed from the DOM tree. If it is truthy a copy of the compiled
+ * element is added to the DOM tree.
+ *
+ * @example
+
+
+ Click me:
+ Show when checked:
+
+ This is removed when the checkbox is unchecked.
+
+
+
+ .animate-if {
+ background:white;
+ border:1px solid black;
+ padding:10px;
+ }
- * expect(counter.getText()).toContain('0');
- * expect(debug.getText()).toContain('true');
- * });
- *
- *
- */
- var ngChangeDirective = valueFn({
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- ctrl.$viewChangeListeners.push(function() {
- scope.$eval(attr.ngChange);
- });
- }
- });
+ .animate-if.ng-enter, .animate-if.ng-leave {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ }
+ .animate-if.ng-enter,
+ .animate-if.ng-leave.ng-leave-active {
+ opacity:0;
+ }
- var requiredDirective = function() {
+ .animate-if.ng-leave,
+ .animate-if.ng-enter.ng-enter-active {
+ opacity:1;
+ }
+
+
+ */
+ var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
- require: '?ngModel',
- link: function(scope, elm, attr, ctrl) {
- if (!ctrl) return;
- attr.required = true; // force truthy in case we are on non input element
+ multiElement: true,
+ transclude: 'element',
+ priority: 600,
+ terminal: true,
+ restrict: 'A',
+ $$tlb: true,
+ link: function($scope, $element, $attr, ctrl, $transclude) {
+ var block, childScope, previousElements;
+ $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
- var validator = function(value) {
- if (attr.required && ctrl.$isEmpty(value)) {
- ctrl.$setValidity('required', false);
- return;
+ if (value) {
+ if (!childScope) {
+ $transclude(function(clone, newScope) {
+ childScope = newScope;
+ 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.
+ block = {
+ clone: clone
+ };
+ $animate.enter(clone, $element.parent(), $element);
+ });
+ }
} else {
- ctrl.$setValidity('required', true);
- return value;
+ if (previousElements) {
+ previousElements.remove();
+ previousElements = null;
+ }
+ if (childScope) {
+ childScope.$destroy();
+ childScope = null;
+ }
+ if (block) {
+ previousElements = getBlockNodes(block.clone);
+ $animate.leave(previousElements).done(function(response) {
+ if (response !== false) previousElements = null;
+ });
+ block = null;
+ }
}
- };
-
- ctrl.$formatters.push(validator);
- ctrl.$parsers.unshift(validator);
-
- attr.$observe('required', function() {
- validator(ctrl.$viewValue);
});
}
};
- };
-
+ }];
/**
* @ngdoc directive
- * @name ngList
+ * @name ngInclude
+ * @restrict ECA
+ * @scope
+ * @priority -400
*
* @description
- * Text input that converts between a delimited string and an array of strings. The delimiter
- * can be a fixed string (by default a comma) or a regular expression.
+ * Fetches, compiles and includes an external HTML fragment.
+ *
+ * By default, the template URL is restricted to the same domain and protocol as the
+ * application document. This is done by calling {@link $sce#getTrustedResourceUrl
+ * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
+ * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
+ * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to AngularJS's {@link
+ * ng.$sce Strict Contextual Escaping}.
+ *
+ * In addition, the browser's
+ * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
+ * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
+ * policy may further restrict whether the template is successfully loaded.
+ * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://`
+ * access on some browsers.
+ *
+ * @animations
+ * | 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.
+ *
+ * @param {string} ngInclude|src AngularJS expression evaluating to URL. If the source is a string constant,
+ * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
+ * @param {string=} onload Expression to evaluate when a new partial is loaded.
+ *
+ * **Note:** When using onload on SVG elements in IE11, the browser will try to call
+ * a function with the name on the window element, which will usually throw a
+ * "function is undefined" error. To fix this, you can instead use `data-onload` or a
+ * different form that {@link guide/directive#normalization matches} `onload`.
+ *
*
- * @element input
- * @param {string=} ngList optional delimiter that should be used to split the value. If
- * specified in form `/something/` then the value will be converted into a regular expression.
+ * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
+ * $anchorScroll} to scroll the viewport after the content is loaded.
+ *
+ * - If the attribute is not set, disable scrolling.
+ * - If the attribute is set without value, enable scrolling.
+ * - Otherwise enable scrolling only if the expression evaluates to truthy value.
*
* @example
-
+
-
-
- List:
-
- Required!
-
- names = {{names}}
- myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
- myForm.namesInput.$error = {{myForm.namesInput.$error}}
- myForm.$valid = {{myForm.$valid}}
- myForm.$error.required = {{!!myForm.$error.required}}
-
+
+
+ (blank)
+
+ url of the template:
{{template.url}}
+
+
+
+
+
+ angular.module('includeExample', ['ngAnimate'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.templates =
+ [{ name: 'template1.html', url: 'template1.html'},
+ { name: 'template2.html', url: 'template2.html'}];
+ $scope.template = $scope.templates[0];
+ }]);
+
+
+ Content of template1.html
+
+
+ Content of template2.html
+
+
+ .slide-animate-container {
+ position:relative;
+ background:white;
+ border:1px solid black;
+ height:40px;
+ overflow:hidden;
+ }
+
+ .slide-animate {
+ padding:10px;
+ }
+
+ .slide-animate.ng-enter, .slide-animate.ng-leave {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+
+ position:absolute;
+ top:0;
+ left:0;
+ right:0;
+ bottom:0;
+ display:block;
+ padding:10px;
+ }
+
+ .slide-animate.ng-enter {
+ top:-50px;
+ }
+ .slide-animate.ng-enter.ng-enter-active {
+ top:0;
+ }
+
+ .slide-animate.ng-leave {
+ top:0;
+ }
+ .slide-animate.ng-leave.ng-leave-active {
+ top:50px;
+ }
- var listInput = element(by.model('names'));
- var names = element(by.binding('{{names}}'));
- var valid = element(by.binding('myForm.namesInput.$valid'));
- var error = element(by.css('span.error'));
+ var templateSelect = element(by.model('template'));
+ var includeElem = element(by.css('[ng-include]'));
- it('should initialize to model', function() {
- expect(names.getText()).toContain('["igor","misko","vojta"]');
- expect(valid.getText()).toContain('true');
- expect(error.getCssValue('display')).toBe('none');
- });
+ it('should load template1.html', function() {
+ expect(includeElem.getText()).toMatch(/Content of template1.html/);
+ });
- it('should be invalid if empty', function() {
- listInput.clear();
- listInput.sendKeys('');
+ it('should load template2.html', function() {
+ if (browser.params.browser === 'firefox') {
+ // Firefox can't handle using selects
+ // See https://github.com/angular/protractor/issues/480
+ return;
+ }
+ templateSelect.click();
+ templateSelect.all(by.css('option')).get(2).click();
+ expect(includeElem.getText()).toMatch(/Content of template2.html/);
+ });
- expect(names.getText()).toContain('');
- expect(valid.getText()).toContain('false');
- expect(error.getCssValue('display')).not.toBe('none'); });
+ it('should change to blank', function() {
+ if (browser.params.browser === 'firefox') {
+ // Firefox can't handle using selects
+ return;
+ }
+ templateSelect.click();
+ templateSelect.all(by.css('option')).get(0).click();
+ expect(includeElem.isPresent()).toBe(false);
+ });
*/
- var ngListDirective = function() {
- return {
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- var match = /\/(.*)\//.exec(attr.ngList),
- separator = match && new RegExp(match[1]) || attr.ngList || ',';
- var parse = function(viewValue) {
- // If the viewValue is invalid (say required but empty) it will be `undefined`
- if (isUndefined(viewValue)) return;
- var list = [];
+ /**
+ * @ngdoc event
+ * @name ngInclude#$includeContentRequested
+ * @eventType emit on the scope ngInclude was declared in
+ * @description
+ * Emitted every time the ngInclude content is requested.
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
+ */
+
+
+ /**
+ * @ngdoc event
+ * @name ngInclude#$includeContentLoaded
+ * @eventType emit on the current ngInclude scope
+ * @description
+ * Emitted every time the ngInclude content is reloaded.
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
+ */
+
+
+ /**
+ * @ngdoc event
+ * @name ngInclude#$includeContentError
+ * @eventType emit on the scope ngInclude was declared in
+ * @description
+ * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
+ */
+ var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
+ function($templateRequest, $anchorScroll, $animate) {
+ return {
+ restrict: 'ECA',
+ priority: 400,
+ terminal: true,
+ transclude: 'element',
+ controller: angular.noop,
+ compile: function(element, attr) {
+ var srcExp = attr.ngInclude || attr.src,
+ onloadExp = attr.onload || '',
+ autoScrollExp = attr.autoscroll;
- if (viewValue) {
- forEach(viewValue.split(separator), function(value) {
- if (value) list.push(trim(value));
- });
- }
+ return function(scope, $element, $attr, ctrl, $transclude) {
+ var changeCounter = 0,
+ currentScope,
+ previousElement,
+ currentElement;
- return list;
- };
+ var cleanupLastIncludeContent = function() {
+ if (previousElement) {
+ previousElement.remove();
+ previousElement = null;
+ }
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+ if (currentElement) {
+ $animate.leave(currentElement).done(function(response) {
+ if (response !== false) previousElement = null;
+ });
+ previousElement = currentElement;
+ currentElement = null;
+ }
+ };
- ctrl.$parsers.push(parse);
- ctrl.$formatters.push(function(value) {
- if (isArray(value)) {
- return value.join(', ');
- }
+ scope.$watch(srcExp, function ngIncludeWatchAction(src) {
+ var afterAnimation = function(response) {
+ if (response !== false && isDefined(autoScrollExp) &&
+ (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
+ }
+ };
+ var thisChangeId = ++changeCounter;
- return undefined;
- });
+ if (src) {
+ //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;
- // Override the standard $isEmpty because an empty array means the input is empty.
- ctrl.$isEmpty = function(value) {
- return !value || !value.length;
- };
- }
- };
- };
+ if (thisChangeId !== changeCounter) return;
+ var newScope = scope.$new();
+ ctrl.template = response;
+ // Note: This will also link all children of ng-include that were contained in the original
+ // html. If that content contains controllers, ... they could pollute/change the scope.
+ // However, using ng-include on an element with additional content does not make sense...
+ // Note: We can't remove them in the cloneAttchFn of $transclude as that
+ // function is called before linking the content, which would apply child
+ // directives to non existing elements.
+ var clone = $transclude(newScope, function(clone) {
+ cleanupLastIncludeContent();
+ $animate.enter(clone, null, $element).done(afterAnimation);
+ });
- var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
- /**
- * @ngdoc directive
- * @name ngValue
- *
- * @description
- * Binds the given expression to the value of `input[select]` or `input[radio]`, so
- * that when the element is selected, the `ngModel` of that element is set to the
- * bound value.
- *
- * `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as
- * shown below.
- *
- * @element input
- * @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
- * of the `input` element
- *
- * @example
-
-
-
-
- Which is your favorite?
-
- {{name}}
-
-
- You chose {{my.favorite}}
-
-
-
- var favorite = element(by.binding('my.favorite'));
+ currentScope = newScope;
+ currentElement = clone;
- it('should initialize to model', function() {
- expect(favorite.getText()).toContain('unicorns');
- });
- it('should bind the values to the inputs', function() {
- element.all(by.model('my.favorite')).get(0).click();
- expect(favorite.getText()).toContain('pizza');
- });
-
-
- */
- var ngValueDirective = function() {
- return {
- priority: 100,
- compile: function(tpl, tplAttr) {
- if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
- return function ngValueConstantLink(scope, elm, attr) {
- attr.$set('value', scope.$eval(attr.ngValue));
- };
- } else {
- return function ngValueLink(scope, elm, attr) {
- scope.$watch(attr.ngValue, function valueWatchAction(value) {
- attr.$set('value', value);
+ currentScope.$emit('$includeContentLoaded', src);
+ scope.$eval(onloadExp);
+ }, function() {
+ if (scope.$$destroyed) return;
+
+ if (thisChangeId === changeCounter) {
+ cleanupLastIncludeContent();
+ scope.$emit('$includeContentError', src);
+ }
+ });
+ scope.$emit('$includeContentRequested', src);
+ } else {
+ cleanupLastIncludeContent();
+ ctrl.template = null;
+ }
});
};
}
- }
- };
- };
+ };
+ }];
+
+// This directive is called during the $transclude call of the first `ngInclude` directive.
+// It will replace and compile the content of the element with the loaded template.
+// We need this directive so that the element content is already filled when
+// the link function of another directive on the same element as ngInclude
+// is called.
+ var ngIncludeFillContentDirective = ['$compile',
+ function($compile) {
+ return {
+ restrict: 'ECA',
+ priority: -400,
+ require: 'ngInclude',
+ link: function(scope, $element, $attr, ctrl) {
+ 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, window.document).childNodes)(scope,
+ function namespaceAdaptedClone(clone) {
+ $element.append(clone);
+ }, {futureParentElement: $element});
+ return;
+ }
+
+ $element.html(ctrl.template);
+ $compile($element.contents())(scope);
+ }
+ };
+ }];
/**
* @ngdoc directive
- * @name ngBind
+ * @name ngInit
* @restrict AC
- *
- * @description
- * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element
- * with the value of a given expression, and to update the text content when the value of that
- * expression changes.
- *
- * Typically, you don't use `ngBind` directly, but instead you use the double curly markup like
- * `{{ expression }}` which is similar but less verbose.
- *
- * It is preferable to use `ngBind` instead of `{{ expression }}` when a template is momentarily
- * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an
- * element attribute, it makes the bindings invisible to the user while the page is loading.
- *
- * An alternative solution to this problem would be using the
- * {@link ng.directive:ngCloak ngCloak} directive.
- *
- *
+ * @priority 450
* @element ANY
- * @param {expression} ngBind {@link guide/expression Expression} to evaluate.
*
- * @example
- * Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
-
-
-
-
- Enter name:
- Hello !
-
-
-
- it('should check ng-bind', function() {
- var nameInput = element(by.model('name'));
-
- expect(element(by.binding('name')).getText()).toBe('Whirled');
- nameInput.clear();
- nameInput.sendKeys('world');
- expect(element(by.binding('name')).getText()).toBe('world');
- });
-
-
- */
- var ngBindDirective = ngDirective(function(scope, element, attr) {
- element.addClass('ng-binding').data('$binding', attr.ngBind);
- scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
- // We are purposefully using == here rather than === because we want to
- // catch when value is "null or undefined"
- // jshint -W041
- element.text(value == undefined ? '' : value);
- });
- });
-
-
- /**
- * @ngdoc directive
- * @name ngBindTemplate
+ * @param {expression} ngInit {@link guide/expression Expression} to eval.
*
* @description
- * The `ngBindTemplate` directive specifies that the element
- * text content should be replaced with the interpolation of the template
- * in the `ngBindTemplate` attribute.
- * Unlike `ngBind`, the `ngBindTemplate` can contain multiple `{{` `}}`
- * expressions. This directive is needed since some HTML elements
- * (such as TITLE and OPTION) cannot contain SPAN elements.
+ * The `ngInit` directive allows you to evaluate an expression in the
+ * current scope.
*
- * @element ANY
- * @param {string} ngBindTemplate template of form
- * {{ expression }} to eval.
+ *
+ * This directive can be abused to add unnecessary amounts of logic into your templates.
+ * There are only a few appropriate uses of `ngInit`:
+ *
+ * aliasing special properties of {@link ng.directive:ngRepeat `ngRepeat`},
+ * as seen in the demo below.
+ * initializing data during development, or for examples, as seen throughout these docs.
+ * injecting data via server side scripting.
+ *
+ *
+ * Besides these few cases, you should use {@link guide/component Components} or
+ * {@link guide/controller Controllers} rather than `ngInit` to initialize values on a scope.
+ *
+ *
+ *
+ * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make
+ * sure you have parentheses to ensure correct operator precedence:
+ *
+ * `
`
+ *
+ *
*
* @example
- * Try it here: enter text in text box and watch the greeting change.
-
+
-
- Salutation:
- Name:
-
+
+
+
+ list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};
+
+
- it('should check ng-bind', function() {
- var salutationElem = element(by.binding('salutation'));
- var salutationInput = element(by.model('salutation'));
- var nameInput = element(by.model('name'));
-
- expect(salutationElem.getText()).toBe('Hello World!');
-
- salutationInput.clear();
- salutationInput.sendKeys('Greetings');
- nameInput.clear();
- nameInput.sendKeys('user');
-
- expect(salutationElem.getText()).toBe('Greetings user!');
+ it('should alias index positions', function() {
+ var elements = element.all(by.css('.example-init'));
+ expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
+ expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
+ expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
+ expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
});
*/
- var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
- return function(scope, element, attr) {
- // TODO: move this to scenario runner
- var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
- element.addClass('ng-binding').data('$binding', interpolateFn);
- attr.$observe('ngBindTemplate', function(value) {
- element.text(value);
- });
- };
- }];
-
+ var ngInitDirective = ngDirective({
+ priority: 450,
+ compile: function() {
+ return {
+ pre: function(scope, element, attrs) {
+ scope.$eval(attrs.ngInit);
+ }
+ };
+ }
+ });
/**
* @ngdoc directive
- * @name ngBindHtml
+ * @name ngList
+ * @restrict A
+ * @priority 100
+ *
+ * @param {string=} ngList optional delimiter that should be used to split the value.
*
* @description
- * Creates a binding that will innerHTML the result of evaluating the `expression` into the current
- * element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link
- * ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize`
- * is available, for example, by including {@link ngSanitize} in your module's dependencies (not in
- * core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to
- * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example
- * under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}.
+ * Text input that converts between a delimited string and an array of strings. The default
+ * delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
+ * delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
+ *
+ * The behaviour of the directive is affected by the use of the `ngTrim` attribute.
+ * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
+ * list item is respected. This implies that the user of the directive is responsible for
+ * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
+ * tab or newline character.
+ * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
+ * when joining the list items back together) and whitespace around each list item is stripped
+ * before it is added to the model.
*
- * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you
- * will have an exception (instead of an exploit.)
+ * @example
+ * ### Validation
+ *
+ *
+ *
+ * angular.module('listExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.names = ['morpheus', 'neo', 'trinity'];
+ * }]);
+ *
+ *
+ *
+ * List:
+ *
+ *
+ * Required!
+ *
+ *
+ * names = {{names}}
+ * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
+ * myForm.namesInput.$error = {{myForm.namesInput.$error}}
+ * myForm.$valid = {{myForm.$valid}}
+ * myForm.$error.required = {{!!myForm.$error.required}}
+ *
+ *
+ *
+ * var listInput = element(by.model('names'));
+ * var names = element(by.exactBinding('names'));
+ * var valid = element(by.binding('myForm.namesInput.$valid'));
+ * var error = element(by.css('span.error'));
+ *
+ * it('should initialize to model', function() {
+ * expect(names.getText()).toContain('["morpheus","neo","trinity"]');
+ * expect(valid.getText()).toContain('true');
+ * expect(error.getCssValue('display')).toBe('none');
+ * });
*
- * @element ANY
- * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
+ * it('should be invalid if empty', function() {
+ * listInput.clear();
+ * listInput.sendKeys('');
+ *
+ * expect(names.getText()).toContain('');
+ * expect(valid.getText()).toContain('false');
+ * expect(error.getCssValue('display')).not.toBe('none');
+ * });
+ *
+ *
*
* @example
- Try it here: enter text in text box and watch the greeting change.
+ * ### Splitting on newline
+ *
+ *
+ *
+ *
+ * {{ list | json }}
+ *
+ *
+ * it("should split the text by newlines", function() {
+ * var listInput = element(by.model('list'));
+ * var output = element(by.binding('list | json'));
+ * listInput.sendKeys('abc\ndef\nghi');
+ * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
+ * });
+ *
+ *
+ *
+ */
+ var ngListDirective = function() {
+ return {
+ restrict: 'A',
+ priority: 100,
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ var ngList = attr.ngList || ', ';
+ var trimValues = attr.ngTrim !== 'false';
+ var separator = trimValues ? trim(ngList) : ngList;
-
-
-
-
+ var parse = function(viewValue) {
+ // If the viewValue is invalid (say required but empty) it will be `undefined`
+ if (isUndefined(viewValue)) return;
-
- angular.module('ngBindHtmlExample', ['ngSanitize'])
+ var list = [];
- .controller('ngBindHtmlCtrl', ['$scope', function ngBindHtmlCtrl($scope) {
- $scope.myHTML =
- 'I am an HTML
string with links! and other stuff ';
- }]);
-
+ if (viewValue) {
+ forEach(viewValue.split(separator), function(value) {
+ if (value) list.push(trimValues ? trim(value) : value);
+ });
+ }
-
- it('should check ng-bind-html', function() {
- expect(element(by.binding('myHTML')).getText()).toBe(
- 'I am an HTMLstring with links! and other stuff');
- });
-
-
- */
- var ngBindHtmlDirective = ['$sce', '$parse', function($sce, $parse) {
- return function(scope, element, attr) {
- element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
+ return list;
+ };
- var parsed = $parse(attr.ngBindHtml);
- function getStringValue() { return (parsed(scope) || '').toString(); }
+ ctrl.$parsers.push(parse);
+ ctrl.$formatters.push(function(value) {
+ if (isArray(value)) {
+ return value.join(ngList);
+ }
- scope.$watch(getStringValue, function ngBindHtmlWatchAction(value) {
- element.html($sce.getTrustedHtml(parsed(scope)) || '');
- });
- };
- }];
+ return undefined;
+ });
- function classDirective(name, selector) {
- name = 'ngClass' + name;
- return ['$animate', function($animate) {
- return {
- restrict: 'AC',
- link: function(scope, element, attr) {
- var oldVal;
+ // Override the standard $isEmpty because an empty array means the input is empty.
+ ctrl.$isEmpty = function(value) {
+ return !value || !value.length;
+ };
+ }
+ };
+ };
- scope.$watch(attr[name], ngClassWatchAction, true);
+ /* global VALID_CLASS: true,
+ INVALID_CLASS: true,
+ PRISTINE_CLASS: true,
+ DIRTY_CLASS: true,
+ UNTOUCHED_CLASS: true,
+ TOUCHED_CLASS: true,
+ PENDING_CLASS: true,
+ addSetValidityMethod: true,
+ setupValidity: true,
+ defaultModelOptions: false
+*/
- attr.$observe('class', function(value) {
- ngClassWatchAction(scope.$eval(attr[name]));
- });
+ var VALID_CLASS = 'ng-valid',
+ INVALID_CLASS = 'ng-invalid',
+ PRISTINE_CLASS = 'ng-pristine',
+ DIRTY_CLASS = 'ng-dirty',
+ UNTOUCHED_CLASS = 'ng-untouched',
+ TOUCHED_CLASS = 'ng-touched',
+ EMPTY_CLASS = 'ng-empty',
+ NOT_EMPTY_CLASS = 'ng-not-empty';
- if (name !== 'ngClass') {
- scope.$watch('$index', function($index, old$index) {
- // jshint bitwise: false
- var mod = $index & 1;
- if (mod !== old$index & 1) {
- var classes = arrayClasses(scope.$eval(attr[name]));
- mod === selector ?
- addClasses(classes) :
- removeClasses(classes);
- }
- });
- }
+ var ngModelMinErr = minErr('ngModel');
- function addClasses(classes) {
- var newClasses = digestClassCounts(classes, 1);
- attr.$addClass(newClasses);
- }
+ /**
+ * @ngdoc type
+ * @name ngModel.NgModelController
+ * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
+ * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
+ * is set.
+ *
+ * @property {*} $modelValue The value in the model that the control is bound to.
+ *
+ * @property {Array.
} $parsers Array of functions to execute, as a pipeline, whenever
+ * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue
+ `$viewValue`} from the DOM, usually via user input.
+ See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation.
+ Note that the `$parsers` are not called when the bound ngModel expression changes programmatically.
- function removeClasses(classes) {
- var newClasses = digestClassCounts(classes, -1);
- attr.$removeClass(newClasses);
- }
+ The functions are called in array order, each passing
+ its return value through to the next. The last return value is forwarded to the
+ {@link ngModel.NgModelController#$validators `$validators`} collection.
- function digestClassCounts (classes, count) {
- var classCounts = element.data('$classCounts') || {};
- var classesToUpdate = [];
- forEach(classes, function (className) {
- if (count > 0 || classCounts[className]) {
- classCounts[className] = (classCounts[className] || 0) + count;
- if (classCounts[className] === +(count > 0)) {
- classesToUpdate.push(className);
- }
- }
- });
- element.data('$classCounts', classCounts);
- return classesToUpdate.join(' ');
- }
+ Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
+ `$viewValue`}.
- function updateClasses (oldClasses, newClasses) {
- var toAdd = arrayDifference(newClasses, oldClasses);
- var toRemove = arrayDifference(oldClasses, newClasses);
- toRemove = digestClassCounts(toRemove, -1);
- toAdd = digestClassCounts(toAdd, 1);
+ Returning `undefined` from a parser means a parse error occurred. In that case,
+ no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
+ will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
+ is set to `true`. The parse error is stored in `ngModel.$error.parse`.
- if (toAdd.length === 0) {
- $animate.removeClass(element, toRemove);
- } else if (toRemove.length === 0) {
- $animate.addClass(element, toAdd);
- } else {
- $animate.setClass(element, toAdd, toRemove);
- }
- }
+ This simple example shows a parser that would convert text input value to lowercase:
+ * ```js
+ * function parse(value) {
+ * if (value) {
+ * return value.toLowerCase();
+ * }
+ * }
+ * ngModelController.$parsers.push(parse);
+ * ```
- function ngClassWatchAction(newVal) {
- if (selector === true || scope.$index % 2 === selector) {
- var newClasses = arrayClasses(newVal || []);
- if (!oldVal) {
- addClasses(newClasses);
- } else if (!equals(newVal,oldVal)) {
- var oldClasses = arrayClasses(oldVal);
- updateClasses(oldClasses, newClasses);
- }
- }
- oldVal = copy(newVal);
- }
- }
- };
+ *
+ * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
+ the bound ngModel expression changes programmatically. The `$formatters` are not called when the
+ value of the control is changed by user interaction.
- function arrayDifference(tokens1, tokens2) {
- var values = [];
+ Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue
+ `$modelValue`} for display in the control.
- outer:
- for(var i = 0; i < tokens1.length; i++) {
- var token = tokens1[i];
- for(var j = 0; j < tokens2.length; j++) {
- if(token == tokens2[j]) continue outer;
- }
- values.push(token);
- }
- return values;
- }
+ The functions are called in reverse array order, each passing the value through to the
+ next. The last return value is used as the actual DOM value.
- function arrayClasses (classVal) {
- if (isArray(classVal)) {
- return classVal;
- } else if (isString(classVal)) {
- return classVal.split(' ');
- } else if (isObject(classVal)) {
- var classes = [], i = 0;
- forEach(classVal, function(v, k) {
- if (v) {
- classes.push(k);
- }
- });
- return classes;
- }
- return classVal;
- }
- }];
- }
+ This simple example shows a formatter that would convert the model value to uppercase:
- /**
- * @ngdoc directive
- * @name ngClass
- * @restrict AC
+ * ```js
+ * function format(value) {
+ * if (value) {
+ * return value.toUpperCase();
+ * }
+ * }
+ * ngModel.$formatters.push(format);
+ * ```
*
- * @description
- * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding
- * an expression that represents all classes to be added.
+ * @property {Object.} $validators A collection of validators that are applied
+ * whenever the model value changes. The key value within the object refers to the name of the
+ * validator while the function refers to the validation operation. The validation operation is
+ * provided with the model value as an argument and must return a true or false value depending
+ * on the response of that validation.
*
- * The directive operates in three different ways, depending on which of three types the expression
- * evaluates to:
+ * ```js
+ * ngModel.$validators.validCharacters = function(modelValue, viewValue) {
+ * var value = modelValue || viewValue;
+ * return /[0-9]+/.test(value) &&
+ * /[a-z]+/.test(value) &&
+ * /[A-Z]+/.test(value) &&
+ * /\W+/.test(value);
+ * };
+ * ```
*
- * 1. If the expression evaluates to a string, the string should be one or more space-delimited class
- * names.
+ * @property {Object.} $asyncValidators A collection of validations that are expected to
+ * perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
+ * is expected to return a promise when it is run during the model validation process. Once the promise
+ * is delivered then the validation status will be set to true when fulfilled and false when rejected.
+ * When the asynchronous validators are triggered, each of the validators will run in parallel and the model
+ * value will only be updated once all validators have been fulfilled. As long as an asynchronous validator
+ * is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators
+ * will only run once all synchronous validators have passed.
*
- * 2. If the expression evaluates to an array, each element of the array should be a string that is
- * one or more space-delimited class names.
+ * Please note that if $http is used then it is important that the server returns a success HTTP response code
+ * in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
*
- * 3. If the expression evaluates to an object, then for each key-value pair of the
- * object with a truthy value the corresponding key is used as a class name.
+ * ```js
+ * ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
+ * var value = modelValue || viewValue;
+ *
+ * // Lookup user by username
+ * return $http.get('/api/users/' + value).
+ * then(function resolved() {
+ * //username exists, this means validation fails
+ * return $q.reject('exists');
+ * }, function rejected() {
+ * //username does not exist, therefore this validation passes
+ * return true;
+ * });
+ * };
+ * ```
*
- * The directive won't add duplicate classes if a particular class was already set.
+ * @property {Array.} $viewChangeListeners Array of functions to execute whenever
+ * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change
+ * to {@link ngModel.NgModelController#$modelValue `$modelValue`}.
+ * It is called with no arguments, and its return value is ignored.
+ * This can be used in place of additional $watches against the model value.
*
- * When the expression changes, the previously added classes are removed and only then the
- * new classes are added.
+ * @property {Object} $error An object hash with all failing validator ids as keys.
+ * @property {Object} $pending An object hash with all pending validator ids as keys.
*
- * @animations
- * add - happens just before the class is applied to the element
- * remove - happens just before the class is removed from the element
+ * @property {boolean} $untouched True if control has not lost focus yet.
+ * @property {boolean} $touched True if control has lost focus.
+ * @property {boolean} $pristine True if user has not interacted with the control yet.
+ * @property {boolean} $dirty True if user has already interacted with the control.
+ * @property {boolean} $valid True if there is no error.
+ * @property {boolean} $invalid True if at least one error on the control.
+ * @property {string} $name The name attribute of the control.
*
- * @element ANY
- * @param {expression} ngClass {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class
- * names, an array, or a map of class names to boolean values. In the case of a map, the
- * names of the properties whose values are truthy will be added as css classes to the
- * element.
+ * @description
+ *
+ * `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
+ * The controller contains services for data-binding, validation, CSS updates, and value formatting
+ * and parsing. It purposefully does not contain any logic which deals with DOM rendering or
+ * listening to DOM events.
+ * Such DOM related logic should be provided by other directives which make use of
+ * `NgModelController` for data-binding to control elements.
+ * AngularJS provides this DOM logic for most {@link input `input`} elements.
+ * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
+ * custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
+ *
+ * @example
+ * ### Custom Control Example
+ * This example shows how to use `NgModelController` with a custom control to achieve
+ * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
+ * collaborate together to achieve the desired result.
+ *
+ * `contenteditable` is an HTML5 attribute, which tells the browser to let the element
+ * contents be edited in place by the user.
*
- * @example Example that demonstrates basic bindings via ngClass directive.
-
+ * We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
+ * module to automatically remove "bad" content like inline event listener (e.g. ``).
+ * However, as we are using `$sce` the model can still decide to provide unsafe content if it marks
+ * that content using the `$sce` service.
+ *
+ *
+
+ [contenteditable] {
+ border: 1px solid black;
+ background-color: white;
+ min-height: 20px;
+ }
+
+ .ng-invalid {
+ border: 1px solid red;
+ }
+
+
+
+ angular.module('customControl', ['ngSanitize']).
+ directive('contenteditable', ['$sce', function($sce) {
+ return {
+ restrict: 'A', // only activate on element attribute
+ require: '?ngModel', // get a hold of NgModelController
+ link: function(scope, element, attrs, ngModel) {
+ if (!ngModel) return; // do nothing if no ng-model
+
+ // Specify how UI should be updated
+ ngModel.$render = function() {
+ element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
+ };
+
+ // Listen for change events to enable binding
+ element.on('blur keyup change', function() {
+ scope.$evalAsync(read);
+ });
+ read(); // initialize
+
+ // Write data to the model
+ function read() {
+ var html = element.html();
+ // When we clear the content editable the browser leaves a behind
+ // If strip-br attribute is provided then we strip this out
+ if (attrs.stripBr && html === ' ') {
+ html = '';
+ }
+ ngModel.$setViewValue(html);
+ }
+ }
+ };
+ }]);
+
- Map Syntax Example
- deleted (apply "strike" class)
- important (apply "bold" class)
- error (apply "red" class)
-
- Using String Syntax
-
+
+ Change me!
+ Required!
- Using Array Syntax
-
-
-
+
+
-
- .strike {
- text-decoration: line-through;
- }
- .bold {
- font-weight: bold;
- }
- .red {
- color: red;
- }
+
+ it('should data-bind and become invalid', function() {
+ if (browser.params.browser === 'safari' || browser.params.browser === 'firefox') {
+ // SafariDriver can't handle contenteditable
+ // and Firefox driver can't clear contenteditables very well
+ return;
+ }
+ var contentEditable = element(by.css('[contenteditable]'));
+ var content = 'Change me!';
+
+ expect(contentEditable.getText()).toEqual(content);
+
+ contentEditable.clear();
+ contentEditable.sendKeys(protractor.Key.BACK_SPACE);
+ expect(contentEditable.getText()).toEqual('');
+ expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
+ });
-
- var ps = element.all(by.css('p'));
+ *
+ *
+ *
+ */
+ NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate'];
+ function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) {
+ this.$viewValue = Number.NaN;
+ this.$modelValue = Number.NaN;
+ this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
+ this.$validators = {};
+ this.$asyncValidators = {};
+ this.$parsers = [];
+ this.$formatters = [];
+ this.$viewChangeListeners = [];
+ this.$untouched = true;
+ this.$touched = false;
+ this.$pristine = true;
+ this.$dirty = false;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$error = {}; // keep invalid keys here
+ this.$$success = {}; // keep valid keys here
+ this.$pending = undefined; // keep pending keys here
+ this.$name = $interpolate($attr.name || '', false)($scope);
+ this.$$parentForm = nullFormCtrl;
+ this.$options = defaultModelOptions;
+ this.$$updateEvents = '';
+ // Attach the correct context to the event handler function for updateOn
+ this.$$updateEventHandler = this.$$updateEventHandler.bind(this);
+
+ this.$$parsedNgModel = $parse($attr.ngModel);
+ this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
+ this.$$ngModelGet = this.$$parsedNgModel;
+ this.$$ngModelSet = this.$$parsedNgModelAssign;
+ this.$$pendingDebounce = null;
+ this.$$parserValid = undefined;
+
+ this.$$currentValidationRunId = 0;
+
+ // https://github.com/angular/angular.js/issues/15833
+ // Prevent `$$scope` from being iterated over by `copy` when NgModelController is deep watched
+ Object.defineProperty(this, '$$scope', {value: $scope});
+ this.$$attr = $attr;
+ this.$$element = $element;
+ this.$$animate = $animate;
+ this.$$timeout = $timeout;
+ this.$$parse = $parse;
+ this.$$q = $q;
+ this.$$exceptionHandler = $exceptionHandler;
+
+ setupValidity(this);
+ setupModelWatcher(this);
+ }
- it('should let you toggle the class', function() {
+ NgModelController.prototype = {
+ $$initGetterSetters: function() {
+ if (this.$options.getOption('getterSetter')) {
+ var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
+ invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
- expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
- expect(ps.first().getAttribute('class')).not.toMatch(/red/);
+ this.$$ngModelGet = function($scope) {
+ var modelValue = this.$$parsedNgModel($scope);
+ if (isFunction(modelValue)) {
+ modelValue = invokeModelGetter($scope);
+ }
+ return modelValue;
+ };
+ this.$$ngModelSet = function($scope, newValue) {
+ if (isFunction(this.$$parsedNgModel($scope))) {
+ invokeModelSetter($scope, {$$$p: newValue});
+ } else {
+ this.$$parsedNgModelAssign($scope, newValue);
+ }
+ };
+ } else if (!this.$$parsedNgModel.assign) {
+ throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}',
+ this.$$attr.ngModel, startingTag(this.$$element));
+ }
+ },
- element(by.model('important')).click();
- expect(ps.first().getAttribute('class')).toMatch(/bold/);
- element(by.model('error')).click();
- expect(ps.first().getAttribute('class')).toMatch(/red/);
- });
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$render
+ *
+ * @description
+ * Called when the view needs to be updated. It is expected that the user of the ng-model
+ * directive will implement this method.
+ *
+ * The `$render()` method is invoked in the following situations:
+ *
+ * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
+ * committed value then `$render()` is called to update the input control.
+ * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
+ * 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 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.
+ */
+ $render: noop,
- it('should let you toggle string example', function() {
- expect(ps.get(1).getAttribute('class')).toBe('');
- element(by.model('style')).clear();
- element(by.model('style')).sendKeys('red');
- expect(ps.get(1).getAttribute('class')).toBe('red');
- });
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$isEmpty
+ *
+ * @description
+ * This is called when we need to determine if the value of an input is empty.
+ *
+ * For instance, the required directive does this to work out if the input has data or not.
+ *
+ * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
+ *
+ * You can override this for input directives whose concept of being empty is different from the
+ * default. The `checkboxInputType` directive does this because in its case a value of `false`
+ * implies empty.
+ *
+ * @param {*} value The value of the input to check for emptiness.
+ * @returns {boolean} True if `value` is "empty".
+ */
+ $isEmpty: function(value) {
+ // eslint-disable-next-line no-self-compare
+ return isUndefined(value) || value === '' || value === null || value !== value;
+ },
- it('array example should have 3 classes', function() {
- expect(ps.last().getAttribute('class')).toBe('');
- element(by.model('style1')).sendKeys('bold');
- element(by.model('style2')).sendKeys('strike');
- element(by.model('style3')).sendKeys('red');
- expect(ps.last().getAttribute('class')).toBe('bold strike red');
- });
-
-
+ $$updateEmptyClasses: function(value) {
+ if (this.$isEmpty(value)) {
+ this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, EMPTY_CLASS);
+ } else {
+ this.$$animate.removeClass(this.$$element, EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS);
+ }
+ },
- ## Animations
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setPristine
+ *
+ * @description
+ * Sets the control to its pristine state.
+ *
+ * This method can be called to remove the `ng-dirty` class and set the control to its pristine
+ * state (`ng-pristine` class). A model is considered to be pristine when the control
+ * has not been changed from when first compiled.
+ */
+ $setPristine: function() {
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$$animate.removeClass(this.$$element, DIRTY_CLASS);
+ this.$$animate.addClass(this.$$element, PRISTINE_CLASS);
+ },
- The example below demonstrates how to perform animations using ngClass.
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setDirty
+ *
+ * @description
+ * Sets the control to its dirty state.
+ *
+ * This method can be called to remove the `ng-pristine` class and set the control to its dirty
+ * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
+ * from when first compiled.
+ */
+ $setDirty: function() {
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$$parentForm.$setDirty();
+ },
-
-
-
-
-
- Sample Text
-
-
- .base-class {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setUntouched
+ *
+ * @description
+ * Sets the control to its untouched state.
+ *
+ * This method can be called to remove the `ng-touched` class and set the control to its
+ * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
+ * by default, however this function can be used to restore that state if the model has
+ * already been touched by the user.
+ */
+ $setUntouched: function() {
+ this.$touched = false;
+ this.$untouched = true;
+ this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS);
+ },
- .base-class.my-class {
- color: red;
- font-size:3em;
- }
-
-
- it('should check ng-class', function() {
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setTouched
+ *
+ * @description
+ * Sets the control to its touched state.
+ *
+ * This method can be called to remove the `ng-untouched` class and set the control to its
+ * touched state (`ng-touched` class). A model is considered to be touched when the user has
+ * first focused the control element and then shifted focus away from the control (blur event).
+ */
+ $setTouched: function() {
+ this.$touched = true;
+ this.$untouched = false;
+ this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS);
+ },
- element(by.id('setbtn')).click();
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$rollbackViewValue
+ *
+ * @description
+ * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
+ * which may be caused by a pending debounced event or because the input is waiting for some
+ * future event.
+ *
+ * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
+ * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of
+ * sync with the ngModel's `$modelValue`.
+ *
+ * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
+ * and reset the input to the last committed view value.
+ *
+ * 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 AngularJS's
+ * dirty checking mechanism is not able to tell whether the model has actually changed or not.
+ *
+ * The `$rollbackViewValue()` method should be called before programmatically changing the model of an
+ * input which may have such events pending. This is important in order to make sure that the
+ * input field will be updated with the new model value and any pending operations are cancelled.
+ *
+ * @example
+ *
+ *
+ * angular.module('cancel-update-example', [])
+ *
+ * .controller('CancelUpdateController', ['$scope', function($scope) {
+ * $scope.model = {value1: '', value2: ''};
+ *
+ * $scope.setEmpty = function(e, value, rollback) {
+ * if (e.keyCode === 27) {
+ * e.preventDefault();
+ * if (rollback) {
+ * $scope.myForm[value].$rollbackViewValue();
+ * }
+ * $scope.model[value] = '';
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
Both of these inputs are only updated if they are blurred. Hitting escape should
+ * empty them. Follow these steps and observe the difference:
+ *
+ * Type something in the input. You will see that the model is not yet updated
+ * Press the Escape key.
+ *
+ * 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.
+ *
+ * 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.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
Without $rollbackViewValue():
+ *
+ * value1: "{{ model.value1 }}"
+ *
+ *
+ *
+ *
With $rollbackViewValue():
+ *
+ * value2: "{{ model.value2 }}"
+ *
+ *
+ *
+ *
+
+ div {
+ display: table-cell;
+ }
+ div:nth-child(1) {
+ padding-right: 30px;
+ }
- expect(element(by.css('.base-class')).getAttribute('class')).
- toMatch(/my-class/);
+
+ *
+ */
+ $rollbackViewValue: function() {
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ this.$viewValue = this.$$lastCommittedViewValue;
+ this.$render();
+ },
- element(by.id('clearbtn')).click();
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$validate
+ *
+ * @description
+ * Runs each of the registered validators (first synchronous validators and then
+ * asynchronous validators).
+ * If the validity changes to invalid, the model will be set to `undefined`,
+ * unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
+ * If the validity changes to valid, it will set the model to the last available valid
+ * `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
+ */
+ $validate: function() {
+ // ignore $validate before model is initialized
+ if (isNumberNaN(this.$modelValue)) {
+ return;
+ }
- expect(element(by.css('.base-class')).getAttribute('class')).not.
- toMatch(/my-class/);
- });
-
-
+ var viewValue = this.$$lastCommittedViewValue;
+ // Note: we use the $$rawModelValue as $modelValue might have been
+ // set to undefined during a view -> model update that found validation
+ // errors. We can't parse the view here, since that could change
+ // the model although neither viewValue nor the model on the scope changed
+ var modelValue = this.$$rawModelValue;
+
+ var prevValid = this.$valid;
+ var prevModelValue = this.$modelValue;
+
+ var allowInvalid = this.$options.getOption('allowInvalid');
+
+ var that = this;
+ this.$$runValidators(modelValue, viewValue, function(allValid) {
+ // If there was no change in validity, don't update the model
+ // This prevents changing an invalid modelValue to undefined
+ if (!allowInvalid && prevValid !== allValid) {
+ // Note: Don't check this.$valid here, as we could have
+ // external validators (e.g. calculated on the server),
+ // that just call $setValidity and need the model value
+ // to calculate their validity.
+ that.$modelValue = allValid ? modelValue : undefined;
+
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
+ }
+ }
+ });
+ },
+ $$runValidators: function(modelValue, viewValue, doneCallback) {
+ this.$$currentValidationRunId++;
+ var localValidationRunId = this.$$currentValidationRunId;
+ var that = this;
- ## ngClass and pre-existing CSS3 Transitions/Animations
- The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure.
- Upon animation ngAnimate will apply supplementary CSS classes to track the start and end of an animation, but this will not hinder
- any pre-existing CSS transitions already on the element. To get an idea of what happens during a class-based animation, be sure
- to view the step by step details of {@link ngAnimate.$animate#addclass $animate.addClass} and
- {@link ngAnimate.$animate#removeclass $animate.removeClass}.
- */
- var ngClassDirective = classDirective('', true);
+ // check parser error
+ if (!processParseErrors()) {
+ validationDone(false);
+ return;
+ }
+ if (!processSyncValidators()) {
+ validationDone(false);
+ return;
+ }
+ processAsyncValidators();
- /**
- * @ngdoc directive
- * @name ngClassOdd
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result
- * of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
-
-
-
-
-
- {{name}}
-
-
-
-
-
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
-
-
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
-
-
- */
- var ngClassOddDirective = classDirective('Odd', 0);
+ function processParseErrors() {
+ var errorKey = that.$$parserName || 'parse';
+ if (isUndefined(that.$$parserValid)) {
+ setValidity(errorKey, null);
+ } else {
+ if (!that.$$parserValid) {
+ forEach(that.$validators, function(v, name) {
+ setValidity(name, null);
+ });
+ forEach(that.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ }
+ // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
+ setValidity(errorKey, that.$$parserValid);
+ return that.$$parserValid;
+ }
+ return true;
+ }
- /**
- * @ngdoc directive
- * @name ngClassEven
- * @restrict AC
- *
- * @description
- * The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except they work in
- * conjunction with `ngRepeat` and take effect only on odd (even) rows.
- *
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
- *
- * @element ANY
- * @param {expression} ngClassEven {@link guide/expression Expression} to eval. The
- * result of the evaluation can be a string representing space delimited class names or an array.
- *
- * @example
-
-
-
-
-
- {{name}}
-
-
-
-
-
- .odd {
- color: red;
- }
- .even {
- color: blue;
- }
-
-
- it('should check ng-class-odd and ng-class-even', function() {
- expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
- toMatch(/odd/);
- expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
- toMatch(/even/);
- });
-
-
- */
- var ngClassEvenDirective = classDirective('Even', 1);
+ function processSyncValidators() {
+ var syncValidatorsValid = true;
+ forEach(that.$validators, function(validator, name) {
+ var result = Boolean(validator(modelValue, viewValue));
+ syncValidatorsValid = syncValidatorsValid && result;
+ setValidity(name, result);
+ });
+ if (!syncValidatorsValid) {
+ forEach(that.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ return false;
+ }
+ return true;
+ }
- /**
- * @ngdoc directive
- * @name ngCloak
- * @restrict AC
- *
- * @description
- * The `ngCloak` directive is used to prevent the Angular html template from being briefly
- * displayed by the browser in its raw (uncompiled) form while your application is loading. Use this
- * directive to avoid the undesirable flicker effect caused by the html template display.
- *
- * The directive can be applied to the `` element, but the preferred usage is to apply
- * multiple `ngCloak` directives to small portions of the page to permit progressive rendering
- * of the browser view.
- *
- * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and
- * `angular.min.js`.
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
- *
- * ```css
- * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- * display: none !important;
- * }
- * ```
- *
- * When this css rule is loaded by the browser, all html elements (including their children) that
- * are tagged with the `ngCloak` directive are hidden. When Angular encounters this directive
- * during the compilation of the template it deletes the `ngCloak` element attribute, making
- * the compiled element visible.
- *
- * For the best result, the `angular.js` script must be loaded in the head section of the html
- * document; alternatively, the css rule above must be included in the external stylesheet of the
- * application.
- *
- * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they
- * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css
- * class `ng-cloak` in addition to the `ngCloak` directive as shown in the example below.
- *
- * @element ANY
- *
- * @example
-
-
- {{ 'hello' }}
- {{ 'hello IE7' }}
-
-
- it('should remove the template directive and css class', function() {
- expect($('#template1').getAttribute('ng-cloak')).
- toBeNull();
- expect($('#template2').getAttribute('ng-cloak')).
- toBeNull();
- });
-
-
- *
- */
- var ngCloakDirective = ngDirective({
- compile: function(element, attr) {
- attr.$set('ngCloak', undefined);
- element.removeClass('ng-cloak');
- }
- });
+ function processAsyncValidators() {
+ var validatorPromises = [];
+ var allValid = true;
+ forEach(that.$asyncValidators, function(validator, name) {
+ var promise = validator(modelValue, viewValue);
+ if (!isPromiseLike(promise)) {
+ throw ngModelMinErr('nopromise',
+ 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise);
+ }
+ setValidity(name, undefined);
+ validatorPromises.push(promise.then(function() {
+ setValidity(name, true);
+ }, function() {
+ allValid = false;
+ setValidity(name, false);
+ }));
+ });
+ if (!validatorPromises.length) {
+ validationDone(true);
+ } else {
+ that.$$q.all(validatorPromises).then(function() {
+ validationDone(allValid);
+ }, noop);
+ }
+ }
- /**
- * @ngdoc directive
- * @name ngController
- *
- * @description
- * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular
- * supports the principles behind the Model-View-Controller design pattern.
- *
- * MVC components in angular:
- *
- * * Model — The Model is scope properties; scopes are attached to the DOM where scope properties
- * are accessed through bindings.
- * * View — The template (HTML with data bindings) that is rendered into the View.
- * * Controller — The `ngController` directive specifies a Controller class; the class contains business
- * logic behind the application to decorate the scope with functions and values
- *
- * Note that you can also attach controllers to the DOM by declaring it in a route definition
- * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller
- * again using `ng-controller` in the template itself. This will cause the controller to be attached
- * and executed twice.
- *
- * @element ANY
- * @scope
- * @param {expression} ngController Name of a globally accessible constructor function or an
- * {@link guide/expression expression} that on the current scope evaluates to a
- * constructor function. The controller instance can be published into a scope property
- * by specifying `as propertyName`.
- *
- * @example
- * Here is a simple form for editing user contact information. Adding, removing, clearing, and
- * greeting are methods declared on the controller (see source tab). These methods can
- * easily be called from the angular markup. Notice that the scope becomes the `this` for the
- * controller's instance. This allows for easy access to the view data from the controller. Also
- * notice that any changes to the data are automatically reflected in the View without the need
- * for a manual update. The example is shown in two different declaration styles you may use
- * according to preference.
-
-
-
-
- Name:
- [
greet ]
- Contact:
-
-
-
-
- it('should check controller as', function() {
- var container = element(by.id('ctrl-as-exmpl'));
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$commitViewValue
+ *
+ * @description
+ * Commit a pending update to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
+ * usually handles calling this in response to input events.
+ */
+ $commitViewValue: function() {
+ var viewValue = this.$viewValue;
- expect(container.findElement(by.model('settings.name'))
- .getAttribute('value')).toBe('John Smith');
+ this.$$timeout.cancel(this.$$pendingDebounce);
- var firstRepeat =
- container.findElement(by.repeater('contact in settings.contacts').row(0));
- var secondRepeat =
- container.findElement(by.repeater('contact in settings.contacts').row(1));
+ // If the view value has not changed then we should just exit, except in the case where there is
+ // a native validator on the element. In this case the validation state may have changed even though
+ // the viewValue has stayed empty.
+ if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) {
+ return;
+ }
+ this.$$updateEmptyClasses(viewValue);
+ this.$$lastCommittedViewValue = viewValue;
- expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('408 555 1212');
- expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('john.smith@example.org');
+ // change to dirty
+ if (this.$pristine) {
+ this.$setDirty();
+ }
+ this.$$parseAndValidate();
+ },
- firstRepeat.findElement(by.linkText('clear')).click();
+ $$parseAndValidate: function() {
+ var viewValue = this.$$lastCommittedViewValue;
+ var modelValue = viewValue;
+ var that = this;
- expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('');
+ this.$$parserValid = isUndefined(modelValue) ? undefined : true;
- container.findElement(by.linkText('add')).click();
+ if (this.$$parserValid) {
+ for (var i = 0; i < this.$parsers.length; i++) {
+ modelValue = this.$parsers[i](modelValue);
+ if (isUndefined(modelValue)) {
+ this.$$parserValid = false;
+ break;
+ }
+ }
+ }
+ if (isNumberNaN(this.$modelValue)) {
+ // this.$modelValue has not been touched yet...
+ this.$modelValue = this.$$ngModelGet(this.$$scope);
+ }
+ var prevModelValue = this.$modelValue;
+ var allowInvalid = this.$options.getOption('allowInvalid');
+ this.$$rawModelValue = modelValue;
- expect(container.findElement(by.repeater('contact in settings.contacts').row(2))
- .findElement(by.model('contact.value'))
- .getAttribute('value'))
- .toBe('yourname@example.org');
- });
-
-
-
-
-
-
- Name:
- [
greet ]
- Contact:
-
-
-
-
- it('should check controller', function() {
- var container = element(by.id('ctrl-exmpl'));
+ $$writeModelToScope: function() {
+ this.$$ngModelSet(this.$$scope, this.$modelValue);
+ forEach(this.$viewChangeListeners, function(listener) {
+ try {
+ listener();
+ } catch (e) {
+ // eslint-disable-next-line no-invalid-this
+ this.$$exceptionHandler(e);
+ }
+ }, this);
+ },
- expect(container.findElement(by.model('name'))
- .getAttribute('value')).toBe('John Smith');
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setViewValue
+ *
+ * @description
+ * Update the view value.
+ *
+ * This method should be called when a control wants to change the view value; typically,
+ * this is done from within a DOM event handler. For example, the {@link ng.directive:input input}
+ * directive calls it when the value of the input changes and {@link ng.directive:select select}
+ * calls it when an option is selected.
+ *
+ * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
+ * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
+ * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and
+ * `$asyncValidators` are called and the value is applied to `$modelValue`.
+ * Finally, the value is set to the **expression** specified in the `ng-model` attribute and
+ * all the registered change listeners, in the `$viewChangeListeners` list are called.
+ *
+ * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
+ * and the `default` trigger is not listed, all those actions will remain pending until one of the
+ * `updateOn` events is triggered on the DOM element.
+ * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
+ * directive is used with a custom debounce for this particular event.
+ * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce`
+ * is specified, once the timer runs out.
+ *
+ * When used with standard inputs, the view value will always be a string (which is in some cases
+ * parsed into another type, such as a `Date` object for `input[date]`.)
+ * 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 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.
+ *
+ *
+ * In any case, the value passed to the method should always reflect the current value
+ * of the control. For example, if you are calling `$setViewValue` for an input element,
+ * you should pass the input DOM value. Otherwise, the control and the scope model become
+ * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change
+ * the control's DOM value in any way. If we want to change the control's DOM value
+ * programmatically, we should update the `ngModel` scope expression. Its new value will be
+ * picked up by the model controller, which will run it through the `$formatters`, `$render` it
+ * to update the DOM, and finally call `$validate` on it.
+ *
+ *
+ * @param {*} value value from the view.
+ * @param {string} trigger Event that triggered the update.
+ */
+ $setViewValue: function(value, trigger) {
+ this.$viewValue = value;
+ if (this.$options.getOption('updateOnDefault')) {
+ this.$$debounceViewValueCommit(trigger);
+ }
+ },
- var firstRepeat =
- container.findElement(by.repeater('contact in contacts').row(0));
- var secondRepeat =
- container.findElement(by.repeater('contact in contacts').row(1));
+ $$debounceViewValueCommit: function(trigger) {
+ var debounceDelay = this.$options.getOption('debounce');
- expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('408 555 1212');
- expect(secondRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('john.smith@example.org');
+ if (isNumber(debounceDelay[trigger])) {
+ debounceDelay = debounceDelay[trigger];
+ } else if (isNumber(debounceDelay['default'])) {
+ debounceDelay = debounceDelay['default'];
+ }
- firstRepeat.findElement(by.linkText('clear')).click();
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ var that = this;
+ if (debounceDelay > 0) { // this fails if debounceDelay is an object
+ this.$$pendingDebounce = this.$$timeout(function() {
+ that.$commitViewValue();
+ }, debounceDelay);
+ } else if (this.$$scope.$root.$$phase) {
+ this.$commitViewValue();
+ } else {
+ this.$$scope.$apply(function() {
+ that.$commitViewValue();
+ });
+ }
+ },
- expect(firstRepeat.findElement(by.model('contact.value')).getAttribute('value'))
- .toBe('');
+ /**
+ * @ngdoc method
+ *
+ * @name ngModel.NgModelController#$overrideModelOptions
+ *
+ * @description
+ *
+ * Override the current model options settings programmatically.
+ *
+ * The previous `ModelOptions` value will not be modified. Instead, a
+ * new `ModelOptions` object will inherit from the previous one overriding
+ * or inheriting settings that are defined in the given parameter.
+ *
+ * See {@link ngModelOptions} for information about what options can be specified
+ * and how model option inheritance works.
+ *
+ *
+ * **Note:** this function only affects the options set on the `ngModelController`,
+ * and not the options on the {@link ngModelOptions} directive from which they might have been
+ * obtained initially.
+ *
+ *
+ *
+ * **Note:** it is not possible to override the `getterSetter` option.
+ *
+ *
+ * @param {Object} options a hash of settings to override the previous options
+ *
+ */
+ $overrideModelOptions: function(options) {
+ this.$options = this.$options.createChild(options);
+ this.$$setUpdateOnEvents();
+ },
- container.findElement(by.linkText('add')).click();
+ /**
+ * @ngdoc method
+ *
+ * @name ngModel.NgModelController#$processModelValue
- expect(container.findElement(by.repeater('contact in contacts').row(2))
- .findElement(by.model('contact.value'))
- .getAttribute('value'))
- .toBe('yourname@example.org');
- });
-
-
+ * @description
+ *
+ * Runs the model -> view pipeline on the current
+ * {@link ngModel.NgModelController#$modelValue $modelValue}.
+ *
+ * The following actions are performed by this method:
+ *
+ * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters}
+ * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue}
+ * - the `ng-empty` or `ng-not-empty` class is set on the element
+ * - if the `$viewValue` has changed:
+ * - {@link ngModel.NgModelController#$render $render} is called on the control
+ * - the {@link ngModel.NgModelController#$validators $validators} are run and
+ * the validation status is set.
+ *
+ * This method is called by ngModel internally when the bound scope value changes.
+ * Application developers usually do not have to call this function themselves.
+ *
+ * This function can be used when the `$viewValue` or the rendered DOM value are not correctly
+ * formatted and the `$modelValue` must be run through the `$formatters` again.
+ *
+ * @example
+ * Consider a text input with an autocomplete list (for fruit), where the items are
+ * objects with a name and an id.
+ * A user enters `ap` and then selects `Apricot` from the list.
+ * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`,
+ * but the rendered value will still be `ap`.
+ * The widget can then call `ctrl.$processModelValue()` to run the model -> view
+ * pipeline again, which formats the object to the string `Apricot`,
+ * then updates the `$viewValue`, and finally renders it in the DOM.
+ *
+ *
+
+
+
+ Search Fruit:
+
+
+
+ Model:
+
{{selectedFruit | json}}
+
+
+
+
+ angular.module('inputExample', [])
+ .controller('inputController', function($scope) {
+ $scope.items = [
+ {name: 'Apricot', id: 443},
+ {name: 'Clementine', id: 972},
+ {name: 'Durian', id: 169},
+ {name: 'Jackfruit', id: 982},
+ {name: 'Strawberry', id: 863}
+ ];
+ })
+ .component('basicAutocomplete', {
+ bindings: {
+ items: '<',
+ onSelect: '&'
+ },
+ templateUrl: 'autocomplete.html',
+ controller: function($element, $scope) {
+ var that = this;
+ var ngModel;
+
+ that.$postLink = function() {
+ ngModel = $element.find('input').controller('ngModel');
+
+ ngModel.$formatters.push(function(value) {
+ return (value && value.name) || value;
+ });
+
+ ngModel.$parsers.push(function(value) {
+ var match = value;
+ for (var i = 0; i < that.items.length; i++) {
+ if (that.items[i].name === value) {
+ match = that.items[i];
+ break;
+ }
+ }
+
+ return match;
+ });
+ };
+
+ that.selectItem = function(item) {
+ ngModel.$setViewValue(item);
+ ngModel.$processModelValue();
+ that.onSelect({item: item});
+ };
+ }
+ });
+
+
+
+
+ *
+ *
+ */
+ $processModelValue: function() {
+ var viewValue = this.$$format();
+
+ if (this.$viewValue !== viewValue) {
+ this.$$updateEmptyClasses(viewValue);
+ this.$viewValue = this.$$lastCommittedViewValue = viewValue;
+ this.$render();
+ // It is possible that model and view value have been updated during render
+ this.$$runValidators(this.$modelValue, this.$viewValue, noop);
+ }
+ },
+
+ /**
+ * This method is called internally to run the $formatters on the $modelValue
+ */
+ $$format: function() {
+ var formatters = this.$formatters,
+ idx = formatters.length;
+
+ var viewValue = this.$modelValue;
+ while (idx--) {
+ viewValue = formatters[idx](viewValue);
+ }
+
+ return viewValue;
+ },
+
+ /**
+ * This method is called internally when the bound scope value changes.
+ */
+ $$setModelValue: function(modelValue) {
+ this.$modelValue = this.$$rawModelValue = modelValue;
+ this.$$parserValid = undefined;
+ this.$processModelValue();
+ },
+
+ $$setUpdateOnEvents: function() {
+ if (this.$$updateEvents) {
+ this.$$element.off(this.$$updateEvents, this.$$updateEventHandler);
+ }
+
+ this.$$updateEvents = this.$options.getOption('updateOn');
+ if (this.$$updateEvents) {
+ this.$$element.on(this.$$updateEvents, this.$$updateEventHandler);
+ }
+ },
+
+ $$updateEventHandler: function(ev) {
+ this.$$debounceViewValueCommit(ev && ev.type);
+ }
+ };
+
+ function setupModelWatcher(ctrl) {
+ // model -> value
+ // Note: we cannot use a normal scope.$watch as we want to detect the following:
+ // 1. scope value is 'a'
+ // 2. user enters 'b'
+ // 3. ng-change kicks in and reverts scope value to 'a'
+ // -> scope value did not change since the last digest as
+ // ng-change executes in apply phase
+ // 4. view should be changed back to 'a'
+ ctrl.$$scope.$watch(function ngModelWatch(scope) {
+ var modelValue = ctrl.$$ngModelGet(scope);
+
+ // if scope model value and ngModel value are out of sync
+ // This cannot be moved to the action function, because it would not catch the
+ // case where the model is changed in the ngChange function or the model setter
+ if (modelValue !== ctrl.$modelValue &&
+ // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
+ // eslint-disable-next-line no-self-compare
+ (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
+ ) {
+ ctrl.$$setModelValue(modelValue);
+ }
+
+ return modelValue;
+ });
+ }
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setValidity
+ *
+ * @description
+ * Change the validity state, and notify the form.
+ *
+ * This method can be called within $parsers/$formatters or a custom validation implementation.
+ * However, in most cases it should be sufficient to use the `ngModel.$validators` and
+ * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
+ * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
+ * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
+ * The `validationErrorKey` should be in camelCase and will get converted into dash-case
+ * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
+ * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
+ * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and
+ * when `$asyncValidators` do not run because any of the `$validators` failed.
*/
- var ngControllerDirective = [function() {
- return {
- scope: true,
- controller: '@',
- priority: 500
- };
- }];
+ addSetValidityMethod({
+ clazz: NgModelController,
+ set: function(object, property) {
+ object[property] = true;
+ },
+ unset: function(object, property) {
+ delete object[property];
+ }
+ });
+
/**
* @ngdoc directive
- * @name ngCsp
+ * @name ngModel
+ * @restrict A
+ * @priority 1
+ * @param {expression} ngModel assignable {@link guide/expression Expression} to bind to.
*
- * @element html
* @description
- * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
+ * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
+ * property on the scope using {@link ngModel.NgModelController NgModelController},
+ * which is created and exposed by this directive.
*
- * This is necessary when developing things like Google Chrome Extensions.
+ * `ngModel` is responsible for:
*
- * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
- * For us to be compatible, we just need to implement the "getterFn" in $parse without violating
- * any of these restrictions.
+ * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
+ * 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`, `ng-empty`, `ng-not-empty`) including animations.
+ * - Registering the control with its parent {@link ng.directive:form form}.
*
- * AngularJS uses `Function(string)` generated functions as a speed optimization. Applying the `ngCsp`
- * directive will cause Angular to use CSP compatibility mode. When this mode is on AngularJS will
- * evaluate all expressions up to 30% slower than in non-CSP mode, but no security violations will
- * be raised.
+ * Note: `ngModel` will try to bind to the property given by evaluating the expression on the
+ * current scope. If the property doesn't already exist on this scope, it will be created
+ * implicitly and added to the scope.
*
- * CSP forbids JavaScript to inline stylesheet rules. In non CSP mode Angular automatically
- * includes some CSS rules (e.g. {@link ng.directive:ngCloak ngCloak}).
- * To make those directives work in CSP mode, include the `angular-csp.css` manually.
+ * For best practices on using `ngModel`, see:
*
- * In order to use this feature put the `ngCsp` directive on the root element of the application.
+ * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
*
- * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
+ * For basic examples, how to use `ngModel`, see:
*
- * @example
- * This example shows how to apply the `ngCsp` directive to the `html` tag.
- ```html
-
-
- ...
- ...
-
- ```
- */
-
-// ngCsp is not implemented as a proper directive any more, because we need it be processed while we bootstrap
-// the system (before $parse is instantiated), for this reason we just have a csp() fn that looks for ng-csp attribute
-// anywhere in the current doc
-
- /**
- * @ngdoc directive
- * @name ngClick
+ * - {@link ng.directive:input input}
+ * - {@link input[text] text}
+ * - {@link input[checkbox] checkbox}
+ * - {@link input[radio] radio}
+ * - {@link input[number] number}
+ * - {@link input[email] email}
+ * - {@link input[url] url}
+ * - {@link input[date] date}
+ * - {@link input[datetime-local] datetime-local}
+ * - {@link input[time] time}
+ * - {@link input[month] month}
+ * - {@link input[week] week}
+ * - {@link ng.directive:select select}
+ * - {@link ng.directive:textarea textarea}
*
- * @description
- * The ngClick directive allows you to specify custom behavior when
- * an element is clicked.
+ * ## Complex Models (objects or collections)
*
- * @element ANY
- * @priority 0
- * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon
- * click. ({@link guide/expression#-event- Event object is available as `$event`})
+ * 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.
*
- * @example
-
-
-
- Increment
-
- count: {{count}}
-
-
- it('should check ng-click', function() {
- expect(element(by.binding('count')).getText()).toMatch('0');
- element(by.css('button')).click();
- expect(element(by.binding('count')).getText()).toMatch('1');
- });
-
-
- */
- /*
- * A directive that allows creation of custom onclick handlers that are defined as angular
- * expressions and are compiled and executed within the current scope.
+ * The model must be assigned an entirely new object or collection before a re-rendering will occur.
*
- * Events that are handled via these handler are always configured not to propagate further.
- */
- var ngEventDirectives = {};
- forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
- function(name) {
- var directiveName = directiveNormalize('ng-' + name);
- ngEventDirectives[directiveName] = ['$parse', function($parse) {
- return {
- compile: function($element, attr) {
- var fn = $parse(attr[directiveName]);
- return function(scope, element, attr) {
- element.on(lowercase(name), function(event) {
- scope.$apply(function() {
- fn(scope, {$event:event});
- });
- });
- };
- }
- };
- }];
- }
- );
-
- /**
- * @ngdoc directive
- * @name ngDblclick
+ * 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.
*
- * @description
- * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event.
+ * 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.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon
- * a dblclick. (The Event object is available as `$event`)
+ * ## CSS classes
+ * The following CSS classes are added and removed on the associated input/select/textarea element
+ * depending on the validity of the model.
*
- * @example
-
-
-
- Increment (on double click)
-
- count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngMousedown
+ * - `ng-valid`: the model is valid
+ * - `ng-invalid`: the model is invalid
+ * - `ng-valid-[key]`: for each valid key added by `$setValidity`
+ * - `ng-invalid-[key]`: for each invalid key added by `$setValidity`
+ * - `ng-pristine`: the control hasn't been interacted with yet
+ * - `ng-dirty`: the control has been interacted with
+ * - `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
*
- * @description
- * The ngMousedown directive allows you to specify custom behavior on mousedown event.
+ * Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon
- * mousedown. ({@link guide/expression#-event- Event object is available as `$event`})
+ * @animations
+ * 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 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.
+ *
+ * The following example shows a simple way to utilize CSS transitions to style an input element
+ * that has been rendered as invalid after it has been validated:
+ *
+ *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-input {
+ * transition:0.5s linear all;
+ * background: white;
+ * }
+ * .my-input.ng-invalid {
+ * background: red;
+ * color:white;
+ * }
+ *
*
* @example
-
+ * ### Basic Usage
+ *
-
- Increment (on mouse down)
-
- count: {{count}}
+
+
+
+ Update input to see transitions when valid/invalid.
+ Integer is a valid value.
+
+
+
+
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngMouseup
+ *
*
- * @description
- * Specify custom behavior on mouseup event.
+ * @example
+ * ### Binding to a getter/setter
*
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon
- * mouseup. ({@link guide/expression#-event- Event object is available as `$event`})
+ * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
+ * function that returns a representation of the model when called with zero arguments, and sets
+ * the internal state of a model when called with an argument. It's sometimes useful to use this
+ * for models that have an internal representation that's different from what the model exposes
+ * to the view.
+ *
+ *
+ * **Best Practice:** It's best to keep getters fast because AngularJS is likely to call them more
+ * frequently than other parts of your code.
+ *
+ *
+ * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
+ * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
+ * a ``, which will enable this behavior for all ` `s within it. See
+ * {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
+ *
+ * The following example shows how to use `ngModel` with a getter/setter:
*
* @example
-
+ *
-
- Increment (on mouse up)
-
- count: {{count}}
+
+
+ Name:
+
+
+
+
user.name =
+
-
+
+ angular.module('getterSetterExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var _name = 'Brian';
+ $scope.user = {
+ name: function(newName) {
+ // Note that newName can be undefined for two reasons:
+ // 1. Because it is called as a getter and thus called with no arguments
+ // 2. Because the property should actually be set to undefined. This happens e.g. if the
+ // input is invalid
+ return arguments.length ? (_name = newName) : _name;
+ }
+ };
+ }]);
+
+ *
*/
+ var ngModelDirective = ['$rootScope', function($rootScope) {
+ return {
+ restrict: 'A',
+ require: ['ngModel', '^?form', '^?ngModelOptions'],
+ controller: NgModelController,
+ // Prelink needs to run before any input directive
+ // so that we can set the NgModelOptions in NgModelController
+ // before anyone else uses it.
+ priority: 1,
+ compile: function ngModelCompile(element) {
+ // Setup initial state of the control
+ element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
+
+ return {
+ pre: function ngModelPreLink(scope, element, attr, ctrls) {
+ var modelCtrl = ctrls[0],
+ formCtrl = ctrls[1] || modelCtrl.$$parentForm,
+ optionsCtrl = ctrls[2];
+
+ if (optionsCtrl) {
+ modelCtrl.$options = optionsCtrl.$options;
+ }
+
+ modelCtrl.$$initGetterSetters();
+
+ // notify others, especially parent forms
+ formCtrl.$addControl(modelCtrl);
+
+ attr.$observe('name', function(newValue) {
+ if (modelCtrl.$name !== newValue) {
+ modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
+ }
+ });
+
+ scope.$on('$destroy', function() {
+ modelCtrl.$$parentForm.$removeControl(modelCtrl);
+ });
+ },
+ post: function ngModelPostLink(scope, element, attr, ctrls) {
+ var modelCtrl = ctrls[0];
+ modelCtrl.$$setUpdateOnEvents();
+
+ function setTouched() {
+ modelCtrl.$setTouched();
+ }
+
+ element.on('blur', function() {
+ if (modelCtrl.$touched) return;
+
+ if ($rootScope.$$phase) {
+ scope.$evalAsync(setTouched);
+ } else {
+ scope.$apply(setTouched);
+ }
+ });
+ }
+ };
+ }
+ };
+ }];
+
+ /* exported defaultModelOptions */
+ var defaultModelOptions;
+ var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
/**
- * @ngdoc directive
- * @name ngMouseover
- *
+ * @ngdoc type
+ * @name ModelOptions
* @description
- * Specify custom behavior on mouseover event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon
- * mouseover. ({@link guide/expression#-event- Event object is available as `$event`})
- *
- * @example
-
-
-
- Increment (when mouse is over)
-
- count: {{count}}
-
-
+ * A container for the options set by the {@link ngModelOptions} directive
*/
+ function ModelOptions(options) {
+ this.$$options = options;
+ }
+
+ ModelOptions.prototype = {
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#getOption
+ * @param {string} name the name of the option to retrieve
+ * @returns {*} the value of the option
+ * @description
+ * Returns the value of the given option
+ */
+ getOption: function(name) {
+ return this.$$options[name];
+ },
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#createChild
+ * @param {Object} options a hash of options for the new child that will override the parent's options
+ * @return {ModelOptions} a new `ModelOptions` object initialized with the given options.
+ */
+ createChild: function(options) {
+ var inheritAll = false;
+
+ // make a shallow copy
+ options = extend({}, options);
+
+ // Inherit options from the parent if specified by the value `"$inherit"`
+ forEach(options, /* @this */ function(option, key) {
+ if (option === '$inherit') {
+ if (key === '*') {
+ inheritAll = true;
+ } else {
+ options[key] = this.$$options[key];
+ // `updateOn` is special so we must also inherit the `updateOnDefault` option
+ if (key === 'updateOn') {
+ options.updateOnDefault = this.$$options.updateOnDefault;
+ }
+ }
+ } else {
+ if (key === 'updateOn') {
+ // If the `updateOn` property contains the `default` event then we have to remove
+ // it from the event list and set the `updateOnDefault` flag.
+ options.updateOnDefault = false;
+ options[key] = trim(option.replace(DEFAULT_REGEXP, function() {
+ options.updateOnDefault = true;
+ return ' ';
+ }));
+ }
+ }
+ }, this);
+
+ if (inheritAll) {
+ // We have a property of the form: `"*": "$inherit"`
+ delete options['*'];
+ defaults(options, this.$$options);
+ }
+
+ // Finally add in any missing defaults
+ defaults(options, defaultModelOptions.$$options);
+
+ return new ModelOptions(options);
+ }
+ };
+
+
+ defaultModelOptions = new ModelOptions({
+ updateOn: '',
+ updateOnDefault: true,
+ debounce: 0,
+ getterSetter: false,
+ allowInvalid: false,
+ timezone: null
+ });
/**
* @ngdoc directive
- * @name ngMouseenter
+ * @name ngModelOptions
+ * @restrict A
+ * @priority 10
*
* @description
- * Specify custom behavior on mouseenter event.
+ * This directive allows you to modify the behaviour of {@link ngModel} directives within your
+ * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel}
+ * directives will use the options of their nearest `ngModelOptions` ancestor.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon
- * mouseenter. ({@link guide/expression#-event- Event object is available as `$event`})
+ * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as
+ * an AngularJS expression. This expression should evaluate to an object, whose properties contain
+ * the settings. For example: `
-
-
- Increment (when mouse enters)
-
- count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngMouseleave
+ * ## Inheriting Options
*
- * @description
- * Specify custom behavior on mouseleave event.
+ * You can specify that an `ngModelOptions` setting should be inherited from a parent `ngModelOptions`
+ * directive by giving it the value of `"$inherit"`.
+ * Then it will inherit that setting from the first `ngModelOptions` directive found by traversing up the
+ * DOM tree. If there is no ancestor element containing an `ngModelOptions` directive then default settings
+ * will be used.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon
- * mouseleave. ({@link guide/expression#-event- Event object is available as `$event`})
+ * For example given the following fragment of HTML
*
- * @example
-
-
-
- Increment (when mouse leaves)
-
- count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngMousemove
*
- * @description
- * Specify custom behavior on mousemove event.
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
*
- * @element ANY
- * @priority 0
- * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon
- * mousemove. ({@link guide/expression#-event- Event object is available as `$event`})
+ * the `input` element will have the following settings
*
- * @example
-
-
-
- Increment (when mouse moves)
-
- count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngKeydown
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 0 }
+ * ```
*
- * @description
- * Specify custom behavior on keydown event.
+ * Notice that the `debounce` setting was not inherited and used the default value instead.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngKeydown {@link guide/expression Expression} to evaluate upon
- * keydown. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
+ * You can specify that all undefined settings are automatically inherited from an ancestor by
+ * including a property with key of `"*"` and value of `"$inherit"`.
*
- * @example
-
-
-
- key down count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngKeyup
+ * For example given the following fragment of HTML
*
- * @description
- * Specify custom behavior on keyup event.
*
- * @element ANY
- * @priority 0
- * @param {expression} ngKeyup {@link guide/expression Expression} to evaluate upon
- * keyup. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
*
- * @example
-
-
-
- key up count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngKeypress
+ * the `input` element will have the following settings
*
- * @description
- * Specify custom behavior on keypress event.
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 200 }
+ * ```
*
- * @element ANY
- * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon
- * keypress. ({@link guide/expression#-event- Event object is available as `$event`}
- * and can be interrogated for keyCode, altKey, etc.)
+ * Notice that the `debounce` setting now inherits the value from the outer `
` element.
*
- * @example
-
-
-
- key press count: {{count}}
-
-
- */
-
-
- /**
- * @ngdoc directive
- * @name ngSubmit
+ * If you are creating a reusable component then you should be careful when using `"*": "$inherit"`
+ * since you may inadvertently inherit a setting in the future that changes the behavior of your component.
*
- * @description
- * Enables binding angular expressions to onsubmit events.
*
- * Additionally it prevents the default action (which for form means sending the request to the
- * server and reloading the current page), but only if the form does not contain `action`,
- * `data-action`, or `x-action` attributes.
+ * ## Triggering and debouncing model updates
*
- * @element form
- * @priority 0
- * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
- * ({@link guide/expression#-event- Event object is available as `$event`})
+ * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
+ * trigger a model update and/or a debouncing delay so that the actual update only takes place when
+ * a timer expires; this timer will be reset after another change takes place.
*
- * @example
-
-
-
-
- Enter text and hit enter:
-
-
- list={{list}}
-
-
-
- it('should check ng-submit', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- expect(element(by.input('text')).getAttribute('value')).toBe('');
- });
- it('should ignore empty strings', function() {
- expect(element(by.binding('list')).getText()).toBe('list=[]');
- element(by.css('#submit')).click();
- element(by.css('#submit')).click();
- expect(element(by.binding('list')).getText()).toContain('hello');
- });
-
-
- */
-
- /**
- * @ngdoc directive
- * @name ngFocus
+ * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
+ * be different from the value in the actual model. This means that if you update the model you
+ * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in
+ * order to make sure it is synchronized with the model and that any debounced action is canceled.
*
- * @description
- * Specify custom behavior on focus event.
+ * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue}
+ * method is by making sure the input is placed inside a form that has a `name` attribute. This is
+ * important because `form` controllers are published to the related scope under the name in their
+ * `name` attribute.
*
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
- * focus. ({@link guide/expression#-event- Event object is available as `$event`})
+ * Any pending changes will take place immediately when an enclosing form is submitted via the
+ * `submit` event. Note that `ngClick` events will occur before the model is updated. Use `ngSubmit`
+ * to have access to the updated model.
*
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
- /**
- * @ngdoc directive
- * @name ngBlur
+ * ### Overriding immediate updates
*
- * @description
- * Specify custom behavior on blur event.
+ * The following example shows how to override immediate updates. Changes on the inputs within the
+ * form will update the model only when the control loses focus (blur event). If `escape` key is
+ * pressed while the input field is focused, the value is reset to the value in the current model.
*
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngBlur {@link guide/expression Expression} to evaluate upon
- * blur. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ *
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say', data: '' };
+ *
+ * $scope.cancel = function(e) {
+ * if (e.keyCode === 27) {
+ * $scope.userForm.userName.$rollbackViewValue();
+ * }
+ * };
+ * }]);
+ *
+ *
+ * var model = element(by.binding('user.name'));
+ * var input = element(by.model('user.name'));
+ * var other = element(by.model('user.data'));
+ *
+ * it('should allow custom events', function() {
+ * input.sendKeys(' hello');
+ * input.click();
+ * expect(model.getText()).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say hello');
+ * });
*
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
- /**
- * @ngdoc directive
- * @name ngCopy
+ * it('should $rollbackViewValue when model changes', function() {
+ * input.sendKeys(' hello');
+ * expect(input.getAttribute('value')).toEqual('say hello');
+ * input.sendKeys(protractor.Key.ESCAPE);
+ * expect(input.getAttribute('value')).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say');
+ * });
+ *
+ *
*
- * @description
- * Specify custom behavior on copy event.
+ * ### Debouncing updates
*
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon
- * copy. ({@link guide/expression#-event- Event object is available as `$event`})
+ * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
+ * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
+ *
+ *
+ *
+ *
+ *
+ * Name:
+ *
+ * Clear
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say' };
+ * }]);
+ *
+ *
+ *
+ *
+ * ## Model updates and validation
+ *
+ * The default behaviour in `ngModel` is that the model value is set to `undefined` when the
+ * validation determines that the value is invalid. By setting the `allowInvalid` property to true,
+ * the model will still be updated even if the value is invalid.
+ *
+ *
+ * ## Connecting to the scope
+ *
+ * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
+ * on the scope refers to a "getter/setter" function rather than the value itself.
+ *
+ * The following example shows how to bind to getter/setters:
+ *
+ *
+ *
+ *
+ *
+ *
+ * Name:
+ *
+ *
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('getterSetterExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * var _name = 'Brian';
+ * $scope.user = {
+ * name: function(newName) {
+ * return angular.isDefined(newName) ? (_name = newName) : _name;
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
+ * ## Specifying timezones
+ *
+ * You can specify the timezone that date/time input directives expect by providing its name in the
+ * `timezone` property.
+ *
+ *
+ * ## Programmatically changing options
+ *
+ * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
+ * watched for changes. However, it is possible to override the options on a single
+ * {@link ngModel.NgModelController} instance with
+ * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}.
+ *
+ *
+ * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
+ * and its descendents. Valid keys are:
+ * - `updateOn`: string specifying which event should the input be bound to. You can set several
+ * events using an space delimited list. There is a special event called `default` that
+ * matches the default events belonging to the control. These are the events that are bound to
+ * the control, and when fired, update the `$viewValue` via `$setViewValue`.
+ *
+ * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event,
+ * since different control types use different default events.
+ *
+ * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates
+ * Triggering and debouncing model updates}.
+ *
+ * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
+ * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
+ * custom value for each event. For example:
+ * ```
+ * ng-model-options="{
+ * updateOn: 'default blur click',
+ * debounce: { 'default': 500, 'blur': 0 }
+ * }"
+ * ```
+ *
+ * "default" also applies to all events that are listed in `updateOn` but are not
+ * listed in `debounce`, i.e. "click" would also be debounced by 500 milliseconds.
+ *
+ * - `allowInvalid`: boolean value which indicates that the model can be set with values that did
+ * not validate correctly instead of the default behavior of setting the model to undefined.
+ * - `getterSetter`: boolean value which determines whether or not to treat functions bound to
+ * `ngModel` as getters/setters.
+ * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
+ * `
`, `
`, ... . It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
*
- * @example
-
-
-
- copied: {{copied}}
-
-
*/
+ var ngModelOptionsDirective = function() {
+ NgModelOptionsController.$inject = ['$attrs', '$scope'];
+ function NgModelOptionsController($attrs, $scope) {
+ this.$$attrs = $attrs;
+ this.$$scope = $scope;
+ }
+ NgModelOptionsController.prototype = {
+ $onInit: function() {
+ var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions;
+ var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions);
+
+ this.$options = parentOptions.createChild(modelOptionsDefinition);
+ }
+ };
+
+ return {
+ restrict: 'A',
+ // ngModelOptions needs to run before ngModel and input directives
+ priority: 10,
+ require: {parentCtrl: '?^^ngModelOptions'},
+ bindToController: true,
+ controller: NgModelOptionsController
+ };
+ };
+
+
+// shallow copy over values from `src` that are not already specified on `dst`
+ function defaults(dst, src) {
+ forEach(src, function(value, key) {
+ if (!isDefined(dst[key])) {
+ dst[key] = value;
+ }
+ });
+ }
/**
* @ngdoc directive
- * @name ngCut
+ * @name ngNonBindable
+ * @restrict AC
+ * @priority 1000
+ * @element ANY
*
* @description
- * Specify custom behavior on cut event.
- *
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon
- * cut. ({@link guide/expression#-event- Event object is available as `$event`})
+ * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current
+ * DOM element, including directives on the element itself that have a lower priority than
+ * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives
+ * and bindings but which should be ignored by AngularJS. This could be the case if you have a site
+ * that displays snippets of code, for instance.
*
* @example
-
+ * In this example there are two locations where a simple interpolation binding (`{{}}`) is present,
+ * but the one wrapped in `ngNonBindable` is left alone.
+ *
+
-
- cut: {{cut}}
+ Normal: {{1 + 2}}
+ Ignored: {{1 + 2}}
+
+
+ it('should check ng-non-bindable', function() {
+ expect(element(by.binding('1 + 2')).getText()).toContain('3');
+ expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
+ });
*/
+ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+
+ /* exported ngOptionsDirective */
+
+ /* global jqLiteRemove */
+
+ var ngOptionsMinErr = minErr('ngOptions');
/**
* @ngdoc directive
- * @name ngPaste
+ * @name ngOptions
+ * @restrict A
*
* @description
- * Specify custom behavior on paste event.
*
- * @element window, input, select, textarea, a
- * @priority 0
- * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon
- * paste. ({@link guide/expression#-event- Event object is available as `$event`})
+ * The `ngOptions` attribute can be used to dynamically generate a list of ``
+ * elements for the `` element using the array or object obtained by evaluating the
+ * `ngOptions` comprehension expression.
*
- * @example
-
-
-
- pasted: {{paste}}
-
-
- */
-
- /**
- * @ngdoc directive
- * @name ngIf
- * @restrict A
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the ``'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
*
- * @description
- * The `ngIf` directive removes or recreates a portion of the DOM tree based on an
- * {expression}. If the expression assigned to `ngIf` evaluates to a false
- * value then the element is removed from the DOM, otherwise a clone of the
- * element is reinserted into the DOM.
+ * When an item in the `` menu is selected, the array element or object property
+ * represented by the selected option will be bound to the model identified by the `ngModel`
+ * directive.
*
- * `ngIf` differs from `ngShow` and `ngHide` in that `ngIf` completely removes and recreates the
- * element in the DOM rather than changing its visibility via the `display` css property. A common
- * case when this difference is significant is when using css selectors that rely on an element's
- * position within the DOM, such as the `:first-child` or `:last-child` pseudo-classes.
+ * Optionally, a single hard-coded `` element, with the value set to an empty string, can
+ * be nested into the `` element. This element will then represent the `null` or "not selected"
+ * option. See example below for demonstration.
*
- * Note that when an element is removed using `ngIf` its scope is destroyed and a new scope
- * is created when the element is restored. The scope created within `ngIf` inherits from
- * its parent scope using
- * [prototypal inheritance](https://github.com/angular/angular.js/wiki/The-Nuances-of-Scope-Prototypal-Inheritance).
- * An important implication of this is if `ngModel` is used within `ngIf` to bind to
- * a javascript primitive defined in the parent scope. In this case any modifications made to the
- * variable within the child scope will override (hide) the value in the parent scope.
+ * ## Complex Models (objects or collections)
*
- * Also, `ngIf` recreates elements using their compiled state. An example of this behavior
- * is if an element's class attribute is directly modified after it's compiled, using something like
- * jQuery's `.addClass()` method, and the element is later removed. When `ngIf` recreates the element
- * the added class will be lost because the original compiled state is used to regenerate the element.
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding the select to a model that is an object or a collection.
*
- * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
- * and `leave` effects.
+ * One issue occurs if you want to preselect an option. For example, if you set
+ * the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection,
+ * because the objects are not identical. So by default, you should always reference the item in your collection
+ * for preselections, e.g.: `$scope.selected = $scope.collection[3]`.
*
- * @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
+ * Another solution is to use a `track by` clause, because then `ngOptions` will track the identity
+ * of the item not by reference, but by the result of the `track by` expression. For example, if your
+ * collection items have an id property, you would `track by item.id`.
*
- * @element ANY
- * @scope
- * @priority 600
- * @param {expression} ngIf If the {@link guide/expression expression} is falsy then
- * the element is removed from the DOM tree. If it is truthy a copy of the compiled
- * element is added to the DOM tree.
+ * A different issue with objects or collections is that ngModel won't detect if an object property or
+ * a collection item changes. For that reason, `ngOptions` additionally watches the model using
+ * `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute.
+ * This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection
+ * has not changed identity, but only a property on the object or an item in the collection changes.
*
- * @example
-
-
- Click me:
- Show when checked:
-
- I'm removed when the checkbox is unchecked.
-
-
-
- .animate-if {
- background:white;
- border:1px solid black;
- padding:10px;
- }
-
- .animate-if.ng-enter, .animate-if.ng-leave {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- }
-
- .animate-if.ng-enter,
- .animate-if.ng-leave.ng-leave-active {
- opacity:0;
- }
-
- .animate-if.ng-leave,
- .animate-if.ng-enter.ng-enter-active {
- opacity:1;
- }
-
-
- */
- var ngIfDirective = ['$animate', function($animate) {
- return {
- transclude: 'element',
- priority: 600,
- terminal: true,
- restrict: 'A',
- $$tlb: true,
- link: function ($scope, $element, $attr, ctrl, $transclude) {
- var block, childScope, previousElements;
- $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
-
- if (toBoolean(value)) {
- if (!childScope) {
- childScope = $scope.$new();
- $transclude(childScope, function (clone) {
- clone[clone.length++] = document.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 it's template arrives.
- block = {
- clone: clone
- };
- $animate.enter(clone, $element.parent(), $element);
- });
- }
- } else {
- if(previousElements) {
- previousElements.remove();
- previousElements = null;
- }
- if(childScope) {
- childScope.$destroy();
- childScope = null;
- }
- if(block) {
- previousElements = getBlockElements(block.clone);
- $animate.leave(previousElements, function() {
- previousElements = null;
- });
- block = null;
- }
- }
- });
- }
- };
- }];
-
- /**
- * @ngdoc directive
- * @name ngInclude
- * @restrict ECA
+ * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
+ * if the model is an array). This means that changing a property deeper than the first level inside the
+ * object/collection will not trigger a re-rendering.
*
- * @description
- * Fetches, compiles and includes an external HTML fragment.
+ * ## `select` **`as`**
*
- * By default, the template URL is restricted to the same domain and protocol as the
- * application document. This is done by calling {@link ng.$sce#getTrustedResourceUrl
- * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
- * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
- * [wrap them](ng.$sce#trustAsResourceUrl) as trusted values. Refer to Angular's {@link
- * ng.$sce Strict Contextual Escaping}.
+ * Using `select` **`as`** will bind the result of the `select` expression to the model, but
+ * the value of the `` and `` html elements will be either the index (for array data sources)
+ * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
+ * is used, the result of that expression will be set as the value of the `option` and `select` elements.
*
- * In addition, the browser's
- * [Same Origin Policy](https://code.google.com/p/browsersec/wiki/Part2#Same-origin_policy_for_XMLHttpRequest)
- * and [Cross-Origin Resource Sharing (CORS)](http://www.w3.org/TR/cors/)
- * policy may further restrict whether the template is successfully loaded.
- * For example, `ngInclude` won't work for cross-domain requests on all browsers and for `file://`
- * 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.
+ * ### `select` **`as`** and **`track by`**
*
- * The enter and leave animation occur concurrently.
+ *
+ * Be careful when using `select` **`as`** and **`track by`** in the same expression.
+ *
*
- * @scope
- * @priority 400
+ * Given this array of items on the $scope:
*
- * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
- * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
- * @param {string=} onload Expression to evaluate when a new partial is loaded.
+ * ```js
+ * $scope.items = [{
+ * id: 1,
+ * label: 'aLabel',
+ * subItem: { name: 'aSubItem' }
+ * }, {
+ * id: 2,
+ * label: 'bLabel',
+ * subItem: { name: 'bSubItem' }
+ * }];
+ * ```
*
- * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
- * $anchorScroll} to scroll the viewport after the content is loaded.
+ * This will work:
*
- * - If the attribute is not set, disable scrolling.
- * - If the attribute is set without value, enable scrolling.
- * - Otherwise enable scrolling only if the expression evaluates to truthy value.
+ * ```html
+ *
+ * ```
+ * ```js
+ * $scope.selected = $scope.items[0];
+ * ```
+ *
+ * but this will not work:
+ *
+ * ```html
+ *
+ * ```
+ * ```js
+ * $scope.selected = $scope.items[0].subItem;
+ * ```
+ *
+ * In both examples, the **`track by`** expression is applied successfully to each `item` in the
+ * `items` array. Because the selected option has been set programmatically in the controller, the
+ * **`track by`** expression is also applied to the `ngModel` value. In the first example, the
+ * `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with
+ * no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`**
+ * expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
+ * is not matched against any ` ` and the `` appears as having no selected value.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {comprehension_expression} ngOptions in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
+ * (for including a filter with `track by`)
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`disable when`** `disable`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent ``
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the ``
+ * DOM element.
+ * * `disable`: The result of this expression will be used to disable the rendered ``
+ * element. Return `true` to disable.
+ * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
+ * used to identify the objects in the array. The `trackexpr` will most likely refer to the
+ * `value` variable (e.g. `value.propertyName`). With this the selection is preserved
+ * even when the options are recreated (e.g. reloaded from the server).
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} required The control is considered valid only if value is entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
* @example
-
+
-
-
- (blank)
-
- url of the template:
{{template.url}}
+
+
-
-
- function Ctrl($scope) {
- $scope.templates =
- [ { name: 'template1.html', url: 'template1.html'},
- { name: 'template2.html', url: 'template2.html'} ];
- $scope.template = $scope.templates[0];
- }
-
-
- Content of template1.html
-
-
- Content of template2.html
-
-
- .slide-animate-container {
- position:relative;
- background:white;
- border:1px solid black;
- height:40px;
- overflow:hidden;
- }
+ Color (null not allowed):
+
+
+ Color (null allowed):
+
+
+ -- choose color --
+
+
- .slide-animate {
- padding:10px;
- }
+ Color grouped by shade:
+
+
+
- .slide-animate.ng-enter, .slide-animate.ng-leave {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
- transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ Color grouped by shade, with some disabled:
+
+
+
- position:absolute;
- top:0;
- left:0;
- right:0;
- bottom:0;
- display:block;
- padding:10px;
- }
- .slide-animate.ng-enter {
- top:-50px;
- }
- .slide-animate.ng-enter.ng-enter-active {
- top:0;
- }
- .slide-animate.ng-leave {
- top:0;
- }
- .slide-animate.ng-leave.ng-leave-active {
- top:50px;
- }
+ Select bogus .
+
+
+ Currently selected: {{ {selected_color:myColor} }}
+
+
+
- var templateSelect = element(by.model('template'));
- var includeElem = element(by.css('[ng-include]'));
-
- it('should load template1.html', function() {
- expect(includeElem.getText()).toMatch(/Content of template1.html/);
- });
-
- it('should load template2.html', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- // See https://github.com/angular/protractor/issues/480
- return;
- }
- templateSelect.click();
- templateSelect.element.all(by.css('option')).get(2).click();
- expect(includeElem.getText()).toMatch(/Content of template2.html/);
- });
-
- it('should change to blank', function() {
- if (browser.params.browser == 'firefox') {
- // Firefox can't handle using selects
- return;
- }
- templateSelect.click();
- templateSelect.element.all(by.css('option')).get(0).click();
- expect(includeElem.isPresent()).toBe(false);
- });
+ it('should check ng-options', function() {
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
+ element.all(by.model('myColor')).first().click();
+ element.all(by.css('select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
+ element(by.css('.nullable select[ng-model="myColor"]')).click();
+ element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
+ });
*/
+ /* eslint-disable max-len */
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999
+ var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+ // 1: value expression (valueFn)
+ // 2: label expression (displayFn)
+ // 3: group by expression (groupByFn)
+ // 4: disable when expression (disableWhenFn)
+ // 5: array item variable name
+ // 6: object item key variable name
+ // 7: object item value variable name
+ // 8: collection expression
+ // 9: track by expression
+ /* eslint-enable */
+
+
+ var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
+
+ function parseOptionsExpression(optionsExp, selectElement, scope) {
+
+ var match = optionsExp.match(NG_OPTIONS_REGEXP);
+ if (!(match)) {
+ throw ngOptionsMinErr('iexp',
+ 'Expected expression in form of ' +
+ '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' +
+ ' but got \'{0}\'. Element: {1}',
+ optionsExp, startingTag(selectElement));
+ }
- /**
- * @ngdoc event
- * @name ngInclude#$includeContentRequested
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted every time the ngInclude content is requested.
- */
+ // Extract the parts from the ngOptions expression
+
+ // The variable name for the value of the item in the collection
+ var valueName = match[5] || match[7];
+ // The variable name for the key of the item in the collection
+ var keyName = match[6];
+
+ // An expression that generates the viewValue for an option if there is a label expression
+ var selectAs = / as /.test(match[0]) && match[1];
+ // An expression that is used to track the id of each object in the options collection
+ var trackBy = match[9];
+ // An expression that generates the viewValue for an option if there is no label expression
+ var valueFn = $parse(match[2] ? match[1] : valueName);
+ var selectAsFn = selectAs && $parse(selectAs);
+ var viewValueFn = selectAsFn || valueFn;
+ var trackByFn = trackBy && $parse(trackBy);
+
+ // Get the value by which we are going to track the option
+ // if we have a trackFn then use that (passing scope and locals)
+ // otherwise just hash the given viewValue
+ var getTrackByValueFn = trackBy ?
+ function(value, locals) { return trackByFn(scope, locals); } :
+ function getHashOfValue(value) { return hashKey(value); };
+ var getTrackByValue = function(value, key) {
+ return getTrackByValueFn(value, getLocals(value, key));
+ };
+ var displayFn = $parse(match[2] || match[1]);
+ var groupByFn = $parse(match[3] || '');
+ var disableWhenFn = $parse(match[4] || '');
+ var valuesFn = $parse(match[8]);
+
+ var locals = {};
+ var getLocals = keyName ? function(value, key) {
+ locals[keyName] = key;
+ locals[valueName] = value;
+ return locals;
+ } : function(value) {
+ locals[valueName] = value;
+ return locals;
+ };
- /**
- * @ngdoc event
- * @name ngInclude#$includeContentLoaded
- * @eventType emit on the current ngInclude scope
- * @description
- * Emitted every time the ngInclude content is reloaded.
- */
- var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce',
- function($http, $templateCache, $anchorScroll, $animate, $sce) {
- return {
- restrict: 'ECA',
- priority: 400,
- terminal: true,
- transclude: 'element',
- controller: angular.noop,
- compile: function(element, attr) {
- var srcExp = attr.ngInclude || attr.src,
- onloadExp = attr.onload || '',
- autoScrollExp = attr.autoscroll;
- return function(scope, $element, $attr, ctrl, $transclude) {
- var changeCounter = 0,
- currentScope,
- previousElement,
- currentElement;
+ function Option(selectValue, viewValue, label, group, disabled) {
+ this.selectValue = selectValue;
+ this.viewValue = viewValue;
+ this.label = label;
+ this.group = group;
+ this.disabled = disabled;
+ }
- var cleanupLastIncludeContent = function() {
- if(previousElement) {
- previousElement.remove();
- previousElement = null;
- }
- if(currentScope) {
- currentScope.$destroy();
- currentScope = null;
- }
- if(currentElement) {
- $animate.leave(currentElement, function() {
- previousElement = null;
- });
- previousElement = currentElement;
- currentElement = null;
- }
- };
+ function getOptionValuesKeys(optionValues) {
+ var optionValuesKeys;
- scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
- var afterAnimation = function() {
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
- }
- };
- var thisChangeId = ++changeCounter;
+ if (!keyName && isArrayLike(optionValues)) {
+ optionValuesKeys = optionValues;
+ } else {
+ // if object, extract keys, in enumeration order, unsorted
+ optionValuesKeys = [];
+ for (var itemKey in optionValues) {
+ if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
+ optionValuesKeys.push(itemKey);
+ }
+ }
+ }
+ return optionValuesKeys;
+ }
- if (src) {
- $http.get(src, {cache: $templateCache}).success(function(response) {
- if (thisChangeId !== changeCounter) return;
- var newScope = scope.$new();
- ctrl.template = response;
+ return {
+ trackBy: trackBy,
+ getTrackByValue: getTrackByValue,
+ getWatchables: $parse(valuesFn, function(optionValues) {
+ // Create a collection of things that we would like to watch (watchedArray)
+ // so that they can all be watched using a single $watchCollection
+ // that only runs the handler once if anything changes
+ var watchedArray = [];
+ optionValues = optionValues || [];
+
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+
+ 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
+ if (match[2] || match[1]) {
+ var label = displayFn(scope, locals);
+ watchedArray.push(label);
+ }
- // Note: This will also link all children of ng-include that were contained in the original
- // html. If that content contains controllers, ... they could pollute/change the scope.
- // However, using ng-include on an element with additional content does not make sense...
- // Note: We can't remove them in the cloneAttchFn of $transclude as that
- // function is called before linking the content, which would apply child
- // directives to non existing elements.
- var clone = $transclude(newScope, function(clone) {
- cleanupLastIncludeContent();
- $animate.enter(clone, null, $element, afterAnimation);
- });
+ // Only need to watch the disableWhenFn if there is a specific disable expression
+ if (match[4]) {
+ var disableWhen = disableWhenFn(scope, locals);
+ watchedArray.push(disableWhen);
+ }
+ }
+ return watchedArray;
+ }),
- currentScope = newScope;
- currentElement = clone;
+ getOptions: function() {
+
+ var optionItems = [];
+ var selectValueMap = {};
+
+ // The option values were already computed in the `getWatchables` fn,
+ // which must have been called to trigger `getOptions`
+ var optionValues = valuesFn(scope) || [];
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+ var locals = getLocals(value, key);
+ var viewValue = viewValueFn(scope, locals);
+ var selectValue = getTrackByValueFn(viewValue, locals);
+ var label = displayFn(scope, locals);
+ var group = groupByFn(scope, locals);
+ var disabled = disableWhenFn(scope, locals);
+ var optionItem = new Option(selectValue, viewValue, label, group, disabled);
+
+ optionItems.push(optionItem);
+ selectValueMap[selectValue] = optionItem;
+ }
- currentScope.$emit('$includeContentLoaded');
- scope.$eval(onloadExp);
- }).error(function() {
- if (thisChangeId === changeCounter) cleanupLastIncludeContent();
- });
- scope.$emit('$includeContentRequested');
- } else {
- cleanupLastIncludeContent();
- ctrl.template = null;
- }
- });
+ return {
+ items: optionItems,
+ selectValueMap: selectValueMap,
+ getOptionFromViewValue: function(value) {
+ return selectValueMap[getTrackByValue(value)];
+ },
+ getViewValueFromOption: function(option) {
+ // If the viewValue could be an object that may be mutated by the application,
+ // we need to make a copy and not return the reference to the value on the option.
+ return trackBy ? copy(option.viewValue) : option.viewValue;
+ }
};
}
};
- }];
+ }
-// This directive is called during the $transclude call of the first `ngInclude` directive.
-// It will replace and compile the content of the element with the loaded template.
-// We need this directive so that the element content is already filled when
-// the link function of another directive on the same element as ngInclude
-// is called.
- var ngIncludeFillContentDirective = ['$compile',
- function($compile) {
- return {
- restrict: 'ECA',
- priority: -400,
- require: 'ngInclude',
- link: function(scope, $element, $attr, ctrl) {
- $element.html(ctrl.template);
- $compile($element.contents())(scope);
+
+ // Support: IE 9 only
+ // We can't just jqLite('') since jqLite is not smart enough
+ // to create it in and IE barfs otherwise.
+ var optionTemplate = window.document.createElement('option'),
+ optGroupTemplate = window.document.createElement('optgroup');
+
+ function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
+
+ var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
+ var multiple = attr.multiple;
+
+ // The emptyOption allows the application developer to provide their own custom "empty"
+ // option when the viewValue does not match any of the option values.
+ for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
+ if (children[i].value === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = children.eq(i);
+ break;
}
+ }
+
+ // The empty option will be compiled and rendered before we first generate the options
+ selectElement.empty();
+
+ var providedEmptyOption = !!selectCtrl.emptyOption;
+
+ var unknownOption = jqLite(optionTemplate.cloneNode(false));
+ unknownOption.val('?');
+
+ 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();
+
+ // Overwrite the implementation. ngOptions doesn't use hashes
+ selectCtrl.generateUnknownOptionValue = function(val) {
+ return '?';
};
- }];
- /**
- * @ngdoc directive
- * @name ngInit
- * @restrict AC
- *
- * @description
- * The `ngInit` directive allows you to evaluate an expression in the
- * current scope.
- *
- *
- * The only appropriate use of `ngInit` is for aliasing special properties of
- * {@link ng.directive:ngRepeat `ngRepeat`}, as seen in the demo below. Besides this case, you
- * should use {@link guide/controller controllers} rather than `ngInit`
- * to initialize values on a scope.
- *
- *
- * **Note**: If you have assignment in `ngInit` along with {@link ng.$filter `$filter`}, make
- * sure you have parenthesis for correct precedence:
- *
- *
- *
- *
- *
- * @priority 450
- *
- * @element ANY
- * @param {expression} ngInit {@link guide/expression Expression} to eval.
- *
- * @example
-
-
-
-
-
-
- list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};
-
-
-
-
-
- it('should alias index positions', function() {
- var elements = element.all(by.css('.example-init'));
- expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
- expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
- expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
- expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
- });
-
-
- */
- var ngInitDirective = ngDirective({
- priority: 450,
- compile: function() {
- return {
- pre: function(scope, element, attrs) {
- scope.$eval(attrs.ngInit);
+ // Update the controller methods for multiple selectable options
+ if (!multiple) {
+
+ selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ var selectedOption = selectElement[0].options[selectElement[0].selectedIndex];
+ var option = options.getOptionFromViewValue(value);
+
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ if (selectedOption) selectedOption.removeAttribute('selected');
+
+ if (option) {
+ // Don't update the option when it is already selected.
+ // For example, the browser will select the first option by default. In that case,
+ // most properties are set automatically - except the `selected` attribute, which we
+ // set always
+
+ if (selectElement[0].value !== option.selectValue) {
+ selectCtrl.removeUnknownOption();
+
+ selectElement[0].value = option.selectValue;
+ option.element.selected = true;
+ }
+
+ option.element.setAttribute('selected', 'selected');
+ } else {
+ selectCtrl.selectUnknownOrEmptyOption(value);
+ }
+ };
+
+ selectCtrl.readValue = function readNgOptionsValue() {
+
+ var selectedOption = options.selectValueMap[selectElement.val()];
+
+ if (selectedOption && !selectedOption.disabled) {
+ selectCtrl.unselectEmptyOption();
+ selectCtrl.removeUnknownOption();
+ return options.getViewValueFromOption(selectedOption);
+ }
+ return null;
+ };
+
+ // If we are using `track by` then we must watch the tracked value on the model
+ // since ngModel only watches for object identity change
+ // FIXME: When a user selects an option, this watch will fire needlessly
+ if (ngOptions.trackBy) {
+ scope.$watch(
+ function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
+ function() { ngModelCtrl.$render(); }
+ );
+ }
+
+ } else {
+
+ selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ // Only set `.selected` if necessary, in order to prevent some browsers from
+ // scrolling to ` ` elements that are outside the `` element's viewport.
+ var selectedOptions = values && values.map(getAndUpdateSelectedOption) || [];
+
+ options.items.forEach(function(option) {
+ if (option.element.selected && !includes(selectedOptions, option)) {
+ option.element.selected = false;
+ }
+ });
+ };
+
+
+ selectCtrl.readValue = function readNgOptionsMultiple() {
+ var selectedValues = selectElement.val() || [],
+ selections = [];
+
+ forEach(selectedValues, function(value) {
+ var option = options.selectValueMap[value];
+ if (option && !option.disabled) selections.push(options.getViewValueFromOption(option));
+ });
+
+ return selections;
+ };
+
+ // If we are using `track by` then we must watch these tracked values on the model
+ // since ngModel only watches for object identity change
+ if (ngOptions.trackBy) {
+
+ scope.$watchCollection(function() {
+ if (isArray(ngModelCtrl.$viewValue)) {
+ return ngModelCtrl.$viewValue.map(function(value) {
+ return ngOptions.getTrackByValue(value);
+ });
+ }
+ }, function() {
+ ngModelCtrl.$render();
+ });
+
+ }
+ }
+
+ if (providedEmptyOption) {
+
+ // compile the element since there might be bindings in it
+ $compile(selectCtrl.emptyOption)(scope);
+
+ selectElement.prepend(selectCtrl.emptyOption);
+
+ if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
+ // This means the empty option has currently no actual DOM node, probably because
+ // it has been modified by a transclusion directive.
+ selectCtrl.hasEmptyOption = false;
+
+ // Redefine the registerOption function, which will catch
+ // options that are added by ngIf etc. (rendering of the node is async because of
+ // lazy transclusion)
+ selectCtrl.registerOption = function(optionScope, optionEl) {
+ if (optionEl.val() === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = optionEl;
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ // This ensures the new empty option is selected if previously no option was selected
+ ngModelCtrl.$render();
+
+ optionEl.on('$destroy', function() {
+ var needsRerender = selectCtrl.$isEmptyOptionSelected();
+
+ selectCtrl.hasEmptyOption = false;
+ selectCtrl.emptyOption = undefined;
+
+ if (needsRerender) ngModelCtrl.$render();
+ });
+ }
+ };
+
+ } else {
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ }
+
+ }
+
+ // We will re-render the option elements if the option values or labels change
+ scope.$watchCollection(ngOptions.getWatchables, updateOptions);
+
+ // ------------------------------------------------------------------ //
+
+ function addOptionElement(option, parent) {
+ var optionElement = optionTemplate.cloneNode(false);
+ parent.appendChild(optionElement);
+ updateOptionElement(option, optionElement);
+ }
+
+ function getAndUpdateSelectedOption(viewValue) {
+ var option = options.getOptionFromViewValue(viewValue);
+ var element = option && option.element;
+
+ if (element && !element.selected) element.selected = true;
+
+ return option;
+ }
+
+ function updateOptionElement(option, element) {
+ option.element = element;
+ element.disabled = option.disabled;
+ // Support: IE 11 only, Edge 12-13 only
+ // NOTE: The label must be set before the value, otherwise IE 11 & Edge create unresponsive
+ // selects in certain circumstances when multiple selects are next to each other and display
+ // the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
+ // See https://github.com/angular/angular.js/issues/11314 for more info.
+ // This is unfortunately untestable with unit / e2e tests
+ if (option.label !== element.label) {
+ element.label = option.label;
+ element.textContent = option.label;
+ }
+ element.value = option.selectValue;
+ }
+
+ function updateOptions() {
+ var previousValue = options && selectCtrl.readValue();
+
+ // We must remove all current options, but cannot simply set innerHTML = null
+ // since the providedEmptyOption might have an ngIf on it that inserts comments which we
+ // must preserve.
+ // Instead, iterate over the current option elements and remove them or their optgroup
+ // parents
+ if (options) {
+
+ for (var i = options.items.length - 1; i >= 0; i--) {
+ var option = options.items[i];
+ if (isDefined(option.group)) {
+ jqLiteRemove(option.element.parentNode);
+ } else {
+ jqLiteRemove(option.element);
+ }
+ }
+ }
+
+ options = ngOptions.getOptions();
+
+ var groupElementMap = {};
+
+ options.items.forEach(function addOption(option) {
+ var groupElement;
+
+ if (isDefined(option.group)) {
+
+ // This option is to live in a group
+ // See if we have already created this group
+ groupElement = groupElementMap[option.group];
+
+ if (!groupElement) {
+
+ groupElement = optGroupTemplate.cloneNode(false);
+ listFragment.appendChild(groupElement);
+
+ // Update the label on the group element
+ // "null" is special cased because of Safari
+ groupElement.label = option.group === null ? 'null' : option.group;
+
+ // Store it for use later
+ groupElementMap[option.group] = groupElement;
+ }
+
+ addOptionElement(option, groupElement);
+
+ } else {
+
+ // This option is not in a group
+ addOptionElement(option, listFragment);
+ }
+ });
+
+ 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();
+ var isNotPrimitive = ngOptions.trackBy || multiple;
+ if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
+ ngModelCtrl.$setViewValue(nextValue);
+ ngModelCtrl.$render();
+ }
}
- };
+ }
}
- });
- /**
- * @ngdoc directive
- * @name ngNonBindable
- * @restrict AC
- * @priority 1000
- *
- * @description
- * The `ngNonBindable` directive tells Angular not to compile or bind the contents of the current
- * DOM element. This is useful if the element contains what appears to be Angular directives and
- * bindings but which should be ignored by Angular. This could be the case if you have a site that
- * displays snippets of code, for instance.
- *
- * @element ANY
- *
- * @example
- * In this example there are two locations where a simple interpolation binding (`{{}}`) is present,
- * but the one wrapped in `ngNonBindable` is left alone.
- *
- * @example
-
-
- Normal: {{1 + 2}}
- Ignored: {{1 + 2}}
-
-
- it('should check ng-non-bindable', function() {
- expect(element(by.binding('1 + 2')).getText()).toContain('3');
- expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
- });
-
-
- */
- var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+ return {
+ restrict: 'A',
+ terminal: true,
+ require: ['select', 'ngModel'],
+ link: {
+ pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
+ // Deactivate the SelectController.register method to prevent
+ // option directives from accidentally registering themselves
+ // (and unwanted $destroy handlers etc.)
+ ctrls[0].registerOption = noop;
+ },
+ post: ngOptionsPostLink
+ }
+ };
+ }];
/**
* @ngdoc directive
@@ -19507,27 +31067,27 @@
* @description
* `ngPluralize` is a directive that displays messages according to en-US localization rules.
* These rules are bundled with angular.js, but can be overridden
- * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive
+ * (see {@link guide/i18n AngularJS i18n} dev guide). You configure ngPluralize directive
* by specifying the mappings between
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
* and the strings to be displayed.
*
- * # Plural categories and explicit number rules
+ * ## Plural categories and explicit number rules
* There are two
* [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
- * in Angular's default en-US locale: "one" and "other".
+ * in AngularJS's default en-US locale: "one" and "other".
*
* While a plural category may match many numbers (for example, in en-US locale, "other" can match
* any number that is not 1), an explicit number rule can only match one number. For example, the
* explicit number rule for "3" matches the number 3. There are examples of plural categories
* and explicit number rules throughout the rest of this documentation.
*
- * # Configuring ngPluralize
+ * ## Configuring ngPluralize
* You configure ngPluralize by providing 2 attributes: `count` and `when`.
* You can also provide an optional attribute, `offset`.
*
* The value of the `count` attribute can be either a string or an {@link guide/expression
- * Angular expression}; these are evaluated on the current scope for its bound value.
+ * AngularJS expression}; these are evaluated on the current scope for its bound value.
*
* The `when` attribute specifies the mappings between plural categories and the actual
* string to be displayed. The value of the attribute should be a JSON object.
@@ -19537,8 +31097,8 @@
* ```html
*
+ * 'one': '1 person is viewing.',
+ * 'other': '{} people are viewing.'}">
*
*```
*
@@ -19549,11 +31109,14 @@
* show "a dozen people are viewing".
*
* You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted
- * into pluralized strings. In the previous example, Angular will replace `{}` with
+ * into pluralized strings. In the previous example, AngularJS will replace `{}` with
* `{{personCount}}` . The closed braces `{}` is a placeholder
* for {{numberExpression}} .
*
- * # Configuring ngPluralize with offset
+ * If no rule is defined for a category, then an empty string is displayed and a warning is generated.
+ * Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
+ *
+ * ## Configuring ngPluralize with offset
* The `offset` attribute allows further customization of pluralized text, which can result in
* a better user experience. For example, instead of the message "4 people are viewing this document",
* you might display "John, Kate and 2 others are viewing this document".
@@ -19563,10 +31126,10 @@
* ```html
*
+ * '1': '{{person1}} is viewing.',
+ * '2': '{{person1}} and {{person2}} are viewing.',
+ * 'one': '{{person1}}, {{person2}} and one other person are viewing.',
+ * 'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
*
* ```
*
@@ -19574,8 +31137,8 @@
* three explicit number rules 0, 1 and 2.
* When one person, perhaps John, views the document, "John is viewing" will be shown.
* When three people view the document, no explicit number rule is found, so
- * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
- * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing"
+ * an offset of 2 is taken off 3, and AngularJS uses 1 to decide the plural category.
+ * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing"
* is shown.
*
* Note that when you specify offsets, you must provide explicit number rules for
@@ -19588,19 +31151,20 @@
* @param {number=} offset Offset to deduct from the total number.
*
* @example
-
+
-
- Person 1:
- Person 2:
- Number of People:
+
+
Person 1:
+
Person 2:
+
Number of People:
Without Offset:
@@ -19670,10 +31234,11 @@
*/
- var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
- var BRACE = /{}/g;
+ var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) {
+ var BRACE = /{}/g,
+ IS_WHEN = /^when(Minus)?(.+)$/;
+
return {
- restrict: 'EA',
link: function(scope, element, attr) {
var numberExp = attr.count,
whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs
@@ -19682,41 +31247,64 @@
whensExpFns = {},
startSymbol = $interpolate.startSymbol(),
endSymbol = $interpolate.endSymbol(),
- isWhen = /^when(Minus)?(.+)$/;
+ braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol,
+ watchRemover = angular.noop,
+ lastCount;
forEach(attr, function(expression, attributeName) {
- if (isWhen.test(attributeName)) {
- whens[lowercase(attributeName.replace('when', '').replace('Minus', '-'))] =
- element.attr(attr.$attr[attributeName]);
+ var tmpMatch = IS_WHEN.exec(attributeName);
+ if (tmpMatch) {
+ var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]);
+ whens[whenKey] = element.attr(attr.$attr[attributeName]);
}
});
forEach(whens, function(expression, key) {
- whensExpFns[key] =
- $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' +
- offset + endSymbol));
+ whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement));
+
});
- scope.$watch(function ngPluralizeWatch() {
- var value = parseFloat(scope.$eval(numberExp));
+ scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
+ var count = parseFloat(newVal);
+ var countIsNaN = isNumberNaN(count);
- if (!isNaN(value)) {
- //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise,
- //check it against pluralization rules in $locale service
- if (!(value in whens)) value = $locale.pluralCat(value - offset);
- return whensExpFns[value](scope, element, true);
- } else {
- return '';
+ if (!countIsNaN && !(count in whens)) {
+ // If an explicit number rule such as 1, 2, 3... is defined, just use it.
+ // Otherwise, check it against pluralization rules in $locale service.
+ count = $locale.pluralCat(count - offset);
+ }
+
+ // If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
+ // In JS `NaN !== NaN`, so we have to explicitly check.
+ if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) {
+ watchRemover();
+ var whenExpFn = whensExpFns[count];
+ if (isUndefined(whenExpFn)) {
+ if (newVal != null) {
+ $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp);
+ }
+ watchRemover = noop;
+ updateElementText();
+ } else {
+ watchRemover = scope.$watch(whenExpFn, updateElementText);
+ }
+ lastCount = count;
}
- }, function ngPluralizeWatchAction(newVal) {
- element.text(newVal);
});
+
+ function updateElementText(newText) {
+ element.text(newText || '');
+ }
}
};
}];
+ /* exported ngRepeatDirective */
+
/**
* @ngdoc directive
* @name ngRepeat
+ * @multiElement
+ * @restrict A
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -19734,10 +31322,200 @@
* | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). |
* | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). |
*
- * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
- * This may be useful when, for instance, nesting ngRepeats.
+ *
+ * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
+ * This may be useful when, for instance, nesting ngRepeats.
+ *
+ *
+ *
+ * ## Iterating over object properties
+ *
+ * It is possible to get `ngRepeat` to iterate over the properties of an object using the following
+ * syntax:
+ *
+ * ```js
+ *
...
+ * ```
+ *
+ * However, there are a few limitations compared to array iteration:
+ *
+ * - The JavaScript specification does not define the order of keys
+ * returned for an object, so AngularJS 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).
+ *
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by AngularJS for public (`$`) and private (`$$`) properties.
+ *
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw an error if used with one.
+ *
+ * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
+ * or implement a `$watch` on the object yourself.
+ *
+ *
+ * ## Tracking and Duplicates
+ *
+ * `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
+ * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM:
+ *
+ * * When an item is added, a new instance of the template is added to the DOM.
+ * * When an item is removed, its template instance is removed from the DOM.
+ * * When items are reordered, their respective templates are reordered in the DOM.
+ *
+ * To minimize creation of DOM elements, `ngRepeat` uses a function
+ * to "keep track" of all items in the collection and their corresponding DOM elements.
+ * For example, if an item is added to the collection, `ngRepeat` will know that all other items
+ * already have DOM elements, and will not re-render them.
+ *
+ * All different types of tracking functions, their syntax, and and their support for duplicate
+ * items in collections can be found in the
+ * {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
*
- * # Special repeat start and end points
+ *
+ * **Best Practice:** If you are working with objects that have a unique identifier property, you
+ * should track by this identifier instead of the object instance,
+ * e.g. `item in items track by item.id`.
+ * 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 significantly improves rendering performance.
+ *
+ *
+ * ### Effects of DOM Element re-use
+ *
+ * When DOM elements are re-used, ngRepeat updates the scope for the element, which will
+ * automatically update any active bindings on the template. However, other
+ * functionality will not be updated, because the element is not re-created:
+ *
+ * - Directives are not re-compiled
+ * - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not
+ * updated if they have stabilized.
+ *
+ * The above affects all kinds of element re-use due to tracking, but may be especially visible
+ * when tracking by `$index` due to the way ngRepeat re-uses elements.
+ *
+ * The following example shows the effects of different actions with tracking:
+
+
+
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ var friends = [
+ {name:'John', age:25},
+ {name:'Mary', age:40},
+ {name:'Peter', age:85}
+ ];
+
+ $scope.removeFirst = function() {
+ $scope.friends.shift();
+ };
+
+ $scope.updateAge = function() {
+ $scope.friends.forEach(function(el) {
+ el.age = el.age + 5;
+ });
+ };
+
+ $scope.copy = function() {
+ $scope.friends = angular.copy($scope.friends);
+ };
+
+ $scope.reset = function() {
+ $scope.friends = angular.copy(friends);
+ };
+
+ $scope.reset();
+ });
+
+
+
+
+ When you click "Update Age", only the first list updates the age, because all others have
+ a one-time binding on the age property. If you then click "Copy", the current friend list
+ is copied, and now the second list updates the age, because the identity of the collection items
+ has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
+ items are already known according to their tracking functions.
+
+ When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
+ due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
+ DOM element for the new first collection item, and so on. Since the age property is one-time
+ bound, the value remains from the collection item which was previously at this index.
+
+
+
+
Remove First
+
Update Age
+
Copy
+
Reset List
+
+
track by $id(friend)
(default):
+
+
+ {{friend.name}} is {{friend.age}} years old.
+
+
+
track by $id(friend)
(default), with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
track by friend.name
, with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
track by $index
, with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
+
+
+ .example-animate-container {
+ background:white;
+ border:1px solid black;
+ list-style:none;
+ margin:0;
+ padding:0 10px;
+ }
+
+ .animate-repeat {
+ line-height:30px;
+ list-style:none;
+ box-sizing:border-box;
+ }
+
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter,
+ .animate-repeat.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ .animate-repeat.ng-leave.ng-leave-active,
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter {
+ opacity:0;
+ max-height:0;
+ }
+
+ .animate-repeat.ng-leave,
+ .animate-repeat.ng-move.ng-move-active,
+ .animate-repeat.ng-enter.ng-enter-active {
+ opacity:1;
+ max-height:30px;
+ }
+
+
+
+ *
+ * ## Special repeat start and end points
* To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
* the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
* The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
@@ -19782,11 +31560,13 @@
* 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
+ * | 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 |
*
- * **.leave** - when an item is removed from the list or when an item is filtered out
- *
- * **.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
@@ -19804,54 +31584,91 @@
*
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
*
- * * `variable in expression track by tracking_expression` – You can also provide an optional tracking function
- * which can be used to associate the objects in the collection with the DOM elements. If no tracking function
- * is specified the ng-repeat associates elements by identity in the collection. It is an error to have
- * more than one tracking function to resolve to the same key. (This would mean that two distinct objects are
- * mapped to the same DOM element, which is not possible.) Filters should be applied to the expression,
- * before specifying a tracking expression.
+ * * `variable in expression track by tracking_expression` – You can also provide an optional tracking expression
+ * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression
+ * is specified, ng-repeat associates elements by identity. It is an error to have
+ * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
+ * mapped to the same DOM element, which is not possible.)
+ *
+ * *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`.
+ * This implies that the DOM elements will be associated by item identity in the collection.
+ *
+ * The built-in `$id()` function can be used to assign a unique
+ * `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements
+ * with the corresponding item in the collection by identity. Moving the same object would move
+ * the DOM element in the same way in the DOM.
+ * Note that the default id function does not support duplicate primitive values (`number`, `string`),
+ * but supports duplictae non-primitive values (`object`) that are *equal* in shape.
+ *
+ * *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking
+ * id, for example with a function, or using a property on the collection items.
+ * `item in items track by item.id` is a typical pattern when the items have a unique identifier,
+ * e.g. database id. In this case the object identity does not matter. Two objects are considered
+ * equivalent as long as their `id` property is same.
+ * Tracking by unique identifier is the most performant way and should be used whenever possible.
+ *
+ * *$index*: This special property tracks the collection items by their index, and
+ * re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can
+ * be used for a performance improvement if no unique identfier is available and the identity of
+ * the collection items cannot be easily computed. It also allows duplicates.
+ *
+ *
+ * Note: Re-using DOM elements can have unforeseen effects. Read the
+ * {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for
+ * more info.
+ *
+ *
+ *
+ * Note: the `track by` expression must come last - after any filters, and the alias expression:
+ * `item in items | filter:searchText as results track by item.id`
+ *
*
- * For example: `item in items` is equivalent to `item in items track by $id(item)'. This implies that the DOM elements
- * will be associated by item identity in the array.
+ * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
+ * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
+ * when a filter is active on the repeater, but the filtered result set is empty.
*
- * For example: `item in items track by $id(item)`. A built in `$id()` function can be used to assign a unique
- * `$$hashKey` property to each item in the array. This property is then used as a key to associated DOM elements
- * with the corresponding item in the array by identity. Moving the same object in array would move the DOM
- * element in the same way in the DOM.
+ * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
+ * the items have been processed through the filter.
*
- * For example: `item in items track by item.id` is a typical pattern when the items come from the database. In this
- * case the object identity does not matter. Two objects are considered equivalent as long as their `id`
- * property is same.
+ * Please note that `as [variable name] is not an operator but rather a part of ngRepeat
+ * micro-syntax so it can be used only after all filters (and not as operator, inside an expression).
*
- * For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
- * to items in conjunction with a tracking expression.
+ * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` .
*
* @example
- * This example initializes the scope to a list of names and
- * then uses `ngRepeat` to display every person:
-
+ * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
+ * results by name or by age. New (entering) and removed (leaving) items are animated.
+
-
+
+
+ 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'}
+ ];
+ });
+
.example-animate-container {
background:white;
@@ -19862,7 +31679,7 @@
}
.animate-repeat {
- line-height:40px;
+ line-height:30px;
list-style:none;
box-sizing:border-box;
}
@@ -19870,7 +31687,6 @@
.animate-repeat.ng-move,
.animate-repeat.ng-enter,
.animate-repeat.ng-leave {
- -webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
}
@@ -19885,7 +31701,7 @@
.animate-repeat.ng-move.ng-move-active,
.animate-repeat.ng-enter.ng-enter-active {
opacity:1;
- max-height:40px;
+ max-height:30px;
}
@@ -19912,39 +31728,74 @@
*/
- 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');
+
+ var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
+ // TODO(perf): generate setters to shave off ~40ms or 1-1.5%
+ scope[valueIdentifier] = value;
+ if (keyIdentifier) scope[keyIdentifier] = key;
+ scope.$index = index;
+ scope.$first = (index === 0);
+ scope.$last = (index === (arrayLength - 1));
+ scope.$middle = !(scope.$first || scope.$last);
+ // eslint-disable-next-line no-bitwise
+ scope.$odd = !(scope.$even = (index & 1) === 0);
+ };
+
+ var getBlockStart = function(block) {
+ return block.clone[0];
+ };
+
+ var getBlockEnd = function(block) {
+ return block.clone[block.clone.length - 1];
+ };
+
+
return {
+ restrict: 'A',
+ multiElement: true,
transclude: 'element',
priority: 1000,
terminal: true,
$$tlb: true,
- link: function($scope, $element, $attr, ctrl, $transclude){
+ compile: function ngRepeatCompile($element, $attr) {
var expression = $attr.ngRepeat;
- var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
- trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
- lhs, rhs, valueIdentifier, keyIdentifier,
- hashFnLocals = {$id: hashKey};
+ var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
+
+ var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
if (!match) {
- throw ngRepeatMinErr('iexp', "Expected expression in form of '_item_ in _collection_[ track by _id_]' but got '{0}'.",
+ throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
expression);
}
- lhs = match[1];
- rhs = match[2];
- trackByExp = match[3];
+ var lhs = match[1];
+ var rhs = match[2];
+ var aliasAs = match[3];
+ var trackByExp = match[4];
+
+ match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);
+
+ if (!match) {
+ throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
+ lhs);
+ }
+ var valueIdentifier = match[3] || match[1];
+ var keyIdentifier = match[2];
+
+ if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
+ /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
+ throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
+ aliasAs);
+ }
+
+ var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
+ var hashFnLocals = {$id: hashKey};
if (trackByExp) {
trackByExpGetter = $parse(trackByExp);
- trackByIdExpFn = function(key, value, index) {
- // assign key, value, and $index to the locals so that they can be used in hash functions
- if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
- hashFnLocals[valueIdentifier] = value;
- hashFnLocals.$index = index;
- return trackByExpGetter($scope, hashFnLocals);
- };
} else {
trackByIdArrayFn = function(key, value) {
return hashKey(value);
@@ -19954,171 +31805,172 @@
};
}
- match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
- if (!match) {
- throw ngRepeatMinErr('iidexp', "'_item_' in '_item_ in _collection_' should be an identifier or '(_key_, _value_)' expression, but got '{0}'.",
- lhs);
- }
- valueIdentifier = match[3] || match[1];
- keyIdentifier = match[2];
-
- // Store a list of elements from previous run. This is a hash where key is the item from the
- // iterator, and the value is objects with following properties.
- // - scope: bound scope
- // - element: previous element.
- // - index: position
- var lastBlockMap = {};
-
- //watch props
- $scope.$watchCollection(rhs, function ngRepeatAction(collection){
- var index, length,
- previousNode = $element[0], // current position of the node
- nextNode,
- // Same as lastBlockMap but it has the current state. It will become the
- // lastBlockMap on the next iteration.
- nextBlockMap = {},
- arrayLength,
- childScope,
- key, value, // key/value of iteration
- trackById,
- trackByIdFn,
- collectionKeys,
- block, // last object information {scope, element, id}
- nextBlockOrder = [],
- elementsToRemove;
-
-
- if (isArrayLike(collection)) {
- collectionKeys = collection;
- trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
- } else {
- trackByIdFn = trackByIdExpFn || trackByIdObjFn;
- // if object, extract keys, sort them and use to determine order of iteration over obj props
- collectionKeys = [];
- for (key in collection) {
- if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
- collectionKeys.push(key);
- }
+ return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
+
+ if (trackByExpGetter) {
+ trackByIdExpFn = function(key, value, index) {
+ // assign key, value, and $index to the locals so that they can be used in hash functions
+ if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
+ hashFnLocals[valueIdentifier] = value;
+ hashFnLocals.$index = index;
+ return trackByExpGetter($scope, hashFnLocals);
+ };
+ }
+
+ // Store a list of elements from previous run. This is a hash where key is the item from the
+ // iterator, and the value is objects with following properties.
+ // - scope: bound scope
+ // - clone: previous element.
+ // - index: position
+ //
+ // We are using no-proto object so that we don't need to guard against inherited props via
+ // hasOwnProperty.
+ var lastBlockMap = createMap();
+
+ //watch props
+ $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
+ var index, length,
+ previousNode = $element[0], // node that cloned nodes should be inserted after
+ // initialized to the comment node anchor
+ nextNode,
+ // Same as lastBlockMap but it has the current state. It will become the
+ // lastBlockMap on the next iteration.
+ nextBlockMap = createMap(),
+ collectionLength,
+ key, value, // key/value of iteration
+ trackById,
+ trackByIdFn,
+ collectionKeys,
+ block, // last object information {scope, element, id}
+ nextBlockOrder,
+ elementsToRemove;
+
+ if (aliasAs) {
+ $scope[aliasAs] = collection;
}
- collectionKeys.sort();
- }
-
- arrayLength = collectionKeys.length;
-
- // locate existing items
- length = nextBlockOrder.length = collectionKeys.length;
- for(index = 0; index < length; index++) {
- key = (collection === collectionKeys) ? index : collectionKeys[index];
- value = collection[key];
- trackById = trackByIdFn(key, value, index);
- assertNotHasOwnProperty(trackById, '`track by` id');
- if(lastBlockMap.hasOwnProperty(trackById)) {
- block = lastBlockMap[trackById];
- delete lastBlockMap[trackById];
- nextBlockMap[trackById] = block;
- nextBlockOrder[index] = block;
- } else if (nextBlockMap.hasOwnProperty(trackById)) {
- // restore lastBlockMap
- forEach(nextBlockOrder, function(block) {
- if (block && block.scope) lastBlockMap[block.id] = block;
- });
- // This is a duplicate and we need to throw an error
- throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}",
- expression, trackById);
+
+ if (isArrayLike(collection)) {
+ collectionKeys = collection;
+ trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
} else {
- // new never before seen block
- nextBlockOrder[index] = { id: trackById };
- nextBlockMap[trackById] = false;
+ trackByIdFn = trackByIdExpFn || trackByIdObjFn;
+ // if object, extract keys, in enumeration order, unsorted
+ collectionKeys = [];
+ for (var itemKey in collection) {
+ if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
+ collectionKeys.push(itemKey);
+ }
+ }
}
- }
- // remove existing items
- for (key in lastBlockMap) {
- // lastBlockMap is our own object so we don't need to use special hasOwnPropertyFn
- if (lastBlockMap.hasOwnProperty(key)) {
- block = lastBlockMap[key];
- elementsToRemove = getBlockElements(block.clone);
+ collectionLength = collectionKeys.length;
+ nextBlockOrder = new Array(collectionLength);
+
+ // locate existing items
+ for (index = 0; index < collectionLength; index++) {
+ key = (collection === collectionKeys) ? index : collectionKeys[index];
+ value = collection[key];
+ trackById = trackByIdFn(key, value, index);
+ if (lastBlockMap[trackById]) {
+ // found previously seen block
+ block = lastBlockMap[trackById];
+ delete lastBlockMap[trackById];
+ nextBlockMap[trackById] = block;
+ nextBlockOrder[index] = block;
+ } else if (nextBlockMap[trackById]) {
+ // if collision detected. restore lastBlockMap and throw an error
+ forEach(nextBlockOrder, function(block) {
+ if (block && block.scope) lastBlockMap[block.id] = block;
+ });
+ throw ngRepeatMinErr('dupes',
+ 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
+ expression, trackById, value);
+ } else {
+ // new never before seen block
+ nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
+ nextBlockMap[trackById] = true;
+ }
+ }
+
+ // remove leftover items
+ for (var blockKey in lastBlockMap) {
+ block = lastBlockMap[blockKey];
+ elementsToRemove = getBlockNodes(block.clone);
$animate.leave(elementsToRemove);
- forEach(elementsToRemove, function(element) { element[NG_REMOVED] = true; });
+ if (elementsToRemove[0].parentNode) {
+ // if the element was not removed yet because of pending animation, mark it as deleted
+ // so that we can ignore it later
+ for (index = 0, length = elementsToRemove.length; index < length; index++) {
+ elementsToRemove[index][NG_REMOVED] = true;
+ }
+ }
block.scope.$destroy();
}
- }
- // we are not using forEach for perf reasons (trying to avoid #call)
- for (index = 0, length = collectionKeys.length; index < length; index++) {
- key = (collection === collectionKeys) ? index : collectionKeys[index];
- value = collection[key];
- block = nextBlockOrder[index];
- if (nextBlockOrder[index - 1]) previousNode = getBlockEnd(nextBlockOrder[index - 1]);
+ // we are not using forEach for perf reasons (trying to avoid #call)
+ for (index = 0; index < collectionLength; index++) {
+ key = (collection === collectionKeys) ? index : collectionKeys[index];
+ value = collection[key];
+ block = nextBlockOrder[index];
- if (block.scope) {
- // if we have already seen this object, then we need to reuse the
- // associated scope/element
- childScope = block.scope;
+ if (block.scope) {
+ // if we have already seen this object, then we need to reuse the
+ // associated scope/element
- nextNode = previousNode;
- do {
- nextNode = nextNode.nextSibling;
- } while(nextNode && nextNode[NG_REMOVED]);
+ nextNode = previousNode;
- if (getBlockStart(block) != nextNode) {
- // existing item which got moved
- $animate.move(getBlockElements(block.clone), null, jqLite(previousNode));
- }
- previousNode = getBlockEnd(block);
- } else {
- // new item which we don't know about
- childScope = $scope.$new();
- }
+ // skip nodes that are already pending removal via leave animation
+ do {
+ nextNode = nextNode.nextSibling;
+ } while (nextNode && nextNode[NG_REMOVED]);
- childScope[valueIdentifier] = value;
- if (keyIdentifier) childScope[keyIdentifier] = key;
- childScope.$index = index;
- childScope.$first = (index === 0);
- childScope.$last = (index === (arrayLength - 1));
- childScope.$middle = !(childScope.$first || childScope.$last);
- // jshint bitwise: false
- childScope.$odd = !(childScope.$even = (index&1) === 0);
- // jshint bitwise: true
-
- if (!block.scope) {
- $transclude(childScope, function(clone) {
- clone[clone.length++] = document.createComment(' end ngRepeat: ' + expression + ' ');
- $animate.enter(clone, null, jqLite(previousNode));
- previousNode = clone;
- block.scope = childScope;
- // 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 it's template arrives.
- block.clone = clone;
- nextBlockMap[block.id] = block;
- });
+ if (getBlockStart(block) !== nextNode) {
+ // existing item which got moved
+ $animate.move(getBlockNodes(block.clone), null, previousNode);
+ }
+ previousNode = getBlockEnd(block);
+ updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
+ } else {
+ // new item which we don't know about
+ $transclude(function ngRepeatTransclude(clone, scope) {
+ block.scope = scope;
+ // http://jsperf.com/clone-vs-createcomment
+ var endNode = ngRepeatEndComment.cloneNode(false);
+ clone[clone.length++] = endNode;
+
+ $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
+ // by a directive with templateUrl when its template arrives.
+ block.clone = clone;
+ nextBlockMap[block.id] = block;
+ updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
+ });
+ }
}
- }
- lastBlockMap = nextBlockMap;
- });
+ lastBlockMap = nextBlockMap;
+ });
+ };
}
};
-
- function getBlockStart(block) {
- return block.clone[0];
- }
-
- function getBlockEnd(block) {
- return block.clone[block.clone.length - 1];
- }
}];
+ var NG_HIDE_CLASS = 'ng-hide';
+ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
/**
* @ngdoc directive
* @name ngShow
+ * @multiElement
*
* @description
- * The `ngShow` directive shows or hides the given HTML element based on the expression
- * provided to the ngShow attribute. The element is shown or hidden by removing or adding
- * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngShow` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngShow` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
*
@@ -20128,59 +31980,58 @@
*
* ```
*
- * When the ngShow expression evaluates to false then the ng-hide CSS class is added to the class attribute
- * on the element causing it to become hidden. When true, the ng-hide CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
- * ### Overriding .ng-hide
+ * ### Overriding `.ng-hide`
+ *
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
- * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by
- * restating the styles for the .ng-hide class in CSS:
* ```css
- * .ng-hide {
- * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default...
- * display:block!important;
- *
- * //this is just another form of hiding an element
- * position:absolute;
- * top:-9999px;
- * left:-9999px;
- * }
+ * .ng-hide:not(.ng-hide-animate) {
+ * /* These are just alternative ways of hiding an element */
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
+ * }
* ```
*
- * Just remember to include the important flag so the CSS override will function.
+ * By default you don't need to override anything in CSS and the animations will work around the
+ * display style.
*
- *
- * **Note:** Here is a list of values that ngShow will consider as a falsy value (case insensitive):
- * "f" / "0" / "false" / "no" / "n" / "[]"
- *
- *
- * ## A note about animations with ngShow
+ * @animations
+ * | 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. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass except that
- * you must also include the !important flag to override the display property
- * so that you can perform an animation when the element is hidden during the time of the animation.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /* A working example can be found at the bottom of this page. */
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * transition:0.5s linear all;
- * display:block!important;
- * }
+ * transition: all 0.5s linear;
+ * }
*
* .my-element.ng-hide-add { ... }
* .my-element.ng-hide-add.ng-hide-add-active { ... }
@@ -20188,83 +32039,121 @@
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * @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
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngShow If the {@link guide/expression expression} is truthy
- * then the element is shown or hidden respectively.
+ * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the
+ * element is shown/hidden respectively.
*
* @example
-
+ * A simple example, animating the element's opacity:
+ *
+
- Click me:
-
- Show:
-
- I show up when your checkbox is checked.
-
-
-
- Hide:
-
- I hide when your checkbox is checked.
-
+ Show:
+
+ I show up when your checkbox is checked.
-
- @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css);
+
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
+
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+
+
+ *
+ *
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+
+
+ Show:
+
+ I show up when your checkbox is checked.
+
- .animate-show {
- -webkit-transition:all linear 0.5s;
- transition:all linear 0.5s;
- line-height:20px;
- opacity:1;
- padding:10px;
- border:1px solid black;
- background:white;
+ body {
+ overflow: hidden;
+ perspective: 1000px;
}
- .animate-show.ng-hide-add,
- .animate-show.ng-hide-remove {
- display:block!important;
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
}
- .animate-show.ng-hide {
- line-height:0;
- opacity:0;
- padding:0 10px;
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding:10px;
- border:1px solid black;
- background:white;
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
}
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
});
*/
var ngShowDirective = ['$animate', function($animate) {
- return function(scope, element, attr) {
- scope.$watch(attr.ngShow, function ngShowWatchAction(value){
- $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
- });
+ return {
+ restrict: 'A',
+ multiElement: true,
+ link: function(scope, element, attr) {
+ scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
+ // we're adding a temporary, animation-specific class for ng-hide since this way
+ // we can control when the element is actually displayed on screen without having
+ // to have a global/greedy CSS selector that breaks when other animations are run.
+ // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
+ $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
+ tempClasses: NG_HIDE_IN_PROGRESS_CLASS
+ });
+ });
+ }
};
}];
@@ -20272,75 +32161,77 @@
/**
* @ngdoc directive
* @name ngHide
+ * @multiElement
*
* @description
- * The `ngHide` directive shows or hides the given HTML element based on the expression
- * provided to the ngHide attribute. The element is shown or hidden by removing or adding
- * the `ng-hide` CSS class onto the element. The `.ng-hide` CSS class is predefined
- * in AngularJS and sets the display style to none (using an !important flag).
- * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
+ * The `ngHide` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngHide` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
*
* ```html
*
- *
+ *
*
*
- *
+ *
* ```
*
- * When the ngHide expression evaluates to true then the .ng-hide CSS class is added to the class attribute
- * on the element causing it to become hidden. When false, the ng-hide CSS class is removed
- * from the element causing the element not to appear hidden.
+ * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
*
- * ## Why is !important used?
+ * ## Why is `!important` used?
*
- * You may be wondering why !important is used for the .ng-hide CSS class. This is because the `.ng-hide` selector
- * can be easily overridden by heavier selectors. For example, something as simple
- * as changing the display style on a HTML list item would make hidden elements appear visible.
- * This also becomes a bigger issue when dealing with CSS frameworks.
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
*
- * By using !important, the show and hide behavior will work as expected despite any clash between CSS selector
- * specificity (when !important isn't used with any conflicting styles). If a developer chooses to override the
- * styling to change how to hide an element then it is just a matter of using !important in their own CSS code.
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
*
- * ### Overriding .ng-hide
+ * ### Overriding `.ng-hide`
+ *
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
*
- * If you wish to change the hide behavior with ngShow/ngHide then this can be achieved by
- * restating the styles for the .ng-hide class in CSS:
* ```css
- * .ng-hide {
- * //!annotate CSS Specificity|Not to worry, this will override the AngularJS default...
- * display:block!important;
- *
- * //this is just another form of hiding an element
- * position:absolute;
- * top:-9999px;
- * left:-9999px;
- * }
+ * .ng-hide:not(.ng-hide-animate) {
+ * /* These are just alternative ways of hiding an element */
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
+ * }
* ```
*
- * Just remember to include the important flag so the CSS override will function.
- *
- *
- * **Note:** Here is a list of values that ngHide will consider as a falsy value (case insensitive):
- * "f" / "0" / "false" / "no" / "n" / "[]"
- *
+ * By default you don't need to override in CSS anything and the animations will work around the
+ * display style.
*
- * ## A note about animations with ngHide
+ * @animations
+ * | 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. |
*
- * Animations in ngShow/ngHide work with the show and hide events that are triggered when the directive expression
- * is true and false. This system works like the animation system present with ngClass, except that
- * you must also include the !important flag to override the display property so
- * that you can perform an animation when the element is hidden during the time of the animation.
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
*
* ```css
- * //
- * //a working example can be found at the bottom of this page
- * //
+ * /* A working example can be found at the bottom of this page. */
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
- * transition:0.5s linear all;
- * display:block!important;
- * }
+ * transition: all 0.5s linear;
+ * }
*
* .my-element.ng-hide-add { ... }
* .my-element.ng-hide-add.ng-hide-add-active { ... }
@@ -20348,83 +32239,119 @@
* .my-element.ng-hide-remove.ng-hide-remove-active { ... }
* ```
*
- * @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
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
- * the element is shown or hidden respectively.
+ * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the
+ * element is hidden/shown respectively.
*
* @example
-
+ * A simple example, animating the element's opacity:
+ *
+
- Click me:
-
- Show:
-
- I show up when your checkbox is checked.
-
-
-
- Hide:
-
- I hide when your checkbox is checked.
-
+ Hide:
+
+ I hide when your checkbox is checked.
-
- @import url(//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap-glyphicons.css);
+
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
+
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
+
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+
+
+ *
+ *
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+
+
+ Hide:
+
+ I hide when your checkbox is checked.
+
- .animate-hide {
- -webkit-transition:all linear 0.5s;
- transition:all linear 0.5s;
- line-height:20px;
- opacity:1;
- padding:10px;
- border:1px solid black;
- background:white;
+ body {
+ overflow: hidden;
+ perspective: 1000px;
}
- .animate-hide.ng-hide-add,
- .animate-hide.ng-hide-remove {
- display:block!important;
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
}
- .animate-hide.ng-hide {
- line-height:0;
- opacity:0;
- padding:0 10px;
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
}
.check-element {
- padding:10px;
- border:1px solid black;
- background:white;
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
}
- var thumbsUp = element(by.css('span.glyphicon-thumbs-up'));
- var thumbsDown = element(by.css('span.glyphicon-thumbs-down'));
-
- it('should check ng-show / ng-hide', function() {
- expect(thumbsUp.isDisplayed()).toBeFalsy();
- expect(thumbsDown.isDisplayed()).toBeTruthy();
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
- element(by.model('checked')).click();
-
- expect(thumbsUp.isDisplayed()).toBeTruthy();
- expect(thumbsDown.isDisplayed()).toBeFalsy();
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
});
*/
var ngHideDirective = ['$animate', function($animate) {
- return function(scope, element, attr) {
- scope.$watch(attr.ngHide, function ngHideWatchAction(value){
- $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide');
- });
+ return {
+ restrict: 'A',
+ multiElement: true,
+ link: function(scope, element, attr) {
+ scope.$watch(attr.ngHide, function ngHideWatchAction(value) {
+ // The comment inside of the ngShowDirective explains why we add and
+ // remove a temporary class for the show/hide animation
+ $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, {
+ tempClasses: NG_HIDE_IN_PROGRESS_CLASS
+ });
+ });
+ }
};
}];
@@ -20436,15 +32363,26 @@
* @description
* The `ngStyle` directive allows you to set CSS style on an HTML element conditionally.
*
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `style`
+ * attribute, when using the `ngStyle` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
+ *
* @element ANY
- * @param {expression} ngStyle {@link guide/expression Expression} which evals to an
- * object whose keys are CSS style names and values are corresponding values for those CSS
- * keys.
+ * @param {expression} ngStyle
+ *
+ * {@link guide/expression Expression} which evals to an
+ * object whose keys are CSS style names and values are corresponding values for those CSS
+ * keys.
+ *
+ * Since some CSS style names are not valid keys for an object, they must be quoted.
+ * See the 'background-color' style in the example below.
*
* @example
-
+
-
+
+
Sample Text
@@ -20460,7 +32398,7 @@
it('should check ng-style', function() {
expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
- element(by.css('input[value=set]')).click();
+ element(by.css('input[value=\'set color\']')).click();
expect(colorSpan.getCssValue('color')).toBe('rgba(255, 0, 0, 1)');
element(by.css('input[value=clear]')).click();
expect(colorSpan.getCssValue('color')).toBe('rgba(0, 0, 0, 1)');
@@ -20504,51 +32442,61 @@
*
* @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
+ *
+ * ```
*
* ...
* ...
* ...
*
+ * ```
*
*
* @scope
- * @priority 800
- * @param {*} ngSwitch|on expression to match against ng-switch-when .
+ * @priority 1200
+ * @param {*} ngSwitch|on expression to match against ng-switch-when
.
* On child elements add:
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
* case will be displayed. If the same match appears multiple times, all the
- * elements will be displayed.
+ * elements will be displayed. It is possible to associate multiple values to
+ * the same `ngSwitchWhen` by defining the optional attribute
+ * `ngSwitchWhenSeparator`. The separator will be used to split the value of
+ * the `ngSwitchWhen` attribute into multiple tokens, and the element will show
+ * if any of the `ngSwitch` evaluates to any of these tokens.
* * `ngSwitchDefault`: the default case when no other case match. If there
* are multiple default cases, all of them will be displayed when no other
* case match.
*
*
* @example
-
+
-
+
-
selection={{selection}}
+
selection={{selection}}
-
Settings Div
+
Settings Div
Home Span
default
- function Ctrl($scope) {
- $scope.items = ['settings', 'home', 'other'];
- $scope.selection = $scope.items[0];
- }
+ angular.module('switchExample', ['ngAnimate'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.items = ['settings', 'home', 'options', 'other'];
+ $scope.selection = $scope.items[0];
+ }]);
.animate-switch-container {
@@ -20564,7 +32512,6 @@
}
.animate-switch.ng-animate {
- -webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
position:absolute;
@@ -20591,68 +32538,68 @@
expect(switchElem.getText()).toMatch(/Settings Div/);
});
it('should change to home', function() {
- select.element.all(by.css('option')).get(1).click();
+ select.all(by.css('option')).get(1).click();
expect(switchElem.getText()).toMatch(/Home Span/);
});
+ it('should change to settings via "options"', function() {
+ select.all(by.css('option')).get(2).click();
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
it('should select default', function() {
- select.element.all(by.css('option')).get(2).click();
+ select.all(by.css('option')).get(3).click();
expect(switchElem.getText()).toMatch(/default/);
});
*/
- var ngSwitchDirective = ['$animate', function($animate) {
+ var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
return {
- restrict: 'EA',
require: 'ngSwitch',
// asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
+ controller: ['$scope', function NgSwitchController() {
this.cases = {};
}],
link: function(scope, element, attr, ngSwitchController) {
var watchExpr = attr.ngSwitch || attr.on,
- selectedTranscludes,
- selectedElements,
- previousElements,
+ selectedTranscludes = [],
+ selectedElements = [],
+ previousLeaveAnimations = [],
selectedScopes = [];
+ var spliceFactory = function(array, index) {
+ return function(response) {
+ if (response !== false) array.splice(index, 1);
+ };
+ };
+
scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
- var i, ii = selectedScopes.length;
- if(ii > 0) {
- if(previousElements) {
- for (i = 0; i < ii; i++) {
- previousElements[i].remove();
- }
- previousElements = null;
- }
+ var i, ii;
- previousElements = [];
- for (i= 0; i
-
-
-
-
-
- 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');
- });
-
-
+ * ### Basic transclusion
+ * This example demonstrates basic transclusion of content into a component directive.
+ *
+ *
+ *
+ *
+ *
+ *
+ * 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');
+ * });
+ *
+ *
+ *
+ * @example
+ * ### Transclude fallback content
+ * This example shows how to use `NgTransclude` with fallback content, that
+ * is displayed if no transcluded content is provided.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Button2
+ *
+ *
+ *
+ * it('should have different transclude element content', function() {
+ * expect(element(by.id('fallback')).getText()).toBe('Button1');
+ * expect(element(by.id('modified')).getText()).toBe('Button2');
+ * });
+ *
+ *
*
+ * @example
+ * ### Multi-slot transclusion
+ * This example demonstrates using multi-slot transclusion in a component directive.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * {{title}}
+ * {{text}}
+ *
+ *
+ *
+ *
+ * angular.module('multiSlotTranscludeExample', [])
+ * .directive('pane', function() {
+ * return {
+ * restrict: 'E',
+ * transclude: {
+ * 'title': '?paneTitle',
+ * 'body': 'paneBody',
+ * 'footer': '?paneFooter'
+ * },
+ * template: '' +
+ * '
Fallback Title
' +
+ * '
' +
+ * '' +
+ * '
'
+ * };
+ * })
+ * .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...';
+ * }]);
+ *
+ *
+ * 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');
+ * });
+ *
+ *
*/
- var ngTranscludeDirective = ngDirective({
- link: function($scope, $element, $attrs, controller, $transclude) {
- if (!$transclude) {
- throw minErr('ngTransclude')('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);
- });
- }
- });
+ var ngTranscludeMinErr = minErr('ngTransclude');
+ var ngTranscludeDirective = ['$compile', function($compile) {
+ return {
+ restrict: 'EAC',
+ compile: function ngTranscludeCompile(tElement) {
+
+ // Remove and cache any original content to act as a fallback
+ var fallbackLinkFn = $compile(tElement.contents());
+ tElement.empty();
+
+ return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
+
+ if (!$transclude) {
+ throw ngTranscludeMinErr('orphan',
+ 'Illegal use of ngTransclude directive in the template! ' +
+ 'No parent directive that requires a transclusion found. ' +
+ 'Element: {0}',
+ startingTag($element));
+ }
+
+
+ // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ $attrs.ngTransclude = '';
+ }
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
+
+ // If the slot is required and no transclusion content is provided then this call will throw an error
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
+
+ // If the slot is optional and no transclusion content is provided then use the fallback content
+ if (slotName && !$transclude.isSlotFilled(slotName)) {
+ useFallbackContent();
+ }
+
+ function ngTranscludeCloneAttachFn(clone, transcludedScope) {
+ if (clone.length && notWhitespace(clone)) {
+ $element.append(clone);
+ } else {
+ useFallbackContent();
+ // There is nothing linked against the transcluded scope since no content was available,
+ // so it should be safe to clean up the generated scope.
+ transcludedScope.$destroy();
+ }
+ }
+
+ function useFallbackContent() {
+ // Since this is the fallback content rather than the transcluded content,
+ // we link against the scope of this directive rather than the transcluded scope
+ fallbackLinkFn($scope, function(clone) {
+ $element.append(clone);
+ });
+ }
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
+ };
+ }
+ };
+ }];
/**
* @ngdoc directive
@@ -20770,7 +32880,7 @@
* @param {string} id Cache name of the template.
*
* @example
-
+
-
-
-
- Color (null not allowed):
-
+ */
+ var SelectController =
+ ['$element', '$scope', /** @this */ function($element, $scope) {
+
+ var self = this,
+ optionsMap = new NgMap();
+
+ self.selectValueMap = {}; // Keys are the hashed values, values the original values
+
+ // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
+ self.ngModelCtrl = noopNgModelController;
+ self.multiple = false;
+
+ // The "unknown" option is one that is prepended to the list if the viewValue
+ // does not match any of the options. When it is rendered the value of the unknown
+ // option is '? XXX ?' where XXX is the hashKey of the value that is not known.
+ //
+ // Support: IE 9 only
+ // We can't just jqLite('
') since jqLite is not smart enough
+ // to create it in and IE barfs otherwise.
+ self.unknownOption = jqLite(window.document.createElement('option'));
+
+ // The empty option is an option with the value '' that the application developer can
+ // provide inside the select. It is always selectable and indicates that a "null" selection has
+ // been made by the user.
+ // If the select has an empty option, and the model of the select is set to "undefined" or "null",
+ // the empty option is selected.
+ // If the model is set to a different unmatched value, the unknown option is rendered and
+ // selected, i.e both are present, because a "null" selection and an unknown value are different.
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+
+ self.renderUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ $element.prepend(self.unknownOption);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
- Color (null allowed):
-
-
- -- choose color --
-
-
+ self.updateUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
- Color grouped by shade:
-
-
+ self.generateUnknownOptionValue = function(val) {
+ return '? ' + hashKey(val) + ' ?';
+ };
+ self.removeUnknownOption = function() {
+ if (self.unknownOption.parent()) self.unknownOption.remove();
+ };
- Select bogus .
-
- Currently selected: {{ {selected_color:color} }}
-
-
-
-
-
- it('should check ng-options', function() {
- expect(element(by.binding('{selected_color:color}')).getText()).toMatch('red');
- element.all(by.select('color')).first().click();
- element.all(by.css('select[ng-model="color"] option')).first().click();
- expect(element(by.binding('{selected_color:color}')).getText()).toMatch('black');
- element(by.css('.nullable select[ng-model="color"]')).click();
- element.all(by.css('.nullable select[ng-model="color"] option')).first().click();
- expect(element(by.binding('{selected_color:color}')).getText()).toMatch('null');
- });
-
-
- */
+ self.selectEmptyOption = function() {
+ if (self.emptyOption) {
+ $element.val('');
+ setOptionSelectedStatus(self.emptyOption, true);
+ }
+ };
- var ngOptionsDirective = valueFn({ terminal: true });
-// jshint maxlen: false
- var selectDirective = ['$compile', '$parse', function($compile, $parse) {
- //000011111111110000000000022222222220000000000000000000003333333333000000000000004444444444444440000000005555555555555550000000666666666666666000000000000000777777777700000000000000000008888888888
- var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/,
- nullModelCtrl = {$setViewValue: noop};
-// jshint maxlen: 100
+ self.unselectEmptyOption = function() {
+ if (self.hasEmptyOption) {
+ setOptionSelectedStatus(self.emptyOption, false);
+ }
+ };
- return {
- restrict: 'E',
- require: ['select', '?ngModel'],
- controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
- var self = this,
- optionsMap = {},
- ngModelCtrl = nullModelCtrl,
- nullOption,
- unknownOption;
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
+
+ // Read the value of the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.readValue = function readSingleValue() {
+ var val = $element.val();
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
+ var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
+ if (self.hasOption(realVal)) {
+ return realVal;
+ }
- self.databound = $attrs.ngModel;
+ return null;
+ };
- self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
- ngModelCtrl = ngModelCtrl_;
- nullOption = nullOption_;
- unknownOption = unknownOption_;
- };
+ // Write the value to the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.writeValue = function writeSingleValue(value) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
+ if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
+ if (self.hasOption(value)) {
+ self.removeUnknownOption();
- self.addOption = function(value) {
- assertNotHasOwnProperty(value, '"option value"');
- optionsMap[value] = true;
+ var hashedVal = hashKey(value);
+ $element.val(hashedVal in self.selectValueMap ? hashedVal : value);
- if (ngModelCtrl.$viewValue == value) {
- $element.val(value);
- if (unknownOption.parent()) unknownOption.remove();
- }
- };
+ // Set selected attribute and property on selected option for screen readers
+ var selectedOption = $element[0].options[$element[0].selectedIndex];
+ setOptionSelectedStatus(jqLite(selectedOption), true);
+ } else {
+ self.selectUnknownOrEmptyOption(value);
+ }
+ };
- self.removeOption = function(value) {
- if (this.hasOption(value)) {
- delete optionsMap[value];
- if (ngModelCtrl.$viewValue == value) {
- this.renderUnknownOption(value);
+ // Tell the select control that an option, with the given value, has been added
+ self.addOption = function(value, element) {
+ // Skip comment nodes, as they only pollute the `optionsMap`
+ if (element[0].nodeType === NODE_TYPE_COMMENT) return;
+
+ assertNotHasOwnProperty(value, '"option value"');
+ if (value === '') {
+ self.hasEmptyOption = true;
+ self.emptyOption = element;
+ }
+ var count = optionsMap.get(value) || 0;
+ optionsMap.set(value, count + 1);
+ // Only render at the end of a digest. This improves render performance when many options
+ // are added during a digest and ensures all relevant options are correctly marked as selected
+ scheduleRender();
+ };
+
+ // Tell the select control that an option, with the given value, has been removed
+ self.removeOption = function(value) {
+ var count = optionsMap.get(value);
+ if (count) {
+ if (count === 1) {
+ optionsMap.delete(value);
+ if (value === '') {
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
}
+ } else {
+ optionsMap.set(value, count - 1);
}
- };
+ }
+ };
+ // Check whether the select control has an option matching the given value
+ self.hasOption = function(value) {
+ return !!optionsMap.get(value);
+ };
- self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
- unknownOption.val(unknownVal);
- $element.prepend(unknownOption);
- $element.val(unknownVal);
- unknownOption.prop('selected', true); // needed for IE
- };
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$hasEmptyOption
+ *
+ * @description
+ *
+ * Returns `true` if the select element currently has an empty option
+ * element, i.e. an option that signifies that the select is empty / the selection is null.
+ *
+ */
+ self.$hasEmptyOption = function() {
+ return self.hasEmptyOption;
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isUnknownOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element's unknown option is selected. The unknown option is added
+ * and automatically selected whenever the select model doesn't match any option.
+ *
+ */
+ self.$isUnknownOptionSelected = function() {
+ // Presence of the unknown option means it is selected
+ return $element[0].options[0] === self.unknownOption[0];
+ };
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isEmptyOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element has an empty option and this empty option is currently
+ * selected. Returns `false` if the select element has no empty option or it is not selected.
+ *
+ */
+ self.$isEmptyOptionSelected = function() {
+ return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0];
+ };
- self.hasOption = function(value) {
- return optionsMap.hasOwnProperty(value);
- };
+ self.selectUnknownOrEmptyOption = function(value) {
+ if (value == null && self.emptyOption) {
+ self.removeUnknownOption();
+ self.selectEmptyOption();
+ } else if (self.unknownOption.parent().length) {
+ self.updateUnknownOption(value);
+ } else {
+ self.renderUnknownOption(value);
+ }
+ };
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
+ var renderScheduled = false;
+ function scheduleRender() {
+ if (renderScheduled) return;
+ renderScheduled = true;
+ $scope.$$postDigest(function() {
+ renderScheduled = false;
+ self.ngModelCtrl.$render();
});
- }],
+ }
- link: function(scope, element, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
- if (!ctrls[1]) return;
-
- var selectCtrl = ctrls[0],
- ngModelCtrl = ctrls[1],
- multiple = attr.multiple,
- optionsExp = attr.ngOptions,
- nullOption = false, // if false, user will not be able to select it (used by ngOptions)
- emptyOption,
- // we can't just jqLite('') since jqLite is not smart enough
- // to create it in and IE barfs otherwise.
- optionTemplate = jqLite(document.createElement('option')),
- optGroupTemplate =jqLite(document.createElement('optgroup')),
- unknownOption = optionTemplate.clone();
-
- // find "null" option
- for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
- if (children[i].value === '') {
- emptyOption = nullOption = children.eq(i);
- break;
- }
- }
+ var updateScheduled = false;
+ function scheduleViewValueUpdate(renderAfter) {
+ if (updateScheduled) return;
- selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
+ updateScheduled = true;
- // required validator
- if (multiple) {
- ngModelCtrl.$isEmpty = function(value) {
- return !value || value.length === 0;
- };
- }
+ $scope.$$postDigest(function() {
+ if ($scope.$$destroyed) return;
- if (optionsExp) setupAsOptions(scope, element, ngModelCtrl);
- else if (multiple) setupAsMultiple(scope, element, ngModelCtrl);
- else setupAsSingle(scope, element, ngModelCtrl, selectCtrl);
+ updateScheduled = false;
+ self.ngModelCtrl.$setViewValue(self.readValue());
+ if (renderAfter) self.ngModelCtrl.$render();
+ });
+ }
- ////////////////////////////
+ self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
+ if (optionAttrs.$attr.ngValue) {
+ // The value attribute is set by ngValue
+ var oldVal, hashedVal = NaN;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
- function setupAsSingle(scope, selectElement, ngModelCtrl, selectCtrl) {
- ngModelCtrl.$render = function() {
- var viewValue = ngModelCtrl.$viewValue;
+ if (isDefined(hashedVal)) {
+ self.removeOption(oldVal);
+ delete self.selectValueMap[hashedVal];
+ removal = true;
+ }
- if (selectCtrl.hasOption(viewValue)) {
- if (unknownOption.parent()) unknownOption.remove();
- selectElement.val(viewValue);
- if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
- } else {
- if (isUndefined(viewValue) && emptyOption) {
- selectElement.val('');
- } else {
- selectCtrl.renderUnknownOption(viewValue);
- }
+ hashedVal = hashKey(newVal);
+ oldVal = newVal;
+ self.selectValueMap[hashedVal] = newVal;
+ self.addOption(newVal, optionElement);
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
+ // from firing a second time. Other $observers on value will also get the result of the
+ // ngValue expression, not the hashed value
+ optionElement.attr('value', hashedVal);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
}
- };
- selectElement.on('change', function() {
- scope.$apply(function() {
- if (unknownOption.parent()) unknownOption.remove();
- ngModelCtrl.$setViewValue(selectElement.val());
- });
});
- }
-
- function setupAsMultiple(scope, selectElement, ctrl) {
- var lastView;
- ctrl.$render = function() {
- var items = new HashMap(ctrl.$viewValue);
- forEach(selectElement.find('option'), function(option) {
- option.selected = isDefined(items.get(option.value));
- });
- };
+ } else if (interpolateValueFn) {
+ // The value attribute is interpolated
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ // This method is overwritten in ngOptions and has side-effects!
+ self.readValue();
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(oldVal)) {
+ self.removeOption(oldVal);
+ removal = true;
+ }
+ oldVal = newVal;
+ self.addOption(newVal, optionElement);
- // we have to do it on each watch since ngModel watches reference, but
- // we need to work of an array, so we need to see if anything was inserted/removed
- scope.$watch(function selectMultipleWatch() {
- if (!equals(lastView, ctrl.$viewValue)) {
- lastView = copy(ctrl.$viewValue);
- ctrl.$render();
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
}
});
+ } else if (interpolateTextFn) {
+ // The text content is interpolated
+ optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
+ optionAttrs.$set('value', newVal);
+ var previouslySelected = optionElement.prop('selected');
+ if (oldVal !== newVal) {
+ self.removeOption(oldVal);
+ }
+ self.addOption(newVal, optionElement);
- selectElement.on('change', function() {
- scope.$apply(function() {
- var array = [];
- forEach(selectElement.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
- }
- });
- ctrl.$setViewValue(array);
- });
+ if (oldVal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
});
+ } else {
+ // The value attribute is static
+ self.addOption(optionAttrs.value, optionElement);
}
- function setupAsOptions(scope, selectElement, ctrl) {
- var match;
-
- if (!(match = optionsExp.match(NG_OPTIONS_REGEXP))) {
- throw ngOptionsMinErr('iexp',
- "Expected expression in form of " +
- "'_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '{0}'. Element: {1}",
- optionsExp, startingTag(selectElement));
- }
-
- var displayFn = $parse(match[2] || match[1]),
- valueName = match[4] || match[6],
- keyName = match[5],
- groupByFn = $parse(match[3] || ''),
- valueFn = $parse(match[2] ? match[1] : valueName),
- valuesFn = $parse(match[7]),
- track = match[8],
- trackFn = track ? $parse(match[8]) : null,
- // This is an array of array of existing option groups in DOM.
- // We try to reuse these if possible
- // - optionGroupsCache[0] is the options with no option group
- // - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
- optionGroupsCache = [[{element: selectElement, label:''}]];
-
- if (nullOption) {
- // compile the element since there might be bindings in it
- $compile(nullOption)(scope);
-
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- nullOption.removeClass('ng-scope');
-
- // we need to remove it before calling selectElement.empty() because otherwise IE will
- // remove the label from the element. wtf?
- nullOption.remove();
- }
-
- // clear contents, we'll add what's needed based on the model
- selectElement.empty();
-
- selectElement.on('change', function() {
- scope.$apply(function() {
- var optionGroup,
- collection = valuesFn(scope) || [],
- locals = {},
- key, value, optionElement, index, groupIndex, length, groupLength, trackIndex;
-
- if (multiple) {
- value = [];
- for (groupIndex = 0, groupLength = optionGroupsCache.length;
- groupIndex < groupLength;
- groupIndex++) {
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroupsCache[groupIndex];
-
- for(index = 1, length = optionGroup.length; index < length; index++) {
- if ((optionElement = optionGroup[index].element)[0].selected) {
- key = optionElement.val();
- if (keyName) locals[keyName] = key;
- if (trackFn) {
- for (trackIndex = 0; trackIndex < collection.length; trackIndex++) {
- locals[valueName] = collection[trackIndex];
- if (trackFn(scope, locals) == key) break;
- }
- } else {
- locals[valueName] = collection[key];
- }
- value.push(valueFn(scope, locals));
- }
- }
- }
- } else {
- key = selectElement.val();
- if (key == '?') {
- value = undefined;
- } else if (key === ''){
- value = null;
- } else {
- if (trackFn) {
- for (trackIndex = 0; trackIndex < collection.length; trackIndex++) {
- locals[valueName] = collection[trackIndex];
- if (trackFn(scope, locals) == key) {
- value = valueFn(scope, locals);
- break;
- }
- }
- } else {
- locals[valueName] = collection[key];
- if (keyName) locals[keyName] = key;
- value = valueFn(scope, locals);
- }
- }
- // Update the null option's selected property here so $render cleans it up correctly
- if (optionGroupsCache[0].length > 1) {
- if (optionGroupsCache[0][1].id !== key) {
- optionGroupsCache[0][1].selected = false;
- }
- }
- }
- ctrl.$setViewValue(value);
- });
- });
- ctrl.$render = render;
-
- // TODO(vojta): can't we optimize this ?
- scope.$watch(render);
-
- function render() {
- // Temporary location for the option groups before we render them
- var optionGroups = {'':[]},
- optionGroupNames = [''],
- optionGroupName,
- optionGroup,
- option,
- existingParent, existingOptions, existingOption,
- modelValue = ctrl.$modelValue,
- values = valuesFn(scope) || [],
- keys = keyName ? sortedKeys(values) : values,
- key,
- groupLength, length,
- groupIndex, index,
- locals = {},
- selected,
- selectedSet = false, // nothing is selected yet
- lastElement,
- element,
- label;
-
- if (multiple) {
- if (trackFn && isArray(modelValue)) {
- selectedSet = new HashMap([]);
- for (var trackIndex = 0; trackIndex < modelValue.length; trackIndex++) {
- locals[valueName] = modelValue[trackIndex];
- selectedSet.put(trackFn(scope, locals), modelValue[trackIndex]);
- }
- } else {
- selectedSet = new HashMap(modelValue);
- }
+ optionAttrs.$observe('disabled', function(newVal) {
+
+ // Since model updates will also select disabled options (like ngOptions),
+ // we only have to handle options becoming disabled, not enabled
+
+ if (newVal === 'true' || newVal && optionElement.prop('selected')) {
+ if (self.multiple) {
+ scheduleViewValueUpdate(true);
+ } else {
+ self.ngModelCtrl.$setViewValue(null);
+ self.ngModelCtrl.$render();
}
+ }
+ });
+
+ optionElement.on('$destroy', function() {
+ var currentValue = self.readValue();
+ var removeValue = optionAttrs.value;
+
+ self.removeOption(removeValue);
+ scheduleRender();
+
+ if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
+ currentValue === removeValue
+ ) {
+ // When multiple (selected) options are destroyed at the same time, we don't want
+ // to run a model update for each of them. Instead, run a single update in the $$postDigest
+ scheduleViewValueUpdate(true);
+ }
+ });
+ };
+ }];
- // We now build up the list of options we need (we merge later)
- for (index = 0; length = keys.length, index < length; index++) {
+ /**
+ * @ngdoc directive
+ * @name select
+ * @restrict E
+ *
+ * @description
+ * HTML `select` element with AngularJS data-binding.
+ *
+ * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
+ * between the scope and the `` control (including setting default values).
+ * It also handles dynamic `` elements, which can be added using the {@link ngRepeat `ngRepeat}` or
+ * {@link ngOptions `ngOptions`} directives.
+ *
+ * When an item in the `` menu is selected, the value of the selected option will be bound
+ * to the model identified by the `ngModel` directive. With static or repeated options, this is
+ * the content of the `value` attribute or the textContent of the ``, if the value attribute is missing.
+ * Value and textContent can be interpolated.
+ *
+ * The {@link select.SelectController select controller} exposes utility functions that can be used
+ * to manipulate the select's behavior.
+ *
+ * ## Matching model and option values
+ *
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
+ * value against the value of the available options.
+ *
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
+ * value will always be a `string` which means that the model value must also be a string.
+ * Otherwise the `select` directive cannot match them correctly.
+ *
+ * To bind the model to a non-string value, you can use one of the following strategies:
+ * - the {@link ng.ngOptions `ngOptions`} directive
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
+ * - model $parsers / $formatters to convert the string value
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
+ *
+ * If the viewValue of `ngModel` does not match any of the options, then the control
+ * will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
+ *
+ * Optionally, a single hard-coded ` ` element, with the value set to an empty string, can
+ * be nested into the `` element. This element will then represent the `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * ## Choosing between `ngRepeat` and `ngOptions`
+ *
+ * In many cases, `ngRepeat` can be used on `` elements instead of {@link ng.directive:ngOptions
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the ``'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
+ *
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
+ * Chrome and Internet Explorer / Edge.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} multiple Allows multiple options to be selected. The selected values will be
+ * bound to the model as an array.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds required attribute and required validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
+ * when you want to data-bind to the required attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when selected option(s) changes due to user
+ * interaction with the select element.
+ * @param {string=} ngOptions sets the options that the select is populated with and defines what is
+ * set on the model on selection. See {@link ngOptions `ngOptions`}.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
+ *
+ *
+ * @knownIssue
+ *
+ * In Firefox, the select model is only updated when the select element is blurred. For example,
+ * when switching between options with the keyboard, the select model is only set to the
+ * currently selected option when the select is blurred, e.g via tab key or clicking the mouse
+ * outside the select.
+ *
+ * This is due to an ambiguity in the select element specification. See the
+ * [issue on the Firefox bug tracker](https://bugzilla.mozilla.org/show_bug.cgi?id=126379)
+ * for more information, and this
+ * [Github comment for a workaround](https://github.com/angular/angular.js/issues/9134#issuecomment-130800488)
+ *
+ * @example
+ * ### Simple `select` elements with static options
+ *
+ *
+ *
+ *
+ *