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); +          }); +        }; +      } +    } +  }; +}]; | 
