【问题标题】:Angular 1.6 Custom validation directive with $setValidity带有 $setValidity 的 Angular 1.6 自定义验证指令
【发布时间】:2016-12-25 19:23:42
【问题描述】:

我正在尝试编写一个自定义指令,该指令根据其他值验证输入字段,这些值也需要在指令中可用。我通过使用带有范围变量的隔离范围来做到这一点。更具体地说,我想将产品的客户价格(即其净价格)与购买价格进行比较,如果差异为负(客户价格设置为 0 除外),我想让客户-price 输入(及其周边形式)无效。这是我的指令:

export class CheckMarkupDirective implements ng.IDirective {
    public static create(): ng.IDirective {
        return {
            restrict: "A",
            require: "ngModel",
            scope: {
                netPrice: "<",
                markupAmount: "<"
            },
            link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModelCtrl: ng.INgModelController) => {

                let netPrice: number;
                let markupAmount: number;
                scope.$watchGroup(["netPrice", "markupAmount"], (newValues, oldValues) => {

                    [netPrice, markupAmount] = newValues;

                    if (markupAmount >= 0) {
                        ngModelCtrl.$setValidity("markup", true);
                    } else {
                        ngModelCtrl.$setValidity("markup", netPrice === 0);
                    }
                    ngModelCtrl.$validate();
                });
            }
        };
    }
}

这就是我在一个被表单标签包围的 ng-form div 中使用它的方式:

<input type="text" id="customer-price" name="customerPrice"
       ng-model="ctrl.product.customerPrice"
       ng-change="ctrl.customerPriceChangeDetected()" 
       check-markup markup-amount="ctrl.product.markupAmount"
       net-price="ctrl.product.netPrice" />

它有效,但问题是验证部分似乎“时间错误”,这意味着如果我输入一个导致“标记”第一次变为负数的值,那么表单的 $invalid值设置为假。但是当下一个输入为负时,验证将启动并起作用。我认为我的问题是我在不同步骤之间进行了大量计算,但我很难知道是什么导致验证如此偏离。我想我希望对 Angular JS 机制有更深入了解的人来看看我是否在做明显错误的事情。如果我的描述有点含糊,请提前致谢。

编辑:我还想包括在 ng-change 上触发的方法:

public customerPriceChangeDetected(): void {
    this.setNetPriceFromCustomerPrice();
    this.setMarkup();
    this.changeDetected();
}
private setNetPriceFromCustomerPrice(): void {
    let customerPrice = this.product.customerPrice;
    let vatRate = this.product.vatRate;
    let netPrice = (customerPrice / (1 + vatRate));
    this.product.netPrice = parseFloat(accounting.toFixed(netPrice, 2));
}
private setMarkup(): void {
    let purchasePrice = this.product.purchasePrice;
    let markupAmount = this.product.netPrice - purchasePrice;
    this.product.markupAmount = markupAmount;
    this.product.markupPercent = markupAmount / purchasePrice;
}
public changeDetected(): void {
    let isValid = this.validationService ? this.validationService.isValid : false;
    this.toggleSaveButton(isValid);
}

验证服务 getter 基本上返回 form.$valid 并且对于我所有其他自定义验证器都可以正常工作。

编辑 2: 添加了屏幕截图,显示周围的 ng-form 标签似乎至少将其 $invalid 属性设置为 true:

编辑 3: 这是转译的 JS:

var CheckMarkupDirective = (function () {
function CheckMarkupDirective() {
}
CheckMarkupDirective.create = function () {
    return {
        restrict: "A",
        require: "ngModel",
        scope: {
            netPrice: "<",
            markupAmount: "<"
        },
        link: function (scope, element, attrs, ngModelCtrl) {
            var netPrice;
            var markupAmount;
            scope.$watchGroup(["netPrice", "markupAmount"], function (newValues, oldValues) {
                netPrice = newValues[0], markupAmount = newValues[1];
                if (!markupAmount || !netPrice)
                    return;
                if (markupAmount >= 0) {
                    ngModelCtrl.$setValidity("markup", true);
                }
                else {
                    ngModelCtrl.$setValidity("markup", netPrice === 0);
                }
                //ngModelCtrl.$validate();
            });
        }
    };
};
return CheckMarkupDirective; }());

...这是我的 html 的精简版:

<form autocomplete="off" class="form-horizontal" role="form" name="productDetailsForm" novalidate data-ng-init="ctrl.setForm(this,'productDetailsForm')">
<div data-ng-form="section2">
    <div class="form-group">
        <label for="purchase-price" class="col-sm-4 control-label">Purchase price</label>
        <div class="col-sm-4">
            <input type="text" class="form-control" id="purchase-price" name="purchasePrice"
                   data-ng-model="ctrl.product.purchasePrice"
                   data-ng-change="ctrl.purchasePriceChangeDetected();"
                   data-decimal="Currency" />
        </div>
    </div>
    <div class="form-group">
        <label for="vat-rate" class="col-sm-4 control-label">VAT rate</label>
        <div class="col-sm-4">
            <select class="form-control" id="vat-rate"
                    data-ng-model="ctrl.product.vatRate"
                    data-ng-change="ctrl.vatRateChangeDetected()"
                    data-ng-options="vatRate.value as vatRate.text for vatRate in ctrl.vatRates"></select>
        </div>
    </div>
    <div class="form-group" data-has-error-feedback="productDetailsForm.section2.customerPrice">
        <label for="customer-price" class="col-sm-4 control-label">Customer price</label>
        <div class="col-sm-4">
            <input type="text" class="form-control" id="customer-price" name="customerPrice"
                   data-ng-model="ctrl.product.customerPrice"
                   data-ng-change="ctrl.customerPriceChangeDetected();"
                   data-decimal="Currency"
                   data-check-markup
                   data-markup-amount="ctrl.product.markupAmount"
                   data-net-price="ctrl.product.netPrice" />
            <invalid-feedback item="productDetailsForm.section2.customerPrice"></invalid-feedback>
            <validation-feedback type="markup" item="productDetailsForm.section2.customerPrice" data-l10n-bind="ADMINISTRATION.PRODUCTS.NET_PRICE.INVALID"></validation-feedback>
        </div>
        <div class="col-sm-4">
            <div class="form-group" style="margin-bottom: 0;">
                <label for="net-price" class="col-lg-5 col-md-5 col-sm-5 col-xs-5" style="font-weight: normal; margin-top: 7px;">
                    <span data-l10n-bind="ADMINISTRATION.PRODUCTS.NET_PRICE"></span>
                </label>
                <label class="col-lg-7 col-md-7 col-sm-7 col-xs-7" style="font-weight: normal; margin-top: 7px;">
                    <span id="net-price">{{ ctrl.product.netPrice | currency }}</span>
                </label>
            </div>
        </div>
    </div>
    <div class="form-group" data-has-error-feedback="productDetailsForm.section2.markup">
        <label for="markup-amount" class="col-sm-4 col-xs-4 control-label">Markup</label>
        <div class="col-sm-8 col-xs-8">
            <label id="markup-percent" class="control-label" data-ng-class="{'text-danger': ctrl.product.markupPercent < 0}">
                {{ ctrl.product.markupPercent * 100 | number: 2 }}%
            </label>
            <label id="markup-amount" class="control-label" data-ng-class="{'text-danger': ctrl.product.markupAmount < 0}">
                ({{ ctrl.product.markupAmount | currency }})
            </label>
        </div>
    </div>
</div>

我在指令中的手表内设置了断点,由于某种奇怪的原因,当我第一次在客户价格输入中输入新值时,手表似乎没有触发。相反,我发现自己直接在 changeDetected() 方法中。我现在真的很困惑。我认为这个问题与验证之前触发的 ng-change 指令有关。我可能有一个错误的逻辑,导致在指令有时间实际更改有效性之前触发我的验证服务的 isValid 检查。

【问题讨论】:

  • 我不确定你为什么打电话给$validate()。此外,当标记为无效的字段时不会调用 ng-change,因为 ng-model 不会根据无效值更新。所以尝试在表单外打印customerPrice/markupAmount/netPrice的值,看看值是否刷新。
  • 您好,感谢您抽出宝贵时间帮助我。我在手表内使用了 console.log 来查看每次更改 customer-price 输入字段的值时 markupAmount 和 netprice 都会更新,因此至少部分似乎按预期工作(?)在我看来,验证总是落后一步,因此如果我从有效输入开始并将其更改为无效输入(负标记),表单仍将具有 $invalid=false。真正令人讨厌的部分是带有 ng-form 的 div 似乎有 $invalid=true Whick 真的让我感到困惑。

标签: angularjs validation directive angularjs-1.6


【解决方案1】:

尝试删除隔离范围并直接评估属性:

export class CheckMarkupDirective implements ng.IDirective {
    public static create(): ng.IDirective {
        return {
            restrict: "A",
            require: "ngModel",
            /* REMOVE isolate scope
            scope: {
                netPrice: "<",
                markupAmount: "<"
            },
            */
            link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ngModelCtrl: ng.INgModelController) => {

                let netPrice: number;
                let markupAmount: number;
                //scope.$watchGroup(["netPrice", "markupAmount"],
                //WATCH attributes directly
                scope.$watchGroup([attrs.netPrice, attrs.markupAmount], (newValues, oldValues) => {

                    [netPrice, markupAmount] = newValues;

                    if (markupAmount >= 0) {
                        ngModelCtrl.$setValidity("markup", true);
                    } else {
                        ngModelCtrl.$setValidity("markup", netPrice === 0);
                    }
                    ngModelCtrl.$validate();
                });
            }
        };
    }
}

inputng-modelng-change 指令需要一个没有范围的元素。这消除了一次性绑定观察者以及与这些指令对抗的隔离作用域的复杂性。

【讨论】:

  • 尝试删除隔离范围并改用 attrs.markupAmount 和 attrs.netPrice 但我仍然得到相同的行为。我不知道关于 ng-model 和 ng-change 的部分,但并不期望范围。不知道我是否真的理解它。 :(
【解决方案2】:

我已经复制了我认为您的表单正在执行的操作,如果我在 所有 字段(vatRate、purchasePrice、customerPrice)上添加 ng-change 没有问题。

你能检查一下我所做的匹配是否符合你的打字稿给出的要求吗?如果不是,您可以尝试将结果显示为 javascript 吗?

angular.module('test',[]).directive('checkMarkup', [function(){
  return {
            restrict: "A",
            require: "ngModel",
            scope: {
                netPrice: "<",
                markupAmount: "<"
            },
            link: (scope, element, attrs, ngModelCtrl) => {
                var netPrice;
                var markupAmount;
                scope.$watchGroup(["netPrice", "markupAmount"], (newValues, oldValues) => {
                    netPrice= newValues[0];
                    markupAmount = newValues[1];
                    if (markupAmount >= 0) {
                        ngModelCtrl.$setValidity("markup", true);
                    } else {
                        ngModelCtrl.$setValidity("markup", netPrice === 0);
                    }
                    ngModelCtrl.$validate();
                });
            }
        };
}]).controller('ctrl', ['$scope', function($scope){
  $scope.customerPriceChangeDetected = function(){
    setNetPriceFromCustomerPrice();
    setMarkup();
    
};
function setNetPriceFromCustomerPrice() {
    var customerPrice = $scope.product.customerPrice;
    var vatRate = parseFloat($scope.product.vatRate);
    var netPrice = (customerPrice / (1 + vatRate));
    $scope.product.netPrice = netPrice;
};
function setMarkup(){
    var purchasePrice = $scope.product.purchasePrice;
    var markupAmount = $scope.product.netPrice - purchasePrice;
    $scope.product.markupAmount = markupAmount;
    $scope.product.markupPercent = markupAmount / purchasePrice;
}
}]);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.1/angular.min.js"></script>
<div ng-app="test" ng-controller="ctrl">
  <form name="form">
      purchasePrice : <input type="text"  name="purchasePrice"
       ng-model="product.purchasePrice"
       ng-change="customerPriceChangeDetected()" 
        />  <br/>
   vatRate : <input type="text"  name="vatRate"
       ng-model="product.vatRate"
       ng-change="customerPriceChangeDetected()" 
        />  <br/>
    
  Customer price : <input type="text" id="customer-price" name="customerPrice"
       ng-model="product.customerPrice"
       ng-change="customerPriceChangeDetected()" 
       check-markup markup-amount="product.markupAmount"
       net-price="product.netPrice" /> <br/>
  </form>
  markupAmount : {{product.markupAmount}} <br/>
  netPrice : {{product.netPrice}} <br/>
  vatRate : {{$scope.product.vatRate}}
   customerPrice invalid : {{form.customerPrice.$invalid}}<br/>
  form invalid : {{form.$invalid}}
</div>

【讨论】:

  • 感谢您的帮助,我想我已经设法将其缩小到我的 ng-change 方法 customePriceChangeDetected 在指令有任何机会之前触发我的验证服务的 isValid-check(表单)实际改变客户价格输入的有效性。我想我需要重新考虑一下逻辑。
  • $watch 在角循环结束时触发。奇怪的是,使用该示例,我没有重现您的问题。你的逻辑对我来说也很奇怪,我要做的就是将你的整个产品对象传递给你的指令并执行指令中的所有操作(使用双向绑定来更新 netPrice/markupAmount)
  • 你可能是对的。问题是我开始时已经在 productDetailsForm 控制器内部进行了计算,然后在稍后阶段添加了您不应该使用负标记保存的要求,这就是我一直在思考的原因关于我的指令纯粹是作为自定义验证,而不是实际执行任何计算的东西。
  • @Kristopher 好吧,我仍然想知道为什么它不起作用。请注意,在某些情况下,您可以只在控制器中处理验证,加载内容视图后,您的表单将位于this.&lt;formName&gt;.&lt;inputName&gt; 下。在大多数情况下,这显然不是推荐的选项,但在需要在更高级别注册的指令中掩盖有关净价格/加价金额的内容似乎也不是它的常规用法。
  • 我仍在为所有 Angular 1.x 概念而苦苦挣扎(我感觉 Angular 2 中的 observables 会给我带来很多痛苦)但据我所知,控制器不应该“意识到”视图(即控制器内没有 DOM 操作)?无论如何,我的问题是(是) ng-change 调用在指令内的 watch 被触发之前触发,除其他外,change-method 触发验证服务,以便它在指令有机会执行之前验证表单东西。反正我就是这么想的。
猜你喜欢
  • 1970-01-01
  • 2013-08-23
  • 1970-01-01
  • 2016-03-17
  • 2017-05-26
  • 2014-03-07
  • 2014-07-15
  • 1970-01-01
  • 2018-04-11
相关资源
最近更新 更多