Files
angular.js/src/ng/directive/input.js
Georgios Kalpakas 5c1fdff691 feat(ngMaxlength): add support for disabling max length limit
Previously, setting the maxlength to a negative number, would make all
input values invalid (since their length should be less than maxlength,
which is impossible).
This commit changes the behaviour of maxlength/ngMaxlength, effectively
disabling the maxlength validation (always returning true) when maxlength
is set to a negative number. This is more inline to how the HTML5
`maxlength` attribute works (both in browsers and according to the spec:
http://dev.w3.org/html5/spec-preview/attributes-common-to-form-controls.html#attr-fe-maxlength).

Related to #9874
Closes #9995
2014-11-22 16:00:43 +01:00

3204 lines
126 KiB
JavaScript

'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
<example name="text-input-directive" module="textInputExample">
<file name="index.html">
<script>
angular.module('textInputExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.text = 'guest';
$scope.word = /^\s*\w*\s*$/;
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
Single word: <input type="text" name="input" ng-model="text"
ng-pattern="word" required ng-trim="false">
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.pattern">
Single word only!</span>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="date-input-directive" module="dateInputExample">
<file name="index.html">
<script>
angular.module('dateInputExample', [])
.controller('DateController', ['$scope', function($scope) {
$scope.value = new Date(2013, 9, 22);
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
Pick a date in 2013:
<input type="date" id="exampleInput" name="input" ng-model="value"
placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.date">
Not a valid date!</span>
<tt>value = {{value | date: "yyyy-MM-dd"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="datetimelocal-input-directive" module="dateExample">
<file name="index.html">
<script>
angular.module('dateExample', [])
.controller('DateController', ['$scope', function($scope) {
$scope.value = new Date(2010, 11, 28, 14, 57);
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
Pick a date between in 2013:
<input type="datetime-local" id="exampleInput" name="input" ng-model="value"
placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.datetimelocal">
Not a valid date!</span>
<tt>value = {{value | date: "yyyy-MM-ddTHH:mm:ss"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="time-input-directive" module="timeExample">
<file name="index.html">
<script>
angular.module('timeExample', [])
.controller('DateController', ['$scope', function($scope) {
$scope.value = new Date(1970, 0, 1, 14, 57, 0);
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
Pick a between 8am and 5pm:
<input type="time" id="exampleInput" name="input" ng-model="value"
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.time">
Not a valid date!</span>
<tt>value = {{value | date: "HH:mm:ss"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="week-input-directive" module="weekExample">
<file name="index.html">
<script>
angular.module('weekExample', [])
.controller('DateController', ['$scope', function($scope) {
$scope.value = new Date(2013, 0, 3);
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
Pick a date between in 2013:
<input id="exampleInput" type="week" name="input" ng-model="value"
placeholder="YYYY-W##" min="2012-W32" max="2013-W52" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.week">
Not a valid date!</span>
<tt>value = {{value | date: "yyyy-Www"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="month-input-directive" module="monthExample">
<file name="index.html">
<script>
angular.module('monthExample', [])
.controller('DateController', ['$scope', function($scope) {
$scope.value = new Date(2013, 9, 1);
}]);
</script>
<form name="myForm" ng-controller="DateController as dateCtrl">
Pick a month int 2013:
<input id="exampleInput" type="month" name="input" ng-model="value"
placeholder="yyyy-MM" min="2013-01" max="2013-12" required />
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.month">
Not a valid month!</span>
<tt>value = {{value | date: "yyyy-MM"}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="number-input-directive" module="numberExample">
<file name="index.html">
<script>
angular.module('numberExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.value = 12;
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
Number: <input type="number" name="input" ng-model="value"
min="0" max="99" required>
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.number">
Not valid number!</span>
<tt>value = {{value}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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.
*
* <div class="alert alert-warning">
* **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})
* </div>
*
* @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
<example name="url-input-directive" module="urlExample">
<file name="index.html">
<script>
angular.module('urlExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.text = 'http://google.com';
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
URL: <input type="url" name="input" ng-model="text" required>
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.url">
Not valid url!</span>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
<tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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.
*
* <div class="alert alert-warning">
* **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})
* </div>
*
* @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
<example name="email-input-directive" module="emailExample">
<file name="index.html">
<script>
angular.module('emailExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.text = 'me@example.com';
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
Email: <input type="email" name="input" ng-model="text" required>
<span class="error" ng-show="myForm.input.$error.required">
Required!</span>
<span class="error" ng-show="myForm.input.$error.email">
Not valid email!</span>
<tt>text = {{text}}</tt><br/>
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
<tt>myForm.$error.email = {{!!myForm.$error.email}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="radio-input-directive" module="radioExample">
<file name="index.html">
<script>
angular.module('radioExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.color = 'blue';
$scope.specialValue = {
"id": "12345",
"value": "green"
};
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
<input type="radio" ng-model="color" value="red"> Red <br/>
<input type="radio" ng-model="color" ng-value="specialValue"> Green <br/>
<input type="radio" ng-model="color" value="blue"> Blue <br/>
<tt>color = {{color | json}}</tt><br/>
</form>
Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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
<example name="checkbox-input-directive" module="checkboxExample">
<file name="index.html">
<script>
angular.module('checkboxExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.value1 = true;
$scope.value2 = 'YES'
}]);
</script>
<form name="myForm" ng-controller="ExampleController">
Value1: <input type="checkbox" ng-model="value1"> <br/>
Value2: <input type="checkbox" ng-model="value2"
ng-true-value="'YES'" ng-false-value="'NO'"> <br/>
<tt>value1 = {{value1}}</tt><br/>
<tt>value2 = {{value2}}</tt><br/>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
'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.
*
* <div class="alert alert-warning">
* **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]`.
* </div>
*
* @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
<example name="input-directive" module="inputExample">
<file name="index.html">
<script>
angular.module('inputExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = {name: 'guest', last: 'visitor'};
}]);
</script>
<div ng-controller="ExampleController">
<form name="myForm">
User name: <input type="text" name="userName" ng-model="user.name" required>
<span class="error" ng-show="myForm.userName.$error.required">
Required!</span><br>
Last name: <input type="text" name="lastName" ng-model="user.last"
ng-minlength="3" ng-maxlength="10">
<span class="error" ng-show="myForm.lastName.$error.minlength">
Too short!</span>
<span class="error" ng-show="myForm.lastName.$error.maxlength">
Too long!</span><br>
</form>
<hr>
<tt>user = {{user}}</tt><br/>
<tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br>
<tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br>
<tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br>
<tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br>
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
<tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br>
<tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br>
</div>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
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.<Function>} $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.<Function>} $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.<string, function>} $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.<string, function>} $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.<Function>} $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. `<span onclick="...">`).
* However, as we are using `$sce` the model can still decide to provide unsafe content if it marks
* that content using the `$sce` service.
*
* <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
<file name="style.css">
[contenteditable] {
border: 1px solid black;
background-color: white;
min-height: 20px;
}
.ng-invalid {
border: 1px solid red;
}
</file>
<file name="script.js">
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 <br> behind
// If strip-br attribute is provided then we strip this out
if ( attrs.stripBr && html == '<br>' ) {
html = '';
}
ngModel.$setViewValue(html);
}
}
};
}]);
</file>
<file name="index.html">
<form name="myForm">
<div contenteditable
name="myWidget" ng-model="userContent"
strip-br="true"
required>Change me!</div>
<span ng-show="myForm.myWidget.$error.required">Required!</span>
<hr>
<textarea ng-model="userContent"></textarea>
</form>
</file>
<file name="protractor.js" type="protractor">
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/);
});
</file>
* </example>
*
*
*/
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.
*
* <example name="ng-model-cancel-update" module="cancel-update-example">
* <file name="app.js">
* 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 = '';
* }
* };
* }]);
* </file>
* <file name="index.html">
* <div ng-controller="CancelUpdateController">
* <p>Try typing something in each input. See that the model only updates when you
* blur off the input.
* </p>
* <p>Now see what happens if you start typing then press the Escape key</p>
*
* <form name="myForm" ng-model-options="{ updateOn: 'blur' }">
* <p>With $rollbackViewValue()</p>
* <input name="myInput1" ng-model="myValue" ng-keydown="resetWithCancel($event)"><br/>
* myValue: "{{ myValue }}"
*
* <p>Without $rollbackViewValue()</p>
* <input name="myInput2" ng-model="myValue" ng-keydown="resetWithoutCancel($event)"><br/>
* myValue: "{{ myValue }}"
* </form>
* </div>
* </file>
* </example>
*/
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:
*
* <pre>
* //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;
* }
* </pre>
*
* @example
* <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
<file name="index.html">
<script>
angular.module('inputExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.val = '1';
}]);
</script>
<style>
.my-input {
-webkit-transition:all linear 0.5s;
transition:all linear 0.5s;
background: transparent;
}
.my-input.ng-invalid {
color:white;
background: red;
}
</style>
Update input to see transitions when valid/invalid.
Integer is a valid value.
<form name="testForm" ng-controller="ExampleController">
<input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
</form>
</file>
* </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 than what the model exposes
* to the view.
*
* <div class="alert alert-success">
* **Best Practice:** It's best to keep getters fast because Angular is likely to call them more
* frequently than other parts of your code.
* </div>
*
* 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 `<form>`, which will enable this behavior for all `<input>`s within it. See
* {@link ng.directive:ngModelOptions `ngModelOptions`} for more.
*
* The following example shows how to use `ngModel` with a getter/setter:
*
* @example
* <example name="ngModel-getter-setter" module="getterSetterExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ getterSetter: true }" />
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('getterSetterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
var _name = 'Brian';
$scope.user = {
name: function(newName) {
if (angular.isDefined(newName)) {
_name = newName;
}
return _name;
}
};
}]);
</file>
* </example>
*/
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] || nullFormCtrl;
modelCtrl.$$setOptions(ctrls[2] && ctrls[2].$options);
// notify others, especially parent forms
formCtrl.$addControl(modelCtrl);
attr.$observe('name', function(newValue) {
if (modelCtrl.$name !== newValue) {
formCtrl.$$renameControl(modelCtrl, newValue);
}
});
scope.$on('$destroy', function() {
formCtrl.$removeControl(modelCtrl);
});
},
post: function ngModelPostLink(scope, element, attr, ctrls) {
var modelCtrl = ctrls[0];
if (modelCtrl.$options && modelCtrl.$options.updateOn) {
element.on(modelCtrl.$options.updateOn, function(ev) {
modelCtrl.$$debounceViewValueCommit(ev && ev.type);
});
}
element.on('blur', function(ev) {
if (modelCtrl.$touched) return;
if ($rootScope.$$phase) {
scope.$evalAsync(modelCtrl.$setTouched);
} else {
scope.$apply(modelCtrl.$setTouched);
}
});
}
};
}
};
}];
/**
* @ngdoc directive
* @name ngChange
*
* @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 input
* @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
* in input value.
*
* @example
* <example name="ngChange-directive" module="changeExample">
* <file name="index.html">
* <script>
* angular.module('changeExample', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.counter = 0;
* $scope.change = function() {
* $scope.counter++;
* };
* }]);
* </script>
* <div ng-controller="ExampleController">
* <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
* <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
* <label for="ng-change-example2">Confirmed</label><br />
* <tt>debug = {{confirmed}}</tt><br/>
* <tt>counter = {{counter}}</tt><br/>
* </div>
* </file>
* <file name="protractor.js" type="protractor">
* 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');
* });
* </file>
* </example>
*/
var ngChangeDirective = valueFn({
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
ctrl.$viewChangeListeners.push(function() {
scope.$eval(attr.ngChange);
});
}
});
var requiredDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
attr.required = true; // force truthy in case we are on non input element
ctrl.$validators.required = function(value) {
return !attr.required || !ctrl.$isEmpty(value);
};
attr.$observe('required', function() {
ctrl.$validate();
});
}
};
};
var patternDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var regexp, patternExp = attr.ngPattern || attr.pattern;
attr.$observe('pattern', function(regex) {
if (isString(regex) && regex.length > 0) {
regex = new RegExp('^' + regex + '$');
}
if (regex && !regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(elm));
}
regexp = regex || undefined;
ctrl.$validate();
});
ctrl.$validators.pattern = function(value) {
return ctrl.$isEmpty(value) || isUndefined(regexp) || regexp.test(value);
};
}
};
};
var maxlengthDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = 0;
attr.$observe('maxlength', function(value) {
maxlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
return (maxlength < 0) || ctrl.$isEmpty(modelValue) || (viewValue.length <= maxlength);
};
}
};
};
var minlengthDirective = function() {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var minlength = 0;
attr.$observe('minlength', function(value) {
minlength = int(value) || 0;
ctrl.$validate();
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
return ctrl.$isEmpty(modelValue) || viewValue.length >= minlength;
};
}
};
};
/**
* @ngdoc directive
* @name ngList
*
* @description
* Text input that converts between a delimited string and an array of strings. The default
* delimiter is a comma followed by a space - equivalent to `ng-list=", "`. You can specify a custom
* delimiter as the value of the `ngList` attribute - for example, `ng-list=" | "`.
*
* The behaviour of the directive is affected by the use of the `ngTrim` attribute.
* * If `ngTrim` is set to `"false"` then whitespace around both the separator and each
* list item is respected. This implies that the user of the directive is responsible for
* dealing with whitespace but also allows you to use whitespace as a delimiter, such as a
* tab or newline character.
* * Otherwise whitespace around the delimiter is ignored when splitting (although it is respected
* when joining the list items back together) and whitespace around each list item is stripped
* before it is added to the model.
*
* ### Example with Validation
*
* <example name="ngList-directive" module="listExample">
* <file name="app.js">
* angular.module('listExample', [])
* .controller('ExampleController', ['$scope', function($scope) {
* $scope.names = ['morpheus', 'neo', 'trinity'];
* }]);
* </file>
* <file name="index.html">
* <form name="myForm" ng-controller="ExampleController">
* List: <input name="namesInput" ng-model="names" ng-list required>
* <span class="error" ng-show="myForm.namesInput.$error.required">
* Required!</span>
* <br>
* <tt>names = {{names}}</tt><br/>
* <tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
* <tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
* <tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
* <tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
* </form>
* </file>
* <file name="protractor.js" type="protractor">
* 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');
* });
* </file>
* </example>
*
* ### Example - splitting on whitespace
* <example name="ngList-directive-newlines">
* <file name="index.html">
* <textarea ng-model="list" ng-list="&#10;" ng-trim="false"></textarea>
* <pre>{{ list | json }}</pre>
* </file>
* <file name="protractor.js" type="protractor">
* 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]');
* });
* </file>
* </example>
*
* @element input
* @param {string=} ngList optional delimiter that should be used to split the value.
*/
var ngListDirective = function() {
return {
restrict: 'A',
priority: 100,
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
// We want to control whitespace trimming so we use this convoluted approach
// to access the ngList attribute, which doesn't pre-trim the attribute
var ngList = element.attr(attr.$attr.ngList) || ', ';
var trimValues = attr.ngTrim !== 'false';
var separator = trimValues ? trim(ngList) : ngList;
var parse = function(viewValue) {
// If the viewValue is invalid (say required but empty) it will be `undefined`
if (isUndefined(viewValue)) return;
var list = [];
if (viewValue) {
forEach(viewValue.split(separator), function(value) {
if (value) list.push(trimValues ? trim(value) : value);
});
}
return list;
};
ctrl.$parsers.push(parse);
ctrl.$formatters.push(function(value) {
if (isArray(value)) {
return value.join(ngList);
}
return undefined;
});
// Override the standard $isEmpty because an empty array means the input is empty.
ctrl.$isEmpty = function(value) {
return !value || !value.length;
};
}
};
};
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
/**
* @ngdoc directive
* @name ngValue
*
* @description
* Binds the given expression to the value of `<option>` or {@link input[radio] `input[radio]`},
* so that when the element is selected, the {@link ngModel `ngModel`} of that element is set to
* the bound value.
*
* `ngValue` is useful when dynamically generating lists of radio buttons using
* {@link ngRepeat `ngRepeat`}, as shown below.
*
* Likewise, `ngValue` can be used to generate `<option>` elements for
* the {@link select `select`} element. In that case however, only strings are supported
* for the `value `attribute, so the resulting `ngModel` will always be a string.
* Support for `select` models with non-string values is available via `ngOptions`.
*
* @element input
* @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
* of the `input` element
*
* @example
<example name="ngValue-directive" module="valueExample">
<file name="index.html">
<script>
angular.module('valueExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.names = ['pizza', 'unicorns', 'robots'];
$scope.my = { favorite: 'unicorns' };
}]);
</script>
<form ng-controller="ExampleController">
<h2>Which is your favorite?</h2>
<label ng-repeat="name in names" for="{{name}}">
{{name}}
<input type="radio"
ng-model="my.favorite"
ng-value="name"
id="{{name}}"
name="favorite">
</label>
<div>You chose {{my.favorite}}</div>
</form>
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
*/
var ngValueDirective = function() {
return {
restrict: 'A',
priority: 100,
compile: function(tpl, tplAttr) {
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
return function ngValueConstantLink(scope, elm, attr) {
attr.$set('value', scope.$eval(attr.ngValue));
};
} else {
return function ngValueLink(scope, elm, attr) {
scope.$watch(attr.ngValue, function valueWatchAction(value) {
attr.$set('value', value);
});
};
}
}
};
};
/**
* @ngdoc directive
* @name ngModelOptions
*
* @description
* Allows tuning how model updates are done. Using `ngModelOptions` you can specify a custom list of
* events that will trigger a model update and/or a debouncing delay so that the actual update only
* takes place when a timer expires; this timer will be reset after another change takes place.
*
* Given the nature of `ngModelOptions`, the value displayed inside input fields in the view might
* be different than the value in the actual model. This means that if you update the model you
* should also invoke {@link ngModel.NgModelController `$rollbackViewValue`} on the relevant input field in
* order to make sure it is synchronized with the model and that any debounced action is canceled.
*
* 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.
*
* `ngModelOptions` has an effect on the element it's declared on and its descendants.
*
* @param {Object} ngModelOptions options to apply to the current model. Valid keys are:
* - `updateOn`: string specifying which event should the input be bound to. You can set several
* events using an space delimited list. There is a special event called `default` that
* matches the default events belonging of the control.
* - `debounce`: integer value which contains the debounce model update value in milliseconds. A
* value of 0 triggers an immediate update. If an object is supplied instead, you can specify a
* custom value for each event. For example:
* `ng-model-options="{ updateOn: 'default blur', debounce: {'default': 500, 'blur': 0} }"`
* - `allowInvalid`: boolean value which indicates that the model can be set with values that did
* not validate correctly instead of the default behavior of setting the model to undefined.
* - `getterSetter`: boolean value which determines whether or not to treat functions bound to
`ngModel` as getters/setters.
* - `timezone`: Defines the timezone to be used to read/write the `Date` instance in the model for
* `<input type="date">`, `<input type="time">`, ... . Right now, the only supported value is `'UTC'`,
* otherwise the default timezone of the browser will be used.
*
* @example
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.
<example name="ngModelOptions-directive-blur" module="optionsExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ updateOn: 'blur' }"
ng-keyup="cancel($event)" /><br />
Other data:
<input type="text" ng-model="user.data" /><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
</div>
</file>
<file name="app.js">
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();
}
};
}]);
</file>
<file name="protractor.js" type="protractor">
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');
});
</file>
</example>
This one shows how to debounce model changes. Model will be updated only 1 sec after last change.
If the `Clear` button is pressed, any debounced action is canceled and the value becomes empty.
<example name="ngModelOptions-directive-debounce" module="optionsExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ debounce: 1000 }" />
<button ng-click="userForm.userName.$rollbackViewValue(); user.name=''">Clear</button><br />
</form>
<pre>user.name = <span ng-bind="user.name"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('optionsExample', [])
.controller('ExampleController', ['$scope', function($scope) {
$scope.user = { name: 'say' };
}]);
</file>
</example>
This one shows how to bind to getter/setters:
<example name="ngModelOptions-directive-getter-setter" module="getterSetterExample">
<file name="index.html">
<div ng-controller="ExampleController">
<form name="userForm">
Name:
<input type="text" name="userName"
ng-model="user.name"
ng-model-options="{ getterSetter: true }" />
</form>
<pre>user.name = <span ng-bind="user.name()"></span></pre>
</div>
</file>
<file name="app.js">
angular.module('getterSetterExample', [])
.controller('ExampleController', ['$scope', function($scope) {
var _name = 'Brian';
$scope.user = {
name: function(newName) {
return angular.isDefined(newName) ? (_name = newName) : _name;
}
};
}]);
</file>
</example>
*/
var ngModelOptionsDirective = function() {
return {
restrict: 'A',
controller: ['$scope', '$attrs', function($scope, $attrs) {
var that = this;
this.$options = $scope.$eval($attrs.ngModelOptions);
// Allow adding/overriding bound events
if (this.$options.updateOn !== undefined) {
this.$options.updateOnDefault = false;
// extract "default" pseudo-event from list of events that can trigger a model update
this.$options.updateOn = trim(this.$options.updateOn.replace(DEFAULT_REGEXP, function() {
that.$options.updateOnDefault = true;
return ' ';
}));
} else {
this.$options.updateOnDefault = true;
}
}]
};
};
// helper methods
function addSetValidityMethod(context) {
var ctrl = context.ctrl,
$element = context.$element,
classCache = {},
set = context.set,
unset = context.unset,
parentForm = context.parentForm,
$animate = context.$animate;
classCache[INVALID_CLASS] = !(classCache[VALID_CLASS] = $element.hasClass(VALID_CLASS));
ctrl.$setValidity = setValidity;
function setValidity(validationErrorKey, state, options) {
if (state === undefined) {
createAndSet('$pending', validationErrorKey, options);
} else {
unsetAndCleanup('$pending', validationErrorKey, options);
}
if (!isBoolean(state)) {
unset(ctrl.$error, validationErrorKey, options);
unset(ctrl.$$success, validationErrorKey, options);
} else {
if (state) {
unset(ctrl.$error, validationErrorKey, options);
set(ctrl.$$success, validationErrorKey, options);
} else {
set(ctrl.$error, validationErrorKey, options);
unset(ctrl.$$success, validationErrorKey, options);
}
}
if (ctrl.$pending) {
cachedToggleClass(PENDING_CLASS, true);
ctrl.$valid = ctrl.$invalid = undefined;
toggleValidationCss('', null);
} else {
cachedToggleClass(PENDING_CLASS, false);
ctrl.$valid = isObjectEmpty(ctrl.$error);
ctrl.$invalid = !ctrl.$valid;
toggleValidationCss('', ctrl.$valid);
}
// re-read the state as the set/unset methods could have
// combined state in ctrl.$error[validationError] (used for forms),
// where setting/unsetting only increments/decrements the value,
// and does not replace it.
var combinedState;
if (ctrl.$pending && ctrl.$pending[validationErrorKey]) {
combinedState = undefined;
} else if (ctrl.$error[validationErrorKey]) {
combinedState = false;
} else if (ctrl.$$success[validationErrorKey]) {
combinedState = true;
} else {
combinedState = null;
}
toggleValidationCss(validationErrorKey, combinedState);
parentForm.$setValidity(validationErrorKey, combinedState, ctrl);
}
function createAndSet(name, value, options) {
if (!ctrl[name]) {
ctrl[name] = {};
}
set(ctrl[name], value, options);
}
function unsetAndCleanup(name, value, options) {
if (ctrl[name]) {
unset(ctrl[name], value, options);
}
if (isObjectEmpty(ctrl[name])) {
ctrl[name] = undefined;
}
}
function cachedToggleClass(className, switchValue) {
if (switchValue && !classCache[className]) {
$animate.addClass($element, className);
classCache[className] = true;
} else if (!switchValue && classCache[className]) {
$animate.removeClass($element, className);
classCache[className] = false;
}
}
function toggleValidationCss(validationErrorKey, isValid) {
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
cachedToggleClass(VALID_CLASS + validationErrorKey, isValid === true);
cachedToggleClass(INVALID_CLASS + validationErrorKey, isValid === false);
}
}
function isObjectEmpty(obj) {
if (obj) {
for (var prop in obj) {
return false;
}
}
return true;
}