AngularJS setting default values for directive - angularjs

In my parent controller:
//soemtimes view invoice has taxtotal defined
$scope.viewinvoice = {taxtotal:4}
//sometimes viewinvoice does not have taxtotal defined
$scope.viewinvoice = {}
//sometimes it is defined but equal to 0
$scope.viewinvoice = {taxtotal:0}
In my parent view:
<div class="span6">
<invoice invoice='viewinvoice'></invoice>
</div>
My directive:
.directive('invoice', [ function () {
return {
restrict: 'E',
scope: {
invoice:'='
},
replace: true,
template: '<div>
<input type="checkbox" ng-model="taxflag">
<div> {{ calculate_grand_total() }} </div>
</div>',
link: function($scope, element, attrs) {
}
};
}]);
In my directive I want to set $scope.taxflag true based on the property: $scope.invoice.taxtotal, the problem is if $scope.invoice.taxtotal is undefined I want to set the $scope.taxflag to false, and if the $scope.invoice.taxtotal is greater than 0 and is defined I want the $scope.taxflag set to true.
i.e.
if($scope.invoice.hasOwnProperty('taxtotal')){
if($scope.invoice.taxtotal > 0 ) {
$scope.taxflag = true;
} else {
$scope.taxflag = false;
}
} else {
$scope.invoice.taxtotal = 0;
$scope.taxflag = false;
}
I want this (above code) to act like 'initialization' code so whenever my 'viewinvoice' changes in the parent the $scope.taxflag and $scope.invoice.taxtotal will both be initially be set up correctly
I also want to trigger a change whenever the checkbox is checked:
i.e.
$scope.$watch('taxflag',function(newValue){
if(newValue) {
$scope.invoice.taxtotal = 5
} else {
$scope.invoice.taxtotal = 0;
}
});
I am also using this $scope.invoice.taxtotal elsewhere in a function {{ calculate_grand_total() }}
(in my directives view)
ie.
$scope.calculate_grand_total = function() {
return $scope.invoice.taxtotal + 5;
}
but this is unable to render because $scope.invoice.taxtotal is not defined (at least initially) !!
Does this make sense? I have tried so many different combinations but I can't seem to get it working as I want it to.

I've created this plunkr that tries to capture your question:
http://plnkr.co/edit/02QAC8m9xyF4pSyxnfOf
Basically, any code that depends on a value that can change should be in a watch. This means that your initialization code for setting taxflag belongs in a watch, so that it can update the tax flag if things change. This looks something like:
$scope.$watch('invoice.taxtotal', function(taxTotal) {
if (!angular.isNumber(taxTotal)) {
$scope.taxflag = false
return;
}
if (taxTotal > 0) {
$scope.taxflag = true;
} else {
$scope.taxflag = false;
}
});
Keep in mind watches are always executed the first time to initialize the values, so they essentially serve as both initialization code and update code.
As far as your calculate_grand_total function, if it is up to you to define what it should return if tax total or invoice are undefined. Simply check whether it is undefined and return the appropriate value, for example in my plunkr I return empty string:
$scope.calculate_grand_total = function() {
if ($scope.invoice && angular.isNumber($scope.invoice.taxtotal)) {
return $scope.invoice.taxtotal + 5;
} else {
return "";
}
}
I'm not sure if my plunkr works exactly like you want it or not, but it should get you started in the right direction. Or serve as a starting point for further clarification.

related:
if you only want to set first-run defaults you can use angular.merge
inside your link:
link: function(scope, element, attr, ctrl) {
// ....
var settings_default = {
setting1: true,
setting2: {
x: 0,
y: 10
},
};
// order is important. last item overwrites previous
scope.settings = angular.merge({}, settings_default, scope.settings);
// ....
})

Related

How to revalidate a form with multiple dependent fields?

I'm fairly new to Angular. I have a form where the user need to assign port numbers to 9 different port input fields (context: it's a form for a server environment configuration). The validation requirement is that no port number can be assigned twice, so each of the 9 port numbers needs to be unique.
For that, I have a custom validation directive called "srb-unique-port", which I assign to my input fields.
Directive:
(function () {
'use strict';
angular
.module('account')
.directive('srbUniquePort', [srbUniquePort]);
function srbUniquePort() {
return {
restrict: 'A',
require: 'ngModel',
scope: true,
link: function (scope, element, attrs, ngModel) {
ngModel.$validators.srbUniquePort = function (val) {
if (val == null || val == undefined || val == "" || val==0) return true;
var fieldName = attrs.name;
var configuration = scope.$eval(attrs.srbUniquePort);
var portFieldsToCheck = [
"myRestServicePort",
"myRestServicePortSSL",
"alfrescoPortHttp",
"alfrescoPortHttps",
"alfrescoPortTomcatShutdown",
"alfrescoPortAJP",
"alfrescoPortMySql",
"alfrescoPortJOD",
"alfrescoPortVti"
];
for (var i = 0; i < portFieldsToCheck.length; i++) {
if (fieldName!=portFieldsToCheck[i] && configuration[portFieldsToCheck[i]] == val) {
return false;
}
}
return true;
}
}
}
}
})();
HTML form (excerpt, just showing 2 of the 9 fields):
...
<md-input-container>
<label for="company" translate>COMPANY.CONFIGURATION.DBLIB_WEB_SRVC_PORT</label>
<input ng-model="vm.configuration.dblibWebSrvcPort" name="dblibWebSrvcPort" srb-unique-port="vm.configuration">
<div ng-messages="configurationForm.dblibWebSrvcPort.$error">
<div ng-message when="srbUniquePort">
<span translate>COMPANY.CONFIGURATION.VALIDATION.PORT_NOT_UNIQUE</span>
</div>
</div>
</md-input-container>
<md-input-container>
<label for="company" translate>COMPANY.CONFIGURATION.DBLIB_WEB_SRVC_PORT_SSL</label>
<input ng-model="vm.configuration.dblibWebSrvcPortSLL" name="dblibWebSrvcPortSLL" srb-unique-port="vm.configuration">
<div ng-messages="configurationForm.dblibWebSrvcPortSLL.$error">
<div ng-message when="srbUniquePort">
<span translate>COMPANY.CONFIGURATION.VALIDATION.PORT_NOT_UNIQUE</span>
</div>
</div>
</md-input-container>
...
It basically works for the field that I am current entering a value into. But the problem is that when I change the value of one input field, I need to re-validate all other depending fields as well. But I am not sure what the best way is in order to not run into an endless loop here, since all fields have the "srb-unique-port" assigned.
I already looked on StackOverflow and found this very similar question:
Angular directive with scope.$watch to force validation of other fields
with this plunker sample code:
http://plnkr.co/edit/YnxDDAUCS2K7KyXT1AXP?p=preview
but the example provided there is different: it's only about a password and a password repeat field, where only one field has the validation directive assigned.
So it differs from my case.
I tried to add this in my above code:
scope.$watch(ngModel, function (newValue, oldValue) {
ngModel.$validate();
});
but this causes endless loops (why does the ngModel frequently change here without any further action other than a validation which should always result to the same?).
This is the solution I ended up with. Looks a bit hacked to me, but it works.
(function () {
'use strict';
angular
.module('account')
.directive('srbUniquePort', [srbUniquePort]);
function srbUniquePort() {
return {
restrict: 'A',
require: 'ngModel',
scope: true,
link: function (scope, element, attrs, ngModel) {
function hasAValue(field) {
return !!field;
}
ngModel.$validators.srbUniquePort = function (val) {
var fieldName = attrs.name;
var configuration = scope.$eval(attrs.srbUniquePort);
var portFieldsToCheck = [
"dblibWebSrvcPort",
"dblibWebSrvcPortSLL",
"myRestServicePort",
"myRestServicePortSSL",
"alfrescoPortHttp",
"alfrescoPortHttps",
"alfrescoPortTomcatShutdown",
"alfrescoPortAJP",
"alfrescoPortMySql",
"alfrescoPortJOD",
"alfrescoPortVti"
];
configuration[fieldName] = val;
if (scope.$parent.configuration == undefined) {
scope.$parent.configuration = JSON.parse(JSON.stringify(configuration));
}
scope.$parent.configuration[fieldName] = val;
// compare each port field with each other and in case if equality,
// remember it by putting a "false" into the validityMap helper variable
var validityMap = [];
for (var i = 0; i < portFieldsToCheck.length; i++) {
for (var j = 0; j < portFieldsToCheck.length; j++) {
if (portFieldsToCheck[i] != portFieldsToCheck[j]) {
var iFieldHasAValue = hasAValue(scope.$parent.configuration[portFieldsToCheck[i]]);
var jFieldHasAValue = hasAValue(scope.$parent.configuration[portFieldsToCheck[j]]);
var valHasAValue = hasAValue(val);
if (iFieldHasAValue && jFieldHasAValue
&& scope.$parent.configuration[portFieldsToCheck[i]] == scope.$parent.configuration[portFieldsToCheck[j]]
) {
validityMap[portFieldsToCheck[i]] = false;
validityMap[portFieldsToCheck[j]] = false;
}
}
}
}
// in the end, loop through all port fields and set
// the validity here manually
for (var i = 0; i < portFieldsToCheck.length; i++) {
var valid = validityMap[portFieldsToCheck[i]];
if (valid == undefined) valid = true;
ngModel.$$parentForm[portFieldsToCheck[i]].$setValidity("srbUniquePort", valid);
}
// ending with the standard validation for the current field
for (var i = 0; i < portFieldsToCheck.length; i++) {
if (fieldName != portFieldsToCheck[i] && configuration[portFieldsToCheck[i]] == val) {
return false;
}
}
return true;
}
}
}
}
})();

Directive is not updating the view with async data, using controllerAs and bindToController

I'm having some trouble getting a directive to update my view.
In my controller I set the intial values for the attributes of the <tiki-list> directive. Then after, 2 seconds, I'm updating vm.listObjectSelected to test its async behaviour.
However, the view is not reflecting the update.
Controller:
var listObject = [{"display":"display1", "value":"value1"}, {"display":"display2", "value":"value2"}, {"display":"display3", "value":"value3"}]
vm.listObject = listObject
vm.listObjectSelected = [{"display":"display1", "value":"value1"}]
$timeout(function(){
vm.listObjectSelected = [{"display":"display1", "value":"value1"}, {"display":"display3", "value":"value3"}]
}, 2000)
HTML
<tiki-list max="" type="multi" list="editController.listObject" selected="editController.listObjectSelected"></tiki-list>
Directive
(function(){
'use strict';
angular.module("tiki").directive("tikiList", tikiList)
function tikiList(helper){
var directive = {
restrict:"EA",
scope:{
list: "=", //the object to repeat over, this contains 2 array's
retunObject: "=", //the array that is outputted
selected: "=", //preselected values
max: "=", //maximum range, other elements are greyed out, starts at 0
title:"#title", //the title of this list
type:"#type", //[single, multi]
},
templateUrl:"js/directive/list.html",
link:link,
bindToController: true,
controllerAs:"vm",
controller:controller
}
return directive
function link(scope, el, attr, ctrl){
scope.vm.onClick = onClick
// preprocess the "list" if there is a "selected" attribute
// the "selected" attribute is an object that contains the selected items
// return a "selectedItems" array containing the indeces of matching display names
// add the .active property to the "list" object to the correct indeces
if(scope.vm.selected){
var selectedItems = helper.isItemInList(helper.createArrayFromProperty(scope.vm.selected, "display"), helper.createArrayFromProperty(scope.vm.list, "display"))
for(var i = 0; i < selectedItems.length; i++){
scope.vm.list[selectedItems[i]].active = true
}
}
// add the property .disabled to the "list" if there is a max attribute
// the property will be added to all indeces that exceed the max value
if(scope.vm.max){
for(var y = 0; y < scope.vm.list.length; y++){
if(y >= scope.vm.max){
scope.vm.list[y].disabled = true
}
}
}
function onClick(index){
// only allow items that are in range of the "max" attribute are allowed to be clicked
if(!scope.vm.max || index < scope.vm.max){
if(scope.vm.type === "single"){
angular.forEach(scope.vm.list, function(val, key){
scope.vm.list[key].active = false
})
scope.vm.list[index].active = true
}
if(scope.vm.type === "multi"){
scope.vm.list[index].active = !scope.vm.list[index].active
}
}
}
scope.vm.listing = scope.vm.list
}
}
controller.$inject = [];
function controller(){
}
})()
Directive template
<ul class="listOptions">
<li class="listOptions-title" ng-class="{'show':title}">{{vm.title}}</li>
<li ng-click="vm.onClick($index)" ng-class="{'active':list.active, 'disabled':list.disabled}" ng-repeat="list in vm.listing track by $index">{{list.display}}</li>
</ul>
I think it has something to do with controllerAs but I can't wrap my head around it.
thx,
I think the reason is that the Array is a reference type. When you change the data in service or at async step, the data point goes to a new location in memory, but the data in directive or controller doesn't change.
Instead of writing your function like this:
$timeout(function(){
vm.listObjectSelected = [{"display":"display1", "value":"value1"}, {"display":"display3", "value":"value3"}]
}, 2000)
You should try doing it this way:
$(timeout(function(){vm.listObjectSelected.push({you need data here})},200)
or you can use a promise ,you can return a promise and get it in directive ,use
promise.then(function(){//let data = service.data again}
Hope this can help you.

Update view with formatted value, model with numeric value

I have a need for input boxes to display values formatted based on the user's locale but the model must store the value in en-US locale and all of this happens on blur. I've got the formatting of the fields working when the user clicks off of them but I cannot figure out how to set the model value. In my code formatedValue is being set correctly for the user to view but how do I update the model value to be "valueToFormat"? I've tried
scope.$modelValue = valueToFormat;
and it works when watching it thru the debugger but as soon as the view is rendered the value reverts to the $viewValue. How can I accomplish this?
element.bind('blur', function () {
var val = ctrl.$modelValue;
parse(val);
})
ctrl.$formatters.push(function(value) {
if(!value) {
return value;
}
var valueToFormat = getActualValue(value, decimalDelimiter, thousandsDelimiter, decimals, '%') || '0';
return viewMask.apply(prepareNumberToFormatter(valueToFormat, decimals));
});
function parse(value) {
if(!value) {
return value;
}
var valueToFormat = getActualValue(value, decimalDelimiter, thousandsDelimiter, decimals) || '0';
var formatedValue = viewMask.apply(prepareNumberToFormatter(valueToFormat, decimals));
var actualNumber = parseFloat(modelMask.apply(valueToFormat));
ctrl.$viewValue = formatedValue;
ctrl.$render();
return valueToFormat;
}
You should be able to use a filter to solve this issue. For example this is a date filter that we use. This is with typescript so a little tweaking will be necessary for straight java-script but it gets the point across.
app.filter('MyDateFilter', ['$filter', function ($filter: ng.IFilterService)
{
return function (value:any, format)
{
if (typeof value == "string")
{
var stringValue = <string>value;
//If there is no / or - then assume its not a date
if (stringValue.indexOf('/') == -1 && stringValue.indexOf('-') == -1)
return value;
}
var parsedDate = Date.parse(value);
if (isNaN(parsedDate))
return value;
else
return $filter('date')(value, format);
}
}]);
And then in the html.
<div ng-repeat="value in values" >
{{value | MyDateFilter:"MM/dd/yyyy" }}
</div>
Since it seems like you want to change an input display value. here is a solution we use for that. I created a custom directive and inside of there you can manipulte the $render. So:
app.directive("myDateDisplayInput", ['$filter', function DatepickerDirective(filter: ng.IFilterService): ng.IDirective
{
return {
restrict: 'A',
require: 'ngModel',
link: (scope: ng.IScope, element, attrs, ngModelCtrl: ng.INgModelController) =>
{
ngModelCtrl.$render = () =>
{
var date = ngModelCtrl.$modelValue ?
filter('date')(ngModelCtrl.$modelValue, "MM/dd/yyyy") :
ngModelCtrl.$viewValue;
jqueryElement.val(date);
};
}
};
}
This will format the value to be what you want. If you only want this to happen on blur then you add
ng-model-options="{ updateOn: 'blur' }"
<input type="text" my-date-display-input ng-model-options="{ updateOn: 'blur' }" ng-model="unformattedDate"/>
Some pieces might be missing but this should get the idea across.

AngularJS ngClass not working in ngRepeat in custom directive inside ngRepeat

Here's a plunker:
http://plnkr.co/edit/hJsZFCBZhFT5rRo1lgSZ?p=preview
It's a simple directive that is supposed to show a given number of dots and highlight a subset of them based on the value, min, and max given, similar to a star rating. It works great when on its own (as you can see in the last row of the plunkr), but not in an ngRepeat. The problem is the ngClass on the directive's own ngRepeat seems to fail, even though the expression used in the ngClass seems to be evaluating correctly outside of the ngClass. I've been tearing my hair out over this for the last few hours, and I just can't figure it out.
Here's the actual code:
HTML:
<div ng-repeat="row in [1,2,3,4,5]">
<match-dots value="{{ $index+1 }}" min="0" max="10">{{ isDotActive($index) }}</match-dots>
</div>
JS:
angular.module('app', [])
.directive('matchDots', function() {
return {
restrict: 'EA',
replace: true,
transclude: true,
template:
"<span class='match-dots-group'>" +
"<span class='match-dots-dot' ng-repeat='dot in dotsList()' ng-class='{ active: isDotActive($index) }' ng-transclude></span>" +
"</span>",
scope: {
dots: '#',
min: '#',
max: '#',
value: '#'
},
link: function(scope, elem) {
// save the previous class applied so we can remove it when the class changes
var previousClass = '';
/**
* set the class for the dots-group based on how many dots are active (n-active)
*/
scope.setDotsClass = function() {
if(previousClass != '') {
elem.removeClass(previousClass);
}
previousClass = "active-"+scope.numActiveDots();
elem.addClass(previousClass);
}
},
controller: function($scope) {
$scope.dots = angular.isUndefined($scope.dots)?5:parseInt($scope.dots);
$scope.min = angular.isUndefined($scope.min)?0:parseInt($scope.min);
$scope.max = angular.isUndefined($scope.max)?100:parseInt($scope.max);
/**
* create a list of numbers
* #returns {Array}
*/
$scope.dotsList = function() {
var arr = [];
for(var i=1; i<=$scope.dots; i++) {
arr.push(i);
}
return arr;
};
/**
* should the given dot be lit up?
* #param {number} dot
* #returns {boolean}
*/
$scope.isDotActive = function(dot) {
return dot < $scope.numActiveDots();
};
/**
* How many dots are active?
* #returns {number}
*/
$scope.numActiveDots = function() {
var activeDots = Math.ceil(($scope.value-$scope.min) / (($scope.max-$scope.min) / $scope.dots));
return isNaN(activeDots)?0:activeDots;
};
// make sure the value is an number
$scope.$watch('value', function(newValue) {
$scope.value = parseFloat(newValue);
$scope.setDotsClass();
});
// make sure the number of dots is actually a positive integer
$scope.$watch('dots', function(newDots) {
if(angular.isUndefined(newDots) || !newDots || newDots < 1) {
$scope.dots = 5;
} else {
$scope.dots = parseInt(newDots);
}
$scope.setDotsClass();
});
// make sure the min is not greater than the max
$scope.$watch('max', function(newMax) {
if(angular.isUndefined(newMax)) {
$scope.max = newMax = 100;
}
if(angular.isString(newMax)) {
$scope.max = parseFloat(newMax);
} else if(newMax < $scope.min) {
$scope.max = $scope.min;
$scope.min = newMax;
}
$scope.setDotsClass();
});
// make sure the max is not less than the min
$scope.$watch('min', function(newMin) {
if(angular.isUndefined(newMin)) {
$scope.min = newMin = 0;
}
if(angular.isString(newMin)) {
$scope.min = parseFloat(newMin);
} else if(newMin > $scope.max) {
$scope.min = $scope.max;
$scope.max = newMin;
}
$scope.setDotsClass();
});
}
}
});
The first thing that looked suspicious to me is that you were reassigning the $scope.dots property and your numActiveDots function, quite often was ending up with NaN. That property is passed into your directive as a string, then you are parsing/reassigning it as number. I tried renaming your parsed value to $scope.dotsinternal. That seems to make it work.
http://plnkr.co/edit/STwuts2D169iBLDoTtE2?p=preview
Change this:
$scope.dots = angular.isUndefined($scope.dots)?5:parseInt($scope.dots);
to this:
$scope.dotsinternal = angular.isUndefined($scope.dots)?5:parseInt($scope.dots);
then updated all the references to $scope.dots to $scope.dotsinternal.

Why is ng-maxlength not re-evaluated?

I have a form, that has one input field and three check boxes. Depending on which check box is selected the max length on the field needs to change. I have a input field defined like this
<input placeholder="ID" type="text" id="form_ID" name="searchId" autofocus
data-ng-model="vm.searchCriteria.searchId" data-ng-required="vm.isSearchIdRequired"
data-ng-minlength="1" data-ng-maxlength="{{searchIdMaxLength}}"
data-ng-class="{'input-error': vm.isSearchIdValid}">
and one of the checkboxes
<input type="checkbox" id="checkbox1" class="hidden-field"
data-ng-model="vm.searchCriteria.searchIdInSrId" data-ng-checked="vm.searchCriteria.searchIdInSrId"
data-ng-change="processSearchIdOptionsChange()">
So everytime user changes which checkbox is/are selected processSearchIdOptionsChange gets called, and searchIdMaxLength changes it's value. This is all working fine and I can see the value being changed on the $scope. But, my initial max length is still being applied. Following error pops up after initial max number of chars is reached. Why?
<span class="error" data-ng-show="(searchForm.$dirty && searchForm.searchId.$error.maxlength)">Too long!</span>
This is the intended behaviour of ng-maxlength. Verified from source : https://github.com/angular/angular.js/blob/master/src/ng/directive/input.js?source=c#L523
The value is parsed only once and cached :
var maxlength = int(attr.ngMaxlength);
If you want to observe the change you need to create your own directive which uses something like
scope.$watch(attr.namespaceMaxLength,function(){
// clear old validator. Add new one.
})
After a lot of trial and error here is the directive that does what I need. Please if you have any suggestions or improvements share them, I have only 7 days of angular under my belt, and javascript is something that i have a cursory knowledge of.
(function () {
'use strict';
angular.module('commonModule')
.directive('srMaxlength', ['$window', srMaxlength]);
function srMaxlength($window) {
// Usage:
// use if you need to switch max length validation dynamically based on
// Creates:
// removes old validator for max length and creates new one
var directive = {
require: 'ngModel',
link: link,
restrict: 'A'
};
return directive;
function link(scope, element, attrs, ctrl) {
attrs.$observe("srMaxlength", function (newval) {
var maxlength = parseInt(newval, 10);
var name = "srMaxLengthValidator";
for (var i = ctrl.$parsers.length - 1; i >= 0; i--) {
if (ctrl.$parsers[i].name !== undefined && ctrl.$parsers[i].name == name) {
ctrl.$parsers.splice(i, 1);
}
}
for (var j = ctrl.$formatters.length - 1; j >= 0; j--) {
if (ctrl.$formatters[j].name !== undefined && ctrl.$formatters[j].name == name) {
ctrl.$formatters.splice(j, 1);
}
}
ctrl.$parsers.push(maxLengthValidator);
ctrl.$formatters.push(maxLengthValidator);
//name the function so we can find it always by the name
maxLengthValidator.name = name;
function maxLengthValidator(value) {
if (!ctrl.$isEmpty(value) && value.length > maxlength) {
ctrl.$setValidity('maxlength', false);
return undefined;
} else {
ctrl.$setValidity('maxlength', true);
return value;
}
}
});
}
}
})();

Resources