button inside directive not being disabled - javascript

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);
}

Related

Restrict ng-href by validation in AngularJS

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')

Angularjs $watchCollection on $rootScope not triggered when values updated from directive

I'm running into a bit of an issue solving a problem with some Angularjs functionality I'm working on.
The basic idea is that I have a system where certain criteria must be met before the user is allowed to advance to the next section of the app. One example of this is that a user must both add a comment, and click a link (in the real app, this is a file download) in order for them to advance.
You can take a look at the full example here: https://jsfiddle.net/d81xxweu/10/
I'll assume that the HTML is pretty self explanatory and move on to what I'm doing with my Angular module. My app declaration and initialization are as follows:
var myApp = angular.module('myApp', ['ngRoute']);
myApp.run(function ($rootScope) {
// Both of these must be met in order for the user to proceed with 'special-button'
$rootScope.criteria = {
criteria1: false,
criteria2: false
};
});
This is pretty simple. I'm attaching an object called criteria to the root scope of the application in order to make it accessible to my directives and controllers. I have a directive that renders the link which allows the user to advance once the criteria are met. In this example the text of the link changes from "Waiting..." to "Click to proceed" to indicate we may advance.
myApp.directive('specialButton', function ($rootScope) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
scope.linkText = 'Waiting...';
var setLinkState = function(currentCriteria) {
var criteriaMet = true;
for(var k in $rootScope.criteria) {
if($rootScope.criteria[k] == false) {
criteriaMet = false;
}
}
if(criteriaMet) {
scope.linkText = 'Click to proceed';
}
};
// Watch for changes to this object at the root scope level
$rootScope.$watchCollection('criteria', function(newValues) {
setLinkState(newValues);
});
}
};
});
So in order to trigger the watch statement we've set on this directive I can add a comment as allowed by this controller:
myApp.controller('comments', function ($scope, $rootScope) {
$scope.commentText = '';
$scope.comments = [];
$scope.addComment = function () {
$scope.comments.push({ commentText: $scope.commentText });
$scope.commentText = ''
// When the user adds a comment they have met the first criteria
$rootScope.criteria.criteria1 = true;
};
});
The previous is my controller for displaying/adding comments. I set criteria1 to true here to indicate the user has added a comment. This actually works fine, and the $watchCollection in the specialButton directive is called as expected.
The problem arises when I try to perform the same action from the link that must be clicked in order to advance. This is rendered with a directive as it is my understanding that in a case such as this a directive makes more sense than a controller, unlike the comment list/form.
myApp.directive('requiredLink', function($rootScope) {
return {
scope: true,
template: "<a href='#'>Click me!</a>",
replace: true,
link: function(scope, el, attrs) {
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
});
}
};
});
As you can see here I pass in $rootScope just as in the controller. However when I set criteria2 to true the $watchCollection is not triggered.
So what ends up happening is if I add a comment first, then click the other button, I do not see specialButton update its text because the second change never triggers the watch. If, however, I click the link first, then add a comment, specialButton updates as expected. The click of requiredLink IS updating the data, but not triggering the watch. So when I then add a comment and the $watch is triggered it sees that BOTH have been set to true.
Thanks in advance for any help you can offer in resolving this issue; I appreciate your time.
Your actual problem is you are update $rootScope from the event which is outside the angular context, so its obivious that angular binding will not update because digest cycle doesn't get fired in that case. You need to fire it manually by using $apply() method of $rootScope
el.bind('click', function(evt) {
evt.preventDefault();
// When the user clicks this link they have met the second criteria
$rootScope.criteria.criteria2 = true;
$rootScope.$apply(); //this will run digest cycle & will fire `watchCollection` `$watcher`
});
Demo Plunkr
Though this solution work but I'll suggest you to use service instead
of using $rootScope
For implementation using service you need to follow below things that would help you.
Your service should be using criteria variable in object form, should follow the dot rule so that the respective reference will update using JavaScript prototypal
Sevice
app.service('dataService', function(){
this.criteria = {
criteria1: false,
criteria2: false
};
//...here would be other sharable data.
})
Whenever you want to use it any where you need to inject it in function of controller, directive, filter wherever you want.
And while putting watch on service variable from directive you need to do something like below.
Directive
myApp.directive('specialButton', function (dataService) {
return {
scope: true,
template: "<a href='#'>{{ linkText }}</a>",
replace: true,
link: function (scope, el, attrs) {
//.. other code
// deep watch will watch on whole object making last param true
scope.$watch(function(){
return dataService.criteria //this will get get evaluated on criteria change
}, function(newValues) {
setLinkState(newValues);
}, true);
}
};
});

AngularJS: Custom validator trying to validate immediately

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.

Angular custom validation directive - how to properly skip ng-disabled field?

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);
}});

Updating attrs value inside directive - how to do it in AngularJS

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.

Categories

Resources