Validation – Part 2 – Client-Side

Validation – Part 2 – Client-Side

In Part 1 we looked at our basic framework for validation – a set of attributes, an interface, and some configuration objects. Now we’ll see how these can be used in conjunction with jQuery to provide client-side validation.

As we already saw, every validation attribute has a ToJson method which serializes attributes into a dictionary. Our goal is end up with something like:

$('#saveUserForm').validate(
   {
      rules:
      {
         Email:{required:true,pattern:'.+@.+\\..+'},
         Password:{required:true,min:4,max:30}
      }      
   }
);

HtmlValidation

Before we look at the actual jQuery validate function, lets see how we can take our ValidatorConfiguration to generate this type of JSON object. Like we did with the core ValidationConfiguration, we’ll cache our JSON strings per object type so that they aren’t generated over and over again. Here’s the core body of our class:

public static class HtmlValidation
{
private static readonly IDictionary<Type, string> _rules = new Dictionary<Type, string>();

public static void Initialize(IDictionary<Type, EntityValidationInfo> rules)
{
foreach (var rule in rules)
{
_rules.Add(rule.Key, BuildJson(rule.Value));
}
}
private static string BuildJson(EntityValidationInfo entity)
{
//todo
}
}

We initialize this our HtmlValidation class in our Application_Start:

ValidatorConfiguration.Initialize("MyAssembly", "MyAssembly.Web");  //from part 1
HtmlValidation.Initialize(ValidatorConfiguration.Rules);

Next we implement the BuildJson method:

private static string BuildJson(EntityValidationInfo entity)
{
    var sb = new StringBuilder("rules:{");
    foreach (var property in entity.Properties)
    {                
        sb.Append(property.Property.Name);
        sb.Append(":{");
        foreach (BaseValidatorAttribute attribute in property.Validators)
        {
            foreach (var kvp in attribute.ToJson())
            {
                sb.AppendFormat("{0}:{1},", kvp.Key, kvp.Value);
            }
        }
        var tip = property.Property.GetCustomAttributes(typeof(TipAttribute), true);
        if (tip != null)
        {
            sb.AppendFormat("tip:'{0}'", (((TipAttribute)tip[0]).Tip).Replace("'", "\\'"));
        }
        sb.RemoveLastIf(',');
        sb.Append("},");
    }
    return sb.RemoveLast().Append("}").ToString();
}

Nothing too fancy here. We ToJson each attribute of each property and join them into a JSON string. Those RemoveLast and RemoveLastIf methods on StringBuilder are little extension methods I use to stay sane:

public static class StringBuilderExtensions
{
    public static StringBuilder RemoveLast(this StringBuilder sb)
    {
        return sb.RemoveLast(1);
    }
    public static StringBuilder RemoveLastIf(this StringBuilder sb, char c)
    {
        if (sb.Length > 0 && sb[sb.Length - 1] == c)
        {
            sb.RemoveLast();
        }
        return sb;            
    }
    public static StringBuilder RemoveLast(this StringBuilder sb, int count)
    {
        if (sb.Length > count)
        {
            sb.Remove(sb.Length - count, count);
        }
        return sb;
    }        
}

With that done, we just need a method that makes our cached _rules accessible. I wrote this an an HtmlHelper extension for MVC, but there isn’t anything MVC-specific about it. You should be able to easily get this working in WebForms as well:

public static string RulesFor<T>(this HtmlHelper html)
{            
    return _rules[typeof (T)];            
} 

Now we can use that from within our aspx page/view:

$('#register').validate({<%=Html.RulesFor<User>() %>});   

$.validate

Our final task in this part is to build our validate function using jQuery. I’ve written a 2-part primer (part 1, part 2) in the past, so I’ll assume that you are familiar with jQuery. Needless to say that I’m a big fan of it and rather enjoy writing JavaScript plugins (funny how much I hated JavaScript not too long ago). We’ll look at the validate plugin in chunks, and I’ll provide the complete code at the end. First, a skeleton:

(function($)
{
    $.fn.validate = function(options, command)
    {
        var defaults = { };
        options = $.extend(defaults, options);
        var rules = options.rules;        
        
        return this.each(function()
        {   
            //make sure this isn't called twice
            if (this.validator) { return false; }
            var $form = $(this);    
            var $fields = $('input,select,textarea', $form);

            var v =
            {
                initialize: function()
                {              
                },
                validateField: function(field)
                {                                                              
                },            
                markAsInvalid: function($field, tip)
                {                                                     
                },
                markAsValid: function($field)
                {
                }  
            }                     
            this.validator = v;
            v.initialize();         

        });
    };
})(jQuery);

The initialize method hooks up to the form’s submit event and validates each field, it also gives focus to the first invalid field:

initialize: function()
{              
    $form.submit(function()
    {
        var isValid = true;
        $fields.each(function(i, field)
        {                            
            if (!v.validateField(field) && isValid) 
            { 
                isValid = false;
                $(field).focus();
            }
        });                                                
        return isValid;
    });                    
},

By returning false when there’s an error, we prevent the form from submitting. The actual validation takes place inside the validateField method. Since we only have a few simple validators, everything happens within that method:

validateField: function(field)
{                                           
    var rule = rules[field.name];                    
    if (!rule) { return true; }                    
    var $field = $(field);
    var value = $field.val();
    var isValid = true;
    if (rule.required && value.length == 0) { isValid = false ; }
    else if (rule.min && rule.min > value.length) { isValid = false; }
    else if (rule.max && rule.max < value.length) { isValid = false; }
    else if (rule.pattern && !rule.pattern.test(value)) { isValid = false; }
    if (!isValid)
    {                        
        v.markAsInvalid($field, rule.tip);                        
    }
    else
    {
        v.markAsValid($field);                        
    }
    return isValid;                    
}, 

The last two methods, markAsValid and markAsInvalid display or remove an actual error

markAsInvalid: function($field, tip)
{                                    
    if (tip)
    {
        var $tip = $field.siblings('.tip');
        if ($tip.length == 0)
        {                        
            $tip = $('<label>').attr('for', $field.attr('name')).addClass('tip').insertAfter($field);                        
        }
        $tip.html(tip).fadeIn();
    }
    $field.addClass('error');
},
markAsValid: function($field)
{
    $field.siblings('.tip').fadeOut();
    $field.removeClass('error');
}  

if markAsValid is given a tip, it’ll add and fadeIn a label next to the field. It’ll also add a class named error to the field. markAsValid does the opposite – fading out the tip and removing the class.

Improvement

We’ll add a slight improvement to our initialize method – not only will we hook into the form’s submit event, but also each field’s blur event, so that users get a more responsive feel. Luckily, all the infrastructure is already in place, so all we do is add the following code to initialize:

$fields.each(function(i, field)
{
    var $field = $(field);
    $field.blur(function()
    {
        v.validateField(this);
    });                                            
});

Code

Believe it or not, all that code make up only two classes. Since it was all broken up, here they are.

First, the HtmlValidation class (minus the StringBuilder extentions):

public static class HtmlValidationExtensions
{
    private static readonly IDictionary<Type, string> _rules = new Dictionary<Type, string>();    
    public static string RulesFor<T>(this HtmlHelper html)
    {            
        return _rules[typeof (T)];            
    }
    public static void Initialize(IDictionary<Type, EntityValidationInfo> rules)
    {            
        foreach (var rule in rules)
        {
            _rules.Add(rule.Key, BuildJson(rule.Value));
        }
    }
    private static string BuildJson(EntityValidationInfo entity)
    {
        var sb = new StringBuilder("rules:{");
        foreach (var property in entity.Properties)
        {                
            sb.Append(property.Property.Name);
            sb.Append(":{");
            foreach (BaseValidatorAttribute attribute in property.Validators)
            {
                foreach (var kvp in attribute.ToJson())
                {
                    sb.AppendFormat("{0}:{1},", kvp.Key, kvp.Value);
                }
            }
            var tip = property.Property.GetCustomAttributes(typeof(TipAttribute), true);
            if (tip != null)
            {
                sb.AppendFormat("tip:'{0}'", (((TipAttribute)tip[0]).Tip).Replace("'", "\\'"));
            }
            sb.RemoveLastIf(',');
            sb.Append("},");
        }
        return sb.RemoveLast().Append("}").ToString();
    }
}

Next, the $.validate plugin:

(function($)
{
    $.fn.validate = function(options, command)
    {
        var defaults = { };
        options = $.extend(defaults, options);
        var rules = options.rules;        
        
        return this.each(function()
        {
            if (this.validator) { return false; }
            var $form = $(this);    
            var $fields = $('input,select,textarea', $form);

            var v =
            {
                initialize: function()
                {
                    $fields.each(function(i, field)
                    {
                        var $field = $(field);
                        $field.blur(function()
                        {
                            v.validateField(this);
                        });                                      
                    });
                    $form.submit(function()
                    {
                        var isValid = true;
                        $fields.each(function(i, field)
                        {                            
                            if (!v.validateField(field) && isValid) 
                            { 
                                isValid = false;
                                $(field).focus();
                            }
                        });                                                
                        return isValid;
                    });                    
                },
                validateField: function(field)
                {                                           
                    var rule = rules[field.name];                    
                    if (!rule) { return true; }                    
                    var $field = $(field);
                    var value = $field.val();
                    var isValid = true;
                    if (rule.required && value.length == 0) { isValid = false ; }
                    else if (rule.min && rule.min > value.length) { isValid = false; }
                    else if (rule.max && rule.max < value.length) { isValid = false; }
                    else if (rule.pattern && !rule.pattern.test(value)) { isValid = false; }
                    if (!isValid)
                    {                        
                        v.markAsInvalid($field, rule.tip);                        
                    }
                    else
                    {
                        v.markAsValid($field);                        
                    }
                    return isValid;
                    
                },            
                markAsInvalid: function($field, tip)
                {                                    
                    if (tip)
                    {
                        var $tip = $field.siblings('.tip');
                        if ($tip.length == 0)
                        {                        
                            $tip = $('<label>').attr('for', $field.attr('name')).addClass('tip').insertAfter($field);                        
                        }
                        $tip.html(tip).fadeIn();
                    }
                    $field.addClass('error');
                },
                markAsValid: function($field)
                {
                    $field.siblings('.tip').fadeOut();
                    $field.removeClass('error');
                }  
            }                     
            this.validator = v;
            v.initialize();         
        });
    };
})(jQuery);

Conclusion

That’s a lot of code, but hopefully it’ll help you get started. In part 3 we’ll look at server-side validation, and how to tie that back into the client.

This entry was posted in Uncategorized. Bookmark the permalink. Follow any comments here with the RSS feed for this post.

3 Responses to Validation – Part 2 – Client-Side

  1. Slava says:

    I seen another good post related to validation (in case it might be interesting for someone) – http://igor.quatrocode.com/2008/12/dsl-jquery.html
    Unfortunately, it is in Russian :), but there are pieces of code…and sample solution is attached.
    Also, Google Translator might be tested on that link :)

  2. karl says:

    @JJ:
    Highly likely at the end of everything. It’ll be basic, but enough to help out a little more. I realize this is code-segment intensive which isn’t practical.

  3. JJ says:

    First, thank you for the good read.
    Second, could you maybe post a sample application? I just need to see it run to make it all click for me. Thanks