'use strict';
/* global VALID_CLASS: true,
INVALID_CLASS: true,
PRISTINE_CLASS: true,
DIRTY_CLASS: true,
UNTOUCHED_CLASS: true,
TOUCHED_CLASS: true,
*/
// Regex code is obtained from SO: https://stackoverflow.com/questions/3143070/javascript-regex-iso-datetime#answer-3143231
var ISO_DATE_REGEXP = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/;
var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/;
var MONTH_REGEXP = /^(\d{4})-(\d\d)$/;
var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/;
var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/;
var $ngModelMinErr = new minErr('ngModel');
var inputType = {
/**
* @ngdoc input
* @name input[text]
*
* @description
* Standard HTML text input with angular data binding, inherited by most of the `input` elements.
*
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required Adds `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 {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=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
* a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object then this is used directly.
* If the expression is 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$')`.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
* @example
var text = element(by.binding('text'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('text'));
it('should initialize to model', function() {
expect(text.getText()).toContain('guest');
expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
input.clear();
input.sendKeys('');
expect(text.getText()).toEqual('text =');
expect(valid.getText()).toContain('false');
});
it('should be invalid if multi word', function() {
input.clear();
input.sendKeys('hello world');
expect(valid.getText()).toContain('false');
});
*/
'text': textInputType,
/**
* @ngdoc input
* @name input[date]
*
* @description
* Input with date validation and transformation. In browsers that do not yet support
* the HTML5 date input, a text element will be used. In that case, text must be entered in a valid ISO-8601
* date format (yyyy-MM-dd), for example: `2009-01-06`. Since many
* modern browsers do not yet support this input type, it is important to provide cues to users on the
* expected input format via a placeholder or label.
*
* The model must always be a Date object, otherwise Angular 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.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO date string (yyyy-MM-dd).
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
* a valid ISO date string (yyyy-MM-dd).
* @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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value | date: "yyyy-MM-dd"'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
// 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 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 Angular 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.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO datetime format (yyyy-MM-ddTHH:mm:ss).
* @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).
* @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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value | date: "yyyy-MM-ddTHH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
// 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 input
* @name input[time]
*
* @description
* Input with time validation and transformation. In browsers that do not yet support
* the HTML5 date input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
* 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 Angular 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.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO time format (HH:mm:ss).
* @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).
* @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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value | date: "HH:mm:ss"'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
// 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 Angular 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.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
* valid ISO week format (yyyy-W##).
* @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##).
* @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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value | date: "yyyy-Www"'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
// 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 Angular 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 angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be
* a valid ISO month format (yyyy-MM).
* @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).
* @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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value | date: "yyyy-MM"'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
// 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.
*
* The model must always be a number, otherwise Angular will throw an error.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
* @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 {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=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
* a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object then this is used directly.
* If the expression is 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$')`.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var value = element(by.binding('value'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('value'));
it('should initialize to model', function() {
expect(value.getText()).toContain('12');
expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
input.clear();
input.sendKeys('');
expect(value.getText()).toEqual('value =');
expect(valid.getText()).toContain('false');
});
it('should be invalid if over max', function() {
input.clear();
input.sendKeys('123');
expect(value.getText()).toEqual('value =');
expect(valid.getText()).toContain('false');
});
*/
'number': numberInputType,
/**
* @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.
*
*
* **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 angular 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
* 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 {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=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
* a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object then this is used directly.
* If the expression is 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$')`.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var text = element(by.binding('text'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('text'));
it('should initialize to model', function() {
expect(text.getText()).toContain('http://google.com');
expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
input.clear();
input.sendKeys('');
expect(text.getText()).toEqual('text =');
expect(valid.getText()).toContain('false');
});
it('should be invalid if not url', function() {
input.clear();
input.sendKeys('box');
expect(valid.getText()).toContain('false');
});
*/
'url': urlInputType,
/**
* @ngdoc input
* @name input[email]
*
* @description
* Text input with email validation. Sets the `email` validation error key if not a valid email
* address.
*
*
* **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
* used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can
* use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide})
*
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {string=} required 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 {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=} pattern Similar to `ngPattern` except that the attribute value is the actual string
* that contains the regular expression body that will be converted to a regular expression
* as in the ngPattern directive.
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel value does not match
* a RegExp found by evaluating the Angular expression given in the attribute value.
* If the expression evaluates to a RegExp object then this is used directly.
* If the expression is 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$')`.
* @param {string=} ngChange Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
var text = element(by.binding('text'));
var valid = element(by.binding('myForm.input.$valid'));
var input = element(by.model('text'));
it('should initialize to model', function() {
expect(text.getText()).toContain('me@example.com');
expect(valid.getText()).toContain('true');
});
it('should be invalid if empty', function() {
input.clear();
input.sendKeys('');
expect(text.getText()).toEqual('text =');
expect(valid.getText()).toContain('false');
});
it('should be invalid if not email', function() {
input.clear();
input.sendKeys('xxx');
expect(valid.getText()).toContain('false');
});
*/
'email': emailInputType,
/**
* @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.
* @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
* interaction with the input element.
* @param {string} ngValue Angular expression which sets the value to which the expression should
* be set when selected.
*
* @example
Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
it('should change state', function() {
var color = element(by.binding('color'));
expect(color.getText()).toContain('blue');
element.all(by.model('color')).get(0).click();
expect(color.getText()).toContain('red');
});
*/
'radio': radioInputType,
/**
* @ngdoc input
* @name input[checkbox]
*
* @description
* HTML checkbox.
*
* @param {string} ngModel Assignable angular expression to data-bind to.
* @param {string=} name Property name of the form under which the control is published.
* @param {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 Angular expression to be executed when input changes due to user
* interaction with the input element.
*
* @example
it('should change state', function() {
var value1 = element(by.binding('value1'));
var value2 = element(by.binding('value2'));
expect(value1.getText()).toContain('true');
expect(value2.getText()).toContain('YES');
element(by.model('value1')).click();
element(by.model('value2')).click();
expect(value1.getText()).toContain('false');
expect(value2.getText()).toContain('NO');
});
*/
'checkbox': checkboxInputType,
'hidden': noop,
'button': noop,
'submit': noop,
'reset': noop,
'file': noop
};
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);
}
function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
var placeholder = element[0].placeholder, noevent = {};
var type = lowercase(element[0].type);
// In composition mode, users are still inputing intermediate text buffer,
// hold the listener until composition is done.
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
if (!$sniffer.android) {
var composing = false;
element.on('compositionstart', function(data) {
composing = true;
});
element.on('compositionend', function() {
composing = false;
listener();
});
}
var listener = function(ev) {
if (composing) return;
var value = element.val(),
event = ev && ev.type;
// IE (11 and under) seem to emit an 'input' event if the placeholder value changes.
// We don't want to dirty the value when this happens, so we abort here. Unfortunately,
// IE also sends input events for other non-input-related things, (such as focusing on a
// form control), so this change is not entirely enough to solve this.
if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) {
placeholder = element[0].placeholder;
return;
}
// 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.on('input', listener);
} else {
var timeout;
var deferListener = function(ev) {
if (!timeout) {
timeout = $browser.defer(function() {
listener(ev);
timeout = null;
});
}
};
element.on('keydown', function(event) {
var key = event.keyCode;
// ignore
// command modifiers arrows
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
deferListener(event);
});
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
if ($sniffer.hasEvent('paste')) {
element.on('paste cut', 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);
ctrl.$render = function() {
element.val(ctrl.$isEmpty(ctrl.$modelValue) ? '' : ctrl.$viewValue);
};
}
function weekParser(isoWeek, existingDate) {
if (isDate(isoWeek)) {
return isoWeek;
}
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, date) {
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 (date) {
map = {
yyyy: date.getFullYear(),
MM: date.getMonth() + 1,
dd: date.getDate(),
HH: date.getHours(),
mm: date.getMinutes(),
ss: date.getSeconds(),
sss: date.getMilliseconds() / 1000
};
} else {
map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
}
forEach(parts, function(part, index) {
if (index < mapping.length) {
map[mapping[index]] = +part;
}
});
return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
}
}
return NaN;
};
}
function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
badInputChecker(scope, element, attr, ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var timezone = ctrl && ctrl.$options && ctrl.$options.timezone;
var previousDate;
ctrl.$$parserName = type;
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!
var parsedDate = parseDate(value, previousDate);
if (timezone === 'UTC') {
parsedDate.setMinutes(parsedDate.getMinutes() - parsedDate.getTimezoneOffset());
}
return parsedDate;
}
return undefined;
});
ctrl.$formatters.push(function(value) {
if (!ctrl.$isEmpty(value)) {
if (!isDate(value)) {
throw $ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
}
previousDate = value;
if (previousDate && timezone === 'UTC') {
var timezoneOffset = 60000 * previousDate.getTimezoneOffset();
previousDate = new Date(previousDate.getTime() + timezoneOffset);
}
return $filter('date')(value, format, timezone);
} else {
previousDate = null;
}
return '';
});
if (isDefined(attr.min) || attr.ngMin) {
var minVal;
ctrl.$validators.min = function(value) {
return ctrl.$isEmpty(value) || isUndefined(minVal) || parseDate(value) >= minVal;
};
attr.$observe('min', function(val) {
minVal = parseObservedDateValue(val);
ctrl.$validate();
});
}
if (isDefined(attr.max) || attr.ngMax) {
var maxVal;
ctrl.$validators.max = function(value) {
return ctrl.$isEmpty(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
};
attr.$observe('max', function(val) {
maxVal = parseObservedDateValue(val);
ctrl.$validate();
});
}
// Override the standard $isEmpty to detect invalid dates as well
ctrl.$isEmpty = function(value) {
// Invalid Date: getTime() returns NaN
return !value || (value.getTime && value.getTime() !== value.getTime());
};
function parseObservedDateValue(val) {
return isDefined(val) ? (isDate(val) ? val : parseDate(val)) : undefined;
}
};
}
function badInputChecker(scope, element, attr, ctrl) {
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) || {};
// Detect bug in FF35 for input[email] (https://bugzilla.mozilla.org/show_bug.cgi?id=1064430):
// - also sets validity.badInput (should only be validity.typeMismatch).
// - see http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#e-mail-state-(type=email)
// - can ignore this case as we can still read out the erroneous email...
return validity.badInput && !validity.typeMismatch ? undefined : value;
});
}
}
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
badInputChecker(scope, element, attr, ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
ctrl.$$parserName = 'number';
ctrl.$parsers.push(function(value) {
if (ctrl.$isEmpty(value)) return null;
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
return undefined;
});
ctrl.$formatters.push(function(value) {
if (!ctrl.$isEmpty(value)) {
if (!isNumber(value)) {
throw $ngModelMinErr('numfmt', 'Expected `{0}` to be a number', value);
}
value = value.toString();
}
return value;
});
if (attr.min || attr.ngMin) {
var minVal;
ctrl.$validators.min = function(value) {
return ctrl.$isEmpty(value) || isUndefined(minVal) || value >= minVal;
};
attr.$observe('min', function(val) {
if (isDefined(val) && !isNumber(val)) {
val = parseFloat(val, 10);
}
minVal = isNumber(val) && !isNaN(val) ? val : undefined;
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
});
}
if (attr.max || attr.ngMax) {
var maxVal;
ctrl.$validators.max = function(value) {
return ctrl.$isEmpty(value) || isUndefined(maxVal) || value <= maxVal;
};
attr.$observe('max', function(val) {
if (isDefined(val) && !isNumber(val)) {
val = parseFloat(val, 10);
}
maxVal = isNumber(val) && !isNaN(val) ? val : undefined;
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
});
}
}
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// Note: no badInputChecker here by purpose as `url` is only a validation
// in browsers, i.e. we can always read out input.value even if it is not valid!
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
stringBasedInputType(ctrl);
ctrl.$$parserName = 'url';
ctrl.$validators.url = function(value) {
return ctrl.$isEmpty(value) || URL_REGEXP.test(value);
};
}
function emailInputType(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);
ctrl.$$parserName = 'email';
ctrl.$validators.email = function(value) {
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
};
}
function radioInputType(scope, element, attr, ctrl) {
// make the name unique, if not defined
if (isUndefined(attr.name)) {
element.attr('name', nextUid());
}
var listener = function(ev) {
if (element[0].checked) {
ctrl.$setViewValue(attr.value, ev && ev.type);
}
};
element.on('click', listener);
ctrl.$render = function() {
var value = attr.value;
element[0].checked = (value == ctrl.$viewValue);
};
attr.$observe('value', ctrl.$render);
}
function parseConstantExpr($parse, context, name, expression, fallback) {
var parseFn;
if (isDefined(expression)) {
parseFn = $parse(expression);
if (!parseFn.constant) {
throw minErr('ngModel')('constexpr', 'Expected constant expression for `{0}`, but saw ' +
'`{1}`.', name, expression);
}
return parseFn(context);
}
return fallback;
}
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);
var listener = function(ev) {
ctrl.$setViewValue(element[0].checked, ev && ev.type);
};
element.on('click', listener);
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
};
// Override the standard `$isEmpty` because an empty checkbox is never equal to the trueValue
ctrl.$isEmpty = function(value) {
return value !== trueValue;
};
ctrl.$formatters.push(function(value) {
return equals(value, trueValue);
});
ctrl.$parsers.push(function(value) {
return value ? trueValue : falseValue;
});
}
/**
* @ngdoc directive
* @name textarea
* @restrict E
*
* @description
* HTML textarea element control with angular 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=} 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
* 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 {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
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
*/
/**
* @ngdoc directive
* @name input
* @restrict E
*
* @description
* 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.
*
*
* **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 angular 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
* interaction with the input element.
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
* This parameter is ignored for input[type=password] controls, which will never trim the
* input.
*
* @example
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(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() {
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() {
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() {
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() {
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', '$filter', '$parse',
function($browser, $sniffer, $filter, $parse) {
return {
restrict: 'E',
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',
UNTOUCHED_CLASS = 'ng-untouched',
TOUCHED_CLASS = 'ng-touched',
PENDING_CLASS = 'ng-pending';
/**
* @ngdoc type
* @name ngModel.NgModelController
*
* @property {string} $viewValue Actual string value in the view.
* @property {*} $modelValue The value in the model that the control is bound to.
* @property {Array.} $parsers Array of functions to execute, as a pipeline, whenever
the control reads value from the DOM. The functions are called in array order, each passing
its return value through to the next. The last return value is forwarded to the
{@link ngModel.NgModelController#$validators `$validators`} collection.
Parsers are used to sanitize / convert the {@link ngModel.NgModelController#$viewValue
`$viewValue`}.
Returning `undefined` from a parser means a parse error occurred. In that case,
no {@link ngModel.NgModelController#$validators `$validators`} will run and the `ngModel`
will be set to `undefined` unless {@link ngModelOptions `ngModelOptions.allowInvalid`}
is set to `true`. The parse error is stored in `ngModel.$error.parse`.
*
* @property {Array.} $formatters Array of functions to execute, as a pipeline, whenever
the model value changes. The functions are called in reverse array order, each passing the value through to the
next. The last return value is used as the actual DOM value.
Used to format / convert values for display in the control.
* ```js
* function formatter(value) {
* if (value) {
* return value.toUpperCase();
* }
* }
* ngModel.$formatters.push(formatter);
* ```
*
* @property {Object.} $validators A collection of validators that are applied
* whenever the model value changes. The key value within the object refers to the name of the
* validator while the function refers to the validation operation. The validation operation is
* provided with the model value as an argument and must return a true or false value depending
* on the response of that validation.
*
* ```js
* ngModel.$validators.validCharacters = function(modelValue, viewValue) {
* var value = modelValue || viewValue;
* return /[0-9]+/.test(value) &&
* /[a-z]+/.test(value) &&
* /[A-Z]+/.test(value) &&
* /\W+/.test(value);
* };
* ```
*
* @property {Object.} $asyncValidators A collection of validations that are expected to
* perform an asynchronous validation (e.g. a HTTP request). The validation function that is provided
* is expected to return a promise when it is run during the model validation process. Once the promise
* is delivered then the validation status will be set to true when fulfilled and false when rejected.
* When the asynchronous validators are triggered, each of the validators will run in parallel and the model
* value will only be updated once all validators have been fulfilled. As long as an asynchronous validator
* is unfulfilled, its key will be added to the controllers `$pending` property. Also, all asynchronous validators
* will only run once all synchronous validators have passed.
*
* Please note that if $http is used then it is important that the server returns a success HTTP response code
* in order to fulfill the validation and a status level of `4xx` in order to reject the validation.
*
* ```js
* ngModel.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {
* var value = modelValue || viewValue;
*
* // Lookup user by username
* return $http.get('/api/users/' + value).
* then(function resolved() {
* //username exists, this means validation fails
* return $q.reject('exists');
* }, function rejected() {
* //username does not exist, therefore this validation passes
* return true;
* });
* };
* ```
*
* @property {Array.} $viewChangeListeners Array of functions to execute whenever the
* view value has changed. It is called with no arguments, and its return value is ignored.
* This can be used in place of additional $watches against the model value.
*
* @property {Object} $error An object hash with all failing validator ids as keys.
* @property {Object} $pending An object hash with all pending validator ids as keys.
*
* @property {boolean} $untouched True if control has not lost focus yet.
* @property {boolean} $touched True if control has lost focus.
* @property {boolean} $pristine True if user has not interacted with the control yet.
* @property {boolean} $dirty True if user has already interacted with the control.
* @property {boolean} $valid True if there is no error.
* @property {boolean} $invalid True if at least one error on the control.
* @property {string} $name The name attribute of the control.
*
* @description
*
* `NgModelController` provides API for the {@link ngModel `ngModel`} directive.
* The controller contains services for data-binding, validation, CSS updates, and value formatting
* and parsing. It purposefully does not contain any logic which deals with DOM rendering or
* listening to DOM events.
* Such DOM related logic should be provided by other directives which make use of
* `NgModelController` for data-binding to control elements.
* Angular provides this DOM logic for most {@link input `input`} elements.
* At the end of this page you can find a {@link ngModel.NgModelController#custom-control-example
* custom control example} that uses `ngModelController` to bind to `contenteditable` elements.
*
* @example
* ### Custom Control Example
* This example shows how to use `NgModelController` with a custom control to achieve
* data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
* collaborate together to achieve the desired result.
*
* Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element
* contents be edited in place by the user. This will not work on older browsers.
*
* 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);
}
}
};
}]);
it('should data-bind and become invalid', function() {
if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
// SafariDriver can't handle contenteditable
// and Firefox driver can't clear contenteditables very well
return;
}
var contentEditable = element(by.css('[contenteditable]'));
var content = 'Change me!';
expect(contentEditable.getText()).toEqual(content);
contentEditable.clear();
contentEditable.sendKeys(protractor.Key.BACK_SPACE);
expect(contentEditable.getText()).toEqual('');
expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
});
*
*
*
*/
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate', '$timeout', '$rootScope', '$q', '$interpolate',
function($scope, $exceptionHandler, $attr, $element, $parse, $animate, $timeout, $rootScope, $q, $interpolate) {
this.$viewValue = Number.NaN;
this.$modelValue = Number.NaN;
this.$$rawModelValue = undefined; // stores the parsed modelValue / model set from scope regardless of validity.
this.$validators = {};
this.$asyncValidators = {};
this.$parsers = [];
this.$formatters = [];
this.$viewChangeListeners = [];
this.$untouched = true;
this.$touched = false;
this.$pristine = true;
this.$dirty = false;
this.$valid = true;
this.$invalid = false;
this.$error = {}; // keep invalid keys here
this.$$success = {}; // keep valid keys here
this.$pending = undefined; // keep pending keys here
this.$name = $interpolate($attr.name || '', false)($scope);
var parsedNgModel = $parse($attr.ngModel),
pendingDebounce = null,
ctrl = this;
var ngModelGet = function ngModelGet() {
var modelValue = parsedNgModel($scope);
if (ctrl.$options && ctrl.$options.getterSetter && isFunction(modelValue)) {
modelValue = modelValue();
}
return modelValue;
};
var ngModelSet = function ngModelSet(newValue) {
var getterSetter;
if (ctrl.$options && ctrl.$options.getterSetter &&
isFunction(getterSetter = parsedNgModel($scope))) {
getterSetter(ctrl.$modelValue);
} else {
parsedNgModel.assign($scope, ctrl.$modelValue);
}
};
this.$$setOptions = function(options) {
ctrl.$options = options;
if (!parsedNgModel.assign && (!options || !options.getterSetter)) {
throw $ngModelMinErr('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
$attr.ngModel, startingTag($element));
}
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$render
*
* @description
* Called when the view needs to be updated. It is expected that the user of the ng-model
* directive will implement this method.
*
* The `$render()` method is invoked in the following situations:
*
* * `$rollbackViewValue()` is called. If we are rolling back the view value to the last
* committed value then `$render()` is called to update the input control.
* * The value referenced by `ng-model` is changed programmatically and both the `$modelValue` and
* the `$viewValue` are different to last time.
*
* Since `ng-model` does not do a deep watch, `$render()` is only invoked if the values of
* `$modelValue` and `$viewValue` are actually different to their previous value. If `$modelValue`
* or `$viewValue` are objects (rather than a string or number) then `$render()` will not be
* invoked if you only change a property on the objects.
*/
this.$render = noop;
/**
* @ngdoc method
* @name ngModel.NgModelController#$isEmpty
*
* @description
* This is called when we need to determine if the value of the input is empty.
*
* For instance, the required directive does this to work out if the input has data or not.
* The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
*
* You can override this for input directives whose concept of being empty is different to the
* default. The `checkboxInputType` directive does this because in its case a value of `false`
* implies empty.
*
* @param {*} value Model value to check.
* @returns {boolean} True if `value` is empty.
*/
this.$isEmpty = function(value) {
return isUndefined(value) || value === '' || value === null || value !== value;
};
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
currentValidationRunId = 0;
/**
* @ngdoc method
* @name ngModel.NgModelController#$setValidity
*
* @description
* Change the validity state, and notify the form.
*
* This method can be called within $parsers/$formatters or a custom validation implementation.
* However, in most cases it should be sufficient to use the `ngModel.$validators` and
* `ngModel.$asyncValidators` collections which will call `$setValidity` automatically.
*
* @param {string} validationErrorKey Name of the validator. The `validationErrorKey` will be assigned
* to either `$error[validationErrorKey]` or `$pending[validationErrorKey]`
* (for unfulfilled `$asyncValidators`), so that it is available for data-binding.
* The `validationErrorKey` should be in camelCase and will get converted into dash-case
* for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
* class and can be bound to as `{{someForm.someControl.$error.myError}}` .
* @param {boolean} isValid Whether the current state is valid (true), invalid (false), pending (undefined),
* or skipped (null). Pending is used for unfulfilled `$asyncValidators`.
* Skipped is used by Angular when validators do not run because of parse errors and
* when `$asyncValidators` do not run because any of the `$validators` failed.
*/
addSetValidityMethod({
ctrl: this,
$element: $element,
set: function(object, property) {
object[property] = true;
},
unset: function(object, property) {
delete object[property];
},
parentForm: parentForm,
$animate: $animate
});
/**
* @ngdoc method
* @name ngModel.NgModelController#$setPristine
*
* @description
* Sets the control to its pristine state.
*
* This method can be called to remove the `ng-dirty` class and set the control to its pristine
* state (`ng-pristine` class). A model is considered to be pristine when the control
* has not been changed from when first compiled.
*/
this.$setPristine = function() {
ctrl.$dirty = false;
ctrl.$pristine = true;
$animate.removeClass($element, DIRTY_CLASS);
$animate.addClass($element, PRISTINE_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setDirty
*
* @description
* Sets the control to its dirty state.
*
* This method can be called to remove the `ng-pristine` class and set the control to its dirty
* state (`ng-dirty` class). A model is considered to be dirty when the control has been changed
* from when first compiled.
*/
this.$setDirty = function() {
ctrl.$dirty = true;
ctrl.$pristine = false;
$animate.removeClass($element, PRISTINE_CLASS);
$animate.addClass($element, DIRTY_CLASS);
parentForm.$setDirty();
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setUntouched
*
* @description
* Sets the control to its untouched state.
*
* This method can be called to remove the `ng-touched` class and set the control to its
* untouched state (`ng-untouched` class). Upon compilation, a model is set as untouched
* by default, however this function can be used to restore that state if the model has
* already been touched by the user.
*/
this.$setUntouched = function() {
ctrl.$touched = false;
ctrl.$untouched = true;
$animate.setClass($element, UNTOUCHED_CLASS, TOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setTouched
*
* @description
* Sets the control to its touched state.
*
* This method can be called to remove the `ng-untouched` class and set the control to its
* touched state (`ng-touched` class). A model is considered to be touched when the user has
* first focused the control element and then shifted focus away from the control (blur event).
*/
this.$setTouched = function() {
ctrl.$touched = true;
ctrl.$untouched = false;
$animate.setClass($element, TOUCHED_CLASS, UNTOUCHED_CLASS);
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$rollbackViewValue
*
* @description
* Cancel an update and reset the input element's value to prevent an update to the `$modelValue`,
* which may be caused by a pending debounced event or because the input is waiting for a some
* future event.
*
* If you have an input that uses `ng-model-options` to set up debounced events or events such
* as blur you can have a situation where there is a period when the `$viewValue`
* is out of synch with the ngModel's `$modelValue`.
*
* In this case, you can run into difficulties if you try to update the ngModel's `$modelValue`
* programmatically before these debounced/future events have resolved/occurred, because Angular's
* dirty checking mechanism is not able to tell whether the model has actually changed or not.
*
* The `$rollbackViewValue()` method should be called before programmatically changing the model of an
* input which may have such events pending. This is important in order to make sure that the
* input field will be updated with the new model value and any pending operations are cancelled.
*
*
*
* angular.module('cancel-update-example', [])
*
* .controller('CancelUpdateController', ['$scope', function($scope) {
* $scope.resetWithCancel = function(e) {
* if (e.keyCode == 27) {
* $scope.myForm.myInput1.$rollbackViewValue();
* $scope.myValue = '';
* }
* };
* $scope.resetWithoutCancel = function(e) {
* if (e.keyCode == 27) {
* $scope.myValue = '';
* }
* };
* }]);
*
*
*
*
Try typing something in each input. See that the model only updates when you
* blur off the input.
*
*
Now see what happens if you start typing then press the Escape key
*
*
*
*
*
*/
this.$rollbackViewValue = function() {
$timeout.cancel(pendingDebounce);
ctrl.$viewValue = ctrl.$$lastCommittedViewValue;
ctrl.$render();
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$validate
*
* @description
* Runs each of the registered validators (first synchronous validators and then
* asynchronous validators).
* If the validity changes to invalid, the model will be set to `undefined`,
* unless {@link ngModelOptions `ngModelOptions.allowInvalid`} is `true`.
* If the validity changes to valid, it will set the model to the last available valid
* modelValue, i.e. either the last parsed value or the last value set from the scope.
*/
this.$validate = function() {
// ignore $validate before model is initialized
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
return;
}
var viewValue = ctrl.$$lastCommittedViewValue;
// Note: we use the $$rawModelValue as $modelValue might have been
// set to undefined during a view -> model update that found validation
// errors. We can't parse the view here, since that could change
// the model although neither viewValue nor the model on the scope changed
var modelValue = ctrl.$$rawModelValue;
// Check if the there's a parse error, so we don't unset it accidentially
var parserName = ctrl.$$parserName || 'parse';
var parserValid = ctrl.$error[parserName] ? false : undefined;
var prevValid = ctrl.$valid;
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
// If there was no change in validity, don't update the model
// This prevents changing an invalid modelValue to undefined
if (!allowInvalid && prevValid !== allValid) {
// Note: Don't check ctrl.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
ctrl.$modelValue = allValid ? modelValue : undefined;
if (ctrl.$modelValue !== prevModelValue) {
ctrl.$$writeModelToScope();
}
}
});
};
this.$$runValidators = function(parseValid, modelValue, viewValue, doneCallback) {
currentValidationRunId++;
var localValidationRunId = currentValidationRunId;
// check parser error
if (!processParseErrors(parseValid)) {
validationDone(false);
return;
}
if (!processSyncValidators()) {
validationDone(false);
return;
}
processAsyncValidators();
function processParseErrors(parseValid) {
var errorKey = ctrl.$$parserName || 'parse';
if (parseValid === undefined) {
setValidity(errorKey, null);
} else {
setValidity(errorKey, parseValid);
if (!parseValid) {
forEach(ctrl.$validators, function(v, name) {
setValidity(name, null);
});
forEach(ctrl.$asyncValidators, function(v, name) {
setValidity(name, null);
});
return false;
}
}
return true;
}
function processSyncValidators() {
var syncValidatorsValid = true;
forEach(ctrl.$validators, function(validator, name) {
var result = validator(modelValue, viewValue);
syncValidatorsValid = syncValidatorsValid && result;
setValidity(name, result);
});
if (!syncValidatorsValid) {
forEach(ctrl.$asyncValidators, function(v, name) {
setValidity(name, null);
});
return false;
}
return true;
}
function processAsyncValidators() {
var validatorPromises = [];
var allValid = true;
forEach(ctrl.$asyncValidators, function(validator, name) {
var promise = validator(modelValue, viewValue);
if (!isPromiseLike(promise)) {
throw $ngModelMinErr("$asyncValidators",
"Expected asynchronous validator to return a promise but got '{0}' instead.", promise);
}
setValidity(name, undefined);
validatorPromises.push(promise.then(function() {
setValidity(name, true);
}, function(error) {
allValid = false;
setValidity(name, false);
}));
});
if (!validatorPromises.length) {
validationDone(true);
} else {
$q.all(validatorPromises).then(function() {
validationDone(allValid);
}, noop);
}
}
function setValidity(name, isValid) {
if (localValidationRunId === currentValidationRunId) {
ctrl.$setValidity(name, isValid);
}
}
function validationDone(allValid) {
if (localValidationRunId === currentValidationRunId) {
doneCallback(allValid);
}
}
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$commitViewValue
*
* @description
* Commit a pending update to the `$modelValue`.
*
* Updates may be pending by a debounced event or because the input is waiting for a some future
* event defined in `ng-model-options`. this method is rarely needed as `NgModelController`
* usually handles calling this in response to input events.
*/
this.$commitViewValue = function() {
var viewValue = ctrl.$viewValue;
$timeout.cancel(pendingDebounce);
// If the view value has not changed then we should just exit, except in the case where there is
// a native validator on the element. In this case the validation state may have changed even though
// the viewValue has stayed empty.
if (ctrl.$$lastCommittedViewValue === viewValue && (viewValue !== '' || !ctrl.$$hasNativeValidators)) {
return;
}
ctrl.$$lastCommittedViewValue = viewValue;
// change to dirty
if (ctrl.$pristine) {
this.$setDirty();
}
this.$$parseAndValidate();
};
this.$$parseAndValidate = function() {
var viewValue = ctrl.$$lastCommittedViewValue;
var modelValue = viewValue;
var parserValid = isUndefined(modelValue) ? undefined : true;
if (parserValid) {
for (var i = 0; i < ctrl.$parsers.length; i++) {
modelValue = ctrl.$parsers[i](modelValue);
if (isUndefined(modelValue)) {
parserValid = false;
break;
}
}
}
if (isNumber(ctrl.$modelValue) && isNaN(ctrl.$modelValue)) {
// ctrl.$modelValue has not been touched yet...
ctrl.$modelValue = ngModelGet();
}
var prevModelValue = ctrl.$modelValue;
var allowInvalid = ctrl.$options && ctrl.$options.allowInvalid;
ctrl.$$rawModelValue = modelValue;
if (allowInvalid) {
ctrl.$modelValue = modelValue;
writeToModelIfNeeded();
}
ctrl.$$runValidators(parserValid, modelValue, viewValue, function(allValid) {
if (!allowInvalid) {
// Note: Don't check ctrl.$valid here, as we could have
// external validators (e.g. calculated on the server),
// that just call $setValidity and need the model value
// to calculate their validity.
ctrl.$modelValue = allValid ? modelValue : undefined;
writeToModelIfNeeded();
}
});
function writeToModelIfNeeded() {
if (ctrl.$modelValue !== prevModelValue) {
ctrl.$$writeModelToScope();
}
}
};
this.$$writeModelToScope = function() {
ngModelSet(ctrl.$modelValue);
forEach(ctrl.$viewChangeListeners, function(listener) {
try {
listener();
} catch (e) {
$exceptionHandler(e);
}
});
};
/**
* @ngdoc method
* @name ngModel.NgModelController#$setViewValue
*
* @description
* Update the view value.
*
* This method should be called when an input directive want to change the view value; typically,
* this is done from within a DOM event handler.
*
* For example {@link ng.directive:input input} calls it when the value of the input changes and
* {@link ng.directive:select select} calls it when an option is selected.
*
* If the new `value` is an object (rather than a string or a number), we should make a copy of the
* object before passing it to `$setViewValue`. This is because `ngModel` does not perform a deep
* watch of objects, it only looks for a change of identity. If you only change the property of
* the object then ngModel will not realise that the object has changed and will not invoke the
* `$parsers` and `$validators` pipelines.
*
* For this reason, you should not change properties of the copy once it has been passed to
* `$setViewValue`. Otherwise you may cause the model value on the scope to change incorrectly.
*
* When this method is called, the new `value` will be staged for committing through the `$parsers`
* and `$validators` pipelines. If there are no special {@link ngModelOptions} specified then the staged
* value sent directly for processing, finally to be applied to `$modelValue` and then the
* **expression** specified in the `ng-model` attribute.
*
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
*
* In case the {@link ng.directive:ngModelOptions ngModelOptions} directive is used with `updateOn`
* and the `default` trigger is not listed, all those actions will remain pending until one of the
* `updateOn` events is triggered on the DOM element.
* All these actions will be debounced if the {@link ng.directive:ngModelOptions ngModelOptions}
* directive is used with a custom debounce for this particular event.
*
* Note that calling this function does not trigger a `$digest`.
*
* @param {string} value Value from the view.
* @param {string} trigger Event that triggered the update.
*/
this.$setViewValue = function(value, trigger) {
ctrl.$viewValue = value;
if (!ctrl.$options || ctrl.$options.updateOnDefault) {
ctrl.$$debounceViewValueCommit(trigger);
}
};
this.$$debounceViewValueCommit = function(trigger) {
var debounceDelay = 0,
options = ctrl.$options,
debounce;
if (options && isDefined(options.debounce)) {
debounce = options.debounce;
if (isNumber(debounce)) {
debounceDelay = debounce;
} else if (isNumber(debounce[trigger])) {
debounceDelay = debounce[trigger];
} else if (isNumber(debounce['default'])) {
debounceDelay = debounce['default'];
}
}
$timeout.cancel(pendingDebounce);
if (debounceDelay) {
pendingDebounce = $timeout(function() {
ctrl.$commitViewValue();
}, debounceDelay);
} else if ($rootScope.$$phase) {
ctrl.$commitViewValue();
} else {
$scope.$apply(function() {
ctrl.$commitViewValue();
});
}
};
// model -> value
// Note: we cannot use a normal scope.$watch as we want to detect the following:
// 1. scope value is 'a'
// 2. user enters 'b'
// 3. ng-change kicks in and reverts scope value to 'a'
// -> scope value did not change since the last digest as
// ng-change executes in apply phase
// 4. view should be changed back to 'a'
$scope.$watch(function ngModelWatch() {
var modelValue = ngModelGet();
// if scope model value and ngModel value are out of sync
// TODO(perf): why not move this to the action fn?
if (modelValue !== ctrl.$modelValue) {
ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
var formatters = ctrl.$formatters,
idx = formatters.length;
var viewValue = modelValue;
while (idx--) {
viewValue = formatters[idx](viewValue);
}
if (ctrl.$viewValue !== viewValue) {
ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
ctrl.$render();
ctrl.$$runValidators(undefined, modelValue, viewValue, noop);
}
}
return modelValue;
});
}];
/**
* @ngdoc directive
* @name ngModel
*
* @element input
* @priority 1
*
* @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`) 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}
*
* # 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
*
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
*
* ## Animation Hooks
*
* Animations within models are triggered when any of the associated CSS classes are added and removed
* on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
* `.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
*
Update input to see transitions when valid/invalid.
Integer is a valid value.
*
*
* ## 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 than what the model exposes
* to the view.
*
*
* **Best Practice:** It's best to keep getters fast because Angular 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 `