/
ajax-toggle-buttons.html
302 lines (278 loc) · 23.5 KB
/
ajax-toggle-buttons.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
<!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"> </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"> </span></a>
<p><a href="/"><span class="fa fa-home"> </span></a> » <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"> </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"> </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"> </span> Favorite</a>
</li>
<li>When the request succeeds the button becomes <a href="#" class="btn btn-warning"><span class="fa fa-star"> </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"> </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"> </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"><</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"><</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"><<</span> <span class="s1">' '</span><span class="o">.</span><span class="n">html_safe</span> <span class="o"><<</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"><%#</span><span class="c"> views/posts/show.html.erb </span><span class="cp">%></span><span class="x"> </span>
<span class="cp"><%=</span> <span class="n">div_for</span> <span class="vi">@post</span> <span class="k">do</span> <span class="cp">%></span><span class="x"> </span>
<span class="x"> </span><span class="cp"><%=</span> <span class="n">link_to_toggle_post_favorite</span> <span class="vi">@post</span> <span class="cp">%></span><span class="x"> </span>
<span class="cp"><%</span> <span class="k">end</span> <span class="cp">%></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-<%= @post.id %> .favorite'</span><span class="p">).</span><span class="nx">replaceWith</span><span class="p">(</span><span class="s2">"<%=j link_to_toggle_post_favorite @post %>"</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-<%= @post.id %> .favorite'</span><span class="p">).</span><span class="nx">replaceWith</span><span class="p">(</span><span class="s2">"<%=j link_to_toggle_post_favorite @post %>"</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"> </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"> </span></a>
<p><a href="/"><span class="fa fa-home"> </span></a> » <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"> </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"> </span> GitHub</a> <a title="Twitter" class="white" href="https://www.linkedin.com/in/dancunning"><span class="fa fa-linkedin-square"> </span> LinkedIn</a> <a title="Email" class="white" href="mailto:dan@topdan.com"><span class="fa fa-envelope"> </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>