Directive doesn't update scope variable - angularjs

I have a directive that formats a phone number to (xxx) xxx-xxxx. The formatting works as expected, however, the model on the input field isn't updating unless you enter one character passed the standard 10. So after the user enters 10 digits the number automatically formats inside of the input field. But the scope variable set on ng-model does not save the formatting unless you enter an additional character.
Here's the directive:
(function (app) {
'use strict';
app.directive('formatPhone', [
function() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, elem) {
elem.on('keyup', function() {
var origVal = elem.val().replace(/[^\d]/g, '');
if(origVal.length === 10) {
var str = origVal.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
var phone = str.slice(0, -1) + str.slice(-1);
jQuery('#phonenumber').val(phone);
}
});
}
};
}
]);
}(window.app));
And here is the input field, using the directive as an attribute:
<input type="text" placeholder="Phone" name="phone" id="phonenumber" class="user-search-input create-short form-control" ng-model="userCtrl.userPost.user.phoneNumber" required format-phone>
I've tried adding a $watch to it, also with no luck.

Try like
.directive('formatPhone', [
function() {
return {
require: 'ngModel',
restrict: 'A',
priority:999999, //<-- Give a higher priority
scope: {
model:'=ngModel' //<-- A 2 way binding to read actual bound value not the ngModel view value
},
link: function(scope, elem, attrs, ctrl) {
var unwatch = scope.$watch('model', function(val){
if(!val) return;
var origVal = val.replace(/[^\d]/g, '');
if(origVal.length === 10) {
var str = origVal.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3');
var phone = str.slice(0, -1) + str.slice(-1);
ctrl.$setViewValue(phone);
ctrl.$render();
unwatch();
}
});
}
};
}
]);

Related

AngularJS Custom Validation Directives - How to avoid using isolated scope

I'm attempting to use multiple angular validators on a text field, but have run into the Multiple directives requesting isolated scope error. (Please read on before closing as a duplicate.)
All the solutions I've seen so far, recommend removing the scope: {...} from the offending directives, however for my scenario, I need to evaluate variables from the controller (and $watch them for changes).
I've tried using attrs.$observe, but I can't work out how to get the evaluated variables into the $validator function. (Additionally, I can't $observe the ngModel).
Please let me know if there's a another way to solve this issue.
Here's the smallest example I could put together. N.B. maxLength validator's scope is commented out, essentially disabling it:
angular
.module('app', [])
// validates the min length of a string...
.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: {
ngModel: '=',
minLen: '='
},
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('minLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.minLen) ||
angular.isUndefined(value) ||
value.length >= scope.minLen;
};
}
};
})
.directive("maxLen", function() {
return {
require: 'ngModel',
restrict: 'A',
// Commented out for now - causes error.
// scope: {
// ngModel: '=',
// maxLen: "="
// },
link: function(scope, element, attrs, ngModelCtrl) {
scope.$watch('ngModel', function(){
ngModelCtrl.$validate();
});
scope.$watch('maxLen', function(){
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.maxLength = function(modelValue, viewValue) {
var value = modelValue || viewValue;
return angular.isUndefined(scope.maxLen) ||
angular.isUndefined(value) ||
value.length >= scope.maxLen;
};
}
};
})
// this controller just initialises variables...
.controller('CustomController', function() {
var vm = this;
vm.toggleText = function(){
if (vm.text === 'aaa') {
vm.text = 'bbbbb';
} else {
vm.text = 'aaa';
}
}
vm.toggle = function(){
if (vm.minLen === 3) {
vm.minLen = 4;
vm.maxLen = 12;
} else {
vm.minLen = 3;
vm.maxLen = 10;
}
};
vm.toggleText();
vm.toggle();
return vm;
})
;
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.0/angular.min.js"></script>
<div ng-app="app" ng-controller="CustomController as ctrl">
<ng-form name="ctrl.form">
<label>Enter {{ctrl.minLen}}-{{ctrl.maxLen}} characters: </label>
<input
name="text"
type="text"
ng-model="ctrl.text"
min-len="ctrl.minLen"
max-len="ctrl.maxLen"
/>
</ng-form>
<br/><br/>
<button ng-click="ctrl.toggle()">Modify validation lengths</button>
<button ng-click="ctrl.toggleText()">Modify ngModel (text)</button>
<h3>Validation (just list $error for now)</h3>
<pre>{{ ctrl.form.text.$error | json }}</pre>
</div>
remove isolated scope
You dont need to observe, watch ngModel - when it changes, angular will run validator for you.
Decide how you want to use your directive: my-val-dir="{{valName}}" vs my-val-dir="valName". In first case you use attrs.$observe('myValDir'), in second $watch(attrs.myValDir) & $eval.
When your value gonna be something simple, like number or short string - first way seems good, when value is something big, i.e. array - use second approach.
Remove isolate scope and use scope.$watch to evaluate the attribute:
app.directive("minLen", function() {
return {
require: 'ngModel',
restrict: 'A',
scope: false,
// ngModel: '=',
// minLen: '='
// },
link: function(scope, element, attrs, ngModelCtrl) {
var minLen = 0;
scope.$watch(attrs.minLen, function(value){
minLen = toInt(value) || 0;
ngModelCtrl.$validate();
});
ngModelCtrl.$validators.minLength = function(modelValue, viewValue) {
return ngModelCtrl.$isEmpty(viewValue) || viewValue.length >= minLen;
};
}
};
})
There is no need to watch the ngModel attribute as the ngModelController will automatically invoke the functions in the $validators collection when the model changes.
When the watch expression is a string, the string attribute will be evaluated as an Angular Expression.
For more information, see AngularJS scope API Reference - $watch.

Allow commas and decimal points in input field AngularJS

I've come across an AngularJS filter than changes numeric values in a input field into a formatted number i.e 2500 becomes 2,500 and when applied to the modal it parses as an integer.
I was wonder if it was possible to add decimal points and keep the formatting for the user so 2500.24 becomes 2,500.24.
The script is as below:
angular.directive('format', function ($filter) {
'use strict';
return {
require: '?ngModel',
link: function (scope, elem, attrs, ctrl) {
if (!ctrl) {
return;
}
ctrl.$formatters.unshift(function () {
return $filter('currency')(ctrl.$modelValue, '', 2);
});
ctrl.$parsers.unshift(function (viewValue) {
var plainNumber = viewValue.replace(/[\,]/g, ''),
b = $filter('currency')(plainNumber, '', 2);
elem.val(b);
return plainNumber;
});
}
};
});
And the pattern I use is:
<input format pattern="[0-9]+([\.,][0-9]+)*" />
You can use the plugin ng-currency-mask.
https://github.com/vtex/ng-currency-mask
var app = angular.module('app', ['vtex.ngCurrencyMask']);
<input type="text" name="value" placeholder="0.00" ng-model="value" currency-mask />

How can I set the validity of an ng-model in a directive?

I have this oversimplified directive for getting a phone number, and I want the ng-model to be invalid if the phone number is less than 10 digits. I was testing out $setValidity but it's not says that is not a function.
angular.module('valkyrie').directive('phonenumber', function(){
return{
scope: {phonemodel: '=model'},
template: '<input ng-model="inputValue" type="tel" class="form-control">',
link: function(scope, element, attributes){
scope.$watch('inputValue', function(value, oldValue) {
value = String(value);
var number = value.replace(/[^0-9]+/g, '');
scope.phonemodel = number;
scope.phonemodel.$setValidity('phone', false);
console.log(scope.phonemodel);
});
},
}
});
As Aluan Haddad mentioned in the comments, you need to require the ngModel directive within your directive. That will give you access to the ngModelController, and then you can just do ngModelController.$setValidity.
See a working demo here: http://codepen.io/miparnisari/pen/LxPoVw.
angular.module('valkyrie', [])
.controller('parent', function ($scope) {
$scope.phone = '';
})
.directive('phonenumber', function() {
return {
require: 'ngModel',
template: '<input ng-model="inputValue" type="tel" class="form-control">',
link: function(scope, element, attributes, ctrl) {
scope.$watch('inputValue', function(value, oldValue) {
if (!value) return;
scope.ngModel = value.replace(/[^0-9]+/g, '');
if (scope.ngModel.length < 10)
ctrl.$setValidity('phoneLength', false);
else
ctrl.$setValidity('phoneLength', true);
});
},
};
});

Validation of complex form element

I have created a complex form element to avoid code duplication.
However I can't make it to behave same as normal input field.
HTML
<input name="first" ng-model="ctrl.first" type="text" required is-number />
<complex-input name="second" ng-model="ctrl.second" required></complex-input>
JS/ng
// main input directive
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='passedModel' is-number type='text' child-element /></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
// is number validator
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
// hacky directive to pass back validators from child field
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childValidators = modelCtrl.$validators;
}
}
});
When I run it content of both fields errors is following.
On init:
First: {"required":true}
Second: {"required":true}
If I enter string:
First: {"isNumber":true}
Second: {**"required":true**,"isNumber":true}
If I enter number:
First: {}
Second: {}
I would expect both input and complex-inputto behave same. Problem is obviously that is-number validation on inner input is blocking model on outer complex-input so it's value is not set, unless you enter number.
What am I doing wrong?
Is there a nicer/cleaner way to do this and possibly avoid the ugly childElement directive?
Please find test plnkr here: https://plnkr.co/edit/Flw03Je1O45wpY0wf8om
UPDATE: Complex input is not a simple wrapper for input. In reality in can have multiple inputs that together compile a single value.
You can solve both problems (the childElement and the correct validation) by letting complex-element be only a wrapper around the real input field.
To do this :
The complex-element directive has to use something else than name, for example "input-name"
The input in the complex-element directive template need to use that name
You need to pass from complex-element to the input field whatever else you need (validations, events etc..)
For example, the following is your code modified and works as you expect :
var app = angular.module("myApp", []);
app.controller("MyCtrl", function($scope){
var vm = this;
vm.first = "";
vm.second = "";
});
app.directive("complexInput", function(){
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel',
name: '#inputName'
},
template: "<div><input ng-model='passedModel' name='{{name}}' is-number type='text'/ required></div>",
link: function(scope, elem, attr, modelCtrl) {
angular.extend(modelCtrl.$validators, scope.childValidators || {});
}
}
});
app.directive('isNumber', function(){
return {
require: "^?ngModel",
link: function(scope, elem, attr, modelCtrl) {
modelCtrl.$validators.isNumber = function (modelValue, viewValue) {
var value = modelValue || viewValue;
return !isNaN(value);
};
}
}
});
HTML
<p>COMPLEX:<br/><complex-input required input-name="second" ng-model="ctrl.second"></complex-input></p>
See the plunker here : https://plnkr.co/edit/R8rJr53Cdo2kWAA7zwJA
Solution was to add ng-model-options='{ allowInvalid: true }' on inner input.
This forces inner input to update model even if it's invalid.
However more nicer solution would be to pass entire model from child elements to parent directive and then iterate through their $validators.
app.directive('childElement', function(){
return {
require: "^?ngModel",
priority: 10,
link: function(scope, elem, attr, modelCtrl) {
if (!modelCtrl) return;
scope.childModels.push(modelCtrl);
}
}
});
Complex input here has two inputs that combined give a final value.
First one has to be number but it's not required, while the second one is required.
app.directive("complexInput", function(){
function preLink(scope, elem, attr, modelCtrl) {
// create local scope value
scope.inner1 = "";
scope.inner2 = "";
scope.childModels = [];
}
// do some nice mapping of inner errors
function mapKeys(index, validator) {
return validator + "." + index;
}
function postLink(scope, elem, attr, modelCtrl) {
// copy value on change to passedModel
// could be complex state
scope.$watch('inner1', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.$watch('inner2', function(value){
scope.passedModel = scope.inner1 + scope.inner2;
});
scope.childModels.forEach(function(childModel, index){
childModel.$viewChangeListeners.push(function(){
Object.keys(childModel.$validators).forEach(function(validatorKey){
modelCtrl.$setValidity(mapKeys(index, validatorKey), !childModel.$error[validatorKey]);
});
});
});
}
return {
require: "^?ngModel",
scope: {
passedModel: '=ngModel'
},
template: "<div><input ng-model='inner1' is-number type='text' child-element /><br/><input ng-model='inner2' required type='text' child-element /></div>",
compile: function(element, attributes){
return { pre: preLink, post: postLink };
}
}
});

How can you limit the value from input using AngularJS?

I am looking for ways to limit the value inside the input to 4 and process the 4 digit value unto my controller.
<input type="number" class="form-control input-lg"
ng-model="search.main" placeholder="enter first 4 digits: 09XX">
{{ search.main | limitTo:4}}
Can always make a directive for it:
app.directive("limitTo", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
angular.element(elem).on("keypress", function(e) {
if (this.value.length == limit) e.preventDefault();
});
}
}
}]);
<input limit-to="4" type="number" class="form-control input-lg" ng-model="search.main" placeholder="enter first 4 digits: 09XX">
You can always use ng-minlength, ng-maxlength for string length and min, max for number limits. Try this
<input type="number"
name="input"
class="form-control input-lg"
ng-model="search.main"
placeholder="enter first 4 digits: 09XX"
ng-minlength="1"
ng-maxlength="4"
min="1"
max="9999">
DEMO FIDDLE
No need to use an AngularJS directive.
Just use the good old standard html attribute maxlength.
<input type="text" maxlength="30" />
I used the accepted answer as a launching point, but this is what I came up with.
angular.module('beastTimerApp')
.directive('awLimitLength', function () {
return {
restrict: "A",
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
attrs.$set("ngTrim", "false");
var limitLength = parseInt(attrs.awLimitLength, 10);// console.log(attrs);
scope.$watch(attrs.ngModel, function(newValue) {
if(ngModel.$viewValue.length>limitLength){
ngModel.$setViewValue( ngModel.$viewValue.substring(0, limitLength ) );
ngModel.$render();
}
});
}
};
});
usage
<input name="name" type="text" ng-model="nameVar" aw-limit-length="10"/>
The key is to use $setViewValue() and $render() to set and display the changes respectively. This will make sure $viewValue and $modelValue are correct and displayed properly. You also want to set ngTrim to false to prevent the user adding whitespaces beyond the limit. This answer is an amalgamation of #tymeJV's answer and this blog post... https://justforchangesake.wordpress.com/2015/01/10/useful-angularjs-directives/
Will do it allowing backspaces and deletes.
app.directive("limitTo", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
angular.element(elem).on("keydown", function() {
if(event.keyCode > 47 && event.keyCode < 127) {
if (this.value.length == limit)
return false;
}
});
}
}
}]);
you can use this code:
<input type="number" class="form-control input-lg"
ng-model="search.main"
ng-keypress="limitKeypress($event,search.main,4)"
placeholder="enter first 4 digits: 09XX">
and AngularJS code:
$scope.limitKeypress = function ($event, value, maxLength) {
if (value != undefined && value.toString().length >= maxLength) {
$event.preventDefault();
}
}
We can use ng-value instead:
ng-value="(minutes > 60 ? minutes = 0 : minutes)"
In html code:
<input type="text" class="form-control" ng-model="minutes" ng-maxlength="2" ng-pattern="/^[0-9]*$/" ng-value="(minutes > 60 ? minutes = 0 : minutes)"/>
This will check for the value and if it is greater than 60, it replaces the value with 0.
As there is a problem in above directive (answer of tymeJV). If you enter maximum length once, then it will not accept any other character even the backspace. That is if limit-to="4" and if you entered 4 character in input box, then you will not able to delete it. SO here is the updated answer.
app.directive("limitTo", [function () {
return {
restrict: "A",
link: function (scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
elem.bind('keypress', function (e) {
// console.log(e.charCode)
if (elem[0].value.length >= limit) {
console.log(e.charCode)
e.preventDefault();
return false;
}
});
}
}
}]);
Angular material has a directive mdMaxlength, if you want to cut off the input at this length, you can use this directive:
app.directive("forceMaxlength", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.mdMaxlength);
angular.element(elem).on("keydown", function() {
if (this.value.length >= limit) {
this.value = this.value.substr(0,limit-1);
return false;
}
});
}
}
}]);
Usage:
<input type="text" md-maxlength="30" force-maxlength=""/>
We can write the directive to listen to the keypress event. But for some old browsers, this event is not triggered. Thats why i created a directive in such a way that there's no dependency on browser specific events limitation.
I created an angular directive to limit the number of text in the input box.
'use strict';
angular.module("appName")
.directive("limitTo", [function() {
return {
restrict: "A",
require: "ngModel",
link: function(scope, elem, attrs, ctrl) {
var limit = parseInt(attrs.limitTo);
ctrl.$parsers.push(function (value) {
if (value.length > limit){
value = value.substr(0, limit);
ctrl.$setViewValue(value);
ctrl.$render();
}
return value;
});
}
}}]);
Usage: <input limit-to="3" ng-model="test"/> would allow only 3 characters in input box.
In Angular Js Material we can limit input field by "maxLimit", for example we need limit of input text should b 60 character as:
maxLimit ='60'
complete code:
<form-input-field flex
class="input-style-1"
title="Quick response tag title"
placeholder="Write a catchy title to help users get more information on the service card"
form-name="parent.qrtForm"
show-error="showError"
name="title"
maxLength="65"
text-limit="65"
required="true"
ng-model="newQrt.tagName">
Run this operation on any change to the number:
var log = Math.log(number) / Math.log(10);
if (log >= 4)
number = Math.floor(number / Math.pow(10, Math.ceil(log - 4)));
This will always limit "number" to 4 digits.
I am reiterating what #Danilo Kobold said.
I had to make sure that users can enter only numbers (0-9) [Without 'e' or '.' or '-'] and a limit of 4 values only.
app.directive("limitTo", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
var exclude = /Backspace|Enter/;
angular.element(elem).on("keydown", function(e) {
if (event.keyCode > 47 && event.keyCode < 58) {
if (this.value.length == limit) e.preventDefault();
} else if (!exclude.test(event.key)) {
e.preventDefault();
}
});
}
}
}]);
If you want to use only limit then use
app.directive("limitTo", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
var exclude = /Backspace|Enter/;
angular.element(elem).on("keydown", function(e) {
if (!exclude.test(event.key)) {
if (this.value.length == limit) e.preventDefault();
}
});
}
}
}]);
You can add more keys if you want to the exclude variable, like this:
var exclude = /Backspace|Enter|Tab|Delete|Del|ArrowUp|Up|ArrowDown|Down|ArrowLeft|Left|ArrowRight|Right/;
Got idea from this post
Hope I helped someone.
You can just use
ui-mask="9999"
as attribute in your view.
**
app.directive("limitTo", [function() {
return {
restrict: "A",
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.limitTo);
var exclude = /Backspace|Enter/;
angular.element(elem).on("keydown", function(e) {
if (e.keyCode > 47 && e.keyCode < 58) {
if (this.value.length == limit) e.preventDefault();
} else if (!exclude.test(e.key)) {
e.preventDefault();
}
});
}
}
}]);
**
Use this directive if you want to restrict max length for a input field which is present as part of custom directive template. This is the native html5 way of restricting max-lenth. This will also handle the copy paste case also to restrict till the maxlength on pasting.
app.directive('inputWrapper', function() {
return {
restrict: 'A',
template: "<input type='text'/>"
};
});
app.directive('maxInputLength', function() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
var limit = parseInt(attrs.maxInputLength);
angular.element(elem).find('input').attr('maxlength', limit);
}
};
});
<input-wrapper max-input-lenth="35"></input-wrapper>
Do this instead
<input type="text" class="form-control input-lg" ng-model="search.main" placeholder="enter first 4 digits: 09XX" maxlength="4" num-only>{{ search.main | limitTo:4}}
use the type "text", then use maxlength to limit it and also use the num-only option to make it number input only

Resources