AngularJS client-side validation in directive - angularjs

I'm using a directive to encapsulate a partial form. There's an enclosing form which passes the model values into the directive. Here's the basic layout:
<form name="userForm" class="form-horizontal" ng-submit="vm.onSubmitForm(userForm.$valid)" novalidate>
<fieldset>
<legend>Account</legend>
<div class="form-group" control-validator="" validator-condition="vm.hasTriedToSubmit">
<label class="col-md-2 control-label">Email (username):</label>
<div class="col-md-10">
<input class="form-control" type="email"
id="email" name="email"
placeholder="Email"
required
ng-model="vm.formData.email">
<control-validator-message>Email is required.</control-validator-message>
</div>
</div>
<!-- some other fields -->
<div ng-if="vm.isUserChecked()">
<!-- directive which is rendered conditionally -->
<dt-user user="vm.user" display-name-fields="false"></dt-user>
</div>
</fieldset>
So the idea is that if the user directive is rendered, some of its fields will be required. This actually works as it is, but I don't get the validation message displayed, nor do I get the error CSS applied to the required fields. I am stopped from submitting the form if a required directive field isn't present, and the fields in the regular parts of the form show the messages and error CSS, but I'm not having luck with those in the directive. Basically I need a way to signal the enclosing form from the directive to trigger the validation.

I think the issue you have is the not the validation, but when to show the errors from the validation, correct? Here is a small example of how I did this
<div ng-controller="subCtrl">
<form name="groupEdit" ng-submit="groupEditSubmit()">
<input required
name="firstName"
ng-class="{ 'highlight-error' : groupEdit.showError &&
groupEdit.firstName.$invalid }" />
<button ng-click="groupEditSubmit()">group edit submit</button>
</form>
</div>
.controller('subCtrl',function($scope) {
$scope.groupEditSubmit = function() {
$scope.groupEdit.showError = $scope.groupEdit.$invalid;
}
});

The problem was a mistake in scope. The validator-condition "vm.hasTriedToSubmit" was part of the outer controller, not the directive's controller. I modified my scope interface to include this value, added it to the scope initializer in the directive, and passed it in where the directive is used.
The interface:
export interface IUserScope extends ng.IScope {
user: UserViewModel;
hasTriedToSubmit: boolean;
displayNameFields: boolean; }
The directive:
var userDirectiveArray = [
(): ng.IDirective => ({
restrict: "E",
replace: true,
scope: {
user: '=',
hasTriedToSubmit: '=',
displayNameFields: '='
},
templateUrl: "/path/user.directive.tpl.html",
controllerAs: 'vm',
controller: UserDirectiveController
})
];
Using the directive:
<dt-user user="vm.formData" has-tried-to-submit="vm.hasTriedToSubmit" display-name-fields="true"></dt-user>
Some checks happen while a submission is attempted, which is where the "vm.hasTriedToSubmit" value is used. It was being evaluated on the outer controller, but in the directive it simply defaulted to "false", so my error feedback wasn't displayed.

Related

AngularJS form validation: indicate required fields to user

I would like my form labels to display a red asterisk next to the label when the corresponding form control has a required attribute.
Instead of hard coding the asterisk, I desire a way to append the asterisk to the label dynamically during page load if the label's corresponding input, select or textarea is required (the element the label corresponds to).
I created the directive below, and the directive works. But is there a better, more native way to accomplish my goal? This directive finds all the div.form-group containers and adds a red * character after the label if the corresponding form control inside the div.form-group has a required attribute.
myApp.directive('labelsRequired',function(){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attrs){
elem.find('div.form-group').each(function(i, formGroup){
var formControls = $(formGroup).find('input, select, textarea');
console.log(formControls)
if (0 !== formControls.length && undefined !== $(formControls[0]).attr('required')){
jLabel = $(formGroup).find('label');
jLabel.html(jLabel.html()+ "<span class='red-color'>*</span>");
}
})
}
}
});
The directive assumes all inputs, selects, and textareas are inside a div.form-group container.
<div class='form-group'>
<label>First Name</label><!-- this label gets an asterisk -->
<input name='fname' required />
</div>
<div class='form-group'>
<label>Favorite Food</label><!-- this label does not get an asterisk -->
<input name='favFood' />
</div>
You don't need a directive, there are built-in form properties you can use with filters like ng-show, look:
<div ng-app="myApp" ng-controller="myCtrl">
<form name="userForm" novalidate>
<div class='form-group'>
<label>First Name</label>
<input name='fname' ng-model="fname" required />
<label ng-show="userForm.fname.$dirty && userForm.fname.$error.required">* Required field</label>
</div>
<button type="submit">Submit</button>
</form>
</div>
If you define an ng-model for the input you can deal with it looking if it is filled or not. You can also check it only after the user "dirty" it with userForm.fname.$dirty, so the label will be shown only after a user try to input something but then clear it. Try playing with it here JSFiddle
Building off of Corey's answer:
I just used compile rather than link, as I saw that my required attribute was not being applied to my input elements. I also included a select tag for any dropdowns that I had.
app.directive('inputRequired', function () {
return {
restrict: 'A',
compile: function (elem) {
elem.find('label').append("<sup><i class='fa fa-asterisk'></i></sup>");
elem.find('input').attr('required', 'required');
elem.find('select').attr('required', 'required');
}
};
});
If you're not using the built-in Angular validation, you could restructure your directive and attach it to your .form-group element. Like this:
app.directive('inputRequired', function() {
return {
restrict: 'A',
link: function(scope, elem, attr) {
elem.find('label').append('<span>*</span>');
elem.find('input').attr('required', 'required');
}
};
});
Your HTML would then look like:
<div class="form-group" input-required>
<label>Name</label>
<input name="name" />
</div>
<div class="form-group">
<label>Food</label>
<input name="food" />
</div>
However, if you haven't looked into the built-in validation with Angular, I would recommend using it.
This might come too late and it might not be too elegant but it works, if anyone needs it:
<label ng-show="userForm.fname.$validators.hasOwnProperty('required')">* Required field</label>

Passing value to ng-model in custom directive

Right, I'm probably missing something fairly obvious here but here goes. I have a bit of HTML from bootstrap that I want to reuse so I wanted to make it into a custom directive.
<label class="toggle">
<input ng-model='model' type="checkbox" class="toggleInput">
<div class="track">
<div ng-show="test" class="toggle-label on">
{{onText}}
</div>
<div ng-show="!test" class="toggle-label off">
{{offText}}
</div>
<div class="handle"></div>
</div></label>
snippet of html I want to use in my pages that Angular will recompile into the above:
<toggle on-text="On" off-text="Off" ng-model="myModelName"></toggle>
My directive is as follows:
.directive('toggle', function() {
return {
restrict: 'AE',
templateUrl: 'views/toggle.view.html',
replace: true,
scope: {
onText: '#',
offText: '#',
ngModel : '=',
},
};
});
However, when looking at the html markup the ng-model attribute has not changed to 'myModelName' and still shows just 'modal' so hasn't updated.
What am I doing wrong?
Thanks all
Try replacing
<input ng-model='model' type="checkbox" class="toggleInput">
with
<input ng-model='{{ngModel}}' type="checkbox" class="toggleInput">
because you want to bind the value of the incoming model name into the ng-model attribute of the template inside the directive.
Figured it out.... used compile in the link function to dynamically add the model to the elements I needed:
return $compile($('input.toggleInput',element).attr('ng-model', model))(scope);

Angular directive with custom / conditional actions

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

Angular directive attributes aren't being passed through

I'm trying to shorten my form code by making directives for each element, however my directive is displaying none of what I'm passing to it and the model isn't being bound.
HTML:
<formstring dataBinding="project.title" dataTitle="Title" dataPlaceholder="title" />
directive:
app.directive('formstring', function () {
return {
restrict: 'AEC',
dataBinding: '=',
dataTitle: '#dataTitle',
dataPlaceholder: '#dataPlaceholder',
dataHelp: '#dataHelp',
templateUrl: '/app/js/directives/form/string.html',
};
});
string.html:
<div class="form-group"> 2 <label for="{{dataTitle}}" class="col-sm-2 control-label">{{dataTitle}}</label >
<div class="col-sm-10">
<input type="text" class="form-control" id="{{dataTitle}}" placeholder="{{da taPlaceholder}}" ng-model="dataBinding">
<p ng-show="dataHelp" class="help-block">{{dataHelp}}</p>
</div>
</div>
project is a $scope object that has an attribute 'title'.
What am I missing? Why does this show up at a blank input with none of the attributes filled in and why does the binding not work?
You haven't understood how directives are configured correctly. I suggest you read the documentation, it may help you understand better.
In the meantime, here is what your HTML, directive code and template should look like (there is also a working demonstration on Plunkr):
HTML:
<formstring data-binding="project.title"
data-title="Title Demo"
data-placeholder="title placeholder"
data-help="My help text">
</formstring>
directive:
app.directive('formstring', function () {
return {
restrict: 'E',
scope: {
binding: '=',
title: '#',
placeholder: '#',
help: '#'
},
templateUrl: '/app/js/directives/form/string.html',
};
});
Template (string.html):
<div class="form-group">
<label for="{{title}}" class="col-sm-2 control-label">
{{title}}
</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="{{title}}" placeholder="{{placeholder}}" ng-model="binding">
<p ng-show="help" class="help-block">{{help}}</p>
</div>
</div>
You need to change how you're creating isolate scope:
app.directive('formstring', function () {
return {
restrict: 'AEC',
scope: {
dataBinding: '=',
dataTitle: '#dataTitle',
dataPlaceholder: '#dataPlaceholder',
dataHelp: '#dataHelp'
},
templateUrl: '/app/js/directives/form/string.html',
};
});
Read the doc for details about what an isolate/isolated scope means because it has an effect on the overall scope.
edit:
I didn't notice this additional issue before. Your camel case scope properties become snake cased when you use your directive (see Mobin's answer):
<formstring data-binding="project.title" data-title="Title" data-placeholder="title" />
In your template, however, the properties are still camel cased as you have:
<div class="form-group"> 2 <label for="{{dataTitle}}" class="col-sm-2 control-label">{{dataTitle}}</label >
<div class="col-sm-10">
<input type="text" class="form-control" id="{{dataTitle}}" placeholder="{{da taPlaceholder}}" ng-model="dataBinding">
<p ng-show="dataHelp" class="help-block">{{dataHelp}}</p>
</div>
</div>
This is because the bindings in your template are JSON properties, whereas the attributes when you use your directive's properties are XML properties.
There are minor tweaks I'd apply to your template, for instance id="{{dataTitle}}" can easily break HTML standards that require the id attribute is unique... you probably want to use name="{{dataTitle}}" instead. name could still cause issues, but it wont' break document.getElementById for example.
Also, I'd use ng-bind whenever possible:
<p ng-show="dataHelp" class="help-block" ng-bind="dataHelp"></p>
It should be
<formstring data-binding="project.title" data-title="Title" data-placeholder="title" />
.

AngularJS: Fields added dynamically are not registered on FormController

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/

Resources