本节我们将通过几个具体的例子来讲解Angular表单。
简单的表单
ngModel指令实现了双向的数据绑定,即模型和视图双向同步,同时它也为其他指令提供了API来扩展ngModel的行为。让我们看如下示例:
<div ng-controller="ExampleController"> <form novalidate class="simple-form"> Name: <input type="text" ng-model="user.name" /><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 /> <input type="button" ng-click="reset()" value="Reset" /> <input type="submit" ng-click="update(user)" value="Save" /> </form> <pre>form = {{user | json}}</pre> <pre>master = {{master | json}}</pre> </div> <script> angular.module('formExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.master = {}; $scope.update = function(user) { $scope.master = angular.copy(user); }; $scope.reset = function() { $scope.user = angular.copy($scope.master); }; $scope.reset(); }]); </script>
在上述代码中,我们定义了个简单的表单,novalidate表明没有添加任何校验逻辑,在表单组定义了各种输入控件绑定到模型user的不同属性,当用户输入是会看到form信息不停的更新,点击“Save”按钮会把当前表单的值保存到master变量中,master信息会相应的更新, 点击"Reset"信息会重置表单的信息,即把之前保存的快照master赋值给user. 这里通过angular.copy函数做了深拷贝。
运行结果如下:
表单的CSS样式
angular为表单控件添加了额外的样式来提升用户体验:
- ng-valid: 数据合法时的CSS样式
- ng-invalid: 数据不合法时的CSS样式
- ng-valid-[key]: 通过$setValidity指定的key合法时生效的样式
- ng-invalid-[key]: 通过$setValidity指定的key不合法时生效的样式
- ng-pristine: 未曾与表单控件交互时的样式
- ng-dirty: 与控件发生了交互后生效的样式
- ng-touched: 控件失去焦点的样式
- ng-untouched: 控件没有失去焦点时的样式
- ng-pending: 等待异步校验过程中的(in-progress)生效的样式
基于上面的例子我们加上了校验,并通过上述样式体现出来,user的name和email属性对应的控件加上了required属性, 当用户未输入name和email信息时会标红提示。
<div ng-controller="ExampleController"> <form novalidate class="css-form"> Name: <input type="text" ng-model="user.name" 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 /> <input type="button" ng-click="reset()" value="Reset" /> <input type="submit" ng-click="update(user)" value="Save" /> </form> </div> <style type="text/css"> .css-form input.ng-invalid.ng-touched { background-color: #FA787E; } .css-form input.ng-valid.ng-touched { background-color: #78FA89; } </style> <script> angular.module('formExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.master = {}; $scope.update = function(user) { $scope.master = angular.copy(user); }; $scope.reset = function() { $scope.user = angular.copy($scope.master); }; $scope.reset(); }]); </script>
运行结果:
绑定及控制表单状态
每一个表单都有一个表单控制器与之对应。可以通过name属性将表单实例公开到作用域。同理一个输入控件可以通过ngModel指令持有一个NgModelController,并且可以通过name属性变成表单实例的属性,name属性值即表单实例的属性。
这就意味着表单控件的内部状态可以通过angular的标准方式来绑定。
我们就上面的例子进行一下扩展:
- 在用户和控件交互后显示定制的错误消息(通过$touched服务组件)
- 在提交表单按钮时显示定制的错误消息(通过$submitted服务组件)
index.html
<div ng-controller="ExampleController"> <form name="form" class="css-form" novalidate> Name: <input type="text" ng-model="user.name" name="uName" required="" /> <br /> <div ng-show="form.$submitted || form.uName.$touched"> <div ng-show="form.uName.$error.required">Tell us your name.</div> </div> E-mail: <input type="email" ng-model="user.email" name="uEmail" required="" /> <br /> <div ng-show="form.$submitted || form.uEmail.$touched"> <span ng-show="form.uEmail.$error.required">Tell us your email.</span> <span ng-show="form.uEmail.$error.email">This is not a valid email.</span> </div> 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" required="" /> <br /> <div ng-show="form.$submitted || form.userAgree.$touched"> <div ng-show="!user.agree || !user.agreeSign">Please agree and sign.</div> </div> <input type="button" ng-click="reset(form)" value="Reset" /> <input type="submit" ng-click="update(user)" value="Save" /> </form> </div>
script.js
angular.module('formExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.master = {}; $scope.update = function(user) { $scope.master = angular.copy(user); }; $scope.reset = function(form) { if (form) { form.$setPristine(); form.$setUntouched(); } $scope.user = angular.copy($scope.master); }; $scope.reset(); }]);
在上面的html中,通过name="form" 暴露了form实例,既可以在 $scope.reset = function(form) 通过参数注入,同样<inputtype="email"ng-model="user.email"name="uEmail"required=""/>,通过name="uEmail"将该控件绑定到form.uEmail属性.
运行结果:
定制触发器更新模型
默认情况下,表单的每次输入都会触发与模型的同步和表单的校验。我们可以通过ngModelOptions指令改变模型同步和表单验证的触发条件。例如ng-model-options="{ updateOn: 'blur' }",会使Angular在输入控件失去焦点时更新模型或者进行校验。多个事件通过空白隔开,例如:
ng-model-options="{ updateOn: 'mousedown blur' }". 如果保持默认触.发条件并添加新的触发事件,加上“default”选项,
例如:ng-model-options="{ updateOn: 'default blur' }".
下面例子中,对模型的同步和表单校验将会在表单控件失去焦点是被触发:
index.html
<div ng-controller="ExampleController"> <form> Name: <input type="text" ng-model="user.name" ng-model-options="{ updateOn: 'blur' }" /><br /> Other data: <input type="text" ng-model="user.data" /><br /> </form> <pre>username = "{{user.name}}"</pre> <pre>userdata = "{{user.data}}"</pre> </div>
script.js
angular.module('customTriggerExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.user = {}; }]);
运行结果:
在name属性对应的控件中通过ng-model-options="{ updateOn: 'blur' }" 设置了在blur事件发生时更新username属性而属性userdata这在每次输入后即更新模型,结果如图所示, username并没有实时更新。
延迟模型更新(去抖)
我们可以通过debounce属性让模型延迟更新。这个延迟对解析器,校验器和模型的标记(例如$dirty和$pristine)都生效。例如:ng-model-options="{ debounce: 500 }"
将会在内容更新后半秒钟更新模型和校验表单。如果使用自定义的触发器,可以每个触发器的每个事件设置延迟更新,例如:ng-model-options="{ updateOn: 'default blur', debounce: { default: 500, blur: 0 }。这些属性设置对所在的元素及子元素或控件有效。如下例子中我们设置了延迟250毫秒更新模型:
index.html
<div ng-controller="ExampleController"> <form> Name: <input type="text" ng-model="user.name" ng-model-options="{ debounce: 250 }" /><br /> </form> <pre>username = "{{user.name}}"</pre> </div>
script.js
angular.module('debounceExample', []) .controller('ExampleController', ['$scope', function($scope) { $scope.user = {}; }]);
自定义校验
Angular对HTML5最常用的输入控件及校验属性提供了支持,例如 text, number, url, email, radio, checkbox控件,required,pattern, minLength, maxLength, min, max属性。我们可以为ngMedelController自定义校验器($validators)。$validators里面的每个方法包含两个参数,modelValue和viewValue, 返回值布尔类型。Angular会调用$setValidity设置校验结果。校验方法会在输入变化($setViewValue)或者模型更新后被调用。校验会在$parsers和$formatter运行成功后进行。校验的错误信息会保存在ngModelController.$error中。
另外Angular还提供了$asyncValidators对象处理异步校验,例如基于ajax的校验。 该对象中的方法需返回promise对象,校验成功被设成resolved状态,失败为rejected状态,正在进行中的校验则存在ngModelController.$pending属性中。
在下面的例子中我们创建了两个指令:
- 一个integer指令来校验输入是否为合法的整数。例如1.23为非法的整数。这里我们校验的是控件中的输入,不是所绑定的模型,因为$parsers会把input[number]控件中的输入转换成数字。
- 一个username指令,对用户输入异步校验。我们通过$q来mock服务器请求。
index.html
<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">The value is not a 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> Username: <input type="text" ng-model="name" name="name" username />{{name}}<br /> <span ng-show="form.name.$pending.username">Checking if this name is available ...</span> <span ng-show="form.name.$error.username">This username is already taken!</span> </div> </form>
script.js
var app = angular.module('form-example1', []); var INTEGER_REGEXP = /^\-?\d+$/; app.directive('integer', function() { return { require: 'ngModel', link: function(scope, elm, attrs, ctrl) { ctrl.$validators.integer = function(modelValue, viewValue) { if (ctrl.$isEmpty(modelValue)) { // consider empty models to be valid return true; } if (INTEGER_REGEXP.test(viewValue)) { // it is valid return true; } // it is invalid return false; }; } }; }); app.directive('username', function($q, $timeout) { return { require: 'ngModel', link: function(scope, elm, attrs, ctrl) { var usernames = ['Jim', 'John', 'Jill', 'Jackie']; ctrl.$asyncValidators.username = function(modelValue, viewValue) { if (ctrl.$isEmpty(modelValue)) { // consider empty model valid return $q.when(); } var def = $q.defer(); $timeout(function() { // Mock a delayed response if (usernames.indexOf(modelValue) === -1) { // The username is available def.resolve(); } else { def.reject(); } }, 2000); return def.promise; }; } }; });
运行结果:
Username的校验是异步的可以看到通过$q.defer() mock了延迟。
重写Angular内建的校验器
Angular默认使用$validators校验器, 我们可以根据需求重写或者去掉内建的校验器。下面的例子中我们重写了input[email]校验器,email地址必须包含顶级域名example.com.
index.html
<form name="form" class="css-form" novalidate> <div> Overwritten Email: <input type="email" ng-model="myEmail" overwrite-email name="overwrittenEmail" /> <span ng-show="form.overwrittenEmail.$error.email">This email format is invalid!</span><br> Model: {{myEmail}} </div> </form>
script.js
var app = angular.module('form-example-modify-validators', []); app.directive('overwriteEmail', function() { var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@example\.com$/i; return { require: 'ngModel', restrict: '', link: function(scope, elm, attrs, ctrl) { // only apply the validator if ngModel is present and Angular has added the email validator if (ctrl && ctrl.$validators.email) { // this will overwrite the default Angular email validator ctrl.$validators.email = function(modelValue) { return ctrl.$isEmpty(modelValue) || EMAIL_REGEXP.test(modelValue); }; } } }; });
在link方法中我们加入了对email顶级域名的检查,可以从运行结果中看到,只有但email通过校验是,才会给模型赋值。
运行结果:
自定义表单控件
Angular实现了所有的HTML表单基本控件,可以满足大部分需求。如果我们需要更灵活的表单控件,可以将其实现为指令。
可以通过如下方法实现双向数据绑定,是控件能够和ngModel一起工作:
- 实现$render方法,在NgModelController.$formatters 执行完后渲染数据。
- 调用$setViewValue方法来同步视图控件和模型。通常在DOM事件监听器里面调用。
可以查看$compileProvider.directive
API获得更多的知识.
让我们通过具体的例子详解contentEditable元素的双向数据绑定。
index.html
<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>
script.js
angular.module('form-example2', []).directive('contenteditable', function() { return { require: 'ngModel', link: function(scope, elm, attrs, ctrl) { // view -> model elm.on('blur', function() { scope.$apply(function() { ctrl.$setViewValue(elm.html()); }); }); // model -> view ctrl.$render = function() { elm.html(ctrl.$viewValue); }; // load init value from DOM ctrl.$setViewValue(elm.html()); } }; });
运行结果: