diff --git a/Src/knockout.validation.js b/Src/knockout.validation.js index ab1422c3..d802680d 100644 --- a/Src/knockout.validation.js +++ b/Src/knockout.validation.js @@ -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 } }; @@ -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); } }; } ()); @@ -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 @@ -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)) { @@ -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) { @@ -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); } @@ -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); }); }; diff --git a/Tests/validation-tests.js b/Tests/validation-tests.js index 65896b6f..1e5c29cd 100644 --- a/Tests/validation-tests.js +++ b/Tests/validation-tests.js @@ -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 }),