mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-09 09:00:34 +08:00
Do not trim input[type=password] values BREAKING CHANGE: Previously, input[type=password] would trim values by default, and would require an explicit ng-trim="false" to disable the trimming behaviour. After this CL, ng-trim no longer effects input[type=password], and will never trim the password value. Closes #8250 Closes #8230 Conflicts: src/ng/directive/input.js
1647 lines
60 KiB
JavaScript
1647 lines
60 KiB
JavaScript
'use strict';
|
|
|
|
/* global VALID_CLASS: true,
|
|
INVALID_CLASS: true,
|
|
PRISTINE_CLASS: true,
|
|
DIRTY_CLASS: true
|
|
*/
|
|
|
|
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
|
|
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
|
|
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
|
|
|
|
var inputType = {
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[text]
|
|
*
|
|
* @description
|
|
* Standard HTML text input with angular data binding, inherited by most of the `input` elements.
|
|
*
|
|
* *NOTE* Not every feature offered is available for all input types.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Adds `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
|
|
* This parameter is ignored for input[type=password] controls, which will never trim the
|
|
* input.
|
|
*
|
|
* @example
|
|
<example name="text-input-directive" module="textInputExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('textInputExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.text = 'guest';
|
|
$scope.word = /^\s*\w*\s*$/;
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
Single word: <input type="text" name="input" ng-model="text"
|
|
ng-pattern="word" required ng-trim="false">
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.pattern">
|
|
Single word only!</span>
|
|
|
|
<tt>text = {{text}}</tt><br/>
|
|
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
|
|
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var text = element(by.binding('text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('text'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(text.getText()).toContain('guest');
|
|
expect(valid.getText()).toContain('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input.clear();
|
|
input.sendKeys('');
|
|
|
|
expect(text.getText()).toEqual('text =');
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
|
|
it('should be invalid if multi word', function() {
|
|
input.clear();
|
|
input.sendKeys('hello world');
|
|
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'text': textInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[number]
|
|
*
|
|
* @description
|
|
* Text input with number validation and transformation. Sets the `number` validation
|
|
* error if not a valid number.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} min Sets the `min` validation error key if the value entered is less than `min`.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater than `max`.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="number-input-directive" module="numberExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('numberExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.value = 12;
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
Number: <input type="number" name="input" ng-model="value"
|
|
min="0" max="99" required>
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.number">
|
|
Not valid number!</span>
|
|
<tt>value = {{value}}</tt><br/>
|
|
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
|
|
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var value = element(by.binding('value'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('value'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(value.getText()).toContain('12');
|
|
expect(valid.getText()).toContain('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input.clear();
|
|
input.sendKeys('');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
input.clear();
|
|
input.sendKeys('123');
|
|
expect(value.getText()).toEqual('value =');
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'number': numberInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[url]
|
|
*
|
|
* @description
|
|
* Text input with URL validation. Sets the `url` validation error key if the content is not a
|
|
* valid URL.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="url-input-directive" module="urlExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('urlExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.text = 'http://google.com';
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
URL: <input type="url" name="input" ng-model="text" required>
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.url">
|
|
Not valid url!</span>
|
|
<tt>text = {{text}}</tt><br/>
|
|
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
|
|
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
|
|
<tt>myForm.$error.url = {{!!myForm.$error.url}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var text = element(by.binding('text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('text'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(text.getText()).toContain('http://google.com');
|
|
expect(valid.getText()).toContain('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input.clear();
|
|
input.sendKeys('');
|
|
|
|
expect(text.getText()).toEqual('text =');
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
|
|
it('should be invalid if not url', function() {
|
|
input.clear();
|
|
input.sendKeys('box');
|
|
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'url': urlInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[email]
|
|
*
|
|
* @description
|
|
* Text input with email validation. Sets the `email` validation error key if not a valid email
|
|
* address.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="email-input-directive" module="emailExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('emailExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.text = 'me@example.com';
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
Email: <input type="email" name="input" ng-model="text" required>
|
|
<span class="error" ng-show="myForm.input.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.input.$error.email">
|
|
Not valid email!</span>
|
|
<tt>text = {{text}}</tt><br/>
|
|
<tt>myForm.input.$valid = {{myForm.input.$valid}}</tt><br/>
|
|
<tt>myForm.input.$error = {{myForm.input.$error}}</tt><br/>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
|
|
<tt>myForm.$error.email = {{!!myForm.$error.email}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var text = element(by.binding('text'));
|
|
var valid = element(by.binding('myForm.input.$valid'));
|
|
var input = element(by.model('text'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(text.getText()).toContain('me@example.com');
|
|
expect(valid.getText()).toContain('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input.clear();
|
|
input.sendKeys('');
|
|
expect(text.getText()).toEqual('text =');
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
|
|
it('should be invalid if not email', function() {
|
|
input.clear();
|
|
input.sendKeys('xxx');
|
|
|
|
expect(valid.getText()).toContain('false');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'email': emailInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[radio]
|
|
*
|
|
* @description
|
|
* HTML radio button.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string} value The value to which the expression should be set when selected.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {string} ngValue Angular expression which sets the value to which the expression should
|
|
* be set when selected.
|
|
*
|
|
* @example
|
|
<example name="radio-input-directive" module="radioExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('radioExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.color = 'blue';
|
|
$scope.specialValue = {
|
|
"id": "12345",
|
|
"value": "green"
|
|
};
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
<input type="radio" ng-model="color" value="red"> Red <br/>
|
|
<input type="radio" ng-model="color" ng-value="specialValue"> Green <br/>
|
|
<input type="radio" ng-model="color" value="blue"> Blue <br/>
|
|
<tt>color = {{color | json}}</tt><br/>
|
|
</form>
|
|
Note that `ng-value="specialValue"` sets radio item's value to be the value of `$scope.specialValue`.
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
it('should change state', function() {
|
|
var color = element(by.binding('color'));
|
|
|
|
expect(color.getText()).toContain('blue');
|
|
|
|
element.all(by.model('color')).get(0).click();
|
|
|
|
expect(color.getText()).toContain('red');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'radio': radioInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc input
|
|
* @name input[checkbox]
|
|
*
|
|
* @description
|
|
* HTML checkbox.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} ngTrueValue The value to which the expression should be set when selected.
|
|
* @param {string=} ngFalseValue The value to which the expression should be set when not selected.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<example name="checkbox-input-directive" module="checkboxExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('checkboxExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.value1 = true;
|
|
$scope.value2 = 'YES'
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
Value1: <input type="checkbox" ng-model="value1"> <br/>
|
|
Value2: <input type="checkbox" ng-model="value2"
|
|
ng-true-value="YES" ng-false-value="NO"> <br/>
|
|
<tt>value1 = {{value1}}</tt><br/>
|
|
<tt>value2 = {{value2}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
it('should change state', function() {
|
|
var value1 = element(by.binding('value1'));
|
|
var value2 = element(by.binding('value2'));
|
|
|
|
expect(value1.getText()).toContain('true');
|
|
expect(value2.getText()).toContain('YES');
|
|
|
|
element(by.model('value1')).click();
|
|
element(by.model('value2')).click();
|
|
|
|
expect(value1.getText()).toContain('false');
|
|
expect(value2.getText()).toContain('NO');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
'checkbox': checkboxInputType,
|
|
|
|
'hidden': noop,
|
|
'button': noop,
|
|
'submit': noop,
|
|
'reset': noop,
|
|
'file': noop
|
|
};
|
|
|
|
// A helper function to call $setValidity and return the value / undefined,
|
|
// a pattern that is repeated a lot in the input validation logic.
|
|
function validate(ctrl, validatorName, validity, value){
|
|
ctrl.$setValidity(validatorName, validity);
|
|
return validity ? value : undefined;
|
|
}
|
|
|
|
function testFlags(validity, flags) {
|
|
var i, flag;
|
|
if (flags) {
|
|
for (i=0; i<flags.length; ++i) {
|
|
flag = flags[i];
|
|
if (validity[flag]) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Pass validity so that behaviour can be mocked easier.
|
|
function addNativeHtml5Validators(ctrl, validatorName, badFlags, ignoreFlags, validity) {
|
|
if (isObject(validity)) {
|
|
ctrl.$$hasNativeValidators = true;
|
|
var validator = function(value) {
|
|
// Don't overwrite previous validation, don't consider valueMissing to apply (ng-required can
|
|
// perform the required validation)
|
|
if (!ctrl.$error[validatorName] &&
|
|
!testFlags(validity, ignoreFlags) &&
|
|
testFlags(validity, badFlags)) {
|
|
ctrl.$setValidity(validatorName, false);
|
|
return;
|
|
}
|
|
return value;
|
|
};
|
|
ctrl.$parsers.push(validator);
|
|
}
|
|
}
|
|
|
|
function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
var validity = element.prop(VALIDITY_STATE_PROPERTY);
|
|
var placeholder = element[0].placeholder, noevent = {};
|
|
var type = element[0].type.toLowerCase();
|
|
ctrl.$$validityState = validity;
|
|
|
|
// In composition mode, users are still inputing intermediate text buffer,
|
|
// hold the listener until composition is done.
|
|
// More about composition events: https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent
|
|
if (!$sniffer.android) {
|
|
var composing = false;
|
|
|
|
element.on('compositionstart', function(data) {
|
|
composing = true;
|
|
});
|
|
|
|
element.on('compositionend', function() {
|
|
composing = false;
|
|
listener();
|
|
});
|
|
}
|
|
|
|
var listener = function(ev) {
|
|
if (composing) return;
|
|
var value = element.val();
|
|
|
|
// IE (11 and under) seem to emit an 'input' event if the placeholder value changes.
|
|
// We don't want to dirty the value when this happens, so we abort here. Unfortunately,
|
|
// IE also sends input events for other non-input-related things, (such as focusing on a
|
|
// form control), so this change is not entirely enough to solve this.
|
|
if (msie && (ev || noevent).type === 'input' && element[0].placeholder !== placeholder) {
|
|
placeholder = element[0].placeholder;
|
|
return;
|
|
}
|
|
|
|
// By default we will trim the value
|
|
// If the attribute ng-trim exists we will avoid trimming
|
|
// If input type is 'password', the value is never trimmed
|
|
if (type !== 'password' && (toBoolean(attr.ngTrim || 'T'))) {
|
|
value = trim(value);
|
|
}
|
|
|
|
// If a control is suffering from bad input, browsers discard its value, so it may be
|
|
// necessary to revalidate even if the control's value is the same empty value twice in
|
|
// a row.
|
|
var revalidate = validity && ctrl.$$hasNativeValidators;
|
|
if (ctrl.$viewValue !== value || (value === '' && revalidate)) {
|
|
if (scope.$$phase) {
|
|
ctrl.$setViewValue(value);
|
|
} else {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(value);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// if the browser does support "input" event, we are fine - except on IE9 which doesn't fire the
|
|
// input event on backspace, delete or cut
|
|
if ($sniffer.hasEvent('input')) {
|
|
element.on('input', listener);
|
|
} else {
|
|
var timeout;
|
|
|
|
var deferListener = function() {
|
|
if (!timeout) {
|
|
timeout = $browser.defer(function() {
|
|
listener();
|
|
timeout = null;
|
|
});
|
|
}
|
|
};
|
|
|
|
element.on('keydown', function(event) {
|
|
var key = event.keyCode;
|
|
|
|
// ignore
|
|
// command modifiers arrows
|
|
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
|
|
|
|
deferListener();
|
|
});
|
|
|
|
// if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
|
|
if ($sniffer.hasEvent('paste')) {
|
|
element.on('paste cut', deferListener);
|
|
}
|
|
}
|
|
|
|
// if user paste into input using mouse on older browser
|
|
// or form autocomplete on newer browser, we need "change" event to catch it
|
|
element.on('change', listener);
|
|
|
|
ctrl.$render = function() {
|
|
element.val(ctrl.$isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
|
|
};
|
|
|
|
// pattern validator
|
|
var pattern = attr.ngPattern,
|
|
patternValidator,
|
|
match;
|
|
|
|
if (pattern) {
|
|
var validateRegex = function(regexp, value) {
|
|
return validate(ctrl, 'pattern', ctrl.$isEmpty(value) || regexp.test(value), value);
|
|
};
|
|
match = pattern.match(/^\/(.*)\/([gim]*)$/);
|
|
if (match) {
|
|
pattern = new RegExp(match[1], match[2]);
|
|
patternValidator = function(value) {
|
|
return validateRegex(pattern, value);
|
|
};
|
|
} else {
|
|
patternValidator = function(value) {
|
|
var patternObj = scope.$eval(pattern);
|
|
|
|
if (!patternObj || !patternObj.test) {
|
|
throw minErr('ngPattern')('noregexp',
|
|
'Expected {0} to be a RegExp but was {1}. Element: {2}', pattern,
|
|
patternObj, startingTag(element));
|
|
}
|
|
return validateRegex(patternObj, value);
|
|
};
|
|
}
|
|
|
|
ctrl.$formatters.push(patternValidator);
|
|
ctrl.$parsers.push(patternValidator);
|
|
}
|
|
|
|
// min length validator
|
|
if (attr.ngMinlength) {
|
|
var minlength = int(attr.ngMinlength);
|
|
var minLengthValidator = function(value) {
|
|
return validate(ctrl, 'minlength', ctrl.$isEmpty(value) || value.length >= minlength, value);
|
|
};
|
|
|
|
ctrl.$parsers.push(minLengthValidator);
|
|
ctrl.$formatters.push(minLengthValidator);
|
|
}
|
|
|
|
// max length validator
|
|
if (attr.ngMaxlength) {
|
|
var maxlength = int(attr.ngMaxlength);
|
|
var maxLengthValidator = function(value) {
|
|
return validate(ctrl, 'maxlength', ctrl.$isEmpty(value) || value.length <= maxlength, value);
|
|
};
|
|
|
|
ctrl.$parsers.push(maxLengthValidator);
|
|
ctrl.$formatters.push(maxLengthValidator);
|
|
}
|
|
}
|
|
|
|
var numberBadFlags = ['badInput'];
|
|
|
|
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
var empty = ctrl.$isEmpty(value);
|
|
if (empty || NUMBER_REGEXP.test(value)) {
|
|
ctrl.$setValidity('number', true);
|
|
return value === '' ? null : (empty ? value : parseFloat(value));
|
|
} else {
|
|
ctrl.$setValidity('number', false);
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
addNativeHtml5Validators(ctrl, 'number', numberBadFlags, null, ctrl.$$validityState);
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return ctrl.$isEmpty(value) ? '' : '' + value;
|
|
});
|
|
|
|
if (attr.min) {
|
|
var minValidator = function(value) {
|
|
var min = parseFloat(attr.min);
|
|
return validate(ctrl, 'min', ctrl.$isEmpty(value) || value >= min, value);
|
|
};
|
|
|
|
ctrl.$parsers.push(minValidator);
|
|
ctrl.$formatters.push(minValidator);
|
|
}
|
|
|
|
if (attr.max) {
|
|
var maxValidator = function(value) {
|
|
var max = parseFloat(attr.max);
|
|
return validate(ctrl, 'max', ctrl.$isEmpty(value) || value <= max, value);
|
|
};
|
|
|
|
ctrl.$parsers.push(maxValidator);
|
|
ctrl.$formatters.push(maxValidator);
|
|
}
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return validate(ctrl, 'number', ctrl.$isEmpty(value) || isNumber(value), value);
|
|
});
|
|
}
|
|
|
|
function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
var urlValidator = function(value) {
|
|
return validate(ctrl, 'url', ctrl.$isEmpty(value) || URL_REGEXP.test(value), value);
|
|
};
|
|
|
|
ctrl.$formatters.push(urlValidator);
|
|
ctrl.$parsers.push(urlValidator);
|
|
}
|
|
|
|
function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
|
|
textInputType(scope, element, attr, ctrl, $sniffer, $browser);
|
|
|
|
var emailValidator = function(value) {
|
|
return validate(ctrl, 'email', ctrl.$isEmpty(value) || EMAIL_REGEXP.test(value), value);
|
|
};
|
|
|
|
ctrl.$formatters.push(emailValidator);
|
|
ctrl.$parsers.push(emailValidator);
|
|
}
|
|
|
|
function radioInputType(scope, element, attr, ctrl) {
|
|
// make the name unique, if not defined
|
|
if (isUndefined(attr.name)) {
|
|
element.attr('name', nextUid());
|
|
}
|
|
|
|
element.on('click', function() {
|
|
if (element[0].checked) {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(attr.value);
|
|
});
|
|
}
|
|
});
|
|
|
|
ctrl.$render = function() {
|
|
var value = attr.value;
|
|
element[0].checked = (value == ctrl.$viewValue);
|
|
};
|
|
|
|
attr.$observe('value', ctrl.$render);
|
|
}
|
|
|
|
function checkboxInputType(scope, element, attr, ctrl) {
|
|
var trueValue = attr.ngTrueValue,
|
|
falseValue = attr.ngFalseValue;
|
|
|
|
if (!isString(trueValue)) trueValue = true;
|
|
if (!isString(falseValue)) falseValue = false;
|
|
|
|
element.on('click', function() {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(element[0].checked);
|
|
});
|
|
});
|
|
|
|
ctrl.$render = function() {
|
|
element[0].checked = ctrl.$viewValue;
|
|
};
|
|
|
|
// Override the standard `$isEmpty` because a value of `false` means empty in a checkbox.
|
|
ctrl.$isEmpty = function(value) {
|
|
return value !== trueValue;
|
|
};
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return value === trueValue;
|
|
});
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
return value ? trueValue : falseValue;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name textarea
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* HTML textarea element control with angular data-binding. The data-binding and validation
|
|
* properties of this element are exactly the same as those of the
|
|
* {@link ng.directive:input input element}.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {string=} ngRequired Adds `required` attribute and `required` validation constraint to
|
|
* the element when the ngRequired expression evaluates to true. Use `ngRequired` instead of
|
|
* `required` when you want to data-bind to the `required` attribute.
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name input
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* HTML input element control with angular data-binding. Input control follows HTML5 input types
|
|
* and polyfills the HTML5 validation behavior for older browsers.
|
|
*
|
|
* *NOTE* Not every feature offered is available for all input types.
|
|
*
|
|
* @param {string} ngModel Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {boolean=} ngRequired Sets `required` attribute if set to true
|
|
* @param {number=} ngMinlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ngMaxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ngPattern Sets `pattern` validation error key if the value does not match the
|
|
* RegExp pattern expression. Expected value is `/regexp/` for inline patterns or `regexp` for
|
|
* patterns defined as scope expressions.
|
|
* @param {string=} ngChange Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
* @param {boolean=} [ngTrim=true] If set to false Angular will not automatically trim the input.
|
|
* This parameter is ignored for input[type=password] controls, which will never trim the
|
|
* input.
|
|
*
|
|
* @example
|
|
<example name="input-directive" module="inputExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('inputExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.user = {name: 'guest', last: 'visitor'};
|
|
}]);
|
|
</script>
|
|
<div ng-controller="ExampleController">
|
|
<form name="myForm">
|
|
User name: <input type="text" name="userName" ng-model="user.name" required>
|
|
<span class="error" ng-show="myForm.userName.$error.required">
|
|
Required!</span><br>
|
|
Last name: <input type="text" name="lastName" ng-model="user.last"
|
|
ng-minlength="3" ng-maxlength="10">
|
|
<span class="error" ng-show="myForm.lastName.$error.minlength">
|
|
Too short!</span>
|
|
<span class="error" ng-show="myForm.lastName.$error.maxlength">
|
|
Too long!</span><br>
|
|
</form>
|
|
<hr>
|
|
<tt>user = {{user}}</tt><br/>
|
|
<tt>myForm.userName.$valid = {{myForm.userName.$valid}}</tt><br>
|
|
<tt>myForm.userName.$error = {{myForm.userName.$error}}</tt><br>
|
|
<tt>myForm.lastName.$valid = {{myForm.lastName.$valid}}</tt><br>
|
|
<tt>myForm.lastName.$error = {{myForm.lastName.$error}}</tt><br>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br>
|
|
<tt>myForm.$error.minlength = {{!!myForm.$error.minlength}}</tt><br>
|
|
<tt>myForm.$error.maxlength = {{!!myForm.$error.maxlength}}</tt><br>
|
|
</div>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var user = element(by.binding('{{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', function($browser, $sniffer) {
|
|
return {
|
|
restrict: 'E',
|
|
require: '?ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
if (ctrl) {
|
|
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl, $sniffer,
|
|
$browser);
|
|
}
|
|
}
|
|
};
|
|
}];
|
|
|
|
var VALID_CLASS = 'ng-valid',
|
|
INVALID_CLASS = 'ng-invalid',
|
|
PRISTINE_CLASS = 'ng-pristine',
|
|
DIRTY_CLASS = 'ng-dirty';
|
|
|
|
/**
|
|
* @ngdoc type
|
|
* @name ngModel.NgModelController
|
|
*
|
|
* @property {string} $viewValue Actual string value in the view.
|
|
* @property {*} $modelValue The value in the model, that the control is bound to.
|
|
* @property {Array.<Function>} $parsers Array of functions to execute, as a pipeline, whenever
|
|
the control reads value from the DOM. Each function is called, in turn, passing the value
|
|
through to the next. The last return value is used to populate the model.
|
|
Used to sanitize / convert the value as well as validation. For validation,
|
|
the parsers should update the validity state using
|
|
{@link ngModel.NgModelController#$setValidity $setValidity()},
|
|
and return `undefined` for invalid values.
|
|
|
|
*
|
|
* @property {Array.<Function>} $formatters Array of functions to execute, as a pipeline, whenever
|
|
the model value changes. Each function is called, in turn, passing the value through to the
|
|
next. Used to format / convert values for display in the control and validation.
|
|
* ```js
|
|
* function formatter(value) {
|
|
* if (value) {
|
|
* return value.toUpperCase();
|
|
* }
|
|
* }
|
|
* ngModel.$formatters.push(formatter);
|
|
* ```
|
|
*
|
|
* @property {Array.<Function>} $viewChangeListeners Array of functions to execute whenever the
|
|
* view value has changed. It is called with no arguments, and its return value is ignored.
|
|
* This can be used in place of additional $watches against the model value.
|
|
*
|
|
* @property {Object} $error An object hash with all errors as keys.
|
|
*
|
|
* @property {boolean} $pristine True if user has not interacted with the control yet.
|
|
* @property {boolean} $dirty True if user has already interacted with the control.
|
|
* @property {boolean} $valid True if there is no error.
|
|
* @property {boolean} $invalid True if at least one error on the control.
|
|
*
|
|
* @description
|
|
*
|
|
* `NgModelController` provides API for the `ng-model` directive. The controller contains
|
|
* services for data-binding, validation, CSS updates, and value formatting and parsing. It
|
|
* purposefully does not contain any logic which deals with DOM rendering or listening to
|
|
* DOM events. Such DOM related logic should be provided by other directives which make use of
|
|
* `NgModelController` for data-binding.
|
|
*
|
|
* ## Custom Control Example
|
|
* This example shows how to use `NgModelController` with a custom control to achieve
|
|
* data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`)
|
|
* collaborate together to achieve the desired result.
|
|
*
|
|
* Note that `contenteditable` is an HTML5 attribute, which tells the browser to let the element
|
|
* contents be edited in place by the user. This will not work on older browsers.
|
|
*
|
|
* We are using the {@link ng.service:$sce $sce} service here and include the {@link ngSanitize $sanitize}
|
|
* module to automatically remove "bad" content like inline event listener (e.g. `<span onclick="...">`).
|
|
* However, as we are using `$sce` the model can still decide to to provide unsafe content if it marks
|
|
* that content using the `$sce` service.
|
|
*
|
|
* <example name="NgModelController" module="customControl" deps="angular-sanitize.js">
|
|
<file name="style.css">
|
|
[contenteditable] {
|
|
border: 1px solid black;
|
|
background-color: white;
|
|
min-height: 20px;
|
|
}
|
|
|
|
.ng-invalid {
|
|
border: 1px solid red;
|
|
}
|
|
|
|
</file>
|
|
<file name="script.js">
|
|
angular.module('customControl', ['ngSanitize']).
|
|
directive('contenteditable', ['$sce', function($sce) {
|
|
return {
|
|
restrict: 'A', // only activate on element attribute
|
|
require: '?ngModel', // get a hold of NgModelController
|
|
link: function(scope, element, attrs, ngModel) {
|
|
if(!ngModel) return; // do nothing if no ng-model
|
|
|
|
// Specify how UI should be updated
|
|
ngModel.$render = function() {
|
|
element.html($sce.getTrustedHtml(ngModel.$viewValue || ''));
|
|
};
|
|
|
|
// Listen for change events to enable binding
|
|
element.on('blur keyup change', function() {
|
|
scope.$apply(read);
|
|
});
|
|
read(); // initialize
|
|
|
|
// Write data to the model
|
|
function read() {
|
|
var html = element.html();
|
|
// When we clear the content editable the browser leaves a <br> behind
|
|
// If strip-br attribute is provided then we strip this out
|
|
if( attrs.stripBr && html == '<br>' ) {
|
|
html = '';
|
|
}
|
|
ngModel.$setViewValue(html);
|
|
}
|
|
}
|
|
};
|
|
}]);
|
|
</file>
|
|
<file name="index.html">
|
|
<form name="myForm">
|
|
<div contenteditable
|
|
name="myWidget" ng-model="userContent"
|
|
strip-br="true"
|
|
required>Change me!</div>
|
|
<span ng-show="myForm.myWidget.$error.required">Required!</span>
|
|
<hr>
|
|
<textarea ng-model="userContent"></textarea>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
it('should data-bind and become invalid', function() {
|
|
if (browser.params.browser == 'safari' || browser.params.browser == 'firefox') {
|
|
// SafariDriver can't handle contenteditable
|
|
// and Firefox driver can't clear contenteditables very well
|
|
return;
|
|
}
|
|
var contentEditable = element(by.css('[contenteditable]'));
|
|
var content = 'Change me!';
|
|
|
|
expect(contentEditable.getText()).toEqual(content);
|
|
|
|
contentEditable.clear();
|
|
contentEditable.sendKeys(protractor.Key.BACK_SPACE);
|
|
expect(contentEditable.getText()).toEqual('');
|
|
expect(contentEditable.getAttribute('class')).toMatch(/ng-invalid-required/);
|
|
});
|
|
</file>
|
|
* </example>
|
|
*
|
|
*
|
|
*/
|
|
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', '$element', '$parse', '$animate',
|
|
function($scope, $exceptionHandler, $attr, $element, $parse, $animate) {
|
|
this.$viewValue = Number.NaN;
|
|
this.$modelValue = Number.NaN;
|
|
this.$parsers = [];
|
|
this.$formatters = [];
|
|
this.$viewChangeListeners = [];
|
|
this.$pristine = true;
|
|
this.$dirty = false;
|
|
this.$valid = true;
|
|
this.$invalid = false;
|
|
this.$name = $attr.name;
|
|
|
|
var ngModelGet = $parse($attr.ngModel),
|
|
ngModelSet = ngModelGet.assign;
|
|
|
|
if (!ngModelSet) {
|
|
throw minErr('ngModel')('nonassign', "Expression '{0}' is non-assignable. Element: {1}",
|
|
$attr.ngModel, startingTag($element));
|
|
}
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ngModel.NgModelController#$render
|
|
*
|
|
* @description
|
|
* Called when the view needs to be updated. It is expected that the user of the ng-model
|
|
* directive will implement this method.
|
|
*/
|
|
this.$render = noop;
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ngModel.NgModelController#$isEmpty
|
|
*
|
|
* @description
|
|
* This is called when we need to determine if the value of the input is empty.
|
|
*
|
|
* For instance, the required directive does this to work out if the input has data or not.
|
|
* The default `$isEmpty` function checks whether the value is `undefined`, `''`, `null` or `NaN`.
|
|
*
|
|
* You can override this for input directives whose concept of being empty is different to the
|
|
* default. The `checkboxInputType` directive does this because in its case a value of `false`
|
|
* implies empty.
|
|
*
|
|
* @param {*} value Reference to check.
|
|
* @returns {boolean} True if `value` is empty.
|
|
*/
|
|
this.$isEmpty = function(value) {
|
|
return isUndefined(value) || value === '' || value === null || value !== value;
|
|
};
|
|
|
|
var parentForm = $element.inheritedData('$formController') || nullFormCtrl,
|
|
invalidCount = 0, // used to easily determine if we are valid
|
|
$error = this.$error = {}; // keep invalid keys here
|
|
|
|
|
|
// Setup initial state of the control
|
|
$element.addClass(PRISTINE_CLASS);
|
|
toggleValidCss(true);
|
|
|
|
// convenience method for easy toggling of classes
|
|
function toggleValidCss(isValid, validationErrorKey) {
|
|
validationErrorKey = validationErrorKey ? '-' + snake_case(validationErrorKey, '-') : '';
|
|
$animate.removeClass($element, (isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey);
|
|
$animate.addClass($element, (isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
|
|
}
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ngModel.NgModelController#$setValidity
|
|
*
|
|
* @description
|
|
* Change the validity state, and notifies the form when the control changes validity. (i.e. it
|
|
* does not notify form if given validator is already marked as invalid).
|
|
*
|
|
* This method should be called by validators - i.e. the parser or formatter functions.
|
|
*
|
|
* @param {string} validationErrorKey Name of the validator. the `validationErrorKey` will assign
|
|
* to `$error[validationErrorKey]=!isValid` so that it is available for data-binding.
|
|
* The `validationErrorKey` should be in camelCase and will get converted into dash-case
|
|
* for class name. Example: `myError` will result in `ng-valid-my-error` and `ng-invalid-my-error`
|
|
* class and can be bound to as `{{someForm.someControl.$error.myError}}` .
|
|
* @param {boolean} isValid Whether the current state is valid (true) or invalid (false).
|
|
*/
|
|
this.$setValidity = function(validationErrorKey, isValid) {
|
|
// Purposeful use of ! here to cast isValid to boolean in case it is undefined
|
|
// jshint -W018
|
|
if ($error[validationErrorKey] === !isValid) return;
|
|
// jshint +W018
|
|
|
|
if (isValid) {
|
|
if ($error[validationErrorKey]) invalidCount--;
|
|
if (!invalidCount) {
|
|
toggleValidCss(true);
|
|
this.$valid = true;
|
|
this.$invalid = false;
|
|
}
|
|
} else {
|
|
toggleValidCss(false);
|
|
this.$invalid = true;
|
|
this.$valid = false;
|
|
invalidCount++;
|
|
}
|
|
|
|
$error[validationErrorKey] = !isValid;
|
|
toggleValidCss(isValid, validationErrorKey);
|
|
|
|
parentForm.$setValidity(validationErrorKey, isValid, this);
|
|
};
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ngModel.NgModelController#$setPristine
|
|
*
|
|
* @description
|
|
* Sets the control to its pristine state.
|
|
*
|
|
* This method can be called to remove the 'ng-dirty' class and set the control to its pristine
|
|
* state (ng-pristine class).
|
|
*/
|
|
this.$setPristine = function () {
|
|
this.$dirty = false;
|
|
this.$pristine = true;
|
|
$animate.removeClass($element, DIRTY_CLASS);
|
|
$animate.addClass($element, PRISTINE_CLASS);
|
|
};
|
|
|
|
/**
|
|
* @ngdoc method
|
|
* @name ngModel.NgModelController#$setViewValue
|
|
*
|
|
* @description
|
|
* Update the view value.
|
|
*
|
|
* This method should be called when the view value changes, typically from within a DOM event handler.
|
|
* For example {@link ng.directive:input input} and
|
|
* {@link ng.directive:select select} directives call it.
|
|
*
|
|
* It will update the $viewValue, then pass this value through each of the functions in `$parsers`,
|
|
* which includes any validators. The value that comes out of this `$parsers` pipeline, be applied to
|
|
* `$modelValue` and the **expression** specified in the `ng-model` attribute.
|
|
*
|
|
* Lastly, all the registered change listeners, in the `$viewChangeListeners` list, are called.
|
|
*
|
|
* Note that calling this function does not trigger a `$digest`.
|
|
*
|
|
* @param {string} value Value from the view.
|
|
*/
|
|
this.$setViewValue = function(value) {
|
|
this.$viewValue = value;
|
|
|
|
// change to dirty
|
|
if (this.$pristine) {
|
|
this.$dirty = true;
|
|
this.$pristine = false;
|
|
$animate.removeClass($element, PRISTINE_CLASS);
|
|
$animate.addClass($element, DIRTY_CLASS);
|
|
parentForm.$setDirty();
|
|
}
|
|
|
|
forEach(this.$parsers, function(fn) {
|
|
value = fn(value);
|
|
});
|
|
|
|
if (this.$modelValue !== value) {
|
|
this.$modelValue = value;
|
|
ngModelSet($scope, value);
|
|
forEach(this.$viewChangeListeners, function(listener) {
|
|
try {
|
|
listener();
|
|
} catch(e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// model -> value
|
|
var ctrl = this;
|
|
|
|
$scope.$watch(function ngModelWatch() {
|
|
var value = ngModelGet($scope);
|
|
|
|
// if scope model value and ngModel value are out of sync
|
|
if (ctrl.$modelValue !== value) {
|
|
|
|
var formatters = ctrl.$formatters,
|
|
idx = formatters.length;
|
|
|
|
ctrl.$modelValue = value;
|
|
while(idx--) {
|
|
value = formatters[idx](value);
|
|
}
|
|
|
|
if (ctrl.$viewValue !== value) {
|
|
ctrl.$viewValue = value;
|
|
ctrl.$render();
|
|
}
|
|
}
|
|
|
|
return value;
|
|
});
|
|
}];
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ngModel
|
|
*
|
|
* @element input
|
|
*
|
|
* @description
|
|
* The `ngModel` directive binds an `input`,`select`, `textarea` (or custom form control) to a
|
|
* property on the scope using {@link ngModel.NgModelController NgModelController},
|
|
* which is created and exposed by this directive.
|
|
*
|
|
* `ngModel` is responsible for:
|
|
*
|
|
* - Binding the view into the model, which other directives such as `input`, `textarea` or `select`
|
|
* require.
|
|
* - Providing validation behavior (i.e. required, number, email, url).
|
|
* - Keeping the state of the control (valid/invalid, dirty/pristine, validation errors).
|
|
* - Setting related css classes on the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`) including animations.
|
|
* - Registering the control with its parent {@link ng.directive:form form}.
|
|
*
|
|
* Note: `ngModel` will try to bind to the property given by evaluating the expression on the
|
|
* current scope. If the property doesn't already exist on this scope, it will be created
|
|
* implicitly and added to the scope.
|
|
*
|
|
* For best practices on using `ngModel`, see:
|
|
*
|
|
* - [https://github.com/angular/angular.js/wiki/Understanding-Scopes]
|
|
*
|
|
* For basic examples, how to use `ngModel`, see:
|
|
*
|
|
* - {@link ng.directive:input input}
|
|
* - {@link input[text] text}
|
|
* - {@link input[checkbox] checkbox}
|
|
* - {@link input[radio] radio}
|
|
* - {@link input[number] number}
|
|
* - {@link input[email] email}
|
|
* - {@link input[url] url}
|
|
* - {@link ng.directive:select select}
|
|
* - {@link ng.directive:textarea textarea}
|
|
*
|
|
* # CSS classes
|
|
* The following CSS classes are added and removed on the associated input/select/textarea element
|
|
* depending on the validity of the model.
|
|
*
|
|
* - `ng-valid` is set if the model is valid.
|
|
* - `ng-invalid` is set if the model is invalid.
|
|
* - `ng-pristine` is set if the model is pristine.
|
|
* - `ng-dirty` is set if the model is dirty.
|
|
*
|
|
* Keep in mind that ngAnimate can detect each of these classes when added and removed.
|
|
*
|
|
* ## Animation Hooks
|
|
*
|
|
* Animations within models are triggered when any of the associated CSS classes are added and removed
|
|
* on the input element which is attached to the model. These classes are: `.ng-pristine`, `.ng-dirty`,
|
|
* `.ng-invalid` and `.ng-valid` as well as any other validations that are performed on the model itself.
|
|
* The animations that are triggered within ngModel are similar to how they work in ngClass and
|
|
* animations can be hooked into using CSS transitions, keyframes as well as JS animations.
|
|
*
|
|
* The following example shows a simple way to utilize CSS transitions to style an input element
|
|
* that has been rendered as invalid after it has been validated:
|
|
*
|
|
* <pre>
|
|
* //be sure to include ngAnimate as a module to hook into more
|
|
* //advanced animations
|
|
* .my-input {
|
|
* transition:0.5s linear all;
|
|
* background: white;
|
|
* }
|
|
* .my-input.ng-invalid {
|
|
* background: red;
|
|
* color:white;
|
|
* }
|
|
* </pre>
|
|
*
|
|
* @example
|
|
* <example deps="angular-animate.js" animations="true" fixBase="true" module="inputExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('inputExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.val = '1';
|
|
}]);
|
|
</script>
|
|
<style>
|
|
.my-input {
|
|
-webkit-transition:all linear 0.5s;
|
|
transition:all linear 0.5s;
|
|
background: transparent;
|
|
}
|
|
.my-input.ng-invalid {
|
|
color:white;
|
|
background: red;
|
|
}
|
|
</style>
|
|
Update input to see transitions when valid/invalid.
|
|
Integer is a valid value.
|
|
<form name="testForm" ng-controller="ExampleController">
|
|
<input ng-model="val" ng-pattern="/^\d+$/" name="anim" class="my-input" />
|
|
</form>
|
|
</file>
|
|
* </example>
|
|
*/
|
|
var ngModelDirective = function() {
|
|
return {
|
|
require: ['ngModel', '^?form'],
|
|
controller: NgModelController,
|
|
link: function(scope, element, attr, ctrls) {
|
|
// notify others, especially parent forms
|
|
|
|
var modelCtrl = ctrls[0],
|
|
formCtrl = ctrls[1] || nullFormCtrl;
|
|
|
|
formCtrl.$addControl(modelCtrl);
|
|
|
|
scope.$on('$destroy', function() {
|
|
formCtrl.$removeControl(modelCtrl);
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ngChange
|
|
*
|
|
* @description
|
|
* Evaluate the given expression when the user changes the input.
|
|
* The expression is evaluated immediately, unlike the JavaScript onchange event
|
|
* which only triggers at the end of a change (usually, when the user leaves the
|
|
* form element or presses the return key).
|
|
* The expression is not evaluated when the value change is coming from the model.
|
|
*
|
|
* Note, this directive requires `ngModel` to be present.
|
|
*
|
|
* @element input
|
|
* @param {expression} ngChange {@link guide/expression Expression} to evaluate upon change
|
|
* in input value.
|
|
*
|
|
* @example
|
|
* <example name="ngChange-directive" module="changeExample">
|
|
* <file name="index.html">
|
|
* <script>
|
|
* angular.module('changeExample', [])
|
|
* .controller('ExampleController', ['$scope', function($scope) {
|
|
* $scope.counter = 0;
|
|
* $scope.change = function() {
|
|
* $scope.counter++;
|
|
* };
|
|
* }]);
|
|
* </script>
|
|
* <div ng-controller="ExampleController">
|
|
* <input type="checkbox" ng-model="confirmed" ng-change="change()" id="ng-change-example1" />
|
|
* <input type="checkbox" ng-model="confirmed" id="ng-change-example2" />
|
|
* <label for="ng-change-example2">Confirmed</label><br />
|
|
* <tt>debug = {{confirmed}}</tt><br/>
|
|
* <tt>counter = {{counter}}</tt><br/>
|
|
* </div>
|
|
* </file>
|
|
* <file name="protractor.js" type="protractor">
|
|
* var counter = element(by.binding('counter'));
|
|
* var debug = element(by.binding('confirmed'));
|
|
*
|
|
* it('should evaluate the expression if changing from view', function() {
|
|
* expect(counter.getText()).toContain('0');
|
|
*
|
|
* element(by.id('ng-change-example1')).click();
|
|
*
|
|
* expect(counter.getText()).toContain('1');
|
|
* expect(debug.getText()).toContain('true');
|
|
* });
|
|
*
|
|
* it('should not evaluate the expression if changing from model', function() {
|
|
* element(by.id('ng-change-example2')).click();
|
|
|
|
* expect(counter.getText()).toContain('0');
|
|
* expect(debug.getText()).toContain('true');
|
|
* });
|
|
* </file>
|
|
* </example>
|
|
*/
|
|
var ngChangeDirective = valueFn({
|
|
require: 'ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
ctrl.$viewChangeListeners.push(function() {
|
|
scope.$eval(attr.ngChange);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
var requiredDirective = function() {
|
|
return {
|
|
require: '?ngModel',
|
|
link: function(scope, elm, attr, ctrl) {
|
|
if (!ctrl) return;
|
|
attr.required = true; // force truthy in case we are on non input element
|
|
|
|
var validator = function(value) {
|
|
if (attr.required && ctrl.$isEmpty(value)) {
|
|
ctrl.$setValidity('required', false);
|
|
return;
|
|
} else {
|
|
ctrl.$setValidity('required', true);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
ctrl.$formatters.push(validator);
|
|
ctrl.$parsers.unshift(validator);
|
|
|
|
attr.$observe('required', function() {
|
|
validator(ctrl.$viewValue);
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ngList
|
|
*
|
|
* @description
|
|
* Text input that converts between a delimited string and an array of strings. The delimiter
|
|
* can be a fixed string (by default a comma) or a regular expression.
|
|
*
|
|
* @element input
|
|
* @param {string=} ngList optional delimiter that should be used to split the value. If
|
|
* specified in form `/something/` then the value will be converted into a regular expression.
|
|
*
|
|
* @example
|
|
<example name="ngList-directive" module="listExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('listExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.names = ['igor', 'misko', 'vojta'];
|
|
}]);
|
|
</script>
|
|
<form name="myForm" ng-controller="ExampleController">
|
|
List: <input name="namesInput" ng-model="names" ng-list required>
|
|
<span class="error" ng-show="myForm.namesInput.$error.required">
|
|
Required!</span>
|
|
<br>
|
|
<tt>names = {{names}}</tt><br/>
|
|
<tt>myForm.namesInput.$valid = {{myForm.namesInput.$valid}}</tt><br/>
|
|
<tt>myForm.namesInput.$error = {{myForm.namesInput.$error}}</tt><br/>
|
|
<tt>myForm.$valid = {{myForm.$valid}}</tt><br/>
|
|
<tt>myForm.$error.required = {{!!myForm.$error.required}}</tt><br/>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var listInput = element(by.model('names'));
|
|
var names = element(by.binding('{{names}}'));
|
|
var valid = element(by.binding('myForm.namesInput.$valid'));
|
|
var error = element(by.css('span.error'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(names.getText()).toContain('["igor","misko","vojta"]');
|
|
expect(valid.getText()).toContain('true');
|
|
expect(error.getCssValue('display')).toBe('none');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
listInput.clear();
|
|
listInput.sendKeys('');
|
|
|
|
expect(names.getText()).toContain('');
|
|
expect(valid.getText()).toContain('false');
|
|
expect(error.getCssValue('display')).not.toBe('none'); });
|
|
</file>
|
|
</example>
|
|
*/
|
|
var ngListDirective = function() {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
var match = /\/(.*)\//.exec(attr.ngList),
|
|
separator = match && new RegExp(match[1]) || attr.ngList || ',';
|
|
|
|
var parse = function(viewValue) {
|
|
// If the viewValue is invalid (say required but empty) it will be `undefined`
|
|
if (isUndefined(viewValue)) return;
|
|
|
|
var list = [];
|
|
|
|
if (viewValue) {
|
|
forEach(viewValue.split(separator), function(value) {
|
|
if (value) list.push(trim(value));
|
|
});
|
|
}
|
|
|
|
return list;
|
|
};
|
|
|
|
ctrl.$parsers.push(parse);
|
|
ctrl.$formatters.push(function(value) {
|
|
if (isArray(value)) {
|
|
return value.join(', ');
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
|
|
// Override the standard $isEmpty because an empty array means the input is empty.
|
|
ctrl.$isEmpty = function(value) {
|
|
return !value || !value.length;
|
|
};
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
|
/**
|
|
* @ngdoc directive
|
|
* @name ngValue
|
|
*
|
|
* @description
|
|
* Binds the given expression to the value of `input[select]` or `input[radio]`, so
|
|
* that when the element is selected, the `ngModel` of that element is set to the
|
|
* bound value.
|
|
*
|
|
* `ngValue` is useful when dynamically generating lists of radio buttons using `ng-repeat`, as
|
|
* shown below.
|
|
*
|
|
* @element input
|
|
* @param {string=} ngValue angular expression, whose value will be bound to the `value` attribute
|
|
* of the `input` element
|
|
*
|
|
* @example
|
|
<example name="ngValue-directive" module="valueExample">
|
|
<file name="index.html">
|
|
<script>
|
|
angular.module('valueExample', [])
|
|
.controller('ExampleController', ['$scope', function($scope) {
|
|
$scope.names = ['pizza', 'unicorns', 'robots'];
|
|
$scope.my = { favorite: 'unicorns' };
|
|
}]);
|
|
</script>
|
|
<form ng-controller="ExampleController">
|
|
<h2>Which is your favorite?</h2>
|
|
<label ng-repeat="name in names" for="{{name}}">
|
|
{{name}}
|
|
<input type="radio"
|
|
ng-model="my.favorite"
|
|
ng-value="name"
|
|
id="{{name}}"
|
|
name="favorite">
|
|
</label>
|
|
<div>You chose {{my.favorite}}</div>
|
|
</form>
|
|
</file>
|
|
<file name="protractor.js" type="protractor">
|
|
var favorite = element(by.binding('my.favorite'));
|
|
|
|
it('should initialize to model', function() {
|
|
expect(favorite.getText()).toContain('unicorns');
|
|
});
|
|
it('should bind the values to the inputs', function() {
|
|
element.all(by.model('my.favorite')).get(0).click();
|
|
expect(favorite.getText()).toContain('pizza');
|
|
});
|
|
</file>
|
|
</example>
|
|
*/
|
|
var ngValueDirective = function() {
|
|
return {
|
|
priority: 100,
|
|
compile: function(tpl, tplAttr) {
|
|
if (CONSTANT_VALUE_REGEXP.test(tplAttr.ngValue)) {
|
|
return function ngValueConstantLink(scope, elm, attr) {
|
|
attr.$set('value', scope.$eval(attr.ngValue));
|
|
};
|
|
} else {
|
|
return function ngValueLink(scope, elm, attr) {
|
|
scope.$watch(attr.ngValue, function valueWatchAction(value) {
|
|
attr.$set('value', value);
|
|
});
|
|
};
|
|
}
|
|
}
|
|
};
|
|
};
|