From 317adb36a480c60f41b6f69bc67d66fe1b08bdae Mon Sep 17 00:00:00 2001 From: Vojta Jina Date: Mon, 12 Mar 2012 01:25:05 -0700 Subject: [PATCH] docs(guide.forms): Update forms guide --- docs/content/guide/dev_guide.forms.ngdoc | 858 ++++++++--------------- docs/src/templates/docs.css | 4 - 2 files changed, 301 insertions(+), 561 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. - -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 +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. -# Form - -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: - -- 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: - - - `$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. - - - `$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 ]`. - - -# 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: - -- implements methods: - - - `$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) - -- responds to events: - - - `$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. - - -# CSS - -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}) - - -# Example - -The following example demonstrates: - - - 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. +# 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). - - -
- -
- -
- - - Customer name is required! -

- - -
-
- , - -

- - - Incomplete address: - - Missing state! - - Invalid state! - - Missing zip! - - Invalid zip! - -
- - - -
- -
- Debug View: -
form={{form|json}}
-
master={{master|json}}
-
userForm={{userForm|json}}
-
addressForm={{addressForm|json}}
-
+ $scope.reset(); + } +
- - 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(); - }); - 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'); - }); -
-# Life-cycle -- The `
` 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 - `` element using the jQuery `.data()` method for later retrieval under the key `$form`. - The form also sets up these listeners: +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}. - - `$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. - -- `` 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. - - +Note, that we use `novalidate` to disable browser's native form validation. -- When the user interacts with the widget: +## Scoping issues - 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). +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}. -- 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`. +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. -# Writing Your Own Widget +# 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: -This example shows how to implement a custom HTML editor widget in Angular. +- `ng-valid` +- `ng-invalid` +- `ng-pristine` +- `ng-dirty` - - - - -
-
- HTML:
- -
-
editorForm = {{editorForm|json}}
- -
- - 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/); - }); - -
- - - -# HTML Inputs - -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. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameFormatHTMLUI{{input#}}
textString<input type="text" ng:model="input1">{{input1|json}}
textareaString<textarea ng:model="input2"></textarea>{{input2|json}}
radioString - <input type="radio" ng:model="input3" value="A">
- <input type="radio" ng:model="input3" value="B"> -
- - - {{input3|json}}
checkboxBoolean<input type="checkbox" ng:model="input4">{{input4|json}}
pulldownString - <select ng:model="input5">
-   <option value="c">C</option>
-   <option value="d">D</option>
- </select>
-
- - {{input5|json}}
multiselectArray - <select ng:model="input6" multiple size="4">
-   <option value="e">E</option>
-   <option value="f">F</option>
- </select>
-
- - {{input6|json}}
-
- + +
+
+ Name:
+ E-mail:
+ Gender: male + female
+ + +
+
- 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"]'); - }); -
+ + + +
-#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. -
-  
-
- - -
-
- -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. -
-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);
-  }));
-});
-
- -## Custom widgets - -This example demonstrates a login form, where the password has custom validation rules. -
-  
-
- - -
-
- -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. -
-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('');
-
-    // 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);
-  }));
-});
-
+# 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}. + +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. + +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:** + +- 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` + + + +
+
+ Name:
+ E-mail:
+ Invalid: + Please tell us your email. + This is not a valid email.
+
+ + Gender: male + female
+ + I agree: +
+
Please agree and sign.
+ + + +
+
+ + +
+
+ + + +# 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`. + + + + +
+
+
+ Size (integer 0 - 10): {{size}}
+ This is not valid integer! + The value must be in range 0 to 10! +
+ +
+ Length (float): {{length}}
+ This is not valid number! +
+
+
+ + +
+
+ + +# 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. + +See {@link api/angular.module.ng.$compileProvider.directive $compileProvider.directive} for more info. + +This example shows how easy it is to add a support for binding contentEditable elements. + + + + + +
Some
+
model = {{content}}
+ + +
+
diff --git a/docs/src/templates/docs.css b/docs/src/templates/docs.css index 08a98f61..ee8eaaef 100644 --- a/docs/src/templates/docs.css +++ b/docs/src/templates/docs.css @@ -49,10 +49,6 @@ li { margin: 0.3em 0 0.3em 0; } -.ng-invalid { - border: 1px solid red; -} - .member { border: 1px solid gray; margin: 2em 0;