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