Creating a Custom Formtastic File Upload Input with Image Thumbnail

Just today, I ran into a situation where I wanted to modify the output generated by Formtastic while building an input element. In this particular scenario, the input field was a file input in which the user is expected to upload an image (PNG, JPG, or GIF). The customization I wanted was the ability to display the thumbnail of the current image if one has already been uploaded. A screenshot of the goal can be seen below.

formtastic-attachment

My first approach was not at all elegant, although it achieved the goal. Depending on whether this is a new or existing record, I conditionally display the input differently.

<%= form.inputs do %>
    <% if form.object.new_record? -%>
        <%= form.input :image, :required => true, :hint => 'Maximum size of 3MB. JPG, GIF, PNG.' %>
    <% else -%>
        <li class="file input required" id="profile_image_input">
            <label class="label" for="profile_image">Image</label>
            <%= image_tag form.object.image.url(:thumb), :class => 'attachment' %>
            <%= form.file_field :image %>
            <p class="inline-hints">Maximum size of 3MB. JPG, GIF, PNG.</p>
        </li>
    <% end -%>
<% end %>

There are a few problems with this. For one, I have repeated code between the two conditions. For example, the “hint” string is repeated, as well as the required setting. If I ever change the value in one spot, I have to remember to change it in the other. The other bigger problem is that my code is based upon the internal rendering performed by Formtastic. That means that if I ever update Formtastic, I need to make sure my custom HTML is updated to match as well.

Being somewhat of a perfectionist (well, at least as close as I can get) when it comes to my code, I wanted to address these issues right away rather than putting it on a “to do” list that would always take second place to other more important tasks.

After doing a bit of digging, I was reminded about Formtastic’s ability to utilize custom inputs. This can be seen on the “Creating New Inputs Based on Existing Ones” section of Formtastic’s README. In only 5-10 minutes of coding, I was able to completely refactor my original approach. First, I created my custom input class.

class AttachmentInput < Formtastic::Inputs::FileInput
  def image_html_options
    {:class => 'attachment'}.merge(options[:image_html] || {})
  end
 
  def to_html
    input_wrapping do
      label_html <<
      image_html <<
      builder.file_field(method, input_html_options)
    end
  end
 
protected
 
  def image_html
    return "".html_safe if builder.object.new_record?
 
    url = case options[:image]
    when Symbol
      builder.object.send(options[:image])
    when Proc
      options[:image].call(builder.object)
    else
      options[:image].to_s
    end
 
    builder.template.image_tag(url, image_html_options).html_safe
  end
end

Now, with my input class created, I could simply specify the type of input as :attachment instead of using the default of :file.

<%= form.input :image, :as => :attachment,
                       :required => true,
                       :hint => 'Maximum size of 3MB. JPG, GIF, PNG.',
                       :image => proc { |o| o.image.url(:thumb) } %>

Note the additional :image option. In order to know what URL to render for the image, this option must be supplied. In the example above, I utilized a Proc object to generate the URL string. The Proc receives the object that the form is currently working with. In this case, it is the instance of my model.

Alternatively, I set up my AttachmentInput class to accept a Symbol representing the name of a method on the object to call, as well as any other object type that is simply cast to a String. Examples of these implementations can be seen below.

<%= form.input :image, :as => :attachment, :image => :to_s %>
<%= form.input :image, :as => :attachment, :image => "profile.png" %>

Formtastic inputs also accepts custom HTML options via :input_html, :wrapper_html, and :button_html Hash options depending on what is currently being generated. Similarly, a new :image_html Hash option can be provided when building an AttachmentInput.

<%= form.input :image, :as => :attachment,
                       :image => :to_s,
                       :image_html => {:class => 'thumbnail', :alt => 'Profile Photo'} %>

All that’s left now is to style the rendered HTML. Nothing fancy here.

form img.attachment {
    float: left;
    padding: 4px;
    margin: 0 10px 0 0;
    border: 1px solid #ccc;
    background-color: #fff;
}

And there you have it! With just a few extra minutes of research and coding, I was able to clean up my code significantly.

Leave a Reply

Your email address will not be published. Required fields are marked *