Gene
Gene

Reputation: 46960

Rails input text maxlength from ActiveRecord limit

I our Rails app, we'd like to prevent a user from physically entering more characters in a text field than can be recorded in the corresponding database field. This seems much friendlier than letting him/her type too much and then be told to try again after the fact.

In other words, all View input text fields should have a maxlength= attribute set equal to the :limit of the corresponding ActiveRecord Model varchar() field.

Is there a way to cause this to happen automatically?

If not, is there a DRY idiom, helper function, or metaprogramming hack that will make it so?

Upvotes: 1

Views: 2412

Answers (2)

theftprevention
theftprevention

Reputation: 5213

Rails 4 solution

The following is tested and working:

# config/initializers/text_field_extensions.rb
module ActionView
  module Helpers
    module Tags
      class TextField

        alias_method :render_old, :render

        def render
          prototype = @object.class
          unless prototype == NilClass
            maxlength = nil
            validator = prototype.validators.detect do |v|
              v.instance_of?(ActiveModel::Validations::LengthValidator) &&
              v.attributes.include?(@method_name.to_sym) &&
              v.options.include?(:maximum)
            end
            if !validator.nil?
              maxlength = validator.options[:maximum]
            else
              column = prototype.columns.detect do |c|
                c.name == @method_name &&
                c.respond_to?(:limit)
              end
              maxlength = column.limit unless column.nil?
            end
            @options.reverse_merge!(maxlength: maxlength) unless maxlength.nil?
          end
          render_old
        end

      end
    end
  end
end

This solution borrows from Deefour's answer, but applies the patch to ActionView's TextField class instead. All other text-based input types (password_field, email_field, search_field, and so on) use tags that inherit from TextField, which means this fix will apply to them as well. The only exception to this is the text_area method, which uses a different tag, and won't function the same way unless this patch is separately applied to ActionView::Helpers::Tags::TextArea.

Deefour's solution looks to the ActiveRecord database columns to determine the max-length. While this answered Gene's question perfectly, I found that the limits on the database columns were usually unrelated to (i.e. higher than) the limits we actually want on the field. The most desirable max-length will typically come from a LengthValidator we've set on the model. So, first, this patch looks for a LengthValidator on the model that (a) applies to the attribute being used for the field, and (b) provides a maximum length. If it doesn't get anything from that, it will use the limit specified on the database column.

Finally, like Deefour's answer, the usage of @options.reverse_merge! means that the maxlength attribute of the field can be overridden by specifying a :maxlength in the options argument:

<%= form_for @user do |f| %>
  <%= f.text_field :first_name, :maxlength => 25 %>
<% end %>

Setting :maxlength to nil will remove the attribute entirely.

Finally, keep in mind that providing the maxlength option of a TextField will automatically set the size attribute of the input element to the same value. Personally, I think the size attribute is antiquated and was made obsolete by CSS, so I chose to remove it. To do that, replace the line involving @options.reverse_merge! with the following:

@options.reverse_merge!(maxlength: maxlength, size: nil) unless maxlength.nil?

Upvotes: 3

deefour
deefour

Reputation: 35360

Something like the following (untested) monkey patch might help

module ActionView
  module Helpers

    old_text_field = instance_method(:text_field)                                                           # store a reference to the original text_field method

    define_method(:text_field) do |object_name, method, options = {}|                                       # completely redefine the text_field method
      klass     = InstanceTag.new(object_name, method, self, options.delete(:object)).retrieve_object.class # get the class for the object bound to the field
      column    = klass.columns.reject { |c| c.name != method.to_s }.first if klass.present?                # get the column from the class
      maxlength = column.limit if defined?(column) && column.respond_to?(:limit)                            # set the maxlength to the limit for the column if it exists

      options.reverse_merge!( maxlength: maxlength ) if defined?(maxlength)                                 # merge the maxlength option in with the rest

      old_text_field.bind(self).(object_name, method, options)                                              # pass it up to the original text_field method
    end
  end
end

Upvotes: 1

Related Questions