Angular directive making required field readonly - angularjs

I'm trying to make a input field required and readonly.
The html standard does not allow this, so I tried to make my own directive for it, but the keydown/keypress bind is never made. Is there a way around this and why this does not work?
ngApp.directive('required', function() {
return {
require: '?input',
restrict: 'A',
link: function($scope,$element,$attr) {
if ($attr.hasOwnProperty('readonly')) {
$element.removeAttr('readonly');
$element.bind('keydown keypress', function(e){
e.preventDefault();
});
}
}
};
});
plnkr.co/edit/XhTxPpqnKwSFKJflSHZW?p=preview .... second text field, required is never validated by the system!
cheers
c_bb

EDIT: I just re-read your question and thought about it again, and my original answer is crap. You really do want novalidate on your form, and then you show errors in an angular type way rather than trying to use html form validation, which isn't going to work for you for the reasons you mention in the comments.
You want your form to looks something like this:
<form name="testForm" ng-submit="submitForm(testForm.$valid)" novalidate >
<input type="text" name="value" ng-model="test1" required><br>
<input type="text" name="value2" ng-model="test2" required readonly><br>
<p ng-show="testForm.value2.$error.required" class="help-block">Please select a whatever</p>
<input type="submit" ng-click='testForm.$valid && mySubmitFunction()'>
</form>
This will give you an error message based on the stock angular validation for required.
and you need some kind of a submit handler in your controller, like this:
$scope.submitForm = function(isValid) {
// check to make sure the form is completely valid
if (isValid) {
alert('our form is amazing');
} else {
console.log('fail')
}
};
Do whatever sensible things you need in the submit function.
//OLD, TERRIBLE ANSWER BELOW
If the readonly attribute does indeed override the required attribute the 'proper' solution would probably to write some custom validation (see article at http://ng-learn.org/2014/02/Writing_Custom_Validitions/).
However, if you just want a cheap way out, you could put a ng-minlength=1 requirement on the input. If the user can't put anything in the field manually, and there has to be something in the field for it to be valid, I think you've basically got what you want.

Related

Angular: force custom form validator to run on any field input

I've written a custom validator for a password field, in order to verify the following scenarios:
if user has id defined, then password is always valid (can be empty, meaning no change)
if user does not have id defined, then password must not be empty (new users must have a password)
Problem is, I have noticed the validator is run only when the user interacts with the field (unlike required which is run on any input apparently). This makes the form appear valid even if password empty for a new user. Once I interact with the password input, everything seems fine.
I have solved the business logic requirement through the poorly documented ngRequired directive, but I would really like to understand the issue regarding the custom validator in case I run into it again.
You can try ui-validate with your business here:
$scope.passwordValidation = {
length: '$value.length >= 5 || $value.length == 0'
};
$scope.password2Validation = {
same_passwords: 'userPassword.value==$value'
};
And then your HTML:
<input id="password" class="form-control" type="password" ng-model="userPassword.value"
name="password" ui-validate="passwordValidation"/>
<input id="password2" class="form-control" type="password" ng-model="userPassword.confirm"
name="password2" ui-validate="password2Validation" ui-validate-watch="'userPassword.value'"/>
Please note the ui-validate-watch to tell ui-validate to care about the other password field.
After giving up on this, I ran into the problem again and I could not find a workaround. Fortunatel, I found a solution:
When you make a custom validator you need to bind it to the model field, not the form field. This makes it validate correctly all the time (one can assume due to differences between $modelValue and $viewValue properties that are on the form field which fudges up things). Please see code below for reference:
<input type="password" class="form-control" id="confirmpass" name="confirmpass" placeholder="Repeat Password"
ng-model="controller.selectedUser.passwordRepeat"
compare-to="controller.selectedUser.password"/>
And custom validator:
angular.module("compareTo", []).directive("compareTo", function() {
return {
require: "ngModel",
scope: {
otherModelValue: "=compareTo"
},
link: function(scope, element, attributes, ngModel) {
ngModel.$validators.compareTo = function(modelValue) {
return modelValue == scope.otherModelValue;
};
scope.$watch("otherModelValue", function() {
ngModel.$validate();
});
}
};
);

Detect if the user fills the form with the autofill option of google chrome

If anyone can guide me on how to detect if the user fills the form with the autofill option of google chrome.
I have a directive where each time the user fills in the field and change the blur sends an event to google analytics.
But also I need to detect whether the user has filled the form with the autofill option of chrome and push the data for each of the fields to google analytics.
Part of my directive:
element.bind('blur', function (e) {
if ((e.target.value !== 0) && typeof value !== 'undefined') {
if (_.has(ga_data, 'sendEvent')) {
analyticsService.sendEvent(ga_data.sendEvent);
}
if (_.has(ga_data, 'action') && ga_data.action === 'blur') {
analyticsService.sendEvent(ga_data);
}
}
});
You can use two way data binding here and watch the ng model on the form field for changes
<form method="post">
First name:<input type="text" name="fname" ng-model="user.fname"/><br />
Last name: <input type="text" name="lname" ng-model="user.lname"/><br />
E-mail: <input type="text" name="email" ng-model="user.email"/><br />
Phone: <input type="text" name="phone" ng-model="user.phone"/><br />
Address: <input type="text" name="address" ng-model="user.address"/><br />
</form>
Then inside your angular controller you can do something of this sort.
angular.module('app', []).controller('AppController', function($scope){
$scope.user = { };
$scope.$watch('user', function(nv, ov){
console.log(nv);
}, true);
});
There might be some cases that you need to handle though, to prevent sending multiple requests because $watch function will be triggered every time the value in the text field changes.
Here is a fiddle which triggers $watch when any value in a form field changes, be it via autofill or manual entry by user.
In this case, the way to detect when Chrome auto-fills a form instead of the user doing it is by detecting the absence of an event having occurred, such as the keyup event. Consider the block of HTML and Javascript below. I have the text input field marked with a data attribute that is initially set to false. If the user fills out anything in the form by typing, then the attribute is set to true. The moment when you record whether or not the user filled out the form is on form submit. Then you can check the fields of the form and see if the user entered the input his or herself.
<form onsubmit="dosomething()">
<label for="somefield"></label>
<input type="text" id="somefield" onkeyup="this.setAttribute('data-entered', (this.value != '').toString());" data-entered="false" />
<input type="submit" value="submit"/>
</form>
The reason why you need to use a keyboard event and send the information when the form is submitted is because you can only tell if auto-fill took place when any of the fields have values even when the user typed nothing in. This part is less about code, and is more about what needs to be done so to take a proper measurement.
Based on input-directive src, angular sets up a listener for cases of change, input, paste etc.
And whenever the browser autofills the input element, this listener is called and viewValue is commited through array of $parsers as of here ngModel directive src.
So eventually you can avoid additional scope.$watch and just rely on $parsers to send ga track event just on linking phase of each input element with directive. And btw don't forget to destroy you parser func right after first usage (i.e. browser autofill), so it will not spam further on viewValue change.
Here's an example:
angular
.module('app', [])
.directive('detectPrefill', function() {
return {
require: 'ngModel',
link: {
pre: function(scope, element, attrs, ctrl) {
function detectPrefill (viewValue) {
//send GA data
//...
// just checking that detectPrefill func is destroyed after first usage
viewValue && console.log(viewValue);
ctrl.$parsers.splice(
ctrl.$parsers.indexOf(detectPrefill),
1
);
return viewValue;
}
ctrl.$parsers.push(detectPrefill);
}
}
};
});
Hope this helps.

handle submit event in directive as well

I’m quite new to Angular but I love it already!
I need to write a couple of reusable components for a wizard.
I would like to handle the submit event from the form in the directive as well, is this possible?
<form ng-submit="submit()" name="exampleForm">
<foo data="someData"></foo> <!-- I need to handle the submit event in directive as well-->
<input type="submit" id="submit" value="Submit"/>
</form>
If the user presses enter or clicks the button on the form the directive has to make a call to the backend and double check the data.
If the check is successful the form will be valid.
I built a simple example here
Thanks in advance!
Stefan
Yes it's possible.
The first issue is targeting the correct $scope. Right now your code targets the submit() function on the controller. The <form> can't see the $scope of the directive due to how the html elements are nested.
Since you want to submit() from the directive, then the directive template should also include the <form> element and the submit input button.
Invoking foo will look like this:
<foo data="someData" name="exampleForm"></foo>
If you would rather keep foo the way it is (and that's a totally legit way to go about it) then you'll need a new directive with the submit() function. So you'd have 2 directives working together (Very angular! Many wow!).
What you will want here is a custom directive for form validation. Custom form validators are added to the $validators object on the ngModelController.
For a simple example for an "integer" directive (found in the Angular docs here under "Custom Validation"), the directive would be defined like this:
var INTEGER_REGEXP = /^\-?\d+$/;
app.directive('integer', function() { return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.integer = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (INTEGER_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
} }; });
And it would be used in the form like this:
<form name="form" class="css-form" novalidate>
<input type="number" ng-model="size" name="size" min="0" max="10" integer />{{size}}<br />
</form>
There is also an option for asynchronous validation, adding the validator to the $asyncValidatorsobject. More info is found at the link mentioned above. The working example using both is found here
Here's a working example:
http://plnkr.co/edit/sckCOq3a50PBjat2uixk?p=preview
I've created another 2-way binding on the directive's scope (called "submit"), so you can specify the name of the submit function as it will be seen from ExampleController's scope (I used "directiveSubmit" here).
scope: {
data: '=',
submit: '='
},
.
<foo data="someData" submit="directiveSubmit"></foo>
The directive's controller then creates that function and assigns it to its own scope, and that also assigns it to ExampleController's scope via the magic of 2-way binding.
Next you can run that submit function from ExampleController's scope and it will actually be referring to the newly created function in the directive.
<form ng-submit="directiveSubmit(exampleForm)" name="exampleForm" novalidate>
Notice that I'm also passing the exampleForm into that function. Without that the directive will have a hard time getting access to it.
Finally, I've also added novalidate to the form because if you don't do that some browsers (i.e. Chrome) will handle form validation in a way that may (or may not) be undesirable. In Chrome try removing that and submitting, then you'll see what I mean (I also made the "name" field required).

decoupling $scope.form from controller

In my angular controller, I have to reset some form fields to pristine at a certain point (when some other field changes). The code looks something like this ...
$scope.fieldCallback = function() {
$scope.data.Field = undefined;
$scope.form.field.$setPristine();
}
this of course causes problem once I run my unit tests, because form.field will only be there if you actually parse the document (which of course in the case of unit tests is not happening). I know it is possible to do something along the lines of
var element = angular.element(...[some html]...);
$compile(element)($scope);
but this does not really seem like a good solution to me.
Is there any way to decouple the controller from the html so I don't have to mock this like described above?
Thx in advance!
If I am correctly getting your question, you could try implementing this while focus out of form
<form name="testForm" ... ... ng-blur="func()">
<input type="text" name="Field" />
</form
and in js file
$scope.func = function(){
$scope.testForm.Field.$setPristine();
}
you can drirectly use expresion in ngBlur
ng-blur="focus=false"

Disabling form validation

Angularjs is running my forms through the FormController (eg tracking pristine, dirty, etc). I don't need this functionality; I'm sure it's adding overhead to my $digests.
How can I shut it off?
AFAIK there is no simple switch to turn off AngularJS validation. Actually most of the validation happens in the NgModelController and input directives - basically code in the input.js file. So, to get rid of the built-in validation you would have to re-develop code from this file (plus some others, like select).
Did you identify validation code as a performance bottleneck in your application?
UPDATE : This does NOT work ... well at least not in a way you'd like it to. Adding ng-non-bindable to the form or any input breaks ALL binding. So, your ng-model in the inputs won't work anymore. Sorry ....
ng-non-bindable is the solution to this problem.
It will prevent AngularJS from seeing the form as a directive. This will make AngularJS ignore the entire form:
<form name="inviteContactForm" ng-non-bindable>
This will make AngularJS ignore one part of a form:
<input type="email" name="email" ng-non-bindable>
You can read a bit about my whining on this issue here. http://calendee.com/preventing-angularjs-from-hijacking-forms/
Internally Angular creates factories out of directives by adding the Directive suffix to the directive name. So you can replace the validation and input directive factories with no-operational ones.
var noopDirective = function() { return function () {}; };
angular.module('myModule')
.factory('requiredDirective', noopDirective)
.factory('ngRequiredDirective', noopDirective)
.factory('inputDirective', noopDirective)
.factory('textareaDirective', noopDirective); // etc...
Similar to previous answer from #Chui Tey, having a directive that requires 'ngModel'. This is what I did
for disabling validations in runtime:
//disabling all validators
forEach(ngModelController.$validators, function(validator, validatorName){
ngModelController.$setValidity(validatorName, true);//mark invalid as valid
var originalValidator = ngModelController.$validators[validatorName]; //we save the original validator for being able to restore it in future
ngModelController.$validators[validatorName] = _.wrap(true);//overwrite the validator
ngModelController.$validators[validatorName].originalValidator = originalValidator;
});
//for restoring validations
forEach(ngModelController.$validators, function(validator, validatorName){
if(ngModelController.$validators[validatorName].originalValidator){
ngModelController.$validators[validatorName] = ngModelController.$validators[validatorName].originalValidator;
}
});
ngModelController.$validate(); //triger validations
A colleague suggested a nifty way of disabling validators. Here's an implementation:
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="true" />Enabled
<input type="radio" name="enableValidation" ng-model="$ctrl.validationEnabled" ng-value="false" />Disabled
<input type="number"
name="age"
ng-model="$ctrl.age"
min="20"
disable-validation="!$ctrl.validationEnabled" />
When disable-validation is true, then all validation rules automatically passes.
function disableValidation(scope, elem, attrs, ngModelController) {
function wrapOriginalValidators() {
var originalValidators = angular.copy(ngModelController.$validators);
Object.keys(originalValidators).forEach(function(key) {
ngModelController.$validators[key] = function(modelValue, viewValue) {
return scope.$eval(attrs.disableValidation) || originalValidators[key](modelValue, viewValue);
}
});
}
function watchDisableCriteria() {
scope.$watch(attrs.disableValidation, function() {
// trigger validation
var originalViewValue = ngModelController.$viewValue;
scope.$applyAsync(function() {
ngModelController.$setViewValue('');
});
scope.$applyAsync(function() {
ngModelController.$setViewValue(originalViewValue);
});
});
}
wrapOriginalValidators();
watchDisableCriteria();
}
angular.module('app', [])
.directive('disableValidation', function() {
return {
require: 'ngModel',
link: disableValidation
};
});
Obviously, you'd not use this for performance reasons, but when you
need to dynamically enable or disable validations.
Sample: https://plnkr.co/edit/EM1tGb

Resources