Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
281 lines (261 sloc) 21.6 KB
<!DOCTYPE html>
<html>
<head>
<title>Progress Bars - topdan.com</title>
<meta charset="UTF-8">
<meta name="description" content="How to create a progress bar using Ruby on Rails, AJAX, and Twitter Bootstrap.">
<meta name="author" content="Dan Cunning">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@itopdan">
<meta name="twitter:title" content="Progress Bars">
<meta name="twitter:description" content="How to create a progress bar using Ruby on Rails, AJAX, and Twitter Bootstrap.">
<meta name="twitter:url" content="http://www.topdan.com/ruby-on-rails/progress-bars.html">
<meta name="og:locale" content="en_US">
<meta name="og:type" content="article">
<meta name="og:url" content="http://www.topdan.com/ruby-on-rails/progress-bars.html">
<meta name="og:title" content="Progress Bars">
<meta name="og:description" content="How to create a progress bar using Ruby on Rails, AJAX, and Twitter Bootstrap.">
<link rel="shortcut icon" href="/assets/favicon-e45cdd6cc07e8858a985e6014e38a603.png">
<link rel="stylesheet" media="all" href="/assets/site-d30c732907c1f2982374b8bab9355d72.css"><!--[if lt IE 9]><script src='//html5shim.googlecode.com/svn/trunk/html5.js'></script><![endif]-->
<script type="text/javascript">
if (!document.cookie || document.cookie.indexOf('tracking_off') == -1) {
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-12957077-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
}
</script>
</head>
<body id="progress-bars">
<div class="container bg-black full-width">
<div class="row navigation">
<a id="top"></a> <a href="/ruby-on-rails/form-builder-label-and-full-error-messages.html" class="previous"><span class="fa fa-arrow-left">&nbsp;</span> <span class="desktop">f.label and errors.full_messages</span><span class="mobile">Previous</span></a> <a href="/ruby-on-rails/aws-s3-browser.html" class="next"><span class="desktop">Browsing Files & Directories in S3</span><span class="mobile">Next</span> <span class="fa fa-arrow-right">&nbsp;</span></a>
<p><a href="/"><span class="fa fa-home">&nbsp;</span></a> &nbsp;»&nbsp;<span class="title desktop"><a href="/ruby-on-rails/index.html">Ruby on Rails</a></span><span class="mobile"><a href="/ruby-on-rails/index.html"><span class="fa fa-folder-open">&nbsp;</span></a></span></p>
</div>
<div class="row bg-white">
<div class="col-md-12">
<div class="width-640 ml-auto mr-auto">
<p class="mt-05e mb-15e ta-right c-ccc">Published by Dan on Sep 8, 2015</p>
<h1 class="center of-yh mb-1e"><a href="/ruby-on-rails/progress-bars.html">Progress Bars</a></h1>
<div class="alert b-ccc mb-0">
<p>How to create a progress bar using Ruby on Rails, AJAX, and Twitter Bootstrap.</p>
</div>
<p class="mb-0 center">Filed under <a href="/ruby-on-rails/features.html">Features</a></p>
</div>
</div>
</div>
<div class="row bg-white">
<div class="col-md-12">
<div class="width-640 ml-auto mr-auto post">
<!-- post:content:start -->
<ul>
<li>
<a href="#introduction">Introduction</a>
</li>
<li>
<a href="#demo">Demo</a>
</li>
<li>
<a href="#how-it-works">How it Works</a>
</li>
<li>
<a href="#the-code">The Code</a>
</li>
<li>
<a href="#wrapup">Wrap-up</a>
</li>
</ul>
<h2 id="introduction"><a href="#introduction">Introduction</a></h2>
<p>Progress Bars are essential for long-running tasks that require communication to the user their request was received and is being processed. For example, many applications allow importing spreadsheets or CSV files which could take several minutes to process. These applications save the uploaded file, start a background worker, and allow the client to track its progress.</p>
<p>Two approaches handle this problem: pinging and <a href="https://github.com/rails/actioncable">websockets</a>. Here, I'll tackle the simpler pinging approach.</p>
<h2 id="demo"><a href="#demo">Demo</a></h2>
<p>Click to the following button to see the progress bar go <a id="start" href="#" class="btn btn-primary btn-sm">Start</a></p>
<div id="demo-progress" class="b-ccc bra-5 pt-1e pb-1e pl-1e pr-1e">
<div class="progress progress-striped">
<div class="progress-bar" style="width: 0%;">
0%
</div>
</div>
<div class="message fw-bold center monospace">
Not Running...
</div>
</div>
<p class="fs-12 c-ccc center">This webpage is not hosted by Rails, so the above UX is just a simulation.</p>
<h2 id="how-it-works"><a href="#how-it-works">How it Works</a></h2>
<ol>
<li>When start is clicked, <code>jquery_ujs.js</code> recognizes its <code>data-remote</code> and <code>data-type</code> attributes and requests javascript from Rails via AJAX.</li>
<li>Rails receives the request, passes a new <code>progress_bar</code> into <code>ActiveJob</code> and returns javascript that starts asking Rails for updates.</li>
<li>The <code>ActiveJob</code> updates <code>progress_bar.message</code> and <code>progress_bar.percent</code> as it processes the data until it finishes.</li>
<li>When <code>progress_bar.percent == 100</code> the javascript stops asking Rails for updates.</li>
</ol>
<h2 id="the-code"><a href="#the-code">The Code</a></h2>
<h3 id="model"><a href="#model"></a></h3>
<div class="highlight">
<pre><span class="c1"># app/models/progress_bar.rb</span>
<span class="k">class</span> <span class="nc">ProgressBar</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="n">belongs_to</span> <span class="ss">:user</span>
<span class="n">validates_presence_of</span> <span class="ss">:user_id</span><span class="p">,</span> <span class="ss">:message</span>
<span class="n">validates_numericality_of</span> <span class="ss">:percent</span><span class="p">,</span>
<span class="ss">greater_than_or_equal_to</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="ss">less_than_or_equal_to</span><span class="p">:</span> <span class="mi">100</span>
<span class="k">end</span>
</pre>
</div>
<h3><a href="migration"></a></h3>
<div class="highlight">
<pre><span class="c1"># rails g migration create_progress_bars</span>
<span class="k">class</span> <span class="nc">CreateProgressBars</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Migration</span><span class="o">[</span><span class="mi">5</span><span class="o">.</span><span class="mi">0</span><span class="o">]</span>
<span class="k">def</span> <span class="nf">change</span>
<span class="n">create_table</span> <span class="ss">:progress_bars</span> <span class="k">do</span> <span class="o">|</span><span class="n">t</span><span class="o">|</span>
<span class="n">t</span><span class="o">.</span><span class="n">text</span> <span class="ss">:message</span>
<span class="n">t</span><span class="o">.</span><span class="n">integer</span> <span class="ss">:percent</span>
<span class="n">t</span><span class="o">.</span><span class="n">integer</span> <span class="ss">:user_id</span>
<span class="n">t</span><span class="o">.</span><span class="n">timestamps</span> <span class="ss">null</span><span class="p">:</span> <span class="kp">false</span>
<span class="k">end</span>
<span class="n">add_index</span> <span class="ss">:progress_bars</span><span class="p">,</span> <span class="ss">:user_id</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="routes"><a href="#routes"></a></h3>
<div class="highlight">
<pre><span class="c1"># config/routes.rb</span>
<span class="n">resources</span> <span class="ss">:progress_bars</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="s1">'show'</span>
<span class="n">resources</span> <span class="ss">:demos</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="sx">%w(new create)</span>
</pre>
</div>
<h3 id="demo-controller"><a href="#demo-controller"></a></h3>
<div class="highlight">
<pre><span class="c1"># app/controllers/demos_controller.rb</span>
<span class="k">class</span> <span class="nc">DemosController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">create</span>
<span class="vi">@progress_bar</span> <span class="o">=</span> <span class="n">current_user</span><span class="o">.</span><span class="n">progress_bars</span><span class="o">.</span><span class="n">create!</span><span class="p">(</span><span class="ss">message</span><span class="p">:</span> <span class="s1">'Queued'</span><span class="p">,</span> <span class="ss">percent</span><span class="p">:</span> <span class="mi">0</span><span class="p">)</span>
<span class="no">DemoWorker</span><span class="o">.</span><span class="n">perform_later</span><span class="p">(</span><span class="vi">@progress_bar</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="demo-new"><a href="#demo-new"></a></h3>
<div class="highlight">
<pre><span class="c">&lt;!-- app/views/demos/new.html.erb --&gt;</span>
<span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"demo"</span> <span class="na">data-ping-time=</span><span class="s">"1000"</span><span class="nt">&gt;</span>
<span class="cp">&lt;%=</span> <span class="n">link_to</span> <span class="s1">'Start'</span><span class="p">,</span> <span class="n">url_for</span><span class="p">(</span><span class="ss">action</span><span class="p">:</span> <span class="s1">'create'</span><span class="p">),</span> <span class="s1">'data-remote'</span> <span class="o">=&gt;</span> <span class="kp">true</span><span class="p">,</span> <span class="s1">'data-type'</span> <span class="o">=&gt;</span> <span class="s1">'script'</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress"</span><span class="nt">&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"progress-bar"</span> <span class="na">style=</span><span class="s">"width: 0%;"</span><span class="nt">&gt;&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"message"</span><span class="nt">&gt;</span>Not Running...<span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</pre>
</div>
<h3 id="demo-create"><a href="#demo-create"></a></h3>
<div class="highlight">
<pre><span class="c1">// app/views/demos/create.js.erb</span>
<span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">progressBar</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">ProgressBar</span><span class="p">(</span><span class="s2">"#demo"</span><span class="p">,</span> <span class="s2">"&lt;%= progress_bar_path @progress_bar %&gt;"</span><span class="p">);</span>
<span class="nx">progressBar</span><span class="p">.</span><span class="nx">start</span><span class="p">();</span>
<span class="p">})();</span>
</pre>
</div>
<h3 id="progress-bar"><a href="#progress-bar"></a></h3>
<div class="highlight">
<pre><span class="c1"># app/javascripts/progress_bar.coffee</span>
<span class="k">class</span> <span class="nx">@ProgressBar</span>
<span class="nv">constructor: </span><span class="nf">(elem, url) -&gt;</span>
<span class="vi">@elem = </span><span class="nx">$</span><span class="p">(</span><span class="nx">elem</span><span class="p">)</span>
<span class="vi">@url = </span><span class="nx">url</span>
<span class="vi">@message = </span><span class="nx">@elem</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s">'.message'</span><span class="p">)</span>
<span class="vi">@bar = </span><span class="nx">@elem</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s">'.progress-bar'</span><span class="p">)</span>
<span class="vi">@pingTime = </span><span class="nb">parseInt</span><span class="p">(</span><span class="nx">@elem</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="s">'ping-time'</span><span class="p">))</span>
<span class="nv">start: </span><span class="nf">=&gt;</span>
<span class="nx">$</span><span class="p">.</span><span class="nx">ajax</span><span class="p">({</span>
<span class="nv">url: </span><span class="nx">@url</span><span class="p">,</span>
<span class="nv">dataType: </span><span class="s">'json'</span><span class="p">,</span>
<span class="nv">success: </span><span class="nf">(data) =&gt;</span>
<span class="nx">@message</span><span class="p">.</span><span class="nx">html</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">message</span><span class="p">)</span>
<span class="nv">percent = </span><span class="s">"</span><span class="si">#{</span><span class="nx">data</span><span class="p">.</span><span class="nx">percent</span><span class="si">}</span><span class="s">%"</span>
<span class="nx">@bar</span><span class="p">.</span><span class="nx">css</span><span class="p">(</span><span class="s">'width'</span><span class="p">,</span> <span class="nx">percent</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">percent</span><span class="p">)</span>
<span class="k">if</span> <span class="nx">data</span><span class="p">.</span><span class="nx">percent</span> <span class="o">&lt;</span> <span class="mi">100</span>
<span class="nx">setTimeout</span><span class="p">(</span><span class="nx">@start</span><span class="p">,</span> <span class="nx">@pingTime</span><span class="p">)</span>
<span class="p">})</span>
</pre>
</div>
<h3 id="progress-bars-controller"><a href="#progress-bars-controller"></a></h3>
<div class="highlight">
<pre><span class="c1"># app/controllers/progress_bars_controller.rb</span>
<span class="k">class</span> <span class="nc">ProgressBarsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="k">def</span> <span class="nf">show</span>
<span class="vi">@progress_bar</span> <span class="o">=</span> <span class="n">current_user</span><span class="o">.</span><span class="n">progress_bars</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">params</span><span class="o">[</span><span class="ss">:id</span><span class="o">]</span><span class="p">)</span>
<span class="n">render</span> <span class="ss">json</span><span class="p">:</span> <span class="vi">@progress_bar</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="demo-worker"><a href="#demo-worker"></a></h3>
<div class="highlight">
<pre><span class="c1"># app/workers/demo_worker.rb</span>
<span class="k">class</span> <span class="nc">DemoWorker</span> <span class="o">&lt;</span> <span class="no">ActiveJob</span><span class="o">::</span><span class="no">Base</span>
<span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">progress_bar</span><span class="p">)</span>
<span class="vi">@progress_bars</span><span class="o">.</span><span class="n">update_attributes!</span><span class="p">({</span>
<span class="ss">message</span><span class="p">:</span> <span class="s1">'Working ...'</span><span class="p">,</span>
<span class="ss">percent</span><span class="p">:</span> <span class="mi">0</span>
<span class="p">})</span>
<span class="mi">10</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
<span class="nb">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="vi">@progress_bar</span><span class="o">.</span><span class="n">update_attributes!</span><span class="p">(</span><span class="ss">percent</span><span class="p">:</span> <span class="n">i</span> <span class="o">*</span> <span class="mi">10</span><span class="p">)</span>
<span class="k">end</span>
<span class="vi">@progress_bars</span><span class="o">.</span><span class="n">update_attributes!</span><span class="p">({</span>
<span class="ss">message</span><span class="p">:</span> <span class="s1">'Finished!'</span><span class="p">,</span>
<span class="ss">percent</span><span class="p">:</span> <span class="mi">0</span>
<span class="p">})</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h2 id="wrapup"><a href="#wrapup">Wrap-up</a></h2>
<p>Get all that? The button inside <code>demos#new</code> invokes <code>#create</code> via AJAX, starting a client-side pinging of <code>progress_bars#show</code> while <code>demo_worker#perform</code> runs in the background.</p>
<p>Of course it's just a demo. A real worker would do something useful like import a data file. Here are a few more thoughts:</p>
<ul>
<li>Should you use <a href="http://redis.io/">Redis</a> for ephemeral data like progress bars?
</li>
<li>How do you notify the user of errors that may occur inside your worker?</li>
<li>Do you want the progress bar to popup in a <a href="http://www.w3schools.com/bootstrap/bootstrap_modal.asp">modal</a>?
</li>
<li>Would you allow multiple progress bars on the same screen?</li>
<li>Can the user cancel the background process?</li>
</ul>
<p>All easily built on top of this initial implementation, but I'll leave them to you. Feel free to contact me with any questions or comments at <a href="mailto:dan@topdan.com">dan@topdan.com</a>.</p><!-- post:content:end -->
</div>
</div>
</div>
<div class="row navigation bb-ccc">
<a href="/ruby-on-rails/form-builder-label-and-full-error-messages.html" class="previous"><span class="fa fa-arrow-left">&nbsp;</span> <span class="desktop">f.label and errors.full_messages</span><span class="mobile">Previous</span></a> <a href="/ruby-on-rails/aws-s3-browser.html" class="next"><span class="desktop">Browsing Files & Directories in S3</span><span class="mobile">Next</span> <span class="fa fa-arrow-right">&nbsp;</span></a>
<p><a href="/"><span class="fa fa-home">&nbsp;</span></a> &nbsp;»&nbsp;<span class="title desktop"><a href="/ruby-on-rails/index.html">Ruby on Rails</a></span><span class="mobile"><a href="/ruby-on-rails/index.html"><span class="fa fa-folder-open">&nbsp;</span></a></span></p>
</div>
<div class="row introduction pb-2e bb-ccc mb-1e">
<div class="col-md-6 mt-2e center">
<h1 class="mt-0"><a title="Dan Cunning" class="fs-45 fw-normal shadow-silver" href="/">Dan Cunning</a></h1>
<p class="reference mt-05e"><a title="github" class="white" href="https://github.com/topdan"><span class="fa fa-github">&nbsp;</span> GitHub</a> <a title="Twitter" class="white" href="https://www.linkedin.com/in/dancunning"><span class="fa fa-linkedin-square">&nbsp;</span> LinkedIn</a> <a title="Email" class="white" href="mailto:dan@topdan.com"><span class="fa fa-envelope">&nbsp;</span> Email</a></p>
</div>
<div class="col-sm-offset-1 col-md-4 mt-2e">
<img width="75" height="75" class="img-circle fl-left mr-10" src="/assets/dan-79508ca0775ace507f1dc34d151bba0f.jpg" alt="Dan">
<p class="underline-links">I'm a <a href="/ruby-on-rails/index.html">Ruby on Rails</a> contractor from Atlanta GA, focusing on simplicity and usability through solid design. <a class="nowrap" href="/dan-cunning.html">Read more »</a></p>
</div>
</div>
<div class="row fs-12 mt-1e center">
<div class="col-md-6">
<p class="underline-links mb-1e">View Source: <a href="https://github.com/topdan/www/blob/master/ruby-on-rails/progress-bars.html">HTML</a> | <a href="https://github.com/topdan/www/blob/master/ruby-on-rails/progress-bars.json">JSON</a> | <a href="https://github.com/topdan/www/blob/master/ruby-on-rails/progress-bars.html.md">View</a></p>
</div>
<div class="col-md-6">
<p class="mb-1e">© 2012-2019 Dan Cunning. All rights reserved.</p>
</div>
</div>
</div>
<script src="/assets/site-1e22bb3074710f189d8b5da964539ecb.js"></script>
</body>
</html>