Reputation: 24581
In the most primitive angular app I am trying to create a directive for an input field which changes parent's ng-model value.
HTML:
<form novalidate>
<input ng-model="ctrl.myvalue" mydirective minlength="19" />
{{ ctrl.myvalue }}
</form>
JS:
var app = angular.module('app', []);
app.directive('mydirective', function(){
return {
scope: { ngModel: '=' },
link: function(scope, el) {
el.on('input', function(e) {
this.value = this.value.replace(/ /g,'');
scope.ngModel = this.value;
})
}
}
})
app.controller('MyController', function(){
this.myvalue = '';
})
The problem is that if I use this directive together with minlength
or pattern
for an input validation it gets a specific behavior: every second letter you type in the input disappears; also ng-model
gets undefined
value. Without validations the code works perfectly.
I also tried to create a custom validation as a workaround but it has the same effect.
Could you explain that or propose the way to go?
Upvotes: 3
Views: 1808
Reputation: 4689
You can use unsift, as well render for the first iteration.
Usually you can use ctrl.$setViewValue
but you can be sure no relaunch when the value don;t change...
var testModule = angular.module('myModule', []);
testModule.controller('testCntrl', ['$scope', function ($scope) {
$scope.test = 'sdfsd fsdf sdfsd sdf';
}]);
testModule.directive('cleanSpaces', [function () {
return {
require: '?ngModel',
link: function (scope, $elem, attrs, ctrl) {
if (!ctrl) return;
var filterSpaces = function (str) {
return str.replace(/ /g, '');
}
ctrl.$parsers.unshift(function (viewValue) {
var elem = $elem[0],
pos = elem.selectionStart,
value = '';
if (pos !== viewValue.length) {
var valueInit = filterSpaces(
viewValue.substring(0, elem.selectionStart));
pos = valueInit.length;
}
//I launch the regular expression,
// maybe you prefer parse the rest
// of the substring and concat.
value = filterSpaces(viewValue);
$elem.val(value);
elem.setSelectionRange(pos, pos);
ctrl.$setViewValue(value);
return value;
});
ctrl.$render = function () {
if (ctrl.$viewValue) {
ctrl.$setViewValue(filterSpaces(ctrl.$viewValue));
}
};
}
};
}]);
http://jsfiddle.net/luarmr/m4dmz0tn/
UPDATE I update the fiddle with the last code and a validation example in angular and update the html with ng-trim (ngModel.$parsers ingore whitespace at the end of ng-model value).
Upvotes: 2
Reputation: 6958
Use Angular's NgModelController. I'm just adding to the $parsers (the functions that execute as the view is updated, but before the value is persisted to the model). Here, I am pushing the function onto the $parsers pipeline. Keep in mind that the model won't be populated until the minlength validation has been satisfied. The code snippet shows both the $viewValue and the modelValue
var app = angular.module('app', []);
app.directive('mydirective', function() {
return {
require: 'ngModel',
priority: 100,
link: function(scope, el, attrs, ngModelCtrl) {
// $parsers from view/DOM to model
ngModelCtrl.$parsers.push(function(value) {
console.log(value);
return value && value.replace(/ /g, '');
});
}
}
})
app.controller('MyController', function() {
this.myvalue = '';
})
<script src="https://code.angularjs.org/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="MyController as ctrl">
<form name="myForm" novalidate>
<input ng-model="ctrl.myvalue" name="myValue" mydirective minlength="19" /><br /><br />Model Value: {{ ctrl.myvalue }}<br /><br />
View Value: {{ myForm.myValue.$viewValue }}
</form>
</div>
Update: If you are trying to perform custom validation, just forget about the minlength/required stuff and just write your own. It's probably not the nicest behavior to alter the text as the user types. This example will put spaces into the viewValue on the blur event. I still think ngModelController is the way to go, but I don't know enough of what you are trying to accomplish to give you something closer to what you are looking for.
var app = angular.module('app', []);
app.directive('creditCardValidator', function() {
return {
require: 'ngModel',
priority: 100,
link: function(scope, el, attrs, ngModelCtrl) {
// 16 characters
attrs.$set('maxlength', 16);
var noSpaces = function noSpaces(value) {
return value.replace(/ /g, '');
}
var withSpaces = function withSpaces(value) {
if (ngModelCtrl.$isEmpty(value)) {
return;
}
var spacedValue = value.replace(/(\d{4})(\d{4})(\d{4})(\d{4})/, '$1 $2 $3 $4');
return spacedValue || undefined;
}
ngModelCtrl.$parsers.push(noSpaces);
ngModelCtrl.$formatters.push(withSpaces);
ngModelCtrl.$validators.validCreditCard = function(modelValue, viewValue) {
var value = noSpaces(modelValue || viewValue);
var valid = /^\d{16}$/.test(value);
return valid;
};
el.on('blur', function() {
if (ngModelCtrl.$valid) {
ngModelCtrl.$setViewValue(withSpaces(ngModelCtrl.$modelValue));
ngModelCtrl.$render();
}
});
}
}
})
app.controller('MyController', function() {
this.myvalue = '';
})
<script src="https://code.angularjs.org/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="MyController as ctrl">
<form name="myForm" novalidate>
<input ng-model="ctrl.myvalue" name="myValue" ng-model-options="{allowInvalid: true}" credit-card-validator />
<br />
<br />Model Value: {{ ctrl.myvalue }}
<br />
<br />View Value: {{ myForm.myValue.$viewValue }}
<br />
<br />Error: {{ myForm.myValue.$error }}
</form>
</div>
Upvotes: 2