diff options
| author | Misko Hevery | 2012-03-23 14:03:24 -0700 |
|---|---|---|
| committer | Misko Hevery | 2012-03-28 11:16:35 -0700 |
| commit | 2430f52bb97fa9d682e5f028c977c5bf94c5ec38 (patch) | |
| tree | e7529b741d70199f36d52090b430510bad07f233 /src/ng/directive/input.js | |
| parent | 944098a4e0f753f06b40c73ca3e79991cec6c2e2 (diff) | |
| download | angular.js-2430f52bb97fa9d682e5f028c977c5bf94c5ec38.tar.bz2 | |
chore(module): move files around in preparation for more modules
Diffstat (limited to 'src/ng/directive/input.js')
| -rw-r--r-- | src/ng/directive/input.js | 1194 |
1 files changed, 1194 insertions, 0 deletions
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js new file mode 100644 index 00000000..348c9f25 --- /dev/null +++ b/src/ng/directive/input.js @@ -0,0 +1,1194 @@ +'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('{"last":"visitor","name":"guest"}'); + 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('{"last":"","name":"guest"}'); + 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); + }); + }; + } + } + }; +}]; |
