My webapp is using Angular 1.4.8. I have a directive that validates a form input using $validators. Only input that starts with number 5, 6 and 9 and contains 8 numbers are valid.
angular
.module('directive.customNumber', [])
.directive('customNumber', customNumber);
function customNumber() {
var REGEXP = /^([569][0-9]{7})/;
return {
require: ['ngModel', '^form'],
link: function(scope, elm, attrs, ctrl) {
ctrl.$validators.customNumber = function(modelValue, viewValue) {
if(ctrl.$isEmpty(modelValue)) {
// consider empty models to be valid
return true;
}
return REGEXP.test(viewValue);
};
}
};
}
Usage:
<form name="form">
<input type="text" name="myInput" custom-number>
</form>
Now I want to write a unit test for this directive using Jasmine. This is my test case:
describe('Directive', function() {
var $scope;
beforeEach(function() {
module('directive.customNumber');
inject(function($rootScope, $compile) {
$scope = $rootScope;
var template = '<form name="form"><input type="text" name="myInput" custom-number></form>';
$compile(template)($scope);
$scope.digest();
});
});
it('should not accept invalid input', function() {
var form = $scope.form;
form.myInput.$setViewValue('abc');
expect(form.$valid).toBeFalsy();
expect(form.myInput.$error.mobile).toBeDefined();
});
});
Running this throws an error "TypeError: Cannot set property 'customNumber' of undefined at this line:
ctrl.$validators.customNumber = function(....
I am not sure why $validators become undefined in the test but works fine in normal environment. Even if I get rid of this error by manually creating $validators object before use, the test fails because the customNumber validator is never being run (know by logging), so form.$valid is always true.
How can I properly test this directive?
On your directive ctrl is an array whare ctrl[0] is the ngModel and ctrl[1] is the formController
Related
I have a directive defined as:
module.directive('inputChanged', function () {
function link(scope, element, attrs) {
var field = attrs.ngModel;
if (field) {
var fn = "model.changed('" + field + "')";
element.attr('ng-change', fn);
}
}
return {
restrict: 'A',
link: link
}
});
which I am using like:
<input ng-model="model.user.middleName" input-changed type="text" class="k-textbox">
The goal is to dynamically inject the ng-change with the model field as parameter. My scenario is actually a bit more complex, but I am simplifying it for this question. This is why I need to inject it dynamically and not place it directly in the input markup.
This works and I can see the ng-change in the markup once the page is rendered.
<input ng-model="model.user.middleName.value" type="text" class="k-textbox ng-valid ng-not-empty ng-dirty ng-valid-parse ng-touched" ng-change="model.changed('model.user.middleName.value')" aria-invalid="false">
The problem is that model.changed(...) is not firing. If I hardcode it instead of using the directive, everything works as expected.
What am I missing?
Thank you.
You need to compile the element after adding the ng-change directive.
angular
.module('app')
.directive('inputChanged', inputChanged);
function inputChanged($compile) {
var directive = {
restrict: 'A',
link: link,
terminal: true,
priority: 1000,
};
return directive;
function link(scope, element, attrs) {
var field = attrs.ngModel;
if (field) {
var fn = "main.changed(" + field + ")";
// Remove the attribute to prevent
// an infinite compile loop
element.removeAttr('input-changed');
element.attr('ng-change', fn);
$compile(element)(scope);
}
}
};
Working plunker.
More information about adding directives from a directive in this post.
I have the following (https://jsfiddle.net/f30bj43t/5/) HTML:
<div ng-controller="DataController as vm">
<div ng-repeat="name in vm.users track by $index">{{name}}</div>
<form name="form" validation="vm.errors">
<input validator ng-model="vm.name" name="vm.name" placeholder="name" type="text" />
Add
</form>
</div>
This form adds names to a list and the controller is:
app.controller("DataController", DataController);
function DataController() {
var vm = this;
vm.name = "Mary";
vm.users = ["Alice", "Peter"];
vm.errors = [];
vm.add = function(name) {
if (name == "Mary") {
var error = { property: "name", message: "name cannot be Mary"};
if (vm.errors.length == 0)
vm.errors.push(error);
} else {
vm.users.push(name);
vm.errors = [];
}
}
}
On the form I added validation="vm.errors" that defines which variable holds the errors to be used in each validator directive ...
Then in each validator directive I will use that variable to pick the correct error and display it ...
app.directive("validation", validation);
function validation() {
var validation = {
controller: ["$scope", controller],
replace: false,
restrict: "A",
scope: {
validation: "="
}
};
return validation;
function controller($scope) {
this.getErrors = function () {
return $scope.validation;
}
}
}
app.directive("validator", validator);
function validator() {
var validator = {
link: link,
replace: false,
require: "^validation",
restrict: "A"
};
return validator;
function link(scope, element, attributes, controller) {
var errors = controller.getErrors();
console.log(errors);
// do something with errors
}
}
PROBLEM
In link function of validator directive I need to track changes of the variable passed by validation="vm.errors" so I can in each validator check if a error occurs and take action.
But console.log(errors) seems to have no effect ...
How can I solve this?
You can do it with either $watch or $broadcast and $on.
I preffer to use the $on, because watching a variable consumes more than listening to an event and I'd say that to your case it's better to use $broadcast and $on, because you're not watching a scope variable, but a variable from a controller.
If you want to learn more about $on and $watch, here's a suggestion: Angular JS $watch vs $on
And here's your JSFiddle with the modifications.
Here you have a working jsfiddle: https://jsfiddle.net/f30bj43t/8/
I am logging the vm.errors variable into the console each time it changes:
function link(scope, element, attributes, controller) {
scope.$watch('vm.errors.length', function () {
if(scope.vm.errors){
console.log(scope.vm.errors);
}
});
}
This is possible due to the scope inheritance. On your case, vm is a variable added to the controller's scope, and all directives inside this scope will inherit it by default (there are ways to avoid that).
Anyhow it seems you are creating too much directives. On your case, for me would be enough having everything in the controller.
Cheers
Javier
trying to test my directive with jasmine but is not failing where it should because of the wrong date(.demo):
describe("Unit: Testing Directives - ", function() {
var $compile, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe("Date Validation Directive - ", function(){
it('should show an date as valid', function(){
$rootScope.demo = '10/01/881';
var templateHTML = angular.element('<input class="blah" type="tel" ng-model="demo" my-date />');
var element = $compile(templateHTML)($rootScope);
$rootScope.$digest();
expect(element.hasClass('ng-valid')).toBe(true);
expect(element.hasClass('ng-invalid')).toBe(false);
});
});
});
this is what my directive looks like:
var app = angular.module('app', []);
app.directive("myDate", function () {
return {
restrict: "A", //only activate on element attribute
require: "ngModel", //get hold of NgModelController
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
var date_regexp = /^(0?[1-9]|[12][0-9]|3[01])[\/\-](0?[1-9]|1[012])[\/\-]\d{4}$/;
if (date_regexp.test(viewValue)) {
// it is valid
ctrl.$setValidity("myDate", true);
return viewValue;
} else {
// it is invalid, return undefined (no model update)
ctrl.$setValidity("myDate", false);
return undefined;
}
});
}
};
});
how can I get it working?
plunkr:http://plnkr.co/edit/GYBvynRqdTTqXxnk7thN?p=preview
This is because of the programmatic assignment of the model value. $parsers run when he value is modified through DOM. When you change the model value programatically it is $formatters that run. So in your directive if you change $parsers to $formatters it will fail the test as you expected.
Plnkr
However you may need both of them in your directive and your real testing should be to test the logic inside the parsers/formatters, not how it is run (It has already been tested by angular) or how the classes are added by angular. If your validations are exposed through say a validator service, or even your directive's controller which provides the validation that would be a good target to test.
I have a directive as below which i want to cover as part of my jasmine unit test but not sure how to get the template value and the values inside the link in my test case. This is the first time i am trying to unit test a directive.
angular.module('newFrame', ['ngResource'])
.directive('newFrame', [
function () {
function onAdd() {
$log.info('Clicked onAdd()');
}
return {
restrict: 'E',
replace: 'true',
transclude: true,
scope: {
filter: '=',
expand: '='
},
template:
'<div class="voice ">' +
'<section class="module">' +
'<h3>All Frames (00:11) - Summary View</h3>' +
'<button class="btn" ng-disabled="isDisabled" ng-hide="isReadOnly" ng-click="onAdd()">Add a frame</button>' +
'</section>' +
'</div>',
link: function (scope) {
scope.isDisabled = false;
scope.isReadOnly = false;
scope.onAdd = onAdd();
}
};
}
]);
Here is an example with explanation:
describe('newFrame', function() {
var $compile,
$rootScope,
$scope,
$log,
getElement;
beforeEach(function() {
// Load module and wire up $log correctly
module('newFrame', function($provide) {
$provide.value('$log', console);
});
// Retrieve needed services
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$log = _$log_;
});
// Function to retrieve a compiled element linked to passed scope
getCompiledElement = function(scope) {
var element = $compile('<new-frame></new-frame>')(scope);
$rootScope.$digest();
return element;
}
// Set up spies
spyOn($log, 'info').and.callThrough();
});
it('test', function() {
// Prepare scope for the specific test
$scope.filter = 'Filter';
$scope.expand = false;
// This will be the compiled element wrapped in jqLite
// To get reference to the DOM element do: element[0]
var element = getCompiledElement($scope);
// Get a reference to the button element wrapped in jqLite
var button = element.find('button');
// Verify button is not hidden by ng-hide
expect(button.hasClass('ng-hide')).toBe(false);
// Verify button is not disabled
expect(button.attr('disabled')).toBeFalsy();
// Click the button and verify that it generated a call to $log.info
button.triggerHandler('click');
expect($log.info).toHaveBeenCalled();
});
});
Demo: http://plnkr.co/edit/tOJ0puOd6awgVvRLmfAD?p=preview
Note that I changed the code for the directive:
Injected the $log service
Changed scope.onAdd = onAdd(); to scope.onAdd = onAdd;
After reading the angular documentation for directives,i was able to solve this. Since the restrict is marked as E, the directive can only be injected through a element name. Earlier i was trying through div like below.
angular.element('<div new-frame></div>')
This will work if restrict is marked as A (attributes). Now i changed my injection in he spec file to match the directive with element name.
angular.element('<new-frame></new-frame>')
Now i was able to get the template and scope attributes in my spec. Just to be sure to accept everything, the combination of A (aatributes), E (elements) and C (class name) can be used in the restrict or any 2 as needed.
I have a simple form with a directive:
<form name="testform">
<input name="testfield" type="text" ng-model="test.name" ng-minlength="3" my-validate>
</form>
Since Angular validates the field itself when entering less than 3 characters, it sets testform.testfield.$invalid.
What I want to achieve is to add some custom validation functionality in a directive and react to this angular validation/set the validation result e.g. with the help of watchers or events like so:
angular.module('myApp.directives').directive('myValidate', [function() {
return {
link: function(scope, elem, attrs) {
elem.on('keyup', function() {
// do some custom validation
// if valid, set elem to $valid
// else set $error
// something like elem.set('$valid');
}
}
};
}]);
How can I achieve the combination of HTML5, Angular and custom form validation while using Angulars testform.testfield.$invalid, testform.testfield.$valid, testform.testfield.$error etc. logic?
By default Angular provides some directives for input validation (like required, pattern, min, max etc.). If these directives doesn't suffice, custom validation is available. To be able to do that, the directive requires the model used for validation, then it can set the validity of the element like below:
.directive('myValidate', [ function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$setValidity('myValidate', false);
}
};
}]);
A Plunker with such a directive which will become invalid when you enter '0' is HERE.
You can read more about custom validation on Angular forms documentation.
You can do custom validation and combine it with Angular's built in validation, you just need to require ngModel, pass ctrl as the 4th value to your link function, and use ctrl.$setValidity to indicate if your custom validation passes or fails.
The example below will be invalid if the length < 3 OR if the value is the string 'invalid':
var app = angular.module('app', []);
app.controller('controller', ['$scope',
function(scope) {
scope.test = {
name: ''
};
}
]);
app.directive('myValidate', [
function() {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
ctrl.$parsers.unshift(function(value) {
var valid = true;
if (value === 'invalid') {
valid = false;
}
ctrl.$setValidity(attrs.name, valid);
return valid ? value : undefined;
});
}
};
}
]);
Markup:
<body ng-app="app">
<div ng-controller="controller">
<form name="testform">
<input name="testform" type="text" ng-model="test.name" ng-minlength="3" my-validate /><br/>
</form>
<span style="color:green" ng-show="testform.testform.$valid">** valid **</span>
<span style="color:red" ng-show="testform.testform.$invalid">** invalid **</span>
</div>
<script src="script.js"></script>
</body>
Here is a plunker with this code: http://plnkr.co/edit/6XIKYvGwMSh1zpLRbbEm
The Angular documentation talks about this, though it wasn't totally obvious: https://docs.angularjs.org/api/ng/type/ngModel.NgModelController
This blog helped me get a hold on this: http://www.benlesh.com/2012/12/angular-js-custom-validation-via.html