diff options
| author | Vojta Jina | 2012-03-12 01:25:05 -0700 | 
|---|---|---|
| committer | Vojta Jina | 2012-03-12 01:40:12 -0700 | 
| commit | 317adb36a480c60f41b6f69bc67d66fe1b08bdae (patch) | |
| tree | 21145885146ed16e5c32aca5f78483af462a7d9c /docs/content/guide | |
| parent | 1b9277bf6f16f714bba418dd5a7bf719206fe4d6 (diff) | |
| download | angular.js-317adb36a480c60f41b6f69bc67d66fe1b08bdae.tar.bz2 | |
docs(guide.forms): Update forms guide
Diffstat (limited to 'docs/content/guide')
| -rw-r--r-- | docs/content/guide/dev_guide.forms.ngdoc | 836 | 
1 files changed, 290 insertions, 546 deletions
diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc index cbb73abc..c79b9683 100644 --- a/docs/content/guide/dev_guide.forms.ngdoc +++ b/docs/content/guide/dev_guide.forms.ngdoc @@ -2,592 +2,336 @@  @name Developer Guide: Forms  @description -# Overview +Forms and form controls (`input`, `select`, `textarea`) are user's gateway to your application - +that's how your application accepts input from the user. -Forms allow users to enter data into your application. Forms represent the bidirectional data -bindings in Angular. +In order to provide good user experience while gathering user input, it is important to validate +this input and give the user hints on how to correct errors. Angular provides several mechanisms +that make this easier, but keep in mind that while client-side validation plays an important role in +providing good user experience, it can be easily circumvented and thus a server-side validation is +still necessary. -Forms consist of all of the following: -  - the individual widgets with which users interact -  - the validation rules for widgets -  - the form, a collection of widgets that contains aggregated validation information +# Simple form +The most important directive is {@link api/angular.module.ng.$compileProvider.directive.ng:model ng-model}, +which tells Angular to do two-way data binding. That means, the value in the form control is +synchronized in both directions with the bound model (specified as value of `ng-model` attribute). -# Form +<doc:example> +<doc:source> +<div ng-controller="Controller"> +  <form novalidate class="simple-form"> +    Name: <input type="text" ng-model="user.name" ng-model-instant /><br /> +    E-mail: <input type="email" ng-model="user.email" /><br /> +    Gender: <input type="radio" ng-model="user.gender" value="male" />male +    <input type="radio" ng-model="user.gender" value="female" />female<br /> +    <button ng-click="reset()">RESET</button> +    <button ng-click="update(user)">SAVE</button> +  </form> +  <!-- reading these values outside <form> scope is possible only because we defined these objects +  on the parent scope, and ng-model only change properties of this object --> +  <pre>form = {{user | json}}</pre> +  <pre>master = {{master | json}}</pre> +</div> + +<script type="text/javascript"> +  function Controller($scope) { +    $scope.master= {}; + +    $scope.update = function(user) { +      $scope.master= angular.copy(user); +    }; + +    $scope.reset = function() { +      $scope.user = angular.copy($scope.master); +    }; + +    $scope.reset(); +  } +  </script> +</doc:source> +</doc:example> -A form groups a set of widgets together into a single logical data-set. A form is created using -the {@link api/angular.module.ng.$compileProvider.directive.form <form>} element that calls the -{@link api/angular.module.ng.$formFactory $formFactory} service. The form is responsible for managing -the widgets and for tracking validation information. -A form is: +Note, that the `user.name` is updated immediately - that's because of +{@link api/angular.module.ng.$compileProvide.directive.ng:model-instant ng-model-instant}. -- The collection which contains widgets or other forms. -- Responsible for marshaling data from the model into a widget. This is -  triggered by {@link api/angular.module.ng.$rootScope.Scope#$watch $watch} of the model expression. -- Responsible for marshaling data from the widget into the model. This is -  triggered by the widget emitting the `$viewChange` event. -- Responsible for updating the validation state of the widget, when the widget emits -  `$valid` / `$invalid` event. The validation state is useful for controlling the validation -   errors shown to the user in it consist of: +Note, that we use `novalidate` to disable browser's native form validation. -  - `$valid` / `$invalid`: Complementary set of booleans which show if a widget is valid / invalid. -  - `$error`: an object which has a property for each validation key emited by the widget. -              The value of the key is always true. If widget is valid, then the `$error` -              object has no properties. For example if the widget emits -              `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be -              updated to `$error.REQUIRED == true`. -- Responsible for aggregating widget validation information into the form. +## Scoping issues -  - `$valid` / `$invalid`: Complementary set of booleans which show if all the child widgets -              (or forms) are valid or if any are invalid. -  - `$error`: an object which has a property for each validation key emited by the -              child widget. The value of the key is an array of widgets which fired the invalid -              event. If all child widgets are valid then, then the `$error` object has no -              properties. For example if a child widget emits -              `$invalid` event with `REQUIRED` key. The internal state of the `$error` would be -              updated to `$error.REQUIRED == [ widgetWhichEmitedInvalid ]`. +Angular sets the model value onto current scope. However it can be confusing where are the scope +borders - in other words, which directives create new scope. +It's crucial to understand how prototypical inheritance works as well as +{@link dev_guide.scopes.internals Angular's scopes}. +In this example, there are actually two directives, that create new scope (`ng-controller` and `form`). +Angular sets the value onto the current scope, so the first input sets value to `scope.user.name`, +where `scope` is the scope on `form` element. Therefore you would not be able to read the value +outside the `form`, because that's a parent scope. That's why we defined the `$scope.user` object +on the parent scope (on `div` element), because `ng-model` access this object through prototypical +inheritance and bind to this object (defined on the parent scope) and we can access it even on +parent scope. -# Widgets -In Angular, a widget is the term used for the UI with which the user input. Examples of -bult-in Angular widgets are {@link api/angular.module.ng.$compileProvider.directive.input input} and -{@link api/angular.module.ng.$compileProvider.directive.select select}. Widgets provide the rendering and the user -interaction logic. Widgets should be declared inside a form, if no form is provided an implicit -form {@link api/angular.module.ng.$formFactory $formFactory.rootForm} form is used. -Widgets are implemented as Angular controllers. A widget controller: +# Using CSS classes +Angular puts some basic css classes onto the form element as well as individual form control +elements, to allow you to style them differently, depending on their state. These css classes are: --  implements methods: +- `ng-valid` +- `ng-invalid` +- `ng-pristine` +- `ng-dirty` -  - `$render` - Updates the DOM from the internal state as represented by `$viewValue`. -  - `$parseView` - Translate `$viewValue` to `$modelValue`. (`$modelValue` will be assigned to -     the model scope by the form) -  - `$parseModel` - Translate `$modelValue` to `$viewValue`. (`$viewValue` will be assigned to -     the DOM inside the `$render` method) +Here is the same example with some very basic css, displaying validity of each form control. +Both `user.name` and `user.email` are required, but we display the red background only when they +are dirty, which means the user has already interacted with them. -- responds to events: +<doc:example> +<doc:source> +<div ng-controller="Controller"> +  <form novalidate class="css-form"> +    Name: <input type="text" ng-model="user.name" ng-model-instant required /><br /> +    E-mail: <input type="email" ng-model="user.email" required /><br /> +    Gender: <input type="radio" ng-model="user.gender" value="male" />male +    <input type="radio" ng-model="user.gender" value="female" />female<br /> +    <button ng-click="reset()">RESET</button> +    <button ng-click="update(user)">SAVE</button> +  </form> +</div> + +<style type="text/css"> +  .css-form input.ng-invalid.ng-dirty { +    background-color: #FA787E; +  } + +  .css-form input.ng-valid.ng-dirty { +    background-color: #78FA89; +  } +</style> + +<script type="text/javascript"> +  function Controller($scope) { +    $scope.master= {}; + +    $scope.update = function(user) { +      $scope.master= angular.copy(user); +    }; + +    $scope.reset = function() { +      $scope.user = angular.copy($scope.master); +    }; + +    $scope.reset(); +  } +  </script> +</doc:source> +</doc:example> -  - `$validate` - Emitted by the form when the form determines that the widget needs to validate -    itself. There may be more then one listener on the `$validate` event. The widget responds -    by emitting `$valid` / `$invalid` event of its own. -- emits events: -  - `$viewChange` - Emitted when the user interacts with the widget and it is necessary to update -     the model. -  - `$valid` - Emitted when the widget determines that it is valid (usually as a response to -    `$validate` event or inside `$parseView()` or `$parseModel()` method). -  - `$invalid` - Emitted when the widget determines that it is invalid (usually as a response to -    `$validate` event or inside `$parseView()` or `$parseModel()` method). -  - `$destroy` - Emitted when the widget element is removed from the DOM. +# Binding to form / form control state +Each form has an object, that keeps the state of the whole form. This object is an instance of +{@link api/angular.module.ng.$compileProvide.directive.form.FormController FormController}. +In a similar way, each form control with `ng-model` directive has an object, that keeps the state of +the form control. This object is an instance of +{@link api/angular.module.ng.$compileProvide.directive.form.NgModelController NgModelController}. -# CSS +The css classes used in the previous example are nothing else than just a reflection of these objects. +But using css classes is not flexible enough - we need to do more. So this example shows, how to +access these state objects and how to bind to them. -Angular-defined widgets and forms set `ng-valid` and `ng-invalid` classes on themselves to allow -the web-designer a way to style them. If you write your own widgets, then their `$render()` -methods must set the appropriate CSS classes to allow styling. -(See {@link dev_guide.templates.css-styling CSS}) +Note, we added `name` attribute to the form element as well as to the form controls, so that we have access +these objects. When a form has `name` attribute, its `FormController` is published onto the scope. +In a similar way, if a form control has `name` attribute, a reference to its `NgModelController` is +stored on the `FormController`. +**Some changes to notice:** -# Example +- RESET button is enabled only if form has some changes +- SAVE button is enabled only if form has some changes and is valid +- custom error messages for `user.email` and `user.agree` -The following example demonstrates: +<doc:example> +<doc:source> +<div ng-controller="Controller"> +  <form name="form" class="css-form" novalidate> +    Name: <input type="text" ng-model="user.name" name="userName" required /><br /> +    E-mail: <input type="email" ng-model="user.email" name="userEmail" required/><br /> +    <span ng-show="form.userEmail.dirty && form.userEmail.invalid">Invalid: +      <span ng-show="form.userEmail.error.REQUIRED">Please tell us your email.</span> +      <span ng-show="form.userEmail.error.EMAIL">This is not a valid email.</span><br /> +    </span> + +    Gender: <input type="radio" ng-model="user.gender" value="male" />male +    <input type="radio" ng-model="user.gender" value="female" />female<br /> + +    <input type="checkbox" ng-model="user.agree" name="userAgree" required />I agree: +    <input ng-show="user.agree" type="text" ng-model="user.agreeSign" ng-model-instant required /><br /> +    <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div> + +    <button ng-click="reset()" disabled="{{isUnchanged(user)}}">RESET</button> +    <button ng-click="update(user)" disabled="{{form.invalid || isUnchanged(user)}}">SAVE</button> +  </form> +</div> + +<script type="text/javascript"> +  function Controller($scope) { +    $scope.master= {}; + +    $scope.update = function(user) { +      $scope.master= angular.copy(user); +    }; + +    $scope.reset = function() { +      $scope.user = angular.copy($scope.master); +    }; + +    $scope.isUnchanged = function(user) { +      return angular.equals(user, $scope.master); +    }; + +    $scope.reset(); +  } +</script> +</doc:source> +</doc:example> -  - How an error is displayed when a required field is empty. -  - Error highlighting. -  - How form submission is disabled when the form is invalid. -  - The internal state of the widget and form in the the 'Debug View' area. -<doc:example> +# Advanced  / custom validation + +Angular provides basic implementation for most common html5 {@link api/angular.module.ng.$compileProvider.directive.input input} +types ({@link api/angular.module.ng.$compileProvider.directive.input.text text}, {@link api/angular.module.ng.$compileProvider.directive.input.number number}, {@link api/angular.module.ng.$compileProvider.directive.input.url url}, {@link api/angular.module.ng.$compileProvider.directive.input.email email}, {@link api/angular.module.ng.$compileProvider.directive.input.radio radio}, {@link api/angular.module.ng.$compileProvider.directive.input.checkbox checkbox}), as well as some directives for validation (`required`, `pattern`, `minlength`, `maxlength`, `min`, `max`). + +However, when this is not enough for your application, you can simply define a custom directive. +This directive can require `ngModel`, which means it can't exist without `ng-model` and its linking +function gets fourth argument - an instance of `NgModelController`, which is a communication channel +to `ng-model`, that allows you to hook into the validation process. + +## Model to View update +Whenever the bound model changes, all functions in {@link api/angular.module.ng.$compileProvider.directive.ng:model.NgModelController#formatters NgModelController#formatters} array are pipe-lined, so that each of these functions has an opportunity to format the value and change validity state of the form control through {@link api/angualar.module.ng.$compileProvider.directive.ng:model.NgModelController#setValidity NgModelController#setValidity}. + +## View to Model update +In a similar way, whenever a form control calls {@link api/angular.module.ng.$compileProvider.directive.ng:model.NgModelController#setViewValue NgModelController#setViewValue}, all functions in {@link api/angular.module.ng.$compileProvider.directive.ng:model.NgModelController#parsers NgModelController#parsers} array are pipe-lined, so that each of these functions has an opportunity to correct/convert the value and change validity state of the form control through {@link api/angualar.module.ng.$compileProvider.directive.ng:model.NgModelController#setValidity NgModelController#setValidity}. + +In this example we create two simple directives. The first one is `integer` and it validates whether the input is valid integer, so for example `1.23` is an invalid value. Note, that we unshift the array instead of pushing - that's because we want to get a string value, so we need to execute the validation function before a conversion to number happens. + +The second directive is `smart-float`. It parses both `1.2` and `1,2` into a valid float number `1.2`. Note, we can't use input type `number` here - browser would not allow user to type invalid number such as `1,2`. + + +<doc:example module="form-example1">  <doc:source> -   <style> -     .ng-invalid { border: solid 1px red; } -     .ng-form {display: block;} -   </style> -   <script> -   function UserFormCntl($scope) { -     $scope.state = /^\w\w$/; -     $scope.zip = /^\d\d\d\d\d$/; -     $scope.master = { -       customer: 'John Smith', -       address:{ -         line1: '123 Main St.', -         city:'Anytown', -         state:'AA', -         zip:'12345' -       } -     }; - -     $scope.cancel = function() { -       $scope.form = angular.copy($scope.master); -     }; - -     $scope.save = function() { -       $scope.master = $scope.form; -       $scope.cancel(); -     }; - -     $scope.isCancelDisabled = function() { -       return angular.equals($scope.master, $scope.form); -     }; - -     $scope.isSaveDisabled = function() { -       return $scope.userForm.invalid || angular.equals($scope.master, $scope.form); -     }; - -     $scope.cancel(); -   } -   </script> -   <div ng:controller="UserFormCntl"> - -     <form name="userForm"> - -       <label>Name:</label><br/> -       <input type="text" name="customer" ng:model="form.customer" required/> -       <span class="error" ng:show="userForm.customer.error.REQUIRED"> -         Customer name is required!</span> -       <br/><br/> - -       <ng:form name="addressForm"> -         <label>Address:</label> <br/> -         <input type="text" name="line1" size="33" required -                ng:model="form.address.line1"/> <br/> -         <input type="text" name="city" size="12" required -                ng:model="form.address.city"/>, -         <input type="text" name="state" ng:pattern="state" size="2" required -                ng:model="form.address.state"/> -         <input type="text" name="zip" ng:pattern="zip" size="5" required -                ng:model="form.address.zip"/><br/><br/> - -         <span class="error" ng:show="addressForm.invalid"> -           Incomplete address: -           <span class="error" ng:show="addressForm.state.error.REQUIRED"> -             Missing state!</span> -           <span class="error" ng:show="addressForm.state.error.PATTERN"> -             Invalid state!</span> -           <span class="error" ng:show="addressForm.zip.error.REQUIRED"> -             Missing zip!</span> -           <span class="error" ng:show="addressForm.zip.error.PATTERN"> -             Invalid zip!</span> -         </span> -       </ng:form> - -       <button ng:click="cancel()" -               ng:disabled="{{isCancelDisabled()}}">Cancel</button> -       <button ng:click="save()" -               ng:disabled="{{isSaveDisabled()}}">Save</button> -     </form> - -     <hr/> -     Debug View: -     <pre>form={{form|json}}</pre> -     <pre>master={{master|json}}</pre> -     <pre>userForm={{userForm|json}}</pre> -     <pre>addressForm={{addressForm|json}}</pre> -   </div> -</doc:source> -<doc:scenario> -  it('should enable save button', function() { -    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); -    input('form.customer').enter(''); -    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); -    input('form.customer').enter('change'); -    expect(element(':button:contains(Save)').attr('disabled')).toBeFalsy(); -    element(':button:contains(Save)').click(); -    expect(element(':button:contains(Save)').attr('disabled')).toBeTruthy(); +<div ng-controller="Controller"> +  <form name="form" class="css-form" novalidate> +    <div> +      Size (integer 0 - 10): <input type="number" ng-model="size" name="size" min="0" max="10" integer />{{size}}<br /> +      <span ng-show="form.size.error.INTEGER">This is not valid integer!</span> +      <span ng-show="form.size.error.MIN || form.size.error.MAX">The value must be in range 0 to 10!</span> +    </div> + +    <div> +      Length (float): <input type="text" ng-model="length" name="length" smart-float />{{length}}<br /> +      <span ng-show="form.length.error.FLOAT">This is not valid number!</span> +    </div> +  </form> +</div> + +<script type="text/javascript"> +  var app = angular.module('form-example1', []); + +  var INTEGER_REGEXP = /^\-?\d*$/; +  app.directive('integer', function() { +    return { +      require: 'ngModel', +      link: function(scope, elm, attrs, ctrl) { +        ctrl.parsers.unshift(function(viewValue) { +          if (INTEGER_REGEXP.test(viewValue)) { +            // it is valid +            ctrl.setValidity('INTEGER', true); +            return viewValue; +          } else { +            // it is invalid, return undefined (no model update) +            ctrl.setValidity('INTEGER', false); +            return undefined; +          } +        }); +      } +    };    }); -  it('should enable cancel button', function() { -    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy(); -    input('form.customer').enter('change'); -    expect(element(':button:contains(Cancel)').attr('disabled')).toBeFalsy(); -    element(':button:contains(Cancel)').click(); -    expect(element(':button:contains(Cancel)').attr('disabled')).toBeTruthy(); -    expect(element(':input[ng\\:model="form.customer"]').val()).toEqual('John Smith'); + +  var FLOAT_REGEXP = /^\-?\d+((\.|\,)\d+)?$/; +  app.directive('smartFloat', function() { +    return { +      require: 'ngModel', +      link: function(scope, elm, attrs, ctrl) { +        ctrl.parsers.unshift(function(viewValue) { +          if (FLOAT_REGEXP.test(viewValue)) { +            ctrl.setValidity('FLOAT', true); +            return parseFloat(viewValue.replace(',', '.')); +          } else { +            ctrl.setValidity('FLOAT', false); +            return undefined; +          } +        }); +      } +    };    }); -</doc:scenario> +</script> +</doc:source>  </doc:example> -# Life-cycle - -- The `<form>` element triggers creation of a new form {@link dev_guide.scopes scope} using the -  {@link api/angular.module.ng.$formFactory $formfactory}. The new form scope is added to the -  `<form>` element using the jQuery `.data()` method for later retrieval under the key `$form`. -  The form also sets up these listeners: - -  - `$destroy` - This event is emitted by nested widget when it is removed from the view. It gives -     the form a chance to clean up any validation references to the destroyed widget. -  - `$valid` / `$invalid` - This event is emitted by the widget on validation state change. - -- `<input>` element triggers the creation of the widget using the -  {@link api/angular.module.ng.$formFactory $formfactory.$createWidget()} method. The `$createWidget()` -  creates new widget instance by calling the current scope {@link api/angular.module.ng.$rootScope.Scope#$new .$new()} and -  registers these listeners: - -  - `$watch` on the model scope. -  - `$viewChange` event on the widget scope. -  - `$validate` event on the widget scope. -  - Element `change` event when the user enters data. - -<img class="center" src="img/form_data_flow.png" border="1" /> - - -- When the user interacts with the widget: - -  1. The DOM element fires the `change` event which the widget intercepts. Widget then emits -     a `$viewChange` event which includes the new user-entered value. (Remember that the DOM events -     are outside of the Angular environment so the widget must emit its event within the -     {@link api/angular.module.ng.$rootScope.Scope#$apply $apply} method). -  2. The form's `$viewChange` listener copies the user-entered value to the widget's `$viewValue` -     property. Since the `$viewValue` is the raw value as entered by user, it may need to be -     translated to a different format/type (for example, translating a string to a number). -     If you need your widget to translate between the internal `$viewValue` and the external -     `$modelValue` state, you must declare a `$parseView()` method. The `$parseView()` method -     will copy `$viewValue` to `$modelValue` and perform any necessary translations. -  3. The `$modelValue` is written into the application model. -  4. The form then emits a `$validate` event, giving the widget's validators chance to validate the -     input. There can be any number of validators registered. Each validator may in turn -     emit a `$valid` / `$invalid` event with the validator's validation key. For example `REQUIRED`. -  5. Form listens to `$valid`/`$invalid` events and updates both the form as well as the widget -     scope with the validation state. The validation updates the `$valid` and `$invalid`, property -     as well as `$error` object. The widget's `$error` object is updated with the validation key -     such that `$error.REQUIRED == true` when the validation emits `$invalid` with `REQUIRED` -     validation key. Similarly the form's `$error` object gets updated, but instead of boolean -     `true` it contains an array of invalid widgets (widgets which fired `$invalid` event with -     `REQUIRED` validation key). - -- When the model is updated: - -  1. The model `$watch` listener assigns the model value to `$modelValue` on the widget. -  2. The form then calls `$parseModel` method on widget if present. The method converts the -     value to renderable format and assigns it to `$viewValue` (for example converting number to a -     string.) -  3. The form then emits a `$validate` which behaves as described above. -  4. The form then calls `$render` method on the widget to update the DOM structure from the -     `$viewValue`. - - - -# Writing Your Own Widget - -This example shows how to implement a custom HTML editor widget in Angular. - -    <doc:example module="formModule"> -      <doc:source> -        <script> -          function EditorCntl($scope) { -            $scope.htmlContent = '<b>Hello</b> <i>World</i>!'; -          } -          angular.module('formModule', []).directive('ngHtmlEditor', function ($sanitize) { -            return { -              require: 'ngModel', -              link: function(scope, elm, attr, ctrl) { -                attr.$set('contentEditable', true); - -                ctrl.$render = function() { -                  elm.html(ctrl.viewValue); -                }; - -                ctrl.formatters.push(function(value) { -                  try { -                    value = $sanitize(value || ''); -                    ctrl.setValidity('HTML', true); -                  } catch (e) { -                    ctrl.setValidity('HTML', false); -                  } - -                }); - -                elm.bind('keyup', function() { -                  scope.$apply(function() { -                    ctrl.read(elm.html()); -                  }); -                }); - -              } -            }; -          }); -        </script> -        <form name='editorForm' ng:controller="EditorCntl"> -          <div ng:html-editor ng: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> +# Implementing custom form control (using ng-model) +Angular has all the basic form controls implemented ({@link api/angular.module.ng.$compileProvider.directive.input input}, {@link api/angular.module.ng.$compileProvider.directive.select select}, {@link api/angular.module.ng.$compileProvider.directive.textarea textarea}), so most of the time you should be just fine with them. However, if you need more flexibility, you can write your own form control - it's gonna be a directive again. +You basically need to do two things to get it working together with `ng-model` binding: +- implement `render` method, that knows how to reflect value change to view, +- call `setViewValue` method, whenever the view value changes - that's usually inside DOM Event listener. -# HTML Inputs +See {@link api/angular.module.ng.$compileProvider.directive $compileProvider.directive} for more info. -The most common widgets you will use will be in the form of the -standard HTML set. These widgets are bound using the `name` attribute -to an expression. In addition, they can have `required` attribute to further control their -validation. -<doc:example> -  <doc:source> -     <script> -       function Ctrl($scope) { -         $scope.input1 = ''; -         $scope.input2 = ''; -         $scope.input3 = 'A'; -         $scope.input4 = false; -         $scope.input5 = 'c'; -         $scope.input6 = []; -       } -     </script> -    <table style="font-size:.9em;" ng:controller="Ctrl"> -      <tr> -        <th>Name</th> -        <th>Format</th> -        <th>HTML</th> -        <th>UI</th> -        <th ng:non-bindable>{{input#}}</th> -      </tr> -      <tr> -        <th>text</th> -        <td>String</td> -        <td><tt><input type="text" ng:model="input1"></tt></td> -        <td><input type="text" ng:model="input1" size="4"></td> -        <td><tt>{{input1|json}}</tt></td> -      </tr> -      <tr> -        <th>textarea</th> -        <td>String</td> -        <td><tt><textarea ng:model="input2"></textarea></tt></td> -        <td><textarea ng:model="input2" cols='6'></textarea></td> -        <td><tt>{{input2|json}}</tt></td> -      </tr> -      <tr> -        <th>radio</th> -        <td>String</td> -        <td><tt> -          <input type="radio" ng:model="input3" value="A"><br> -          <input type="radio" ng:model="input3" value="B"> -        </tt></td> -        <td> -          <input type="radio" ng:model="input3" value="A"> -          <input type="radio" ng:model="input3" value="B"> -        </td> -        <td><tt>{{input3|json}}</tt></td> -      </tr> -      <tr> -        <th>checkbox</th> -        <td>Boolean</td> -        <td><tt><input type="checkbox" ng:model="input4"></tt></td> -        <td><input type="checkbox" ng:model="input4"></td> -        <td><tt>{{input4|json}}</tt></td> -      </tr> -      <tr> -        <th>pulldown</th> -        <td>String</td> -        <td><tt> -          <select ng:model="input5"><br> -            <option value="c">C</option><br> -            <option value="d">D</option><br> -          </select><br> -        </tt></td> -        <td> -          <select ng:model="input5"> -            <option value="c">C</option> -            <option value="d">D</option> -          </select> -        </td> -        <td><tt>{{input5|json}}</tt></td> -      </tr> -      <tr> -        <th>multiselect</th> -        <td>Array</td> -        <td><tt> -          <select ng:model="input6" multiple size="4"><br> -            <option value="e">E</option><br> -            <option value="f">F</option><br> -          </select><br> -        </tt></td> -        <td> -          <select ng:model="input6" multiple size="4"> -            <option value="e">E</option> -            <option value="f">F</option> -          </select> -        </td> -        <td><tt>{{input6|json}}</tt></td> -      </tr> -    </table> -  </doc:source> -  <doc:scenario> - -    it('should exercise text', function() { -     input('input1').enter('Carlos'); -     expect(binding('input1')).toEqual('"Carlos"'); -    }); -    it('should exercise textarea', function() { -     input('input2').enter('Carlos'); -     expect(binding('input2')).toEqual('"Carlos"'); -    }); -    it('should exercise radio', function() { -     expect(binding('input3')).toEqual('"A"'); -     input('input3').select('B'); -     expect(binding('input3')).toEqual('"B"'); -     input('input3').select('A'); -     expect(binding('input3')).toEqual('"A"'); -    }); -    it('should exercise checkbox', function() { -     expect(binding('input4')).toEqual('false'); -     input('input4').check(); -     expect(binding('input4')).toEqual('true'); -    }); -    it('should exercise pulldown', function() { -     expect(binding('input5')).toEqual('"c"'); -     select('input5').option('d'); -     expect(binding('input5')).toEqual('"d"'); -    }); -    it('should exercise multiselect', function() { -     expect(binding('input6')).toEqual('[]'); -     select('input6').options('e'); -     expect(binding('input6')).toEqual('["e"]'); -     select('input6').options('e', 'f'); -     expect(binding('input6')).toEqual('["e","f"]'); -    }); -  </doc:scenario> -</doc:example> +This example shows how easy it is to add a support for binding contentEditable elements. + +<doc:example module="form-example2"> +<doc:source> +<script type="text/javascript"> +  angular.module('form-example2', []).directive('contenteditable', function() { +    return { +      require: 'ngModel', +      link: function(scope, elm, attrs, ctrl) { +        // view -> model +        elm.bind('blur', function() { +          scope.$apply(function() { +            ctrl.setViewValue(elm.html()); +          }); +        }); + +        // model -> view +        ctrl.render = function(value) { +          elm.html(value); +        }; -#Testing - -When unit-testing a controller it may be desirable to have a reference to form and to simulate -different form validation states. - -This example demonstrates a login form, where the login button is enabled only when the form is -properly filled out. -<pre> -  <div ng:controller="LoginController"> -    <form name="loginForm"> -      <input type="text" ng:model="username" required/> -      <input type="password" ng:model="password" required/> -      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login> -    </form> -  </div> -</pre> - -In the unit tests we do not have access to the DOM, and therefore the `loginForm` reference does -not get set on the controller. This example shows how it can be unit-tested, by creating a mock -form. -<pre> -function LoginController() { -  this.disableLogin = function() { -    return this.loginForm.$invalid; -  }; -} - -describe('LoginController', function() { -  it('should disable login button when form is invalid', inject(function($rootScope) { -    var loginController = $rootScope.$new(LoginController); - -    // In production the 'loginForm' form instance gets set from the view, -    // but in unit-test we have to set it manually. -    loginController.loginForm = scope.$service('$formFactory')(); - -    expect(loginController.disableLogin()).toBe(false); - -    // Now simulate an invalid form -    loginController.loginForm.$emit('$invalid', 'MyReason'); -    expect(loginController.disableLogin()).toBe(true); - -    // Now simulate a valid form -    loginController.loginForm.$emit('$valid', 'MyReason'); -    expect(loginController.disableLogin()).toBe(false); -  })); -}); -</pre> - -## Custom widgets - -This example demonstrates a login form, where the password has custom validation rules. -<pre> -  <div ng:controller="LoginController"> -    <form name="loginForm"> -      <input type="text" ng:model="username" required/> -      <input type="@StrongPassword" ng:model="password" required/> -      <button ng:disabled="{{!disableLogin()}}" ng:click="login()">Login</login> -    </form> -  </div> -</pre> - -In the unit tests we do not have access to the DOM, and therefore the `loginForm` and custom -input type reference does not get set on the controller. This example shows how it can be -unit-tested, by creating a mock form and a mock custom input type. -<pre> -function LoginController(){ -  this.disableLogin = function() { -    return this.loginForm.$invalid; -  }; - -  this.StrongPassword = function(element) { -    var widget = this; -    element.attr('type', 'password'); // act as password. -    this.$on('$validate', function(){ -      widget.$emit(widget.$viewValue.length > 5 ? '$valid' : '$invalid', 'PASSWORD'); -    }); -  }; -} - -describe('LoginController', function() { -  it('should disable login button when form is invalid', inject(function($rootScope) { -    var loginController = $rootScope.$new(LoginController); -    var input = angular.element('<input>'); - -    // In production the 'loginForm' form instance gets set from the view, -    // but in unit-test we have to set it manually. -    loginController.loginForm = scope.$service('$formFactory')(); - -    // now instantiate a custom input type -    loginController.loginForm.$createWidget({ -      scope: loginController, -      model: 'password', -      alias: 'password', -      controller: loginController.StrongPassword, -      controllerArgs: [input] -    }); - -    // Verify that the custom password input type sets the input type to password -    expect(input.attr('type')).toEqual('password'); - -    expect(loginController.disableLogin()).toBe(false); - -    // Now simulate an invalid form -    loginController.loginForm.password.$emit('$invalid', 'PASSWORD'); -    expect(loginController.disableLogin()).toBe(true); - -    // Now simulate a valid form -    loginController.loginForm.password.$emit('$valid', 'PASSWORD'); -    expect(loginController.disableLogin()).toBe(false); - -    // Changing model state, should also influence the form validity -    loginController.password = 'abc'; // too short so it should be invalid -    scope.$digest(); -    expect(loginController.loginForm.password.$invalid).toBe(true); - -    // Changeing model state, should also influence the form validity -    loginController.password = 'abcdef'; // should be valid -    scope.$digest(); -    expect(loginController.loginForm.password.$valid).toBe(true); -  })); -}); -</pre> +        // load init value from DOM +        ctrl.setViewValue(elm.html()); +      } +    }; +  }); +</script> +<div contentEditable="true" ng-model="content" title="Click to edit">Some</div> +<pre>model = {{content}}</pre> +<style type="text/css"> +  div[contentEditable] { +    cursor: pointer; +    background-color: #D0D0D0; +  } +</style> +</doc:source> +</doc:example>  | 
