Creating a Custom Formtastic File Upload Input with Image Thumbnail
March 26th, 2012 // 11:50 am @ matt
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.
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.
