*
* @property {RegExp} pattern The regular expression pattern used to match values of this type when
* coming from a substring of a URL.
@@ -1008,7 +1114,7 @@ Type.prototype.encode = function(val, key) {
* @methodOf ui.router.util.type:Type
*
* @description
- * Converts a string URL parameter value to a custom/native value.
+ * Converts a parameter value (from URL string or transition param) to a custom/native value.
*
* @param {string} val The URL parameter value to decode.
* @param {string} key The name of the parameter in which `val` is stored. Can be used for
@@ -1042,6 +1148,76 @@ Type.prototype.$subPattern = function() {
Type.prototype.pattern = /.*/;
+Type.prototype.toString = function() { return "{Type:" + this.name + "}"; };
+
+/*
+ * Wraps an existing custom Type as an array of Type, depending on 'mode'.
+ * e.g.:
+ * - urlmatcher pattern "/path?{queryParam[]:int}"
+ * - url: "/path?queryParam=1&queryParam=2
+ * - $stateParams.queryParam will be [1, 2]
+ * if `mode` is "auto", then
+ * - url: "/path?queryParam=1 will create $stateParams.queryParam: 1
+ * - url: "/path?queryParam=1&queryParam=2 will create $stateParams.queryParam: [1, 2]
+ */
+Type.prototype.$asArray = function(mode, isSearch) {
+ if (!mode) return this;
+ if (mode === "auto" && !isSearch) throw new Error("'auto' array mode is for query parameters only");
+ return new ArrayType(this, mode);
+
+ function ArrayType(type, mode) {
+ function bindTo(type, callbackName) {
+ return function() {
+ return type[callbackName].apply(type, arguments);
+ };
+ }
+
+ // Wrap non-array value as array
+ function arrayWrap(val) { return isArray(val) ? val : (isDefined(val) ? [ val ] : []); }
+ // Unwrap array value for "auto" mode. Return undefined for empty array.
+ function arrayUnwrap(val) {
+ switch(val.length) {
+ case 0: return undefined;
+ case 1: return mode === "auto" ? val[0] : val;
+ default: return val;
+ }
+ }
+ function falsey(val) { return !val; }
+
+ // Wraps type (.is/.encode/.decode) functions to operate on each value of an array
+ function arrayHandler(callback, allTruthyMode) {
+ return function handleArray(val) {
+ val = arrayWrap(val);
+ var result = map(val, callback);
+ if (allTruthyMode === true)
+ return filter(result, falsey).length === 0;
+ return arrayUnwrap(result);
+ };
+ }
+
+ // Wraps type (.equals) functions to operate on each value of an array
+ function arrayEqualsHandler(callback) {
+ return function handleArray(val1, val2) {
+ var left = arrayWrap(val1), right = arrayWrap(val2);
+ if (left.length !== right.length) return false;
+ for (var i = 0; i < left.length; i++) {
+ if (!callback(left[i], right[i])) return false;
+ }
+ return true;
+ };
+ }
+
+ this.encode = arrayHandler(bindTo(type, 'encode'));
+ this.decode = arrayHandler(bindTo(type, 'decode'));
+ this.is = arrayHandler(bindTo(type, 'is'), true);
+ this.equals = arrayEqualsHandler(bindTo(type, 'equals'));
+ this.pattern = type.pattern;
+ this.$arrayMode = mode;
+ }
+};
+
+
+
/**
* @ngdoc object
* @name ui.router.util.$urlMatcherFactory
@@ -1051,50 +1227,67 @@ Type.prototype.pattern = /.*/;
* is also available to providers under the name `$urlMatcherFactoryProvider`.
*/
function $UrlMatcherFactory() {
+ $$UMFP = this;
- var isCaseInsensitive = false, isStrictMode = true;
+ var isCaseInsensitive = false, isStrictMode = true, defaultSquashPolicy = false;
- var enqueue = true, typeQueue = [], injector, defaultTypes = {
+ function valToString(val) { return val != null ? val.toString().replace(/\//g, "%2F") : val; }
+ function valFromString(val) { return val != null ? val.toString().replace(/%2F/g, "/") : val; }
+// TODO: in 1.0, make string .is() return false if value is undefined by default.
+// function regexpMatches(val) { /*jshint validthis:true */ return isDefined(val) && this.pattern.test(val); }
+ function regexpMatches(val) { /*jshint validthis:true */ return this.pattern.test(val); }
+
+ var $types = {}, enqueue = true, typeQueue = [], injector, defaultTypes = {
+ string: {
+ encode: valToString,
+ decode: valFromString,
+ is: regexpMatches,
+ pattern: /[^/]*/
+ },
int: {
- decode: function(val) {
- return parseInt(val, 10);
- },
- is: function(val) {
- if (!isDefined(val)) return false;
- return this.decode(val.toString()) === val;
- },
+ encode: valToString,
+ decode: function(val) { return parseInt(val, 10); },
+ is: function(val) { return isDefined(val) && this.decode(val.toString()) === val; },
pattern: /\d+/
},
bool: {
- encode: function(val) {
- return val ? 1 : 0;
- },
- decode: function(val) {
- return parseInt(val, 10) === 0 ? false : true;
- },
- is: function(val) {
- return val === true || val === false;
- },
+ encode: function(val) { return val ? 1 : 0; },
+ decode: function(val) { return parseInt(val, 10) !== 0; },
+ is: function(val) { return val === true || val === false; },
pattern: /0|1/
},
- string: {
- pattern: /[^\/]*/
- },
date: {
- equals: function (a, b) {
- return a.toISOString() === b.toISOString();
- },
- decode: function (val) {
- return new Date(val);
- },
encode: function (val) {
- return [
- val.getFullYear(),
+ if (!this.is(val))
+ return undefined;
+ return [ val.getFullYear(),
('0' + (val.getMonth() + 1)).slice(-2),
('0' + val.getDate()).slice(-2)
].join("-");
},
- pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/
+ decode: function (val) {
+ if (this.is(val)) return val;
+ var match = this.capture.exec(val);
+ return match ? new Date(match[1], match[2] - 1, match[3]) : undefined;
+ },
+ is: function(val) { return val instanceof Date && !isNaN(val.valueOf()); },
+ equals: function (a, b) { return this.is(a) && this.is(b) && a.toISOString() === b.toISOString(); },
+ pattern: /[0-9]{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[1-2][0-9]|3[0-1])/,
+ capture: /([0-9]{4})-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])/
+ },
+ json: {
+ encode: angular.toJson,
+ decode: angular.fromJson,
+ is: angular.isObject,
+ equals: angular.equals,
+ pattern: /[^/]*/
+ },
+ any: { // does not encode/decode
+ encode: angular.identity,
+ decode: angular.identity,
+ is: angular.identity,
+ equals: angular.equals,
+ pattern: /.*/
}
};
@@ -1127,9 +1320,12 @@ function $UrlMatcherFactory() {
* Defines whether URL matching should be case sensitive (the default behavior), or not.
*
* @param {boolean} value `false` to match URL in a case sensitive manner; otherwise `true`;
+ * @returns {boolean} the current value of caseInsensitive
*/
this.caseInsensitive = function(value) {
- isCaseInsensitive = value;
+ if (isDefined(value))
+ isCaseInsensitive = value;
+ return isCaseInsensitive;
};
/**
@@ -1140,10 +1336,36 @@ function $UrlMatcherFactory() {
* @description
* Defines whether URLs should match trailing slashes, or not (the default behavior).
*
- * @param {boolean} value `false` to match trailing slashes in URLs, otherwise `true`.
+ * @param {boolean=} value `false` to match trailing slashes in URLs, otherwise `true`.
+ * @returns {boolean} the current value of strictMode
*/
this.strictMode = function(value) {
- isStrictMode = value;
+ if (isDefined(value))
+ isStrictMode = value;
+ return isStrictMode;
+ };
+
+ /**
+ * @ngdoc function
+ * @name ui.router.util.$urlMatcherFactory#defaultSquashPolicy
+ * @methodOf ui.router.util.$urlMatcherFactory
+ *
+ * @description
+ * Sets the default behavior when generating or matching URLs with default parameter values.
+ *
+ * @param {string} value A string that defines the default parameter URL squashing behavior.
+ * `nosquash`: When generating an href with a default parameter value, do not squash the parameter value from the URL
+ * `slash`: When generating an href with a default parameter value, squash (remove) the parameter value, and, if the
+ * parameter is surrounded by slashes, squash (remove) one slash from the URL
+ * any other string, e.g. "~": When generating an href with a default parameter value, squash (remove)
+ * the parameter value from the URL and replace it with this string.
+ */
+ this.defaultSquashPolicy = function(value) {
+ if (!isDefined(value)) return defaultSquashPolicy;
+ if (value !== true && value !== false && !isString(value))
+ throw new Error("Invalid squash policy: " + value + ". Valid policies: false, true, arbitrary-string");
+ defaultSquashPolicy = value;
+ return value;
};
/**
@@ -1153,7 +1375,7 @@ function $UrlMatcherFactory() {
*
* @description
* Creates a {@link ui.router.util.type:UrlMatcher `UrlMatcher`} for the specified pattern.
- *
+ *
* @param {string} pattern The URL pattern.
* @param {Object} config The config object hash.
* @returns {UrlMatcher} The UrlMatcher.
@@ -1196,8 +1418,11 @@ function $UrlMatcherFactory() {
* generate URLs with typed parameters.
*
* @param {string} name The type name.
- * @param {Object|Function} def The type definition. See
+ * @param {Object|Function} definition The type definition. See
* {@link ui.router.util.type:Type `Type`} for information on the values accepted.
+ * @param {Object|Function} definitionFn (optional) A function that is injected before the app
+ * runtime starts. The result of this function is merged into the existing `definition`.
+ * See {@link ui.router.util.type:Type `Type`} for information on the values accepted.
*
* @returns {Object} Returns `$urlMatcherFactoryProvider`.
*
@@ -1245,7 +1470,7 @@ function $UrlMatcherFactory() {
* // Defines a custom type that gets a value from a service,
* // where each service gets different types of values from
* // a backend API:
- * $urlMatcherFactoryProvider.type('dbObject', function(Users, Posts) {
+ * $urlMatcherFactoryProvider.type('dbObject', {}, function(Users, Posts) {
*
* // Matches up services to URL parameter names
* var services = {
@@ -1290,42 +1515,195 @@ function $UrlMatcherFactory() {
* });
*
*/
- this.type = function (name, def) {
- if (!isDefined(def)) return UrlMatcher.prototype.$types[name];
- typeQueue.push({ name: name, def: def });
- if (!enqueue) flushTypeQueue();
+ this.type = function (name, definition, definitionFn) {
+ if (!isDefined(definition)) return $types[name];
+ if ($types.hasOwnProperty(name)) throw new Error("A type named '" + name + "' has already been defined.");
+
+ $types[name] = new Type(extend({ name: name }, definition));
+ if (definitionFn) {
+ typeQueue.push({ name: name, def: definitionFn });
+ if (!enqueue) flushTypeQueue();
+ }
return this;
};
+ // `flushTypeQueue()` waits until `$urlMatcherFactory` is injected before invoking the queued `definitionFn`s
+ function flushTypeQueue() {
+ while(typeQueue.length) {
+ var type = typeQueue.shift();
+ if (type.pattern) throw new Error("You cannot override a type's .pattern at runtime.");
+ angular.extend($types[type.name], injector.invoke(type.def));
+ }
+ }
+
+ // Register default types. Store them in the prototype of $types.
+ forEach(defaultTypes, function(type, name) { $types[name] = new Type(extend({name: name}, type)); });
+ $types = inherit($types, {});
+
/* No need to document $get, since it returns this */
this.$get = ['$injector', function ($injector) {
injector = $injector;
enqueue = false;
- UrlMatcher.prototype.$types = {};
flushTypeQueue();
forEach(defaultTypes, function(type, name) {
- if (!UrlMatcher.prototype.$types[name]) UrlMatcher.prototype.$types[name] = new Type(type);
+ if (!$types[name]) $types[name] = new Type(type);
});
return this;
}];
- // To ensure proper order of operations in object configuration, and to allow internal
- // types to be overridden, `flushTypeQueue()` waits until `$urlMatcherFactory` is injected
- // before actually wiring up and assigning type definitions
- function flushTypeQueue() {
- forEach(typeQueue, function(type) {
- if (UrlMatcher.prototype.$types[type.name]) {
- throw new Error("A type named '" + type.name + "' has already been defined.");
+ this.Param = function Param(id, type, config, location) {
+ var self = this;
+ config = unwrapShorthand(config);
+ type = getType(config, type, location);
+ var arrayMode = getArrayMode();
+ type = arrayMode ? type.$asArray(arrayMode, location === "search") : type;
+ if (type.name === "string" && !arrayMode && location === "path" && config.value === undefined)
+ config.value = ""; // for 0.2.x; in 0.3.0+ do not automatically default to ""
+ var isOptional = config.value !== undefined;
+ var squash = getSquashPolicy(config, isOptional);
+ var replace = getReplace(config, arrayMode, isOptional, squash);
+
+ function unwrapShorthand(config) {
+ var keys = isObject(config) ? objectKeys(config) : [];
+ var isShorthand = indexOf(keys, "value") === -1 && indexOf(keys, "type") === -1 &&
+ indexOf(keys, "squash") === -1 && indexOf(keys, "array") === -1;
+ if (isShorthand) config = { value: config };
+ config.$$fn = isInjectable(config.value) ? config.value : function () { return config.value; };
+ return config;
+ }
+
+ function getType(config, urlType, location) {
+ if (config.type && urlType) throw new Error("Param '"+id+"' has two type configurations.");
+ if (urlType) return urlType;
+ if (!config.type) return (location === "config" ? $types.any : $types.string);
+ return config.type instanceof Type ? config.type : new Type(config.type);
+ }
+
+ // array config: param name (param[]) overrides default settings. explicit config overrides param name.
+ function getArrayMode() {
+ var arrayDefaults = { array: (location === "search" ? "auto" : false) };
+ var arrayParamNomenclature = id.match(/\[\]$/) ? { array: true } : {};
+ return extend(arrayDefaults, arrayParamNomenclature, config).array;
+ }
+
+ /**
+ * returns false, true, or the squash value to indicate the "default parameter url squash policy".
+ */
+ function getSquashPolicy(config, isOptional) {
+ var squash = config.squash;
+ if (!isOptional || squash === false) return false;
+ if (!isDefined(squash) || squash == null) return defaultSquashPolicy;
+ if (squash === true || isString(squash)) return squash;
+ throw new Error("Invalid squash policy: '" + squash + "'. Valid policies: false, true, or arbitrary string");
+ }
+
+ function getReplace(config, arrayMode, isOptional, squash) {
+ var replace, configuredKeys, defaultPolicy = [
+ { from: "", to: (isOptional || arrayMode ? undefined : "") },
+ { from: null, to: (isOptional || arrayMode ? undefined : "") }
+ ];
+ replace = isArray(config.replace) ? config.replace : [];
+ if (isString(squash))
+ replace.push({ from: squash, to: undefined });
+ configuredKeys = map(replace, function(item) { return item.from; } );
+ return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace);
+ }
+
+ /**
+ * [Internal] Get the default value of a parameter, which may be an injectable function.
+ */
+ function $$getDefaultValue() {
+ if (!injector) throw new Error("Injectable functions cannot be called at configuration time");
+ return injector.invoke(config.$$fn);
+ }
+
+ /**
+ * [Internal] Gets the decoded representation of a value if the value is defined, otherwise, returns the
+ * default value, which may be the result of an injectable function.
+ */
+ function $value(value) {
+ function hasReplaceVal(val) { return function(obj) { return obj.from === val; }; }
+ function $replace(value) {
+ var replacement = map(filter(self.replace, hasReplaceVal(value)), function(obj) { return obj.to; });
+ return replacement.length ? replacement[0] : value;
}
- var def = new Type(isInjectable(type.def) ? injector.invoke(type.def) : type.def);
- UrlMatcher.prototype.$types[type.name] = def;
+ value = $replace(value);
+ return isDefined(value) ? self.type.decode(value) : $$getDefaultValue();
+ }
+
+ function toString() { return "{Param:" + id + " " + type + " squash: '" + squash + "' optional: " + isOptional + "}"; }
+
+ extend(this, {
+ id: id,
+ type: type,
+ location: location,
+ array: arrayMode,
+ squash: squash,
+ replace: replace,
+ isOptional: isOptional,
+ value: $value,
+ dynamic: undefined,
+ config: config,
+ toString: toString
});
+ };
+
+ function ParamSet(params) {
+ extend(this, params || {});
}
+
+ ParamSet.prototype = {
+ $$new: function() {
+ return inherit(this, extend(new ParamSet(), { $$parent: this}));
+ },
+ $$keys: function () {
+ var keys = [], chain = [], parent = this,
+ ignore = objectKeys(ParamSet.prototype);
+ while (parent) { chain.push(parent); parent = parent.$$parent; }
+ chain.reverse();
+ forEach(chain, function(paramset) {
+ forEach(objectKeys(paramset), function(key) {
+ if (indexOf(keys, key) === -1 && indexOf(ignore, key) === -1) keys.push(key);
+ });
+ });
+ return keys;
+ },
+ $$values: function(paramValues) {
+ var values = {}, self = this;
+ forEach(self.$$keys(), function(key) {
+ values[key] = self[key].value(paramValues && paramValues[key]);
+ });
+ return values;
+ },
+ $$equals: function(paramValues1, paramValues2) {
+ var equal = true, self = this;
+ forEach(self.$$keys(), function(key) {
+ var left = paramValues1 && paramValues1[key], right = paramValues2 && paramValues2[key];
+ if (!self[key].type.equals(left, right)) equal = false;
+ });
+ return equal;
+ },
+ $$validates: function $$validate(paramValues) {
+ var result = true, isOptional, val, param, self = this;
+
+ forEach(this.$$keys(), function(key) {
+ param = self[key];
+ val = paramValues[key];
+ isOptional = !val && param.isOptional;
+ result = result && (isOptional || !!param.type.is(val));
+ });
+ return result;
+ },
+ $$parent: undefined
+ };
+
+ this.ParamSet = ParamSet;
}
// Register as a provider so it's available to other providers
angular.module('ui.router.util').provider('$urlMatcherFactory', $UrlMatcherFactory);
+angular.module('ui.router.util').run(['$urlMatcherFactory', function($urlMatcherFactory) { }]);
/**
* @ngdoc object
@@ -1594,7 +1972,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
$get.$inject = ['$location', '$rootScope', '$injector', '$browser'];
function $get( $location, $rootScope, $injector, $browser) {
- var baseHref = $browser.baseHref(), location = $location.url();
+ var baseHref = $browser.baseHref(), location = $location.url(), lastPushedUrl;
function appendBasePath(url, isHtml5, absolute) {
if (baseHref === '/') return url;
@@ -1606,6 +1984,9 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
function update(evt) {
if (evt && evt.defaultPrevented) return;
+ var ignoreUpdate = lastPushedUrl && $location.url() === lastPushedUrl;
+ lastPushedUrl = undefined;
+ if (ignoreUpdate) return true;
function check(rule) {
var handled = rule($injector, $location);
@@ -1678,6 +2059,7 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
push: function(urlMatcher, params, options) {
$location.url(urlMatcher.format(params || {}));
+ lastPushedUrl = options && options.$$avoidResync ? $location.url() : undefined;
if (options && options.replace) $location.replace();
},
@@ -1710,6 +2092,10 @@ function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
if (!urlMatcher.validates(params)) return null;
var isHtml5 = $locationProvider.html5Mode();
+ if (angular.isObject(isHtml5)) {
+ isHtml5 = isHtml5.enabled;
+ }
+
var url = urlMatcher.format(params);
options = options || {};
@@ -1799,12 +2185,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
return state.url ? state : (state.parent ? state.parent.navigable : null);
},
+ // Own parameters for this state. state.url.params is already built at this point. Create and add non-url params
+ ownParams: function(state) {
+ var params = state.url && state.url.params || new $$UMFP.ParamSet();
+ forEach(state.params || {}, function(config, id) {
+ if (!params[id]) params[id] = new $$UMFP.Param(id, null, config, "config");
+ });
+ return params;
+ },
+
// Derive parameters for this state and ensure they're a super-set of parent's parameters
params: function(state) {
- if (!state.params) {
- return state.url ? state.url.params : state.parent.params;
- }
- return state.params;
+ return state.parent && state.parent.params ? extend(state.parent.params.$$new(), state.ownParams) : new $$UMFP.ParamSet();
},
// If there is no explicit multi-view configuration, make one up so we don't have
@@ -1822,28 +2214,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
return views;
},
- ownParams: function(state) {
- state.params = state.params || {};
-
- if (!state.parent) {
- return objectKeys(state.params);
- }
- var paramNames = {}; forEach(state.params, function (v, k) { paramNames[k] = true; });
-
- forEach(state.parent.params, function (v, k) {
- if (!paramNames[k]) {
- throw new Error("Missing required parameter '" + k + "' in state '" + state.name + "'");
- }
- paramNames[k] = false;
- });
- var ownParams = [];
-
- forEach(paramNames, function (own, p) {
- if (own) ownParams.push(p);
- });
- return ownParams;
- },
-
// Keep a full path from the root down to this state as this is needed for state activation.
path: function(state) {
return state.parent ? state.parent.path.concat(state) : []; // exclude root from path
@@ -1872,6 +2242,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
if (path) {
if (!base) throw new Error("No reference point given for path '" + name + "'");
+ base = findState(base);
+
var rel = name.split("."), i = 0, pathLength = rel.length, current = base;
for (; i < pathLength; i++) {
@@ -1904,6 +2276,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
queue[parentName].push(state);
}
+ function flushQueuedChildren(parentName) {
+ var queued = queue[parentName] || [];
+ while(queued.length) {
+ registerState(queued.shift());
+ }
+ }
+
function registerState(state) {
// Wrap a new object around the state so we can store our private details easily.
state = inherit(state, {
@@ -1919,6 +2298,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
// Get parent name
var parentName = (name.indexOf('.') !== -1) ? name.substring(0, name.lastIndexOf('.'))
: (isString(state.parent)) ? state.parent
+ : (isObject(state.parent) && isString(state.parent.name)) ? state.parent.name
: '';
// If parent is not registered yet, add state to queue and register later
@@ -1935,17 +2315,13 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
if (!state[abstractKey] && state.url) {
$urlRouterProvider.when(state.url, ['$match', '$stateParams', function ($match, $stateParams) {
if ($state.$current.navigable != state || !equalForKeys($match, $stateParams)) {
- $state.transitionTo(state, $match, { location: false });
+ $state.transitionTo(state, $match, { inherit: true, location: false });
}
}]);
}
// Register any queued children
- if (queue[name]) {
- for (var i = 0; i < queue[name].length; i++) {
- registerState(queue[name][i]);
- }
- }
+ flushQueuedChildren(name);
return state;
}
@@ -1962,12 +2338,12 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
//match greedy starts
if (globSegments[0] === '**') {
- segments = segments.slice(segments.indexOf(globSegments[1]));
+ segments = segments.slice(indexOf(segments, globSegments[1]));
segments.unshift('**');
}
//match greedy ends
if (globSegments[globSegments.length - 1] === '**') {
- segments.splice(segments.indexOf(globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE);
+ segments.splice(indexOf(segments, globSegments[globSegments.length - 2]) + 1, Number.MAX_VALUE);
segments.push('**');
}
@@ -2112,9 +2488,12 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* Registers a state configuration under a given state name. The stateConfig object
* has the following acceptable properties.
*
+ * @param {string} name A unique state name, e.g. "home", "about", "contacts".
+ * To create a parent/child state use a dot, e.g. "about.sales", "home.newest".
+ * @param {object} stateConfig State configuration object.
+ * @param {string|function=} stateConfig.template
*
- *
- * - **`template`** - {string|function=} - html template as a string or a function that returns
+ * html template as a string or a function that returns
* an html template as a string which should be used by the uiView directives. This property
* takes precedence over templateUrl.
*
@@ -2123,9 +2502,17 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* - {array.<object>} - state parameters extracted from the current $location.path() by
* applying the current state
*
+ *
template:
+ * "
inline template definition
" +
+ * ""
+ *
template: function(params) {
+ * return "
generated template
"; }
+ *
+ *
+ * @param {string|function=} stateConfig.templateUrl
*
*
- * - **`templateUrl`** - {string|function=} - path or function that returns a path to an html
+ * path or function that returns a path to an html
* template that should be used by uiView.
*
* If `templateUrl` is a function, it will be called with the following parameters:
@@ -2133,84 +2520,250 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* - {array.<object>} - state parameters extracted from the current $location.path() by
* applying the current state
*
- *
+ *
*
+ * @param {string|function=} stateConfig.controller
*
*
- * - **`controller`** - {string|function=} - Controller fn that should be associated with newly
+ * Controller fn that should be associated with newly
* related scope or the name of a registered controller if passed as a string.
- *
+ * Optionally, the ControllerAs may be declared here.
+ *
controller: "MyRegisteredController"
+ *
controller:
+ * "MyRegisteredController as fooCtrl"}
+ *
+ * @param {function=} stateConfig.controllerProvider
*
*
- * - **`controllerProvider`** - {function=} - Injectable provider function that returns
- * the actual controller or string.
+ * Injectable provider function that returns the actual controller or string.
+ *
*
+ * @param {string=} stateConfig.controllerAs
*
*
- * - **`controllerAs`** – {string=} – A controller alias name. If present the controller will be
+ * A controller alias name. If present the controller will be
* published to scope under the controllerAs name.
+ *
controllerAs: "myCtrl"
*
+ * @param {object=} stateConfig.resolve
*
*
- * - **`resolve`** - {object.<string, function>=} - An optional map of dependencies which
+ * An optional map<string, function> of dependencies which
* should be injected into the controller. If any of these dependencies are promises,
- * the router will wait for them all to be resolved or one to be rejected before the
- * controller is instantiated. If all the promises are resolved successfully, the values
- * of the resolved promises are injected and $stateChangeSuccess event is fired. If any
- * of the promises are rejected the $stateChangeError event is fired. The map object is:
+ * the router will wait for them all to be resolved before the controller is instantiated.
+ * If all the promises are resolved successfully, the $stateChangeSuccess event is fired
+ * and the values of the resolved promises are injected into any controllers that reference them.
+ * If any of the promises are rejected the $stateChangeError event is fired.
+ *
+ * The map object is:
*
* - key - {string}: name of dependency to be injected into controller
* - factory - {string|function}: If string then it is alias for service. Otherwise if function,
* it is injected and return value it treated as dependency. If result is a promise, it is
* resolved before its value is injected into controller.
*
+ *
+ *
+ * @param {string=} stateConfig.url
*
*
- * - **`url`** - {string=} - A url with optional parameters. When a state is navigated or
+ * A url fragment with optional parameters. When a state is navigated or
* transitioned to, the `$stateParams` service will be populated with any
* parameters that were passed.
*
- *
- *
- * - **`params`** - {object=} - An array of parameter names or regular expressions. Only
- * use this within a state if you are not using url. Otherwise you can specify your
- * parameters within the url. When a state is navigated or transitioned to, the
- * $stateParams service will be populated with any parameters that were passed.
- *
+ * examples:
+ *
*
- * - **`views`** - {object=} - Use the views property to set up multiple views or to target views
- * manually/explicitly.
- *
+ * @param {boolean=} [stateConfig.abstract=false]
*
- *
- * - **`abstract`** - {boolean=} - An abstract state will never be directly activated,
+ * An abstract state will never be directly activated,
* but can provide inherited properties to its common children states.
+ *
abstract: true
*
+ * @param {function=} stateConfig.onEnter
*
*
- * - **`onEnter`** - {object=} - Callback function for when a state is entered. Good way
+ * Callback function for when a state is entered. Good way
* to trigger an action or dispatch an event, such as opening a dialog.
- * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax.
+ * If minifying your scripts, make sure to explictly annotate this function,
+ * because it won't be automatically annotated by your build tools.
+ *
+ *
*
+ * @param {function=} stateConfig.onExit
*
*
- * - **`onExit`** - {object=} - Callback function for when a state is exited. Good way to
+ * Callback function for when a state is exited. Good way to
* trigger an action or dispatch an event, such as opening a dialog.
- * If minifying your scripts, make sure to use the `['injection1', 'injection2', function(injection1, injection2){}]` syntax.
+ * If minifying your scripts, make sure to explictly annotate this function,
+ * because it won't be automatically annotated by your build tools.
+ *
+ *
*
+ * @param {boolean=} [stateConfig.reloadOnSearch=true]
*
*
- * - **`reloadOnSearch = true`** - {boolean=} - If `false`, will not retrigger the same state
+ * If `false`, will not retrigger the same state
* just because a search/query parameter has changed (via $location.search() or $location.hash()).
* Useful for when you'd like to modify $location.search() without triggering a reload.
+ *
reloadOnSearch: false
*
+ * @param {object=} stateConfig.data
*
*
- * - **`data`** - {object=} - Arbitrary data object, useful for custom configuration.
+ * Arbitrary data object, useful for custom configuration. The parent state's `data` is
+ * prototypally inherited. In other words, adding a data property to a state adds it to
+ * the entire subtree via prototypal inheritance.
+ *
+ *
data: {
+ * requiredRole: 'foo'
+ * }
+ *
+ * @param {object=} stateConfig.params
+ *
+ *
+ * A map which optionally configures parameters declared in the `url`, or
+ * defines additional non-url parameters. For each parameter being
+ * configured, add a configuration object keyed to the name of the parameter.
+ *
+ * Each parameter configuration object may contain the following properties:
+ *
+ * - ** value ** - {object|function=}: specifies the default value for this
+ * parameter. This implicitly sets this parameter as optional.
+ *
+ * When UI-Router routes to a state and no value is
+ * specified for this parameter in the URL or transition, the
+ * default value will be used instead. If `value` is a function,
+ * it will be injected and invoked, and the return value used.
+ *
+ * *Note*: `undefined` is treated as "no default value" while `null`
+ * is treated as "the default value is `null`".
+ *
+ * *Shorthand*: If you only need to configure the default value of the
+ * parameter, you may use a shorthand syntax. In the **`params`**
+ * map, instead mapping the param name to a full parameter configuration
+ * object, simply set map it to the default parameter value, e.g.:
+ *
+ *
+ *
+ * - ** array ** - {boolean=}: *(default: false)* If true, the param value will be
+ * treated as an array of values. If you specified a Type, the value will be
+ * treated as an array of the specified Type. Note: query parameter values
+ * default to a special `"auto"` mode.
+ *
+ * For query parameters in `"auto"` mode, if multiple values for a single parameter
+ * are present in the URL (e.g.: `/foo?bar=1&bar=2&bar=3`) then the values
+ * are mapped to an array (e.g.: `{ foo: [ '1', '2', '3' ] }`). However, if
+ * only one value is present (e.g.: `/foo?bar=1`) then the value is treated as single
+ * value (e.g.: `{ foo: '1' }`).
+ *
+ *
params: {
+ * param1: { array: true }
+ * }
+ *
+ * - ** squash ** - {bool|string=}: `squash` configures how a default parameter value is represented in the URL when
+ * the current parameter value is the same as the default value. If `squash` is not set, it uses the
+ * configured default squash policy.
+ * (See {@link ui.router.util.$urlMatcherFactory#methods_defaultSquashPolicy `defaultSquashPolicy()`})
+ *
+ * There are three squash settings:
+ *
+ * - false: The parameter's default value is not squashed. It is encoded and included in the URL
+ * - true: The parameter's default value is omitted from the URL. If the parameter is preceeded and followed
+ * by slashes in the state's `url` declaration, then one of those slashes are omitted.
+ * This can allow for cleaner looking URLs.
+ * - `""`: The parameter's default value is replaced with an arbitrary placeholder of your choice.
+ *
+ *
@@ -2219,7 +2772,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* // stateName can be a single top-level name (must be unique).
* $stateProvider.state("home", {});
*
- * // Or it can be a nested state name. This state is a child of the
+ * // Or it can be a nested state name. This state is a child of the
* // above "home" state.
* $stateProvider.state("home.newest", {});
*
@@ -2233,9 +2786,6 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* .state("contacts", {});
*
*
- * @param {string} name A unique state name, e.g. "home", "about", "contacts".
- * To create a parent/child state use a dot, e.g. "about.sales", "home.newest".
- * @param {object} definition State configuration object.
*/
this.state = state;
function state(name, definition) {
@@ -2271,8 +2821,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* you're coming from.
*/
this.$get = $get;
- $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter'];
- function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) {
+ $get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter', '$location', '$urlMatcherFactory'];
+ function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter, $location, $urlMatcherFactory) {
var TransitionSuperseded = $q.reject(new Error('transition superseded'));
var TransitionPrevented = $q.reject(new Error('transition prevented'));
@@ -2376,12 +2926,15 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
* `reload()` is just an alias for:
*
+ *
+ * @returns {promise} A promise representing the state of the new transition. See
+ * {@link ui.router.state.$state#methods_go $state.go}.
*/
$state.reload = function reload() {
- $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: false });
+ return $state.transitionTo($state.current, $stateParams, { reload: true, inherit: false, notify: true });
};
/**
@@ -2523,6 +3076,9 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
}
if (toState[abstractKey]) throw new Error("Cannot transition to abstract state '" + to + "'");
if (options.inherit) toParams = inheritParams($stateParams, toParams || {}, $state.$current, toState);
+ if (!toState.params.$$validates(toParams)) return TransitionFailed;
+
+ toParams = toState.params.$$values(toParams);
to = toState;
var toPath = to.path;
@@ -2531,7 +3087,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
var keep = 0, state = toPath[keep], locals = root.locals, toLocals = [];
if (!options.reload) {
- while (state && state === fromPath[keep] && equalForKeys(toParams, fromParams, state.ownParams)) {
+ while (state && state === fromPath[keep] && state.ownParams.$$equals(toParams, fromParams)) {
locals = toLocals[keep] = state.locals;
keep++;
state = toPath[keep];
@@ -2550,7 +3106,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
}
// Filter parameters before we pass them to event handlers etc.
- toParams = filterByKeys(objectKeys(to.params), toParams || {});
+ toParams = filterByKeys(to.params.$$keys(), toParams || {});
// Broadcast start event and cancel the transition if requested
if (options.notify) {
@@ -2598,7 +3154,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
for (var l = keep; l < toPath.length; l++, state = toPath[l]) {
locals = toLocals[l] = inherit(locals);
- resolved = resolveState(state, toParams, state === to, resolved, locals);
+ resolved = resolveState(state, toParams, state === to, resolved, locals, options);
}
// Once everything is resolved, we are ready to perform the actual transition
@@ -2640,7 +3196,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
if (options.location && to.navigable) {
$urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, {
- replace: options.location === 'replace'
+ $$avoidResync: true, replace: options.location === 'replace'
});
}
@@ -2705,8 +3261,8 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
*
* @description
* Similar to {@link ui.router.state.$state#methods_includes $state.includes},
- * but only checks for the full state name. If params is supplied then it will be
- * tested for strict equality against the current active params object, so all params
+ * but only checks for the full state name. If params is supplied then it will be
+ * tested for strict equality against the current active params object, so all params
* must match with none missing and no extras.
*
* @example
@@ -2722,23 +3278,23 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
*
-
-
- ## 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 ng.$animate#addClass $animate.addClass} and
- {@link ng.$animate#removeClass $animate.removeClass}.
*/
-var ngClassDirective = classDirective('', true);
+
/**
* @ngdoc directive
- * @name ngClassOdd
- * @restrict AC
+ * @name ngKeypress
*
* @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}.
+ * Specify custom behavior on keypress event.
*
* @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.
+ * @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
-
-
-
- {{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/);
- });
+
+ key press count: {{count}}
*/
-var ngClassOddDirective = classDirective('Odd', 0);
+
/**
* @ngdoc directive
- * @name ngClassEven
- * @restrict AC
+ * @name ngSubmit
*
* @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.
+ * Enables binding angular expressions to onsubmit events.
*
- * This directive can be applied only within the scope of an
- * {@link ng.directive:ngRepeat ngRepeat}.
+ * 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.
*
- * @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.
+ *
+ * **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.
+ *
+ *
+ * @element form
+ * @priority 0
+ * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
+ * ({@link guide/expression#-event- Event object is available as `$event`})
*
* @example
-
+
-
-
- 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/);
+ 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');
+ });
*/
-var ngClassEvenDirective = classDirective('Even', 1);
/**
* @ngdoc directive
- * @name ngCloak
- * @restrict AC
+ * @name ngFocus
*
* @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.
+ * Specify custom behavior on focus event.
*
- * 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.
+ * 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.
*
- * `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}).
+ * @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`})
*
- * ```css
- * [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
- * display: none !important;
- * }
- * ```
+ * @example
+ * See {@link ng.directive:ngClick ngClick}
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngBlur
*
- * 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.
+ * @description
+ * Specify custom behavior on blur event.
*
- * 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.
+ * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
+ * an element has lost focus.
*
- * 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.
+ * 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.
*
- * @element ANY
+ * @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`})
+ *
+ * @example
+ * See {@link ng.directive:ngClick ngClick}
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngCopy
+ *
+ * @description
+ * Specify custom behavior on copy event.
+ *
+ * @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`})
*
* @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();
- });
+
+ copied: {{copied}}
- *
*/
-var ngCloakDirective = ngDirective({
- compile: function(element, attr) {
- attr.$set('ngCloak', undefined);
- element.removeClass('ng-cloak');
- }
-});
/**
* @ngdoc directive
- * @name ngController
+ * @name ngCut
*
* @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 — Models are the properties of a scope; 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
- * @priority 500
- * @param {expression} ngController Name of a constructor function registered with the current
- * {@link ng.$controllerProvider $controllerProvider} 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
- * `ng-controller="as propertyName"`.
+ * Specify custom behavior on cut event.
*
- * If the current `$controllerProvider` is configured to use globals (via
- * {@link ng.$controllerProvider#allowGlobals `$controllerProvider.allowGlobals()` }), this may
- * also be the name of a globally accessible constructor function (not recommended).
+ * @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`})
*
* @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. Any changes to the data are automatically reflected
- * in the View without the need for a manual update.
- *
- * Two different declaration styles are included below:
- *
- * * one binds methods and properties directly onto the controller using `this`:
- * `ng-controller="SettingsController1 as settings"`
- * * one injects `$scope` into the controller:
- * `ng-controller="SettingsController2"`
- *
- * The second option is more common in the Angular community, and is generally used in boilerplates
- * and in this guide. However, there are advantages to binding properties directly to the controller
- * and avoiding scope.
- *
- * * Using `controller as` makes it obvious which controller you are accessing in the template when
- * multiple controllers apply to an element.
- * * If you are writing your controllers as classes you have easier access to the properties and
- * methods, which will appear on the scope, from inside the controller code.
- * * Since there is always a `.` in the bindings, you don't have to worry about prototypal
- * inheritance masking primitives.
- *
- * This example demonstrates the `controller as` syntax.
- *
- *
- *
- *
- *
- *
- * angular.module('controllerAsExample', [])
- * .controller('SettingsController1', SettingsController1);
- *
- * function SettingsController1() {
- * this.name = "John Smith";
- * this.contacts = [
- * {type: 'phone', value: '408 555 1212'},
- * {type: 'email', value: 'john.smith@example.org'} ];
- * }
- *
- * SettingsController1.prototype.greet = function() {
- * alert(this.name);
- * };
+
+
+
+ cut: {{cut}}
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngPaste
*
- * SettingsController1.prototype.addContact = function() {
- * this.contacts.push({type: 'email', value: 'yourname@example.org'});
- * };
+ * @description
+ * Specify custom behavior on paste event.
*
- * SettingsController1.prototype.removeContact = function(contactToRemove) {
- * var index = this.contacts.indexOf(contactToRemove);
- * this.contacts.splice(index, 1);
- * };
+ * @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`})
*
- * SettingsController1.prototype.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- *
- *
- * it('should check controller as', function() {
- * var container = element(by.id('ctrl-as-exmpl'));
- * expect(container.element(by.model('settings.name'))
- * .getAttribute('value')).toBe('John Smith');
+ * @example
+
+
+
+ pasted: {{paste}}
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngIf
+ * @restrict A
*
- * var firstRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in settings.contacts').row(1));
+ * @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.
*
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
+ * `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.
*
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
+ * 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.
*
- * firstRepeat.element(by.linkText('clear')).click();
+ * 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.
*
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
+ * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
+ * and `leave` effects.
*
- * container.element(by.linkText('add')).click();
+ * @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
*
- * expect(container.element(by.repeater('contact in settings.contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- *
- *
+ * @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.
*
- * This example demonstrates the "attach to `$scope`" style of controller.
- *
- *
- *
- *
- *
- *
- * angular.module('controllerExample', [])
- * .controller('SettingsController2', ['$scope', SettingsController2]);
- *
- * function SettingsController2($scope) {
- * $scope.name = "John Smith";
- * $scope.contacts = [
- * {type:'phone', value:'408 555 1212'},
- * {type:'email', value:'john.smith@example.org'} ];
- *
- * $scope.greet = function() {
- * alert($scope.name);
- * };
- *
- * $scope.addContact = function() {
- * $scope.contacts.push({type:'email', value:'yourname@example.org'});
- * };
- *
- * $scope.removeContact = function(contactToRemove) {
- * var index = $scope.contacts.indexOf(contactToRemove);
- * $scope.contacts.splice(index, 1);
- * };
- *
- * $scope.clearContact = function(contact) {
- * contact.type = 'phone';
- * contact.value = '';
- * };
- * }
- *
- *
- * it('should check controller', function() {
- * var container = element(by.id('ctrl-exmpl'));
- *
- * expect(container.element(by.model('name'))
- * .getAttribute('value')).toBe('John Smith');
- *
- * var firstRepeat =
- * container.element(by.repeater('contact in contacts').row(0));
- * var secondRepeat =
- * container.element(by.repeater('contact in contacts').row(1));
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('408 555 1212');
- * expect(secondRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('john.smith@example.org');
- *
- * firstRepeat.element(by.linkText('clear')).click();
- *
- * expect(firstRepeat.element(by.model('contact.value')).getAttribute('value'))
- * .toBe('');
- *
- * container.element(by.linkText('add')).click();
- *
- * expect(container.element(by.repeater('contact in contacts').row(2))
- * .element(by.model('contact.value'))
- * .getAttribute('value'))
- * .toBe('yourname@example.org');
- * });
- *
- *
+ * @example
+
+
+ Click me:
+ Show when checked:
+
+ This is 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 ngControllerDirective = [function() {
+var ngIfDirective = ['$animate', function($animate) {
return {
+ multiElement: true,
+ transclude: 'element',
+ priority: 600,
+ terminal: true,
restrict: 'A',
- scope: true,
- controller: '@',
- priority: 500
+ $$tlb: true,
+ link: function($scope, $element, $attr, ctrl, $transclude) {
+ var block, childScope, previousElements;
+ $scope.$watch($attr.ngIf, function ngIfWatchAction(value) {
+
+ if (value) {
+ if (!childScope) {
+ $transclude(function(clone, newScope) {
+ childScope = newScope;
+ 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 its 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 = getBlockNodes(block.clone);
+ $animate.leave(previousElements).then(function() {
+ previousElements = null;
+ });
+ block = null;
+ }
+ }
+ });
+ }
};
}];
/**
* @ngdoc directive
- * @name ngCsp
+ * @name ngInclude
+ * @restrict ECA
*
- * @element html
* @description
- * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
- *
- * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
+ * Fetches, compiles and includes an external HTML fragment.
*
- * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
- * For Angular to be CSP compatible there are only two things that we need to do differently:
+ * 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 Angular's {@link
+ * ng.$sce Strict Contextual Escaping}.
*
- * - don't use `Function` constructor to generate optimized value getters
- * - don't inject custom stylesheet into the document
+ * 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.
*
- * 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.
+ * @animations
+ * enter - animation is used to bring new content into the browser.
+ * leave - animation is used to animate existing content away.
*
- * 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.
+ * The enter and leave animation occur concurrently.
*
- * Angular tries to autodetect if CSP is active and automatically turn on the CSP-safe mode. This
- * autodetection however triggers a CSP error to be logged in the console:
+ * @scope
+ * @priority 400
*
- * ```
- * Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of
- * script in the following Content Security Policy directive: "default-src 'self'". Note that
- * 'script-src' was not explicitly set, so 'default-src' is used as a fallback.
- * ```
+ * @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.
*
- * This error is harmless but annoying. To prevent the error from showing up, put the `ngCsp`
- * directive on the root element of the application or on the `angular.js` script tag, whichever
- * appears first in the html document.
+ * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
+ * $anchorScroll} to scroll the viewport after the content is loaded.
*
- * *Note: This directive is only available in the `ng-csp` and `data-ng-csp` attribute form.*
+ * - 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
- * This example shows how to apply the `ngCsp` directive to the `html` tag.
- ```html
-
-
- ...
- ...
-
- ```
- * @example
- // Note: the suffix `.csp` in the example name triggers
- // csp mode in our http server!
-
-
-
+
+
+ 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;
+ }
- function clearErrors() {
- getAndClearSevereErrors();
- }
+ .slide-animate {
+ padding:10px;
+ }
- function expectNoErrors() {
- getAndClearSevereErrors().then(function(filteredLog) {
- expect(filteredLog.length).toEqual(0);
- if (filteredLog.length) {
- console.log('browser console errors: ' + util.inspect(filteredLog));
- }
- });
- }
+ .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;
- function expectError(regex) {
- getAndClearSevereErrors().then(function(filteredLog) {
- var found = false;
- filteredLog.forEach(function(log) {
- if (log.message.match(regex)) {
- found = true;
- }
- });
- if (!found) {
- throw new Error('expected an error that matches ' + regex);
- }
- });
- }
+ position:absolute;
+ top:0;
+ left:0;
+ right:0;
+ bottom:0;
+ display:block;
+ padding:10px;
+ }
- beforeEach(function() {
- util = require('util');
- webdriver = require('protractor/node_modules/selenium-webdriver');
- });
+ .slide-animate.ng-enter {
+ top:-50px;
+ }
+ .slide-animate.ng-enter.ng-enter-active {
+ top:0;
+ }
- // For now, we only test on Chrome,
- // as Safari does not load the page with Protractor's injected scripts,
- // and Firefox webdriver always disables content security policy (#6358)
- if (browser.params.browser !== 'chrome') {
- return;
- }
+ .slide-animate.ng-leave {
+ top:0;
+ }
+ .slide-animate.ng-leave.ng-leave-active {
+ top:50px;
+ }
+
+
+ var templateSelect = element(by.model('template'));
+ var includeElem = element(by.css('[ng-include]'));
- it('should not report errors when the page is loaded', function() {
- // clear errors so we are not dependent on previous tests
- clearErrors();
- // Need to reload the page as the page is already loaded when
- // we come here
- browser.driver.getCurrentUrl().then(function(url) {
- browser.get(url);
- });
- expectNoErrors();
- });
+ it('should load template1.html', function() {
+ expect(includeElem.getText()).toMatch(/Content of template1.html/);
+ });
- it('should evaluate expressions', function() {
- expect(counter.getText()).toEqual('0');
- incBtn.click();
- expect(counter.getText()).toEqual('1');
- expectNoErrors();
- });
+ 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/);
+ });
- it('should throw and report an error when using "eval"', function() {
- evilBtn.click();
- expect(evilError.getText()).toMatch(/Content Security Policy/);
- expectError(/Content Security Policy/);
- });
-
-
- */
+ 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);
+ });
+
+
+ */
-// ngCsp is not implemented as a proper directive any more, because we need it be processed while we
-// bootstrap the system (before $parse is instantiated), for this reason we just have
-// the csp.isActive() fn that looks for ng-csp attribute anywhere in the current doc
/**
- * @ngdoc directive
- * @name ngClick
- *
+ * @ngdoc event
+ * @name ngInclude#$includeContentRequested
+ * @eventType emit on the scope ngInclude was declared in
* @description
- * The ngClick directive allows you to specify custom behavior when
- * an element is clicked.
- *
- * @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`})
+ * Emitted every time the ngInclude content is requested.
*
- * @example
-
-
-
-
- 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 collection of directives that allows creation of custom event handlers that are defined as
- * angular expressions and are compiled and executed within the current scope.
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
*/
-var ngEventDirectives = {};
-// For events that might fire synchronously during DOM manipulation
-// we need to execute their event handlers asynchronously using $evalAsync,
-// so that they are not executed in an inconsistent state.
-var forceAsyncEvents = {
- 'blur': true,
- 'focus': true
-};
-forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '),
- function(eventName) {
- var directiveName = directiveNormalize('ng-' + eventName);
- ngEventDirectives[directiveName] = ['$parse', '$rootScope', function($parse, $rootScope) {
- return {
- restrict: 'A',
- compile: function($element, attr) {
- var fn = $parse(attr[directiveName]);
- return function ngEventHandler(scope, element) {
- element.on(eventName, function(event) {
- var callback = function() {
- fn(scope, {$event:event});
- };
- if (forceAsyncEvents[eventName] && $rootScope.$$phase) {
- scope.$evalAsync(callback);
- } else {
- scope.$apply(callback);
- }
- });
- };
- }
- };
- }];
- }
-);
/**
- * @ngdoc directive
- * @name ngDblclick
- *
+ * @ngdoc event
+ * @name ngInclude#$includeContentLoaded
+ * @eventType emit on the current ngInclude scope
* @description
- * The `ngDblclick` directive allows you to specify custom behavior on a dblclick event.
- *
- * @element ANY
- * @priority 0
- * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon
- * a dblclick. (The Event object is available as `$event`)
+ * Emitted every time the ngInclude content is reloaded.
*
- * @example
-
-
-
- count: {{count}}
-
-
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
*/
/**
- * @ngdoc directive
- * @name ngMousedown
- *
+ * @ngdoc event
+ * @name ngInclude#$includeContentError
+ * @eventType emit on the scope ngInclude was declared in
* @description
- * The ngMousedown directive allows you to specify custom behavior on mousedown event.
- *
- * @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`})
+ * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
*
- * @example
-
-
-
- count: {{count}}
-
-
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
*/
+var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce',
+ function($templateRequest, $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;
+
+ var cleanupLastIncludeContent = function() {
+ if (previousElement) {
+ previousElement.remove();
+ previousElement = null;
+ }
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+ if (currentElement) {
+ $animate.leave(currentElement).then(function() {
+ previousElement = null;
+ });
+ previousElement = currentElement;
+ currentElement = null;
+ }
+ };
+
+ scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
+ var afterAnimation = function() {
+ if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
+ }
+ };
+ var thisChangeId = ++changeCounter;
+
+ 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 (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).then(afterAnimation);
+ });
+
+ currentScope = newScope;
+ currentElement = clone;
+
+ currentScope.$emit('$includeContentLoaded', src);
+ scope.$eval(onloadExp);
+ }, function() {
+ 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 (/SVG/.test($element[0].toString())) {
+ // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
+ // support innerHTML, so detect this here and try to generate the contents
+ // specially.
+ $element.empty();
+ $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
+ function namespaceAdaptedClone(clone) {
+ $element.append(clone);
+ }, {futureParentElement: $element});
+ return;
+ }
+ $element.html(ctrl.template);
+ $compile($element.contents())(scope);
+ }
+ };
+ }];
/**
* @ngdoc directive
- * @name ngMouseup
+ * @name ngInit
+ * @restrict AC
*
* @description
- * Specify custom behavior on mouseup event.
- *
- * @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`})
+ * The `ngInit` directive allows you to evaluate an expression in the
+ * current scope.
*
- * @example
-
-
-
- count: {{count}}
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ngMouseover
+ *
+ * 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:
+ *
+ * ``
+ *
+ *
*
- * @description
- * Specify custom behavior on mouseover event.
+ * @priority 450
*
* @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`})
+ * @param {expression} ngInit {@link guide/expression Expression} to eval.
*
* @example
-
+
-
- count: {{count}}
+
+
+
+
+ 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);
+ }
+ };
+ }
+});
/**
* @ngdoc directive
- * @name ngMouseenter
+ * @name ngList
*
* @description
- * Specify custom behavior on mouseenter event.
+ * 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=" | "`.
*
- * @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 behaviour of the directive is affected by the use of the `ngTrim` attribute.
+ * * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
+ * list item is respected. This implies that the user of the directive is responsible for
+ * dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
+ * tab or newline character.
+ * * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
+ * when joining the list items back together) and whitespace around each list item is stripped
+ * before it is added to the model.
*
- * @example
-
-
-
- count: {{count}}
-
-
+ * ### Example with Validation
+ *
+ *
+ *
+ * angular.module('listExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.names = ['morpheus', 'neo', 'trinity'];
+ * }]);
+ *
+ *
+ *
+ *
+ *
+ * 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]');
+ * });
+ *
+ *
+ *
+ * @element input
+ * @param {string=} ngList optional delimiter that should be used to split the value.
*/
+var ngListDirective = function() {
+ return {
+ restrict: 'A',
+ priority: 100,
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ // We want to control whitespace trimming so we use this convoluted approach
+ // to access the ngList attribute, which doesn't pre-trim the attribute
+ var ngList = element.attr(attr.$attr.ngList) || ', ';
+ var trimValues = attr.ngTrim !== 'false';
+ var separator = trimValues ? trim(ngList) : ngList;
+
+ var parse = function(viewValue) {
+ // If the viewValue is invalid (say required but empty) it will be `undefined`
+ if (isUndefined(viewValue)) return;
+
+ var list = [];
+
+ if (viewValue) {
+ forEach(viewValue.split(separator), function(value) {
+ if (value) list.push(trimValues ? trim(value) : value);
+ });
+ }
+
+ return list;
+ };
+
+ ctrl.$parsers.push(parse);
+ ctrl.$formatters.push(function(value) {
+ if (isArray(value)) {
+ return value.join(ngList);
+ }
+ return undefined;
+ });
+
+ // Override the standard $isEmpty because an empty array means the input is empty.
+ ctrl.$isEmpty = function(value) {
+ return !value || !value.length;
+ };
+ }
+ };
+};
+
+/* global VALID_CLASS: true,
+ INVALID_CLASS: true,
+ PRISTINE_CLASS: true,
+ DIRTY_CLASS: true,
+ UNTOUCHED_CLASS: true,
+ TOUCHED_CLASS: true,
+*/
+
+var VALID_CLASS = 'ng-valid',
+ INVALID_CLASS = 'ng-invalid',
+ PRISTINE_CLASS = 'ng-pristine',
+ DIRTY_CLASS = 'ng-dirty',
+ UNTOUCHED_CLASS = 'ng-untouched',
+ TOUCHED_CLASS = 'ng-touched',
+ PENDING_CLASS = 'ng-pending';
+
+
+var $ngModelMinErr = new minErr('ngModel');
/**
- * @ngdoc directive
- * @name ngMouseleave
+ * @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. 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.
+
+Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
+`$viewValue`}.
+
+Returning `undefined` from a parser means a parse error occurred. In that case,
+no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
+will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
+is set to `true`. The parse error is stored in `ngModel.$error.parse`.
+
+ *
+ * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
+ the model value changes. The functions are called in reverse array order, each passing the value through to the
+ next. The last return value is used as the actual DOM value.
+ Used to format / convert values for display in the control.
+ * ```js
+ * function formatter(value) {
+ * if (value) {
+ * return value.toUpperCase();
+ * }
+ * }
+ * ngModel.$formatters.push(formatter);
+ * ```
+ *
+ * @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.
+ *
+ * ```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);
+ * };
+ * ```
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * ```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;
+ * });
+ * };
+ * ```
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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.
*
* @description
- * Specify custom behavior on mouseleave event.
*
- * @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`})
+ * `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.
+ * Angular 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
-
-
-
- count: {{count}}
-
-
- */
+ * ### 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.
+ *
+ * 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);
+ }
+ }
+ };
+ }]);
+
+
+
+
Change me!
+ Required!
+
+
+
+
+
+ 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 NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
+ function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $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);
+
+
+ var parsedNgModel = $parse($attr.ngModel),
+ parsedNgModelAssign = parsedNgModel.assign,
+ ngModelGet = parsedNgModel,
+ ngModelSet = parsedNgModelAssign,
+ pendingDebounce = null,
+ parserValid,
+ ctrl = this;
+
+ this.$$setOptions = function(options) {
+ ctrl.$options = options;
+ if (options && options.getterSetter) {
+ var invokeModelGetter = $parse($attr.ngModel + '()'),
+ invokeModelSetter = $parse($attr.ngModel + '($$$p)');
+
+ ngModelGet = function($scope) {
+ var modelValue = parsedNgModel($scope);
+ if (isFunction(modelValue)) {
+ modelValue = invokeModelGetter($scope);
+ }
+ return modelValue;
+ };
+ ngModelSet = function($scope, newValue) {
+ if (isFunction(parsedNgModel($scope))) {
+ invokeModelSetter($scope, {$$$p: ctrl.$modelValue});
+ } else {
+ parsedNgModelAssign($scope, ctrl.$modelValue);
+ }
+ };
+ } else if (!parsedNgModel.assign) {
+ throw $ngModelMinErr('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.
+ *
+ * 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 to 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 to their previous value. If `$modelValue`
+ * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
+ * invoked if you only change a property on the objects.
+ */
+ this.$render = noop;
+
+ /**
+ * @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 to 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".
+ */
+ this.$isEmpty = function(value) {
+ return isUndefined(value) || value === '' || value === null || value !== value;
+ };
+
+ var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
+ currentValidationRunId = 0;
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setValidity
+ *
+ * @description
+ * Change the validity state, and notify the form.
+ *
+ * This method can be called within $parsers/$formatters or a custom validation implementation.
+ * However, in most cases it should be sufficient to use the `ngModel.$validators` and
+ * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
+ * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
+ * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
+ * The `validationErrorKey` should be in camelCase and will get converted into dash-case
+ * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
+ * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
+ * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by Angular when validators do not run because of parse errors and
+ * when `$asyncValidators` do not run because any of the `$validators` failed.
+ */
+ addSetValidityMethod({
+ ctrl: this,
+ $element: $element,
+ set: function(object, property) {
+ object[property] = true;
+ },
+ unset: function(object, property) {
+ delete object[property];
+ },
+ parentForm: parentForm,
+ $animate: $animate
+ });
+
+ /**
+ * @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.
+ */
+ this.$setPristine = function() {
+ ctrl.$dirty = false;
+ ctrl.$pristine = true;
+ $animate.removeClass($element, DIRTY_CLASS);
+ $animate.addClass($element, PRISTINE_CLASS);
+ };
+
+ /**
+ * @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.
+ */
+ this.$setDirty = function() {
+ ctrl.$dirty = true;
+ ctrl.$pristine = false;
+ $animate.removeClass($element, PRISTINE_CLASS);
+ $animate.addClass($element, DIRTY_CLASS);
+ parentForm.$setDirty();
+ };
+
+ /**
+ * @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.
+ */
+ this.$setUntouched = function() {
+ ctrl.$touched = false;
+ ctrl.$untouched = true;
+ $animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_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).
+ */
+ this.$setTouched = function() {
+ ctrl.$touched = true;
+ ctrl.$untouched = false;
+ $animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
+ };
+
+ /**
+ * @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 a some
+ * future event.
+ *
+ * If you have an input that uses `ng-model-options` to set up debounced events or events such
+ * as blur you can have a situation where there is a period when the `$viewValue`
+ * is out of synch with the ngModel's `$modelValue`.
+ *
+ * In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
+ * programmatically before these debounced/future events have resolved/occurred, because Angular's
+ * dirty checking mechanism is not able to tell whether the model has actually changed or not.
+ *
+ * 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.
+ *
+ *
+ *
+ * angular.module('cancel-update-example', [])
+ *
+ * .controller('CancelUpdateController', ['$scope', function($scope) {
+ * $scope.resetWithCancel = function(e) {
+ * if (e.keyCode == 27) {
+ * $scope.myForm.myInput1.$rollbackViewValue();
+ * $scope.myValue = '';
+ * }
+ * };
+ * $scope.resetWithoutCancel = function(e) {
+ * if (e.keyCode == 27) {
+ * $scope.myValue = '';
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
Try typing something in each input. See that the model only updates when you
+ * blur off the input.
+ *
+ *
Now see what happens if you start typing then press the Escape key
+ *
+ *
+ *
With $rollbackViewValue()
+ *
+ * myValue: "{{ myValue }}"
+ *
+ *
Without $rollbackViewValue()
+ *
+ * myValue: "{{ myValue }}"
+ *
+ *
+ *
+ *
+ */
+ this.$rollbackViewValue = function() {
+ $timeout.cancel(pendingDebounce);
+ ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
+ ctrl.$render();
+ };
+
+ /**
+ * @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.
+ */
+ this.$validate = function() {
+ // ignore $validate before model is initialized
+ if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ var viewValue = ctrl.$$lastCommittedViewValue;
+ // Note: we use the $$rawModelValue as $modelValue might have been
+ // set to undefined during a view -> model update that found validation
+ // errors. We can't parse the view here, since that could change
+ // the model although neither viewValue nor the model on the scope changed
+ var modelValue = ctrl.$$rawModelValue;
+
+ var prevValid = ctrl.$valid;
+ var prevModelValue = ctrl.$modelValue;
+
+ var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+
+ ctrl.$$runValidators(modelValue, viewValue, function(allValid) {
+ // If there was no change in validity, don't update the model
+ // This prevents changing an invalid modelValue to undefined
+ if (!allowInvalid && prevValid !== allValid) {
+ // Note: Don't check ctrl.$valid here, as we could have
+ // external validators (e.g. calculated on the server),
+ // that just call $setValidity and need the model value
+ // to calculate their validity.
+ ctrl.$modelValue = allValid ? modelValue : undefined;
+
+ if (ctrl.$modelValue !== prevModelValue) {
+ ctrl.$$writeModelToScope();
+ }
+ }
+ });
+
+ };
+
+ this.$$runValidators = function(modelValue, viewValue, doneCallback) {
+ currentValidationRunId++;
+ var localValidationRunId = currentValidationRunId;
+
+ // check parser error
+ if (!processParseErrors()) {
+ validationDone(false);
+ return;
+ }
+ if (!processSyncValidators()) {
+ validationDone(false);
+ return;
+ }
+ processAsyncValidators();
+
+ function processParseErrors() {
+ var errorKey = ctrl.$$parserName || 'parse';
+ if (parserValid === undefined) {
+ setValidity(errorKey, null);
+ } else {
+ if (!parserValid) {
+ forEach(ctrl.$validators, function(v, name) {
+ setValidity(name, null);
+ });
+ forEach(ctrl.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ }
+ // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
+ setValidity(errorKey, parserValid);
+ return parserValid;
+ }
+ return true;
+ }
+
+ function processSyncValidators() {
+ var syncValidatorsValid = true;
+ forEach(ctrl.$validators, function(validator, name) {
+ var result = validator(modelValue, viewValue);
+ syncValidatorsValid = syncValidatorsValid && result;
+ setValidity(name, result);
+ });
+ if (!syncValidatorsValid) {
+ forEach(ctrl.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ return false;
+ }
+ return true;
+ }
+
+ function processAsyncValidators() {
+ var validatorPromises = [];
+ var allValid = true;
+ forEach(ctrl.$asyncValidators, function(validator, name) {
+ var promise = validator(modelValue, viewValue);
+ if (!isPromiseLike(promise)) {
+ throw $ngModelMinErr("$asyncValidators",
+ "Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
+ }
+ setValidity(name, undefined);
+ validatorPromises.push(promise.then(function() {
+ setValidity(name, true);
+ }, function(error) {
+ allValid = false;
+ setValidity(name, false);
+ }));
+ });
+ if (!validatorPromises.length) {
+ validationDone(true);
+ } else {
+ $q.all(validatorPromises).then(function() {
+ validationDone(allValid);
+ }, noop);
+ }
+ }
+
+ function setValidity(name, isValid) {
+ if (localValidationRunId === currentValidationRunId) {
+ ctrl.$setValidity(name, isValid);
+ }
+ }
+
+ function validationDone(allValid) {
+ if (localValidationRunId === currentValidationRunId) {
+
+ doneCallback(allValid);
+ }
+ }
+ };
+
+ /**
+ * @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.
+ */
+ this.$commitViewValue = function() {
+ var viewValue = ctrl.$viewValue;
+
+ $timeout.cancel(pendingDebounce);
+
+ // If the view value has not changed then we should just exit, except in the case where there is
+ // a native validator on the element. In this case the validation state may have changed even though
+ // the viewValue has stayed empty.
+ if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
+ return;
+ }
+ ctrl.$$lastCommittedViewValue = viewValue;
+
+ // change to dirty
+ if (ctrl.$pristine) {
+ this.$setDirty();
+ }
+ this.$$parseAndValidate();
+ };
+
+ this.$$parseAndValidate = function() {
+ var viewValue = ctrl.$$lastCommittedViewValue;
+ var modelValue = viewValue;
+ parserValid = isUndefined(modelValue) ? undefined : true;
+
+ if (parserValid) {
+ for (var i = 0; i < ctrl.$parsers.length; i++) {
+ modelValue = ctrl.$parsers[i](modelValue);
+ if (isUndefined(modelValue)) {
+ parserValid = false;
+ break;
+ }
+ }
+ }
+ if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
+ // ctrl.$modelValue has not been touched yet...
+ ctrl.$modelValue = ngModelGet($scope);
+ }
+ var prevModelValue = ctrl.$modelValue;
+ var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
+ ctrl.$$rawModelValue = modelValue;
+
+ if (allowInvalid) {
+ ctrl.$modelValue = modelValue;
+ writeToModelIfNeeded();
+ }
+
+ // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
+ // This can happen if e.g. $setViewValue is called from inside a parser
+ ctrl.$$runValidators(modelValue, ctrl.$$lastCommittedViewValue, function(allValid) {
+ if (!allowInvalid) {
+ // Note: Don't check ctrl.$valid here, as we could have
+ // external validators (e.g. calculated on the server),
+ // that just call $setValidity and need the model value
+ // to calculate their validity.
+ ctrl.$modelValue = allValid ? modelValue : undefined;
+ writeToModelIfNeeded();
+ }
+ });
+
+ function writeToModelIfNeeded() {
+ if (ctrl.$modelValue !== prevModelValue) {
+ ctrl.$$writeModelToScope();
+ }
+ }
+ };
+
+ this.$$writeModelToScope = function() {
+ ngModelSet($scope, ctrl.$modelValue);
+ forEach(ctrl.$viewChangeListeners, function(listener) {
+ try {
+ listener();
+ } catch (e) {
+ $exceptionHandler(e);
+ }
+ });
+ };
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setViewValue
+ *
+ * @description
+ * Update the view value.
+ *
+ * This method should be called when an input directive want to change the view value; typically,
+ * this is done from within a DOM event handler.
+ *
+ * For example {@link ng.directive:input input} calls it when the value of the input changes and
+ * {@link ng.directive:select select} calls it when an option is selected.
+ *
+ * If the new `value` is an object (rather than a string or a number), we should make a copy of the
+ * object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep
+ * watch of objects, it only looks for a change of identity. If you only change the property of
+ * the object then ngModel will not realise that the object has changed and 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.
+ *
+ * When this method is called, the new `value` will be staged for committing through the `$parsers`
+ * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
+ * value sent directly for processing, finally to be applied to `$modelValue` and then the
+ * **expression** specified in the `ng-model` attribute.
+ *
+ * Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
+ *
+ * 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 calling this function does not trigger a `$digest`.
+ *
+ * @param {string} value Value from the view.
+ * @param {string} trigger Event that triggered the update.
+ */
+ this.$setViewValue = function(value, trigger) {
+ ctrl.$viewValue = value;
+ if (!ctrl.$options || ctrl.$options.updateOnDefault) {
+ ctrl.$$debounceViewValueCommit(trigger);
+ }
+ };
+ this.$$debounceViewValueCommit = function(trigger) {
+ var debounceDelay = 0,
+ options = ctrl.$options,
+ debounce;
-/**
- * @ngdoc directive
- * @name ngMousemove
- *
- * @description
- * Specify custom behavior on mousemove event.
- *
- * @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`})
- *
- * @example
-
-
-
- count: {{count}}
-
-
- */
+ if (options && isDefined(options.debounce)) {
+ debounce = options.debounce;
+ if (isNumber(debounce)) {
+ debounceDelay = debounce;
+ } else if (isNumber(debounce[trigger])) {
+ debounceDelay = debounce[trigger];
+ } else if (isNumber(debounce['default'])) {
+ debounceDelay = debounce['default'];
+ }
+ }
+ $timeout.cancel(pendingDebounce);
+ if (debounceDelay) {
+ pendingDebounce = $timeout(function() {
+ ctrl.$commitViewValue();
+ }, debounceDelay);
+ } else if ($rootScope.$$phase) {
+ ctrl.$commitViewValue();
+ } else {
+ $scope.$apply(function() {
+ ctrl.$commitViewValue();
+ });
+ }
+ };
-/**
- * @ngdoc directive
- * @name ngKeydown
- *
- * @description
- * Specify custom behavior on keydown event.
- *
- * @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.)
- *
- * @example
-
-
-
- key down count: {{count}}
-
-
- */
+ // 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'
+ $scope.$watch(function ngModelWatch() {
+ var modelValue = ngModelGet($scope);
+ // if scope model value and ngModel value are out of sync
+ // TODO(perf): why not move this to the action fn?
+ if (modelValue !== ctrl.$modelValue) {
+ ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
+ parserValid = undefined;
-/**
- * @ngdoc directive
- * @name ngKeyup
- *
- * @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.)
- *
- * @example
-
-
-
Typing in the input box below updates the key count
- key up count: {{count}}
+ var formatters = ctrl.$formatters,
+ idx = formatters.length;
-
Typing in the input box below updates the keycode
-
-
event keyCode: {{ event.keyCode }}
-
event altKey: {{ event.altKey }}
-
-
- */
+ var viewValue = modelValue;
+ while (idx--) {
+ viewValue = formatters[idx](viewValue);
+ }
+ if (ctrl.$viewValue !== viewValue) {
+ ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
+ ctrl.$render();
+ ctrl.$$runValidators(modelValue, viewValue, noop);
+ }
+ }
-/**
- * @ngdoc directive
- * @name ngKeypress
- *
- * @description
- * Specify custom behavior on keypress event.
- *
- * @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.)
- *
- * @example
-
-
-
- key press count: {{count}}
-
-
- */
+ return modelValue;
+ });
+}];
/**
* @ngdoc directive
- * @name ngSubmit
- *
- * @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.
- *
- *
- * **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.
- *
- *
- * @element form
- * @priority 0
- * @param {expression} ngSubmit {@link guide/expression Expression} to eval.
- * ({@link guide/expression#-event- Event object is available as `$event`})
+ * @name ngModel
*
- * @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
+ * @element input
+ * @priority 1
*
* @description
- * Specify custom behavior on focus event.
- *
- * 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.
- *
- * @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`})
+ * 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.
*
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngBlur
+ * `ngModel` is responsible for:
*
- * @description
- * Specify custom behavior on blur event.
+ * - 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`) including animations.
+ * - Registering the control with its parent {@link ng.directive:form form}.
*
- * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
- * an element has lost focus.
+ * 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.
*
- * 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.
+ * For best practices on using `ngModel`, see:
*
- * @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`})
+ * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
*
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ngCopy
+ * For basic examples, how to use `ngModel`, see:
*
- * @description
- * Specify custom behavior on copy event.
+ * - {@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}
*
- * @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`})
+ * # 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
-
-
-
- copied: {{copied}}
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ngCut
+ * - `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
*
- * @description
- * Specify custom behavior on cut event.
+ * Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
- * @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`})
+ * ## Animation Hooks
*
- * @example
-
-
-
- cut: {{cut}}
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ngPaste
+ * 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.
*
- * @description
- * Specify custom behavior on paste 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:
*
- * @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`})
+ *
+ * //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
-
+ *
-
- pasted: {{paste}}
+
+
+ Update input to see transitions when valid/invalid.
+ Integer is a valid value.
+
+
+
-
- */
-
-/**
- * @ngdoc directive
- * @name ngIf
- * @restrict A
- *
- * @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.
- *
- * `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 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.
+ * ## Binding to a getter/setter
*
- * 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.
+ * 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 than what the model exposes
+ * to the view.
*
- * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
- * and `leave` effects.
+ *
+ * **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
+ * frequently than other parts of your code.
+ *
*
- * @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
+ * 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.
*
- * @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.
+ * The following example shows how to use `ngModel` with a getter/setter:
*
* @example
-
-
- Click me:
- Show when checked:
-
- This is 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;
- }
-
-
+ *
+
+
+
+ Name:
+
+
+
user.name =
+
+
+
+ angular.module('getterSetterExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var _name = 'Brian';
+ $scope.user = {
+ name: function(newName) {
+ if (angular.isDefined(newName)) {
+ _name = newName;
+ }
+ return _name;
+ }
+ };
+ }]);
+
+ *
*/
-var ngIfDirective = ['$animate', function($animate) {
+var ngModelDirective = ['$rootScope', function($rootScope) {
return {
- 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) {
+ 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] || nullFormCtrl;
- if (value) {
- if (!childScope) {
- $transclude(function(clone, newScope) {
- childScope = newScope;
- 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 its 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 = getBlockNodes(block.clone);
- $animate.leave(previousElements).then(function() {
- previousElements = null;
- });
- block = null;
+ modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
+
+ // notify others, especially parent forms
+ formCtrl.$addControl(modelCtrl);
+
+ attr.$observe('name', function(newValue) {
+ if (modelCtrl.$name !== newValue) {
+ formCtrl.$$renameControl(modelCtrl, newValue);
}
+ });
+
+ scope.$on('$destroy', function() {
+ formCtrl.$removeControl(modelCtrl);
+ });
+ },
+ post: function ngModelPostLink(scope, element, attr, ctrls) {
+ var modelCtrl = ctrls[0];
+ if (modelCtrl.$options && modelCtrl.$options.updateOn) {
+ element.on(modelCtrl.$options.updateOn, function(ev) {
+ modelCtrl.$$debounceViewValueCommit(ev && ev.type);
+ });
}
- });
+
+ element.on('blur', function(ev) {
+ if (modelCtrl.$touched) return;
+
+ if ($rootScope.$$phase) {
+ scope.$evalAsync(modelCtrl.$setTouched);
+ } else {
+ scope.$apply(modelCtrl.$setTouched);
+ }
+ });
+ }
+ };
}
};
}];
+var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
+
/**
* @ngdoc directive
- * @name ngInclude
- * @restrict ECA
+ * @name ngModelOptions
*
* @description
- * 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 Angular'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
- * enter - animation is used to bring new content into the browser.
- * leave - animation is used to animate existing content away.
+ * Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
+ * events that will trigger a model update and/or a debouncing delay so that the actual update only
+ * takes place when a timer expires; this timer will be reset after another change takes place.
*
- * The enter and leave animation occur concurrently.
+ * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
+ * be different than 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.
*
- * @scope
- * @priority 400
+ * 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.
*
- * @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.
+ * 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.
*
- * @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
- * $anchorScroll} to scroll the viewport after the content is loaded.
+ * `ngModelOptions` has an effect on the element it's declared on and its descendants.
*
- * - 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.
+ * @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
+ * - `updateOn`: string specifying which event should the input be bound to. You can set several
+ * events using an space delimited list. There is a special event called `default` that
+ * matches the default events belonging of the control.
+ * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
+ * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
+ * custom value for each event. For example:
+ * `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
+ * - `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
+ * ``, ``, ... . Right now, the only supported value is `'UTC'`,
+ * otherwise the default timezone of the browser will be used.
*
* @example
-
-
-
-
- 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 {
- -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;
+ 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.
- position:absolute;
- top:0;
- left:0;
- right:0;
- bottom:0;
- display:block;
- padding:10px;
- }
+
+
+
+
+
+ angular.module('optionsExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.user = { name: 'say', data: '' };
- .slide-animate.ng-leave {
- top:0;
- }
- .slide-animate.ng-leave.ng-leave-active {
- top:50px;
- }
+ $scope.cancel = function(e) {
+ if (e.keyCode == 27) {
+ $scope.userForm.userName.$rollbackViewValue();
+ }
+ };
+ }]);
- 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/);
- });
+ var model = element(by.binding('user.name'));
+ var input = element(by.model('user.name'));
+ var other = element(by.model('user.data'));
- 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/);
+ it('should allow custom events', function() {
+ input.sendKeys(' hello');
+ input.click();
+ expect(model.getText()).toEqual('say');
+ other.click();
+ expect(model.getText()).toEqual('say hello');
});
- 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);
+ 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');
});
- */
+ This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
+ If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
-/**
- * @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.
- */
+
+
+
+
+ Name:
+
+
+
+
user.name =
+
+
+
+ angular.module('optionsExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.user = { name: 'say' };
+ }]);
+
+
+ This one shows how to bind to getter/setters:
-/**
- * @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.
+
+
+
+
+ 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;
+ }
+ };
+ }]);
+
+
*/
+var ngModelOptionsDirective = function() {
+ return {
+ restrict: 'A',
+ controller: ['$scope', '$attrs', function($scope, $attrs) {
+ var that = this;
+ this.$options = $scope.$eval($attrs.ngModelOptions);
+ // Allow adding/overriding bound events
+ if (this.$options.updateOn !== undefined) {
+ this.$options.updateOnDefault = false;
+ // extract "default" pseudo-event from list of events that can trigger a model update
+ this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
+ that.$options.updateOnDefault = true;
+ return ' ';
+ }));
+ } else {
+ this.$options.updateOnDefault = true;
+ }
+ }]
+ };
+};
-/**
- * @ngdoc event
- * @name ngInclude#$includeContentError
- * @eventType emit on the scope ngInclude was declared in
- * @description
- * Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299)
- *
- * @param {Object} angularEvent Synthetic event object.
- * @param {String} src URL of content to load.
- */
-var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce',
- function($templateRequest, $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;
+// helper methods
+function addSetValidityMethod(context) {
+ var ctrl = context.ctrl,
+ $element = context.$element,
+ classCache = {},
+ set = context.set,
+ unset = context.unset,
+ parentForm = context.parentForm,
+ $animate = context.$animate;
- var cleanupLastIncludeContent = function() {
- if (previousElement) {
- previousElement.remove();
- previousElement = null;
- }
- if (currentScope) {
- currentScope.$destroy();
- currentScope = null;
- }
- if (currentElement) {
- $animate.leave(currentElement).then(function() {
- previousElement = null;
- });
- previousElement = currentElement;
- currentElement = null;
- }
- };
+ classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
- scope.$watch($sce.parseAsResourceUrl(srcExp), function ngIncludeWatchAction(src) {
- var afterAnimation = function() {
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
- }
- };
- var thisChangeId = ++changeCounter;
+ ctrl.$setValidity = setValidity;
- 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 (thisChangeId !== changeCounter) return;
- var newScope = scope.$new();
- ctrl.template = response;
+ function setValidity(validationErrorKey, state, controller) {
+ if (state === undefined) {
+ createAndSet('$pending', validationErrorKey, controller);
+ } else {
+ unsetAndCleanup('$pending', validationErrorKey, controller);
+ }
+ if (!isBoolean(state)) {
+ unset(ctrl.$error, validationErrorKey, controller);
+ unset(ctrl.$$success, validationErrorKey, controller);
+ } else {
+ if (state) {
+ unset(ctrl.$error, validationErrorKey, controller);
+ set(ctrl.$$success, validationErrorKey, controller);
+ } else {
+ set(ctrl.$error, validationErrorKey, controller);
+ unset(ctrl.$$success, validationErrorKey, controller);
+ }
+ }
+ if (ctrl.$pending) {
+ cachedToggleClass(PENDING_CLASS, true);
+ ctrl.$valid = ctrl.$invalid = undefined;
+ toggleValidationCss('', null);
+ } else {
+ cachedToggleClass(PENDING_CLASS, false);
+ ctrl.$valid = isObjectEmpty(ctrl.$error);
+ ctrl.$invalid = !ctrl.$valid;
+ toggleValidationCss('', ctrl.$valid);
+ }
- // 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).then(afterAnimation);
- });
+ // re-read the state as the set/unset methods could have
+ // combined state in ctrl.$error[validationError] (used for forms),
+ // where setting/unsetting only increments/decrements the value,
+ // and does not replace it.
+ var combinedState;
+ if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
+ combinedState = undefined;
+ } else if (ctrl.$error[validationErrorKey]) {
+ combinedState = false;
+ } else if (ctrl.$$success[validationErrorKey]) {
+ combinedState = true;
+ } else {
+ combinedState = null;
+ }
- currentScope = newScope;
- currentElement = clone;
+ toggleValidationCss(validationErrorKey, combinedState);
+ parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
+ }
- currentScope.$emit('$includeContentLoaded', src);
- scope.$eval(onloadExp);
- }, function() {
- if (thisChangeId === changeCounter) {
- cleanupLastIncludeContent();
- scope.$emit('$includeContentError', src);
- }
- });
- scope.$emit('$includeContentRequested', src);
- } else {
- cleanupLastIncludeContent();
- ctrl.template = null;
- }
- });
- };
+ function createAndSet(name, value, controller) {
+ if (!ctrl[name]) {
+ ctrl[name] = {};
}
- };
-}];
+ set(ctrl[name], value, controller);
+ }
-// 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 (/SVG/.test($element[0].toString())) {
- // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
- // support innerHTML, so detect this here and try to generate the contents
- // specially.
- $element.empty();
- $compile(jqLiteBuildFragment(ctrl.template, document).childNodes)(scope,
- function namespaceAdaptedClone(clone) {
- $element.append(clone);
- }, undefined, undefined, $element);
- return;
- }
+ function unsetAndCleanup(name, value, controller) {
+ if (ctrl[name]) {
+ unset(ctrl[name], value, controller);
+ }
+ if (isObjectEmpty(ctrl[name])) {
+ ctrl[name] = undefined;
+ }
+ }
+
+ function cachedToggleClass(className, switchValue) {
+ if (switchValue && !classCache[className]) {
+ $animate.addClass($element, className);
+ classCache[className] = true;
+ } else if (!switchValue && classCache[className]) {
+ $animate.removeClass($element, className);
+ classCache[className] = false;
+ }
+ }
- $element.html(ctrl.template);
- $compile($element.contents())(scope);
- }
- };
- }];
+ function toggleValidationCss(validationErrorKey, isValid) {
+ validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
-/**
- * @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:
- *
-
-
- 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);
- }
- };
+ cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
+ cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
}
-});
+}
+
+function isObjectEmpty(obj) {
+ if (obj) {
+ for (var prop in obj) {
+ return false;
+ }
+ }
+ return true;
+}
/**
* @ngdoc directive
@@ -23660,7 +24137,9 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
*/
var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
- var BRACE = /{}/g;
+ var BRACE = /{}/g,
+ IS_WHEN = /^when(Minus)?(.+)$/;
+
return {
restrict: 'EA',
link: function(scope, element, attr) {
@@ -23671,34 +24150,44 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
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 = isNaN(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);
- } 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 exlicitly check.
+ if ((count !== lastCount) && !(countIsNaN && isNaN(lastCount))) {
+ watchRemover();
+ watchRemover = scope.$watch(whensExpFns[count], updateElementText);
+ lastCount = count;
}
- }, function ngPluralizeWatchAction(newVal) {
- element.text(newVal);
});
+
+ function updateElementText(newText) {
+ element.text(newText || '');
+ }
}
};
}];
@@ -23726,6 +24215,78 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
* 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
+ *
...
+ * ```
+ *
+ * You need to be aware that the JavaScript specification does not define what order
+ * it will return the keys for an object. In order to have a guaranteed deterministic order
+ * for the keys, Angular versions up to and including 1.3 **sort the keys alphabetically**.
+ *
+ * If this is not desired, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
+ * or implement a `$watch` on the object yourself.
+ *
+ * In version 1.4 we will remove the sorting, since it seems that browsers generally follow the
+ * strategy of providing keys in the order in which they were defined, although there are exceptions
+ * when keys are deleted and reinstated.
+ *
+ *
+ * # Tracking and Duplicates
+ *
+ * When the contents of the collection change, `ngRepeat` 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.
+ *
+ * By default, `ngRepeat` does not allow duplicate items in arrays. This is because when
+ * there are duplicates, it is not possible to maintain a one-to-one mapping between collection
+ * items and DOM elements.
+ *
+ * If you do need to repeat duplicate items, you can substitute the default tracking behavior
+ * with your own using the `track by` expression.
+ *
+ * For example, you may track items by the index of each item in the collection, using the
+ * special scope property `$index`:
+ * ```html
+ *
+ * {{n}}
+ *
+ * ```
+ *
+ * You may use arbitrary expressions in `track by`, including references to custom functions
+ * on the scope:
+ * ```html
+ *
+ * {{n}}
+ *
+ * ```
+ *
+ * If you are working with objects that have an identifier property, you can track
+ * by the identifier instead of the whole object. Should you reload your data later, `ngRepeat`
+ * will not have to rebuild the DOM elements for items it has already rendered, even if the
+ * JavaScript objects in the collection have been substituted for new ones:
+ * ```html
+ *
+ * {{model.name}}
+ *
+ * ```
+ *
+ * When no `track by` expression is provided, it is equivalent to tracking by the built-in
+ * `$id` function, which tracks items by their identity:
+ * ```html
+ *
+ * {{obj.prop}}
+ *
+ * ```
+ *
* # 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.
@@ -23793,12 +24354,12 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
*
* 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.) If filters are used in the expression, they should be
+ * applied before the tracking expression.
*
* 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.
@@ -23960,7 +24521,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
var aliasAs = match[3];
var trackByExp = match[4];
- match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
+ 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}'.",
@@ -23970,7 +24531,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
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)$/.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);
}
@@ -24069,7 +24630,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
});
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, toJson(value));
+ expression, trackById, value);
} else {
// new never before seen block
nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
@@ -24180,17 +24741,18 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change
+ * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
* the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
- * class in CSS:
+ * class CSS. Note that the selector that needs to be used is actually `.ng-hide:not(.ng-hide-animate)` to cope
+ * with extra animation classes that can be added.
*
* ```css
- * .ng-hide {
+ * .ng-hide:not(.ng-hide-animate) {
* /* this is just another form of hiding an element */
- * display:block!important;
- * position:absolute;
- * top:-9999px;
- * left:-9999px;
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
* }
* ```
*
@@ -24210,13 +24772,13 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
* .my-element.ng-hide-add, .my-element.ng-hide-remove {
* /* this is required as of 1.3x to properly
* apply all styling in a show/hide animation */
- * transition:0s linear all;
+ * transition: 0s linear all;
* }
*
* .my-element.ng-hide-add-active,
* .my-element.ng-hide-remove-active {
* /* the transition is defined in the active class */
- * transition:1s linear all;
+ * transition: 1s linear all;
* }
*
* .my-element.ng-hide-add { ... }
@@ -24258,29 +24820,29 @@ var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
.animate-show {
- line-height:20px;
- opacity:1;
- padding:10px;
- border:1px solid black;
- background:white;
+ line-height: 20px;
+ opacity: 1;
+ padding: 10px;
+ border: 1px solid black;
+ background: white;
}
.animate-show.ng-hide-add.ng-hide-add-active,
.animate-show.ng-hide-remove.ng-hide-remove-active {
- -webkit-transition:all linear 0.5s;
- transition:all linear 0.5s;
+ -webkit-transition: all linear 0.5s;
+ transition: all linear 0.5s;
}
.animate-show.ng-hide {
- line-height:0;
- opacity:0;
- padding:0 10px;
+ line-height: 0;
+ opacity: 0;
+ padding: 0 10px;
}
.check-element {
- padding:10px;
- border:1px solid black;
- background:white;
+ padding: 10px;
+ border: 1px solid black;
+ background: white;
}
@@ -24354,17 +24916,17 @@ var ngShowDirective = ['$animate', function($animate) {
*
* ### Overriding `.ng-hide`
*
- * By default, the `.ng-hide` class will style the element with `display:none!important`. If you wish to change
+ * By default, the `.ng-hide` class will style the element with `display: none!important`. If you wish to change
* the hide behavior with ngShow/ngHide then this can be achieved by restating the styles for the `.ng-hide`
* class in CSS:
*
* ```css
* .ng-hide {
* /* this is just another form of hiding an element */
- * display:block!important;
- * position:absolute;
- * top:-9999px;
- * left:-9999px;
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
* }
* ```
*
@@ -24381,7 +24943,7 @@ var ngShowDirective = ['$animate', function($animate) {
* //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;
+ * transition: 0.5s linear all;
* }
*
* .my-element.ng-hide-add { ... }
@@ -24423,25 +24985,25 @@ var ngShowDirective = ['$animate', function($animate) {
.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;
+ -webkit-transition: all linear 0.5s;
+ transition: all linear 0.5s;
+ line-height: 20px;
+ opacity: 1;
+ padding: 10px;
+ border: 1px solid black;
+ background: white;
}
.animate-hide.ng-hide {
- line-height:0;
- opacity:0;
- padding:0 10px;
+ line-height: 0;
+ opacity: 0;
+ padding: 0 10px;
}
.check-element {
- padding:10px;
- border:1px solid black;
- background:white;
+ padding: 10px;
+ border: 1px solid black;
+ background: white;
}
@@ -24523,12 +25085,12 @@ var ngHideDirective = ['$animate', function($animate) {
*/
var ngStyleDirective = ngDirective(function(scope, element, attr) {
- scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
+ scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) { element.css(style, '');});
}
if (newStyles) element.css(newStyles);
- }, true);
+ });
});
/**
@@ -24770,7 +25332,7 @@ var ngSwitchDefaultDirective = ngDirective({
}]);
-
+
{{text}}
@@ -24848,7 +25410,6 @@ var scriptDirective = ['$templateCache', function($templateCache) {
compile: function(element, attr) {
if (attr.type == 'text/ng-template') {
var templateUrl = attr.id,
- // IE is not consistent, in scripts we have to read .text but in other nodes we have to read .textContent
text = element[0].text;
$templateCache.put(templateUrl, text);
@@ -24870,14 +25431,15 @@ var ngOptionsMinErr = minErr('ngOptions');
*
* The `ngOptions` attribute can be used to dynamically generate a list of `
`
* elements for the `
*
- * ## `select as`
+ * ## `select` **`as`**
*
- * Using `select as` will bind the result of the `select as` expression to the model, but
+ * Using `select` **`as`** will bind the result of the `select` expression to the model, but
* the value of the `` and `
show-tick
You don't need this class any more, because the select options are built by yourself. you can get full control of what content a select should have. so you need to add tick by yourself.
Add <span class="glyphicon glyphicon-ok check-mark"></span> inside <a> tag which is a direct child of nya-bs-option directive. make sure the content of option is before the check-mark element.
style an option, custom HTML of option, data-icon, showSubText
diff --git a/docs/src/content/examples/basic-usage.md b/docs/src/content/examples/basic-usage.md
index 0338263..c20346a 100644
--- a/docs/src/content/examples/basic-usage.md
+++ b/docs/src/content/examples/basic-usage.md
@@ -13,8 +13,8 @@ Static Options don't change. you need to add `nya-bs-option` class to each of yo
-