From 8024a5742c46a42ef204988ff7362a4fc14b7a2d Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Fri, 25 May 2012 10:29:54 -0700 Subject: doc(NgModelController) add example and $render documentation Closes#930 --- docs/content/guide/dev_guide.forms.ngdoc | 324 ------------------------------- docs/content/guide/forms.ngdoc | 324 +++++++++++++++++++++++++++++++ src/ng/directive/input.js | 99 +++++++++- test/ng/directive/inputSpec.js | 16 +- 4 files changed, 430 insertions(+), 333 deletions(-) delete mode 100644 docs/content/guide/dev_guide.forms.ngdoc create mode 100644 docs/content/guide/forms.ngdoc diff --git a/docs/content/guide/dev_guide.forms.ngdoc b/docs/content/guide/dev_guide.forms.ngdoc deleted file mode 100644 index 6621e231..00000000 --- a/docs/content/guide/dev_guide.forms.ngdoc +++ /dev/null @@ -1,324 +0,0 @@ -@ngdoc overview -@name Developer Guide: Forms -@description - -Controls (`input`, `select`, `textarea`) are a way for user to enter data. -Form is a collection of controls for the purpose of grouping related controls together. - -Form and controls provide validation services, so that the user can be notified of invalid input. -This provides a better user experience, because the user gets instant feedback on how to correct the error. -Keep in mind that while client-side validation plays an important role in providing good user experience, it can easily be circumvented and thus can not be trusted. -Server-side validation is still necessary for a secure application. - - -# Simple form -The key directive in understanding two-way data-binding is {@link api/angular.module.ng.$compileProvider.directive.ngModel ngModel}. -The `ngModel` directive provides the two-way data-binding by synchronizing the model to the view, as well as view to the model. -In addition it provides {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController API} for other directives to augment its behavior. - - - -
-
- Name:
- E-mail:
- Gender: male - female
- - -
-
form = {{user | json}}
-
master = {{master | json}}
-
- - -
-
- - -Note that `novalidate` is used to disable browser's native form validation. - - - -# Using CSS classes - -To allow styling of form as well as controls, `ngModel` add these CSS classes: - -- `ng-valid` -- `ng-invalid` -- `ng-pristine` -- `ng-dirty` - -Following example uses the CSS to display validity of each form control. -In the example both `user.name` and `user.email` are required, but are rendered with red background only when they are dirty. -This ensures that the user is not distracted with an error until after interacting with the control, and failing to satisfy its validity. - - - -
-
- Name: -
- E-mail:
- Gender: male - female
- - -
-
- - - - -
-
- - - -# Binding to form and control state - -A form is in instance of {@link api/angular.module.ng.$compileProvider.directive.form.FormController FormController}. -The form instance can optionally be published into the scope using the `name` attribute. -Similarly control is an instance of {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController NgModelController}. -The control instance can similarly be published into the form instance using the `name` attribute. -This implies that the internal state of both the form and the control is available for binding in the view using the standard binding primitives. - -This allows us to extend the above example with these features: - -- 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: - Tell us your email. - This is not a valid email. -
- - Gender: male - female
- - - I agree:
-
Please agree and sign.
- - - -
-
- - -
-
- - - -# 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`). - -Defining your own validator can be done by defining your own directive which adds a custom validation function to the `ngModel` {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController controller}. -To get a hold of the controller the directive specifies a dependency as shown in the example below. -The validation can occur in two places: - - * **Model to View update** - - Whenever the bound model changes, all functions in {@link api/angular.module.ng.$compileProvider.directive.ngModel.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/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setValidity NgModelController#$setValidity}. - - * **View to Model update** - - In a similar way, whenever a user interacts with a control, the controll calls {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setViewValue NgModelController#$setViewValue}. -This in turn pipelines all functions in {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$parsers NgModelController#$parsers} array, so that each of these functions has an opportunity to convert the value and change validity state of the form control through {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setValidity NgModelController#$setValidity}. - -In the following example we create two directives. - - * The first one is `integer` and it validates whether the input is a valid integer. - For example `1.23` is an invalid value, since it contains a fraction. - Note, that we unshift the array instead of pushing. - This is because we want to be first parser and consume the control string value, as we need to execute the validation function before a conversion to number occurs. - - * The second directive is a `smart-float`. - It parses both `1.2` and `1,2` into a valid float number `1.2`. - Note that, we can't use input type `number` here as HTML5 browsers would not allow the user to type what it would consider an 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 a valid float number! -
-
-
- - -
-
- - -# Implementing custom form control (using `ngModel`) -Angular implements all of the basic HTML form controls ({@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}), which should be sufficient for most cases. -However, if you need more flexibility, you can write your own form control as a directive. - -In order for custom control to work with `ngModel` and to achieve two-way data-binding it needs to: - - - implement `render` method, which is responsible for rendering the data after it passed the {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$formatters NgModelController#$formatters}, - - call `$setViewValue` method, whenever the user interacts with the control and model needs to be updated. This is usually done inside a DOM Event listener. - -See {@link guide/directive $compileProvider.directive} for more info. - -The following example shows how to add two-way data-binding to contentEditable elements. - - - - - -
Some
-
model = {{content}}
- - -
-
diff --git a/docs/content/guide/forms.ngdoc b/docs/content/guide/forms.ngdoc new file mode 100644 index 00000000..c21ffc93 --- /dev/null +++ b/docs/content/guide/forms.ngdoc @@ -0,0 +1,324 @@ +@ngdoc overview +@name Forms +@description + +Controls (`input`, `select`, `textarea`) are a way for user to enter data. +Form is a collection of controls for the purpose of grouping related controls together. + +Form and controls provide validation services, so that the user can be notified of invalid input. +This provides a better user experience, because the user gets instant feedback on how to correct the error. +Keep in mind that while client-side validation plays an important role in providing good user experience, it can easily be circumvented and thus can not be trusted. +Server-side validation is still necessary for a secure application. + + +# Simple form +The key directive in understanding two-way data-binding is {@link api/angular.module.ng.$compileProvider.directive.ngModel ngModel}. +The `ngModel` directive provides the two-way data-binding by synchronizing the model to the view, as well as view to the model. +In addition it provides {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController API} for other directives to augment its behavior. + + + +
+
+ Name:
+ E-mail:
+ Gender: male + female
+ + +
+
form = {{user | json}}
+
master = {{master | json}}
+
+ + +
+
+ + +Note that `novalidate` is used to disable browser's native form validation. + + + +# Using CSS classes + +To allow styling of form as well as controls, `ngModel` add these CSS classes: + +- `ng-valid` +- `ng-invalid` +- `ng-pristine` +- `ng-dirty` + +Following example uses the CSS to display validity of each form control. +In the example both `user.name` and `user.email` are required, but are rendered with red background only when they are dirty. +This ensures that the user is not distracted with an error until after interacting with the control, and failing to satisfy its validity. + + + +
+
+ Name: +
+ E-mail:
+ Gender: male + female
+ + +
+
+ + + + +
+
+ + + +# Binding to form and control state + +A form is in instance of {@link api/angular.module.ng.$compileProvider.directive.form.FormController FormController}. +The form instance can optionally be published into the scope using the `name` attribute. +Similarly control is an instance of {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController NgModelController}. +The control instance can similarly be published into the form instance using the `name` attribute. +This implies that the internal state of both the form and the control is available for binding in the view using the standard binding primitives. + +This allows us to extend the above example with these features: + +- 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: + Tell us your email. + This is not a valid email. +
+ + Gender: male + female
+ + + I agree:
+
Please agree and sign.
+ + + +
+
+ + +
+
+ + + +# 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`). + +Defining your own validator can be done by defining your own directive which adds a custom validation function to the `ngModel` {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController controller}. +To get a hold of the controller the directive specifies a dependency as shown in the example below. +The validation can occur in two places: + + * **Model to View update** - + Whenever the bound model changes, all functions in {@link api/angular.module.ng.$compileProvider.directive.ngModel.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/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setValidity NgModelController#$setValidity}. + + * **View to Model update** - + In a similar way, whenever a user interacts with a control, the controll calls {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setViewValue NgModelController#$setViewValue}. +This in turn pipelines all functions in {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$parsers NgModelController#$parsers} array, so that each of these functions has an opportunity to convert the value and change validity state of the form control through {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$setValidity NgModelController#$setValidity}. + +In the following example we create two directives. + + * The first one is `integer` and it validates whether the input is a valid integer. + For example `1.23` is an invalid value, since it contains a fraction. + Note, that we unshift the array instead of pushing. + This is because we want to be first parser and consume the control string value, as we need to execute the validation function before a conversion to number occurs. + + * The second directive is a `smart-float`. + It parses both `1.2` and `1,2` into a valid float number `1.2`. + Note that, we can't use input type `number` here as HTML5 browsers would not allow the user to type what it would consider an 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 a valid float number! +
+
+
+ + +
+
+ + +# Implementing custom form control (using `ngModel`) +Angular implements all of the basic HTML form controls ({@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}), which should be sufficient for most cases. +However, if you need more flexibility, you can write your own form control as a directive. + +In order for custom control to work with `ngModel` and to achieve two-way data-binding it needs to: + + - implement `render` method, which is responsible for rendering the data after it passed the {@link api/angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$formatters NgModelController#$formatters}, + - call `$setViewValue` method, whenever the user interacts with the control and model needs to be updated. This is usually done inside a DOM Event listener. + +See {@link guide/directive $compileProvider.directive} for more info. + +The following example shows how to add two-way data-binding to contentEditable elements. + + + + + +
Some
+
model = {{content}}
+ + +
+
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 6e394afd..5fe32125 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -628,6 +628,7 @@ function checkboxInputType(scope, element, attr, ctrl) { /** * @ngdoc directive * @name angular.module.ng.$compileProvider.directive.textarea + * @restrict E * * @description * HTML textarea element control with angular data-binding. The data-binding and validation @@ -782,6 +783,79 @@ var VALID_CLASS = 'ng-valid', * * @description * + * `NgModelController` provides API for the `ng-model` directive. The controller contains + * services for data-binding, validation, CSS update, value formatting and parsing. It + * specifically does not contain any logic which deals with DOM rendering or listening to + * DOM events. The `NgModelController` is meant to be extended by other directives where, the + * directive provides DOM manipulation and the `NgModelController` provides the data-binding. + * + * This example shows how to use `NgModelController` with a custom control to achieve + * data-binding. Notice how different directives (`contenteditable`, `ng-model`, and `required`) + * collaborate together to achieve the desired result. + * + * + + [contenteditable] { + border: 1px solid black; + background-color: white; + min-height: 20px; + } + + .ng-invalid { + border: 1px solid red; + } + + + + angular.module('customControl', []). + directive('contenteditable', function() { + return { + restrict: 'A', // only activate on element attribute + require: '?ngModel', // get a hold of NgModelController + link: function(scope, element, attrs, ngModel) { + if(!ngModel) return; // do nothing if no ng-model + + // Specify how UI should be updated + ngModel.$render = function() { + element.html(ngModel.$viewValue || ''); + }; + + // Listen for change events to enable binding + element.bind('blur keyup change', function() { + scope.$apply(read); + }); + read(); // initialize + + // Write data to the model + function read() { + ngModel.$setViewValue(element.html()); + } + } + }; + }); + + +
+
Change me!
+ Required! +
+ +
+
+ + it('should data-bind and become invalid', function() { + var contentEditable = element('[contenteditable]'); + + expect(contentEditable.text()).toEqual('Change me!'); + input('userContent').enter(''); + expect(contentEditable.text()).toEqual(''); + expect(contentEditable.prop('className')).toMatch(/ng-invalid-required/); + }); + + *
+ * */ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$element', function($scope, $exceptionHandler, $attr, ngModel, $element) { @@ -794,9 +868,19 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e this.$dirty = false; this.$valid = true; this.$invalid = false; - this.$render = noop; this.$name = $attr.name; + /** + * @ngdoc function + * @name angular.module.ng.$compileProvider.directive.ngModel.NgModelController#$render + * @methodOf angular.module.ng.$compileProvider.directive.ngModel.NgModelController + * + * @description + * Called when the view needs to be updated. It is expected that the user of the ng-model + * directive will implement this method. + */ + this.$render = noop; + var parentForm = $element.inheritedData('$formController') || nullFormCtrl, invalidCount = 0, // used to easily determine if we are valid $error = this.$error = {}; // keep invalid keys here @@ -958,7 +1042,7 @@ var NgModelController = ['$scope', '$exceptionHandler', '$attrs', 'ngModel', '$e * - {@link angular.module.ng.$compileProvider.directive.textarea textarea} * */ -var ngModelDirective = [function() { +var ngModelDirective = function() { return { inject: { ngModel: 'accessor' @@ -978,7 +1062,7 @@ var ngModelDirective = [function() { }); } }; -}]; +}; /** @@ -1039,11 +1123,12 @@ var ngChangeDirective = valueFn({ }); -var requiredDirective = [function() { +var requiredDirective = function() { return { require: '?ngModel', link: function(scope, elm, attr, ctrl) { if (!ctrl) return; + attr.required = true; // force truthy in case we are on non input element var validator = function(value) { if (attr.required && (isEmpty(value) || value === false)) { @@ -1063,7 +1148,7 @@ var requiredDirective = [function() { }); } }; -}]; +}; /** @@ -1144,7 +1229,7 @@ var ngListDirective = function() { var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; -var ngValueDirective = [function() { +var ngValueDirective = function() { return { priority: 100, compile: function(tpl, tplAttr) { @@ -1162,4 +1247,4 @@ var ngValueDirective = [function() { } } }; -}]; +}; diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 45e3e1bf..d7ca7aea 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -285,8 +285,9 @@ describe('input', function() { var formElm, inputElm, scope, $compile, changeInputValueTo; function compileInput(inputHtml) { - formElm = jqLite('
' + inputHtml + '
'); - inputElm = formElm.find('input'); + inputElm = jqLite(inputHtml); + formElm = jqLite('
'); + formElm.append(inputElm); $compile(formElm)(scope); } @@ -633,6 +634,17 @@ describe('input', function() { expect(inputElm.val()).toBe('0') expect(scope.form.alias.$error.required).toBeFalsy(); }); + + it('should register required on non boolean elements', function() { + compileInput('
'); + + scope.$apply(function() { + scope.value = ''; + }); + + expect(inputElm).toBeInvalid(); + expect(scope.form.alias.$error.required).toBeTruthy(); + }); }); }); -- cgit v1.2.3