Adding DOM Elements to a Page

Problem

You need to add elements to a form on the fly, without going through a complete request/redisplay cycle; for example, you have a web-based image gallery that has a form for users to upload images. You want to allow trusted users to upload any number of images at a time. In other words, if the form starts out with one file upload tag, and the users want to upload an additional image, they should be able to add another file upload element with a single click.

Solution

Use the link_to_remote JavaScript helper. This helper lets you use the XMLHttpRequest object to update only the portion of the page that you need.

Include the Prototype JavaScript libraries in your controller's layout.

app/views/layouts/upload.rhtml:

<html>
 <head> 
 <title>File Upload</title>
 <%= javascript_include_tag 'prototype' %>
 </head> 
 <body> 
 <%= yield %>
 </body> 
</html>

Now place a call to link_to_remote in your view. The call should include the id of the page element that you want to update, the controller action that should be triggered, and the position of new elements being inserted.

app/views/upload/index.rhtml:

<h1>File Upload</h1>
<% if flash[:notice] %>
 <p ><%= flash[:notice] %></p>
<% end %>
<% form_tag({ :action => "add" }, 
 :id => id, :enctype =>
 "multipart/form-data") do %>
 <b>Files:</b>
 <%= link_to_remote "Add field",
 :update => "files",
 :url => { :action => "add_field" },
 :position => "after" %>;
 <div id="files">
 <%= render :partial => 'file_input' %>
 </div> 
 <%= submit_tag(value = "Add Files", options = {}) %>
<% end %>

Create a partial template with the file input field:

app/views/upload/_file_input.rhtml

<input name="assets[]" type="file"><br />

Finally, define the add_field action in your controller to return the HTML for additional file input fields. All that's needed is a fragment of HTML:

app/controllers/upload_controller.rb:

class UploadController < ApplicationController
 def index
 end
 def add
 begin 
 total = params[:assets].length
 params[:assets].each do |file|
 Asset.save_file(file)
 end 
 flash[:notice] = "#{total} files uploaded successfully" 
 rescue 
 raise 
 end
 redirect_to :action => "index" 
 end 
 def add_field
 render :partial => 'file_input'
 end end

app/models/asset.rb:

class Asset < ActiveRecord::Base
 def self.save_file(upload)
 begin 
 FileUtils.mkdir(upload_path) unless File.directory?(upload_path)
 bytes = upload
 if upload.kind_of?(StringIO)
 upload.rewind
 bytes = upload.read 
 end 
 name = upload.full_original_filename
 File.open(upload_path(name), "wb") { |f| f.write(bytes) }
 File.chmod(0644, upload_path(name) ) 
 rescue 
 raise 
 end
 end
 def self.upload_path(file=nil)
 "#{RAILS_ROOT}/public/files/#{file.nil? ? '' : file}" 
 end 
end

Discussion

The solution uses the link_to_remote function to add additional file selection fields to the form.

When the user clicks the "Add field" link, the browser doesn't perform a full page refresh. Instead, the XMLHttpRequest object makes its own request to the server and listens for a response to that request. When that response is received, JavaScript on the web page updates the portion of the DOM that was specified by the :update option of the link_to_remote method. This update causes the browser to refresh the parts of the page that were changedbut only those parts, not the entire web page.

The :update option is passed "files," matching the ID of the div tag that we want to update. The :url option takes the same parameters as url_for. We pass it a hash specifying that the add_field action is to handle the XMLHttpRequest object. Finally, the :position option specifies that the new elements of output are to be placed after any existing elements that are within the element specified by the :update option. The available options to :position are: :before, :top, :bottom, or :after.

shows a form that allows users to upload an arbitrary number of files by adding file selection elements as needed.

Figure 8-1. A form that uses JavaScript to add more input elements to itself.

See Also