/ Published in: JavaScript
Constraints plugin (requires annotation plugin).
This is the second draft version that has been severly refactoed both internally and externally.
Expand |
Embed | Plain Text
/*--------------------------------------------------------------------------------- * * 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);
You need to login to post a comment.
