- Enter number:
- Default formatting: {{val | number}}
- No fractions: {{val | number:0}}
- Negative number: {{-val | number:4}}
+
+ Enter number:
+ Default formatting: {{val | number}}
+ No fractions: {{val | number:0}}
+ Negative number: {{-val | number:4}}
-
-
+
+
it('should format numbers', function() {
- expect(binding('val | number')).toBe('1,234.568');
- expect(binding('val | number:0')).toBe('1,235');
- expect(binding('-val | number:4')).toBe('-1,234.5679');
+ expect(element(by.id('number-default')).getText()).toBe('1,234.568');
+ expect(element(by.binding('val | number:0')).getText()).toBe('1,235');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-1,234.5679');
});
it('should update', function() {
- input('val').enter('3374.333');
- expect(binding('val | number')).toBe('3,374.333');
- expect(binding('val | number:0')).toBe('3,374');
- expect(binding('-val | number:4')).toBe('-3,374.3330');
- });
-
-
+ element(by.model('val')).clear();
+ element(by.model('val')).sendKeys('3374.333');
+ expect(element(by.id('number-default')).getText()).toBe('3,374.333');
+ expect(element(by.binding('val | number:0')).getText()).toBe('3,374');
+ expect(element(by.binding('-val | number:4')).getText()).toBe('-3,374.3330');
+ });
+
+
*/
-
-
numberFilter.$inject = ['$locale'];
function numberFilter($locale) {
var formats = $locale.NUMBER_FORMATS;
return function(number, fractionSize) {
- return formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
- fractionSize);
+
+ // if null or undefined pass it through
+ return (number == null)
+ ? number
+ : formatNumber(number, formats.PATTERNS[0], formats.GROUP_SEP, formats.DECIMAL_SEP,
+ fractionSize);
};
}
-var DECIMAL_SEP = '.';
-function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
- if (isNaN(number) || !isFinite(number)) return '';
+/**
+ * Parse a number (as a string) into three components that can be used
+ * for formatting the number.
+ *
+ * (Significant bits of this parse algorithm came from https://github.com/MikeMcl/big.js/)
+ *
+ * @param {string} numStr The number to parse
+ * @return {object} An object describing this number, containing the following keys:
+ * - d : an array of digits containing leading zeros as necessary
+ * - i : the number of the digits in `d` that are to the left of the decimal point
+ * - e : the exponent for numbers that would need more than `MAX_DIGITS` digits in `d`
+ *
+ */
+function parse(numStr) {
+ var exponent = 0, digits, numberOfIntegerDigits;
+ var i, j, zeros;
- var isNegative = number < 0;
- number = Math.abs(number);
- var numStr = number + '',
- formatedText = '',
- parts = [];
-
- var hasExponent = false;
- if (numStr.indexOf('e') !== -1) {
- var match = numStr.match(/([\d\.]+)e(-?)(\d+)/);
- if (match && match[2] == '-' && match[3] > fractionSize + 1) {
- numStr = '0';
- } else {
- formatedText = numStr;
- hasExponent = true;
- }
+ // Decimal point?
+ if ((numberOfIntegerDigits = numStr.indexOf(DECIMAL_SEP)) > -1) {
+ numStr = numStr.replace(DECIMAL_SEP, '');
}
- if (!hasExponent) {
- var fractionLen = (numStr.split(DECIMAL_SEP)[1] || '').length;
+ // Exponential form?
+ if ((i = numStr.search(/e/i)) > 0) {
+ // Work out the exponent.
+ if (numberOfIntegerDigits < 0) numberOfIntegerDigits = i;
+ numberOfIntegerDigits += +numStr.slice(i + 1);
+ numStr = numStr.substring(0, i);
+ } else if (numberOfIntegerDigits < 0) {
+ // There was no decimal point or exponent so it is an integer.
+ numberOfIntegerDigits = numStr.length;
+ }
- // determine fractionSize if it is not specified
- if (isUndefined(fractionSize)) {
- fractionSize = Math.min(Math.max(pattern.minFrac, fractionLen), pattern.maxFrac);
- }
+ // Count the number of leading zeros.
+ for (i = 0; numStr.charAt(i) === ZERO_CHAR; i++) { /* empty */ }
- var pow = Math.pow(10, fractionSize);
- number = Math.round(number * pow) / pow;
- var fraction = ('' + number).split(DECIMAL_SEP);
- var whole = fraction[0];
- fraction = fraction[1] || '';
-
- var pos = 0,
- lgroup = pattern.lgSize,
- group = pattern.gSize;
-
- if (whole.length >= (lgroup + group)) {
- pos = whole.length - lgroup;
- for (var i = 0; i < pos; i++) {
- if ((pos - i)%group === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
- }
-
- for (i = pos; i < whole.length; i++) {
- if ((whole.length - i)%lgroup === 0 && i !== 0) {
- formatedText += groupSep;
- }
- formatedText += whole.charAt(i);
- }
-
- // format fraction part.
- while(fraction.length < fractionSize) {
- fraction += '0';
- }
-
- if (fractionSize && fractionSize !== "0") formatedText += decimalSep + fraction.substr(0, fractionSize);
+ if (i === (zeros = numStr.length)) {
+ // The digits are all zero.
+ digits = [0];
+ numberOfIntegerDigits = 1;
} else {
+ // Count the number of trailing zeros
+ zeros--;
+ while (numStr.charAt(zeros) === ZERO_CHAR) zeros--;
- if (fractionSize > 0 && number > -1 && number < 1) {
- formatedText = number.toFixed(fractionSize);
+ // Trailing zeros are insignificant so ignore them
+ numberOfIntegerDigits -= i;
+ digits = [];
+ // Convert string to array of digits without leading/trailing zeros.
+ for (j = 0; i <= zeros; i++, j++) {
+ digits[j] = +numStr.charAt(i);
}
}
- parts.push(isNegative ? pattern.negPre : pattern.posPre);
- parts.push(formatedText);
- parts.push(isNegative ? pattern.negSuf : pattern.posSuf);
- return parts.join('');
+ // If the number overflows the maximum allowed digits then use an exponent.
+ if (numberOfIntegerDigits > MAX_DIGITS) {
+ digits = digits.splice(0, MAX_DIGITS - 1);
+ exponent = numberOfIntegerDigits - 1;
+ numberOfIntegerDigits = 1;
+ }
+
+ return { d: digits, e: exponent, i: numberOfIntegerDigits };
}
-function padNumber(num, digits, trim) {
+/**
+ * Round the parsed number to the specified number of decimal places
+ * This function changed the parsedNumber in-place
+ */
+function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
+ var digits = parsedNumber.d;
+ var fractionLen = digits.length - parsedNumber.i;
+
+ // determine fractionSize if it is not specified; `+fractionSize` converts it to a number
+ fractionSize = (isUndefined(fractionSize)) ? Math.min(Math.max(minFrac, fractionLen), maxFrac) : +fractionSize;
+
+ // The index of the digit to where rounding is to occur
+ var roundAt = fractionSize + parsedNumber.i;
+ var digit = digits[roundAt];
+
+ if (roundAt > 0) {
+ // Drop fractional digits beyond `roundAt`
+ digits.splice(Math.max(parsedNumber.i, roundAt));
+
+ // Set non-fractional digits beyond `roundAt` to 0
+ for (var j = roundAt; j < digits.length; j++) {
+ digits[j] = 0;
+ }
+ } else {
+ // We rounded to zero so reset the parsedNumber
+ fractionLen = Math.max(0, fractionLen);
+ parsedNumber.i = 1;
+ digits.length = Math.max(1, roundAt = fractionSize + 1);
+ digits[0] = 0;
+ for (var i = 1; i < roundAt; i++) digits[i] = 0;
+ }
+
+ if (digit >= 5) {
+ if (roundAt - 1 < 0) {
+ for (var k = 0; k > roundAt; k--) {
+ digits.unshift(0);
+ parsedNumber.i++;
+ }
+ digits.unshift(1);
+ parsedNumber.i++;
+ } else {
+ digits[roundAt - 1]++;
+ }
+ }
+
+ // Pad out with zeros to get the required fraction length
+ for (; fractionLen < Math.max(0, fractionSize); fractionLen++) digits.push(0);
+
+
+ // Do any carrying, e.g. a digit was rounded up to 10
+ var carry = digits.reduceRight(function(carry, d, i, digits) {
+ d = d + carry;
+ digits[i] = d % 10;
+ return Math.floor(d / 10);
+ }, 0);
+ if (carry) {
+ digits.unshift(carry);
+ parsedNumber.i++;
+ }
+}
+
+/**
+ * Format a number into a string
+ * @param {number} number The number to format
+ * @param {{
+ * minFrac, // the minimum number of digits required in the fraction part of the number
+ * maxFrac, // the maximum number of digits required in the fraction part of the number
+ * gSize, // number of digits in each group of separated digits
+ * lgSize, // number of digits in the last group of digits before the decimal separator
+ * negPre, // the string to go in front of a negative number (e.g. `-` or `(`))
+ * posPre, // the string to go in front of a positive number
+ * negSuf, // the string to go after a negative number (e.g. `)`)
+ * posSuf // the string to go after a positive number
+ * }} pattern
+ * @param {string} groupSep The string to separate groups of number (e.g. `,`)
+ * @param {string} decimalSep The string to act as the decimal separator (e.g. `.`)
+ * @param {[type]} fractionSize The size of the fractional part of the number
+ * @return {string} The number formatted as a string
+ */
+function formatNumber(number, pattern, groupSep, decimalSep, fractionSize) {
+
+ if (!(isString(number) || isNumber(number)) || isNaN(number)) return '';
+
+ var isInfinity = !isFinite(number);
+ var isZero = false;
+ var numStr = Math.abs(number) + '',
+ formattedText = '',
+ parsedNumber;
+
+ if (isInfinity) {
+ formattedText = '\u221e';
+ } else {
+ parsedNumber = parse(numStr);
+
+ roundNumber(parsedNumber, fractionSize, pattern.minFrac, pattern.maxFrac);
+
+ var digits = parsedNumber.d;
+ var integerLen = parsedNumber.i;
+ var exponent = parsedNumber.e;
+ var decimals = [];
+ isZero = digits.reduce(function(isZero, d) { return isZero && !d; }, true);
+
+ // pad zeros for small numbers
+ while (integerLen < 0) {
+ digits.unshift(0);
+ integerLen++;
+ }
+
+ // extract decimals digits
+ if (integerLen > 0) {
+ decimals = digits.splice(integerLen, digits.length);
+ } else {
+ decimals = digits;
+ digits = [0];
+ }
+
+ // format the integer digits with grouping separators
+ var groups = [];
+ if (digits.length >= pattern.lgSize) {
+ groups.unshift(digits.splice(-pattern.lgSize, digits.length).join(''));
+ }
+ while (digits.length > pattern.gSize) {
+ groups.unshift(digits.splice(-pattern.gSize, digits.length).join(''));
+ }
+ if (digits.length) {
+ groups.unshift(digits.join(''));
+ }
+ formattedText = groups.join(groupSep);
+
+ // append the decimal digits
+ if (decimals.length) {
+ formattedText += decimalSep + decimals.join('');
+ }
+
+ if (exponent) {
+ formattedText += 'e+' + exponent;
+ }
+ }
+ if (number < 0 && !isZero) {
+ return pattern.negPre + formattedText + pattern.negSuf;
+ } else {
+ return pattern.posPre + formattedText + pattern.posSuf;
+ }
+}
+
+function padNumber(num, digits, trim, negWrap) {
var neg = '';
- if (num < 0) {
- neg = '-';
- num = -num;
+ if (num < 0 || (negWrap && num <= 0)) {
+ if (negWrap) {
+ num = -num + 1;
+ } else {
+ num = -num;
+ neg = '-';
+ }
}
num = '' + num;
- while(num.length < digits) num = '0' + num;
- if (trim)
+ while (num.length < digits) num = ZERO_CHAR + num;
+ if (trim) {
num = num.substr(num.length - digits);
+ }
return neg + num;
}
-function dateGetter(name, size, offset, trim) {
+function dateGetter(name, size, offset, trim, negWrap) {
offset = offset || 0;
return function(date) {
var value = date['get' + name]();
- if (offset > 0 || value > -offset)
+ if (offset > 0 || value > -offset) {
value += offset;
- if (value === 0 && offset == -12 ) value = 12;
- return padNumber(value, size, trim);
+ }
+ if (value === 0 && offset === -12) value = 12;
+ return padNumber(value, size, trim, negWrap);
};
}
-function dateStrGetter(name, shortForm) {
+function dateStrGetter(name, shortForm, standAlone) {
return function(date, formats) {
var value = date['get' + name]();
- var get = uppercase(shortForm ? ('SHORT' + name) : name);
+ var propPrefix = (standAlone ? 'STANDALONE' : '') + (shortForm ? 'SHORT' : '');
+ var get = uppercase(propPrefix + name);
return formats[get][value];
};
}
-function timeZoneGetter(date) {
- var zone = -1 * date.getTimezoneOffset();
- var paddedZone = (zone >= 0) ? "+" : "";
+function timeZoneGetter(date, formats, offset) {
+ var zone = -1 * offset;
+ var paddedZone = (zone >= 0) ? '+' : '';
paddedZone += padNumber(Math[zone > 0 ? 'floor' : 'ceil'](zone / 60), 2) +
padNumber(Math.abs(zone % 60), 2);
@@ -10195,18 +22858,53 @@ function timeZoneGetter(date) {
return paddedZone;
}
+function getFirstThursdayOfYear(year) {
+ // 0 = index of January
+ var dayOfWeekOnFirst = (new Date(year, 0, 1)).getDay();
+ // 4 = index of Thursday (+1 to account for 1st = 5)
+ // 11 = index of *next* Thursday (+1 account for 1st = 12)
+ return new Date(year, 0, ((dayOfWeekOnFirst <= 4) ? 5 : 12) - dayOfWeekOnFirst);
+}
+
+function getThursdayThisWeek(datetime) {
+ return new Date(datetime.getFullYear(), datetime.getMonth(),
+ // 4 = index of Thursday
+ datetime.getDate() + (4 - datetime.getDay()));
+}
+
+function weekGetter(size) {
+ return function(date) {
+ var firstThurs = getFirstThursdayOfYear(date.getFullYear()),
+ thisThurs = getThursdayThisWeek(date);
+
+ var diff = +thisThurs - +firstThurs,
+ result = 1 + Math.round(diff / 6.048e8); // 6.048e8 ms per week
+
+ return padNumber(result, size);
+ };
+}
+
function ampmGetter(date, formats) {
return date.getHours() < 12 ? formats.AMPMS[0] : formats.AMPMS[1];
}
+function eraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERAS[0] : formats.ERAS[1];
+}
+
+function longEraGetter(date, formats) {
+ return date.getFullYear() <= 0 ? formats.ERANAMES[0] : formats.ERANAMES[1];
+}
+
var DATE_FORMATS = {
- yyyy: dateGetter('FullYear', 4),
- yy: dateGetter('FullYear', 2, 0, true),
- y: dateGetter('FullYear', 1),
+ yyyy: dateGetter('FullYear', 4, 0, false, true),
+ yy: dateGetter('FullYear', 2, 0, true, true),
+ y: dateGetter('FullYear', 1, 0, false, true),
MMMM: dateStrGetter('Month'),
MMM: dateStrGetter('Month', true),
MM: dateGetter('Month', 2, 1),
M: dateGetter('Month', 1, 1),
+ LLLL: dateStrGetter('Month', false, true),
dd: dateGetter('Date', 2),
d: dateGetter('Date', 1),
HH: dateGetter('Hours', 2),
@@ -10217,19 +22915,28 @@ var DATE_FORMATS = {
m: dateGetter('Minutes', 1),
ss: dateGetter('Seconds', 2),
s: dateGetter('Seconds', 1),
+ // while ISO 8601 requires fractions to be prefixed with `.` or `,`
+ // we can be just safely rely on using `sss` since we currently don't support single or two digit fractions
+ sss: dateGetter('Milliseconds', 3),
EEEE: dateStrGetter('Day'),
EEE: dateStrGetter('Day', true),
a: ampmGetter,
- Z: timeZoneGetter
+ Z: timeZoneGetter,
+ ww: weekGetter(2),
+ w: weekGetter(1),
+ G: eraGetter,
+ GG: eraGetter,
+ GGG: eraGetter,
+ GGGG: longEraGetter
};
-var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+|H+|h+|m+|s+|a|Z))(.*)/,
- NUMBER_STRING = /^\d+$/;
+var DATE_FORMATS_SPLIT = /((?:[^yMLdHhmsaZEwG']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|L+|d+|H+|h+|m+|s+|a|Z|G+|w+))([\s\S]*)/,
+ NUMBER_STRING = /^-?\d+$/;
/**
* @ngdoc filter
- * @name ng.filter:date
- * @function
+ * @name date
+ * @kind function
*
* @description
* Formats `date` to a string based on the requested `format`.
@@ -10243,93 +22950,117 @@ var DATE_FORMATS_SPLIT = /((?:[^yMdHhmsaZE']+)|(?:'(?:[^']|'')*')|(?:E+|y+|M+|d+
* * `'MMM'`: Month in year (Jan-Dec)
* * `'MM'`: Month in year, padded (01-12)
* * `'M'`: Month in year (1-12)
+ * * `'LLLL'`: Stand-alone month in year (January-December)
* * `'dd'`: Day in month, padded (01-31)
* * `'d'`: Day in month (1-31)
* * `'EEEE'`: Day in Week,(Sunday-Saturday)
* * `'EEE'`: Day in Week, (Sun-Sat)
* * `'HH'`: Hour in day, padded (00-23)
* * `'H'`: Hour in day (0-23)
- * * `'hh'`: Hour in am/pm, padded (01-12)
- * * `'h'`: Hour in am/pm, (1-12)
+ * * `'hh'`: Hour in AM/PM, padded (01-12)
+ * * `'h'`: Hour in AM/PM, (1-12)
* * `'mm'`: Minute in hour, padded (00-59)
* * `'m'`: Minute in hour (0-59)
* * `'ss'`: Second in minute, padded (00-59)
* * `'s'`: Second in minute (0-59)
- * * `'a'`: am/pm marker
+ * * `'sss'`: Millisecond in second, padded (000-999)
+ * * `'a'`: AM/PM marker
* * `'Z'`: 4 digit (+sign) representation of the timezone offset (-1200-+1200)
+ * * `'ww'`: Week of year, padded (00-53). Week 01 is the week with the first Thursday of the year
+ * * `'w'`: Week of year (0-53). Week 1 is the week with the first Thursday of the year
+ * * `'G'`, `'GG'`, `'GGG'`: The abbreviated form of the era string (e.g. 'AD')
+ * * `'GGGG'`: The long form of the era string (e.g. 'Anno Domini')
*
* `format` string can also be one of the following predefined
* {@link guide/i18n localizable formats}:
*
* * `'medium'`: equivalent to `'MMM d, y h:mm:ss a'` for en_US locale
- * (e.g. Sep 3, 2010 12:05:08 pm)
- * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 pm)
- * * `'fullDate'`: equivalent to `'EEEE, MMMM d,y'` for en_US locale
+ * (e.g. Sep 3, 2010 12:05:08 PM)
+ * * `'short'`: equivalent to `'M/d/yy h:mm a'` for en_US locale (e.g. 9/3/10 12:05 PM)
+ * * `'fullDate'`: equivalent to `'EEEE, MMMM d, y'` for en_US locale
* (e.g. Friday, September 3, 2010)
* * `'longDate'`: equivalent to `'MMMM d, y'` for en_US locale (e.g. September 3, 2010)
* * `'mediumDate'`: equivalent to `'MMM d, y'` for en_US locale (e.g. Sep 3, 2010)
* * `'shortDate'`: equivalent to `'M/d/yy'` for en_US locale (e.g. 9/3/10)
- * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 pm)
- * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 pm)
+ * * `'mediumTime'`: equivalent to `'h:mm:ss a'` for en_US locale (e.g. 12:05:08 PM)
+ * * `'shortTime'`: equivalent to `'h:mm a'` for en_US locale (e.g. 12:05 PM)
*
- * `format` string can contain literal values. These need to be quoted with single quotes (e.g.
- * `"h 'in the morning'"`). In order to output single quote, use two single quotes in a sequence
+ * `format` string can contain literal values. These need to be escaped by surrounding with single quotes (e.g.
+ * `"h 'in the morning'"`). In order to output a single quote, escape it - i.e., two single quotes in a sequence
* (e.g. `"h 'o''clock'"`).
*
+ * Any other characters in the `format` string will be output as-is.
+ *
* @param {(Date|number|string)} date Date to format either as Date object, milliseconds (string or
- * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.SSSZ and its
+ * number) or various ISO 8601 datetime string formats (e.g. yyyy-MM-ddTHH:mm:ss.sssZ and its
* shorter versions like yyyy-MM-ddTHH:mmZ, yyyy-MM-dd or yyyyMMddTHHmmssZ). If no timezone is
* specified in the string input, the time is considered to be in the local timezone.
* @param {string=} format Formatting rules (see Description). If not specified,
* `mediumDate` is used.
+ * @param {string=} timezone Timezone to be used for formatting. It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
* @returns {string} Formatted string or the input if input is not recognized as date/millis.
*
* @example
-
-
+
+
{{1288323623006 | date:'medium'}} :
- {{1288323623006 | date:'medium'}}
+ {{1288323623006 | date:'medium'}}
{{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}} :
- {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
+ {{1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'}}
{{1288323623006 | date:'MM/dd/yyyy @ h:mma'}} :
- {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
-
-
+ {{'1288323623006' | date:'MM/dd/yyyy @ h:mma'}}
+ {{1288323623006 | date:"MM/dd/yyyy 'at' h:mma"}} :
+ {{'1288323623006' | date:"MM/dd/yyyy 'at' h:mma"}}
+
+
it('should format date', function() {
- expect(binding("1288323623006 | date:'medium'")).
+ expect(element(by.binding("1288323623006 | date:'medium'")).getText()).
toMatch(/Oct 2\d, 2010 \d{1,2}:\d{2}:\d{2} (AM|PM)/);
- expect(binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).
- toMatch(/2010\-10\-2\d \d{2}:\d{2}:\d{2} (\-|\+)?\d{4}/);
- expect(binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).
+ expect(element(by.binding("1288323623006 | date:'yyyy-MM-dd HH:mm:ss Z'")).getText()).
+ toMatch(/2010-10-2\d \d{2}:\d{2}:\d{2} (-|\+)?\d{4}/);
+ expect(element(by.binding("'1288323623006' | date:'MM/dd/yyyy @ h:mma'")).getText()).
toMatch(/10\/2\d\/2010 @ \d{1,2}:\d{2}(AM|PM)/);
+ expect(element(by.binding("'1288323623006' | date:\"MM/dd/yyyy 'at' h:mma\"")).getText()).
+ toMatch(/10\/2\d\/2010 at \d{1,2}:\d{2}(AM|PM)/);
});
-
-
+
+
*/
dateFilter.$inject = ['$locale'];
function dateFilter($locale) {
var R_ISO8601_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?::?(\d\d)(?::?(\d\d)(?:\.(\d+))?)?)?(Z|([+-])(\d\d):?(\d\d))?)?$/;
- function jsonStringToDate(string){
+ // 1 2 3 4 5 6 7 8 9 10 11
+ function jsonStringToDate(string) {
var match;
- if (match = string.match(R_ISO8601_STR)) {
+ if ((match = string.match(R_ISO8601_STR))) {
var date = new Date(0),
tzHour = 0,
- tzMin = 0;
+ tzMin = 0,
+ dateSetter = match[8] ? date.setUTCFullYear : date.setFullYear,
+ timeSetter = match[8] ? date.setUTCHours : date.setHours;
+
if (match[9]) {
- tzHour = int(match[9] + match[10]);
- tzMin = int(match[9] + match[11]);
+ tzHour = toInt(match[9] + match[10]);
+ tzMin = toInt(match[9] + match[11]);
}
- date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3]));
- date.setUTCHours(int(match[4]||0) - tzHour, int(match[5]||0) - tzMin, int(match[6]||0), int(match[7]||0));
+ dateSetter.call(date, toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
+ var h = toInt(match[4] || 0) - tzHour;
+ var m = toInt(match[5] || 0) - tzMin;
+ var s = toInt(match[6] || 0);
+ var ms = Math.round(parseFloat('0.' + (match[7] || 0)) * 1000);
+ timeSetter.call(date, h, m, s, ms);
return date;
}
return string;
}
- return function(date, format) {
+ return function(date, format, timezone) {
var text = '',
parts = [],
fn, match;
@@ -10337,22 +23068,18 @@ function dateFilter($locale) {
format = format || 'mediumDate';
format = $locale.DATETIME_FORMATS[format] || format;
if (isString(date)) {
- if (NUMBER_STRING.test(date)) {
- date = int(date);
- } else {
- date = jsonStringToDate(date);
- }
+ date = NUMBER_STRING.test(date) ? toInt(date) : jsonStringToDate(date);
}
if (isNumber(date)) {
date = new Date(date);
}
- if (!isDate(date)) {
+ if (!isDate(date) || !isFinite(date.getTime())) {
return date;
}
- while(format) {
+ while (format) {
match = DATE_FORMATS_SPLIT.exec(format);
if (match) {
parts = concat(parts, match, 1);
@@ -10363,10 +23090,15 @@ function dateFilter($locale) {
}
}
- forEach(parts, function(value){
+ var dateTimezoneOffset = date.getTimezoneOffset();
+ if (timezone) {
+ dateTimezoneOffset = timezoneToOffset(timezone, dateTimezoneOffset);
+ date = convertTimezoneToLocal(date, timezone, true);
+ }
+ forEach(parts, function(value) {
fn = DATE_FORMATS[value];
- text += fn ? fn(date, $locale.DATETIME_FORMATS)
- : value.replace(/(^'|'$)/g, '').replace(/''/g, "'");
+ text += fn ? fn(date, $locale.DATETIME_FORMATS, dateTimezoneOffset)
+ : value === '\'\'' ? '\'' : value.replace(/(^'|'$)/g, '').replace(/''/g, '\'');
});
return text;
@@ -10376,8 +23108,8 @@ function dateFilter($locale) {
/**
* @ngdoc filter
- * @name ng.filter:json
- * @function
+ * @name json
+ * @kind function
*
* @description
* Allows you to convert a JavaScript object into JSON string.
@@ -10386,35 +23118,44 @@ function dateFilter($locale) {
* the binding is automatically converted to JSON.
*
* @param {*} object Any JavaScript object (including arrays and primitive types) to filter.
+ * @param {number=} spacing The number of spaces to use per indentation, defaults to 2.
* @returns {string} JSON string.
*
*
- * @example:
-
-
- {{ {'name':'value'} | json }}
-
-
+ * @example
+
+
+ {{ {'name':'value'} | json }}
+ {{ {'name':'value'} | json:4 }}
+
+
it('should jsonify filtered objects', function() {
- expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/);
+ expect(element(by.id('default-spacing')).getText()).toMatch(/\{\n {2}"name": ?"value"\n}/);
+ expect(element(by.id('custom-spacing')).getText()).toMatch(/\{\n {4}"name": ?"value"\n}/);
});
-
-
+
+
*
*/
function jsonFilter() {
- return function(object) {
- return toJson(object, true);
+ return function(object, spacing) {
+ if (isUndefined(spacing)) {
+ spacing = 2;
+ }
+ return toJson(object, spacing);
};
}
/**
* @ngdoc filter
- * @name ng.filter:lowercase
- * @function
+ * @name lowercase
+ * @kind function
* @description
* Converts string to lowercase.
+ *
+ * See the {@link ng.uppercase uppercase filter documentation} for a functionally identical example.
+ *
* @see angular.lowercase
*/
var lowercaseFilter = valueFn(lowercase);
@@ -10422,243 +23163,873 @@ var lowercaseFilter = valueFn(lowercase);
/**
* @ngdoc filter
- * @name ng.filter:uppercase
- * @function
+ * @name uppercase
+ * @kind function
* @description
* Converts string to uppercase.
- * @see angular.uppercase
+ * @example
+
+
+
+
+
+
{{title}}
+
+ {{title | uppercase}}
+
+
+
*/
var uppercaseFilter = valueFn(uppercase);
/**
- * @ngdoc function
- * @name ng.filter:limitTo
- * @function
+ * @ngdoc filter
+ * @name limitTo
+ * @kind function
*
* @description
- * Creates a new array containing only a specified number of elements in an array. The elements
- * are taken from either the beginning or the end of the source array, as specified by the
- * value and sign (positive or negative) of `limit`.
+ * Creates a new array or string containing only a specified number of elements. The elements are
+ * taken from either the beginning or the end of the source array, string or number, as specified by
+ * the value and sign (positive or negative) of `limit`. Other array-like objects are also supported
+ * (e.g. array subclasses, NodeLists, jqLite/jQuery collections etc). If a number is used as input,
+ * it is converted to a string.
*
- * Note: This function is used to augment the `Array` type in Angular expressions. See
- * {@link ng.$filter} for more information about Angular arrays.
- *
- * @param {Array} array Source array to be limited.
- * @param {string|Number} limit The length of the returned array. If the `limit` number is
- * positive, `limit` number of items from the beginning of the source array are copied.
- * If the number is negative, `limit` number of items from the end of the source array are
- * copied. The `limit` will be trimmed if it exceeds `array.length`
- * @returns {Array} A new sub-array of length `limit` or less if input array had less than `limit`
- * elements.
+ * @param {Array|ArrayLike|string|number} input - Array/array-like, string or number to be limited.
+ * @param {string|number} limit - The length of the returned array or string. If the `limit` number
+ * is positive, `limit` number of items from the beginning of the source array/string are copied.
+ * If the number is negative, `limit` number of items from the end of the source array/string
+ * are copied. The `limit` will be trimmed if it exceeds `array.length`. If `limit` is undefined,
+ * the input will be returned unchanged.
+ * @param {(string|number)=} begin - Index at which to begin limitation. As a negative index,
+ * `begin` indicates an offset from the end of `input`. Defaults to `0`.
+ * @returns {Array|string} A new sub-array or substring of length `limit` or less if the input had
+ * less than `limit` elements.
*
* @example
-
-
+
+
-
- Limit {{numbers}} to:
-
Output: {{ numbers | limitTo:limit }}
+
+
+ Limit {{numbers}} to:
+
+
+
Output numbers: {{ numbers | limitTo:numLimit }}
+
+ Limit {{letters}} to:
+
+
+
Output letters: {{ letters | limitTo:letterLimit }}
+
+ Limit {{longNumber}} to:
+
+
+
Output long number: {{ longNumber | limitTo:longNumberLimit }}
-
-
- it('should limit the numer array to first three items', function() {
- expect(element('.doc-example-live input[ng-model=limit]').val()).toBe('3');
- expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3]');
+
+
+ var numLimitInput = element(by.model('numLimit'));
+ var letterLimitInput = element(by.model('letterLimit'));
+ var longNumberLimitInput = element(by.model('longNumberLimit'));
+ var limitedNumbers = element(by.binding('numbers | limitTo:numLimit'));
+ var limitedLetters = element(by.binding('letters | limitTo:letterLimit'));
+ var limitedLongNumber = element(by.binding('longNumber | limitTo:longNumberLimit'));
+
+ it('should limit the number array to first three items', function() {
+ expect(numLimitInput.getAttribute('value')).toBe('3');
+ expect(letterLimitInput.getAttribute('value')).toBe('3');
+ expect(longNumberLimitInput.getAttribute('value')).toBe('3');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abc');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 234');
});
- it('should update the output when -3 is entered', function() {
- input('limit').enter(-3);
- expect(binding('numbers | limitTo:limit')).toEqual('[7,8,9]');
- });
+ // There is a bug in safari and protractor that doesn't like the minus key
+ // it('should update the output when -3 is entered', function() {
+ // numLimitInput.clear();
+ // numLimitInput.sendKeys('-3');
+ // letterLimitInput.clear();
+ // letterLimitInput.sendKeys('-3');
+ // longNumberLimitInput.clear();
+ // longNumberLimitInput.sendKeys('-3');
+ // expect(limitedNumbers.getText()).toEqual('Output numbers: [7,8,9]');
+ // expect(limitedLetters.getText()).toEqual('Output letters: ghi');
+ // expect(limitedLongNumber.getText()).toEqual('Output long number: 342');
+ // });
it('should not exceed the maximum size of input array', function() {
- input('limit').enter(100);
- expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3,4,5,6,7,8,9]');
+ numLimitInput.clear();
+ numLimitInput.sendKeys('100');
+ letterLimitInput.clear();
+ letterLimitInput.sendKeys('100');
+ longNumberLimitInput.clear();
+ longNumberLimitInput.sendKeys('100');
+ expect(limitedNumbers.getText()).toEqual('Output numbers: [1,2,3,4,5,6,7,8,9]');
+ expect(limitedLetters.getText()).toEqual('Output letters: abcdefghi');
+ expect(limitedLongNumber.getText()).toEqual('Output long number: 2345432342');
});
-
-
- */
-function limitToFilter(){
- return function(array, limit) {
- if (!(array instanceof Array)) return array;
- limit = int(limit);
- var out = [],
- i, n;
-
- // check that array is iterable
- if (!array || !(array instanceof Array))
- return out;
-
- // if abs(limit) exceeds maximum length, trim it
- if (limit > array.length)
- limit = array.length;
- else if (limit < -array.length)
- limit = -array.length;
-
- if (limit > 0) {
- i = 0;
- n = limit;
+
+
+*/
+function limitToFilter() {
+ return function(input, limit, begin) {
+ if (Math.abs(Number(limit)) === Infinity) {
+ limit = Number(limit);
} else {
- i = array.length + limit;
- n = array.length;
+ limit = toInt(limit);
}
+ if (isNumberNaN(limit)) return input;
- for (; i
= 0) {
+ return sliceFn(input, begin, begin + limit);
+ } else {
+ if (begin === 0) {
+ return sliceFn(input, limit, input.length);
+ } else {
+ return sliceFn(input, Math.max(0, begin + limit), begin);
+ }
}
+ };
+}
- return out;
- }
+function sliceFn(input, begin, end) {
+ if (isString(input)) return input.slice(begin, end);
+
+ return slice.call(input, begin, end);
}
/**
- * @ngdoc function
- * @name ng.filter:orderBy
- * @function
+ * @ngdoc filter
+ * @name orderBy
+ * @kind function
*
* @description
- * Orders a specified `array` by the `expression` predicate.
+ * Returns an array containing the items from the specified `collection`, ordered by a `comparator`
+ * function based on the values computed using the `expression` predicate.
*
- * Note: this function is used to augment the `Array` type in Angular expressions. See
- * {@link ng.$filter} for more informaton about Angular arrays.
+ * For example, `[{id: 'foo'}, {id: 'bar'}] | orderBy:'id'` would result in
+ * `[{id: 'bar'}, {id: 'foo'}]`.
*
- * @param {Array} array The array to sort.
- * @param {function(*)|string|Array.<(function(*)|string)>} expression A predicate to be
- * used by the comparator to determine the order of elements.
+ * The `collection` can be an Array or array-like object (e.g. NodeList, jQuery object, TypedArray,
+ * String, etc).
+ *
+ * The `expression` can be a single predicate, or a list of predicates each serving as a tie-breaker
+ * for the preceding one. The `expression` is evaluated against each item and the output is used
+ * for comparing with other items.
+ *
+ * You can change the sorting order by setting `reverse` to `true`. By default, items are sorted in
+ * ascending order.
+ *
+ * The comparison is done using the `comparator` function. If none is specified, a default, built-in
+ * comparator is used (see below for details - in a nutshell, it compares numbers numerically and
+ * strings alphabetically).
+ *
+ * ### Under the hood
+ *
+ * Ordering the specified `collection` happens in two phases:
+ *
+ * 1. All items are passed through the predicate (or predicates), and the returned values are saved
+ * along with their type (`string`, `number` etc). For example, an item `{label: 'foo'}`, passed
+ * through a predicate that extracts the value of the `label` property, would be transformed to:
+ * ```
+ * {
+ * value: 'foo',
+ * type: 'string',
+ * index: ...
+ * }
+ * ```
+ * **Note:** `null` values use `'null'` as their type.
+ * 2. The comparator function is used to sort the items, based on the derived values, types and
+ * indices.
+ *
+ * If you use a custom comparator, it will be called with pairs of objects of the form
+ * `{value: ..., type: '...', index: ...}` and is expected to return `0` if the objects are equal
+ * (as far as the comparator is concerned), `-1` if the 1st one should be ranked higher than the
+ * second, or `1` otherwise.
+ *
+ * In order to ensure that the sorting will be deterministic across platforms, if none of the
+ * specified predicates can distinguish between two items, `orderBy` will automatically introduce a
+ * dummy predicate that returns the item's index as `value`.
+ * (If you are using a custom comparator, make sure it can handle this predicate as well.)
+ *
+ * If a custom comparator still can't distinguish between two items, then they will be sorted based
+ * on their index using the built-in comparator.
+ *
+ * Finally, in an attempt to simplify things, if a predicate returns an object as the extracted
+ * value for an item, `orderBy` will try to convert that object to a primitive value, before passing
+ * it to the comparator. The following rules govern the conversion:
+ *
+ * 1. If the object has a `valueOf()` method that returns a primitive, its return value will be
+ * used instead.
+ * (If the object has a `valueOf()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 2. If the object has a custom `toString()` method (i.e. not the one inherited from `Object`) that
+ * returns a primitive, its return value will be used instead.
+ * (If the object has a `toString()` method that returns another object, then the returned object
+ * will be used in subsequent steps.)
+ * 3. No conversion; the object itself is used.
+ *
+ * ### The default comparator
+ *
+ * The default, built-in comparator should be sufficient for most usecases. In short, it compares
+ * numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
+ * using their index in the original collection, sorts values of different types by type and puts
+ * `undefined` and `null` values at the end of the sorted list.
+ *
+ * More specifically, it follows these steps to determine the relative order of items:
+ *
+ * 1. If the compared values are of different types:
+ * - If one of the values is undefined, consider it "greater than" the other.
+ * - Else if one of the values is null, consider it "greater than" the other.
+ * - Else compare the types themselves alphabetically.
+ * 2. If both values are of type `string`, compare them alphabetically in a case- and
+ * locale-insensitive way.
+ * 3. If both values are objects, compare their indices instead.
+ * 4. Otherwise, return:
+ * - `0`, if the values are equal (by strict equality comparison, i.e. using `===`).
+ * - `-1`, if the 1st value is "less than" the 2nd value (compared using the `<` operator).
+ * - `1`, otherwise.
+ *
+ * **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
+ * saved as numbers and not strings.
+ * **Note:** For the purpose of sorting, `null` and `undefined` are considered "greater than"
+ * any other value (with undefined "greater than" null). This effectively means that `null`
+ * and `undefined` values end up at the end of a list sorted in ascending order.
+ * **Note:** `null` values use `'null'` as their type to be able to distinguish them from objects.
+ *
+ * @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
+ * @param {(Function|string|Array.)=} expression - A predicate (or list of
+ * predicates) to be used by the comparator to determine the order of elements.
*
* Can be one of:
*
- * - `function`: Getter function. The result of this function will be sorted using the
- * `<`, `=`, `>` operator.
- * - `string`: An Angular expression which evaluates to an object to order by, such as 'name'
- * to sort by a property called 'name'. Optionally prefixed with `+` or `-` to control
- * ascending or descending sort order (for example, +name or -name).
- * - `Array`: An array of function or string predicates. The first predicate in the array
- * is used for sorting, but when two items are equivalent, the next predicate is used.
+ * - `Function`: A getter function. This function will be called with each item as argument and
+ * the return value will be used for sorting.
+ * - `string`: An AngularJS expression. This expression will be evaluated against each item and the
+ * result will be used for sorting. For example, use `'label'` to sort by a property called
+ * `label` or `'label.substring(0, 3)'` to sort by the first 3 characters of the `label`
+ * property.
+ * (The result of a constant expression is interpreted as a property name to be used for
+ * comparison. For example, use `'"special name"'` (note the extra pair of quotes) to sort by a
+ * property called `special name`.)
+ * An expression can be optionally prefixed with `+` or `-` to control the sorting direction,
+ * ascending or descending. For example, `'+label'` or `'-label'`. If no property is provided,
+ * (e.g. `'+'` or `'-'`), the collection element itself is used in comparisons.
+ * - `Array`: An array of function and/or string predicates. If a predicate cannot determine the
+ * relative order of two items, the next predicate is used as a tie-breaker.
+ *
+ * **Note:** If the predicate is missing or empty then it defaults to `'+'`.
+ *
+ * @param {boolean=} reverse - If `true`, reverse the sorting order.
+ * @param {(Function)=} comparator - The comparator function used to determine the relative order of
+ * value pairs. If omitted, the built-in comparator will be used.
+ *
+ * @returns {Array} - The sorted array.
*
- * @param {boolean=} reverse Reverse the order the array.
- * @returns {Array} Sorted copy of the source array.
*
* @example
-
-
-
-
-
Sorting predicate = {{predicate}}; reverse = {{reverse}}
-
- [
unsorted ]
-
+ * ### Ordering a table with `ngRepeat`
+ *
+ * The example below demonstrates a simple {@link ngRepeat ngRepeat}, where the data is sorted by
+ * age in descending order (expression is set to `'-age'`). The `comparator` is not set, which means
+ * it defaults to the built-in comparator.
+ *
+
+
+
+
- Name
- (^ )
- Phone Number
- Age
+ Name
+ Phone Number
+ Age
-
+
{{friend.name}}
{{friend.phone}}
{{friend.age}}
-
-
- it('should be reverse ordered by aged', function() {
- expect(binding('predicate')).toBe('-age');
- expect(repeater('table.friend', 'friend in friends').column('friend.age')).
- toEqual(['35', '29', '21', '19', '10']);
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']);
+
+
+ angular.module('orderByExample1', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var names = element.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by age in reverse order', function() {
+ expect(names.get(0).getText()).toBe('Adam');
+ expect(names.get(1).getText()).toBe('Julie');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('John');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Changing parameters dynamically
+ *
+ * All parameters can be changed dynamically. The next example shows how you can make the columns of
+ * a table sortable, by binding the `expression` and `reverse` parameters to scope properties.
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
+
+
+
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample2', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = friends;
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = ($scope.propertyName === propertyName) ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
});
- it('should reorder the table when user selects different predicate', function() {
- element('.doc-example-live a:contains("Name")').click();
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']);
- expect(repeater('table.friend', 'friend in friends').column('friend.age')).
- toEqual(['35', '10', '29', '19', '21']);
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
- element('.doc-example-live a:contains("Phone")').click();
- expect(repeater('table.friend', 'friend in friends').column('friend.phone')).
- toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']);
- expect(repeater('table.friend', 'friend in friends').column('friend.name')).
- toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']);
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
});
-
-
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Using `orderBy` inside a controller
+ *
+ * It is also possible to call the `orderBy` filter manually, by injecting `orderByFilter`, and
+ * calling it with the desired parameters. (Alternatively, you could inject the `$filter` factory
+ * and retrieve the `orderBy` filter with `$filter('orderBy')`.)
+ *
+
+
+
+
Sort by = {{propertyName}}; reverse = {{reverse}}
+
+
Set to unsorted
+
+
+
+
+ Name
+
+
+
+ Phone Number
+
+
+
+ Age
+
+
+
+
+ {{friend.name}}
+ {{friend.phone}}
+ {{friend.age}}
+
+
+
+
+
+ angular.module('orderByExample3', [])
+ .controller('ExampleController', ['$scope', 'orderByFilter', function($scope, orderBy) {
+ var friends = [
+ {name: 'John', phone: '555-1212', age: 10},
+ {name: 'Mary', phone: '555-9876', age: 19},
+ {name: 'Mike', phone: '555-4321', age: 21},
+ {name: 'Adam', phone: '555-5678', age: 35},
+ {name: 'Julie', phone: '555-8765', age: 29}
+ ];
+
+ $scope.propertyName = 'age';
+ $scope.reverse = true;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+
+ $scope.sortBy = function(propertyName) {
+ $scope.reverse = (propertyName !== null && $scope.propertyName === propertyName)
+ ? !$scope.reverse : false;
+ $scope.propertyName = propertyName;
+ $scope.friends = orderBy(friends, $scope.propertyName, $scope.reverse);
+ };
+ }]);
+
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+ .sortorder:after {
+ content: '\25b2'; // BLACK UP-POINTING TRIANGLE
+ }
+ .sortorder.reverse:after {
+ content: '\25bc'; // BLACK DOWN-POINTING TRIANGLE
+ }
+
+
+ // Element locators
+ var unsortButton = element(by.partialButtonText('unsorted'));
+ var nameHeader = element(by.partialButtonText('Name'));
+ var phoneHeader = element(by.partialButtonText('Phone'));
+ var ageHeader = element(by.partialButtonText('Age'));
+ var firstName = element(by.repeater('friends').column('friend.name').row(0));
+ var lastName = element(by.repeater('friends').column('friend.name').row(4));
+
+ it('should sort friends by some property, when clicking on the column header', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ phoneHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Mary');
+
+ nameHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('Mike');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+ });
+
+ it('should sort friends in reverse order, when clicking on the same column', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Adam');
+
+ ageHeader.click();
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+ });
+
+ it('should restore the original order, when clicking "Set to unsorted"', function() {
+ expect(firstName.getText()).toBe('Adam');
+ expect(lastName.getText()).toBe('John');
+
+ unsortButton.click();
+ expect(firstName.getText()).toBe('John');
+ expect(lastName.getText()).toBe('Julie');
+ });
+
+
+ *
+ *
+ * @example
+ * ### Using a custom comparator
+ *
+ * If you have very specific requirements about the way items are sorted, you can pass your own
+ * comparator function. For example, you might need to compare some strings in a locale-sensitive
+ * way. (When specifying a custom comparator, you also need to pass a value for the `reverse`
+ * argument - passing `false` retains the default sorting order, i.e. ascending.)
+ *
+
+
+
+
+
Locale-sensitive Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
Default Comparator
+
+
+ Name
+ Favorite Letter
+
+
+ {{friend.name}}
+ {{friend.favoriteLetter}}
+
+
+
+
+
+
+ angular.module('orderByExample4', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.friends = [
+ {name: 'John', favoriteLetter: 'Ä'},
+ {name: 'Mary', favoriteLetter: 'Ü'},
+ {name: 'Mike', favoriteLetter: 'Ö'},
+ {name: 'Adam', favoriteLetter: 'H'},
+ {name: 'Julie', favoriteLetter: 'Z'}
+ ];
+
+ $scope.localeSensitiveComparator = function(v1, v2) {
+ // If we don't get strings, just compare by index
+ if (v1.type !== 'string' || v2.type !== 'string') {
+ return (v1.index < v2.index) ? -1 : 1;
+ }
+
+ // Compare strings alphabetically, taking locale into account
+ return v1.value.localeCompare(v2.value);
+ };
+ }]);
+
+
+ .friends-container {
+ display: inline-block;
+ margin: 0 30px;
+ }
+
+ .friends {
+ border-collapse: collapse;
+ }
+
+ .friends th {
+ border-bottom: 1px solid;
+ }
+ .friends td, .friends th {
+ border-left: 1px solid;
+ padding: 5px 10px;
+ }
+ .friends td:first-child, .friends th:first-child {
+ border-left: none;
+ }
+
+
+ // Element locators
+ var container = element(by.css('.custom-comparator'));
+ var names = container.all(by.repeater('friends').column('friend.name'));
+
+ it('should sort friends by favorite letter (in correct alphabetical order)', function() {
+ expect(names.get(0).getText()).toBe('John');
+ expect(names.get(1).getText()).toBe('Adam');
+ expect(names.get(2).getText()).toBe('Mike');
+ expect(names.get(3).getText()).toBe('Mary');
+ expect(names.get(4).getText()).toBe('Julie');
+ });
+
+
+ *
*/
orderByFilter.$inject = ['$parse'];
-function orderByFilter($parse){
- return function(array, sortPredicate, reverseOrder) {
- if (!isArray(array)) return array;
- if (!sortPredicate) return array;
- sortPredicate = isArray(sortPredicate) ? sortPredicate: [sortPredicate];
- sortPredicate = map(sortPredicate, function(predicate){
- var descending = false, get = predicate || identity;
- if (isString(predicate)) {
- if ((predicate.charAt(0) == '+' || predicate.charAt(0) == '-')) {
- descending = predicate.charAt(0) == '-';
+function orderByFilter($parse) {
+ return function(array, sortPredicate, reverseOrder, compareFn) {
+
+ if (array == null) return array;
+ if (!isArrayLike(array)) {
+ throw minErr('orderBy')('notarray', 'Expected array but received: {0}', array);
+ }
+
+ if (!isArray(sortPredicate)) { sortPredicate = [sortPredicate]; }
+ if (sortPredicate.length === 0) { sortPredicate = ['+']; }
+
+ var predicates = processPredicates(sortPredicate);
+
+ var descending = reverseOrder ? -1 : 1;
+
+ // Define the `compare()` function. Use a default comparator if none is specified.
+ var compare = isFunction(compareFn) ? compareFn : defaultCompare;
+
+ // The next three lines are a version of a Swartzian Transform idiom from Perl
+ // (sometimes called the Decorate-Sort-Undecorate idiom)
+ // See https://en.wikipedia.org/wiki/Schwartzian_transform
+ var compareValues = Array.prototype.map.call(array, getComparisonObject);
+ compareValues.sort(doComparison);
+ array = compareValues.map(function(item) { return item.value; });
+
+ return array;
+
+ function getComparisonObject(value, index) {
+ // NOTE: We are adding an extra `tieBreaker` value based on the element's index.
+ // This will be used to keep the sort stable when none of the input predicates can
+ // distinguish between two elements.
+ return {
+ value: value,
+ tieBreaker: {value: index, type: 'number', index: index},
+ predicateValues: predicates.map(function(predicate) {
+ return getPredicateValue(predicate.get(value), index);
+ })
+ };
+ }
+
+ function doComparison(v1, v2) {
+ for (var i = 0, ii = predicates.length; i < ii; i++) {
+ var result = compare(v1.predicateValues[i], v2.predicateValues[i]);
+ if (result) {
+ return result * predicates[i].descending * descending;
+ }
+ }
+
+ return (compare(v1.tieBreaker, v2.tieBreaker) || defaultCompare(v1.tieBreaker, v2.tieBreaker)) * descending;
+ }
+ };
+
+ function processPredicates(sortPredicates) {
+ return sortPredicates.map(function(predicate) {
+ var descending = 1, get = identity;
+
+ if (isFunction(predicate)) {
+ get = predicate;
+ } else if (isString(predicate)) {
+ if ((predicate.charAt(0) === '+' || predicate.charAt(0) === '-')) {
+ descending = predicate.charAt(0) === '-' ? -1 : 1;
predicate = predicate.substring(1);
}
- get = $parse(predicate);
- }
- return reverseComparator(function(a,b){
- return compare(get(a),get(b));
- }, descending);
- });
- var arrayCopy = [];
- for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); }
- return arrayCopy.sort(reverseComparator(comparator, reverseOrder));
-
- function comparator(o1, o2){
- for ( var i = 0; i < sortPredicate.length; i++) {
- var comp = sortPredicate[i](o1, o2);
- if (comp !== 0) return comp;
- }
- return 0;
- }
- function reverseComparator(comp, descending) {
- return toBoolean(descending)
- ? function(a,b){return comp(b,a);}
- : comp;
- }
- function compare(v1, v2){
- var t1 = typeof v1;
- var t2 = typeof v2;
- if (t1 == t2) {
- if (t1 == "string") {
- v1 = v1.toLowerCase();
- v2 = v2.toLowerCase();
+ if (predicate !== '') {
+ get = $parse(predicate);
+ if (get.constant) {
+ var key = get();
+ get = function(value) { return value[key]; };
+ }
}
- if (v1 === v2) return 0;
- return v1 < v2 ? -1 : 1;
- } else {
- return t1 < t2 ? -1 : 1;
}
+ return {get: get, descending: descending};
+ });
+ }
+
+ function isPrimitive(value) {
+ switch (typeof value) {
+ case 'number': /* falls through */
+ case 'boolean': /* falls through */
+ case 'string':
+ return true;
+ default:
+ return false;
}
}
+
+ function objectValue(value) {
+ // If `valueOf` is a valid function use that
+ if (isFunction(value.valueOf)) {
+ value = value.valueOf();
+ if (isPrimitive(value)) return value;
+ }
+ // If `toString` is a valid function and not the one from `Object.prototype` use that
+ if (hasCustomToString(value)) {
+ value = value.toString();
+ if (isPrimitive(value)) return value;
+ }
+
+ return value;
+ }
+
+ function getPredicateValue(value, index) {
+ var type = typeof value;
+ if (value === null) {
+ type = 'null';
+ } else if (type === 'object') {
+ value = objectValue(value);
+ }
+ return {value: value, type: type, index: index};
+ }
+
+ function defaultCompare(v1, v2) {
+ var result = 0;
+ var type1 = v1.type;
+ var type2 = v2.type;
+
+ if (type1 === type2) {
+ var value1 = v1.value;
+ var value2 = v2.value;
+
+ if (type1 === 'string') {
+ // Compare strings case-insensitively
+ value1 = value1.toLowerCase();
+ value2 = value2.toLowerCase();
+ } else if (type1 === 'object') {
+ // For basic objects, use the position of the object
+ // in the collection instead of the value
+ if (isObject(value1)) value1 = v1.index;
+ if (isObject(value2)) value2 = v2.index;
+ }
+
+ if (value1 !== value2) {
+ result = value1 < value2 ? -1 : 1;
+ }
+ } else {
+ result = (type1 === 'undefined') ? 1 :
+ (type2 === 'undefined') ? -1 :
+ (type1 === 'null') ? 1 :
+ (type2 === 'null') ? -1 :
+ (type1 < type2) ? -1 : 1;
+ }
+
+ return result;
+ }
}
function ngDirective(directive) {
if (isFunction(directive)) {
directive = {
link: directive
- }
+ };
}
directive.restrict = directive.restrict || 'AC';
return valueFn(directive);
@@ -10666,76 +24037,69 @@ function ngDirective(directive) {
/**
* @ngdoc directive
- * @name ng.directive:a
+ * @name a
* @restrict E
*
* @description
- * Modifies the default behavior of html A tag, so that the default action is prevented when href
- * attribute is empty.
+ * Modifies the default behavior of the html a tag so that the default action is prevented when
+ * the href attribute is empty.
*
- * The reasoning for this change is to allow easy creation of action links with `ngClick` directive
- * without changing the location or causing page reloads, e.g.:
- * `Save `
+ * For dynamically creating `href` attributes for a tags, see the {@link ng.ngHref `ngHref`} directive.
*/
var htmlAnchorDirective = valueFn({
restrict: 'E',
compile: function(element, attr) {
+ if (!attr.href && !attr.xlinkHref) {
+ return function(scope, element) {
+ // If the linked element is not an anchor tag anymore, do nothing
+ if (element[0].nodeName.toLowerCase() !== 'a') return;
- if (msie <= 8) {
-
- // turn link into a stylable link in IE
- // but only if it doesn't have name attribute, in which case it's an anchor
- if (!attr.href && !attr.name) {
- attr.$set('href', '');
- }
-
- // add a comment node to anchors to workaround IE bug that causes element content to be reset
- // to new attribute content if attribute is updated with value containing @ and element also
- // contains value with @
- // see issue #1949
- element.append(document.createComment('IE fix'));
- }
-
- return function(scope, element) {
- element.bind('click', function(event){
- // if we have no href url, then don't navigate anywhere.
- if (!element.attr('href')) {
- event.preventDefault();
- }
- });
+ // SVGAElement does not use the href attribute, but rather the 'xlinkHref' attribute.
+ var href = toString.call(element.prop('href')) === '[object SVGAnimatedString]' ?
+ 'xlink:href' : 'href';
+ element.on('click', function(event) {
+ // if we have no href url, then don't navigate anywhere.
+ if (!element.attr(href)) {
+ event.preventDefault();
+ }
+ });
+ };
}
}
});
/**
* @ngdoc directive
- * @name ng.directive:ngHref
+ * @name ngHref
* @restrict A
+ * @priority 99
*
* @description
- * Using Angular markup like {{hash}} in an href attribute makes
- * the page open to a wrong URL, if the user clicks that link before
- * angular has a chance to replace the {{hash}} with actual URL, the
- * link will be broken and will most likely return a 404 error.
- * The `ngHref` directive solves this problem.
+ * Using AngularJS markup like `{{hash}}` in an href attribute will
+ * make the link go to the wrong URL if the user clicks it before
+ * AngularJS has a chance to replace the `{{hash}}` markup with its
+ * value. Until AngularJS replaces the markup the link will be broken
+ * and will most likely return a 404 error. The `ngHref` directive
+ * solves this problem.
*
- * The buggy way to write it:
- *
- *
- *
+ * The wrong way to write it:
+ * ```html
+ * link1
+ * ```
*
* The correct way to write it:
- *
- *
- *
+ * ```html
+ * link1
+ * ```
*
* @element A
* @param {template} ngHref any string which can contain `{{}}` markup.
*
* @example
- * This example uses `link` variable inside `href` attribute:
-
-
+ * This example shows various combinations of `href`, `ng-href` and `ng-click` attributes
+ * in links and their different behaviors:
+
+
link 1 (link, don't reload)
link 2 (link, don't reload)
@@ -10743,70 +24107,87 @@ var htmlAnchorDirective = valueFn({
anchor (link, don't reload)
anchor (no link)
link (link, change location)
-
-
+
+
it('should execute ng-click but not reload when href without value', function() {
- element('#link-1').click();
- expect(input('value').val()).toEqual('1');
- expect(element('#link-1').attr('href')).toBe("");
+ element(by.id('link-1')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('1');
+ expect(element(by.id('link-1')).getAttribute('href')).toBe('');
});
it('should execute ng-click but not reload when href empty string', function() {
- element('#link-2').click();
- expect(input('value').val()).toEqual('2');
- expect(element('#link-2').attr('href')).toBe("");
+ element(by.id('link-2')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('2');
+ expect(element(by.id('link-2')).getAttribute('href')).toBe('');
});
it('should execute ng-click and change url when ng-href specified', function() {
- expect(element('#link-3').attr('href')).toBe("/123");
+ expect(element(by.id('link-3')).getAttribute('href')).toMatch(/\/123$/);
- element('#link-3').click();
- expect(browser().window().path()).toEqual('/123');
+ element(by.id('link-3')).click();
+
+ // At this point, we navigate away from an AngularJS page, so we need
+ // to use browser.driver to get the base webdriver.
+
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/123$/);
+ });
+ }, 5000, 'page should navigate to /123');
});
it('should execute ng-click but not reload when href empty string and name specified', function() {
- element('#link-4').click();
- expect(input('value').val()).toEqual('4');
- expect(element('#link-4').attr('href')).toBe('');
+ element(by.id('link-4')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('4');
+ expect(element(by.id('link-4')).getAttribute('href')).toBe('');
});
it('should execute ng-click but not reload when no href but name specified', function() {
- element('#link-5').click();
- expect(input('value').val()).toEqual('5');
- expect(element('#link-5').attr('href')).toBe(undefined);
+ element(by.id('link-5')).click();
+ expect(element(by.model('value')).getAttribute('value')).toEqual('5');
+ expect(element(by.id('link-5')).getAttribute('href')).toBe(null);
});
it('should only change url when only ng-href', function() {
- input('value').enter('6');
- expect(element('#link-6').attr('href')).toBe('6');
+ element(by.model('value')).clear();
+ element(by.model('value')).sendKeys('6');
+ expect(element(by.id('link-6')).getAttribute('href')).toMatch(/\/6$/);
- element('#link-6').click();
- expect(browser().location().url()).toEqual('/6');
+ element(by.id('link-6')).click();
+
+ // At this point, we navigate away from an AngularJS page, so we need
+ // to use browser.driver to get the base webdriver.
+ browser.wait(function() {
+ return browser.driver.getCurrentUrl().then(function(url) {
+ return url.match(/\/6$/);
+ });
+ }, 5000, 'page should navigate to /6');
});
-
-
+
+
*/
/**
* @ngdoc directive
- * @name ng.directive:ngSrc
+ * @name ngSrc
* @restrict A
+ * @priority 99
*
* @description
- * Using Angular markup like `{{hash}}` in a `src` attribute doesn't
+ * Using AngularJS markup like `{{hash}}` in a `src` attribute doesn't
* work right: The browser will fetch from the URL with the literal
- * text `{{hash}}` until Angular replaces the expression inside
+ * text `{{hash}}` until AngularJS replaces the expression inside
* `{{hash}}`. The `ngSrc` directive solves this problem.
*
* The buggy way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* The correct way to write it:
- *
- *
- *
+ * ```html
+ *
+ * ```
*
* @element IMG
* @param {template} ngSrc any string which can contain `{{}}` markup.
@@ -10814,241 +24195,388 @@ var htmlAnchorDirective = valueFn({
/**
* @ngdoc directive
- * @name ng.directive:ngDisabled
+ * @name ngSrcset
* @restrict A
+ * @priority 99
+ *
+ * @description
+ * Using AngularJS markup like `{{hash}}` in a `srcset` attribute doesn't
+ * work right: The browser will fetch from the URL with the literal
+ * text `{{hash}}` until AngularJS replaces the expression inside
+ * `{{hash}}`. The `ngSrcset` directive solves this problem.
+ *
+ * The buggy way to write it:
+ * ```html
+ *
+ * ```
+ *
+ * The correct way to write it:
+ * ```html
+ *
+ * ```
+ *
+ * @element IMG
+ * @param {template} ngSrcset any string which can contain `{{}}` markup.
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngDisabled
+ * @restrict A
+ * @priority 100
*
* @description
*
- * The following markup will make the button enabled on Chrome/Firefox but not on IE8 and older IEs:
- *
- *
- * Disabled
- *
- *
+ * This directive sets the `disabled` attribute on the element (typically a form control,
+ * e.g. `input`, `button`, `select` etc.) if the
+ * {@link guide/expression expression} inside `ngDisabled` evaluates to truthy.
*
- * The HTML specs do not require browsers to preserve the special attributes such as disabled.
- * (The presence of them means true and absence means false)
- * This prevents the angular compiler from correctly retrieving the binding expression.
- * To solve this problem, we introduce the `ngDisabled` directive.
+ * A special directive is necessary because we cannot use interpolation inside the `disabled`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
-
-
- Click me to toggle:
+
+
+ Click me to toggle:
Button
-
-
+
+
it('should toggle button', function() {
- expect(element('.doc-example-live :button').prop('disabled')).toBeFalsy();
- input('checked').check();
- expect(element('.doc-example-live :button').prop('disabled')).toBeTruthy();
+ expect(element(by.css('button')).getAttribute('disabled')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('button')).getAttribute('disabled')).toBeTruthy();
});
-
-
+
+
*
- * @element INPUT
- * @param {expression} ngDisabled Angular expression that will be evaluated.
+ * @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
+ * then the `disabled` attribute will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngChecked
+ * @name ngChecked
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specs do not require browsers to preserve the special attributes such as checked.
- * (The presence of them means true and absence means false)
- * This prevents the angular compiler from correctly retrieving the binding expression.
- * To solve this problem, we introduce the `ngChecked` directive.
+ * Sets the `checked` attribute on the element, if the expression inside `ngChecked` is truthy.
+ *
+ * Note that this directive should not be used together with {@link ngModel `ngModel`},
+ * as this can lead to unexpected behavior.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `checked`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
* @example
-
-
- Check me to check both:
-
-
-
+
+
+ Check me to check both:
+
+
+
it('should check both checkBoxes', function() {
- expect(element('.doc-example-live #checkSlave').prop('checked')).toBeFalsy();
- input('master').check();
- expect(element('.doc-example-live #checkSlave').prop('checked')).toBeTruthy();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeFalsy();
+ element(by.model('leader')).click();
+ expect(element(by.id('checkFollower')).getAttribute('checked')).toBeTruthy();
});
-
-
+
+
*
* @element INPUT
- * @param {expression} ngChecked Angular expression that will be evaluated.
+ * @param {expression} ngChecked If the {@link guide/expression expression} is truthy,
+ * then the `checked` attribute will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngMultiple
+ * @name ngReadonly
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specs do not require browsers to preserve the special attributes such as multiple.
- * (The presence of them means true and absence means false)
- * This prevents the angular compiler from correctly retrieving the binding expression.
- * To solve this problem, we introduce the `ngMultiple` directive.
+ *
+ * Sets the `readonly` attribute on the element, if the expression inside `ngReadonly` is truthy.
+ * Note that `readonly` applies only to `input` elements with specific types. [See the input docs on
+ * MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-readonly) for more information.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `readonly`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
*
* @example
-
-
- Check me check multiple:
-
- Misko
- Igor
- Vojta
- Di
-
-
-
- it('should toggle multiple', function() {
- expect(element('.doc-example-live #select').prop('multiple')).toBeFalsy();
- input('checked').check();
- expect(element('.doc-example-live #select').prop('multiple')).toBeTruthy();
- });
-
-
- *
- * @element SELECT
- * @param {expression} ngMultiple Angular expression that will be evaluated.
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngReadonly
- * @restrict A
- *
- * @description
- * The HTML specs do not require browsers to preserve the special attributes such as readonly.
- * (The presence of them means true and absence means false)
- * This prevents the angular compiler from correctly retrieving the binding expression.
- * To solve this problem, we introduce the `ngReadonly` directive.
- * @example
-
-
- Check me to make text readonly:
-
-
-
+
+
+ Check me to make text readonly:
+
+
+
it('should toggle readonly attr', function() {
- expect(element('.doc-example-live :text').prop('readonly')).toBeFalsy();
- input('checked').check();
- expect(element('.doc-example-live :text').prop('readonly')).toBeTruthy();
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeFalsy();
+ element(by.model('checked')).click();
+ expect(element(by.css('[type="text"]')).getAttribute('readonly')).toBeTruthy();
});
-
-
+
+
*
* @element INPUT
- * @param {string} expression Angular expression that will be evaluated.
+ * @param {expression} ngReadonly If the {@link guide/expression expression} is truthy,
+ * then special attribute "readonly" will be set on the element
*/
/**
* @ngdoc directive
- * @name ng.directive:ngSelected
+ * @name ngSelected
* @restrict A
+ * @priority 100
*
* @description
- * The HTML specs do not require browsers to preserve the special attributes such as selected.
- * (The presence of them means true and absence means false)
- * This prevents the angular compiler from correctly retrieving the binding expression.
- * To solve this problem, we introduced the `ngSelected` directive.
+ *
+ * Sets the `selected` attribute on the element, if the expression inside `ngSelected` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `selected`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ *
+ * **Note:** `ngSelected` does not interact with the `select` and `ngModel` directives, it only
+ * sets the `selected` attribute on the element. If you are using `ngModel` on the select, you
+ * should not use `ngSelected` on the options, as `ngModel` will set the select value and
+ * selected options.
+ *
+ *
* @example
-
-
- Check me to select:
-
+
+
+ Check me to select:
+
Hello!
Greetings!
-
-
+
+
it('should select Greetings!', function() {
- expect(element('.doc-example-live #greet').prop('selected')).toBeFalsy();
- input('selected').check();
- expect(element('.doc-example-live #greet').prop('selected')).toBeTruthy();
+ expect(element(by.id('greet')).getAttribute('selected')).toBeFalsy();
+ element(by.model('selected')).click();
+ expect(element(by.id('greet')).getAttribute('selected')).toBeTruthy();
});
-
-
+
+
*
* @element OPTION
- * @param {string} expression Angular expression that will be evaluated.
+ * @param {expression} ngSelected If the {@link guide/expression expression} is truthy,
+ * then special attribute "selected" will be set on the element
*/
+/**
+ * @ngdoc directive
+ * @name ngOpen
+ * @restrict A
+ * @priority 100
+ *
+ * @description
+ *
+ * Sets the `open` attribute on the element, if the expression inside `ngOpen` is truthy.
+ *
+ * A special directive is necessary because we cannot use interpolation inside the `open`
+ * attribute. See the {@link guide/interpolation interpolation guide} for more info.
+ *
+ * ## A note about browser compatibility
+ *
+ * Internet Explorer and Edge do not support the `details` element, it is
+ * recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
+ *
+ * @example
+
+
+ Toggle details:
+
+ List
+
+ Apple
+ Orange
+ Durian
+
+
+
+
+ it('should toggle open', function() {
+ expect(element(by.id('details')).getAttribute('open')).toBeFalsy();
+ element(by.model('open')).click();
+ expect(element(by.id('details')).getAttribute('open')).toBeTruthy();
+ });
+
+
+ *
+ * @element DETAILS
+ * @param {expression} ngOpen If the {@link guide/expression expression} is truthy,
+ * then special attribute "open" will be set on the element
+ */
var ngAttributeAliasDirectives = {};
-
// boolean attrs are evaluated
forEach(BOOLEAN_ATTR, function(propName, attrName) {
+ // binding to multiple is not supported
+ if (propName === 'multiple') return;
+
+ function defaultLinkFn(scope, element, attr) {
+ scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
+ attr.$set(attrName, !!value);
+ });
+ }
+
var normalized = directiveNormalize('ng-' + attrName);
+ var linkFn = defaultLinkFn;
+
+ if (propName === 'checked') {
+ linkFn = function(scope, element, attr) {
+ // ensuring ngChecked doesn't interfere with ngModel when both are set on the same input
+ if (attr.ngModel !== attr[normalized]) {
+ defaultLinkFn(scope, element, attr);
+ }
+ };
+ }
+
ngAttributeAliasDirectives[normalized] = function() {
return {
+ restrict: 'A',
priority: 100,
- compile: function() {
- return function(scope, element, attr) {
- scope.$watch(attr[normalized], function ngBooleanAttrWatchAction(value) {
- attr.$set(attrName, !!value);
- });
- };
- }
+ link: linkFn
};
};
});
-
-// ng-src, ng-href are interpolated
-forEach(['src', 'href'], function(attrName) {
- var normalized = directiveNormalize('ng-' + attrName);
- ngAttributeAliasDirectives[normalized] = function() {
+// aliased input attrs are evaluated
+forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
+ ngAttributeAliasDirectives[ngAttr] = function() {
return {
- priority: 99, // it needs to run after the attributes are interpolated
+ priority: 100,
link: function(scope, element, attr) {
- attr.$observe(normalized, function(value) {
- if (!value)
- return;
+ //special case ngPattern when a literal regular expression value
+ //is used as the expression (this way we don't have to watch anything).
+ if (ngAttr === 'ngPattern' && attr.ngPattern.charAt(0) === '/') {
+ var match = attr.ngPattern.match(REGEX_STRING_REGEXP);
+ if (match) {
+ attr.$set('ngPattern', new RegExp(match[1], match[2]));
+ return;
+ }
+ }
- attr.$set(attrName, value);
-
- // on IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
- // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
- // to set the property as well to achieve the desired effect.
- // we use attr[attrName] value since $set can sanitize the url.
- if (msie) element.prop(attrName, attr[attrName]);
+ scope.$watch(attr[ngAttr], function ngAttrAliasWatchAction(value) {
+ attr.$set(ngAttr, value);
});
}
};
};
});
+// ng-src, ng-srcset, ng-href are interpolated
+forEach(['src', 'srcset', 'href'], function(attrName) {
+ var normalized = directiveNormalize('ng-' + attrName);
+ ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) {
+ return {
+ priority: 99, // it needs to run after the attributes are interpolated
+ link: function(scope, element, attr) {
+ var propName = attrName,
+ name = attrName;
+
+ if (attrName === 'href' &&
+ toString.call(element.prop('href')) === '[object SVGAnimatedString]') {
+ name = 'xlinkHref';
+ attr.$attr[name] = 'xlink:href';
+ propName = null;
+ }
+
+ // We need to sanitize the url at least once, in case it is a constant
+ // non-interpolated attribute.
+ attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized]));
+
+ attr.$observe(normalized, function(value) {
+ if (!value) {
+ if (attrName === 'href') {
+ attr.$set(name, null);
+ }
+ return;
+ }
+
+ attr.$set(name, value);
+
+ // Support: IE 9-11 only
+ // On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
+ // then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
+ // to set the property as well to achieve the desired effect.
+ // We use attr[attrName] value since $set might have sanitized the url.
+ if (msie && propName) element.prop(propName, attr[name]);
+ });
+ }
+ };
+ }];
+});
+
+/* global -nullFormCtrl, -PENDING_CLASS, -SUBMITTED_CLASS
+ */
var nullFormCtrl = {
$addControl: noop,
+ $getControls: valueFn([]),
+ $$renameControl: nullFormRenameControl,
$removeControl: noop,
$setValidity: noop,
- $setDirty: noop
-};
+ $setDirty: noop,
+ $setPristine: noop,
+ $setSubmitted: noop,
+ $$setSubmitted: noop
+},
+PENDING_CLASS = 'ng-pending',
+SUBMITTED_CLASS = 'ng-submitted';
+
+function nullFormRenameControl(control, name) {
+ control.$name = name;
+}
/**
- * @ngdoc object
- * @name ng.directive:form.FormController
+ * @ngdoc type
+ * @name form.FormController
*
* @property {boolean} $pristine True if user has not interacted with the form yet.
* @property {boolean} $dirty True if user has already interacted with the form.
* @property {boolean} $valid True if all of the containing forms and controls are valid.
* @property {boolean} $invalid True if at least one containing control or form is invalid.
+ * @property {boolean} $submitted True if user has submitted the form even if its invalid.
*
- * @property {Object} $error Is an object hash, containing references to all invalid controls or
- * forms, where:
+ * @property {Object} $pending An object hash, containing references to controls or forms with
+ * pending validators, where:
*
- * - keys are validation tokens (error names) — such as `required`, `url` or `email`),
- * - values are arrays of controls or forms that are invalid with given error.
+ * - keys are validations tokens (error names).
+ * - values are arrays of controls or forms that have a pending validator for the given error name.
+ *
+ * See {@link form.FormController#$error $error} for a list of built-in validation tokens.
+ *
+ * @property {Object} $error An object hash, containing references to controls or forms with failing
+ * validators, where:
+ *
+ * - keys are validation tokens (error names),
+ * - values are arrays of controls or forms that have a failing validator for the given error name.
+ *
+ * Built-in validation tokens:
+ * - `email`
+ * - `max`
+ * - `maxlength`
+ * - `min`
+ * - `minlength`
+ * - `number`
+ * - `pattern`
+ * - `required`
+ * - `url`
+ * - `date`
+ * - `datetimelocal`
+ * - `time`
+ * - `week`
+ * - `month`
*
* @description
- * `FormController` keeps track of all its controls and nested forms as well as state of them,
+ * `FormController` keeps track of all its controls and nested forms as well as the state of them,
* such as being valid/invalid or dirty/pristine.
*
* Each {@link ng.directive:form form} directive creates an instance
@@ -11056,121 +24584,172 @@ var nullFormCtrl = {
*
*/
//asks for $scope to fool the BC controller module
-FormController.$inject = ['$element', '$attrs', '$scope'];
-function FormController(element, attrs) {
- var form = this,
- parentForm = element.parent().controller('form') || nullFormCtrl,
- invalidCount = 0, // used to easily determine if we are valid
- errors = form.$error = {};
+FormController.$inject = ['$element', '$attrs', '$scope', '$animate', '$interpolate'];
+function FormController($element, $attrs, $scope, $animate, $interpolate) {
+ this.$$controls = [];
// init state
- form.$name = attrs.name || attrs.ngForm;
- form.$dirty = false;
- form.$pristine = true;
- form.$valid = true;
- form.$invalid = false;
+ this.$error = {};
+ this.$$success = {};
+ this.$pending = undefined;
+ this.$name = $interpolate($attrs.name || $attrs.ngForm || '')($scope);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$submitted = false;
+ this.$$parentForm = nullFormCtrl;
- parentForm.$addControl(form);
+ this.$$element = $element;
+ this.$$animate = $animate;
- // Setup initial state of the control
- element.addClass(PRISTINE_CLASS);
- toggleValidCss(true);
-
- // convenience method for easy toggling of classes
- function toggleValidCss(isValid, validationErrorKey) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- element.
- removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
- addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
- }
+ setupValidity(this);
+}
+FormController.prototype = {
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$addControl
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$rollbackViewValue
*
* @description
- * Register a control with the form.
+ * Rollback all form controls pending updates to the `$modelValue`.
*
- * Input elements using ngModelController do this automatically when they are linked.
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is typically needed by the reset button of
+ * a form that uses `ng-model-options` to pend updates.
*/
- form.$addControl = function(control) {
- if (control.$name && !form.hasOwnProperty(control.$name)) {
- form[control.$name] = control;
- }
- };
+ $rollbackViewValue: function() {
+ forEach(this.$$controls, function(control) {
+ control.$rollbackViewValue();
+ });
+ },
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$removeControl
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$commitViewValue
+ *
+ * @description
+ * Commit all form controls pending updates to the `$modelValue`.
+ *
+ * Updates may be pending by a debounced event or because the input is waiting for a some future
+ * event defined in `ng-model-options`. This method is rarely needed as `NgModelController`
+ * usually handles calling this in response to input events.
+ */
+ $commitViewValue: function() {
+ forEach(this.$$controls, function(control) {
+ control.$commitViewValue();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$addControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
+ *
+ * @description
+ * Register a control with the form. Input elements using ngModelController do this automatically
+ * when they are linked.
+ *
+ * Note that the current state of the control will not be reflected on the new parent form. This
+ * is not an issue with normal use, as freshly compiled and linked controls are in a `$pristine`
+ * state.
+ *
+ * However, if the method is used programmatically, for example by adding dynamically created controls,
+ * or controls that have been previously removed without destroying their corresponding DOM element,
+ * it's the developers responsibility to make sure the current state propagates to the parent form.
+ *
+ * For example, if an input control is added that is already `$dirty` and has `$error` properties,
+ * calling `$setDirty()` and `$validate()` afterwards will propagate the state to the parent form.
+ */
+ $addControl: function(control) {
+ // Breaking change - before, inputs whose name was "hasOwnProperty" were quietly ignored
+ // and not added to the scope. Now we throw an error.
+ assertNotHasOwnProperty(control.$name, 'input');
+ this.$$controls.push(control);
+
+ if (control.$name) {
+ this[control.$name] = control;
+ }
+
+ control.$$parentForm = this;
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$getControls
+ * @returns {Array} the controls that are currently part of this form
+ *
+ * @description
+ * This method returns a **shallow copy** of the controls that are currently part of this form.
+ * The controls can be instances of {@link form.FormController `FormController`}
+ * ({@link ngForm "child-forms"}) and of {@link ngModel.NgModelController `NgModelController`}.
+ * If you need access to the controls of child-forms, you have to call `$getControls()`
+ * recursively on them.
+ * This can be used for example to iterate over all controls to validate them.
+ *
+ * The controls can be accessed normally, but adding to, or removing controls from the array has
+ * no effect on the form. Instead, use {@link form.FormController#$addControl `$addControl()`} and
+ * {@link form.FormController#$removeControl `$removeControl()`} for this use-case.
+ * Likewise, adding a control to, or removing a control from the form is not reflected
+ * in the shallow copy. That means you should get a fresh copy from `$getControls()` every time
+ * you need access to the controls.
+ */
+ $getControls: function() {
+ return shallowCopy(this.$$controls);
+ },
+
+ // Private API: rename a form control
+ $$renameControl: function(control, newName) {
+ var oldName = control.$name;
+
+ if (this[oldName] === control) {
+ delete this[oldName];
+ }
+ this[newName] = control;
+ control.$name = newName;
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$removeControl
+ * @param {object} control control object, either a {@link form.FormController} or an
+ * {@link ngModel.NgModelController}
*
* @description
* Deregister a control from the form.
*
* Input elements using ngModelController do this automatically when they are destroyed.
+ *
+ * Note that only the removed control's validation state (`$errors`etc.) will be removed from the
+ * form. `$dirty`, `$submitted` states will not be changed, because the expected behavior can be
+ * different from case to case. For example, removing the only `$dirty` control from a form may or
+ * may not mean that the form is still `$dirty`.
*/
- form.$removeControl = function(control) {
- if (control.$name && form[control.$name] === control) {
- delete form[control.$name];
+ $removeControl: function(control) {
+ if (control.$name && this[control.$name] === control) {
+ delete this[control.$name];
}
- forEach(errors, function(queue, validationToken) {
- form.$setValidity(validationToken, true, control);
- });
- };
+ forEach(this.$pending, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$error, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+ forEach(this.$$success, function(value, name) {
+ // eslint-disable-next-line no-invalid-this
+ this.$setValidity(name, null, control);
+ }, this);
+
+ arrayRemove(this.$$controls, control);
+ control.$$parentForm = nullFormCtrl;
+ },
/**
- * @ngdoc function
- * @name ng.directive:form.FormController#$setValidity
- * @methodOf ng.directive:form.FormController
- *
- * @description
- * Sets the validity of a form control.
- *
- * This method will also propagate to parent forms.
- */
- form.$setValidity = function(validationToken, isValid, control) {
- var queue = errors[validationToken];
-
- if (isValid) {
- if (queue) {
- arrayRemove(queue, control);
- if (!queue.length) {
- invalidCount--;
- if (!invalidCount) {
- toggleValidCss(isValid);
- form.$valid = true;
- form.$invalid = false;
- }
- errors[validationToken] = false;
- toggleValidCss(true, validationToken);
- parentForm.$setValidity(validationToken, true, form);
- }
- }
-
- } else {
- if (!invalidCount) {
- toggleValidCss(isValid);
- }
- if (queue) {
- if (includes(queue, control)) return;
- } else {
- errors[validationToken] = queue = [];
- invalidCount++;
- toggleValidCss(false, validationToken);
- parentForm.$setValidity(validationToken, false, form);
- }
- queue.push(control);
-
- form.$valid = false;
- form.$invalid = true;
- }
- };
-
- /**
- * @ngdoc function
- * @name ng.directive:form.FormController#$setDirty
- * @methodOf ng.directive:form.FormController
+ * @ngdoc method
+ * @name form.FormController#$setDirty
*
* @description
* Sets the form to a dirty state.
@@ -11178,66 +24757,198 @@ function FormController(element, attrs) {
* This method can be called to add the 'ng-dirty' class and set the form to a dirty
* state (ng-dirty class). This method will also propagate to parent forms.
*/
- form.$setDirty = function() {
- element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
- form.$dirty = true;
- form.$pristine = false;
- parentForm.$setDirty();
- };
+ $setDirty: function() {
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$parentForm.$setDirty();
+ },
-}
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setPristine
+ *
+ * @description
+ * Sets the form to its pristine state.
+ *
+ * This method sets the form's `$pristine` state to true, the `$dirty` state to false, removes
+ * the `ng-dirty` class and adds the `ng-pristine` class. Additionally, it sets the `$submitted`
+ * state to false.
+ *
+ * This method will also propagate to all the controls contained in this form.
+ *
+ * Setting a form back to a pristine state is often useful when we want to 'reuse' a form after
+ * saving or resetting it.
+ */
+ $setPristine: function() {
+ this.$$animate.setClass(this.$$element, PRISTINE_CLASS, DIRTY_CLASS + ' ' + SUBMITTED_CLASS);
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$submitted = false;
+ forEach(this.$$controls, function(control) {
+ control.$setPristine();
+ });
+ },
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setUntouched
+ *
+ * @description
+ * Sets the form to its untouched state.
+ *
+ * This method can be called to remove the 'ng-touched' class and set the form controls to their
+ * untouched state (ng-untouched class).
+ *
+ * Setting a form controls back to their untouched state is often useful when setting the form
+ * back to its pristine state.
+ */
+ $setUntouched: function() {
+ forEach(this.$$controls, function(control) {
+ control.$setUntouched();
+ });
+ },
+
+ /**
+ * @ngdoc method
+ * @name form.FormController#$setSubmitted
+ *
+ * @description
+ * Sets the form to its `$submitted` state. This will also set `$submitted` on all child and
+ * parent forms of the form.
+ */
+ $setSubmitted: function() {
+ var rootForm = this;
+ while (rootForm.$$parentForm && (rootForm.$$parentForm !== nullFormCtrl)) {
+ rootForm = rootForm.$$parentForm;
+ }
+ rootForm.$$setSubmitted();
+ },
+
+ $$setSubmitted: function() {
+ this.$$animate.addClass(this.$$element, SUBMITTED_CLASS);
+ this.$submitted = true;
+ forEach(this.$$controls, function(control) {
+ if (control.$$setSubmitted) {
+ control.$$setSubmitted();
+ }
+ });
+ }
+};
+
+/**
+ * @ngdoc method
+ * @name form.FormController#$setValidity
+ *
+ * @description
+ * Change the validity state of the form, and notify the parent form (if any).
+ *
+ * Application developers will rarely need to call this method directly. It is used internally, by
+ * {@link ngModel.NgModelController#$setValidity NgModelController.$setValidity()}, to propagate a
+ * control's validity state to the parent `FormController`.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be
+ * assigned to either `$error[validationErrorKey]` or `$pending[validationErrorKey]` (for
+ * unfulfilled `$asyncValidators`), so that it is available for data-binding. The
+ * `validationErrorKey` should be in camelCase and will get converted into dash-case for
+ * class name. Example: `myError` will result in `ng-valid-my-error` and
+ * `ng-invalid-my-error` classes and can be bound to as `{{ someForm.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending
+ * (undefined), or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and when
+ * `$asyncValidators` do not run because any of the `$validators` failed.
+ * @param {NgModelController | FormController} controller - The controller whose validity state is
+ * triggering the change.
+ */
+addSetValidityMethod({
+ clazz: FormController,
+ set: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ object[property] = [controller];
+ } else {
+ var index = list.indexOf(controller);
+ if (index === -1) {
+ list.push(controller);
+ }
+ }
+ },
+ unset: function(object, property, controller) {
+ var list = object[property];
+ if (!list) {
+ return;
+ }
+ arrayRemove(list, controller);
+ if (list.length === 0) {
+ delete object[property];
+ }
+ }
+});
/**
* @ngdoc directive
- * @name ng.directive:ngForm
+ * @name ngForm
* @restrict EAC
*
* @description
- * Nestable alias of {@link ng.directive:form `form`} directive. HTML
- * does not allow nesting of form elements. It is useful to nest forms, for example if the validity of a
- * sub-group of controls needs to be determined.
+ * Helper directive that makes it possible to create control groups inside a
+ * {@link ng.directive:form `form`} directive.
+ * These "child forms" can be used, for example, to determine the validity of a sub-group of
+ * controls.
*
- * @param {string=} name|ngForm Name of the form. If specified, the form controller will be published into
- * related scope, under this name.
+ *
+ * **Note**: `ngForm` cannot be used as a replacement for `
+ *
+ * @param {string=} ngForm|name Name of the form. If specified, the form controller will
+ * be published into the related scope, under this name.
*
*/
/**
* @ngdoc directive
- * @name ng.directive:form
+ * @name form
* @restrict E
*
* @description
* Directive that instantiates
- * {@link ng.directive:form.FormController FormController}.
+ * {@link form.FormController FormController}.
*
- * If `name` attribute is specified, the form controller is published onto the current scope under
+ * If the `name` attribute is specified, the form controller is published onto the current scope under
* this name.
*
- * # Alias: {@link ng.directive:ngForm `ngForm`}
+ * ## Alias: {@link ng.directive:ngForm `ngForm`}
*
- * In angular forms can be nested. This means that the outer form is valid when all of the child
- * forms are valid as well. However browsers do not allow nesting of `
+
+
+ var value = element(by.binding('example.value | date: "yyyy-MM-dd"'));
+ var valid = element(by.binding('myForm.input.$valid'));
- it('should be invalid if empty', function() {
- input('text').enter('');
- expect(binding('text')).toEqual('');
- expect(binding('myForm.input.$valid')).toEqual('false');
- });
+ // currently protractor/webdriver does not support
+ // sending keys to all known HTML5 input controls
+ // for various browsers (see https://github.com/angular/protractor/issues/562).
+ function setInput(val) {
+ // set the value of the element and force validation.
+ var scr = "var ipt = document.getElementById('exampleInput'); " +
+ "ipt.value = '" + val + "';" +
+ "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
+ browser.executeScript(scr);
+ }
- it('should be invalid if multi word', function() {
- input('text').enter('hello world');
- expect(binding('myForm.input.$valid')).toEqual('false');
- });
-
-
- */
- 'text': textInputType,
+ it('should initialize to model', function() {
+ expect(value.getText()).toContain('2013-10-22');
+ expect(valid.getText()).toContain('myForm.input.$valid = true');
+ });
+ it('should be invalid if empty', function() {
+ setInput('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+ it('should be invalid if over max', function() {
+ setInput('2015-01-01');
+ expect(value.getText()).toContain('');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+
+ */
+ 'date': createDateInputType('date', DATE_REGEXP,
+ createDateParser(DATE_REGEXP, ['yyyy', 'MM', 'dd']),
+ 'yyyy-MM-dd'),
+
+ /**
+ * @ngdoc input
+ * @name input[datetime-local]
+ *
+ * @description
+ * Input with datetime validation and transformation. In browsers that do not yet support
+ * the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * local datetime format (yyyy-MM-ddTHH:mm:ss), for example: `2010-12-28T14:57:00`.
+ *
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
+ * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
+ *
+ * The timezone to be used to read/write the `Date` instance in the model can be defined using
+ * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
+ *
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
+ * inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
+ * Note that `min` will also add native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
+ * inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
+ * Note that `max` will also add native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+
+
+
+
+ Pick a date between in 2013:
+
+
+
+ Required!
+
+ Not a valid date!
+
+ value = {{example.value | date: "yyyy-MM-ddTHH:mm:ss"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+
+ var value = element(by.binding('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
+ var valid = element(by.binding('myForm.input.$valid'));
+
+ // currently protractor/webdriver does not support
+ // sending keys to all known HTML5 input controls
+ // for various browsers (https://github.com/angular/protractor/issues/562).
+ function setInput(val) {
+ // set the value of the element and force validation.
+ var scr = "var ipt = document.getElementById('exampleInput'); " +
+ "ipt.value = '" + val + "';" +
+ "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
+ browser.executeScript(scr);
+ }
+
+ it('should initialize to model', function() {
+ expect(value.getText()).toContain('2010-12-28T14:57:00');
+ expect(valid.getText()).toContain('myForm.input.$valid = true');
+ });
+
+ it('should be invalid if empty', function() {
+ setInput('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+ it('should be invalid if over max', function() {
+ setInput('2015-01-01T23:59:00');
+ expect(value.getText()).toContain('');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+
+ */
+ 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP,
+ createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']),
+ 'yyyy-MM-ddTHH:mm:ss.sss'),
/**
- * @ngdoc inputType
- * @name ng.directive:input.number
+ * @ngdoc input
+ * @name input[time]
+ *
+ * @description
+ * Input with time validation and transformation. In browsers that do not yet support
+ * the HTML5 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
+ * Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
+ *
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
+ * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
+ *
+ * The timezone to be used to read/write the `Date` instance in the model can be defined using
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
+ * this is the timezone of the browser.
+ *
+ * The format of the displayed time can be adjusted with the
+ * {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
+ * and `timeStripZeroSeconds`.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the
+ * `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the
+ * `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+
+
+
+
+ Pick a time between 8am and 5pm:
+
+
+
+ Required!
+
+ Not a valid date!
+
+ value = {{example.value | date: "HH:mm:ss"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+
+ var value = element(by.binding('example.value | date: "HH:mm:ss"'));
+ var valid = element(by.binding('myForm.input.$valid'));
+
+ // currently protractor/webdriver does not support
+ // sending keys to all known HTML5 input controls
+ // for various browsers (https://github.com/angular/protractor/issues/562).
+ function setInput(val) {
+ // set the value of the element and force validation.
+ var scr = "var ipt = document.getElementById('exampleInput'); " +
+ "ipt.value = '" + val + "';" +
+ "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
+ browser.executeScript(scr);
+ }
+
+ it('should initialize to model', function() {
+ expect(value.getText()).toContain('14:57:00');
+ expect(valid.getText()).toContain('myForm.input.$valid = true');
+ });
+
+ it('should be invalid if empty', function() {
+ setInput('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+ it('should be invalid if over max', function() {
+ setInput('23:59:00');
+ expect(value.getText()).toContain('');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+
+ */
+ 'time': createDateInputType('time', TIME_REGEXP,
+ createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']),
+ 'HH:mm:ss.sss'),
+
+ /**
+ * @ngdoc input
+ * @name input[week]
+ *
+ * @description
+ * Input with week-of-the-year validation and transformation to Date. In browsers that do not yet support
+ * the HTML5 week input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * week format (yyyy-W##), for example: `2013-W02`.
+ *
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
+ * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
+ *
+ * The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
+ * due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
+ * year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
+ *
+ * The timezone to be used to read/write the `Date` instance in the model can be defined using
+ * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+
+
+
+
+ Pick a date between in 2013:
+
+
+
+
+ Required!
+
+ Not a valid date!
+
+ value = {{example.value | date: "yyyy-Www"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+
+ var value = element(by.binding('example.value | date: "yyyy-Www"'));
+ var valid = element(by.binding('myForm.input.$valid'));
+
+ // currently protractor/webdriver does not support
+ // sending keys to all known HTML5 input controls
+ // for various browsers (https://github.com/angular/protractor/issues/562).
+ function setInput(val) {
+ // set the value of the element and force validation.
+ var scr = "var ipt = document.getElementById('exampleInput'); " +
+ "ipt.value = '" + val + "';" +
+ "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
+ browser.executeScript(scr);
+ }
+
+ it('should initialize to model', function() {
+ expect(value.getText()).toContain('2013-W01');
+ expect(valid.getText()).toContain('myForm.input.$valid = true');
+ });
+
+ it('should be invalid if empty', function() {
+ setInput('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+ it('should be invalid if over max', function() {
+ setInput('2015-W01');
+ expect(value.getText()).toContain('');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+
+ */
+ 'week': createDateInputType('week', WEEK_REGEXP, weekParser, 'yyyy-Www'),
+
+ /**
+ * @ngdoc input
+ * @name input[month]
+ *
+ * @description
+ * Input with month validation and transformation. In browsers that do not yet support
+ * the HTML5 month input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
+ * month format (yyyy-MM), for example: `2009-01`.
+ *
+ * The model must always be a Date object, otherwise AngularJS will throw an error.
+ * Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
+ * If the model is not set to the first of the month, the next view to model update will set it
+ * to the first of the month.
+ *
+ * The timezone to be used to read/write the `Date` instance in the model can be defined using
+ * {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
+ * attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add
+ * native HTML5 constraint validation.
+ * @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
+ * attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add
+ * native HTML5 constraint validation.
+ * @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
+ * the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
+ * @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
+ * the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
+
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
+ * interaction with the input element.
+ *
+ * @example
+
+
+
+
+ Pick a month in 2013:
+
+
+
+ Required!
+
+ Not a valid month!
+
+ value = {{example.value | date: "yyyy-MM"}}
+ myForm.input.$valid = {{myForm.input.$valid}}
+ myForm.input.$error = {{myForm.input.$error}}
+ myForm.$valid = {{myForm.$valid}}
+ myForm.$error.required = {{!!myForm.$error.required}}
+
+
+
+ var value = element(by.binding('example.value | date: "yyyy-MM"'));
+ var valid = element(by.binding('myForm.input.$valid'));
+
+ // currently protractor/webdriver does not support
+ // sending keys to all known HTML5 input controls
+ // for various browsers (https://github.com/angular/protractor/issues/562).
+ function setInput(val) {
+ // set the value of the element and force validation.
+ var scr = "var ipt = document.getElementById('exampleInput'); " +
+ "ipt.value = '" + val + "';" +
+ "angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
+ browser.executeScript(scr);
+ }
+
+ it('should initialize to model', function() {
+ expect(value.getText()).toContain('2013-10');
+ expect(valid.getText()).toContain('myForm.input.$valid = true');
+ });
+
+ it('should be invalid if empty', function() {
+ setInput('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+ it('should be invalid if over max', function() {
+ setInput('2015-01');
+ expect(value.getText()).toContain('');
+ expect(valid.getText()).toContain('myForm.input.$valid = false');
+ });
+
+
+ */
+ 'month': createDateInputType('month', MONTH_REGEXP,
+ createDateParser(MONTH_REGEXP, ['yyyy', 'MM']),
+ 'yyyy-MM'),
+
+ /**
+ * @ngdoc input
+ * @name input[number]
*
* @description
* Text input with number validation and transformation. Sets the `number` validation
* error if not a valid number.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * The model must always be of type `number` otherwise AngularJS will throw an error.
+ * Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt}
+ * error docs for more information and an example of how to convert your model if necessary.
+ *
+ *
+ *
+ *
+ * @knownIssue
+ *
+ * ### HTML5 constraint validation and `allowInvalid`
+ *
+ * In browsers that follow the
+ * [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
+ * `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}.
+ * If a non-number is entered in the input, the browser will report the value as an empty string,
+ * which means the view / model values in `ngModel` and subsequently the scope value
+ * will also be an empty string.
+ *
+ * @knownIssue
+ *
+ * ### Large numbers and `step` validation
+ *
+ * The `step` validation will not work correctly for very large numbers (e.g. 9999999999) due to
+ * Javascript's arithmetic limitations. If you need to handle large numbers, purpose-built
+ * libraries (e.g. https://github.com/MikeMcl/big.js/), can be included into AngularJS by
+ * {@link guide/forms#modifying-built-in-validators overwriting the validators}
+ * for `number` and / or `step`, or by {@link guide/forms#custom-validation applying custom validators}
+ * to an `input[text]` element. The source for `input[number]` type can be used as a starting
+ * point for both implementations.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
+ * Can be interpolated.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
+ * Can be interpolated.
+ * @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
+ * but does not trigger HTML5 native validation. Takes an expression.
+ * @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
+ * Can be interpolated.
+ * @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint,
+ * but does not trigger HTML5 native validation. Takes an expression.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
@@ -11447,67 +25966,96 @@ var inputType = {
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
* minlength.
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
- * maxlength.
- * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
+ * any length.
+ * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
+ * that contains the regular expression body that will be converted to a regular expression
+ * as in the ngPattern directive.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
+ * If the expression evaluates to a RegExp object, then this is used directly.
+ * If the expression evaluates to a string, then it will be converted to a RegExp
+ * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
+ * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
-
-
+
+
-
- Number:
-
- Required!
-
- Not valid number!
- value = {{value}}
+
+ Number:
+
+
+
+
+ Required!
+
+ Not valid number!
+
+ value = {{example.value}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
-
-
+
+
+ var value = element(by.binding('example.value'));
+ var valid = element(by.binding('myForm.input.$valid'));
+ var input = element(by.model('example.value'));
+
it('should initialize to model', function() {
- expect(binding('value')).toEqual('12');
- expect(binding('myForm.input.$valid')).toEqual('true');
+ expect(value.getText()).toContain('12');
+ expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
- input('value').enter('');
- expect(binding('value')).toEqual('');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('false');
});
it('should be invalid if over max', function() {
- input('value').enter('123');
- expect(binding('value')).toEqual('');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('123');
+ expect(value.getText()).toEqual('value =');
+ expect(valid.getText()).toContain('false');
});
-
-
+
+
*/
'number': numberInputType,
/**
- * @ngdoc inputType
- * @name ng.directive:input.url
+ * @ngdoc input
+ * @name input[url]
*
* @description
* Text input with URL validation. Sets the `url` validation error key if the content is not a
* valid URL.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * **Note:** `input[url]` uses a regex to validate urls that is derived from the regex
+ * used in Chromium. If you need stricter validation, you can use `ng-pattern` or modify
+ * the built-in validators (see the {@link guide/forms Forms guide})
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -11516,66 +26064,99 @@ var inputType = {
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
* minlength.
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
- * maxlength.
- * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
+ * any length.
+ * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
+ * that contains the regular expression body that will be converted to a regular expression
+ * as in the ngPattern directive.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
+ * If the expression evaluates to a RegExp object, then this is used directly.
+ * If the expression evaluates to a string, then it will be converted to a RegExp
+ * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
+ * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
-
-
+
+
-
- URL:
-
- Required!
-
- Not valid url!
- text = {{text}}
+
+ URL:
+
+
+
+
+ Required!
+
+ Not valid url!
+
+ text = {{url.text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
myForm.$error.url = {{!!myForm.$error.url}}
-
-
+
+
+ var text = element(by.binding('url.text'));
+ var valid = element(by.binding('myForm.input.$valid'));
+ var input = element(by.model('url.text'));
+
it('should initialize to model', function() {
- expect(binding('text')).toEqual('http://google.com');
- expect(binding('myForm.input.$valid')).toEqual('true');
+ expect(text.getText()).toContain('http://google.com');
+ expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
- input('text').enter('');
- expect(binding('text')).toEqual('');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('');
+
+ expect(text.getText()).toEqual('text =');
+ expect(valid.getText()).toContain('false');
});
it('should be invalid if not url', function() {
- input('text').enter('xxx');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('box');
+
+ expect(valid.getText()).toContain('false');
});
-
-
+
+
*/
'url': urlInputType,
/**
- * @ngdoc inputType
- * @name ng.directive:input.email
+ * @ngdoc input
+ * @name input[email]
*
* @description
* Text input with email validation. Sets the `email` validation error key if not a valid email
* address.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
+ * used in Chromium, which may not fulfill your app's requirements.
+ * If you need stricter (e.g. requiring a top-level domain), or more relaxed validation
+ * (e.g. allowing IPv6 address literals) you can use `ng-pattern` or
+ * modify the built-in validators (see the {@link guide/forms Forms guide}).
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -11584,415 +26165,1105 @@ var inputType = {
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
* minlength.
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
- * maxlength.
- * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of
+ * any length.
+ * @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
+ * that contains the regular expression body that will be converted to a regular expression
+ * as in the ngPattern directive.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
+ * If the expression evaluates to a RegExp object, then this is used directly.
+ * If the expression evaluates to a string, then it will be converted to a RegExp
+ * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
+ * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
-
-
+
+
-
- Email:
-
- Required!
-
- Not valid email!
- text = {{text}}
+
+ Email:
+
+
+
+
+ Required!
+
+ Not valid email!
+
+ text = {{email.text}}
myForm.input.$valid = {{myForm.input.$valid}}
myForm.input.$error = {{myForm.input.$error}}
myForm.$valid = {{myForm.$valid}}
myForm.$error.required = {{!!myForm.$error.required}}
myForm.$error.email = {{!!myForm.$error.email}}
-
-
+
+
+ var text = element(by.binding('email.text'));
+ var valid = element(by.binding('myForm.input.$valid'));
+ var input = element(by.model('email.text'));
+
it('should initialize to model', function() {
- expect(binding('text')).toEqual('me@example.com');
- expect(binding('myForm.input.$valid')).toEqual('true');
+ expect(text.getText()).toContain('me@example.com');
+ expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
- input('text').enter('');
- expect(binding('text')).toEqual('');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('');
+ expect(text.getText()).toEqual('text =');
+ expect(valid.getText()).toContain('false');
});
it('should be invalid if not email', function() {
- input('text').enter('xxx');
- expect(binding('myForm.input.$valid')).toEqual('false');
+ input.clear();
+ input.sendKeys('xxx');
+
+ expect(valid.getText()).toContain('false');
});
-
-
+
+
*/
'email': emailInputType,
/**
- * @ngdoc inputType
- * @name ng.directive:input.radio
+ * @ngdoc input
+ * @name input[radio]
*
* @description
* HTML radio button.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
- * @param {string} value The value to which the expression should be set when selected.
+ * **Note:**
+ * All inputs controlled by {@link ngModel ngModel} (including those of type `radio`) will use the
+ * value of their `name` attribute to determine the property under which their
+ * {@link ngModel.NgModelController NgModelController} will be published on the parent
+ * {@link form.FormController FormController}. Thus, if you use the same `name` for multiple
+ * inputs of a form (e.g. a group of radio inputs), only _one_ `NgModelController` will be
+ * published on the parent `FormController` under that name. The rest of the controllers will
+ * continue to work as expected, but you won't be able to access them as properties on the parent
+ * `FormController`.
+ *
+ *
+ *
+ * In plain HTML forms, the `name` attribute is used to identify groups of radio inputs, so
+ * that the browser can manage their state (checked/unchecked) based on the state of other
+ * inputs in the same group.
+ *
+ *
+ * In AngularJS forms, this is not necessary. The input's state will be updated based on the
+ * value of the underlying model data.
+ *
+ *
+ *
+ *
+ * If you omit the `name` attribute on a radio input, `ngModel` will automatically assign it a
+ * unique name.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string} value The value to which the `ngModel` expression should be set when selected.
+ * Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
+ * too. Use `ngValue` if you need complex models (`number`, `object`, ...).
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio
+ * is selected. Should be used instead of the `value` attribute if you need
+ * a non-string `ngModel` (`boolean`, `array`, ...).
*
* @example
-
-
+
+
-
- Red
- Green
- Blue
- color = {{color}}
+
+
+
+ Red
+
+
+
+ Green
+
+
+
+ Blue
+
+ color = {{color.name | json}}
-
-
+ Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
+
+
it('should change state', function() {
- expect(binding('color')).toEqual('blue');
+ var inputs = element.all(by.model('color.name'));
+ var color = element(by.binding('color.name'));
- input('color').select('red');
- expect(binding('color')).toEqual('red');
+ expect(color.getText()).toContain('blue');
+
+ inputs.get(0).click();
+ expect(color.getText()).toContain('red');
+
+ inputs.get(1).click();
+ expect(color.getText()).toContain('green');
});
-
-
+
+
*/
'radio': radioInputType,
+ /**
+ * @ngdoc input
+ * @name input[range]
+ *
+ * @description
+ * Native range input with validation and transformation.
+ *
+ * The model for the range input must always be a `Number`.
+ *
+ * IE9 and other browsers that do not support the `range` type fall back
+ * to a text input without any default values for `min`, `max` and `step`. Model binding,
+ * validation and number parsing are nevertheless supported.
+ *
+ * Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
+ * in a way that never allows the input to hold an invalid value. That means:
+ * - any non-numerical value is set to `(max + min) / 2`.
+ * - any numerical value that is less than the current min val, or greater than the current max val
+ * is set to the min / max val respectively.
+ * - additionally, the current `step` is respected, so the nearest value that satisfies a step
+ * is used.
+ *
+ * See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
+ * for more info.
+ *
+ * This has the following consequences for AngularJS:
+ *
+ * Since the element value should always reflect the current model value, a range input
+ * will set the bound ngModel expression to the value that the browser has set for the
+ * input element. For example, in the following input ` `,
+ * if the application sets `model.value = null`, the browser will set the input to `'50'`.
+ * AngularJS will then set the model to `50`, to prevent input and model value being out of sync.
+ *
+ * That means the model for range will immediately be set to `50` after `ngModel` has been
+ * initialized. It also means a range input can never have the required error.
+ *
+ * This does not only affect changes to the model value, but also to the values of the `min`,
+ * `max`, and `step` attributes. When these change in a way that will cause the browser to modify
+ * the input value, AngularJS will also update the model value.
+ *
+ * Automatic value adjustment also means that a range input element can never have the `required`,
+ * `min`, or `max` errors.
+ *
+ * However, `step` is currently only fully implemented by Firefox. Other browsers have problems
+ * when the step value changes dynamically - they do not adjust the element value correctly, but
+ * instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step`
+ * error on the input, and set the model to `undefined`.
+ *
+ * Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
+ * not set the `min` and `max` attributes, which means that the browser won't automatically adjust
+ * the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} min Sets the `min` validation to ensure that the value entered is greater
+ * than `min`. Can be interpolated.
+ * @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
+ * Can be interpolated.
+ * @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
+ * Can be interpolated.
+ * @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due
+ * to user interaction with the input element.
+ * @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
+ * element. **Note** : `ngChecked` should not be used alongside `ngModel`.
+ * Checkout {@link ng.directive:ngChecked ngChecked} for usage.
+ *
+ * @example
+
+
+
+
+
+ Model as range:
+
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}}
+
+
+
+
+ * ## Range Input with ngMin & ngMax attributes
+
+ * @example
+
+
+
+
+ Model as range:
+
+ Model as number:
+ Min:
+ Max:
+ value = {{value}}
+ myForm.range.$valid = {{myForm.range.$valid}}
+ myForm.range.$error = {{myForm.range.$error}}
+
+
+
+
+ */
+ 'range': rangeInputType,
/**
- * @ngdoc inputType
- * @name ng.directive:input.checkbox
+ * @ngdoc input
+ * @name input[checkbox]
*
* @description
* HTML checkbox.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} ngTrueValue The value to which the expression should be set when selected.
- * @param {string=} ngFalseValue The value to which the expression should be set when not selected.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * @param {expression=} ngTrueValue The value to which the expression should be set when selected.
+ * @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
-
-
+
+
-
- Value1:
- Value2:
- value1 = {{value1}}
- value2 = {{value2}}
+
+ Value1:
+
+
+ Value2:
+
+
+ value1 = {{checkboxModel.value1}}
+ value2 = {{checkboxModel.value2}}
-
-
+
+
it('should change state', function() {
- expect(binding('value1')).toEqual('true');
- expect(binding('value2')).toEqual('YES');
+ var value1 = element(by.binding('checkboxModel.value1'));
+ var value2 = element(by.binding('checkboxModel.value2'));
- input('value1').check();
- input('value2').check();
- expect(binding('value1')).toEqual('false');
- expect(binding('value2')).toEqual('NO');
+ expect(value1.getText()).toContain('true');
+ expect(value2.getText()).toContain('YES');
+
+ element(by.model('checkboxModel.value1')).click();
+ element(by.model('checkboxModel.value2')).click();
+
+ expect(value1.getText()).toContain('false');
+ expect(value2.getText()).toContain('NO');
});
-
-
+
+
*/
'checkbox': checkboxInputType,
'hidden': noop,
'button': noop,
'submit': noop,
- 'reset': noop
+ 'reset': noop,
+ 'file': noop
};
-
-function isEmpty(value) {
- return isUndefined(value) || value === '' || value === null || value !== value;
+function stringBasedInputType(ctrl) {
+ ctrl.$formatters.push(function(value) {
+ return ctrl.$isEmpty(value) ? value : value.toString();
+ });
}
-
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ stringBasedInputType(ctrl);
+}
- var listener = function() {
- var value = trim(element.val());
+function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ var type = lowercase(element[0].type);
- if (ctrl.$viewValue !== value) {
- scope.$apply(function() {
- ctrl.$setViewValue(value);
- });
+ // In composition mode, users are still inputting intermediate text buffer,
+ // hold the listener until composition is done.
+ // More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
+ if (!$sniffer.android) {
+ var composing = false;
+
+ element.on('compositionstart', function() {
+ composing = true;
+ });
+
+ // Support: IE9+
+ element.on('compositionupdate', function(ev) {
+ // End composition when ev.data is empty string on 'compositionupdate' event.
+ // When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate'
+ // instead of 'compositionend'.
+ if (isUndefined(ev.data) || ev.data === '') {
+ composing = false;
+ }
+ });
+
+ element.on('compositionend', function() {
+ composing = false;
+ listener();
+ });
+ }
+
+ var timeout;
+
+ var listener = function(ev) {
+ if (timeout) {
+ $browser.defer.cancel(timeout);
+ timeout = null;
+ }
+ if (composing) return;
+ var value = element.val(),
+ event = ev && ev.type;
+
+ // By default we will trim the value
+ // If the attribute ng-trim exists we will avoid trimming
+ // If input type is 'password', the value is never trimmed
+ if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) {
+ value = trim(value);
+ }
+
+ // If a control is suffering from bad input (due to native validators), browsers discard its
+ // value, so it may be necessary to revalidate (by calling $setViewValue again) even if the
+ // control's value is the same empty value twice in a row.
+ if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) {
+ ctrl.$setViewValue(value, event);
}
};
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
// input event on backspace, delete or cut
if ($sniffer.hasEvent('input')) {
- element.bind('input', listener);
+ element.on('input', listener);
} else {
- var timeout;
-
- var deferListener = function() {
+ var deferListener = function(ev, input, origValue) {
if (!timeout) {
timeout = $browser.defer(function() {
- listener();
timeout = null;
+ if (!input || input.value !== origValue) {
+ listener(ev);
+ }
});
}
};
- element.bind('keydown', function(event) {
+ element.on('keydown', /** @this */ function(event) {
var key = event.keyCode;
// ignore
// command modifiers arrows
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
- deferListener();
+ deferListener(event, this, this.value);
});
- // if user paste into input using mouse, we need "change" event to catch it
- element.bind('change', listener);
-
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
+ // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it
if ($sniffer.hasEvent('paste')) {
- element.bind('paste cut', deferListener);
+ element.on('paste cut drop', deferListener);
}
}
+ // if user paste into input using mouse on older browser
+ // or form autocomplete on newer browser, we need "change" event to catch it
+ element.on('change', listener);
+
+ // Some native input types (date-family) have the ability to change validity without
+ // firing any input/change events.
+ // For these event types, when native validators are present and the browser supports the type,
+ // check for validity changes on various DOM events.
+ if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
+ element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) {
+ if (!timeout) {
+ var validity = this[VALIDITY_STATE_PROPERTY];
+ var origBadInput = validity.badInput;
+ var origTypeMismatch = validity.typeMismatch;
+ timeout = $browser.defer(function() {
+ timeout = null;
+ if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
+ listener(ev);
+ }
+ });
+ }
+ });
+ }
ctrl.$render = function() {
- element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
- };
-
- // pattern validator
- var pattern = attr.ngPattern,
- patternValidator;
-
- var validate = function(regexp, value) {
- if (isEmpty(value) || regexp.test(value)) {
- ctrl.$setValidity('pattern', true);
- return value;
- } else {
- ctrl.$setValidity('pattern', false);
- return undefined;
+ // Workaround for Firefox validation #12102.
+ var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;
+ if (element.val() !== value) {
+ element.val(value);
}
};
+}
- if (pattern) {
- if (pattern.match(/^\/(.*)\/$/)) {
- pattern = new RegExp(pattern.substr(1, pattern.length - 2));
- patternValidator = function(value) {
- return validate(pattern, value)
- };
- } else {
- patternValidator = function(value) {
- var patternObj = scope.$eval(pattern);
+function weekParser(isoWeek, existingDate) {
+ if (isDate(isoWeek)) {
+ return isoWeek;
+ }
- if (!patternObj || !patternObj.test) {
- throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
+ if (isString(isoWeek)) {
+ WEEK_REGEXP.lastIndex = 0;
+ var parts = WEEK_REGEXP.exec(isoWeek);
+ if (parts) {
+ var year = +parts[1],
+ week = +parts[2],
+ hours = 0,
+ minutes = 0,
+ seconds = 0,
+ milliseconds = 0,
+ firstThurs = getFirstThursdayOfYear(year),
+ addDays = (week - 1) * 7;
+
+ if (existingDate) {
+ hours = existingDate.getHours();
+ minutes = existingDate.getMinutes();
+ seconds = existingDate.getSeconds();
+ milliseconds = existingDate.getMilliseconds();
+ }
+
+ return new Date(year, 0, firstThurs.getDate() + addDays, hours, minutes, seconds, milliseconds);
+ }
+ }
+
+ return NaN;
+}
+
+function createDateParser(regexp, mapping) {
+ return function(iso, previousDate) {
+ var parts, map;
+
+ if (isDate(iso)) {
+ return iso;
+ }
+
+ if (isString(iso)) {
+ // When a date is JSON'ified to wraps itself inside of an extra
+ // set of double quotes. This makes the date parsing code unable
+ // to match the date string and parse it as a date.
+ if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') {
+ iso = iso.substring(1, iso.length - 1);
+ }
+ if (ISO_DATE_REGEXP.test(iso)) {
+ return new Date(iso);
+ }
+ regexp.lastIndex = 0;
+ parts = regexp.exec(iso);
+
+ if (parts) {
+ parts.shift();
+ if (previousDate) {
+ map = {
+ yyyy: previousDate.getFullYear(),
+ MM: previousDate.getMonth() + 1,
+ dd: previousDate.getDate(),
+ HH: previousDate.getHours(),
+ mm: previousDate.getMinutes(),
+ ss: previousDate.getSeconds(),
+ sss: previousDate.getMilliseconds() / 1000
+ };
+ } else {
+ map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
}
- return validate(patternObj, value);
- };
+
+ forEach(parts, function(part, index) {
+ if (index < mapping.length) {
+ map[mapping[index]] = +part;
+ }
+ });
+
+ var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
+ if (map.yyyy < 100) {
+ // In the constructor, 2-digit years map to 1900-1999.
+ // Use `setFullYear()` to set the correct year.
+ date.setFullYear(map.yyyy);
+ }
+
+ return date;
+ }
}
- ctrl.$formatters.push(patternValidator);
- ctrl.$parsers.push(patternValidator);
- }
+ return NaN;
+ };
+}
- // min length validator
- if (attr.ngMinlength) {
- var minlength = int(attr.ngMinlength);
- var minLengthValidator = function(value) {
- if (!isEmpty(value) && value.length < minlength) {
- ctrl.$setValidity('minlength', false);
- return undefined;
- } else {
- ctrl.$setValidity('minlength', true);
- return value;
+function createDateInputType(type, regexp, parseDate, format) {
+ return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, type);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var isTimeType = type === 'time' || type === 'datetimelocal';
+ var previousDate;
+ var previousTimezone;
+
+ ctrl.$parsers.push(function(value) {
+ if (ctrl.$isEmpty(value)) return null;
+
+ if (regexp.test(value)) {
+ // Note: We cannot read ctrl.$modelValue, as there might be a different
+ // parser/formatter in the processing chain so that the model
+ // contains some different data format!
+ return parseDateAndConvertTimeZoneToLocal(value, previousDate);
}
- };
+ ctrl.$$parserName = type;
+ return undefined;
+ });
- ctrl.$parsers.push(minLengthValidator);
- ctrl.$formatters.push(minLengthValidator);
- }
-
- // max length validator
- if (attr.ngMaxlength) {
- var maxlength = int(attr.ngMaxlength);
- var maxLengthValidator = function(value) {
- if (!isEmpty(value) && value.length > maxlength) {
- ctrl.$setValidity('maxlength', false);
- return undefined;
- } else {
- ctrl.$setValidity('maxlength', true);
- return value;
+ ctrl.$formatters.push(function(value) {
+ if (value && !isDate(value)) {
+ throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
}
- };
+ if (isValidDate(value)) {
+ previousDate = value;
+ var timezone = ctrl.$options.getOption('timezone');
- ctrl.$parsers.push(maxLengthValidator);
- ctrl.$formatters.push(maxLengthValidator);
+ if (timezone) {
+ previousTimezone = timezone;
+ previousDate = convertTimezoneToLocal(previousDate, timezone, true);
+ }
+
+ return formatter(value, timezone);
+ } else {
+ previousDate = null;
+ previousTimezone = null;
+ return '';
+ }
+ });
+
+ if (isDefined(attr.min) || attr.ngMin) {
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ var parsedMinVal = parseObservedDateValue(minVal);
+
+ ctrl.$validators.min = function(value) {
+ return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
+ };
+ attr.$observe('min', function(val) {
+ if (val !== minVal) {
+ parsedMinVal = parseObservedDateValue(val);
+ minVal = val;
+ ctrl.$validate();
+ }
+ });
+ }
+
+ if (isDefined(attr.max) || attr.ngMax) {
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseObservedDateValue(maxVal);
+
+ ctrl.$validators.max = function(value) {
+ return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
+ };
+ attr.$observe('max', function(val) {
+ if (val !== maxVal) {
+ parsedMaxVal = parseObservedDateValue(val);
+ maxVal = val;
+ ctrl.$validate();
+ }
+ });
+ }
+
+ function isValidDate(value) {
+ // Invalid Date: getTime() returns NaN
+ return value && !(value.getTime && value.getTime() !== value.getTime());
+ }
+
+ function parseObservedDateValue(val) {
+ return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val;
+ }
+
+ function parseDateAndConvertTimeZoneToLocal(value, previousDate) {
+ var timezone = ctrl.$options.getOption('timezone');
+
+ if (previousTimezone && previousTimezone !== timezone) {
+ // If the timezone has changed, adjust the previousDate to the default timezone
+ // so that the new date is converted with the correct timezone offset
+ previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone));
+ }
+
+ var parsedDate = parseDate(value, previousDate);
+
+ if (!isNaN(parsedDate) && timezone) {
+ parsedDate = convertTimezoneToLocal(parsedDate, timezone);
+ }
+ return parsedDate;
+ }
+
+ function formatter(value, timezone) {
+ var targetFormat = format;
+
+ if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
+ targetFormat = format
+ .replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
+ .replace(/:$/, '');
+ }
+
+ var formatted = $filter('date')(value, targetFormat, timezone);
+
+ if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
+ formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
+ }
+
+ return formatted;
+ }
+ };
+}
+
+function badInputChecker(scope, element, attr, ctrl, parserName) {
+ var node = element[0];
+ var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
+ if (nativeValidation) {
+ ctrl.$parsers.push(function(value) {
+ var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
+ if (validity.badInput || validity.typeMismatch) {
+ ctrl.$$parserName = parserName;
+ return undefined;
+ }
+
+ return value;
+ });
}
}
-function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
-
+function numberFormatterParser(ctrl) {
ctrl.$parsers.push(function(value) {
- var empty = isEmpty(value);
- if (empty || NUMBER_REGEXP.test(value)) {
- ctrl.$setValidity('number', true);
- return value === '' ? null : (empty ? value : parseFloat(value));
- } else {
- ctrl.$setValidity('number', false);
- return undefined;
- }
+ if (ctrl.$isEmpty(value)) return null;
+ if (NUMBER_REGEXP.test(value)) return parseFloat(value);
+
+ ctrl.$$parserName = 'number';
+ return undefined;
});
ctrl.$formatters.push(function(value) {
- return isEmpty(value) ? '' : '' + value;
- });
-
- if (attr.min) {
- var min = parseFloat(attr.min);
- var minValidator = function(value) {
- if (!isEmpty(value) && value < min) {
- ctrl.$setValidity('min', false);
- return undefined;
- } else {
- ctrl.$setValidity('min', true);
- return value;
+ if (!ctrl.$isEmpty(value)) {
+ if (!isNumber(value)) {
+ throw ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
}
- };
-
- ctrl.$parsers.push(minValidator);
- ctrl.$formatters.push(minValidator);
- }
-
- if (attr.max) {
- var max = parseFloat(attr.max);
- var maxValidator = function(value) {
- if (!isEmpty(value) && value > max) {
- ctrl.$setValidity('max', false);
- return undefined;
- } else {
- ctrl.$setValidity('max', true);
- return value;
- }
- };
-
- ctrl.$parsers.push(maxValidator);
- ctrl.$formatters.push(maxValidator);
- }
-
- ctrl.$formatters.push(function(value) {
-
- if (isEmpty(value) || isNumber(value)) {
- ctrl.$setValidity('number', true);
- return value;
- } else {
- ctrl.$setValidity('number', false);
- return undefined;
+ value = value.toString();
}
+ return value;
});
}
+function parseNumberAttrVal(val) {
+ if (isDefined(val) && !isNumber(val)) {
+ val = parseFloat(val);
+ }
+ return !isNumberNaN(val) ? val : undefined;
+}
+
+function isNumberInteger(num) {
+ // See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
+ // (minus the assumption that `num` is a number)
+
+ // eslint-disable-next-line no-bitwise
+ return (num | 0) === num;
+}
+
+function countDecimals(num) {
+ var numString = num.toString();
+ var decimalSymbolIndex = numString.indexOf('.');
+
+ if (decimalSymbolIndex === -1) {
+ if (-1 < num && num < 1) {
+ // It may be in the exponential notation format (`1e-X`)
+ var match = /e-(\d+)$/.exec(numString);
+
+ if (match) {
+ return Number(match[1]);
+ }
+ }
+
+ return 0;
+ }
+
+ return numString.length - decimalSymbolIndex - 1;
+}
+
+function isValidForStep(viewValue, stepBase, step) {
+ // At this point `stepBase` and `step` are expected to be non-NaN values
+ // and `viewValue` is expected to be a valid stringified number.
+ var value = Number(viewValue);
+
+ var isNonIntegerValue = !isNumberInteger(value);
+ var isNonIntegerStepBase = !isNumberInteger(stepBase);
+ var isNonIntegerStep = !isNumberInteger(step);
+
+ // Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
+ // `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
+ if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) {
+ var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0;
+ var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0;
+ var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0;
+
+ var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals);
+ var multiplier = Math.pow(10, decimalCount);
+
+ value = value * multiplier;
+ stepBase = stepBase * multiplier;
+ step = step * multiplier;
+
+ if (isNonIntegerValue) value = Math.round(value);
+ if (isNonIntegerStepBase) stepBase = Math.round(stepBase);
+ if (isNonIntegerStep) step = Math.round(step);
+ }
+
+ return (value - stepBase) % step === 0;
+}
+
+function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ badInputChecker(scope, element, attr, ctrl, 'number');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var parsedMinVal;
+
+ if (isDefined(attr.min) || attr.ngMin) {
+ var minVal = attr.min || $parse(attr.ngMin)(scope);
+ parsedMinVal = parseNumberAttrVal(minVal);
+
+ ctrl.$validators.min = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
+ };
+
+ attr.$observe('min', function(val) {
+ if (val !== minVal) {
+ parsedMinVal = parseNumberAttrVal(val);
+ minVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ });
+ }
+
+ if (isDefined(attr.max) || attr.ngMax) {
+ var maxVal = attr.max || $parse(attr.ngMax)(scope);
+ var parsedMaxVal = parseNumberAttrVal(maxVal);
+
+ ctrl.$validators.max = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
+ };
+
+ attr.$observe('max', function(val) {
+ if (val !== maxVal) {
+ parsedMaxVal = parseNumberAttrVal(val);
+ maxVal = val;
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ });
+ }
+
+ if (isDefined(attr.step) || attr.ngStep) {
+ var stepVal = attr.step || $parse(attr.ngStep)(scope);
+ var parsedStepVal = parseNumberAttrVal(stepVal);
+
+ ctrl.$validators.step = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
+ isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
+ };
+
+ attr.$observe('step', function(val) {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ if (val !== stepVal) {
+ parsedStepVal = parseNumberAttrVal(val);
+ stepVal = val;
+ ctrl.$validate();
+ }
+
+ });
+
+ }
+}
+
+function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
+ badInputChecker(scope, element, attr, ctrl, 'range');
+ numberFormatterParser(ctrl);
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+
+ var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
+ minVal = supportsRange ? 0 : undefined,
+ maxVal = supportsRange ? 100 : undefined,
+ stepVal = supportsRange ? 1 : undefined,
+ validity = element[0].validity,
+ hasMinAttr = isDefined(attr.min),
+ hasMaxAttr = isDefined(attr.max),
+ hasStepAttr = isDefined(attr.step);
+
+ var originalRender = ctrl.$render;
+
+ ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
+ //Browsers that implement range will set these values automatically, but reading the adjusted values after
+ //$render would cause the min / max validators to be applied with the wrong value
+ function rangeRender() {
+ originalRender();
+ ctrl.$setViewValue(element.val());
+ } :
+ originalRender;
+
+ if (hasMinAttr) {
+ minVal = parseNumberAttrVal(attr.min);
+
+ ctrl.$validators.min = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMinValidator() { return true; } :
+ // non-support browsers validate the min val
+ function minValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
+ };
+
+ setInitialValueAndObserver('min', minChange);
+ }
+
+ if (hasMaxAttr) {
+ maxVal = parseNumberAttrVal(attr.max);
+
+ ctrl.$validators.max = supportsRange ?
+ // Since all browsers set the input to a valid value, we don't need to check validity
+ function noopMaxValidator() { return true; } :
+ // non-support browsers validate the max val
+ function maxValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
+ };
+
+ setInitialValueAndObserver('max', maxChange);
+ }
+
+ if (hasStepAttr) {
+ stepVal = parseNumberAttrVal(attr.step);
+
+ ctrl.$validators.step = supportsRange ?
+ function nativeStepValidator() {
+ // Currently, only FF implements the spec on step change correctly (i.e. adjusting the
+ // input element value to a valid value). It's possible that other browsers set the stepMismatch
+ // validity error instead, so we can at least report an error in that case.
+ return !validity.stepMismatch;
+ } :
+ // ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
+ function stepValidator(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
+ isValidForStep(viewValue, minVal || 0, stepVal);
+ };
+
+ setInitialValueAndObserver('step', stepChange);
+ }
+
+ function setInitialValueAndObserver(htmlAttrName, changeFn) {
+ // interpolated attributes set the attribute value only after a digest, but we need the
+ // attribute value when the input is first rendered, so that the browser can adjust the
+ // input value based on the min/max value
+ element.attr(htmlAttrName, attr[htmlAttrName]);
+ var oldVal = attr[htmlAttrName];
+ attr.$observe(htmlAttrName, function wrappedObserver(val) {
+ if (val !== oldVal) {
+ oldVal = val;
+ changeFn(val);
+ }
+ });
+ }
+
+ function minChange(val) {
+ minVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the minVal is greater than the element value
+ if (minVal > elVal) {
+ elVal = minVal;
+ element.val(elVal);
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function maxChange(val) {
+ maxVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ if (supportsRange) {
+ var elVal = element.val();
+ // IE11 doesn't set the el val correctly if the maxVal is less than the element value
+ if (maxVal < elVal) {
+ element.val(maxVal);
+ // IE11 and Chrome don't set the value to the minVal when max < min
+ elVal = maxVal < minVal ? minVal : maxVal;
+ }
+ ctrl.$setViewValue(elVal);
+ } else {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ }
+ }
+
+ function stepChange(val) {
+ stepVal = parseNumberAttrVal(val);
+ // ignore changes before model is initialized
+ if (isNumberNaN(ctrl.$modelValue)) {
+ return;
+ }
+
+ // Some browsers don't adjust the input value correctly, but set the stepMismatch error
+ if (!supportsRange) {
+ // TODO(matsko): implement validateLater to reduce number of validations
+ ctrl.$validate();
+ } else if (ctrl.$viewValue !== element.val()) {
+ ctrl.$setViewValue(element.val());
+ }
+ }
+}
+
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ // Note: no badInputChecker here by purpose as `url` is only a validation
+ // in browsers, i.e. we can always read out input.value even if it is not valid!
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ stringBasedInputType(ctrl);
- var urlValidator = function(value) {
- if (isEmpty(value) || URL_REGEXP.test(value)) {
- ctrl.$setValidity('url', true);
- return value;
- } else {
- ctrl.$setValidity('url', false);
- return undefined;
- }
+ ctrl.$validators.url = function(modelValue, viewValue) {
+ var value = modelValue || viewValue;
+ return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
};
-
- ctrl.$formatters.push(urlValidator);
- ctrl.$parsers.push(urlValidator);
}
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
- textInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ // Note: no badInputChecker here by purpose as `url` is only a validation
+ // in browsers, i.e. we can always read out input.value even if it is not valid!
+ baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
+ stringBasedInputType(ctrl);
- var emailValidator = function(value) {
- if (isEmpty(value) || EMAIL_REGEXP.test(value)) {
- ctrl.$setValidity('email', true);
- return value;
- } else {
- ctrl.$setValidity('email', false);
- return undefined;
- }
+ ctrl.$validators.email = function(modelValue, viewValue) {
+ var value = modelValue || viewValue;
+ return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
};
-
- ctrl.$formatters.push(emailValidator);
- ctrl.$parsers.push(emailValidator);
}
function radioInputType(scope, element, attr, ctrl) {
+ var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false';
// make the name unique, if not defined
if (isUndefined(attr.name)) {
element.attr('name', nextUid());
}
- element.bind('click', function() {
+ var listener = function(ev) {
+ var value;
if (element[0].checked) {
- scope.$apply(function() {
- ctrl.$setViewValue(attr.value);
- });
+ value = attr.value;
+ if (doTrim) {
+ value = trim(value);
+ }
+ ctrl.$setViewValue(value, ev && ev.type);
}
- });
+ };
+
+ element.on('change', listener);
ctrl.$render = function() {
var value = attr.value;
- element[0].checked = (value == ctrl.$viewValue);
+ if (doTrim) {
+ value = trim(value);
+ }
+ element[0].checked = (value === ctrl.$viewValue);
};
attr.$observe('value', ctrl.$render);
}
-function checkboxInputType(scope, element, attr, ctrl) {
- var trueValue = attr.ngTrueValue,
- falseValue = attr.ngFalseValue;
+function parseConstantExpr($parse, context, name, expression, fallback) {
+ var parseFn;
+ if (isDefined(expression)) {
+ parseFn = $parse(expression);
+ if (!parseFn.constant) {
+ throw ngModelMinErr('constexpr', 'Expected constant expression for `{0}`, but saw ' +
+ '`{1}`.', name, expression);
+ }
+ return parseFn(context);
+ }
+ return fallback;
+}
- if (!isString(trueValue)) trueValue = true;
- if (!isString(falseValue)) falseValue = false;
+function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
+ var trueValue = parseConstantExpr($parse, scope, 'ngTrueValue', attr.ngTrueValue, true);
+ var falseValue = parseConstantExpr($parse, scope, 'ngFalseValue', attr.ngFalseValue, false);
- element.bind('click', function() {
- scope.$apply(function() {
- ctrl.$setViewValue(element[0].checked);
- });
- });
+ var listener = function(ev) {
+ ctrl.$setViewValue(element[0].checked, ev && ev.type);
+ };
+
+ element.on('change', listener);
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
};
+ // Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false`
+ // This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert
+ // it to a boolean.
+ ctrl.$isEmpty = function(value) {
+ return value === false;
+ };
+
ctrl.$formatters.push(function(value) {
- return value === trueValue;
+ return equals(value, trueValue);
});
ctrl.$parsers.push(function(value) {
@@ -12003,15 +27274,15 @@ function checkboxInputType(scope, element, attr, ctrl) {
/**
* @ngdoc directive
- * @name ng.directive:textarea
+ * @name textarea
* @restrict E
*
* @description
- * HTML textarea element control with angular data-binding. The data-binding and validation
+ * HTML textarea element control with AngularJS data-binding. The data-binding and validation
* properties of this element are exactly the same as those of the
* {@link ng.directive:input input element}.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
@@ -12020,644 +27291,320 @@ function checkboxInputType(scope, element, attr, ctrl) {
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
* minlength.
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
- * maxlength.
- * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
+ * length.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
+ * If the expression evaluates to a RegExp object, then this is used directly.
+ * If the expression evaluates to a string, then it will be converted to a RegExp
+ * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
+ * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
+ *
+ * @knownIssue
+ *
+ * When specifying the `placeholder` attribute of ``, Internet Explorer will temporarily
+ * insert the placeholder value as the textarea's content. If the placeholder value contains
+ * interpolation (`{{ ... }}`), an error will be logged in the console when AngularJS tries to update
+ * the value of the by-then-removed text node. This doesn't affect the functionality of the
+ * textarea, but can be undesirable.
+ *
+ * You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of
+ * `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can
+ * find more details on `ngAttr` in the
+ * [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the
+ * Developer Guide.
*/
/**
* @ngdoc directive
- * @name ng.directive:input
+ * @name input
* @restrict E
*
* @description
- * HTML input element control with angular data-binding. Input control follows HTML5 input types
- * and polyfills the HTML5 validation behavior for older browsers.
+ * HTML input element control. When used together with {@link ngModel `ngModel`}, it provides data-binding,
+ * input state control, and validation.
+ * Input control follows HTML5 input types and polyfills the HTML5 validation behavior for older browsers.
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ *
+ * **Note:** Not every feature offered is available for all input types.
+ * Specifically, data binding and event handling via `ng-model` is unsupported for `input[file]`.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Sets `required` validation error key if the value is not entered.
* @param {boolean=} ngRequired Sets `required` attribute if set to true
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
* minlength.
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
- * maxlength.
- * @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
- * RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
- * patterns defined as scope expressions.
- * @param {string=} ngChange Angular expression to be executed when input changes due to user
+ * maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
+ * length.
+ * @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
+ * value does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
+ * If the expression evaluates to a RegExp object, then this is used directly.
+ * If the expression evaluates to a string, then it will be converted to a RegExp
+ * after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
+ * `new RegExp('^abc$')`.
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ * @param {string=} ngChange AngularJS expression to be executed when input changes due to user
* interaction with the input element.
+ * @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
+ * This parameter is ignored for input[type=password] controls, which will never trim the
+ * input.
*
* @example
-
-
+
+
-
+
- User name:
-
- Required!
- Last name:
-
- Too short!
-
- Too long!
+
+ User name:
+
+
+
+
+ Required!
+
+
+ Last name:
+
+
+
+
+ Too short!
+
+ Too long!
+
user = {{user}}
-
myForm.userName.$valid = {{myForm.userName.$valid}}
-
myForm.userName.$error = {{myForm.userName.$error}}
-
myForm.lastName.$valid = {{myForm.lastName.$valid}}
-
myForm.lastName.$error = {{myForm.lastName.$error}}
-
myForm.$valid = {{myForm.$valid}}
-
myForm.$error.required = {{!!myForm.$error.required}}
-
myForm.$error.minlength = {{!!myForm.$error.minlength}}
-
myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
+
myForm.userName.$valid = {{myForm.userName.$valid}}
+
myForm.userName.$error = {{myForm.userName.$error}}
+
myForm.lastName.$valid = {{myForm.lastName.$valid}}
+
myForm.lastName.$error = {{myForm.lastName.$error}}
+
myForm.$valid = {{myForm.$valid}}
+
myForm.$error.required = {{!!myForm.$error.required}}
+
myForm.$error.minlength = {{!!myForm.$error.minlength}}
+
myForm.$error.maxlength = {{!!myForm.$error.maxlength}}
-
-
+
+
+ var user = element(by.exactBinding('user'));
+ var userNameValid = element(by.binding('myForm.userName.$valid'));
+ var lastNameValid = element(by.binding('myForm.lastName.$valid'));
+ var lastNameError = element(by.binding('myForm.lastName.$error'));
+ var formValid = element(by.binding('myForm.$valid'));
+ var userNameInput = element(by.model('user.name'));
+ var userLastInput = element(by.model('user.last'));
+
it('should initialize to model', function() {
- expect(binding('user')).toEqual('{"name":"guest","last":"visitor"}');
- expect(binding('myForm.userName.$valid')).toEqual('true');
- expect(binding('myForm.$valid')).toEqual('true');
+ expect(user.getText()).toContain('{"name":"guest","last":"visitor"}');
+ expect(userNameValid.getText()).toContain('true');
+ expect(formValid.getText()).toContain('true');
});
it('should be invalid if empty when required', function() {
- input('user.name').enter('');
- expect(binding('user')).toEqual('{"last":"visitor"}');
- expect(binding('myForm.userName.$valid')).toEqual('false');
- expect(binding('myForm.$valid')).toEqual('false');
+ userNameInput.clear();
+ userNameInput.sendKeys('');
+
+ expect(user.getText()).toContain('{"last":"visitor"}');
+ expect(userNameValid.getText()).toContain('false');
+ expect(formValid.getText()).toContain('false');
});
it('should be valid if empty when min length is set', function() {
- input('user.last').enter('');
- expect(binding('user')).toEqual('{"name":"guest","last":""}');
- expect(binding('myForm.lastName.$valid')).toEqual('true');
- expect(binding('myForm.$valid')).toEqual('true');
+ userLastInput.clear();
+ userLastInput.sendKeys('');
+
+ expect(user.getText()).toContain('{"name":"guest","last":""}');
+ expect(lastNameValid.getText()).toContain('true');
+ expect(formValid.getText()).toContain('true');
});
it('should be invalid if less than required min length', function() {
- input('user.last').enter('xx');
- expect(binding('user')).toEqual('{"name":"guest"}');
- expect(binding('myForm.lastName.$valid')).toEqual('false');
- expect(binding('myForm.lastName.$error')).toMatch(/minlength/);
- expect(binding('myForm.$valid')).toEqual('false');
+ userLastInput.clear();
+ userLastInput.sendKeys('xx');
+
+ expect(user.getText()).toContain('{"name":"guest"}');
+ expect(lastNameValid.getText()).toContain('false');
+ expect(lastNameError.getText()).toContain('minlength');
+ expect(formValid.getText()).toContain('false');
});
it('should be invalid if longer than max length', function() {
- input('user.last').enter('some ridiculously long name');
- expect(binding('user'))
- .toEqual('{"name":"guest"}');
- expect(binding('myForm.lastName.$valid')).toEqual('false');
- expect(binding('myForm.lastName.$error')).toMatch(/maxlength/);
- expect(binding('myForm.$valid')).toEqual('false');
+ userLastInput.clear();
+ userLastInput.sendKeys('some ridiculously long name');
+
+ expect(user.getText()).toContain('{"name":"guest"}');
+ expect(lastNameValid.getText()).toContain('false');
+ expect(lastNameError.getText()).toContain('maxlength');
+ expect(formValid.getText()).toContain('false');
});
-
-
+
+
*/
-var inputDirective = ['$browser', '$sniffer', function($browser, $sniffer) {
+var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
+ function($browser, $sniffer, $filter, $parse) {
return {
restrict: 'E',
- require: '?ngModel',
- link: function(scope, element, attr, ctrl) {
- if (ctrl) {
- (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
- $browser);
+ require: ['?ngModel'],
+ link: {
+ pre: function(scope, element, attr, ctrls) {
+ if (ctrls[0]) {
+ (inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrls[0], $sniffer,
+ $browser, $filter, $parse);
+ }
}
}
};
}];
-var VALID_CLASS = 'ng-valid',
- INVALID_CLASS = 'ng-invalid',
- PRISTINE_CLASS = 'ng-pristine',
- DIRTY_CLASS = 'ng-dirty';
-/**
- * @ngdoc object
- * @name ng.directive:ngModel.NgModelController
- *
- * @property {string} $viewValue Actual string value in the view.
- * @property {*} $modelValue The value in the model, that the control is bound to.
- * @property {Array.
} $parsers Array of functions to execute, as a pipeline, whenever
- the control reads value from the DOM. Each function is called, in turn, passing the value
- through to the next. Used to sanitize / convert the value as well as validation.
-
- For validation, the parsers should update the validity state using
- {@link ng.directive:ngModel.NgModelController#$setValidity $setValidity()},
- and return `undefined` for invalid values.
- *
- * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
- * the model value changes. Each function is called, in turn, passing the value through to the
- * next. Used to format / convert values for display in the control and validation.
- *
- * function formatter(value) {
- * if (value) {
- * return value.toUpperCase();
- * }
- * }
- * ngModel.$formatters.push(formatter);
- *
- * @property {Object} $error An bject hash with all errors as keys.
- *
- * @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.
- *
- * @description
- *
- * `NgModelController` provides API for the `ng-model` directive. The controller contains
- * services for data-binding, validation, CSS update, value formatting and parsing. It
- * specifically does not contain any logic which deals with DOM rendering or listening to
- * DOM events. The `NgModelController` is meant to be extended by other directives where, the
- * directive provides DOM manipulation and the `NgModelController` provides the data-binding.
- * Note that you cannot use `NgModelController` in a directive with an isolated scope,
- * as, in that case, the `ng-model` value gets put into the isolated scope and does not get
- * propogated to the parent scope.
- *
- *
- * 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] {
- border: 1px solid black;
- background-color: white;
- min-height: 20px;
- }
-
- .ng-invalid {
- border: 1px solid red;
- }
-
-
-
- angular.module('customControl', []).
- directive('contenteditable', function() {
- return {
- restrict: 'A', // only activate on element attribute
- require: '?ngModel', // get a hold of NgModelController
- link: function(scope, element, attrs, ngModel) {
- if(!ngModel) return; // do nothing if no ng-model
-
- // Specify how UI should be updated
- ngModel.$render = function() {
- element.html(ngModel.$viewValue || '');
- };
-
- // Listen for change events to enable binding
- element.bind('blur keyup change', function() {
- scope.$apply(read);
- });
- read(); // initialize
-
- // Write data to the model
- function read() {
- var html = element.html();
- // When we clear the content editable the browser leaves a behind
- // If strip-br attribute is provided then we strip this out
- if( attrs.stripBr && html == ' ' ) {
- html = '';
- }
- ngModel.$setViewValue(html);
- }
- }
- };
- });
-
-
-
- Change me!
- Required!
-
-
-
-
-
- it('should data-bind and become invalid', function() {
- var contentEditable = element('[contenteditable]');
-
- expect(contentEditable.text()).toEqual('Change me!');
- input('userContent').enter('');
- expect(contentEditable.text()).toEqual('');
- expect(contentEditable.prop('className')).toMatch(/ng-invalid-required/);
- });
-
- *
- *
- */
-var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse',
- function($scope, $exceptionHandler, $attr, $element, $parse) {
- this.$viewValue = Number.NaN;
- this.$modelValue = Number.NaN;
- this.$parsers = [];
- this.$formatters = [];
- this.$viewChangeListeners = [];
- this.$pristine = true;
- this.$dirty = false;
- this.$valid = true;
- this.$invalid = false;
- this.$name = $attr.name;
-
- var ngModelGet = $parse($attr.ngModel),
- ngModelSet = ngModelGet.assign;
-
- if (!ngModelSet) {
- throw Error(NON_ASSIGNABLE_MODEL_EXPRESSION + $attr.ngModel +
- ' (' + startingTag($element) + ')');
- }
-
- /**
- * @ngdoc function
- * @name ng.directive:ngModel.NgModelController#$render
- * @methodOf ng.directive:ngModel.NgModelController
- *
- * @description
- * Called when the view needs to be updated. It is expected that the user of the ng-model
- * directive will implement this method.
- */
- this.$render = noop;
-
- var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
- invalidCount = 0, // used to easily determine if we are valid
- $error = this.$error = {}; // keep invalid keys here
-
-
- // Setup initial state of the control
- $element.addClass(PRISTINE_CLASS);
- toggleValidCss(true);
-
- // convenience method for easy toggling of classes
- function toggleValidCss(isValid, validationErrorKey) {
- validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
- $element.
- removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
- addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
- }
-
- /**
- * @ngdoc function
- * @name ng.directive:ngModel.NgModelController#$setValidity
- * @methodOf ng.directive:ngModel.NgModelController
- *
- * @description
- * Change the validity state, and notifies the form when the control changes validity. (i.e. it
- * does not notify form if given validator is already marked as invalid).
- *
- * This method should be called by validators - i.e. the parser or formatter functions.
- *
- * @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
- * to `$error[validationErrorKey]=isValid` so that it is available for data-binding.
- * The `validationErrorKey` should be in camelCase and will get converted into dash-case
- * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
- * class and can be bound to as `{{someForm.someControl.$error.myError}}` .
- * @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
- */
- this.$setValidity = function(validationErrorKey, isValid) {
- if ($error[validationErrorKey] === !isValid) return;
-
- if (isValid) {
- if ($error[validationErrorKey]) invalidCount--;
- if (!invalidCount) {
- toggleValidCss(true);
- this.$valid = true;
- this.$invalid = false;
- }
- } else {
- toggleValidCss(false);
- this.$invalid = true;
- this.$valid = false;
- invalidCount++;
- }
-
- $error[validationErrorKey] = !isValid;
- toggleValidCss(isValid, validationErrorKey);
-
- parentForm.$setValidity(validationErrorKey, isValid, this);
- };
-
-
- /**
- * @ngdoc function
- * @name ng.directive:ngModel.NgModelController#$setViewValue
- * @methodOf ng.directive:ngModel.NgModelController
- *
- * @description
- * Read a value from view.
- *
- * This method should be called from within a DOM event handler.
- * For example {@link ng.directive:input input} or
- * {@link ng.directive:select select} directives call it.
- *
- * It internally calls all `$parsers` (including validators) and updates the `$modelValue` and the actual model path.
- * Lastly it calls all registered change listeners.
- *
- * @param {string} value Value from the view.
- */
- this.$setViewValue = function(value) {
- this.$viewValue = value;
-
- // change to dirty
- if (this.$pristine) {
- this.$dirty = true;
- this.$pristine = false;
- $element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
- parentForm.$setDirty();
- }
-
- forEach(this.$parsers, function(fn) {
- value = fn(value);
- });
-
- if (this.$modelValue !== value) {
- this.$modelValue = value;
- ngModelSet($scope, value);
- forEach(this.$viewChangeListeners, function(listener) {
- try {
- listener();
- } catch(e) {
- $exceptionHandler(e);
- }
- })
+var hiddenInputBrowserCacheDirective = function() {
+ var valueProperty = {
+ configurable: true,
+ enumerable: false,
+ get: function() {
+ return this.getAttribute('value') || '';
+ },
+ set: function(val) {
+ this.setAttribute('value', val);
}
};
- // model -> value
- var ctrl = this;
-
- $scope.$watch(function ngModelWatch() {
- var value = ngModelGet($scope);
-
- // if scope model value and ngModel value are out of sync
- if (ctrl.$modelValue !== value) {
-
- var formatters = ctrl.$formatters,
- idx = formatters.length;
-
- ctrl.$modelValue = value;
- while(idx--) {
- value = formatters[idx](value);
- }
-
- if (ctrl.$viewValue !== value) {
- ctrl.$viewValue = value;
- ctrl.$render();
- }
- }
- });
-}];
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngModel
- *
- * @element input
- *
- * @description
- * Is a directive that tells Angular to do two-way data binding. It works together with `input`,
- * `select`, `textarea`. You can easily write your own directives to use `ngModel` as well.
- *
- * `ngModel` is responsible for:
- *
- * - 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 state of the control (valid/invalid, dirty/pristine, validation errors),
- * - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`),
- * - register the control with parent {@link ng.directive:form form}.
- *
- * 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.
- *
- * For basic examples, how to use `ngModel`, see:
- *
- * - {@link ng.directive:input input}
- * - {@link ng.directive:input.text text}
- * - {@link ng.directive:input.checkbox checkbox}
- * - {@link ng.directive:input.radio radio}
- * - {@link ng.directive:input.number number}
- * - {@link ng.directive:input.email email}
- * - {@link ng.directive:input.url url}
- * - {@link ng.directive:select select}
- * - {@link ng.directive:textarea textarea}
- *
- */
-var ngModelDirective = function() {
return {
- require: ['ngModel', '^?form'],
- controller: NgModelController,
- link: function(scope, element, attr, ctrls) {
- // notify others, especially parent forms
+ restrict: 'E',
+ priority: 200,
+ compile: function(_, attr) {
+ if (lowercase(attr.type) !== 'hidden') {
+ return;
+ }
- var modelCtrl = ctrls[0],
- formCtrl = ctrls[1] || nullFormCtrl;
+ return {
+ pre: function(scope, element, attr, ctrls) {
+ var node = element[0];
- formCtrl.$addControl(modelCtrl);
+ // Support: Edge
+ // Moving the DOM around prevents autofillling
+ if (node.parentNode) {
+ node.parentNode.insertBefore(node, node.nextSibling);
+ }
- element.bind('$destroy', function() {
- formCtrl.$removeControl(modelCtrl);
- });
- }
- };
-};
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngChange
- * @restrict E
- *
- * @description
- * Evaluate given expression when user changes the input.
- * The expression is not evaluated when the value change is coming from the model.
- *
- * Note, this directive requires `ngModel` to be present.
- *
- * @element input
- *
- * @example
- *
- *
- *
- *
- *
- *
- * Confirmed
- * debug = {{confirmed}}
- * counter = {{counter}}
- *
- *
- *
- * it('should evaluate the expression if changing from view', function() {
- * expect(binding('counter')).toEqual('0');
- * element('#ng-change-example1').click();
- * expect(binding('counter')).toEqual('1');
- * expect(binding('confirmed')).toEqual('true');
- * });
- *
- * it('should not evaluate the expression if changing from model', function() {
- * element('#ng-change-example2').click();
- * expect(binding('counter')).toEqual('0');
- * expect(binding('confirmed')).toEqual('true');
- * });
- *
- *
- */
-var ngChangeDirective = valueFn({
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- ctrl.$viewChangeListeners.push(function() {
- scope.$eval(attr.ngChange);
- });
- }
-});
-
-
-var requiredDirective = function() {
- return {
- require: '?ngModel',
- link: function(scope, elm, attr, ctrl) {
- if (!ctrl) return;
- attr.required = true; // force truthy in case we are on non input element
-
- var validator = function(value) {
- if (attr.required && (isEmpty(value) || value === false)) {
- ctrl.$setValidity('required', false);
- return;
- } else {
- ctrl.$setValidity('required', true);
- return value;
+ // Support: FF, IE
+ // Avoiding direct assignment to .value prevents autofillling
+ if (Object.defineProperty) {
+ Object.defineProperty(node, 'value', valueProperty);
+ }
}
};
-
- ctrl.$formatters.push(validator);
- ctrl.$parsers.unshift(validator);
-
- attr.$observe('required', function() {
- validator(ctrl.$viewValue);
- });
}
};
};
-/**
- * @ngdoc directive
- * @name ng.directive:ngList
- *
- * @description
- * Text input that converts between comma-separated string into an array of strings.
- *
- * @element input
- * @param {string=} ngList optional delimiter that should be used to split the value. If
- * specified in form `/something/` then the value will be converted into a regular expression.
- *
- * @example
-
-
-
-
- List:
-
- Required!
-
- names = {{names}}
- myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
- myForm.namesInput.$error = {{myForm.namesInput.$error}}
- myForm.$valid = {{myForm.$valid}}
- myForm.$error.required = {{!!myForm.$error.required}}
-
-
-
- it('should initialize to model', function() {
- expect(binding('names')).toEqual('["igor","misko","vojta"]');
- expect(binding('myForm.namesInput.$valid')).toEqual('true');
- expect(element('span.error').css('display')).toBe('none');
- });
-
- it('should be invalid if empty', function() {
- input('names').enter('');
- expect(binding('names')).toEqual('[]');
- expect(binding('myForm.namesInput.$valid')).toEqual('false');
- expect(element('span.error').css('display')).not().toBe('none');
- });
-
-
- */
-var ngListDirective = function() {
- return {
- require: 'ngModel',
- link: function(scope, element, attr, ctrl) {
- var match = /\/(.*)\//.exec(attr.ngList),
- separator = match && new RegExp(match[1]) || attr.ngList || ',';
-
- var parse = function(viewValue) {
- var list = [];
-
- if (viewValue) {
- forEach(viewValue.split(separator), function(value) {
- if (value) list.push(trim(value));
- });
- }
-
- return list;
- };
-
- ctrl.$parsers.push(parse);
- ctrl.$formatters.push(function(value) {
- if (isArray(value)) {
- return value.join(', ');
- }
-
- return undefined;
- });
- }
- };
-};
-
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
+/**
+ * @ngdoc directive
+ * @name ngValue
+ * @restrict A
+ * @priority 100
+ *
+ * @description
+ * Binds the given expression to the value of the element.
+ *
+ * It is mainly used on {@link input[radio] `input[radio]`} and option elements,
+ * so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its
+ * {@link select `select`} parent element) is set to the bound value. It is especially useful
+ * for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below.
+ *
+ * It can also be used to achieve one-way binding of a given expression to an input element
+ * such as an `input[text]` or a `textarea`, when that element does not use ngModel.
+ *
+ * @element ANY
+ * @param {string=} ngValue AngularJS expression, whose value will be bound to the `value` attribute
+ * and `value` property of the element.
+ *
+ * @example
+
+
+
+
+ Which is your favorite?
+
+ {{name}}
+
+
+ You chose {{my.favorite}}
+
+
+
+ var favorite = element(by.binding('my.favorite'));
+ it('should initialize to model', function() {
+ expect(favorite.getText()).toContain('unicorns');
+ });
+ it('should bind the values to the inputs', function() {
+ element.all(by.model('my.favorite')).get(0).click();
+ expect(favorite.getText()).toContain('pizza');
+ });
+
+
+ */
var ngValueDirective = function() {
+ /**
+ * inputs use the value attribute as their default value if the value property is not set.
+ * Once the value property has been set (by adding input), it will not react to changes to
+ * the value attribute anymore. Setting both attribute and property fixes this behavior, and
+ * makes it possible to use ngValue as a sort of one-way bind.
+ */
+ function updateElementValue(element, attr, value) {
+ // Support: IE9 only
+ // In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`).
+ var propValue = isDefined(value) ? value : (msie === 9) ? '' : null;
+ element.prop('value', propValue);
+ attr.$set('value', value);
+ }
+
return {
+ restrict: 'A',
priority: 100,
compile: function(tpl, tplAttr) {
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
- return function(scope, elm, attr) {
- attr.$set('value', scope.$eval(attr.ngValue));
+ return function ngValueConstantLink(scope, elm, attr) {
+ var value = scope.$eval(attr.ngValue);
+ updateElementValue(elm, attr, value);
};
} else {
- return function(scope, elm, attr) {
+ return function ngValueLink(scope, elm, attr) {
scope.$watch(attr.ngValue, function valueWatchAction(value) {
- attr.$set('value', value);
+ updateElementValue(elm, attr, value);
});
};
}
@@ -12667,18 +27614,19 @@ var ngValueDirective = function() {
/**
* @ngdoc directive
- * @name ng.directive:ngBind
+ * @name ngBind
+ * @restrict AC
*
* @description
- * The `ngBind` attribute tells Angular to replace the text content of the specified HTML element
+ * The `ngBind` attribute tells AngularJS to replace the text content of the specified HTML element
* with the value of a given expression, and to update the text content when the value of that
* expression changes.
*
* Typically, you don't use `ngBind` directly, but instead you use the double curly markup like
* `{{ expression }}` which is similar but less verbose.
*
- * It is preferrable to use `ngBind` instead of `{{ expression }}` when a template is momentarily
- * displayed by the browser in its raw state before Angular compiles it. Since `ngBind` is an
+ * It is preferable to use `ngBind` instead of `{{ expression }}` if a template is momentarily
+ * displayed by the browser in its raw state before AngularJS compiles it. Since `ngBind` is an
* element attribute, it makes the bindings invisible to the user while the page is loading.
*
* An alternative solution to this problem would be using the
@@ -12690,38 +27638,51 @@ var ngValueDirective = function() {
*
* @example
* Enter a name in the Live Preview text box; the greeting below the text box changes instantly.
-
-
+
+
-
- Enter name:
+
+ Enter name:
Hello !
-
-
+
+
it('should check ng-bind', function() {
- expect(using('.doc-example-live').binding('name')).toBe('Whirled');
- using('.doc-example-live').input('name').enter('world');
- expect(using('.doc-example-live').binding('name')).toBe('world');
+ var nameInput = element(by.model('name'));
+
+ expect(element(by.binding('name')).getText()).toBe('Whirled');
+ nameInput.clear();
+ nameInput.sendKeys('world');
+ expect(element(by.binding('name')).getText()).toBe('world');
});
-
-
+
+
*/
-var ngBindDirective = ngDirective(function(scope, element, attr) {
- element.addClass('ng-binding').data('$binding', attr.ngBind);
- scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
- element.text(value == undefined ? '' : value);
- });
-});
+var ngBindDirective = ['$compile', function($compile) {
+ return {
+ restrict: 'AC',
+ compile: function ngBindCompile(templateElement) {
+ $compile.$$addBindingClass(templateElement);
+ return function ngBindLink(scope, element, attr) {
+ $compile.$$addBindingInfo(element, attr.ngBind);
+ element = element[0];
+ scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
+ element.textContent = stringify(value);
+ });
+ };
+ }
+ };
+}];
/**
* @ngdoc directive
- * @name ng.directive:ngBindTemplate
+ * @name ngBindTemplate
*
* @description
* The `ngBindTemplate` directive specifies that the element
@@ -12737,143 +27698,396 @@ var ngBindDirective = ngDirective(function(scope, element, attr) {
*
* @example
* Try it here: enter text in text box and watch the greeting change.
-
-
+
+
-
- Salutation:
- Name:
+
-
-
+
+
it('should check ng-bind', function() {
- expect(using('.doc-example-live').binding('salutation')).
- toBe('Hello');
- expect(using('.doc-example-live').binding('name')).
- toBe('World');
- using('.doc-example-live').input('salutation').enter('Greetings');
- using('.doc-example-live').input('name').enter('user');
- expect(using('.doc-example-live').binding('salutation')).
- toBe('Greetings');
- expect(using('.doc-example-live').binding('name')).
- toBe('user');
+ var salutationElem = element(by.binding('salutation'));
+ var salutationInput = element(by.model('salutation'));
+ var nameInput = element(by.model('name'));
+
+ expect(salutationElem.getText()).toBe('Hello World!');
+
+ salutationInput.clear();
+ salutationInput.sendKeys('Greetings');
+ nameInput.clear();
+ nameInput.sendKeys('user');
+
+ expect(salutationElem.getText()).toBe('Greetings user!');
});
-
-
+
+
*/
-var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
- return function(scope, element, attr) {
- // TODO: move this to scenario runner
- var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
- element.addClass('ng-binding').data('$binding', interpolateFn);
- attr.$observe('ngBindTemplate', function(value) {
- element.text(value);
- });
- }
+var ngBindTemplateDirective = ['$interpolate', '$compile', function($interpolate, $compile) {
+ return {
+ compile: function ngBindTemplateCompile(templateElement) {
+ $compile.$$addBindingClass(templateElement);
+ return function ngBindTemplateLink(scope, element, attr) {
+ var interpolateFn = $interpolate(element.attr(attr.$attr.ngBindTemplate));
+ $compile.$$addBindingInfo(element, interpolateFn.expressions);
+ element = element[0];
+ attr.$observe('ngBindTemplate', function(value) {
+ element.textContent = isUndefined(value) ? '' : value;
+ });
+ };
+ }
+ };
}];
/**
* @ngdoc directive
- * @name ng.directive:ngBindHtmlUnsafe
+ * @name ngBindHtml
*
* @description
- * Creates a binding that will innerHTML the result of evaluating the `expression` into the current
- * element. *The innerHTML-ed content will not be sanitized!* You should use this directive only if
- * {@link ngSanitize.directive:ngBindHtml ngBindHtml} directive is too
- * restrictive and when you absolutely trust the source of the content you are binding to.
+ * Evaluates the expression and inserts the resulting HTML into the element in a secure way. By default,
+ * the resulting HTML content will be sanitized using the {@link ngSanitize.$sanitize $sanitize} service.
+ * To utilize this functionality, ensure that `$sanitize` is available, for example, by including {@link
+ * ngSanitize} in your module's dependencies (not in core AngularJS). In order to use {@link ngSanitize}
+ * in your module's dependencies, you need to include "angular-sanitize.js" in your application.
*
- * See {@link ngSanitize.$sanitize $sanitize} docs for examples.
+ * You may also bypass sanitization for values you know are safe. To do so, bind to
+ * an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example
+ * under {@link ng.$sce#show-me-an-example-using-sce- Strict Contextual Escaping (SCE)}.
+ *
+ * Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you
+ * will have an exception (instead of an exploit.)
*
* @element ANY
- * @param {expression} ngBindHtmlUnsafe {@link guide/expression Expression} to evaluate.
+ * @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
+ *
+ * @example
+
+
+
+
+
+
+
+ angular.module('bindHtmlExample', ['ngSanitize'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.myHTML =
+ 'I am an HTML
string with ' +
+ 'links! and other stuff ';
+ }]);
+
+
+
+ it('should check ng-bind-html', function() {
+ expect(element(by.binding('myHTML')).getText()).toBe(
+ 'I am an HTMLstring with links! and other stuff');
+ });
+
+
*/
-var ngBindHtmlUnsafeDirective = [function() {
- return function(scope, element, attr) {
- element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe);
- scope.$watch(attr.ngBindHtmlUnsafe, function ngBindHtmlUnsafeWatchAction(value) {
- element.html(value || '');
- });
+var ngBindHtmlDirective = ['$sce', '$parse', '$compile', function($sce, $parse, $compile) {
+ return {
+ restrict: 'A',
+ compile: function ngBindHtmlCompile(tElement, tAttrs) {
+ var ngBindHtmlGetter = $parse(tAttrs.ngBindHtml);
+ var ngBindHtmlWatch = $parse(tAttrs.ngBindHtml, function sceValueOf(val) {
+ // Unwrap the value to compare the actual inner safe value, not the wrapper object.
+ return $sce.valueOf(val);
+ });
+ $compile.$$addBindingClass(tElement);
+
+ return function ngBindHtmlLink(scope, element, attr) {
+ $compile.$$addBindingInfo(element, attr.ngBindHtml);
+
+ scope.$watch(ngBindHtmlWatch, function ngBindHtmlWatchAction() {
+ // The watched value is the unwrapped value. To avoid re-escaping, use the direct getter.
+ var value = ngBindHtmlGetter(scope);
+ element.html($sce.getTrustedHtml(value) || '');
+ });
+ };
+ }
};
}];
+/**
+ * @ngdoc directive
+ * @name ngChange
+ * @restrict A
+ *
+ * @description
+ * Evaluate the given expression when the user changes the input.
+ * The expression is evaluated immediately, unlike the JavaScript onchange event
+ * which only triggers at the end of a change (usually, when the user leaves the
+ * form element or presses the return key).
+ *
+ * The `ngChange` expression is only evaluated when a change in the input value causes
+ * a new value to be committed to the model.
+ *
+ * It will not be evaluated:
+ * * if the value returned from the `$parsers` transformation pipeline has not changed
+ * * if the input has continued to be invalid since the model will stay `null`
+ * * if the model is changed programmatically and not by a change to the input value
+ *
+ *
+ * Note, this directive requires `ngModel` to be present.
+ *
+ * @element ANY
+ * @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
+ * in input value.
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ *
+ * Confirmed
+ * debug = {{confirmed}}
+ * counter = {{counter}}
+ *
+ *
+ *
+ * var counter = element(by.binding('counter'));
+ * var debug = element(by.binding('confirmed'));
+ *
+ * it('should evaluate the expression if changing from view', function() {
+ * expect(counter.getText()).toContain('0');
+ *
+ * element(by.id('ng-change-example1')).click();
+ *
+ * expect(counter.getText()).toContain('1');
+ * expect(debug.getText()).toContain('true');
+ * });
+ *
+ * it('should not evaluate the expression if changing from model', function() {
+ * element(by.id('ng-change-example2')).click();
+
+ * expect(counter.getText()).toContain('0');
+ * expect(debug.getText()).toContain('true');
+ * });
+ *
+ *
+ */
+var ngChangeDirective = valueFn({
+ restrict: 'A',
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ ctrl.$viewChangeListeners.push(function() {
+ scope.$eval(attr.ngChange);
+ });
+ }
+});
+
+/* exported
+ ngClassDirective,
+ ngClassEvenDirective,
+ ngClassOddDirective
+*/
+
function classDirective(name, selector) {
name = 'ngClass' + name;
- return ngDirective(function(scope, element, attr) {
- var oldVal = undefined;
+ var indexWatchExpression;
- scope.$watch(attr[name], ngClassWatchAction, true);
+ return ['$parse', function($parse) {
+ return {
+ restrict: 'AC',
+ link: function(scope, element, attr) {
+ var classCounts = element.data('$classCounts');
+ var oldModulo = true;
+ var oldClassString;
- attr.$observe('class', function(value) {
- var ngClass = scope.$eval(attr[name]);
- ngClassWatchAction(ngClass, ngClass);
- });
+ if (!classCounts) {
+ // Use createMap() to prevent class assumptions involving property
+ // names in Object.prototype
+ classCounts = createMap();
+ element.data('$classCounts', classCounts);
+ }
-
- if (name !== 'ngClass') {
- scope.$watch('$index', function($index, old$index) {
- var mod = $index & 1;
- if (mod !== old$index & 1) {
- if (mod === selector) {
- addClass(scope.$eval(attr[name]));
- } else {
- removeClass(scope.$eval(attr[name]));
+ if (name !== 'ngClass') {
+ if (!indexWatchExpression) {
+ indexWatchExpression = $parse('$index', function moduloTwo($index) {
+ // eslint-disable-next-line no-bitwise
+ return $index & 1;
+ });
}
+
+ scope.$watch(indexWatchExpression, ngClassIndexWatchAction);
}
- });
- }
+ scope.$watch($parse(attr[name], toClassString), ngClassWatchAction);
- function ngClassWatchAction(newVal) {
- if (selector === true || scope.$index % 2 === selector) {
- if (oldVal && !equals(newVal,oldVal)) {
- removeClass(oldVal);
+ function addClasses(classString) {
+ classString = digestClassCounts(split(classString), 1);
+ attr.$addClass(classString);
+ }
+
+ function removeClasses(classString) {
+ classString = digestClassCounts(split(classString), -1);
+ attr.$removeClass(classString);
+ }
+
+ function updateClasses(oldClassString, newClassString) {
+ var oldClassArray = split(oldClassString);
+ var newClassArray = split(newClassString);
+
+ var toRemoveArray = arrayDifference(oldClassArray, newClassArray);
+ var toAddArray = arrayDifference(newClassArray, oldClassArray);
+
+ var toRemoveString = digestClassCounts(toRemoveArray, -1);
+ var toAddString = digestClassCounts(toAddArray, 1);
+
+ attr.$addClass(toAddString);
+ attr.$removeClass(toRemoveString);
+ }
+
+ function digestClassCounts(classArray, count) {
+ var classesToUpdate = [];
+
+ forEach(classArray, function(className) {
+ if (count > 0 || classCounts[className]) {
+ classCounts[className] = (classCounts[className] || 0) + count;
+ if (classCounts[className] === +(count > 0)) {
+ classesToUpdate.push(className);
+ }
+ }
+ });
+
+ return classesToUpdate.join(' ');
+ }
+
+ function ngClassIndexWatchAction(newModulo) {
+ // This watch-action should run before the `ngClassWatchAction()`, thus it
+ // adds/removes `oldClassString`. If the `ngClass` expression has changed as well, the
+ // `ngClassWatchAction()` will update the classes.
+ if (newModulo === selector) {
+ addClasses(oldClassString);
+ } else {
+ removeClasses(oldClassString);
+ }
+
+ oldModulo = newModulo;
+ }
+
+ function ngClassWatchAction(newClassString) {
+ if (oldModulo === selector) {
+ updateClasses(oldClassString, newClassString);
+ }
+
+ oldClassString = newClassString;
}
- addClass(newVal);
}
- oldVal = copy(newVal);
+ };
+ }];
+
+ // Helpers
+ function arrayDifference(tokens1, tokens2) {
+ if (!tokens1 || !tokens1.length) return [];
+ if (!tokens2 || !tokens2.length) return tokens1;
+
+ var values = [];
+
+ outer:
+ for (var i = 0; i < tokens1.length; i++) {
+ var token = tokens1[i];
+ for (var j = 0; j < tokens2.length; j++) {
+ if (token === tokens2[j]) continue outer;
+ }
+ values.push(token);
}
+ return values;
+ }
- function removeClass(classVal) {
- if (isObject(classVal) && !isArray(classVal)) {
- classVal = map(classVal, function(v, k) { if (v) return k });
- }
- element.removeClass(isArray(classVal) ? classVal.join(' ') : classVal);
+ function split(classString) {
+ return classString && classString.split(' ');
+ }
+
+ function toClassString(classValue) {
+ if (!classValue) return classValue;
+
+ var classString = classValue;
+
+ if (isArray(classValue)) {
+ classString = classValue.map(toClassString).join(' ');
+ } else if (isObject(classValue)) {
+ classString = Object.keys(classValue).
+ filter(function(key) { return classValue[key]; }).
+ join(' ');
+ } else if (!isString(classValue)) {
+ classString = classValue + '';
}
-
- function addClass(classVal) {
- if (isObject(classVal) && !isArray(classVal)) {
- classVal = map(classVal, function(v, k) { if (v) return k });
- }
- if (classVal) {
- element.addClass(isArray(classVal) ? classVal.join(' ') : classVal);
- }
- }
- });
+ return classString;
+ }
}
/**
* @ngdoc directive
- * @name ng.directive:ngClass
+ * @name ngClass
+ * @restrict AC
+ * @element ANY
*
* @description
- * The `ngClass` allows you to set CSS classes on HTML an element, dynamically, by databinding
+ * The `ngClass` directive allows you to dynamically set CSS classes on an HTML element by databinding
* an expression that represents all classes to be added.
*
+ * The directive operates in three different ways, depending on which of three types the expression
+ * evaluates to:
+ *
+ * 1. If the expression evaluates to a string, the string should be one or more space-delimited class
+ * names.
+ *
+ * 2. If the expression evaluates to an object, then for each key-value pair of the
+ * object with a truthy value the corresponding key is used as a class name.
+ *
+ * 3. If the expression evaluates to an array, each element of the array should either be a string as in
+ * type 1 or an object as in type 2. This means that you can mix strings and objects together in an array
+ * to give you more control over what CSS classes appear. See the code below for an example of this.
+ *
+ *
* The directive won't add duplicate classes if a particular class was already set.
*
- * When the expression changes, the previously added classes are removed and only then the
- * new classes are added.
+ * When the expression changes, the previously added classes are removed and only then are the
+ * new classes added.
+ *
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `class`
+ * attribute, when using the `ngClass` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
+ * | {@link ng.$animate#setClass setClass} | just before classes are added and classes are removed from the element at the same time |
+ *
+ * ### 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 $animate#addClass $animate.addClass} and
+ {@link $animate#removeClass $animate.removeClass}.
*
- * @element ANY
* @param {expression} ngClass {@link guide/expression Expression} to eval. The result
* of the evaluation can be a string representing space delimited class
* names, an array, or a map of class names to boolean values. In the case of a map, the
@@ -12881,31 +28095,131 @@ function classDirective(name, selector) {
* element.
*
* @example
-
+ * ### Basic
+
-
-
-
- Sample Text
+ Map Syntax Example
+
+
+ deleted (apply "strike" class)
+
+
+
+ important (apply "bold" class)
+
+
+
+ error (apply "has-error" class)
+
+
+ Using String Syntax
+
+
+ Using Array Syntax
+
+
+
+
+ Using Array and Map Syntax
+
+ warning (apply "orange" class)
- .my-class {
- color: red;
+ .strike {
+ text-decoration: line-through;
+ }
+ .bold {
+ font-weight: bold;
+ }
+ .red {
+ color: red;
+ }
+ .has-error {
+ color: red;
+ background-color: yellow;
+ }
+ .orange {
+ color: orange;
}
-
+
+ var ps = element.all(by.css('p'));
+
+ it('should let you toggle the class', function() {
+
+ expect(ps.first().getAttribute('class')).not.toMatch(/bold/);
+ expect(ps.first().getAttribute('class')).not.toMatch(/has-error/);
+
+ element(by.model('important')).click();
+ expect(ps.first().getAttribute('class')).toMatch(/bold/);
+
+ element(by.model('error')).click();
+ expect(ps.first().getAttribute('class')).toMatch(/has-error/);
+ });
+
+ it('should let you toggle string example', function() {
+ expect(ps.get(1).getAttribute('class')).toBe('');
+ element(by.model('style')).clear();
+ element(by.model('style')).sendKeys('red');
+ expect(ps.get(1).getAttribute('class')).toBe('red');
+ });
+
+ it('array example should have 3 classes', function() {
+ expect(ps.get(2).getAttribute('class')).toBe('');
+ element(by.model('style1')).sendKeys('bold');
+ element(by.model('style2')).sendKeys('strike');
+ element(by.model('style3')).sendKeys('red');
+ expect(ps.get(2).getAttribute('class')).toBe('bold strike red');
+ });
+
+ it('array with map example should have 2 classes', function() {
+ expect(ps.last().getAttribute('class')).toBe('');
+ element(by.model('style4')).sendKeys('bold');
+ element(by.model('warning')).click();
+ expect(ps.last().getAttribute('class')).toBe('bold orange');
+ });
+
+
+
+ @example
+ ### Animations
+
+ The example below demonstrates how to perform animations using ngClass.
+
+
+
+
+
+
+ Sample Text
+
+
+ .base-class {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ }
+
+ .base-class.my-class {
+ color: red;
+ font-size:3em;
+ }
+
+
it('should check ng-class', function() {
- expect(element('.doc-example-live span').prop('className')).not().
+ expect(element(by.css('.base-class')).getAttribute('class')).not.
toMatch(/my-class/);
- using('.doc-example-live').element(':button:first').click();
+ element(by.id('setbtn')).click();
- expect(element('.doc-example-live span').prop('className')).
+ expect(element(by.css('.base-class')).getAttribute('class')).
toMatch(/my-class/);
- using('.doc-example-live').element(':button:last').click();
+ element(by.id('clearbtn')).click();
- expect(element('.doc-example-live span').prop('className')).not().
+ expect(element(by.css('.base-class')).getAttribute('class')).not.
toMatch(/my-class/);
});
@@ -12915,22 +28229,29 @@ var ngClassDirective = classDirective('', true);
/**
* @ngdoc directive
- * @name ng.directive:ngClassOdd
+ * @name ngClassOdd
+ * @restrict AC
*
* @description
* The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except it works in
- * conjunction with `ngRepeat` and takes affect only on odd (even) rows.
+ * {@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 a scope of an
+ * This directive can be applied only within the scope of an
* {@link ng.directive:ngRepeat ngRepeat}.
*
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
+ *
* @element ANY
* @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result
* of the evaluation can be a string representing space delimited class names or an array.
*
* @example
-
+
@@ -12948,36 +28269,99 @@ var ngClassDirective = classDirective('', true);
color: blue;
}
-
+
it('should check ng-class-odd and ng-class-even', function() {
- expect(element('.doc-example-live li:first span').prop('className')).
+ expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
toMatch(/odd/);
- expect(element('.doc-example-live li:last span').prop('className')).
+ expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
toMatch(/even/);
});
+ *
+ *
+ * @example
+ * An example on how to implement animations using `ngClassOdd`:
+ *
+
+
+
+
+
+ .odd {
+ background: rgba(255, 255, 0, 0.25);
+ }
+
+ .odd-add, .odd-remove {
+ transition: 1.5s;
+ }
+
+
+ it('should add new entries to the beginning of the list', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.count()).toBe(4);
+ expect(rows.get(0).getText()).toBe('Item 3');
+ expect(rows.get(1).getText()).toBe('Item 2');
+
+ button.click();
+
+ expect(rows.count()).toBe(5);
+ expect(rows.get(0).getText()).toBe('Item 4');
+ expect(rows.get(1).getText()).toBe('Item 3');
+ });
+
+ it('should add odd class to odd entries', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.get(0).getAttribute('class')).toMatch(/odd/);
+ expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/);
+
+ button.click();
+
+ expect(rows.get(0).getAttribute('class')).toMatch(/odd/);
+ expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/);
+ });
+
+
*/
var ngClassOddDirective = classDirective('Odd', 0);
/**
* @ngdoc directive
- * @name ng.directive:ngClassEven
+ * @name ngClassEven
+ * @restrict AC
*
* @description
* The `ngClassOdd` and `ngClassEven` directives work exactly as
- * {@link ng.directive:ngClass ngClass}, except it works in
- * conjunction with `ngRepeat` and takes affect only on odd (even) rows.
+ * {@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 a scope of an
+ * This directive can be applied only within the scope of an
* {@link ng.directive:ngRepeat ngRepeat}.
*
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
+ *
* @element ANY
* @param {expression} ngClassEven {@link guide/expression Expression} to eval. The
* result of the evaluation can be a string representing space delimited class names or an array.
*
* @example
-
+
@@ -12995,69 +28379,124 @@ var ngClassOddDirective = classDirective('Odd', 0);
color: blue;
}
-
+
it('should check ng-class-odd and ng-class-even', function() {
- expect(element('.doc-example-live li:first span').prop('className')).
+ expect(element(by.repeater('name in names').row(0).column('name')).getAttribute('class')).
toMatch(/odd/);
- expect(element('.doc-example-live li:last span').prop('className')).
+ expect(element(by.repeater('name in names').row(1).column('name')).getAttribute('class')).
toMatch(/even/);
});
+ *
+ *
+ * @example
+ * An example on how to implement animations using `ngClassEven`:
+ *
+
+
+
+
+
+ .even {
+ background: rgba(255, 255, 0, 0.25);
+ }
+
+ .even-add, .even-remove {
+ transition: 1.5s;
+ }
+
+
+ it('should add new entries to the beginning of the list', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.count()).toBe(4);
+ expect(rows.get(0).getText()).toBe('Item 3');
+ expect(rows.get(1).getText()).toBe('Item 2');
+
+ button.click();
+
+ expect(rows.count()).toBe(5);
+ expect(rows.get(0).getText()).toBe('Item 4');
+ expect(rows.get(1).getText()).toBe('Item 3');
+ });
+
+ it('should add even class to even entries', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.get(0).getAttribute('class')).not.toMatch(/even/);
+ expect(rows.get(1).getAttribute('class')).toMatch(/even/);
+
+ button.click();
+
+ expect(rows.get(0).getAttribute('class')).not.toMatch(/even/);
+ expect(rows.get(1).getAttribute('class')).toMatch(/even/);
+ });
+
+
*/
var ngClassEvenDirective = classDirective('Even', 1);
/**
* @ngdoc directive
- * @name ng.directive:ngCloak
+ * @name ngCloak
+ * @restrict AC
*
* @description
- * The `ngCloak` directive is used to prevent the Angular html template from being briefly
+ * The `ngCloak` directive is used to prevent the AngularJS html template from being briefly
* displayed by the browser in its raw (uncompiled) form while your application is loading. Use this
* directive to avoid the undesirable flicker effect caused by the html template display.
*
- * The directive can be applied to the `` element, but typically a fine-grained application is
- * prefered in order to benefit from progressive rendering of the browser view.
+ * The directive can be applied to the `` element, but the preferred usage is to apply
+ * multiple `ngCloak` directives to small portions of the page to permit progressive rendering
+ * of the browser view.
*
- * `ngCloak` works in cooperation with a css rule that is embedded within `angular.js` and
- * `angular.min.js` files. Following is the css rule:
+ * `ngCloak` works in cooperation with the following css rule embedded within `angular.js` and
+ * `angular.min.js`.
+ * For CSP mode please add `angular-csp.css` to your html file (see {@link ng.directive:ngCsp ngCsp}).
*
- *
+ * ```css
* [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
* display: none !important;
* }
- *
+ * ```
*
* When this css rule is loaded by the browser, all html elements (including their children) that
- * are tagged with the `ng-cloak` directive are hidden. When Angular comes across this directive
- * during the compilation of the template it deletes the `ngCloak` element attribute, which
- * makes the compiled element visible.
+ * are tagged with the `ngCloak` directive are hidden. When AngularJS encounters this directive
+ * during the compilation of the template it deletes the `ngCloak` element attribute, making
+ * the compiled element visible.
*
- * For the best result, `angular.js` script must be loaded in the head section of the html file;
- * alternatively, the css rule (above) must be included in the external stylesheet of the
+ * For the best result, the `angular.js` script must be loaded in the head section of the html
+ * document; alternatively, the css rule above must be included in the external stylesheet of the
* application.
*
- * Legacy browsers, like IE7, do not provide attribute selector support (added in CSS 2.1) so they
- * cannot match the `[ng\:cloak]` selector. To work around this limitation, you must add the css
- * class `ngCloak` in addition to `ngCloak` directive as shown in the example below.
- *
* @element ANY
*
* @example
-
-
+
+
{{ 'hello' }}
- {{ 'hello IE7' }}
-
-
+ {{ 'world' }}
+
+
it('should remove the template directive and css class', function() {
- expect(element('.doc-example-live #template1').attr('ng-cloak')).
- not().toBeDefined();
- expect(element('.doc-example-live #template2').attr('ng-cloak')).
- not().toBeDefined();
+ expect($('#template1').getAttribute('ng-cloak')).
+ toBeNull();
+ expect($('#template2').getAttribute('ng-cloak')).
+ toBeNull();
});
-
-
+
+
*
*/
var ngCloakDirective = ngDirective({
@@ -13069,428 +28508,1126 @@ var ngCloakDirective = ngDirective({
/**
* @ngdoc directive
- * @name ng.directive:ngController
+ * @name ngController
*
* @description
- * The `ngController` directive assigns behavior to a scope. This is a key aspect of how angular
+ * The `ngController` directive attaches a controller class to the view. This is a key aspect of how angular
* supports the principles behind the Model-View-Controller design pattern.
*
* MVC components in angular:
*
- * * Model — The Model is data in scope properties; scopes are attached to the DOM.
- * * View — The template (HTML with data bindings) is rendered into the View.
- * * Controller — The `ngController` directive specifies a Controller class; the class has
- * methods that typically express the business logic behind the application.
+ * * 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 an alternative way to define controllers is via the {@link ng.$route $route} service.
+ * Note that you can also attach controllers to the DOM by declaring it in a route definition
+ * via the {@link ngRoute.$route $route} service. A common mistake is to declare the controller
+ * again using `ng-controller` in the template itself. This will cause the controller to be attached
+ * and executed twice.
*
* @element ANY
* @scope
- * @param {expression} ngController Name of a globally accessible constructor function or an
- * {@link guide/expression expression} that on the current scope evaluates to a
- * constructor function.
+ * @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"`.
*
* @example
* Here is a simple form for editing user contact information. Adding, removing, clearing, and
- * greeting are methods declared on the $scope by the controller (see source tab). These methods can
- * easily be called from the angular markup. Notice that any changes to the data are automatically
- * reflected in the View without the need for a manual update.
-
-
-
-
- Name:
- [
greet ]
- Contact:
-
-
-
-
- it('should check controller', function() {
- expect(element('.doc-example-live div>:input').val()).toBe('John Smith');
- expect(element('.doc-example-live li:nth-child(1) input').val())
- .toBe('408 555 1212');
- expect(element('.doc-example-live li:nth-child(2) input').val())
- .toBe('john.smith@example.org');
-
- element('.doc-example-live li:first a:contains("clear")').click();
- expect(element('.doc-example-live li:first input').val()).toBe('');
-
- element('.doc-example-live li:last a:contains("add")').click();
- expect(element('.doc-example-live li:nth-child(3) input').val())
- .toBe('yourname@example.org');
- });
-
-
*/
var ngControllerDirective = [function() {
return {
+ restrict: 'A',
scope: true,
- controller: '@'
+ controller: '@',
+ priority: 500
};
}];
/**
* @ngdoc directive
- * @name ng.directive:ngCsp
- * @priority 1000
+ * @name ngCsp
*
- * @element html
+ * @restrict A
+ * @element ANY
* @description
- * Enables [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) support.
*
- * This is necessary when developing things like Google Chrome Extensions.
+ * AngularJS has some features that can conflict with certain restrictions that are applied when using
+ * [CSP (Content Security Policy)](https://developer.mozilla.org/en/Security/CSP) rules.
*
- * CSP forbids apps to use `eval` or `Function(string)` generated functions (among other things).
- * For us to be compatible, we just need to implement the "getterFn" in $parse without violating
- * any of these restrictions.
+ * If you intend to implement CSP with these rules then you must tell AngularJS not to use these
+ * features.
*
- * AngularJS uses `Function(string)` generated functions as a speed optimization. By applying `ngCsp`
- * it is be possible to opt into the CSP compatible 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.
+ * This is necessary when developing things like Google Chrome Extensions or Universal Windows Apps.
*
- * In order to use this feature put `ngCsp` directive on the root element of the application.
+ *
+ * The following default rules in CSP affect AngularJS:
+ *
+ * * The use of `eval()`, `Function(string)` and similar functions to dynamically create and execute
+ * code from strings is forbidden. AngularJS makes use of this in the {@link $parse} service to
+ * provide a 30% increase in the speed of evaluating AngularJS expressions. (This CSP rule can be
+ * disabled with the CSP keyword `unsafe-eval`, but it is generally not recommended as it would
+ * weaken the protections offered by CSP.)
+ *
+ * * The use of inline resources, such as inline `
+
+ Enter text and hit enter:
+
+
+ list={{list}}
+
+
+
+ it('should check ng-submit', function() {
+ expect(element(by.binding('list')).getText()).toBe('list=[]');
+ element(by.css('#submit')).click();
+ expect(element(by.binding('list')).getText()).toContain('hello');
+ expect(element(by.model('text')).getAttribute('value')).toBe('');
+ });
+ it('should ignore empty strings', function() {
+ expect(element(by.binding('list')).getText()).toBe('list=[]');
+ element(by.css('#submit')).click();
+ element(by.css('#submit')).click();
+ expect(element(by.binding('list')).getText()).toContain('hello');
+ });
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngFocus
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @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.
+ *
+ * @param {expression} ngFocus {@link guide/expression Expression} to evaluate upon
+ * focus. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+ * See {@link ng.directive:ngClick ngClick}
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngBlur
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on blur event.
+ *
+ * A [blur event](https://developer.mozilla.org/en-US/docs/Web/Events/blur) fires when
+ * an element has lost focus.
+ *
+ * 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.
+ *
+ * @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
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on copy event.
+ *
+ * @param {expression} ngCopy {@link guide/expression Expression} to evaluate upon
+ * copy. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ copied: {{copied}}
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngCut
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on cut event.
+ *
+ * @param {expression} ngCut {@link guide/expression Expression} to evaluate upon
+ * cut. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ cut: {{cut}}
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngPaste
+ * @restrict A
+ * @element window, input, select, textarea, a
+ * @priority 0
+ *
+ * @description
+ * Specify custom behavior on paste event.
+ *
+ * @param {expression} ngPaste {@link guide/expression Expression} to evaluate upon
+ * paste. ({@link guide/expression#-event- Event object is available as `$event`})
+ *
+ * @example
+
+
+
+ pasted: {{paste}}
+
+
+ */
+
+/**
+ * @ngdoc directive
+ * @name ngIf
+ * @restrict A
+ * @multiElement
+ *
+ * @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.
+ *
+ * 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.
+ *
+ * Additionally, you can provide animations via the `ngAnimate` module to animate the `enter`
+ * and `leave` effects.
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | just after the `ngIf` contents change and a new DOM element is created and injected into the `ngIf` container |
+ * | {@link ng.$animate#leave leave} | just before the `ngIf` contents are removed from the DOM |
+ *
+ * @element ANY
+ * @scope
+ * @priority 600
+ * @param {expression} ngIf If the {@link guide/expression expression} is falsy then
+ * the element is removed from the DOM tree. If it is truthy a copy of the compiled
+ * element is added to the DOM tree.
+ *
+ * @example
+
+
+ Click me:
+ Show when checked:
+
+ This is removed when the checkbox is unchecked.
+
+
+
+ .animate-if {
+ background:white;
+ border:1px solid black;
+ padding:10px;
+ }
+
+ .animate-if.ng-enter, .animate-if.ng-leave {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+ }
+
+ .animate-if.ng-enter,
+ .animate-if.ng-leave.ng-leave-active {
+ opacity:0;
+ }
+
+ .animate-if.ng-leave,
+ .animate-if.ng-enter.ng-enter-active {
+ opacity:1;
+ }
+
+
+ */
+var ngIfDirective = ['$animate', '$compile', function($animate, $compile) {
return {
- priority: 1000,
- compile: function() {
- $sniffer.csp = true;
+ 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) {
+
+ if (value) {
+ if (!childScope) {
+ $transclude(function(clone, newScope) {
+ childScope = newScope;
+ clone[clone.length++] = $compile.$$createComment('end ngIf', $attr.ngIf);
+ // Note: We only need the first/last node of the cloned nodes.
+ // However, we need to keep the reference to the jqlite wrapper as it might be changed later
+ // by a directive with templateUrl when its template arrives.
+ block = {
+ clone: clone
+ };
+ $animate.enter(clone, $element.parent(), $element);
+ });
+ }
+ } else {
+ if (previousElements) {
+ previousElements.remove();
+ previousElements = null;
+ }
+ if (childScope) {
+ childScope.$destroy();
+ childScope = null;
+ }
+ if (block) {
+ previousElements = getBlockNodes(block.clone);
+ $animate.leave(previousElements).done(function(response) {
+ if (response !== false) previousElements = null;
+ });
+ block = null;
+ }
+ }
+ });
}
};
}];
/**
* @ngdoc directive
- * @name ng.directive:ngClick
- *
- * @description
- * The ngClick allows you to specify custom behavior when
- * element is clicked.
- *
- * @element ANY
- * @param {expression} ngClick {@link guide/expression Expression} to evaluate upon
- * click. (Event object is available as `$event`)
- *
- * @example
-
-
-
- Increment
-
- count: {{count}}
-
-
- it('should check ng-click', function() {
- expect(binding('count')).toBe('0');
- element('.doc-example-live :button').click();
- expect(binding('count')).toBe('1');
- });
-
-
- */
-/*
- * A directive that allows creation of custom onclick handlers that are defined as angular
- * expressions and are compiled and executed within the current scope.
- *
- * Events that are handled via these handler are always configured not to propagate further.
- */
-var ngEventDirectives = {};
-forEach(
- 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave submit'.split(' '),
- function(name) {
- var directiveName = directiveNormalize('ng-' + name);
- ngEventDirectives[directiveName] = ['$parse', function($parse) {
- return function(scope, element, attr) {
- var fn = $parse(attr[directiveName]);
- element.bind(lowercase(name), function(event) {
- scope.$apply(function() {
- fn(scope, {$event:event});
- });
- });
- };
- }];
- }
-);
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngDblclick
- *
- * @description
- * The `ngDblclick` directive allows you to specify custom behavior on dblclick event.
- *
- * @element ANY
- * @param {expression} ngDblclick {@link guide/expression Expression} to evaluate upon
- * dblclick. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMousedown
- *
- * @description
- * The ngMousedown directive allows you to specify custom behavior on mousedown event.
- *
- * @element ANY
- * @param {expression} ngMousedown {@link guide/expression Expression} to evaluate upon
- * mousedown. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMouseup
- *
- * @description
- * Specify custom behavior on mouseup event.
- *
- * @element ANY
- * @param {expression} ngMouseup {@link guide/expression Expression} to evaluate upon
- * mouseup. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMouseover
- *
- * @description
- * Specify custom behavior on mouseover event.
- *
- * @element ANY
- * @param {expression} ngMouseover {@link guide/expression Expression} to evaluate upon
- * mouseover. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMouseenter
- *
- * @description
- * Specify custom behavior on mouseenter event.
- *
- * @element ANY
- * @param {expression} ngMouseenter {@link guide/expression Expression} to evaluate upon
- * mouseenter. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMouseleave
- *
- * @description
- * Specify custom behavior on mouseleave event.
- *
- * @element ANY
- * @param {expression} ngMouseleave {@link guide/expression Expression} to evaluate upon
- * mouseleave. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngMousemove
- *
- * @description
- * Specify custom behavior on mousemove event.
- *
- * @element ANY
- * @param {expression} ngMousemove {@link guide/expression Expression} to evaluate upon
- * mousemove. (Event object is available as `$event`)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngKeydown
- *
- * @description
- * Specify custom behavior on keydown event.
- *
- * @element ANY
- * @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
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngKeyup
- *
- * @description
- * Specify custom behavior on keyup event.
- *
- * @element ANY
- * @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
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngKeypress
- *
- * @description
- * Specify custom behavior on keypress event.
- *
- * @element ANY
- * @param {expression} ngKeypress {@link guide/expression Expression} to evaluate upon
- * keypress. (Event object is available as `$event` and can be interrogated for keyCode, altKey, etc.)
- *
- * @example
- * See {@link ng.directive:ngClick ngClick}
- */
-
-
-/**
- * @ngdoc directive
- * @name ng.directive: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 an `action`
- * attribute**.
- *
- * @element form
- * @param {expression} ngSubmit {@link guide/expression Expression} to eval. (Event object is available as `$event`)
- *
- * @example
-
-
-
-
- Enter text and hit enter:
-
-
- list={{list}}
-
-
-
- it('should check ng-submit', function() {
- expect(binding('list')).toBe('[]');
- element('.doc-example-live #submit').click();
- expect(binding('list')).toBe('["hello"]');
- expect(input('text').val()).toBe('');
- });
- it('should ignore empty strings', function() {
- expect(binding('list')).toBe('[]');
- element('.doc-example-live #submit').click();
- element('.doc-example-live #submit').click();
- expect(binding('list')).toBe('["hello"]');
- });
-
-
- */
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngInclude
+ * @name ngInclude
* @restrict ECA
+ * @scope
+ * @priority -400
*
* @description
* Fetches, compiles and includes an external HTML fragment.
*
- * Keep in mind that Same Origin Policy applies to included resources
- * (e.g. ngInclude won't work for cross-domain requests on all browsers and for
- * file:// access on some browsers).
+ * By default, the template URL is restricted to the same domain and protocol as the
+ * application document. This is done by calling {@link $sce#getTrustedResourceUrl
+ * $sce.getTrustedResourceUrl} on it. To load templates from other domains or protocols
+ * you may either {@link ng.$sceDelegateProvider#resourceUrlWhitelist whitelist them} or
+ * {@link $sce#trustAsResourceUrl wrap them} as trusted values. Refer to AngularJS's {@link
+ * ng.$sce Strict Contextual Escaping}.
*
- * @scope
+ * 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.
*
- * @param {string} ngInclude|src angular expression evaluating to URL. If the source is a string constant,
- * make sure you wrap it in quotes, e.g. `src="'myPartialTemplate.html'"`.
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when the expression changes, on the new include |
+ * | {@link ng.$animate#leave leave} | when the expression changes, on the old include |
+ *
+ * The enter and leave animation occur concurrently.
+ *
+ * @param {string} ngInclude|src AngularJS expression evaluating to URL. If the source is a string constant,
+ * make sure you wrap it in **single** quotes, e.g. `src="'myPartialTemplate.html'"`.
* @param {string=} onload Expression to evaluate when a new partial is loaded.
- *
+ *
+ * **Note:** When using onload on SVG elements in IE11, the browser will try to call
+ * a function with the name on the window element, which will usually throw a
+ * "function is undefined" error. To fix this, you can instead use `data-onload` or a
+ * different form that {@link guide/directive#normalization matches} `onload`.
+ *
+ *
* @param {string=} autoscroll Whether `ngInclude` should call {@link ng.$anchorScroll
* $anchorScroll} to scroll the viewport after the content is loaded.
*
@@ -13499,24 +29636,27 @@ forEach(
* - Otherwise enable scrolling only if the expression evaluates to truthy value.
*
* @example
-
+
-
+
(blank)
- url of the template:
{{template.url}}
+ url of the template:
{{template.url}}
-
+
- function Ctrl($scope) {
- $scope.templates =
- [ { name: 'template1.html', url: 'template1.html'}
- , { name: 'template2.html', url: 'template2.html'} ];
- $scope.template = $scope.templates[0];
- }
+ 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
@@ -13524,19 +29664,72 @@ forEach(
Content of template2.html
-
+
+ .slide-animate-container {
+ position:relative;
+ background:white;
+ border:1px solid black;
+ height:40px;
+ overflow:hidden;
+ }
+
+ .slide-animate {
+ padding:10px;
+ }
+
+ .slide-animate.ng-enter, .slide-animate.ng-leave {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+
+ position:absolute;
+ top:0;
+ left:0;
+ right:0;
+ bottom:0;
+ display:block;
+ padding:10px;
+ }
+
+ .slide-animate.ng-enter {
+ top:-50px;
+ }
+ .slide-animate.ng-enter.ng-enter-active {
+ top:0;
+ }
+
+ .slide-animate.ng-leave {
+ top:0;
+ }
+ .slide-animate.ng-leave.ng-leave-active {
+ top:50px;
+ }
+
+
+ var templateSelect = element(by.model('template'));
+ var includeElem = element(by.css('[ng-include]'));
+
it('should load template1.html', function() {
- expect(element('.doc-example-live [ng-include]').text()).
- toMatch(/Content of template1.html/);
+ expect(includeElem.getText()).toMatch(/Content of template1.html/);
});
+
it('should load template2.html', function() {
- select('template').option('1');
- expect(element('.doc-example-live [ng-include]').text()).
- toMatch(/Content of template2.html/);
+ 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 change to blank', function() {
- select('template').option('');
- expect(element('.doc-example-live [ng-include]').text()).toEqual('');
+ 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);
});
@@ -13545,175 +29738,3115 @@ forEach(
/**
* @ngdoc event
- * @name ng.directive:ngInclude#$includeContentLoaded
- * @eventOf ng.directive:ngInclude
+ * @name ngInclude#$includeContentRequested
+ * @eventType emit on the scope ngInclude was declared in
+ * @description
+ * Emitted every time the ngInclude content is requested.
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
+ */
+
+
+/**
+ * @ngdoc event
+ * @name ngInclude#$includeContentLoaded
* @eventType emit on the current ngInclude scope
* @description
* Emitted every time the ngInclude content is reloaded.
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
*/
-var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile',
- function($http, $templateCache, $anchorScroll, $compile) {
+
+
+/**
+ * @ngdoc event
+ * @name ngInclude#$includeContentError
+ * @eventType emit on the scope ngInclude was declared in
+ * @description
+ * Emitted when a template HTTP request yields an erroneous response (status < 200 || status > 299)
+ *
+ * @param {Object} angularEvent Synthetic event object.
+ * @param {String} src URL of content to load.
+ */
+var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate',
+ function($templateRequest, $anchorScroll, $animate) {
return {
restrict: 'ECA',
+ priority: 400,
terminal: true,
+ transclude: 'element',
+ controller: angular.noop,
compile: function(element, attr) {
var srcExp = attr.ngInclude || attr.src,
onloadExp = attr.onload || '',
autoScrollExp = attr.autoscroll;
- return function(scope, element) {
+ return function(scope, $element, $attr, ctrl, $transclude) {
var changeCounter = 0,
- childScope;
+ currentScope,
+ previousElement,
+ currentElement;
- var clearContent = function() {
- if (childScope) {
- childScope.$destroy();
- childScope = null;
+ var cleanupLastIncludeContent = function() {
+ if (previousElement) {
+ previousElement.remove();
+ previousElement = null;
+ }
+ if (currentScope) {
+ currentScope.$destroy();
+ currentScope = null;
+ }
+ if (currentElement) {
+ $animate.leave(currentElement).done(function(response) {
+ if (response !== false) previousElement = null;
+ });
+ previousElement = currentElement;
+ currentElement = null;
}
-
- element.html('');
};
scope.$watch(srcExp, function ngIncludeWatchAction(src) {
+ var afterAnimation = function(response) {
+ if (response !== false && isDefined(autoScrollExp) &&
+ (!autoScrollExp || scope.$eval(autoScrollExp))) {
+ $anchorScroll();
+ }
+ };
var thisChangeId = ++changeCounter;
if (src) {
- $http.get(src, {cache: $templateCache}).success(function(response) {
+ //set the 2nd param to true to ignore the template request error so that the inner
+ //contents and scope can be cleaned up.
+ $templateRequest(src, true).then(function(response) {
+ if (scope.$$destroyed) return;
+
if (thisChangeId !== changeCounter) return;
+ var newScope = scope.$new();
+ ctrl.template = response;
- if (childScope) childScope.$destroy();
- childScope = scope.$new();
+ // Note: This will also link all children of ng-include that were contained in the original
+ // html. If that content contains controllers, ... they could pollute/change the scope.
+ // However, using ng-include on an element with additional content does not make sense...
+ // Note: We can't remove them in the cloneAttchFn of $transclude as that
+ // function is called before linking the content, which would apply child
+ // directives to non existing elements.
+ var clone = $transclude(newScope, function(clone) {
+ cleanupLastIncludeContent();
+ $animate.enter(clone, null, $element).done(afterAnimation);
+ });
- element.html(response);
- $compile(element.contents())(childScope);
+ currentScope = newScope;
+ currentElement = clone;
- if (isDefined(autoScrollExp) && (!autoScrollExp || scope.$eval(autoScrollExp))) {
- $anchorScroll();
- }
-
- childScope.$emit('$includeContentLoaded');
+ currentScope.$emit('$includeContentLoaded', src);
scope.$eval(onloadExp);
- }).error(function() {
- if (thisChangeId === changeCounter) clearContent();
+ }, function() {
+ if (scope.$$destroyed) return;
+
+ if (thisChangeId === changeCounter) {
+ cleanupLastIncludeContent();
+ scope.$emit('$includeContentError', src);
+ }
});
- } else clearContent();
+ scope.$emit('$includeContentRequested', src);
+ } else {
+ cleanupLastIncludeContent();
+ ctrl.template = null;
+ }
});
};
}
};
}];
+// This directive is called during the $transclude call of the first `ngInclude` directive.
+// It will replace and compile the content of the element with the loaded template.
+// We need this directive so that the element content is already filled when
+// the link function of another directive on the same element as ngInclude
+// is called.
+var ngIncludeFillContentDirective = ['$compile',
+ function($compile) {
+ return {
+ restrict: 'ECA',
+ priority: -400,
+ require: 'ngInclude',
+ link: function(scope, $element, $attr, ctrl) {
+ if (toString.call($element[0]).match(/SVG/)) {
+ // WebKit: https://bugs.webkit.org/show_bug.cgi?id=135698 --- SVG elements do not
+ // support innerHTML, so detect this here and try to generate the contents
+ // specially.
+ $element.empty();
+ $compile(jqLiteBuildFragment(ctrl.template, window.document).childNodes)(scope,
+ function namespaceAdaptedClone(clone) {
+ $element.append(clone);
+ }, {futureParentElement: $element});
+ return;
+ }
+
+ $element.html(ctrl.template);
+ $compile($element.contents())(scope);
+ }
+ };
+ }];
+
/**
* @ngdoc directive
- * @name ng.directive:ngInit
- *
- * @description
- * The `ngInit` directive specifies initialization tasks to be executed
- * before the template enters execution mode during bootstrap.
- *
+ * @name ngInit
+ * @restrict AC
+ * @priority 450
* @element ANY
+ *
* @param {expression} ngInit {@link guide/expression Expression} to eval.
*
+ * @description
+ * The `ngInit` directive allows you to evaluate an expression in the
+ * current scope.
+ *
+ *
+ * This directive can be abused to add unnecessary amounts of logic into your templates.
+ * There are only a few appropriate uses of `ngInit`:
+ *
+ * aliasing special properties of {@link ng.directive:ngRepeat `ngRepeat`},
+ * as seen in the demo below.
+ * initializing data during development, or for examples, as seen throughout these docs.
+ * injecting data via server side scripting.
+ *
+ *
+ * Besides these few cases, you should use {@link guide/component Components} or
+ * {@link guide/controller Controllers} rather than `ngInit` to initialize values on a scope.
+ *
+ *
+ *
+ * **Note**: If you have assignment in `ngInit` along with a {@link ng.$filter `filter`}, make
+ * sure you have parentheses to ensure correct operator precedence:
+ *
+ * `
`
+ *
+ *
+ *
* @example
-
-
-
- {{greeting}} {{person}}!
-
-
-
- it('should check greeting', function() {
- expect(binding('greeting')).toBe('Hello');
- expect(binding('person')).toBe('World');
+
+
+
+
+
+
+ list[ {{outerIndex}} ][ {{innerIndex}} ] = {{value}};
+
+
+
+
+
+ it('should alias index positions', function() {
+ var elements = element.all(by.css('.example-init'));
+ expect(elements.get(0).getText()).toBe('list[ 0 ][ 0 ] = a;');
+ expect(elements.get(1).getText()).toBe('list[ 0 ][ 1 ] = b;');
+ expect(elements.get(2).getText()).toBe('list[ 1 ][ 0 ] = c;');
+ expect(elements.get(3).getText()).toBe('list[ 1 ][ 1 ] = d;');
});
-
-
+
+
*/
var ngInitDirective = ngDirective({
+ priority: 450,
compile: function() {
return {
pre: function(scope, element, attrs) {
scope.$eval(attrs.ngInit);
}
- }
+ };
}
});
/**
* @ngdoc directive
- * @name ng.directive:ngNonBindable
- * @priority 1000
+ * @name ngList
+ * @restrict A
+ * @priority 100
+ *
+ * @param {string=} ngList optional delimiter that should be used to split the value.
*
* @description
- * Sometimes it is necessary to write code which looks like bindings but which should be left alone
- * by angular. Use `ngNonBindable` to make angular ignore a chunk of HTML.
+ * 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
+ * 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
- * In this example there are two location where a simple binding (`{{}}`) is present, but the one
- * wrapped in `ngNonBindable` is left alone.
+ * ### Validation
+ *
+ *
+ *
+ * angular.module('listExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.names = ['morpheus', 'neo', 'trinity'];
+ * }]);
+ *
+ *
+ *
+ * List:
+ *
+ *
+ * Required!
+ *
+ *
+ * names = {{names}}
+ * myForm.namesInput.$valid = {{myForm.namesInput.$valid}}
+ * myForm.namesInput.$error = {{myForm.namesInput.$error}}
+ * myForm.$valid = {{myForm.$valid}}
+ * myForm.$error.required = {{!!myForm.$error.required}}
+ *
+ *
+ *
+ * var listInput = element(by.model('names'));
+ * var names = element(by.exactBinding('names'));
+ * var valid = element(by.binding('myForm.namesInput.$valid'));
+ * var error = element(by.css('span.error'));
+ *
+ * it('should initialize to model', function() {
+ * expect(names.getText()).toContain('["morpheus","neo","trinity"]');
+ * expect(valid.getText()).toContain('true');
+ * expect(error.getCssValue('display')).toBe('none');
+ * });
+ *
+ * it('should be invalid if empty', function() {
+ * listInput.clear();
+ * listInput.sendKeys('');
+ *
+ * expect(names.getText()).toContain('');
+ * expect(valid.getText()).toContain('false');
+ * expect(error.getCssValue('display')).not.toBe('none');
+ * });
+ *
+ *
*
* @example
-
-
- Normal: {{1 + 2}}
- Ignored: {{1 + 2}}
-
-
- it('should check ng-non-bindable', function() {
- expect(using('.doc-example-live').binding('1 + 2')).toBe('3');
- expect(using('.doc-example-live').element('div:last').text()).
- toMatch(/1 \+ 2/);
- });
-
-
+ * ### Splitting on newline
+ *
+ *
+ *
+ *
+ * {{ list | json }}
+ *
+ *
+ * it("should split the text by newlines", function() {
+ * var listInput = element(by.model('list'));
+ * var output = element(by.binding('list | json'));
+ * listInput.sendKeys('abc\ndef\nghi');
+ * expect(output.getText()).toContain('[\n "abc",\n "def",\n "ghi"\n]');
+ * });
+ *
+ *
+ *
*/
-var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+var ngListDirective = function() {
+ return {
+ restrict: 'A',
+ priority: 100,
+ require: 'ngModel',
+ link: function(scope, element, attr, ctrl) {
+ var ngList = attr.ngList || ', ';
+ var trimValues = attr.ngTrim !== 'false';
+ var separator = trimValues ? trim(ngList) : ngList;
+
+ var parse = function(viewValue) {
+ // If the viewValue is invalid (say required but empty) it will be `undefined`
+ if (isUndefined(viewValue)) return;
+
+ 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,
+ PENDING_CLASS: true,
+ addSetValidityMethod: true,
+ setupValidity: true,
+ defaultModelOptions: false
+*/
+
+
+var VALID_CLASS = 'ng-valid',
+ INVALID_CLASS = 'ng-invalid',
+ PRISTINE_CLASS = 'ng-pristine',
+ DIRTY_CLASS = 'ng-dirty',
+ UNTOUCHED_CLASS = 'ng-untouched',
+ TOUCHED_CLASS = 'ng-touched',
+ EMPTY_CLASS = 'ng-empty',
+ NOT_EMPTY_CLASS = 'ng-not-empty';
+
+var ngModelMinErr = minErr('ngModel');
+
+/**
+ * @ngdoc type
+ * @name ngModel.NgModelController
+ * @property {*} $viewValue The actual value from the control's view. For `input` elements, this is a
+ * String. See {@link ngModel.NgModelController#$setViewValue} for information about when the $viewValue
+ * is set.
+ *
+ * @property {*} $modelValue The value in the model that the control is bound to.
+ *
+ * @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever
+ * the control updates the ngModelController with a new {@link ngModel.NgModelController#$viewValue
+ `$viewValue`} from the DOM, usually via user input.
+ See {@link ngModel.NgModelController#$setViewValue `$setViewValue()`} for a detailed lifecycle explanation.
+ Note that the `$parsers` are not called when the bound ngModel expression changes programmatically.
+
+ 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`.
+
+ This simple example shows a parser that would convert text input value to lowercase:
+ * ```js
+ * function parse(value) {
+ * if (value) {
+ * return value.toLowerCase();
+ * }
+ * }
+ * ngModelController.$parsers.push(parse);
+ * ```
+
+ *
+ * @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
+ the bound ngModel expression changes programmatically. The `$formatters` are not called when the
+ value of the control is changed by user interaction.
+
+ Formatters are used to format / convert the {@link ngModel.NgModelController#$modelValue
+ `$modelValue`} for display in the control.
+
+ The functions are called in reverse array order, each passing the value through to the
+ next. The last return value is used as the actual DOM value.
+
+ This simple example shows a formatter that would convert the model value to uppercase:
+
+ * ```js
+ * function format(value) {
+ * if (value) {
+ * return value.toUpperCase();
+ * }
+ * }
+ * ngModel.$formatters.push(format);
+ * ```
+ *
+ * @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
+ * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change
+ * to {@link ngModel.NgModelController#$modelValue `$modelValue`}.
+ * It is called with no arguments, and its return value is ignored.
+ * This can be used in place of additional $watches against the model value.
+ *
+ * @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
+ *
+ * `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
+ * The controller contains services for data-binding, validation, CSS updates, and value formatting
+ * and parsing. It purposefully does not contain any logic which deals with DOM rendering or
+ * listening to DOM events.
+ * Such DOM related logic should be provided by other directives which make use of
+ * `NgModelController` for data-binding to control elements.
+ * AngularJS provides this DOM logic for most {@link input `input`} elements.
+ * At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
+ * custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
+ *
+ * @example
+ * ### Custom Control Example
+ * This example shows how to use `NgModelController` with a custom control to achieve
+ * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
+ * collaborate together to achieve the desired result.
+ *
+ * `contenteditable` is an HTML5 attribute, which tells the browser to let the element
+ * contents be edited in place by the user.
+ *
+ * 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/);
+ });
+
+ *
+ *
+ *
+ */
+NgModelController.$inject = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$q', '$interpolate'];
+function NgModelController($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $q, $interpolate) {
+ this.$viewValue = Number.NaN;
+ this.$modelValue = Number.NaN;
+ this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
+ this.$validators = {};
+ this.$asyncValidators = {};
+ this.$parsers = [];
+ this.$formatters = [];
+ this.$viewChangeListeners = [];
+ this.$untouched = true;
+ this.$touched = false;
+ this.$pristine = true;
+ this.$dirty = false;
+ this.$valid = true;
+ this.$invalid = false;
+ this.$error = {}; // keep invalid keys here
+ this.$$success = {}; // keep valid keys here
+ this.$pending = undefined; // keep pending keys here
+ this.$name = $interpolate($attr.name || '', false)($scope);
+ this.$$parentForm = nullFormCtrl;
+ this.$options = defaultModelOptions;
+ this.$$updateEvents = '';
+ // Attach the correct context to the event handler function for updateOn
+ this.$$updateEventHandler = this.$$updateEventHandler.bind(this);
+
+ this.$$parsedNgModel = $parse($attr.ngModel);
+ this.$$parsedNgModelAssign = this.$$parsedNgModel.assign;
+ this.$$ngModelGet = this.$$parsedNgModel;
+ this.$$ngModelSet = this.$$parsedNgModelAssign;
+ this.$$pendingDebounce = null;
+ this.$$parserValid = undefined;
+ this.$$parserName = 'parse';
+
+ this.$$currentValidationRunId = 0;
+
+ this.$$scope = $scope;
+ this.$$rootScope = $scope.$root;
+ this.$$attr = $attr;
+ this.$$element = $element;
+ this.$$animate = $animate;
+ this.$$timeout = $timeout;
+ this.$$parse = $parse;
+ this.$$q = $q;
+ this.$$exceptionHandler = $exceptionHandler;
+
+ setupValidity(this);
+ setupModelWatcher(this);
+}
+
+NgModelController.prototype = {
+ $$initGetterSetters: function() {
+ if (this.$options.getOption('getterSetter')) {
+ var invokeModelGetter = this.$$parse(this.$$attr.ngModel + '()'),
+ invokeModelSetter = this.$$parse(this.$$attr.ngModel + '($$$p)');
+
+ this.$$ngModelGet = function($scope) {
+ var modelValue = this.$$parsedNgModel($scope);
+ if (isFunction(modelValue)) {
+ modelValue = invokeModelGetter($scope);
+ }
+ return modelValue;
+ };
+ this.$$ngModelSet = function($scope, newValue) {
+ if (isFunction(this.$$parsedNgModel($scope))) {
+ invokeModelSetter($scope, {$$$p: newValue});
+ } else {
+ this.$$parsedNgModelAssign($scope, newValue);
+ }
+ };
+ } else if (!this.$$parsedNgModel.assign) {
+ throw ngModelMinErr('nonassign', 'Expression \'{0}\' is non-assignable. Element: {1}',
+ this.$$attr.ngModel, startingTag(this.$$element));
+ }
+ },
+
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$render
+ *
+ * @description
+ * Called when the view needs to be updated. It is expected that the user of the ng-model
+ * directive will implement this method.
+ *
+ * The `$render()` method is invoked in the following situations:
+ *
+ * * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
+ * committed value then `$render()` is called to update the input control.
+ * * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
+ * the `$viewValue` are different from last time.
+ *
+ * Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
+ * `$modelValue` and `$viewValue` are actually different from their previous values. If `$modelValue`
+ * or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
+ * invoked if you only change a property on the objects.
+ */
+ $render: noop,
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$isEmpty
+ *
+ * @description
+ * This is called when we need to determine if the value of an input is empty.
+ *
+ * For instance, the required directive does this to work out if the input has data or not.
+ *
+ * The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
+ *
+ * You can override this for input directives whose concept of being empty is different from the
+ * default. The `checkboxInputType` directive does this because in its case a value of `false`
+ * implies empty.
+ *
+ * @param {*} value The value of the input to check for emptiness.
+ * @returns {boolean} True if `value` is "empty".
+ */
+ $isEmpty: function(value) {
+ // eslint-disable-next-line no-self-compare
+ return isUndefined(value) || value === '' || value === null || value !== value;
+ },
+
+ $$updateEmptyClasses: function(value) {
+ if (this.$isEmpty(value)) {
+ this.$$animate.removeClass(this.$$element, NOT_EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, EMPTY_CLASS);
+ } else {
+ this.$$animate.removeClass(this.$$element, EMPTY_CLASS);
+ this.$$animate.addClass(this.$$element, NOT_EMPTY_CLASS);
+ }
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setPristine
+ *
+ * @description
+ * Sets the control to its pristine state.
+ *
+ * This method can be called to remove the `ng-dirty` class and set the control to its pristine
+ * state (`ng-pristine` class). A model is considered to be pristine when the control
+ * has not been changed from when first compiled.
+ */
+ $setPristine: function() {
+ this.$dirty = false;
+ this.$pristine = true;
+ this.$$animate.removeClass(this.$$element, DIRTY_CLASS);
+ this.$$animate.addClass(this.$$element, PRISTINE_CLASS);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setDirty
+ *
+ * @description
+ * Sets the control to its dirty state.
+ *
+ * This method can be called to remove the `ng-pristine` class and set the control to its dirty
+ * state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
+ * from when first compiled.
+ */
+ $setDirty: function() {
+ this.$dirty = true;
+ this.$pristine = false;
+ this.$$animate.removeClass(this.$$element, PRISTINE_CLASS);
+ this.$$animate.addClass(this.$$element, DIRTY_CLASS);
+ this.$$parentForm.$setDirty();
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setUntouched
+ *
+ * @description
+ * Sets the control to its untouched state.
+ *
+ * This method can be called to remove the `ng-touched` class and set the control to its
+ * untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
+ * by default, however this function can be used to restore that state if the model has
+ * already been touched by the user.
+ */
+ $setUntouched: function() {
+ this.$touched = false;
+ this.$untouched = true;
+ this.$$animate.setClass(this.$$element, UNTOUCHED_CLASS, TOUCHED_CLASS);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setTouched
+ *
+ * @description
+ * Sets the control to its touched state.
+ *
+ * This method can be called to remove the `ng-untouched` class and set the control to its
+ * touched state (`ng-touched` class). A model is considered to be touched when the user has
+ * first focused the control element and then shifted focus away from the control (blur event).
+ */
+ $setTouched: function() {
+ this.$touched = true;
+ this.$untouched = false;
+ this.$$animate.setClass(this.$$element, TOUCHED_CLASS, UNTOUCHED_CLASS);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$rollbackViewValue
+ *
+ * @description
+ * Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
+ * which may be caused by a pending debounced event or because the input is waiting for some
+ * future event.
+ *
+ * If you have an input that uses `ng-model-options` to set up debounced updates or updates that
+ * depend on special events such as `blur`, there can be a period when the `$viewValue` is out of
+ * sync with the ngModel's `$modelValue`.
+ *
+ * In this case, you can use `$rollbackViewValue()` to manually cancel the debounced / future update
+ * and reset the input to the last committed view value.
+ *
+ * It is also possible that you run into difficulties if you try to update the ngModel's `$modelValue`
+ * programmatically before these debounced/future events have resolved/occurred, because AngularJS's
+ * dirty checking mechanism is not able to tell whether the model has actually changed or not.
+ *
+ * The `$rollbackViewValue()` method should be called before programmatically changing the model of an
+ * input which may have such events pending. This is important in order to make sure that the
+ * input field will be updated with the new model value and any pending operations are cancelled.
+ *
+ * @example
+ *
+ *
+ * angular.module('cancel-update-example', [])
+ *
+ * .controller('CancelUpdateController', ['$scope', function($scope) {
+ * $scope.model = {value1: '', value2: ''};
+ *
+ * $scope.setEmpty = function(e, value, rollback) {
+ * if (e.keyCode === 27) {
+ * e.preventDefault();
+ * if (rollback) {
+ * $scope.myForm[value].$rollbackViewValue();
+ * }
+ * $scope.model[value] = '';
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
Both of these inputs are only updated if they are blurred. Hitting escape should
+ * empty them. Follow these steps and observe the difference:
+ *
+ * Type something in the input. You will see that the model is not yet updated
+ * Press the Escape key.
+ *
+ * In the first example, nothing happens, because the model is already '', and no
+ * update is detected. If you blur the input, the model will be set to the current view.
+ *
+ * In the second example, the pending update is cancelled, and the input is set back
+ * to the last committed view value (''). Blurring the input does nothing.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
Without $rollbackViewValue():
+ *
+ * value1: "{{ model.value1 }}"
+ *
+ *
+ *
+ *
With $rollbackViewValue():
+ *
+ * value2: "{{ model.value2 }}"
+ *
+ *
+ *
+ *
+
+ div {
+ display: table-cell;
+ }
+ div:nth-child(1) {
+ padding-right: 30px;
+ }
+
+
+ *
+ */
+ $rollbackViewValue: function() {
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ this.$viewValue = this.$$lastCommittedViewValue;
+ this.$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.
+ */
+ $validate: function() {
+
+ // ignore $validate before model is initialized
+ if (isNumberNaN(this.$modelValue)) {
+ return;
+ }
+
+ var viewValue = this.$$lastCommittedViewValue;
+ // Note: we use the $$rawModelValue as $modelValue might have been
+ // set to undefined during a view -> model update that found validation
+ // errors. We can't parse the view here, since that could change
+ // the model although neither viewValue nor the model on the scope changed
+ var modelValue = this.$$rawModelValue;
+
+ var prevValid = this.$valid;
+ var prevModelValue = this.$modelValue;
+
+ var allowInvalid = this.$options.getOption('allowInvalid');
+
+ var that = this;
+ this.$$runValidators(modelValue, viewValue, function(allValid) {
+ // If there was no change in validity, don't update the model
+ // This prevents changing an invalid modelValue to undefined
+ if (!allowInvalid && prevValid !== allValid) {
+ // Note: Don't check this.$valid here, as we could have
+ // external validators (e.g. calculated on the server),
+ // that just call $setValidity and need the model value
+ // to calculate their validity.
+ that.$modelValue = allValid ? modelValue : undefined;
+
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
+ }
+ }
+ });
+ },
+
+ $$runValidators: function(modelValue, viewValue, doneCallback) {
+ this.$$currentValidationRunId++;
+ var localValidationRunId = this.$$currentValidationRunId;
+ var that = this;
+
+ // check parser error
+ if (!processParseErrors()) {
+ validationDone(false);
+ return;
+ }
+ if (!processSyncValidators()) {
+ validationDone(false);
+ return;
+ }
+ processAsyncValidators();
+
+ function processParseErrors() {
+ var errorKey = that.$$parserName;
+
+ if (isUndefined(that.$$parserValid)) {
+ setValidity(errorKey, null);
+ } else {
+ if (!that.$$parserValid) {
+ forEach(that.$validators, function(v, name) {
+ setValidity(name, null);
+ });
+ forEach(that.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ }
+
+ // Set the parse error last, to prevent unsetting it, should a $validators key == parserName
+ setValidity(errorKey, that.$$parserValid);
+ return that.$$parserValid;
+ }
+ return true;
+ }
+
+ function processSyncValidators() {
+ var syncValidatorsValid = true;
+ forEach(that.$validators, function(validator, name) {
+ var result = Boolean(validator(modelValue, viewValue));
+ syncValidatorsValid = syncValidatorsValid && result;
+ setValidity(name, result);
+ });
+ if (!syncValidatorsValid) {
+ forEach(that.$asyncValidators, function(v, name) {
+ setValidity(name, null);
+ });
+ return false;
+ }
+ return true;
+ }
+
+ function processAsyncValidators() {
+ var validatorPromises = [];
+ var allValid = true;
+ forEach(that.$asyncValidators, function(validator, name) {
+ var promise = validator(modelValue, viewValue);
+ if (!isPromiseLike(promise)) {
+ throw ngModelMinErr('nopromise',
+ 'Expected asynchronous validator to return a promise but got \'{0}\' instead.', promise);
+ }
+ setValidity(name, undefined);
+ validatorPromises.push(promise.then(function() {
+ setValidity(name, true);
+ }, function() {
+ allValid = false;
+ setValidity(name, false);
+ }));
+ });
+ if (!validatorPromises.length) {
+ validationDone(true);
+ } else {
+ that.$$q.all(validatorPromises).then(function() {
+ validationDone(allValid);
+ }, noop);
+ }
+ }
+
+ function setValidity(name, isValid) {
+ if (localValidationRunId === that.$$currentValidationRunId) {
+ that.$setValidity(name, isValid);
+ }
+ }
+
+ function validationDone(allValid) {
+ if (localValidationRunId === that.$$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.
+ */
+ $commitViewValue: function() {
+ var viewValue = this.$viewValue;
+
+ this.$$timeout.cancel(this.$$pendingDebounce);
+
+ // If the view value has not changed then we should just exit, except in the case where there is
+ // a native validator on the element. In this case the validation state may have changed even though
+ // the viewValue has stayed empty.
+ if (this.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !this.$$hasNativeValidators)) {
+ return;
+ }
+ this.$$updateEmptyClasses(viewValue);
+ this.$$lastCommittedViewValue = viewValue;
+
+ // change to dirty
+ if (this.$pristine) {
+ this.$setDirty();
+ }
+ this.$$parseAndValidate();
+ },
+
+ $$parseAndValidate: function() {
+ var viewValue = this.$$lastCommittedViewValue;
+ var modelValue = viewValue;
+ var that = this;
+
+ this.$$parserValid = isUndefined(modelValue) ? undefined : true;
+
+ // Reset any previous parse error
+ this.$setValidity(this.$$parserName, null);
+ this.$$parserName = 'parse';
+
+ if (this.$$parserValid) {
+ for (var i = 0; i < this.$parsers.length; i++) {
+ modelValue = this.$parsers[i](modelValue);
+ if (isUndefined(modelValue)) {
+ this.$$parserValid = false;
+ break;
+ }
+ }
+ }
+ if (isNumberNaN(this.$modelValue)) {
+ // this.$modelValue has not been touched yet...
+ this.$modelValue = this.$$ngModelGet(this.$$scope);
+ }
+ var prevModelValue = this.$modelValue;
+ var allowInvalid = this.$options.getOption('allowInvalid');
+ this.$$rawModelValue = modelValue;
+
+ if (allowInvalid) {
+ this.$modelValue = modelValue;
+ writeToModelIfNeeded();
+ }
+
+ // Pass the $$lastCommittedViewValue here, because the cached viewValue might be out of date.
+ // This can happen if e.g. $setViewValue is called from inside a parser
+ this.$$runValidators(modelValue, this.$$lastCommittedViewValue, function(allValid) {
+ if (!allowInvalid) {
+ // Note: Don't check this.$valid here, as we could have
+ // external validators (e.g. calculated on the server),
+ // that just call $setValidity and need the model value
+ // to calculate their validity.
+ that.$modelValue = allValid ? modelValue : undefined;
+ writeToModelIfNeeded();
+ }
+ });
+
+ function writeToModelIfNeeded() {
+ if (that.$modelValue !== prevModelValue) {
+ that.$$writeModelToScope();
+ }
+ }
+ },
+
+ $$writeModelToScope: function() {
+ this.$$ngModelSet(this.$$scope, this.$modelValue);
+ forEach(this.$viewChangeListeners, function(listener) {
+ try {
+ listener();
+ } catch (e) {
+ // eslint-disable-next-line no-invalid-this
+ this.$$exceptionHandler(e);
+ }
+ }, this);
+ },
+
+ /**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setViewValue
+ *
+ * @description
+ * Update the view value.
+ *
+ * This method should be called when a control wants to change the view value; typically,
+ * this is done from within a DOM event handler. For example, the {@link ng.directive:input input}
+ * directive calls it when the value of the input changes and {@link ng.directive:select select}
+ * calls it when an option is selected.
+ *
+ * When `$setViewValue` is called, the new `value` will be staged for committing through the `$parsers`
+ * and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
+ * value is sent directly for processing through the `$parsers` pipeline. After this, the `$validators` and
+ * `$asyncValidators` are called and the value is applied to `$modelValue`.
+ * Finally, the value is set to the **expression** specified in the `ng-model` attribute and
+ * all the registered change listeners, in the `$viewChangeListeners` list are called.
+ *
+ * In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
+ * and the `default` trigger is not listed, all those actions will remain pending until one of the
+ * `updateOn` events is triggered on the DOM element.
+ * All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
+ * directive is used with a custom debounce for this particular event.
+ * Note that a `$digest` is only triggered once the `updateOn` events are fired, or if `debounce`
+ * is specified, once the timer runs out.
+ *
+ * When used with standard inputs, the view value will always be a string (which is in some cases
+ * parsed into another type, such as a `Date` object for `input[date]`.)
+ * However, custom controls might also pass objects to this method. In this case, we should make
+ * a copy of the object before passing it to `$setViewValue`. This is because `ngModel` does not
+ * perform a deep watch of objects, it only looks for a change of identity. If you only change
+ * the property of the object then ngModel will not realize that the object has changed and
+ * will not invoke the `$parsers` and `$validators` pipelines. For this reason, you should
+ * not change properties of the copy once it has been passed to `$setViewValue`.
+ * Otherwise you may cause the model value on the scope to change incorrectly.
+ *
+ *
+ * In any case, the value passed to the method should always reflect the current value
+ * of the control. For example, if you are calling `$setViewValue` for an input element,
+ * you should pass the input DOM value. Otherwise, the control and the scope model become
+ * out of sync. It's also important to note that `$setViewValue` does not call `$render` or change
+ * the control's DOM value in any way. If we want to change the control's DOM value
+ * programmatically, we should update the `ngModel` scope expression. Its new value will be
+ * picked up by the model controller, which will run it through the `$formatters`, `$render` it
+ * to update the DOM, and finally call `$validate` on it.
+ *
+ *
+ * @param {*} value value from the view.
+ * @param {string} trigger Event that triggered the update.
+ */
+ $setViewValue: function(value, trigger) {
+ this.$viewValue = value;
+ if (this.$options.getOption('updateOnDefault')) {
+ this.$$debounceViewValueCommit(trigger);
+ }
+ },
+
+ $$debounceViewValueCommit: function(trigger) {
+ var debounceDelay = this.$options.getOption('debounce');
+
+ if (isNumber(debounceDelay[trigger])) {
+ debounceDelay = debounceDelay[trigger];
+ } else if (isNumber(debounceDelay['default']) &&
+ this.$options.getOption('updateOn').indexOf(trigger) === -1
+ ) {
+ debounceDelay = debounceDelay['default'];
+ } else if (isNumber(debounceDelay['*'])) {
+ debounceDelay = debounceDelay['*'];
+ }
+
+ this.$$timeout.cancel(this.$$pendingDebounce);
+ var that = this;
+ if (debounceDelay > 0) { // this fails if debounceDelay is an object
+ this.$$pendingDebounce = this.$$timeout(function() {
+ that.$commitViewValue();
+ }, debounceDelay);
+ } else if (this.$$rootScope.$$phase) {
+ this.$commitViewValue();
+ } else {
+ this.$$scope.$apply(function() {
+ that.$commitViewValue();
+ });
+ }
+ },
+
+ /**
+ * @ngdoc method
+ *
+ * @name ngModel.NgModelController#$overrideModelOptions
+ *
+ * @description
+ *
+ * Override the current model options settings programmatically.
+ *
+ * The previous `ModelOptions` value will not be modified. Instead, a
+ * new `ModelOptions` object will inherit from the previous one overriding
+ * or inheriting settings that are defined in the given parameter.
+ *
+ * See {@link ngModelOptions} for information about what options can be specified
+ * and how model option inheritance works.
+ *
+ *
+ * **Note:** this function only affects the options set on the `ngModelController`,
+ * and not the options on the {@link ngModelOptions} directive from which they might have been
+ * obtained initially.
+ *
+ *
+ *
+ * **Note:** it is not possible to override the `getterSetter` option.
+ *
+ *
+ * @param {Object} options a hash of settings to override the previous options
+ *
+ */
+ $overrideModelOptions: function(options) {
+ this.$options = this.$options.createChild(options);
+ this.$$setUpdateOnEvents();
+ },
+
+ /**
+ * @ngdoc method
+ *
+ * @name ngModel.NgModelController#$processModelValue
+
+ * @description
+ *
+ * Runs the model -> view pipeline on the current
+ * {@link ngModel.NgModelController#$modelValue $modelValue}.
+ *
+ * The following actions are performed by this method:
+ *
+ * - the `$modelValue` is run through the {@link ngModel.NgModelController#$formatters $formatters}
+ * and the result is set to the {@link ngModel.NgModelController#$viewValue $viewValue}
+ * - the `ng-empty` or `ng-not-empty` class is set on the element
+ * - if the `$viewValue` has changed:
+ * - {@link ngModel.NgModelController#$render $render} is called on the control
+ * - the {@link ngModel.NgModelController#$validators $validators} are run and
+ * the validation status is set.
+ *
+ * This method is called by ngModel internally when the bound scope value changes.
+ * Application developers usually do not have to call this function themselves.
+ *
+ * This function can be used when the `$viewValue` or the rendered DOM value are not correctly
+ * formatted and the `$modelValue` must be run through the `$formatters` again.
+ *
+ * @example
+ * Consider a text input with an autocomplete list (for fruit), where the items are
+ * objects with a name and an id.
+ * A user enters `ap` and then selects `Apricot` from the list.
+ * Based on this, the autocomplete widget will call `$setViewValue({name: 'Apricot', id: 443})`,
+ * but the rendered value will still be `ap`.
+ * The widget can then call `ctrl.$processModelValue()` to run the model -> view
+ * pipeline again, which formats the object to the string `Apricot`,
+ * then updates the `$viewValue`, and finally renders it in the DOM.
+ *
+ *
+
+
+
+ Search Fruit:
+
+
+
+ Model:
+
{{selectedFruit | json}}
+
+
+
+
+ angular.module('inputExample', [])
+ .controller('inputController', function($scope) {
+ $scope.items = [
+ {name: 'Apricot', id: 443},
+ {name: 'Clementine', id: 972},
+ {name: 'Durian', id: 169},
+ {name: 'Jackfruit', id: 982},
+ {name: 'Strawberry', id: 863}
+ ];
+ })
+ .component('basicAutocomplete', {
+ bindings: {
+ items: '<',
+ onSelect: '&'
+ },
+ templateUrl: 'autocomplete.html',
+ controller: function($element, $scope) {
+ var that = this;
+ var ngModel;
+
+ that.$postLink = function() {
+ ngModel = $element.find('input').controller('ngModel');
+
+ ngModel.$formatters.push(function(value) {
+ return (value && value.name) || value;
+ });
+
+ ngModel.$parsers.push(function(value) {
+ var match = value;
+ for (var i = 0; i < that.items.length; i++) {
+ if (that.items[i].name === value) {
+ match = that.items[i];
+ break;
+ }
+ }
+
+ return match;
+ });
+ };
+
+ that.selectItem = function(item) {
+ ngModel.$setViewValue(item);
+ ngModel.$processModelValue();
+ that.onSelect({item: item});
+ };
+ }
+ });
+
+
+
+
+ *
+ *
+ */
+ $processModelValue: function() {
+ var viewValue = this.$$format();
+
+ if (this.$viewValue !== viewValue) {
+ this.$$updateEmptyClasses(viewValue);
+ this.$viewValue = this.$$lastCommittedViewValue = viewValue;
+ this.$render();
+ // It is possible that model and view value have been updated during render
+ this.$$runValidators(this.$modelValue, this.$viewValue, noop);
+ }
+ },
+
+ /**
+ * This method is called internally to run the $formatters on the $modelValue
+ */
+ $$format: function() {
+ var formatters = this.$formatters,
+ idx = formatters.length;
+
+ var viewValue = this.$modelValue;
+ while (idx--) {
+ viewValue = formatters[idx](viewValue);
+ }
+
+ return viewValue;
+ },
+
+ /**
+ * This method is called internally when the bound scope value changes.
+ */
+ $$setModelValue: function(modelValue) {
+ this.$modelValue = this.$$rawModelValue = modelValue;
+ this.$$parserValid = undefined;
+ this.$processModelValue();
+ },
+
+ $$setUpdateOnEvents: function() {
+ if (this.$$updateEvents) {
+ this.$$element.off(this.$$updateEvents, this.$$updateEventHandler);
+ }
+
+ this.$$updateEvents = this.$options.getOption('updateOn');
+ if (this.$$updateEvents) {
+ this.$$element.on(this.$$updateEvents, this.$$updateEventHandler);
+ }
+ },
+
+ $$updateEventHandler: function(ev) {
+ this.$$debounceViewValueCommit(ev && ev.type);
+ }
+};
+
+function setupModelWatcher(ctrl) {
+ // model -> value
+ // Note: we cannot use a normal scope.$watch as we want to detect the following:
+ // 1. scope value is 'a'
+ // 2. user enters 'b'
+ // 3. ng-change kicks in and reverts scope value to 'a'
+ // -> scope value did not change since the last digest as
+ // ng-change executes in apply phase
+ // 4. view should be changed back to 'a'
+ ctrl.$$scope.$watch(function ngModelWatch(scope) {
+ var modelValue = ctrl.$$ngModelGet(scope);
+
+ // if scope model value and ngModel value are out of sync
+ // This cannot be moved to the action function, because it would not catch the
+ // case where the model is changed in the ngChange function or the model setter
+ if (modelValue !== ctrl.$modelValue &&
+ // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
+ // eslint-disable-next-line no-self-compare
+ (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
+ ) {
+ ctrl.$$setModelValue(modelValue);
+ }
+
+ return modelValue;
+ });
+}
+
+/**
+ * @ngdoc method
+ * @name ngModel.NgModelController#$setValidity
+ *
+ * @description
+ * Change the validity state, and notify the form.
+ *
+ * This method can be called within $parsers/$formatters or a custom validation implementation.
+ * However, in most cases it should be sufficient to use the `ngModel.$validators` and
+ * `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
+ *
+ * @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
+ * to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
+ * (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
+ * The `validationErrorKey` should be in camelCase and will get converted into dash-case
+ * for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
+ * classes and can be bound to as `{{ someForm.someControl.$error.myError }}`.
+ * @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
+ * or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
+ * Skipped is used by AngularJS when validators do not run because of parse errors and
+ * when `$asyncValidators` do not run because any of the `$validators` failed.
+ */
+addSetValidityMethod({
+ clazz: NgModelController,
+ set: function(object, property) {
+ object[property] = true;
+ },
+ unset: function(object, property) {
+ delete object[property];
+ }
+});
+
/**
* @ngdoc directive
- * @name ng.directive:ngPluralize
+ * @name ngModel
+ * @restrict A
+ * @priority 1
+ * @param {expression} ngModel assignable {@link guide/expression Expression} to bind to.
+ *
+ * @description
+ * The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
+ * property on the scope using {@link ngModel.NgModelController NgModelController},
+ * which is created and exposed by this directive.
+ *
+ * `ngModel` is responsible for:
+ *
+ * - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
+ * require.
+ * - Providing validation behavior (i.e. required, number, email, url).
+ * - Keeping the state of the control (valid/invalid, dirty/pristine, touched/untouched, validation errors).
+ * - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`, `ng-touched`,
+ * `ng-untouched`, `ng-empty`, `ng-not-empty`) including animations.
+ * - Registering the control with its parent {@link ng.directive:form form}.
+ *
+ * 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.
+ *
+ * For best practices on using `ngModel`, see:
+ *
+ * - [Understanding Scopes](https://github.com/angular/angular.js/wiki/Understanding-Scopes)
+ *
+ * For basic examples, how to use `ngModel`, see:
+ *
+ * - {@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}
+ *
+ * ## Complex Models (objects or collections)
+ *
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding inputs to models that are objects (e.g. `Date`) or collections (e.g. arrays). If only properties of the
+ * object or collection change, `ngModel` will not be notified and so the input will not be re-rendered.
+ *
+ * The model must be assigned an entirely new object or collection before a re-rendering will occur.
+ *
+ * Some directives have options that will cause them to use a custom `$watchCollection` on the model expression
+ * - for example, `ngOptions` will do so when a `track by` clause is included in the comprehension expression or
+ * if the select is given the `multiple` attribute.
+ *
+ * The `$watchCollection()` method only does a shallow comparison, meaning that changing properties deeper than the
+ * first level of the object (or only changing the properties of an item in the collection if it's an array) will still
+ * not trigger a re-rendering of the model.
+ *
+ * ## CSS classes
+ * The following CSS classes are added and removed on the associated input/select/textarea element
+ * depending on the validity of the model.
+ *
+ * - `ng-valid`: the model is valid
+ * - `ng-invalid`: the model is invalid
+ * - `ng-valid-[key]`: for each valid key added by `$setValidity`
+ * - `ng-invalid-[key]`: for each invalid key added by `$setValidity`
+ * - `ng-pristine`: the control hasn't been interacted with yet
+ * - `ng-dirty`: the control has been interacted with
+ * - `ng-touched`: the control has been blurred
+ * - `ng-untouched`: the control hasn't been blurred
+ * - `ng-pending`: any `$asyncValidators` are unfulfilled
+ * - `ng-empty`: the view does not contain a value or the value is deemed "empty", as defined
+ * by the {@link ngModel.NgModelController#$isEmpty} method
+ * - `ng-not-empty`: the view contains a non-empty value
+ *
+ * Keep in mind that ngAnimate can detect each of these classes when added and removed.
+ *
+ * @animations
+ * Animations within models are triggered when any of the associated CSS classes are added and removed
+ * on the input element which is attached to the model. These classes include: `.ng-pristine`, `.ng-dirty`,
+ * `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
+ * The animations that are triggered within ngModel are similar to how they work in ngClass and
+ * animations can be hooked into using CSS transitions, keyframes as well as JS animations.
+ *
+ * The following example shows a simple way to utilize CSS transitions to style an input element
+ * that has been rendered as invalid after it has been validated:
+ *
+ *
+ * //be sure to include ngAnimate as a module to hook into more
+ * //advanced animations
+ * .my-input {
+ * transition:0.5s linear all;
+ * background: white;
+ * }
+ * .my-input.ng-invalid {
+ * background: red;
+ * color:white;
+ * }
+ *
+ *
+ * @example
+ * ### Basic Usage
+ *
+
+
+
+
+ Update input to see transitions when valid/invalid.
+ Integer is a valid value.
+
+
+
+
+
+ *
+ *
+ * @example
+ * ### Binding to a getter/setter
+ *
+ * Sometimes it's helpful to bind `ngModel` to a getter/setter function. A getter/setter is a
+ * function that returns a representation of the model when called with zero arguments, and sets
+ * the internal state of a model when called with an argument. It's sometimes useful to use this
+ * for models that have an internal representation that's different from what the model exposes
+ * to the view.
+ *
+ *
+ * **Best Practice:** It's best to keep getters fast because AngularJS is likely to call them more
+ * frequently than other parts of your code.
+ *
+ *
+ * You use this behavior by adding `ng-model-options="{ getterSetter: true }"` to an element that
+ * has `ng-model` attached to it. You can also add `ng-model-options="{ getterSetter: true }"` to
+ * a ``, which will enable this behavior for all ` `s within it. See
+ * {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
+ *
+ * The following example shows how to use `ngModel` with a getter/setter:
+ *
+ * @example
+ *
+
+
+
+ Name:
+
+
+
+
user.name =
+
+
+
+ angular.module('getterSetterExample', [])
+ .controller('ExampleController', ['$scope', function($scope) {
+ var _name = 'Brian';
+ $scope.user = {
+ name: function(newName) {
+ // Note that newName can be undefined for two reasons:
+ // 1. Because it is called as a getter and thus called with no arguments
+ // 2. Because the property should actually be set to undefined. This happens e.g. if the
+ // input is invalid
+ return arguments.length ? (_name = newName) : _name;
+ }
+ };
+ }]);
+
+ *
+ */
+var ngModelDirective = ['$rootScope', function($rootScope) {
+ return {
+ restrict: 'A',
+ require: ['ngModel', '^?form', '^?ngModelOptions'],
+ controller: NgModelController,
+ // Prelink needs to run before any input directive
+ // so that we can set the NgModelOptions in NgModelController
+ // before anyone else uses it.
+ priority: 1,
+ compile: function ngModelCompile(element) {
+ // Setup initial state of the control
+ element.addClass(PRISTINE_CLASS).addClass(UNTOUCHED_CLASS).addClass(VALID_CLASS);
+
+ return {
+ pre: function ngModelPreLink(scope, element, attr, ctrls) {
+ var modelCtrl = ctrls[0],
+ formCtrl = ctrls[1] || modelCtrl.$$parentForm,
+ optionsCtrl = ctrls[2];
+
+ if (optionsCtrl) {
+ modelCtrl.$options = optionsCtrl.$options;
+ }
+
+ modelCtrl.$$initGetterSetters();
+
+ // notify others, especially parent forms
+ formCtrl.$addControl(modelCtrl);
+
+ attr.$observe('name', function(newValue) {
+ if (modelCtrl.$name !== newValue) {
+ modelCtrl.$$parentForm.$$renameControl(modelCtrl, newValue);
+ }
+ });
+
+ scope.$on('$destroy', function() {
+ modelCtrl.$$parentForm.$removeControl(modelCtrl);
+ });
+ },
+ post: function ngModelPostLink(scope, element, attr, ctrls) {
+ var modelCtrl = ctrls[0];
+ modelCtrl.$$setUpdateOnEvents();
+
+ function setTouched() {
+ modelCtrl.$setTouched();
+ }
+
+ element.on('blur', function() {
+ if (modelCtrl.$touched) return;
+
+ if ($rootScope.$$phase) {
+ scope.$evalAsync(setTouched);
+ } else {
+ scope.$apply(setTouched);
+ }
+ });
+ }
+ };
+ }
+ };
+}];
+
+/* exported defaultModelOptions */
+var defaultModelOptions;
+var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
+
+/**
+ * @ngdoc type
+ * @name ModelOptions
+ * @description
+ * A container for the options set by the {@link ngModelOptions} directive
+ */
+function ModelOptions(options) {
+ this.$$options = options;
+}
+
+ModelOptions.prototype = {
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#getOption
+ * @param {string} name the name of the option to retrieve
+ * @returns {*} the value of the option
+ * @description
+ * Returns the value of the given option
+ */
+ getOption: function(name) {
+ return this.$$options[name];
+ },
+
+ /**
+ * @ngdoc method
+ * @name ModelOptions#createChild
+ * @param {Object} options a hash of options for the new child that will override the parent's options
+ * @return {ModelOptions} a new `ModelOptions` object initialized with the given options.
+ */
+ createChild: function(options) {
+ var inheritAll = false;
+
+ // make a shallow copy
+ options = extend({}, options);
+
+ // Inherit options from the parent if specified by the value `"$inherit"`
+ forEach(options, /** @this */ function(option, key) {
+ if (option === '$inherit') {
+ if (key === '*') {
+ inheritAll = true;
+ } else {
+ options[key] = this.$$options[key];
+ // `updateOn` is special so we must also inherit the `updateOnDefault` option
+ if (key === 'updateOn') {
+ options.updateOnDefault = this.$$options.updateOnDefault;
+ }
+ }
+ } else {
+ if (key === 'updateOn') {
+ // If the `updateOn` property contains the `default` event then we have to remove
+ // it from the event list and set the `updateOnDefault` flag.
+ options.updateOnDefault = false;
+ options[key] = trim(option.replace(DEFAULT_REGEXP, function() {
+ options.updateOnDefault = true;
+ return ' ';
+ }));
+ }
+ }
+ }, this);
+
+ if (inheritAll) {
+ // We have a property of the form: `"*": "$inherit"`
+ delete options['*'];
+ defaults(options, this.$$options);
+ }
+
+ // Finally add in any missing defaults
+ defaults(options, defaultModelOptions.$$options);
+
+ return new ModelOptions(options);
+ }
+};
+
+
+defaultModelOptions = new ModelOptions({
+ updateOn: '',
+ updateOnDefault: true,
+ debounce: 0,
+ getterSetter: false,
+ allowInvalid: false,
+ timezone: null
+});
+
+
+/**
+ * @ngdoc directive
+ * @name ngModelOptions
+ * @restrict A
+ * @priority 10
+ *
+ * @description
+ * This directive allows you to modify the behaviour of {@link ngModel} directives within your
+ * application. You can specify an `ngModelOptions` directive on any element. All {@link ngModel}
+ * directives will use the options of their nearest `ngModelOptions` ancestor.
+ *
+ * The `ngModelOptions` settings are found by evaluating the value of the attribute directive as
+ * an AngularJS expression. This expression should evaluate to an object, whose properties contain
+ * the settings. For example: `
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 0 }
+ * ```
+ *
+ * Notice that the `debounce` setting was not inherited and used the default value instead.
+ *
+ * You can specify that all undefined settings are automatically inherited from an ancestor by
+ * including a property with key of `"*"` and value of `"$inherit"`.
+ *
+ * For example given the following fragment of HTML
+ *
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * the `input` element will have the following settings
+ *
+ * ```js
+ * { allowInvalid: true, updateOn: 'default', debounce: 200 }
+ * ```
+ *
+ * Notice that the `debounce` setting now inherits the value from the outer `` element.
+ *
+ * If you are creating a reusable component then you should be careful when using `"*": "$inherit"`
+ * since you may inadvertently inherit a setting in the future that changes the behavior of your component.
+ *
+ *
+ * ## Triggering and debouncing model updates
+ *
+ * The `updateOn` and `debounce` properties allow you to specify a custom list of events that will
+ * trigger a model update and/or a debouncing delay so that the actual update only takes place when
+ * a timer expires; this timer will be reset after another change takes place.
+ *
+ * Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
+ * be different from the value in the actual model. This means that if you update the model you
+ * should also invoke {@link ngModel.NgModelController#$rollbackViewValue} on the relevant input field in
+ * order to make sure it is synchronized with the model and that any debounced action is canceled.
+ *
+ * The easiest way to reference the control's {@link ngModel.NgModelController#$rollbackViewValue}
+ * 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.
+ *
+ * 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.
+ *
+ * ### Overriding immediate updates
+ *
+ * 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.
+ *
+ *
+ *
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say', data: '' };
+ *
+ * $scope.cancel = function(e) {
+ * if (e.keyCode === 27) {
+ * $scope.userForm.userName.$rollbackViewValue();
+ * }
+ * };
+ * }]);
+ *
+ *
+ * var model = element(by.binding('user.name'));
+ * var input = element(by.model('user.name'));
+ * var other = element(by.model('user.data'));
+ *
+ * it('should allow custom events', function() {
+ * input.sendKeys(' hello');
+ * input.click();
+ * expect(model.getText()).toEqual('say');
+ * other.click();
+ * expect(model.getText()).toEqual('say hello');
+ * });
+ *
+ * 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');
+ * });
+ *
+ *
+ *
+ * ### Debouncing updates
+ *
+ * The next example shows how to debounce model changes. Model will be updated only 1 sec after last change.
+ * If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
+ *
+ *
+ *
+ *
+ *
+ * Name:
+ *
+ * Clear
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('optionsExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.user = { name: 'say' };
+ * }]);
+ *
+ *
+ *
+ * ### Default events, extra triggers, and catch-all debounce values
+ *
+ * This example shows the relationship between "default" update events and
+ * additional `updateOn` triggers.
+ *
+ * `default` events are those that are bound to the control, and when fired, update the `$viewValue`
+ * via {@link ngModel.NgModelController#$setViewValue $setViewValue}. Every event that is not listed
+ * in `updateOn` is considered a "default" event, since different control types have different
+ * default events.
+ *
+ * The control in this example updates by "default", "click", and "blur", with different `debounce`
+ * values. You can see that "click" doesn't have an individual `debounce` value -
+ * therefore it uses the `*` debounce value.
+ *
+ * There is also a button that calls {@link ngModel.NgModelController#$setViewValue $setViewValue}
+ * directly with a "custom" event. Since "custom" is not defined in the `updateOn` list,
+ * it is considered a "default" event and will update the
+ * control if "default" is defined in `updateOn`, and will receive the "default" debounce value.
+ * Note that this is just to illustrate how custom controls would possibly call `$setViewValue`.
+ *
+ * You can change the `updateOn` and `debounce` configuration to test different scenarios. This
+ * is done with {@link ngModel.NgModelController#$overrideModelOptions $overrideModelOptions}.
+ *
+
+
+
+
+
+ angular.module('optionsExample', [])
+ .component('modelUpdateDemo', {
+ templateUrl: 'template.html',
+ controller: function() {
+ this.name = 'Chinua';
+
+ this.options = {
+ updateOn: 'default blur click',
+ debounce: {
+ default: 2000,
+ blur: 0,
+ '*': 1000
+ }
+ };
+
+ this.updateEvents = function() {
+ var eventList = this.options.updateOn.split(' ');
+ eventList.push('*');
+ var events = {};
+
+ for (var i = 0; i < eventList.length; i++) {
+ events[eventList[i]] = this.options.debounce[eventList[i]];
+ }
+
+ this.events = events;
+ };
+
+ this.updateOptions = function() {
+ var options = angular.extend(this.options, {
+ updateOn: Object.keys(this.events).join(' ').replace('*', ''),
+ debounce: this.events
+ });
+
+ this.form.input.$overrideModelOptions(options);
+ };
+
+ // Initialize the event form
+ this.updateEvents();
+ }
+ });
+
+
+
+ Input:
+
+ Model: {{$ctrl.name}}
+
+ Trigger setViewValue with 'some value' and 'custom' event
+
+
+
+ updateOn
+
+
+
+
+
+
+
+
+
+ *
+ *
+ * ## Model updates and validation
+ *
+ * The default behaviour in `ngModel` is that the model value is set to `undefined` when the
+ * validation determines that the value is invalid. By setting the `allowInvalid` property to true,
+ * the model will still be updated even if the value is invalid.
+ *
+ *
+ * ## Connecting to the scope
+ *
+ * By setting the `getterSetter` property to true you are telling ngModel that the `ngModel` expression
+ * on the scope refers to a "getter/setter" function rather than the value itself.
+ *
+ * The following example shows how to bind to getter/setters:
+ *
+ *
+ *
+ *
+ *
+ *
+ * Name:
+ *
+ *
+ *
+ *
user.name =
+ *
+ *
+ *
+ * angular.module('getterSetterExample', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * var _name = 'Brian';
+ * $scope.user = {
+ * name: function(newName) {
+ * return angular.isDefined(newName) ? (_name = newName) : _name;
+ * }
+ * };
+ * }]);
+ *
+ *
+ *
+ *
+ * ## Programmatically changing options
+ *
+ * The `ngModelOptions` expression is only evaluated once when the directive is linked; it is not
+ * watched for changes. However, it is possible to override the options on a single
+ * {@link ngModel.NgModelController} instance with
+ * {@link ngModel.NgModelController#$overrideModelOptions `NgModelController#$overrideModelOptions()`}.
+ * See also the example for
+ * {@link ngModelOptions#default-events-extra-triggers-and-catch-all-debounce-values
+ * Default events, extra triggers, and catch-all debounce values}.
+ *
+ *
+ * ## Specifying timezones
+ *
+ * You can specify the timezone that date/time input directives expect by providing its name in the
+ * `timezone` property.
+ *
+ *
+ * ## Formatting the value of `time` and `datetime-local`
+ *
+ * With the options `timeSecondsFormat` and `timeStripZeroSeconds` it is possible to adjust the value
+ * that is displayed in the control. Note that browsers may apply their own formatting
+ * in the user interface.
+ *
+
+
+
+
+
+ angular.module('timeExample', [])
+ .component('timeExample', {
+ templateUrl: 'timeExample.html',
+ controller: function() {
+ this.time = new Date(1970, 0, 1, 14, 57, 0);
+
+ this.options = {
+ timeSecondsFormat: 'ss',
+ timeStripZeroSeconds: true
+ };
+
+ this.optionChange = function() {
+ this.timeForm.timeFormatted.$overrideModelOptions(this.options);
+ this.time = new Date(this.time);
+ };
+ }
+ });
+
+
+
+ Default :
+
+ With options :
+
+
+
+ Options:
+ timeSecondsFormat
:
+
+
+ timeStripZeroSeconds
:
+
+
+
+ *
+ *
+ * @param {Object} ngModelOptions options to apply to {@link ngModel} directives on this element and
+ * and its descendents.
+ *
+ * **General options**:
+ *
+ * - `updateOn`: string specifying which event should the input be bound to. You can set several
+ * events using an space delimited list. There is a special event called `default` that
+ * matches the default events belonging to the control. These are the events that are bound to
+ * the control, and when fired, update the `$viewValue` via `$setViewValue`.
+ *
+ * `ngModelOptions` considers every event that is not listed in `updateOn` a "default" event,
+ * since different control types use different default events.
+ *
+ * See also the section {@link ngModelOptions#triggering-and-debouncing-model-updates
+ * Triggering and debouncing model updates}.
+ *
+ * - `debounce`: integer value which contains the debounce model update value in milliseconds. A
+ * value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
+ * custom value for each event. For example:
+ * ```
+ * ng-model-options="{
+ * updateOn: 'default blur',
+ * debounce: { 'default': 500, 'blur': 0 }
+ * }"
+ * ```
+ * You can use the `*` key to specify a debounce value that applies to all events that are not
+ * specifically listed. In the following example, `mouseup` would have a debounce delay of 1000:
+ * ```
+ * ng-model-options="{
+ * updateOn: 'default blur mouseup',
+ * debounce: { 'default': 500, 'blur': 0, '*': 1000 }
+ * }"
+ * ```
+ * - `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.
+ *
+ *
+ * **Input-type specific options**:
+ *
+ * - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
+ * `
`, `
`, ... . It understands UTC/GMT and the
+ * continental US time zone abbreviations, but for general use, use a time zone offset, for
+ * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
+ * If not specified, the timezone of the browser will be used.
+ * Note that changing the timezone will have no effect on the current date, and is only applied after
+ * the next input / model change.
+ *
+ * - `timeSecondsFormat`: Defines if the `time` and `datetime-local` types should show seconds and
+ * milliseconds. The option follows the format string of {@link date date filter}.
+ * By default, the options is `undefined` which is equal to `'ss.sss'` (seconds and milliseconds).
+ * The other options are `'ss'` (strips milliseconds), and `''` (empty string), which strips both
+ * seconds and milliseconds.
+ * Note that browsers that support `time` and `datetime-local` require the hour and minutes
+ * part of the time string, and may show the value differently in the user interface.
+ * {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
+ *
+ * - `timeStripZeroSeconds`: Defines if the `time` and `datetime-local` types should strip the
+ * seconds and milliseconds from the formatted value if they are zero. This option is applied
+ * after `timeSecondsFormat`.
+ * This option can be used to make the formatting consistent over different browsers, as some
+ * browsers with support for `time` will natively hide the milliseconds and
+ * seconds if they are zero, but others won't, and browsers that don't implement these input
+ * types will always show the full string.
+ * {@link ngModelOptions#formatting-the-value-of-time-and-datetime-local- See the example}.
+ *
+ */
+var ngModelOptionsDirective = function() {
+ NgModelOptionsController.$inject = ['$attrs', '$scope'];
+ function NgModelOptionsController($attrs, $scope) {
+ this.$$attrs = $attrs;
+ this.$$scope = $scope;
+ }
+ NgModelOptionsController.prototype = {
+ $onInit: function() {
+ var parentOptions = this.parentCtrl ? this.parentCtrl.$options : defaultModelOptions;
+ var modelOptionsDefinition = this.$$scope.$eval(this.$$attrs.ngModelOptions);
+
+ this.$options = parentOptions.createChild(modelOptionsDefinition);
+ }
+ };
+
+ return {
+ restrict: 'A',
+ // ngModelOptions needs to run before ngModel and input directives
+ priority: 10,
+ require: {parentCtrl: '?^^ngModelOptions'},
+ bindToController: true,
+ controller: NgModelOptionsController
+ };
+};
+
+
+// shallow copy over values from `src` that are not already specified on `dst`
+function defaults(dst, src) {
+ forEach(src, function(value, key) {
+ if (!isDefined(dst[key])) {
+ dst[key] = value;
+ }
+ });
+}
+
+/**
+ * @ngdoc directive
+ * @name ngNonBindable
+ * @restrict AC
+ * @priority 1000
+ * @element ANY
+ *
+ * @description
+ * The `ngNonBindable` directive tells AngularJS not to compile or bind the contents of the current
+ * DOM element, including directives on the element itself that have a lower priority than
+ * `ngNonBindable`. This is useful if the element contains what appears to be AngularJS directives
+ * and bindings but which should be ignored by AngularJS. This could be the case if you have a site
+ * that displays snippets of code, for instance.
+ *
+ * @example
+ * In this example there are two locations where a simple interpolation binding (`{{}}`) is present,
+ * but the one wrapped in `ngNonBindable` is left alone.
+ *
+
+
+ Normal: {{1 + 2}}
+ Ignored: {{1 + 2}}
+
+
+ it('should check ng-non-bindable', function() {
+ expect(element(by.binding('1 + 2')).getText()).toContain('3');
+ expect(element.all(by.css('div')).last().getText()).toMatch(/1 \+ 2/);
+ });
+
+
+ */
+var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
+
+/* exported ngOptionsDirective */
+
+/* global jqLiteRemove */
+
+var ngOptionsMinErr = minErr('ngOptions');
+
+/**
+ * @ngdoc directive
+ * @name ngOptions
+ * @restrict A
+ *
+ * @description
+ *
+ * The `ngOptions` attribute can be used to dynamically generate a list of `
`
+ * elements for the `` element using the array or object obtained by evaluating the
+ * `ngOptions` comprehension expression.
+ *
+ * In many cases, {@link ng.directive:ngRepeat ngRepeat} can be used on `` elements instead of
+ * `ngOptions` to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the ``'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
+ *
+ * When an item in the `` menu is selected, the array element or object property
+ * represented by the selected option will be bound to the model identified by the `ngModel`
+ * directive.
+ *
+ * Optionally, a single hard-coded `` element, with the value set to an empty string, can
+ * be nested into the `` element. This element will then represent the `null` or "not selected"
+ * option. See example below for demonstration.
+ *
+ * ## Complex Models (objects or collections)
+ *
+ * By default, `ngModel` watches the model by reference, not value. This is important to know when
+ * binding the select to a model that is an object or a collection.
+ *
+ * One issue occurs if you want to preselect an option. For example, if you set
+ * the model to an object that is equal to an object in your collection, `ngOptions` won't be able to set the selection,
+ * because the objects are not identical. So by default, you should always reference the item in your collection
+ * for preselections, e.g.: `$scope.selected = $scope.collection[3]`.
+ *
+ * Another solution is to use a `track by` clause, because then `ngOptions` will track the identity
+ * of the item not by reference, but by the result of the `track by` expression. For example, if your
+ * collection items have an id property, you would `track by item.id`.
+ *
+ * A different issue with objects or collections is that ngModel won't detect if an object property or
+ * a collection item changes. For that reason, `ngOptions` additionally watches the model using
+ * `$watchCollection`, when the expression contains a `track by` clause or the the select has the `multiple` attribute.
+ * This allows ngOptions to trigger a re-rendering of the options even if the actual object/collection
+ * has not changed identity, but only a property on the object or an item in the collection changes.
+ *
+ * Note that `$watchCollection` does a shallow comparison of the properties of the object (or the items in the collection
+ * if the model is an array). This means that changing a property deeper than the first level inside the
+ * object/collection will not trigger a re-rendering.
+ *
+ * ## `select` **`as`**
+ *
+ * Using `select` **`as`** will bind the result of the `select` expression to the model, but
+ * the value of the `` and `` html elements will be either the index (for array data sources)
+ * or property name (for object data sources) of the value within the collection. If a **`track by`** expression
+ * is used, the result of that expression will be set as the value of the `option` and `select` elements.
+ *
+ *
+ * ### `select` **`as`** and **`track by`**
+ *
+ *
+ * Be careful when using `select` **`as`** and **`track by`** in the same expression.
+ *
+ *
+ * Given this array of items on the $scope:
+ *
+ * ```js
+ * $scope.items = [{
+ * id: 1,
+ * label: 'aLabel',
+ * subItem: { name: 'aSubItem' }
+ * }, {
+ * id: 2,
+ * label: 'bLabel',
+ * subItem: { name: 'bSubItem' }
+ * }];
+ * ```
+ *
+ * This will work:
+ *
+ * ```html
+ *
+ * ```
+ * ```js
+ * $scope.selected = $scope.items[0];
+ * ```
+ *
+ * but this will not work:
+ *
+ * ```html
+ *
+ * ```
+ * ```js
+ * $scope.selected = $scope.items[0].subItem;
+ * ```
+ *
+ * In both examples, the **`track by`** expression is applied successfully to each `item` in the
+ * `items` array. Because the selected option has been set programmatically in the controller, the
+ * **`track by`** expression is also applied to the `ngModel` value. In the first example, the
+ * `ngModel` value is `items[0]` and the **`track by`** expression evaluates to `items[0].id` with
+ * no issue. In the second example, the `ngModel` value is `items[0].subItem` and the **`track by`**
+ * expression evaluates to `items[0].subItem.id` (which is undefined). As a result, the model value
+ * is not matched against any ` ` and the `` appears as having no selected value.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
+ * @param {comprehension_expression} ngOptions in one of the following forms:
+ *
+ * * for array data sources:
+ * * `label` **`for`** `value` **`in`** `array`
+ * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array`
+ * * `label` **`group by`** `group` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`disable when`** `disable` **`for`** `value` **`in`** `array` **`track by`** `trackexpr`
+ * * `label` **`for`** `value` **`in`** `array` | orderBy:`orderexpr` **`track by`** `trackexpr`
+ * (for including a filter with `track by`)
+ * * for object data sources:
+ * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
+ * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `label` **`disable when`** `disable` **`for (`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`group by`** `group`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ * * `select` **`as`** `label` **`disable when`** `disable`
+ * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
+ *
+ * Where:
+ *
+ * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
+ * * `value`: local variable which will refer to each item in the `array` or each property value
+ * of `object` during iteration.
+ * * `key`: local variable which will refer to a property name in `object` during iteration.
+ * * `label`: The result of this expression will be the label for `` element. The
+ * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
+ * * `select`: The result of this expression will be bound to the model of the parent ``
+ * element. If not specified, `select` expression will default to `value`.
+ * * `group`: The result of this expression will be used to group options using the ``
+ * DOM element.
+ * * `disable`: The result of this expression will be used to disable the rendered ``
+ * element. Return `true` to disable.
+ * * `trackexpr`: Used when working with an array of objects. The result of this expression will be
+ * used to identify the objects in the array. The `trackexpr` will most likely refer to the
+ * `value` variable (e.g. `value.propertyName`). With this the selection is preserved
+ * even when the options are recreated (e.g. reloaded from the server).
+ * @param {string=} name Property name of the form under which the control is published.
+ * @param {string=} required The control is considered valid only if value is entered.
+ * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
+ * `required` when you want to data-bind to the `required` attribute.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
+ *
+ * @example
+
+
+
+
+
+
+
Color (null not allowed):
+
+
+
Color (null allowed):
+
+
+ -- choose color --
+
+
+
+
Color grouped by shade:
+
+
+
+
+
Color grouped by shade, with some disabled:
+
+
+
+
+
+
+ Select
bogus .
+
+
+ Currently selected: {{ {selected_color:myColor} }}
+
+
+
+
+
+ it('should check ng-options', function() {
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('red');
+ element.all(by.model('myColor')).first().click();
+ element.all(by.css('select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('black');
+ element(by.css('.nullable select[ng-model="myColor"]')).click();
+ element.all(by.css('.nullable select[ng-model="myColor"] option')).first().click();
+ expect(element(by.binding('{selected_color:myColor}')).getText()).toMatch('null');
+ });
+
+
+ */
+
+/* eslint-disable max-len */
+// //00001111111111000000000002222222222000000000000000000000333333333300000000000000000000000004444444444400000000000005555555555555000000000666666666666600000007777777777777000000000000000888888888800000000000000000009999999999
+var NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?(?:\s+disable\s+when\s+([\s\S]+?))?\s+for\s+(?:([$\w][$\w]*)|(?:\(\s*([$\w][$\w]*)\s*,\s*([$\w][$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
+ // 1: value expression (valueFn)
+ // 2: label expression (displayFn)
+ // 3: group by expression (groupByFn)
+ // 4: disable when expression (disableWhenFn)
+ // 5: array item variable name
+ // 6: object item key variable name
+ // 7: object item value variable name
+ // 8: collection expression
+ // 9: track by expression
+/* eslint-enable */
+
+
+var ngOptionsDirective = ['$compile', '$document', '$parse', function($compile, $document, $parse) {
+
+ function parseOptionsExpression(optionsExp, selectElement, scope) {
+
+ var match = optionsExp.match(NG_OPTIONS_REGEXP);
+ if (!(match)) {
+ throw ngOptionsMinErr('iexp',
+ 'Expected expression in form of ' +
+ '\'_select_ (as _label_)? for (_key_,)?_value_ in _collection_\'' +
+ ' but got \'{0}\'. Element: {1}',
+ optionsExp, startingTag(selectElement));
+ }
+
+ // Extract the parts from the ngOptions expression
+
+ // The variable name for the value of the item in the collection
+ var valueName = match[5] || match[7];
+ // The variable name for the key of the item in the collection
+ var keyName = match[6];
+
+ // An expression that generates the viewValue for an option if there is a label expression
+ var selectAs = / as /.test(match[0]) && match[1];
+ // An expression that is used to track the id of each object in the options collection
+ var trackBy = match[9];
+ // An expression that generates the viewValue for an option if there is no label expression
+ var valueFn = $parse(match[2] ? match[1] : valueName);
+ var selectAsFn = selectAs && $parse(selectAs);
+ var viewValueFn = selectAsFn || valueFn;
+ var trackByFn = trackBy && $parse(trackBy);
+
+ // Get the value by which we are going to track the option
+ // if we have a trackFn then use that (passing scope and locals)
+ // otherwise just hash the given viewValue
+ var getTrackByValueFn = trackBy ?
+ function(value, locals) { return trackByFn(scope, locals); } :
+ function getHashOfValue(value) { return hashKey(value); };
+ var getTrackByValue = function(value, key) {
+ return getTrackByValueFn(value, getLocals(value, key));
+ };
+
+ var displayFn = $parse(match[2] || match[1]);
+ var groupByFn = $parse(match[3] || '');
+ var disableWhenFn = $parse(match[4] || '');
+ var valuesFn = $parse(match[8]);
+
+ var locals = {};
+ var getLocals = keyName ? function(value, key) {
+ locals[keyName] = key;
+ locals[valueName] = value;
+ return locals;
+ } : function(value) {
+ locals[valueName] = value;
+ return locals;
+ };
+
+
+ function Option(selectValue, viewValue, label, group, disabled) {
+ this.selectValue = selectValue;
+ this.viewValue = viewValue;
+ this.label = label;
+ this.group = group;
+ this.disabled = disabled;
+ }
+
+ function getOptionValuesKeys(optionValues) {
+ var optionValuesKeys;
+
+ if (!keyName && isArrayLike(optionValues)) {
+ optionValuesKeys = optionValues;
+ } else {
+ // if object, extract keys, in enumeration order, unsorted
+ optionValuesKeys = [];
+ for (var itemKey in optionValues) {
+ if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
+ optionValuesKeys.push(itemKey);
+ }
+ }
+ }
+ return optionValuesKeys;
+ }
+
+ return {
+ trackBy: trackBy,
+ getTrackByValue: getTrackByValue,
+ getWatchables: $parse(valuesFn, function(optionValues) {
+ // Create a collection of things that we would like to watch (watchedArray)
+ // so that they can all be watched using a single $watchCollection
+ // that only runs the handler once if anything changes
+ var watchedArray = [];
+ optionValues = optionValues || [];
+
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+
+ var locals = getLocals(value, key);
+ var selectValue = getTrackByValueFn(value, locals);
+ watchedArray.push(selectValue);
+
+ // Only need to watch the displayFn if there is a specific label expression
+ if (match[2] || match[1]) {
+ var label = displayFn(scope, locals);
+ watchedArray.push(label);
+ }
+
+ // Only need to watch the disableWhenFn if there is a specific disable expression
+ if (match[4]) {
+ var disableWhen = disableWhenFn(scope, locals);
+ watchedArray.push(disableWhen);
+ }
+ }
+ return watchedArray;
+ }),
+
+ getOptions: function() {
+
+ var optionItems = [];
+ var selectValueMap = {};
+
+ // The option values were already computed in the `getWatchables` fn,
+ // which must have been called to trigger `getOptions`
+ var optionValues = valuesFn(scope) || [];
+ var optionValuesKeys = getOptionValuesKeys(optionValues);
+ var optionValuesLength = optionValuesKeys.length;
+
+ for (var index = 0; index < optionValuesLength; index++) {
+ var key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
+ var value = optionValues[key];
+ var locals = getLocals(value, key);
+ var viewValue = viewValueFn(scope, locals);
+ var selectValue = getTrackByValueFn(viewValue, locals);
+ var label = displayFn(scope, locals);
+ var group = groupByFn(scope, locals);
+ var disabled = disableWhenFn(scope, locals);
+ var optionItem = new Option(selectValue, viewValue, label, group, disabled);
+
+ optionItems.push(optionItem);
+ selectValueMap[selectValue] = optionItem;
+ }
+
+ return {
+ items: optionItems,
+ selectValueMap: selectValueMap,
+ getOptionFromViewValue: function(value) {
+ return selectValueMap[getTrackByValue(value)];
+ },
+ getViewValueFromOption: function(option) {
+ // If the viewValue could be an object that may be mutated by the application,
+ // we need to make a copy and not return the reference to the value on the option.
+ return trackBy ? copy(option.viewValue) : option.viewValue;
+ }
+ };
+ }
+ };
+ }
+
+
+ // Support: IE 9 only
+ // We can't just jqLite(' ') since jqLite is not smart enough
+ // to create it in and IE barfs otherwise.
+ var optionTemplate = window.document.createElement('option'),
+ optGroupTemplate = window.document.createElement('optgroup');
+
+ function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
+
+ var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
+ var multiple = attr.multiple;
+
+ // The emptyOption allows the application developer to provide their own custom "empty"
+ // option when the viewValue does not match any of the option values.
+ for (var i = 0, children = selectElement.children(), ii = children.length; i < ii; i++) {
+ if (children[i].value === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = children.eq(i);
+ break;
+ }
+ }
+
+ // The empty option will be compiled and rendered before we first generate the options
+ selectElement.empty();
+
+ var providedEmptyOption = !!selectCtrl.emptyOption;
+
+ var unknownOption = jqLite(optionTemplate.cloneNode(false));
+ unknownOption.val('?');
+
+ var options;
+ var ngOptions = parseOptionsExpression(attr.ngOptions, selectElement, scope);
+ // This stores the newly created options before they are appended to the select.
+ // Since the contents are removed from the fragment when it is appended,
+ // we only need to create it once.
+ var listFragment = $document[0].createDocumentFragment();
+
+ // Overwrite the implementation. ngOptions doesn't use hashes
+ selectCtrl.generateUnknownOptionValue = function(val) {
+ return '?';
+ };
+
+ // Update the controller methods for multiple selectable options
+ if (!multiple) {
+
+ selectCtrl.writeValue = function writeNgOptionsValue(value) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ var selectedOption = selectElement[0].options[selectElement[0].selectedIndex];
+ var option = options.getOptionFromViewValue(value);
+
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ if (selectedOption) selectedOption.removeAttribute('selected');
+
+ if (option) {
+ // Don't update the option when it is already selected.
+ // For example, the browser will select the first option by default. In that case,
+ // most properties are set automatically - except the `selected` attribute, which we
+ // set always
+
+ if (selectElement[0].value !== option.selectValue) {
+ selectCtrl.removeUnknownOption();
+
+ selectElement[0].value = option.selectValue;
+ option.element.selected = true;
+ }
+
+ option.element.setAttribute('selected', 'selected');
+ } else {
+ selectCtrl.selectUnknownOrEmptyOption(value);
+ }
+ };
+
+ selectCtrl.readValue = function readNgOptionsValue() {
+
+ var selectedOption = options.selectValueMap[selectElement.val()];
+
+ if (selectedOption && !selectedOption.disabled) {
+ selectCtrl.unselectEmptyOption();
+ selectCtrl.removeUnknownOption();
+ return options.getViewValueFromOption(selectedOption);
+ }
+ return null;
+ };
+
+ // If we are using `track by` then we must watch the tracked value on the model
+ // since ngModel only watches for object identity change
+ // FIXME: When a user selects an option, this watch will fire needlessly
+ if (ngOptions.trackBy) {
+ scope.$watch(
+ function() { return ngOptions.getTrackByValue(ngModelCtrl.$viewValue); },
+ function() { ngModelCtrl.$render(); }
+ );
+ }
+
+ } else {
+
+ selectCtrl.writeValue = function writeNgOptionsMultiple(values) {
+ // The options might not be defined yet when ngModel tries to render
+ if (!options) return;
+
+ // Only set `.selected` if necessary, in order to prevent some browsers from
+ // scrolling to ` ` elements that are outside the `` element's viewport.
+ var selectedOptions = values && values.map(getAndUpdateSelectedOption) || [];
+
+ options.items.forEach(function(option) {
+ if (option.element.selected && !includes(selectedOptions, option)) {
+ option.element.selected = false;
+ }
+ });
+ };
+
+
+ selectCtrl.readValue = function readNgOptionsMultiple() {
+ var selectedValues = selectElement.val() || [],
+ selections = [];
+
+ forEach(selectedValues, function(value) {
+ var option = options.selectValueMap[value];
+ if (option && !option.disabled) selections.push(options.getViewValueFromOption(option));
+ });
+
+ return selections;
+ };
+
+ // If we are using `track by` then we must watch these tracked values on the model
+ // since ngModel only watches for object identity change
+ if (ngOptions.trackBy) {
+
+ scope.$watchCollection(function() {
+ if (isArray(ngModelCtrl.$viewValue)) {
+ return ngModelCtrl.$viewValue.map(function(value) {
+ return ngOptions.getTrackByValue(value);
+ });
+ }
+ }, function() {
+ ngModelCtrl.$render();
+ });
+
+ }
+ }
+
+ if (providedEmptyOption) {
+
+ // compile the element since there might be bindings in it
+ $compile(selectCtrl.emptyOption)(scope);
+
+ selectElement.prepend(selectCtrl.emptyOption);
+
+ if (selectCtrl.emptyOption[0].nodeType === NODE_TYPE_COMMENT) {
+ // This means the empty option has currently no actual DOM node, probably because
+ // it has been modified by a transclusion directive.
+ selectCtrl.hasEmptyOption = false;
+
+ // Redefine the registerOption function, which will catch
+ // options that are added by ngIf etc. (rendering of the node is async because of
+ // lazy transclusion)
+ selectCtrl.registerOption = function(optionScope, optionEl) {
+ if (optionEl.val() === '') {
+ selectCtrl.hasEmptyOption = true;
+ selectCtrl.emptyOption = optionEl;
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ // This ensures the new empty option is selected if previously no option was selected
+ ngModelCtrl.$render();
+
+ optionEl.on('$destroy', function() {
+ var needsRerender = selectCtrl.$isEmptyOptionSelected();
+
+ selectCtrl.hasEmptyOption = false;
+ selectCtrl.emptyOption = undefined;
+
+ if (needsRerender) ngModelCtrl.$render();
+ });
+ }
+ };
+
+ } else {
+ // remove the class, which is added automatically because we recompile the element and it
+ // becomes the compilation root
+ selectCtrl.emptyOption.removeClass('ng-scope');
+ }
+
+ }
+
+ // We will re-render the option elements if the option values or labels change
+ scope.$watchCollection(ngOptions.getWatchables, updateOptions);
+
+ // ------------------------------------------------------------------ //
+
+ function addOptionElement(option, parent) {
+ var optionElement = optionTemplate.cloneNode(false);
+ parent.appendChild(optionElement);
+ updateOptionElement(option, optionElement);
+ }
+
+ function getAndUpdateSelectedOption(viewValue) {
+ var option = options.getOptionFromViewValue(viewValue);
+ var element = option && option.element;
+
+ if (element && !element.selected) element.selected = true;
+
+ return option;
+ }
+
+ function updateOptionElement(option, element) {
+ option.element = element;
+ element.disabled = option.disabled;
+ // Support: IE 11 only, Edge 12-13 only
+ // NOTE: The label must be set before the value, otherwise IE 11 & Edge create unresponsive
+ // selects in certain circumstances when multiple selects are next to each other and display
+ // the option list in listbox style, i.e. the select is [multiple], or specifies a [size].
+ // See https://github.com/angular/angular.js/issues/11314 for more info.
+ // This is unfortunately untestable with unit / e2e tests
+ if (option.label !== element.label) {
+ element.label = option.label;
+ element.textContent = option.label;
+ }
+ element.value = option.selectValue;
+ }
+
+ function updateOptions() {
+ var previousValue = options && selectCtrl.readValue();
+
+ // We must remove all current options, but cannot simply set innerHTML = null
+ // since the providedEmptyOption might have an ngIf on it that inserts comments which we
+ // must preserve.
+ // Instead, iterate over the current option elements and remove them or their optgroup
+ // parents
+ if (options) {
+
+ for (var i = options.items.length - 1; i >= 0; i--) {
+ var option = options.items[i];
+ if (isDefined(option.group)) {
+ jqLiteRemove(option.element.parentNode);
+ } else {
+ jqLiteRemove(option.element);
+ }
+ }
+ }
+
+ options = ngOptions.getOptions();
+
+ var groupElementMap = {};
+
+ options.items.forEach(function addOption(option) {
+ var groupElement;
+
+ if (isDefined(option.group)) {
+
+ // This option is to live in a group
+ // See if we have already created this group
+ groupElement = groupElementMap[option.group];
+
+ if (!groupElement) {
+
+ groupElement = optGroupTemplate.cloneNode(false);
+ listFragment.appendChild(groupElement);
+
+ // Update the label on the group element
+ // "null" is special cased because of Safari
+ groupElement.label = option.group === null ? 'null' : option.group;
+
+ // Store it for use later
+ groupElementMap[option.group] = groupElement;
+ }
+
+ addOptionElement(option, groupElement);
+
+ } else {
+
+ // This option is not in a group
+ addOptionElement(option, listFragment);
+ }
+ });
+
+ selectElement[0].appendChild(listFragment);
+
+ ngModelCtrl.$render();
+
+ // Check to see if the value has changed due to the update to the options
+ if (!ngModelCtrl.$isEmpty(previousValue)) {
+ var nextValue = selectCtrl.readValue();
+ var isNotPrimitive = ngOptions.trackBy || multiple;
+ if (isNotPrimitive ? !equals(previousValue, nextValue) : previousValue !== nextValue) {
+ ngModelCtrl.$setViewValue(nextValue);
+ ngModelCtrl.$render();
+ }
+ }
+ }
+ }
+
+ return {
+ restrict: 'A',
+ terminal: true,
+ require: ['select', 'ngModel'],
+ link: {
+ pre: function ngOptionsPreLink(scope, selectElement, attr, ctrls) {
+ // Deactivate the SelectController.register method to prevent
+ // option directives from accidentally registering themselves
+ // (and unwanted $destroy handlers etc.)
+ ctrls[0].registerOption = noop;
+ },
+ post: ngOptionsPostLink
+ }
+ };
+}];
+
+/**
+ * @ngdoc directive
+ * @name ngPluralize
* @restrict EA
*
* @description
- * # Overview
* `ngPluralize` is a directive that displays messages according to en-US localization rules.
* These rules are bundled with angular.js, but can be overridden
- * (see {@link guide/i18n Angular i18n} dev guide). You configure ngPluralize directive
+ * (see {@link guide/i18n AngularJS i18n} dev guide). You configure ngPluralize directive
* by specifying the mappings between
- * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
- * plural categories} and the strings to be displayed.
+ * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
+ * and the strings to be displayed.
*
- * # Plural categories and explicit number rules
+ * ## Plural categories and explicit number rules
* There are two
- * {@link http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
- * plural categories} in Angular's default en-US locale: "one" and "other".
+ * [plural categories](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html)
+ * in AngularJS's default en-US locale: "one" and "other".
*
- * While a pural category may match many numbers (for example, in en-US locale, "other" can match
+ * While a plural category may match many numbers (for example, in en-US locale, "other" can match
* any number that is not 1), an explicit number rule can only match one number. For example, the
* explicit number rule for "3" matches the number 3. There are examples of plural categories
* and explicit number rules throughout the rest of this documentation.
*
- * # Configuring ngPluralize
+ * ## Configuring ngPluralize
* You configure ngPluralize by providing 2 attributes: `count` and `when`.
* You can also provide an optional attribute, `offset`.
*
* The value of the `count` attribute can be either a string or an {@link guide/expression
- * Angular expression}; these are evaluated on the current scope for its bound value.
+ * AngularJS expression}; these are evaluated on the current scope for its bound value.
*
* The `when` attribute specifies the mappings between plural categories and the actual
* string to be displayed. The value of the attribute should be a JSON object.
*
* The following example shows how to configure ngPluralize:
*
- *
+ * ```html
*
*
- *
+ *```
*
* In the example, `"0: Nobody is viewing."` is an explicit number rule. If you did not
* specify this rule, 0 would be matched to the "other" category and "0 people are viewing"
@@ -13721,19 +32854,22 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
* other numbers, for example 12, so that instead of showing "12 people are viewing", you can
* show "a dozen people are viewing".
*
- * You can use a set of closed braces(`{}`) as a placeholder for the number that you want substituted
- * into pluralized strings. In the previous example, Angular will replace `{}` with
+ * You can use a set of closed braces (`{}`) as a placeholder for the number that you want substituted
+ * into pluralized strings. In the previous example, AngularJS will replace `{}` with
* `{{personCount}}` . The closed braces `{}` is a placeholder
* for {{numberExpression}} .
*
- * # Configuring ngPluralize with offset
+ * If no rule is defined for a category, then an empty string is displayed and a warning is generated.
+ * Note that some locales define more categories than `one` and `other`. For example, fr-fr defines `few` and `many`.
+ *
+ * ## Configuring ngPluralize with offset
* The `offset` attribute allows further customization of pluralized text, which can result in
* a better user experience. For example, instead of the message "4 people are viewing this document",
* you might display "John, Kate and 2 others are viewing this document".
* The offset attribute allows you to offset a number by any desired value.
* Let's take a look at an example:
*
- *
+ * ```html
*
*
- *
+ * ```
*
* Notice that we are still using two plural categories(one, other), but we added
* three explicit number rules 0, 1 and 2.
* When one person, perhaps John, views the document, "John is viewing" will be shown.
* When three people view the document, no explicit number rule is found, so
- * an offset of 2 is taken off 3, and Angular uses 1 to decide the plural category.
- * In this case, plural category 'one' is matched and "John, Marry and one other person are viewing"
+ * an offset of 2 is taken off 3, and AngularJS uses 1 to decide the plural category.
+ * In this case, plural category 'one' is matched and "John, Mary and one other person are viewing"
* is shown.
*
* Note that when you specify offsets, you must provide explicit number rules for
@@ -13756,24 +32892,25 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
* you must provide explicit number rules for 0, 1, 2 and 3. You must also provide plural strings for
* plural categories "one" and "other".
*
- * @param {string|expression} count The variable to be bounded to.
- * @param {string} when The mapping between plural category to its correspoding strings.
+ * @param {string|expression} count The variable to be bound to.
+ * @param {string} when The mapping between plural category to its corresponding strings.
* @param {number=} offset Offset to deduct from the total number.
*
* @example
-
-
+
+
-
- Person 1:
- Person 2:
- Number of People:
+
+ Person 1:
+ Person 2:
+ Number of People:
Without Offset:
@@ -13793,92 +32930,422 @@ var ngNonBindableDirective = ngDirective({ terminal: true, priority: 1000 });
'other': '{{person1}}, {{person2}} and {} other people are viewing.'}">
-
-
+
+
it('should show correct pluralized string', function() {
- expect(element('.doc-example-live ng-pluralize:first').text()).
- toBe('1 person is viewing.');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Igor is viewing.');
+ var withoutOffset = element.all(by.css('ng-pluralize')).get(0);
+ var withOffset = element.all(by.css('ng-pluralize')).get(1);
+ var countInput = element(by.model('personCount'));
- using('.doc-example-live').input('personCount').enter('0');
- expect(element('.doc-example-live ng-pluralize:first').text()).
- toBe('Nobody is viewing.');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Nobody is viewing.');
+ expect(withoutOffset.getText()).toEqual('1 person is viewing.');
+ expect(withOffset.getText()).toEqual('Igor is viewing.');
- using('.doc-example-live').input('personCount').enter('2');
- expect(element('.doc-example-live ng-pluralize:first').text()).
- toBe('2 people are viewing.');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Igor and Misko are viewing.');
+ countInput.clear();
+ countInput.sendKeys('0');
- using('.doc-example-live').input('personCount').enter('3');
- expect(element('.doc-example-live ng-pluralize:first').text()).
- toBe('3 people are viewing.');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Igor, Misko and one other person are viewing.');
+ expect(withoutOffset.getText()).toEqual('Nobody is viewing.');
+ expect(withOffset.getText()).toEqual('Nobody is viewing.');
- using('.doc-example-live').input('personCount').enter('4');
- expect(element('.doc-example-live ng-pluralize:first').text()).
- toBe('4 people are viewing.');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Igor, Misko and 2 other people are viewing.');
+ countInput.clear();
+ countInput.sendKeys('2');
+
+ expect(withoutOffset.getText()).toEqual('2 people are viewing.');
+ expect(withOffset.getText()).toEqual('Igor and Misko are viewing.');
+
+ countInput.clear();
+ countInput.sendKeys('3');
+
+ expect(withoutOffset.getText()).toEqual('3 people are viewing.');
+ expect(withOffset.getText()).toEqual('Igor, Misko and one other person are viewing.');
+
+ countInput.clear();
+ countInput.sendKeys('4');
+
+ expect(withoutOffset.getText()).toEqual('4 people are viewing.');
+ expect(withOffset.getText()).toEqual('Igor, Misko and 2 other people are viewing.');
});
-
- it('should show data-binded names', function() {
- using('.doc-example-live').input('personCount').enter('4');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Igor, Misko and 2 other people are viewing.');
-
- using('.doc-example-live').input('person1').enter('Di');
- using('.doc-example-live').input('person2').enter('Vojta');
- expect(element('.doc-example-live ng-pluralize:last').text()).
- toBe('Di, Vojta and 2 other people are viewing.');
+ it('should show data-bound names', function() {
+ var withOffset = element.all(by.css('ng-pluralize')).get(1);
+ var personCount = element(by.model('personCount'));
+ var person1 = element(by.model('person1'));
+ var person2 = element(by.model('person2'));
+ personCount.clear();
+ personCount.sendKeys('4');
+ person1.clear();
+ person1.sendKeys('Di');
+ person2.clear();
+ person2.sendKeys('Vojta');
+ expect(withOffset.getText()).toEqual('Di, Vojta and 2 other people are viewing.');
});
-
-
+
+
*/
-var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interpolate) {
- var BRACE = /{}/g;
+var ngPluralizeDirective = ['$locale', '$interpolate', '$log', function($locale, $interpolate, $log) {
+ var BRACE = /{}/g,
+ IS_WHEN = /^when(Minus)?(.+)$/;
+
return {
- restrict: 'EA',
link: function(scope, element, attr) {
var numberExp = attr.count,
- whenExp = element.attr(attr.$attr.when), // this is because we have {{}} in attrs
+ whenExp = attr.$attr.when && element.attr(attr.$attr.when), // we have {{}} in attrs
offset = attr.offset || 0,
- whens = scope.$eval(whenExp),
+ whens = scope.$eval(whenExp) || {},
whensExpFns = {},
startSymbol = $interpolate.startSymbol(),
- endSymbol = $interpolate.endSymbol();
+ endSymbol = $interpolate.endSymbol(),
+ braceReplacement = startSymbol + numberExp + '-' + offset + endSymbol,
+ watchRemover = angular.noop,
+ lastCount;
- forEach(whens, function(expression, key) {
- whensExpFns[key] =
- $interpolate(expression.replace(BRACE, startSymbol + numberExp + '-' +
- offset + endSymbol));
- });
-
- scope.$watch(function ngPluralizeWatch() {
- var value = parseFloat(scope.$eval(numberExp));
-
- if (!isNaN(value)) {
- //if explicit number rule such as 1, 2, 3... is defined, just use it. Otherwise,
- //check it against pluralization rules in $locale service
- if (!(value in whens)) value = $locale.pluralCat(value - offset);
- return whensExpFns[value](scope, element, true);
- } else {
- return '';
+ forEach(attr, function(expression, attributeName) {
+ var tmpMatch = IS_WHEN.exec(attributeName);
+ if (tmpMatch) {
+ var whenKey = (tmpMatch[1] ? '-' : '') + lowercase(tmpMatch[2]);
+ whens[whenKey] = element.attr(attr.$attr[attributeName]);
}
- }, function ngPluralizeWatchAction(newVal) {
- element.text(newVal);
});
+ forEach(whens, function(expression, key) {
+ whensExpFns[key] = $interpolate(expression.replace(BRACE, braceReplacement));
+
+ });
+
+ scope.$watch(numberExp, function ngPluralizeWatchAction(newVal) {
+ var count = parseFloat(newVal);
+ var countIsNaN = isNumberNaN(count);
+
+ if (!countIsNaN && !(count in whens)) {
+ // If an explicit number rule such as 1, 2, 3... is defined, just use it.
+ // Otherwise, check it against pluralization rules in $locale service.
+ count = $locale.pluralCat(count - offset);
+ }
+
+ // If both `count` and `lastCount` are NaN, we don't need to re-register a watch.
+ // In JS `NaN !== NaN`, so we have to explicitly check.
+ if ((count !== lastCount) && !(countIsNaN && isNumberNaN(lastCount))) {
+ watchRemover();
+ var whenExpFn = whensExpFns[count];
+ if (isUndefined(whenExpFn)) {
+ if (newVal != null) {
+ $log.debug('ngPluralize: no rule defined for \'' + count + '\' in ' + whenExp);
+ }
+ watchRemover = noop;
+ updateElementText();
+ } else {
+ watchRemover = scope.$watch(whenExpFn, updateElementText);
+ }
+ lastCount = count;
+ }
+ });
+
+ function updateElementText(newText) {
+ element.text(newText || '');
+ }
}
};
}];
/**
* @ngdoc directive
- * @name ng.directive:ngRepeat
+ * @name ngRef
+ * @restrict A
+ *
+ * @description
+ * The `ngRef` attribute tells AngularJS to assign the controller of a component (or a directive)
+ * to the given property in the current scope. It is also possible to add the jqlite-wrapped DOM
+ * element to the scope.
+ *
+ * If the element with `ngRef` is destroyed `null` is assigned to the property.
+ *
+ * Note that if you want to assign from a child into the parent scope, you must initialize the
+ * target property on the parent scope, otherwise `ngRef` will assign on the child scope.
+ * This commonly happens when assigning elements or components wrapped in {@link ngIf} or
+ * {@link ngRepeat}. See the second example below.
+ *
+ *
+ * @element ANY
+ * @param {string} ngRef property name - A valid AngularJS expression identifier to which the
+ * controller or jqlite-wrapped DOM element will be bound.
+ * @param {string=} ngRefRead read value - The name of a directive (or component) on this element,
+ * or the special string `$element`. If a name is provided, `ngRef` will
+ * assign the matching controller. If `$element` is provided, the element
+ * itself is assigned (even if a controller is available).
+ *
+ *
+ * @example
+ * ### Simple toggle
+ * This example shows how the controller of the component toggle
+ * is reused in the template through the scope to use its logic.
+ *
+ *
+ *
+ * Toggle
+ *
+ * You are using a component in the same template to show it.
+ *
+ *
+ *
+ * angular.module('myApp', [])
+ * .component('myToggle', {
+ * controller: function ToggleController() {
+ * var opened = false;
+ * this.isOpen = function() { return opened; };
+ * this.toggle = function() { opened = !opened; };
+ * }
+ * });
+ *
+ *
+ * it('should publish the toggle into the scope', function() {
+ * var toggle = element(by.buttonText('Toggle'));
+ * expect(toggle.evaluate('myToggle.isOpen()')).toEqual(false);
+ * toggle.click();
+ * expect(toggle.evaluate('myToggle.isOpen()')).toEqual(true);
+ * });
+ *
+ *
+ *
+ * @example
+ * ### ngRef inside scopes
+ * This example shows how `ngRef` works with child scopes. The `ngRepeat`-ed `myWrapper` components
+ * are assigned to the scope of `myRoot`, because the `toggles` property has been initialized.
+ * The repeated `myToggle` components are published to the child scopes created by `ngRepeat`.
+ * `ngIf` behaves similarly - the assignment of `myToggle` happens in the `ngIf` child scope,
+ * because the target property has not been initialized on the `myRoot` component controller.
+ *
+ *
+ *
+ *
+ *
+ *
+ * angular.module('myApp', [])
+ * .component('myRoot', {
+ * templateUrl: 'root.html',
+ * controller: function() {
+ * this.wrappers = []; // initialize the array so that the wrappers are assigned into the parent scope
+ * }
+ * })
+ * .component('myToggle', {
+ * template: 'myToggle ',
+ * transclude: true,
+ * controller: function ToggleController() {
+ * var opened = false;
+ * this.isOpen = function() { return opened; };
+ * this.toggle = function() { opened = !opened; };
+ * }
+ * })
+ * .component('myWrapper', {
+ * transclude: true,
+ * template: 'myWrapper ' +
+ * 'ngRepeatToggle.isOpen(): {{$ctrl.ngRepeatToggle.isOpen() | json}}
' +
+ * ' '
+ * });
+ *
+ *
+ * myRoot
+ * Outer Toggle
+ * outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}
+ * wrappers assigned to root
+ *
+ * wrapper.ngRepeatToggle.isOpen(): {{wrapper.ngRepeatToggle.isOpen() | json}}
+ *
+ *
+ *
+ *
+ *
ngIfToggle.isOpen(): {{ngIfToggle.isOpen()}} // This is always undefined because it's
+ * assigned to the child scope created by ngIf.
+ *
+ *
+
ngIf
+ *
ngIf Toggle
+ *
ngIfToggle.isOpen(): {{ngIfToggle.isOpen() | json}}
+ *
outerToggle.isOpen(): {{$ctrl.outerToggle.isOpen() | json}}
+ *
+ *
+ *
+ * ul {
+ * list-style: none;
+ * padding-left: 0;
+ * }
+ *
+ * li[ng-repeat] {
+ * background: lightgreen;
+ * padding: 8px;
+ * margin: 8px;
+ * }
+ *
+ * [ng-if] {
+ * background: lightgrey;
+ * padding: 8px;
+ * }
+ *
+ * my-root {
+ * background: lightgoldenrodyellow;
+ * padding: 8px;
+ * display: block;
+ * }
+ *
+ * my-wrapper {
+ * background: lightsalmon;
+ * padding: 8px;
+ * display: block;
+ * }
+ *
+ * my-toggle {
+ * background: lightblue;
+ * padding: 8px;
+ * display: block;
+ * }
+ *
+ *
+ * var OuterToggle = function() {
+ * this.toggle = function() {
+ * element(by.buttonText('Outer Toggle')).click();
+ * };
+ * this.isOpen = function() {
+ * return element.all(by.binding('outerToggle.isOpen()')).first().getText();
+ * };
+ * };
+ * var NgRepeatToggle = function(i) {
+ * var parent = element.all(by.repeater('(index, value) in [1,2,3]')).get(i - 1);
+ * this.toggle = function() {
+ * element(by.buttonText('ngRepeat Toggle ' + i)).click();
+ * };
+ * this.isOpen = function() {
+ * return parent.element(by.binding('ngRepeatToggle.isOpen() | json')).getText();
+ * };
+ * this.isOuterOpen = function() {
+ * return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
+ * };
+ * };
+ * var NgRepeatToggles = function() {
+ * var toggles = [1,2,3].map(function(i) { return new NgRepeatToggle(i); });
+ * this.forEach = function(fn) {
+ * toggles.forEach(fn);
+ * };
+ * this.isOuterOpen = function(i) {
+ * return toggles[i - 1].isOuterOpen();
+ * };
+ * };
+ * var NgIfToggle = function() {
+ * var parent = element(by.css('[ng-if]'));
+ * this.toggle = function() {
+ * element(by.buttonText('ngIf Toggle')).click();
+ * };
+ * this.isOpen = function() {
+ * return by.binding('ngIfToggle.isOpen() | json').getText();
+ * };
+ * this.isOuterOpen = function() {
+ * return parent.element(by.binding('outerToggle.isOpen() | json')).getText();
+ * };
+ * };
+ *
+ * it('should toggle the outer toggle', function() {
+ * var outerToggle = new OuterToggle();
+ * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
+ * outerToggle.toggle();
+ * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
+ * });
+ *
+ * it('should toggle all outer toggles', function() {
+ * var outerToggle = new OuterToggle();
+ * var repeatToggles = new NgRepeatToggles();
+ * var ifToggle = new NgIfToggle();
+ * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): false');
+ * expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): false');
+ * expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): false');
+ * expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): false');
+ * expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
+ * outerToggle.toggle();
+ * expect(outerToggle.isOpen()).toEqual('outerToggle.isOpen(): true');
+ * expect(repeatToggles.isOuterOpen(1)).toEqual('outerToggle.isOpen(): true');
+ * expect(repeatToggles.isOuterOpen(2)).toEqual('outerToggle.isOpen(): true');
+ * expect(repeatToggles.isOuterOpen(3)).toEqual('outerToggle.isOpen(): true');
+ * expect(ifToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): true');
+ * });
+ *
+ * it('should toggle each repeat iteration separately', function() {
+ * var repeatToggles = new NgRepeatToggles();
+ *
+ * repeatToggles.forEach(function(repeatToggle) {
+ * expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): false');
+ * expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
+ * repeatToggle.toggle();
+ * expect(repeatToggle.isOpen()).toEqual('ngRepeatToggle.isOpen(): true');
+ * expect(repeatToggle.isOuterOpen()).toEqual('outerToggle.isOpen(): false');
+ * });
+ * });
+ *
+ *
+ *
+ */
+
+var ngRefMinErr = minErr('ngRef');
+
+var ngRefDirective = ['$parse', function($parse) {
+ return {
+ priority: -1, // Needed for compatibility with element transclusion on the same element
+ restrict: 'A',
+ compile: function(tElement, tAttrs) {
+ // Get the expected controller name, converts
into "someThing"
+ var controllerName = directiveNormalize(nodeName_(tElement));
+
+ // Get the expression for value binding
+ var getter = $parse(tAttrs.ngRef);
+ var setter = getter.assign || function() {
+ throw ngRefMinErr('nonassign', 'Expression in ngRef="{0}" is non-assignable!', tAttrs.ngRef);
+ };
+
+ return function(scope, element, attrs) {
+ var refValue;
+
+ if (attrs.hasOwnProperty('ngRefRead')) {
+ if (attrs.ngRefRead === '$element') {
+ refValue = element;
+ } else {
+ refValue = element.data('$' + attrs.ngRefRead + 'Controller');
+
+ if (!refValue) {
+ throw ngRefMinErr(
+ 'noctrl',
+ 'The controller for ngRefRead="{0}" could not be found on ngRef="{1}"',
+ attrs.ngRefRead,
+ tAttrs.ngRef
+ );
+ }
+ }
+ } else {
+ refValue = element.data('$' + controllerName + 'Controller');
+ }
+
+ refValue = refValue || element;
+
+ setter(scope, refValue);
+
+ // when the element is removed, remove it (nullify it)
+ element.on('$destroy', function() {
+ // only remove it if value has not changed,
+ // because animations (and other procedures) may duplicate elements
+ if (getter(scope) === refValue) {
+ setter(scope, null);
+ }
+ });
+ };
+ }
+ };
+}];
+
+/* exported ngRepeatDirective */
+
+/**
+ * @ngdoc directive
+ * @name ngRepeat
+ * @multiElement
+ * @restrict A
*
* @description
* The `ngRepeat` directive instantiates a template once per item from a collection. Each template
@@ -13887,278 +33354,1118 @@ var ngPluralizeDirective = ['$locale', '$interpolate', function($locale, $interp
*
* Special properties are exposed on the local scope of each template instance, including:
*
- * * `$index` – `{number}` – iterator offset of the repeated element (0..length-1)
- * * `$first` – `{boolean}` – true if the repeated element is first in the iterator.
- * * `$middle` – `{boolean}` – true if the repeated element is between the first and last in the iterator.
- * * `$last` – `{boolean}` – true if the repeated element is last in the iterator.
+ * | Variable | Type | Details |
+ * |-----------|-----------------|-----------------------------------------------------------------------------|
+ * | `$index` | {@type number} | iterator offset of the repeated element (0..length-1) |
+ * | `$first` | {@type boolean} | true if the repeated element is first in the iterator. |
+ * | `$middle` | {@type boolean} | true if the repeated element is between the first and last in the iterator. |
+ * | `$last` | {@type boolean} | true if the repeated element is last in the iterator. |
+ * | `$even` | {@type boolean} | true if the iterator position `$index` is even (otherwise false). |
+ * | `$odd` | {@type boolean} | true if the iterator position `$index` is odd (otherwise false). |
*
+ *
+ * Creating aliases for these properties is possible with {@link ng.directive:ngInit `ngInit`}.
+ * This may be useful when, for instance, nesting ngRepeats.
+ *
+ *
+ *
+ * ## Iterating over object properties
+ *
+ * It is possible to get `ngRepeat` to iterate over the properties of an object using the following
+ * syntax:
+ *
+ * ```js
+ * ...
+ * ```
+ *
+ * However, there are a few limitations compared to array iteration:
+ *
+ * - The JavaScript specification does not define the order of keys
+ * returned for an object, so AngularJS relies on the order returned by the browser
+ * when running `for key in myObj`. Browsers generally follow the strategy of providing
+ * keys in the order in which they were defined, although there are exceptions when keys are deleted
+ * and reinstated. See the
+ * [MDN page on `delete` for more info](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/delete#Cross-browser_notes).
+ *
+ * - `ngRepeat` will silently *ignore* object keys starting with `$`, because
+ * it's a prefix used by AngularJS for public (`$`) and private (`$$`) properties.
+ *
+ * - The built-in filters {@link ng.orderBy orderBy} and {@link ng.filter filter} do not work with
+ * objects, and will throw an error if used with one.
+ *
+ * If you are hitting any of these limitations, the recommended workaround is to convert your object into an array
+ * that is sorted into the order that you prefer before providing it to `ngRepeat`. You could
+ * do this with a filter such as [toArrayFilter](http://ngmodules.org/modules/angular-toArrayFilter)
+ * or implement a `$watch` on the object yourself.
+ *
+ *
+ * ## Tracking and Duplicates
+ *
+ * `ngRepeat` uses {@link $rootScope.Scope#$watchCollection $watchCollection} to detect changes in
+ * the collection. When a change happens, `ngRepeat` then makes the corresponding changes to the DOM:
+ *
+ * * When an item is added, a new instance of the template is added to the DOM.
+ * * When an item is removed, its template instance is removed from the DOM.
+ * * When items are reordered, their respective templates are reordered in the DOM.
+ *
+ * To minimize creation of DOM elements, `ngRepeat` uses a function
+ * to "keep track" of all items in the collection and their corresponding DOM elements.
+ * For example, if an item is added to the collection, `ngRepeat` will know that all other items
+ * already have DOM elements, and will not re-render them.
+ *
+ * All different types of tracking functions, their syntax, and their support for duplicate
+ * items in collections can be found in the
+ * {@link ngRepeat#ngRepeat-arguments ngRepeat expression description}.
+ *
+ *
+ * **Best Practice:** If you are working with objects that have a unique identifier property, you
+ * should track by this identifier instead of the object instance,
+ * e.g. `item in items track by item.id`.
+ * Should you reload your data later, `ngRepeat` will not have to rebuild the DOM elements for items
+ * it has already rendered, even if the JavaScript objects in the collection have been substituted
+ * for new ones. For large collections, this significantly improves rendering performance.
+ *
+ *
+ * ### Effects of DOM Element re-use
+ *
+ * When DOM elements are re-used, ngRepeat updates the scope for the element, which will
+ * automatically update any active bindings on the template. However, other
+ * functionality will not be updated, because the element is not re-created:
+ *
+ * - Directives are not re-compiled
+ * - {@link guide/expression#one-time-binding one-time expressions} on the repeated template are not
+ * updated if they have stabilized.
+ *
+ * The above affects all kinds of element re-use due to tracking, but may be especially visible
+ * when tracking by `$index` due to the way ngRepeat re-uses elements.
+ *
+ * The following example shows the effects of different actions with tracking:
+
+
+
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ var friends = [
+ {name:'John', age:25},
+ {name:'Mary', age:40},
+ {name:'Peter', age:85}
+ ];
+
+ $scope.removeFirst = function() {
+ $scope.friends.shift();
+ };
+
+ $scope.updateAge = function() {
+ $scope.friends.forEach(function(el) {
+ el.age = el.age + 5;
+ });
+ };
+
+ $scope.copy = function() {
+ $scope.friends = angular.copy($scope.friends);
+ };
+
+ $scope.reset = function() {
+ $scope.friends = angular.copy(friends);
+ };
+
+ $scope.reset();
+ });
+
+
+
+
+ When you click "Update Age", only the first list updates the age, because all others have
+ a one-time binding on the age property. If you then click "Copy", the current friend list
+ is copied, and now the second list updates the age, because the identity of the collection items
+ has changed and the list must be re-rendered. The 3rd and 4th list stay the same, because all the
+ items are already known according to their tracking functions.
+
+ When you click "Remove First", the 4th list has the wrong age on both remaining items. This is
+ due to tracking by $index: when the first collection item is removed, ngRepeat reuses the first
+ DOM element for the new first collection item, and so on. Since the age property is one-time
+ bound, the value remains from the collection item which was previously at this index.
+
+
+
+
Remove First
+
Update Age
+
Copy
+
Reset List
+
+
track by $id(friend)
(default):
+
+
+ {{friend.name}} is {{friend.age}} years old.
+
+
+
track by $id(friend)
(default), with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
track by friend.name
, with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
track by $index
, with age one-time binding:
+
+
+ {{friend.name}} is {{::friend.age}} years old.
+
+
+
+
+
+ .example-animate-container {
+ background:white;
+ border:1px solid black;
+ list-style:none;
+ margin:0;
+ padding:0 10px;
+ }
+
+ .animate-repeat {
+ line-height:30px;
+ list-style:none;
+ box-sizing:border-box;
+ }
+
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter,
+ .animate-repeat.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ .animate-repeat.ng-leave.ng-leave-active,
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter {
+ opacity:0;
+ max-height:0;
+ }
+
+ .animate-repeat.ng-leave,
+ .animate-repeat.ng-move.ng-move-active,
+ .animate-repeat.ng-enter.ng-enter-active {
+ opacity:1;
+ max-height:30px;
+ }
+
+
+
+ *
+ * ## Special repeat start and end points
+ * To repeat a series of elements instead of just one parent element, ngRepeat (as well as other ng directives) supports extending
+ * the range of the repeater by defining explicit start and end points by using **ng-repeat-start** and **ng-repeat-end** respectively.
+ * The **ng-repeat-start** directive works the same as **ng-repeat**, but will repeat all the HTML code (including the tag it's defined on)
+ * up to and including the ending HTML tag where **ng-repeat-end** is placed.
+ *
+ * The example below makes use of this feature:
+ * ```html
+ *
+ * Header {{ item }}
+ *
+ *
+ * Body {{ item }}
+ *
+ *
+ * Footer {{ item }}
+ *
+ * ```
+ *
+ * And with an input of {@type ['A','B']} for the items variable in the example above, the output will evaluate to:
+ * ```html
+ *
+ *
+ * Body A
+ *
+ *
+ *
+ *
+ * Body B
+ *
+ *
+ * ```
+ *
+ * The custom start and end points for ngRepeat also support all other HTML directive syntax flavors provided in AngularJS (such
+ * as **data-ng-repeat-start**, **x-ng-repeat-start** and **ng:repeat-start**).
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | when a new item is added to the list or when an item is revealed after a filter |
+ * | {@link ng.$animate#leave leave} | when an item is removed from the list or when an item is filtered out |
+ * | {@link ng.$animate#move move } | when an adjacent item is filtered out causing a reorder or when the item contents are reordered |
+ *
+ * See the example below for defining CSS animations with ngRepeat.
*
* @element ANY
* @scope
* @priority 1000
- * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. Two
+ * @param {repeat_expression} ngRepeat The expression indicating how to enumerate a collection. These
* formats are currently supported:
*
* * `variable in expression` – where variable is the user defined loop variable and `expression`
* is a scope expression giving the collection to enumerate.
*
- * For example: `track in cd.tracks`.
+ * For example: `album in artist.albums`.
*
* * `(key, value) in expression` – where `key` and `value` can be any user defined identifiers,
* and `expression` is the scope expression giving the collection to enumerate.
*
* For example: `(name, age) in {'adam':10, 'amalie':12}`.
*
+ * * `variable in expression track by tracking_expression` – You can also provide an optional tracking expression
+ * which can be used to associate the objects in the collection with the DOM elements. If no tracking expression
+ * is specified, ng-repeat associates elements by identity. It is an error to have
+ * more than one tracking expression value resolve to the same key. (This would mean that two distinct objects are
+ * mapped to the same DOM element, which is not possible.)
+ *
+ * *Default tracking: $id()*: `item in items` is equivalent to `item in items track by $id(item)`.
+ * This implies that the DOM elements will be associated by item identity in the collection.
+ *
+ * The built-in `$id()` function can be used to assign a unique
+ * `$$hashKey` property to each item in the collection. This property is then used as a key to associated DOM elements
+ * with the corresponding item in the collection by identity. Moving the same object would move
+ * the DOM element in the same way in the DOM.
+ * Note that the default id function does not support duplicate primitive values (`number`, `string`),
+ * but supports duplictae non-primitive values (`object`) that are *equal* in shape.
+ *
+ * *Custom Expression*: It is possible to use any AngularJS expression to compute the tracking
+ * id, for example with a function, or using a property on the collection items.
+ * `item in items track by item.id` is a typical pattern when the items have a unique identifier,
+ * e.g. database id. In this case the object identity does not matter. Two objects are considered
+ * equivalent as long as their `id` property is same.
+ * Tracking by unique identifier is the most performant way and should be used whenever possible.
+ *
+ * *$index*: This special property tracks the collection items by their index, and
+ * re-uses the DOM elements that match that index, e.g. `item in items track by $index`. This can
+ * be used for a performance improvement if no unique identfier is available and the identity of
+ * the collection items cannot be easily computed. It also allows duplicates.
+ *
+ *
+ * Note: Re-using DOM elements can have unforeseen effects. Read the
+ * {@link ngRepeat#tracking-and-duplicates section on tracking and duplicates} for
+ * more info.
+ *
+ *
+ *
+ * Note: the `track by` expression must come last - after any filters, and the alias expression:
+ * `item in items | filter:searchText as results track by item.id`
+ *
+ *
+ * * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
+ * intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
+ * when a filter is active on the repeater, but the filtered result set is empty.
+ *
+ * For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
+ * the items have been processed through the filter.
+ *
+ * Please note that `as [variable name]` is not an operator but rather a part of ngRepeat
+ * micro-syntax so it can be used only after all filters (and not as operator, inside an expression).
+ *
+ * For example: `item in items | filter : x | orderBy : order | limitTo : limit as results track by item.id` .
+ *
* @example
- * This example initializes the scope to a list of names and
- * then uses `ngRepeat` to display every person:
-
-
-
- I have {{friends.length}} friends. They are:
-
-
- [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
-
-
-
-
-
- it('should check ng-repeat', function() {
- var r = using('.doc-example-live').repeater('ul li');
- expect(r.count()).toBe(2);
- expect(r.row(0)).toEqual(["1","John","25"]);
- expect(r.row(1)).toEqual(["2","Mary","28"]);
- });
-
-
- */
-var ngRepeatDirective = ngDirective({
- transclude: 'element',
- priority: 1000,
- terminal: true,
- compile: function(element, attr, linker) {
- return function(scope, iterStartElement, attr){
- var expression = attr.ngRepeat;
- var match = expression.match(/^\s*(.+)\s+in\s+(.*)\s*$/),
- lhs, rhs, valueIdent, keyIdent;
- if (! match) {
- throw Error("Expected ngRepeat in form of '_item_ in _collection_' but got '" +
- expression + "'.");
- }
- lhs = match[1];
- rhs = match[2];
- match = lhs.match(/^(?:([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\))$/);
- if (!match) {
- throw Error("'item' in 'item in collection' should be identifier or (key, value) but got '" +
- lhs + "'.");
- }
- valueIdent = match[3] || match[1];
- keyIdent = match[2];
-
- // Store a list of elements from previous run. This is a hash where key is the item from the
- // iterator, and the value is an array of objects with following properties.
- // - scope: bound scope
- // - element: previous element.
- // - index: position
- // We need an array of these objects since the same object can be returned from the iterator.
- // We expect this to be a rare case.
- var lastOrder = new HashQueueMap();
-
- scope.$watch(function ngRepeatWatch(scope){
- var index, length,
- collection = scope.$eval(rhs),
- cursor = iterStartElement, // current position of the node
- // Same as lastOrder but it has the current state. It will become the
- // lastOrder on the next iteration.
- nextOrder = new HashQueueMap(),
- arrayBound,
- childScope,
- key, value, // key/value of iteration
- array,
- last; // last object information {scope, element, index}
-
-
-
- if (!isArray(collection)) {
- // if object, extract keys, sort them and use to determine order of iteration over obj props
- array = [];
- for(key in collection) {
- if (collection.hasOwnProperty(key) && key.charAt(0) != '$') {
- array.push(key);
- }
- }
- array.sort();
- } else {
- array = collection || [];
- }
-
- arrayBound = array.length-1;
-
- // we are not using forEach for perf reasons (trying to avoid #call)
- for (index = 0, length = array.length; index < length; index++) {
- key = (collection === array) ? index : array[index];
- value = collection[key];
-
- last = lastOrder.shift(value);
-
- if (last) {
- // if we have already seen this object, then we need to reuse the
- // associated scope/element
- childScope = last.scope;
- nextOrder.push(value, last);
-
- if (index === last.index) {
- // do nothing
- cursor = last.element;
- } else {
- // existing item which got moved
- last.index = index;
- // This may be a noop, if the element is next, but I don't know of a good way to
- // figure this out, since it would require extra DOM access, so let's just hope that
- // the browsers realizes that it is noop, and treats it as such.
- cursor.after(last.element);
- cursor = last.element;
- }
- } else {
- // new item which we don't know about
- childScope = scope.$new();
- }
-
- childScope[valueIdent] = value;
- if (keyIdent) childScope[keyIdent] = key;
- childScope.$index = index;
-
- childScope.$first = (index === 0);
- childScope.$last = (index === arrayBound);
- childScope.$middle = !(childScope.$first || childScope.$last);
-
- if (!last) {
- linker(childScope, function(clone){
- cursor.after(clone);
- last = {
- scope: childScope,
- element: (cursor = clone),
- index: index
- };
- nextOrder.push(value, last);
- });
- }
- }
-
- //shrink children
- for (key in lastOrder) {
- if (lastOrder.hasOwnProperty(key)) {
- array = lastOrder[key];
- while(array.length) {
- value = array.pop();
- value.element.remove();
- value.scope.$destroy();
- }
- }
- }
-
- lastOrder = nextOrder;
+ * This example uses `ngRepeat` to display a list of people. A filter is used to restrict the displayed
+ * results by name or by age. New (entering) and removed (leaving) items are animated.
+
+
+
+ I have {{friends.length}} friends. They are:
+
+
+
+ [{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
+
+
+ No results found...
+
+
+
+
+
+ angular.module('ngRepeat', ['ngAnimate']).controller('repeatController', function($scope) {
+ $scope.friends = [
+ {name:'John', age:25, gender:'boy'},
+ {name:'Jessie', age:30, gender:'girl'},
+ {name:'Johanna', age:28, gender:'girl'},
+ {name:'Joy', age:15, gender:'girl'},
+ {name:'Mary', age:28, gender:'girl'},
+ {name:'Peter', age:95, gender:'boy'},
+ {name:'Sebastian', age:50, gender:'boy'},
+ {name:'Erika', age:27, gender:'girl'},
+ {name:'Patrick', age:40, gender:'boy'},
+ {name:'Samantha', age:60, gender:'girl'}
+ ];
});
- };
- }
-});
+
+
+ .example-animate-container {
+ background:white;
+ border:1px solid black;
+ list-style:none;
+ margin:0;
+ padding:0 10px;
+ }
+ .animate-repeat {
+ line-height:30px;
+ list-style:none;
+ box-sizing:border-box;
+ }
+
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter,
+ .animate-repeat.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ .animate-repeat.ng-leave.ng-leave-active,
+ .animate-repeat.ng-move,
+ .animate-repeat.ng-enter {
+ opacity:0;
+ max-height:0;
+ }
+
+ .animate-repeat.ng-leave,
+ .animate-repeat.ng-move.ng-move-active,
+ .animate-repeat.ng-enter.ng-enter-active {
+ opacity:1;
+ max-height:30px;
+ }
+
+
+ var friends = element.all(by.repeater('friend in friends'));
+
+ it('should render initial data set', function() {
+ expect(friends.count()).toBe(10);
+ expect(friends.get(0).getText()).toEqual('[1] John who is 25 years old.');
+ expect(friends.get(1).getText()).toEqual('[2] Jessie who is 30 years old.');
+ expect(friends.last().getText()).toEqual('[10] Samantha who is 60 years old.');
+ expect(element(by.binding('friends.length')).getText())
+ .toMatch("I have 10 friends. They are:");
+ });
+
+ it('should update repeater when filter predicate changes', function() {
+ expect(friends.count()).toBe(10);
+
+ element(by.model('q')).sendKeys('ma');
+
+ expect(friends.count()).toBe(2);
+ expect(friends.get(0).getText()).toEqual('[1] Mary who is 28 years old.');
+ expect(friends.last().getText()).toEqual('[2] Samantha who is 60 years old.');
+ });
+
+
+ */
+var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $animate, $compile) {
+ var NG_REMOVED = '$$NG_REMOVED';
+ var ngRepeatMinErr = minErr('ngRepeat');
+
+ var updateScope = function(scope, index, valueIdentifier, value, keyIdentifier, key, arrayLength) {
+ // TODO(perf): generate setters to shave off ~40ms or 1-1.5%
+ scope[valueIdentifier] = value;
+ if (keyIdentifier) scope[keyIdentifier] = key;
+ scope.$index = index;
+ scope.$first = (index === 0);
+ scope.$last = (index === (arrayLength - 1));
+ scope.$middle = !(scope.$first || scope.$last);
+ // eslint-disable-next-line no-bitwise
+ scope.$odd = !(scope.$even = (index & 1) === 0);
+ };
+
+ var getBlockStart = function(block) {
+ return block.clone[0];
+ };
+
+ var getBlockEnd = function(block) {
+ return block.clone[block.clone.length - 1];
+ };
+
+ var trackByIdArrayFn = function($scope, key, value) {
+ return hashKey(value);
+ };
+
+ var trackByIdObjFn = function($scope, key) {
+ return key;
+ };
+
+ return {
+ restrict: 'A',
+ multiElement: true,
+ transclude: 'element',
+ priority: 1000,
+ terminal: true,
+ $$tlb: true,
+ compile: function ngRepeatCompile($element, $attr) {
+ var expression = $attr.ngRepeat;
+ var ngRepeatEndComment = $compile.$$createComment('end ngRepeat', expression);
+
+ var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/);
+
+ if (!match) {
+ throw ngRepeatMinErr('iexp', 'Expected expression in form of \'_item_ in _collection_[ track by _id_]\' but got \'{0}\'.',
+ expression);
+ }
+
+ var lhs = match[1];
+ var rhs = match[2];
+ var aliasAs = match[3];
+ var trackByExp = match[4];
+
+ match = lhs.match(/^(?:(\s*[$\w]+)|\(\s*([$\w]+)\s*,\s*([$\w]+)\s*\))$/);
+
+ if (!match) {
+ throw ngRepeatMinErr('iidexp', '\'_item_\' in \'_item_ in _collection_\' should be an identifier or \'(_key_, _value_)\' expression, but got \'{0}\'.',
+ lhs);
+ }
+ var valueIdentifier = match[3] || match[1];
+ var keyIdentifier = match[2];
+
+ if (aliasAs && (!/^[$a-zA-Z_][$a-zA-Z0-9_]*$/.test(aliasAs) ||
+ /^(null|undefined|this|\$index|\$first|\$middle|\$last|\$even|\$odd|\$parent|\$root|\$id)$/.test(aliasAs))) {
+ throw ngRepeatMinErr('badident', 'alias \'{0}\' is invalid --- must be a valid JS identifier which is not a reserved name.',
+ aliasAs);
+ }
+
+ var trackByIdExpFn;
+
+ if (trackByExp) {
+ var hashFnLocals = {$id: hashKey};
+ var trackByExpGetter = $parse(trackByExp);
+
+ trackByIdExpFn = function($scope, key, value, index) {
+ // assign key, value, and $index to the locals so that they can be used in hash functions
+ if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
+ hashFnLocals[valueIdentifier] = value;
+ hashFnLocals.$index = index;
+ return trackByExpGetter($scope, hashFnLocals);
+ };
+ }
+
+ return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
+
+ // Store a list of elements from previous run. This is a hash where key is the item from the
+ // iterator, and the value is objects with following properties.
+ // - scope: bound scope
+ // - clone: previous element.
+ // - index: position
+ //
+ // We are using no-proto object so that we don't need to guard against inherited props via
+ // hasOwnProperty.
+ var lastBlockMap = createMap();
+
+ //watch props
+ $scope.$watchCollection(rhs, function ngRepeatAction(collection) {
+ var index, length,
+ previousNode = $element[0], // node that cloned nodes should be inserted after
+ // initialized to the comment node anchor
+ nextNode,
+ // Same as lastBlockMap but it has the current state. It will become the
+ // lastBlockMap on the next iteration.
+ nextBlockMap = createMap(),
+ collectionLength,
+ key, value, // key/value of iteration
+ trackById,
+ trackByIdFn,
+ collectionKeys,
+ block, // last object information {scope, element, id}
+ nextBlockOrder,
+ elementsToRemove;
+
+ if (aliasAs) {
+ $scope[aliasAs] = collection;
+ }
+
+ if (isArrayLike(collection)) {
+ collectionKeys = collection;
+ trackByIdFn = trackByIdExpFn || trackByIdArrayFn;
+ } else {
+ trackByIdFn = trackByIdExpFn || trackByIdObjFn;
+ // if object, extract keys, in enumeration order, unsorted
+ collectionKeys = [];
+ for (var itemKey in collection) {
+ if (hasOwnProperty.call(collection, itemKey) && itemKey.charAt(0) !== '$') {
+ collectionKeys.push(itemKey);
+ }
+ }
+ }
+
+ collectionLength = collectionKeys.length;
+ nextBlockOrder = new Array(collectionLength);
+
+ // locate existing items
+ for (index = 0; index < collectionLength; index++) {
+ key = (collection === collectionKeys) ? index : collectionKeys[index];
+ value = collection[key];
+ trackById = trackByIdFn($scope, key, value, index);
+ if (lastBlockMap[trackById]) {
+ // found previously seen block
+ block = lastBlockMap[trackById];
+ delete lastBlockMap[trackById];
+ nextBlockMap[trackById] = block;
+ nextBlockOrder[index] = block;
+ } else if (nextBlockMap[trackById]) {
+ // if collision detected. restore lastBlockMap and throw an error
+ forEach(nextBlockOrder, function(block) {
+ if (block && block.scope) lastBlockMap[block.id] = block;
+ });
+ throw ngRepeatMinErr('dupes',
+ 'Duplicates in a repeater are not allowed. Use \'track by\' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}',
+ expression, trackById, value);
+ } else {
+ // new never before seen block
+ nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined};
+ nextBlockMap[trackById] = true;
+ }
+ }
+
+ // Clear the value property from the hashFnLocals object to prevent a reference to the last value
+ // being leaked into the ngRepeatCompile function scope
+ if (hashFnLocals) {
+ hashFnLocals[valueIdentifier] = undefined;
+ }
+
+ // remove leftover items
+ for (var blockKey in lastBlockMap) {
+ block = lastBlockMap[blockKey];
+ elementsToRemove = getBlockNodes(block.clone);
+ $animate.leave(elementsToRemove);
+ if (elementsToRemove[0].parentNode) {
+ // if the element was not removed yet because of pending animation, mark it as deleted
+ // so that we can ignore it later
+ for (index = 0, length = elementsToRemove.length; index < length; index++) {
+ elementsToRemove[index][NG_REMOVED] = true;
+ }
+ }
+ block.scope.$destroy();
+ }
+
+ // we are not using forEach for perf reasons (trying to avoid #call)
+ for (index = 0; index < collectionLength; index++) {
+ key = (collection === collectionKeys) ? index : collectionKeys[index];
+ value = collection[key];
+ block = nextBlockOrder[index];
+
+ if (block.scope) {
+ // if we have already seen this object, then we need to reuse the
+ // associated scope/element
+
+ nextNode = previousNode;
+
+ // skip nodes that are already pending removal via leave animation
+ do {
+ nextNode = nextNode.nextSibling;
+ } while (nextNode && nextNode[NG_REMOVED]);
+
+ if (getBlockStart(block) !== nextNode) {
+ // existing item which got moved
+ $animate.move(getBlockNodes(block.clone), null, previousNode);
+ }
+ previousNode = getBlockEnd(block);
+ updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
+ } else {
+ // new item which we don't know about
+ $transclude(function ngRepeatTransclude(clone, scope) {
+ block.scope = scope;
+ // http://jsperf.com/clone-vs-createcomment
+ var endNode = ngRepeatEndComment.cloneNode(false);
+ clone[clone.length++] = endNode;
+
+ $animate.enter(clone, null, previousNode);
+ previousNode = endNode;
+ // Note: We only need the first/last node of the cloned nodes.
+ // However, we need to keep the reference to the jqlite wrapper as it might be changed later
+ // by a directive with templateUrl when its template arrives.
+ block.clone = clone;
+ nextBlockMap[block.id] = block;
+ updateScope(block.scope, index, valueIdentifier, value, keyIdentifier, key, collectionLength);
+ });
+ }
+ }
+ lastBlockMap = nextBlockMap;
+ });
+ };
+ }
+ };
+}];
+
+var NG_HIDE_CLASS = 'ng-hide';
+var NG_HIDE_IN_PROGRESS_CLASS = 'ng-hide-animate';
/**
* @ngdoc directive
- * @name ng.directive:ngShow
+ * @name ngShow
+ * @multiElement
*
* @description
- * The `ngShow` and `ngHide` directives show or hide a portion of the DOM tree (HTML)
- * conditionally.
+ * The `ngShow` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngShow` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * When the `ngShow` expression evaluates to a falsy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When truthy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
+ *
+ * ## Why is `!important` used?
+ *
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
+ *
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
+ *
+ * ### Overriding `.ng-hide`
+ *
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
+ *
+ * ```css
+ * .ng-hide:not(.ng-hide-animate) {
+ * /* These are just alternative ways of hiding an element */
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
+ * }
+ * ```
+ *
+ * By default you don't need to override anything in CSS and the animations will work around the
+ * display style.
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngShow` expression evaluates to a non truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngShow` expression evaluates to a truthy value and just before contents are set to visible. |
+ *
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
+ *
+ * ```css
+ * /* A working example can be found at the bottom of this page. */
+ * .my-element.ng-hide-add, .my-element.ng-hide-remove {
+ * transition: all 0.5s linear;
+ * }
+ *
+ * .my-element.ng-hide-add { ... }
+ * .my-element.ng-hide-add.ng-hide-add-active { ... }
+ * .my-element.ng-hide-remove { ... }
+ * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
+ * ```
+ *
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngShow If the {@link guide/expression expression} is truthy
- * then the element is shown or hidden respectively.
+ * @param {expression} ngShow If the {@link guide/expression expression} is truthy/falsy then the
+ * element is shown/hidden respectively.
*
* @example
-
-
- Click me:
- Show: I show up when your checkbox is checked.
- Hide: I hide when your checkbox is checked.
-
-
- it('should check ng-show / ng-hide', function() {
- expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
- expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
+ * A simple example, animating the element's opacity:
+ *
+
+
+ Show:
+
+ I show up when your checkbox is checked.
+
+
+
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
- input('checked').check();
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
- expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
- expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
- });
-
-
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+
+
+ *
+ *
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+
+
+ Show:
+
+ I show up when your checkbox is checked.
+
+
+
+ body {
+ overflow: hidden;
+ perspective: 1000px;
+ }
+
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
+ }
+
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngShow', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(false);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(true);
+ });
+
+
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngShow to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
-//TODO(misko): refactor to remove element from the DOM
-var ngShowDirective = ngDirective(function(scope, element, attr){
- scope.$watch(attr.ngShow, function ngShowWatchAction(value){
- element.css('display', toBoolean(value) ? '' : 'none');
- });
-});
+var ngShowDirective = ['$animate', function($animate) {
+ return {
+ restrict: 'A',
+ multiElement: true,
+ link: function(scope, element, attr) {
+ scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
+ // we're adding a temporary, animation-specific class for ng-hide since this way
+ // we can control when the element is actually displayed on screen without having
+ // to have a global/greedy CSS selector that breaks when other animations are run.
+ // Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
+ $animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
+ tempClasses: NG_HIDE_IN_PROGRESS_CLASS
+ });
+ });
+ }
+ };
+}];
/**
* @ngdoc directive
- * @name ng.directive:ngHide
+ * @name ngHide
+ * @multiElement
*
* @description
- * The `ngHide` and `ngShow` directives hide or show a portion of the DOM tree (HTML)
- * conditionally.
+ * The `ngHide` directive shows or hides the given HTML element based on the expression provided to
+ * the `ngHide` attribute.
+ *
+ * The element is shown or hidden by removing or adding the `.ng-hide` CSS class onto the element.
+ * The `.ng-hide` CSS class is predefined in AngularJS and sets the display style to none (using an
+ * `!important` flag). For CSP mode please add `angular-csp.css` to your HTML file (see
+ * {@link ng.directive:ngCsp ngCsp}).
+ *
+ * ```html
+ *
+ *
+ *
+ *
+ *
+ * ```
+ *
+ * When the `ngHide` expression evaluates to a truthy value then the `.ng-hide` CSS class is added
+ * to the class attribute on the element causing it to become hidden. When falsy, the `.ng-hide`
+ * CSS class is removed from the element causing the element not to appear hidden.
+ *
+ * ## Why is `!important` used?
+ *
+ * You may be wondering why `!important` is used for the `.ng-hide` CSS class. This is because the
+ * `.ng-hide` selector can be easily overridden by heavier selectors. For example, something as
+ * simple as changing the display style on a HTML list item would make hidden elements appear
+ * visible. This also becomes a bigger issue when dealing with CSS frameworks.
+ *
+ * By using `!important`, the show and hide behavior will work as expected despite any clash between
+ * CSS selector specificity (when `!important` isn't used with any conflicting styles). If a
+ * developer chooses to override the styling to change how to hide an element then it is just a
+ * matter of using `!important` in their own CSS code.
+ *
+ * ### Overriding `.ng-hide`
+ *
+ * By default, the `.ng-hide` class will style the element with `display: none !important`. If you
+ * wish to change the hide behavior with `ngShow`/`ngHide`, you can simply overwrite the styles for
+ * the `.ng-hide` CSS class. Note that the selector that needs to be used is actually
+ * `.ng-hide:not(.ng-hide-animate)` to cope with extra animation classes that can be added.
+ *
+ * ```css
+ * .ng-hide:not(.ng-hide-animate) {
+ * /* These are just alternative ways of hiding an element */
+ * display: block!important;
+ * position: absolute;
+ * top: -9999px;
+ * left: -9999px;
+ * }
+ * ```
+ *
+ * By default you don't need to override in CSS anything and the animations will work around the
+ * display style.
+ *
+ * @animations
+ * | Animation | Occurs |
+ * |-----------------------------------------------------|------------------------------------------------------------------------------------------------------------|
+ * | {@link $animate#addClass addClass} `.ng-hide` | After the `ngHide` expression evaluates to a truthy value and just before the contents are set to hidden. |
+ * | {@link $animate#removeClass removeClass} `.ng-hide` | After the `ngHide` expression evaluates to a non truthy value and just before contents are set to visible. |
+ *
+ * Animations in `ngShow`/`ngHide` work with the show and hide events that are triggered when the
+ * directive expression is true and false. This system works like the animation system present with
+ * `ngClass` except that you must also include the `!important` flag to override the display
+ * property so that the elements are not actually hidden during the animation.
+ *
+ * ```css
+ * /* A working example can be found at the bottom of this page. */
+ * .my-element.ng-hide-add, .my-element.ng-hide-remove {
+ * transition: all 0.5s linear;
+ * }
+ *
+ * .my-element.ng-hide-add { ... }
+ * .my-element.ng-hide-add.ng-hide-add-active { ... }
+ * .my-element.ng-hide-remove { ... }
+ * .my-element.ng-hide-remove.ng-hide-remove-active { ... }
+ * ```
+ *
+ * Keep in mind that, as of AngularJS version 1.3, there is no need to change the display property
+ * to block during animation states - ngAnimate will automatically handle the style toggling for you.
*
* @element ANY
- * @param {expression} ngHide If the {@link guide/expression expression} is truthy then
- * the element is shown or hidden respectively.
+ * @param {expression} ngHide If the {@link guide/expression expression} is truthy/falsy then the
+ * element is hidden/shown respectively.
*
* @example
-
-
- Click me:
- Show: I show up when you checkbox is checked?
- Hide: I hide when you checkbox is checked?
-
-
- it('should check ng-show / ng-hide', function() {
- expect(element('.doc-example-live span:first:hidden').count()).toEqual(1);
- expect(element('.doc-example-live span:last:visible').count()).toEqual(1);
+ * A simple example, animating the element's opacity:
+ *
+
+
+ Hide:
+
+ I hide when your checkbox is checked.
+
+
+
+ .animate-show-hide.ng-hide {
+ opacity: 0;
+ }
- input('checked').check();
+ .animate-show-hide.ng-hide-add,
+ .animate-show-hide.ng-hide-remove {
+ transition: all linear 0.5s;
+ }
- expect(element('.doc-example-live span:first:visible').count()).toEqual(1);
- expect(element('.doc-example-live span:last:hidden').count()).toEqual(1);
- });
-
-
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+
+
+ *
+ *
+ * @example
+ * A more complex example, featuring different show/hide animations:
+ *
+
+
+ Hide:
+
+ I hide when your checkbox is checked.
+
+
+
+ body {
+ overflow: hidden;
+ perspective: 1000px;
+ }
+
+ .funky-show-hide.ng-hide-add {
+ transform: rotateZ(0);
+ transform-origin: right;
+ transition: all 0.5s ease-in-out;
+ }
+
+ .funky-show-hide.ng-hide-add.ng-hide-add-active {
+ transform: rotateZ(-135deg);
+ }
+
+ .funky-show-hide.ng-hide-remove {
+ transform: rotateY(90deg);
+ transform-origin: left;
+ transition: all 0.5s ease;
+ }
+
+ .funky-show-hide.ng-hide-remove.ng-hide-remove-active {
+ transform: rotateY(0);
+ }
+
+ .check-element {
+ border: 1px solid black;
+ opacity: 1;
+ padding: 10px;
+ }
+
+
+ it('should check ngHide', function() {
+ var checkbox = element(by.model('checked'));
+ var checkElem = element(by.css('.check-element'));
+
+ expect(checkElem.isDisplayed()).toBe(true);
+ checkbox.click();
+ expect(checkElem.isDisplayed()).toBe(false);
+ });
+
+
+ *
+ * @knownIssue
+ *
+ * ### Flickering when using ngHide to toggle between elements
+ *
+ * When using {@link ngShow} and / or {@link ngHide} to toggle between elements, it can
+ * happen that both the element to show and the element to hide are visible for a very short time.
+ *
+ * This usually happens when the {@link ngAnimate ngAnimate module} is included, but no actual animations
+ * are defined for {@link ngShow} / {@link ngHide}. Internet Explorer is affected more often than
+ * other browsers.
+ *
+ * There are several way to mitigate this problem:
+ *
+ * - {@link guide/animations#how-to-selectively-enable-disable-and-skip-animations Disable animations on the affected elements}.
+ * - Use {@link ngIf} or {@link ngSwitch} instead of {@link ngShow} / {@link ngHide}.
+ * - Use the special CSS selector `ng-hide.ng-hide-animate` to set `{display: none}` or similar on the affected elements.
+ * - Use `ng-class="{'ng-hide': expression}` instead of instead of {@link ngShow} / {@link ngHide}.
+ * - Define an animation on the affected elements.
*/
-//TODO(misko): refactor to remove element from the DOM
-var ngHideDirective = ngDirective(function(scope, element, attr){
- scope.$watch(attr.ngHide, function ngHideWatchAction(value){
- element.css('display', toBoolean(value) ? 'none' : '');
- });
-});
+var ngHideDirective = ['$animate', function($animate) {
+ return {
+ restrict: 'A',
+ multiElement: true,
+ link: function(scope, element, attr) {
+ scope.$watch(attr.ngHide, function ngHideWatchAction(value) {
+ // The comment inside of the ngShowDirective explains why we add and
+ // remove a temporary class for the show/hide animation
+ $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, {
+ tempClasses: NG_HIDE_IN_PROGRESS_CLASS
+ });
+ });
+ }
+ };
+}];
/**
* @ngdoc directive
- * @name ng.directive:ngStyle
+ * @name ngStyle
+ * @restrict AC
*
* @description
* The `ngStyle` directive allows you to set CSS style on an HTML element conditionally.
*
+ * @knownIssue
+ * You should not use {@link guide/interpolation interpolation} in the value of the `style`
+ * attribute, when using the `ngStyle` directive on the same element.
+ * See {@link guide/interpolation#known-issues here} for more info.
+ *
* @element ANY
- * @param {expression} ngStyle {@link guide/expression Expression} which evals to an
- * object whose keys are CSS style names and values are corresponding values for those CSS
- * keys.
+ * @param {expression} ngStyle
+ *
+ * {@link guide/expression Expression} which evals to an
+ * object whose keys are CSS style names and values are corresponding values for those CSS
+ * keys.
+ *
+ * Since some CSS style names are not valid keys for an object, they must be quoted.
+ * See the 'background-color' style in the example below.
*
* @example
-
+
-
+
+
Sample Text
@@ -14169,398 +34476,517 @@ var ngHideDirective = ngDirective(function(scope, element, attr){
color: black;
}
-
+
+ var colorSpan = element(by.css('span'));
+
it('should check ng-style', function() {
- expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
- element('.doc-example-live :button[value=set]').click();
- expect(element('.doc-example-live span').css('color')).toBe('rgb(255, 0, 0)');
- element('.doc-example-live :button[value=clear]').click();
- expect(element('.doc-example-live span').css('color')).toBe('rgb(0, 0, 0)');
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
+ element(by.css('input[value=\'set color\']')).click();
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(255, 0, 0, 1\)|rgb\(255, 0, 0\)/);
+ element(by.css('input[value=clear]')).click();
+ expect(colorSpan.getCssValue('color')).toMatch(/rgba\(0, 0, 0, 1\)|rgb\(0, 0, 0\)/);
});
*/
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, '');});
+ forEach(oldStyles, function(val, style) { element.css(style, ''); });
}
if (newStyles) element.css(newStyles);
- }, true);
+ });
});
/**
* @ngdoc directive
- * @name ng.directive:ngSwitch
+ * @name ngSwitch
* @restrict EA
*
* @description
- * Conditionally change the DOM structure.
+ * The `ngSwitch` directive is used to conditionally swap DOM structure on your template based on a scope expression.
+ * Elements within `ngSwitch` but without `ngSwitchWhen` or `ngSwitchDefault` directives will be preserved at the location
+ * as specified in the template.
+ *
+ * The directive itself works similar to ngInclude, however, instead of downloading template code (or loading it
+ * from the template cache), `ngSwitch` simply chooses one of the nested elements and makes it visible based on which element
+ * matches the value obtained from the evaluated expression. In other words, you define a container element
+ * (where you place the directive), place an expression on the **`on="..."` attribute**
+ * (or the **`ng-switch="..."` attribute**), define any inner elements inside of the directive and place
+ * a when attribute per element. The when attribute is used to inform ngSwitch which element to display when the on
+ * expression is evaluated. If a matching expression is not found via a when attribute then an element with the default
+ * attribute is displayed.
+ *
+ *
+ * Be aware that the attribute values to match against cannot be expressions. They are interpreted
+ * as literal string values to match against.
+ * For example, **`ng-switch-when="someVal"`** will match against the string `"someVal"` not against the
+ * value of the expression `$scope.someVal`.
+ *
+
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#enter enter} | after the ngSwitch contents change and the matched child element is placed inside the container |
+ * | {@link ng.$animate#leave leave} | after the ngSwitch contents change and just before the former contents are removed from the DOM |
*
* @usage
+ *
+ * ```
*
* ...
* ...
- * ...
* ...
*
+ * ```
+ *
*
* @scope
- * @param {*} ngSwitch|on expression to match against ng-switch-when .
- * @paramDescription
- * On child elments add:
+ * @priority 1200
+ * @param {*} ngSwitch|on expression to match against ng-switch-when
.
+ * On child elements add:
*
* * `ngSwitchWhen`: the case statement to match against. If match then this
- * case will be displayed.
- * * `ngSwitchDefault`: the default case when no other casses match.
+ * case will be displayed. If the same match appears multiple times, all the
+ * elements will be displayed. It is possible to associate multiple values to
+ * the same `ngSwitchWhen` by defining the optional attribute
+ * `ngSwitchWhenSeparator`. The separator will be used to split the value of
+ * the `ngSwitchWhen` attribute into multiple tokens, and the element will show
+ * if any of the `ngSwitch` evaluates to any of these tokens.
+ * * `ngSwitchDefault`: the default case when no other case match. If there
+ * are multiple default cases, all of them will be displayed when no other
+ * case match.
+ *
*
* @example
-
-
-
-
-
-
-
selection={{selection}}
-
-
-
Settings Div
-
Home Span
-
default
-
+
+
+
+
+
+
selection={{selection}}
+
+
+
Settings Div
+
Home Span
+
default
-
-
- it('should start in settings', function() {
- expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Settings Div/);
- });
- it('should change to home', function() {
- select('selection').option('home');
- expect(element('.doc-example-live [ng-switch]').text()).toMatch(/Home Span/);
- });
- it('should select deafault', function() {
- select('selection').option('other');
- expect(element('.doc-example-live [ng-switch]').text()).toMatch(/default/);
- });
-
-
- */
-var NG_SWITCH = 'ng-switch';
-var ngSwitchDirective = valueFn({
- restrict: 'EA',
- require: 'ngSwitch',
- // asks for $scope to fool the BC controller module
- controller: ['$scope', function ngSwitchController() {
- this.cases = {};
- }],
- link: function(scope, element, attr, ctrl) {
- var watchExpr = attr.ngSwitch || attr.on,
- selectedTransclude,
- selectedElement,
- selectedScope;
+
+
+
+ angular.module('switchExample', ['ngAnimate'])
+ .controller('ExampleController', ['$scope', function($scope) {
+ $scope.items = ['settings', 'home', 'options', 'other'];
+ $scope.selection = $scope.items[0];
+ }]);
+
+
+ .animate-switch-container {
+ position:relative;
+ background:white;
+ border:1px solid black;
+ height:40px;
+ overflow:hidden;
+ }
- scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
- if (selectedElement) {
- selectedScope.$destroy();
- selectedElement.remove();
- selectedElement = selectedScope = null;
+ .animate-switch {
+ padding:10px;
}
- if ((selectedTransclude = ctrl.cases['!' + value] || ctrl.cases['?'])) {
- scope.$eval(attr.change);
- selectedScope = scope.$new();
- selectedTransclude(selectedScope, function(caseElement) {
- selectedElement = caseElement;
- element.append(caseElement);
- });
+
+ .animate-switch.ng-animate {
+ transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
+
+ position:absolute;
+ top:0;
+ left:0;
+ right:0;
+ bottom:0;
}
- });
- }
-});
+
+ .animate-switch.ng-leave.ng-leave-active,
+ .animate-switch.ng-enter {
+ top:-50px;
+ }
+ .animate-switch.ng-leave,
+ .animate-switch.ng-enter.ng-enter-active {
+ top:0;
+ }
+
+
+ var switchElem = element(by.css('[ng-switch]'));
+ var select = element(by.model('selection'));
+
+ it('should start in settings', function() {
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
+ it('should change to home', function() {
+ select.all(by.css('option')).get(1).click();
+ expect(switchElem.getText()).toMatch(/Home Span/);
+ });
+ it('should change to settings via "options"', function() {
+ select.all(by.css('option')).get(2).click();
+ expect(switchElem.getText()).toMatch(/Settings Div/);
+ });
+ it('should select default', function() {
+ select.all(by.css('option')).get(3).click();
+ expect(switchElem.getText()).toMatch(/default/);
+ });
+
+
+ */
+var ngSwitchDirective = ['$animate', '$compile', function($animate, $compile) {
+ return {
+ require: 'ngSwitch',
+
+ // asks for $scope to fool the BC controller module
+ controller: ['$scope', function NgSwitchController() {
+ this.cases = {};
+ }],
+ link: function(scope, element, attr, ngSwitchController) {
+ var watchExpr = attr.ngSwitch || attr.on,
+ selectedTranscludes = [],
+ selectedElements = [],
+ previousLeaveAnimations = [],
+ selectedScopes = [];
+
+ var spliceFactory = function(array, index) {
+ return function(response) {
+ if (response !== false) array.splice(index, 1);
+ };
+ };
+
+ scope.$watch(watchExpr, function ngSwitchWatchAction(value) {
+ var i, ii;
+
+ // Start with the last, in case the array is modified during the loop
+ while (previousLeaveAnimations.length) {
+ $animate.cancel(previousLeaveAnimations.pop());
+ }
+
+ for (i = 0, ii = selectedScopes.length; i < ii; ++i) {
+ var selected = getBlockNodes(selectedElements[i].clone);
+ selectedScopes[i].$destroy();
+ var runner = previousLeaveAnimations[i] = $animate.leave(selected);
+ runner.done(spliceFactory(previousLeaveAnimations, i));
+ }
+
+ selectedElements.length = 0;
+ selectedScopes.length = 0;
+
+ if ((selectedTranscludes = ngSwitchController.cases['!' + value] || ngSwitchController.cases['?'])) {
+ forEach(selectedTranscludes, function(selectedTransclude) {
+ selectedTransclude.transclude(function(caseElement, selectedScope) {
+ selectedScopes.push(selectedScope);
+ var anchor = selectedTransclude.element;
+ caseElement[caseElement.length++] = $compile.$$createComment('end ngSwitchWhen');
+ var block = { clone: caseElement };
+
+ selectedElements.push(block);
+ $animate.enter(caseElement, anchor.parent(), anchor);
+ });
+ });
+ }
+ });
+ }
+ };
+}];
var ngSwitchWhenDirective = ngDirective({
transclude: 'element',
- priority: 500,
+ priority: 1200,
require: '^ngSwitch',
- compile: function(element, attrs, transclude) {
- return function(scope, element, attr, ctrl) {
- ctrl.cases['!' + attrs.ngSwitchWhen] = transclude;
- };
+ multiElement: true,
+ link: function(scope, element, attrs, ctrl, $transclude) {
+
+ var cases = attrs.ngSwitchWhen.split(attrs.ngSwitchWhenSeparator).sort().filter(
+ // Filter duplicate cases
+ function(element, index, array) { return array[index - 1] !== element; }
+ );
+
+ forEach(cases, function(whenCase) {
+ ctrl.cases['!' + whenCase] = (ctrl.cases['!' + whenCase] || []);
+ ctrl.cases['!' + whenCase].push({ transclude: $transclude, element: element });
+ });
}
});
var ngSwitchDefaultDirective = ngDirective({
transclude: 'element',
- priority: 500,
+ priority: 1200,
require: '^ngSwitch',
- compile: function(element, attrs, transclude) {
- return function(scope, element, attr, ctrl) {
- ctrl.cases['?'] = transclude;
- };
- }
+ multiElement: true,
+ link: function(scope, element, attr, ctrl, $transclude) {
+ ctrl.cases['?'] = (ctrl.cases['?'] || []);
+ ctrl.cases['?'].push({ transclude: $transclude, element: element });
+ }
});
/**
* @ngdoc directive
- * @name ng.directive:ngTransclude
+ * @name ngTransclude
+ * @restrict EAC
*
* @description
- * Insert the transcluded DOM here.
+ * Directive that marks the insertion point for the transcluded DOM of the nearest parent directive that uses transclusion.
+ *
+ * You can specify that you want to insert a named transclusion slot, instead of the default slot, by providing the slot name
+ * as the value of the `ng-transclude` or `ng-transclude-slot` attribute.
+ *
+ * If the transcluded content is not empty (i.e. contains one or more DOM nodes, including whitespace text nodes), any existing
+ * content of this element will be removed before the transcluded content is inserted.
+ * If the transcluded content is empty (or only whitespace), the existing content is left intact. This lets you provide fallback
+ * content in the case that no transcluded content is provided.
*
* @element ANY
*
+ * @param {string} ngTransclude|ngTranscludeSlot the name of the slot to insert at this point. If this is not provided, is empty
+ * or its value is the same as the name of the attribute then the default slot is used.
+ *
* @example
-
-
-
-
-
-
- it('should have transcluded', function() {
- input('title').enter('TITLE');
- input('text').enter('TEXT');
- expect(binding('title')).toEqual('TITLE');
- expect(binding('text')).toEqual('TEXT');
- });
-
-
+ * ### Basic transclusion
+ * This example demonstrates basic transclusion of content into a component directive.
+ *
+ *
+ *
+ *
+ *
+ *
+ * it('should have transcluded', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.binding('title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * });
+ *
+ *
*
- */
-var ngTranscludeDirective = ngDirective({
- controller: ['$transclude', '$element', function($transclude, $element) {
- $transclude(function(clone) {
- $element.append(clone);
- });
- }]
-});
-
-/**
- * @ngdoc directive
- * @name ng.directive:ngView
- * @restrict ECA
- *
- * @description
- * # Overview
- * `ngView` is a directive that complements the {@link ng.$route $route} service by
- * including the rendered template of the current route into the main layout (`index.html`) file.
- * Every time the current route changes, the included view changes with it according to the
- * configuration of the `$route` service.
- *
- * @scope
* @example
-
-
-
- Choose:
-
Moby |
-
Moby: Ch1 |
-
Gatsby |
-
Gatsby: Ch4 |
-
Scarlet Letter
-
-
-
-
-
$location.path() = {{$location.path()}}
-
$route.current.templateUrl = {{$route.current.templateUrl}}
-
$route.current.params = {{$route.current.params}}
-
$route.current.scope.name = {{$route.current.scope.name}}
-
$routeParams = {{$routeParams}}
-
-
-
-
- controller: {{name}}
- Book Id: {{params.bookId}}
-
-
-
- controller: {{name}}
- Book Id: {{params.bookId}}
- Chapter Id: {{params.chapterId}}
-
-
-
- angular.module('ngView', [], function($routeProvider, $locationProvider) {
- $routeProvider.when('/Book/:bookId', {
- templateUrl: 'book.html',
- controller: BookCntl
- });
- $routeProvider.when('/Book/:bookId/ch/:chapterId', {
- templateUrl: 'chapter.html',
- controller: ChapterCntl
- });
-
- // configure html5 to get links working on jsfiddle
- $locationProvider.html5Mode(true);
- });
-
- function MainCntl($scope, $route, $routeParams, $location) {
- $scope.$route = $route;
- $scope.$location = $location;
- $scope.$routeParams = $routeParams;
- }
-
- function BookCntl($scope, $routeParams) {
- $scope.name = "BookCntl";
- $scope.params = $routeParams;
- }
-
- function ChapterCntl($scope, $routeParams) {
- $scope.name = "ChapterCntl";
- $scope.params = $routeParams;
- }
-
-
-
- it('should load and compile correct template', function() {
- element('a:contains("Moby: Ch1")').click();
- var content = element('.doc-example-live [ng-view]').text();
- expect(content).toMatch(/controller\: ChapterCntl/);
- expect(content).toMatch(/Book Id\: Moby/);
- expect(content).toMatch(/Chapter Id\: 1/);
-
- element('a:contains("Scarlet")').click();
- content = element('.doc-example-live [ng-view]').text();
- expect(content).toMatch(/controller\: BookCntl/);
- expect(content).toMatch(/Book Id\: Scarlet/);
- });
-
-
+ * ### Transclude fallback content
+ * This example shows how to use `NgTransclude` with fallback content, that
+ * is displayed if no transcluded content is provided.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * Button2
+ *
+ *
+ *
+ * it('should have different transclude element content', function() {
+ * expect(element(by.id('fallback')).getText()).toBe('Button1');
+ * expect(element(by.id('modified')).getText()).toBe('Button2');
+ * });
+ *
+ *
+ *
+ * @example
+ * ### Multi-slot transclusion
+ * This example demonstrates using multi-slot transclusion in a component directive.
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * {{title}}
+ * {{text}}
+ *
+ *
+ *
+ *
+ * angular.module('multiSlotTranscludeExample', [])
+ * .directive('pane', function() {
+ * return {
+ * restrict: 'E',
+ * transclude: {
+ * 'title': '?paneTitle',
+ * 'body': 'paneBody',
+ * 'footer': '?paneFooter'
+ * },
+ * template: '' +
+ * '
Fallback Title
' +
+ * '
' +
+ * '' +
+ * '
'
+ * };
+ * })
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.title = 'Lorem Ipsum';
+ * $scope.link = 'https://google.com';
+ * $scope.text = 'Neque porro quisquam est qui dolorem ipsum quia dolor...';
+ * }]);
+ *
+ *
+ * it('should have transcluded the title and the body', function() {
+ * var titleElement = element(by.model('title'));
+ * titleElement.clear();
+ * titleElement.sendKeys('TITLE');
+ * var textElement = element(by.model('text'));
+ * textElement.clear();
+ * textElement.sendKeys('TEXT');
+ * expect(element(by.css('.title')).getText()).toEqual('TITLE');
+ * expect(element(by.binding('text')).getText()).toEqual('TEXT');
+ * expect(element(by.css('.footer')).getText()).toEqual('Fallback Footer');
+ * });
+ *
+ *
*/
-
-
-/**
- * @ngdoc event
- * @name ng.directive:ngView#$viewContentLoaded
- * @eventOf ng.directive:ngView
- * @eventType emit on the current ngView scope
- * @description
- * Emitted every time the ngView content is reloaded.
- */
-var ngViewDirective = ['$http', '$templateCache', '$route', '$anchorScroll', '$compile',
- '$controller',
- function($http, $templateCache, $route, $anchorScroll, $compile,
- $controller) {
+var ngTranscludeMinErr = minErr('ngTransclude');
+var ngTranscludeDirective = ['$compile', function($compile) {
return {
- restrict: 'ECA',
- terminal: true,
- link: function(scope, element, attr) {
- var lastScope,
- onloadExp = attr.onload || '';
+ restrict: 'EAC',
+ compile: function ngTranscludeCompile(tElement) {
- scope.$on('$routeChangeSuccess', update);
- update();
+ // Remove and cache any original content to act as a fallback
+ var fallbackLinkFn = $compile(tElement.contents());
+ tElement.empty();
+ return function ngTranscludePostLink($scope, $element, $attrs, controller, $transclude) {
- function destroyLastScope() {
- if (lastScope) {
- lastScope.$destroy();
- lastScope = null;
+ if (!$transclude) {
+ throw ngTranscludeMinErr('orphan',
+ 'Illegal use of ngTransclude directive in the template! ' +
+ 'No parent directive that requires a transclusion found. ' +
+ 'Element: {0}',
+ startingTag($element));
}
- }
- function clearContent() {
- element.html('');
- destroyLastScope();
- }
- function update() {
- var locals = $route.current && $route.current.locals,
- template = locals && locals.$template;
+ // If the attribute is of the form: `ng-transclude="ng-transclude"` then treat it like the default
+ if ($attrs.ngTransclude === $attrs.$attr.ngTransclude) {
+ $attrs.ngTransclude = '';
+ }
+ var slotName = $attrs.ngTransclude || $attrs.ngTranscludeSlot;
- if (template) {
- element.html(template);
- destroyLastScope();
+ // If the slot is required and no transclusion content is provided then this call will throw an error
+ $transclude(ngTranscludeCloneAttachFn, null, slotName);
- var link = $compile(element.contents()),
- current = $route.current,
- controller;
+ // If the slot is optional and no transclusion content is provided then use the fallback content
+ if (slotName && !$transclude.isSlotFilled(slotName)) {
+ useFallbackContent();
+ }
- lastScope = current.scope = scope.$new();
- if (current.controller) {
- locals.$scope = lastScope;
- controller = $controller(current.controller, locals);
- element.children().data('$ngControllerController', controller);
+ function ngTranscludeCloneAttachFn(clone, transcludedScope) {
+ if (clone.length && notWhitespace(clone)) {
+ $element.append(clone);
+ } else {
+ useFallbackContent();
+ // There is nothing linked against the transcluded scope since no content was available,
+ // so it should be safe to clean up the generated scope.
+ transcludedScope.$destroy();
}
-
- link(lastScope);
- lastScope.$emit('$viewContentLoaded');
- lastScope.$eval(onloadExp);
-
- // $anchorScroll might listen on event...
- $anchorScroll();
- } else {
- clearContent();
}
- }
+
+ function useFallbackContent() {
+ // Since this is the fallback content rather than the transcluded content,
+ // we link against the scope of this directive rather than the transcluded scope
+ fallbackLinkFn($scope, function(clone) {
+ $element.append(clone);
+ });
+ }
+
+ function notWhitespace(nodes) {
+ for (var i = 0, ii = nodes.length; i < ii; i++) {
+ var node = nodes[i];
+ if (node.nodeType !== NODE_TYPE_TEXT || node.nodeValue.trim()) {
+ return true;
+ }
+ }
+ }
+ };
}
};
}];
/**
* @ngdoc directive
- * @name ng.directive:script
+ * @name script
+ * @restrict E
*
* @description
- * Load content of a script tag, with type `text/ng-template`, into `$templateCache`, so that the
- * template can be used by `ngInclude`, `ngView` or directive templates.
+ * Load the content of a `
Load inlined template
-
-
+
+
it('should load template defined inside script tag', function() {
- element('#tpl-link').click();
- expect(element('#tpl-content').text()).toMatch(/Content of the template/);
+ element(by.css('#tpl-link')).click();
+ expect(element(by.css('#tpl-content')).getText()).toMatch(/Content of the template/);
});
-
-
+
+
*/
var scriptDirective = ['$templateCache', function($templateCache) {
return {
restrict: 'E',
terminal: true,
compile: function(element, attr) {
- if (attr.type == 'text/ng-template') {
+ 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);
@@ -14569,590 +34995,1502 @@ var scriptDirective = ['$templateCache', function($templateCache) {
};
}];
+/* exported selectDirective, optionDirective */
+
+var noopNgModelController = { $setViewValue: noop, $render: noop };
+
+function setOptionSelectedStatus(optionEl, value) {
+ optionEl.prop('selected', value);
+ /**
+ * When unselecting an option, setting the property to null / false should be enough
+ * However, screenreaders might react to the selected attribute instead, see
+ * https://github.com/angular/angular.js/issues/14419
+ * Note: "selected" is a boolean attr and will be removed when the "value" arg in attr() is false
+ * or null
+ */
+ optionEl.attr('selected', value);
+}
+
+/**
+ * @ngdoc type
+ * @name select.SelectController
+ *
+ * @description
+ * The controller for the {@link ng.select select} directive. The controller exposes
+ * a few utility methods that can be used to augment the behavior of a regular or an
+ * {@link ng.ngOptions ngOptions} select element.
+ *
+ * @example
+ * ### Set a custom error when the unknown option is selected
+ *
+ * This example sets a custom error "unknownValue" on the ngModelController
+ * when the select element's unknown option is selected, i.e. when the model is set to a value
+ * that is not matched by any option.
+ *
+ *
+ *
+ *
+ *
+ * Single select:
+ *
+ * Option 1
+ * Option 2
+ *
+ *
+ * Error: The current model doesn't match any option
+ *
+ * Force unknown option
+ *
+ *
+ *
+ *
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueError', function() {
+ * return {
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * ngModelCtrl.$validators.unknownValue = function(modelValue, viewValue) {
+ * if (selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return true;
+ * };
+ * }
+ *
+ * };
+ * });
+ *
+ *
+ *
+ *
+ * @example
+ * ### Set the "required" error when the unknown option is selected.
+ *
+ * By default, the "required" error on the ngModelController is only set on a required select
+ * when the empty option is selected. This example adds a custom directive that also sets the
+ * error when the unknown option is selected.
+ *
+ *
+ *
+ *
+ *
+ * Select:
+ *
+ * Option 1
+ * Option 2
+ *
+ * Error: Please select a value
+ *
+ * Force unknown option
+ *
+ *
+ *
+ *
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.selected = null;
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.selected = 'nonsense';
+ * };
+ * }])
+ * .directive('unknownValueRequired', function() {
+ * return {
+ * priority: 1, // This directive must run after the required directive has added its validator
+ * require: ['ngModel', 'select'],
+ * link: function(scope, element, attrs, ctrls) {
+ * var ngModelCtrl = ctrls[0];
+ * var selectCtrl = ctrls[1];
+ *
+ * var originalRequiredValidator = ngModelCtrl.$validators.required;
+ *
+ * ngModelCtrl.$validators.required = function() {
+ * if (attrs.required && selectCtrl.$isUnknownOptionSelected()) {
+ * return false;
+ * }
+ *
+ * return originalRequiredValidator.apply(this, arguments);
+ * };
+ * }
+ * };
+ * });
+ *
+ *
+ * it('should show the error message when the unknown option is selected', function() {
+
+ var error = element(by.className('error'));
+
+ expect(error.getText()).toBe('Error: Please select a value');
+
+ element(by.cssContainingText('option', 'Option 1')).click();
+
+ expect(error.isPresent()).toBe(false);
+
+ element(by.tagName('button')).click();
+
+ expect(error.getText()).toBe('Error: Please select a value');
+ });
+ *
+ *
+ *
+ *
+ */
+var SelectController =
+ ['$element', '$scope', /** @this */ function($element, $scope) {
+
+ var self = this,
+ optionsMap = new NgMap();
+
+ self.selectValueMap = {}; // Keys are the hashed values, values the original values
+
+ // If the ngModel doesn't get provided then provide a dummy noop version to prevent errors
+ self.ngModelCtrl = noopNgModelController;
+ self.multiple = false;
+
+ // The "unknown" option is one that is prepended to the list if the viewValue
+ // does not match any of the options. When it is rendered the value of the unknown
+ // option is '? XXX ?' where XXX is the hashKey of the value that is not known.
+ //
+ // Support: IE 9 only
+ // We can't just jqLite('
') since jqLite is not smart enough
+ // to create it in and IE barfs otherwise.
+ self.unknownOption = jqLite(window.document.createElement('option'));
+
+ // The empty option is an option with the value '' that the application developer can
+ // provide inside the select. It is always selectable and indicates that a "null" selection has
+ // been made by the user.
+ // If the select has an empty option, and the model of the select is set to "undefined" or "null",
+ // the empty option is selected.
+ // If the model is set to a different unmatched value, the unknown option is rendered and
+ // selected, i.e both are present, because a "null" selection and an unknown value are different.
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+
+ self.renderUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ $element.prepend(self.unknownOption);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
+
+ self.updateUnknownOption = function(val) {
+ var unknownVal = self.generateUnknownOptionValue(val);
+ self.unknownOption.val(unknownVal);
+ setOptionSelectedStatus(self.unknownOption, true);
+ $element.val(unknownVal);
+ };
+
+ self.generateUnknownOptionValue = function(val) {
+ return '? ' + hashKey(val) + ' ?';
+ };
+
+ self.removeUnknownOption = function() {
+ if (self.unknownOption.parent()) self.unknownOption.remove();
+ };
+
+ self.selectEmptyOption = function() {
+ if (self.emptyOption) {
+ $element.val('');
+ setOptionSelectedStatus(self.emptyOption, true);
+ }
+ };
+
+ self.unselectEmptyOption = function() {
+ if (self.hasEmptyOption) {
+ setOptionSelectedStatus(self.emptyOption, false);
+ }
+ };
+
+ $scope.$on('$destroy', function() {
+ // disable unknown option so that we don't do work when the whole select is being destroyed
+ self.renderUnknownOption = noop;
+ });
+
+ // Read the value of the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.readValue = function readSingleValue() {
+ var val = $element.val();
+ // ngValue added option values are stored in the selectValueMap, normal interpolations are not
+ var realVal = val in self.selectValueMap ? self.selectValueMap[val] : val;
+
+ if (self.hasOption(realVal)) {
+ return realVal;
+ }
+
+ return null;
+ };
+
+
+ // Write the value to the select control, the implementation of this changes depending
+ // upon whether the select can have multiple values and whether ngOptions is at work.
+ self.writeValue = function writeSingleValue(value) {
+ // Make sure to remove the selected attribute from the previously selected option
+ // Otherwise, screen readers might get confused
+ var currentlySelectedOption = $element[0].options[$element[0].selectedIndex];
+ if (currentlySelectedOption) setOptionSelectedStatus(jqLite(currentlySelectedOption), false);
+
+ if (self.hasOption(value)) {
+ self.removeUnknownOption();
+
+ var hashedVal = hashKey(value);
+ $element.val(hashedVal in self.selectValueMap ? hashedVal : value);
+
+ // Set selected attribute and property on selected option for screen readers
+ var selectedOption = $element[0].options[$element[0].selectedIndex];
+ setOptionSelectedStatus(jqLite(selectedOption), true);
+ } else {
+ self.selectUnknownOrEmptyOption(value);
+ }
+ };
+
+
+ // Tell the select control that an option, with the given value, has been added
+ self.addOption = function(value, element) {
+ // Skip comment nodes, as they only pollute the `optionsMap`
+ if (element[0].nodeType === NODE_TYPE_COMMENT) return;
+
+ assertNotHasOwnProperty(value, '"option value"');
+ if (value === '') {
+ self.hasEmptyOption = true;
+ self.emptyOption = element;
+ }
+ var count = optionsMap.get(value) || 0;
+ optionsMap.set(value, count + 1);
+ // Only render at the end of a digest. This improves render performance when many options
+ // are added during a digest and ensures all relevant options are correctly marked as selected
+ scheduleRender();
+ };
+
+ // Tell the select control that an option, with the given value, has been removed
+ self.removeOption = function(value) {
+ var count = optionsMap.get(value);
+ if (count) {
+ if (count === 1) {
+ optionsMap.delete(value);
+ if (value === '') {
+ self.hasEmptyOption = false;
+ self.emptyOption = undefined;
+ }
+ } else {
+ optionsMap.set(value, count - 1);
+ }
+ }
+ };
+
+ // Check whether the select control has an option matching the given value
+ self.hasOption = function(value) {
+ return !!optionsMap.get(value);
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$hasEmptyOption
+ *
+ * @description
+ *
+ * Returns `true` if the select element currently has an empty option
+ * element, i.e. an option that signifies that the select is empty / the selection is null.
+ *
+ */
+ self.$hasEmptyOption = function() {
+ return self.hasEmptyOption;
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isUnknownOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element's unknown option is selected. The unknown option is added
+ * and automatically selected whenever the select model doesn't match any option.
+ *
+ */
+ self.$isUnknownOptionSelected = function() {
+ // Presence of the unknown option means it is selected
+ return $element[0].options[0] === self.unknownOption[0];
+ };
+
+ /**
+ * @ngdoc method
+ * @name select.SelectController#$isEmptyOptionSelected
+ *
+ * @description
+ *
+ * Returns `true` if the select element has an empty option and this empty option is currently
+ * selected. Returns `false` if the select element has no empty option or it is not selected.
+ *
+ */
+ self.$isEmptyOptionSelected = function() {
+ return self.hasEmptyOption && $element[0].options[$element[0].selectedIndex] === self.emptyOption[0];
+ };
+
+ self.selectUnknownOrEmptyOption = function(value) {
+ if (value == null && self.emptyOption) {
+ self.removeUnknownOption();
+ self.selectEmptyOption();
+ } else if (self.unknownOption.parent().length) {
+ self.updateUnknownOption(value);
+ } else {
+ self.renderUnknownOption(value);
+ }
+ };
+
+ var renderScheduled = false;
+ function scheduleRender() {
+ if (renderScheduled) return;
+ renderScheduled = true;
+ $scope.$$postDigest(function() {
+ renderScheduled = false;
+ self.ngModelCtrl.$render();
+ });
+ }
+
+ var updateScheduled = false;
+ function scheduleViewValueUpdate(renderAfter) {
+ if (updateScheduled) return;
+
+ updateScheduled = true;
+
+ $scope.$$postDigest(function() {
+ if ($scope.$$destroyed) return;
+
+ updateScheduled = false;
+ self.ngModelCtrl.$setViewValue(self.readValue());
+ if (renderAfter) self.ngModelCtrl.$render();
+ });
+ }
+
+
+ self.registerOption = function(optionScope, optionElement, optionAttrs, interpolateValueFn, interpolateTextFn) {
+
+ if (optionAttrs.$attr.ngValue) {
+ // The value attribute is set by ngValue
+ var oldVal, hashedVal;
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(hashedVal)) {
+ self.removeOption(oldVal);
+ delete self.selectValueMap[hashedVal];
+ removal = true;
+ }
+
+ hashedVal = hashKey(newVal);
+ oldVal = newVal;
+ self.selectValueMap[hashedVal] = newVal;
+ self.addOption(newVal, optionElement);
+ // Set the attribute directly instead of using optionAttrs.$set - this stops the observer
+ // from firing a second time. Other $observers on value will also get the result of the
+ // ngValue expression, not the hashed value
+ optionElement.attr('value', hashedVal);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+
+ });
+ } else if (interpolateValueFn) {
+ // The value attribute is interpolated
+ optionAttrs.$observe('value', function valueAttributeObserveAction(newVal) {
+ // This method is overwritten in ngOptions and has side-effects!
+ self.readValue();
+
+ var removal;
+ var previouslySelected = optionElement.prop('selected');
+
+ if (isDefined(oldVal)) {
+ self.removeOption(oldVal);
+ removal = true;
+ }
+ oldVal = newVal;
+ self.addOption(newVal, optionElement);
+
+ if (removal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+ });
+ } else if (interpolateTextFn) {
+ // The text content is interpolated
+ optionScope.$watch(interpolateTextFn, function interpolateWatchAction(newVal, oldVal) {
+ optionAttrs.$set('value', newVal);
+ var previouslySelected = optionElement.prop('selected');
+ if (oldVal !== newVal) {
+ self.removeOption(oldVal);
+ }
+ self.addOption(newVal, optionElement);
+
+ if (oldVal && previouslySelected) {
+ scheduleViewValueUpdate();
+ }
+ });
+ } else {
+ // The value attribute is static
+ self.addOption(optionAttrs.value, optionElement);
+ }
+
+
+ optionAttrs.$observe('disabled', function(newVal) {
+
+ // Since model updates will also select disabled options (like ngOptions),
+ // we only have to handle options becoming disabled, not enabled
+
+ if (newVal === 'true' || newVal && optionElement.prop('selected')) {
+ if (self.multiple) {
+ scheduleViewValueUpdate(true);
+ } else {
+ self.ngModelCtrl.$setViewValue(null);
+ self.ngModelCtrl.$render();
+ }
+ }
+ });
+
+ optionElement.on('$destroy', function() {
+ var currentValue = self.readValue();
+ var removeValue = optionAttrs.value;
+
+ self.removeOption(removeValue);
+ scheduleRender();
+
+ if (self.multiple && currentValue && currentValue.indexOf(removeValue) !== -1 ||
+ currentValue === removeValue
+ ) {
+ // When multiple (selected) options are destroyed at the same time, we don't want
+ // to run a model update for each of them. Instead, run a single update in the $$postDigest
+ scheduleViewValueUpdate(true);
+ }
+ });
+ };
+}];
+
/**
* @ngdoc directive
- * @name ng.directive:select
+ * @name select
* @restrict E
*
* @description
- * HTML `SELECT` element with angular data-binding.
+ * HTML `select` element with AngularJS data-binding.
*
- * # `ngOptions`
+ * The `select` directive is used together with {@link ngModel `ngModel`} to provide data-binding
+ * between the scope and the `` control (including setting default values).
+ * It also handles dynamic `` elements, which can be added using the {@link ngRepeat `ngRepeat}` or
+ * {@link ngOptions `ngOptions`} directives.
*
- * Optionally `ngOptions` attribute can be used to dynamically generate a list of ` `
- * elements for a `` element using an array or an object obtained by evaluating the
- * `ngOptions` expression.
+ * When an item in the `` menu is selected, the value of the selected option will be bound
+ * to the model identified by the `ngModel` directive. With static or repeated options, this is
+ * the content of the `value` attribute or the textContent of the ``, if the value attribute is missing.
+ * Value and textContent can be interpolated.
*
- * When an item in the `` menu is selected, the value of array element or object property
- * represented by the selected option will be bound to the model identified by the `ngModel`
- * directive of the parent select element.
+ * The {@link select.SelectController select controller} exposes utility functions that can be used
+ * to manipulate the select's behavior.
+ *
+ * ## Matching model and option values
+ *
+ * In general, the match between the model and an option is evaluated by strictly comparing the model
+ * value against the value of the available options.
+ *
+ * If you are setting the option value with the option's `value` attribute, or textContent, the
+ * value will always be a `string` which means that the model value must also be a string.
+ * Otherwise the `select` directive cannot match them correctly.
+ *
+ * To bind the model to a non-string value, you can use one of the following strategies:
+ * - the {@link ng.ngOptions `ngOptions`} directive
+ * ({@link ng.select#using-select-with-ngoptions-and-setting-a-default-value})
+ * - the {@link ng.ngValue `ngValue`} directive, which allows arbitrary expressions to be
+ * option values ({@link ng.select#using-ngvalue-to-bind-the-model-to-an-array-of-objects Example})
+ * - model $parsers / $formatters to convert the string value
+ * ({@link ng.select#binding-select-to-a-non-string-value-via-ngmodel-parsing-formatting Example})
+ *
+ * If the viewValue of `ngModel` does not match any of the options, then the control
+ * will automatically add an "unknown" option, which it then removes when the mismatch is resolved.
*
* Optionally, a single hard-coded `` element, with the value set to an empty string, can
- * be nested into the `` element. This element will then represent `null` or "not selected"
+ * be nested into the `` element. This element will then represent the `null` or "not selected"
* option. See example below for demonstration.
*
- * Note: `ngOptions` provides iterator facility for `` element which should be used instead
- * of {@link ng.directive:ngRepeat ngRepeat} when you want the
- * `select` model to be bound to a non-string value. This is because an option element can currently
- * be bound to string values only.
+ * ## Choosing between `ngRepeat` and `ngOptions`
*
- * @param {string} ngModel Assignable angular expression to data-bind to.
+ * In many cases, `ngRepeat` can be used on ` ` elements instead of {@link ng.directive:ngOptions
+ * ngOptions} to achieve a similar result. However, `ngOptions` provides some benefits:
+ * - more flexibility in how the ``'s model is assigned via the `select` **`as`** part of the
+ * comprehension expression
+ * - reduced memory consumption by not creating a new scope for each repeated instance
+ * - increased render speed by creating the options in a documentFragment instead of individually
+ *
+ * Specifically, select with repeated options slows down significantly starting at 2000 options in
+ * Chrome and Internet Explorer / Edge.
+ *
+ *
+ * @param {string} ngModel Assignable AngularJS expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
- * @param {string=} required The control is considered valid only if value is entered.
- * @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
- * the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
- * `required` when you want to data-bind to the `required` attribute.
- * @param {comprehension_expression=} ngOptions in one of the following forms:
+ * @param {string=} multiple Allows multiple options to be selected. The selected values will be
+ * bound to the model as an array.
+ * @param {string=} required Sets `required` validation error key if the value is not entered.
+ * @param {string=} ngRequired Adds required attribute and required validation constraint to
+ * the element when the ngRequired expression evaluates to true. Use ngRequired instead of required
+ * when you want to data-bind to the required attribute.
+ * @param {string=} ngChange AngularJS expression to be executed when selected option(s) changes due to user
+ * interaction with the select element.
+ * @param {string=} ngOptions sets the options that the select is populated with and defines what is
+ * set on the model on selection. See {@link ngOptions `ngOptions`}.
+ * @param {string=} ngAttrSize sets the size of the select element dynamically. Uses the
+ * {@link guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes ngAttr} directive.
*
- * * for array data sources:
- * * `label` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`for`** `value` **`in`** `array`
- * * `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * `select` **`as`** `label` **`group by`** `group` **`for`** `value` **`in`** `array`
- * * for object data sources:
- * * `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`for (`**`key` **`,`** `value`**`) in`** `object`
- * * `label` **`group by`** `group` **`for (`**`key`**`,`** `value`**`) in`** `object`
- * * `select` **`as`** `label` **`group by`** `group`
- * **`for` `(`**`key`**`,`** `value`**`) in`** `object`
- *
- * Where:
- *
- * * `array` / `object`: an expression which evaluates to an array / object to iterate over.
- * * `value`: local variable which will refer to each item in the `array` or each property value
- * of `object` during iteration.
- * * `key`: local variable which will refer to a property name in `object` during iteration.
- * * `label`: The result of this expression will be the label for `` element. The
- * `expression` will most likely refer to the `value` variable (e.g. `value.propertyName`).
- * * `select`: The result of this expression will be bound to the model of the parent ``
- * element. If not specified, `select` expression will default to `value`.
- * * `group`: The result of this expression will be used to group options using the ``
- * DOM element.
*
* @example
-
-
-
-
-
-
- Color (null not allowed):
-
-
- Color (null allowed):
-
-
- -- chose color --
-
-
-
- Color grouped by shade:
-
-
-
-
- Select
bogus .
-
- Currently selected: {{ {selected_color:color} }}
-
-
-
-
-
- it('should check ng-options', function() {
- expect(binding('{selected_color:color}')).toMatch('red');
- select('color').option('0');
- expect(binding('{selected_color:color}')).toMatch('black');
- using('.nullable').select('color').option('');
- expect(binding('{selected_color:color}')).toMatch('null');
- });
-
-
+ * ### Simple `select` elements with static options
+ *
+ *
+ *
+ *
+ *
+ * Single select:
+ *
+ * Option 1
+ * Option 2
+ *
+ *
+ * Single select with "not selected" option and dynamic option values:
+ *
+ * ---Please select---
+ * Option 1
+ * Option 2
+ *
+ * Force unknown option
+ * singleSelect = {{data.singleSelect}}
+ *
+ *
+ * Multiple select:
+ *
+ * Option 1
+ * Option 2
+ * Option 3
+ *
+ * multipleSelect = {{data.multipleSelect}}
+ *
+ *
+ *
+ *
+ * angular.module('staticSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * singleSelect: null,
+ * multipleSelect: [],
+ * option1: 'option-1'
+ * };
+ *
+ * $scope.forceUnknownOption = function() {
+ * $scope.data.singleSelect = 'nonsense';
+ * };
+ * }]);
+ *
+ *
+ *
+ * @example
+ * ### Using `ngRepeat` to generate `select` options
+ *
+ *
+ *
+ *
+ * Repeat select:
+ *
+ * {{option.name}}
+ *
+ *
+ *
+ * model = {{data.model}}
+ *
+ *
+ *
+ * angular.module('ngrepeatSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * model: null,
+ * availableOptions: [
+ * {id: '1', name: 'Option A'},
+ * {id: '2', name: 'Option B'},
+ * {id: '3', name: 'Option C'}
+ * ]
+ * };
+ * }]);
+ *
+ *
+ *
+ * @example
+ * ### Using `ngValue` to bind the model to an array of objects
+ *
+ *
+ *
+ *
+ * ngvalue select:
+ *
+ * {{option.name}}
+ *
+ *
+ *
+ *
model = {{data.model | json}}
+ *
+ *
+ *
+ * angular.module('ngvalueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * model: null,
+ * availableOptions: [
+ {value: 'myString', name: 'string'},
+ {value: 1, name: 'integer'},
+ {value: true, name: 'boolean'},
+ {value: null, name: 'null'},
+ {value: {prop: 'value'}, name: 'object'},
+ {value: ['a'], name: 'array'}
+ * ]
+ * };
+ * }]);
+ *
+ *
+ *
+ * @example
+ * ### Using `select` with `ngOptions` and setting a default value
+ * See the {@link ngOptions ngOptions documentation} for more `ngOptions` usage examples.
+ *
+ *
+ *
+ *
+ *
+ * Make a choice:
+ *
+ *
+ *
+ * option = {{data.selectedOption}}
+ *
+ *
+ *
+ * angular.module('defaultValueSelect', [])
+ * .controller('ExampleController', ['$scope', function($scope) {
+ * $scope.data = {
+ * availableOptions: [
+ * {id: '1', name: 'Option A'},
+ * {id: '2', name: 'Option B'},
+ * {id: '3', name: 'Option C'}
+ * ],
+ * selectedOption: {id: '3', name: 'Option C'} //This sets the default value of the select in the ui
+ * };
+ * }]);
+ *
+ *
+ *
+ * @example
+ * ### Binding `select` to a non-string value via `ngModel` parsing / formatting
+ *
+ *
+ *
+ *
+ * Zero
+ * One
+ * Two
+ *
+ * {{ model }}
+ *
+ *
+ * angular.module('nonStringSelect', [])
+ * .run(function($rootScope) {
+ * $rootScope.model = { id: 2 };
+ * })
+ * .directive('convertToNumber', function() {
+ * return {
+ * require: 'ngModel',
+ * link: function(scope, element, attrs, ngModel) {
+ * ngModel.$parsers.push(function(val) {
+ * return parseInt(val, 10);
+ * });
+ * ngModel.$formatters.push(function(val) {
+ * return '' + val;
+ * });
+ * }
+ * };
+ * });
+ *
+ *
+ * it('should initialize to model', function() {
+ * expect(element(by.model('model.id')).$('option:checked').getText()).toEqual('Two');
+ * });
+ *
+ *
+ *
*/
-
-var ngOptionsDirective = valueFn({ terminal: true });
-var selectDirective = ['$compile', '$parse', function($compile, $parse) {
- //0000111110000000000022220000000000000000000000333300000000000000444444444444444440000000005555555555555555500000006666666666666666600000000000000077770
- var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/,
- nullModelCtrl = {$setViewValue: noop};
+var selectDirective = function() {
return {
restrict: 'E',
require: ['select', '?ngModel'],
- controller: ['$element', '$scope', '$attrs', function($element, $scope, $attrs) {
- var self = this,
- optionsMap = {},
- ngModelCtrl = nullModelCtrl,
- nullOption,
- unknownOption;
+ controller: SelectController,
+ priority: 1,
+ link: {
+ pre: selectPreLink,
+ post: selectPostLink
+ }
+ };
+ function selectPreLink(scope, element, attr, ctrls) {
- self.databound = $attrs.ngModel;
+ var selectCtrl = ctrls[0];
+ var ngModelCtrl = ctrls[1];
-
- self.init = function(ngModelCtrl_, nullOption_, unknownOption_) {
- ngModelCtrl = ngModelCtrl_;
- nullOption = nullOption_;
- unknownOption = unknownOption_;
+ // if ngModel is not defined, we don't need to do anything but set the registerOption
+ // function to noop, so options don't get added internally
+ if (!ngModelCtrl) {
+ selectCtrl.registerOption = noop;
+ return;
}
- self.addOption = function(value) {
- optionsMap[value] = true;
+ selectCtrl.ngModelCtrl = ngModelCtrl;
- if (ngModelCtrl.$viewValue == value) {
- $element.val(value);
- if (unknownOption.parent()) unknownOption.remove();
- }
- };
-
-
- self.removeOption = function(value) {
- if (this.hasOption(value)) {
- delete optionsMap[value];
- if (ngModelCtrl.$viewValue == value) {
- this.renderUnknownOption(value);
- }
- }
- };
-
-
- self.renderUnknownOption = function(val) {
- var unknownVal = '? ' + hashKey(val) + ' ?';
- unknownOption.val(unknownVal);
- $element.prepend(unknownOption);
- $element.val(unknownVal);
- unknownOption.prop('selected', true); // needed for IE
- }
-
-
- self.hasOption = function(value) {
- return optionsMap.hasOwnProperty(value);
- }
-
- $scope.$on('$destroy', function() {
- // disable unknown option so that we don't do work when the whole select is being destroyed
- self.renderUnknownOption = noop;
+ // When the selected item(s) changes we delegate getting the value of the select control
+ // to the `readValue` method, which can be changed if the select can have multiple
+ // selected values or if the options are being generated by `ngOptions`
+ element.on('change', function() {
+ selectCtrl.removeUnknownOption();
+ scope.$apply(function() {
+ ngModelCtrl.$setViewValue(selectCtrl.readValue());
+ });
});
- }],
- link: function(scope, element, attr, ctrls) {
- // if ngModel is not defined, we don't need to do anything
- if (!ctrls[1]) return;
+ // If the select allows multiple values then we need to modify how we read and write
+ // values from and to the control; also what it means for the value to be empty and
+ // we have to add an extra watch since ngModel doesn't work well with arrays - it
+ // doesn't trigger rendering if only an item in the array changes.
+ if (attr.multiple) {
+ selectCtrl.multiple = true;
- var selectCtrl = ctrls[0],
- ngModelCtrl = ctrls[1],
- multiple = attr.multiple,
- optionsExp = attr.ngOptions,
- nullOption = false, // if false, user will not be able to select it (used by ngOptions)
- emptyOption,
- // we can't just jqLite('') since jqLite is not smart enough
- // to create it in and IE barfs otherwise.
- optionTemplate = jqLite(document.createElement('option')),
- optGroupTemplate =jqLite(document.createElement('optgroup')),
- unknownOption = optionTemplate.clone();
-
- // find "null" option
- for(var i = 0, children = element.children(), ii = children.length; i < ii; i++) {
- if (children[i].value == '') {
- emptyOption = nullOption = children.eq(i);
- break;
- }
- }
-
- selectCtrl.init(ngModelCtrl, nullOption, unknownOption);
-
- // required validator
- if (multiple && (attr.required || attr.ngRequired)) {
- var requiredValidator = function(value) {
- ngModelCtrl.$setValidity('required', !attr.required || (value && value.length));
- return value;
- };
-
- ngModelCtrl.$parsers.push(requiredValidator);
- ngModelCtrl.$formatters.unshift(requiredValidator);
-
- attr.$observe('required', function() {
- requiredValidator(ngModelCtrl.$viewValue);
- });
- }
-
- if (optionsExp) Options(scope, element, ngModelCtrl);
- else if (multiple) Multiple(scope, element, ngModelCtrl);
- else Single(scope, element, ngModelCtrl, selectCtrl);
-
-
- ////////////////////////////
-
-
-
- function Single(scope, selectElement, ngModelCtrl, selectCtrl) {
- ngModelCtrl.$render = function() {
- var viewValue = ngModelCtrl.$viewValue;
-
- if (selectCtrl.hasOption(viewValue)) {
- if (unknownOption.parent()) unknownOption.remove();
- selectElement.val(viewValue);
- if (viewValue === '') emptyOption.prop('selected', true); // to make IE9 happy
- } else {
- if (isUndefined(viewValue) && emptyOption) {
- selectElement.val('');
- } else {
- selectCtrl.renderUnknownOption(viewValue);
+ // Read value now needs to check each option to see if it is selected
+ selectCtrl.readValue = function readMultipleValue() {
+ var array = [];
+ forEach(element.find('option'), function(option) {
+ if (option.selected && !option.disabled) {
+ var val = option.value;
+ array.push(val in selectCtrl.selectValueMap ? selectCtrl.selectValueMap[val] : val);
}
- }
+ });
+ return array;
};
- selectElement.bind('change', function() {
- scope.$apply(function() {
- if (unknownOption.parent()) unknownOption.remove();
- ngModelCtrl.$setViewValue(selectElement.val());
- });
- });
- }
+ // Write value now needs to set the selected property of each matching option
+ selectCtrl.writeValue = function writeMultipleValue(value) {
+ forEach(element.find('option'), function(option) {
+ var shouldBeSelected = !!value && (includes(value, option.value) ||
+ includes(value, selectCtrl.selectValueMap[option.value]));
+ var currentlySelected = option.selected;
+
+ // Support: IE 9-11 only, Edge 12-15+
+ // In IE and Edge adding options to the selection via shift+click/UP/DOWN
+ // will de-select already selected options if "selected" on those options was set
+ // more than once (i.e. when the options were already selected)
+ // So we only modify the selected property if necessary.
+ // Note: this behavior cannot be replicated via unit tests because it only shows in the
+ // actual user interface.
+ if (shouldBeSelected !== currentlySelected) {
+ setOptionSelectedStatus(jqLite(option), shouldBeSelected);
+ }
- function Multiple(scope, selectElement, ctrl) {
- var lastView;
- ctrl.$render = function() {
- var items = new HashMap(ctrl.$viewValue);
- forEach(selectElement.find('option'), function(option) {
- option.selected = isDefined(items.get(option.value));
});
};
// we have to do it on each watch since ngModel watches reference, but
// we need to work of an array, so we need to see if anything was inserted/removed
+ var lastView, lastViewRef = NaN;
scope.$watch(function selectMultipleWatch() {
- if (!equals(lastView, ctrl.$viewValue)) {
- lastView = copy(ctrl.$viewValue);
- ctrl.$render();
+ if (lastViewRef === ngModelCtrl.$viewValue && !equals(lastView, ngModelCtrl.$viewValue)) {
+ lastView = shallowCopy(ngModelCtrl.$viewValue);
+ ngModelCtrl.$render();
}
+ lastViewRef = ngModelCtrl.$viewValue;
});
- selectElement.bind('change', function() {
- scope.$apply(function() {
- var array = [];
- forEach(selectElement.find('option'), function(option) {
- if (option.selected) {
- array.push(option.value);
- }
- });
- ctrl.$setViewValue(array);
- });
- });
- }
+ // If we are a multiple select then value is now a collection
+ // so the meaning of $isEmpty changes
+ ngModelCtrl.$isEmpty = function(value) {
+ return !value || value.length === 0;
+ };
- function Options(scope, selectElement, ctrl) {
- var match;
-
- if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) {
- throw Error(
- "Expected ngOptions in form of '_select_ (as _label_)? for (_key_,)?_value_ in _collection_'" +
- " but got '" + optionsExp + "'.");
- }
-
- var displayFn = $parse(match[2] || match[1]),
- valueName = match[4] || match[6],
- keyName = match[5],
- groupByFn = $parse(match[3] || ''),
- valueFn = $parse(match[2] ? match[1] : valueName),
- valuesFn = $parse(match[7]),
- // This is an array of array of existing option groups in DOM. We try to reuse these if possible
- // optionGroupsCache[0] is the options with no option group
- // optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
- optionGroupsCache = [[{element: selectElement, label:''}]];
-
- if (nullOption) {
- // compile the element since there might be bindings in it
- $compile(nullOption)(scope);
-
- // remove the class, which is added automatically because we recompile the element and it
- // becomes the compilation root
- nullOption.removeClass('ng-scope');
-
- // we need to remove it before calling selectElement.html('') because otherwise IE will
- // remove the label from the element. wtf?
- nullOption.remove();
- }
-
- // clear contents, we'll add what's needed based on the model
- selectElement.html('');
-
- selectElement.bind('change', function() {
- scope.$apply(function() {
- var optionGroup,
- collection = valuesFn(scope) || [],
- locals = {},
- key, value, optionElement, index, groupIndex, length, groupLength;
-
- if (multiple) {
- value = [];
- for (groupIndex = 0, groupLength = optionGroupsCache.length;
- groupIndex < groupLength;
- groupIndex++) {
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroupsCache[groupIndex];
-
- for(index = 1, length = optionGroup.length; index < length; index++) {
- if ((optionElement = optionGroup[index].element)[0].selected) {
- key = optionElement.val();
- if (keyName) locals[keyName] = key;
- locals[valueName] = collection[key];
- value.push(valueFn(scope, locals));
- }
- }
- }
- } else {
- key = selectElement.val();
- if (key == '?') {
- value = undefined;
- } else if (key == ''){
- value = null;
- } else {
- locals[valueName] = collection[key];
- if (keyName) locals[keyName] = key;
- value = valueFn(scope, locals);
- }
- }
- ctrl.$setViewValue(value);
- });
- });
-
- ctrl.$render = render;
-
- // TODO(vojta): can't we optimize this ?
- scope.$watch(render);
-
- function render() {
- var optionGroups = {'':[]}, // Temporary location for the option groups before we render them
- optionGroupNames = [''],
- optionGroupName,
- optionGroup,
- option,
- existingParent, existingOptions, existingOption,
- modelValue = ctrl.$modelValue,
- values = valuesFn(scope) || [],
- keys = keyName ? sortedKeys(values) : values,
- groupLength, length,
- groupIndex, index,
- locals = {},
- selected,
- selectedSet = false, // nothing is selected yet
- lastElement,
- element,
- label;
-
- if (multiple) {
- selectedSet = new HashMap(modelValue);
- }
-
- // We now build up the list of options we need (we merge later)
- for (index = 0; length = keys.length, index < length; index++) {
- locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index];
- optionGroupName = groupByFn(scope, locals) || '';
- if (!(optionGroup = optionGroups[optionGroupName])) {
- optionGroup = optionGroups[optionGroupName] = [];
- optionGroupNames.push(optionGroupName);
- }
- if (multiple) {
- selected = selectedSet.remove(valueFn(scope, locals)) != undefined;
- } else {
- selected = modelValue === valueFn(scope, locals);
- selectedSet = selectedSet || selected; // see if at least one item is selected
- }
- label = displayFn(scope, locals); // what will be seen by the user
- label = label === undefined ? '' : label; // doing displayFn(scope, locals) || '' overwrites zero values
- optionGroup.push({
- id: keyName ? keys[index] : index, // either the index into array or key from object
- label: label,
- selected: selected // determine if we should be selected
- });
- }
- if (!multiple) {
- if (nullOption || modelValue === null) {
- // insert null option if we have a placeholder, or the model is null
- optionGroups[''].unshift({id:'', label:'', selected:!selectedSet});
- } else if (!selectedSet) {
- // option could not be found, we have to insert the undefined item
- optionGroups[''].unshift({id:'?', label:'', selected:true});
- }
- }
-
- // Now we need to update the list of DOM nodes to match the optionGroups we computed above
- for (groupIndex = 0, groupLength = optionGroupNames.length;
- groupIndex < groupLength;
- groupIndex++) {
- // current option group name or '' if no group
- optionGroupName = optionGroupNames[groupIndex];
-
- // list of options for that group. (first item has the parent)
- optionGroup = optionGroups[optionGroupName];
-
- if (optionGroupsCache.length <= groupIndex) {
- // we need to grow the optionGroups
- existingParent = {
- element: optGroupTemplate.clone().attr('label', optionGroupName),
- label: optionGroup.label
- };
- existingOptions = [existingParent];
- optionGroupsCache.push(existingOptions);
- selectElement.append(existingParent.element);
- } else {
- existingOptions = optionGroupsCache[groupIndex];
- existingParent = existingOptions[0]; // either SELECT (no group) or OPTGROUP element
-
- // update the OPTGROUP label if not the same.
- if (existingParent.label != optionGroupName) {
- existingParent.element.attr('label', existingParent.label = optionGroupName);
- }
- }
-
- lastElement = null; // start at the beginning
- for(index = 0, length = optionGroup.length; index < length; index++) {
- option = optionGroup[index];
- if ((existingOption = existingOptions[index+1])) {
- // reuse elements
- lastElement = existingOption.element;
- if (existingOption.label !== option.label) {
- lastElement.text(existingOption.label = option.label);
- }
- if (existingOption.id !== option.id) {
- lastElement.val(existingOption.id = option.id);
- }
- // lastElement.prop('selected') provided by jQuery has side-effects
- if (lastElement[0].selected !== option.selected) {
- lastElement.prop('selected', (existingOption.selected = option.selected));
- }
- } else {
- // grow elements
-
- // if it's a null option
- if (option.id === '' && nullOption) {
- // put back the pre-compiled element
- element = nullOption;
- } else {
- // jQuery(v1.4.2) Bug: We should be able to chain the method calls, but
- // in this version of jQuery on some browser the .text() returns a string
- // rather then the element.
- (element = optionTemplate.clone())
- .val(option.id)
- .attr('selected', option.selected)
- .text(option.label);
- }
-
- existingOptions.push(existingOption = {
- element: element,
- label: option.label,
- id: option.id,
- selected: option.selected
- });
- if (lastElement) {
- lastElement.after(element);
- } else {
- existingParent.element.append(element);
- }
- lastElement = element;
- }
- }
- // remove any excessive OPTIONs in a group
- index++; // increment since the existingOptions[0] is parent element not OPTION
- while(existingOptions.length > index) {
- existingOptions.pop().element.remove();
- }
- }
- // remove any excessive OPTGROUPs from select
- while(optionGroupsCache.length > groupIndex) {
- optionGroupsCache.pop()[0].element.remove();
- }
- }
}
}
- }
-}];
+ function selectPostLink(scope, element, attrs, ctrls) {
+ // if ngModel is not defined, we don't need to do anything
+ var ngModelCtrl = ctrls[1];
+ if (!ngModelCtrl) return;
+
+ var selectCtrl = ctrls[0];
+
+ // We delegate rendering to the `writeValue` method, which can be changed
+ // if the select can have multiple selected values or if the options are being
+ // generated by `ngOptions`.
+ // This must be done in the postLink fn to prevent $render to be called before
+ // all nodes have been linked correctly.
+ ngModelCtrl.$render = function() {
+ selectCtrl.writeValue(ngModelCtrl.$viewValue);
+ };
+ }
+};
+
+
+// The option directive is purely designed to communicate the existence (or lack of)
+// of dynamically created (and destroyed) option elements to their containing select
+// directive via its controller.
var optionDirective = ['$interpolate', function($interpolate) {
- var nullSelectCtrl = {
- addOption: noop,
- removeOption: noop
- };
-
return {
restrict: 'E',
priority: 100,
compile: function(element, attr) {
- if (isUndefined(attr.value)) {
- var interpolateFn = $interpolate(element.text(), true);
- if (!interpolateFn) {
+ var interpolateValueFn, interpolateTextFn;
+
+ if (isDefined(attr.ngValue)) {
+ // Will be handled by registerOption
+ } else if (isDefined(attr.value)) {
+ // If the value attribute is defined, check if it contains an interpolation
+ interpolateValueFn = $interpolate(attr.value, true);
+ } else {
+ // If the value attribute is not defined then we fall back to the
+ // text content of the option element, which may be interpolated
+ interpolateTextFn = $interpolate(element.text(), true);
+ if (!interpolateTextFn) {
attr.$set('value', element.text());
}
}
- return function (scope, element, attr) {
+ return function(scope, element, attr) {
+ // This is an optimization over using ^^ since we don't want to have to search
+ // all the way to the root of the DOM for every single option element
var selectCtrlName = '$selectController',
parent = element.parent(),
selectCtrl = parent.data(selectCtrlName) ||
parent.parent().data(selectCtrlName); // in case we are in optgroup
- if (selectCtrl && selectCtrl.databound) {
- // For some reason Opera defaults to true and if not overridden this messes up the repeater.
- // We don't want the view to drive the initialization of the model anyway.
- element.prop('selected', false);
- } else {
- selectCtrl = nullSelectCtrl;
+ if (selectCtrl) {
+ selectCtrl.registerOption(scope, element, attr, interpolateValueFn, interpolateTextFn);
}
-
- if (interpolateFn) {
- scope.$watch(interpolateFn, function interpolateWatchAction(newVal, oldVal) {
- attr.$set('value', newVal);
- if (newVal !== oldVal) selectCtrl.removeOption(oldVal);
- selectCtrl.addOption(newVal);
- });
- } else {
- selectCtrl.addOption(attr.value);
- }
-
- element.bind('$destroy', function() {
- selectCtrl.removeOption(attr.value);
- });
};
}
- }
+ };
}];
-var styleDirective = valueFn({
- restrict: 'E',
- terminal: true
+/**
+ * @ngdoc directive
+ * @name ngRequired
+ * @restrict A
+ *
+ * @param {expression} ngRequired AngularJS expression. If it evaluates to `true`, it sets the
+ * `required` attribute to the element and adds the `required`
+ * {@link ngModel.NgModelController#$validators `validator`}.
+ *
+ * @description
+ *
+ * ngRequired adds the required {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for {@link input `input`} and {@link select `select`} controls, but can also be
+ * applied to custom controls.
+ *
+ * The directive sets the `required` attribute on the element if the AngularJS expression inside
+ * `ngRequired` evaluates to true. A special directive for setting `required` is necessary because we
+ * cannot use interpolation inside `required`. See the {@link guide/interpolation interpolation guide}
+ * for more info.
+ *
+ * The validator will set the `required` error key to true if the `required` attribute is set and
+ * calling {@link ngModel.NgModelController#$isEmpty `NgModelController.$isEmpty`} with the
+ * {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`} returns `true`. For example, the
+ * `$isEmpty()` implementation for `input[text]` checks the length of the `$viewValue`. When developing
+ * custom controls, `$isEmpty()` can be overwritten to account for a $viewValue that is not string-based.
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ * Toggle required:
+ *
+ *
+ * This input must be filled if `required` is true:
+ *
+ *
+ * required error set? = {{form.input.$error.required}}
+ * model = {{model}}
+ *
+ *
+ *
+ *
+ var required = element(by.binding('form.input.$error.required'));
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should set the required error', function() {
+ expect(required.getText()).toContain('true');
+
+ input.sendKeys('123');
+ expect(required.getText()).not.toContain('true');
+ expect(model.getText()).toContain('123');
+ });
+ *
+ *
+ */
+var requiredDirective = ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ require: '?ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+ // For boolean attributes like required, presence means true
+ var value = attr.hasOwnProperty('required') || $parse(attr.ngRequired)(scope);
+
+ if (!attr.ngRequired) {
+ // force truthy in case we are on non input element
+ // (input elements do this automatically for boolean attributes like required)
+ attr.required = true;
+ }
+
+ ctrl.$validators.required = function(modelValue, viewValue) {
+ return !value || !ctrl.$isEmpty(viewValue);
+ };
+
+ attr.$observe('required', function(newVal) {
+
+ if (value !== newVal) {
+ value = newVal;
+ ctrl.$validate();
+ }
+ });
+ }
+ };
+}];
+
+/**
+ * @ngdoc directive
+ * @name ngPattern
+ * @restrict A
+ *
+ * @param {expression|RegExp} ngPattern AngularJS expression that must evaluate to a `RegExp` or a `String`
+ * parsable into a `RegExp`, or a `RegExp` literal. See above for
+ * more details.
+ *
+ * @description
+ *
+ * ngPattern adds the pattern {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `pattern` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * does not match a RegExp which is obtained from the `ngPattern` attribute value:
+ * - the value is an AngularJS expression:
+ * - If the expression evaluates to a RegExp object, then this is used directly.
+ * - If the expression evaluates to a string, then it will be converted to a RegExp after wrapping it
+ * in `^` and `$` characters. For instance, `"abc"` will be converted to `new RegExp('^abc$')`.
+ * - If the value is a RegExp literal, e.g. `ngPattern="/^\d+$/"`, it is used directly.
+ *
+ *
+ * **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
+ * start at the index of the last search's match, thus not taking the whole input value into
+ * account.
+ *
+ *
+ *
+ * **Note:** This directive is also added when the plain `pattern` attribute is used, with two
+ * differences:
+ *
+ *
+ * `ngPattern` does not set the `pattern` attribute and therefore HTML5 constraint validation is
+ * not available.
+ *
+ *
+ * The `ngPattern` attribute must be an expression, while the `pattern` value must be
+ * interpolated.
+ *
+ *
+ *
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ * Set a pattern (regex string):
+ *
+ *
+ * This input is restricted by the current pattern:
+ *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}}
+ *
+ *
+ *
+ *
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default pattern', function() {
+ input.sendKeys('aaa');
+ expect(model.getText()).not.toContain('aaa');
+
+ input.clear().then(function() {
+ input.sendKeys('123');
+ expect(model.getText()).toContain('123');
+ });
+ });
+ *
+ *
+ */
+var patternDirective = ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ require: '?ngModel',
+ compile: function(tElm, tAttr) {
+ var patternExp;
+ var parseFn;
+
+ if (tAttr.ngPattern) {
+ patternExp = tAttr.ngPattern;
+
+ // ngPattern might be a scope expression, or an inlined regex, which is not parsable.
+ // We get value of the attribute here, so we can compare the old and the new value
+ // in the observer to avoid unnecessary validations
+ if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
+ parseFn = function() { return tAttr.ngPattern; };
+ } else {
+ parseFn = $parse(tAttr.ngPattern);
+ }
+ }
+
+ return function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var attrVal = attr.pattern;
+
+ if (attr.ngPattern) {
+ attrVal = parseFn(scope);
+ } else {
+ patternExp = attr.pattern;
+ }
+
+ var regexp = parsePatternAttr(attrVal, patternExp, elm);
+
+ attr.$observe('pattern', function(newVal) {
+ var oldRegexp = regexp;
+
+ regexp = parsePatternAttr(newVal, patternExp, elm);
+
+ if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
+ ctrl.$validate();
+ }
+ });
+
+ ctrl.$validators.pattern = function(modelValue, viewValue) {
+ // HTML5 pattern constraint validates the input value, so we validate the viewValue
+ return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
+ };
+ };
+ }
+
+ };
+}];
+
+/**
+ * @ngdoc directive
+ * @name ngMaxlength
+ * @restrict A
+ *
+ * @param {expression} ngMaxlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `maxlength`
+ * {@link ngModel.NgModelController#$validators validator}.
+ *
+ * @description
+ *
+ * ngMaxlength adds the maxlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `maxlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is longer than the integer obtained by evaluating the AngularJS expression given in the
+ * `ngMaxlength` attribute value.
+ *
+ *
+ * **Note:** This directive is also added when the plain `maxlength` attribute is used, with two
+ * differences:
+ *
+ *
+ * `ngMaxlength` does not set the `maxlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ *
+ *
+ * The `ngMaxlength` attribute must be an expression, while the `maxlength` value must be
+ * interpolated.
+ *
+ *
+ *
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ * Set a maxlength:
+ *
+ *
+ * This input is restricted by the current maxlength:
+ *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}}
+ *
+ *
+ *
+ *
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default maxlength', function() {
+ input.sendKeys('abcdef');
+ expect(model.getText()).not.toContain('abcdef');
+
+ input.clear().then(function() {
+ input.sendKeys('abcde');
+ expect(model.getText()).toContain('abcde');
+ });
+ });
+ *
+ *
+ */
+var maxlengthDirective = ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ require: '?ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
+ var maxlengthParsed = parseLength(maxlength);
+
+ attr.$observe('maxlength', function(value) {
+ if (maxlength !== value) {
+ maxlengthParsed = parseLength(value);
+ maxlength = value;
+ ctrl.$validate();
+ }
+ });
+ ctrl.$validators.maxlength = function(modelValue, viewValue) {
+ return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
+ };
+ }
+ };
+}];
+
+/**
+ * @ngdoc directive
+ * @name ngMinlength
+ * @restrict A
+ *
+ * @param {expression} ngMinlength AngularJS expression that must evaluate to a `Number` or `String`
+ * parsable into a `Number`. Used as value for the `minlength`
+ * {@link ngModel.NgModelController#$validators validator}.
+ *
+ * @description
+ *
+ * ngMinlength adds the minlength {@link ngModel.NgModelController#$validators `validator`} to {@link ngModel `ngModel`}.
+ * It is most often used for text-based {@link input `input`} controls, but can also be applied to custom text-based controls.
+ *
+ * The validator sets the `minlength` error key if the {@link ngModel.NgModelController#$viewValue `ngModel.$viewValue`}
+ * is shorter than the integer obtained by evaluating the AngularJS expression given in the
+ * `ngMinlength` attribute value.
+ *
+ *
+ * **Note:** This directive is also added when the plain `minlength` attribute is used, with two
+ * differences:
+ *
+ *
+ * `ngMinlength` does not set the `minlength` attribute and therefore HTML5 constraint
+ * validation is not available.
+ *
+ *
+ * The `ngMinlength` value must be an expression, while the `minlength` value must be
+ * interpolated.
+ *
+ *
+ *
+ *
+ * @example
+ *
+ *
+ *
+ *
+ *
+ * Set a minlength:
+ *
+ *
+ * This input is restricted by the current minlength:
+ *
+ *
+ * input valid? = {{form.input.$valid}}
+ * model = {{model}}
+ *
+ *
+ *
+ *
+ var model = element(by.binding('model'));
+ var input = element(by.id('input'));
+
+ it('should validate the input with the default minlength', function() {
+ input.sendKeys('ab');
+ expect(model.getText()).not.toContain('ab');
+
+ input.sendKeys('abc');
+ expect(model.getText()).toContain('abc');
+ });
+ *
+ *
+ */
+var minlengthDirective = ['$parse', function($parse) {
+ return {
+ restrict: 'A',
+ require: '?ngModel',
+ link: function(scope, elm, attr, ctrl) {
+ if (!ctrl) return;
+
+ var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
+ var minlengthParsed = parseLength(minlength) || -1;
+
+ attr.$observe('minlength', function(value) {
+ if (minlength !== value) {
+ minlengthParsed = parseLength(value) || -1;
+ minlength = value;
+ ctrl.$validate();
+ }
+
+ });
+ ctrl.$validators.minlength = function(modelValue, viewValue) {
+ return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
+ };
+ }
+ };
+}];
+
+
+function parsePatternAttr(regex, patternExp, elm) {
+ if (!regex) return undefined;
+
+ if (isString(regex)) {
+ regex = new RegExp('^' + regex + '$');
+ }
+
+ if (!regex.test) {
+ throw minErr('ngPattern')('noregexp',
+ 'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
+ regex, startingTag(elm));
+ }
+
+ return regex;
+}
+
+function parseLength(val) {
+ var intVal = toInt(val);
+ return isNumberNaN(intVal) ? -1 : intVal;
+}
+
+if (window.angular.bootstrap) {
+ // AngularJS is already loaded, so we can return here...
+ if (window.console) {
+ console.log('WARNING: Tried to load AngularJS more than once.');
+ }
+ return;
+}
+
+// try to bind to jquery now so that one can write jqLite(fn)
+// but we will rebind on bootstrap again.
+bindJQuery();
+
+publishExternalAPI(angular);
+
+angular.module("ngLocale", [], ["$provide", function($provide) {
+var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
+function getDecimals(n) {
+ n = n + '';
+ var i = n.indexOf('.');
+ return (i == -1) ? 0 : n.length - i - 1;
+}
+
+function getVF(n, opt_precision) {
+ var v = opt_precision;
+
+ if (undefined === v) {
+ v = Math.min(getDecimals(n), 3);
+ }
+
+ var base = Math.pow(10, v);
+ var f = ((n * base) | 0) % base;
+ return {v: v, f: f};
+}
+
+$provide.value("$locale", {
+ "DATETIME_FORMATS": {
+ "AMPMS": [
+ "AM",
+ "PM"
+ ],
+ "DAY": [
+ "Sunday",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday"
+ ],
+ "ERANAMES": [
+ "Before Christ",
+ "Anno Domini"
+ ],
+ "ERAS": [
+ "BC",
+ "AD"
+ ],
+ "FIRSTDAYOFWEEK": 6,
+ "MONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+ "SHORTDAY": [
+ "Sun",
+ "Mon",
+ "Tue",
+ "Wed",
+ "Thu",
+ "Fri",
+ "Sat"
+ ],
+ "SHORTMONTH": [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec"
+ ],
+ "STANDALONEMONTH": [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December"
+ ],
+ "WEEKENDRANGE": [
+ 5,
+ 6
+ ],
+ "fullDate": "EEEE, MMMM d, y",
+ "longDate": "MMMM d, y",
+ "medium": "MMM d, y h:mm:ss a",
+ "mediumDate": "MMM d, y",
+ "mediumTime": "h:mm:ss a",
+ "short": "M/d/yy h:mm a",
+ "shortDate": "M/d/yy",
+ "shortTime": "h:mm a"
+ },
+ "NUMBER_FORMATS": {
+ "CURRENCY_SYM": "$",
+ "DECIMAL_SEP": ".",
+ "GROUP_SEP": ",",
+ "PATTERNS": [
+ {
+ "gSize": 3,
+ "lgSize": 3,
+ "maxFrac": 3,
+ "minFrac": 0,
+ "minInt": 1,
+ "negPre": "-",
+ "negSuf": "",
+ "posPre": "",
+ "posSuf": ""
+ },
+ {
+ "gSize": 3,
+ "lgSize": 3,
+ "maxFrac": 2,
+ "minFrac": 2,
+ "minInt": 1,
+ "negPre": "-\u00a4",
+ "negSuf": "",
+ "posPre": "\u00a4",
+ "posSuf": ""
+ }
+ ]
+ },
+ "id": "en-us",
+ "localeID": "en_US",
+ "pluralCat": function(n, opt_precision) { var i = n | 0; var vf = getVF(n, opt_precision); if (i == 1 && vf.v == 0) { return PLURAL_CATEGORY.ONE; } return PLURAL_CATEGORY.OTHER;}
});
+}]);
- //try to bind to jquery now so that one can write angular.element().read()
- //but we will rebind on bootstrap again.
- bindJQuery();
-
- publishExternalAPI(angular);
-
- jqLite(document).ready(function() {
- angularInit(document, bootstrap);
+ jqLite(function() {
+ angularInit(window.document, bootstrap);
});
-})(window, document);
-angular.element(document).find('head').append('');
\ No newline at end of file
+})(window);
+
+!window.angular.$$csp().noInlineStyle && window.angular.element(document.head).prepend(window.angular.element('