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')
Related
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);
}
};
});
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.
I am getting into AngularJS, and I've been trying to understand directives because they are pretty much mandatory if you want to work with the DOM (when using AngularJS, correct me if I'm wrong). So here is the scenario, I am trying to create a simple login system (I am actually using the MEAN stack - MongoDB, ExpressJS, AngularJS, NodeJS). I'm not too worried about security (or otherwise less than perfect code) because I am just trying to learn how to use the frameworks. Here is the relevant code:
MemberModule.js:
var MemberModule = angular.module('MemberModule', ['ui.bootstrap']);
MemberModule.controller('MemberListController', function ($scope, $html)) {
$scope.members = [];
$scope.newMember = {
done : false
};
$scope.doneFilter = { done : true };
$scope.notDoneFilter = { done : false };
//various methods...
});
MemberModule.directive('usernameDir', ['$interval', function($interval) {
function link(scope, element, attrs) {
var newMember,
timeoutId;
function updateUsername() {
element.text(scope.newMember.username);
}
scope.$watch(attrs.myCurrentTime, function(value) {
format = value;
updateTime();
});
element.on('$destroy', function() {
$interval.cancel(timeoutId);
});
// start the UI update process; save the timeoutId for canceling
timeoutId = $interval(function() {
UpdateTime(); // update DOM
}, 1000);
}
return {
link: link
};
});
MemberModule.directive('passwordDir', function () {
// The above name 'myDirective' will be parsed out as 'my-directive'
// for in-markup uses.
return {
restrict: 'E',
transclude: true,
scope: {
'sub' : '&ngSubmit'
},
template: 'home'
}
});
As you can see above, I created the main angular.module and called it MemberModule - which gets referenced in my HTML (I am using jade templates - so by HTML I mean layout.jade). After that I created the controller with its various methods that I need. Finally, I created the directives which is what I need help with. I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
The relevant form HTML ('index.jade'):
extends layout
block content
div.container(ng-controller="MemberListController", ng-init="setMembers( #{JSON.stringify(members)} )")
h1 Welcome
h2 Sign Up
form(novalidate, ng-submit="addNewMember()")
input( type="text", username-dir info="userdir")
br
input( type="password", password-dir info="passdir")
br
input( type="password" )
br
button.btn.btn-primary(class="sub", type="submit") Submit
h2 Adding...
span(username dir)
span(password dir)
I am just pasting what I have so far so you can see where I am at in terms of progress. I am fully aware that my code is not functional as is - I am just looking for some help in pointing out what needs to go where to accomplish my goal. I realize that the two directives (while trying to attain the same goal) are not using the same style of directive code - this is just because of where I am at in terms of trying things. Again, my goal is (specifically for the username and password):
I am trying to assign a DOM input element (in a form) to an object attribute, and then redirect (or render) a jade template (home.jade).
Thanks.
Big ups to Julian Hollmann (check comments):
"You don't need both directives at all. Just use ng-model (docs.angularjs.org/api/ng/directive/ngModel) to bind your scope data to the input elements. Then use ng-submit to call a function in the controller."
Bingo - thanks!
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);
}});