mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-19 16:10:22 +00:00
Instead of using our custom serializer we now use the native one and use the replacer function to customize the serialization to preserve most of the previous behavior (ignore $ and $$ properties as well as window, document and scope instances).
1194 lines
40 KiB
JavaScript
1194 lines
40 KiB
JavaScript
'use strict';
|
|
|
|
var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/;
|
|
var EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/;
|
|
var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/;
|
|
|
|
var inputType = {
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.text
|
|
*
|
|
* @description
|
|
* Standard HTML text input with angular data binding.
|
|
*
|
|
* @param {string} ng-model 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 {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.text = 'guest';
|
|
$scope.word = /^\w*$/;
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
Single word: <input type="text" name="input" ng-model="text"
|
|
ng-pattern="word" required>
|
|
<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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('text')).toEqual('guest');
|
|
expect(binding('myForm.input.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('text').enter('');
|
|
expect(binding('text')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be invalid if multi word', function() {
|
|
input('text').enter('hello world');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'text': textInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.number
|
|
*
|
|
* @description
|
|
* Text input with number validation and transformation. Sets the `number` validation
|
|
* error if not a valid number.
|
|
*
|
|
* @param {string} ng-model 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 then `min`.
|
|
* @param {string=} max Sets the `max` validation error key if the value entered is greater then `min`.
|
|
* @param {string=} required Sets `required` validation error key if the value is not entered.
|
|
* @param {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.value = 12;
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
Number: <input type="number" name="input" ng-model="value"
|
|
min="0" max="99" required>
|
|
<span class="error" ng-show="myForm.list.$error.required">
|
|
Required!</span>
|
|
<span class="error" ng-show="myForm.list.$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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('value')).toEqual('12');
|
|
expect(binding('myForm.input.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('value').enter('');
|
|
expect(binding('value')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be invalid if over max', function() {
|
|
input('value').enter('123');
|
|
expect(binding('value')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'number': numberInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.url
|
|
*
|
|
* @description
|
|
* Text input with URL validation. Sets the `url` validation error key if the content is not a
|
|
* valid URL.
|
|
*
|
|
* @param {string} ng-model 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 {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.text = 'http://google.com';
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('text')).toEqual('http://google.com');
|
|
expect(binding('myForm.input.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('text').enter('');
|
|
expect(binding('text')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be invalid if not url', function() {
|
|
input('text').enter('xxx');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'url': urlInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.email
|
|
*
|
|
* @description
|
|
* Text input with email validation. Sets the `email` validation error key if not a valid email
|
|
* address.
|
|
*
|
|
* @param {string} ng-model 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 {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.text = 'me@example.com';
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('text')).toEqual('me@example.com');
|
|
expect(binding('myForm.input.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('text').enter('');
|
|
expect(binding('text')).toEqual('');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be invalid if not email', function() {
|
|
input('text').enter('xxx');
|
|
expect(binding('myForm.input.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'email': emailInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.radio
|
|
*
|
|
* @description
|
|
* HTML radio button.
|
|
*
|
|
* @param {string} ng-model 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.color = 'blue';
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
<input type="radio" ng-model="color" value="red"> Red <br/>
|
|
<input type="radio" ng-model="color" value="green"> Green <br/>
|
|
<input type="radio" ng-model="color" value="blue"> Blue <br/>
|
|
<tt>color = {{color}}</tt><br/>
|
|
</form>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should change state', function() {
|
|
expect(binding('color')).toEqual('blue');
|
|
|
|
input('color').select('red');
|
|
expect(binding('color')).toEqual('red');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'radio': radioInputType,
|
|
|
|
|
|
/**
|
|
* @ngdoc inputType
|
|
* @name angular.module.ng.$compileProvider.directive.input.checkbox
|
|
*
|
|
* @description
|
|
* HTML checkbox.
|
|
*
|
|
* @param {string} ng-model Assignable angular expression to data-bind to.
|
|
* @param {string=} name Property name of the form under which the control is published.
|
|
* @param {string=} ng-true-value The value to which the expression should be set when selected.
|
|
* @param {string=} ng-false-value The value to which the expression should be set when not selected.
|
|
* @param {string=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.value1 = true;
|
|
$scope.value2 = 'YES'
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should change state', function() {
|
|
expect(binding('value1')).toEqual('true');
|
|
expect(binding('value2')).toEqual('YES');
|
|
|
|
input('value1').check();
|
|
input('value2').check();
|
|
expect(binding('value1')).toEqual('false');
|
|
expect(binding('value2')).toEqual('NO');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
'checkbox': checkboxInputType,
|
|
|
|
'hidden': noop,
|
|
'button': noop,
|
|
'submit': noop,
|
|
'reset': noop
|
|
};
|
|
|
|
|
|
function isEmpty(value) {
|
|
return isUndefined(value) || value === '' || value === null || value !== value;
|
|
}
|
|
|
|
|
|
function textInputType(scope, element, attr, ctrl) {
|
|
element.bind('blur', function() {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(trim(element.val()));
|
|
});
|
|
});
|
|
|
|
ctrl.$render = function() {
|
|
element.val(isEmpty(ctrl.$viewValue) ? '' : ctrl.$viewValue);
|
|
};
|
|
|
|
// pattern validator
|
|
var pattern = attr.ngPattern,
|
|
patternValidator;
|
|
|
|
var validate = function(regexp, value) {
|
|
if (isEmpty(value) || regexp.test(value)) {
|
|
ctrl.$setValidity('pattern', true);
|
|
return value;
|
|
} else {
|
|
ctrl.$setValidity('pattern', false);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
if (pattern) {
|
|
if (pattern.match(/^\/(.*)\/$/)) {
|
|
pattern = new RegExp(pattern.substr(1, pattern.length - 2));
|
|
patternValidator = function(value) {
|
|
return validate(pattern, value)
|
|
};
|
|
} else {
|
|
patternValidator = function(value) {
|
|
var patternObj = scope.$eval(pattern);
|
|
|
|
if (!patternObj || !patternObj.test) {
|
|
throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj);
|
|
}
|
|
return validate(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) {
|
|
if (!isEmpty(value) && value.length < minlength) {
|
|
ctrl.$setValidity('minlength', false);
|
|
return undefined;
|
|
} else {
|
|
ctrl.$setValidity('minlength', true);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
ctrl.$parsers.push(minLengthValidator);
|
|
ctrl.$formatters.push(minLengthValidator);
|
|
}
|
|
|
|
// max length validator
|
|
if (attr.ngMaxlength) {
|
|
var maxlength = int(attr.ngMaxlength);
|
|
var maxLengthValidator = function(value) {
|
|
if (!isEmpty(value) && value.length > maxlength) {
|
|
ctrl.$setValidity('maxlength', false);
|
|
return undefined;
|
|
} else {
|
|
ctrl.$setValidity('maxlength', true);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
ctrl.$parsers.push(maxLengthValidator);
|
|
ctrl.$formatters.push(maxLengthValidator);
|
|
}
|
|
};
|
|
|
|
function numberInputType(scope, element, attr, ctrl) {
|
|
textInputType(scope, element, attr, ctrl);
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
var empty = 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;
|
|
}
|
|
});
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return isEmpty(value) ? '' : '' + value;
|
|
});
|
|
|
|
if (attr.min) {
|
|
var min = parseFloat(attr.min);
|
|
var minValidator = function(value) {
|
|
if (!isEmpty(value) && value < min) {
|
|
ctrl.$setValidity('min', false);
|
|
return undefined;
|
|
} else {
|
|
ctrl.$setValidity('min', true);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
ctrl.$parsers.push(minValidator);
|
|
ctrl.$formatters.push(minValidator);
|
|
}
|
|
|
|
if (attr.max) {
|
|
var max = parseFloat(attr.max);
|
|
var maxValidator = function(value) {
|
|
if (!isEmpty(value) && value > max) {
|
|
ctrl.$setValidity('max', false);
|
|
return undefined;
|
|
} else {
|
|
ctrl.$setValidity('max', true);
|
|
return value;
|
|
}
|
|
};
|
|
|
|
ctrl.$parsers.push(maxValidator);
|
|
ctrl.$formatters.push(maxValidator);
|
|
}
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
|
|
if (isEmpty(value) || isNumber(value)) {
|
|
ctrl.$setValidity('number', true);
|
|
return value;
|
|
} else {
|
|
ctrl.$setValidity('number', false);
|
|
return undefined;
|
|
}
|
|
});
|
|
}
|
|
|
|
function urlInputType(scope, element, attr, ctrl) {
|
|
textInputType(scope, element, attr, ctrl);
|
|
|
|
var urlValidator = function(value) {
|
|
if (isEmpty(value) || URL_REGEXP.test(value)) {
|
|
ctrl.$setValidity('url', true);
|
|
return value;
|
|
} else {
|
|
ctrl.$setValidity('url', false);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
ctrl.$formatters.push(urlValidator);
|
|
ctrl.$parsers.push(urlValidator);
|
|
}
|
|
|
|
function emailInputType(scope, element, attr, ctrl) {
|
|
textInputType(scope, element, attr, ctrl);
|
|
|
|
var emailValidator = function(value) {
|
|
if (isEmpty(value) || EMAIL_REGEXP.test(value)) {
|
|
ctrl.$setValidity('email', true);
|
|
return value;
|
|
} else {
|
|
ctrl.$setValidity('email', false);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
ctrl.$formatters.push(emailValidator);
|
|
ctrl.$parsers.push(emailValidator);
|
|
}
|
|
|
|
function radioInputType(scope, element, attr, ctrl) {
|
|
// correct the name
|
|
element.attr('name', attr.id + '@' + attr.name);
|
|
|
|
element.bind('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.bind('click', function() {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(element[0].checked);
|
|
});
|
|
});
|
|
|
|
ctrl.$render = function() {
|
|
element[0].checked = ctrl.$viewValue;
|
|
};
|
|
|
|
ctrl.$formatters.push(function(value) {
|
|
return value === trueValue;
|
|
});
|
|
|
|
ctrl.$parsers.push(function(value) {
|
|
return value ? trueValue : falseValue;
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name angular.module.ng.$compileProvider.directive.textarea
|
|
*
|
|
* @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 angular.module.ng.$compileProvider.directive.input input element}.
|
|
*
|
|
* @param {string} ng-model 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 {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*/
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name angular.module.ng.$compileProvider.directive.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.
|
|
*
|
|
* @param {string} ng-model 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 {number=} ng-minlength Sets `minlength` validation error key if the value is shorter than
|
|
* minlength.
|
|
* @param {number=} ng-maxlength Sets `maxlength` validation error key if the value is longer than
|
|
* maxlength.
|
|
* @param {string=} ng-pattern 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=} ng-change Angular expression to be executed when input changes due to user
|
|
* interaction with the input element.
|
|
*
|
|
* @example
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.user = {name: 'guest', last: 'visitor'};
|
|
}
|
|
</script>
|
|
<div ng-controller="Ctrl">
|
|
<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.userName.$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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('user')).toEqual('{"name":"guest","last":"visitor"}');
|
|
expect(binding('myForm.userName.$valid')).toEqual('true');
|
|
expect(binding('myForm.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty when required', function() {
|
|
input('user.name').enter('');
|
|
expect(binding('user')).toEqual('{"last":"visitor"}');
|
|
expect(binding('myForm.userName.$valid')).toEqual('false');
|
|
expect(binding('myForm.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be valid if empty when min length is set', function() {
|
|
input('user.last').enter('');
|
|
expect(binding('user')).toEqual('{"name":"guest","last":""}');
|
|
expect(binding('myForm.lastName.$valid')).toEqual('true');
|
|
expect(binding('myForm.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if less than required min length', function() {
|
|
input('user.last').enter('xx');
|
|
expect(binding('user')).toEqual('{"name":"guest"}');
|
|
expect(binding('myForm.lastName.$valid')).toEqual('false');
|
|
expect(binding('myForm.lastName.$error')).toMatch(/minlength/);
|
|
expect(binding('myForm.$valid')).toEqual('false');
|
|
});
|
|
|
|
it('should be invalid if longer than max length', function() {
|
|
input('user.last').enter('some ridiculously long name');
|
|
expect(binding('user'))
|
|
.toEqual('{"name":"guest"}');
|
|
expect(binding('myForm.lastName.$valid')).toEqual('false');
|
|
expect(binding('myForm.lastName.$error')).toMatch(/maxlength/);
|
|
expect(binding('myForm.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc:example>
|
|
*/
|
|
var inputDirective = [function() {
|
|
return {
|
|
restrict: 'E',
|
|
require: '?ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
if (ctrl) {
|
|
(inputType[lowercase(attr.type)] || inputType.text)(scope, element, attr, ctrl);
|
|
}
|
|
}
|
|
};
|
|
}];
|
|
|
|
var VALID_CLASS = 'ng-valid',
|
|
INVALID_CLASS = 'ng-invalid',
|
|
PRISTINE_CLASS = 'ng-pristine',
|
|
DIRTY_CLASS = 'ng-dirty';
|
|
|
|
/**
|
|
* @ngdoc object
|
|
* @name angular.module.ng.$compileProvider.directive.ng-model.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 Whenever the control reads value from the DOM, it executes
|
|
* all of these functions to sanitize / convert the value as well as validate.
|
|
*
|
|
* @property {Array.<Function>} $formatters Whenever the model value changes, it executes all of
|
|
* these functions to convert the value as well as validate.
|
|
*
|
|
* @property {Object} $error An bject 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
|
|
*
|
|
*/
|
|
var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element',
|
|
function($scope, $exceptionHandler, $attr, ngModel, $element) {
|
|
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.$render = noop;
|
|
this.$name = $attr.name;
|
|
|
|
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, '-') : '';
|
|
$element.
|
|
removeClass((isValid ? INVALID_CLASS : VALID_CLASS) + validationErrorKey).
|
|
addClass((isValid ? VALID_CLASS : INVALID_CLASS) + validationErrorKey);
|
|
}
|
|
|
|
/**
|
|
* @ngdoc function
|
|
* @name angular.module.ng.$compileProvider.directive.ng-model.NgModelController#$setValidity
|
|
* @methodOf angular.module.ng.$compileProvider.directive.ng-model.NgModelController
|
|
*
|
|
* @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) {
|
|
if ($error[validationErrorKey] === !isValid) return;
|
|
|
|
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 function
|
|
* @name angular.module.ng.$compileProvider.directive.ng-model.NgModelController#$setViewValue
|
|
* @methodOf angular.module.ng.$compileProvider.directive.ng-model.NgModelController
|
|
*
|
|
* @description
|
|
* Read a value from view.
|
|
*
|
|
* This method should be called from within a DOM event handler.
|
|
* For example {@link angular.module.ng.$compileProvider.directive.input input} or
|
|
* {@link angular.module.ng.$compileProvider.directive.select select} directives call it.
|
|
*
|
|
* It internally calls all `formatters` and if resulted value is valid, updates the model and
|
|
* calls all registered change listeners.
|
|
*
|
|
* @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;
|
|
$element.removeClass(PRISTINE_CLASS).addClass(DIRTY_CLASS);
|
|
parentForm.$setDirty();
|
|
}
|
|
|
|
forEach(this.$parsers, function(fn) {
|
|
value = fn(value);
|
|
});
|
|
|
|
if (this.$modelValue !== value) {
|
|
this.$modelValue = value;
|
|
ngModel(value);
|
|
forEach(this.$viewChangeListeners, function(listener) {
|
|
try {
|
|
listener();
|
|
} catch(e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
})
|
|
}
|
|
};
|
|
|
|
// model -> value
|
|
var ctrl = this;
|
|
$scope.$watch(function() {
|
|
return ngModel();
|
|
}, function(value) {
|
|
|
|
// ignore change from view
|
|
if (ctrl.$modelValue === value) return;
|
|
|
|
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();
|
|
}
|
|
});
|
|
}];
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name angular.module.ng.$compileProvider.directive.ng-model
|
|
*
|
|
* @element input
|
|
*
|
|
* @description
|
|
* Is directive that tells Angular to do two-way data binding. It works together with `input`,
|
|
* `select`, `textarea`. You can easily write your own directives to use `ng-model` as well.
|
|
*
|
|
* `ng-model` 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 state of the control (valid/invalid, dirty/pristine, validation errors),
|
|
* - setting related css class onto the element (`ng-valid`, `ng-invalid`, `ng-dirty`, `ng-pristine`),
|
|
* - register the control with parent {@link angular.module.ng.$compileProvider.directive.form form}.
|
|
*
|
|
* For basic examples, how to use `ng-model`, see:
|
|
*
|
|
* - {@link angular.module.ng.$compileProvider.directive.input input}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.text text}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.checkbox checkbox}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.radio radio}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.number number}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.email email}
|
|
* - {@link angular.module.ng.$compileProvider.directive.input.url url}
|
|
* - {@link angular.module.ng.$compileProvider.directive.select select}
|
|
* - {@link angular.module.ng.$compileProvider.directive.textarea textarea}
|
|
*
|
|
*/
|
|
var ngModelDirective = [function() {
|
|
return {
|
|
inject: {
|
|
ngModel: 'accessor'
|
|
},
|
|
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);
|
|
|
|
element.bind('$destroy', function() {
|
|
formCtrl.$removeControl(modelCtrl);
|
|
});
|
|
}
|
|
};
|
|
}];
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name angular.module.ng.$compileProvider.directive.ng-change
|
|
* @restrict E
|
|
*
|
|
* @description
|
|
* Evaluate given expression when user changes the input.
|
|
* The expression is not evaluated when the value change is coming from the model.
|
|
*
|
|
* Note, this directive requires `ng-model` to be present.
|
|
*
|
|
* @element input
|
|
*
|
|
* @example
|
|
* <doc:example>
|
|
* <doc:source>
|
|
* <script>
|
|
* function Controller($scope) {
|
|
* $scope.counter = 0;
|
|
* $scope.change = function() {
|
|
* $scope.counter++;
|
|
* };
|
|
* }
|
|
* </script>
|
|
* <div ng-controller="Controller">
|
|
* <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 />
|
|
* debug = {{confirmed}}<br />
|
|
* counter = {{counter}}
|
|
* </div>
|
|
* </doc:source>
|
|
* <doc:scenario>
|
|
* it('should evaluate the expression if changing from view', function() {
|
|
* expect(binding('counter')).toEqual('0');
|
|
* element('#ng-change-example1').click();
|
|
* expect(binding('counter')).toEqual('1');
|
|
* expect(binding('confirmed')).toEqual('true');
|
|
* });
|
|
*
|
|
* it('should not evaluate the expression if changing from model', function() {
|
|
* element('#ng-change-example2').click();
|
|
* expect(binding('counter')).toEqual('0');
|
|
* expect(binding('confirmed')).toEqual('true');
|
|
* });
|
|
* </doc:scenario>
|
|
* </doc:example>
|
|
*/
|
|
var ngChangeDirective = valueFn({
|
|
require: 'ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
ctrl.$viewChangeListeners.push(function() {
|
|
scope.$eval(attr.ngChange);
|
|
});
|
|
}
|
|
});
|
|
|
|
|
|
/**
|
|
* @ngdoc directive
|
|
* @name angular.module.ng.$compileProvider.directive.ng-model-instant
|
|
*
|
|
* @element input
|
|
*
|
|
* @description
|
|
* By default, Angular udpates the model only on `blur` event - when the input looses focus.
|
|
* If you want to update after every key stroke, use `ng-model-instant`.
|
|
*
|
|
* @example
|
|
* <doc:example>
|
|
* <doc:source>
|
|
* First name: <input type="text" ng-model="firstName" /><br />
|
|
* Last name: <input type="text" ng-model="lastName" ng-model-instant /><br />
|
|
*
|
|
* First name ({{firstName}}) is only updated on `blur` event, but the last name ({{lastName}})
|
|
* is updated immediately, because of using `ng-model-instant`.
|
|
* </doc:source>
|
|
* <doc:scenario>
|
|
* it('should update first name on blur', function() {
|
|
* input('firstName').enter('santa', 'blur');
|
|
* expect(binding('firstName')).toEqual('santa');
|
|
* });
|
|
*
|
|
* it('should update last name immediately', function() {
|
|
* input('lastName').enter('santa', 'keydown');
|
|
* expect(binding('lastName')).toEqual('santa');
|
|
* });
|
|
* </doc:scenario>
|
|
* </doc:example>
|
|
*/
|
|
var ngModelInstantDirective = ['$browser', function($browser) {
|
|
return {
|
|
require: 'ngModel',
|
|
link: function(scope, element, attr, ctrl) {
|
|
var handler = function() {
|
|
scope.$apply(function() {
|
|
ctrl.$setViewValue(trim(element.val()));
|
|
});
|
|
};
|
|
|
|
var timeout;
|
|
element.bind('keydown', function(event) {
|
|
var key = event.keyCode;
|
|
|
|
// command modifiers arrows
|
|
if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return;
|
|
|
|
if (!timeout) {
|
|
timeout = $browser.defer(function() {
|
|
handler();
|
|
timeout = null;
|
|
});
|
|
}
|
|
});
|
|
|
|
element.bind('change input', handler);
|
|
}
|
|
};
|
|
}];
|
|
|
|
|
|
var requiredDirective = [function() {
|
|
return {
|
|
require: '?ngModel',
|
|
link: function(scope, elm, attr, ctrl) {
|
|
if (!ctrl) return;
|
|
|
|
var validator = function(value) {
|
|
if (attr.required && (isEmpty(value) || value === false)) {
|
|
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 angular.module.ng.$compileProvider.directive.ng-list
|
|
*
|
|
* @description
|
|
* Text input that converts between comma-seperated string into an array of strings.
|
|
*
|
|
* @element input
|
|
* @param {string=} ng-list 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
|
|
<doc:example>
|
|
<doc:source>
|
|
<script>
|
|
function Ctrl($scope) {
|
|
$scope.names = ['igor', 'misko', 'vojta'];
|
|
}
|
|
</script>
|
|
<form name="myForm" ng-controller="Ctrl">
|
|
List: <input name="namesInput" ng-model="names" ng-list required>
|
|
<span class="error" ng-show="myForm.list.$error.required">
|
|
Required!</span>
|
|
<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>
|
|
</doc:source>
|
|
<doc:scenario>
|
|
it('should initialize to model', function() {
|
|
expect(binding('names')).toEqual('["igor","misko","vojta"]');
|
|
expect(binding('myForm.namesInput.$valid')).toEqual('true');
|
|
});
|
|
|
|
it('should be invalid if empty', function() {
|
|
input('names').enter('');
|
|
expect(binding('names')).toEqual('[]');
|
|
expect(binding('myForm.namesInput.$valid')).toEqual('false');
|
|
});
|
|
</doc:scenario>
|
|
</doc: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) {
|
|
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) && !equals(parse(ctrl.$viewValue), value)) {
|
|
return value.join(', ');
|
|
}
|
|
|
|
return undefined;
|
|
});
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
|
|
|
|
var ngValueDirective = [function() {
|
|
return {
|
|
priority: 100,
|
|
compile: function(tpl, attr) {
|
|
if (CONSTANT_VALUE_REGEXP.test(attr.ngValue)) {
|
|
return function(scope) {
|
|
attr.$set('value', scope.$eval(attr.ngValue));
|
|
};
|
|
} else {
|
|
attr.$observers.value = [];
|
|
|
|
return function(scope) {
|
|
scope.$watch(attr.ngValue, function(value) {
|
|
attr.$set('value', value, false);
|
|
});
|
|
};
|
|
}
|
|
}
|
|
};
|
|
}];
|