Skip to content
Open
46 changes: 39 additions & 7 deletions Src/knockout.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
//
Expand Down Expand Up @@ -922,13 +933,19 @@
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'];
delete observable['isValid'];
delete observable['isValidating'];
delete observable['__valid__'];
delete observable['isModified'];
if(configuration.enableErrorDetails) delete observable['errorDetails'];
};
} else if (enable === false && utils.isValidatable(observable)) {

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
};
Expand Down
121 changes: 121 additions & 0 deletions Tests/validation-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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