Wednesday, September 05, 2012

Knockout Model and Validating "inner" Models

More and more technologies, projects and products are basing their foundation on MVC and MVVM principles. The reason is that it makes the project or product to be maintained easily without much headache. The latest trend in client based web development is also shifting towards MVVM model and the tool that is getting most widely used is the Knockout javascript library. It’s a great tool that eases binding making even the complex javascript tasks to be designed pretty easily like for example dynamically rendering design for multiple contacts and collecting details, etc..,

Basic Knockout Model Validation

Validating a basic knockout model is a straight forward task. We just have to use the extend method on the observable attributes and set the necessary parameters. Consider the following model:

function ClientModel (id, firstName, lastName, email) {
        var self = this;
        self.Id = ko.observable(id);
        self.FirstName = ko.observable(firstName).extend({ required: true, maxLength: 50 });
        self.LastName = ko.observable(lastName).extend({ required: true, maxLength: 50 });
        self.Email = ko.observable(email).extend({ maxLength: 50, email: true });

        self.save = function () {
            if (self.errors().length != 0) { //verifying the errors attribute using the count of errors
                self.errors.showAllMessages(); //displaying the corresponding error messages
                return; //returning the control providing user a chance to correct the issues
            }

            $.ajax({
                type: "POST",
                url: "Save",
                data: ko.toJSON(self),
                contentType: 'application/json',
                success: function (mydata) {
                    alert("Success");
                },
                error: function (xhr, ajaxOptions, thrownError) {
                    alert("save method error status: " + xhr.status);
                    alert("save method error thrown: " + thrownError);
                }
            });
        };
};

$(document).ready(function () {
var clientModel = new ClientModel(1, "Hello", "World", "hell0@world.com");
clientModel.errors = ko.validation.group(clientModel); //setting up the errors attribute to the model to capture the validation error message
ko.applyBindings(clientModel);
});

The above is a basic model with validations added for the attributes using the extend method (More validation can be found here). The validation error messages are shown when the save method of the model is executed.

Deep Knockout Model Validation

Often we face situation where we will have a requirement for a complex model than the basic one. For example: our client may request us to design a solution to allow multiple contact numbers for a customer to be collected. In this case, the underlying model is changed as follows.

function Contact(id, phoneType, phone) {
        var self = this;
        self.ClientId = ko.observable(id);
        self.PhoneType = ko.observable(phoneType).extend({ required: true });
        self.Phone = ko.observable(phone).extend({ required: true });
};

function ClientModel (id, firstName, lastName, email, contacts) {
       var self = this;
       self.Id = ko.observable(id);
       self.FirstName = ko.observable(firstName).extend({ required: true, maxLength: 50 });
       self.LastName = ko.observable(lastName).extend({ required: true, maxLength: 50 });
       self.Email = ko.observable(email).extend({ maxLength: 50, email: true });

self.Contacts = ko.observableArray(ko.utils.arrayMap(contacts, function (aContact) { return aContact; }));

       self.save = function () {
            if (self.errors().length != 0) { //verifying the errors attribute using the count of errors
                self.errors.showAllMessages(); //displaying the corresponding error messages
                return; //returning the control providing user a chance to correct the issues
            }

            $.ajax({
                type: "POST",
                url: "Save",
                data: ko.toJSON(self),
                contentType: 'application/json',
                success: function (mydata) {
                    alert("Success");
                },
                error: function (xhr, ajaxOptions, thrownError) {
                    alert("save method error status: " + xhr.status);
                    alert("save method error thrown: " + thrownError);
                }
            });
       };
};

$(document).ready(function () {
       var contacts = { new Contact(1, "Home", "1234567890"), new Contact(1, "Work", "9876543210") };
var clientModel = new ClientModel(1, "Hello", "World", "hell0@world.com", contacts);
clientModel.errors = ko.validation.group(clientModel); //setting up the errors attribute to the model to capture the validation error message
ko.applyBindings(clientModel);
});

The above is a also a model with validations added for the attributes using the extend method. The validation error messages excepts the contacts will be displayed when the save method of the model is executed. The reason is that we have not made the validation “deep” so the inner validations are not executed. In order to achieve this, we will have to include the following Knockout initialization block:

ko.validation.init({grouping: { deep: true }, messagesOnModified: false });

Adding the above line of code should make the inner validations work in most cases. But if it still doesn’t work, then we will have to add the errors attribute to the Contact model just like we have added for the ClientModel. The below listing shows the updated Contact model with errors attribute added:

function Contact(id, phoneType, phone) {
        var self = this;
        self.ClientId = ko.observable(id);
        self.PhoneType = ko.observable(phoneType).extend({ required: true });
        self.Phone = ko.observable(phone).extend({ required: true });

self.errors = ko.validation.group(self); //setting up the errors attribute to the Contact model to capture the validation error message
};

Having added the errors attribute to the Contact model, we also need to update the save method of the ClientModel to verify the errors collection attribute and return if in case there are errors. The updated save method is listed below:

self.save = function () {
       var errorExists = false;

if (self.Contacts().errors().length != 0) { //verifying the errors attribute from the Contact model
              self.Contacts().errors.showAllMessages(); //displaying the corresponding error messages
              errorExists = true; //set error flag to stop further processing
       }

if (self.errors().length != 0) { //verifying the errors attribute using the count of errors
              self.errors.showAllMessages(); //displaying the corresponding error messages
              errorExists = true; //set error flag to stop further processing
       }

if (errorExists) {
              return; //returning the control providing user a chance to correct the issues
       }

      
       $.ajax({
              type: "POST",
              url: "Save",
              data: ko.toJSON(self),
              contentType: 'application/json',
              success: function (mydata) {
                     alert("Success");
              },
              error: function (xhr, ajaxOptions, thrownError) {
                     alert("save method error status: " + xhr.status);
                    alert("save method error thrown: " + thrownError);
              }
            });
};

From the above updated save method, all the errors corresponding to the main model (ClientModel) and the child model (Contact) will be displayed.
Hope this helps!!!

2 comments:

Airn5475 said...

I'm not sure I follow your usage of:
self.Contacts().errors

How does it not return undefined?

Contacts() is a simple array, to which you never assign the errors property.

Am I missing something?
I am having trouble with nested arrays and validation.

Hemant said...

Hi Airn,

errors() is available to any template/class you create. We don't need to explicitly define errors() for our class. Knockout model takes care of this.

Thanks,
Hemant.