Issues with ng-required directive angular js - angularjs

I want my input to be required based on condition.
<input type="text" custom-directive ng-required="isRequired" />
I have defined property isRequired as boolean false. Required functionality works fine according to the isRequired property. But inside the custom directive i am getting $attrs.required as true for all scenarios.
module.directive('customDirective', function () {
var link = function ($scope, $el, $attrs, $controllers) {
$scope.required = $attrs.required;
}
return {
restrict: 'A',
link: link
};
});
Thanks

I am unable to explain why $scope.required would be true in this case (perhaps you are setting it to true somewhere else and should be using a child or isolation scope?)... I would expect it to be undefined based on what you have provided. The other possibility is that you have multiple elements with this directive in the same scope and one of them is setting that value to true for all of them.
By replacing your $scope.required = $attrs.required; line with
$attrs.$observe('required', function(value) {
$scope.required = value;
});
it should properly pick up the value of the required attribute (and when it changes!).
If you have that directive being applied to multiple elements in the same scope you should use a child scope. All you need to do is add scope: true to the object returned by your directive.
If this does not work, please post more of your application's code.

Related

How to use the last valid modelValue if a model becomes invalid?

I'm working on an application that saves changes automatically when the user changes something, for example the value of an input field. I have written a autosave directive that is added to all form fields that should trigger save events automatically.
template:
<input ng-model="fooCtrl.name" autosave>
<input ng-model="fooCtrl.email" autosave>
directive:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
function saveIfModelChanged () {
// save object containing name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
So far, this all works fine for me. However, when I add validation into the mix, for example validating the input field to be a valid email address, the modelValue is set to undefined as soon as the viewValue is changed to an invalid email address.
What I would like to do is this: Remember the last valid modelValue and use this when autosaving. If the user changes the email address to be invalid, the object containing name and email should still be saved to the server. Using the current valid name and the last valid email.
I started out by saving the last valid modelValue like this:
template with validation added:
<input type="email" ng-model="fooCtrl.name" autosave required>
<input ng-model="fooCtrl.email" autosave required>
directive with saving lastModelValue:
.directive('autosave', ['$parse', function ($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ngModel) {
var lastModelValue;
function saveIfModelChanged () {
// remeber last valid modelValue
if (ngModel.$valid) {
lastModelValue = ngModel.$modelValue;
}
// save object containing current or last valid
// name and email to server ...
}
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
}
};
}]);
My question is, how to use lastModelValue while saving, but preserving the invalid value in the view?
EDIT:
Another possibility, as suggested by Jugnu below, would be wrapping and manipulating the build in validators.
I tried to following: wrap all existing validators and remember the last valid value, to restore it if validations fails:
Object.keys(ngModel.$validators).forEach(function(validatorName, index) {
var validator = ngModel.$validators[validatorName];
ngModel.$validators[validatorName] = createWrapper(validatorName, validator, ngModel);
});
function createWrapper(validatorName, validator, ngModel){
var lastValid;
return function (modelValue){
var result = validator(modelValue);
if(result) {
lastValid = modelValue;
}else{
// what to do here? maybe asign the value like this:
// $parse(attrs.ngModel).assign(scope, lastValid);
}
return result;
};
}
But I'm not sure how to continue with this approach either. Can I set the model value without AngularJS kicking in and try to validate that newly set value?
I have created a simple directive that serves as a wrapper on the ng-model directive and will keep always the latest valid model value. It's called valid-ng-model and should replace the usage of ng-model on places where you want to have the latest valid value.
I've created an example use case here, I hope you will like it. Any ideas for improvements are welcomed.
This is the implementation code for valid-ng-model directive.
app.directive('validNgModel', function ($compile) {
return {
terminal: true,
priority: 1000,
scope: {
validNgModel: '=validNgModel'
},
link: function link(scope, element, attrs) {
// NOTE: add ngModel directive with custom model defined on the isolate scope
scope.customNgModel = angular.copy(scope.validNgModel);
element.attr('ng-model', 'customNgModel');
element.removeAttr('valid-ng-model');
// NOTE: recompile the element without this directive
var compiledElement = $compile(element)(scope);
var ngModelCtrl = compiledElement.controller('ngModel');
// NOTE: Synchronizing (inner ngModel -> outside valid model)
scope.$watch('customNgModel', function (newModelValue) {
if (ngModelCtrl.$valid) {
scope.validNgModel = newModelValue;
}
});
// NOTE: Synchronizing (outside model -> inner ngModel)
scope.$watch('validNgModel', function (newOutsideModelValue) {
scope.customNgModel = newOutsideModelValue;
});
}
};
});
Edit: directive implementation without isolate scope: Plunker.
Since you are sending the entire object for each field modification, you have to keep the last valid state of that entire object somewhere. Use case I have in mind:
You have a valid object { name: 'Valid', email: 'Valid' }.
You change the name to invalid; the autosave directive placed at the name input knows its own last valid value, so the correct object gets sent.
You change the email to invalid too. The autosave directive placed at the email input knows its own last valid value but NOT that of name. If the last known good values are not centralized, an object like { name: 'inalid', email: 'Valid' } will be sent.
So the suggestion:
Keep a sanitized copy of the object you are editing. By sanitized I mean that any invalid initial values should be replaced by valid pristine ones (e.g. zeros, nulls etc). Expose that copy as a controller member, e.g. fooCtrl.lastKnowngood.
Let autosave know the last known good state, e.g. as:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required />
Keep the last known good local value in that object; utilize the ng-model expression, e.g. as:
var lastKnownGoodExpr = $parse(attrs.autosave);
var modelExpr = $parse(attrs.ngModel);
function saveIfModelChanged () {
var lastKnownGood = lastKnownGoodExpr(scope);
if (ngModel.$valid) {
// trick here; explanation later
modelExpr.assign({fooCtrl: lastKnownGood}, ngModel.$modelValue);
}
// send the lastKnownGood object to the server!!!
}
Send the lastKnownGood object.
The trick, its shortcomings and how can it be improved: When setting the local model value to the lastKnownGood object you use a context object different than the current scope; this object assumes that the controller is called fooCtrl (see the line modelExpr.assign({fooCtrl: lastKnownGood}, ...)). If you want a more general directive, you may want to pass the root as a different attribute, e.g.:
<input ng-model="fooCtrl.email" autosave="fooCtrl.lastKnowngood" required
autosave-fake-root="fooCtrl" />
You may also do some parsing of the ng-model expression yourself to determine the first component, e.g. substring 0 → 1st occurence of the dot (again simplistic).
Another shortcoming is how you handle more complex paths (in the general case), e.g. fooCtrl.persons[13].address['home'].street - but that seems not to be your use case.
By the way, this:
ngModel.$viewChangeListeners.push(function () {
saveIfModelChanged();
});
can be simplified as:
ngModel.$viewChangeListeners.push(saveIfModelChanged);
Angular default validators will only assign value to model if its valid email address.To overcome that you will need to override default validators.
For more reference see : https://docs.angularjs.org/guide/forms#modifying-built-in-validators
You can create a directive that will assign invalide model value to some scope variable and then you can use it.
I have created a small demo for email validation but you can extend it to cover all other validator.
Here is fiddle : http://plnkr.co/edit/EwuyRI5uGlrGfyGxOibl?p=preview

Setting attrs dynamically for ui-bootstrap tooltip / popover

I'm trying to programmatically toggle tooltips (like mentioned here: https://stackoverflow.com/a/23377441) and got it fully functional except for one issue. In order for it to work I must have tooltip-trigger and tooltip attributes hardcoded as follows:
<input type="text" tooltip-trigger="show" tooltip="" field1>
In my working directive, I'm able to change the tooltip attributes and trigger a tooltip, but if I try to leave those two attributes out and attempt to set them dynamically, ui-bootstrap doesn't pick them up and no tooltip gets displayed.
html
<input type="text" field2>
js
myApp.directive('field2', function($timeout) {
return {
scope: true,
restrict: 'A',
link: function(scope, element, attrs) {
scope.$watch('errors', function() {
var id = "field2";
if (scope.errors[id]) {
$timeout(function(){
// these attrs dont take effect...
attrs.$set('tooltip-trigger', 'show');
attrs.$set('tooltip-placement', 'top');
attrs.$set('tooltip', scope.errors[id]);
element.triggerHandler('show');
});
element.bind("click", function(e){
element.triggerHandler('hide');
});
}
});
},
};
});
I'd prefer not to hardcode these attributes in the html, so how do I go about setting these attributes dynamically and get ui-bootstrap to pick them up?
Here is a plunker that has a working (field1) and non working (field2) directive: http://plnkr.co/edit/mP0JD8KHt4ZR3n0vF46e
You can do this, but you have to change a couple of things in your approach.
Plunker Demo
Directive
app.directive("errorTooltip", function($compile, $interpolate, $timeout) {
return {
scope: true,
link: function($scope, $element, $attrs) {
var errorObj = $attrs.errorTooltip;
var inputName = $attrs.name;
var startSym = $interpolate.startSymbol();
var endSym = $interpolate.endSymbol();
var content = startSym+errorObj+'.'+inputName+endSym;
$element.attr('tooltip-trigger', 'show');
$element.attr('tooltip-placement', 'top');
$element.attr('tooltip', content);
$element.removeAttr('error-tooltip');
$compile($element)($scope);
$scope.$watch(errorObj, function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true);
$element.on('click', function(e){
$element.triggerHandler('hide');
});
}
};
});
The super long detailed explanation:
Okay, so from the top: #Travis is correct in that you can't just inject the attributes after the fact. The tooltip attributes that you place on the element are directives themselves, so the tooltip needs to be compiled when it's appended. That's not a problem, you can use the $compile service to do this, but you need to do it just once for the element.
Also, you need to bind the tooltip text (the value given to the tooltip attribute) to an expression. I do that by passing in a concatenated value of $interpolate.startSymbol() + the scope value that you want to display (in the demo it is the fieldx property of the errors object) + the $interpolate.endSymbol(). This basically evaluates to something like: {{error.field1}}. I use the $interpolate service start and end symbols because it just makes the directive more componentized, so you can use it on other projects where you might have multiple frameworks and be using something other than double curly-braces for your Angular expressions. It's not necessary though and you could instead do: '{{'+errorObj+'.'+inputName+'}}'. In this case, you don't have to add the $interpolate service as a dependency.
As you can see, to make the directive truly reuseable, rather than hard-coding the error field, I set the value given to the directive attribute to the name of the object that will be watched and use the input name value as the object property.
The chief thing you need to remember is that before you compile, you have to remove the error-tooltip attribute from the element because if you don't you'll wind up in an infinite loop and crash hard! Basically, the compile service is going to take the element that the directive is attached to and compile it with all of the attributes your directive added, if you leave the error-tooltip attribute, it's going to try and recompile that directive too.
Lastly, you can take advantage of the fact that the tooltip will not display if its text value is empty or undefined (see line 192). That means you only have to watch the errors object not the individual property on the error associated with the tooltip. Make sure that you set the equality operator on the $watch to true, so that it will trigger if any of the object's properties are changed:
$scope.$watch('errors', function() {
$timeout(function(){
$element.triggerHandler('show');
});
}, true); //<--equality operator
In the demo, you can see the effect of changing the errors object. If you click the Set Errors button the tooltip will display for both the first and second inputs. Click the Change Error Values and the tooltip displays for the first and third inputs.
TL;DR:
Add the directive to your markup by setting the value to be the name of the object that will contain all of the errors. Make sure to give the field a name attribute that corresponds to the property key name in the object that will contain the errors for that input, such as:
<input class="form-control" ng-model="demo.field1" name="field1" error-tooltip="errors" />

How to implement an ng-change for a custom directive

I have a directive with a template like
<div>
<div ng-repeat="item in items" ng-click="updateModel(item)">
<div>
My directive is declared as:
return {
templateUrl: '...',
restrict: 'E',
require: '^ngModel',
scope: {
items: '=',
ngModel: '=',
ngChange: '&'
},
link: function postLink(scope, element, attrs)
{
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange();
}
}
}
I would like to have ng-change called when an item is clicked and the value of foo has been changed already.
That is, if my directive is implemented as:
<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>
I would expect to call bar when the value of foo has been updated.
With code given above, ngChange is successfully called, but it is called with the old value of foo instead of the new updated value.
One way to solve the problem is to call ngChange inside a timeout to execute it at some point in the future, when the value of foo has been already changed. But this solution make me loose control over the order in which things are supposed to be executed and I assume that there should be a more elegant solution.
I could also use a watcher over foo in the parent scope, but this solution doesn't really give an ngChange method to be implmented and I have been told that watchers are great memory consumers.
Is there a way to make ngChange be executed synchronously without a timeout or a watcher?
Example: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview
If you require ngModel you can just call $setViewValue on the ngModelController, which implicitly evaluates ng-change. The fourth parameter to the linking function should be the ngModelCtrl. The following code will make ng-change work for your directive.
link : function(scope, element, attrs, ngModelCtrl){
scope.updateModel = function(item) {
ngModelCtrl.$setViewValue(item);
}
}
In order for your solution to work, please remove ngChange and ngModel from isolate scope of myDirective.
Here's a plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview
tl;dr
In my experience you just need to inherit from the ngModelCtrl. the ng-change expression will be automatically evaluated when you use the method ngModelCtrl.$setViewValue
angular.module("myApp").directive("myDirective", function(){
return {
require:"^ngModel", // this is important,
scope:{
... // put the variables you need here but DO NOT have a variable named ngModel or ngChange
},
link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically eval your ng-change
};
}
};
});
More precisely
ng-change is evaluated during the ngModelCtrl.$commitViewValue() IF the object reference of your ngModel has changed. the method $commitViewValue() is called automatically by $setViewValue(value, trigger) if you do not use the trigger argument or have not precised any ngModelOptions.
I specified that the ng-change would be automatically triggered if the reference of the $viewValue changed. When your ngModel is a string or an int, you don't have to worry about it. If your ngModel is an object and your just changing some of its properties, then $setViewValue will not eval ngChange.
If we take the code example from the start of the post
scope.setValue = function(value){
ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
var vv = ctrl.$viewValue;
vv.prop1 = prop1Value;
ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
After some research, it seems that the best approach is to use $timeout(callback, 0).
It automatically launches a $digest cycle just after the callback is executed.
So, in my case, the solution was to use
$timeout(scope.ngChange, 0);
This way, it doesn't matter what is the signature of your callback, it will be executed just as you defined it in the parent scope.
Here is the plunkr with such changes: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview
Samuli Ulmanen and lucienBertin's answers nail it, although a bit of further reading in the AngularJS documentation provides further advise on how to handle this (see https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).
Specifically in the cases where you are passing objects to $setViewValue(myObj). AngularJS Documentatation states:
When used with standard inputs, the view value will always be a string (which is in some cases parsed into another type, such as a Date object for input[date].) However, custom controls might also pass objects to this method. In this case, we should make a copy of the object before passing it to $setViewValue. This is because ngModel does not perform a deep watch of objects, it only looks for a change of identity. If you only change the property of the object then ngModel will not realize that the object has changed and will not invoke the $parsers and $validators pipelines. For this reason, you should not change properties of the copy once it has been passed to $setViewValue. Otherwise you may cause the model value on the scope to change incorrectly.
For my specific case, my model is a moment date object, so I must clone the object first before then calling setViewValue. I am lucky here as moment provides a simple clone method: var b = moment(a);
link : function(scope, elements, attrs, ctrl) {
scope.updateModel = function (value) {
if (ctrl.$viewValue == value) {
var copyOfObject = moment(value);
ctrl.$setViewValue(copyOfObject);
}
else
{
ctrl.$setViewValue(value);
}
};
}
The fundamental issue here is that the underlying model does not get updated until the digest cycle that happens after scope.updateModel has finished executing. If the ngChange function requires details of the update that is being made then those details can be made available explicitly to ngChange, rather than relying on the model updating having been previously applied.
This can be done by providing a map of local variable names to values when calling ngChange. In this scenario, you can mapping the new value of the model to a name which can be referenced in the ng-change expression.
For example:
scope.updateModel = function(item)
{
scope.ngModel = item;
scope.ngChange({newValue: item});
}
In the HTML:
<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>
See: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

AngularJS: how to compile form in a directive

I'm trying to create a custom directive that set 'required' and 'disabled' attributes to the element (form input). The directive gets this data from the scope object. But clearing of required field doesn't set my form to invalide state. I think it's about form compilation after changing the attribute. I tried to do that but got an infinite loop :(
How to compile my form correctly?
Here is plunker
You could just use ng-disabled and ng-required, instead of adding the attributes in a directive.
<div>
<label for="username">username2</label>
<input ng-model="data.account.username2"
ng-disabled="paintData['data.account.username2'] == 'RO'"
ng-required="paintData['data.account.username2'] == 'R'" />
</div>
Alternatively, you could define a directive template that uses ng-disabled and ng-required, and replace the original element with it:
.directive('metaValidate', function($compile) {
return {
restrict: 'A',
replace: true,
scope: {
model: '=',
paint: '='
},
template: '<input ng-model="model" ng-disabled="readonly" ng-required="required"/>',
link: function(scope, element, attrs) {
scope.required = scope.paint[element.attr('model')] === 'R';
scope.readonly = scope.paint[element.attr('model')] === 'RO';
}
};
});
Then, use it like this:
<input model="data.account.username2" meta-validate paint="paintData"/>
I prefer the first approach, because it can respond to changes to paintData. But, you have to repeat the property name several times.
If you want to try this code, here is an update of your Plunker.
Recompiling the element could work, but in order to avoid the infinite loop you need to first remove the meta-validate attribute (which would cause yet more compiling etc):
/* At the end of the directive's link function */
attrs.$set('metaValidate', undefined);
$compile(element)(scope);
See, also, this short demo.

How to update an input's value after calling $setViewValue

I have a form input with a ng-model as well as my custom directive which reads cookie data and should set the input value:
<form>
<input type="text" id="name" ng-model="name" my-cookie-directive>
</form>
My directive:
angular.module('myApp.directives').directive('myCookieDirective', ['CookieService', function(CookieService) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
var cookieVal = CookieService.getCookie(attrs.ngModel);
if(cookieVal != '') {
ctrl.$setViewValue(cookieVal);
elem.val(cookieVal); //not very cool hum?
}
}
};
}]);
When logging ctrl.$modelValue I can see that the right cookie data was set to my controller variable name but the input stays blank. I know that $setViewValue does not trigger a $digest and therefore tried ctrl.$render() but nothing happens.
I ended up using jQuery to set the input's value which is not satisfying at all.
Thanks in advance :)
You are correct in not wanting to use jQuery to set the input's value. Why would you be using Angular if you are going to do that then?
You can see a new Plunker here, using a different approach to the ones being mentioned. My suggestion: use NgModelController when you want to handle validations and format the model value.
For your current situation, you can use an isolated scope in the directive, and pass to it the scope property you want to update with the cookie value. Then in the directive, you can simply do:
scope.cookieField = cookieVal;
And Angular will handle the data binding and update the view value to match the model value. Plus, this is completely reusable.
Use $render and wrap everything in a function passed to $evalAsync:
if(cookieVal !== '') {
scope.$evalAsync(function(){
ctrl.$setViewValue(cookieVal);
ctrl.$render();
});
}
Plunker demo

Resources