Keeping Angular form validation DRY - angularjs

I have a form that has maybe 15-20 fields. Each one of them contains an attribute that looks something like this:
ng-class='addressForm.city.$invalid && addressForm.city.$touched ? "error" : ""'
So I have that long string of code repeated 15-20 times. This sets my DRY alarm bells off bigtime.
I could devise my own way to make this more DRY but I don't want to re-invent the wheel. Is there a gererally-accepted way of keeping Angular form validation DRY?

If it's just for styling use CSS.
Angular will add .ng-invalid and .ng-touched to input elements that are invalid and touched.
Or you could wrap the whole thing in a directive something like
angular.module('module').directive('errorClass', function(){
return{
require: 'ngModel',
link: function(scope, el, attr, model) {
function setClass() {
if(model.$touched && model.$invalid) {
if(!el.hasClass('error')) {
el.addClass('error');
}
} else {
el.removeClass('error');
}
}
scope.$watch(function(){ return model.$touched; }, setClass);
scope.$watch(function(){ return model.$invalid; }, setClass);
}
}
});
Also i havn't actually used this directive, so it may need some tweaking.

As #lain said, you don't have to add another class (like error) if the field is invalid, Angular adds that for you by default, it's just the name that differs (ng-invalid).
You can see how is that used here (the official form example from Angular).
If you still want to do this in your way, this is the implementation of my latest comment, using ngChange directive.
The html:
<input type="text" ng-model="addressForm.city" required ng-change="fieldChanged(this, 'city')">
The change event:
$scope.fieldChanged = function(el, fieldName){
if($scope.addressForm[fieldName].$invalid && $scope.addressForm[fieldName].$touched) angular.element(el).addClass('error');
else angular.element(el).removeClass('error');
}
This is not good practice (to manipulate the DOM in the controller), you should implement that in a directive, but binding a directive to each field would add watchers and I, personally, try to avoid as much as possible using too many watchers.
A more elegant option would be to combine this ngChange with ngClass or simply go with that simple DOM manipulation within controller. It's your choise :)

I ended up creating my own directive for this. I believe the following directive, when applied, will behave equivalently to this:
form(name='addressForm')
input(
type='text'
name='city'
ng-class='addressForm.city.$invalid && (addressForm.city.$touched || addressForm.$submitted) ? "error" : ""'
)
Instead of all that, I can do:
form(name='addressForm')
input(
type='text'
name='city'
validate-for='addressForm'
)
The directive will check validity on:
Blur
Form submission
Value change
Here's the code (ES6):
'use strict';
class ValidateFor {
constructor() {
this.restrict = 'A';
this.require = 'ngModel';
this.link = ($scope, $element, $attrs, ngModel) => {
var form = $scope[$attrs.validateFor];
var field = form[$element.attr('name')];
$scope.$on('form-submitted', () => {
this.checkForErrors(field, $element);
});
$scope.$watch(() => ngModel.$modelValue, () => {
if (field.$touched) {
this.checkForErrors(field, $element);
}
});
$element.bind('blur', () => {
this.checkForErrors(field, $element);
});
};
}
checkForErrors(field, $element) {
if (field.$invalid) {
$element.addClass('error');
} else {
$element.removeClass('error');
}
}
}
ValidateFor.$inject = [];
You could probably even eliminate the necessity for supplying the form name in validate-for. I just did it that way because I have some nested form situations.

valdr looks great. I haven't used it yet, but I will try it, and will update this post later.

Related

Modifying a directive to 'watch' a ctrl property?

I have a directive which checks if they click submit on a form. If so, it adds the 'submitted' class. This allows me to style the form inputs as red only when they have submitted a form (I hate having it red in real-time as they're typing).
'use strict';
app.directive('formSubmitted', function() {
return {
restrict: 'A',
require: 'form',
link: function(scope, element, attrs, ctrl) {
ctrl.$submitted = false;
element.on('submit', function() {
scope.$apply(function() {
ctrl.$submitted = true;
element.addClass('submitted');
});
});
}
};
});
The problem is 'resetting' a form once it has been submitted successfully... I need to remove the submitted class from inside the controller. I have tried a lot of ways to do this without success... as in this pseudo-code...
angular.element($scope.myForm).removeClass('submitted');
What I am thinking is instead of doing that from the controller (which doesn't work anyway), that I try to make the 'submitted' class mirror the $submitted property on ctrl... This way I could do...
$scope.myForm.$submitted = false and the class would update appropriately.
I have no idea even where to begin with though, and googling isn't helping...
Thanks!
A simple approach I have used in situations like this is leveraging the Angular ngClass directive and binding to a property on the controller that maintains whether the state is submitted or not. Something like so:
<button ng-click="isSubmitted = !isSubmitted">Submit</button>
<form ng-class="{submitted: isSubmitted}">
</form>
You can use the ng-class directive:
<form name="myForm" ng-class="{submitted: $submitted}">
See the doc here: https://docs.angularjs.org/api/ng/directive/ngClass
Within the controller handling the form submission, you certainly have a submit function:
$scope.submit = function (form) {
$scope.$submitted = true;
if (form.$invalid) {
return;
}
// Actually send data to backend, eventually receiving a promise
promiseFormBackend = MyService.sendForm();
promiseFromBackend.then(function () {
$scope.$submitted = false: // resetting the form class
});
}

AngularJS - how to focus on an element via a controller

I know the question has been asked multiple time, but I can't seem to find anywhere how to focus to an element from within a controller. What is the best approach? Would it be better to do a directive? But if so, then how would I call it within my controller? Or is it better to create a service then?
What I already have and works properly from within HTML code is a directive:
.directive('ngxFocus', ['$timeout', function($timeout) {
return function(scope, element, attr) {
$timeout(function () {
element.focus();
}, 10);
};
}])
Can I call directive within controller? I'm still learning AngularJS and I'm a bit confused on what the best approach is in this case. I really want to do it via the controller, at the moment I use a simple 1 line of jQuery to focus, but yeah it's not the Angular way and so I'd like to go with the correct way.
Note
To be more specific with an example, let say I have 10 inputs in the HTML and let say that inside the execution of a function (defined in the controller), I want to focus on 1 of the multiple inputs directly from the function (again this is all declared inside the controller). I would rather not write anything inside the HTML code, if possible, but instead call a focus function or something that will focus to the input I chose. I know I could write it simply in jQuery with $('input12').focus(); but I want to know how to do it the AngularJS way. All the answers I get so far are based on writing a Directive, that also equals to writing something inside the HTML, isn't there any other way???
Example
For more explicit example of my form... I have a first input connected to a Yahoo web service (stock market), this input filled by the user will hold a stock quotes symbol that can be anywhere in the world and then the user will choose (from a dropdown) his Bank account... now from there, my controller will check that the stock quotes market is in the same currency as the user's bank account (ex.: GOOG is US currency, if user's account is in $CAD, it will fail because GOOG is in $USD). If currency isn't the same, I want to advise my user and to do so I would seriously prefer to focus on the field so he could change his symbol if he made an error.
I you're trying to work with elements in controller, be sure you're going wrong, the controller's target in to bind data received from services to view, not to manipulate view.
If you want to focus on an element with route change:
app.directive('focuser', ['$location', function ($location) {
return {
restrict: 'A',
link: function ($scope, $element) {
$scope.$watch(function () {
//simply focus
$element.focus();
//or more specific
if ($location.$$url == '/specific/path') {
$element.focus();
}
});
}
};
}]);
I've made this directive:
app.directive('rfocus',function(){
return {
restrict: 'A',
controller: function($scope, $element, $attrs){
var fooName = 'setFocus' + $attrs.rfocus;
$scope[fooName] = function(){
$element.focus();
}
},
}
});
It adds to controller's $scope function to set focus on element. Name of the function is based on value given in attribute.
Using: <input type="text" rfocus="Input1"/> will create function setFocusInput1() which you can use in your controller.
Here is the fiddle: http://jsfiddle.net/aartek/2PJMQ/
I've recently started to learn Angular, too, but hopefully I can provide you a different way of approaching this.
I've been using some basic jQuery to focus, too, so in that regard, I can't really help you. However, with regard to calling a directive within a controller, I can't find any articles that say "yes, you can", or "no, you can't". I know that you can declare a controller within a directive, though, so you miiiiight be able to do something like this:
.directive('ngxFocus', function() {
return {
restrict: 'A',
controller: //do your controller stuff here
}])
I know it's an old question, but here's an other approach, where you set to true a variable in the controller, and that's this action that set the focus to your element.
Try this:
myAngularModule.directive('goFocus', ['$timeout', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attrs) {
scope.$watch(attrs.goFocus, function (newValue) {
if (newValue) {
$timeout(function () {
element[0].focus();
}, 100);
}
});
element.bind("blur", function (e) {
$timeout(function () {
scope.$apply(attrs.goFocus + "=false");
}, 10);
});
element.bind("focus", function (e) {
$timeout(function () {
scope.$apply(attrs.goFocus + "=true");
}, 10);
});
}
}
}]);
In HTML template:
<input go-focus="focusvar1" type="text" ng-model="mytext1" />
<input go-focus="focusvar2" type="text" ng-model="mytext2" />
<input go-focus="focusvar3" type="text" ng-model="mytext3" />
<button ng-click="doFocus()">OK</button>
In javascript angular controller:
myAngularModule.controller('myController', function () {
var self = this;
self.doFocus = function () {
// do some logic and focus your field
self.focusvar2 = true;
};
});

JQuery UI Spinner is not updating ng-model in angular

Angular's ng-model is not updating when using jquery-ui spinner.
Here is the jsfiddle http://jsfiddle.net/gCzg7/1/
<div ng-app>
<div ng-controller="SpinnerCtrl">
<input type="text" id="spinner" ng-model="spinner"/><br/>
Value: {{spinner}}
</div>
</div>
<script>
$('#spinner').spinner({});
</script>
If you update the text box by typing it works fine (you can see the text change). But if you use the up or down arrows the model does not change.
Late answer, but... there's a very simple and clean "Angular way" to make sure that the spinner's spin events handle the update against ngModel without resorting to $apply (and especially without resorting to $parse or an emulation thereof).
All you need to do is define a very small directive with two traits:
The directive is placed as an attribute on the input element you want to turn into a spinner; and
The directive configures the spinner such that the spin event listener calls the ngModel controller's $setViewValue method with the spin event value.
Here's the directive in all its clear, tiny glory:
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
Note that $setViewValue is intended for exactly this situation:
This method should be called when an input directive wants to change
the view value; typically, this is done from within a DOM event
handler.
Here's a link to a working demo.
If the demo link provided above dies for some reason, here's the full example script:
(function () {
'use strict';
angular.module('ExampleApp', [])
.controller('ExampleController', ExampleController)
.directive('jqSpinner', jqSpinner);
function ExampleController() {
var c = this;
c.exampleValue = 123;
};
function jqSpinner() {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, c) {
element.spinner({
spin: function (event, ui) {
c.$setViewValue(ui.value);
}
});
}
};
};
})();
And the minimal example template:
<div ng-app="ExampleApp" ng-controller="ExampleController as c">
<input jq-spinner ng-model="c.exampleValue" />
<p>{{c.exampleValue}}</p>
</div>
Your fiddle is showing something else.
Besides this: Angular can not know about any changes that occur from outside its scope without being aknowledged.
If you change a variable of the angular-scope from OUTSIDE angular, you need to call the apply()-Method to make Angular recognize those changes. Despite that implementing a spinner can be easily achieved with angular itself, in your case you must:
1. Move the spinner inside the SpinnerCtrl
2. Add the following to the SpinnerCtrl:
$('#spinner').spinner({
change: function( event, ui ) {
$scope.apply();
}
}
If you really need or want the jQuery-Plugin, then its probably best to not even have it in the controller itself, but put it inside a directive, since all DOM-Manipulation is ment to happen within directives in angular. But this is something that the AngularJS-Tutorials will also tell you.
Charminbear is right about needing $scope.$apply(). Their were several problems with this approach however. The 'change' event only fires when the spinner's focus is removed. So you have to click the spinner then click somewhere else. The 'spin' event is fired on each click. In addition, the model needs to be updated before $scope.$apply() is called.
Here is a working jsfiddle http://jsfiddle.net/3PVdE/
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
var mdlAttr = $(this).attr('ng-model').split(".");
if (mdlAttr.length > 1) {
var objAttr = mdlAttr[mdlAttr.length - 1];
var s = $scope[mdlAttr[0]];
for (var i = 0; i < mdlAttr.length - 2; i++) {
s = s[mdlAttr[i]];
}
s[objAttr] = ui.value;
} else {
$scope[mdlAttr[0]] = ui.value;
}
$scope.$apply();
}
}, 0);
});
Here's a similar question and approach https://stackoverflow.com/a/12167566/584761
as #Charminbear said angular is not aware of the change.
However the problem is not angular is not aware of a change to the model rather that it is not aware to the change of the input.
here is a directive that fixes that:
directives.directive('numeric', function() {
return function(scope, element, attrs) {
$(element).spinner({
change: function(event, ui) {
$(element).change();
}
});
};
});
by running $(element).change() you inform angular that the input has changed and then angular updates the model and rebinds.
note change runs on blur of the input this might not be what you want.
I know I'm late to the party, but I do it by updating the model with the ui.value in the spin event. Here's the updated fiddle.
function SpinnerCtrl($scope, $timeout) {
$timeout(function () {
$('#spinner').spinner({
spin: function (event, ui) {
$scope.spinner = ui.value;
$scope.$apply();
}
}, 0);
});
}
If this method is "wrong", any suggestions would be appreciated.
Here is a solution that updates the model like coder’s solution, but it uses $parse instead of parsing the ng-model parameter itself.
app.directive('spinner', function($parse) {
return function(scope, element, attrs) {
$(element).spinner({
spin: function(event, ui) {
setTimeout(function() {
scope.$apply(function() {
scope._spinnerVal = = element.val();
$parse(attrs.ngModel + "=_spinnerVal")(scope);
delete scope._spinnerVal;
});
}, 0);
}
});
};
});

Check duplicate values for a text filed in Angular form

Folks,
I have a text-field in a form. When a user enters a value in this text field,
I would like to check it against a array of values to ensure this value doesn't already exists.
I started off writing a directive and since I am new to this I am quiet lost
<input type="text" ng-model="user.name" placeholder="Enter Name" required checkDuplicateName existing-names="allUsers" tooltip/>
I the above code allUsers looks like this
var allUsers = ['john','james','Tim'];
My directive is as follows:
angular.module('formInputs').directive('checkDuplicateName', function() {
return {
restrict : 'A',
require : 'ngModel',
link: function(scope, element, attrs, ctrl) {
scope.$watch(attrs.ngModel,function(){
for (var i =0; i<attrs.existing-names.length;i++) {
if (attrs.existing-names[i] === attrs.ngModel) {
attrs.$set('tooltip','Name already exsists');
return;
}
}
});
}
}
});
The problem I am having is that the directive is not getting fired. What am i missing here ?
I've created a fiddle here that has a working version of your idea. It was just a couple small things, it doesn't look like you are passing your data correctly. When you access attrs it just gives you the string that is in the markup so attrs.ngModel will just give you "users.name" as a string.
I created an isolate scope so that you can see the scoping.
scope: {
model: '=ngModel',
users: '=existingNames'
},
Also, angular turns this-case-in-html into camel case in the js like: thisCaseInHtml
Edit: if you don't want to isolate scope, that's fine too. You just need to be evaluating the strings on the scope instead.
Here's an updated fiddle http://jsfiddle.net/ddJ4Z/10/ with the changes. Notice how the values are evaluated on the scope.
scope.$watch(attrs.ngModel,function(newVal){
var users = scope.$eval(attrs.existingNames) || [];
for (var i =0; i<users.length;i++) {
if (users[i] === newVal) {
window.alert(newVal);
return;
}
}
});
Hope this helps!

filters on ng-model in an input

I have a text input and I don't want to allow users to use spaces, and everything typed will be turned into lowercase.
I know I'm not allowed to use filters on ng-model eg.
ng-model='tags | lowercase | no_spaces'
I looked at creating my own directive but adding functions to $parsers and $formatters didn't update the input, only other elements that had ng-model on it.
How can I change the input of that I'm currently typing in?
I'm essentially trying to create the 'tags' feature that works just like the one here on StackOverflow.
I believe that the intention of AngularJS inputs and the ngModel direcive is that invalid input should never end up in the model. The model should always be valid. The problem with having invalid model is that we might have watchers that fire and take (inappropriate) actions based on invalid model.
As I see it, the proper solution here is to plug into the $parsers pipeline and make sure that invalid input doesn't make it into the model. I'm not sure how did you try to approach things or what exactly didn't work for you with $parsers but here is a simple directive that solves your problem (or at least my understanding of the problem):
app.directive('customValidation', function(){
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
modelCtrl.$parsers.push(function (inputValue) {
var transformedInput = inputValue.toLowerCase().replace(/ /g, '');
if (transformedInput!=inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
};
});
As soon as the above directive is declared it can be used like so:
<input ng-model="sth" ng-trim="false" custom-validation>
As in solution proposed by #Valentyn Shybanov we need to use the ng-trim directive if we want to disallow spaces at the beginning / end of the input.
The advantage of this approach is 2-fold:
Invalid value is not propagated to the model
Using a directive it is easy to add this custom validation to any input without duplicating watchers over and over again
I would suggest to watch model value and update it upon chage: http://plnkr.co/edit/Mb0uRyIIv1eK8nTg3Qng?p=preview
The only interesting issue is with spaces: In AngularJS 1.0.3 ng-model on input automatically trims string, so it does not detect that model was changed if you add spaces at the end or at start (so spaces are not automatically removed by my code). But in 1.1.1 there is 'ng-trim' directive that allows to disable this functionality (commit). So I've decided to use 1.1.1 to achieve exact functionality you described in your question.
A solution to this problem could be to apply the filters on controller side :
$scope.tags = $filter('lowercase')($scope.tags);
Don't forget to declare $filter as dependency.
If you are using read only input field, you can use ng-value with filter.
for example:
ng-value="price | number:8"
Use a directive which adds to both the $formatters and $parsers collections to ensure that the transformation is performed in both directions.
See this other answer for more details including a link to jsfiddle.
I had a similar problem and used
ng-change="handler(objectInScope)"
in my handler I call a method of the objectInScope to modify itself correctly (coarse input). In the controller I have initiated somewhere that
$scope.objectInScope = myObject;
I know this doesn't use any fancy filters or watchers... but it's simple and works great. The only down-side to this is that the objectInScope is sent in the call to the handler...
If you are doing complex, async input validation it might be worth it to abstract ng-model up one level as part of a custom class with its own validation methods.
https://plnkr.co/edit/gUnUjs0qHQwkq2vPZlpO?p=preview
html
<div>
<label for="a">input a</label>
<input
ng-class="{'is-valid': vm.store.a.isValid == true, 'is-invalid': vm.store.a.isValid == false}"
ng-keyup="vm.store.a.validate(['isEmpty'])"
ng-model="vm.store.a.model"
placeholder="{{vm.store.a.isValid === false ? vm.store.a.warning : ''}}"
id="a" />
<label for="b">input b</label>
<input
ng-class="{'is-valid': vm.store.b.isValid == true, 'is-invalid': vm.store.b.isValid == false}"
ng-keyup="vm.store.b.validate(['isEmpty'])"
ng-model="vm.store.b.model"
placeholder="{{vm.store.b.isValid === false ? vm.store.b.warning : ''}}"
id="b" />
</div>
code
(function() {
const _ = window._;
angular
.module('app', [])
.directive('componentLayout', layout)
.controller('Layout', ['Validator', Layout])
.factory('Validator', function() { return Validator; });
/** Layout controller */
function Layout(Validator) {
this.store = {
a: new Validator({title: 'input a'}),
b: new Validator({title: 'input b'})
};
}
/** layout directive */
function layout() {
return {
restrict: 'EA',
templateUrl: 'layout.html',
controller: 'Layout',
controllerAs: 'vm',
bindToController: true
};
}
/** Validator factory */
function Validator(config) {
this.model = null;
this.isValid = null;
this.title = config.title;
}
Validator.prototype.isEmpty = function(checkName) {
return new Promise((resolve, reject) => {
if (/^\s+$/.test(this.model) || this.model.length === 0) {
this.isValid = false;
this.warning = `${this.title} cannot be empty`;
reject(_.merge(this, {test: checkName}));
}
else {
this.isValid = true;
resolve(_.merge(this, {test: checkName}));
}
});
};
/**
* #memberof Validator
* #param {array} checks - array of strings, must match defined Validator class methods
*/
Validator.prototype.validate = function(checks) {
Promise
.all(checks.map(check => this[check](check)))
.then(res => { console.log('pass', res) })
.catch(e => { console.log('fail', e) })
};
})();
You can try this
$scope.$watch('tags ',function(){
$scope.tags = $filter('lowercase')($scope.tags);
});
I came here to look for a solution that would actively change an input text and mask it with * for all but last 4 digits as we type. This is achieved by $formatters
eg: Account Num input box: 1234567890AHSB1 should display in the input box as **********AHSB
Answer is just a slight variation of that given by #pkozlowski.opensource above.
angular.module('myApp').directive('npiMask', function() {
return {
require: 'ngModel',
link: function($scope, element, attrs, modelCtrl) {
modelCtrl.$formatters.push(function(inputValue) {
var transformedInput = inputValue.toString().replace(/.(?=.{4,}$)/g, '*');
if (transformedInput !== inputValue) {
modelCtrl.$setViewValue(transformedInput);
modelCtrl.$render();
}
return transformedInput;
});
}
};
});
<input-text
name="accountNum"
label="{{'LOAN_REPAY.ADD_LOAN.ACCOUNT_NUM_LABEL' | translate}}"
ng-model="vm.model.formData.loanDetails.accountNum"
is-required="true"
maxlength="35"
size="4"
npi-mask>
</input-text>

Resources