Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
303 lines (278 sloc) 23.5 KB
<!DOCTYPE html>
<html>
<head>
<title>Ajax Toggle Buttons - topdan.com</title>
<meta charset="UTF-8">
<meta name="description" content="How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.">
<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="Ajax Toggle Buttons">
<meta name="twitter:description" content="How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.">
<meta name="twitter:url" content="http://www.topdan.com/ruby-on-rails/ajax-toggle-buttons.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/ajax-toggle-buttons.html">
<meta name="og:title" content="Ajax Toggle Buttons">
<meta name="og:description" content="How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.">
<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="ajax-toggle-buttons">
<div class="container bg-black full-width">
<div class="row navigation">
<a id="top"></a> <a href="/ruby-on-rails/minimum-viable-test-suite.html" class="previous"><span class="fa fa-arrow-left">&nbsp;</span> <span class="desktop">Minimum Viable Test Suite</span><span class="mobile">Previous</span></a> <a href="/ruby-on-rails/finding-your-most-active-users-with-google-analytics.html" class="next"><span class="desktop">Finding Your Most Active Users</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 Apr 16, 2014</p>
<h1 class="center of-yh mb-1e"><a href="/ruby-on-rails/ajax-toggle-buttons.html">Ajax Toggle Buttons</a></h1>
<div class="alert b-ccc mb-0">
<p>How to create toggle buttons using Ruby on Rails, ajax, and unobtrusive javascript. Straight-forward and boring, just how I like it.</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>Toggle buttons are useful for communicating and changing state. Here's what the user sees:</p>
<ul class="user-experience">
<li>Clicks on <a href="#" class="btn btn-default"><span class="fa fa-star">&nbsp;</span> Favorite</a>
</li>
<li>An ajax request is started and the button becomes <a href="#" class="btn btn-default"><span class="fa fa-refresh fa-spin">&nbsp;</span> Favorite</a>
</li>
<li>When the request succeeds the button becomes <a href="#" class="btn btn-warning"><span class="fa fa-star">&nbsp;</span> Favorite</a>
</li>
</ul>
<p>This pattern presents itself in every one of my applications, so I wanted to document how I implement it in Ruby on Rails, as it is a good introduction to ajax, unobtrusive javascript, and Rails handling javascript requests.</p>
<h2 id="demo"><a href="#demo">Demo</a></h2>
<p class="alert alert-info">Click the buttons below and look at your web-developer tools for how the ajax response swaps the links, but note this site runs on a static webserver, so the paths and HTTP methods don't mirror an actual Ruby on Rails application.</p>
<div id="post-1" class="my-post">
<p class="actions"><a href="/static/ruby-on-rails/ajax-toggle-buttons/1/favorite/put.js" data-method="GET" data-remote="true" data-type="script" class="btn btn-default favorite"><span class="fa fa-star">&nbsp;</span> Favorite</a> <a href="/static/ruby-on-rails/ajax-toggle-buttons/1/lock/delete.js" data-method="GET" data-remote="true" data-type="script" class="btn btn-danger lock"><span class="fa fa-lock">&nbsp;</span> Locked</a></p>
</div>
<h2 id="how-it-works"><a href="#how-it-works">How it Works</a></h2>
<ol>
<li>A Rails helper method renders the button in its current state.</li>
<li>This button has an <code>href</code> along with <code>data-method</code>, <code>data-type=script</code>, and <code>data-remote=true</code> attributes which instruct <code>jquery_ujs.js</code> to perform an ajax request and evaluate the result as javascript.</li>
<li>When the user clicks either button, <a href="#loading-js">a jQuery listener</a> switches to the loading icon while another listener switches it back on completion.
</li>
<li>In Rails, <a href="#routes">the main resource has a child</a> which provides <code>#update</code> and <code>#destroy</code> actions for <a href="#controller">toggling on and off</a> respectively.
</li>
<li>After saving the change, Rails responds with one line of javascript that <a href="#update-js">replaces the button with its new state</a>.
</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"># models/post.rb</span>
<span class="k">class</span> <span class="nc">Post</span> <span class="o">&lt;</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span>
<span class="k">def</span> <span class="nf">favorited?</span>
<span class="n">favorited_at</span> <span class="o">!=</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">favorite</span>
<span class="nb">self</span><span class="o">.</span><span class="n">favorited_at</span> <span class="o">=</span> <span class="no">Time</span><span class="o">.</span><span class="n">now</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">favorite!</span>
<span class="n">favorite</span>
<span class="n">save!</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">unfavorite</span>
<span class="nb">self</span><span class="o">.</span><span class="n">favorited_at</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">unfavorite!</span>
<span class="n">unfavorite</span>
<span class="n">save!</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">:posts</span> <span class="k">do</span>
<span class="n">resource</span> <span class="ss">:favorite</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="sx">%w(update destroy)</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="controller"><a href="#controller"></a></h3>
<div class="highlight">
<pre><span class="c1"># controllers/posts/favorites_controller.rb</span>
<span class="k">class</span> <span class="nc">Posts</span><span class="o">::</span><span class="no">FavoritesController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
<span class="n">before_action</span> <span class="ss">:load_post</span>
<span class="k">def</span> <span class="nf">update</span>
<span class="vi">@post</span><span class="o">.</span><span class="n">favorite!</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">destroy</span>
<span class="vi">@post</span><span class="o">.</span><span class="n">unfavorite!</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">load_post</span>
<span class="vi">@post</span> <span class="o">=</span> <span class="no">Post</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">:post_id</span><span class="o">]</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="application-helper"><a href="#application-helper"></a></h3>
<div class="highlight">
<pre><span class="c1"># helpers/posts_helper.rb</span>
<span class="k">module</span> <span class="nn">PostsHelper</span>
<span class="k">def</span> <span class="nf">link_to_toggle_post_favorite</span><span class="p">(</span><span class="n">post</span><span class="p">)</span>
<span class="n">url</span> <span class="o">=</span> <span class="n">post_favorite_path</span><span class="p">(</span><span class="n">post</span><span class="p">)</span>
<span class="k">if</span> <span class="n">post</span><span class="o">.</span><span class="n">favorited?</span>
<span class="n">link_to_with_icon</span><span class="p">(</span><span class="s1">'icon-star'</span><span class="p">,</span> <span class="s1">'Favorite'</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="p">{</span>
<span class="nb">method</span><span class="p">:</span> <span class="s1">'DELETE'</span><span class="p">,</span>
<span class="ss">remote</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span>
<span class="ss">class</span><span class="p">:</span> <span class="s1">'favorite btn btn-primary'</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">else</span>
<span class="n">link_to_with_icon</span><span class="p">(</span><span class="s1">'icon-star'</span><span class="p">,</span> <span class="s1">'Favorite'</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="p">{</span>
<span class="nb">method</span><span class="p">:</span> <span class="s1">'PUT'</span><span class="p">,</span>
<span class="ss">remote</span><span class="p">:</span> <span class="kp">true</span><span class="p">,</span>
<span class="ss">class</span><span class="p">:</span> <span class="s1">'favorite btn'</span><span class="p">,</span>
<span class="p">})</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">link_to_with_icon</span><span class="p">(</span><span class="n">icon_css</span><span class="p">,</span> <span class="n">title</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="p">{})</span>
<span class="n">icon</span> <span class="o">=</span> <span class="n">content_tag</span><span class="p">(</span><span class="ss">:i</span><span class="p">,</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">class</span><span class="p">:</span> <span class="n">icon_css</span><span class="p">)</span>
<span class="n">title_with_icon</span> <span class="o">=</span> <span class="n">icon</span> <span class="o">&lt;&lt;</span> <span class="s1">' '</span><span class="o">.</span><span class="n">html_safe</span> <span class="o">&lt;&lt;</span> <span class="n">h</span><span class="p">(</span><span class="n">title</span><span class="p">)</span>
<span class="n">link_to</span><span class="p">(</span><span class="n">title_with_icon</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre>
</div>
<h3 id="gemfile"><a href="#gemfile"></a></h3>
<div class="highlight">
<pre><span class="c1"># Gemfile</span>
<span class="n">gem</span> <span class="s1">'jquery-rails'</span>
</pre>
</div>
<h3 id="application-js"><a href="#application-js"></a></h3>
<div class="highlight">
<pre><span class="c1">// assets/javascript/application.js</span>
<span class="c1">//</span>
<span class="c1">// jquery_ujs allows us to use 'data-remote',</span>
<span class="c1">// 'data-type', and 'data-method' attributes</span>
<span class="c1">//</span>
<span class="c1">//= require jquery</span>
<span class="c1">//= require jquery_ujs</span>
<span class="c1">//= require_tree .</span>
</pre>
</div>
<h3 id="loading-js"><a href="#loading-js"></a></h3>
<div class="highlight">
<pre><span class="cm">/* assets/javascripts/loading.js */</span>
<span class="c1">// This isn't necessarily specific to toggle buttons</span>
<span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// Change the link's icon while the request is performing</span>
<span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="s1">'click'</span><span class="p">,</span> <span class="s1">'a[data-remote]'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">,</span> <span class="nx">b</span><span class="p">,</span> <span class="nx">c</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">icon</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">find</span><span class="p">(</span><span class="s1">'i'</span><span class="p">);</span>
<span class="nx">icon</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="s1">'old-class'</span><span class="p">,</span> <span class="nx">icon</span><span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="s1">'class'</span><span class="p">));</span>
<span class="nx">icon</span><span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="s1">'class'</span><span class="p">,</span> <span class="s1">'icon-refresh'</span><span class="p">);</span>
<span class="p">});</span>
<span class="c1">// Change the link's icon back after it's finished.</span>
<span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">on</span><span class="p">(</span><span class="s1">'ajax:complete'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">var</span> <span class="nx">icon</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">).</span><span class="nx">find</span><span class="p">(</span><span class="s1">'i'</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">icon</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="s1">'old-class'</span><span class="p">))</span> <span class="p">{</span>
<span class="nx">icon</span><span class="p">.</span><span class="nx">attr</span><span class="p">(</span><span class="s1">'class'</span><span class="p">,</span> <span class="nx">icon</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="s1">'old-class'</span><span class="p">));</span>
<span class="nx">icon</span><span class="p">.</span><span class="nx">data</span><span class="p">(</span><span class="s1">'old-class'</span><span class="p">,</span> <span class="kc">null</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">})</span>
<span class="c1">// Don't fail silently</span>
<span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">ajaxError</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span> <span class="nx">event</span><span class="p">,</span> <span class="nx">jqxhr</span><span class="p">,</span> <span class="nx">settings</span><span class="p">,</span> <span class="nx">exception</span> <span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">jqxhr</span><span class="p">.</span><span class="nx">status</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">alert</span><span class="p">(</span><span class="s2">"We're sorry, but something went wrong ("</span> <span class="o">+</span> <span class="nx">jqxhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">+</span> <span class="s1">')'</span><span class="p">);</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="p">})</span>
</pre>
</div>
<h3 id="erb"><a href="#erb"></a></h3>
<div class="highlight">
<pre><span class="cp">&lt;%#</span><span class="c"> views/posts/show.html.erb </span><span class="cp">%&gt;</span><span class="x"> </span>
<span class="cp">&lt;%=</span> <span class="n">div_for</span> <span class="vi">@post</span> <span class="k">do</span> <span class="cp">%&gt;</span><span class="x"> </span>
<span class="x"> </span><span class="cp">&lt;%=</span> <span class="n">link_to_toggle_post_favorite</span> <span class="vi">@post</span> <span class="cp">%&gt;</span><span class="x"> </span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span><span class="x"> </span>
</pre>
</div>
<h3 id="update-js"><a href="#update-js"></a></h3>
<div class="highlight">
<pre><span class="cm">/* views/posts/favorites/update.js.erb */</span>
<span class="nx">$</span><span class="p">(</span><span class="s1">'#post-&lt;%= @post.id %&gt; .favorite'</span><span class="p">).</span><span class="nx">replaceWith</span><span class="p">(</span><span class="s2">"&lt;%=j link_to_toggle_post_favorite @post %&gt;"</span><span class="p">);</span>
</pre>
</div>
<h3 id="destroy-js"><a href="#destroy-js"></a></h3>
<div class="highlight">
<pre><span class="cm">/* views/posts/favorites/destroy.js.erb */</span>
<span class="nx">$</span><span class="p">(</span><span class="s1">'#post-&lt;%= @post.id %&gt; .favorite'</span><span class="p">).</span><span class="nx">replaceWith</span><span class="p">(</span><span class="s2">"&lt;%=j link_to_toggle_post_favorite @post %&gt;"</span><span class="p">);</span>
</pre>
</div>
<h2 id="wrapup"><a href="#wrapup">Wrap-up</a></h2>
<p>Unobtrusive javascript allows the application logic to stay in Ruby.</p>
<p>We follow "The Rails' Way" to <code>#update</code> and <code>#destroy</code> in the controller, which will help the application gracefully grow when we add more functionality to posts like additional toggles, fields, or a public RESTful API.</p>
<p>Let me know what else you think could be improved.</p><!-- post:content:end -->
</div>
</div>
</div>
<div class="row navigation bb-ccc">
<a href="/ruby-on-rails/minimum-viable-test-suite.html" class="previous"><span class="fa fa-arrow-left">&nbsp;</span> <span class="desktop">Minimum Viable Test Suite</span><span class="mobile">Previous</span></a> <a href="/ruby-on-rails/finding-your-most-active-users-with-google-analytics.html" class="next"><span class="desktop">Finding Your Most Active Users</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/ajax-toggle-buttons.html">HTML</a> | <a href="https://github.com/topdan/www/blob/master/ruby-on-rails/ajax-toggle-buttons.json">JSON</a> | <a href="https://github.com/topdan/www/blob/master/ruby-on-rails/ajax-toggle-buttons.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>