AngularJS - Blur + Changed? - javascript

What is the easiest way to combine ng-changed and ng-blur?
I've found this post: How to let ng-model not update immediately?
However, this does no longer work in angluar 1.2+
Is there any way to achieve the same behavior?
I guess I have to store a copy of the old value myself and compare the new value to that on blur if I try to do the same, or is there any easier way ?

Use ng-model options.
Like this, ng-change will only trigger when the input is blurred.
<input type="text"
ng-model="a.b"
ng-model-options="{updateOn: 'blur'}"
ng-change="onchange()"/>

This does what I want.
It stores the value on focus, and compares it to the new value on blur, if changed, it triggers the expression in the attribute.
app.directive('changeOnBlur', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elm, attrs, ngModelCtrl) {
if (attrs.type === 'radio' || attrs.type === 'checkbox')
return;
var expressionToCall = attrs.changeOnBlur;
var oldValue = null;
elm.bind('focus',function() {
scope.$apply(function() {
oldValue = elm.val();
console.log(oldValue);
});
})
elm.bind('blur', function() {
scope.$apply(function() {
var newValue = elm.val();
console.log(newValue);
if (newValue !== oldValue){
scope.$eval(expressionToCall);
}
//alert('changed ' + oldValue);
});
});
}
};
});
usage:
<input ng-model="foo" change-on-blur="someFunc()" />

How about this solution. Works for me:
<input ng-init="oldValue = value" ng-model="value"
ng-blur="oldValue != value && callYourFunc(foo)">

I am using AngularJs 1.2.x and stumbled upon the ng-change issue of firing on each change. ng-blur can be used but it fires even though there is no change in the value. So both cannot be used efficiently.
With Angularjs 1.3.x, things are easier using ng-model-options like below
to invoke change function "onBlur"
ng-change="ctrl.onchange()" ng-model-options="{updateOn: 'blur'}"
And
to delay invocation of change function by 500ms
ng-change="ctrl.onchange()" ng-model-options='{ debounce: 500 }'"
Now coming to back to the question of getting such things with AngularJs 1.2.x
to invoke change function "onBlur"
html
<input type="text" ng-model="ctrl.a.c" sd-change-on-blur="ctrl.onchange()" />
or
<input type="text" ng-model="ctrl.a.c" sd-change-on-blur="ctrl.onchange(ctrl.a.c)" />
JS
app.directive('sdChangeOnBlur', function() {
return {
restrict: 'A',
scope: {
sdChangeOnBlur: '&'
},
link: function(scope, elm, attrs) {
if (attrs.type === 'radio' || attrs.type === 'checkbox')
return;
var parameters = getParameters(attrs.sdChangeOnBlur);
var oldValue = null;
elm.bind('focus', function() {
scope.$apply(function() {
oldValue = elm.val();
});
})
elm.bind('blur', function() {
scope.$apply(function() {
if (elm.val() != oldValue) {
var params = {};
if (parameters && parameters.length > 0) {
for (var n = 0; n < parameters.length; n++) {
params[parameters[n]] = scope.$parent.$eval(parameters[n]);
}
} else {
params = null;
}
if (params == null) {
scope.sdChangeOnBlur();
} else {
scope.sdChangeOnBlur(params)
}
}
});
});
}
};
});
function getParameters(functionStr) {
var paramStr = functionStr.slice(functionStr.indexOf('(') + 1, functionStr.indexOf(')'));
var params;
if (paramStr) {
params = paramStr.split(",");
}
var paramsT = [];
for (var n = 0; params && n < params.length; n++) {
paramsT.push(params[n].trim());
}
return paramsT;
}
to delay invocation of change function by 500ms
html
<input type="text" ng-model="name" sd-change="onChange(name)" sd-change-delay="300"/>
OR
<input type="text" ng-model="name" sd-change="onChange()" sd-change-delay="300"/>
JS
app.directive('sdChange', ['$timeout',
function($timeout) {
return {
restrict: 'A',
scope: {
sdChange: '&',
sdChangeDelay: '#' //optional
},
link: function(scope, elm, attr) {
if (attr.type === 'radio' || attr.type === 'checkbox') {
return;
}
if (!scope.sdChangeDelay) {
scope.sdChangeDelay = 500; //defauld delay
}
var parameters = getParameters(attr.sdChange);
var delayTimer;
elm.bind('keydown keypress', function() {
if (delayTimer !== null) {
$timeout.cancel(delayTimer);
}
delayTimer = $timeout(function() {
var params = {};
if (parameters && parameters.length > 0) {
for (var n = 0; n < parameters.length; n++) {
params[parameters[n]] = scope.$parent.$eval(parameters[n]);
}
} else {
params = null;
}
if (params == null) {
scope.sdChange();
} else {
scope.sdChange(params)
}
delayTimer = null;
}, scope.sdChangeDelay);
scope.$on(
"$destroy",
function(event) {
$timeout.cancel(delayTimer);
console.log("Destroyed");
}
);
});
}
};
}
]);
function getParameters(functionStr) {
var paramStr = functionStr.slice(functionStr.indexOf('(') + 1, functionStr.indexOf(')'));
var params;
if (paramStr) {
params = paramStr.split(",");
}
var paramsT = [];
for (var n = 0; params && n < params.length; n++) {
paramsT.push(params[n].trim());
}
return paramsT;
}
plnkrs for both approaches are
http://plnkr.co/edit/r5t0KwMtNeOhgnaidKhS?p=preview
http://plnkr.co/edit/9PGbYGCDCtB52G8bJkjx?p=info

how about this plunkr?
using angular's built in ng-blur, update your "persisted value" on blur
<input type="text" ng-model="boxValue" ng-blur="doneEditing(boxValue)">
when saving, verify the value is different
$scope.doneEditing = function(v) {
if (v !== $scope.persistedValue) // only save when value is different
$scope.persistedValue=v;
}
There is no special option on ng-blur for pre-checking equality that I'm aware of. A simple if seems to do the trick

Newer versions of AngularJS (now in 1.3 beta) supports this natively. See my answer here

The solution that worked for me was as follows:
<input id="fieldId" type="text"
ng-model="form.fields.thisField"
ng-model-options="{ updateOn: 'blur' }"
ng-change="form.save()" />
I found this solution here
That page says it requires AngularJS version 1.3.0+ . I tested it with AngularJS version 1.5.11 and it works in that version for me.

In app.html
<input type="text" name="name" ng-model="$ctrl.user.name" ng-blur="$ctrl.saveChanges()" ng-change="$ctrl.onFieldChange()"/>
in app.ts
public onFieldChange() {
this.isValuesModified = true;
}
//save changes on blur event
public saveChanges() {
//check if value is modified
if(this.isValuesModified){
//Do your stuff here
}
}

This is how I resolved this.
<input ng-focus="oldValue=value" ng-model="value" ng-blur="oldValue!=value && youFunction()" />

Related

How init a controller variable from the view? (Angular JS)

I'm working with a custom label witch controller contains a max value, this is for a horizontal bar that reperesents a percentage.
Since the max value for the horizontal bar it's defined dynamically I need to set it directly on the view, so when the ng-repeat starts, it set the caorrect max value.
Currently i have it like this (the view):
<div class="scroll">
<table class="tbody">
<tr ng-repeat="specialItem in specialItems">
<td class="tcellsmall alg_justify" title="{{specialItem.label | translate}}">
<label class='{{specialItem.class}}' ng-click="openLightbox(specialItem)">{{specialItem.label | translate}}</label>
</td>
<td class="tcell" ng-repeat="area in areas">
<span ng-if="specialItem[area] > 0">{{specialItem[area]}}</span>
<horizontal-bar type="{{item}}" value="{{specialItem[area]}}" ng-init="hValues.maxValue = specialItem.itemTotal " ng-if="specialItem[area] > 0" ng-cloak ng-click="clickMeter(area, specialItem.type, specialItem.custom)" area="{{area}}"/>
</td>
<td class="tcellsmall alg_right" ng-click="clickMeter('Plant', specialItem.type, specialItem.custom)">
<label class="cur_pointer">{{specialItem.itemTotal}}</label>
</td>
</tr>
</table>
</div>
And my controller is:
app.directive('horizontalBar', function () {
return {
restrict: 'E',
scope: {
type: '#',
value: '#',
barColor: '#',
threshold: '#', // only used by the Restriktionsverfolgung page
area: '#'
},
templateUrl: 'views/tools/horizontalBar/horizontalBar.html',
replace: true,
controller: ['$scope', '$rootScope', function ($scope, $rootScope) {
$rootScope.hValues = {};
var min = 0;
$rootScope.hValues.maxValue = 0;
var max = $rootScope.hValues.maxValue;
// var max = 300;
var ranges = new AssemblyLive.Models.RangeCollection();
// Init component
$scope.valuePercent = 0;
if ($scope.value !== undefined) {
$scope.$watchCollection('value', function (newValue) {
$scope.SetValue($scope.value);
});
}
// If data is found in the Config file, load it
if (Config.Areas[$scope.type] !== undefined) {
min = Config.Areas[$scope.type].minimum;
max = Config.Areas[$scope.type].maximum;
if (Config.Areas[$scope.type].ranges !== undefined) {
for (var u in Config.Areas[$scope.type].ranges)
ranges.Add(new AssemblyLive.Models.Range(parseFloat(Config.Areas[$scope.type].ranges[u].from), parseFloat(Config.Areas[$scope.type].ranges[u].to), Config.Areas[$scope.type].ranges[u].style));
}
}
//Functions
$scope.SetColor = function (color) {
$scope.backgroundColor = color;
};
$scope.SetValue = function (value) {
value = Math.round(value);
if (value <= min) value = min;
if (value >= max) value = max;
$scope.valuePercent = value / max * 100;
$scope.color = ranges.getValue($scope.valuePercent);
// Only for the Restriktionsverfolgung page
if ($scope.threshold !== undefined) {
if ($scope.valuePercent >= Number($scope.threshold))
$scope.color = "div_error";
else if ($scope.valuePercent >= Number($scope.threshold - 2))
$scope.color = "div_warning";
else
$scope.color = "div_success";
} else if (Utils.isEmpty($scope.color)) {
if (!Utils.isEmpty($scope.barColor)) {
$scope.color = $scope.barColor;
} else {
$scope.color = "grayfill";
}
} else {
$scope.color = $scope.barColor;
}
};
// Meter was clicked
$scope.clickMeter = function () {
if ($scope.type !== '') {
if (Config.Areas[$scope.type] !== undefined && Config.Areas[$scope.type].onClickURL !== undefined) {
window.location.href = Config.Areas[$scope.type].onClickURL;
}
}
};
}],
controllerAs: 'hmCtrl',
link: function ($scope, element, attrs) {
}
};
});
As you can see in the code, I have my $rootScope variables ready for set the value on the view, but it's not working.
Do you have any idea why?
----update----
Using a plugin for check the scopes in firefox, i can see that the max value is been set correctly.
Thanks
Using a plugin for check the scopes in firefox, i can see that the max value is been set correctly.

Angular Unit testing with Jasmine / Karma

I'm trying to write a unit test script for custom directive with jasmine and karma.
Current application is Angular 1.28; Issue is above code not running properly. Can anyone help/guide me that where i'm making mistake; or how to write unit test case for custom directives. Any suggestions or help will be greatly appreciated, thanks.
Directive as below:
angular.module('phone.filter', [])
.directive('phoneField', ['$filter', function ($filter) {
var phoneFilter = $filter('phoneFilter'),
reversePhone = $filter('reversePhone');
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var formatter = value => phoneFilter(value),
parser = function (value) {
var formatted = reversePhone(value);
element.val(phoneFilter(formatted));
//prevents a phone number from being all zeros
ngModel.$setValidity('phone', /^(?!0{10})(\d{10})?$/.test(value.replace(/\D/g, '')))
return formatted;
}
ngModel.$formatters.push(formatter);
return ngModel.$parsers.unshift(parser);
}
};
}]).filter('phoneFilter', function () {
return function (value) {
var len, val;
if (value) {
val = value.toString().replace(/\D/g, '');
len = val.length;
if (len < 4) {
return val;
} else if ((3 < len && len < 7)) {
return (val.substr(0, 3)) + '-' + (val.substr(3));
} else if (len >= 7) {
return (val.substr(0, 3)) + '-' + (val.substr(3, 3)) + '-' + (val.substr(6));
}
}
return value;
};
}).filter('reversePhone', function () {
return (value = '') => value.replace(/\D/g, '').substr(0)
})
Test scripts as below:
import chai, { expect } from 'chai'
// import { createElement } from '../helpers/directive-testing-helper'
/* import { stub } from 'sinon'
chai.use(require('sinon-chai'))
*/
const module = angular.mock.module
describe('Phone Field validation directive (phoneField)', () => {
let $rootScope,
$compile;
beforeEach(() => module('pru.com.components.phone.filter'))
beforeEach(inject((_$rootScope_, _$compile_) => {
$rootScope = _$rootScope_;
$compile = _$compile_;
}))
it('should not have all zeros', () => {
$rootScope.phone = "0002345678"
let element = $compile('<input type="text" name="phoneNumber" phone-field="" value="0002345678" ng-model="">')($rootScope);
$rootScope.$digest();
chai.expect(new RegExp(/phone-field/)).match(element.html()).to.be.true;
});
})

AngularJS - Show Custom Error Message using Directive

Directive to validate IP:PORT
myApp.directive("validateServerAddress", ['input', function (input) {
var linker = function (scope, element, attrs) {
alert('Tese');
var parts = input.split(":");
var ip = parts[0].split(".");
var port = parts[1];
return validateNum(port, 1, 65535) &&
ip.length == 4 &&
ip.every(function (segment) {
return validateNum(segment, 0, 255);
});
scope.validateNum = function(input, min, max) {
var num = +input;
return num >= min && num <= max && input === num.toString();
}
};
return {
restrict: "EA",
link: linker
};
}]);
HTML Input
<div class="input-group">
<input type="text" validate-serveraddress placeholder="255.255.255.255:5000" name="serveraddress" id="serveraddress" class="color-tooltip form-control" ng-model="serveraddress" required>
</div>
Until user types a valid IP:PORT address, I need to show custom error message 'Invalid Server Address'. The directive works well. Below could be used and custom class can be applied, but I need to show the message just like bootstrap shows for required field.
document.querySelector("input").oninput = function (e) {
this.className = validateIpAndPort(this.value) ? "" : "invalid";
}
.invalid {
color: red;
}
How can I show custom validation using ngMessages via directive as shown in below image ?
Quick directive to validate ip:port I created for you
app.directive("validateAddressPort", function() {
function validateIpAndPort(input) {
var parts = input.split(":");
var ip = parts[0].split(".");
var port = parts[1];
return validateNum(port, 1, 65535) &&
ip.length == 4 &&
ip.every(function (segment) {
return validateNum(segment, 0, 255);
});
}
function validateNum(input, min, max) {
var num = +input;
return num >= min && num <= max && input === num.toString();
}
return {
restrict: "A",
require: "ngModel",
link: function(scope, element, attributes, ngModel) {
ngModel.$validators.invalidaddressport = function(input) {
if(!input) {
return false;
}
return validateIpAndPort(input);
}
}
};
});
html:
<form name="myForm">
<div class="input-group">
<input type="text" ng-model="serveraddress" validate-address-port placeholder="255.255.255.255:5000" name="serveraddress" id="serveraddress" class="color-tooltip form-control" ng-model="serveraddress" required>
</div>
</form>
and now, you can use ngMessages like:
<div ng-messages="myForm.serveraddress.$error" style="color:red" role="alert">
<div ng-message="required">You did not enter a field</div>
<div ng-message="invalidaddressport">Invalid Address:port</div>
</div>
my plunkr: https://plnkr.co/edit/KI9jAqPRkLTYm5EvBKza?p=preview

Pass scope and parameters to directive - AngularJS

I'm trying to set a class in a ng-repeat with a directive.
I need to pass a parameter to this directive: wineQty
if I use scope: { wineQty: '=' } this works however $scope.bullet1Color is undefined in my html and thus doesn't affect the class that I want.
If I use scope: '#' I get the correct class however I can't specify wineQty
Is there a way to combine theses two syntaxes? so that I can access the scope and pass a paramater?
I've tried { wineQty: '#' } but with no luck sadly.
Here's my directive
angular.module('myApp').directive('wineQtyBullets', function () {
return {
restrict: 'A',
scope: { wineQty: '=', totalBullets: '=', numRedBullets: '=', numOrangeBullets: '#', bullet1Color: '#' },
link: function (scope, element, attrs) {
// defaults
var defaultNumRedBullets = 1;
var defaultNumOrangeBullets = 2;
var defaultTotalBullets = 12;
var defaultWineQty = 0;
// set values from attributes
var numRedBullets = scope.numRedBullets ? scope.numRedBullets : defaultNumRedBullets;
var numOrangeBullets = scope.numOrangeBullets ? scope.numOrangeBullets : defaultNumOrangeBullets;
var totalBullets = scope.totalBullets ? scope.totalBullets : defaultTotalBullets;
var wineQty = scope.wineQty ? scope.wineQty : defaultWineQty;
for (var currentBullet = 1; currentBullet <= totalBullets; currentBullet++) {
var bulletColorClass = 'bg-grey';
if (currentBullet <= wineQty) {
if (currentBullet <= numRedBullets) {
bulletColorClass = 'bg-red';
}
else if (currentBullet <= (numOrangeBullets + numRedBullets)) {
bulletColorClass = 'bg-orange';
}
else {
bulletColorClass = 'bg-green';
}
}
scope["bullet" + currentBullet + "Color"] = bulletColorClass;
}
console.log(scope.bullet1Color);
}
};
}
);
Here's my html
<div class="bullets pull-left ml15 mt6" wine-qty="wine.owned_qty" wine-qty-bullets>
<ul>
<li class="bullet"
ng-class="bullet1Color"></li>
</ul>
</div>
I managed to solve the problem, by changing scope to true and accessing the parameters through attrs
If this can help anyone here's the directive:
angular.module('myApp').directive('wineQtyBullets', function () {
return {
restrict: 'A',
scope: true,
link: function (scope, element, attrs) {
var options = { };
angular.forEach([
'numRedBullets',
'numOrangeBullets',
'totalBullets',
'wineQty'
], function (key) {
if (angular.isDefined(attrs[key]))
options[key] = attrs[key];
});
// defaults
var defaultNumRedBullets = 1;
var defaultNumOrangeBullets = 1;
var defaultTotalBullets = 12;
var defaultWineQty = 0;
// set values from attributes
var numRedBullets = parseInt(options.numRedBullets) ? parseInt(options.numRedBullets) : defaultNumRedBullets;
var numOrangeBullets = parseInt(options.numOrangeBullets) ? parseInt(options.numOrangeBullets) : defaultNumOrangeBullets;
var totalBullets = parseInt(options.totalBullets) ? parseInt(options.totalBullets) : defaultTotalBullets;
var wineQty = parseInt(options.wineQty) ? parseInt(options.wineQty) : defaultWineQty;
for (var currentBullet = 1; currentBullet <= totalBullets; currentBullet++) {
var bulletColorClass = 'bg-grey';
if (currentBullet <= wineQty) {
if (wineQty <= numRedBullets) {
bulletColorClass = 'bg-red';
}
else if (wineQty <= (numOrangeBullets + numRedBullets)) {
bulletColorClass = 'bg-orange';
}
else {
bulletColorClass = 'bg-green';
}
}
scope["bullet" + currentBullet + "Color"] = bulletColorClass;
}
}
};
});
Using "=" means 2-way data binding, and it's definitely fine.
The problem might be that your link function are executed only once at the very beginning, when it's possible that the values of your attributes are not yet assigned (may be caused by some AJAX calls).
I would suggest that you wrap all your link function into a scope.$watch function. Like:
link: function (scope, element, attrs) {
scope.$watch(function() {
return {
wineQty: scope.wineQty,
totalBullets: scope.totalBullets,
numRedBullets: scope.numRedBullets,
numOrangeBullets: scope.numOrangeBullets,
bullet1Color: scope.bullet1Color
}
}, function() {
// Your original code here.
})
}
Then your final result will automatically update if your directive got new attribute values.

Possible to use if/else statements for $scope.$watch?

I'm playing around with Angular and trying to see if the above is possible.
<div ng-controller="Ctrl">
<input ng-model="name">
<h1>{{name}}</h1>
<h1>{{age}}</h1>
</div>
var Ctrl = function($scope) {
$scope.name = "Kevin"
$scope.age = "26"
$scope.$watch('name', function() {
if($scope.name = 'Bob') {
$scope.age = '101';
};
});
};
However, doing this just sets the value of name to "Bob" instead of "Kevin".
What am I doing wrong?
Try to use == instead of = in this line: if($scope.name = 'Bob') {
:)
here is what you are looking for try this plunker
$scope.$watch('name', function(newval,old) {
// alert(old + newval)
if (newval == 'Kevin') {
$scope.age = 99;
} else if (newval == 'Bob') {
$scope.age = 101;
}
})
Try changing the input value from Kevin to Bob and vice versa for change
Callback inside watch function should take parameter like this:
$scope.$watch('name', function(newName) {
if (newName == 'Kevin') {
$scope.age = 99;
} else if (newName == 'Bob') {
$scope.age = 101;
}
})
Here's plunker: http://plnkr.co/edit/kTOXrSfRTXE7HCUsZfh2?p=preview
Changing $scope.name between 'Bob' and 'Kevin' affects $scope.age

Categories

Resources