Repository: netceteragroup/valdr Branch: master Commit: 2a6174f18402 Files: 74 Total size: 169.6 KB Directory structure: gitextract_m3qzmqq7/ ├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo/ │ ├── core/ │ │ ├── custom-validator.html │ │ ├── list.html │ │ ├── load-constraints.html │ │ └── simple.html │ ├── demo-api.js │ ├── demoConstraints.json │ ├── index.html │ └── message/ │ ├── angular-translate.html │ ├── angular-validation.html │ ├── message-template.tpl.html │ └── simple.html ├── js.prefix ├── js.suffix ├── karma.conf.js ├── package.json ├── server.js └── src/ ├── core/ │ ├── valdr-service.js │ ├── valdr-service.spec.js │ ├── valdrEnabled-directive.js │ ├── valdrEnabled-directive.spec.js │ ├── valdrFormGroup-directive.js │ ├── valdrFormGroup-directive.spec.js │ ├── valdrFormItem-directive.js │ ├── valdrFormItem-directive.spec.js │ ├── valdrType-directive.js │ ├── valdrType-directive.spec.js │ ├── valdrUtil-service.js │ ├── valdrUtil-service.spec.js │ └── validators/ │ ├── digitsValidator.js │ ├── digitsValidator.spec.js │ ├── emailValidator.js │ ├── emailValidator.spec.js │ ├── futureAndPastSharedValidator.js │ ├── futureValidator.js │ ├── futureValidator.spec.js │ ├── hibernateEmailValidator.js │ ├── hibernateEmailValidator.spec.js │ ├── hibernateUrlValidator.js │ ├── hibernateUrlValidator.spec.js │ ├── maxLengthValidator.js │ ├── maxLengthValidator.spec.js │ ├── maxValidator.js │ ├── maxValidator.spec.js │ ├── minLengthValidator.js │ ├── minLengthValidator.spec.js │ ├── minValidator.js │ ├── minValidator.spec.js │ ├── pastValidator.js │ ├── pastValidator.spec.js │ ├── patternValidator.js │ ├── patternValidator.spec.js │ ├── requiredValidator.js │ ├── requiredValidator.spec.js │ ├── sizeValidator.js │ ├── sizeValidator.spec.js │ ├── urlValidator.js │ └── urlValidator.spec.js ├── message/ │ ├── valdrMessage-directive.js │ ├── valdrMessage-directive.spec.js │ ├── valdrMessage-service.js │ └── valdrMessage-service.spec.js ├── valdr.js └── valdr.spec.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bowerrc ================================================ { "directory": "bower_components" } ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true ================================================ FILE: .gitignore ================================================ .DS_Store node_modules bower_components dist coverage demo/js npm-debug.log *.iml .idea/ ================================================ FILE: .jshintrc ================================================ { "predef": [ "describe", "it", "xit", "expect", "angular", "beforeEach", "spyOn", "inject", "module", "xdescribe", "ddescribe", "moment", "jasmine" ], "esnext": false, "bitwise": true, "curly": true, "eqeqeq": true, "immed": true, "indent": 2, "latedef": false, "newcap": true, "noarg": true, "quotmark": "single", "undef": true, "unused": true, "strict": false, "trailing": true, "smarttabs": true, "browser": true, "node": true } ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "0.10" before_install: - npm install -g grunt-cli karma-cli bower before_script: - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start script: grunt ================================================ FILE: CHANGELOG.md ================================================ # Changelog ## 1.1.6 - 2016-09-14 - Fix: min length validator and size validator validate only length [#107](https://github.com/netceteragroup/valdr/pull/107) - Fix: Fixed memory leak in valdrMessage-directive [#105](https://github.com/netceteragroup/valdr/pull/105) - Fix: Unable to validate multiselect dropdowns [#101](https://github.com/netceteragroup/valdr/issues/101) ## 1.1.5 - 2015-09-22 - Don't throw exception if an input has no name and valdr is disabled [#92](https://github.com/netceteragroup/valdr/pull/92) ## 1.1.4 - 2015-08-11 - Fix: valdrMessage does not add messages of angular validation directives [#90](https://github.com/netceteragroup/valdr/issues/90) ## 1.1.3 - 2015-06-22 - Support to validate non-input/-select/-textarea widgets [#83](https://github.com/netceteragroup/valdr/issues/83) [documentation](https://github.com/netceteragroup/valdr#applying-validation-to-custom-input-widgets) ## 1.1.2 - 2015-05-03 - Support dynamically added and removed form items, see [#62](https://github.com/netceteragroup/valdr/issues/62) - Support showing validation messages for AngularJS built-in validators, see [#58](https://github.com/netceteragroup/valdr/issues/58) ## 1.1.1 - 2015-01-04 - introduce ```valdrEnabled``` directive to conditionally enable/disable validation, see [#54](https://github.com/netceteragroup/valdr/issues/54) - fix bug that after removing constraints from valdr with valdr.removeConstraints(), the validity state of previously validated form items was not reset. see [#55](https://github.com/netceteragroup/valdr/issues/55) ## 1.1.0 - 2014-12-09 - added new valdrFormGroup directive which sets validity state for a group of form items and is responsible for adding and removing validation messages if valdr-message is loaded, see [#11](https://github.com/netceteragroup/valdr/issues/11), fixes [#44](https://github.com/netceteragroup/valdr/issues/44), fixes [#48](https://github.com/netceteragroup/valdr/issues/48) - support multiple aliases for constraint names, see [#30](https://github.com/netceteragroup/valdr/issues/30) - use the latest regular expression to validate e-mail addresses used in AngularJS, see [#33](https://github.com/netceteragroup/valdr/issues/33) - use new $validators pipeline from AngularJS 1.3 instead of $parsers and $formatters for validation, see [#35](https://github.com/netceteragroup/valdr/issues/35) - renamed no-valdr-message to valdr-no-message, see [#42](https://github.com/netceteragroup/valdr/issues/42) - use ng-show in the default message template instead of ng-if, fixes [#49](https://github.com/netceteragroup/valdr/issues/49) **BREAKING CHANGES** - valdr now requires AngularJS 1.3.x. - Before 1.1.0 a class named ```form-group``` was used to group multiple form items and add overall validity state. In 1.1.0 the new directive ```valdr-form-group``` was introduced for this purpose. All valdr validated form fields register with the next parent element with the ```valdr-form-group```directive (if present). The directive sets the form groups validity (```ng-valid```, ```ng-invalid```) and the class ```valdr-invalid-dirty-touched-group``` if one of the form items is invalid, has been changed and the user blurred out of the form item. Besides that, if ```valdr-messages``` is used to add validation messages, the ```valdr-form-group``` directive is the element in the DOM which adds and removes validation messages for all form items in the group. - the attribute ```no-valdr-message``` was renamed to ```valdr-no-message``` to disable message adding for individual form items ## 1.0.2 - 2014-11-18 - added new option ```valdr-no-validate``` to disable valdr validation on specific form elements, see [#41](https://github.com/netceteragroup/valdr/pull/41) - validity is now set for each valdr validator on ngModel controller, see [#37](https://github.com/netceteragroup/valdr/issues/37) - NOTE: this will be the last valdr version with support for AngularJS versions below 1.3 ## 1.0.1 - 2014-07-16 - Revalidate field with $modelValue instead of $viewValue on constraint change event, fixes [#34](https://github.com/netceteragroup/valdr/pull/34) ## 1.0.0 - 2014-06-27 - added support for isolated scopes around valdr-message directive, closes [#32](https://github.com/netceteragroup/valdr/issues/32) ## 0.2.0 - 2014-05-03 - add hibernateEmail validator, closes [#26](https://github.com/netceteragroup/valdr/issues/26) - add hibernateUrl validator, closes [#27](https://github.com/netceteragroup/valdr/issues/27) - add support for $valid {{ demoForm.bio.$valid }}
$valid {{ demoForm.homepage.$valid }}

Address

$valid {{ demoForm.zipCode.$valid }}
$valid {{ demoForm.email.$valid }}

Settings

demoForm.$valid: {{ demoForm.$valid }}

demoForm

{{ demoForm | json }}

constraints

{{ constraints | json }}
================================================ FILE: demo/demo-api.js ================================================ module.exports = function (app) { var url = require('url'), fs = require('fs'); var getConstraints = function (req, res) { function answer(code, data) { res.writeHead(code,{ 'Content-Type':'application/json;charset=utf-8', 'Access-Control-Allow-Origin':'*', 'Access-Control-Allow-Headers':'X-Requested-With' }); res.end(data); } fs.readFile('./demo/demoConstraints.json', function(err, data) { if (err) answer(404, ''); else answer(200, data); }); }; app.get('/api/constraints', getConstraints); }; ================================================ FILE: demo/demoConstraints.json ================================================ { "Person": { "lastName": { "size": { "min": 2, "max": 10, "message": "javax.validation.constraints.Size.message" } }, "firstName": { "size": { "min": 2, "max": 20, "message": "javax.validation.constraints.Size.message" } }, "zipCode": { "size": { "min": 4, "max": 4, "message": "javax.validation.constraints.Size.message" } } } } ================================================ FILE: demo/index.html ================================================ valdr demos

Core

Validation Messages

================================================ FILE: demo/message/angular-translate.html ================================================ Demo

angular-translate Demo

Person

{{ demoForm.lastName.valdrViolations | json }}

constraints

{{ constraints | json }}
================================================ FILE: demo/message/angular-validation.html ================================================ Demo

AngularJS validation messages

This example shows that by using valdrMessage it is possible to register validation messages for both AngularJS built-in and valdr validators.

The last name field has AngularJS and valdr validators, while the age field is of type number and therefore is automatically validated by the AngularJS number validator.

Person

demoForm.$valid = {{demoForm.$valid}}

constraints:

{{ constraints | json }}
================================================ FILE: demo/message/message-template.tpl.html ================================================

{{ violation.message }} {{ violation | json }}

================================================ FILE: demo/message/simple.html ================================================ Demo

Message Demo

Person

Error message disabled for this field.

constraints

{{ constraints | json }}
================================================ FILE: js.prefix ================================================ (function (window, document) { 'use strict'; ================================================ FILE: js.suffix ================================================ })(window, document); ================================================ FILE: karma.conf.js ================================================ module.exports = function (config) { config.set({ basePath: '', frameworks: ['jasmine'], files: [ 'bower_components/angular/angular.js', 'bower_components/angular-translate/angular-translate.js', 'bower_components/angular-mocks/angular-mocks.js', 'bower_components/momentjs/moment.js', 'src/valdr.js', 'src/**/*.js', 'src/**/*.spec.js' ], exclude: [], reporters: ['progress', 'coverage', 'coveralls'], port: 9876, colors: true, // LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG logLevel: config.LOG_INFO, // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera // - Safari (only Mac) // - PhantomJS // - IE (only Windows) browsers: [process.env.TRAVIS ? 'Firefox' : 'Chrome'], captureTimeout: 60000, singleRun: false, preprocessors: { 'src/**/!(*.spec)+(.js)': ['coverage'] }, coverageReporter: { type: 'lcov', dir: 'coverage/' }, plugins: [ 'karma-jasmine', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-phantomjs-launcher', 'karma-coverage', 'karma-coveralls' ] }); }; ================================================ FILE: package.json ================================================ { "name": "valdr", "version": "1.1.6", "description": "A model centric approach to AngularJS form validation", "homepage": "https://github.com/netceteragroup/valdr", "repository": { "type": "git", "url": "git://github.com/netceteragroup/valdr" }, "scripts": { "postinstall": "bower install" }, "author": { "name": "Netcetera AG" }, "license": "MIT", "devDependencies": { "bower": "~1.3.12", "karma": "~0.12.31", "karma-jasmine": "~0.1.5", "karma-chrome-launcher": "~0.1.7", "karma-firefox-launcher": "~0.1.4", "karma-phantomjs-launcher": "0.1.4", "karma-coverage": "~0.2.7", "karma-coveralls": "~0.1.4", "grunt": "~0.4.5", "grunt-karma": "~0.8.2", "grunt-contrib-copy": "~0.7.0", "grunt-contrib-clean": "~0.6.0", "grunt-contrib-concat": "~0.5.0", "grunt-contrib-uglify": "~0.7.0", "grunt-contrib-jshint": "~0.10.0", "grunt-contrib-watch": "~0.6.1", "load-grunt-tasks": "~2.0.0", "express": "~3.4.8", "grunt-express": "~1.2.1" } } ================================================ FILE: server.js ================================================ var express = require('express'); var app = express(); var server = require('http').createServer(app); app.configure(function () { app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(express.errorHandler()); app.use(express.static(__dirname)); app.use(app.router); require('./demo/demo-api')(app, __dirname); }); module.exports = server; // Override: Provide an "use" used by grunt-express. module.exports.use = function () { app.use.apply(app, arguments); }; ================================================ FILE: src/core/valdr-service.js ================================================ angular.module('valdr') .provider('valdr', function () { var constraints = {}, validators = {}, constraintUrl, constraintsLoading, constraintAliases = {}, validatorNames = [ 'valdrRequiredValidator', 'valdrSizeValidator', 'valdrMinLengthValidator', 'valdrMaxLengthValidator', 'valdrMinValidator', 'valdrMaxValidator', 'valdrEmailValidator', 'valdrUrlValidator', 'valdrDigitsValidator', 'valdrFutureValidator', 'valdrPastValidator', 'valdrPatternValidator', 'valdrHibernateEmailValidator' ]; var addConstraints = function (newConstraints) { angular.extend(constraints, newConstraints); }; this.addConstraints = addConstraints; var removeConstraints = function (constraintNames) { if (angular.isArray(constraintNames)) { angular.forEach(constraintNames, function (name) { delete constraints[name]; }); } else if (angular.isString(constraintNames)) { delete constraints[constraintNames]; } }; this.removeConstraints = removeConstraints; this.setConstraintUrl = function (url) { constraintUrl = url; }; this.addValidator = function (validatorName) { validatorNames.push(validatorName); }; this.addConstraintAlias = function (valdrName, alias) { if(!angular.isArray(constraintAliases[valdrName])) { constraintAliases[valdrName] = []; } constraintAliases[valdrName].push(alias); }; this.$get = ['$log', '$injector', '$rootScope', '$http', 'valdrEvents', 'valdrUtil', 'valdrClasses', function ($log, $injector, $rootScope, $http, valdrEvents, valdrUtil, valdrClasses) { // inject all validators angular.forEach(validatorNames, function (validatorName) { var validator = $injector.get(validatorName); validators[validator.name] = validator; // register validator with aliases if(angular.isArray(constraintAliases[validator.name])) { angular.forEach(constraintAliases[validator.name], function (alias) { validators[alias] = validator; }); } }); // load constraints via $http if constraintUrl is configured if (constraintUrl) { constraintsLoading = true; $http.get(constraintUrl).then(function (response) { constraintsLoading = false; addConstraints(response.data); $rootScope.$broadcast(valdrEvents.revalidate); })['finally'](function () { constraintsLoading = false; }); } var constraintsForType = function (type) { if (valdrUtil.has(constraints, type)) { return constraints[type]; } else if (!constraintsLoading) { $log.warn('No constraints for type \'' + type + '\' available.'); } }; return { /** * Validates the value of the given type with the constraints for the given field name. * * @param typeName the type name * @param fieldName the field name * @param value the value to validate * @returns {*} */ validate: function (typeName, fieldName, value) { var validResult = { valid: true }, typeConstraints = constraintsForType(typeName); if (valdrUtil.has(typeConstraints, fieldName)) { var fieldConstraints = typeConstraints[fieldName], fieldIsValid = true, validationResults = [], violations = []; angular.forEach(fieldConstraints, function (constraint, validatorName) { var validator = validators[validatorName]; if (angular.isUndefined(validator)) { $log.warn('No validator defined for \'' + validatorName + '\'. Can not validate field \'' + fieldName + '\''); return validResult; } var valid = validator.validate(value, constraint); var validationResult = { valid: valid, value: value, field: fieldName, type: typeName, validator: validatorName }; angular.extend(validationResult, constraint); validationResults.push(validationResult); if (!valid) { violations.push(validationResult); } fieldIsValid = fieldIsValid && valid; }); return { valid: fieldIsValid, violations: violations.length === 0 ? undefined : violations, validationResults: validationResults.length === 0 ? undefined : validationResults }; } else { return validResult; } }, addConstraints: function (newConstraints) { addConstraints(newConstraints); $rootScope.$broadcast(valdrEvents.revalidate); }, removeConstraints: function (constraintNames) { removeConstraints(constraintNames); $rootScope.$broadcast(valdrEvents.revalidate); }, getConstraints: function () { return constraints; }, setClasses: function (newClasses) { angular.extend(valdrClasses, newClasses); $rootScope.$broadcast(valdrEvents.revalidate); } }; }]; }); ================================================ FILE: src/core/valdr-service.spec.js ================================================ describe('valdr', function () { var valdr, $rootScope, valdrEvents, valdrClasses, sizeValidator, requiredValidator, personConstraints = { 'Person': { 'firstName': { 'size': { 'min': 0, 'max': 10, 'message': 'size' } } } }, addressConstraints = { 'Address': { 'street': { 'required': { 'message': 'required' } } } }; beforeEach(module('valdr')); beforeEach(inject( function (_valdr_, _$rootScope_, _valdrEvents_, _valdrClasses_, _valdrSizeValidator_, _valdrRequiredValidator_) { valdr = _valdr_; $rootScope = _$rootScope_; valdrEvents = _valdrEvents_; valdrClasses = _valdrClasses_; sizeValidator = _valdrSizeValidator_; requiredValidator = _valdrRequiredValidator_; })); describe('addConstraints()', function () { it('should add initial constraints', function () { // when valdr.addConstraints(personConstraints); // then expect(valdr.getConstraints()).toEqual(personConstraints); }); it('should extend initial constraints', function () { // when valdr.addConstraints(personConstraints); valdr.addConstraints(addressConstraints); // then expect(valdr.getConstraints().Person).toEqual(personConstraints.Person); expect(valdr.getConstraints().Address).toEqual(addressConstraints.Address); }); it('should broadcast event when constraints change', function () { // given spyOn($rootScope, '$broadcast'); // when valdr.addConstraints(personConstraints); // then expect($rootScope.$broadcast).toHaveBeenCalledWith(valdrEvents.revalidate); }); }); describe('removeConstraints()', function () { it('should remove single constraint', function () { // given valdr.addConstraints(personConstraints); valdr.addConstraints(addressConstraints); // when valdr.removeConstraints('Person'); // then expect(valdr.getConstraints()).toEqual(addressConstraints); }); it('should remove multiple constraints', function () { // given valdr.addConstraints(personConstraints); valdr.addConstraints(addressConstraints); // when valdr.removeConstraints(['Person', 'Address']); // then expect(valdr.getConstraints()).toEqual({}); }); it('should broadcast event when constraints change', function () { // given valdr.addConstraints(personConstraints); spyOn($rootScope, '$broadcast'); // when valdr.removeConstraints('Person'); // then expect($rootScope.$broadcast).toHaveBeenCalledWith(valdrEvents.revalidate); }); }); describe('validate()', function () { it('should not validate if no constraints are defined', function () { // when var validationResult = valdr.validate('Person', 'firstName', 'Hanueli'); // then expect(validationResult.valid).toBe(true); expect(validationResult.violations).toBeUndefined(); expect(validationResult.validationResults).toBeUndefined(); }); it('should validate with correct validator', function () { // given valdr.addConstraints(personConstraints); spyOn(sizeValidator, 'validate').andCallThrough(); // when var validationResult = valdr.validate('Person', 'firstName', 'Hanueli'); // then expect(validationResult.valid).toBe(true); expect(validationResult.violations).toBeUndefined(); expect(validationResult.validationResults.length).toBe(1); expect(sizeValidator.validate).toHaveBeenCalled(); }); it('should return true if no validator is available for a constraint', function () { // given valdr.addConstraints({ 'Person': { 'firstName': { 'ThereIsNoValidatorForThisConstraint': {} } } }); // when var validationResult = valdr.validate('Person', 'firstName', 'Hanueli'); // then expect(validationResult.valid).toBe(true); }); it('should return invalid state and message if validation fails', function () { // given valdr.addConstraints(personConstraints); spyOn(sizeValidator, 'validate').andCallThrough(); // when var validationResult = valdr.validate('Person', 'firstName', 'Hanueli with a name that is too long'); // then expect(sizeValidator.validate).toHaveBeenCalled(); expect(validationResult.valid).toBe(false); expect(validationResult.violations[0].message).toBe('size'); expect(validationResult.violations[0].field).toBe('firstName'); expect(validationResult.violations[0].type).toBe('Person'); expect(validationResult.violations[0].value).toBe('Hanueli with a name that is too long'); expect(validationResult.violations[0].max).toBe(10); expect(validationResult.violations[0].min).toBe(0); }); it('should return invalid state and message if multiple validations fail', function () { // given valdr.addConstraints({ 'Person': { 'firstName': { 'size': { 'min': 2, 'max': 10, 'message': 'size' }, 'required': { 'message': 'required' } } } }); spyOn(sizeValidator, 'validate').andCallThrough(); spyOn(requiredValidator, 'validate').andCallThrough(); // when var validationResult = valdr.validate('Person', 'firstName', undefined); // then expect(sizeValidator.validate).toHaveBeenCalled(); expect(requiredValidator.validate).toHaveBeenCalled(); expect(validationResult.valid).toBe(false); expect(validationResult.violations[0].message).toBe('required'); }); }); describe('setClasses()', function () { it('should add the given classes to the valdrClasses and broadcast an event', function () { // given spyOn($rootScope, '$broadcast'); var newClass = 'is-valid'; expect(valdrClasses.valid).toBe('ng-valid'); expect(valdrClasses.invalid).toBe('ng-invalid'); // when valdr.setClasses({ valid: newClass }); // then expect($rootScope.$broadcast).toHaveBeenCalledWith(valdrEvents.revalidate); expect(valdrClasses.valid).toBe(newClass); expect(valdrClasses.invalid).toBe('ng-invalid'); // cleanup to prevent side-effects valdr.setClasses({ valid: 'ng-valid' }); }); }); }); describe('valdrProvider', function () { it('should load the constraints via $http', function () { // given var $httpBackend, apiUrl = '/api/validation'; // when module('valdr'); module(function (valdrProvider) { valdrProvider.setConstraintUrl(apiUrl); }); inject(function (_$httpBackend_) { $httpBackend = _$httpBackend_; $httpBackend.expect('GET', apiUrl).respond(200, {}); }); /*jshint unused:false */ inject(function (valdr) { // injecting the valdr service triggers the loading and therefore the GET to the apiUrl $httpBackend.flush(); // then $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); }); it('should support to use aliases for constraint names', function () { // given module('valdr'); module(function (valdrProvider) { valdrProvider.addConstraints({ 'Person': { 'firstName': { 'sizeBetween': { 'min': 0, 'max': 10 }, 'secondSizeBetween': { 'min': 10, 'max': 20 } } } }); valdrProvider.addConstraintAlias('size', 'sizeBetween'); valdrProvider.addConstraintAlias('size', 'secondSizeBetween'); }); inject(function (valdr, valdrSizeValidator) { // when spyOn(valdrSizeValidator, 'validate').andCallThrough(); var validationResult = valdr.validate('Person', 'firstName', 'Hanueli'); // then expect(validationResult.valid).toBe(false); expect(validationResult.violations).toBeDefined(); expect(validationResult.violations.length).toBe(1); expect(validationResult.validationResults.length).toBe(2); expect(valdrSizeValidator.validate).toHaveBeenCalled(); }); }); describe('custom validators', function () { it('should allow to add custom validators', function () { // given module('valdr'); module(function ($provide) { $provide.factory('customValidator', function () { return { name: 'custom', validate: function (value) { return value === 'Hanueli'; } }; }); }); module(function (valdrProvider) { valdrProvider.addValidator('customValidator'); valdrProvider.addConstraints({ 'Person': { 'firstName': { 'custom': {} } } }); }); inject(function (valdr, customValidator) { // when spyOn(customValidator, 'validate').andCallThrough(); var validationResult = valdr.validate('Person', 'firstName', 'Hanueli'); // then expect(validationResult.valid).toBe(true); expect(validationResult.violations).toBeUndefined(); expect(customValidator.validate).toHaveBeenCalled(); }); }); }); }); ================================================ FILE: src/core/valdrEnabled-directive.js ================================================ angular.module('valdr') /** * This directive allows to dynamically enable and disable the validation with valdr. * All form elements in a child node of an element with the 'valdr-enabled' directive will be affected by this. * * Usage: * *
* *
* * If multiple valdr-enabled directives are nested, the one nearest to the validated form element * will take precedence. */ .directive('valdrEnabled', ['valdrEvents', function (valdrEvents) { return { controller: ['$scope', '$attrs', function($scope, $attrs) { $scope.$watch($attrs.valdrEnabled, function () { $scope.$broadcast(valdrEvents.revalidate); }); this.isEnabled = function () { var evaluatedExpression = $scope.$eval($attrs.valdrEnabled); return evaluatedExpression === undefined ? true : evaluatedExpression; }; }] }; }]); ================================================ FILE: src/core/valdrEnabled-directive.spec.js ================================================ describe('valdrEnabled directive', function () { // VARIABLES var $scope, $compile, element, valdr, ngModelController, template, personConstraints = { 'Person': { 'firstName': { 'required': { 'message': 'first name is required' } } } }; // TEST UTILITIES function compileValdrEnabledTemplate() { element = $compile(angular.element(template))($scope); $scope.$digest(); ngModelController = element.find('input').controller('ngModel'); } beforeEach(function () { module('valdr'); }); beforeEach(inject(function ($rootScope, _$compile_, _valdr_) { $compile = _$compile_; valdr = _valdr_; template = '
' + '' + '
'; $scope = $rootScope.$new(); $scope.person = { }; $scope.valdrEnabled = true; $scope.isValdrEnabled = function () { return $scope.valdrEnabled; }; valdr.addConstraints(personConstraints); })); // TEST it('should validate if valdrEnabled is true', function () { // given $scope.valdrEnabled = true; // when compileValdrEnabledTemplate(); // then expect(ngModelController.$valid).toBe(false); }); it('should not validate if valdrEnabled is false', function () { // given $scope.valdrEnabled = false; // when compileValdrEnabledTemplate(); // then expect(ngModelController.$valid).toBe(true); }); it('should validate if no expression is provided', function () { // given template = '
' + '' + '
'; // when compileValdrEnabledTemplate(); // then expect(ngModelController.$valid).toBe(false); }); it('should remove errors when constraints are removed', function () { // given compileValdrEnabledTemplate(); // when valdr.removeConstraints('Person'); // then expect(ngModelController.$valid).toBe(true); }); it('should remove errors when valdr gets disabled', function () { // given compileValdrEnabledTemplate(); // when $scope.$apply(function () { $scope.valdrEnabled = false; }); // then expect(ngModelController.$valid).toBe(true); }); }); ================================================ FILE: src/core/valdrFormGroup-directive.js ================================================ /** * This directive adds the validity state to a form group element surrounding valdr validated input fields. * If valdr-messages is loaded, it also adds the validation messages as last element to the element this this * directive is applied on. */ var valdrFormGroupDirectiveDefinition = ['valdrClasses', 'valdrConfig', function (valdrClasses, valdrConfig) { return { restrict: 'EA', link: function (scope, element) { if (valdrConfig.addFormGroupClass) { element.addClass(valdrClasses.formGroup); } }, controller: ['$scope', '$element', function ($scope, $element) { var formItems = [], messageElements = {}; /** * Checks the state of all valdr validated form items below this element. * @returns {Object} an object containing the states of all form items in this form group */ var getFormGroupState = function () { var formGroupState = { // true if an item in this form group is currently dirty, touched and invalid invalidDirtyTouchedGroup: false, // true if all form items in this group are currently valid valid: true, // contains the validity states of all form items in this group itemStates: [] }; angular.forEach(formItems, function (formItem) { if (formItem.$touched && formItem.$dirty && formItem.$invalid) { formGroupState.invalidDirtyTouchedGroup = true; } if (formItem.$invalid) { formGroupState.valid = false; } var itemState = { name: formItem.$name, touched: formItem.$touched, dirty: formItem.$dirty, valid: formItem.$valid }; formGroupState.itemStates.push(itemState); }); return formGroupState; }; /** * Updates the classes on this element and the valdr message elements based on the validity states * of the items in this form group. * @param formGroupState the current state of this form group and its items */ var updateClasses = function (formGroupState) { // form group state $element.toggleClass(valdrClasses.invalidDirtyTouchedGroup, formGroupState.invalidDirtyTouchedGroup); $element.toggleClass(valdrClasses.valid, formGroupState.valid); $element.toggleClass(valdrClasses.invalid, !formGroupState.valid); // valdr message states angular.forEach(formGroupState.itemStates, function (itemState) { var messageElement = messageElements[itemState.name]; if (messageElement) { messageElement.toggleClass(valdrClasses.valid, itemState.valid); messageElement.toggleClass(valdrClasses.invalid, !itemState.valid); messageElement.toggleClass(valdrClasses.dirty, itemState.dirty); messageElement.toggleClass(valdrClasses.pristine, !itemState.dirty); messageElement.toggleClass(valdrClasses.touched, itemState.touched); messageElement.toggleClass(valdrClasses.untouched, !itemState.touched); } }); }; $scope.$watch(getFormGroupState, updateClasses, true); this.addFormItem = function (ngModelController) { formItems.push(ngModelController); }; this.removeFormItem = function (ngModelController) { var index = formItems.indexOf(ngModelController); if (index >= 0) { formItems.splice(index, 1); } }; this.addMessageElement = function (ngModelController, messageElement) { $element.append(messageElement); messageElements[ngModelController.$name] = messageElement; }; this.removeMessageElement = function (ngModelController) { if (messageElements[ngModelController.$name]) { messageElements[ngModelController.$name].remove(); delete messageElements[ngModelController.$name]; } }; }] }; }]; angular.module('valdr') .directive('valdrFormGroup', valdrFormGroupDirectiveDefinition); ================================================ FILE: src/core/valdrFormGroup-directive.spec.js ================================================ describe('valdrFormGroup directive', function () { // VARIABLES var $scope, $compile, element, valdr, valdrClasses, valdrConfig, ngModelController, personConstraints = { 'Person': { 'firstName': { 'size': { 'min': 0, 'max': 10, 'message': 'size' } }, 'lastName': { 'size': { 'min': 0, 'max': 10, 'message': 'size' } } } }; var formGroupTemplate = '
' + '' + '' + '
'; // TEST UTILITIES function compileTemplate(template) { element = $compile(angular.element(template))($scope); $scope.$digest(); } function compileFormGroupTemplate() { compileTemplate(formGroupTemplate); ngModelController = element.find('input').controller('ngModel'); } beforeEach(function () { module('valdr'); }); beforeEach(inject(function ($rootScope, _$compile_, _valdr_, _valdrClasses_, _valdrConfig_) { $compile = _$compile_; valdr = _valdr_; valdrClasses = _valdrClasses_; valdrConfig = _valdrConfig_; $scope = $rootScope.$new(); $scope.person = { }; valdr.addConstraints(personConstraints); })); describe('valdrFormGroup', function () { beforeEach(function () { compileFormGroupTemplate(); }); describe('form-group class', function () { it ('should add form group class by default', function () { expect(element.hasClass(valdrClasses.formGroup)).toBe(true); }); it ('should not add form group class if option is disabled in valdrConfig', function () { // given valdrConfig.addFormGroupClass = false; // when compileFormGroupTemplate(); // then expect(element.hasClass(valdrClasses.formGroup)).toBe(false); }); }); it('should not set valid and invalidDirtyTouchedGroup classes if all items are valid', function () { expect(element.hasClass(valdrClasses.valid)).toBe(true); expect(element.hasClass(valdrClasses.invalid)).toBe(false); expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false); }); it('should not set invalid class if an item is not valid', function () { // given $scope.person.firstName = 'This name is too long for the constraints.'; // when $scope.$digest(); // then expect(element.hasClass(valdrClasses.invalid)).toBe(true); expect(element.hasClass(valdrClasses.valid)).toBe(false); expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false); }); it('should add invalidDirtyTouchedGroup class if an input is dirty, touched and invalid', function () { // given $scope.person.firstName = 'This name is too long for the constraints.'; ngModelController.$invalid = true; ngModelController.$dirty = true; ngModelController.$touched = true; // when $scope.$digest(); // then expect(element.hasClass(valdrClasses.invalid)).toBe(true); expect(element.hasClass(valdrClasses.valid)).toBe(false); expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(true); }); it('should be valid if no form items are registered', function () { // given var template = '
'; // when compileTemplate(template); // then expect(element.hasClass(valdrClasses.valid)).toBe(true); expect(element.hasClass(valdrClasses.invalid)).toBe(false); expect(element.hasClass(valdrClasses.invalidDirtyTouchedGroup)).toBe(false); }); }); }); ================================================ FILE: src/core/valdrFormItem-directive.js ================================================ /** * This controller is used if no valdrEnabled parent directive is available. */ var nullValdrEnabledController = { isEnabled: function () { return true; } }; /** * This controller is used if no valdrFormGroup parent directive is available. */ var nullValdrFormGroupController = { addFormItem: angular.noop, removeFormItem: angular.noop }; /** * This directive adds validation to all input and select fields as well as to explicitly enabled elements which are * bound to an ngModel and are surrounded by a valdrType directive. To prevent adding validation to specific fields, * the attribute 'valdr-no-validate' can be added to those fields. */ var valdrFormItemDirectiveDefinitionFactory = function (restrict) { return ['valdrEvents', 'valdr', 'valdrUtil', function (valdrEvents, valdr, valdrUtil) { return { restrict: restrict, require: ['?^valdrType', '?^ngModel', '?^valdrFormGroup', '?^valdrEnabled'], link: function (scope, element, attrs, controllers) { var valdrTypeController = controllers[0], ngModelController = controllers[1], valdrFormGroupController = controllers[2] || nullValdrFormGroupController, valdrEnabled = controllers[3] || nullValdrEnabledController, valdrNoValidate = attrs.valdrNoValidate, fieldName = attrs.name; /** * Don't do anything if * - this is an that's not inside of a valdr-type block * - there is no ng-model bound to input * - there is the 'valdr-no-validate' attribute present */ if (!valdrTypeController || !ngModelController || angular.isDefined(valdrNoValidate)) { return; } valdrFormGroupController.addFormItem(ngModelController); if (valdrUtil.isEmpty(fieldName) && valdrEnabled.isEnabled()) { console.warn('Form element with ID "' + attrs.id + '" is not bound to a field name.'); } var updateNgModelController = function (validationResult) { if (valdrEnabled.isEnabled()) { var validatorTokens = ['valdr']; // set validity state for individual valdr validators angular.forEach(validationResult.validationResults, function (result) { var validatorToken = valdrUtil.validatorNameToToken(result.validator); ngModelController.$setValidity(validatorToken, result.valid); validatorTokens.push(validatorToken); }); // set overall validity state of this form item ngModelController.$setValidity('valdr', validationResult.valid); ngModelController.valdrViolations = validationResult.violations; // remove errors for valdr validators which no longer exist angular.forEach(ngModelController.$error, function (value, validatorToken) { if (validatorTokens.indexOf(validatorToken) === -1 && valdrUtil.startsWith(validatorToken, 'valdr')) { ngModelController.$setValidity(validatorToken, true); } }); } else { angular.forEach(ngModelController.$error, function (value, validatorToken) { if (valdrUtil.startsWith(validatorToken, 'valdr')) { ngModelController.$setValidity(validatorToken, true); } }); ngModelController.valdrViolations = undefined; } }; var validate = function (modelValue) { var validationResult = valdr.validate(valdrTypeController.getType(), fieldName, modelValue); updateNgModelController(validationResult); return valdrEnabled.isEnabled() ? validationResult.valid : true; }; ngModelController.$validators.valdr = validate; scope.$on(valdrEvents.revalidate, function () { validate(ngModelController.$modelValue); }); scope.$on('$destroy', function () { valdrFormGroupController.removeFormItem(ngModelController); }); } }; }]; }, valdrFormItemElementDirectiveDefinition = valdrFormItemDirectiveDefinitionFactory('E'), valdrFormItemAttributeDirectiveDefinition = valdrFormItemDirectiveDefinitionFactory('A'); angular.module('valdr') .directive('input', valdrFormItemElementDirectiveDefinition) .directive('select', valdrFormItemElementDirectiveDefinition) .directive('textarea', valdrFormItemElementDirectiveDefinition) .directive('enableValdrValidation', valdrFormItemAttributeDirectiveDefinition); ================================================ FILE: src/core/valdrFormItem-directive.spec.js ================================================ describe('valdrFormItem directive', function () { // VARIABLES var $scope, $compile, element, valdr, valdrEvents, valdrClasses, ngModelController, violations = ['violationsArray'], validationResults = [{ validator: 'required', valid: true }]; var inputTemplate = '
' + '
' + '' + '
' + '
'; var selectTemplate = '
' + '
' + '' + '
' + '
'; var textareaTemplate = '
' + '
' + '' + '
' + '
'; var inputWidgetTemplate = '
' + '
' + '
' + '
' + '
' + '
'; // TEST UTILITIES function compileTemplate(template) { element = $compile(angular.element(template))($scope); $scope.$digest(); } function compileInputTemplate() { compileTemplate(inputTemplate); ngModelController = element.find('input').controller('ngModel'); } // COMMON SETUP beforeEach(function () { module('valdr'); /** * Mock the valdr to always return 'true' when the value equals the string 'valid'. */ module(function ($provide) { $provide.value('valdr', { validate: function (typeName, fieldName, value) { return { valid: value === 'valid', violations: violations, validationResults: validationResults }; } }); }); }); beforeEach(inject(function ($rootScope, _$compile_, _valdr_, _valdrEvents_, _valdrClasses_) { $compile = _$compile_; $scope = $rootScope.$new(); $scope.myObject = { field: 'fieldValue' }; valdr = _valdr_; valdrEvents = _valdrEvents_; valdrClasses = _valdrClasses_; })); // COMMON TESTS function runFormItemCommonTests() { it('should set the validity to false on ngModelController if validation fails', function () { // when $scope.$apply(function () { $scope.myObject.field = 'invalid'; }); // then expect(ngModelController.$valid).toBe(false); expect(ngModelController.valdrViolations).toBe(violations); expect(ngModelController.$validators.valdr).toBeDefined(); }); it('should set the validity to true on ngModelController if validation is ok', function () { // when $scope.$apply(function () { $scope.myObject.field = 'valid'; }); // then expect(ngModelController.$valid).toBe(true); expect(ngModelController.valdrViolations).toBe(violations); }); it('should register in valdrFormGroup', function () { // given var formGroupTemplate = '
' + '' + '
'; compileTemplate(formGroupTemplate); // when $scope.$apply(function () { $scope.myObject.field = 'invalid'; }); // then expect(element.hasClass(valdrClasses.invalid)).toBe(true); }); it('should unregister from valdrFormGroup on $destroy', function () { // given var formGroupTemplate = '
' + '' + '
'; compileTemplate(formGroupTemplate); $scope.$apply(function () { $scope.myObject.field = 'invalid'; }); // when $scope.$broadcast('$destroy'); $scope.$digest(); // then expect(element.hasClass(valdrClasses.invalid)).toBe(false); }); it('should handle constraint changed events', function () { // given spyOn(valdr, 'validate').andCallThrough(); ngModelController.$viewValue = 'viewValue'; // when $scope.$broadcast(valdrEvents.revalidate); // then expect(valdr.validate).toHaveBeenCalledWith(jasmine.any(String), 'fieldName', ngModelController.$modelValue); }); } describe('on input fields', function () { beforeEach(function () { compileInputTemplate(); }); runFormItemCommonTests(); it('should log warning if no field name is provided on the input', function () { // given spyOn(console, 'warn'); var invalidInput = '
' + '
' + '' + '
' + '
'; // when $compile(angular.element(invalidInput))($scope); // then expect(console.warn).toHaveBeenCalledWith('Form element with ID "undefined" is not bound to a field name.'); }); it('should NOT log warning if no field name is provided on the input but valdr is disabled', function () { // given spyOn(console, 'warn'); var invalidInput = '
' + '
' + ' ' + '
' + '
'; // when compileTemplate(invalidInput); // then expect(console.warn).not.toHaveBeenCalled(); }); it('should NOT use valdr validation if valdr-no-validate is set', function () { // given var noValdrValidationInput = '
' + '
' + '' + '
' + '
'; // when compileTemplate(noValdrValidationInput); ngModelController = element.find('input').controller('ngModel'); // then expect(ngModelController.$validators.valdr).toBeUndefined(); }); }); describe('on select elements', function () { beforeEach(function () { compileTemplate(selectTemplate); ngModelController = element.find('select').controller('ngModel'); }); runFormItemCommonTests(); }); describe('on textarea elements', function () { beforeEach(function () { compileTemplate(textareaTemplate); ngModelController = element.find('textarea').controller('ngModel'); }); runFormItemCommonTests(); }); describe('on explicitly enabled elements', function () { beforeEach(function () { compileTemplate(inputWidgetTemplate); ngModelController = element.find('section').controller('ngModel'); }); runFormItemCommonTests(); }); }); ================================================ FILE: src/core/valdrType-directive.js ================================================ angular.module('valdr') /** * The valdrType directive defines the type of the model to be validated. * The directive exposes the type through the controller to allow access to it by wrapped directives. */ .directive('valdrType', function () { return { priority: 1, controller: ['$attrs', function ($attrs) { this.getType = function () { return $attrs.valdrType; }; }] }; }); ================================================ FILE: src/core/valdrType-directive.spec.js ================================================ describe('valdrType directive', function () { var $scope, $compile, element, FormTypeController; var compileTemplate = function () { var element = $compile(angular.element('
'))($scope); $scope.$digest(); return element; }; beforeEach(module('valdr')); beforeEach(inject(function ($rootScope, $controller, _$compile_) { $compile = _$compile_; $scope = $rootScope.$new(); element = compileTemplate(); FormTypeController = element.controller('valdrType'); })); it('should read the type from the attribute', function () { expect(FormTypeController.getType()).toBe('TestClass'); }); it('should allow to nest the directive', function () { // given var element = $compile(angular.element( '
' + '' + '
'))($scope); // when var rootController = element.controller('valdrType'); var nestedController = element.find('span').controller('valdrType'); // then expect(rootController.getType()).toBe('TestClass'); expect(nestedController.getType()).toBe('NestedClass'); }); }); ================================================ FILE: src/core/valdrUtil-service.js ================================================ angular.module('valdr') /** * Exposes utility functions used in validators and valdr core. */ .factory('valdrUtil', [function () { var substringAfterDot = function (string) { if (string.lastIndexOf('.') === -1) { return string; } else { return string.substring(string.lastIndexOf('.') + 1, string.length); } }; var SLUG_CASE_REGEXP = /[A-Z]/g; var slugCase = function (string) { return string.replace(SLUG_CASE_REGEXP, function(letter, pos) { return (pos ? '-' : '') + letter.toLowerCase(); }); }; /** * Converts the given validator name to a validation token. Uses the last part of the validator name after the * dot (if present) and converts camel case to slug case (fooBar -> foo-bar). * @param validatorName the validator name * @returns {string} the validation token */ var validatorNameToToken = function (validatorName) { if (angular.isString(validatorName)) { var name = substringAfterDot(validatorName); name = slugCase(name); return 'valdr-' + name; } else { return validatorName; } }; return { validatorNameToToken: validatorNameToToken, isNaN: function (value) { // `NaN` as a primitive is the only value that is not equal to itself // (perform the [[Class]] check first to avoid errors with some host objects in IE) return this.isNumber(value) && value !== +value; }, isNumber: function (value) { var type = typeof value; return type === 'number' || value && type === 'object' && Object.prototype.toString.call(value) === '[object Number]' || false; }, has: function (object, key) { return object ? Object.prototype.hasOwnProperty.call(object, key) : false; }, /** * @param value the value * @returns {boolean} true if the given value is not null, not undefined, not an empty string, NaN returns false */ notEmpty: function (value) { if (this.isNaN(value)) { return false; } if (angular.isArray(value) && value.length === 0){ return false; } return angular.isDefined(value) && value !== '' && value !== null; }, /** * @param value the value to validate * @returns {boolean} true if the given value is null, undefined, an empty string, NaN returns false */ isEmpty: function (value) { if (this.isNaN(value)) { return false; } return !this.notEmpty(value); }, /** * Checks if a string value starts with a given prefix. * * @param value the value * @param prefix the prefix * @returns {boolean} true if the given value starts with the given prefix. */ startsWith: function (value, prefix) { return angular.isString(value) && angular.isString(prefix) && value.lastIndexOf(prefix, 0) === 0; } }; }]) ; ================================================ FILE: src/core/valdrUtil-service.spec.js ================================================ describe('valdrUtil', function () { var valdrUtil; beforeEach(module('valdr')); beforeEach(inject(function (_valdrUtil_) { valdrUtil = _valdrUtil_; })); describe('validatorNameToToken()', function () { it('should convert camel case to slug case and prepend with valdr', function () { expect(valdrUtil.validatorNameToToken(undefined)).toBe(undefined); expect(valdrUtil.validatorNameToToken('nocamel')).toBe('valdr-nocamel'); expect(valdrUtil.validatorNameToToken('camelCase')).toBe('valdr-camel-case'); expect(valdrUtil.validatorNameToToken('CapitalCamelCase')).toBe('valdr-capital-camel-case'); }); it('should convert remove everything before the last dot and convert to slug case', function () { expect(valdrUtil.validatorNameToToken('bla.nocamel')).toBe('valdr-nocamel'); expect(valdrUtil.validatorNameToToken('bla.camelCase')).toBe('valdr-camel-case'); expect(valdrUtil.validatorNameToToken('bla.CapitalCamelCase')).toBe('valdr-capital-camel-case'); }); }); describe('isNaN()', function () { it('should provide isNaN function', inject(function (valdrUtil) { expect(valdrUtil.isNaN).toBeDefined(); expect(typeof valdrUtil.isNaN).toBe('function'); })); it('should check if input is NaN', function () { expect(valdrUtil.isNaN(NaN)).toBe(true); expect(valdrUtil.isNaN('string')).toBe(false); expect(valdrUtil.isNaN(0)).toBe(false); }); }); describe('isNumber()', function () { it('should provide isNumber function', inject(function (valdrUtil) { expect(valdrUtil.isNumber).toBeDefined(); expect(typeof valdrUtil.isNumber).toBe('function'); })); it('should check if input is a number', function () { expect(valdrUtil.isNumber(NaN)).toBe(true); expect(valdrUtil.isNumber(0)).toBe(true); expect(valdrUtil.isNumber('string')).toBe(false); expect(valdrUtil.isNumber(undefined)).toBe(false); expect(valdrUtil.isNumber(null)).toBe(false); }); }); describe('has()', function () { it('should provide has function', inject(function (valdrUtil) { expect(valdrUtil.has).toBeDefined(); expect(typeof valdrUtil.has).toBe('function'); })); it('should check if object has property', function () { expect(valdrUtil.has({foo: 'a'}, 'foo')).toBe(true); expect(valdrUtil.has({foo: undefined}, 'foo')).toBe(true); expect(valdrUtil.has({}, 'foo')).toBe(false); expect(valdrUtil.has(undefined, 'foo')).toBe(false); }); }); describe('notEmpty()/isEmpty()', function () { it('should validate strings', function () { expect(valdrUtil.notEmpty('string')).toBe(true); expect(valdrUtil.isEmpty('string')).toBe(false); }); it('should validate null value', function () { expect(valdrUtil.notEmpty(null)).toBe(false); expect(valdrUtil.isEmpty(null)).toBe(true); expect(valdrUtil.notEmpty('null')).toBe(true); expect(valdrUtil.isEmpty('null')).toBe(false); }); it('should validate undefined value', function () { expect(valdrUtil.notEmpty(undefined)).toBe(false); expect(valdrUtil.isEmpty(undefined)).toBe(true); }); it('should validate NaN value', function () { // NaN obviously is not a number but we don't know what it is, hence we cannot tell whether it's empty or not expect(valdrUtil.notEmpty(NaN)).toBe(false); expect(valdrUtil.isEmpty(NaN)).toBe(false); }); it('should validate empty string', function () { expect(valdrUtil.notEmpty('')).toBe(false); expect(valdrUtil.isEmpty('')).toBe(true); }); it('should validate arrays', function () { expect(valdrUtil.notEmpty(['Apple', 'Banana'])).toBe(true); expect(valdrUtil.isEmpty(['Apple', 'Banana'])).toBe(false); expect(valdrUtil.isEmpty([])).toBe(true); expect(valdrUtil.notEmpty([])).toBe(false); }); }); describe('startsWith()', function () { it ('should determine if a string starts with the given prefix', function () { expect(valdrUtil.startsWith('myString', 'my')).toBe(true); expect(valdrUtil.startsWith('string', 'string')).toBe(true); expect(valdrUtil.startsWith('', '')).toBe(true); expect(valdrUtil.startsWith('value', '')).toBe(true); }); it('should return false if a string does not start with the specified prefix', function () { expect(valdrUtil.startsWith('', 'prefix')).toBe(false); expect(valdrUtil.startsWith('someThing', 'something')).toBe(false); expect(valdrUtil.startsWith(undefined, 'prefix')).toBe(false); expect(valdrUtil.startsWith(undefined, undefined)).toBe(false); expect(valdrUtil.startsWith('value', undefined)).toBe(false); }); }); }); ================================================ FILE: src/core/validators/digitsValidator.js ================================================ angular.module('valdr') .factory('valdrDigitsValidator', ['valdrUtil', function (valdrUtil) { // matches everything except digits and '.' as decimal separator var regexp = new RegExp('[^.\\d]', 'g'); /** * By converting to number and back to string using toString(), we make sure that '.' is used as decimal separator * and not the locale specific decimal separator. * As we already checked for NaN at this point, we can do this safely. */ var toStringWithoutThousandSeparators = function (value) { return Number(value).toString().replace(regexp, ''); }; var isNotLongerThan = function (valueAsString, maxLengthConstraint) { return !valueAsString ? true : valueAsString.length <= maxLengthConstraint; }; var doValidate = function (value, constraint) { var integerConstraint = constraint.integer, fractionConstraint = constraint.fraction, cleanValueAsString, integerAndFraction; cleanValueAsString = toStringWithoutThousandSeparators(value); integerAndFraction = cleanValueAsString.split('.'); return isNotLongerThan(integerAndFraction[0], integerConstraint) && isNotLongerThan(integerAndFraction[1], fractionConstraint); }; return { name: 'digits', /** * Checks if the value is a number within accepted range. * * @param value the value to validate * @param constraint the validation constraint, it is expected to have integer and fraction properties (maximum * number of integral/fractional digits accepted for this number) * @returns {boolean} true if valid */ validate: function (value, constraint) { if (valdrUtil.isEmpty(value)) { return true; } if (valdrUtil.isNaN(Number(value))) { return false; } return doValidate(value, constraint); } }; }]); ================================================ FILE: src/core/validators/digitsValidator.spec.js ================================================ describe('valdrDigitsValidator', function () { var digitsValidator, constraint; beforeEach(module('valdr')); beforeEach(inject(function (valdrDigitsValidator) { digitsValidator = valdrDigitsValidator; constraint = { integer: '4', fraction: '2' }; })); it('should be named "digits"', function () { expect(digitsValidator.name).toBe('digits'); }); it('should return true for empty, null, and undefined', function () { expect(digitsValidator.validate('', constraint)).toBe(true); expect(digitsValidator.validate(null, constraint)).toBe(true); expect(digitsValidator.validate(undefined, constraint)).toBe(true); }); it('should return false for NaN', function () { expect(digitsValidator.validate(NaN, constraint)).toBe(false); }); it('should return true for valid integers', function () { expect(digitsValidator.validate('10', constraint)).toBe(true); expect(digitsValidator.validate(10, constraint)).toBe(true); expect(digitsValidator.validate(1000, constraint)).toBe(true); expect(digitsValidator.validate(-1000, constraint)).toBe(true); expect(digitsValidator.validate('10.02', constraint)).toBe(true); expect(digitsValidator.validate(10.02, constraint)).toBe(true); expect(digitsValidator.validate(-10.02, constraint)).toBe(true); expect(digitsValidator.validate(9999.99, constraint)).toBe(true); expect(digitsValidator.validate(-9999.99, constraint)).toBe(true); }); it('should return false for invalid numbers and strings', function () { expect(digitsValidator.validate(10.001, constraint)).toBe(false); expect(digitsValidator.validate('10001', constraint)).toBe(false); expect(digitsValidator.validate('10.001', constraint)).toBe(false); expect(digitsValidator.validate(1000.001, constraint)).toBe(false); expect(digitsValidator.validate('string', constraint)).toBe(false); expect(digitsValidator.validate('number', constraint)).toBe(false); expect(digitsValidator.validate('47:11', constraint)).toBe(false); expect(digitsValidator.validate('47;11', constraint)).toBe(false); expect(digitsValidator.validate('47\'11', constraint)).toBe(false); }); it('should validate correctly with fraction > 4', function() { constraint.fraction = 4; expect(digitsValidator.validate(1000.00001, constraint)).toBe(false); expect(digitsValidator.validate(1000.0001, constraint)).toBe(true); }); it('should not choke on integer:1 conditions', function () { constraint.integer = 1; expect(digitsValidator.validate('10', constraint)).toBe(false); expect(digitsValidator.validate(10.0, constraint)).toBe(false); expect(digitsValidator.validate(0.1, constraint)).toBe(true); expect(digitsValidator.validate(0.11, constraint)).toBe(true); expect(digitsValidator.validate(0.111, constraint)).toBe(false); }); it('should not choke on fraction:0 conditions', function () { constraint.fraction = 1; expect(digitsValidator.validate('10', constraint)).toBe(true); expect(digitsValidator.validate(10.0, constraint)).toBe(true); expect(digitsValidator.validate(10, constraint)).toBe(true); expect(digitsValidator.validate(0, constraint)).toBe(true); expect(digitsValidator.validate(0.11, constraint)).toBe(false); expect(digitsValidator.validate(0.111, constraint)).toBe(false); }); }); ================================================ FILE: src/core/validators/emailValidator.js ================================================ angular.module('valdr') .factory('valdrEmailValidator', ['valdrUtil', function (valdrUtil) { // the e-mail pattern used in angular.js var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; return { name: 'email', /** * Checks if the value is a valid email address. * * @param value the value to validate * @returns {boolean} true if valid */ validate: function (value) { return valdrUtil.isEmpty(value) || EMAIL_REGEXP.test(value); } }; }]); ================================================ FILE: src/core/validators/emailValidator.spec.js ================================================ describe('valdrEmailValidator', function () { var emailValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrEmailValidator) { emailValidator = valdrEmailValidator; })); it('should provide the correct name', function () { expect(emailValidator.name).toBe('email'); }); it('should return true for empty values', function () { expect(emailValidator.validate('')).toBe(true); expect(emailValidator.validate(undefined)).toBe(true); }); it('should return true for valid email addresses', function () { expect(emailValidator.validate('hanueli@mountains.ch')).toBe(true); expect(emailValidator.validate('valdr.welds@anything.com')).toBe(true); expect(emailValidator.validate('hanueli@asdf')).toBe(true); expect(emailValidator.validate('hanueli@192.168.1.1')).toBe(true); expect(emailValidator.validate('a@3b.c')).toBe(true); expect(emailValidator.validate('a@b')).toBe(true); }); it('should return false for invalid email addresses', function () { expect(emailValidator.validate('hanueli@')).toBe(false); expect(emailValidator.validate('hanueli@@gmail.com')).toBe(false); expect(emailValidator.validate('hanueli@gm ail.com')).toBe(false); expect(emailValidator.validate('hanueli@gmail..com')).toBe(false); expect(emailValidator.validate('hanueli@gmail.com.')).toBe(false); expect(emailValidator.validate('@anything.com')).toBe(false); expect(emailValidator.validate('...')).toBe(false); expect(emailValidator.validate('.@.')).toBe(false); expect(emailValidator.validate(' aaa.@. ')).toBe(false); expect(emailValidator.validate('a@-b.c')).toBe(false); expect(emailValidator.validate('a@b-.c')).toBe(false); }); }); ================================================ FILE: src/core/validators/futureAndPastSharedValidator.js ================================================ angular.module('valdr') .factory('futureAndPastSharedValidator', ['valdrUtil', function (valdrUtil) { var someAlternativeDateFormats = ['D-M-YYYY', 'D.M.YYYY', 'D/M/YYYY', 'D. M. YYYY', 'YYYY.M.D']; return { validate: function (value, comparison) { var now = moment(), valueAsMoment; if (valdrUtil.isEmpty(value)) { return true; } valueAsMoment = moment(value); for (var i = 0; i < someAlternativeDateFormats.length && !valueAsMoment.isValid(); i++) { valueAsMoment = moment(value, someAlternativeDateFormats[i], true); } return valueAsMoment.isValid() && comparison(valueAsMoment, now); } }; }]); ================================================ FILE: src/core/validators/futureValidator.js ================================================ angular.module('valdr') .factory('valdrFutureValidator', ['futureAndPastSharedValidator', function (futureAndPastSharedValidator) { return { name: 'future', /** * Checks if the value is a date in the future. * * @param value the value to validate * @returns {boolean} true if empty, null, undefined or a date in the future, false otherwise */ validate: function (value) { return futureAndPastSharedValidator.validate(value, function (valueAsMoment, now) { return valueAsMoment.isAfter(now); }); } }; }]); ================================================ FILE: src/core/validators/futureValidator.spec.js ================================================ describe('valdrFutureValidator', function () { var futureValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrFutureValidator) { futureValidator = valdrFutureValidator; })); it('should be named "future"', function () { expect(futureValidator.name).toBe('future'); }); it('should return true for empty values', function () { expect(futureValidator.validate('')).toBe(true); expect(futureValidator.validate(null)).toBe(true); expect(futureValidator.validate(undefined)).toBe(true); }); it('should return false for NaN', function () { expect(futureValidator.validate(NaN)).toBe(false); }); it('should return false for non-dates', function () { expect(futureValidator.validate(' ')).toBe(false); expect(futureValidator.validate('foo')).toBe(false); expect(futureValidator.validate('_1.1.2014')).toBe(false); expect(futureValidator.validate('31.2.2014')).toBe(false); expect(futureValidator.validate('31:1:2014')).toBe(false); }); it('should return false for dates in the past', function () { expect(futureValidator.validate('1.1.1900')).toBe(false); expect(futureValidator.validate('01.01.1900')).toBe(false); expect(futureValidator.validate('1. 1. 1900')).toBe(false); expect(futureValidator.validate('01. 01. 1900')).toBe(false); expect(futureValidator.validate('1-1-1900')).toBe(false); expect(futureValidator.validate('01-01-1900')).toBe(false); expect(futureValidator.validate('1/1/1900')).toBe(false); expect(futureValidator.validate('01/01/1900')).toBe(false); expect(futureValidator.validate('1900.1.1')).toBe(false); expect(futureValidator.validate('1900.01.01')).toBe(false); expect(futureValidator.validate('2000/12/31')).toBe(false); expect(futureValidator.validate('2000-12-31')).toBe(false); expect(futureValidator.validate(moment().subtract(1, 'seconds'))).toBe(false); }); it('should return true for dates in the future', function () { expect(futureValidator.validate('1.1.2900')).toBe(true); expect(futureValidator.validate('2030/12/31')).toBe(true); expect(futureValidator.validate('2030-12-31')).toBe(true); expect(futureValidator.validate(moment().add(10, 'seconds'))).toBe(true); }); }); ================================================ FILE: src/core/validators/hibernateEmailValidator.js ================================================ angular.module('valdr') .factory('valdrHibernateEmailValidator', ['valdrUtil', function (valdrUtil) { var ATOM = '[a-z0-9!#$%&\'*+/=?^_`{|}~-]'; var DOMAIN = '^' + ATOM + '+(\\.' + ATOM + '+)*$'; var IP_DOMAIN = '^\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\]$'; var localPattern = new RegExp('^' + ATOM + '+(\\.' + ATOM + '+)*$', 'i'); var domainPattern = new RegExp(DOMAIN + '|' + IP_DOMAIN, 'i'); return { name: 'hibernateEmail', /** * Checks if the value is a valid email address using the same patterns as Hibernate uses in its bean validation * implementation. * * @param value the value to validate * @returns {boolean} true if valid */ validate: function (value) { if (valdrUtil.isEmpty(value)) { return true; } // split email at '@' and consider local and domain part separately var emailParts = value.split('@'); if (emailParts.length !== 2) { return false; } if (!localPattern.test(emailParts[0])) { return false; } return domainPattern.test(emailParts[1]); } }; }]); ================================================ FILE: src/core/validators/hibernateEmailValidator.spec.js ================================================ describe('valdrEmailValidator', function () { var hibernateEmailValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrHibernateEmailValidator) { hibernateEmailValidator = valdrHibernateEmailValidator; })); it('should provide the correct name', function () { expect(hibernateEmailValidator.name).toBe('hibernateEmail'); }); it('should return true for empty values', function () { expect(hibernateEmailValidator.validate('')).toBe(true); expect(hibernateEmailValidator.validate(undefined)).toBe(true); }); it('should return true for valid email addresses', function () { expect(hibernateEmailValidator.validate('hanueli@mountains.ch')).toBe(true); expect(hibernateEmailValidator.validate('valdr.welds@anything.com')).toBe(true); expect(hibernateEmailValidator.validate('hanueli@asdf')).toBe(true); expect(hibernateEmailValidator.validate('hanueli@192.168.1.1')).toBe(true); }); it('should return false for invalid email addresses', function () { expect(hibernateEmailValidator.validate('hanueli@')).toBe(false); expect(hibernateEmailValidator.validate('hanueli@@gmail.com')).toBe(false); expect(hibernateEmailValidator.validate('hanueli@gm ail.com')).toBe(false); expect(hibernateEmailValidator.validate('hanueli@gmail..com')).toBe(false); expect(hibernateEmailValidator.validate('hanueli@gmail.com.')).toBe(false); expect(hibernateEmailValidator.validate('@anything.com')).toBe(false); expect(hibernateEmailValidator.validate('...')).toBe(false); expect(hibernateEmailValidator.validate('.@.')).toBe(false); expect(hibernateEmailValidator.validate(' aaa.@. ')).toBe(false); expect(hibernateEmailValidator.validate('hanueli.@gmail.com')).toBe(false); }); }); ================================================ FILE: src/core/validators/hibernateUrlValidator.js ================================================ angular.module('valdr') .factory('valdrHibernateUrlValidator', ['valdrUrlValidator', function (valdrUrlValidator) { return { name: 'hibernateUrl', /** * Checks if the value is a valid URL according to the internal URL validator. It'll also ignore the Hibernate * properties protocol, host, and port. See https://github.com/netceteragroup/valdr/issues/27 for the rational * behind this implementation. * * @param value the value to validate * @returns {boolean} true if valid */ validate: function (value) { return valdrUrlValidator.validate(value); } }; }]); ================================================ FILE: src/core/validators/hibernateUrlValidator.spec.js ================================================ describe('valdrHibernateUrlValidator', function () { var urlValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrHibernateUrlValidator) { urlValidator = valdrHibernateUrlValidator; })); it('should provide the correct name', function () { expect(urlValidator.name).toBe('hibernateUrl'); }); it('should return true for empty values', function () { expect(urlValidator.validate('')).toBe(true); expect(urlValidator.validate(undefined)).toBe(true); }); it('should return true for valid url', function () { // some trivial tests expect(urlValidator.validate('http://www.google.ch')).toBe(true); expect(urlValidator.validate('http://server:123/path')).toBe(true); // some tests from http://djpowell.net/atomrdf/0.1/files/uritest.xml expect(urlValidator.validate('http://a/b/c/d;p?q#s')).toBe(true); expect(urlValidator.validate('http://a/b/c/g?y#s')).toBe(true); expect(urlValidator.validate('http://a/b/c/g..')).toBe(true); expect(urlValidator.validate('http://a/b/c/d;p?y')).toBe(true); expect(urlValidator.validate('http://a/b/c/g;x=1/y')).toBe(true); }); it('should return false for invalid url', function () { expect(urlValidator.validate('a@B.c')).toBe(false); expect(urlValidator.validate('hanueli')).toBe(false); }); }); ================================================ FILE: src/core/validators/maxLengthValidator.js ================================================ angular.module('valdr') .factory('valdrMaxLengthValidator', ['valdrUtil', function (valdrUtil) { return { name: 'maxLength', /** * Checks if the value is a string and if it's at most 'constraint.number' of characters long. * * @param value the value to validate * @param constraint with property 'number' * @returns {boolean} true if valid */ validate: function (value, constraint) { var maxLength = constraint.number; if (valdrUtil.isEmpty(value)) { return true; } if (typeof value === 'string') { return value.length <= maxLength; } else { return false; } } }; }]); ================================================ FILE: src/core/validators/maxLengthValidator.spec.js ================================================ describe('valdrMaxLengthValidator', function () { var maxLengthValidator, constraint = { number: 5, message: 'message' }; beforeEach(module('valdr')); beforeEach(inject(function (valdrMaxLengthValidator) { maxLengthValidator = valdrMaxLengthValidator; })); it('should provide the correct name', function () { expect(maxLengthValidator.name).toBe('maxLength'); }); it('should return true if value is valid', function () { // given var value = 'a'; // when var valid = maxLengthValidator.validate(value, constraint); // then expect(valid).toBe(true); }); it('should return false if string exceeds maxLength', function () { // given var value = 'super long string that exceeds maxLength'; // when var valid = maxLengthValidator.validate(value, constraint); // then expect(valid).toBe(false); }); it('should be valid if value is undefined', function () { // given // when var valid = maxLengthValidator.validate(undefined, constraint); // then expect(valid).toBe(true); }); it('should be invalid if value is number', function () { // given constraint.number = 2; // when var valid = maxLengthValidator.validate(123, constraint); // then expect(valid).toBe(false); }); it('should be valid if value is null', function () { // given // when var valid = maxLengthValidator.validate(null, constraint); // then expect(valid).toBe(true); }); }); ================================================ FILE: src/core/validators/maxValidator.js ================================================ angular.module('valdr') .factory('valdrMaxValidator', ['valdrUtil', function (valdrUtil) { return { name: 'max', /** * Checks if the value is a number and lower or equal as the value specified in the constraint. * * @param value the value to validate * @param constraint the validation constraint * @returns {boolean} true if valid */ validate: function (value, constraint) { var maxValue = Number(constraint.value), valueAsNumber = Number(value); if (valdrUtil.isNaN(value)) { return false; } return valdrUtil.isEmpty(value) || valueAsNumber <= maxValue; } }; }]); ================================================ FILE: src/core/validators/maxValidator.spec.js ================================================ describe('valdrMaxValidator', function () { var maxValidator, constraint; beforeEach(module('valdr')); beforeEach(inject(function (valdrMaxValidator) { maxValidator = valdrMaxValidator; constraint = { value: '10' }; })); it('should provide the correct name', function () { expect(maxValidator.name).toBe('max'); }); it('should return true for empty values', function () { expect(maxValidator.validate('', constraint)).toBe(true); expect(maxValidator.validate(null, constraint)).toBe(true); expect(maxValidator.validate(undefined, constraint)).toBe(true); }); it('should return false for NaN', function () { expect(maxValidator.validate(NaN, constraint)).toBe(false); }); it('should return true for valid numbers', function () { expect(maxValidator.validate('10', constraint)).toBe(true); expect(maxValidator.validate(10, constraint)).toBe(true); expect(maxValidator.validate('9', constraint)).toBe(true); expect(maxValidator.validate(9, constraint)).toBe(true); expect(maxValidator.validate('-9', constraint)).toBe(true); expect(maxValidator.validate(-9, constraint)).toBe(true); expect(maxValidator.validate('9.9999999', constraint)).toBe(true); }); it('should return false for invalid numbers and strings', function () { expect(maxValidator.validate(10.001, constraint)).toBe(false); expect(maxValidator.validate('10.000001', constraint)).toBe(false); expect(maxValidator.validate('11', constraint)).toBe(false); expect(maxValidator.validate(11, constraint)).toBe(false); expect(maxValidator.validate('string', constraint)).toBe(false); expect(maxValidator.validate('number', constraint)).toBe(false); }); }); ================================================ FILE: src/core/validators/minLengthValidator.js ================================================ angular.module('valdr') .factory('valdrMinLengthValidator', ['valdrUtil', function (valdrUtil) { return { name: 'minLength', /** * Checks if the value is a string and if it's at least 'constraint.number' of characters long. * * @param value the value to validate * @param constraint with property 'number' * @returns {boolean} true if valid */ validate: function (value, constraint) { var minLength = constraint.number; if (valdrUtil.isEmpty(value)) { return true; } if (typeof value === 'string') { return value.length >= minLength; } else { return false; } } }; }]); ================================================ FILE: src/core/validators/minLengthValidator.spec.js ================================================ describe('valdrMinLengthValidator', function () { var minLengthValidator, constraint = { number: 5, message: 'message' }; beforeEach(module('valdr')); beforeEach(inject(function (valdrMinLengthValidator) { minLengthValidator = valdrMinLengthValidator; })); it('should provide the correct name', function () { expect(minLengthValidator.name).toBe('minLength'); }); it('should return true if value is valid', function () { // given var value = 'valid-value'; // when var valid = minLengthValidator.validate(value, constraint); // then expect(valid).toBe(true); }); it('should return the validation result', function () { // given var value = 'a'; // when var valid = minLengthValidator.validate(value, constraint); // then expect(valid).toBe(false); }); it('should be valid if minLength is 0 and value undefined', function () { // given constraint.number = 0; // when var valid = minLengthValidator.validate(undefined, constraint); // then expect(valid).toBe(true); }); it('should be invalid if value is number', function () { // given constraint.number = 4; // when var valid = minLengthValidator.validate(123, constraint); // then expect(valid).toBe(false); }); it('should be valid if minLength is 0 and value null', function () { // given constraint.number = 0; // when var valid = minLengthValidator.validate(null, constraint); // then expect(valid).toBe(true); }); it('should be valid if minLength is 1 and value undefined', function () { // given constraint.number = 1; // when var valid = minLengthValidator.validate(undefined, constraint); // then expect(valid).toBe(true); }); it('should be valid if minLength is 1 and value null', function () { // given constraint.number = 1; // when var valid = minLengthValidator.validate(null, constraint); // then expect(valid).toBe(true); }); }); ================================================ FILE: src/core/validators/minValidator.js ================================================ angular.module('valdr') .factory('valdrMinValidator', ['valdrUtil', function (valdrUtil) { return { name: 'min', /** * Checks if the value is a number and higher or equal as the value specified in the constraint. * * @param value the value to validate * @param constraint the validation constraint * @returns {boolean} true if valid */ validate: function (value, constraint) { var minValue = Number(constraint.value), valueAsNumber = Number(value); if (valdrUtil.isNaN(value)) { return false; } return valdrUtil.isEmpty(value) || valueAsNumber >= minValue; } }; }]); ================================================ FILE: src/core/validators/minValidator.spec.js ================================================ describe('valdrMinValidator', function () { var minValidator, constraint; beforeEach(module('valdr')); beforeEach(inject(function (valdrMinValidator) { minValidator = valdrMinValidator; constraint = { value: '10' }; })); it('should provide the correct name', function () { expect(minValidator.name).toBe('min'); }); it('should return true for empty values', function () { expect(minValidator.validate('', constraint)).toBe(true); expect(minValidator.validate(null, constraint)).toBe(true); expect(minValidator.validate(undefined, constraint)).toBe(true); }); it('should return false for NaN', function () { expect(minValidator.validate(NaN, constraint)).toBe(false); }); it('should return true for valid numbers', function () { expect(minValidator.validate('10', constraint)).toBe(true); expect(minValidator.validate(10, constraint)).toBe(true); expect(minValidator.validate(10.001, constraint)).toBe(true); expect(minValidator.validate('10.000001', constraint)).toBe(true); expect(minValidator.validate('11', constraint)).toBe(true); expect(minValidator.validate(11, constraint)).toBe(true); }); it('should return false for invalid numbers and strings', function () { expect(minValidator.validate('9', constraint)).toBe(false); expect(minValidator.validate(9, constraint)).toBe(false); expect(minValidator.validate('-9', constraint)).toBe(false); expect(minValidator.validate(-9, constraint)).toBe(false); expect(minValidator.validate('9.9999999', constraint)).toBe(false); expect(minValidator.validate('string', constraint)).toBe(false); expect(minValidator.validate('number', constraint)).toBe(false); expect(minValidator.validate(' ', constraint)).toBe(false); }); }); ================================================ FILE: src/core/validators/pastValidator.js ================================================ angular.module('valdr') .factory('valdrPastValidator', ['futureAndPastSharedValidator', function (futureAndPastSharedValidator) { return { name: 'past', /** * Checks if the value is a date in the past. * * @param value the value to validate * @returns {boolean} true if empty, null, undefined or a date in the past, false otherwise */ validate: function (value) { return futureAndPastSharedValidator.validate(value, function (valueAsMoment, now) { return valueAsMoment.isBefore(now); }); } }; }]); ================================================ FILE: src/core/validators/pastValidator.spec.js ================================================ describe('valdrPastValidator', function () { var pastValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrPastValidator) { pastValidator = valdrPastValidator; })); it('should be named "past"', function () { expect(pastValidator.name).toBe('past'); }); it('should return true for empty values', function () { expect(pastValidator.validate('')).toBe(true); expect(pastValidator.validate(null)).toBe(true); expect(pastValidator.validate(undefined)).toBe(true); }); it('should return false for NaN', function () { expect(pastValidator.validate(NaN)).toBe(false); }); it('should return false for non-dates', function () { expect(pastValidator.validate(' ')).toBe(false); expect(pastValidator.validate('foo')).toBe(false); expect(pastValidator.validate('_1.1.2014')).toBe(false); expect(pastValidator.validate('31.2.2014')).toBe(false); expect(pastValidator.validate('31:1:2014')).toBe(false); }); it('should return false for dates in the future', function () { expect(pastValidator.validate('1.1.2900')).toBe(false); expect(pastValidator.validate('2030/12/31')).toBe(false); expect(pastValidator.validate('2030-12-31')).toBe(false); expect(pastValidator.validate(moment().add(10, 'seconds'))).toBe(false); }); it('should return true for dates in the past', function () { expect(pastValidator.validate('1.1.1900')).toBe(true); expect(pastValidator.validate('01.01.1900')).toBe(true); expect(pastValidator.validate('1. 1. 1900')).toBe(true); expect(pastValidator.validate('01. 01. 1900')).toBe(true); expect(pastValidator.validate('1-1-1900')).toBe(true); expect(pastValidator.validate('01-01-1900')).toBe(true); expect(pastValidator.validate('1/1/1900')).toBe(true); expect(pastValidator.validate('01/01/1900')).toBe(true); expect(pastValidator.validate('1900.1.1')).toBe(true); expect(pastValidator.validate('1900.01.01')).toBe(true); expect(pastValidator.validate('2000/12/31')).toBe(true); expect(pastValidator.validate('2000-12-31')).toBe(true); expect(pastValidator.validate(moment().subtract(10, 'seconds'))).toBe(true); }); }); ================================================ FILE: src/core/validators/patternValidator.js ================================================ angular.module('valdr') .factory('valdrPatternValidator', ['valdrUtil', function (valdrUtil) { var REGEXP_PATTERN = /^\/(.*)\/([gim]*)$/; /** * Converts the given pattern to a RegExp. * The pattern can either be a RegExp object or a string containing a regular expression (`/regexp/`). * This implementation is based on the AngularJS ngPattern validator. * @param pattern the pattern * @returns {RegExp} the RegExp */ var asRegExp = function (pattern) { var match; if (pattern.test) { return pattern; } else { match = pattern.match(REGEXP_PATTERN); if (match) { return new RegExp(match[1], match[2]); } else { throw ('Expected ' + pattern + ' to be a RegExp'); } } }; return { name: 'pattern', /** * Checks if the value matches the pattern defined in the constraint. * * @param value the value to validate * @param constraint the constraint with the regexp as value * @returns {boolean} true if valid */ validate: function (value, constraint) { var pattern = asRegExp(constraint.value); return valdrUtil.isEmpty(value) || pattern.test(value); } }; }]); ================================================ FILE: src/core/validators/patternValidator.spec.js ================================================ describe('valdrPatternValidator', function () { var patternValidator, constraint = { value: '/^[a-z]+$/' }; beforeEach(module('valdr')); beforeEach(inject(function (valdrPatternValidator) { patternValidator = valdrPatternValidator; })); it('should provide the correct name', function () { expect(patternValidator.name).toBe('pattern'); }); it('should return true for empty values', function () { expect(patternValidator.validate('', constraint)).toBe(true); expect(patternValidator.validate(undefined, constraint)).toBe(true); }); it('should return true for matching values', function () { expect(patternValidator.validate('asd', constraint)).toBe(true); }); it('should return false for not matching values', function () { expect(patternValidator.validate('123', constraint)).toBe(false); }); it('should work with RegExp object in constraint', function () { constraint.value = /^[a-z]+$/; expect(patternValidator.validate('asd', constraint)).toBe(true); expect(patternValidator.validate('123', constraint)).toBe(false); }); it('should throw when RegExp is invalid', function () { constraint.value = 'no RegExp'; expect(function () { patternValidator.validate('asd', constraint); }).toThrow(); }); }); ================================================ FILE: src/core/validators/requiredValidator.js ================================================ angular.module('valdr') .factory('valdrRequiredValidator', ['valdrUtil', function (valdrUtil) { return { name: 'required', /** * Checks if the value is not empty. * * @param value the value to validate * @returns {boolean} true if the value is not empty */ validate: function (value) { return valdrUtil.notEmpty(value); } }; }]); ================================================ FILE: src/core/validators/requiredValidator.spec.js ================================================ describe('valdrRequiredValidator', function () { var requiredValidator, valdrUtil; beforeEach(module('valdr')); beforeEach(inject(function (valdrRequiredValidator, _valdrUtil_) { requiredValidator = valdrRequiredValidator; valdrUtil = _valdrUtil_; })); it('should provide the correct name', function () { expect(requiredValidator.name).toBe('required'); }); it('should validate using the valdrUtil', function () { // given spyOn(valdrUtil, 'notEmpty'); var value = 'someValue'; // when requiredValidator.validate(value); // then expect(valdrUtil.notEmpty).toHaveBeenCalledWith(value); }); it('should return the validation result', function () { expect(requiredValidator.validate('value')).toBe(true); }); }); ================================================ FILE: src/core/validators/sizeValidator.js ================================================ angular.module('valdr') .factory('valdrSizeValidator', ['valdrUtil', function (valdrUtil) { return { name: 'size', /** * Checks if the values length is in the range specified by the constraints min and max properties. * * @param value the value to validate * @param constraint with optional values: min, max * @returns {boolean} true if valid */ validate: function (value, constraint) { var minLength = constraint.min || 0, maxLength = constraint.max; value = value || ''; if (valdrUtil.isEmpty(value)) { return true; } return value.length >= minLength && (maxLength === undefined || value.length <= maxLength); } }; }]); ================================================ FILE: src/core/validators/sizeValidator.spec.js ================================================ describe('valdrSizeValidator', function () { var sizeValidator, constraint = { min: 5, max: 20, message: 'message' }; beforeEach(module('valdr')); beforeEach(inject(function (valdrSizeValidator) { sizeValidator = valdrSizeValidator; })); it('should provide the correct name', function () { expect(sizeValidator.name).toBe('size'); }); it('should return true if value is valid', function () { // given var value = 'valid-value'; // when var valid = sizeValidator.validate(value, constraint); // then expect(valid).toBe(true); }); it('should return the validation result', function () { // given var value = 'a'; // when var valid = sizeValidator.validate(value, constraint); // then expect(valid).toBe(false); }); it('should be valid if min is 0 and value undefined', function () { // given constraint.min = 0; // when var valid = sizeValidator.validate(undefined, constraint); // then expect(valid).toBe(true); }); it('should be valid if min is 1 and value undefined', function () { // given constraint.min = 1; // when var valid = sizeValidator.validate(undefined, constraint); // then expect(valid).toBe(true); }); }); ================================================ FILE: src/core/validators/urlValidator.js ================================================ angular.module('valdr') .factory('valdrUrlValidator', ['valdrUtil', function (valdrUtil) { // the url pattern used in angular.js var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/; return { name: 'url', /** * Checks if the value is a valid url. * * @param value the value to validate * @returns {boolean} true if valid */ validate: function (value) { return valdrUtil.isEmpty(value) || URL_REGEXP.test(value); } }; }]); ================================================ FILE: src/core/validators/urlValidator.spec.js ================================================ describe('valdrUrlValidator', function () { var urlValidator; beforeEach(module('valdr')); beforeEach(inject(function (valdrUrlValidator) { urlValidator = valdrUrlValidator; })); it('should provide the correct name', function () { expect(urlValidator.name).toBe('url'); }); it('should return true for empty values', function () { expect(urlValidator.validate('')).toBe(true); expect(urlValidator.validate(undefined)).toBe(true); }); it('should return true for valid url', function () { // some trivial tests expect(urlValidator.validate('http://www.google.ch')).toBe(true); expect(urlValidator.validate('http://server:123/path')).toBe(true); // some tests from http://djpowell.net/atomrdf/0.1/files/uritest.xml expect(urlValidator.validate('http://a/b/c/d;p?q#s')).toBe(true); expect(urlValidator.validate('http://a/b/c/g?y#s')).toBe(true); expect(urlValidator.validate('http://a/b/c/g..')).toBe(true); expect(urlValidator.validate('http://a/b/c/d;p?y')).toBe(true); expect(urlValidator.validate('http://a/b/c/g;x=1/y')).toBe(true); }); it('should return false for invalid url', function () { expect(urlValidator.validate('a@B.c')).toBe(false); expect(urlValidator.validate('hanueli')).toBe(false); }); }); ================================================ FILE: src/message/valdrMessage-directive.js ================================================ /** * This directive appends a validation message to the parent element of any input, select or textarea element, which * is nested in a valdr-type directive and has an ng-model bound to it. * If the form element is wrapped in an element marked with the class defined in valdrClasses.formGroup, * the messages is appended to this element instead of the direct parent. * To prevent adding messages to specific input fields, the attribute 'valdr-no-message' can be added to those input * or select fields. The valdr-message directive is used to do the actual rendering of the violation messages. */ var valdrMessageDirectiveDefinitionFactory = function (restrict) { return ['$compile', function ($compile) { return { restrict: restrict, require: ['?^valdrType', '?^ngModel', '?^valdrFormGroup'], link: function (scope, element, attrs, controllers) { var valdrTypeController = controllers[0], ngModelController = controllers[1], valdrFormGroupController = controllers[2], valdrNoValidate = attrs.valdrNoValidate, valdrNoMessage = attrs.valdrNoMessage, fieldName = attrs.name; /** * Don't do anything if * - this is an that's not inside of a valdr-type or valdr-form-group block * - there is no ng-model bound to input * - there is a 'valdr-no-validate' or 'valdr-no-message' attribute present */ if (!valdrTypeController || !valdrFormGroupController || !ngModelController || angular.isDefined(valdrNoValidate) || angular.isDefined(valdrNoMessage)) { return; } var valdrMessageElement = angular.element(''); $compile(valdrMessageElement)(scope); valdrFormGroupController.addMessageElement(ngModelController, valdrMessageElement); scope.$on('$destroy', function () { valdrFormGroupController.removeMessageElement(ngModelController); }); } }; }]; }, valdrMessageElementDirectiveDefinition = valdrMessageDirectiveDefinitionFactory('E'), valdrMessageAttributeDirectiveDefinition = valdrMessageDirectiveDefinitionFactory('A'); var nullValdrType = { getType: angular.noop }; angular.module('valdr') .directive('input', valdrMessageElementDirectiveDefinition) .directive('select', valdrMessageElementDirectiveDefinition) .directive('textarea', valdrMessageElementDirectiveDefinition) .directive('enableValdrMessage', valdrMessageAttributeDirectiveDefinition) /** * The valdr-message directive is responsible for the rendering of violation messages. The template used for rendering * is defined in the valdrMessage service where it can be overridden or a template URL can be configured. */ .directive('valdrMessage', ['$rootScope', '$injector', 'valdrMessage', 'valdrUtil', function ($rootScope, $injector, valdrMessage, valdrUtil) { return { replace: true, restrict: 'A', scope: { formFieldName: '@valdrMessage' }, templateUrl: function () { return valdrMessage.templateUrl; }, require: ['^form', '?^valdrType'], link: function (scope, element, attrs, controllers) { var formController = controllers[0], valdrTypeController = controllers[1] || nullValdrType; var updateTranslations = function () { if (valdrMessage.translateAvailable && angular.isArray(scope.violations)) { angular.forEach(scope.violations, function (violation) { valdrMessage.$translate(valdrMessage.fieldNameKeyGenerator(violation)) .then(function (translation) { violation.fieldName = translation; }) .catch(angular.noop); }); } }; var createViolation = function (validatorName) { var typeName = valdrTypeController.getType(), fieldName = scope.formFieldName; return { type: typeName, field: fieldName, validator: validatorName, message: valdrMessage.getMessage(typeName, fieldName, validatorName) }; }; var addViolationsToScope = function () { scope.violations = []; angular.forEach(scope.formField.valdrViolations, function (violation) { scope.violations.push(violation); }); if (valdrMessage.angularMessagesEnabled) { angular.forEach(scope.formField.$error, function (isValid, validatorName) { if (!valdrUtil.startsWith(validatorName, 'valdr')) { scope.violations.push(createViolation(validatorName)); } }); } scope.violation = scope.violations[0]; updateTranslations(); }; var removeViolationsFromScope = function () { scope.violations = undefined; scope.violation = undefined; }; var watchFormFieldErrors = function () { scope.formField = formController[scope.formFieldName]; if (scope.formField) { return { valdr: scope.formField.valdrViolations, error: scope.formField.$error }; } }; scope.$watch(watchFormFieldErrors, function () { if (scope.formField && scope.formField.$invalid) { addViolationsToScope(); } else { removeViolationsFromScope(); } }, true); var unregisterTranslateChangeHandler = $rootScope.$on('$translateChangeSuccess', function () { updateTranslations(); }); scope.$on('$destroy', function () { unregisterTranslateChangeHandler(); }); } }; }]); ================================================ FILE: src/message/valdrMessage-directive.spec.js ================================================ describe('valdrMessage input directive', function () { var $scope, $compile; beforeEach(module('valdr')); var compileTemplate = function (template) { var element = $compile(angular.element(template))($scope); $scope.$digest(); return element; }; beforeEach(inject(function ($rootScope, _$compile_) { $compile = _$compile_; $scope = $rootScope.$new(); $scope.myObject = { field: 'fieldValue' }; })); it('should add a the valdr-message directive after the input field and bind it to the correct form field', function () { // given var element = compileTemplate( '
' + '
' + '' + '
' + '
' ); // when var nextElement = element.find('input').next()[0]; //then expect(nextElement.attributes['valdr-message']).toBeDefined(); expect(nextElement.attributes['valdr-message'].value).toBe('fieldName'); }); it('should add a the valdr-message directive to the form group if there is one', function () { // given var element = compileTemplate( '
' + '
' + '
' + '' + '
' + '
' + '
' ); // when var formGroupChildren = element.find('section').children(); var lastInFormGroup = formGroupChildren[formGroupChildren.length - 1]; //then expect(lastInFormGroup.attributes['valdr-message']).toBeDefined(); expect(lastInFormGroup.attributes['valdr-message'].value).toBe('fieldName'); }); it('should NOT add a the valdr-message after the input if valdr-no-message is set', function () { // given var element = compileTemplate( '
' + '
' + '' + '
' + '
' ); // when var nextElement = element.find('input').next(); //then expect(nextElement.length).toBe(0); }); it('should NOT add a the valdr-message after the input is not wrapped in a valdr-type', function () { // given var element = compileTemplate( '
' + '
' + '' + '
' + '
' ); // when var nextElement = element.find('input').next(); //then expect(nextElement.length).toBe(0); }); it('should NOT add valdr-message after the input if valdr-no-validate is set', function () { // given var element = compileTemplate( '
' + '
' + '' + '
' + '
' ); // when var nextElement = element.find('input').next(); //then expect(nextElement.length).toBe(0); }); }); describe('valdrMessage directive', function () { var $scope, $compile, valdrMessage; beforeEach(module('valdr')); var compileTemplate = function (template) { var html = template || '
' + '' + '' + '
'; var element = $compile(angular.element(html))($scope); $scope.$digest(); return element; }; var addViolations = function () { // manually add some violations for the tests (these are usually added if the field is invalid) $scope.testForm.testField.valdrViolations = [ { message: 'message-1' }, { message: 'message-2' } ]; $scope.testForm.testField.$error = { valdr: [] }; $scope.testForm.testField.$invalid = true; $scope.$digest(); }; var compileTemplateAndAddViolations = function (template) { var element = compileTemplate(template); addViolations(); return element; }; beforeEach(inject(function ($rootScope, _$compile_, _valdrMessage_) { $compile = _$compile_; $scope = $rootScope.$new(); valdrMessage = _valdrMessage_; })); it('should display the first message in the default template', function () { // when var element = compileTemplateAndAddViolations(); // then expect(element.find('div').html()).toBe('message-1'); }); it('should update the messages in the default template', function () { // given var element = compileTemplateAndAddViolations(); // when $scope.testForm.testField.valdrViolations[0].message = 'updatedMessage'; $scope.$digest(); // then expect(element.find('div').html()).toBe('updatedMessage'); }); it('should support custom templates', function () { // given valdrMessage.setTemplate('
Number of violations: {{ violations.length }}
'); // when var element = compileTemplateAndAddViolations(); // then expect(element.find('div').html()).toBe('Number of violations: 2'); }); it('should support dynamically added form items', function () { // given $scope.isVisible = false; var element = compileTemplate( '
' + '' + '' + '
'); // when $scope.isVisible = true; $scope.$digest(); addViolations(); // then expect(element.find('div').html()).toBe('message-1'); }); it('should show messages for angular validators', function () { // given valdrMessage.angularMessagesEnabled = true; valdrMessage.addMessages({ 'required': 'This field is required' }); var template = '
' + '' + '' + '
'; // when var element = $compile(angular.element(template))($scope); $scope.$digest(); // then expect(element.find('div').html()).toBe('This field is required'); }); it('should not show messages for angular validators by default', function () { // given valdrMessage.addMessages({ 'required': 'This field is required' }); var template = '
' + '' + '' + '
'; // when var element = $compile(angular.element(template))($scope); $scope.$digest(); // then expect(element.find('div').html()).toBe(''); }); }); describe('valdrMessage directive with angular-translate', function () { var $scope, $compile, $translate, valdrMessage; beforeEach(function () { module('valdr'); module('pascalprecht.translate'); module(function ($translateProvider) { $translateProvider.translations('en', { 'message-1': '{{fieldName}} english.', 'message-2': 'field: {{fieldName}} param: {{param}} secondParam: {{secondParam}}', 'Person.testField': 'Field Name' }); $translateProvider.translations('de', { 'message-1': '{{fieldName}} deutsch.', 'message-2': 'field: {{fieldName}} param: {{param}} secondParam: {{secondParam}}', 'Person.testField': 'Feldname' }); $translateProvider.preferredLanguage('en'); }); }); var compileTemplate = function () { var html = '
' + '' + '' + '
'; var element = $compile(angular.element(html))($scope); $scope.testForm.testField.valdrViolations = [ { message: 'message-1', field: 'testField', type: 'Person', param: '2' }, { message: 'message-2', field: 'testField', type: 'Person', param: '3' } ]; $scope.testForm.testField.$error = { valdr: [] }; $scope.testForm.testField.$invalid = true; $scope.$digest(); return element; }; beforeEach(inject(function ($rootScope, _$compile_, _valdrMessage_, _$translate_) { $compile = _$compile_; $scope = $rootScope.$new(); $translate = _$translate_; valdrMessage = _valdrMessage_; })); it('should translate the field name', function () { // given valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); // when var element = compileTemplate(); // then expect(element.find('div').html()).toBe('Field Name'); }); it('should translate the field name to german', function () { // given $translate.use('de'); valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); // when var element = compileTemplate(); // then expect(element.find('div').html()).toBe('Feldname'); }); it('should update field names on language switch at runtime', function () { // given valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); var element = compileTemplate(); expect(element.find('div').html()).toBe('Field Name'); // when $translate.use('de'); $scope.$digest(); // then expect(element.find('div').html()).toBe('Feldname'); }); it('should allow to use parameters in the translated messages', function () { // given / when var element = compileTemplate(); $scope.testForm.testField.valdrViolations = [ // note: message-2 has parameters defined in the translation tables { message: 'message-2', field: 'testField', type: 'Person', param: '3', secondParam: '4' } ]; $scope.$digest(); // then expect(element.find('span').html()).toBe('field: Field Name param: 3 secondParam: 4'); }); it('should unregister translate change handler on destroy to avoid potential memory leaks', function () { // given var element = compileTemplate(); spyOn(valdrMessage, '$translate').andReturn({ then: function () {} }); $scope.$destroy(); element.remove(); // when $translate.use('de'); $scope.$digest(); // then expect(valdrMessage.$translate).not.toHaveBeenCalled(); }); }); describe('valdrMessage directive with angular-translate and valdrFieldNameKeyGenerator', function () { var $scope, $compile, $translate, valdrMessage; var fieldNameKeyGenerator = function(violation) { return violation.type + '.' + violation.field + '.' + violation.validator + 'Name'; }; beforeEach(function () { module('valdr'); module('pascalprecht.translate'); module(function ($translateProvider, $provide) { $provide.factory('valdrFieldNameKeyGenerator', function() { return fieldNameKeyGenerator; }); $translateProvider.translations('en', { 'message-1': '{{fieldName}} english.', 'message-2': 'field: {{fieldName}} param: {{param}} secondParam: {{secondParam}}', 'Person.testField': 'Field Name', 'Person.testField.requiredName': 'The Field Name' }); $translateProvider.translations('de', { 'message-1': '{{fieldName}} deutsch.', 'message-2': 'field: {{fieldName}} param: {{param}} secondParam: {{secondParam}}', 'Person.testField': 'Feldname', 'Person.testField.requiredName': 'Der Feldname' }); $translateProvider.preferredLanguage('en'); }); }); var compileTemplate = function () { var html = '
' + '' + '' + '
'; var element = $compile(angular.element(html))($scope); $scope.testForm.testField.valdrViolations = [ { message: 'message-1', field: 'testField', type: 'Person', param: '2', validator: 'required' }, { message: 'message-2', field: 'testField', type: 'Person', param: '3', validator: 'required' } ]; $scope.testForm.testField.$error = { valdr: [] }; $scope.testForm.testField.$invalid = true; $scope.$digest(); return element; }; beforeEach(inject(function ($rootScope, _$compile_, _valdrMessage_, _$translate_) { $compile = _$compile_; $scope = $rootScope.$new(); $translate = _$translate_; valdrMessage = _valdrMessage_; })); it('should translate the field name using the fieldNameKeyGenerator', function () { // given valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); // when var element = compileTemplate(); // then expect(element.find('div').html()).toBe('The Field Name'); }); it('should translate the field name to german using the fieldNameKeyGenerator', function () { // given $translate.use('de'); valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); // when var element = compileTemplate(); // then expect(element.find('div').html()).toBe('Der Feldname'); }); it('should update field names on language switch at runtime', function () { // given valdrMessage.setTemplate('
{{ violations[0].fieldName }}
'); var element = compileTemplate(); expect(element.find('div').html()).toBe('The Field Name'); // when $translate.use('de'); $scope.$digest(); // then expect(element.find('div').html()).toBe('Der Feldname'); }); it('should allow to use parameters in the translated messages', function () { // given / when var element = compileTemplate(); $scope.testForm.testField.valdrViolations = [ // note: message-2 has parameters defined in the translation tables { message: 'message-2', field: 'testField', type: 'Person', param: '3', secondParam: '4', validator: 'required' } ]; $scope.$digest(); // then expect(element.find('span').html()).toBe('field: The Field Name param: 3 secondParam: 4'); }); }); ================================================ FILE: src/message/valdrMessage-service.js ================================================ angular.module('valdr') /** * This service provides shared configuration between all valdr-message directive instances like the configured * template to render the violation messages and whether or not angular-translate is available. */ .provider('valdrMessage', function () { var userDefinedTemplateUrl, userDefinedTemplate, messages = {}, defaultTemplateUrl = 'valdr/default-message.html', defaultTemplate = '
' + '{{ violation.message }}' + '
', translateTemplate = '
' + '' + '
'; this.setTemplate = function (template) { userDefinedTemplate = template; }; this.setTemplateUrl = function (templateUrl) { userDefinedTemplateUrl = templateUrl; }; this.addMessages = function (newMessages) { angular.extend(messages, newMessages); }; var addMessages = this.addMessages; this.getMessage = function (typeName, fieldName, validatorName) { var fullMessageKey = typeName + '.' + fieldName + '.' + validatorName; return messages[fullMessageKey] || messages[validatorName] || '[' + validatorName + ']'; }; var getMessage = this.getMessage; this.$get = ['$templateCache', '$injector', function ($templateCache, $injector) { var angularMessagesEnabled = false; function getTranslateService() { try { return $injector.get('$translate'); } catch (error) { return undefined; } } function getFieldNameKeyGenerator() { try { return $injector.get('valdrFieldNameKeyGenerator'); } catch (error) { return function(violation) { return violation.type + '.' + violation.field; }; } } var $translate = getTranslateService(), translateAvailable = angular.isDefined($translate), fieldNameKeyGenerator = getFieldNameKeyGenerator(); function determineTemplate() { if (angular.isDefined(userDefinedTemplate)) { return userDefinedTemplate; } else if (translateAvailable) { return translateTemplate; } else { return defaultTemplate; } } function updateTemplateCache() { $templateCache.put(defaultTemplateUrl, determineTemplate()); if (userDefinedTemplateUrl && userDefinedTemplate) { $templateCache.put(userDefinedTemplateUrl, userDefinedTemplate); } } updateTemplateCache(); return { templateUrl: userDefinedTemplateUrl || defaultTemplateUrl, setTemplate: function (newTemplate) { userDefinedTemplate = newTemplate; updateTemplateCache(); }, translateAvailable: translateAvailable, $translate: $translate, fieldNameKeyGenerator: fieldNameKeyGenerator, addMessages: addMessages, getMessage: getMessage, angularMessagesEnabled: angularMessagesEnabled }; }]; }); ================================================ FILE: src/message/valdrMessage-service.spec.js ================================================ describe('valdrMessage service', function () { var valdrMessage; beforeEach(module('valdr')); beforeEach(inject(function (_valdrMessage_) { valdrMessage = _valdrMessage_; })); it('should provide the default templateUrl', function () { expect(valdrMessage.templateUrl).toBe('valdr/default-message.html'); }); it('should detect that $translate is not available', function () { expect(valdrMessage.translateAvailable).toBe(false); }); it('should provide undefined for $translate', function () { expect(valdrMessage.$translate).toBeUndefined(); }); it('should populate the $templateCache with the default template', function () { inject(function ($templateCache) { // given var templateUrl = valdrMessage.templateUrl; // when var template = $templateCache.get(templateUrl); // then expect(template).toBeDefined(); expect(template).toBe('
{{ violation.message }}
'); }); }); }); describe('valdrMessage service with angular-translate', function () { var valdrMessage; beforeEach(module('valdr')); beforeEach(module('pascalprecht.translate')); beforeEach(inject(function (_valdrMessage_) { valdrMessage = _valdrMessage_; })); it('should provide the default templateUrl', function () { expect(valdrMessage.templateUrl).toBe('valdr/default-message.html'); }); it('should detect that $translate is available', function () { expect(valdrMessage.translateAvailable).toBe(true); }); it('should provide $translate service', function () { expect(valdrMessage.$translate).toBeDefined(); expect(typeof valdrMessage.$translate).toBe('function'); }); it('should populate the $templateCache with the angular-translate template', function () { inject(function ($templateCache) { // given var templateUrl = valdrMessage.templateUrl; // when var template = $templateCache.get(templateUrl); // then expect(template).toBeDefined(); expect(template).toBe('
' + '' + '
'); }); }); }); describe('valdrMessageProvider', function () { it('should provide the custom template URL and NOT populate $templateCache', function () { // given var customTemplateUrl = 'custom/template.html'; module('valdr'); // when module(function (valdrMessageProvider) { valdrMessageProvider.setTemplateUrl(customTemplateUrl); }); // then inject(function (valdrMessage, $templateCache) { expect(valdrMessage.templateUrl).toBe(customTemplateUrl); expect($templateCache.get(valdrMessage.templateUrl)).toBeUndefined(); }); }); it('should populate $templateCache with the custom template', function () { // given var customTemplate = '
my template
'; module('valdr'); // when module(function (valdrMessageProvider) { valdrMessageProvider.setTemplate(customTemplate); }); // then inject(function (valdrMessage, $templateCache) { var template = $templateCache.get(valdrMessage.templateUrl); expect(template).toBe(customTemplate); }); }); it('should populate $templateCache with the custom template and custom URL', function () { // given var customTemplate = '
my template
'; var customTemplateUrl = 'custom/my-template.html'; module('valdr'); // when module(function (valdrMessageProvider) { valdrMessageProvider.setTemplateUrl(customTemplateUrl); valdrMessageProvider.setTemplate(customTemplate); }); // then inject(function (valdrMessage, $templateCache) { expect(valdrMessage.templateUrl).toBe(customTemplateUrl); var template = $templateCache.get(valdrMessage.templateUrl); expect(template).toBe(customTemplate); }); }); it('should allow to add and get messages', function () { // given module('valdr'); // when module(function (valdrMessageProvider) { valdrMessageProvider.addMessages({ 'validator': 'validator-global-error-message', 'Type.field.validator': 'fully-qualified-error-message' }); }); // then inject(function (valdrMessage) { expect(valdrMessage.getMessage('Type', 'field', 'validator')).toEqual('fully-qualified-error-message'); expect(valdrMessage.getMessage(undefined, undefined, 'validator')).toEqual('validator-global-error-message'); expect(valdrMessage.getMessage(undefined, undefined, 'unknown')).toEqual('[unknown]'); }); }); }); ================================================ FILE: src/valdr.js ================================================ angular.module('valdr', ['ng']) .constant('valdrEvents', { 'revalidate': 'valdr-revalidate' }) .value('valdrConfig', { addFormGroupClass: true }) .value('valdrClasses', { formGroup: 'form-group', valid: 'ng-valid', invalid: 'ng-invalid', dirty: 'ng-dirty', pristine: 'ng-pristine', touched: 'ng-touched', untouched: 'ng-untouched', invalidDirtyTouchedGroup: 'valdr-invalid-dirty-touched-group' }); ================================================ FILE: src/valdr.spec.js ================================================ describe('valdr', function () { beforeEach(module('valdr')); it('should provide constant for validation events', inject(function (valdrEvents) { expect(valdrEvents).toBeDefined(); })); it('should provide constant for revalidate event', inject(function (valdrEvents) { expect(valdrEvents.revalidate).toBe('valdr-revalidate'); })); it('should provide a value for valdrClasses', inject(function (valdrClasses) { expect(valdrClasses.valid).toBe('ng-valid'); expect(valdrClasses.invalid).toBe('ng-invalid'); expect(valdrClasses.invalidDirtyTouchedGroup).toBe('valdr-invalid-dirty-touched-group'); })); });