ksylvest/formula

View on GitHub
lib/formula.rb

Summary

Maintainability
C
1 day
Test Coverage
module Formula


  require 'formula/railtie' if defined?(Rails)

  mattr_accessor :box_options
  mattr_accessor :area_options
  mattr_accessor :file_options
  mattr_accessor :field_options
  mattr_accessor :select_options
  @@box_options = {}
  @@area_options = {}
  @@file_options = {}
  @@field_options = {}
  @@select_options = {}

  # Default class assigned to block (<div class="block">...</div>).
  mattr_accessor :block_class
  @@block_class = 'block'

  # Default class assigned to input (<div class="input">...</div>).
  mattr_accessor :input_class
  @@input_class = 'input'

  # Default class assigned to association (<div class="association">...</div>).
  mattr_accessor :association_class
  @@association_class = 'association'

  # Default class assigned to block with errors (<div class="block errors">...</div>).
  mattr_accessor :block_error_class
  @@block_error_class = 'errors'

  # Default class assigned to input with errors (<div class="input errors">...</div>).
  mattr_accessor :input_error_class
  @@input_error_class = false

  # Default class assigned to input with errors (<div class="association errors">...</div>).
  mattr_accessor :association_error_class
  @@association_error_class = false

  # Default class assigned to error (<div class="error">...</div>).
  mattr_accessor :error_class
  @@error_class = 'error'

  # Default class assigned to hint (<div class="hint">...</div>).
  mattr_accessor :hint_class
  @@hint_class = 'hint'

  # Default tag assigned to block (<div class="input">...</div>).
  mattr_accessor :block_tag
  @@block_tag = :div

  # Default tag assigned to input (<div class="input">...</div>).
  mattr_accessor :input_tag
  @@input_tag = :div

  # Default tag assigned to association (<div class="association">...</div>).
  mattr_accessor :association_tag
  @@association_tag = :div

  # Default tag assigned to error (<div class="error">...</div>).
  mattr_accessor :error_tag
  @@error_tag = :div

  # Default tag assigned to hint (<div class="hint">...</div>).
  mattr_accessor :hint_tag
  @@hint_tag = :div

  # Method for file.
  mattr_accessor :file
  @@file = [:file?]

  # Default as.
  mattr_accessor :default_as
  @@default_as = :string


  class FormulaFormBuilder < ActionView::Helpers::FormBuilder


    # Generate a form button.
    #
    # Options:
    #
    # * :container - add custom options to the container
    # * :button    - add custom options to the button
    #
    # Usage:
    #
    #   f.button(:name)
    #
    # Equivalent:
    #
    #   <div class="block">
    #     <%= f.submit("Save")
    #   </div>

    def button(value = nil, options = {})
      options[:button] ||= {}

      options[:container] ||= {}
      options[:container][:class] = arrayorize(options[:container][:class]) << ::Formula.block_class


      @template.content_tag(::Formula.block_tag, options[:container]) do
        submit value, options[:button]
      end
    end


    # Basic container generator for use with blocks.
    #
    # Options:
    #
    # * :hint       - specify a hint to be displayed ('We promise not to spam you.', etc.)
    # * :label      - override the default label used ('Name:', 'URL:', etc.)
    # * :error      - override the default error used ('invalid', 'incorrect', etc.)
    #
    # Usage:
    #
    #   f.block(:name, :label => "Name:", :hint => "Please use your full name.", :container => { :class => 'fill' }) do
    #     ...
    #   end
    #
    # Equivalent:
    #
    #   <div class='block fill'>
    #     <%= f.label(:name, "Name:") %>
    #     ...
    #     <div class="hint">Please use your full name.</div>
    #     <div class="error">...</div>
    #   </div>

    def block(method = nil, options = {}, &block)
      options[:error] ||= error(method) if method

      components = "".html_safe

      if method
        components << self.label(method, options[:label]) if options[:label] or options[:label].nil? and method
      end

      components << @template.capture(&block)

      options[:container] ||= {}
      options[:container][:class] = arrayorize(options[:container][:class]) << ::Formula.block_class << method
      options[:container][:class] << ::Formula.block_error_class if ::Formula.block_error_class.present? and error?(method)

      components << @template.content_tag(::Formula.hint_tag , options[:hint ], :class => ::Formula.hint_class ) if options[:hint ]
      components << @template.content_tag(::Formula.error_tag, options[:error], :class => ::Formula.error_class) if options[:error]

      @template.content_tag(::Formula.block_tag, options[:container]) do
        components
      end
    end


    # Generate a suitable form input for a given method by performing introspection on the type.
    #
    # Options:
    #
    # * :as         - override the default type used (:url, :email, :phone, :password, :number, :text)
    # * :label      - override the default label used ('Name:', 'URL:', etc.)
    # * :error      - override the default error used ('invalid', 'incorrect', etc.)
    # * :input      - add custom options to the input ({ :class => 'goregous' }, etc.)
    # * :container  - add custom options to the container ({ :class => 'gorgeous' }, etc.)
    #
    # Usage:
    #
    #   f.input(:name)
    #   f.input(:email)
    #   f.input(:password_a, :label => "Password", :hint => "It's a secret!", :container => { :class => "half" })
    #   f.input(:password_b, :label => "Password", :hint => "It's a secret!", :container => { :class => "half" })
    #
    # Equivalent:
    #
    #   <div class="block name">
    #     <%= f.label(:name)
    #     <div class="input string"><%= f.text_field(:name)</div>
    #      <div class="error">...</div>
    #   </div>
    #   <div class="block email">
    #     <%= f.label(:email)
    #     <div class="input string"><%= f.email_field(:email)</div>
    #      <div class="error">...</div>
    #   </div>
    #   <div class="block half password_a">
    #     <div class="input">
    #       <%= f.label(:password_a, "Password")
    #       <%= f.password_field(:password_a)
    #       <div class="hint">It's a secret!</div>
    #       <div class="error">...</div>
    #     </div>
    #   </div>
    #   <div class="block half password_b">
    #     <div class="input">
    #       <%= f.label(:password_b, "Password")
    #       <%= f.password_field(:password_b)
    #       <div class="hint">It's a secret!</div>
    #       <div class="error">...</div>
    #     </div>
    #   </div>

    def input(method, options = {})
      options[:as] ||= as(method)
      options[:input] ||= {}

      return hidden_field method, options[:input] if options[:as] == :hidden

      klass = [::Formula.input_class, options[:as]]
      klass << ::Formula.input_error_class if ::Formula.input_error_class.present? and error?(method)

      self.block(method, options) do
        @template.content_tag(::Formula.input_tag, :class => klass) do
          case options[:as]
          when :text     then text_area       method, ::Formula.area_options.merge(options[:input] || {})
          when :file     then file_field      method, ::Formula.file_options.merge(options[:input] || {})
          when :string   then text_field      method, ::Formula.field_options.merge(options[:input] || {})
          when :password then password_field  method, ::Formula.field_options.merge(options[:input] || {})
          when :url      then url_field       method, ::Formula.field_options.merge(options[:input] || {})
          when :email    then email_field     method, ::Formula.field_options.merge(options[:input] || {})
          when :phone    then phone_field     method, ::Formula.field_options.merge(options[:input] || {})
          when :number   then number_field    method, ::Formula.field_options.merge(options[:input] || {})
          when :boolean  then check_box       method, ::Formula.box_options.merge(options[:input] || {})
          when :country  then country_select  method, ::Formula.select_options.merge(options[:input] || {})
          when :date     then date_select     method, ::Formula.select_options.merge(options[:input] || {}), options[:input].delete(:html) || {}
          when :time     then time_select     method, ::Formula.select_options.merge(options[:input] || {}), options[:input].delete(:html) || {}
          when :datetime then datetime_select method, ::Formula.select_options.merge(options[:input] || {}), options[:input].delete(:html) || {}
          when :select   then select          method, options[:choices], ::Formula.select_options.merge(options[:input] || {}), options[:input].delete(:html) || {}
          end
        end
      end
    end


    # Generate a suitable form association for a given method by performing introspection on the type.
    #
    # Options:
    #
    # * :label       - override the default label used ('Name:', 'URL:', etc.)
    # * :error       - override the default error used ('invalid', 'incorrect', etc.)
    # * :association - add custom options to the input ({ :class => 'goregous' }, etc.)
    # * :container   - add custom options to the container ({ :class => 'gorgeous' }, etc.)
    #
    # Usage:
    #
    #   f.association(:category, Category.all, :id, :name, :hint => "What do you do?")
    #   f.association(:category, Category.all, :id, :name, :association => { :prompt => "Category?" })
    #   f.association(:category, Category.all, :id, :name, :association => { :html => { :class => "category" } })
    #
    # Equivalent:
    #
    #   <div>
    #     <div class="association category">
    #       <%= f.label(:category)
    #       <div class="association">
    #         <%= f.collection_select(:category, Category.all, :id, :name) %>
    #       </div>
    #       <div class="hint">What do you do?</div>
    #       <div class="error">...</div>
    #     </div>
    #   </div>
    #   <div>
    #     <div class="association category">
    #       <%= f.label(:category)
    #       <div class="association">
    #         <%= f.collection_select(:category, Category.all, :id, :name, { :prompt => "Category") } %>
    #       </div>
    #       <div class="error">...</div>
    #     </div>
    #   </div>
    #   <div>
    #     <div class="association category">
    #       <%= f.label(:category)
    #       <div class="association">
    #         <%= f.collection_select(:category, Category.all, :id, :name, {}, { :class => "category" } %>
    #       </div>
    #       <div class="error">...</div>
    #     </div>
    #   </div>


    def association(method, collection, value, text, options = {})
      options[:as] ||= :select
      options[:association] ||= {}

      klass = [::Formula.association_class, options[:as]]
      klass << ::Formula.association_error_class if ::Formula.association_error_class.present? and error?(method)

      self.block(method, options) do
        @template.content_tag(::Formula.association_tag, :class => klass) do
          case options[:as]
          when :select then collection_select :"#{method}_id", collection, value, text,
              options[:association], options[:association].delete(:html) || {}
          end
        end
      end
    end


    private


    # Introspection on the column to determine how to render a method. The method is used to
    # identify a method type (if the method corresponds to a column)
    #
    # Returns:
    #
    # * :text     - for columns of type 'text'
    # * :string   - for columns of type 'string'
    # * :integer  - for columns of type 'integer'
    # * :float    - for columns of type 'float'
    # * :decimal  - for columns of type 'decimal'
    # * :datetime - for columns of type 'datetime'
    # * :date     - for columns of type 'date'
    # * :time     - for columns of type 'time'
    # * nil       - for unkown columns

    def type(method)
      if @object.respond_to?(:has_attribute?) && @object.has_attribute?(method)
        column = @object.column_for_attribute(method) if @object.respond_to?(:column_for_attribute)
        return column.type if column
      end
    end


    # Introspection on an association to determine if a method is a file. This
    # is determined by the methods ability to respond to file methods.

    def file?(method)
      @files ||= {}
      @files[method] ||= begin
        file = @object.send(method) if @object && @object.respond_to?(method)
        file && ::Formula.file.any? { |method| file.respond_to?(method) }
      end
    end


    # Introspection on the field and method to determine how to render a method. The method is
    # used to generate form element types.
    #
    # Returns:
    #
    # * :url      - for columns containing 'url'
    # * :email    - for columns containing 'email'
    # * :phone    - for columns containing 'phone'
    # * :password - for columns containing 'password'
    # * :number   - for integer, float or decimal columns
    # * :datetime - for datetime or timestamp columns
    # * :date     - for date column
    # * :time     - for time column
    # * :text     - for time column
    # * :string   - for all other cases

    def as(method)

      case "#{method}"
      when /url/      then return :url
      when /email/    then return :email
      when /phone/    then return :phone
      when /password/ then return :password
      end

      case type(method)
      when :string    then return :string
      when :integer   then return :number
      when :float     then return :number
      when :decimal   then return :number
      when :timestamp then return :datetime
      when :datetime  then return :datetime
      when :date      then return :date
      when :time      then return :time
      when :text      then return :text
      end

      return :file if file?(method)

      return ::Formula.default_as

    end


    # Generate error messages by combining all errors on an object into a comma seperated string
    # representation.

    def error(method)
      errors = @object.errors[method] if @object
      errors.to_sentence if errors.present?
    end


    # Identify if error message exists for a given method by checking for the presence of the object
    # followed by the presence of errors.

    def error?(method)
      @object.present? and @object.errors[method].present?
    end


    # Create an array from a string, a symbol, or an undefined value. The default is to return
    # the value and assume it has already is valid.

    def arrayorize(value)
      case value
      when nil    then return []
      when String then value.to_s.split
      when Symbol then value.to_s.split
      else value
      end
    end


    public


    # Generates a wrapper around fields_form with :builder set to FormulaFormBuilder.
    #
    # Supports:
    #
    # * f.formula_fields_for(@user.company)
    # * f.fieldsula_for(@user.company)
    #
    # Equivalent:
    #
    # * f.fields_for(@user.company, :builder => Formula::FormulaFormBuilder))
    #
    # Usage:
    #
    #   <% f.formula_fields_for(@user.company) do |company_f| %>
    #     <%= company_f.input :url %>
    #     <%= company_f.input :phone %>
    #   <% end %>

    def formula_fields_for(record_or_name_or_array, *args, &block)
      options = args.extract_options!
      options[:builder] ||= self.class
      fields_for(record_or_name_or_array, *(args << options), &block)
    end

    alias :fieldsula_for :formula_fields_for


  end

  module FormulaFormHelper
    @@builder = ::Formula::FormulaFormBuilder


    # Generates a wrapper around form_for with :builder set to FormulaFormBuilder.
    #
    # Supports:
    #
    # * formula_form_for(@user)
    #
    # Equivalent:
    #
    # * form_for(@user, :builder => Formula::FormulaFormBuilder))
    #
    # Usage:
    #
    #   <% formula_form_for(@user) do |f| %>
    #     <%= f.input :email %>
    #     <%= f.input :password %>
    #   <% end %>

    def formula_form_for(record_or_name_or_array, *args, &proc)
      options = args.extract_options!
      options[:builder] ||= @@builder
      form_for(record_or_name_or_array, *(args << options), &proc)
    end

    alias :formula_for :formula_form_for


    # Generates a wrapper around fields_for with :builder set to FormulaFormBuilder.
    #
    # Supports:
    #
    # * f.formula_fields_for(@user.company)
    # * f.fieldsula_for(@user.company)
    #
    # Equivalent:
    #
    # * f.fields_for(@user.company, :builder => Formula::FormulaFormBuilder))
    #
    # Usage:
    #
    #   <% f.formula_fields_for(@user.company) do |company_f| %>
    #     <%= company_f.input :url %>
    #     <%= company_f.input :phone %>
    #   <% end %>

    def formula_fields_for(record_or_name_or_array, *args, &block)
      options = args.extract_options!
      options[:builder] ||= @@builder
      fields_for(record_or_name_or_array, *(args << options), &block)
    end

    alias :fieldsula_for :formula_fields_for

  end

end