diff --git a/misc/tutorial/322_validation.ngdoc b/misc/tutorial/322_validation.ngdoc new file mode 100644 index 0000000000..bab1d0161d --- /dev/null +++ b/misc/tutorial/322_validation.ngdoc @@ -0,0 +1,119 @@ +@ngdoc overview +@name Tutorial: 322 Validation +@description + + + +Feature ui.grid.validate allows validating cells after they are changed. To enable, you must include the +`ui.grid.validate` module and you must include the `ui-grid-validate` directive on your grid element. + +This feature depends on ui.grid.edit. + +Documentation for the validation feature is provided in the api documentation, in particular: + +- {@link api/ui.grid.validate.api:PublicApi publicApi} + +## Validators + +Validation is based on validation functions defined at service level (thus valid through all the application). + +Some custom validators come with the feature and are: + +- `required`: to ensure that a field is not empty. +- `minLength`: to ensure that the value inserted is at least X characters long. +- `maxLength`: to ensure that the value inserted is at most X characters long. + +To define a new validator you should use the {@link +api/ui.grid.validate.service:uiGridValidateService#methods_setValidator setValidator} method. + +To add a validator to a column you just need to add a `validators` property to its `colDef` +object, containing a property for each validator you want to add. The name of the property +will set the validator and the value of the property will be treated as an argument by the validator function. + +When a field does not pass validation it gets a `invalid` class so you can customize it via css. + +The feature adds 2 templates to ui-grid: + +- `cellTitleValidator` wich adds the error message to the title attribute of the cell. +- `cellTooltipValidator` wich depends on ui-bootstrap to add a tooltip. + +## External Factory + +In case you have an external service providing validators, you can add a function calling said service +by setting an external validator factory function via {@link +api/ui.grid.validate.service:uiGridValidateService#methods_setExternalFactoryFunction setExternalFactoryFunction}. + +Please be advised that external validators should accept the same parameters (or at least an ordered subset) as +our validators do (`newValue`, `oldValue`, `rowEntity`, `colDef`); + +@example + + + var app = angular.module('app', ['ngTouch', 'ui.grid', 'ui.grid.edit', 'ui.grid.cellNav', 'ui.grid.validate', 'addressFormatter']); + + angular.module('addressFormatter', []).filter('address', function () { + return function (input) { + return input.street + ', ' + input.city + ', ' + input.state + ', ' + input.zip; + }; + }); + + app.controller('MainCtrl', ['$scope', '$http', '$window', 'uiGridValidateService', function ($scope, $http, $window, uiGridValidateService) { + + uiGridValidateService.setValidator('startWith', + function(argument) { + return function(newValue, oldValue, rowEntity, colDef) { + if (!newValue) { + return true; // We should not test for existence here + } else { + return newValue.startsWith(argument); + } + }; + }, + function(argument) { + return 'You can only insert names starting with: "' + argument + '"'; + } + ); + + $scope.gridOptions = { enableCellEditOnFocus: true }; + + $scope.gridOptions.columnDefs = [ + { name: 'id', enableCellEdit: false, width: '10%' }, + { name: 'name', displayName: 'Name (editable)', width: '20%', + validators: {required: true, startWith: 'M'}, cellTemplate: 'ui-grid/cellTitleValidator' } + ]; + + + + $scope.msg = {}; + + $scope.gridOptions.onRegisterApi = function(gridApi){ + //set gridApi on scope + $scope.gridApi = gridApi; + gridApi.validate.on.validationFailed($scope,function(rowEntity, colDef, newValue, oldValue){ + $window.alert('rowEntity: '+ rowEntity + '\n' + + 'colDef: ' + colDef + '\n' + + 'newValue: ' + newValue + '\n' + + 'oldValue: ' + oldValue); + }); + }; + + $http.get('/data/500_complex.json') + .success(function(data) { + $scope.gridOptions.data = data; + }); + }]); + }); + + +
+
+
+
+ + .grid { + width: 600px; + height: 450px; + } + +
\ No newline at end of file diff --git a/src/features/validate/js/gridValidate.js b/src/features/validate/js/gridValidate.js new file mode 100644 index 0000000000..036d2d4e40 --- /dev/null +++ b/src/features/validate/js/gridValidate.js @@ -0,0 +1,579 @@ +(function () { + 'use strict'; + + /** + * @ngdoc overview + * @name ui.grid.validate + * @description + * + * # ui.grid.validate + * + * + * + * This module provides the ability to validate cells upon change. + * + * Design information: + * ------------------- + * + * Validation is not based on angularjs validation, since it would work only when editing the field. + * + * Instead it adds custom properties to any field considered as invalid. + * + *
+ *
+ * + *
+ */ + + var module = angular.module('ui.grid.validate', ['ui.grid']); + + + /** + * @ngdoc service + * @name ui.grid.validate.service:uiGridValidateService + * + * @description Services for validation features + */ + module.service('uiGridValidateService', ['$sce', '$q', '$http', 'i18nService', 'uiGridConstants', function ($sce, $q, $http, i18nService, uiGridConstants) { + + var service = { + + /** + * @ngdoc object + * @name validatorFactories + * @propertyOf ui.grid.validate.service:uiGridValidateService + * @description object containing all the factories used to validate data.
+ * These factories will be in the form
+ * ``` + * { + * validatorFactory: function(argument) { + * return function(newValue, oldValue, rowEntity, colDef) { + * return true || false || promise + * } + * }, + * messageFunction: function(argument) { + * return string + * } + * } + * ``` + * + * Promises should return true or false as result according to the result of validation. + */ + validatorFactories: {}, + + + /** + * @ngdoc service + * @name setExternalFactoryFunction + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Adds a way to retrieve validators from an external service + *

Validators from this external service have a higher priority than default + * ones + * @param {function} externalFactoryFunction a function that accepts name and argument to pass to a + * validator factory and that returns an object with the same properties as + * you can see in {@link ui.grid.validate.service:uiGridValidateService#properties_validatorFactories validatorFactories} + */ + setExternalFactoryFunction: function(externalFactoryFunction) { + service.externalFactoryFunction = externalFactoryFunction; + }, + + /** + * @ngdoc service + * @name clearExternalFactory + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Removes any link to external factory from this service + */ + clearExternalFactory: function() { + delete service.externalFactoryFunction; + }, + + /** + * @ngdoc service + * @name getValidatorFromExternalFactory + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Retrieves a validator by executing a validatorFactory + * stored in an external service. + * @param {string} name the name of the validator to retrieve + * @param {object} argument an argument to pass to the validator factory + */ + getValidatorFromExternalFactory: function(name, argument) { + return service.externalFactoryFunction(name, argument).validatorFactory(argument); + }, + + /** + * @ngdoc service + * @name getMessageFromExternalFactory + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Retrieves a message stored in an external service. + * @param {string} name the name of the validator + * @param {object} argument an argument to pass to the message function + */ + getMessageFromExternalFactory: function(name, argument) { + return service.externalFactoryFunction(name, argument).messageFunction(argument); + }, + + /** + * @ngdoc service + * @name setValidator + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Adds a new validator to the service + * @param {string} name the name of the validator, must be unique + * @param {function} validatorFactory a factory that return a validatorFunction + * @param {function} messageFunction a function that return the error message + */ + setValidator: function(name, validatorFactory, messageFunction) { + service.validatorFactories[name] = { + validatorFactory: validatorFactory, + messageFunction: messageFunction + }; + }, + + /** + * @ngdoc service + * @name getValidator + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Returns a validator registered to the service + * or retrieved from the external factory + * @param {string} name the name of the validator to retrieve + * @param {object} argument an argument to pass to the validator factory + * @returns {object} the validator function + */ + getValidator: function(name, argument) { + if (service.externalFactoryFunction) { + var validator = service.getValidatorFromExternalFactory(name, argument); + if (validator) { + return validator; + } + } + if (!service.validatorFactories[name]) { + throw ("Invalid validator name: " + name); + } + return service.validatorFactories[name].validatorFactory(argument); + }, + + /** + * @ngdoc service + * @name getMessage + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Returns the error message related to the validator + * @param {string} name the name of the validator + * @param {object} argument an argument to pass to the message function + * @returns {string} the error message related to the validator + */ + getMessage: function(name, argument) { + if (service.externalFactoryFunction) { + var message = service.getMessageFromExternalFactory(name, argument); + if (message) { + return message; + } + } + return service.validatorFactories[name].messageFunction(argument); + }, + + /** + * @ngdoc service + * @name isInvalid + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Returns true if the cell (identified by rowEntity, colDef) is invalid + * @param {object} rowEntity the row entity of the cell + * @param {object} colDef the colDef of the cell + * @returns {boolean} true if the cell is invalid + */ + isInvalid: function (rowEntity, colDef) { + return rowEntity['$$invalid'+colDef.name]; + }, + + /** + * @ngdoc service + * @name setInvalid + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Makes the cell invalid by adding the proper field to the entity + * @param {object} rowEntity the row entity of the cell + * @param {object} colDef the colDef of the cell + */ + setInvalid: function (rowEntity, colDef) { + rowEntity['$$invalid'+colDef.name] = true; + }, + + /** + * @ngdoc service + * @name setValid + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Makes the cell valid by removing the proper error field from the entity + * @param {object} rowEntity the row entity of the cell + * @param {object} colDef the colDef of the cell + */ + setValid: function (rowEntity, colDef) { + delete rowEntity['$$invalid'+colDef.name]; + }, + + /** + * @ngdoc service + * @name setError + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Adds the proper error to the entity errors field + * @param {object} rowEntity the row entity of the cell + * @param {object} colDef the colDef of the cell + * @param {string} validatorName the name of the validator that is failing + */ + setError: function(rowEntity, colDef, validatorName) { + if (!rowEntity['$$errors'+colDef.name]) { + rowEntity['$$errors'+colDef.name] = {}; + } + rowEntity['$$errors'+colDef.name][validatorName] = true; + }, + + /** + * @ngdoc service + * @name clearError + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Removes the proper error from the entity errors field + * @param {object} rowEntity the row entity of the cell + * @param {object} colDef the colDef of the cell + * @param {string} validatorName the name of the validator that is failing + */ + clearError: function(rowEntity, colDef, validatorName) { + if (!rowEntity['$$errors'+colDef.name]) { + return; + } + if (validatorName in rowEntity['$$errors'+colDef.name]) { + delete rowEntity['$$errors'+colDef.name][validatorName]; + } + }, + + /** + * @ngdoc function + * @name getErrorMessages + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description returns an array of i18n-ed error messages. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {array} An array of strings containing all the error messages for the cell + */ + getErrorMessages: function(rowEntity, colDef) { + var errors = []; + + if (!rowEntity['$$errors'+colDef.name] || Object.keys(rowEntity['$$errors'+colDef.name]).length === 0) { + return errors; + } + + Object.keys(rowEntity['$$errors'+colDef.name]).sort().forEach(function(validatorName) { + errors.push(service.getMessage(validatorName, colDef.validators[validatorName])); + }); + + return errors; + }, + + /** + * @ngdoc function + * @name getFormattedErrors + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description returns the error i18n-ed and formatted in html to be shown inside the page. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {object} An object that can be used in a template (like a cellTemplate) to display the + * message inside the page (i.e. inside a div) + */ + getFormattedErrors: function(rowEntity, colDef) { + + var msgString = ""; + + var errors = service.getErrorMessages(rowEntity, colDef); + + if (!errors.length) { + return; + } + + errors.forEach(function(errorMsg) { + msgString += errorMsg + "
"; + }); + + return $sce.trustAsHtml('

' + i18nService.getSafeText('validate.error') + '

' + msgString ); + }, + + /** + * @ngdoc function + * @name getTitleFormattedErrors + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html + * title attribute. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {object} An object that can be used in a template (like a cellTemplate) to display the + * message inside an html title attribute + */ + getTitleFormattedErrors: function(rowEntity, colDef) { + + var newLine = "\n"; + + var msgString = ""; + + var errors = service.getErrorMessages(rowEntity, colDef); + + if (!errors.length) { + return; + } + + errors.forEach(function(errorMsg) { + msgString += errorMsg + newLine; + }); + + return $sce.trustAsHtml(i18nService.getSafeText('validate.error') + newLine + msgString); + }, + + /** + * @ngdoc function + * @name getTitleFormattedErrors + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description Executes all validators on a cell (identified by row entity and column definition) and sets or clears errors + * @param {object} rowEntity the row entity of the cell we want to run the validators on + * @param {object} colDef the column definition of the cell we want to run the validators on + * @param {object} newValue the value the user just entered + * @param {object} oldValue the value the field had before + */ + runValidators: function(rowEntity, colDef, newValue, oldValue, grid) { + + if (newValue === oldValue) { + // If the value has not changed we perform no validation + return; + } + + if (typeof(colDef.name) === 'undefined' || !colDef.name) { + throw new Error('colDef.name is required to perform validation'); + } + + service.setValid(rowEntity, colDef); + + var validateClosureFactory = function(rowEntity, colDef, validatorName) { + return function(value) { + if (!value) { + service.setInvalid(rowEntity, colDef); + service.setError(rowEntity, colDef, validatorName); + if (grid) { + grid.api.validate.raise.validationFailed(rowEntity, colDef, newValue, oldValue); + } + } + }; + }; + + for (var validatorName in colDef.validators) { + service.clearError(rowEntity, colDef, validatorName); + var msg; + var validatorFunction = service.getValidator(validatorName, colDef.validators[validatorName]); + // We pass the arguments as oldValue, newValue so they are in the same order + // as ng-model validators (modelValue, viewValue) + $q.when(validatorFunction(oldValue, newValue, rowEntity, colDef)) + .then(validateClosureFactory(rowEntity, colDef, validatorName) + ); + } + }, + + /** + * @ngdoc function + * @name createDefaultValidators + * @methodOf ui.grid.validate.service:uiGridValidateService + * @description adds the basic validators to the list of service validators + */ + createDefaultValidators: function() { + service.setValidator('minLength', + function (argument) { + return function (oldValue, newValue, rowEntity, colDef) { + if (newValue === undefined || newValue === null || newValue === '') { + return true; + } + return newValue.length >= argument; + }; + }, + function(argument) { + return i18nService.getSafeText('validate.minLength').replace('THRESHOLD', argument); + }); + + service.setValidator('maxLength', + function (argument) { + return function (oldValue, newValue, rowEntity, colDef) { + if (newValue === undefined || newValue === null || newValue === '') { + return true; + } + return newValue.length <= argument; + }; + }, + function(threshold) { + return i18nService.getSafeText('validate.maxLength').replace('THRESHOLD', threshold); + }); + + service.setValidator('required', + function (argument) { + return function (oldValue, newValue, rowEntity, colDef) { + if (argument) { + return !(newValue === undefined || newValue === null || newValue === ''); + } + return true; + }; + }, + function(argument) { + return i18nService.getSafeText('validate.required'); + }); + }, + + initializeGrid: function (scope, grid) { + grid.validate = { + + isInvalid: service.isInvalid, + + getFormattedErrors: service.getFormattedErrors, + + getTitleFormattedErrors: service.getTitleFormattedErrors, + + runValidators: service.runValidators + }; + + /** + * @ngdoc object + * @name ui.grid.validate.api:PublicApi + * + * @description Public Api for validation feature + */ + var publicApi = { + events: { + validate: { + /** + * @ngdoc event + * @name validationFailed + * @eventOf ui.grid.validate.api:PublicApi + * @description raised when one or more failure happened during validation + *
+               *      gridApi.validate.on.validationFailed(scope, function(rowEntity, colDef, newValue, oldValue){...})
+               * 
+ * @param {object} rowEntity the options.data element whose validation failed + * @param {object} colDef the column whose validation failed + * @param {object} newValue new value + * @param {object} oldValue old value + */ + validationFailed: function (rowEntity, colDef, newValue, oldValue) { + } + } + }, + methods: { + validate: { + /** + * @ngdoc function + * @name isInvalid + * @methodOf ui.grid.validate.api:PublicApi + * @description checks if a cell (identified by rowEntity, colDef) is invalid + * @param {object} rowEntity gridOptions.data[] array instance we want to check + * @param {object} colDef the column whose errors we want to check + * @returns {boolean} true if the cell value is not valid + */ + isInvalid: function(rowEntity, colDef) { + return grid.validate.isInvalid(rowEntity, colDef); + }, + /** + * @ngdoc function + * @name getErrorMessages + * @methodOf ui.grid.validate.api:PublicApi + * @description returns an array of i18n-ed error messages. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {array} An array of strings containing all the error messages for the cell + */ + getErrorMessages: function (rowEntity, colDef) { + return grid.validate.getErrorMessages(rowEntity, colDef); + }, + /** + * @ngdoc function + * @name getFormattedErrors + * @methodOf ui.grid.validate.api:PublicApi + * @description returns the error i18n-ed and formatted in html to be shown inside the page. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {object} An object that can be used in a template (like a cellTemplate) to display the + * message inside the page (i.e. inside a div) + */ + getFormattedErrors: function (rowEntity, colDef) { + return grid.validate.getFormattedErrors(rowEntity, colDef); + }, + /** + * @ngdoc function + * @name getTitleFormattedErrors + * @methodOf ui.grid.validate.api:PublicApi + * @description returns the error i18n-ed and formatted in javaScript to be shown inside an html + * title attribute. + * @param {object} rowEntity gridOptions.data[] array instance whose errors we are looking for + * @param {object} colDef the column whose errors we are looking for + * @returns {object} An object that can be used in a template (like a cellTemplate) to display the + * message inside an html title attribute + */ + getTitleFormattedErrors: function (rowEntity, colDef) { + return grid.validate.getTitleFormattedErrors(rowEntity, colDef); + } + } + } + }; + + grid.api.registerEventsFromObject(publicApi.events); + grid.api.registerMethodsFromObject(publicApi.methods); + + if (grid.edit) { + grid.api.edit.on.afterCellEdit(scope, function(rowEntity, colDef, newValue, oldValue) { + grid.validate.runValidators(rowEntity, colDef, newValue, oldValue, grid); + }); + } + + service.createDefaultValidators(); + } + + }; + + return service; + }]); + + + /** + * @ngdoc directive + * @name ui.grid.validate.directive:uiGridValidate + * @element div + * @restrict A + * @description Adds validating features to the ui-grid directive. + * @example + + + var app = angular.module('app', ['ui.grid', 'ui.grid.edit', 'ui.grid.validate']); + + app.controller('MainCtrl', ['$scope', function ($scope) { + $scope.data = [ + { name: 'Bob', title: 'CEO' }, + { name: 'Frank', title: 'Lowly Developer' } + ]; + + $scope.columnDefs = [ + {name: 'name', enableCellEdit: true, validators: {minLength: 3, maxLength: 9}, cellTemplate: 'ui-grid/cellTitleValidator'}, + {name: 'title', enableCellEdit: true, validators: {required: true}, cellTemplate: 'ui-grid/cellTitleValidator'} + ]; + }]); + + +
+
+
+
+
+ */ + + module.directive('uiGridValidate', ['gridUtil', 'uiGridValidateService', function (gridUtil, uiGridValidateService) { + return { + priority: 0, + replace: true, + require: '^uiGrid', + scope: false, + compile: function () { + return { + pre: function ($scope, $elm, $attrs, uiGridCtrl) { + uiGridValidateService.initializeGrid($scope, uiGridCtrl.grid); + }, + post: function ($scope, $elm, $attrs, uiGridCtrl) { + } + }; + } + }; + }]); +})(); \ No newline at end of file diff --git a/src/features/validate/less/validate.less b/src/features/validate/less/validate.less new file mode 100644 index 0000000000..86646d7f67 --- /dev/null +++ b/src/features/validate/less/validate.less @@ -0,0 +1,5 @@ +@import '../../../less/variables'; + +div.ui-grid-cell-contents.invalid { + border: @invalidValueBorder; +} \ No newline at end of file diff --git a/src/features/validate/templates/cellTitleValidator.html b/src/features/validate/templates/cellTitleValidator.html new file mode 100644 index 0000000000..ff0cd35309 --- /dev/null +++ b/src/features/validate/templates/cellTitleValidator.html @@ -0,0 +1,5 @@ +
+ {{COL_FIELD CUSTOM_FILTERS}} +
\ No newline at end of file diff --git a/src/features/validate/templates/cellTooltipValidator.html b/src/features/validate/templates/cellTooltipValidator.html new file mode 100644 index 0000000000..5ad0fa6f65 --- /dev/null +++ b/src/features/validate/templates/cellTooltipValidator.html @@ -0,0 +1,9 @@ +
+ {{COL_FIELD CUSTOM_FILTERS}} +
\ No newline at end of file diff --git a/src/features/validate/test/uiGridValidateDirective.spec.js b/src/features/validate/test/uiGridValidateDirective.spec.js new file mode 100644 index 0000000000..053c8ef1ed --- /dev/null +++ b/src/features/validate/test/uiGridValidateDirective.spec.js @@ -0,0 +1,178 @@ +describe('uiGridValidateDirective', function () { + var scope; + var element; + var recompile; + var digest; + var uiGridConstants; + var $timeout; + + beforeEach(module('ui.grid.validate', 'ui.grid.edit')); + + beforeEach(inject(function ($rootScope, $compile, _uiGridConstants_, _$timeout_, $templateCache) { + + scope = $rootScope.$new(); + scope.options = {enableCellEdit: true}; + scope.options.data = [ + {col1: 'A1', col2: 'B1'}, + {col1: 'A2', col2: 'B2'} + ]; + + scope.options.columnDefs = [ + {field: 'col1', validators: {required: true}, + cellTemplate: 'ui-grid/cellTitleValidator'}, + {field: 'col2', validators: {minLength: 2}, + cellTemplate: 'ui-grid/cellTooltipValidator'} + ]; + + + recompile = function () { + $compile(element)(scope); + $rootScope.$digest(); + }; + + digest = function() { + $rootScope.$digest(); + }; + + uiGridConstants = _uiGridConstants_; + $timeout = _$timeout_; + + })); + + + it('should add a validate property to the grid', function () { + + element = angular.element('
'); + recompile(); + + var gridScope = element.scope().$$childHead; + + var validate = gridScope.grid.validate; + + expect(validate).toBeDefined(); + + }); + + it('should run validators on a edited cell', function () { + + element = angular.element('
'); + recompile(); + + var cells = element.find('.ui-grid-cell-contents.ng-scope'); + + for (var i = 0; i < cells.length; i++) { + var cellContent = cells[i]; + var cellValue = cellContent.textContent; + var event = jQuery.Event("keydown"); + + var cell = angular.element(cellContent.parentElement); + cell.dblclick(); + $timeout.flush(); + expect(cell.find('input').length).toBe(1); + + switch (cellValue) { + case 'A1': + cell.find('input').val('').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(true); + break; + case 'B1': + cell.find('input').val('B').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(true); + break; + case 'A2': + cell.find('input').val('A').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(false); + break; + case 'B2': + cell.find('input').val('B2+').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(false); + break; + } + } + }); + + it('should run validators on a edited invalid cell', function () { + element = angular.element('
'); + recompile(); + + var cells = element.find('.ui-grid-cell-contents.ng-scope'); + var cellContent = cells[0]; + var cellValue = cellContent.textContent; + var event = jQuery.Event("keydown"); + + var cell = angular.element(cellContent.parentElement); + cell.dblclick(); + $timeout.flush(); + expect(cell.find('input').length).toBe(1); + + cell.find('input').val('').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(true); + + cell.dblclick(); + $timeout.flush(); + expect(cell.find('input').length).toBe(1); + + cell.find('input').val('A1').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(false); + }); + + it('should raise an event when validation fails', function () { + + element = angular.element('
'); + recompile(); + + var cells = element.find('.ui-grid-cell-contents.ng-scope'); + var cellContent = cells[1]; + var cellValue = cellContent.textContent; + var event = jQuery.Event("keydown"); + var scope = angular.element(cellContent).scope(); + var grid = scope.grid; + + var listenerObject; + + grid.api.validate.on.validationFailed(scope, function(rowEntity, colDef, newValue, oldValue) { + listenerObject = [rowEntity, colDef, newValue, oldValue]; + }); + + spyOn(grid.api.validate.raise, 'validationFailed').andCallThrough(); + + var cell = angular.element(cellContent.parentElement); + cell.dblclick(); + $timeout.flush(); + expect(cell.find('input').length).toBe(1); + + cell.find('input').val('B').trigger('input'); + event = jQuery.Event("keydown"); + event.keyCode = uiGridConstants.keymap.TAB; + cell.find('input').trigger(event); + digest(); + expect(cellContent.classList.contains('invalid')).toBe(true); + expect(grid.api.validate.raise.validationFailed).toHaveBeenCalled(); + expect(angular.equals(listenerObject, [grid.options.data[0], grid.options.columnDefs[1], 'B', 'B1'])).toBe(true); + + }); +}); diff --git a/src/features/validate/test/uiGridValidateService.spec.js b/src/features/validate/test/uiGridValidateService.spec.js new file mode 100644 index 0000000000..59ddb4141c --- /dev/null +++ b/src/features/validate/test/uiGridValidateService.spec.js @@ -0,0 +1,192 @@ +describe('ui.grid.validate uiGridValidateService', function () { + var uiGridValidateService; + var $rootScope; + var $q; + + beforeEach(module('ui.grid.validate')); + + beforeEach(inject(function (_uiGridValidateService_, _$rootScope_, _$q_) { + uiGridValidateService = _uiGridValidateService_; + $rootScope = _$rootScope_; + $q = _$q_; + })); + + it('should create an empty validatorFactories object', function() { + expect(angular.equals(uiGridValidateService.validatorFactories, {})).toBe(true); + }); + + it('should add a validator when calling setValidator', function() { + uiGridValidateService.setValidator('test', angular.noop, angular.noop); + expect(uiGridValidateService.validatorFactories.test).toBeDefined(); + }); + + it('should return a validator function when calling getValidator with an argument', function() { + var fooFactory = function(argument) { + return function() { + return 'foo'+argument; + }; + }; + uiGridValidateService.setValidator('foo', fooFactory, angular.noop); + expect(uiGridValidateService.getValidator('foo','bar')()).toBe('foobar'); + }); + + it('should return a message function when calling getMessage with an argument', function() { + var messageFunction = function(argument) { + return 'message'+argument; + }; + uiGridValidateService.setValidator('foo', angular.noop, messageFunction); + expect(uiGridValidateService.getMessage('foo','bar')).toBe('messagebar'); + }); + + it('should return true when calling isInvalid on an invalid cell', function() { + var colDef = {name: 'foo'}; + var entity = {'$$invalidfoo': true}; + + expect(uiGridValidateService.isInvalid(entity, colDef)).toBe(true); + }); + + it('should return false when calling isInvalid on a valid cell', function() { + var colDef = {name: 'foo'}; + var entity = {'$$invalidfoo': false}; + + expect(uiGridValidateService.isInvalid(entity, colDef)).toBeFalsy(); + + colDef = {name: 'bar'}; + expect(uiGridValidateService.isInvalid(entity, colDef)).toBeFalsy(); + }); + + it('should set a cell as invalid when calling setInvalid on a valid cell', function() { + var colDef = {name: 'foo'}; + var entity = {}; + + uiGridValidateService.setInvalid(entity, colDef); + expect(entity['$$invalidfoo']).toBe(true); + + entity = {'$$invalidfoo': false}; + + uiGridValidateService.setInvalid(entity, colDef); + expect(entity['$$invalidfoo']).toBe(true); + }); + + it('should set a cell as valid when calling setValid on an invalid cell', function() { + var colDef = {name: 'foo'}; + var entity = {'$$invalidfoo': true}; + + uiGridValidateService.setValid(entity, colDef); + + expect(entity['$$invalidfoo']).toBeUndefined(); + }); + + it('should add an error to a cell when calling setError on that cell', function() { + var colDef = {name: 'foo'}; + var entity = {}; + + uiGridValidateService.setError(entity, colDef, 'bar'); + expect(entity['$$errorsfoo'].bar).toBe(true); + + entity['$$errorsfoo'].bar = false; + + uiGridValidateService.setError(entity, colDef, 'bar'); + expect(entity['$$errorsfoo'].bar).toBe(true); + }); + + it('should remove an error to a cell when calling clearError on that cell', function() { + var colDef = {name: 'foo'}; + var entity = {'$$errorsfoo': {bar: true} }; + + uiGridValidateService.clearError(entity, colDef, 'bar'); + expect(entity['$$errorsfoo'].bar).toBeUndefined(); + + }); + + it('should return an array with all error messages (alphabetically sorted) when calling getErrorMessages on a cell', function() { + var colDef = {name: 'test', validators: {foo: 'foo', bar: 'bar'}}; + var entity = {'$$errorstest': {foo: true, bar: true} }; + + var fooMessage = function(argument) {return argument + 'Message';}; + var barMessage = function(argument) {return argument + 'Message';}; + + uiGridValidateService.setValidator('foo', angular.noop, fooMessage); + uiGridValidateService.setValidator('bar', angular.noop, barMessage); + + var messages = uiGridValidateService.getErrorMessages(entity, colDef); + expect(messages[0]).toBe('barMessage'); + expect(messages[1]).toBe('fooMessage'); + + }); + + it('should execute all validators when calling runValidators on a cell and set/clear errors', function() { + var colDef = {name: 'test', validators: {foo: 'foo', bar: 'bar'}}; + var entity = {}; + + var validatorFactory = function (argument) {return function() {return argument === 'foo';};}; + + uiGridValidateService.setValidator('foo', validatorFactory, angular.noop); + uiGridValidateService.setValidator('bar', validatorFactory, angular.noop); + + uiGridValidateService.runValidators(entity, colDef, 1, 0); + + $rootScope.$apply(); + + expect(entity['$$errorstest'].bar).toBe(true); + expect(entity['$$invalidtest']).toBe(true); + + expect(entity['$$errorstest'].foo).toBeFalsy(); + + + }); + + it('should not execute any validator when calling runValidators with newValue === oldValue', function() { + var colDef = {name: 'test', validators: {foo: 'foo', bar: 'bar'}}; + var entity = {}; + + var validatorFactory = function (argument) {return function() {return argument === 'foo';};}; + + uiGridValidateService.setValidator('foo', validatorFactory, angular.noop); + uiGridValidateService.setValidator('bar', validatorFactory, angular.noop); + + uiGridValidateService.runValidators(entity, colDef, 1, 1); + + $rootScope.$apply(); + + expect(entity['$$errorstest']).toBeUndefined(); + expect(entity['$$invalidtest']).toBeUndefined(); + + }); + + it('should run an external validator if an external validator factory is set', function() { + + var colDef = {name: 'test', validators: {foo: 'foo'}}; + var entity = {}; + + var externalFooValidator = function() {return function() {return false;};}; + var externalFactoryFunction = function(name, argument) { + if (name === 'foo') { + return {validatorFactory: externalFooValidator, messageFunction: angular.noop}; + } + }; + + uiGridValidateService.setExternalFactoryFunction(externalFactoryFunction); + + var validatorFactory = function (argument) {return function() {return argument === 'foo';};}; + + uiGridValidateService.setValidator('foo', validatorFactory, angular.noop); + + uiGridValidateService.runValidators(entity, colDef, 1, 0); + + $rootScope.$apply(); + + expect(entity['$$errorstest'].foo).toBe(true); + expect(entity['$$invalidtest']).toBe(true); + + }); + + it('should call setValidator three times when calling createDefaultValidators', function() { + spyOn(uiGridValidateService, 'setValidator'); + + uiGridValidateService.createDefaultValidators(); + + expect(uiGridValidateService.setValidator.calls.length).toBe(3); + }); + +}); \ No newline at end of file diff --git a/src/js/i18n/en.js b/src/js/i18n/en.js index 55ef6f8f40..02d5b2edff 100644 --- a/src/js/i18n/en.js +++ b/src/js/i18n/en.js @@ -101,6 +101,12 @@ aggregate_min: 'Agg: Min', aggregate_avg: 'Agg: Avg', aggregate_remove: 'Agg: Remove' + }, + validate: { + error: 'Error:', + minLength: 'Value should be at least THRESHOLD characters long.', + maxLength: 'Value should be at most THRESHOLD characters long.', + required: 'A value is needed.' } }); return $delegate; diff --git a/src/js/i18n/it.js b/src/js/i18n/it.js index 0bbeefbe21..8a76a0306e 100644 --- a/src/js/i18n/it.js +++ b/src/js/i18n/it.js @@ -72,6 +72,12 @@ aggregate_min: 'Agg: Minimo', aggregate_avg: 'Agg: Media', aggregate_remove: 'Agg: Rimuovi' + }, + validate: { + error: 'Errore:', + minLength: 'Lunghezza minima pari a THRESHOLD caratteri.', + maxLength: 'Lunghezza massima pari a THRESHOLD caratteri.', + required: 'Necessario inserire un valore.' } }); return $delegate;