Return to Snippet

Revision: 9199
at October 30, 2008 10:18 by kouphax


Updated Code
/*---------------------------------------------------------------------------------
 *
 *  Process Constraints jQuery Plugin
 *
 *---------------------------------------------------------------------------------
 *
 *  This plugin is responsible for apply constraints to a target form.  It 
 *  will examin each fiels annotations to find @Constraints annotation and 
 *  appply the requested constraints to the field.  An number of configuration 
 *  options are available (see below)
 *
 *  @author  James Hughes
 *
 *  -------------------------------------------------------------------------------
 *  24/10/2008 - Initial Version
 *  -------------------------------------------------------------------------------
 *  28/10/2008 - Second Draft: Major Internal Refactoring.  Introduced new public
 *               API's for working with constratins and messages.  Added the 
 *               ability to remove constraints singularly and globally as well as
 *               actually uninstall the plugin.
 *  -------------------------------------------------------------------------------
 *///------------------------------------------------------------------------------
 (function($){
     
    /*-----------------------------------------------------------------------------
     *
     *  isEmpty Utility Function
     *
     *-----------------------------------------------------------------------------
     *
     *  Utility function that ensures a string is either null or an empty string.
     *
     *  @plugin
     *  @param v - the value to check for emptiness
     *
     *///--------------------------------------------------------------------------
    $.isEmpty = function(v){
        return !v || v == "" || $.trim(v) == "";
    };
     
    /*-----------------------------------------------------------------------------
     *
     *  Process Constraints Plugin
     *
     *-----------------------------------------------------------------------------
     *
     *  Extend the jQuery Object to provide a utility function that will perform
     *  the constraint application functionality to the form passed in
     *
     *  @plugin
     *  @param o   - custom options object that accepts
     *
     *      customMessages:    map of custom constraint messages
     *      customConstraints: map of custom constraints to apply
     *      constraintTrigger: event that triggers contraint tests
     *      stopOnFirst:       determines if we break on first error
     *      errorFn:           error handler fn(field, failed)
     *      successFn:         called when all constraints pass
     *
     *///--------------------------------------------------------------------------  
    $.fn.constrain = function(o){
    
        /*-------------------------------------------------------------------------
         *
         *  Apply Options
         *
         *-------------------------------------------------------------------------
         *
         *  Merge the default and custom options resulting in a specific options
         *  map for this function call.
         *
         *///---------------------------------------------------------------------- 
        var options = $.extend({},defaultOptions, o);    

        /*-------------------------------------------------------------------------
         *
         *  Messages
         *
         *-------------------------------------------------------------------------
         *
         *  Constraint messages are built up from a set of defaults.  Merged with 
         *  a set of user defined messages.  When a constraint fails the first 
         *  message looked up is the id of the field followed by the name of the 
         *  constraint eg - txtName.mandatory.  If this does not exist or if the 
         *  field does not have an id it will ue the default message.
         *
         *///----------------------------------------------------------------------           
        if(o.customMessages){
            MessageSource.addMessages(o.customMessages);
        }
        
        /*-------------------------------------------------------------------------
         *
         *  Create Available Constraints
         *
         *-------------------------------------------------------------------------
         *
         *  Represents a map of all available constraints that can be applied to 
         *  a field.  This is created by merging customConstraints map from the 
         *  passed options with the default constraints.
         *
         *///----------------------------------------------------------------------
        if(options.customConstraints){
            ConstraintService.registerAll(options.customConstraints);
        }
        
        /*-------------------------------------------------------------------------
         *
         *  Apply Constraints
         *
         *-------------------------------------------------------------------------
         *
         *  This section discovers the required constraints on a per field basis 
         *  and applies the behaviour to the field
         *
         *///----------------------------------------------------------------------
        return this.each(function(){

            /*---------------------------------------------------------------------
             *
             *  No Constraint Failover
             *
             *---------------------------------------------------------------------
             *
             *  Most of this API is open to the public therefore open to the 
             *  irresponsible, ignorant, clueless and just plain stupid.  We need
             *  to cater for as much worst case edge cases as we can without 
             *  making the good people suffer.  Exit if no constraints defined on 
             *  element.
             *
             *///------------------------------------------------------------------
            if(!$(this).annotations("@Constraints")[0]){ return undefined };
            
            /*---------------------------------------------------------------------
             *
             *  Constraint Discovery
             *
             *---------------------------------------------------------------------
             *
             *  We extract the requested constraints via annotations on the field 
             *  and build a map of applicable constraints which is set as a data 
             *  item on the element.  At the same time we also set another map of 
             *  constratint data that can be passed into the constraint test 
             *  function
             *
             *///------------------------------------------------------------------
            var requestedConstraints = $(this).annotations("@Constraints")[0].data;
            for(constraint in requestedConstraints){
                /* filter out falsey values */
                if(requestedConstraints[constraint]){
                    ConstraintService.bind($(this), constraint, requestedConstraints[constraint]);
                }
            }
            
            /*---------------------------------------------------------------------
             *
             *  Event Binding
             *
             *---------------------------------------------------------------------
             *
             *  Bind the Constraint Trigger Callback to the trigger event.  Also
             *  we are passing in the options object so we still have access to
             *  the options object.  Done in this way to ensure we can unbind the
             *  callback function if the user wants.
             *
             *///------------------------------------------------------------------            
            $(this).bind((this.constraintTrigger = options.constraintTrigger), options, constraintCallback);
        });        
    }
    
    /*-----------------------------------------------------------------------------
     *
     *  Unconstrain Plugin
     *
     *-----------------------------------------------------------------------------
     *
     *  Removes any current constraints applied to the passed in fields as well
     *  as removing data caches and constraint events     
     *
     *  @plugin     
     *
     *///--------------------------------------------------------------------------     
    $.fn.unconstrain = function(){
        return this.each(function(){ ConstraintService.unconstrain(this); });
    }
    
    /*-----------------------------------------------------------------------------
     *
     *  Unconstrain Plugin
     *
     *-----------------------------------------------------------------------------
     *
     *  Helper function to remove all constraints form an entire form.
     *     
     *  @plugin
     *  @param frm - form/id/jQuery representing the form to process     
     *
     *///--------------------------------------------------------------------------             
    $.unconstrainForm = function(frm){
        ConstraintService.unconstrainAll($(frm)[0]);
    }      
     
    /*-----------------------------------------------------------------------------
     *
     *  Process Constraints Plugin
     *
     *-----------------------------------------------------------------------------
     *
     *  Extend the jQuery Object to provide a utility function that will perform
     *  the constraint application functionality to the form passed in
     *
     *  @plugin
     *  @param frm - form/id/jQuery representing the form to process
     *  @param o   - custom options object that accepts
     *
     *      customMessages:    map of custom constraint messages
     *      customConstraints: map of custom constraints to apply
     *      constraintTrigger: event that triggers contraint tests
     *      stopOnFirst:       determines if we break on first error
     *      errorFn:           error handler fn(field, failed)
     *      successFn:         called when all constraints pass
     *
     *///-------------------------------------------------------------------------- 
    $.constrainForm = function(frm,o){
        $.annotated("@Constraints", $(frm)[0]).constrain(o);
    }      
    
    /*-----------------------------------------------------------------------------
     *
     *  Default Options
     *
     *-----------------------------------------------------------------------------
     *
     *  This map represents the global default options that should be used 
     *  when applying constraints.  They can be overridden via custom maps
     *  passed into the functions.
     *
     *///--------------------------------------------------------------------------
    var defaultOptions = {
        customMessages    : {},           // map of custom constraint messages
        customConstraints : {},           // map of custom constraints to apply
        constraintTrigger : 'blur',       // event that triggers contraint tests
        stopOnFirst       : false,        // determines if we break on first error
        errorFn           : function(){}, // error handling callback
        successfn         : function(){}  // successfull callback
    }     

    /*-----------------------------------------------------------------------------
     *
     *  Constraint Service 
     *
     *-----------------------------------------------------------------------------
     *
     *  Singleton class used to handles page level contraint testing and 
     *  registration.
     *
     *  @constructor
     *
     *///--------------------------------------------------------------------------
    var ConstraintService = function(){
    
        /*-------------------------------------------------------------------------
         *
         *  Default Constraints
         *
         *-------------------------------------------------------------------------
         *
         *  Represents a map of all available constraints that can be applied to a 
         *  field.  This is merged with a custom constraints that can be declared 
         *  via the options object to provide a list of all available constraints 
         *  that can be applied to the fields
         *
         *///----------------------------------------------------------------------
        var defaultConstraints = {
            alpha:function(v){
                return $.isEmpty(v) || /^[a-z]*$/i.test(v);
            },
            alphanumeric:function(v){
                return $.isEmpty(v) || /^[a-z0-9]*$/i.test(v);
            },
            mandatory:function(v){
                return !$.isEmpty(v);
            },
            creditCard:function(v){
                return $.isEmpty(v) || /^\d{4}-?\d{4}-?\d{4}-?\d{4}$/.test(v);
            },
            currency:function(v){
                return $.isEmpty(v) || /^(\d{1,3},?(\d{3},?)*\d{3}(\.\d{0,2})?|\d{1,3}(\.\d{0,2})?|\.\d{1,2}?)$/.test(v);
            },
            digitsOnly:function(v){
                return $.isEmpty(v) || /^[0-9]*$/.test(v);
            },
            email:function(v){
                return $.isEmpty(v) || /^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i.test(v);
            },
            inList:function(v,l){
                return $.isEmpty(v) || $.inArray(v,l) > -1;
            },
            ip:function(v){
                return $.isEmpty(v) || /^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])$/.test(v);
            },
            matches:function(v,e){
                return $.isEmpty(v) || (e.constructor === RegExp) ? e.test(v) : r == v;
            },
            max:function(v,m){
                return $.isEmpty(v) || (!isNaN(v) && parseInt(v) <= m);
            },
            maxSize:function(v,m){
                return $.isEmpty(v) || v.length <= m;
            },
            min:function(v,m){
                return $.isEmpty(v) || (!isNaN(v) && parseInt(v) >= m);
            },
            minSize:function(v,m){
                return $.isEmpty(v) || v.length >= m;
            },
            notEqual:function(v,n){
                return $.isEmpty(v) || v != n;
            },
            range:function(v,d,e){

                if($.isEmpty(v)){ return true; }

                switch($(e).data("constraints").type){
                    case 'boolean': return /^(true)|(false)$/i.test(v);
                    case 'date':    return !isNaN(Date.parse(v)) && Date.parse(v) >= Date.parse(d.start) && Date.parse(v) <= Date.parse(d.end);
                    case 'float':   return !isNaN(v) && parseFloat(v) >= d.start && parseFloat(v) <= d.end;
                    case 'int':     return !isNaN(v) && parseInt(v) >= d.start && parseInt(v) <= d.end;
                    case 'string':
                    default:        return v >= d.start && v <= d.end;
                }
            },
            size:function(v,d){
                return $.isEmpty(v) || ((d.end) ? v.length >= d.start && v.length <= d.end : v.length === d.start);
            },
            type:function(v,t){

                if($.isEmpty(v)){ return true; }

                switch(t){
                    case 'boolean': return /^(true)|(false)$/i.test(v);
                    case 'date':    return !isNaN(Date.parse(v));
                    case 'float':   return !isNaN(parseFloat(v));
                    case 'int':     return !isNaN(parseInt(v)) && /^-?[0-9]+$/.test(v);
                    case 'string':  return v.constructor === String;
                    default:        throw(t + " is not a valid field type");
                }
            },
            url:function(v){
                return $.isEmpty(v) || /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/.test(v);
            },
            validator:function(v,f,e){
                return $.isEmpty(v) || f(v,e);
            }
        };
        
        /*-------------------------------------------------------------------------
         *
         *  Manipulate Constraints
         *
         *-------------------------------------------------------------------------
         *
         *  Helper function that wraps the default pre and post functionality.
         *
         *  Accepts a target element and a processing function that accepts the 
         *  constraint cache and constratin data cache for that element and allows
         *  you to edit it before setting the values back to the cache.
         *
         *///----------------------------------------------------------------------        
        var manipulateConstraints = function(el, f){
            
            var c = $(el).data("constraints") || {};
            var cd = $(el).data("constraint-data") || {};                  

            f(c, cd);

            $(el).data("constraints", c);
            $(el).data("constraint-data", cd);                        
        };
        
        /*-------------------------------------------------------------------------
         *
         *  Has Constraints?
         *
         *-------------------------------------------------------------------------
         *
         *  Helper function that d
         *
         *///----------------------------------------------------------------------        
        var hasConstraint = function(el,constraint){
            return !!($(el).data("constraints") || {})[constraint];
        }        
        
        /*-------------------------------------------------------------------------
         *
         *  Initialisation
         *
         *-------------------------------------------------------------------------
         *
         *  Set up the initial state of the Constraint Service.
         *
         *///----------------------------------------------------------------------         
        var availableConstraints = $.extend({},defaultConstraints);
        
        /*-------------------------------------------------------------------------
         *
         *  Constraint Service Public API
         *
         *-------------------------------------------------------------------------
         *
         *  Publically available functions for the Constraints Service
         *
         *///----------------------------------------------------------------------         
        return {
        
            /*---------------------------------------------------------------------
             *
             *  Register Constraint
             *
             *---------------------------------------------------------------------
             *
             *  Registers a single constraint with the Constraint Service so it 
             *  can be used going forward by fields
             *
             *  @param name - name of the constraint to apply
             *  @param test - test function for constraint
             *
             *///------------------------------------------------------------------        
            register: function(name, test){                
                availableConstraints[name] = test;
            },
            
            /*---------------------------------------------------------------------
             *
             *  Register Constraints
             *
             *---------------------------------------------------------------------
             *
             *  Registers a group of constraints at one time.  Takes a map of name
             *  function pairs and applies it to the available constraints
             *
             *  @param constraints - {name:test,...} map of constraints
             *
             *///------------------------------------------------------------------             
            registerAll: function(constraints){
                $.extend(availableConstraints, constraints);
            },
            
            /*---------------------------------------------------------------------
             *
             *  Get Constraint Test
             *
             *---------------------------------------------------------------------
             *
             *  Returns a, if defined, the associated constratin test funciton 
             *  otherwise it returns undefined.
             *
             *  @param name - name of constaint to return
             *
             *///------------------------------------------------------------------                         
            get: function(name){
                return availableConstraints[name];
            },
            
            /*---------------------------------------------------------------------
             *
             *  Bind Constraint
             *
             *---------------------------------------------------------------------
             *
             *  Performs constraint binding work on the passed in element.  More
             *  specifically it loads and cahces the constraint and data on to the
             *  element so the global constraint function can manipulate it.
             *
             *  @param element - target element to bind constraint to
             *  @param constraint - name/id of constraint to apply             
             *  @param data - constratin data applicable to bound contraint
             *
             *///------------------------------------------------------------------                         
            bind: function(element, constraint, data){            
                manipulateConstraints(element, function(constraints,constraintData){
                    constraints[constraint] = ConstraintService.get(constraint);
                    constraintData[constraint] = data;
                });
            },
            
            /*---------------------------------------------------------------------
             *
             *  Unbind Constraint
             *
             *---------------------------------------------------------------------
             *
             *  Removes the constraint from the passed in element including data
             *
             *  @param element - target element to remove constraint from
             *  @param constraint - name/id of constraint to remover                      
             *
             *///------------------------------------------------------------------                                     
            unbind: function(element, constraint){
                manipulateConstraints(element, function(constraints,constraintData){
                    delete(constraints[constraint]);
                    delete(constraintData[constraint]);
                });                
            },
            
            /*---------------------------------------------------------------------
             *
             *  Unbind All Constraints
             *
             *---------------------------------------------------------------------
             *
             *  Globally unbinds constraint from all elements under the passed in
             *  root element (document is default).
             *
             *  @param constraint - name/id of constraint to remove     
             *  @param root - the root element to begin removal     
             *
             *///------------------------------------------------------------------              
            unbindAll : function(constraint, root){
                $.annotated("@Constraints", root).each(function(){
                    if(hasConstraint(this,constraint)){
                        ConstraintService.unbind(this, constraint);
                    }
                });
            },
            
            /*---------------------------------------------------------------------
             *
             *  Unconstrain
             *
             *---------------------------------------------------------------------
             *
             *  Removes constraint related data and events from the element
             *
             *  @param root - the root element to begin removal     
             *
             *///------------------------------------------------------------------             
            unconstrain : function(el){

                /* remove data */
                $(el).removeData("constraints");
                $(el).removeData("constraint-data");

                /* unbind events */
                $(el).unbind(this.constraintTrigger, constraintCallback);

                /* delete dom expando's */
                delete(el.constraintTrigger);                
            },
            
            /*---------------------------------------------------------------------
             *
             *  Unconstrain All
             *
             *---------------------------------------------------------------------
             *
             *  Globally destroys all constraints including data and events
             *
             *  @param root - the root element to begin removal     
             *
             *///------------------------------------------------------------------                                      
            unconstrainAll : function(root){
                $.annotated("@Constraints", root).each(function(){
                    ConstraintService.unconstrain(this)
                });
            }
        }
    }();
    
    /*-----------------------------------------------------------------------------
     *
     *  Message Source
     *
     *-----------------------------------------------------------------------------
     *
     *  This Singleton encapsulates message source functionaltiy related 
     *  specifically to loading messages for constraints.
     *
     *  @constructor
     *
     *///--------------------------------------------------------------------------   
    var MessageSource = (function(){

        /*-------------------------------------------------------------------------
         *
         *  Default Messages
         *
         *-------------------------------------------------------------------------
         *
         *  This map the default messages that should be applied to a 
         *  field when there are no custom messages to be applied to a failed 
         *  constraint.
         *
         *///----------------------------------------------------------------------           
        var defaultMessages = {
            alpha        : "${field} must be composed of alpha characters A-Z,a-z",
            alphaNumeric : "${field} must be composed of alphanumeric characters A-Z,a-z,0-9",
            mandatory    : "${field} is a mandatory field",
            creditCard   : "${field} must be a valid credit card number",
            currency     : "${value} is not a valid currency format",
            digitsOnly   : "${field} must be composed of only digits",
            email        : "${field} must be a valid email address",
            indexed      : "${field} is indexed and no indexed fields contain values",
            inList       : "${field} must be one of the following values: ${data}",
            ip           : "${field} must be a valid IP Address",
            matches      : "${field} does not match the required expression",
            max          : "${field} cannot exceed ${data}",
            maxSize      : "${field} cannot exceeds the maximum length (${data})",
            min          : "${field} cannot be less than ${data}",
            minSize      : "${field} cannot is less than the minimum length (${data})",
            notEqual     : "${field} cannot have the value of ${value}",
            range        : "${field} must be between ${start} and ${end}",
            size         : "${field} is not the stated size",
            type         : "${field} is not the correct type (${data})",
            url          : "${field} must be a valid URL",
            validator    : "${field} has failed custom validation"
        };
        
        var ms = {"default":defaultMessages}
                
        /*-------------------------------------------------------------------------
         *
         *  Format Message Function
         *
         *-------------------------------------------------------------------------
         *
         *  Private function to apply data to a templated message
         *
         *  @param m - the unformatted message
         *  @param e - the element in question
         *  @param k - the key for the data-cache to get extra data from
         *  @param d - the data-cache for extra template options
         *
         *///----------------------------------------------------------------------    
        var formatMessage = function(m,e,k,d){

            var cdata = ((d) ? d[k] : null) || {};

            if(cdata.constructor !== Object){
                cdata = {data : cdata}
            }

            var data = $.extend({}, {field:e.id || e.name, value: $(e).val()}, cdata), fm = m;

            for(d in data){
                fm = fm.replace(new RegExp('\\${'+d+'}','g'), data[d].toString());
            }

            return fm;
        };  
        
        return {   
        
            /*---------------------------------------------------------------------
             *
             *  Get Message Function
             *
             *---------------------------------------------------------------------
             *
             *  This function interrogates a passed element and returns a the 
             *  message that most closely matches the proximity.  If 
             *  <element_name/element_id>.<k>  exists then this is returned as an 
             *  evaluated message.  Otherwise it looks up the global default 
             *  message default.<key>.
             *
             *  @param e - source element
             *  @param k - key for the message
             *
             *///------------------------------------------------------------------
            getMessage:function(e,k){
                return formatMessage(((e && e.id && ms[e.id])?ms[e.id][k]:ms["default"][k]) || "",e,k,(e)?$(e).data("constaint-data"):null);
            },
            
            /*---------------------------------------------------------------------
             *
             *  Add Messages Function
             *
             *---------------------------------------------------------------------
             *
             *  Add new custom messages to the messageSource
             *
             *///------------------------------------------------------------------            
            addMessages:function(c){
                ms = $.extend(true,ms,c);
            },
            
            /*---------------------------------------------------------------------
             *
             *  Add Message Function
             *
             *---------------------------------------------------------------------
             *
             *  Add a new custom message to the messageSource
             *
             *///------------------------------------------------------------------              
            addMessage: function(namespace, msg){
                MessageSource.addMessages({namespace:msg});
            },
            
            /*---------------------------------------------------------------------
             *
             *  Remove Namespace Function
             *
             *---------------------------------------------------------------------
             *
             *  Delete an entire namespace from the messageSource
             *
             *///------------------------------------------------------------------            
            removeNamespace: function(namespace){
                delete(ms[namespace]);
            },
            
            /*---------------------------------------------------------------------
             *
             *  Remove Message Function
             *
             *---------------------------------------------------------------------
             *
             *  Delete a single message from a certain namespace.  Will also
             *  remove the namespace if it is empty.
             *
             *///------------------------------------------------------------------            
            removeMessage: function(namespace, msg){
            
                delete(ms[namespace][msg]);                
            
                for(t in ms[namespace]){
                    return undefined;
                }                
            
                removeNamespace(ms[namespace]);                
            }
        };       
    })();
    
    /*-----------------------------------------------------------------------------
     *
     *  Constraints Callback
     *
     *-----------------------------------------------------------------------------
     *
     *  Handles the constratint management when a constraint action is triggered.
     *
     *///--------------------------------------------------------------------------     
    function constraintCallback(event){
        
        var cs = $(this).data("constraints");
        var cd = $(this).data("constraint-data");

        var failedConstraints = [];

        for(c in cs){

            var pass = cs[c]($(this).val(), cd[c], this);

            if(!pass){

                failedConstraints.push({
                    constraint : c,
                    message    : MessageSource.getMessage(this,c)
                });

                if(event.data.stopOnFirst){ break; }
            }
        }

        event.data[(failedConstraints.length > 0)?'errorFn':'successFn'](this, failedConstraints);
    }       
    
    /*-----------------------------------------------------------------------------
     *
     *  Constraints Public API
     *
     *-----------------------------------------------------------------------------
     *
     *  Extend the core jQuery object to allow access to the public functions
     *  for the Constraint functionality
     *
     *///-------------------------------------------------------------------------- 
    $.extend({         
    
        /* 
         * fn.constrain
         * fn.unconstrain
         * constrainForm
         * unconstrainForm
         */
         
        cs : {
        
            //-- DEFAULT OPTIONS API ----------------------------------------------
            defaultOptions         : defaultOptions,

            //-- CONSTRAINT SERVICE API -------------------------------------------
            register               : ConstraintService.register,
            registerAll            : ConstraintService.registerAll,
            bind                   : ConstraintService.bind,
            unbind                 : ConstraintService.unbind,
            unconstrain            : ConstraintService.unconstrain,
            unconstrainAll         : ConstraintService.unconstrainAll,
                        
            //-- MESSAGE SOURCE API -----------------------------------------------
            addMessages            : MessageSource.addMessages,
            addMessage             : MessageSource.addMessage,
            getMessage             : MessageSource.getMessage,
            deleteMessage          : MessageSource.removeMessage,
            deleteMessageNamespace : MessageSource.removeNamespace,
            
            /*---------------------------------------------------------------------
             *
             *  Unistall Constrains plugin
             *
             *---------------------------------------------------------------------
             *
             *  Unistall the entire plugin by deregistering all events and data
             *  caches in the document but also delete the objects from memory.
             *
             *///------------------------------------------------------------------ 
            uninstall: function(){
               
               /* remove constraint data and events */
               ConstraintService.unconstrainAll(document);
               
               /* delete the object from global object */
               delete(constraintCallback);
               delete(MessageSource);
               delete(ConstraintService);
               delete(defaultOptions);
               delete($.isEmpty);
               delete($.constrainForm);
               delete($.unconstrainForm);
               delete($.fn.constrain);
               delete($.fn.unconstrain);
               delete($.constraints);
            }
        }
    });
    
})(jQuery);

Revision: 9198
at October 25, 2008 13:01 by kouphax


Initial Code
/*---------------------------------------------------------------------------------
 *
 *  Process Constraints jQuery Plugin
 *
 *---------------------------------------------------------------------------------
 *
 *  This plugin is responsible for apply constraints to a target form.  It 
 *  will examin each fiels annotations to find @Constraints annotation and 
 *  appply the requested constraints to the field.  An number of configuration 
 *  options are available (see below)
 *
 *  @author  James Hughes
 *
 *  -------------------------------------------------------------------------------
 *  24/10/2008 - Initial Version
 *  -------------------------------------------------------------------------------
 *
 *///------------------------------------------------------------------------------
 (function($){
     
    /*-----------------------------------------------------------------------------
     *
     *  isEmpty Utility Function
     *
     *-----------------------------------------------------------------------------
     *
     *  Utility function that ensures a string is either null or an empty string.
     *
     *  @plugin
     *  @param v - the value to check for emptiness
     *
     *///--------------------------------------------------------------------------
    $.isEmpty = function(v){
        return !v || v == "" || $.trim(v) == "";
    };
    
    /*-----------------------------------------------------------------------------
     *
     *  Message Source
     *
     *-----------------------------------------------------------------------------
     *
     *  This object encapsulates message source functionaltiy related 
     *  specifically to loading messages for constraints.
     *
     *  @constructor
     *  @param d - default message map
     *  @param c - custom message map
     *  @param o - options map (only accepts the data-cache-key as extraData)
     *
     *///--------------------------------------------------------------------------
    var MessageSource = function(d,c,o){
     
        /*-------------------------------------------------------------------------
         *
         *  Initialisation
         *
         *-------------------------------------------------------------------------
         *
         *  Set up the message source by apply custom messages over default 
         *  messages in a deep manner.  Also apply the instance options
         *
         *///----------------------------------------------------------------------     
        this.ms = $.extend(true,{},d,c);
        this.op = $.extend({}, {extraData:""}, o);
                 
        /*-------------------------------------------------------------------------
         *
         *  Format Message Function
         *
         *-------------------------------------------------------------------------
         *
         *  Private function to apply data to a templated message
         *
         *  @param m - the unformatted message
         *  @param e - the element in question
         *  @param k - the key for the data-cache to get extra data from
         *  @param d - the data-cache for extra template options
         *
         *///----------------------------------------------------------------------    
        this.formatMessage = function(m,e,k,d){
            var cdata = ((d) ? d[k] : null) || {};
            if(cdata.constructor !== Object){
                cdata = {data : cdata}
            }
            var data = $.extend({}, {field:e.id || e.name, value: $(e).val()}, cdata), fm = m;
            for(d in data){
                fm = fm.replace(new RegExp('\\${'+d+'}','g'), data[d].toString());
            }
            return fm;
        };           
    }
    /*-----------------------------------------------------------------------------
     *
     *  Get Message Function
     *
     *-----------------------------------------------------------------------------
     *
     *  This function interrogates a passed element and returns a the message that
     *  most closely matches the proximity.  If <element_name/element_id>.<k> 
     *  exists then this is returned as an evaluated message.  Otherwise it looks
     *  up the global default message default.<key>.
     *
     *  @param e - source element
     *  @param k - key for the message
     *
     *///--------------------------------------------------------------------------
    MessageSource.prototype.getMessage =function(e,k){
        return this.formatMessage(((e && e.id && this.ms[e.id])?this.ms[e.id][k]:this.ms["default"][k]) || "",e,k,(e)?$(e).data(this.op.extraData):null);
    };    
 
    /*-----------------------------------------------------------------------------
     *
     *  Process Constraints Plugin
     *
     *-----------------------------------------------------------------------------
     *
     *  Extend the jQuery Object to provide a utility function that will perform
     *  the constraint application functionality to the form passed in
     *
     *  @plugin
     *  @param frm - form/id/jQuery representing the form to process
     *  @param o   - custom options object that accepts
     *
     *      customMessages:    map of custom constraint messages
     *      customConstraints: map of custom constraints to apply
     *      constraintTrigger: event that triggers contraint tests
     *      stopOnFirst:       determines if we break on first error
     *      errorHandler:      error handler fn(field, failed)
     *      successHandler:    called when all constraints pass
     *
     *///-------------------------------------------------------------------------- 
    $.applyConstraints = function(frm,o){
        /*-------------------------------------------------------------------------
         *
         *  Set options
         *
         *-------------------------------------------------------------------------
         *
         *  Custom options can be passed into the function and these are merged 
         *  with the default options to create a set of options used through out 
         *  the function
         *
         *///----------------------------------------------------------------------
        var defaultOptions = {
            customMessages    : {},           // map of custom constraint messages
            customConstraints : {},           // map of custom constraints to apply
            constraintTrigger : 'blur',       // event that triggers contraint tests
            stopOnFirst       : false,        // determines if we break on first error
            errorHandler      : function(){}, // error handling callback
            successHandler    : function(){}  // successfull callback
        }
        var options = $.extend({},defaultOptions, o);
        /*-------------------------------------------------------------------------
         *
         *  Messages
         *
         *-------------------------------------------------------------------------
         *
         *  Constraint messages are built up from a set of defaults.  Merged with 
         *  a set of user defined messages.  When a constraint fails the first 
         *  message looked up is the id of the field followed by the name of the 
         *  constraint eg - txtName.mandatory.  If this does not exist or if the 
         *  field does not have an id it will ue the default message.
         *
         *///----------------------------------------------------------------------    
        var defaultMessages = {
            default : {
                alpha        : "${field} must be composed of alpha characters A-Z,a-z",
                alphaNumeric : "${field} must be composed of alphanumeric characters A-Z,a-z,0-9",
                mandatory    : "${field} is a mandatory field",
                creditCard   : "${field} must be a valid credit card number",
                currency     : "${value} is not a valid currency format",
                digitsOnly   : "${field} must be composed of only digits",
                email        : "${field} must be a valid email address",
                indexed      : "${field} is indexed and no indexed fields contain values",
                inList       : "${field} must be one of the following values: ${data}",
                ip           : "${field} must be a valid IP Address",
                matches      : "${field} does not match the required expression",
                max          : "${field} cannot exceed ${data}",
                maxSize      : "${field} cannot exceeds the maximum length (${data})",
                min          : "${field} cannot be less than ${data}",
                minSize      : "${field} cannot is less than the minimum length (${data})",
                notEqual     : "${field} cannot have the value of ${value}",
                range        : "${field} must be between ${start} and ${end}",
                size         : "${field} is not the stated size",
                type         : "${field} is not the correct type (${data})",
                url          : "${field} must be a valid URL",
                validator    : "${field} has failed custom validation"
            }
        }
        var messageSource = new MessageSource(defaultMessages, options.customMessages, {extraData:'constraint-data'});
        /*-------------------------------------------------------------------------
         *
         *  Constraint Test Functions
         *
         *-------------------------------------------------------------------------
         *
         *  Represents a map of all available constraints that can be applied to a 
         *  field.  This is merged with a custom constraints that can be declared 
         *  via the options object to provide a list of all available constraints 
         *  that can be applied to the fields
         *
         *///----------------------------------------------------------------------
        var defaultConstraints = {
            alpha:function(v){
                return $.isEmpty(v) || /^[a-z]*$/i.test(v);
            },
            alphanumeric:function(v){
                return $.isEmpty(v) || /^[a-z0-9]*$/i.test(v);
            },
            mandatory:function(v){
                return !$.isEmpty(v);
            },
            creditCard:function(v){
                return $.isEmpty(v) || /^\d{4}-?\d{4}-?\d{4}-?\d{4}$/.test(v);
            },
            currency:function(v){
                return $.isEmpty(v) || /^(\d{1,3},?(\d{3},?)*\d{3}(\.\d{0,2})?|\d{1,3}(\.\d{0,2})?|\.\d{1,2}?)$/.test(v);
            },
            digitsOnly:function(v){
                return $.isEmpty(v) || /^[0-9]*$/.test(v);
            },
            email:function(v){
                return $.isEmpty(v) || /^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i.test(v);
            },
            
            /*---------------*/
            /* EXPERIMENTAL! */
            indexed:function(v,e){           
                var frm = $(e).parent("form");            
                
                if(frm){
                    var notEmpty = $.grep(frm.data("indexed-fields"), 
                        function(){
                           return !$.isEmpty($(this).val())
                        }
                    );
                    
                    return notEmpty.length != 0;
                }else{
                    throw("Indexed constraints cannot be used outside of a form");
                }
            },
            /*---------------*/
            
            inList:function(v,l){
                return $.isEmpty(v) || $.inArray(v,l) > -1;
            },
            ip:function(v){
                return $.isEmpty(v) || /^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])$/.test(v);
            },
            matches:function(v,e){
                return $.isEmpty(v) || (e.constructor === RegExp) ? e.test(v) : r == v;
            },
            max:function(v,m){
                return $.isEmpty(v) || (!isNaN(v) && parseInt(v) <= m);
            },
            maxSize:function(v,m){
                return $.isEmpty(v) || v.length <= m;
            },
            min:function(v,m){
                return $.isEmpty(v) || (!isNaN(v) && parseInt(v) >= m);
            },
            minSize:function(v,m){
                return $.isEmpty(v) || v.length >= m;
            },
            notEqual:function(v,n){
                return $.isEmpty(v) || v != n;
            },
            range:function(v,d,e){
                if($.isEmpty(v)){ return true; }
                switch($(e).data("constraints").type){
                    case 'boolean': return /^(true)|(false)$/i.test(v);
                    case 'date':    return !isNaN(Date.parse(v)) && Date.parse(v) >= Date.parse(d.start) && Date.parse(v) <= Date.parse(d.end);
                    case 'float':   return !isNaN(v) && parseFloat(v) >= d.start && parseFloat(v) <= d.end;
                    case 'int':     return !isNaN(v) && parseInt(v) >= d.start && parseInt(v) <= d.end;
                    case 'string':
                    default:        return v >= d.start && v <= d.end;
                }
            },
            size:function(v,d){
                return $.isEmpty(v) || ((d.end) ? v.length >= d.start && v.length <= d.end : v.length === d.start);
            },
            type:function(v,t){
                if($.isEmpty(v)){ return true; }
                switch(t){
                    case 'boolean': return /^(true)|(false)$/i.test(v);
                    case 'date':    return !isNaN(Date.parse(v));
                    case 'float':   return !isNaN(parseFloat(v));
                    case 'int':     return !isNaN(parseInt(v)) && /^-?[0-9]+$/.test(v);
                    case 'string':  return v.constructor === String;
                    default:        throw(t + " is not a valid field type");
                }
            },
            url:function(v){
                return $.isEmpty(v) || /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/.test(v);
            },
            validator:function(v,f,e){
                return $.isEmpty(v) || f(v,e);
            }
        }
        var availableConstraints = $.extend({},defaultConstraints,options.customConstraints);
        /*-------------------------------------------------------------------------
         *
         *  Apply Constraints
         *
         *-------------------------------------------------------------------------
         *
         *  This section discovers the required constraints on a per field basis 
         *  and applies the behaviour to the field
         *
         *///----------------------------------------------------------------------
        $.annotated("@Constraints", $(frm)[0]).each(function(){

            /*---------------------------------------------------------------------
             *
             *  Constraint Discovery
             *
             *---------------------------------------------------------------------
             *
             *  We extract the requested constraints via annotations on the field 
             *  and build a map of applicable constraints which is set as a data 
             *  item on the element.  At the same time we also set another map of 
             *  constratint data that can be passed into the constraint test 
             *  function
             *
             *///------------------------------------------------------------------
            var requestedConstraints = $(this).annotations("@Constraints")[0].data;
            var applicableConstraints = {};
            var constraintData = {};
            var indexedFields = [];
            for(constraint in requestedConstraints){
                /* filter out falsey values */
                if(requestedConstraints[constraint]){
                    applicableConstraints[constraint] = availableConstraints[constraint];
                    constraintData[constraint] = requestedConstraints[constraint];
                    
                    if(constraint === "indexed"){
                        indexedFields.push(this);
                    }
                }
            }
            $(this).data("constraints", applicableConstraints);
            $(this).data("constraint-data", constraintData);
            
            $(frm).data("indexed-fields", indexedFields);
            /*---------------------------------------------------------------------
             *
             *  Add Constraint Behaviour
             *
             *---------------------------------------------------------------------
             *
             *  This section determines the event that triggers the constraint 
             *  test.  By default it will use the blur event but this could be the 
             *  change or keypress events.
             *
             *  When the trigger event is fired the fields constraints are loaded 
             *  and each one is processed.  When all errors have been collected 
             *  the appropriate handler is fired.
             *
             *///------------------------------------------------------------------
            $(this)[options.constraintTrigger](function(){
                var cs = $(this).data("constraints");
                var cd = $(this).data("constraint-data");
                var failedConstraints = [];
                for(c in cs){
                    var pass = cs[c]($(this).val(), cd[c], this);
                    if(!pass){
                        failedConstraints.push({
                            constraint : c,
                            message    : messageSource.getMessage(this,c)
                        });
                        if(options.stopOnFirst){ break; }
                    }
                }
                options[(failedConstraints.length > 0)?'errorHandler':'successHandler'](this, failedConstraints);
            })
        });
    }       
})(jQuery);

Initial URL


Initial Description
Constraints plugin (requires annotation plugin).

This is the second draft version that has been severly refactoed both internally and externally.

Initial Title
Constraints jQuery Plugin

Initial Tags
javascript, jquery

Initial Language
JavaScript