Skip to content

Instantly share code, notes, and snippets.

@trshafer
Created March 3, 2011 21:51
Show Gist options
  • Save trshafer/853680 to your computer and use it in GitHub Desktop.
Save trshafer/853680 to your computer and use it in GitHub Desktop.
initializers/formtastic.rb
require 'formtastic/validations'
module Formtastic
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
@@validate_by_default = false
cattr_accessor :validate_by_default
end
end
Formtastic::SemanticFormBuilder.validate_by_default = true
var Validations = {
validates_presence_of: function(value, options) {
return (/[^\s]/).test(value) && value;
},
//we've got three types of length
// within which is a range {first: 12, last: 200, excludeEnd: true}
// minimum and maximum are obvi
validates_length_of: function(value, options) {
if (options.within != null) {
// you're welcome aaron: I didn't use a ternary
if (options.within.excludeEnd) {
return (value.length >= options.within.first) && (value.length <
options.within.last);
} else {
//console.log('vlo within include end: '+((value.length >= options.within.first) && (value.length <= options.within.last)))
return ((value.length >= options.within.first) && (value.length
<= options.within.last));
}
} else if (options.maximum != null) {
return value.length <= options.maximum;
} else if (options.minimum != null) {
return value.length >= options.minimum;
} else {
return false;
}
},
validates_numericality_of: function(value, options) {
if (! (/^\d+(\.\d+)?$/).test(value)) {
return false;
} else if (options.greater_than != null) {
return parseInt(value, 10) > options.greater_than;
} else {
return true;
}
},
validates_size_of: function(value, options) {
return Validations.validates_length_of(value, options);
},
validates_format_of: function(value, options) {
reg = new RegExp(options['with'].regexp, options['with'].options);
return reg.test(value);
},
validates_inclusion_of: function(value, options) {
return (options['in'].indexOf(value) != -1);
},
validates_exclusion_of: function(value, options) {
return ! Validations.validates_inclusion_of(value, options);
},
validates_confirmation_of: function(value, options) {
if (console) {
console.log("Validates confirmation of does nothing");
}
return true;
},
validates_acceptance_of: function(value, options) {
console.log("Validates acceptance of does nothing");
return false;
}
};
# Usage:
# semantic_form_for @patient, :validate => true
# this will add data-validate on all inputs that have simple ActiveRecord validations
# it adds a 'validate' class on the form
# using semantic_form_for @patient, :validate => :no_class
# will add the data-validate, but not the validate class on the form
# if you specify :if or :except in the conditions, it'll add :data-conditional-validation => true on the input
# you can set Formtastic::SemanticFormBuilder.validate_by_default = true
# to default validate so you don't have to pass :validate => true on all
# for the input you can specify :skip_validation => true
# This will still add the validations to the dom, but the javascript skips
# validations on that field
#
module ActiveRecord
class Base
def self.validation_name_to_i18n(validation_name)
{
:validates_acceptance_of => 'accepted',
:validates_associated => 'invalid',
:validates_confirmation_of => 'confirmation',
:validates_exclusion_of => 'exclusion',
:validates_format_of => 'invalid',
:validates_inclusion_of => 'inclusion',
:validates_length_of => nil,
:validates_numericality_of => 'not_a_number',
:validates_presence_of => 'blank',
:validates_uniqueness_of => 'taken'
}[validation_name]
end
#when you get back, do vlo cuz you've gotta check the validtion options
def self.default_message_for(validation_name, options)
return I18n.t("activerecord.errors.messages.#{validation_name_to_i18n(validation_name)}") unless validation_name == :validates_length_of
keys = options.keys
if keys.include?(:within)
"must be between #{options[:within].first} and #{options[:within].last} characters"
elsif keys.include?(:maximum) || keys.include?("maximum")
I18n.t("activerecord.errors.messages.too_long")
elsif keys.include?(:minimum)
I18n.t("activerecord.errors.messages.too_short")
end
end
end
end
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
# these are defined in the formbuilder.rb initializer cuz Aaron doesn't like load order
# @@validate_by_default = false
# cattr_accessor :validate_by_default
alias_method :original_input, :input
def input(method, options = {})
return original_input(method, options) unless @options[:validate].present?
options[:input_html] ||= {}
validation_options = field_validations(method, options, false)
if options[:as].to_s.eql?('text_with_counter')
config_text_counter_validations(validation_options[:'data-validate'] || {}, options[:input_html][:'count-max'])
end
validation_options[:'data-validate'] = validation_options[:'data-validate'].to_json
options[:input_html].merge!(validation_options)
original_input(method, options)
end
def field_validations(method, options={}, as_json=true)
refl = (@object.is_a?(Array) ? @object.first : @object).class.reflect_on_validations_for(method)
if refl.present?
conditional = false
validations = refl.inject({}) do |hsh, reflection|
# Rails.logger.info("\n\n#{reflection.inspect}")
# Rails.logger.info("\n\n#{reflection.options.inspect}")
conditional = true if reflection.options[:if].present? or reflection.options[:except].present?
# all of this because the field name kept repeating
message = reflection.options[:message] || ActiveRecord::Base.default_message_for(reflection.macro, reflection.options)
field_name = options[:label].present? ? options[:label].gsub(self.required_or_optional_string(true), '') : method.to_s.titleize
message = "#{field_name} #{message}" unless message =~ /#{field_name}/i
reflection_options = reflection.options.dup.merge(:message => message)
reflection_options.merge!(:allow_blank => true) if options[:force_allow_blank]
# end calculating the message
hsh.merge(reflection.macro => reflection_options)
end
merging_options = {:'data-validate' => (as_json ? validations.to_json : validations) }
# We do it like this because we don't even want the data-condition-validation
merging_options.merge!(:'data-conditional-validation' => conditional) if conditional
if(skip_validation = options.delete(:skip_validation))
merging_options.merge(:'data-skip-validation' => skip_validation)
end
return merging_options
else
return {}
end
end
#used to pass in the validate options into the fields_for
alias_method :old_semantic_fields_for, :semantic_fields_for
def semantic_fields_for(record_or_name_or_array, *args, &proc)
options = args.extract_options!
options.merge!(:validate => @options[:validate]) if @options[:validate].present?
old_semantic_fields_for(record_or_name_or_array, *(args << options), &proc)
end
# Override: This now returns true if you've set a different validation
# other than validates_presence_of. If you've set allow_blank to true
# then it will return false, otherwise it returs true.
# note I made some fat changes here :/
def method_required?(attribute)
return false if @options[:validate] == false
if @object && @object.class.respond_to?(:reflect_on_validations_for)
attribute_sym = attribute.to_s.sub(/_id$/, '').to_sym
@object.class.reflect_on_validations_for(attribute_sym).any? do |validation|
if validation.options[:allow_blank].present?
!validation.options[:allow_blank]
elsif validation.macro == :validates_presence_of
true
elsif validation.macro == :validates_length_of
validation.options.keys.detect { |validation_option| [:minimum, :is, :within].include?(validation_option) }.present?
else
true
end
end
else
@@all_fields_required_by_default
end
end
private
# if input is a text_with_counter, configure data validation;
# take smaller of client-side 'count-max' or server-side 'validates_lenght_of'
def config_text_counter_validations(validations, count_max)
validation_len = validations[:validates_length_of].present? ? validations[:validates_length_of][:maximum] : nil
input_max = if (count_max && validation_len)
(count_max < validation_len) ? count_max : validation_len
elsif (count_max || validation_len)
count_max || validation_len
else
nil
end
if input_max
validations[:validates_length_of] ||= {}
validations[:validates_length_of][:maximum] = input_max
end
end
end
module SemanticFormHelper
#used to add the 'validate' class into the form unless it's no_class
alias_method :old_semantic_form_for, :semantic_form_for
def semantic_form_for(record_or_name_or_array, *args, &proc)
options = add_validation_option(args.extract_options!)
options.deep_merge!(fix_nested_resources(record_or_name_or_array, options))
options.deep_merge!({:html => {:'data-model' => calculate_data_model(record_or_name_or_array) }})
old_semantic_form_for(record_or_name_or_array, *(args << options), &proc)
end
alias_method :old_semantic_remote_form_for, :semantic_remote_form_for
def semantic_remote_form_for(record_or_name_or_array, *args, &proc)
options = add_validation_option(args.extract_options!)
options.deep_merge!(fix_nested_resources(record_or_name_or_array, options))
old_semantic_remote_form_for(record_or_name_or_array, *(args << options), &proc)
end
private
def calculate_data_model(record_or_name_or_array)
record_name = record_or_name_or_array.is_a?(Array) ? record_or_name_or_array.last : record_or_name_or_array
record_name.is_a?(Symbol) ? record_name.to_s : record_name.class.to_s.underscore
end
#:url => (@allergy.new_record? ? patient_allergies_path(@patient) : patient_allergy_path(@patient, @allergy)), :html => {:method => (@allergy.new_record? ? 'post' : 'put')}
def fix_nested_resources(record_or_name_or_array, options)
return {} if !record_or_name_or_array.is_a?(Array) or options.has_key?(:url)
{:url => real_url_for_nested_resources(record_or_name_or_array), :html => {:method => restful_action_for_nested_resources(record_or_name_or_array)}}
end
def real_url_for_nested_resources(array_record)
obj = array_record.last
prefix = array_record.first.class.name.underscore
core = obj.class.name.underscore
if obj.new_record?
send("#{prefix}_#{core.pluralize}_path", array_record.first)
else
send("#{prefix}_#{core}_path", array_record.first, obj)
end
end
def restful_action_for_nested_resources(array_record)
array_record.last.new_record? ? 'post' : 'put'
end
def add_validation_option(options)
options[:validate] = SemanticFormBuilder.validate_by_default if options[:validate].nil?
if options[:validate].present? && options[:validate] != :no_class
options[:html] ||= {}
options[:html][:class] = options[:html][:class] ? "#{options[:html][:class]} validate" : 'validate'
end
options
end
end
end
@trshafer
Copy link
Author

trshafer commented Mar 3, 2011

don't forget to add:
gem 'validation_reflection'
to the Gemfile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment