0637a2124c
This commit updates in-built validators with observers to prevent multiple calls to $validate that could happen on initial linking of the directives in certain circumstances: - when an input is wrapped in a transclude: element directive (e.g. ngRepeat), the order of execution between ngModel and the input / validation directives changes so that the initial observer call happens when ngModel has already been initalized, leading to another call to $validate, which calls *all* defined validators again. Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators. When using validators with scope expressions, the expression value is not available when ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to the observer does the value become available, making a call to $validate a necessity. This commit solves the first problem by storing the validation attribute value so we can compare the current value and the observed value - which will be the same after compilation. The second problem is solved by parsing the validation expression once in the link function, so the value is available when ngModel first validates. Closes #14691 Closes #16760
2371 lines
100 KiB
JavaScript
2371 lines
100 KiB
JavaScript
'use strict';
|
|
|
|
/* global
|
|
VALID_CLASS: false,
|
|
INVALID_CLASS: false,
|
|
PRISTINE_CLASS: false,
|
|
DIRTY_CLASS: false,
|
|
ngModelMinErr: false
|
|
*/
|
|
|
|
// Regex code was initially obtained from SO prior to modification: 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)$/;
|
|
// See valid URLs in RFC3987 (http://tools.ietf.org/html/rfc3987)
|
|
// Note: We are being more lenient, because browsers are too.
|
|
// 1. Scheme
|
|
// 2. Slashes
|
|
// 3. Username
|
|
// 4. Password
|
|
// 5. Hostname
|
|
// 6. Port
|
|
// 7. Path
|
|
// 8. Query
|
|
// 9. Fragment
|
|
// 1111111111111111 222 333333 44444 55555555555555555555555 666 77777777 8888888 999
|
|
var URL_REGEXP = /^[a-z][a-z\d.+-]*:\/*(?:[^:@]+(?::[^@]+)?@)?(?:[^\s:/?#]+|\[[a-f\d:]+])(?::\d+)?(?:\/[^?#]*)?(?:\?[^#]*)?(?:#.*)?$/i;
|
|
// eslint-disable-next-line max-len
|
|
var EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
|
|
var NUMBER_REGEXP = /^\s*(-|\+)?(\d+|(\d*(\.\d*)))([eE][+-]?\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 PARTIAL_VALIDATION_EVENTS = 'keydown wheel mousedown';
|
|
var PARTIAL_VALIDATION_TYPES = createMap();
|
|
forEach('date,datetime-local,month,time,week'.split(','), function(type) {
|
|
PARTIAL_VALIDATION_TYPES[type] = true;
|
|
});
|
|
|
|
var inputType = {
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[text]
|
|
*
|
|
* @description
|
|
* Standard HTML text input with AngularJS data binding, inherited by most of the `input` elements.
|
|
*
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required 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. Setting the attribute to a negative or non-numeric value, allows view values of
|
|
* any length.
|
|
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
|
|
* that contains the regular expression body that will be converted to a regular expression
|
|
* as in the ngPattern directive.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
|
|
* This parameter is ignored for input[type=password] controls, which will never trim the
|
|
* input.
|
|
*
|
|
* @example
|
|
<example name="text-input-directive" module="textInputExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('textInputExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
text: 'guest',
|
|
word: /^\s*\w*\s*$/
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>Single word:
|
|
<input type="text" name="input" ng-model="example.text"
|
|
ng-pattern="example.word" required ng-trim="false">
|
|
</label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.pattern">
|
|
Single word only!</span>
|
|
</div>
|
|
<code>text = {{example.text}}</code><br/>
|
|
<code>myForm.input.$valid = {{myForm.input.$valid}}</code><br/>
|
|
<code>myForm.input.$error = {{myForm.input.$error}}</code><br/>
|
|
<code>myForm.$valid = {{myForm.$valid}}</code><br/>
|
|
<code>myForm.$error.required = {{!!myForm.$error.required}}</code><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var text = element(by.binding('example.text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('example.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 AngularJS will throw an error.
|
|
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
|
*
|
|
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
|
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`. This must be a
|
|
* valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
|
|
* (e.g. `min="{{minDate | date:'yyyy-MM-dd'}}"`). Note that `min` will also add native HTML5
|
|
* constraint validation.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`. This must be
|
|
* a valid ISO date string (yyyy-MM-dd). You can also use interpolation inside this attribute
|
|
* (e.g. `max="{{maxDate | date:'yyyy-MM-dd'}}"`). Note that `max` will also add native HTML5
|
|
* constraint validation.
|
|
* @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO date string
|
|
* the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
|
|
* @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO date string
|
|
* the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="date-input-directive" module="dateInputExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('dateInputExample', [])
|
|
.controller('DateController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: new Date(2013, 9, 22)
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="DateController as dateCtrl">
|
|
<label for="exampleInput">Pick a date in 2013:</label>
|
|
<input type="date" id="exampleInput" name="input" ng-model="example.value"
|
|
placeholder="yyyy-MM-dd" min="2013-01-01" max="2013-12-31" required />
|
|
<div role="alert">
|
|
<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>
|
|
</div>
|
|
<tt>value = {{example.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('example.value | date: "yyyy-MM-dd"'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
|
|
// currently protractor/webdriver does not support
|
|
// sending keys to all known HTML5 input controls
|
|
// for various browsers (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 AngularJS will throw an error.
|
|
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
|
*
|
|
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
|
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
|
|
*
|
|
* The format of the displayed time can be adjusted with the
|
|
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
|
|
* and `timeStripZeroSeconds`.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
|
|
* inside this attribute (e.g. `min="{{minDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
|
|
* Note that `min` will also add native HTML5 constraint validation.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* This must be a valid ISO datetime format (yyyy-MM-ddTHH:mm:ss). You can also use interpolation
|
|
* inside this attribute (e.g. `max="{{maxDatetimeLocal | date:'yyyy-MM-ddTHH:mm:ss'}}"`).
|
|
* Note that `max` will also add native HTML5 constraint validation.
|
|
* @param {(date|string)=} ngMin Sets the `min` validation error key to the Date / ISO datetime string
|
|
* the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
|
|
* @param {(date|string)=} ngMax Sets the `max` validation error key to the Date / ISO datetime string
|
|
* the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="datetimelocal-input-directive" module="dateExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('dateExample', [])
|
|
.controller('DateController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: new Date(2010, 11, 28, 14, 57)
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="DateController as dateCtrl">
|
|
<label for="exampleInput">Pick a date between in 2013:</label>
|
|
<input type="datetime-local" id="exampleInput" name="input" ng-model="example.value"
|
|
placeholder="yyyy-MM-ddTHH:mm:ss" min="2001-01-01T00:00:00" max="2013-12-31T00:00:00" required />
|
|
<div role="alert">
|
|
<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>
|
|
</div>
|
|
<tt>value = {{example.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('example.value | date: "yyyy-MM-ddTHH:mm:ss"'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
|
|
// currently protractor/webdriver does not support
|
|
// sending keys to all known HTML5 input controls
|
|
// for various browsers (https://github.com/angular/protractor/issues/562).
|
|
function setInput(val) {
|
|
// set the value of the element and force validation.
|
|
var scr = "var ipt = document.getElementById('exampleInput'); " +
|
|
"ipt.value = '" + val + "';" +
|
|
"angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
|
|
browser.executeScript(scr);
|
|
}
|
|
|
|
it('should initialize to model', function() {
|
|
expect(value.getText()).toContain('2010-12-28T14:57:00');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
setInput('');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
setInput('2015-01-01T23:59:00');
|
|
expect(value.getText()).toContain('');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
</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 time input, a text element will be used. In that case, the text must be entered in a valid ISO-8601
|
|
* local time format (HH:mm:ss), for example: `14:57:00`. Model must be a Date object. This binding will always output a
|
|
* Date object to the model of January 1, 1970, or local date `new Date(1970, 0, 1, HH, mm, ss)`.
|
|
*
|
|
* The model must always be a Date object, otherwise AngularJS will throw an error.
|
|
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
|
*
|
|
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
|
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions}. By default,
|
|
* this is the timezone of the browser.
|
|
*
|
|
* The format of the displayed time can be adjusted with the
|
|
* {@link ng.directive:ngModelOptions#ngModelOptions-arguments ngModelOptions} `timeSecondsFormat`
|
|
* and `timeStripZeroSeconds`.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
|
|
* attribute (e.g. `min="{{minTime | date:'HH:mm:ss'}}"`). Note that `min` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* This must be a valid ISO time format (HH:mm:ss). You can also use interpolation inside this
|
|
* attribute (e.g. `max="{{maxTime | date:'HH:mm:ss'}}"`). Note that `max` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO time string the
|
|
* `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
|
|
* @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO time string the
|
|
* `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="time-input-directive" module="timeExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('timeExample', [])
|
|
.controller('DateController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: new Date(1970, 0, 1, 14, 57, 0)
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="DateController as dateCtrl">
|
|
<label for="exampleInput">Pick a time between 8am and 5pm:</label>
|
|
<input type="time" id="exampleInput" name="input" ng-model="example.value"
|
|
placeholder="HH:mm:ss" min="08:00:00" max="17:00:00" required />
|
|
<div role="alert">
|
|
<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>
|
|
</div>
|
|
<tt>value = {{example.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('example.value | date: "HH:mm:ss"'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
|
|
// currently protractor/webdriver does not support
|
|
// sending keys to all known HTML5 input controls
|
|
// for various browsers (https://github.com/angular/protractor/issues/562).
|
|
function setInput(val) {
|
|
// set the value of the element and force validation.
|
|
var scr = "var ipt = document.getElementById('exampleInput'); " +
|
|
"ipt.value = '" + val + "';" +
|
|
"angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
|
|
browser.executeScript(scr);
|
|
}
|
|
|
|
it('should initialize to model', function() {
|
|
expect(value.getText()).toContain('14:57:00');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
setInput('');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
setInput('23:59:00');
|
|
expect(value.getText()).toContain('');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
</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 AngularJS will throw an error.
|
|
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
|
*
|
|
* The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
|
|
* due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
|
|
* year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
|
|
*
|
|
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
|
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
|
|
* attribute (e.g. `min="{{minWeek | date:'yyyy-Www'}}"`). Note that `min` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* This must be a valid ISO week format (yyyy-W##). You can also use interpolation inside this
|
|
* attribute (e.g. `max="{{maxWeek | date:'yyyy-Www'}}"`). Note that `max` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
|
|
* the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
|
|
* @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
|
|
* the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="week-input-directive" module="weekExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('weekExample', [])
|
|
.controller('DateController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: new Date(2013, 0, 3)
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="DateController as dateCtrl">
|
|
<label>Pick a date between in 2013:
|
|
<input id="exampleInput" type="week" name="input" ng-model="example.value"
|
|
placeholder="YYYY-W##" min="2012-W32"
|
|
max="2013-W52" required />
|
|
</label>
|
|
<div role="alert">
|
|
<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>
|
|
</div>
|
|
<tt>value = {{example.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('example.value | date: "yyyy-Www"'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
|
|
// currently protractor/webdriver does not support
|
|
// sending keys to all known HTML5 input controls
|
|
// for various browsers (https://github.com/angular/protractor/issues/562).
|
|
function setInput(val) {
|
|
// set the value of the element and force validation.
|
|
var scr = "var ipt = document.getElementById('exampleInput'); " +
|
|
"ipt.value = '" + val + "';" +
|
|
"angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
|
|
browser.executeScript(scr);
|
|
}
|
|
|
|
it('should initialize to model', function() {
|
|
expect(value.getText()).toContain('2013-W01');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
setInput('');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
setInput('2015-W01');
|
|
expect(value.getText()).toContain('');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
</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 AngularJS will throw an error.
|
|
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
|
|
* If the model is not set to the first of the month, the next view to model update will set it
|
|
* to the first of the month.
|
|
*
|
|
* The timezone to be used to read/write the `Date` instance in the model can be defined using
|
|
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
|
|
* attribute (e.g. `min="{{minMonth | date:'yyyy-MM'}}"`). Note that `min` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* This must be a valid ISO month format (yyyy-MM). You can also use interpolation inside this
|
|
* attribute (e.g. `max="{{maxMonth | date:'yyyy-MM'}}"`). Note that `max` will also add
|
|
* native HTML5 constraint validation.
|
|
* @param {(date|string)=} ngMin Sets the `min` validation constraint to the Date / ISO week string
|
|
* the `ngMin` expression evaluates to. Note that it does not set the `min` attribute.
|
|
* @param {(date|string)=} ngMax Sets the `max` validation constraint to the Date / ISO week string
|
|
* the `ngMax` expression evaluates to. Note that it does not set the `max` attribute.
|
|
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="month-input-directive" module="monthExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('monthExample', [])
|
|
.controller('DateController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: new Date(2013, 9, 1)
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="DateController as dateCtrl">
|
|
<label for="exampleInput">Pick a month in 2013:</label>
|
|
<input id="exampleInput" type="month" name="input" ng-model="example.value"
|
|
placeholder="yyyy-MM" min="2013-01" max="2013-12" required />
|
|
<div role="alert">
|
|
<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>
|
|
</div>
|
|
<tt>value = {{example.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('example.value | date: "yyyy-MM"'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
|
|
// currently protractor/webdriver does not support
|
|
// sending keys to all known HTML5 input controls
|
|
// for various browsers (https://github.com/angular/protractor/issues/562).
|
|
function setInput(val) {
|
|
// set the value of the element and force validation.
|
|
var scr = "var ipt = document.getElementById('exampleInput'); " +
|
|
"ipt.value = '" + val + "';" +
|
|
"angular.element(ipt).scope().$apply(function(s) { s.myForm[ipt.name].$setViewValue('" + val + "'); });";
|
|
browser.executeScript(scr);
|
|
}
|
|
|
|
it('should initialize to model', function() {
|
|
expect(value.getText()).toContain('2013-10');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
setInput('');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
setInput('2015-01');
|
|
expect(value.getText()).toContain('');
|
|
expect(valid.getText()).toContain('myForm.input.$valid = false');
|
|
});
|
|
</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.
|
|
*
|
|
* <div class="alert alert-warning">
|
|
* The model must always be of type `number` otherwise AngularJS will throw an error.
|
|
* Be aware that a string containing a number is not enough. See the {@link ngModel:numfmt}
|
|
* error docs for more information and an example of how to convert your model if necessary.
|
|
* </div>
|
|
*
|
|
*
|
|
*
|
|
* @knownIssue
|
|
*
|
|
* ### HTML5 constraint validation and `allowInvalid`
|
|
*
|
|
* In browsers that follow the
|
|
* [HTML5 specification](https://html.spec.whatwg.org/multipage/forms.html#number-state-%28type=number%29),
|
|
* `input[number]` does not work as expected with {@link ngModelOptions `ngModelOptions.allowInvalid`}.
|
|
* If a non-number is entered in the input, the browser will report the value as an empty string,
|
|
* which means the view / model values in `ngModel` and subsequently the scope value
|
|
* will also be an empty string.
|
|
*
|
|
* @knownIssue
|
|
*
|
|
* ### Large numbers and `step` validation
|
|
*
|
|
* The `step` validation will not work correctly for very large numbers (e.g. 9999999999) due to
|
|
* Javascript's arithmetic limitations. If you need to handle large numbers, purpose-built
|
|
* libraries (e.g. https://github.com/MikeMcl/big.js/), can be included into AngularJS by
|
|
* {@link guide/forms#modifying-built-in-validators overwriting the validators}
|
|
* for `number` and / or `step`, or by {@link guide/forms#custom-validation applying custom validators}
|
|
* to an `input[text]` element. The source for `input[number]` type can be used as a starting
|
|
* point for both implementations.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* Can be interpolated.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* Can be interpolated.
|
|
* @param {string=} ngMin Like `min`, sets the `min` validation error key if the value entered is less than `ngMin`,
|
|
* but does not trigger HTML5 native validation. Takes an expression.
|
|
* @param {string=} ngMax Like `max`, sets the `max` validation error key if the value entered is greater than `ngMax`,
|
|
* but does not trigger HTML5 native validation. Takes an expression.
|
|
* @param {string=} step Sets the `step` validation error key if the value entered does not fit the `step` constraint.
|
|
* Can be interpolated.
|
|
* @param {string=} ngStep Like `step`, sets the `step` validation error key if the value entered does not fit the `ngStep` constraint,
|
|
* but does not trigger HTML5 native validation. Takes an expression.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `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. Setting the attribute to a negative or non-numeric value, allows view values of
|
|
* any length.
|
|
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
|
|
* that contains the regular expression body that will be converted to a regular expression
|
|
* as in the ngPattern directive.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="number-input-directive" module="numberExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('numberExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.example = {
|
|
value: 12
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>Number:
|
|
<input type="number" name="input" ng-model="example.value"
|
|
min="0" max="99" required>
|
|
</label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.number">
|
|
Not valid number!</span>
|
|
</div>
|
|
<tt>value = {{example.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('example.value'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('example.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 AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* 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. Setting the attribute to a negative or non-numeric value, allows view values of
|
|
* any length.
|
|
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
|
|
* that contains the regular expression body that will be converted to a regular expression
|
|
* as in the ngPattern directive.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="url-input-directive" module="urlExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('urlExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.url = {
|
|
text: 'http://google.com'
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>URL:
|
|
<input type="url" name="input" ng-model="url.text" required>
|
|
<label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.url">
|
|
Not valid url!</span>
|
|
</div>
|
|
<tt>text = {{url.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('url.text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('url.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 AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* 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. Setting the attribute to a negative or non-numeric value, allows view values of
|
|
* any length.
|
|
* @param {string=} pattern Similar to `ngPattern` except that the attribute value is the actual string
|
|
* that contains the regular expression body that will be converted to a regular expression
|
|
* as in the ngPattern directive.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="email-input-directive" module="emailExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('emailExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.email = {
|
|
text: 'me@example.com'
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>Email:
|
|
<input type="email" name="input" ng-model="email.text" required>
|
|
</label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.email">
|
|
Not valid email!</span>
|
|
</div>
|
|
<tt>text = {{email.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('email.text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('email.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.
|
|
*
|
|
* **Note:**<br>
|
|
* All inputs controlled by {@link ngModel ngModel} (including those of type `radio`) will use the
|
|
* value of their `name` attribute to determine the property under which their
|
|
* {@link ngModel.NgModelController NgModelController} will be published on the parent
|
|
* {@link form.FormController FormController}. Thus, if you use the same `name` for multiple
|
|
* inputs of a form (e.g. a group of radio inputs), only _one_ `NgModelController` will be
|
|
* published on the parent `FormController` under that name. The rest of the controllers will
|
|
* continue to work as expected, but you won't be able to access them as properties on the parent
|
|
* `FormController`.
|
|
*
|
|
* <div class="alert alert-info">
|
|
* <p>
|
|
* In plain HTML forms, the `name` attribute is used to identify groups of radio inputs, so
|
|
* that the browser can manage their state (checked/unchecked) based on the state of other
|
|
* inputs in the same group.
|
|
* </p>
|
|
* <p>
|
|
* In AngularJS forms, this is not necessary. The input's state will be updated based on the
|
|
* value of the underlying model data.
|
|
* </p>
|
|
* </div>
|
|
*
|
|
* <div class="alert alert-success">
|
|
* If you omit the `name` attribute on a radio input, `ngModel` will automatically assign it a
|
|
* unique name.
|
|
* </div>
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string} value The value to which the `ngModel` expression should be set when selected.
|
|
* Note that `value` only supports `string` values, i.e. the scope model needs to be a string,
|
|
* too. Use `ngValue` if you need complex models (`number`, `object`, ...).
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {string} ngValue AngularJS expression to which `ngModel` will be be set when the radio
|
|
* is selected. Should be used instead of the `value` attribute if you need
|
|
* a non-string `ngModel` (`boolean`, `array`, ...).
|
|
*
|
|
* @example
|
|
<example name="radio-input-directive" module="radioExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('radioExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.color = {
|
|
name: 'blue'
|
|
};
|
|
$scope.specialValue = {
|
|
"id": "12345",
|
|
"value": "green"
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>
|
|
<input type="radio" ng-model="color.name" value="red">
|
|
Red
|
|
</label><br/>
|
|
<label>
|
|
<input type="radio" ng-model="color.name" ng-value="specialValue">
|
|
Green
|
|
</label><br/>
|
|
<label>
|
|
<input type="radio" ng-model="color.name" value="blue">
|
|
Blue
|
|
</label><br/>
|
|
<tt>color = {{color.name | 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 inputs = element.all(by.model('color.name'));
|
|
var color = element(by.binding('color.name'));
|
|
|
|
expect(color.getText()).toContain('blue');
|
|
|
|
inputs.get(0).click();
|
|
expect(color.getText()).toContain('red');
|
|
|
|
inputs.get(1).click();
|
|
expect(color.getText()).toContain('green');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'radio': radioInputType,
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[range]
|
|
*
|
|
* @description
|
|
* Native range input with validation and transformation.
|
|
*
|
|
* The model for the range input must always be a `Number`.
|
|
*
|
|
* IE9 and other browsers that do not support the `range` type fall back
|
|
* to a text input without any default values for `min`, `max` and `step`. Model binding,
|
|
* validation and number parsing are nevertheless supported.
|
|
*
|
|
* Browsers that support range (latest Chrome, Safari, Firefox, Edge) treat `input[range]`
|
|
* in a way that never allows the input to hold an invalid value. That means:
|
|
* - any non-numerical value is set to `(max + min) / 2`.
|
|
* - any numerical value that is less than the current min val, or greater than the current max val
|
|
* is set to the min / max val respectively.
|
|
* - additionally, the current `step` is respected, so the nearest value that satisfies a step
|
|
* is used.
|
|
*
|
|
* See the [HTML Spec on input[type=range]](https://www.w3.org/TR/html5/forms.html#range-state-(type=range))
|
|
* for more info.
|
|
*
|
|
* This has the following consequences for AngularJS:
|
|
*
|
|
* Since the element value should always reflect the current model value, a range input
|
|
* will set the bound ngModel expression to the value that the browser has set for the
|
|
* input element. For example, in the following input `<input type="range" ng-model="model.value">`,
|
|
* if the application sets `model.value = null`, the browser will set the input to `'50'`.
|
|
* AngularJS will then set the model to `50`, to prevent input and model value being out of sync.
|
|
*
|
|
* That means the model for range will immediately be set to `50` after `ngModel` has been
|
|
* initialized. It also means a range input can never have the required error.
|
|
*
|
|
* This does not only affect changes to the model value, but also to the values of the `min`,
|
|
* `max`, and `step` attributes. When these change in a way that will cause the browser to modify
|
|
* the input value, AngularJS will also update the model value.
|
|
*
|
|
* Automatic value adjustment also means that a range input element can never have the `required`,
|
|
* `min`, or `max` errors.
|
|
*
|
|
* However, `step` is currently only fully implemented by Firefox. Other browsers have problems
|
|
* when the step value changes dynamically - they do not adjust the element value correctly, but
|
|
* instead may set the `stepMismatch` error. If that's the case, the AngularJS will set the `step`
|
|
* error on the input, and set the model to `undefined`.
|
|
*
|
|
* Note that `input[range]` is not compatible with`ngMax`, `ngMin`, and `ngStep`, because they do
|
|
* not set the `min` and `max` attributes, which means that the browser won't automatically adjust
|
|
* the input value based on their values, and will always assume min = 0, max = 100, and step = 1.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation to ensure that the value entered is greater
|
|
* than `min`. Can be interpolated.
|
|
* @param {string=} max Sets the `max` validation to ensure that the value entered is less than `max`.
|
|
* Can be interpolated.
|
|
* @param {string=} step Sets the `step` validation to ensure that the value entered matches the `step`
|
|
* Can be interpolated.
|
|
* @param {expression=} ngChange AngularJS expression to be executed when the ngModel value changes due
|
|
* to user interaction with the input element.
|
|
* @param {expression=} ngChecked If the expression is truthy, then the `checked` attribute will be set on the
|
|
* element. **Note** : `ngChecked` should not be used alongside `ngModel`.
|
|
* Checkout {@link ng.directive:ngChecked ngChecked} for usage.
|
|
*
|
|
* @example
|
|
<example name="range-input-directive" module="rangeExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('rangeExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.value = 75;
|
|
$scope.min = 10;
|
|
$scope.max = 90;
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
|
|
Model as range: <input type="range" name="range" ng-model="value" min="{{min}}" max="{{max}}">
|
|
<hr>
|
|
Model as number: <input type="number" ng-model="value"><br>
|
|
Min: <input type="number" ng-model="min"><br>
|
|
Max: <input type="number" ng-model="max"><br>
|
|
value = <code>{{value}}</code><br/>
|
|
myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
|
|
myForm.range.$error = <code>{{myForm.range.$error}}</code>
|
|
</form>
|
|
</file>
|
|
</example>
|
|
|
|
* ## Range Input with ngMin & ngMax attributes
|
|
|
|
* @example
|
|
<example name="range-input-directive-ng" module="rangeExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('rangeExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.value = 75;
|
|
$scope.min = 10;
|
|
$scope.max = 90;
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
Model as range: <input type="range" name="range" ng-model="value" ng-min="min" ng-max="max">
|
|
<hr>
|
|
Model as number: <input type="number" ng-model="value"><br>
|
|
Min: <input type="number" ng-model="min"><br>
|
|
Max: <input type="number" ng-model="max"><br>
|
|
value = <code>{{value}}</code><br/>
|
|
myForm.range.$valid = <code>{{myForm.range.$valid}}</code><br/>
|
|
myForm.range.$error = <code>{{myForm.range.$error}}</code>
|
|
</form>
|
|
</file>
|
|
</example>
|
|
|
|
*/
|
|
'range': rangeInputType,
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[checkbox]
|
|
*
|
|
* @description
|
|
* HTML checkbox.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {expression=} ngTrueValue The value to which the expression should be set when selected.
|
|
* @param {expression=} ngFalseValue The value to which the expression should be set when not selected.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="checkbox-input-directive" module="checkboxExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('checkboxExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.checkboxModel = {
|
|
value1 : true,
|
|
value2 : 'YES'
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<label>Value1:
|
|
<input type="checkbox" ng-model="checkboxModel.value1">
|
|
</label><br/>
|
|
<label>Value2:
|
|
<input type="checkbox" ng-model="checkboxModel.value2"
|
|
ng-true-value="'YES'" ng-false-value="'NO'">
|
|
</label><br/>
|
|
<tt>value1 = {{checkboxModel.value1}}</tt><br/>
|
|
<tt>value2 = {{checkboxModel.value2}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
it('should change state', function() {
|
|
var value1 = element(by.binding('checkboxModel.value1'));
|
|
var value2 = element(by.binding('checkboxModel.value2'));
|
|
|
|
expect(value1.getText()).toContain('true');
|
|
expect(value2.getText()).toContain('YES');
|
|
|
|
element(by.model('checkboxModel.value1')).click();
|
|
element(by.model('checkboxModel.value2')).click();
|
|
|
|
expect(value1.getText()).toContain('false');
|
|
expect(value2.getText()).toContain('NO');
|
|
});
|
|
</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 type = lowercase(element[0].type);
|
|
|
|
// In composition mode, users are still inputting intermediate text buffer,
|
|
// hold the listener until composition is done.
|
|
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
|
if (!$sniffer.android) {
|
|
var composing = false;
|
|
|
|
element.on('compositionstart', function() {
|
|
composing = true;
|
|
});
|
|
|
|
// Support: IE9+
|
|
element.on('compositionupdate', function(ev) {
|
|
// End composition when ev.data is empty string on 'compositionupdate' event.
|
|
// When the input de-focusses (e.g. by clicking away), IE triggers 'compositionupdate'
|
|
// instead of 'compositionend'.
|
|
if (isUndefined(ev.data) || ev.data === '') {
|
|
composing = false;
|
|
}
|
|
});
|
|
|
|
element.on('compositionend', function() {
|
|
composing = false;
|
|
listener();
|
|
});
|
|
}
|
|
|
|
var timeout;
|
|
|
|
var listener = function(ev) {
|
|
if (timeout) {
|
|
$browser.defer.cancel(timeout);
|
|
timeout = null;
|
|
}
|
|
if (composing) return;
|
|
var value = element.val(),
|
|
event = ev && ev.type;
|
|
|
|
// By default we will trim the value
|
|
// If the attribute ng-trim exists we will avoid trimming
|
|
// If input type is 'password', the value is never trimmed
|
|
if (type !== 'password' && (!attr.ngTrim || attr.ngTrim !== 'false')) {
|
|
value = trim(value);
|
|
}
|
|
|
|
// If a control is suffering from bad input (due to native validators), browsers discard its
|
|
// value, so it may be necessary to revalidate (by calling $setViewValue again) even if the
|
|
// control's value is the same empty value twice in a row.
|
|
if (ctrl.$viewValue !== value || (value === '' && ctrl.$$hasNativeValidators)) {
|
|
ctrl.$setViewValue(value, event);
|
|
}
|
|
};
|
|
|
|
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
|
|
// input event on backspace, delete or cut
|
|
if ($sniffer.hasEvent('input')) {
|
|
element.on('input', listener);
|
|
} else {
|
|
var deferListener = function(ev, input, origValue) {
|
|
if (!timeout) {
|
|
timeout = $browser.defer(function() {
|
|
timeout = null;
|
|
if (!input || input.value !== origValue) {
|
|
listener(ev);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
element.on('keydown', /** @this */ function(event) {
|
|
var key = event.keyCode;
|
|
|
|
// ignore
|
|
// command modifiers arrows
|
|
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
|
|
|
|
deferListener(event, this, this.value);
|
|
});
|
|
|
|
// if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it
|
|
if ($sniffer.hasEvent('paste')) {
|
|
element.on('paste cut drop', deferListener);
|
|
}
|
|
}
|
|
|
|
// if user paste into input using mouse on older browser
|
|
// or form autocomplete on newer browser, we need "change" event to catch it
|
|
element.on('change', listener);
|
|
|
|
// Some native input types (date-family) have the ability to change validity without
|
|
// firing any input/change events.
|
|
// For these event types, when native validators are present and the browser supports the type,
|
|
// check for validity changes on various DOM events.
|
|
if (PARTIAL_VALIDATION_TYPES[type] && ctrl.$$hasNativeValidators && type === attr.type) {
|
|
element.on(PARTIAL_VALIDATION_EVENTS, /** @this */ function(ev) {
|
|
if (!timeout) {
|
|
var validity = this[VALIDITY_STATE_PROPERTY];
|
|
var origBadInput = validity.badInput;
|
|
var origTypeMismatch = validity.typeMismatch;
|
|
timeout = $browser.defer(function() {
|
|
timeout = null;
|
|
if (validity.badInput !== origBadInput || validity.typeMismatch !== origTypeMismatch) {
|
|
listener(ev);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
ctrl.$render = function() {
|
|
// Workaround for Firefox validation #12102.
|
|
var value = ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue;
|
|
if (element.val() !== value) {
|
|
element.val(value);
|
|
}
|
|
};
|
|
}
|
|
|
|
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, previousDate) {
|
|
var parts, map;
|
|
|
|
if (isDate(iso)) {
|
|
return iso;
|
|
}
|
|
|
|
if (isString(iso)) {
|
|
// When a date is JSON'ified to wraps itself inside of an extra
|
|
// set of double quotes. This makes the date parsing code unable
|
|
// to match the date string and parse it as a date.
|
|
if (iso.charAt(0) === '"' && iso.charAt(iso.length - 1) === '"') {
|
|
iso = iso.substring(1, iso.length - 1);
|
|
}
|
|
if (ISO_DATE_REGEXP.test(iso)) {
|
|
return new Date(iso);
|
|
}
|
|
regexp.lastIndex = 0;
|
|
parts = regexp.exec(iso);
|
|
|
|
if (parts) {
|
|
parts.shift();
|
|
if (previousDate) {
|
|
map = {
|
|
yyyy: previousDate.getFullYear(),
|
|
MM: previousDate.getMonth() + 1,
|
|
dd: previousDate.getDate(),
|
|
HH: previousDate.getHours(),
|
|
mm: previousDate.getMinutes(),
|
|
ss: previousDate.getSeconds(),
|
|
sss: previousDate.getMilliseconds() / 1000
|
|
};
|
|
} else {
|
|
map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 };
|
|
}
|
|
|
|
forEach(parts, function(part, index) {
|
|
if (index < mapping.length) {
|
|
map[mapping[index]] = +part;
|
|
}
|
|
});
|
|
|
|
var date = new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0);
|
|
if (map.yyyy < 100) {
|
|
// In the constructor, 2-digit years map to 1900-1999.
|
|
// Use `setFullYear()` to set the correct year.
|
|
date.setFullYear(map.yyyy);
|
|
}
|
|
|
|
return date;
|
|
}
|
|
}
|
|
|
|
return NaN;
|
|
};
|
|
}
|
|
|
|
function createDateInputType(type, regexp, parseDate, format) {
|
|
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
|
badInputChecker(scope, element, attr, ctrl, type);
|
|
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
var isTimeType = type === 'time' || type === 'datetimelocal';
|
|
var previousDate;
|
|
var previousTimezone;
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
if (ctrl.$isEmpty(value)) return null;
|
|
|
|
if (regexp.test(value)) {
|
|
// Note: We cannot read ctrl.$modelValue, as there might be a different
|
|
// parser/formatter in the processing chain so that the model
|
|
// contains some different data format!
|
|
return parseDateAndConvertTimeZoneToLocal(value, previousDate);
|
|
}
|
|
ctrl.$$parserName = type;
|
|
return undefined;
|
|
});
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
if (value && !isDate(value)) {
|
|
throw ngModelMinErr('datefmt', 'Expected `{0}` to be a date', value);
|
|
}
|
|
if (isValidDate(value)) {
|
|
previousDate = value;
|
|
var timezone = ctrl.$options.getOption('timezone');
|
|
|
|
if (timezone) {
|
|
previousTimezone = timezone;
|
|
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
|
|
}
|
|
|
|
return formatter(value, timezone);
|
|
} else {
|
|
previousDate = null;
|
|
previousTimezone = null;
|
|
return '';
|
|
}
|
|
});
|
|
|
|
if (isDefined(attr.min) || attr.ngMin) {
|
|
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
|
var parsedMinVal = parseObservedDateValue(minVal);
|
|
|
|
ctrl.$validators.min = function(value) {
|
|
return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
|
|
};
|
|
attr.$observe('min', function(val) {
|
|
if (val !== minVal) {
|
|
parsedMinVal = parseObservedDateValue(val);
|
|
minVal = val;
|
|
ctrl.$validate();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isDefined(attr.max) || attr.ngMax) {
|
|
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
|
var parsedMaxVal = parseObservedDateValue(maxVal);
|
|
|
|
ctrl.$validators.max = function(value) {
|
|
return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
|
|
};
|
|
attr.$observe('max', function(val) {
|
|
if (val !== maxVal) {
|
|
parsedMaxVal = parseObservedDateValue(val);
|
|
maxVal = val;
|
|
ctrl.$validate();
|
|
}
|
|
});
|
|
}
|
|
|
|
function isValidDate(value) {
|
|
// Invalid Date: getTime() returns NaN
|
|
return value && !(value.getTime && value.getTime() !== value.getTime());
|
|
}
|
|
|
|
function parseObservedDateValue(val) {
|
|
return isDefined(val) && !isDate(val) ? parseDateAndConvertTimeZoneToLocal(val) || undefined : val;
|
|
}
|
|
|
|
function parseDateAndConvertTimeZoneToLocal(value, previousDate) {
|
|
var timezone = ctrl.$options.getOption('timezone');
|
|
|
|
if (previousTimezone && previousTimezone !== timezone) {
|
|
// If the timezone has changed, adjust the previousDate to the default timezone
|
|
// so that the new date is converted with the correct timezone offset
|
|
previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone));
|
|
}
|
|
|
|
var parsedDate = parseDate(value, previousDate);
|
|
|
|
if (!isNaN(parsedDate) && timezone) {
|
|
parsedDate = convertTimezoneToLocal(parsedDate, timezone);
|
|
}
|
|
return parsedDate;
|
|
}
|
|
|
|
function formatter(value, timezone) {
|
|
var targetFormat = format;
|
|
|
|
if (isTimeType && isString(ctrl.$options.getOption('timeSecondsFormat'))) {
|
|
targetFormat = format
|
|
.replace('ss.sss', ctrl.$options.getOption('timeSecondsFormat'))
|
|
.replace(/:$/, '');
|
|
}
|
|
|
|
var formatted = $filter('date')(value, targetFormat, timezone);
|
|
|
|
if (isTimeType && ctrl.$options.getOption('timeStripZeroSeconds')) {
|
|
formatted = formatted.replace(/(?::00)?(?:\.000)?$/, '');
|
|
}
|
|
|
|
return formatted;
|
|
}
|
|
};
|
|
}
|
|
|
|
function badInputChecker(scope, element, attr, ctrl, parserName) {
|
|
var node = element[0];
|
|
var nativeValidation = ctrl.$$hasNativeValidators = isObject(node.validity);
|
|
if (nativeValidation) {
|
|
ctrl.$parsers.push(function(value) {
|
|
var validity = element.prop(VALIDITY_STATE_PROPERTY) || {};
|
|
if (validity.badInput || validity.typeMismatch) {
|
|
ctrl.$$parserName = parserName;
|
|
return undefined;
|
|
}
|
|
|
|
return value;
|
|
});
|
|
}
|
|
}
|
|
|
|
function numberFormatterParser(ctrl) {
|
|
ctrl.$parsers.push(function(value) {
|
|
if (ctrl.$isEmpty(value)) return null;
|
|
if (NUMBER_REGEXP.test(value)) return parseFloat(value);
|
|
|
|
ctrl.$$parserName = 'number';
|
|
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;
|
|
});
|
|
}
|
|
|
|
function parseNumberAttrVal(val) {
|
|
if (isDefined(val) && !isNumber(val)) {
|
|
val = parseFloat(val);
|
|
}
|
|
return !isNumberNaN(val) ? val : undefined;
|
|
}
|
|
|
|
function isNumberInteger(num) {
|
|
// See http://stackoverflow.com/questions/14636536/how-to-check-if-a-variable-is-an-integer-in-javascript#14794066
|
|
// (minus the assumption that `num` is a number)
|
|
|
|
// eslint-disable-next-line no-bitwise
|
|
return (num | 0) === num;
|
|
}
|
|
|
|
function countDecimals(num) {
|
|
var numString = num.toString();
|
|
var decimalSymbolIndex = numString.indexOf('.');
|
|
|
|
if (decimalSymbolIndex === -1) {
|
|
if (-1 < num && num < 1) {
|
|
// It may be in the exponential notation format (`1e-X`)
|
|
var match = /e-(\d+)$/.exec(numString);
|
|
|
|
if (match) {
|
|
return Number(match[1]);
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
return numString.length - decimalSymbolIndex - 1;
|
|
}
|
|
|
|
function isValidForStep(viewValue, stepBase, step) {
|
|
// At this point `stepBase` and `step` are expected to be non-NaN values
|
|
// and `viewValue` is expected to be a valid stringified number.
|
|
var value = Number(viewValue);
|
|
|
|
var isNonIntegerValue = !isNumberInteger(value);
|
|
var isNonIntegerStepBase = !isNumberInteger(stepBase);
|
|
var isNonIntegerStep = !isNumberInteger(step);
|
|
|
|
// Due to limitations in Floating Point Arithmetic (e.g. `0.3 - 0.2 !== 0.1` or
|
|
// `0.5 % 0.1 !== 0`), we need to convert all numbers to integers.
|
|
if (isNonIntegerValue || isNonIntegerStepBase || isNonIntegerStep) {
|
|
var valueDecimals = isNonIntegerValue ? countDecimals(value) : 0;
|
|
var stepBaseDecimals = isNonIntegerStepBase ? countDecimals(stepBase) : 0;
|
|
var stepDecimals = isNonIntegerStep ? countDecimals(step) : 0;
|
|
|
|
var decimalCount = Math.max(valueDecimals, stepBaseDecimals, stepDecimals);
|
|
var multiplier = Math.pow(10, decimalCount);
|
|
|
|
value = value * multiplier;
|
|
stepBase = stepBase * multiplier;
|
|
step = step * multiplier;
|
|
|
|
if (isNonIntegerValue) value = Math.round(value);
|
|
if (isNonIntegerStepBase) stepBase = Math.round(stepBase);
|
|
if (isNonIntegerStep) step = Math.round(step);
|
|
}
|
|
|
|
return (value - stepBase) % step === 0;
|
|
}
|
|
|
|
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
|
|
badInputChecker(scope, element, attr, ctrl, 'number');
|
|
numberFormatterParser(ctrl);
|
|
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
var parsedMinVal;
|
|
|
|
if (isDefined(attr.min) || attr.ngMin) {
|
|
var minVal = attr.min || $parse(attr.ngMin)(scope);
|
|
parsedMinVal = parseNumberAttrVal(minVal);
|
|
|
|
ctrl.$validators.min = function(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
|
|
};
|
|
|
|
attr.$observe('min', function(val) {
|
|
if (val !== minVal) {
|
|
parsedMinVal = parseNumberAttrVal(val);
|
|
minVal = val;
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
ctrl.$validate();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isDefined(attr.max) || attr.ngMax) {
|
|
var maxVal = attr.max || $parse(attr.ngMax)(scope);
|
|
var parsedMaxVal = parseNumberAttrVal(maxVal);
|
|
|
|
ctrl.$validators.max = function(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
|
|
};
|
|
|
|
attr.$observe('max', function(val) {
|
|
if (val !== maxVal) {
|
|
parsedMaxVal = parseNumberAttrVal(val);
|
|
maxVal = val;
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
ctrl.$validate();
|
|
}
|
|
});
|
|
}
|
|
|
|
if (isDefined(attr.step) || attr.ngStep) {
|
|
var stepVal = attr.step || $parse(attr.ngStep)(scope);
|
|
var parsedStepVal = parseNumberAttrVal(stepVal);
|
|
|
|
ctrl.$validators.step = function(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
|
|
isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
|
|
};
|
|
|
|
attr.$observe('step', function(val) {
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
if (val !== stepVal) {
|
|
parsedStepVal = parseNumberAttrVal(val);
|
|
stepVal = val;
|
|
ctrl.$validate();
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
}
|
|
|
|
function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
badInputChecker(scope, element, attr, ctrl, 'range');
|
|
numberFormatterParser(ctrl);
|
|
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
var supportsRange = ctrl.$$hasNativeValidators && element[0].type === 'range',
|
|
minVal = supportsRange ? 0 : undefined,
|
|
maxVal = supportsRange ? 100 : undefined,
|
|
stepVal = supportsRange ? 1 : undefined,
|
|
validity = element[0].validity,
|
|
hasMinAttr = isDefined(attr.min),
|
|
hasMaxAttr = isDefined(attr.max),
|
|
hasStepAttr = isDefined(attr.step);
|
|
|
|
var originalRender = ctrl.$render;
|
|
|
|
ctrl.$render = supportsRange && isDefined(validity.rangeUnderflow) && isDefined(validity.rangeOverflow) ?
|
|
//Browsers that implement range will set these values automatically, but reading the adjusted values after
|
|
//$render would cause the min / max validators to be applied with the wrong value
|
|
function rangeRender() {
|
|
originalRender();
|
|
ctrl.$setViewValue(element.val());
|
|
} :
|
|
originalRender;
|
|
|
|
if (hasMinAttr) {
|
|
minVal = parseNumberAttrVal(attr.min);
|
|
|
|
ctrl.$validators.min = supportsRange ?
|
|
// Since all browsers set the input to a valid value, we don't need to check validity
|
|
function noopMinValidator() { return true; } :
|
|
// non-support browsers validate the min val
|
|
function minValidator(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
|
|
};
|
|
|
|
setInitialValueAndObserver('min', minChange);
|
|
}
|
|
|
|
if (hasMaxAttr) {
|
|
maxVal = parseNumberAttrVal(attr.max);
|
|
|
|
ctrl.$validators.max = supportsRange ?
|
|
// Since all browsers set the input to a valid value, we don't need to check validity
|
|
function noopMaxValidator() { return true; } :
|
|
// non-support browsers validate the max val
|
|
function maxValidator(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
|
|
};
|
|
|
|
setInitialValueAndObserver('max', maxChange);
|
|
}
|
|
|
|
if (hasStepAttr) {
|
|
stepVal = parseNumberAttrVal(attr.step);
|
|
|
|
ctrl.$validators.step = supportsRange ?
|
|
function nativeStepValidator() {
|
|
// Currently, only FF implements the spec on step change correctly (i.e. adjusting the
|
|
// input element value to a valid value). It's possible that other browsers set the stepMismatch
|
|
// validity error instead, so we can at least report an error in that case.
|
|
return !validity.stepMismatch;
|
|
} :
|
|
// ngStep doesn't set the setp attr, so the browser doesn't adjust the input value as setting step would
|
|
function stepValidator(modelValue, viewValue) {
|
|
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
|
|
isValidForStep(viewValue, minVal || 0, stepVal);
|
|
};
|
|
|
|
setInitialValueAndObserver('step', stepChange);
|
|
}
|
|
|
|
function setInitialValueAndObserver(htmlAttrName, changeFn) {
|
|
// interpolated attributes set the attribute value only after a digest, but we need the
|
|
// attribute value when the input is first rendered, so that the browser can adjust the
|
|
// input value based on the min/max value
|
|
element.attr(htmlAttrName, attr[htmlAttrName]);
|
|
var oldVal = attr[htmlAttrName];
|
|
attr.$observe(htmlAttrName, function wrappedObserver(val) {
|
|
if (val !== oldVal) {
|
|
oldVal = val;
|
|
changeFn(val);
|
|
}
|
|
});
|
|
}
|
|
|
|
function minChange(val) {
|
|
minVal = parseNumberAttrVal(val);
|
|
// ignore changes before model is initialized
|
|
if (isNumberNaN(ctrl.$modelValue)) {
|
|
return;
|
|
}
|
|
|
|
if (supportsRange) {
|
|
var elVal = element.val();
|
|
// IE11 doesn't set the el val correctly if the minVal is greater than the element value
|
|
if (minVal > elVal) {
|
|
elVal = minVal;
|
|
element.val(elVal);
|
|
}
|
|
ctrl.$setViewValue(elVal);
|
|
} else {
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
ctrl.$validate();
|
|
}
|
|
}
|
|
|
|
function maxChange(val) {
|
|
maxVal = parseNumberAttrVal(val);
|
|
// ignore changes before model is initialized
|
|
if (isNumberNaN(ctrl.$modelValue)) {
|
|
return;
|
|
}
|
|
|
|
if (supportsRange) {
|
|
var elVal = element.val();
|
|
// IE11 doesn't set the el val correctly if the maxVal is less than the element value
|
|
if (maxVal < elVal) {
|
|
element.val(maxVal);
|
|
// IE11 and Chrome don't set the value to the minVal when max < min
|
|
elVal = maxVal < minVal ? minVal : maxVal;
|
|
}
|
|
ctrl.$setViewValue(elVal);
|
|
} else {
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
ctrl.$validate();
|
|
}
|
|
}
|
|
|
|
function stepChange(val) {
|
|
stepVal = parseNumberAttrVal(val);
|
|
// ignore changes before model is initialized
|
|
if (isNumberNaN(ctrl.$modelValue)) {
|
|
return;
|
|
}
|
|
|
|
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
|
|
if (!supportsRange) {
|
|
// TODO(matsko): implement validateLater to reduce number of validations
|
|
ctrl.$validate();
|
|
} else if (ctrl.$viewValue !== element.val()) {
|
|
ctrl.$setViewValue(element.val());
|
|
}
|
|
}
|
|
}
|
|
|
|
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
// 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.$validators.url = function(modelValue, viewValue) {
|
|
var value = modelValue || viewValue;
|
|
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.$validators.email = function(modelValue, viewValue) {
|
|
var value = modelValue || viewValue;
|
|
return ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value);
|
|
};
|
|
}
|
|
|
|
function radioInputType(scope, element, attr, ctrl) {
|
|
var doTrim = !attr.ngTrim || trim(attr.ngTrim) !== 'false';
|
|
// make the name unique, if not defined
|
|
if (isUndefined(attr.name)) {
|
|
element.attr('name', nextUid());
|
|
}
|
|
|
|
var listener = function(ev) {
|
|
var value;
|
|
if (element[0].checked) {
|
|
value = attr.value;
|
|
if (doTrim) {
|
|
value = trim(value);
|
|
}
|
|
ctrl.$setViewValue(value, ev && ev.type);
|
|
}
|
|
};
|
|
|
|
element.on('change', listener);
|
|
|
|
ctrl.$render = function() {
|
|
var value = attr.value;
|
|
if (doTrim) {
|
|
value = trim(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 ngModelMinErr('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('change', listener);
|
|
|
|
ctrl.$render = function() {
|
|
element[0].checked = ctrl.$viewValue;
|
|
};
|
|
|
|
// Override the standard `$isEmpty` because the $viewValue of an empty checkbox is always set to `false`
|
|
// This is because of the parser below, which compares the `$modelValue` with `trueValue` to convert
|
|
// it to a boolean.
|
|
ctrl.$isEmpty = function(value) {
|
|
return value === false;
|
|
};
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return equals(value, trueValue);
|
|
});
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
return value ? trueValue : falseValue;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name textarea
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* HTML textarea element control with AngularJS data-binding. The data-binding and validation
|
|
* properties of this element are exactly the same as those of the
|
|
* {@link ng.directive:input input element}.
|
|
*
|
|
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* 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. Setting the attribute to a negative or non-numeric value, allows view values of any
|
|
* length.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
|
|
*
|
|
* @knownIssue
|
|
*
|
|
* When specifying the `placeholder` attribute of `<textarea>`, Internet Explorer will temporarily
|
|
* insert the placeholder value as the textarea's content. If the placeholder value contains
|
|
* interpolation (`{{ ... }}`), an error will be logged in the console when AngularJS tries to update
|
|
* the value of the by-then-removed text node. This doesn't affect the functionality of the
|
|
* textarea, but can be undesirable.
|
|
*
|
|
* You can work around this Internet Explorer issue by using `ng-attr-placeholder` instead of
|
|
* `placeholder` on textareas, whenever you need interpolation in the placeholder value. You can
|
|
* find more details on `ngAttr` in the
|
|
* [Interpolation](guide/interpolation#-ngattr-for-binding-to-arbitrary-attributes) section of the
|
|
* Developer Guide.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name 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 AngularJS expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {boolean=} ngRequired Sets `required` attribute if set to true
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength. Setting the attribute to a negative or non-numeric value, allows view values of any
|
|
* length.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the ngModel {@link ngModel.NgModelController#$viewValue $viewValue}
|
|
* value does not match a RegExp found by evaluating the AngularJS expression given in the attribute value.
|
|
* If the expression evaluates to a RegExp object, then this is used directly.
|
|
* If the expression evaluates to a string, then it will be converted to a RegExp
|
|
* after wrapping it in `^` and `$` characters. For instance, `"abc"` will be converted to
|
|
* `new RegExp('^abc$')`.<br />
|
|
* **Note:** Avoid using the `g` flag on the RegExp, as it will cause each successive search to
|
|
* start at the index of the last search's match, thus not taking the whole input value into
|
|
* account.
|
|
* @param {string=} ngChange AngularJS expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false AngularJS will not automatically trim the input.
|
|
* This parameter is ignored for input[type=password] controls, which will never trim the
|
|
* input.
|
|
*
|
|
* @example
|
|
<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">
|
|
<label>
|
|
User name:
|
|
<input type="text" name="userName" ng-model="user.name" required>
|
|
</label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.userName.$error.required">
|
|
Required!</span>
|
|
</div>
|
|
<label>
|
|
Last name:
|
|
<input type="text" name="lastName" ng-model="user.last"
|
|
ng-minlength="3" ng-maxlength="10">
|
|
</label>
|
|
<div role="alert">
|
|
<span class="error" ng-show="myForm.lastName.$error.minlength">
|
|
Too short!</span>
|
|
<span class="error" ng-show="myForm.lastName.$error.maxlength">
|
|
Too long!</span>
|
|
</div>
|
|
</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 hiddenInputBrowserCacheDirective = function() {
|
|
var valueProperty = {
|
|
configurable: true,
|
|
enumerable: false,
|
|
get: function() {
|
|
return this.getAttribute('value') || '';
|
|
},
|
|
set: function(val) {
|
|
this.setAttribute('value', val);
|
|
}
|
|
};
|
|
|
|
return {
|
|
restrict: 'E',
|
|
priority: 200,
|
|
compile: function(_, attr) {
|
|
if (lowercase(attr.type) !== 'hidden') {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
pre: function(scope, element, attr, ctrls) {
|
|
var node = element[0];
|
|
|
|
// Support: Edge
|
|
// Moving the DOM around prevents autofillling
|
|
if (node.parentNode) {
|
|
node.parentNode.insertBefore(node, node.nextSibling);
|
|
}
|
|
|
|
// Support: FF, IE
|
|
// Avoiding direct assignment to .value prevents autofillling
|
|
if (Object.defineProperty) {
|
|
Object.defineProperty(node, 'value', valueProperty);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
|
|
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ngValue
|
|
* @restrict A
|
|
* @priority 100
|
|
*
|
|
* @description
|
|
* Binds the given expression to the value of the element.
|
|
*
|
|
* It is mainly used on {@link input[radio] `input[radio]`} and option elements,
|
|
* so that when the element is selected, the {@link ngModel `ngModel`} of that element (or its
|
|
* {@link select `select`} parent element) is set to the bound value. It is especially useful
|
|
* for dynamically generated lists using {@link ngRepeat `ngRepeat`}, as shown below.
|
|
*
|
|
* It can also be used to achieve one-way binding of a given expression to an input element
|
|
* such as an `input[text]` or a `textarea`, when that element does not use ngModel.
|
|
*
|
|
* @element ANY
|
|
* @param {string=} ngValue AngularJS expression, whose value will be bound to the `value` attribute
|
|
* and `value` property of the element.
|
|
*
|
|
* @example
|
|
<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() {
|
|
/**
|
|
* inputs use the value attribute as their default value if the value property is not set.
|
|
* Once the value property has been set (by adding input), it will not react to changes to
|
|
* the value attribute anymore. Setting both attribute and property fixes this behavior, and
|
|
* makes it possible to use ngValue as a sort of one-way bind.
|
|
*/
|
|
function updateElementValue(element, attr, value) {
|
|
// Support: IE9 only
|
|
// In IE9 values are converted to string (e.g. `input.value = null` results in `input.value === 'null'`).
|
|
var propValue = isDefined(value) ? value : (msie === 9) ? '' : null;
|
|
element.prop('value', propValue);
|
|
attr.$set('value', value);
|
|
}
|
|
|
|
return {
|
|
restrict: 'A',
|
|
priority: 100,
|
|
compile: function(tpl, tplAttr) {
|
|
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
|
|
return function ngValueConstantLink(scope, elm, attr) {
|
|
var value = scope.$eval(attr.ngValue);
|
|
updateElementValue(elm, attr, value);
|
|
};
|
|
} else {
|
|
return function ngValueLink(scope, elm, attr) {
|
|
scope.$watch(attr.ngValue, function valueWatchAction(value) {
|
|
updateElementValue(elm, attr, value);
|
|
});
|
|
};
|
|
}
|
|
}
|
|
};
|
|
};
|