Validating custom directive inside a form - angularjs

I've a form which contains a custom directive that I want to validate. I could do that if the scope is not an isolate scope, but how to proceed if we have an isolate scope case? Here is an example where the form contains 2 fields: first and last name. First name is directly on the form but the last name is within a directive. When i hit 'Save' only first name gets validated. Can somebody point out what am I missing? Code goes somewhat like this:
Main form:
<form class=" form form-group" name="personForm" ng-submit="saveInfo(personForm.$valid, '#/success')" novalidate>
<label for="fname"><strong>First Name:</strong></label>
<input type="text"
name="fname"
class="form-control"
ng-model="person.firstname"
ng-maxlength=10
ng-minlength=3
ng-required="true">
<div class="error-message" ng-messages="personForm.fname.$error" data-ng-if="interacted(personForm.fname)">
<div ng-message="required">This is a required field</div>
<div ng-message="maxlength">max length should be 10</div>
<div ng-message="minlength">min length should be 3</div>
</div>
<last-name ng-model="person"></last-name>
<br/>
<button class="btn btn-primary" type="submit">Save</button>
and the directive (last name):
<label for="lname"><strong>Last Name:</strong></label>
<input type="text"
name="lname"
class="form-control"
ng-model="person.lastname"
ng-maxlength=20
ng-minlength=5
ng-required="required">
<div ng-messages="personForm.lname.$error" data-ng-if="interacted(personForm.lname)">
<div ng-message="required">This is a required field</div>
<div ng-message="maxlength">max length of last name should be 20</div>
<div ng-message="minlength">min length of last name should be 5</div>
</div>
... and finally here is my javascript:
var app = angular.module('MyApp',['ngRoute','ngMessages']);
app.config(function ($routeProvider) {
$routeProvider
.when('/success', {
templateUrl: 'success.html'
})
.when('/', {
templateUrl: 'first-name.html',
controller: 'MyController'
})
})
app.directive('lastName', function() {
return {
restrict: 'E',
scope: {
person: '=ngModel',
},
require:'ngModel',
templateUrl: 'last-name.html'
}
});
app.controller('MyController', function($scope, $location) {
$scope.person = {};
$scope.submitted = false;
$scope.interacted = function(field) {
return $scope.submitted || field.$dirty;
};
$scope.saveInfo = function(isValid, url) {
$scope.submitted = true;
if (isValid) {
$location.path(url);
} else {
alert('Missing values in mandatory fields');
}
}
});

I kinda solved my problem.This is a "poor man's solution" per se so i implore the experts reading this post to suggest me a better way.
My issue was "how will the directive know, if it's parent form has been submitted" (without sharing the scope). My answer was to send another attribute into the directive that indicated the form submission (scope: { formSubmitted: '='}).
app.directive('lastName', function() {
return {
restrict: 'E',
scope: {
formSubmitted: '='
},
require:'ngModel',
templateUrl: './last-name.html',
link: function(scope, element, attrs, controllers) {
scope.$watch('lastname', function() {
controllers.$setViewValue(scope.lastname);
});
}
};
});
So the directive looks a bit silly :( but works.
<last-name ng-model="person.lastname" form-submitted="submitted"></last-name>
... and the "submitted" was something that was used anyways to detect if submit button was pushed.
So with that and some help of other posts on stack overflow (ng-form: to enclose the elements in the directive), I was able to get it working. Here is the plunker for the same.

Related

Getting controls of forms inside a directive

I'm working on an AngularJS app and created two forms inside a directive. I want to reset both form's $submitted by $setSubmitted(false), however, the problem is that I cannot get two controls at the same time inside the directive controller.
Referred to this solution How to handle multiple forms present in a single page using AngularJS. However, the solution is for a controller, not for a directive.
(function () {
'use strict';
angular
.module('app')
.directive('multiForms', multiForms);
function multiForms() {
return {
restrict: "A",
scope: {
},
controller: function ($scope) {
$scope.functions = {
submit1: submit1,
submit2: submit2,
resetForms: resetForms
};
function resetForms() {
$scope.form1.$setSubmitted(false);
$scope.form2.$setSubmitted(false);
}
},
replace: false,
templateUrl: 'pathToTheHtml.html'
}
}
})();
<div>
<form name="form1" ng-submit="functions.submit1()">
<ng-form name="form1">
<input type="text" ng-model="text1">
<button type="submit">Submit1</button>
</ng-form>
</form>
<form name="form-2" ng-submit="functions.submit2()">
<ng-form name="form2">
<input type="text" ng-model="text2">
<button type="submit">Submit2</button>
</ng-form>
</form>
<button ng-click="functions.resetForms()"></button>
</div>
The expected results is to set $submitted value to false for both form1 and form2. The actual result is both $scope.form1 and $scope.form2 are undefined.
I was playing around and this actually worked. I added vm and binded with the view.
(function () {
'use strict';
angular
.module('app')
.directive('multiForms', multiForms);
function multiForms() {
return {
restrict: "A",
scope: {
},
controller: function ($scope) {
const vm = this;
vm.functions = {
submit1: submit1,
submit2: submit2,
resetForms: resetForms
};
function resetForms() {
vm.form1.$setPristine();
vm.form2.$setPristine();
}
},
controllerAs: 'vm',
bindToController: true,
replace: false,
templateUrl: 'pathToTheHtml.html'
}
}
})();
<div>
<form name="vm.form1" ng-submit="vm.functions.submit1()">
<ng-form name="form1">
<input type="text" ng-model="vm.data.text1">
<button type="submit">Submit1</button>
</ng-form>
</form>
<form name="vm.form-2" ng-submit="vm.functions.submit2()">
<ng-form name="form2">
<input type="text" ng-model="vm.data.text2">
<button type="submit">Submit2</button>
</ng-form>
</form>
<button ng-click="vm.functions.resetForms()"></button>
</div>

Scope not updating in link directive on ng-submit

I can't get the scope object to update on submit. The submit function is called, but the console shows the object does not update. I have tried most similar problem-solutions I have found.
The directive (inside index.html):
<div d-header-login class="header-login"></div>
The directive HTML:
<div class="container_header-login">
<div data-ng-if="!login.opLogin">
<form ng-submit="submit()">
<input type="text" ng-model="username" placeholder="username/email" />
<input type="text" ng-model="scope.password" placeholder="password" />
<br />
<input type="submit" class="btn btn-primary" value="Login"/>
</form>
Register
Forgot password
</div>
<div data-ng-if="login.opLogin">
<!--{{login.opLogin}}-->
Logged in as
</div>
</div>
The directive js
var mHeaderLogin = angular.module('app.directives.mHeaderLogin', ['mMain'])// mMain-dependent due to factory call
.directive('dHeaderLogin', fHeaderLogin);
function fHeaderLogin() {
return {
restrict: 'A',
//replace: true,
scope: {
username: '='
}, //isolate scope
templateUrl: 'app/directives/header-Login/header-Login.html',
controller: function ($scope, simpleFactory) {
$scope.users = simpleFactory.getUsers();
$scope.username = "test";
},
link: function (scope, element, attrs) {
scope.login = { opLogin: false };
scope.submit = function () {
console.log(scope.username);
console.log("fSubmit");
}
}
}
}
The log will show "test"; not what I wrote in the input box

Angular invoke controller fn from directive isolated template

I want on-click event from directive invoke some function from my controller. But for some reason it doesn't work. I want my datepicker to expand when I event is fired. Could you please help me to investigate what is wrong my in my current build?
app.js
app.directive('myDatepicker', function() {
return {
restrict: 'E',
scope :{
model:'=model',
minDate:'=minDate',
isOpened:'=isOpened',
openFunction: '&'
},
templateUrl: 'templates/datepicker/datepicker.html',
link: function(scope, elm, attrs) {
}
};
});
app.controller('FlightDatePickerController', function ($scope) {
$scope.openFunction = function($event, isDepart) {
$event.preventDefault();
$event.stopPropagation();
$scope.departureOpened = true;
};
};
datepicker.html
<fieldset>
<pre>{{model}}</pre>
<div class='input-group'>
<input type="text" class="form-control" datepicker-popup ng-model="{{model}}" min-date="{{minDate}}" is-open="{{isOpened}}" datepicker-options="dateOptions" ng-required="true" close-text="Close" />
<span ng-click='openFunction({event:event}, {isDepart:isDepart})' class="btn btn-default input-group-addon">
<span class="glyphicon glyphicon-calendar"></span>
</span>
</div>
</fieldset>
index.html
<div ng-controller="FlightDatePickerController">
<div class="col-md-2">
<my-datepicker model="departureDate" minDate="minDateDeparture" isOpened="departureOpened" open-function="openFunction($event, isDepart)"></my-datepicker>
</div>
</div>
You can add a controller attribute to your directive, in order to bind some function to your template.
In your case, you can do :
Directive
app.directive('myDatepicker', function() {
return {
restrict: 'E',
scope :{
model:'=model',
minDate:'=minDate',
isOpened:'=isOpened'
},
templateUrl: 'templates/datepicker/datepicker.html',
controller: 'FlightDatePickerController'
};
});
Datepicker.html
<div ng-controller="FlightDatePickerController">
<div class="col-md-2">
<my-datepicker model="departureDate" minDate="minDateDeparture" isOpened="departureOpened"></my-datepicker>
</div>
</div>
Your overall implementation is correct, but you made couple of mistakes.
ng-click should be like adding parameter in JSON like structure.
ng-click='openFunction({event:$event, isDepart:isDepart})'
& then your directive element should have
open-function="openFunction($event, isDepart)"

Get access to form controller validation errors in a custom directive

I have a directive that wraps a form element with some inputs. One of the options is passing in a formName. Usually, with a form with the example name of myForm, to show an error you would do something like myForm.firstName.$error.required.
But, how do I get access to the errors when the form name is dynamically being passed in to the directive?
example usage
<my-custom-form formName='myForm' formSubmit='parentCtrl.foo()'></my-custom-form>
directive
angular.module('example')
.directive('myCustomForm', [
function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'myCustomForm.directive.html',
scope: {
fornName: '#',
formSubmit: '&'
},
require: ['myCustomForm', 'form'],
link: function(scope, element, attrs, ctrls) {
var directiveCtrl = ctrls[0];
var formCtrl = ctrls[1];
scope.data = {};
scope.hasError = function(field) {
// how do i show the errors here?
};
scope.onSubmit = function() {
scope.formSubmit();
};
}
};
}]);
template
<form name="{{ formName }}" ng-submit="onSubmit()" novalidate>
<div class="form-group" ng-class="{'is-invalid': hasError('fullName') }">
<input type="text" name="fullName" ng-model="data.full_name" required />
<div ng-show="hasError('fullName')">
<p>How do I show this error?</p>
</div>
</div>
<div class="form-group" ng-class="{'is-invalid': hasError('email') }">
<input type="text" name="email" ng-model="data.email" ng-minlength="4" required />
<div ng-show="hasError('email')">
<p>How do I show this error?</p>
</div>
</div>
<button type="submit">Submit</button>
</form>
I think the only problem with your code is that the directive requires itself, I don't think that will work. Just removing the myCustomForm from the require works fine.
To check if the field has errors, you just need to check if the $error object in the form controller is empty.
require: ['form'],
link: function(scope, element, attrs, ctrls) {
var formCtrl = ctrls[0];
scope.data = {};
scope.hasError = function(field) {
// Field has errors if $error is not an empty object
return !angular.equals({}, formCtrl[field].$error);
};
Plunker

How an autocomplete/lookahead directive can be used in a decoupled way for multiple separate inputs using separate services?

Currently I have three inputs that I need to use autocomplete on that look something like that:
<div class="actor-container">
<input type="text" class="actors"/>
</div>
<div class="movie-container">
<input type="text" class="movies"/>
</div>
<div class="director-container">
<input type="text" class="directors"/>
</div>
I also have my own autocomplete directive
<div my-autocomplete="my-autocomplete" service="serviceName"></div>
The autocomplete is getting a serviceName as an input to access different data repositories (search in different pools) using an $injector
What is the best AngularJS practice to connect this directive with the other three inputs?
I was thinking of putting it in each input like that:
<div class="actor-container">
<input type="text" class="actors" my-autocomplete="my-autocomplete" service="actorService"/>
</div>
<div class="movie-container">
<input type="text" class="movies" my-autocomplete="my-autocomplete" service="movieService"/>
</div>
<div class="director-container">
<input type="text" class="directors" my-autocomplete="my-autocomplete" service="directorService"/>
</div>
But is this good practice? Or the directive should be placed "outside" once and then use shared service/broadcast/watch etc to communicate with the each of the three inputs using three separate controllers?
<div class="actor-container" ng-controller="addActorCtrl">
<input type="text" class="actors"/>
</div>
<div class="movie-container" ng-controller="addMovieCtrl">
<input type="text" class="movies"/>
</div>
<div class="director-container" ng-controller="addDirectorCtrl">
<input type="text" class="directors"/>
</div>
<div my-autocomplete="my-autocomplete" service="serviceName(how to pass that??)"></div>
What should happen is someone should type in each of those fields and the appropriate autocomplete based on a serviceName and what the user input should pop up. Then the user will click on one of the returned entries and this should be added to the right container. I also wonder where the code object.movies.push() (e.g for the movies - when the user clicks a suggested movie) will be placed..
I would really appreciate if you could provide an example with some code because I am fairly new in AngularJS and I think this would be useful for others too :)
Thanks
This is what I did and I think it is a good practice:
I have a controller for each of the inputs and I add the directives in each of those:
<div ng-controller="addActorCtrl">
<div suggest-dir="suggest-dir" service="actorService" input-string="{{input1}}" select="addActor(suggestEntry)"></div>
<input id="actors" name="actors" type="text" placeholder="" class="input-medium" ng-model="input1"/>
</div>
<div ng-controller="addMovieCtrl">
<div suggest-dir="suggest-dir" service="movieService" input-string="{{input2}}" select="addMovie(suggestEntry)"></div>
<input id="actors" name="actors" type="text" placeholder="" class="input-medium" ng-model="input2"/>
</div>
<div ng-controller="addDirectorCtrl">
<div suggest-dir="suggest-dir" service="directorService" input-string="{{input3}}" select="addDirector(suggestEntry)"></div>
<input id="actors" name="actors" type="text" placeholder="" class="input-medium" ng-model="inpu23"/>
</div>
This is my autocomplete directive
angular.
module('myApp.suggestModule').
directive('suggestDir',function(){
// Runs during compile
return {
scope: {
inputString: '#',
service: '#',
select: '&'
},
controller: [
'$scope',
'$injector',
function($scope, $injector) {
this.inputChanged = function(inputString){
$injector.get($scope.service).suggest(inputString,
function(data){
$scope.suggestEntries = data;
}
);
};
this.entryWasClicked = function(suggestEntry){
$scope.select({suggestEntry:suggestEntry})
}
}],
restrict: 'A',
templateUrl: 'suggestMenu/views/suggest-menu.html',
link: function(scope, element, attrs, controller) {
scope.$watch('inputString', function(newValue, oldValue) {
controller.inputChanged(newValue);
});
}
};
})
This is my autocomplete entry directive which when it is clicked is calling the entryWasClicked from its parent suggestDir. It's parent calls $scope.select but is a & parameter so it is the function that is being passed in <div suggest-dir select="functionToBeCalled/>
directive('suggestEntryDir',function(){
// Runs during compile
return {
require: '^suggestDir',
restrict: 'A',
templateUrl: 'suggestMenu/views/suggest-entry.html',
link: function(scope, element, attrs, controller) {
var suggestEntry = scope.$eval(attrs.suggestEntry);
element.bind('click', function(e){
scope.$apply(function(){ controller.entryWasClicked(suggestEntry)})
});
}
};
});
This is one of the wrapper controllers (that I used for each of the inputs) so everything is decoupled:
controller('addActorCtrl',[
'$scope',
function($scope){
var obj= $scope.obj;
$scope.addActor= function (actor){
if(!obj.actors){
obj.actors = [];
}
obj.actors.push(actors);
};
}]);
This is one of the services (the actorService). Services bring data from the database and behave as repositories:
angular.
module('myApp.actorModule').
constant('restAPI','http://localhost/project/').
factory('actorService',['$http','restAPI',function($http,restAPI){
return {
latest: function (callback){
$http({
method: 'GET',
url: '',
cache: true
}).success(callback);
},
find: function(id, callback){
$http({
method: 'GET',
url: restAPI+'actor/'+id,
cache: true
}).success(callback);
},
suggest: function(stringInput,callback){
$http({
method: 'GET',
url: restAPI+'actor/suggest/'+stringInput,
cache: true
}).success(callback);
},
update: function(id,actor, callback){
$http({
method: 'PUT',
url: restAPI+'actor/'+id,
data: actor
}).success(callback);
}
}
}])
Finally this is the template for my suggestion directive:
<div id="suggestions" ng-show="suggestEntries.length>0 && inputString.length>0">
<div class="scrollbar"><div class="track"><div class="thumb"><div class="end"></div></div></div></div>
<div class="viewport">
<div id="suggestions-list" class="overview">
<div class="types" ng-repeat="suggestEntry in suggestEntries">
<div suggest-entry-dir="suggest-entry-dir" suggest-entry="suggestEntry"></div>
</div>
</div>
</div>
<div class="close" style="width:10px; cursor: pointer; height:10px; background-color:#aaaaff; position:absolute; top: 4px; right: 4px;"></div>
</div>
and this is for the suggestEntry
<div class="suggest-entry suggest-{{suggestEntry.label}}">
{{suggestEntry.textIndexed}}
</div>

Resources