diff --git a/Src/knockout.validation.js b/Src/knockout.validation.js index ab1422c3..853676a6 100644 --- a/Src/knockout.validation.js +++ b/Src/knockout.validation.js @@ -41,9 +41,11 @@ errorClass: null, // single class for error message and element errorElementClass: 'validationElement', // class to decorate error element errorMessageClass: 'validationMessage', // class to decorate error message + enableErrorDetails: false, // add a new property errorDetails which contains the rule, the params, the observable and the message grouping: { - deep: false, //by default grouping is shallow - observable: true //and using observables + deep: false, //by default grouping is shallow + observable: true, //and using observables + errorDetails: false //insert plain error messages } }; @@ -248,7 +250,7 @@ var errors = []; ko.utils.arrayForEach(validatables(), function (observable) { if (!observable.isValid()) { - errors.push(observable.error); + errors.push(options.errorDetails && configuration.enableErrorDetails ? observable.errorDetails : observable.error); } }); return errors; @@ -261,7 +263,7 @@ traverse(obj); // and traverse tree again ko.utils.arrayForEach(validatables(), function (observable) { if (!observable.isValid()) { - errors.push(observable.error); + errors.push(options.errorDetails && configuration.enableErrorDetails ? observable.errorDetails : observable.error); } }); return errors; @@ -743,12 +745,12 @@ msg = null, isModified = false, isValid = false; - + obsv.extend({ validatable: true }); isModified = obsv.isModified(); isValid = obsv.isValid(); - + // create a handler to correctly return an error message var errorMsgAccessor = function () { if (!config.messagesOnModified || isModified) { @@ -877,7 +879,16 @@ if (enable && !utils.isValidatable(observable)) { observable.error = null; // holds the error message, we only need one since we stop processing validators when one is invalid - + if(configuration.enableErrorDetails) { + // holds detailed error information + observable.errorDetails = { + rule: ko.observable(null), + params: ko.observable(null), + observable: observable, + message: ko.observable(null) + }; + + } // observable.rules: // ObservableArray of Rule Contexts, where a Rule Context is simply the name of a rule and the params to supply to it // @@ -922,6 +933,11 @@ observable.__valid__._subscriptions['change'] = []; h_change.dispose(); h_obsValidationTrigger.dispose(); + if(configuration.enableErrorDetails) { + observable.errorDetails.rule.dispose(); + observable.errorDetails.params.dispose(); + observable.errorDetails.message.dispose(); + } delete observable['rules']; delete observable['error']; @@ -929,6 +945,7 @@ delete observable['isValidating']; delete observable['__valid__']; delete observable['isModified']; + if(configuration.enableErrorDetails) delete observable['errorDetails']; }; } else if (enable === false && utils.isValidatable(observable)) { @@ -945,6 +962,11 @@ //not valid, so format the error message and stick it in the 'error' variable observable.error = exports.formatMessage(ctx.message || rule.message, ctx.params); + if(configuration.enableErrorDetails) { + observable.errorDetails.rule(rule); + observable.errorDetails.params(ctx.params); + observable.errorDetails.message(observable.error); + } observable.__valid__(false); return false; } else { @@ -978,6 +1000,11 @@ if (!isValid) { //not valid, so format the error message and stick it in the 'error' variable observable.error = exports.formatMessage(msg || ctx.message || rule.message, ctx.params); + if(configuration.enableErrorDetails) { + observable.errorDetails.rule(rule); + observable.errorDetails.params(ctx.params); + observable.errorDetails.message(observable.error); + } observable.__valid__(isValid); } @@ -1021,6 +1048,11 @@ } //finally if we got this far, make the observable valid again! observable.error = null; + if(configuration.enableErrorDetails) { + observable.errorDetails.rule(null); + observable.errorDetails.params(null); + observable.errorDetails.message(null); + } observable.__valid__(true); return true; }; diff --git a/Tests/validation-tests.js b/Tests/validation-tests.js index 65896b6f..1c67c95d 100644 --- a/Tests/validation-tests.js +++ b/Tests/validation-tests.js @@ -1258,3 +1258,124 @@ asyncTest('Async Rule Is NOT Valid Test', function () { }); //#endregion + +//#region error details + +module('error details'); + +test('errorDetails property is filled when not valid', function () { + ko.validation.init({enableErrorDetails: true }, true); + var testObj = ko.observable('').extend({ required: true }); + + equal(testObj.isValid(), false); + equal(testObj.error, ko.validation.rules.required.message); + + ok(testObj.hasOwnProperty('errorDetails'), 'errorDetails property does not exist.'); + equal(testObj.errorDetails.rule(), ko.validation.rules.required); + equal(testObj.errorDetails.params(), true); + equal(testObj.errorDetails.observable, testObj); + equal(testObj.errorDetails.message(), ko.validation.rules.required.message) + ko.validation.reset(); +}); + +test('errorDetails properties are null when valid', function () { + ko.validation.init({enableErrorDetails: true }, true); + var testObj = ko.observable('').extend({ required: true }); + equal(testObj.isValid(), false); + + testObj('a value'); + + equal(testObj.isValid(), true); + equal(testObj.errorDetails.rule(), null); + equal(testObj.errorDetails.params(), null); + equal(testObj.errorDetails.message(), null); + ko.validation.reset(); +}); + +asyncTest('errorDetails property is filled when not valid async', function () { + ko.validation.init({enableErrorDetails: true }, true); + ko.validation.rules['mustEqualAsync'] = { + async: true, + validator: function (val, otherVal, callBack) { + var isValid = (val === otherVal); + setTimeout(function () { + callBack(isValid); + doAssertions(); + + start(); + }, 10); + }, + message: 'The field must equal {0}' + }; + ko.validation.registerExtenders(); //make sure the new rule is registered + + + var testObj = ko.observable(4); + + var doAssertions = function () { + ok(testObj.hasOwnProperty('errorDetails'), 'errorDetails property does not exist.'); + equal(testObj.errorDetails.rule(), ko.validation.rules['mustEqualAsync']); + equal(testObj.errorDetails.params(), 5); + equal(testObj.errorDetails.observable, testObj); + equal(testObj.errorDetails.message(), 'The field must equal 5') + }; + + testObj.extend({ mustEqualAsync: 5 }); + ko.validation.init({enableErrorDetails: true }, true); +}); + +test('group with errorDetails options works - Not Observable', function () { + ko.validation.init({enableErrorDetails: true }, true); + var vm = { + firstName: ko.observable().extend({ required: true }), + lastName: ko.observable().extend({ required: 2 }) + }; + + var errors = ko.validation.group(vm, { errorDetails: true, observable: false }); + + equals(errors().length, 2, 'Grouping correctly finds 2 invalid properties'); + equals(errors()[0], vm.firstName.errorDetails, 'group with errorDetails returns list of errorDetails'); + equals(errors()[1], vm.lastName.errorDetails, 'group with errorDetails returns list of errorDetails'); + ko.validation.reset(); +}); + +test('group with errorDetails options works - Observable', function () { + ko.validation.init({enableErrorDetails: true }, true); + var vm = { + firstName: ko.observable().extend({ required: true }), + lastName: ko.observable().extend({ required: 2 }) + }; + + var errors = ko.validation.group(vm, { errorDetails: true, observable: true }); + + equals(errors().length, 2, 'Grouping correctly finds 2 invalid properties'); + equals(errors()[0], vm.firstName.errorDetails, 'group with errorDetails returns list of errorDetails'); + equals(errors()[1], vm.lastName.errorDetails, 'group with errorDetails returns list of errorDetails'); + ko.validation.reset(); +}); + +test('errorDetails property is not defined if enableErrorDetails equals false', function () { + // enableErrorDetails is disabled by default + //ko.validation.init({enableErrorDetails: false }, true); + var testObj = ko.observable('').extend({ required: true }); + + equal(testObj.isValid(), false); + equal(testObj.error, ko.validation.rules.required.message); + + ok(!testObj.hasOwnProperty('errorDetails'), 'errorDetails property does exist.'); +}); + +test('going from one invalid state to the next creates the correct errorDetails (required -> maxLength)', function () { + ko.validation.init( { enableErrorDetails: true }, true); + var vm = { item : ko.observable().extend( { maxLength: 2, required: true } ) }; + var errors = ko.validation.group(vm, { deep: true, observable: true, errorDetails: true }); + + equals(errors().length, 1, "has initially one error"); + equals(errors()[0].rule().message, ko.validation.rules.required.message); + + // insert too long text triggering maxLength rule + vm.item('12345'); + equals(errors()[0].rule().message, "Please enter no more than {0} characters."); + ko.validation.reset(); +}); +//#endregion