I have questions about Angular directives. The following is my code:
main controller & the directive:
<div ng-controller='ShopsController'>
<update-createform shop="shop" action='update()'></update-createform>
</div>
directive js:
(this way the direction action will take the 'action' input argument)
angular.module('app')
.directive('updateCreateform', function(){
return {
templateUrl: '/form.html',
restrict : 'E',
scope: {
shop: '=',
action: '&'
}
}
})
form.html template:
<form name="shopForm" ng-submit='action(shopForm.$valid)' novalidate>
<input type='text' name='name' required/>
<input type='text' name='description' required/>
</form>
ShopsController has a method:
exports.update = function(isValid) {
if (isValid) { /* update the shop*/ }
}
What I am doing is I am passing the shop data I get from the server, send it into the form so I can view and/or update the shop info.
It's also that I want to create shop info using the same form. In this case I just send in shop = [] and action='create()' instead.
My controller has an update method that takes the argument isValid. I don't know how to pass the directive shopForm.$valid outside and send it to server.
Two questions:
how do I get isValid variable from the directive?
Following Ari Lerner's ng-book: He said it's possible to do the following:
http://www.scribd.com/doc/215682987/NG-Book-The-Complete-Book-on-AngularJS-2013
instead of using directive above we use
<update-createform shop="shop" on-update='update()' on-create='create()'></update-createform>
and the directive 'action' will change to 'update' when shop is not empty otherwise action equals to 'create'? I tried his code but I cannot get it to work..
Any help would be greatly appreciated!
You can add an argument to action=update(isValid). This then gets resolved on the form submit.
So your html would look like this
<div ng-controller='ShopsController as shopCtrl'>
<update-createform shop="shop" action='shopCtrl.update(isValid)'></update-createform>
</div>
And your form would look like like this
<form name="shopForm" ng-submit='action({isValid:shopForm.$valid})' novalidate>
<input type='text' name='name' required/>
<input type='text' name='description' required/>
<button type="submit">Submit</button>
</form>
and controller would be
.controller('ShopsController', function() {
var exports = this;
exports.update = function(isValid) {
console.log(isValid)
if (isValid) { /* update the shop*/ }
}
})
http://plnkr.co/edit/Qh3HzKGnOo1NTP9Pfsmh?p=preview
OR
There's another way, although personally i find the syntax a little odd. Not that the first solution feels that intuitive either.
http://plnkr.co/edit/CRN9ruRekJiozJIBTe80?p=preview
Found that one in an excellent post about directives by Dan Wahlin
http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters
Related
EDIT:
It seems like the Error or wrong handling is because the scope does not gets updated when the email field is not valid... is there a way to chang that?
this is my first question on stackoverflow so i hope i will do it right.
I am pretty new to angular js and i am creating some basics at the moment.
in my demo app i created a normal form in a bootstrap style and i was planing to create a directive to show the errors in a bootstrap way. so far so good. that was working and my next step was to create a angular js bootstrap tooltip directive when the form is not valid. the thing is, that i wanna do this dynamic.
i post some code to explain it better:
<b>HTML:</b>
<div class="container" ng-controller="LoginCtrl as vm">
<form id="login" name="vm.loginForm" class="form-signin" ng-submit="vm.login()" novalidate>
<div show-errors>
<input type="email" name="username" ng-model="vm.credentials.username" class="form-control" placeholder="Email address" required autofocus>
</div>
<div show-errors>
<input type="password" name="password" ng-model="vm.credentials.password" class="form-control" placeholder="Password" required>
</div>
<button class="btn btn-lg btn-primary btn-block" id="submit" type="submit">
Login
<span class="spinner"><i class="fa fa-refresh fa-spin"></i></span>
</button>
</form>
</div>
<b>showError Directive:</b>
(function () {
angular.module('testapp.Validate', []).directive('showErrors', validationDirective);
/**
* #name validate directive
*/
function validationDirective($compile) {
return {
require: '^form',
restrict: 'A',
link: function (scope, el, attrs, formCtrl) {
var inputNgEl = angular.element(el[0].querySelector("[name]"));
var inputName = inputNgEl.attr('name');
inputNgEl.bind('blur', function () {
toogle(inputNgEl, el, formCtrl[inputName]);
});
scope.$on('show-errors-check', function () {
toogle(inputNgEl, el, formCtrl[inputName]);
});
}
}
}
function toogle(inputNgEl, fromGroup, inputField) {
fromGroup.toggleClass('has-feedback has-error', inputField.$invalid);
if (inputField.$invalid && !fromGroup[0].querySelector(".glyphicon-remove")) {
inputNgEl.after('<span class="glyphicon form-control-feedback glyphicon-remove"></span>');
} else if (!inputField.$invalid) {
if (fromGroup[0].querySelector(".glyphicon-remove") != null) {
fromGroup[0].querySelector(".glyphicon-remove").remove();
}
}
}
})();
That is working so far . it just adds a has-feedback,has-error class to the parent div and a span with an error-icon after the input.
but back to my plan, now I also want to create a dynamic tooltip for the input. so I planed to add something like that in the "toogle" function
inputNgEl.attr('uib-tooltip',"aaaa");
inputNgEl.attr('tooltip-placement',"right");
inputNgEl.attr('tooltip-trigger',"mouseenter");
inputNgEl.attr('tooltip-class',"testTool");
But that is not working because the input field got already compiled before.
so I asked google about it and there I found some solutions with $compile
$compile(inputNgEl)(scope);
But when I am using it, and I type in a valid email address the field gets reset. aaa#aaa (still working) but after I add the aaa#aaa. (the field gets reseted - I guess compiled again). the tooltip would work btw.
Can anybody help me with that or is there a better solution to create a dynamic angular bootstrap tooltip?
Maybe you need to add a $watch on the input element, and see if it changes, add a tooltip in the input element and compile it
I currently have a form like the following:
<form autocomplete="on" enctype="multipart/form-data" accept-charset="UTF-8" method="POST" action="{{trustSrc(SUBMIT_URL)}}">
<input type="text" name="firstinput" ng-model="first"/>
<input type="text" name="secondinput" ng-model="second"/>
<input type='submit'/>
</form>
And a controller like so:
$scope.first = "first";
$scope.second = "second";
$scope.SUBMIT_URL = DATABASE_URL + "forms/submit/";
$scope.trustSrc = function(src) {
return $sce.trustAsResourceUrl(src);
};
And when I submit this form as is, it works just fine with the backend. However I now need to use ng-submit in place of the standard HTML form submit because I need to run a find and replace in $scope.first. The backend expects the data to be posted exactly in the way the original HTML form does. How do I use $http or $resource to post in the exact same format as the HTML form?
It is not very clear what you are trying to accomplish. But, you should tidy up your code a bit, to make the implementation easier:
Don't use different variables for each input's field. Instead, use an object with different keys.
Use ng-click (or ng-submit) for your submission. Action will go inside your JS logic.
Use the novalidate attribute so that Angular can properly format, and validate your form on its own (and you don't get confusing cross-browser effects).
With those in mind, your new markup would be:
<form autocomplete="on" enctype="multipart/form-data" accept-charset="UTF-8" novalidate>
<input type="text" name="first" ng-model="form.first" />
<input type="text" name="second" ng-model="form.second" />
<button ng-click="submit">Submit</button>
</form>
Your JS directive is then:
app.directive('form', function($http)
{
return function(scope, element, attrs)
{
// Leave this empty if you don't have an initial data set
scope.form = {
first : 'First Thing To Write',
second : 'Second item'
}
// Now submission is called
scope.submit = function()
{
// You have access to the variable scope.form
// This contains ALL of your form and is in the right format
// to be sent to an backend API call; it will be a JSON format
// Use $http or $resource to make your request now:
$http.post('/api/call', scope.form)
.success(function(response)
{
// Submission successful. Post-process the response
})
}
}
});
Angular only updates the model from an input[email] after the user has entered a valid email address. How can I add a {{binding}} somewhere on the page that will update with the email value as the user types -- even before the user has typed in a valid email address?
Here's what I've tried so far:
<div ng-app>
<div ng-controller="MyCtrl">
<form name="MyForm" novalidate>
Name: <input type="text" name="name" ng-model="contact.name" /><br/>
Name as you type: {{contact.name}}<br/>
Email: <input type="email" name="email" ng-model="contact.email" /><br/>
Email as you type: {{contact.email}} (doesn't work)<br/>
Also doesn't work: {{$document.forms.MyForm.elements.email.value}}
</form>
</div>
</div>
Controller:
function MyCtrl($scope) {
$scope.contact = {};
}
(fiddle)
The name updates in real-time like I want, but the email doesn't.
I'd like to leave the email validation enabled. I just need some way to bind the un-validated input[email] text, so it updates as the user types.
Update 2014/7/8
I'd like to add an explicit requirement that the type="email" remains unchanged. I do not want to change the semantics of the markup to workaround a limitation of the framework. If need be, I'd rather pull in a complementary dependency (such as jQuery) to shim in the needed functionality.
I'm not opposed to handling validation in the controller — as suggested by rageandqq and charlietfl — if it could be done easily. Looking around though, it looks like it could be tricky (given my requirements).
That is how angularjs works. If you use <input type="email" /> angular is not going to bind your input till input will be valid in this case value must be a proper e-mail address.
please read more here : https://github.com/angular/angular.js/issues/1426
The workaround I've come up with so far is to use jQuery to listen for the input change and update an object on $scope that I've called formRaw. It works. Still, I'm hoping someone will come along and show me a better way.
The updated example:
<div ng-app>
<div ng-controller="MyCtrl">
<form name="MyForm" novalidate>
Name: <input type="text" name="name" ng-model="contact.name" /><br/>
Name as you type: {{contact.name}}<br/>
Email: <input type="email" name="email" ng-model="contact.email" /><br/>
Email Model: {{contact.email}}<br/>
Email Form: {{formRaw.email}}
{{q}}
</form>
</div>
</div>
And controller:
function MyCtrl($scope) {
$scope.contact = {};
$scope.formRaw = {};
$('input[type=email]').on('keyup change', function () {
var input = $(this);
$scope.formRaw[input.attr('name')] = input.val();
$scope.$digest(); // FIXME: there's got to be a better way
});
}
(fiddle)
The type="email" attribute on your E-mail input is what is causing the DOM binding to mess up.
Changing it to type="text" works allows your {{contact.email}} to display correctly.
Edited JSFiddle.
I have the following static form in AngularJS:
<form name="myForm" class="form-horizontal">
<label>First Name:</label>
<input type="text" name="first_name" ng-model="entity.first_name">
<label>Last Name:</label>
<input type="text" name="last_name" ng-model="entity.last_name">
</form>
Angular creates a FormController for me and publishes it into the scope (under the form name). Which means I have access to properties like the following:
$scope.myForm.first_name.$error
$scope.myForm.last_name.$invalid
...
This is super useful!
But in my case I'm building a form dynamically, using directives:
<form name="myForm" class="form-horizontal">
<field which="first_name"></field>
<field which="last_name"></field>
</form>
The <field> directives don't resolve to actual <input> elements until after a while (after I've fetched some data from the server, linked the directives, etc.).
The problem here is that no field properties are defined on the form controller, as if dynamic fields didn't register with the FormController:
// The following properties are UNDEFINED (but $scope.myForm exists)
$scope.myForm.first_name
$scope.myForm.last_name
Any idea why? Any solution/workaround?
You can see the entire code in this jsFiddle:
http://jsfiddle.net/vincedo/3wcYV/
Update 7/31/2015 This has been fixed since 1.3, see here: https://github.com/angular/angular.js/issues/1404#issuecomment-125805732
Original Answer
This is unfortunately a short coming of AngularJS at the moment. Angular's form validation doesn't work with dynamically named fields. You can add the following at the bottom of your HTML to see exactly what's going on:
<pre>{{myForm|json}}</pre>
As you can see, Angular isn't getting the dynamic input name right. There's currently a work around involving nested forms that can get kind of nasty, but it does work and (with a little extra work) will submit the parent form without trouble.
If you want, you can go drum up more support for the issue: GitHub Issue - dynamic element validation. Either way, here's the code:
http://jsfiddle.net/langdonx/6H8Xx/2/
HTML:
<div data-ng-app>
<div data-ng-controller="MyController">
<form id="my_form" name="my_form" action="/echo/jsonp/" method="get">
<div data-ng-repeat="field in form.data.fields">
<ng-form name="form">
<label for="{{ field.name }}">{{ field.label }}:</label>
<input type="text" id="{{ field.name }}" name="{{field.name}}" data-ng-model="field.data" required>
<div class="validation_error" data-ng-show="form['\{\{field.name\}\}'].$error.required">Can't be empty.</div>
</ng-form>
</div>
<input type="submit" />
</form>
</div>
</div>
JavaScript:
MyController.$inject = ["$scope"];
function MyController($scope) {
$scope.form = {};
$scope.form.data = {};
$scope.form.data.fields = []
var f1 = {
"name": "input_1",
"label": "My Label 1",
"data": ""
};
var f2 = {
"name": "input_2",
"label": "My Label 2",
"data": ""
};
$scope.form.data.fields.push(f1);
$scope.form.data.fields.push(f2);
}
I ran into a similar problem myself and what i did to work around it was to place the name of the field before calling $compile on the template. A simple string.replace did the trick. Then again that was only possible because i was getting the field templates in through http and had access to the template text.
update: here is a fiddle with a little hack to make your example work
app.directive('field', function($compile) {
var linker= function(scope, element){
var template = '<input type="text" name="{{fname}}" ng-model="model">'
.replace('{{fname}}', scope.fname);
element.html(template)
$compile(element.contents())(scope)
}
return {
restrict: 'E',
scope: {
fname: '=',
model: '='
},
replace: true,
link: linker
};
});
http://jsfiddle.net/2Ljgfsg9/4/
If I'm building an index page for a blog in AngularJS with comments like this:
<li ng-repeat="post in posts">
<h2>{{post.title}}
<p>{{post.description}}</p>
<h3>Leave a comment</h3>
<form ng-submit="postComment()">
<input type="hidden" value="{{post.id}}">
Name: <input type="text"> <br>
Comment: <textarea></textarea> <br>
<input type="submit" value="Post comment">
</form>
</li>
I can't figure out what the right way is to associate a model with the form to access from the controller's postComment() function. If you use just one model for all the forms, then starting to leave a comment on one post will start to leave a comment on all of them—unless you do some weird stuff with the hidden post.id field, but that starts to feel unidiomatic.
You can pass an object into the function call.
<form ng-submit="postComment(post.id)">
As far as the other inputs, it really depends on the situation, but in this case, the input and textarea is inside an ngRepeat, which creates new scope. For that reason, assigning to a value on the scope will not affect other elements on the page.
<li ng-repeat="post in posts">
<h2>{{post.title}}
<p>{{post.description}}</p>
<h3>Leave a comment</h3>
<form ng-submit="postComment(post.id, commentName, commentText)">
Name: <input type="text" ng-model="commentName"> <br>
Comment: <textarea ng-model="commentText"></textarea> <br>
<input type="submit" value="Post comment">
</form>
</li>
is there any way of taking advantage of the two way communication there? (e.g. add an errmsg field from an AJAX call done in postComment or clear the form on submit?)
I suggest a directive with its own controller. With this approach, the comment model is encapsulated in the directive. The directive needs two pieces of information: 1) the postID 2) what function to call to save the comment. To support showing an error message returned from the server, a callback function is passed along with the comment. This allows the controller to feed the directive any error messages or any other information it might need after a save operation attempt.
app.directive('commentForm', function() {
var template = '<form name="commentForm" '
+ 'ng-submit="save({comment: comment, callbackFn: callbackFn})">'
+ '<h3>Leave a comment</h3>'
+ 'Name: <input type="text" ng-model="comment.name" name="commentName"> <br>'
+ '<span class="error" ng-show="commentForm.commentName.$error.myError">Error</span><br>'
+ 'Comment: <textarea ng-model="comment.text"></textarea> <br>'
+ '<input type="submit" value="Save comment">'
+ '</form>';
return {
restrict: 'E',
template: template,
scope: { postId: '#', save: '&' },
controller: function($scope) {
$scope.callbackFn = function(args) {
console.log('callback', args);
if(args.error.name) {
$scope.commentForm.commentName.$setValidity("myError", false);
} else {
// clear form
$scope.comment.name = '';
$scope.comment.text = '';
}
};
}
};
});
app.controller('MainCtrl', function($scope, $timeout) {
$scope.post = {id: 1};
$scope.saveComment = function(comment, callbackFn) {
console.log('saving...', comment);
// Call $http here... then call the callback.
// We'll simulate doing something with a timeout.
$timeout(function() {
if(comment.name === "name") {
callbackFn( {error: {name: 'try again'}} );
} else {
callbackFn( {error: {}} );
}
}, 1500)
}
});
Use as follows:
<comment-form post-id="{{post.id}}" save="saveComment(comment, callbackFn)"></comment-form>
Note the somewhat odd syntax related to the '&' syntax: when the directive is used in the HTML, you specify arguments for the saveComment() function. In the directive/template, an object/map is used to associate each argument name with its value. The value is a local/directive scope property.
Plnkr. In the plnkr I simulated an AJAX post using a $timeout of 1.5 seconds. If you enter name in the name textbox and click the save button, you'll get an error in 1.5 seconds. Otherwise the form clears after 1.5 seconds.
Depending on how far you want to take this... you could encapsulate your post template into a directive too:
<li ng-repeat="post in posts">
<post=post></post>
<comment-form ...></comment-form>
</li>
You could also put the comment-form inside the post directive template, simplifying the HTML even further:
<li ng-repeat="post in posts">
<post=post></post>
</li>
You could also have a post-list directive, and its template would contain the ng-repeat, reducing the HTML to a single element:
<post-list posts=posts></post-list>
I personally haven't decided how far one should go with custom directives yet. I would be very interested in any comments people have about this.