handle submit event in directive as well - angularjs

I’m quite new to Angular but I love it already!
I need to write a couple of reusable components for a wizard.
I would like to handle the submit event from the form in the directive as well, is this possible?
<form ng-submit="submit()" name="exampleForm">
<foo data="someData"></foo> <!-- I need to handle the submit event in directive as well-->
<input type="submit" id="submit" value="Submit"/>
</form>
If the user presses enter or clicks the button on the form the directive has to make a call to the backend and double check the data.
If the check is successful the form will be valid.
I built a simple example here
Thanks in advance!
Stefan

Yes it's possible.
The first issue is targeting the correct $scope. Right now your code targets the submit() function on the controller. The <form> can't see the $scope of the directive due to how the html elements are nested.
Since you want to submit() from the directive, then the directive template should also include the <form> element and the submit input button.
Invoking foo will look like this:
<foo data="someData" name="exampleForm"></foo>
If you would rather keep foo the way it is (and that's a totally legit way to go about it) then you'll need a new directive with the submit() function. So you'd have 2 directives working together (Very angular! Many wow!).

What you will want here is a custom directive for form validation. Custom form validators are added to the $validators object on the ngModelController.
For a simple example for an "integer" directive (found in the Angular docs here under "Custom Validation"), the directive would be defined like this:
var INTEGER_REGEXP = /^\-?\d+$/;
app.directive('integer', function() { return {
require: 'ngModel',
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.integer = function(modelValue, viewValue) {
if (ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
if (INTEGER_REGEXP.test(viewValue)) {
// it is valid
return true;
}
// it is invalid
return false;
};
} }; });
And it would be used in the form like this:
<form name="form" class="css-form" novalidate>
<input type="number" ng-model="size" name="size" min="0" max="10" integer />{{size}}<br />
</form>
There is also an option for asynchronous validation, adding the validator to the $asyncValidatorsobject. More info is found at the link mentioned above. The working example using both is found here

Here's a working example:
http://plnkr.co/edit/sckCOq3a50PBjat2uixk?p=preview
I've created another 2-way binding on the directive's scope (called "submit"), so you can specify the name of the submit function as it will be seen from ExampleController's scope (I used "directiveSubmit" here).
scope: {
data: '=',
submit: '='
},
.
<foo data="someData" submit="directiveSubmit"></foo>
The directive's controller then creates that function and assigns it to its own scope, and that also assigns it to ExampleController's scope via the magic of 2-way binding.
Next you can run that submit function from ExampleController's scope and it will actually be referring to the newly created function in the directive.
<form ng-submit="directiveSubmit(exampleForm)" name="exampleForm" novalidate>
Notice that I'm also passing the exampleForm into that function. Without that the directive will have a hard time getting access to it.
Finally, I've also added novalidate to the form because if you don't do that some browsers (i.e. Chrome) will handle form validation in a way that may (or may not) be undesirable. In Chrome try removing that and submitting, then you'll see what I mean (I also made the "name" field required).

Related

Retrieve all inputs values from a form AngularJS

I have a massive form with more or less 80 / 90 inputs.
My main problem is How can I pass all those inputs belonging to unique form in an ajax request without map the inputs manually into a object?
I know that with jquery you can use serialize() function selecting the form. Is there any helper function in angular to achieve that?
Thanks
Indeed, as Michal's comment suggested, it doesn't look like you are using ng-model.
Angular's jqLite doesn't support serialize() since with Angular one would typically build a ViewModel that would then be bound to a form.
But, if you are out of luck, you could add jQuery.js to get the serialize() support and create a simple directive - serializer - that would act on a form.
app.directive("serializer", function(){
return {
restrict: "A",
scope: {
onSubmit: "&serializer"
},
link: function(scope, element){
// assuming for brevity that directive is defined on <form>
var form = element;
form.submit(function(event){
event.preventDefault();
var serializedData = form.serialize();
scope.onSubmit({data: serializedData});
});
}
};
});
And use it as follows:
<form serializer="submit(data)">
<input name="foo">
<input name="bar">
<button type="submit">save</button>
</form>
And in the controller:
$scope.submit = function(data){
console.log(data);
}
plunker
EDIT:
If you are using ng-model and in fact have a proper ViewModel, then this is the "Angular way" and so, you should have some object that is bound to the form inputs - you should just submit that.
<form name="foo" ng-submit="onSubmit()">
<input ng-model="fooObject.a">
<input ng-model="fooObject.b">
...
</form>
$scope.fooObject = {};
$scope.onSubmit = function(){
$http.post("url/to/server", {data: $scope.fooObject})
.success(...);
}

Calling methods conditionally inside Angularjs directives

I'm a beginner to Angularjs, what I'm trying to do is call two different methods from a directive depending on the condition.
My work flow is
I have a form in a directive
When i try to add a new record via the form, function in the
directive should call the save() method
when I try to update a new record, function in the directive should
call the update method
This is my current code
#new html form (calling the form directive)
<recipe-form recipeForm="recipeFormData" > </recipe-form>
#edit html form (calling the form directive)
<recipe-form recipeForm="recipeFormData" > </recipe-form>
So in my directive I have the following method
#form directive
<form id="signup-form_id" ng-submit="$parent.Update()">
So what I want to do is, when the directive calls from the #new I want the method to be
<form id="signup-form_id" ng-submit="$parent.Create()">
So what I want to do is, when the directive calls from the #edit I want the method to be
<form id="signup-form_id" ng-submit="$parent.Update()">
I was trying to pass the method as a param, but for some reason it didn't work:
#update form
<recipe-form recipeForm="recipeFormData" update="Update()"> </recipe-form>
You didn't tell how you distinguish between the two actions, but you can always specify the method in a boolean variable (that you specify as a directive attribute, or a controller's scope variable, etc... let's call it updateMethod) and use it like this:
<form id="signup-form_id" ng-submit="updateMethod ? $parent.Update() : $parent.Create()">
Or even two separate forms (especially useful if the forms themselves are different):
<form id="signup-form_id" ng-submit="$parent.Update()" ng-if="updateMethod">
and:
<form id="signup-form_id" ng-submit="$parent.Create()" ng-if="!updateMethod">
The third option would be using a single submit function, but putting an if/else branching inside it.
You actually pass the update method result by writing the () brackets, not its reference.
I advice you to pass your update / create function in your directive attributes. This way is more parametrable, and doesn't refer to $parent wich is not well isolated.
HTML:
<recipe-form custom-data="recipeFormData" method="create"></recipe-form>
<recipe-form custom-data="recipeFormData" method="update"></recipe-form>
Controller :
$scope.recipeFormData = { foo: 'bar' };
$scope.create = function (data) {
// create something
}
$scope.update = function (data) {
// update something
}
Directive :
app.directive('recipeForm', function () {
return {
templateUrl: 'mySuperDirective.html',
// creates an isolated scope
scope: {
customData: '=',
method: '='
}
};
});
mySuperDirective.html :
<form ng-submit="method()">
<input type="text" ng-model="customData.foo">
<button type="submit">OK</button>
</form>

Angular - Form Validation Custom Directive, how to pass result to another element for the error display?

So basically I'm building myself a new directive for form validation and I got it all working perfectly except for 1 thing which I believe is not properly coded. So as you know form validation with a directive is to validate an <input ngx-validation="alpha"> how do I pass the error text, which occurs inside my directive to the other element (a Bootstrap <span class="text-danger">{{ validation_errors["input1"] }}</span> that is right below my input? As for the moment, I created a scope variable which exist inside my controller and it does work...until the user forgets to create that actual variable... So how am I suppose to share the information for 1 element to another? Which by the way, my variable is an array holding all input error messages... Here is my code at the moment:
<!-- html form -->
<input type="text" class="form-control" name="input1" ng-model="form1.input1" ngx-validation="alpha|min_len:3|required" />
<span class="validation text-danger">{{ validation_errors["input1"] }}</span>
JS Code
// my Controller
myApp.controller('Ctrl', ['$scope', '$translate', function ($scope, $translate) {
$scope.form1 = {};
$scope.validation_errors = []; // the scope variable
}]);
// creation of my directive
angular.module('ghiscoding.validation', ['pascalprecht.translate'])
.directive('ngxValidation', function($translate){
return{
require: "ngModel",
link: function(scope, elm, attrs, ctrl) {
// get the ngx-validation attribute
var validationAttr = attrs.ngxValidation;
// rest of the validation code...
// ...
if(!isFieldValid && ctrl.$dirty) {
scope.validation_errors[ctrl.$name] = message;
}else {
scope.validation_errors[ctrl.$name] = "";
}
return value;
};
ctrl.$parsers.unshift(validator);
ctrl.$formatters.unshift(validator);
}
};
});
If you look at my code, the scope variable in question which I use is called $scope.validation_errors = []; which is created in the Controller, without creating it, it will of course fail. You can also see my github, I made it available and also wish that lot of people could use my Angular-Validation directive because it's just so easy the way I've done it :) See my Github Angular-Validation
EDIT
Just to make it clear, the validation part of my directive is working fine. My real problem is simply, how do I pass the error message from the directive (which the directive is connected to the <input>) and pass that error message (a simple string) to the <span> they are 2 different elements, how can I talk to another element within a directive, how can I bind? At the moment I'm using a global variable, which needs to exist in the controller, this isn't good... I am very new to Angular and I'm struggling with directives, so please provide code. Thanks a lot for help.
ANSWER
So to have a full Angular Directive, the last piece of code for the solution was answered here... Since the <span> for displaying my error message is always after my input, I can simply update the next element text with native Angular jqLite...that's it, as simple as that... Here is my new HTML code
<!-- HTML -->
<input type="text" name="input1" validation="alpha|min_len:3|required" />
<span class="validation text-danger"></span>
// Previous piece of code to replace
if(!isFieldValid && ctrl.$dirty) {
scope.validation_errors[ctrl.$name] = message;
}
// REPLACED by this
if(!isFieldValid && ctrl.$dirty) {
elm.next().text(message);
}
You should do the same thing as all the other built-in form validation directives do: use the ngModelController's $setValidity method, using your own key. Your span will then be able to check if an error exists for your validation key using
theFormName.theInputName.$error.yourValidationKey
And the validity of the field, as well as the validity of the enclosing form, will automatically be handled by angular.
See http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController.

AngularJs can't access form object in controller ($scope)

I am using bootstrap-ui more specifically modal windows. And I have a form in a modal, what I want is to instantiate form validation object. So basically I am doing this:
<form name="form">
<div class="form-group">
<label for="answer_rows">Answer rows:</label>
<textarea name="answer_rows" ng-model="question.answer_rows"></textarea>
</div>
</form>
<pre>
{{form | json}}
</pre
I can see form object in the html file without no problem, however if I want to access the form validation object from controller. It just outputs me empty object. Here is controller example:
.controller('EditQuestionCtrl', function ($scope, $modalInstance) {
$scope.question = {};
$scope.form = {};
$scope.update = function () {
console.log($scope.form); //empty object
console.log($scope.question); // can see form input
};
});
What might be the reasons that I can't access $scope.form from controller ?
Just for those who are not using $scope, but rather this, in their controller, you'll have to add the controller alias preceding the name of the form. For example:
<div ng-controller="ClientsController as clients">
<form name="clients.something">
</form>
</div>
and then on the controller:
app.controller('ClientsController', function() {
// setting $setPristine()
this.something.$setPristine();
};
Hope it also contributes to the overall set of answers.
The normal way if ng-controller is a parent of the form element:
please remove this line:
$scope.form = {};
If angular sets the form to your controllers $scope you overwrite it with an empty object.
As the OP stated that is not the case here. He is using $modal.open, so the controller is not the parent of the form. I don't know a nice solution. But this problem can be hacked:
<form name="form" ng-init="setFormScope(this)">
...
and in your controller:
$scope.setFormScope= function(scope){
this.formScope = scope;
}
and later in your update function:
$scope.update = function () {
console.log(this.formScope.form);
};
Look at the source code of the 'modal' of angular ui bootstrap, you will see the directive has
transclude: true
This means the modal window will create a new child scope whose parent here is the controller $scope, as the sibling of the directive scope. Then the 'form' can only be access by the newly created child scope.
One solution is define a var in the controller scope like
$scope.forms = {};
Then for the form name, we use something like forms.formName1. This way we could still access it from our controller by just call $scope.forms.formName1.
This works because the inheritance mechanism in JS is prototype chain. When child scope tries to create the forms.formName1, it first tries to find the forms object in its own scope which definitely does not have it since it is created on the fly. Then it will try to find it from the parent(up to the prototype chain) and here since we have it defined in the controller scope, it uses this 'forms' object we created to define the variable formName1. As a result we could still use it in our controller to do our stuff like:
if($scope.forms.formName1.$valid){
//if form is valid
}
More about transclusion, look at the below Misco's video from 45 min. (this is probably the most accurate explanation of what transcluded scopes are that I've ever found !!!)
www.youtube.com/watch?v=WqmeI5fZcho
No need for the ng-init trickery, because the issue is that $scope.form is not set when the controller code is run. Remove the form = {} initialization and get access to the form using a watch:
$scope.$watch('form', function(form) {
...
});
I use the documented approach.
https://docs.angularjs.org/guide/forms
so, user the form name, on "save" click for example just pass the formName as a parameter and hey presto form available in save method (where formScopeObject is greated based upon the ng-models specifications you set in your form OR if you are editing this would be the object storing the item being edited i.e. a user account)
<form name="formExample" novalidate>
<!-- some form stuff here -->
Name
<input type="text" name="aField" ng-model="aField" required="" />
<br /><br />
<input type="button" ng-click="Save(formExample,formScopeObject)" />
</form>
To expand on the answer by user1338062: A solution I have used multiple times to initialize something in my controller but had to wait until it was actually available to use:
var myVarWatch = $scope.$watch("myVar", function(){
if(myVar){
//do whatever init you need to
myVarWatch(); //make sure you call this to remove the watch
}
});
For those using Angular 1.5, my solution was $watching the form on the $postlink stage:
$postLink() {
this.$scope.$watch(() => this.$scope.form.$valid, () => {
});
}

AngularJS - setting focus to element using NON-ISOLATE directive

I know this question has been asked about 100 times (trust me, I've read them all), but I'm having trouble getting focus to go to an input box when the directive does NOT use isolate scope. The scope.$watch doesn't fire when the backing data changes.
Why not just use the one with isolate scope, you ask? Well, my understanding is that you should ONLY use isolate scope if your directive has a template.
The only differences in the directives is:
// works
app.directive('doesFocus', function ($timeout) {
return {
scope: { trigger: '#doesFocus' },
link: function (scope, element) {
scope.$watch('trigger', function (value) {
// sets focus
}
...
// does not work, and in fact when I inspect attrs.doesNotFocus it is undefined
app.directive('doesNotFocus', function ($timeout) {
return {
scope: false,
link: function (scope, element, attrs) {
scope.$watch(attrs.doesNotFocus, function (value) {
// sets focus
}
...
I'm on week 3 of using Angular, so I must be missing some silly semantic issue.
Here is a fiddle illustrating my issue.
http://jsfiddle.net/tpeiffer/eAFmJ/
EDIT
My actual problem was that my real code was like this (hazard of mocking the problem, you sometimes mask the real problem):
<input should-focus="{{isDrawerOpen()}" ... ></input>
but because I was using a function, not a property, I was missing the required ticks
<input should-focus="{{'isDrawerOpen()'}}" ... ></input>
Making this change fixed the problem and my directive can still be like this:
scope.$watch(attrs.shouldFocus, focusCallback(newValue));
END EDIT
Thanks for helping me in my quest for angular excellence!
Thad
Remove {{}} from your HTML. So instead of:
<input class="filter-item" placeholder="Enter filter"
does-not-focus="{{bottomDrawerOpen}}" type="text">
use
<input class="filter-item" placeholder="Enter filter"
does-not-focus="bottomDrawerOpen" type="text">
Then it works with watching attrs.doesNotFocus:
scope.$watch(attrs.doesNotFocus, function (value) {...} );
Fiddle
Your bottom drawer was watching a function isDrawerOpen(), not a property.
Change
scope.$watch('isDrawerOpen()',...);
to
scope.$watch('toggleBottomDrawer',...);

Resources