I'm attempting to write a custom directive that validates a input field against other values that also needs to available inside the directive. I do this by using isolated scope with scope variables. More specifically I would like to compare the customer-price of a product (i.e. its netprice) with the purchase price and if the differece is negative (with the exception of the customer price being set to 0) I'd like to make the customer-price input (and its surrounding form) invalid. Here's my directive:
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();
});
}
};
}
}
And this is how I'm using it inside a a ng-form div surrounded by a form-tag:
<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" />
It works, after a fashion but the problem is that the validation part seems to be "timed wrong", meaning that if I enter a value that causes the "markup" to become negative the first time, then the form's $invalid value is set to false. But when next the input is negative then the validation will kick in and work. I think my problem is that I'm doing a lot of calculations in between the differen steps, but it's hard for me to know what's causing the validation to be so off. I guess I'd like someone with a deeper knowledge of the Angular JS mechanics to have gander and tell me if I'm doing something obviously wrong. Thanks in advance and sorry if my description is kind of vague.
Edit: Thought I'd also include the methods that are triggered on 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);
}
The validation service getter basically returns form.$valid and works perfectly fine for all my other custom validators.
Edit 2: Added screenshot that show that the surrounding ng-form tag seems to have its $invalid property set to true atleast:
Edit 3: Here's the transpiled 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; }());
...and here's a cut down version of my 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.
Try removing the isolate scope and evaluate the attributes directly:
The
input
,ng-model
, andng-change
directives expect an element without scope. This removes the one-time binding watchers and the complications of an isolate scope fighting those directives.