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
+
+
Alpha This feature is in development.
+There will almost certainly be breaking api changes, or there are major outstanding bugs.
+
+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
+ *
+ * Alpha This feature is in development. There will almost certainly be breaking api changes, or there are major outstanding bugs.
+ *
+ * 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;