diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/Angular.js | 12 | ||||
| -rw-r--r-- | src/AngularPublic.js | 9 | ||||
| -rw-r--r-- | src/scenario/Scenario.js | 2 | ||||
| -rw-r--r-- | src/scenario/dsl.js | 2 | ||||
| -rw-r--r-- | src/service/formFactory.js | 414 | ||||
| -rw-r--r-- | src/widget/form.js | 134 | ||||
| -rw-r--r-- | src/widget/input.js | 1007 | ||||
| -rw-r--r-- | src/widget/select.js | 138 | 
8 files changed, 764 insertions, 954 deletions
| diff --git a/src/Angular.js b/src/Angular.js index fec866f5..1265ad9f 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -91,7 +91,6 @@ var $boolean          = 'boolean',      angular           = window.angular || (window.angular = {}),      angularModule,      /** @name angular.module.ng */ -    angularInputType  = extensionMap(angular, 'inputType', lowercase),      nodeName_,      uid               = ['0', '0', '0'],      DATE_ISOSTRING_LN = 24; @@ -272,17 +271,6 @@ identity.$inject = [];  function valueFn(value) {return function() {return value;};} -function extensionMap(angular, name, transform) { -  var extPoint; -  return angular[name] || (extPoint = angular[name] = function(name, fn, prop){ -    name = (transform || identity)(name); -    if (isDefined(fn)) { -      extPoint[name] = extend(fn, prop || {}); -    } -    return extPoint[name]; -  }); -} -  /**   * @ngdoc function   * @name angular.isUndefined diff --git a/src/AngularPublic.js b/src/AngularPublic.js index d1ae4a18..20ca5edb 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -98,7 +98,13 @@ function publishExternalAPI(angular){              ngSwitchDefault: ngSwitchDefaultDirective,              ngOptions: ngOptionsDirective,              ngView: ngViewDirective, -            ngTransclude: ngTranscludeDirective +            ngTransclude: ngTranscludeDirective, +            ngModel: ngModelDirective, +            ngList: ngListDirective, +            ngChange: ngChangeDirective, +            ngBindImmediate: ngBindImmediateDirective, +            required: requiredDirective, +            ngRequired: requiredDirective            }).          directive(ngEventDirectives).          directive(ngAttributeAliasDirectives); @@ -110,7 +116,6 @@ function publishExternalAPI(angular){        $provide.service('$exceptionHandler', $ExceptionHandlerProvider);        $provide.service('$filter', $FilterProvider);        $provide.service('$interpolate', $InterpolateProvider); -      $provide.service('$formFactory', $FormFactoryProvider);        $provide.service('$http', $HttpProvider);        $provide.service('$httpBackend', $HttpBackendProvider);        $provide.service('$location', $LocationProvider); diff --git a/src/scenario/Scenario.js b/src/scenario/Scenario.js index 7e33181c..cd3c335f 100644 --- a/src/scenario/Scenario.js +++ b/src/scenario/Scenario.js @@ -327,7 +327,7 @@ function browserTrigger(element, type, keys) {  (function(fn){    var parentTrigger = fn.trigger;    fn.trigger = function(type) { -    if (/(click|change|keydown)/.test(type)) { +    if (/(click|change|keydown|blur)/.test(type)) {        var processDefaults = [];        this.each(function(index, node) {          processDefaults.push(browserTrigger(node, type)); diff --git a/src/scenario/dsl.js b/src/scenario/dsl.js index fb0037e0..f6cc8086 100644 --- a/src/scenario/dsl.js +++ b/src/scenario/dsl.js @@ -203,7 +203,7 @@ angular.scenario.dsl('input', function() {      return this.addFutureAction("input '" + this.name + "' enter '" + value + "'", function($window, $document, done) {        var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');        input.val(value); -      input.trigger('keydown'); +      input.trigger('blur');        done();      });    }; diff --git a/src/service/formFactory.js b/src/service/formFactory.js deleted file mode 100644 index b051f7b9..00000000 --- a/src/service/formFactory.js +++ /dev/null @@ -1,414 +0,0 @@ -'use strict'; - -/** - * @ngdoc object - * @name angular.module.ng.$formFactory - * - * @description - * Use `$formFactory` to create a new instance of a {@link angular.module.ng.$formFactory.Form Form} - * controller or to find the nearest form instance for a given DOM element. - * - * The form instance is a collection of widgets, and is responsible for life cycle and validation - * of widget. - * - * Keep in mind that both form and widget instances are {@link api/angular.module.ng.$rootScope.Scope scopes}. - * - * @param {Form=} parentForm The form which should be the parent form of the new form controller. - *   If none specified default to the `rootForm`. - * @returns {Form} A new {@link angular.module.ng.$formFactory.Form Form} instance. - * - * @example - * - * This example shows how one could write a widget which would enable data-binding on - * `contenteditable` feature of HTML. - * -    <doc:example module="formModule"> -      <doc:source> -        <script> -          function EditorCntl($scope) { -            $scope.htmlContent = '<b>Hello</b> <i>World</i>!'; -          } - -          HTMLEditorWidget.$inject = ['$scope', '$element', '$sanitize']; -          function HTMLEditorWidget(scope, element, $sanitize) { -            scope.$parseModel = function() { -              // need to protect for script injection -              try { -                scope.$viewValue = $sanitize( -                  scope.$modelValue || ''); -                if (this.$error.HTML) { -                  // we were invalid, but now we are OK. -                  scope.$emit('$valid', 'HTML'); -                } -              } catch (e) { -                // if HTML not parsable invalidate form. -                scope.$emit('$invalid', 'HTML'); -              } -            } - -            scope.$render = function() { -              element.html(this.$viewValue); -            } - -            element.bind('keyup', function() { -              scope.$apply(function() { -                scope.$emit('$viewChange', element.html()); -              }); -            }); -          } - -       angular.module('formModule', [], function($compileProvider){ -         $compileProvider.directive('ngHtmlEditorModel', function ($formFactory) { -           return function(scope, element, attr) { -             var form = $formFactory.forElement(element), -                 widget; -             element.attr('contentEditable', true); -             widget = form.$createWidget({ -               scope: scope, -               model: attr.ngHtmlEditorModel, -               controller: HTMLEditorWidget, -               controllerArgs: {$element: element}}); -             // if the element is destroyed, then we need to -             // notify the form. -             element.bind('$destroy', function() { -               widget.$destroy(); -             }); -           }; -         }); -       }); -     </script> -     <form name='editorForm' ng:controller="EditorCntl"> -       <div ng:html-editor-model="htmlContent"></div> -       <hr/> -       HTML: <br/> -       <textarea ng:model="htmlContent" cols="80"></textarea> -       <hr/> -       <pre>editorForm = {{editorForm|json}}</pre> -     </form> -   </doc:source> -   <doc:scenario> -     it('should enter invalid HTML', function() { -       expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-valid/); -       input('htmlContent').enter('<'); -       expect(element('form[name=editorForm]').prop('className')).toMatch(/ng-invalid/); -     }); -   </doc:scenario> - </doc:example> - */ - -/** - * @ngdoc object - * @name angular.module.ng.$formFactory.Form - * @description - * The `Form` is a controller which keeps track of the validity of the widgets contained within it. - */ - -function $FormFactoryProvider() { -  var $parse; -  this.$get = ['$rootScope', '$parse', '$controller', -      function($rootScope, $parse_, $controller) { -    $parse = $parse_; -    /** -     * @ngdoc proprety -     * @name rootForm -     * @propertyOf angular.module.ng.$formFactory -     * @description -     * Static property on `$formFactory` -     * -     * Each application ({@link guide/dev_guide.scopes.internals root scope}) gets a root form which -     * is the top-level parent of all forms. -     */ -    formFactory.rootForm = formFactory($rootScope); - - -    /** -     * @ngdoc method -     * @name forElement -     * @methodOf angular.module.ng.$formFactory -     * @description -     * Static method on `$formFactory` service. -     * -     * Retrieve the closest form for a given element or defaults to the `root` form. Used by the -     * {@link angular.module.ng.$compileProvider.directive.form form} element. -     * @param {Element} element The element where the search for form should initiate. -     */ -    formFactory.forElement = function(element) { -      return element.inheritedData('$form') || formFactory.rootForm; -    }; -    return formFactory; - -    function formFactory(parent) { -      var scope = (parent || formFactory.rootForm).$new(); -      $controller(FormController, {$scope: scope}); -      return scope; -    } - -  }]; - -  function propertiesUpdate(widget) { -    widget.$valid = !(widget.$invalid = -      !(widget.$readonly || widget.$disabled || equals(widget.$error, {}))); -  } - -  /** -   * @ngdoc property -   * @name $error -   * @propertyOf angular.module.ng.$formFactory.Form -   * @description -   * Property of the form and widget instance. -   * -   * Summary of all of the errors on the page. If a widget emits `$invalid` with `REQUIRED` key, -   * then the `$error` object will have a `REQUIRED` key with an array of widgets which have -   * emitted this key. `form.$error.REQUIRED == [ widget ]`. -   */ - -  /** -   * @ngdoc property -   * @name $invalid -   * @propertyOf angular.module.ng.$formFactory.Form -   * @description -   * Property of the form and widget instance. -   * -   * True if any of the widgets of the form are invalid. -   */ - -  /** -   * @ngdoc property -   * @name $valid -   * @propertyOf angular.module.ng.$formFactory.Form -   * @description -   * Property of the form and widget instance. -   * -   * True if all of the widgets of the form are valid. -   */ - -  /** -   * @ngdoc event -   * @name angular.module.ng.$formFactory.Form#$valid -   * @eventOf angular.module.ng.$formFactory.Form -   * @eventType listen on form -   * @description -   * Upon receiving the `$valid` event from the widget update the `$error`, `$valid` and `$invalid` -   * properties of both the widget as well as the from. -   * -   * @param {string} validationKey The validation key to be used when updating the `$error` object. -   *    The validation key is what will allow the template to bind to a specific validation error -   *    such as `<div ng:show="form.$error.KEY">error for key</div>`. -   */ - -  /** -   * @ngdoc event -   * @name angular.module.ng.$formFactory.Form#$invalid -   * @eventOf angular.module.ng.$formFactory.Form -   * @eventType listen on form -   * @description -   * Upon receiving the `$invalid` event from the widget update the `$error`, `$valid` and `$invalid` -   * properties of both the widget as well as the from. -   * -   * @param {string} validationKey The validation key to be used when updating the `$error` object. -   *    The validation key is what will allow the template to bind to a specific validation error -   *    such as `<div ng:show="form.$error.KEY">error for key</div>`. -   */ - -  /** -   * @ngdoc event -   * @name angular.module.ng.$formFactory.Form#$validate -   * @eventOf angular.module.ng.$formFactory.Form -   * @eventType emit on widget -   * @description -   * Emit the `$validate` event on the widget, giving a widget a chance to emit a -   * `$valid` / `$invalid` event base on its state. The `$validate` event is triggered when the -   * model or the view changes. -   */ - -  /** -   * @ngdoc event -   * @name angular.module.ng.$formFactory.Form#$viewChange -   * @eventOf angular.module.ng.$formFactory.Form -   * @eventType listen on widget -   * @description -   * A widget is responsible for emitting this event whenever the view changes do to user interaction. -   * The event takes a `$viewValue` parameter, which is the new value of the view. This -   * event triggers a call to `$parseView()` as well as `$validate` event on widget. -   * -   * @param {*} viewValue The new value for the view which will be assigned to `widget.$viewValue`. -   */ - -  FormController.$inject = ['$scope', '$injector']; -  function FormController($scope, $injector) { -    this.$injector = $injector; - -    var form = this.form = $scope, -        $error = form.$error = {}; - -    form.$on('$destroy', function(event){ -      var widget = event.targetScope; -      if (widget.$widgetId) { -        delete form[widget.$widgetId]; -      } -      forEach($error, removeWidget, widget); -    }); - -    form.$on('$valid', function(event, error){ -      var widget = event.targetScope; -      delete widget.$error[error]; -      propertiesUpdate(widget); -      removeWidget($error[error], error, widget); -    }); - -    form.$on('$invalid', function(event, error){ -      var widget = event.targetScope; -      addWidget(error, widget); -      widget.$error[error] = true; -      propertiesUpdate(widget); -    }); - -    propertiesUpdate(form); -    form.$createWidget = bind(this, this.$createWidget); - -    function removeWidget(queue, errorKey, widget) { -      if (queue) { -        widget = widget || this; // so that we can be used in forEach; -        for (var i = 0, length = queue.length; i < length; i++) { -          if (queue[i] === widget) { -            queue.splice(i, 1); -            if (!queue.length) { -              delete $error[errorKey]; -            } -          } -        } -        propertiesUpdate(form); -      } -    } - -    function addWidget(errorKey, widget) { -      var queue = $error[errorKey]; -      if (queue) { -        for (var i = 0, length = queue.length; i < length; i++) { -          if (queue[i] === widget) { -            return; -          } -        } -      } else { -        $error[errorKey] = queue = []; -      } -      queue.push(widget); -      propertiesUpdate(form); -    } -  } - - -  /** -   * @ngdoc method -   * @name $createWidget -   * @methodOf angular.module.ng.$formFactory.Form -   * @description -   * -   * Use form's `$createWidget` instance method to create new widgets. The widgets can be created -   * using an alias which makes the accessible from the form and available for data-binding, -   * useful for displaying validation error messages. -   * -   * The creation of a widget sets up: -   * -   *   - `$watch` of `expression` on `model` scope. This code path syncs the model to the view. -   *      The `$watch` listener will: -   * -   *     - assign the new model value of `expression` to `widget.$modelValue`. -   *     - call `widget.$parseModel` method if present. The `$parseModel` is responsible for copying -   *       the `widget.$modelValue` to `widget.$viewValue` and optionally converting the data. -   *       (For example to convert a number into string) -   *     - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` -   *       event. -   *     - call `widget.$render()` method on widget. The `$render` method is responsible for -   *       reading the `widget.$viewValue` and updating the DOM. -   * -   *   - Listen on `$viewChange` event from the `widget`. This code path syncs the view to the model. -   *     The `$viewChange` listener will: -   * -   *     - assign the value to `widget.$viewValue`. -   *     - call `widget.$parseView` method if present. The `$parseView` is responsible for copying -   *       the `widget.$viewValue` to `widget.$modelValue` and optionally converting the data. -   *       (For example to convert a string into number) -   *     - emits `$validate` event on widget giving a widget a chance to emit `$valid` / `$invalid` -   *       event. -   *     - Assign the  `widget.$modelValue` to the `expression` on the `model` scope. -   * -   *   - Creates these set of properties on the `widget` which are updated as a response to the -   *     `$valid` / `$invalid` events: -   * -   *     - `$error` -  object - validation errors will be published as keys on this object. -   *       Data-binding to this property is useful for displaying the validation errors. -   *     - `$valid` - boolean - true if there are no validation errors -   *     - `$invalid` - boolean - opposite of `$valid`. -   * @param {Object} params Named parameters: -   * -   *   - `scope` - `{Scope}` -  The scope to which the model for this widget is attached. -   *   - `model` - `{string}` - The name of the model property on model scope. -   *   - `controller` - {WidgetController} - The controller constructor function. -   *      The controller constructor should create these instance methods. -   *     - `$parseView()`: optional method responsible for copying `$viewVale` to `$modelValue`. -   *         The method may fire `$valid`/`$invalid` events. -   *     - `$parseModel()`: optional method responsible for copying `$modelVale` to `$viewValue`. -   *         The method may fire `$valid`/`$invalid` events. -   *     - `$render()`: required method which needs to update the DOM of the widget to match the -   *         `$viewValue`. -   * -   *   - `controllerArgs` - `{Array}` (Optional) -  Any extra arguments will be curried to the -   *     WidgetController constructor. -   *   - `onChange` - `{(string|function())}` (Optional) - Expression to execute when user changes the -   *     value. -   *   - `alias` - `{string}` (Optional) - The name of the form property under which the widget -   *     instance should be published. The name should be unique for each form. -   * @returns {Widget} Instance of a widget scope. -   */ -  FormController.prototype.$createWidget = function(params) { -    var form = this.form, -        modelScope = params.scope, -        onChange = params.onChange, -        alias = params.alias, -        scopeGet = $parse(params.model), -        scopeSet = scopeGet.assign, -        widget = form.$new(); - -    this.$injector.instantiate(params.controller, extend({$scope: widget}, params.controllerArgs)); - -    if (!scopeSet) { -      throw Error("Expression '" + params.model + "' is not assignable!"); -    } - -    widget.$error = {}; -    // Set the state to something we know will change to get the process going. -    widget.$modelValue = Number.NaN; -    // watch for scope changes and update the view appropriately -    modelScope.$watch(scopeGet, function(value) { -      if (!equals(widget.$modelValue, value)) { -        widget.$modelValue = value; -        widget.$parseModel ? widget.$parseModel() : (widget.$viewValue = value); -        widget.$emit('$validate'); -        widget.$render && widget.$render(); -      } -    }); - -    widget.$on('$viewChange', function(event, viewValue){ -      if (!equals(widget.$viewValue, viewValue)) { -        widget.$viewValue = viewValue; -        widget.$parseView ? widget.$parseView() : (widget.$modelValue = widget.$viewValue); -        scopeSet(modelScope, widget.$modelValue); -        if (onChange) modelScope.$eval(onChange); -        widget.$emit('$validate'); -      } -    }); - -    propertiesUpdate(widget); - -    // assign the widgetModel to the form -    if (alias && !form.hasOwnProperty(alias)) { -      form[alias] = widget; -      widget.$widgetId = alias; -    } else { -      alias = null; -    } - -    return widget; -  }; -} diff --git a/src/widget/form.js b/src/widget/form.js index deaf38d5..23b07107 100644 --- a/src/widget/form.js +++ b/src/widget/form.js @@ -1,5 +1,86 @@  'use strict'; +FormController.$inject = ['$scope', 'name']; +function FormController($scope, name) { +  var form = this, +      errors = form.error = {}; + +  // publish the form into scope +  name(this); + +  $scope.$on('$destroy', function(event, widget) { +    if (!widget) return; + +    if (widget.widgetId) { +      delete form[widget.widgetId]; +    } +    forEach(errors, removeWidget, widget); +  }); + +  $scope.$on('$valid', function(event, error, widget) { +    removeWidget(errors[error], error, widget); + +    if (equals(errors, {})) { +      form.valid = true; +      form.invalid = false; +    } +  }); + +  $scope.$on('$invalid', function(event, error, widget) { +    addWidget(error, widget); + +    form.valid = false; +    form.invalid = true; +  }); + +  $scope.$on('$viewTouch', function() { +    form.dirty = true; +    form.pristine = false; +  }); + +  // init state +  form.dirty = false; +  form.pristine = true; +  form.valid = true; +  form.invalid = false; + +  function removeWidget(queue, errorKey, widget) { +    if (queue) { +      widget = widget || this; // so that we can be used in forEach; +      for (var i = 0, length = queue.length; i < length; i++) { +        if (queue[i] === widget) { +          queue.splice(i, 1); +          if (!queue.length) { +            delete errors[errorKey]; +          } +        } +      } +    } +  } + +  function addWidget(errorKey, widget) { +    var queue = errors[errorKey]; +    if (queue) { +      for (var i = 0, length = queue.length; i < length; i++) { +        if (queue[i] === widget) { +          return; +        } +      } +    } else { +      errors[errorKey] = queue = []; +    } +    queue.push(widget); +  } +} + +FormController.prototype.registerWidget = function(widget, alias) { +  if (alias && !this.hasOwnProperty(alias)) { +    widget.widgetId = alias; +    this[alias] = widget; +  } +}; + +  /**   * @ngdoc directive   * @name angular.module.ng.$compileProvider.directive.form @@ -57,55 +138,54 @@             $scope.text = 'guest';           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           text: <input type="text" name="input" ng:model="text" required> -           <span class="error" ng:show="myForm.text.$error.REQUIRED">Required!</span> -         </form> +       <form name="myForm" ng:controller="Ctrl"> +         text: <input type="text" name="input" ng:model="text" required> +         <span class="error" ng:show="myForm.input.error.REQUIRED">Required!</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/> -       </div> +         <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'); +         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'); +         expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example>   */ -var ngFormDirective = ['$formFactory', function($formFactory) { +var ngFormDirective = [function() {    return { +    name: 'form',      restrict: 'E', +    scope: true, +    inject: { +      name: 'accessor' +    }, +    controller: FormController,      compile: function() {        return { -        pre: function(scope, formElement, attr) { -          var name = attr.name, -            parentForm = $formFactory.forElement(formElement), -            form = $formFactory(parentForm); -          formElement.data('$form', form); -          formElement.bind('submit', function(event){ +        pre: function(scope, formElement, attr, controller) { +          formElement.data('$form', controller); +          formElement.bind('submit', function(event) {              if (!attr.action) event.preventDefault();            }); -          if (name) { -            scope[name] = form; -          } -          watch('valid'); -          watch('invalid'); -          function watch(name) { -            form.$watch('$' + name, function(value) { + +          forEach(['valid', 'invalid', 'dirty', 'pristine'], function(name) { +            scope.$watch(function() { +              return controller[name]; +            }, function(value) {                formElement[value ? 'addClass' : 'removeClass']('ng-' + name);              }); -          } +          });          }        };      } diff --git a/src/widget/input.js b/src/widget/input.js index 05390b38..6c95327c 100644 --- a/src/widget/input.js +++ b/src/widget/input.js @@ -4,7 +4,6 @@  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 INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;  /** @@ -36,37 +35,36 @@ var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;             $scope.word = /^\w*$/;           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           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> -         </form> +       <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/> -       </div> +         <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'); +          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'); +          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'); +          expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example> @@ -100,48 +98,39 @@ var INTEGER_REGEXP = /^\s*(\-|\+)?\d+\s*$/;             $scope.text = 'me@example.com';           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> +         <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"> +           <span class="error" ng:show="myForm.input.error.REQUIRED">               Required!</span> -           <span class="error" ng:show="myForm.input.$error.EMAIL"> +           <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> -         <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/> -       </div>        </doc:source>        <doc:scenario>          it('should initialize to model', function() {            expect(binding('text')).toEqual('me@example.com'); -          expect(binding('myForm.input.$valid')).toEqual('true'); +          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'); +          expect(binding('myForm.input.valid')).toEqual('false');          });          it('should be invalid if not email', function() {            input('text').enter('xxx'); -          expect(binding('text')).toEqual('xxx'); -          expect(binding('myForm.input.$valid')).toEqual('false'); +          expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example>   */ -angularInputType('email', function(element, widget) { -  widget.$on('$validate', function(event) { -    var value = widget.$viewValue; -    widget.$emit(!value || value.match(EMAIL_REGEXP) ? "$valid" : "$invalid", "EMAIL"); -  }); -});  /** @@ -173,48 +162,39 @@ angularInputType('email', function(element, widget) {             $scope.text = 'http://google.com';           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           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> -         </form> +       <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/> -       </div> +         <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'); +          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'); +          expect(binding('myForm.input.valid')).toEqual('false');          });          it('should be invalid if not url', function() {            input('text').enter('xxx'); -          expect(binding('text')).toEqual('xxx'); -          expect(binding('myForm.input.$valid')).toEqual('false'); +          expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example>   */ -angularInputType('url', function(element, widget) { -  widget.$on('$validate', function(event) { -    var value = widget.$viewValue; -    widget.$emit(!value || value.match(URL_REGEXP) ? "$valid" : "$invalid", "URL"); -  }); -});  /** @@ -241,53 +221,58 @@ angularInputType('url', function(element, widget) {             $scope.names = ['igor', 'misko', 'vojta'];           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           List: <input type="list" name="input" ng:model="names" required> -           <span class="error" ng:show="myForm.list.$error.REQUIRED"> -             Required!</span> -         </form> +       <form name="myForm" ng:controller="Ctrl"> +         List: <input type="list" name="input" ng:model="names" required> +         <span class="error" ng:show="myForm.list.error.REQUIRED"> +           Required!</span>           <tt>names = {{names}}</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/> -       </div> +         <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('names')).toEqual('["igor","misko","vojta"]'); -          expect(binding('myForm.input.$valid')).toEqual('true'); +          expect(binding('myForm.input.valid')).toEqual('true');          });          it('should be invalid if empty', function() {            input('names').enter(''); -          expect(binding('names')).toEqual('[]'); -          expect(binding('myForm.input.$valid')).toEqual('false'); +          expect(binding('names')).toEqual(''); +          expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example>   */ -angularInputType('list', function(element, widget) { -  function parse(viewValue) { -    var list = []; -    forEach(viewValue.split(/\s*,\s*/), function(value){ -      if (value) list.push(trim(value)); -    }); -    return list; -  } -  widget.$parseView = function() { -    isString(widget.$viewValue) && (widget.$modelValue = parse(widget.$viewValue)); -  }; -  widget.$parseModel = function() { -    var modelValue = widget.$modelValue; -    if (isArray(modelValue) -        && (!isString(widget.$viewValue) || !equals(parse(widget.$viewValue), modelValue))) { -      widget.$viewValue =  modelValue.join(', '); +var ngListDirective = function() { +  return { +    require: 'ngModel', +    link: function(scope, element, attr, ctrl) { +      var parse = function(viewValue) { +        var list = []; + +        if (viewValue) { +          forEach(viewValue.split(/\s*,\s*/), function(value) { +            if (value) list.push(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; +      });      }    }; -}); - +};  /**   * @ngdoc inputType @@ -320,113 +305,40 @@ angularInputType('list', function(element, widget) {             $scope.value = 12;           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           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> -         </form> +       <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/> -       </div> +         <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'); +         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'); +         expect(binding('myForm.input.valid')).toEqual('false');          });          it('should be invalid if over max', function() {           input('value').enter('123'); -         expect(binding('value')).toEqual('123'); -         expect(binding('myForm.input.$valid')).toEqual('false'); -        }); -      </doc:scenario> -    </doc:example> - */ -angularInputType('number', numericRegexpInputType(NUMBER_REGEXP, 'NUMBER')); - - -/** - * @ngdoc inputType - * @name angular.inputType.integer - * - * @description - * Text input with integer validation and transformation. Sets the `INTEGER` - * validation error key if not a valid integer. - * - * @param {string} ng:model Assignable angular expression to data-bind to. - * @param {string=} name Property name of the form under which the widgets 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> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           Integer: <input type="integer" 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.INTEGER"> -             Not valid integer!</span> -         </form> -         <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/> -       </div> -      </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('1.2'); -          expect(binding('value')).toEqual('12'); -          expect(binding('myForm.input.$valid')).toEqual('false'); -        }); - -        it('should be invalid if over max', function() { -          input('value').enter('123'); -          expect(binding('value')).toEqual('123'); -          expect(binding('myForm.input.$valid')).toEqual('false'); +         expect(binding('value')).toEqual('12'); +         expect(binding('myForm.input.valid')).toEqual('false');          });        </doc:scenario>      </doc:example>   */ -angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));  /** @@ -452,15 +364,13 @@ angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));             $scope.value2 = 'YES'           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           Value1: <input type="checkbox" ng:model="value1"> <br/> -           Value2: <input type="checkbox" ng:model="value2" -                          ng:true-value="YES" ng:false-value="NO"> <br/> -         </form> +       <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/> -       </div> +        </form>        </doc:source>        <doc:scenario>          it('should change state', function() { @@ -475,31 +385,7 @@ angularInputType('integer', numericRegexpInputType(INTEGER_REGEXP, 'INTEGER'));        </doc:scenario>      </doc:example>   */ -angularInputType('checkbox', function(inputElement, widget) { -  var trueValue = inputElement.attr('ng:true-value'), -      falseValue = inputElement.attr('ng:false-value'); - -  if (!isString(trueValue)) trueValue = true; -  if (!isString(falseValue)) falseValue = false; -  inputElement.bind('click', function() { -    widget.$apply(function() { -      widget.$emit('$viewChange', inputElement[0].checked); -    }); -  }); - -  widget.$render = function() { -    inputElement[0].checked = widget.$viewValue; -  }; - -  widget.$parseModel = function() { -    widget.$viewValue = widget.$modelValue === trueValue; -  }; - -  widget.$parseView = function() { -    widget.$modelValue = widget.$viewValue ? trueValue : falseValue; -  }; -});  /** @@ -523,14 +409,12 @@ angularInputType('checkbox', function(inputElement, widget) {             $scope.color = 'blue';           }         </script> -       <div ng:controller="Ctrl"> -         <form name="myForm"> -           <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/> -         </form> +       <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/> -       </div> +        </form>        </doc:source>        <doc:scenario>          it('should change state', function() { @@ -542,63 +426,6 @@ angularInputType('checkbox', function(inputElement, widget) {        </doc:scenario>      </doc:example>   */ -angularInputType('radio', function(inputElement, widget, attr) { -  //correct the name -  attr.$set('name', widget.$id + '@' + attr.name); -  inputElement.bind('click', function() { -    widget.$apply(function() { -      if (inputElement[0].checked) { -        widget.$emit('$viewChange', attr.value); -      } -    }); -  }); - -  widget.$render = function() { -    inputElement[0].checked = isDefined(attr.value) && (attr.value == widget.$viewValue); -  }; - -  if (inputElement[0].checked) { -    widget.$viewValue = attr.value; -  } -}); - - -function numericRegexpInputType(regexp, error) { -  return function(inputElement, widget) { -    var min = 1 * (inputElement.attr('min') || Number.MIN_VALUE), -        max = 1 * (inputElement.attr('max') || Number.MAX_VALUE); - -    widget.$on('$validate', function(event){ -      var value = widget.$viewValue, -          filled = value && trim(value) != '', -          valid = isString(value) && value.match(regexp); - -      widget.$emit(!filled || valid ? "$valid" : "$invalid", error); -      filled && (value = 1 * value); -      widget.$emit(valid && value < min ? "$invalid" : "$valid", "MIN"); -      widget.$emit(valid && value > max ? "$invalid" : "$valid", "MAX"); -    }); - -    widget.$parseView = function() { -      if (widget.$viewValue.match(regexp)) { -        widget.$modelValue = 1 * widget.$viewValue; -      } else if (widget.$viewValue == '') { -        widget.$modelValue = null; -      } -    }; - -    widget.$parseModel = function() { -      widget.$viewValue = isNumber(widget.$modelValue) -        ? '' + widget.$modelValue -        : ''; -    }; -  }; -} - - -var HTML5_INPUTS_TYPES =  makeMap( -        "search,tel,url,email,datetime,date,month,week,time,datetime-local,number,range,color," + -        "radio,checkbox,text,button,submit,reset,hidden,password");  /** @@ -641,188 +468,67 @@ var HTML5_INPUTS_TYPES =  makeMap(         <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"> +           <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"> +           <span class="error" ng:show="myForm.lastName.error.MINLENGTH">               Too short!</span> -           <span class="error" ng:show="myForm.lastName.$error.MAXLENGTH"> +           <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> +         <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'); +          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","name":""}'); -          expect(binding('myForm.userName.$valid')).toEqual('false'); -          expect(binding('myForm.$valid')).toEqual('false'); +          expect(binding('user')).toEqual('{"last":"visitor","name":null}'); +          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'); +          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('{"last":"xx","name":"guest"}'); -          expect(binding('myForm.lastName.$valid')).toEqual('false'); -          expect(binding('myForm.lastName.$error')).toMatch(/MINLENGTH/); -          expect(binding('myForm.$valid')).toEqual('false'); +          expect(binding('user')).toEqual('{"last":"visitor","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 valid if longer than max length', function() {            input('user.last').enter('some ridiculously long name');            expect(binding('user')) -            .toEqual('{"last":"some ridiculously long name","name":"guest"}'); -          expect(binding('myForm.lastName.$valid')).toEqual('false'); -          expect(binding('myForm.lastName.$error')).toMatch(/MAXLENGTH/); -          expect(binding('myForm.$valid')).toEqual('false'); +            .toEqual('{"last":"visitor","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 = ['$defer', '$formFactory', function($defer, $formFactory) { -  return { -    restrict: 'E', -    link: function(modelScope, inputElement, attr) { -      if (!attr.ngModel) return; - -      var form = $formFactory.forElement(inputElement), -          // We have to use .getAttribute, since jQuery tries to be smart and use the -          // type property. Trouble is some browser change unknown to text. -          type = attr.type || 'text', -          TypeController, -          patternMatch, widget, -          pattern = attr.ngPattern, -          modelExp = attr.ngModel, -          minlength = parseInt(attr.ngMinlength, 10), -          maxlength = parseInt(attr.ngMaxlength, 10), -          loadFromScope = type.match(/^\s*\@\s*(.*)/); - -       if (!pattern) { -         patternMatch = valueFn(true); -       } else { -         if (pattern.match(/^\/(.*)\/$/)) { -           pattern = new RegExp(pattern.substr(1, pattern.length - 2)); -           patternMatch = function(value) { -             return pattern.test(value); -           }; -         } else { -           patternMatch = function(value) { -             var patternObj = modelScope.$eval(pattern); -             if (!patternObj || !patternObj.test) { -               throw new Error('Expected ' + pattern + ' to be a RegExp but was ' + patternObj); -             } -             return patternObj.test(value); -           }; -         } -       } - -      type = lowercase(type); -      TypeController = (loadFromScope -              ? assertArgFn(modelScope.$eval(loadFromScope[1]), loadFromScope[1]) -              : angularInputType(type)) || noop; - -      if (!HTML5_INPUTS_TYPES[type]) { -        try { -          // jquery will not let you so we have to go to bare metal -          inputElement[0].setAttribute('type', 'text'); -        } catch(e){ -          // also turns out that ie8 will not allow changing of types, but since it is not -          // html5 anyway we can ignore the error. -        } -      } - -      //TODO(misko): setting $inject is a hack -      !TypeController.$inject && (TypeController.$inject = ['$element', '$scope', '$attr']); -      widget = form.$createWidget({ -          scope: modelScope, -          model: modelExp, -          onChange: attr.ngChange, -          alias: attr.name, -          controller: TypeController, -          controllerArgs: {$element: inputElement, $attr: attr} -      }); - -      widget.$pristine = !(widget.$dirty = false); - -      widget.$on('$validate', function() { -        var $viewValue = trim(widget.$viewValue), -            inValid = attr.required && !$viewValue, -            tooLong = maxlength && $viewValue && $viewValue.length > maxlength, -            tooShort = minlength && $viewValue && $viewValue.length < minlength, -            missMatch = $viewValue && !patternMatch($viewValue); - -        if (widget.$error.REQUIRED != inValid){ -          widget.$emit(inValid ? '$invalid' : '$valid', 'REQUIRED'); -        } -        if (widget.$error.PATTERN != missMatch){ -          widget.$emit(missMatch ? '$invalid' : '$valid', 'PATTERN'); -        } -        if (widget.$error.MINLENGTH != tooShort){ -          widget.$emit(tooShort ? '$invalid' : '$valid', 'MINLENGTH'); -        } -        if (widget.$error.MAXLENGTH != tooLong){ -          widget.$emit(tooLong ? '$invalid' : '$valid', 'MAXLENGTH'); -        } -      }); - -      forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { -        widget.$watch('$' + name, function(value) { -          inputElement[value ? 'addClass' : 'removeClass']('ng-' + name); -        }); -      }); - -      inputElement.bind('$destroy', function() { -        widget.$destroy(); -      }); - -      if (type != 'checkbox' && type != 'radio') { -        // TODO (misko): checkbox / radio does not really belong here, but until we can do -        // widget registration with CSS, we are hacking it this way. -        widget.$render = function() { -          inputElement.val(widget.$viewValue || ''); -        }; - -        inputElement.bind('keydown change input', function(event){ -          var key = event.keyCode; -          if (/*command*/   key != 91 && -              /*modifiers*/ !(15 < key && key < 19) && -              /*arrow*/     !(37 < key && key < 40)) { -            $defer(function() { -              widget.$dirty = !(widget.$pristine = false); -              var value = trim(inputElement.val()); -              if (widget.$viewValue !== value ) { -                widget.$emit('$viewChange', value); -              } -            }); -          } -        }); -      } -    } -  }; -}];  /** @@ -850,3 +556,452 @@ var inputDirective = ['$defer', '$formFactory', function($defer, $formFactory) {   * @param {string=} ng:change Angular expression to be executed when input changes due to user   *    interaction with the input element.   */ +var inputType = { +  'text': textInputType, +  'number': numberInputType, +  'url': urlInputType, +  'email': emailInputType, + +  'radio': radioInputType, +  '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() { +    var touched = ctrl.touch(), +        value = trim(element.val()); + +    if (ctrl.viewValue !== value) { +      scope.$apply(function() { +        ctrl.read(value); +      }); +    } else if (touched) { +      scope.$apply(); +    } +  }); + +  ctrl.render = function() { +    element.val(ctrl.viewValue || ''); +  }; + +  // pattern validator +  var pattern = attr.ngPattern, +      patternValidator; + +  var emit = function(regexp, value) { +    if (isEmpty(value) || regexp.test(value)) { +      ctrl.emitValidity('PATTERN', true); +      return value; +    } else { +      ctrl.emitValidity('PATTERN', false); +      return undefined; +    } +  }; + +  if (pattern) { +    if (pattern.match(/^\/(.*)\/$/)) { +      pattern = new RegExp(pattern.substr(1, pattern.length - 2)); +      patternValidator = function(value) { +        return emit(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 emit(patternObj, value); +      }; +    } + +    ctrl.formatters.push(patternValidator); +    ctrl.parsers.push(patternValidator); +  } + +  // min length validator +  if (attr.ngMinlength) { +    var minlength = parseInt(attr.ngMinlength, 10); +    var minLengthValidator = function(value) { +      if (!isEmpty(value) && value.length < minlength) { +        ctrl.emitValidity('MINLENGTH', false); +        return undefined; +      } else { +        ctrl.emitValidity('MINLENGTH', true); +        return value; +      } +    }; + +    ctrl.parsers.push(minLengthValidator); +    ctrl.formatters.push(minLengthValidator); +  } + +  // max length validator +  if (attr.ngMaxlength) { +    var maxlength = parseInt(attr.ngMaxlength, 10); +    var maxLengthValidator = function(value) { +      if (!isEmpty(value) && value.length > maxlength) { +        ctrl.emitValidity('MAXLENGTH', false); +        return undefined; +      } else { +        ctrl.emitValidity('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.emitValidity('NUMBER', true); +      return value === '' ? null : (empty ? value : parseFloat(value)); +    } else { +      ctrl.emitValidity('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.emitValidity('MIN', false); +        return undefined; +      } else { +        ctrl.emitValidity('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.emitValidity('MAX', false); +        return undefined; +      } else { +        ctrl.emitValidity('MAX', true); +        return value; +      } +    }; + +    ctrl.parsers.push(maxValidator); +    ctrl.formatters.push(maxValidator); +  } + +  ctrl.formatters.push(function(value) { + +    if (isEmpty(value) || isNumber(value)) { +      ctrl.emitValidity('NUMBER', true); +      return value; +    } else { +      ctrl.emitValidity('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.emitValidity('URL', true); +      return value; +    } else { +      ctrl.emitValidity('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.emitValidity('EMAIL', true); +      return value; +    } else { +      ctrl.emitValidity('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.touch(); +        ctrl.read(attr.value); +      }); +    }; +  }); + +  ctrl.render = function() { +    var value = attr.value; +    element[0].checked = isDefined(value) && (value == ctrl.viewValue); +  }; +} + +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.touch(); +      ctrl.read(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; +  }); +} + + +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 NgModelController = ['$scope', '$exceptionHandler', 'ngModel', +    function($scope, $exceptionHandler, ngModel) { +  this.viewValue = Number.NaN; +  this.modelValue = Number.NaN; +  this.parsers = []; +  this.formatters = []; +  this.error = {}; +  this.pristine = true; +  this.dirty = false; +  this.valid = true; +  this.invalid = false; +  this.render = noop; + +  this.touch = function() { +    if (this.dirty) return false; + +    this.dirty = true; +    this.pristine = false; +    try { +      $scope.$emit('$viewTouch'); +    } catch (e) { +      $exceptionHandler(e); +    } +    return true; +  }; + +  // don't $emit valid if already valid, the same for $invalid +  // not sure about this method name, should the argument be reversed ? emitError ? +  this.emitValidity = function(name, isValid) { + +    if (!isValid && this.error[name]) return; +    if (isValid && !this.error[name]) return; + +    if (!isValid) { +      this.error[name] = true; +      this.invalid = true; +      this.valid = false; +    } + +    if (isValid) { +      delete this.error[name]; +      if (equals(this.error, {})) { +        this.valid = true; +        this.invalid = false; +      } +    } + +    return $scope.$emit(isValid ? '$valid' : '$invalid', name, this); +  }; + +  // view -> model +  this.read = function(value) { +    this.viewValue = value; + +    forEach(this.parsers, function(fn) { +      value = fn(value); +    }); + +    if (isDefined(value) && this.model !== value) { +      this.modelValue = value; +      ngModel(value); +      $scope.$emit('$viewChange', value, this); +    } +  }; + +  // 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 (isDefined(value) && ctrl.viewValue !== value) { +      ctrl.viewValue = value; +      ctrl.render(); +    } +  }); +}]; + + +var ngModelDirective = [function() { +  return { +    inject: { +      ngModel: 'accessor' +    }, +    require: ['ngModel', '^?form'], +    controller: NgModelController, +    link: function(scope, element, attr, controllers) { +      var modelController = controllers[0], +          formController = controllers[1]; + +      if (formController) { +        formController.registerWidget(modelController, attr.name); +      } + +      forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { +        scope.$watch(function() { +          return modelController[name]; +        }, function(value) { +          element[value ? 'addClass' : 'removeClass']('ng-' + name); +        }); +      }); + +      element.bind('$destroy', function() { +        scope.$emit('$destroy', modelController); +      }); +    } +  }; +}]; + + +var ngChangeDirective = valueFn({ +  require: 'ngModel', +  link: function(scope, element, attr, ctrl) { +    scope.$on('$viewChange', function(event, value, widget) { +      if (ctrl === widget) scope.$eval(attr.ngChange); +    }); +  } +}); + + +var ngBindImmediateDirective = ['$browser', function($browser) { +  return { +    require: 'ngModel', +    link: function(scope, element, attr, ctrl) { +      element.bind('keydown change input', function(event) { +        var key = event.keyCode; + +        //    command            modifiers                   arrows +        if (key === 91 || (15 < key && key < 19) || (37 <= key && key <= 40)) return; + +        $browser.defer(function() { +          var touched = ctrl.touch(), +              value = trim(element.val()); + +          if (ctrl.viewValue !== value) { +            scope.$apply(function() { +              ctrl.read(value); +            }); +          } else if (touched) { +            scope.$apply(); +          } +        }); +      }); +    } +  }; +}]; + + +var requiredDirective = [function() { +  return { +    require: '?ngModel', +    link: function(scope, elm, attr, ctrl) { +      if (!ctrl) return; + +      var validator = function(value) { +        if (attr.required && isEmpty(value)) { +          ctrl.emitValidity('REQUIRED', false); +          return null; +        } else { +          ctrl.emitValidity('REQUIRED', true); +          return value; +        } +      }; + +      ctrl.formatters.push(validator); +      ctrl.parsers.unshift(validator); + +      attr.$observe('required', function() { +        validator(ctrl.viewValue); +      }); +    } +  }; +}]; diff --git a/src/widget/select.js b/src/widget/select.js index f70575a6..e7386147 100644 --- a/src/widget/select.js +++ b/src/widget/select.js @@ -123,87 +123,79 @@   */  var ngOptionsDirective = valueFn({ terminal: true }); -var selectDirective = ['$formFactory', '$compile', '$parse', -               function($formFactory,   $compile,   $parse){ +var selectDirective = ['$compile', '$parse', function($compile,   $parse) {                           //00001111100000000000222200000000000000000000003333000000000000044444444444444444000000000555555555555555550000000666666666666666660000000000000007777    var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;    return {      restrict: 'E', -    link: function(modelScope, selectElement, attr) { -      if (!attr.ngModel) return; -      var form = $formFactory.forElement(selectElement), -          multiple = attr.multiple, -          optionsExp = attr.ngOptions, -          modelExp = attr.ngModel, -          widget = form.$createWidget({ -            scope: modelScope, -            model: modelExp, -            onChange: attr.ngChange, -            alias: attr.name, -            controller: ['$scope', optionsExp ? Options : (multiple ? Multiple : Single)]}); - -      selectElement.bind('$destroy', function() { widget.$destroy(); }); - -      widget.$pristine = !(widget.$dirty = false); - -      widget.$on('$validate', function() { -        var valid = !attr.required || !!widget.$modelValue; -        if (valid && multiple && attr.required) valid = !!widget.$modelValue.length; -        if (valid !== !widget.$error.REQUIRED) { -          widget.$emit(valid ? '$valid' : '$invalid', 'REQUIRED'); -        } -      }); +    require: '?ngModel', +    link: function(scope, element, attr, ctrl) { +      if (!ctrl) return; + +      var multiple = attr.multiple, +          optionsExp = attr.ngOptions; + +      // required validator +      if (multiple && (attr.required || attr.ngRequired)) { +        var requiredValidator = function(value) { +          ctrl.emitValidity('REQUIRED', !attr.required || (value && value.length)); +          return value; +        }; -      widget.$on('$viewChange', function() { -        widget.$pristine = !(widget.$dirty = true); -      }); +        ctrl.parsers.push(requiredValidator); +        ctrl.formatters.unshift(requiredValidator); -      forEach(['valid', 'invalid', 'pristine', 'dirty'], function(name) { -        widget.$watch('$' + name, function(value) { -          selectElement[value ? 'addClass' : 'removeClass']('ng-' + name); +        attr.$observe('required', function() { +          requiredValidator(ctrl.viewValue);          }); -      }); +      } + +      if (optionsExp) Options(scope, element, ctrl); +      else if (multiple) Multiple(scope, element, ctrl); +      else Single(scope, element, ctrl); +        //////////////////////////// -      function Multiple(widget) { -        widget.$render = function() { -          var items = new HashMap(this.$viewValue); -          forEach(selectElement.children(), function(option){ -            option.selected = isDefined(items.get(option.value)); -          }); + + +      function Single(scope, selectElement, ctrl) { +        ctrl.render = function() { +          selectElement.val(ctrl.viewValue);          };          selectElement.bind('change', function() { -          widget.$apply(function() { -            var array = []; -            forEach(selectElement.children(), function(option){ -              if (option.selected) { -                array.push(option.value); -              } -            }); -            widget.$emit('$viewChange', array); +          scope.$apply(function() { +            ctrl.touch(); +            ctrl.read(selectElement.val());            });          }); -        } -      function Single(widget) { -        widget.$render = function() { -          selectElement.val(widget.$viewValue); +      function Multiple(scope, selectElement, ctrl) { +        ctrl.render = function() { +          var items = new HashMap(ctrl.viewValue); +          forEach(selectElement.children(), function(option) { +            option.selected = isDefined(items.get(option.value)); +          });          };          selectElement.bind('change', function() { -          widget.$apply(function() { -            widget.$emit('$viewChange', selectElement.val()); +          scope.$apply(function() { +            var array = []; +            forEach(selectElement.children(), function(option) { +              if (option.selected) { +                array.push(option.value); +              } +            }); +            ctrl.touch(); +            ctrl.read(array);            });          }); - -        widget.$viewValue = selectElement.val();        } -      function Options(widget) { +      function Options(scope, selectElement, ctrl) {          var match;          if (! (match = optionsExp.match(NG_OPTIONS_REGEXP))) { @@ -234,15 +226,15 @@ var selectDirective = ['$formFactory', '$compile', '$parse',              // developer declared null option, so user should be able to select it              nullOption = jqLite(option).remove();              // compile the element since there might be bindings in it -            $compile(nullOption)(modelScope); +            $compile(nullOption)(scope);            }          });          selectElement.html(''); // clear contents          selectElement.bind('change', function() { -          widget.$apply(function() { +          scope.$apply(function() {              var optionGroup, -                collection = valuesFn(modelScope) || [], +                collection = valuesFn(scope) || [],                  locals = {},                  key, value, optionElement, index, groupIndex, length, groupLength; @@ -259,7 +251,7 @@ var selectDirective = ['$formFactory', '$compile', '$parse',                      key = optionElement.val();                      if (keyName) locals[keyName] = key;                      locals[valueName] = collection[key]; -                    value.push(valueFn(modelScope, locals)); +                    value.push(valueFn(scope, locals));                    }                  }                } @@ -272,17 +264,21 @@ var selectDirective = ['$formFactory', '$compile', '$parse',                } else {                  locals[valueName] = collection[key];                  if (keyName) locals[keyName] = key; -                value = valueFn(modelScope, locals); +                value = valueFn(scope, locals);                }              } -            if (isDefined(value) && modelScope.$viewVal !== value) { -              widget.$emit('$viewChange', value); +            ctrl.touch(); + +            if (ctrl.viewValue !== value) { +              ctrl.read(value);              }            });          }); -        widget.$watch(render); -        widget.$render = render; +        ctrl.render = render; + +        // TODO(vojta): can't we optimize this ? +        scope.$watch(render);          function render() {            var optionGroups = {'':[]}, // Temporary location for the option groups before we render them @@ -291,8 +287,8 @@ var selectDirective = ['$formFactory', '$compile', '$parse',                optionGroup,                option,                existingParent, existingOptions, existingOption, -              modelValue = widget.$modelValue, -              values = valuesFn(modelScope) || [], +              modelValue = ctrl.modelValue, +              values = valuesFn(scope) || [],                keys = keyName ? sortedKeys(values) : values,                groupLength, length,                groupIndex, index, @@ -313,20 +309,20 @@ var selectDirective = ['$formFactory', '$compile', '$parse',            // We now build up the list of options we need (we merge later)            for (index = 0; length = keys.length, index < length; index++) {                 locals[valueName] = values[keyName ? locals[keyName]=keys[index]:index]; -               optionGroupName = groupByFn(modelScope, locals) || ''; +               optionGroupName = groupByFn(scope, locals) || '';              if (!(optionGroup = optionGroups[optionGroupName])) {                optionGroup = optionGroups[optionGroupName] = [];                optionGroupNames.push(optionGroupName);              }              if (multiple) { -              selected = selectedSet.remove(valueFn(modelScope, locals)) != undefined; +              selected = selectedSet.remove(valueFn(scope, locals)) != undefined;              } else { -              selected = modelValue === valueFn(modelScope, locals); +              selected = modelValue === valueFn(scope, locals);                selectedSet = selectedSet || selected; // see if at least one item is selected              }              optionGroup.push({                id: keyName ? keys[index] : index,   // either the index into array or key from object -              label: displayFn(modelScope, locals) || '', // what will be seen by the user +              label: displayFn(scope, locals) || '', // what will be seen by the user                selected: selected                   // determine if we should be selected              });            } | 
