# FluxValidator - AJAX Form Validation for Rails # Copyright (c) 2007 Peter Gumeson # # FluxValidator is freely distributable under the terms of an MIT license module FluxValidator def self.included(base) base.extend ClassMethods base.class_inheritable_accessor(:flux_validator_configuration) base.flux_validator_configuration = ::FluxValidator::Configuration.new end module ClassMethods # Allows FluxValidator to validate fields that # are not named the same as the model class. # # alias_model , # # Usage Examples: # alias_model :my_user, :user # alias_model 'my_user', User # alias_model :my_user, 'User' # # Now the 'my_user' fields below will be # validated with the User model. # # <%= text_field :my_user, :login %> # <%= text_field 'my_user', :email %> # # def alias_model(name, model) raise ArgumentError, "name cannot be nil for alias_model" if name.nil? raise ArgumentError, "model cannot be nil for alias_model" if model.nil? name = name.to_s.underscore.to_sym klass = model.to_s.camelize.constantize rescue nil unless klass && klass.respond_to?('new') && klass.new.is_a?(ActiveRecord::Base) raise ArgumentError, "alias_model did not recognize #{model} as a valid model object" end flux_validator_configuration.aliases[name] = klass end end #ClassMethods # Stores the aliases for each controller class Configuration #:nodoc: attr_accessor :aliases def initialize @aliases = Hash.new end end # Creates and validates a hash of ActiveRecord objects # based on the names of incoming form params, or based # on the names found in the @mapping hash. # # Example: # # This would use the User model to create an instance variable # named @user and would then call @user.valid?() # # Example: # # If the MyUser class does not exist, you would need to map # my_user to its class name with something like this: # @mapping['my_user'] = User # Now it would use the User model to create an instance variable # named @my_user and would then call @my_user.valid?() # def validate_form @records = {} params.each_pair do |name, value| if value.is_a?(Hash) type = aliases.has_key?(name.to_sym) ? aliases[name.to_sym].to_s : name record = get_instance(name, type, params[name]) unless record.nil? record.valid? @records[name] = record end # TODO: enable field with index functionality #value.each_pair do |iname, ivalue| # if ivalue.is_a?(Hash) # itype = aliases.has_key?(iname.to_sym) ? aliases[iname.to_sym].to_s : iname # record = get_instance("#{name}_#{iname}", itype, params[name][iname]) # unless record.nil? # record.valid? # @records["#{name}_#{iname}"] = record # end # end #end end end render :update do |page| # Create a JavaScript hash of field error # messages keyed by the field name page << "var formErrors = new Hash();" @records.each_pair do |object, record| fields = [] record.errors.each { |attr, msg| fields << attr } fields.uniq.each do |method| id = "#{object}_#{method}" html = inner_error_messages_on(object.to_sym, method.to_sym) page << "formErrors['#{id}'] = '#{escape_javascript(html)}';" end end # Loop through each error message and update # the field if any error message has changed javascript = <<-END_OF_SCRIPT formErrors.each(function(error, index) { var scope = error.key+'_scope'; var fieldSpan = $(error.key+'_field'); if (fieldSpan != null && fieldSpan.hasClassName('fieldWithoutErrors')) { fieldSpan.removeClassName('fieldWithoutErrors'); fieldSpan.addClassName('fieldWithErrors'); } var errorSpan = $(error.key+'_errors'); var html = error.value; if (errorSpan != null && errorSpan.hasClassName('formErrors')) { if (errorSpan.innerHTML != '') { if (!errorSpan.equals(html)) { new Effect.SlideUp(errorSpan, {duration: 0.5, queue:{position:'front',scope:scope}, afterFinish:function(){ errorSpan.update(html); new Effect.SlideDown(errorSpan, {duration: 0.5, queue:{position:'end',scope:scope}}); }}); } else if (errorSpan.id == FluxValidator.source+'_errors') { new Effect.Shake(errorSpan, 10, {queue:{position:'front',scope:scope}}); } } else { errorSpan.update(html); new Effect.SlideDown(errorSpan, {duration: 0.5, queue:{position:'end',scope:scope}}); } } }); END_OF_SCRIPT page << javascript # Loop through each fieldWithErrors span and toggle any # 'fieldWithErrors' class to 'fieldWithoutErrors' if there # is no longer an error on the field javascript = <<-END_OF_SCRIPT var fieldSpans = FluxValidator.form.getElementsByClassName('fieldWithErrors'); fieldSpans.each(function(span, index) { id = span.readAttribute('id'); if (id != null) { key = id.sub('_field', ''); if (formErrors.keys().indexOf(key) == -1) { span.removeClassName('fieldWithErrors'); span.addClassName('fieldWithoutErrors'); } } }); END_OF_SCRIPT page << javascript # Loop through each formErrors span and hide it if there # is no longer an error on the field javascript = <<-END_OF_SCRIPT var errorSpans = $$('span.formErrors'); errorSpans.each(function(span, index) { id = span.readAttribute('id'); key = id.sub('_errors', ''); if (formErrors.keys().indexOf(key) == -1 && span.innerHTML != '') { new Effect.SlideUp(span, {duration: 0.5, queue:'end', afterFinish:function(){ span.update('') }}); } }); END_OF_SCRIPT page << javascript end #render end #validate_form # Let controller respond to validate_(action_name)_form # action calls and route to validate_form instead def method_missing(method_name, *args) if (method_name.to_s.starts_with?('validate_') && method_name.to_s.ends_with?('_form')) send(validate_form, args) elsif (method_name.class != Symbol) # Allow super closure raise ActionController::UnknownAction, "No action responded to #{action_name}", caller end end private # Access the hash of aliased model names # from this controller's configuration def aliases flux_validator_configuration.aliases end # Finds or creates an instance variable with name and type # and initialize it with supplied params hash def get_instance(name=nil, type=nil, params=nil) klass = type.camelize.to_class if klass.nil? message = "FluxValidator could not find a model named #{type.camelize} to validate form fields named '#{name}'. " + "Try adding an alias to your #{self.class} that defines a valid model class for '#{name}':\n\nalias_model :#{name}, \n\n" RAILS_DEFAULT_LOGGER.error("\nERROR: #{message}") return nil end if obj = instance_variable_get("@#{name}") unless obj.instance_of?(klass) message = "FluxValidator found an instance variable named @#{name} but it was not a valid #{type.camelize} object. " + "Make sure your #{self.class}.validate_form() method assigns a #{type.camelize} model to @#{name} and not #{obj.class}." raise(TypeError, message) end else instance = klass.new add_mass_assign_attributes_method(instance) instance.mass_assign_attributes(params) instance_variable_set("@#{name}", instance) end if obj.nil? obj = instance_variable_get("@#{name}") obj.attributes = params end return obj end # Create a +mass_assign_attributes+ method on a single instance of # ActiveRecord that will allow the mass assignment of attributes even # if the model has +attr_protected+ or +attr_accessible+ defined. # This is safe because we never save this instance of the model. def add_mass_assign_attributes_method(obj) klass = class <