Angular 1.6 Пользовательская директива проверки с $setValidity

Я пытаюсь написать пользовательскую директиву, которая проверяет поле ввода на соответствие другим значениям, которые также должны быть доступны внутри директивы. Я делаю это, используя изолированную область видимости с переменными области видимости. В частности, я хотел бы сравнить клиентскую цену продукта (т. е. его чистую цену) с покупной ценой, и если разница отрицательна (за исключением того, что цена клиента установлена ​​​​на 0), я бы хотел, чтобы клиент - ввод цены (и окружающая ее форма) недействителен. Вот моя директива:

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();
                });
            }
        };
    }
}

И вот как я использую его внутри div ng-form, окруженного тегом формы:

<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);
}

Геттер службы проверки в основном возвращает 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>

I've put breakpoints inside the watch in the directive and for some weird reason the watch doesn't seem to trigger the first time I enter a new value into the customer-price input. Instead I find myself directly inside the changeDetected() method. I'm really confused now. I think the problem has something todo with the ng-change directive triggering before the validation. I probably has a faulty logic there which results in the isValid check of my validation service triggering before the directive has had time to actually alter the validity.


person Kristofer    schedule 25.12.2016    source источник
comment
Я не уверен, что понимаю, почему вы звоните $validate(). Кроме того, ng-change не вызывается, когда поле помечено как недопустимое, потому что ng-model не будет обновляться при недопустимом значении. Поэтому попробуйте вывести вне формы значение customerPrice/markupAmount/netPrice, чтобы увидеть, обновляется ли значение.   -  person Walfrat    schedule 25.12.2016
comment
Привет и спасибо, что нашли время, чтобы помочь мне. Я использовал console.log внутри часов, чтобы увидеть, что markupAmount и netprice обновляются каждый раз, когда я изменяю значение поля ввода цены клиента, чтобы эта часть, по крайней мере, работала как ожидалось (?). Мне кажется, что проверка всегда на один шаг позади, поэтому, если я начну с действительного ввода и изменю его на недопустимый (отрицательная разметка), форма все равно будет иметь $invalid=false. Действительно раздражает то, что div с ng-form, кажется, имеет $invalid=true Whick меня действительно смущает.   -  person Kristofer    schedule 25.12.2016


Ответы (2)


Попробуйте удалить область изоляции и оценить атрибуты напрямую:

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();
                });
            }
        };
    }
}

Директивы input, ng-model и ng-change предполагают наличие элемента без области видимости. Это устраняет одноразовые обязательные наблюдатели и сложности, связанные с изолированной областью действия, борющейся с этими директивами.

person georgeawg    schedule 25.12.2016
comment
Попытался удалить область изоляции и вместо этого использовать attrs.markupAmount и attrs.netPrice, но я все равно получаю то же поведение. Я не знал, что такое ng-model и ng-change, не ожидая масштаба. Я тоже не уверен, понимаю ли я это на самом деле. :( - person Kristofer; 25.12.2016

Я воспроизвел то, что, по моему мнению, делает ваша форма, и у меня не возникнет проблем, если я добавлю ng-change во все поля (vatRate, PurchasePrice, customerPrice).

Можете ли вы проверить, соответствует ли то, что я сделал, тому, что дает ваш машинописный текст? Если нет, можете ли вы попытаться показать нам результат в виде 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>

person Walfrat    schedule 25.12.2016
comment
Спасибо за помощь, я думаю, мне удалось сузить ее до моего метода ng-change customePriceChangeDetected, запускающего isValid-check моей службы проверки (формы) до того, как директива получила шанс фактически изменить действительность клиента - ввод цены. Думаю, мне нужно немного переосмыслить логику. - person Kristofer; 26.12.2016
comment
$watch запускается в конце углового цикла. Что странно, так это то, что с этим образцом я не воспроизвожу вашу проблему. Ваша логика мне тоже кажется странной, я бы просто передал весь ваш объект продукта вашей директиве и выполнил все операции внутри директивы (используйте двустороннюю привязку, чтобы иметь возможность обновлять netPrice/markupAmount) - person Walfrat; 26.12.2016
comment
Возможно ты прав. Дело в том, что я начал с расчетов, которые уже были в контроллере для productDetailsForm, а затем на более позднем этапе было добавлено требование, что вы не должны иметь возможность сохранять с отрицательной наценкой, поэтому я думал о моей директиве исключительно как о пользовательской проверке, а не о чем-то, что фактически выполняет какие-либо вычисления. - person Kristofer; 26.12.2016
comment
@Kristopher Ну, мне все еще интересно узнать, почему это не работает. Обратите внимание, что в некоторых случаях вы можете просто обрабатывать проверку в контроллере, после загрузки представления содержимого ваша форма будет находиться под this.<formName>.<inputName>. Это, очевидно, не рекомендуемый вариант в большинстве случаев, но маскирование вещи о чистой цене / наценке в директиве, где они необходимы для регистрации на более высоком уровне, также не кажется регулярным использованием этого. - person Walfrat; 26.12.2016
comment
Я все еще борюсь со всеми концепциями Angular 1.x (и у меня есть ощущение, что observables в Angular 2 доставит мне много горя), но, насколько я понимаю, контроллер не должен знать о представлении ( т.е. никаких DOM-манипуляций внутри контроллера)? В любом случае, моя проблема заключалась в том, что вызов ng-change срабатывает до того, как запускаются часы внутри директивы, и, среди прочего, метод изменения запускает службу проверки, чтобы она проверяла форму до того, как директива имела какой-либо шанс сделать это. вещи. Во всяком случае, я так понимаю. - person Kristofer; 26.12.2016
comment
@Kristofer, это правильно, никаких манипуляций с DOM, для этого есть директивы. Однако большинство контроллеров обычно используются только один раз во всем приложении. Таким образом, допустимо наличие контроллера, привязанного к представлению или требующего представления, имеющего форму ‹formName› с вводом ‹inputName›. Проблема с вашим кодом заключается в том, что вы просматриваете некоторые значения, которые обновляются после изменения другого, эти два уровня косвенности эффективно не являются стандартным использованием для angularjs. - person Walfrat; 26.12.2016
comment
Я, наверное, закончу тем, что выделю весь эконом-раздел формы в отдельный 1,5-компонент со своей логикой. Спасибо за ваш вклад, я ценю это. Я думаю, что у меня достаточно информации, чтобы найти рабочее решение, но, поскольку нет окончательного ответа на мой расплывчатый вопрос, который я могу отметить как таковой, я просто оставлю его как есть. - person Kristofer; 26.12.2016