对用户提交的数据实施复杂的业务约束给大量开发人员带来了独特的挑战。 最近,我和我的团队在Gift Gifts.com上编写应用程序时面临着这样的挑战。 我们需要找到一种方法,允许我们的客户在我们的应用程序内的单个视图中编辑多个产品,其中每个产品都有一套独特的验证规则。
事实证明,这具有挑战性,因为它要求我们在HTML源代码中具有多个<form>
标记,并为每个表单实例维护一个验证模型。 在解决方案之前,我们尝试了许多方法,例如使用ngRepeat
显示子窗体。 我们将为每种产品类型创建一个指令(每个指令在其视图中将具有<form>
)并将该指令绑定到其父控制器。 这使我们能够利用Angular的子/父窗体继承来确保仅在所有子窗体均有效时父窗体才有效。
在本教程中,我们将构建一个简单的产品审查屏幕(突出显示当前应用程序的关键组件)。 我们将有两种产品,每种产品都有自己的指令,每种产品都有独特的验证规则。 将有一个简单的checkout
按钮,可确保两种形式均有效。
如果您迫切希望看到此功能,可以直接跳至我们的演示,或从GitHub repo下载代码。
关于指令的话
指令是通过AngularJS的HTML编译器( $compile
)运行并附加到DOM的HTML代码块。 编译器负责遍历DOM,以寻找可以使用其他已注册指令转换为对象的组件。 指令在孤立的范围内工作,并保持自己的观点。 它们是功能强大的工具,可促进可在整个应用程序之间共享的可重用组件。 如需快速复习,请查看此SitePoint文章或AngularJS文档 。
指令通过两种方式解决了我们的基本问题:首先,每个实例都有一个隔离的范围,其次,该指令使用编译器遍历,从而编译器使用Angular的ngForm
指令在视图的HTML中标识表单元素。 此内置指令允许多个嵌套的form
元素,接受可选的name
属性以实例化Form Controller
,并将与form对象一起返回。
关于表单控制器的一句话
当编译器识别出DOM中的任何表单对象时,它将使用ngForm
指令实例化Form Controller
对象。 该控制器将扫描所有input
select
和textarea
元素,并创建适当的控件。 这些控件需要一个model
属性来设置双向数据绑定,并允许用户通过各种预先建立的验证方法即时获得反馈。 向消费者提供即时反馈可以使他们在发出HTTP请求之前知道哪些信息有效。
内置验证方法
Angular随附14种标准验证方法。 这些措施包括验证min
, max
, required
的名字,但一些。 它们的构建旨在理解和使用几乎所有HTML5输入类型,并且它们与跨浏览器兼容。
<form name="form" novalidate>
Size:
<input type="text" ng-model="size" name="size" ng-required="true" />
<span ng-show="form.size.$error.required">The value is required!</span>
</form>
上面的示例显示了Angular中ngRequired
指令验证器的用法。 此验证可确保在将该字段视为有效之前先对其进行填写。 它不验证任何数据,仅验证用户已输入内容。 具有属性novalidate
表示浏览器不应在提交时进行验证。
专家提示:不要在任何Angular窗体上设置
action
属性。 这将阻止Angular尝试确保不以往返方式提交表单。
定制验证方法
Angular提供了广泛的API,可帮助创建自定义验证规则。 使用此API,您可以为标准验证未涵盖的复杂输入创建和扩展自己的验证规则。 我和我的团队依靠一些自定义验证方法来运行服务器使用的复杂RegEx模式。 如果无法运行复杂的RegEx匹配器,我们可能会向我们的后端服务器发送不正确的数据。 这将向用户呈现错误,从而导致不良的用户体验。 自定义验证器使用指令语法并要求注入ngModel
。 有关更多信息,请参见AngularJS的文档 。
创建控制器
有了这些,我们就可以开始我们的应用程序了。 您可以在此处找到控制器代码的概述。
控制器将成为事物的心脏。 它仅具有少量职责-它的视图将具有一个名为parentForm
的表单元素,将仅具有一个属性,并且其方法将包括registerFormScope
, validateChildForm
和checkout
。
控制器属性
我们将在控制器中需要一个属性:
$scope.formsValid = false;
此属性用于维护表单整体有效性的布尔状态。 单击此属性后,我们将使用该属性禁用“签出”按钮的状态。
方法:registerFormScope
$scope.registerFormScope = function (form, id) {
$scope.parentForm['childForm'+id] = form;
};
调用registerFormScope
,会将其传递给Form Controller
以及在指令实例化中创建的唯一指令ID。 然后,此方法会将表单范围附加到父Form Controller
。
方法:validateChildForm
这是用于与执行验证的后端服务器协调的方法。 当用户正在编辑内容并且需要进行其他验证时,将调用它。 从概念上讲,我们不允许指令执行任何外部通信。
请注意,出于本教程的目的,我已省略了后端组件。 相反,我基于用户输入的数量是否落在某个范围内(产品A为10 – 50,产品B为25-500)拒绝或兑现承诺。
$scope.validateChildForm = function (form, data, product) {
// Reset the forms so they are no longer valid
$scope.formsValid = false;
var deferred = $q.defer();
// Logic to validate the form and data
// Must return either resolve(), or reject() on the promise.
$timeout(function () {
if (angular.isUndefined(data.amount)) {
return deferred.reject(['amount']);
}
if ((data.amount < product.minAmount) ||
(data.amount > product.maxAmount)) {
return deferred.reject(['amount']);
}
deferred.resolve();
});
return deferred.promise;
}
使用$q
服务允许指令遵循具有成功和失败状态的接口。 应用程序界面的性质根据模型数据的编辑在“编辑”和“保存”之间改变。 应该注意的是,一旦用户开始键入,模型数据就会更新。
方法:结帐
单击“签出”表示用户已完成编辑并希望签出。 在将模型数据发送到服务器之前,该可操作项将需要验证指令中加载的所有表单均通过验证。 本文的范围不会涵盖用于将数据发送到服务器的方法。 我鼓励您探索使用$ http服务来实现所有客户端到服务器的通信。
$scope.checkout = function () {
if($scope.parentForm.$valid) {
// Connect with the server to POST data
}
$scope.formsValid = $scope.parentForm.$valid;
};
此方法使用Angular的子窗体的功能来使父窗体无效。 父表单被命名为parentForm
以清楚地说明其与子表单的关系。 当childForm
使用其$setValidity
childForm
方法时,它将自动升至父窗体以在那里设置有效性。 parentForm
所有表单必须有效,其内部$valid
属性才为true。
制定指令
我们的指令必须遵循允许完全互操作性和可扩展性的公共接口。 我们的指令名称取决于它们所包含的产品。
隔离指令范围
实例化的每个指令都将获得一个隔离的作用域,该作用域被本地化为该指令,并且不了解外部属性。 但是,AngularJS确实允许创建利用父母范围方法和属性的指令。 将外部属性传递到本地化范围时,可以指示要设置双向数据绑定。
我们的应用程序将需要一些外部双向数据绑定方法和属性:
scope: {
registerFormScope: '=',
giftData: '=',
validateChildForm: '=',
product: '='
},
方法:registerFormScope
指令的本地范围中的第一个属性是向控制器注册本地scope.form
的方法。 该指令需要一个管道来将本地Form Controller
对象传递给主Controller
。
对象:giftData
这是将在指令视图中使用的集中模型数据。 此信息将是双向数据绑定,以确保在Form Controller
中发生的更新将传播到主Controller
。
方法:validateChildForm
此方法与Controller
内部定义的方法相同。 当用户在指令视图中更新信息时,将调用此方法。
对象:产品
该对象包含有关所购买产品的信息。 我们的演示使用了一个相对较小的对象,仅具有少量属性。 我的团队在现实世界中的应用程序具有大量信息,这些信息可用于在应用程序内做出决策。 它被传递到validateChildForm
以提供正在验证的内容的上下文。
指令链接
我们的指令将使用postLink
函数, postLink
其传递一个范围对象。 除此之外, postLink
函数还接受其他几个参数。 这些如下:
-
scope
–用于访问对每个指令实例创建的隔离作用域。 -
iElement
–用于访问元素项。 从postLink
函数中更新和修改分配给它的元素是唯一安全的。 -
iAttrs
–用于访问实例化该指令的同一标记上的属性。 -
controller
–如果存在外部控制器依赖性,则可在链接功能中使用。 这些必须与Directive Object
的require
属性相对应。 -
transcludeFn
–该功能与“$transclude
Directive Object
的$transclude
参数中列出的功能相同。
link
负责附加所有DOM侦听器,并使用view元素更新DOM。
link: function postLink(scope) {
// Indicates if the form is disabled
scope.disabled = true;
scope.saveForm = function () {
// Code for saving the form data
};
// Register form scope
$timeout(function() {
});
}
注册表格范围
$timeout(function () {
scope.form.fields = ['name','amount'];
scope.registerFormScope(scope.form, scope.$id);
});
在$timeout
包装方法registerFormScope
将执行推迟到执行堆栈的末尾。 这为编译器提供了充足的时间来完成控制器和指令之间的所有必要链接。 scope.form.fields
是一个数组,它是在Form Controller
中找到的属性的名称,这对于设置服务器端验证错误很重要。 registerFormScope
的目的是将Form Controller
发送到父控制器,从而允许将新创建的表单设置为parentForm
的子parentForm
。
验证信息何时更改
scope.saveForm = function () {
scope.validateChildForm(scope.form, scope.giftData, scope.product)
.then(function () {
angular.forEach(scope.form.fields, function (val) {
scope.form.$setValidity(val, true);
scope.form[val].$error.server = false;
});
scope.disabled = true;
}, function (invalidFields) {
angular.forEach(invalidFields, function (val) {
if (angular.isDefined(scope.form[val])) {
scope.form[val].$error.server = true;
scope.form.$setValidity(val, false);
}
});
scope.disabled = false;
});
};
当表单更改并且用户准备对其进行验证时,将调用伪指令中的saveForm
方法。 该方法将依次调用控制器的validateChildForm
方法,并scope.product
Form Controller
, scope.giftData
和scope.product
。 控制器将返回一个承诺,该承诺将根据其他验证规则予以解决或拒绝。
当承诺被拒绝时,控制器将返回无效的字段。 这用于使表单(和parentForm
)无效,并设置其他字段级别的错误。 在我们的演示中,我们在数量字段上使用了一个简单的后验证,并且没有返回失败的原因。 validateChildForm
的拒绝可能像您的应用程序所要求的那样复杂或简单。
当Promise成功返回时,指令需要设置表单中字段的有效性。 该代码还必须清除任何以前确定的服务器错误。 这样可以确保该指令不会错误地向用户提供错误。 设置所有带有$setValidity
parentForm
字段的链接到控制器中的parentForm
,以设置其有效性, parentForm
是所有子窗体均有效。
设定我们的观点
视图不是很复杂,对于我们的演示,我们将产品配对到以下字段: name
和amount
。 在下一步中,我们将探索完成此应用程序所需的视图。
路线图
<div data-ng-app="myApp" ng-controller="stageController">
<div id="main" class="container">
<h1>Review Order</h1>
<form name="parentForm" novalidate>
<div ng-repeat="gift in gifts" class="row">
<div class="col-lg-12"
ng-if="gift.product.type == 'A'"
product-A data-register-form-scope="registerFormScope"
data-gift-data="gift.giftData"
data-validate-child-form="validateChildForm"
data-product="gift.product">
</div>
<div class="col-lg-12"
ng-if="gift.product.type == 'B'"
product-B data-register-form-scope="registerFormScope"
data-gift-data="gift.giftData"
data-validate-child-form="validateChildForm"
data-product="gift.product">
</div>
</div>
</form>
<div class="row">
<div class="col-lg-12">
<button class="btn btn-primary"
data-ng-click="checkout()">Checkout</button>
<div class="alert alert-success"
data-ng-show="formsValid">All forms are valid!</div>
</div>
</div>
</div>
</div>
该视图很重要,因为它设置了父表单,该父表单将用于包装从product指令加载的所有子表单。 在ng-repeat
使用ng-if
确保使用未使用的Form Controller
不会错误地填充DOM。
指令视图
<form name="form" novalidate>
...
<label for="amountInput">Amount</label>
<input id="amountInput" name="amount"
class="text-center form-control" type="tel"
data-ng-model="giftData.amount"
data-ng-pattern="/^(?!\.?$)\d+(\.\d{0,2})?$/"
data-ng-required="true"
data-ng-disabled="disabled"/>
...
<label>Actions</label>
<button class="btn btn-info"
ng-click="disabled=false;"
ng-show="disabled">Edit</button>
<button class="btn btn-success"
ng-click="saveForm()"
ng-show="!disabled">Save</button>
...
<div class="row" data-ng-show="form.$submitted">
<div class="col-lg-12">
<div class="alert alert-danger"
data-ng-show="form.name.$error.required && form.$submitted">
Recipient Name is a required field.
</div>
<div class="alert alert-danger"
data-ng-show="form.amount.$error.pattern && form.$submitted">
The amount is invalid.
</div>
<div class="alert alert-danger"
data-ng-show="form.amount.$error.server && form.$submitted">
The amount is not accepted. Must be between
{{ product.minAmount }} and {{ product.maxAmount}}.
</div>
</div>
</div>
...
</form>
注意:上面的视图在与演示的布局有关的地方被截断了,对本文不重要。
上面的amountInput
设置了一个验证模式,该验证模式将由Angular的ngPattern
验证器强制执行。 上面的字段将使用ngDisabled
构建的ngDisabled
指令,该指令将评估表达式,如果为true
,则将禁用该字段。
在视图的底部,我们显示了所有错误,以向用户单击Save
按钮时提供反馈。 这将在子窗体上设置$submitted
属性。
包起来
将所有部分放在一起,最终得到的是:
请参阅CodePen上的SitePoint ( @SitePoint )进行的Pen AngularJS指令表单验证 。
别忘了,您也可以在GitHub上找到所有代码。
结论
我和我的团队在构造我们的最新应用程序方面学到了很多东西。 了解父母/子女表格的关系使我们能够简化审查屏幕。 使用指令使我们能够开发一种可以在任何上下文中使用的形式,并促进良好的可重用代码。 指令还允许我们拥有经过单元测试的代码,以确保我们的表格能够按预期工作。 我们的应用程序已投入生产,已促成100,000多个订单。
希望您喜欢阅读本文。 如果您有任何问题或意见,我们将很高兴在下面的评论中听到他们的意见。
From: https://www.sitepoint.com/form-based-directives-angularjs/