Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 39 additions & 8 deletions Src/knockout.validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
errorMessageClass: 'validationMessage', // class to decorate error message
grouping: {
deep: false, //by default grouping is shallow
observable: true //and using observables
observable: true, //and using observables
live: false //react to changes to observableArrays if observable === true
}
};

Expand Down Expand Up @@ -147,6 +148,10 @@
if (val === "") {
return true;
}
},
//created issue to solve that in ko https://github.com/SteveSanderson/knockout/issues/619
isObservableArray: function (obj) {
return ko.isObservable(obj) && !(obj.destroyAll === undefined);
}
};
} ());
Expand Down Expand Up @@ -201,6 +206,7 @@
group: function group(obj, options) { // array of observables or viewModel
var options = ko.utils.extend(configuration.grouping, options),
validatables = ko.observableArray([]),
validatablesTemp = [],
result = null,

//anonymous, immediate function to traverse objects hierarchically
Expand All @@ -217,11 +223,16 @@

//make sure it is validatable object
if (!obj.isValid) obj.extend({ validatable: true });
validatables.push(obj);
validatablesTemp.push(obj);

if(options.live && utils.isObservableArray(obj)) {
subscribeToObservableArray(obj);
}
}

//get list of values either from array or object but ignore non-objects
if (val) {
// and destroyed objects
if (val && !val._destroy) {
if (utils.isArray(val)) {
objValues = val;
} else if (utils.isObject(val)) {
Expand All @@ -237,13 +248,31 @@
if (observable && !observable.nodeType) traverse(observable, level + 1);
});
}
},

observableArraySubscriptions = [],
clearObservableArraySubscriptions = function () {
ko.utils.arrayForEach(observableArraySubscriptions, function (subscription) {
subscription.dispose();
});
observableArraySubscriptions = [];
},
traverseAndStoreInValidatables = function() {
clearObservableArraySubscriptions();
validatablesTemp = [];
traverse(obj);
validatables(validatablesTemp);
},
subscribeToObservableArray = function(observableArray) {
observableArraySubscriptions.push(observableArray.subscribe(traverseAndStoreInValidatables));
};

//if using observables then traverse structure once and add observables
if (options.observable) {

traverse(obj);

traverseAndStoreInValidatables();

// TODO: call clearObservableArraySubscriptions on dispose of result -> but ko.computed has no disposeCallback
result = ko.computed(function () {
var errors = [];
ko.utils.arrayForEach(validatables(), function (observable) {
Expand All @@ -254,12 +283,14 @@
return errors;
});

result.retraverse = traverseAndStoreInValidatables;

} else { //if not using observables then every call to error() should traverse the structure
result = function () {
var errors = [];
validatables([]); //clear validatables
validatablesTemp = []; //clear validatables
traverse(obj); // and traverse tree again
ko.utils.arrayForEach(validatables(), function (observable) {
ko.utils.arrayForEach(validatablesTemp, function (observable) {
if (!observable.isValid()) {
errors.push(observable.error);
}
Expand All @@ -277,7 +308,7 @@
// ensure we have latest changes
result();

ko.utils.arrayForEach(validatables(), function (observable) {
ko.utils.arrayForEach(validatablesTemp, function (observable) {
observable.isModified(show);
});
};
Expand Down
107 changes: 107 additions & 0 deletions Tests/validation-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,113 @@ test('Nested Grouping works - Not Observable', function () {
equals(errors().length, 3, 'Grouping correctly finds 3 invalid properties');
});

test('Nested grouping finds items in observableArrays - observable', function () {
var vm = { array: ko.observableArray( [ { one: ko.observable().extend( { required: true } ) } ]) };

var errors = ko.validation.group(vm, { deep: true, observable: true });

equals(errors().length, 1, 'Grouping finds property on object in observableArray');
});

test('Nested grouping does not add items newly inserted into observableArrays to result - observable, not live', function () {
var vm = { array: ko.observableArray() };

var errors = ko.validation.group(vm, { deep: true, observable: true, live: false });

vm.array.push( { one: ko.observable().extend( { required: true } ) });

equals(errors().length, 0, 'grouping does not add newly items newly inserted into observableArrays to result');
});

test('Nested grouping adds items newly inserted into an observableArrays nested in an object in an observableArray to result - observable, live', function () {
var vm = { array: ko.observableArray() };

var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

vm.array.push({ array: ko.observableArray() });
vm.array()[0].array.push( { one: ko.observable().extend( { required: true } ) });

equals(errors().length, 1, 'grouping adds newly items newly inserted into observableArrays to result');
});

test('Nested grouping adds items newly inserted into observableArrays to result - observable, live', function () {
var vm = { array: ko.observableArray() };

var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

vm.array.push( { one: ko.observable().extend( { required: true } ) });

equals(errors().length, 1, 'grouping adds newly items newly inserted into observableArrays to result');
});

test('Nested grouping ignores items nested in destroyed objects - not observable', function () {
var obj = { nested: ko.observable().extend({ required: true }) };

function getErrorCount() {
return errorsFn = ko.validation.group(obj, { deep: true, observable: false, live: false })().length;
}

equal(getErrorCount(), 1, 'obj is not destroyed and should return nested\'s error');

obj._destroy = true;

equal(getErrorCount(), 0, 'obj is destroyed and nested therefore ignored');
});

test('Nested grouping ignores items nested in destroyed objects - observable, live', function () {
var obj = { nested: ko.observable().extend({ required: true }) };
var array = ko.observableArray([obj]);
var vm = { array: array};

var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

equal(errors().length, 1, 'obj is not yet destroyed and nested therefore invalid');
array.destroy(obj);
equal(errors().length, 0, 'obj is destroyed and nested therefore ignored');
});

test('Nested grouping does not cause the reevaluation of computeds depending on the result for every observable', function () {
var vm = { array: ko.observableArray() };
var item = { one: ko.observable().extend( { required: true } ) };

var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

var computedHitCount = 0;
var computed = ko.computed(function () {
computedHitCount++;
errors();
});

vm.array.push(item);
equals(computedHitCount, 2, ' first on while creating the computed, second one for adding the item');
});

test('Nested grouping adds items newly inserted into observableArrays to result - cleares validatables before traversing again - observable, live', function () {
var vm = { array: ko.observableArray() };

var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

vm.array.push({ one: ko.observable().extend({ required: true }) });
vm.array.push({ one: ko.observable().extend({ required: true }) });

equals(errors().length, 2, 'validatables are added only once');
});

test('Retraverse all objects', function () {
var vm = { prop: ko.observable()};
var errors = ko.validation.group(vm, { deep: true, observable: true, live: true });

var complexObject = { name: ko.observable().extend({ required: true }) };
vm.prop(complexObject);

errors.showAllMessages();
equals(errors().length, 0, 'no validation error'); // complex object is not added to validation

errors.retraverse();

equals(errors().length, 1, 'validatables are added only once');
});

test('Issue #31 - Recursively Show All Messages', function () {
var vm = {
one: ko.observable().extend({ required: true }),
Expand Down