mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-01-12 22:45:52 +08:00
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
3204 lines
126 KiB
JavaScript
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=" " 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;
|
|
}
|