How to create a custom angular directive that will override ng-disabled? - javascript

First some background: My application allows users to control whether or not fields are required, disabled, etc. through an admin tool. I have a service that takes a field name and returns me the user defined rules in a format like this:
{
"disabled" : true,
"required" : true
}
I want a custom attribute directive that will control these properties on an input field using the service. I would expect the usage to look something like this:
<input type="text" my-rule="fieldName" ng-model="myfield" />
I'm able to accomplish this easily with a directive like the following:
angular.module('app').directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
$http.get('/api/rule/'+scope.myRule).success(function(rule) {
if (rule.disabled) {
element.attr('disabled','disabled');
}
if (rule.required) {
element.attr('required','required');
}
});
}
}
}
]);
My problem is that if a user does not have a field disabled I may still want to disable it until, for example, another field has been filled out. This should be easy to do with ng-disabled:
<input type="text" my-rule="fieldA" ng-model="fieldA" />
<input type="text" my-rule="fieldB" ng-model="fieldB" ng-disabled="!fieldA" />
However, this does not work because if the user chooses to disable fieldB then the field should always be disabled regardless of the ng-disabled attribute but instead the ng-disabled attribute overrides the user's rule. I tried something like this to remove the ng-disabled if the field is disabled by the user but that does not seem to have an effect:
angular.module('app').directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
$http.get('/api/rule/'+scope.myRule).success(function(rule) {
if (rule.disabled) {
element.attr('disabled','disabled');
element.removeAttr('ng-disabled');
}
if (rule.required) {
element.attr('required','required');
element.removeAttr('ng-required');
}
});
}
}
}
]);
This removes the attribute but it seems at that point it is too late and the field still becomes enabled as soon as fieldA is filled in.
How can I dynamically remove the ng-disabled attribute in my custom directive so that it no longer has an effect on the field?
Update:
I added a code snippet demonstrating my problem.
angular.module('app',[]).directive('myRule', ['$http',
function($http) {
return {
restrict: 'A',
scope: {
myRule: '#'
},
link: function(scope, element, attrs) {
// would normally be making an ajax call to get the rule
var rule = { disabled: scope.myRule != "fieldA" };
if (rule.disabled) {
element.attr('disabled','disabled');
element.removeAttr('ng-disabled');
}
}
}
}
]);
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app">
<p>Field B and Field C have been disabled by the user but since Field C includes an ng-disabled expression it will be incorrectly enabled when Field A is filled out.</p>
<input type="text" my-rule="fieldA" ng-model="fieldA" placeholder="Field A" />
<input type="text" my-rule="fieldB" ng-model="fieldB" placeholder="Field B" />
<input type="text" my-rule="fieldC" ng-model="fieldC" placeholder="Field C" ng-disabled="!fieldA" />
</div>

Tryprop('disabled', boolean) instead of attr(). This will change the element property which is not always the same as the attribute.
Since you are manipulating the DOM outside of angular you should probably tell angular to run a digest also by calling scope.$apply() or $timeout()
Not sure this will work and I think you will probably need a directive to wrap the whole input.
One suggestion is take a look at angular-formly which builds whole forms including conditional validation from object models

Related

Angular directive for range validation

I am trying to make a directive for an input to limit the value between 1-99. On the same input I also have another directive that converts the value to a percentage and am not sure if that is what is getting in the way.
The directive is simple (taken basically from the Angular website):
(function() {
'use strict';
angular
.module('app.model')
.directive('inputRange', inputRange);
function inputRange() {
return {
require: 'ngModel',
restrict: 'A',
link: function(scope, elm, attrs, ctrl) {
var INTEGER_REGEXP = /^-?\d+$/;
ctrl.$validators.inputRange = 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 the section of html with the input field (which is paired with a slider):
<form name="form">
<div sc-slider
ng-model="vm.baseline"
min="0.01"
max="0.99"
initial="{{vm.baseline}}"
step="0.01"
uib-tooltip="The initial estimate of the KIQ's likelihood - prior to any indicator observations."
tooltip-popup-delay="200"
tooltip-popup-close-delay="200"
tooltip-placement="bottom"></div>
<input to-percent
input-range
name="baseline"
style="text-align:center;"
type="text"
min="1"
max="99"
class="form-control"
ng-model="vm.baseline"></input>
<span ng-show="form.baseline.$error.inputRange">The value is not a valid integer!</span>
<span ng-show="form.baseline.$error.min || form.baseline.$error.max">
The value must be in range 1 to 99!</span>
</form>
I have read on SO about priority for directives that share an input but I don't think that is necessarily an issue here. But when I enter a value greater than 99 I'd expect one of the spans below to show up, but nothing is appearing. And my other directive works fine all of the time. Any help is appreciated.
Make your life easier and use ng-max and ng-min attributes on the input element.

AngularJS dynamic required attribute in directive and form validation

I have a directive that receives whether an element should be required or not from a REST api. Right now, I can't seem to get the form to invalidate when an attribute is set to required.
So, in essence I'm able to dynamically add the 'required' attribute from the directive below, but it doesn't invalidate the form. Looking through chrome I see that, even though the required attribute exists, a required entry in the $error array doesn't exist.
app.directive('requireiftrue', function ($compile) {
return {
require: '?ngModel',
link: function (scope, el, attrs, ngModel) {
if (!ngModel) {
return;
}
if(attrs.requireiftrue==="true"){
console.log('should require');
el.attr('required', true);
$compile(el.contents())(scope);
}
else{
console.log('should not require');
}
}
};
});
Here's a jsfiddle to illustrate the problem. And, here's sample JSON returned from my rest API
{
race: false,
martialStatus: true,
}
EDIT: While the accepted answer got me up and running, I still had a good bit of finagling to do.
Namely:
1. Resolving a deferred promise to ensure that my form actually receives the required fields to validate
2. observing my 'requireiftrue' attribute
My solution
module config:
function config($stateProvider) {
$stateProvider
.state('testState', {
url: '/test/form',
controller: 'TestCtrl',
templateUrl: 'test/form/testForm.tpl.html',
resolve: {
formDefaultService: function getFormDefaults($q, dataservice) {
// Set up a promise to return
var deferred = $q.defer();
var myData = dataservice.getFormDefaults();
deferred.resolve(myData);
return deferred.promise;
//return
}
},
data: {
pageTitle: 'Test Info'
}
});
}
And, finally the directive / HTML that receives api data:
Directive:
.directive('requireiftrue', function ($compile) {
return {
require: '?ngModel',
link: function (scope, el, attrs, ngModel) {
if (!ngModel) {
return;
}
attrs.$observe('requireiftrue', function(value){
if(value==="true"){
el.attr('required', true);
el.removeAttr('requireiftrue');
$compile(el[0])(scope);
}
});
}
};
});
HTML:
<input max="40"
requireiftrue={{defaults.validation.name}}
validNumber
id="name"
name="name"
type="text"
ng-model="test.name"
class="form-control">
You had two issues:
The first is el.contents() returned an empty array. so The first thing you should do is change it to el[0]. But had el.contents() worked you would hav had a much more serious problem. You would have been trying to compile a directive that has itself as a directive which would lead to an infinite loop (well until the browser crashed any way).
So here is the revised code:
var app = angular.module('form-example', []);
app.directive('requireiftrue', function ($compile) {
return {
require: '?ngModel',
link: function (scope, el, attrs, ngModel) {
if (!ngModel) {
return;
}
if(attrs.requireiftrue==="true"){
console.log('should require');
el.attr('required', true);
el.removeAttr('requireiftrue');
$compile(el[0])(scope);
}
else{
console.log('should not require');
}
}
};
});
I should note however that now this directive is a one-off. If the model will change, the directive will not be on the element any longer to deal with it.
Instead of using a directive, use ng-init to initialize requireiftrue.
and assign this value to ng-required like ng-required="requireiftrue" as shown below. As you said you are getting the data from rest api, you can initialize requireiftrue with the value you are getting from api, instead of true or false as shown in example below.
Hope this helps you.
Updated fiddle
http://jsfiddle.net/zsrfe513/3/
<form ng-app="form-example" name='fo' class="row form-horizontal" novalidate>
<div class="control-group" ng-form="testReq">
<h3>Form invalid: {{testReq.$invalid}}</h3>
<label class="control-label" for="inputEmail">Email</label>
<div class="controls" ng-init='requireiftrue = true'>
<input id="inputEmail" placeholder="Email" ng-model="email" name='ip' ng-required='requireiftrue'>
<span ng-show="testReq.ip.$dirty && testReq.ip.$error.required">
Required.
</span>
</div>
</div>
</form>
Try:
1. adding the required directive to the input you want to apply validation to
<input id="inputEmail" class="requireiftrue" placeholder="Email"
ng-model="email" requireiftrue="true" required>
2 Defining the directive as type class and adding the directive class to the HTML input field
JS
app.directive('requireiftrue', function ($compile) {
return {
restrict: 'C',
require: '?ngModel',
.....
HTML
<input id="inputEmail" class="requireiftrue" placeholder="Email" ng-model="email" requireiftrue="true" required>
here is a update of your fiddle - http://jsfiddle.net/4fb6wg30/
You just need to add the "required" attribute to the input.
<input max="40"
requireiftrue={{defaults.validation.name}}
validNumber
id="name"
name="name"
type="text"
ng-model="test.name"
class="form-control"
required="required">
I used <input ng-required="true"> worked fine for my angular validation component.
If your using the new angular component make sure to pass in required: "#" instead of required: "="
scope: {
required: '#'
}
I also took this further and required integer and min/max validation the same way.

Does AngularJS store a value in the $error.maxlength object?

I've got a UI page setup through Angular, and I'm trying to take advantage of the built in ng-maxlength validator on an input element. Long story short, I know about $scope.form.$error and how that object has a maxlength property in the case that the validation fails. But I want to display an error message specific to the character length that was violated, and I don't see anywhere that the length that I specified was stored on this object. Does anyone know if it's possible to access this, so I don't have to write out a separate error message for each input that has the max length violated?
EDIT: To answer your question, yes angular does store a boolean value in the $error object that is accessible to your via the key(s) that are set in the object. In the case of the code I provided below and in th jsFiddle, we are setting the key for angular, and the value of either true or false.
Be mindful when setting the value as it is reversed. ex. $setValidity( true ), flips the $error to false.
Ok, here is what I think you were looking for...
In Angularjs v1.2.13 you will not have access to ng-message or the $validator pipeline,
which is why are are using $formatters and $parsers.
In this case, I am using named inputs, but perhaps in your case you need dynamic input names?
Plus, if you are using inputs but no form, then getting the error message to display would have to be done with a separate custom directive.
If so, then please look here for dynamically named input fields for some help.
dynamic input name in Angularjs link
Let me know if this works; I'll make changes as needed to HOOK YOU UP!
In case you don't know, you can write over Angular's maxlength for each individual input.
If you changed 'maxlength' in the updateValidity() function in the directive below, to something like 'butter', then $scope.form.inputname.$error would be something like
$scope.formname.inputname.$error { butter: true }
if you also used ng-maxlength="true", then it would be
$scope.formname.inputname.$error { butter: true, maxlength: true }
Another example if you used ng-maxlength, and capitalized the 'maxlength' in the directive to 'Maxlength'
Then you would get
$scope.formname.inputname.$error { maxlength: true(angular maxlength), Maxlength: true(your maxlength)
And of course if you name it the same, then yours writes over angulars
$scope.formname.inputname.$error { maxlength: true };
The point is YOU can add your own names to the angular $error object; you can write over Angular's; and you can just use what Angular gives you when you use Angular's directives: like ng-required="true", or ng-maxlength="true"
Link to YOUR angularjs version on jsFiddle
jsFiddle LInk
<div ng-app="myApp">
<form name="myForm">
<div ng-controller="MyCtrl">
<br>
<label>Input #1</label>
<br>
<input ng-model="field.myName" name='myName' my-custom-length="8" />
<span ng-show="myForm.myName.$error.maxlength">
Max length exceeded by {{ myForm.myName.maxlength }}
</span>
<br>
<br>
<label>Input #2</label>
<br>
<input ng-model="field.myEmail" name='myEmail' my-custom-length="3" />
<span ng-show="myForm.myEmail.$error.maxlength">
Max length exceeded by {{ myForm.myEmail.maxlength }}
</span>
</div>
</form>
</div>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.field = {};
});
app.directive("myCustomLength", function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
if (!ctrl) { return } // ignore if no ngModel controller
ctrl.$formatters.push(validateInput);
ctrl.$parsers.unshift(validateInput);
function validateInput(value) {
if (!value) {
updateValidity(false);
return;
}
inputLength(value);
var state = value.length > attrs.myCustomLength;
updateValidity(state);
}
function inputLength(value) {
ctrl.maxlength = null;
var length = value.length > attrs.myCustomLength;
if (length) {
ctrl.maxlength = (value.length - attrs.myCustomLength).toString();
}
}
function updateValidity(state) {
ctrl.$setValidity('maxlength', !state);
}
} // end link
} // end return
});
CSS Here if you need it.
input.ng-invalid {
border: 3px solid red !important;
}

I want to be able to set Angulars ng-pattern inside a directive with a template and it's own scope to validate a form

This is part of a much more complicated directive that needs to have its own scope as well as require ngModel and replace the existing input. How can I have the directive add the ng-pattern attribute? As you can see in this jsfiddel the validation doesn't change based on the input if the ng-pattern is added in the template. This is because this will be added to an existing application that has a ton of different attributes already on a ton of different input elements, and I'm trying to make the addition as easy to implement as possible by just adding functionality to the existing input fields without messing up other things.
http://jsfiddle.net/MCq8V/
HTML
<div ng-app="demo" ng-init="" ng-controller="Demo">
<form name="myForm" ng-submit="onSubmit()">
<input lowercase type="text" ng-model="data" name="number">
Valid? {{myForm.number.$valid}}
<input type="submit" value="submit"/>
</form>
</div>
JS
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
require: 'ngModel',
restrict: 'A',
scope:{},
replace: true,
link: function(scope, element, attr, ngModelCntrl) {
},
template: '<input class="something" ng-pattern="/^\d*$/">',
};
});
module.controller('Demo', Demo);
function Demo($scope) {
$scope.data = 'Some Value';
}
Thanks so much for any help! Ideally I would be able to just change something small and keep the ng-pattern, but I think I may have to do the validation setting on my own.
Here's how the pattern attribute is added to input item in a directive I have in my application. Note the use of compile at the end of the link function. In your case, rather than replace the element contents with a template, you'd just work with the existing element input tag.
link: function (scope, element, attrs, formController) {
// assigned template according to form field type
template = (scope.schema["enum"] !== undefined) &&
(scope.schema["enum"] !== null) ?
$templateCache.get("enumField.html") :
$templateCache.get("" + scope.schema.type + "Field.html");
element.html(template);
// update attributes - type, ng-required, ng-pattern, name
if (scope.schema.type === "number" || scope.schema.type === "integer") {
element.find("input").attr("type", "number");
}
element.find("input").attr("ng-required", scope.required);
if (scope.schema.pattern) {
element.find("input").attr("ng-pattern", "/" + scope.schema.pattern + "/");
}
element.find("input").attr("name", scope.field);
// compile template against current scope
return $compile(element.contents())(scope);
}
I tried quite a few things and it seemed that using a directive to replace an input with an input was tricking Angular up somewhere - so this is what I came up with:
http://jsfiddle.net/MCq8V/1/
HTML
<div ng-app="demo" ng-init="" ng-controller="Demo">
<form name="myForm" ng-submit="onSubmit()">
<div lowercase model="data"></div>
Valid? {{myForm.number.$valid}}
<input type="submit" value="submit"/>
</form>
</div>
JS
var module = angular.module("demo", []);
module.directive('lowercase', function() {
return {
restrict: 'A',
scope:{
data:'=model'
},
replace: true,
template: '<input class="something" ng-pattern="/^\\d*$/" name="number" ng-model="data" type="text">',
};
});
module.controller('Demo', Demo);
function Demo($scope) {
$scope.data = 'Some Value';
}
Also, you needed to escape your backslash in your regex with another backslash.

AngularJS Directive - dynamic input name binding

I am attempting to learn a little more about AngularJS' directives and have run into this situation. I would like to make a yes-no radio control that I can reuse. I have gotten most of the way - I think - but need a little push in the right direction.
I have this directive:
app
.directive('yesno', function () {
'use strict';
var config;
config = {
replace: true,
require: 'ngModel',
restrict: 'E',
scope: {
field: '=',
model: '='
},
templateUrl: 'views/yesno.html'
};
return config;
});
...and the template looks like this:
<fieldset class="yesno">
<input id="{{field}}-yes" name="{{field}}" ng-model="model" type="radio" value="yes" />
<label for="{{field}}-yes">Yes</label>
<input id="{{field}}-no" name="{{field}}" ng-model="model" type="radio" value="no" />
<label for="{{field}}-no">No</label>
</fieldset>
...and I am using it like this (simplified):
<form name="person">
<yesno field="'happy'" model="happy" />
</form>
Unfortunately what I am getting in the person object is a property {{field}} instead of happy like I would like. I keep telling myself that something like what I am attempting is possible and I just need to find it; but what.
Help please.
Update
Thank you, #HackedByChinese that helped a little but still not quite there. The problem is that I do want two way binding so that the value of the radios is populated into the parent scope; instead, when I inspect the person object it has a {{field}} property and not a happy property.
I am thinking that this is just something that AngularJS does not support in looking at:
AngularJS: Fields added dynamically are not registered on FormController
...and:
https://github.com/angular/angular.js/issues/1404
Well if you just want field to contain the string value that was entered, you can use the # prefix for the attribute to indicate it is a text binding (it will interpret the value of the attribute as literal text).
scope: {
field: '#',
model: '='
},
Click for demo.
On the other hand, if you need field to bind to the value an expression provided to the attribute (for example, you want to bind to a property on the parent scope), then you need to change the template HTML to evaluate field (simply {{field()}}) because they will be functions. The difference here is if people want to provide string values directly, they'll need to put it in quotes like your original example. I would also recommend a one-way binding, since it seems unlikely your directive would want to modify the parent scope value since it's just a name. Use the & prefix for that.
scope: {
field: '&',
model: '='
},
<fieldset class="yesno">
<input id="{{field()}}-yes" name="{{field()}}" ng-model="model" type="radio" value="yes" />
<label for="{{field()}}-yes">Yes</label>
<input id="{{field()}}-no" name="{{field()}}" ng-model="model" type="radio" value="no" />
<label for="{{field()}}-no">No</label>
</fieldset>
Click for second demo.
I ran into the same problem. The simplest solution is to inject the name value directly into the template string.
It works as long as you don't need the name value to be bound (ie. it doesn't need to change during the lifetime of the directive). Considering the way the name attribute is usually used, I think this constraint is not an problem.
app
.directive('yesno', function () {
return {
replace: true,
restrict: 'E',
scope: {
field: '#',
model: '='
},
template: function(element, attrs) {
return '<fieldset class="yesno"> \
<input id="{{field}}-yes" name="{{field}}" ng-model="model" type="radio" value="yes" /> \
<label for="{{field}}-yes">Yes</label> \
<input id="{{field}}-no" name="{{field}}" ng-model="model" type="radio" value="no" /> \
<label for="{{field}}-no">No</label> \
</fieldset>'.replace('{{field}}', attrs.field, 'g');
}
};
});
This solution is a bit messy, because of the inline html. If you want to load the template from a file as in the original question, you can do it like this:
app
.directive('yesno', ['$http', '$templateCache', '$compile',
function ($http, $templateCache, $compile) {
return {
restrict: 'E',
scope: {
field: '#',
model: '='
},
link: function(scope, element) {
$http.get('views/yesno.html', {cache:$templateCache})
.then(function(response) {
var content = angular.element(response.data.replace('{{field}}', scope.field, 'g'));
element.append(content);
$compile(content)(scope);
});
}
};
}]);

Categories

Resources