I made myself a custom directive and it works fine but now I got a Form which has some disabled field with ng-disabled, I believe that I have to call up the setTimeout function since the ng-disabled could be happening after the fact but I'm not sure I coded it properly... is my code the correct way? I'm not sure if there's a special location to put the setTimeout piece of code, and I'm not even sure it is correct actually... but it does seems to work...so could someone validate and/or update my code if need be?
// Angular - custom Directive
directive('myDirective', function($log) {
return {
require: "ngModel",
link: function(scope, elm, attrs, ctrl) {
validate = function(value) {
.....
}
var validator = function(value) {
// invalidate field before doing validation
ctrl.$setValidity('validation', false);
elm.unbind('keyup').bind(keyup, function() {
// make the regular validation of the field value
var isValid = validate(value); // call validate method
scope.$apply(ctrl.$setValidity('validation', isValid));
});
// for the case of field that might be ng-disabled, we should skip validation
setTimeout(function() {
if(elm.attr('disabled')) {
ctrl.$setValidity('validation', true);
}
}, 0);
return value;
};
// attach the Validator object to the element
ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator);
}
};
});
EDIT
I have to note that this piece of code is a really tiny part of my code, I only took the relevant part of it and yes at first look the unbind('keyup') doesn't make much sense unless you see the real code which is more like unbind('keyup').bind(optionEvnt)...which is actually giving an extra optional feature of choosing the event trigger you want to use on the validator and the default keyup was interfering when I was using blur event. In many Forms validation, I prefer to use the blur event so that's why it's an optional feature.
The real code is available on my Github / Angular-Validation and is available to everyone to use...Take a look and you'll probably love it enough to use it in your code :)
You seem to have a lot of unnecessary code in there, unless I am missing what your actual intent is. This should work.
// Angular - custom Directive
directives.directive('myDirective', function($log) {
return {
require: "ngModel",
link: function(scope, elm, attrs, ctrl) {
var validate = function(value) {
return (value === "valid");
};
var validator = function(value) {
ctrl.$setValidity('validInput', validate(value));
return value;
};
// attach the Validator object to the element
ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator);
// Observe the disabled attribute
attrs.$observe("disabled",function(disabled) {
if(disabled){
// Turn off validation when disabled
ctrl.$setValidity('validation', true);
} else {
// Re-Validate the input when enabled
ctrl.$setValidity('validation', validate(ctrl.$viewValue));
}
});
}
};
});
I think if you use angular's $timeout instead of javascript's native setTimeout() you'll have more luck, since $timeout lets angular know whats happening, what needs to be updated and all that. However I think the best solution for what you're looking to do is observing the disabled attribute of the directive, no need for timers and intervals:
attrs.$observe("disabled",function(value) {
if(value){
ctrl.$setValidity('validation', true);
}else{
ctrl.$setValidity('validation', false);
}});
Related
I've been searching around for a solution on this one and have been stumped. I'm kind of new to AngularJS so I don't know all of the good tricks it's got yet. I have a multi-part form that is HTTP GETed at the end using ng-href. Here's the code snippet that submits everything.
<a ng-href="#/report/{{ctrl.model}}" ng-click="ctrl.createReport()">Finish</a>
now I'm faced with adding validations to this form, and I want to prevent the link from being followed if the validation fails. Validation logic is contained in a controller function, the function will return true or false base on the result of the validation.
Unfortunately, this piece of code is a part of a large implementation developed by someone else. I just want to add the validation part without having to modify too much logic within the code.
Is there any way to put a condition on ng-href? So that when Finish is clicked, the browser will only follow the URL if validation passes. Otherwise, is there anyway to perform the same GET programmatically within the controller? I've looked at using $http.get() and $window.location.href. The former seem to be AJAX which does not redirect the browser to the name URL. The latter, I don't know how to expand the ctrl.model into a proper GET string.
Any thoughts, ideas, suggestion would be greatly appreciated. Thanks!!!
Solution Used
HTML:
<a data-ng-click="ctrl.createReport()">Finish</a>
JS:
if (validate()) {
$location.path('/report/' + angular.toJson(self.model, false));
}
remove the ng-href all together. and use the $location service.
function createReport(){
if(myForm.$valid){
$location.path('/report'+model);
}
}
Unfortunately, this piece of code is a part of a large implementation developed by someone else. I just want to add the validation part without having to modify too much logic within the code.
I have a hack for you. This directive eats the click if the expression returns false. So, the href is never followed by the browser. Also prevents the execution of ng-click.
javascript
module.directive('eatClickIf', ['$parse', '$rootScope',
function($parse, $rootScope) {
return {
// this ensure eatClickIf be compiled before ngClick and ngHref
priority: 100,
restrict: 'A',
compile: function($element, attr) {
var fn = $parse(attr.eatClickIf);
return {
pre: function link(scope, element) {
var eventName = 'click';
element.on(eventName, function(event) {
var callback = function() {
if (fn(scope, {$event: event})) {
// prevents ng-click to be executed
event.stopImmediatePropagation();
// prevents href
event.preventDefault();
return false;
}
};
if ($rootScope.$$phase) {
scope.$evalAsync(callback);
} else {
scope.$apply(callback);
}
});
},
post: function() {}
}
}
}
}
]);
html
<a ng-href="#/report/{{ctrl.model}}"
ng-click="ctrl.createReport()"
eat-click-if="!ctrl.modelIsValid()">Finish</a>
I will suggest you to remove the ng-href value and change path from js condition using $location
<a ng-href="" ng-click="ctrl.createReport()">Finish</a>
Inject $location sservice and do:
$location.path('/newValue')
I have a directive to serve as a credit card form. This form can have many different submit buttons. During the purchase, which is async, I need to make sure that the buttons are disabled. So I'm using a simple observer pattern to accomplish this. The issue I'm having is that when the user clicks a button, the observer pattern is working fine the isolated scope attribute controlling the ng-disable is being set correctly, however the disabled isn't being applied to the buttons. I'm thinking it might be a priority thing?
So heres an observer. The subject is rather mundane. Just validates a form, and has a list of it's observers. Here's where I'm having issues.
.directive('submitCardButton', ['$q', function ($q) {
return {
restrict: 'E',
require: '^createCard',
scope: {
successBack: '&',
buttonVal: '#'
},
template: "<button class='button button-pink full-width small-top' ng-class=\"{disabled: submitting}\" ng-click='getCC()' ng-disabled=\"submitting\">{+ submitting +} {+ buttonVal +}</button>",
link: function (scope, elem, attr, card) {
card.registerButton(scope);
scope.submitting = false;
function getCC () {
card.disableButtons();
card.getCC().then(function (response) {
$q.when(scope.successBack({response:response}))
.finally(card.enableButtons);
}, function () {
card.enableButtons();
});
}
scope.disable = function () {
scope.submitting = true;
console.log(scope.submitting);
};
scope.enable = function () {
scope.submitting = false;
console.log(scope.submitting);
};
scope.getCC = getCC;
} // end link
};// end return
}])// end directive
When I debug, inside the getCC, after I call the disableButtons the submitting is set to true. Howerver the submitting inside the template is still false and therefore not disabled. Any help would be soooo much appreciated.
I created a plunkr that demonstrates the issue I'm having. I'm using a simple user first name last name to show the issue. It works fine if you submit properly. However, if you just submit with out putting any data in, you can see that the submitting flag in the button directive is set to True, but the disabled is not being set properly.
http://plnkr.co/edit/8KTUCNMPBRAFVl1N4nXp?p=preview
In your createCard.getCC() the positive case returns an unresolved promise (it is resolved later with a $timeout), so while the promise is unresolved, the submitting property of submitCardButton's scope is "true" and button is disabled.
In the negative case, the promise is rejected right away (synchronously), and so there is no time for the button to be disabled - the promise rejection handler sets submitting immediately to false.
If you want to see the effect, change the negative use case to this:
if (!(user.firstname && user.lastname)) {
$timeout(function() {
defer.reject('bad user! bad!');
}, 5000);
}
What Angular says...
[ngSubmit] prevents the default action (which for form means sending the request to the server and reloading the current page), but only if the form does not contain action, data-action, or x-action attributes.
So if you were unable to remove the [action] attribute from HTML, how would you override this behavior to inject custom code on form submittion and prevent the defined [action] to get triggered?
One possible solution is to create a directive and override the DOM property "onsubmit". The CONS here is you are forced to configure it on backend when you could reach the same using the angular attribute
app.directive("contactForm", function(){
return {
link: function( scp, elm, att )
{
elm[0].onsubmit = function( evt )
{
/* your custom code here */
}
}
};
});
Thanks in advance
If you want to remove action before ng-submit gets compiled, just create a directive with a higher priority, that removes the attribute.
app.directive('remove-action', function () {
return {
priority: 1, // ngSubmit has priority 0
compile: function (element) {
element.removeAttr('action');
return function link () {};
}
};
});
So I was having a similar problem and here is how I finally solved it.
For angularjs 1.2, it's enough to set action to blank
action = ''
For angularjs 1.3, you have to set action to something like this
action = 'javascript:;'
I am not sure if this is valid html, or best practices, but it does work.
I have a form where some of the inputs are hooked up to a custom validator via a directive. The input should validate on blur, and does so via an asynchronous REST API call.
HTML:
<input type="text"
validate-this
ng-model="thisField"
ng-model-options="{'updateOn': 'blur'}"
ng-pattern="some pattern"
/>
Directive (shortened for brevity):
return {
access: 'A',
require: 'ngModel',
scope: false,
link: function (scope, elem, attrs, ngModel) {
ngModel.$asyncValidators.validateThis = function (modelVal, viewVal) {
if (!modelVal && !viewVal) return $q.defer().promise;
// returns a promise from the api service
return api.doSomeValidation();
};
}
};
The above code works perfectly, but notice the hackish line directly below the validation function signature:
if (!modelVal && !viewVal) return $q.defer().promise;
Without that line, Angular attempts to validate the field immediately on application load instead of only on blur. This is a problem as the actual validation code does some string parsing, and since both modelVal and viewVal are undefined, JavaScript throws an error.
I have tried disabling the functionality that loads data into the fields when the application loads, and the error still happens. The pattern specified in ng-pattern, however, does respect my wishes and only validates on field blur--it does NOT attempt to validate on page load. Is there any way to tell Angular to only validate on blur, or to get it to stop trying to validate as soon as the page loads? Or am I using $asyncValidators incorrectly?
Angular executes the $validate during the signal of each attr.$observe for the input validations directives such as ngPattern. You can see that in their patternDirective function.
I have been trying to find a way around this as I use many of the input validations (pattern, max length, required, etc.) and my $asyncValidators are triggering 7 times during load. This caused the web server to execute for each trigger.
Solutions:
cache the web method response
attach your handler after the page load (or have some type of flag)
bake the ngPattern test into your async handler. I would probably go with this one.
ngModel.$asyncValidators.validateThis = function (modelVal, viewVal) {
var deferred = $q.defer();
if (!modelVal && !viewVal)
deferred.resolve();
else if (!myPattern.test(modelVal))
deferred.reject();
else
api.doSomeValidation(value).then(function (result) {
deferred.resolve();
}, function (result) {
deferred.reject();
})
return deferred.promise;
};
Hopefully this helps as I am in the same boat looking for a solution.
In a nutshell:
I try to do something like this inside my directive - namely change the value of model that is liked to 'trigger' attribute:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
and it does not work. Why? Should I do it other way around?
And here is full story:
I have a dialog that is triggered when showRemoveDialog flag is set. This flag is set when user clicks Remove button.
Here is a dialog's opening tag:
<div remove-dialog trigger="{{showRemoveDialog}}" class="modal fade" id="myModal">
Then I have a directive removeDialog:
myApp.directive("removeDialog", function () {
return {
restrict: 'A',
link: function (scope, element, attrs, controller) {
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
attrs.$observe('trigger', function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
});
},
controller: 'DeleteController'
};
});
As you can see, I observe trigger attribute and when it changes to true (user clicks Remove button), I show the dialog:
$scope.remove = function () {
$scope.showRemoveDialog = true;
};
And it works.
But if the value of trigger changes to false/null I want to close it - for instance Cancel button was clicked, or X icon was clicked. And if one of these two actions occur, I need to set back trigger value to false/null, so that the next time when user click on Remove button value would change from false -> true, and my dialog appears once again.
The problem is that this piece of code does not work:
angular.element(element).on('hidden.bs.modal', function () {
scope.$apply(function () {
attrs.$set('trigger', null);
});
});
I mean it does not set the value of {{showRemoveDialog}} in scope to null. I already tried $apply function, but still in wain.
I guess I'm doing something really wrong in angular. Please help.
Yes, the idea you have come up with is kind of confusing, changing the attribute will not actually change the scope variable, so to fix this you would have to change the scope variable, in this case you know what the variables name is so it would work, but for other elements you might not know what the variable is. To fix this specific issue you would have to do.
scope.showRemoveDialog = null;
scope.$apply();
This is not very dynamic though. Here is what I would do (not tested).
Pass the variable name in as a string
trigger="showRemoveDialog"
Then in your directive get some help from $parse
myApp.directive("removeDialog", function ( $parse ) { ....
The link function...
link: function (scope, element, attrs, controller) {
var variableName = attrs.trigger;
angular.element(element).on('hidden.bs.modal', function () {
$parse(variableName + ' = null')(scope);
scope.$apply(); // Might not be needed.
});
scope.$watch(variableName, function (newValue) {
if (newValue) {
angular.element(element).modal('show');
} else {
angular.element(element).modal('hide');
}
}, true); // true might not be needed.
},
Also you don't need to do angular.element(element) as the element passed to the link function should already be wrapped.
The first argument to the jQuery on() method is the event you're listening for. I've never seen it used with custom events before, only standard Javascript ones like "keydown". So my first question would be have you tested that the event hook is ever called? If not put a console.log("event called"); before you try to set your element's trigger attribute.
Another thing I would mention is that setting an attribute to null like that wont work. Have a look at the AngularJS source code . Instead I would set the attribute to false.
Lastly I would recommend just using the Angular UI Bootstrap library that includes a nice modal feature - or something else, I don't mind but reinventing the wheel here seems unnecessary.