Skip to content
carlschroedl edited this page Feb 12, 2013 · 4 revisions

Table of Contents

Validated View Models in KnockoutJS

License

License: MIT

Summary

ValidatedViewModel helps structure the definition of validation constraints and provides a means to apply groups of validation constraints context-dependently.

Verbose documentation useful for learning is contained on this wiki. Terse documentation suitable for reference is found in the docblocks at the top of each function. Unit tests and instructions on how to run them are found in the same file.

Dependencies

Background/Motivation:

So far our knockout view models are pretty much javascript clones of our framework's server-side entities. Take this simplified view model class:

//src: viewModels.js

//constructor casing, so it's a class! 
var SomeModel = function(){
	var self = this;
	self.prop1 = ko.observable();
	self.prop2 = ko.observable();
	self.prop3 = ko.observable();
};

To avoid repetition, we share these view model classes between all of the views in a project by defining all of them in a common file.

Most views need some form of validation of view model properties to be performed.

We've been using the Knockout-Validation plugin to validate our view models. The Knockout-Validation plugin has a lot of handy built-in constraints, and offers us the ability to integrate our own. The validation plugin recommends that we define our validation constraints by passing constraint definitions to the .extend method of our ko.observables like so:

var SomeModel = function(){
	var self = this;
	self.prop1 = ko.observable().extend({required: true});
	self.prop2 = ko.observable().extend({max: 42});
	self.prop3 = ko.observable(); //not validated
};

But this approach works poorly when one view contains multiple instances of the same view model class that have different validation needs. Additionally, this approach does not work when we share our view model classes between different views when those views have different validation needs. Since we want to re-use our view model classes, we cannot define the validation constraints in the class definition. It is preferable to instead instantiate the view model in view-specific script, and set the constraint definitions there.

ko.validation.init();

var myModel = new SomeModel();

myModel.prop1.extend({required: true});
myModel.prop2.extend({max: 42});

//now make another instance, with different constraint definitions
var myOtherModel = new SomeModel();

myOtherModel.prop2.extend({required: true});

This approach does not scale well. If in a single view you want to instantiate multiple view models with similar validation constraints you end up repeating yourself, or writing annoying logic to avoid doing so.

//copy-and-paste-and-modifffy-err0r-pron3-ly:
//(bad)

ko.validation.init();

var myModel = new SomeModel();

myModel.prop1.extend({required: true});
myModel.prop2.extend({max: 42});

//now make another instance, with different constraint definitions
var myOtherModel = new SomeModel();

myOtherModel.prop1.extend({required: true});
myOtherModel.prop2.extend({max: 42});
////////////////////////////////////////////////////

//now try with more view model instances
//definitely couldn't use copy-and-paste here
//Try several iterations over varying subsets of the SomeModel instances to apply common constraint groups:
//(better, but still bad)

ko.validation.init();

var myMod = new SomeModel();
var yourMod = new SomeModel();
var thatMod = new SomeModel();
var superMod = new SomeModel();
var betterMod = new SomeModel();

//continue instantiating until there are N new SomeModel instances
.
.
.

var modelsToValidate = new Array(myMod, yourMod , thatMod, superMod, betterMod, … );
ko.arrayForEach(modelsToValidate, function(index, model){
	model.prop1.extend({required: true});
	model.prop2.extend({max: 42});
});

//but now 3 of these N also need prop 1 to have a min of 4:
modelsToValidate = new Array(myMod, yourMod, thatMod);
ko.arrayForEach(modelsToValidate, function(index, model){
	model.prop1.extend({min: 4});
});

It would be nice to apply these common groups of constraints consistently and non-repetitively to encourage uniform validation behavior within a single view without writing a bunch of logic. In addition, although validation constraints on view models often differ from view to view, there is often a common subset of identical constraints that is shared between multiple views. It would be even nicer if we could use common groups of constraints consistently and non-repetitively to encourage uniform validation behavior across multiple views. It would also be nice to define these shared constraints in the only script guaranteed to be shared between all of the views in a bundle – the file which defines the view model classes.

Solving the problem with ValidatedViewModel(), self.constraintGroups, and applyConstraintGroup():

We can achieve scalable, reusable, context-dependent validation. We define constraint groups in the view models' class definitions, but we will apply these constraint groups to particular view model instances.

View Model Class Definitions

  • Write your view model class definition as a plain old javascript function, just like you used to.
  • Wrap the function in a call to ValidatedViewModel();
  • Add a new property – self.constraintGroups – and declare your constraint groups within it.
//handy-dandy notebook:
var SomeModel = ValidatedViewModel(function(){
	var self = this;
	self.prop1 = ko.observable();
	self.prop2 = ko.observable();
	self.prop3 = ko.observable(); 

	self.constraintGroups = {
		needy : {	prop1: {required: true},
				prop2: {required: true},
				prop3: {required: true}
			},
		boundy : {	prop1: {max : 42},
				prop2: {min : 4},
				prop3: {min: 3, max: 33}
		}
	};

});

Applying Constraint Groups to ValidatedViewModels

For one view... For another view...
ko.validation.init();

var myMod = new SomeModel();
var yourMod = new SomeModel();
var thatMod = new SomeModel();

myMod.applyConstraintGroup('needy');
yourMod.applyConstraintGroup('boundy');

thatMod.applyConstraintGroup('needy');
thatMod.applyConstraintGroup('boundy');
ko.validation.init();

var myMod = new SomeModel();
var megaMod = new SomeModel();
var thatMod = new SomeModel();

myMod.applyConstraintGroup('boundy');
megaMod.applyConstraintGroup('needy');

//no validation needed for thatMod here!
// ^ Look Ma! No duplication! So consistent! ^

Notes:

  • You can define constraints for any instance of the ko.subscribable() interface (ko.observable, ko.observableArray, ko.computed).
  • If you apply a constraint group, you don't need to do that weird errors assignment thing we had been doing:
myViewModel.errors = ko.validation.group(myViewModel, { /*config stuff here*/ });
  • You can optionally pass configuration options : applyConstraintGroup('groupName',{deep : true})
  • You can alias the constraint group names as follows:
//aliasing a constraint group name
var SomeModel = ValidatedViewModel(function(){
	var self = this;
	self.prop1 = ko.observable();
	self.prop2 = ko.observable();
	self.prop3 = ko.observable(); 

	self.constraintGroups = {
		needy : {	prop1: {required: true},
				prop2: {required: true},
				prop3: {required: true}
			},
	};
	self.constraintGroups.kittens = self.constraintGroups.needy;
});

  • You can apply multiple constraint groups quickly via the applyConstraintGroups method:
//src: your_view_model_class_definitions.js
var SomeModel = ValidatedViewModel(function(){
	var self = this;
	self.prop1 = ko.observable();
	self.prop2 = ko.observable();
	self.prop3 = ko.observable(); 

	self.constraintGroups = {
		needy : {	prop1: {required: true},
				prop2: {required: true},
				prop3: {required: true}
			},
		boundy : {	prop1: {max : 42},
				prop2: {min : 4},
				prop3: {min: 3, max: 33}
		}
	};

});
ko.validation.init();

var myMod = new SomeModel();

myMod.applyConstraintGroups(['needy', 'boundy']);

Additional Features

The ValidatedViewModel also attaches methods that permit the removal of already-applied constraint groups. You may need to remove constraint groups because none of the constraints in a particular group are relevant anymore. You might also need to remove constraint groups if their constraint definitions conflict.

//src: your_view_model_class_definitions.js
var TheModel = ValidatedViewModel(function(){
	var self = this;
	self.prop1 = ko.observable();
	self.prop2 = ko.observable();
	self.prop3 = ko.observable(); 

	self.constraintGroups = {
		highMin : {	prop1: {min : 45},	// conflicting constraint
				prop2: {required: true},
				prop3: {required: true}
		}
		lowMax : {	prop1: {max : 42},	// conflicting constraint
				prop2: {minLength: 3},
				prop3: {maxLength: 5}
		}
	};

});
//src: your_view_specific.js
ko.validation.init();

var myMod = new TheModel();

myMod.applyConstraintGroup('highMin');

//.
//.
//.

//(some context arises in which you must ditch one constraint group in favor of another)
myMod.removeConstraintGroup('highMin');
myMod.applyConstraintGroup('lowMax');

//.
//.
//.

//(some context arises in which you no longer need any constraint group
myMod.removeConstraintGroup('lowMax');

//.
//.
//.

//(some context arises in which you need the constraints from both groups, but you want to remove a particular constraint.
//   In this contrived example, the constraints on prop1 from groups 'highMin' and 'lowMax' are impossible to satisfy.)

myMod.applyConstraintGroups(['highMin', 'lowMax']);
myMod.removeConstraintFromProperty('min', 42, 'prop1');

//now no conflicting constraints remain.