Module: UploadProgress::UploadProgressHelper

Defined in:
lib/upload_progress/lib/upload_progress_helper.rb

Overview

Provides a set of methods to be used in your views to help with the rendering of Ajax enabled status updating during a multipart upload.

The basic mechanism for upload progress is that the form will post to a hidden <iframe> element, then poll a status action that will replace the contents of a status container. Client Javascript hooks are provided for begin and finish of the update.

If you wish to have a DTD that will validate this page, use XHTML Transitional because this DTD supports the <iframe> element.

Typical usage

In your upload view:

<% form_tag_with_upload_progress({ :action => 'create' }) do %>
  <%= file_field "document", "file" %>
  <%= submit_tag "Upload" %>
  <%= upload_status_tag %>
<% end %>

In your controller:

class DocumentController < ApplicationController   
  upload_status_for  :create

  def create
    # ... Your document creation action
  end
end

Javascript callback on begin and finished

In your upload view:

<% form_tag_with_upload_progress({ :action => 'create' }, {
    :begin => "alert('upload beginning')", 
    :finish => "alert('upload finished')" }) do %>
  <%= file_field "document", "file" %>
  <%= submit_tag "Upload" %>
  <%= upload_status_tag %>
<% end %>

CSS Styling of the status text and progress bar

See upload_status_text_tag and upload_status_progress_bar_tag for references of the IDs and CSS classes used.

Default styling is included with the scaffolding CSS.

Constant Summary collapse

FREQUENCY =

Default number of seconds between client updates

2.0
FREQUENCY_DECAY =

Factor to decrease the frequency when the upload_status action returns the same results To disable update decay, set this constant to false

1.8
@@default_messages =

Contains a hash of status messages used for localization of upload_progress_status and upload_progress_text. Each string is evaluated in the helper method context so you can include your own calculations and string iterpolations.

The following keys are defined:

:begin

Displayed before the first byte is received on the server

:update

Contains a human representation of the upload progress

:finish

Displayed when the file upload is complete, before the action has completed. If you are performing extra activity in your action such as processing of the upload, then inform the user of what you are doing by setting upload_progress.message

{
  :begin => '"Upload starting..."',
  :update => '"#{human_size(upload_progress.received_bytes)} of #{human_size(upload_progress.total_bytes)} at #{human_size(upload_progress.bitrate)}/s; #{distance_of_time_in_words(0,upload_progress.remaining_seconds,true)} remaining"',
  :finish => 'upload_progress.message.blank? ? "Upload finished." : upload_progress.message',
}

Instance Method Summary collapse

Instance Method Details

#finish_upload_status(options = {}) ⇒ Object

This method must be called by the action that receives the form post with the upload_progress. By default this method is rendered when the controller declares that the action is the receiver of a form_tag_with_upload_progress posting.

This template will do a javascript redirect to the URL specified in redirect_to if this method is called anywhere in the controller action. When the action performs a redirect, the finish handler will not be called.

If there are errors in the action then you should set the controller instance variable @errors. The @errors object will be converted to a javascript array from @errors.full_messages and passed to the finish handler of form_tag_with_upload_progress

If no errors have occured, the parameter to the finish handler will be undefined.

Example (in view)

<script>
 function do_finish(errors) {
   if (errors) {
     alert(errors);
   }
 }
</script>

<%= form_tag_with_upload_progress {:action => 'create'}, {finish => 'do_finish(arguments[0])'} %>


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 254

def finish_upload_status(options = {})
  # Always trigger the stop/finish callback
  js = "parent.#{upload_update_object}.stop(#{options[:client_js_argument]});\n"

  # Redirect if redirect_to was called in controller
  js << "parent.location.replace('#{escape_javascript options[:redirect_to]}');\n" unless options[:redirect_to].blank?

  # Guard against multiple triggers/redirects on back
  js = "if (parent.#{upload_update_object}) { #{js} }\n".html_safe
  
  ("html", 
    ("head", 
      ("script", "\nfunction finish() { #{js} }".html_safe, 
        {:type => "text/javascript", :language => "javascript"})) + 
    ("body", '', :onload => 'finish()'))
end

#form_tag_with_upload_progress(url_for_options = {}, options = {}, status_url_for_options = {}, *parameters_for_url_method, &block) ⇒ Object

Creates a form tag and hidden <iframe> necessary for the upload progress status messages to be displayed in a designated div on your page.

Initializations

When the upload starts, the content created by upload_status_tag will be filled out with “Upload starting…”. When the upload is finished, “Upload finished.” will be used. Every update inbetween the begin and finish events will be determined by the server upload_status action. Doing this automatically means that the user can use the same form to upload multiple files without refreshing page while still displaying a reasonable progress.

Upload IDs

For the view and the controller to know about the same upload they must share a common upload_id. form_tag_with_upload_progress prepares the next available upload_id when called. There are other methods which use the upload_id so the order in which you include your content is important. Any content that depends on the upload_id will use the one defined form_tag_with_upload_progress otherwise you will need to explicitly declare the upload_id shared among your progress elements.

Status container after the form:

<% form_tag_with_upload_progress do %>
<% end %>

<%= upload_status_tag %>

Status container before form:

<% my_upload_id = next_upload_id %>

<%= upload_status_tag %>

<% form_tag_with_upload_progress :upload_id => my_upload_id do %>
<% end %>

It is recommended that the helpers manage the upload_id parameter.

Options

form_tag_with_upload_progress uses similar options as form_tag yet accepts another hash for the options used for the upload_status action.

url_for_options

The same options used by form_tag including:

:upload_id

the upload id used to uniquely identify this upload

options

similar options to form_tag including:

:begin

Javascript code that executes before the first status update occurs.

:finish

Javascript code that executes after the action that receives the post returns.

:frequency

number of seconds between polls to the upload status action.

status_url_for_options

options passed to url_for to build the url

for the upload status action.

:controller

defines the controller to be used for a custom update status action

:action

defines the action to be used for a custom update status action

Parameters passed to the action defined by status_url_for_options

:upload_id

the upload_id automatically generated by form_tag_with_upload_progress or the user defined id passed to this method.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 141

def form_tag_with_upload_progress(url_for_options = {}, options = {}, status_url_for_options = {}, *parameters_for_url_method, &block)
  
  ## Setup the action URL and the server-side upload_status action for
  ## polling of status during the upload
  
  options = options.dup
  
  upload_id = url_for_options.delete(:upload_id) || next_upload_id
  upload_action_url = url_for(url_for_options)
  
  if status_url_for_options.is_a? Hash
    status_url_for_options = status_url_for_options.merge({
      :action => 'upload_status', 
      :upload_id => upload_id})
  end
  
  status_url = url_for(status_url_for_options)
  
  ## Prepare the form options.  Dynamically change the target and URL to enable the status
  ## updating only if javascript is enabled, otherwise perform the form submission in the same 
  ## frame.
  
  upload_target = options[:target] || upload_target_id
  upload_id_param = "#{upload_action_url.include?('?') ? '&' : '?'}upload_id=#{upload_id}"
  
  ## Externally :begin and :finish are the entry and exit points
  ## Internally, :finish is called :complete
  
  js_options = {
    :decay => options[:decay] || FREQUENCY_DECAY,
    :frequency => options[:frequency] || FREQUENCY,
  }
  
  updater_options = '{' + js_options.map {|k, v| "#{k}:#{v}"}.sort.join(',') + '}'
  
  ## Finish off the updating by forcing the progress bar to 100% and status text because the
  ## results of the post may load and finish in the IFRAME before the last status update
  ## is loaded. 
  
  options[:complete] = "$('#{status_tag_id}').innerHTML='#{escape_javascript upload_progress_text(:finish)}';"
  options[:complete] << "#{upload_progress_update_bar_js(100)};"
  options[:complete] << "#{upload_update_object} = null"
  options[:complete] = "#{options[:complete]}; #{options[:finish]}".html_safe if options[:finish]
  
  options[:script] = true
  
  ## Prepare the periodic updater, clearing any previous updater
  
  updater = "if (#{upload_update_object}) { #{upload_update_object}.stop(); }"
  updater << "#{upload_update_object} = new Ajax.PeriodicalUpdater('#{status_tag_id}',"
  updater << "'#{status_url}', Object.extend(#{options_for_ajax(options)},#{updater_options}))"
  
  updater = "#{options[:begin]}; #{updater}" if options[:begin]
  updater = "#{upload_progress_update_bar_js(0)}; #{updater}"
  updater = "$('#{status_tag_id}').innerHTML='#{escape_javascript upload_progress_text(:begin)}'; #{updater}"
  
  ## Touch up the form action and target to use the given target instead of the
  ## default one. Then start the updater
  
  options[:onsubmit] = "if (this.action.indexOf('upload_id') < 0){ this.action += '#{escape_javascript upload_id_param}'; }"
  options[:onsubmit] << "this.target = '#{escape_javascript upload_target}';"
  options[:onsubmit] << "#{updater}; return true"
  options[:onsubmit] = options[:onsubmit].html_safe
  options[:multipart] = true
  
  [:begin, :finish, :complete, :frequency, :decay, :script].each { |sym| options.delete(sym) }
  
  ## Create the tags
  ## If a target for the form is given then avoid creating the hidden IFRAME
  
  tag = form_tag(upload_action_url, options, *parameters_for_url_method, &block)
  
  unless options[:target]
    tag << ('iframe', '', { 
      :id => upload_target, 
      :name => upload_target,
      :src => '',
      :style => 'width:0px;height:0px;border:0' 
    })
  end
  
  tag
end

#upload_progress_statusObject

The text and Javascript returned by the default upload_status controller action which will replace the contents of the div created by upload_status_text_tag and grow the progress bar background to the appropriate width.

See upload_progress_text and upload_progress_update_bar_js



355
356
357
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 355

def upload_progress_status
  "#{upload_progress_text}<script>#{upload_progress_update_bar_js}</script>".html_safe
end

#upload_progress_text(state = nil) ⇒ Object

Generates a nicely formatted string of current upload progress for UploadProgress::Progress object progress. Addtionally, it will return “Upload starting…” if progress has not been initialized, “Receiving data…” if there is no received data yet, and “Upload finished” when all data has been sent.

You can overload this method to add you own output to the

Example return: 223.5 KB of 1.5 MB at 321.2 KB/s; less than 10 seconds remaining



393
394
395
396
397
398
399
400
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 393

def upload_progress_text(state=nil)
  eval case 
    when state then @@default_messages[state.to_sym]
    when upload_progress.nil? || !upload_progress.started? then @@default_messages[:begin]
    when upload_progress.finished? then @@default_messages[:finish]
    else @@default_messages[:update]
  end 
end

#upload_progress_update_bar_js(percent = nil) ⇒ Object

Javascript helper that will create a script that will change the width of the background progress bar container. Include this in the script portion of your view rendered by your upload_status action to automatically find and update the progress bar.

Example (in controller):

def upload_status
  render :inline => "<script><%= update_upload_progress_bar_js %></script>", :layout => false
end


371
372
373
374
375
376
377
378
379
380
381
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 371

def upload_progress_update_bar_js(percent=nil)
  progress = upload_progress
  percent ||= case 
    when progress.nil? || !progress.started? then 0
    when progress.finished? then 100
    else progress.completed_percent
  end.to_i

  # TODO do animation instead of jumping
  "if($('#{progress_bar_id}')){$('#{progress_bar_id}').firstChild.firstChild.style.width='#{percent}%'}"
end

#upload_status_progress_bar_tag(content = '', options = {}) ⇒ Object

Content helper that will create the element tree that can be easily styled with CSS to create a progress bar effect. The containers are defined as:

<div class="progressBar" id="#{progress_bar_id}">
  <div class="border">
    <div class="background">
      <div class="content"> </div>
    </div>
  </div>
</div>

The content parameter will be included in the inner most div when rendered.

The options will create attributes on the outer most div. To use a different CSS class, pass a different class option.

Example:

<%= upload_status_progress_bar_tag('', {:class => 'progress'}) %>

Example CSS:

div.progressBar {
  margin: 5px;
}

div.progressBar div.border {
  background-color: #fff;
  border: 1px solid grey;
  width: 100%;
}

div.progressBar div.background {
  background-color: #333;
  height: 18px;
  width: 0%;
}


338
339
340
341
342
343
344
345
346
347
348
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 338

def upload_status_progress_bar_tag(content='', options={})
  css = [options[:class], 'progressBar'].compact.join(' ')

  ("div", 
    ("div", 
      ("div", 
        ("div", content, :class => 'foreground'),
      :class => 'background'), 
    :class => 'border'), 
  {:id => progress_bar_id}.merge(options).merge({:class => css}))
end

#upload_status_tag(content = '', options = {}) ⇒ Object

Renders the HTML to contain the upload progress bar above the default messages

Use this method to display the upload status after your form_tag_with_upload_progress



275
276
277
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 275

def upload_status_tag(content='', options={})
  upload_status_progress_bar_tag + upload_status_text_tag(content, options)
end

#upload_status_text_tag(content = nil, options = {}) ⇒ Object

Content helper that will create a div with the proper ID and class that will contain the contents returned by the upload_status action. The container is defined as

<div id="#{status_tag_id}" class="uploadStatus"> </div>

Style this container by selecting the .uploadStatus CSS class.

The content parameter will be included in the inner most div when rendered.

The options will create attributes on the outer most div. To use a different CSS class, pass a different class option.

Example CSS:

.uploadStatus { font-size: 10px; color: grey; }


296
297
298
# File 'lib/upload_progress/lib/upload_progress_helper.rb', line 296

def upload_status_text_tag(content=nil, options={})
  ("div", content, {:id => status_tag_id, :class => 'uploadStatus'}.merge(options))
end