working_with_javascript_in_rails.md 13.2 KB
Newer Older
1
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON http://guides.rubyonrails.org.**
X
Xavier Noria 已提交
2

3
Working with JavaScript in Rails
4
================================
S
Steve Klabnik 已提交
5 6

This guide covers the built-in Ajax/JavaScript functionality of Rails (and
X
Xavier Noria 已提交
7
more); it will enable you to create rich and dynamic Ajax applications with
8 9 10
ease!

After reading this guide, you will know:
S
Steve Klabnik 已提交
11

12
* The basics of Ajax.
13 14
* Unobtrusive JavaScript.
* How Rails' built-in helpers assist you.
15
* How to handle Ajax on the server side.
16
* The Turbolinks gem.
S
Steve Klabnik 已提交
17 18 19

-------------------------------------------------------------------------------

20
An Introduction to Ajax
S
Steve Klabnik 已提交
21 22
------------------------

X
Xavier Noria 已提交
23
In order to understand Ajax, you must first understand what a web browser does
S
Steve Klabnik 已提交
24 25 26 27 28 29 30 31 32 33 34 35 36
normally.

When you type `http://localhost:3000` into your browser's address bar and hit
'Go,' the browser (your 'client') makes a request to the server. It parses the
response, then fetches all associated assets, like JavaScript files,
stylesheets and images. It then assembles the page. If you click a link, it
does the same process: fetch the page, fetch the assets, put it all together,
show you the results. This is called the 'request response cycle.'

JavaScript can also make requests to the server, and parse the response. It
also has the ability to update information on the page. Combining these two
powers, a JavaScript writer can make a web page that can update just parts of
itself, without needing to get the full page data from the server. This is a
X
Xavier Noria 已提交
37
powerful technique that we call Ajax.
S
Steve Klabnik 已提交
38 39 40 41 42

Rails ships with CoffeeScript by default, and so the rest of the examples
in this guide will be in CoffeeScript. All of these lessons, of course, apply
to vanilla JavaScript as well.

X
Xavier Noria 已提交
43
As an example, here's some CoffeeScript code that makes an Ajax request using
S
Steve Klabnik 已提交
44 45
the jQuery library:

46
```coffeescript
S
Steve Klabnik 已提交
47 48 49 50 51 52 53 54 55
$.ajax(url: "/test").done (html) ->
  $("#results").append html
```

This code fetches data from "/test", and then appends the result to the `div`
with an id of `results`.

Rails provides quite a bit of built-in support for building web pages with this
technique. You rarely have to write this code yourself. The rest of this guide
J
Jude Arasu 已提交
56
will show you how Rails can help you write websites in this way, but it's
S
Steve Klabnik 已提交
57 58 59 60 61 62 63 64 65 66 67 68 69
all built on top of this fairly simple technique.

Unobtrusive JavaScript
-------------------------------------

Rails uses a technique called "Unobtrusive JavaScript" to handle attaching
JavaScript to the DOM. This is generally considered to be a best-practice
within the frontend community, but you may occasionally read tutorials that
demonstrate other ways.

Here's the simplest way to write JavaScript. You may see it referred to as
'inline JavaScript':

70
```html
71
<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
S
Steve Klabnik 已提交
72
```
73 74
When clicked, the link background will become red. Here's the problem: what
happens when we have lots of JavaScript we want to execute on a click?
S
Steve Klabnik 已提交
75

76
```html
77
<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>
S
Steve Klabnik 已提交
78 79 80 81 82
```

Awkward, right? We could pull the function definition out of the click handler,
and turn it into CoffeeScript:

83
```coffeescript
84
@paintIt = (element, backgroundColor, textColor) ->
85 86 87
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor
S
Steve Klabnik 已提交
88 89 90 91
```

And then on our page:

92
```html
93
<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
S
Steve Klabnik 已提交
94 95 96 97 98
```

That's a little bit better, but what about multiple links that have the same
effect?

99
```html
100 101 102
<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
S
Steve Klabnik 已提交
103 104 105 106 107 108
```

Not very DRY, eh? We can fix this by using events instead. We'll add a `data-*`
attribute to our link, and then bind a handler to the click event of every link
that has that attribute:

109
```coffeescript
110
@paintIt = (element, backgroundColor, textColor) ->
111 112 113 114 115
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor

$ ->
116 117 118
  $("a[data-background-color]").click (e) ->
    e.preventDefault()

119 120 121 122 123 124 125 126
    backgroundColor = $(this).data("background-color")
    textColor = $(this).data("text-color")
    paintIt(this, backgroundColor, textColor)
```
```html
<a href="#" data-background-color="#990000">Paint it red</a>
<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
S
Steve Klabnik 已提交
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
```

We call this 'unobtrusive' JavaScript because we're no longer mixing our
JavaScript into our HTML. We've properly separated our concerns, making future
change easy. We can easily add behavior to any link by adding the data
attribute. We can run all of our JavaScript through a minimizer and
concatenator. We can serve our entire JavaScript bundle on every page, which
means that it'll get downloaded on the first page load and then be cached on
every page after that. Lots of little benefits really add up.

The Rails team strongly encourages you to write your CoffeeScript (and
JavaScript) in this style, and you can expect that many libraries will also
follow this pattern.

Built-in Helpers
----------------------

Rails provides a bunch of view helper methods written in Ruby to assist you
X
Xavier Noria 已提交
145
in generating HTML. Sometimes, you want to add a little Ajax to those elements,
S
Steve Klabnik 已提交
146 147
and Rails has got your back in those cases.

X
Xavier Noria 已提交
148
Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
S
Steve Klabnik 已提交
149
parts: the JavaScript half and the Ruby half.
150

L
Larry Kyrala 已提交
151
Unless you have disabled the Asset Pipeline,
S
Steve Klabnik 已提交
152 153
[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js)
provides the JavaScript half, and the regular Ruby view helpers add appropriate
L
Larry Kyrala 已提交
154
tags to your DOM.
S
Steve Klabnik 已提交
155 156 157 158

### form_for

[`form_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)
159
is a helper that assists with writing forms. `form_for` takes a `:remote`
S
Steve Klabnik 已提交
160 161
option. It works like this:

162
```erb
163
<%= form_for(@article, remote: true) do |f| %>
S
Steve Klabnik 已提交
164 165 166 167 168 169
  ...
<% end %>
```

This will generate the following HTML:

170
```html
171
<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
S
Steve Klabnik 已提交
172 173 174 175
  ...
</form>
```

176
Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather
S
Steve Klabnik 已提交
177 178 179 180 181 182
than by the browser's normal submit mechanism.

You probably don't want to just sit there with a filled out `<form>`, though.
You probably want to do something upon a successful submission. To do that,
bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:

183
```coffeescript
S
Steve Klabnik 已提交
184
$(document).ready ->
185 186
  $("#new_article").on("ajax:success", (e, data, status, xhr) ->
    $("#new_article").append xhr.responseText
187
  ).on "ajax:error", (e, xhr, status, error) ->
188
    $("#new_article").append "<p>ERROR</p>"
S
Steve Klabnik 已提交
189 190 191
```

Obviously, you'll want to be a bit more sophisticated than that, but it's a
192
start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
S
Steve Klabnik 已提交
193 194 195 196 197 198 199

### form_tag

[`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag)
is very similar to `form_for`. It has a `:remote` option that you can use like
this:

200
```erb
201
<%= form_tag('/articles', remote: true) do %>
202 203 204 205 206 207 208
  ...
<% end %>
```

This will generate the following HTML:

```html
209
<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post">
210 211
  ...
</form>
S
Steve Klabnik 已提交
212 213 214 215 216 217 218 219
```

Everything else is the same as `form_for`. See its documentation for full
details.

### link_to

[`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
220
is a helper that assists with generating links. It has a `:remote` option you
S
Steve Klabnik 已提交
221 222
can use like this:

223
```erb
224
<%= link_to "an article", @article, remote: true %>
S
Steve Klabnik 已提交
225 226 227 228
```

which generates

229
```html
230
<a href="/articles/1" data-remote="true">an article</a>
S
Steve Klabnik 已提交
231 232
```

X
Xavier Noria 已提交
233
You can bind to the same Ajax events as `form_for`. Here's an example. Let's
234
assume that we have a list of articles that can be deleted with just one
235
click. We would generate some HTML like this:
S
Steve Klabnik 已提交
236

237
```erb
238
<%= link_to "Delete article", @article, remote: true, method: :delete %>
239
```
S
Steve Klabnik 已提交
240 241 242

and write some CoffeeScript like this:

243
```coffeescript
244 245
$ ->
  $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
246
    alert "The article was deleted."
S
Steve Klabnik 已提交
247 248 249 250 251 252
```

### button_to

[`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this:

253
```erb
254
<%= button_to "An article", @article, remote: true %>
S
Steve Klabnik 已提交
255 256 257 258
```

this generates

259
```html
260
<form action="/articles/1" class="button_to" data-remote="true" method="post">
261
  <input type="submit" value="An article" />
S
Steve Klabnik 已提交
262 263 264 265 266
</form>
```

Since it's just a `<form>`, all of the information on `form_for` also applies.

267
Server-Side Concerns
S
Steve Klabnik 已提交
268 269
--------------------

X
Xavier Noria 已提交
270 271
Ajax isn't just client-side, you also need to do some work on the server
side to support it. Often, people like their Ajax requests to return JSON
S
Steve Klabnik 已提交
272 273 274 275 276 277 278 279
rather than HTML. Let's discuss what it takes to make that happen.

### A Simple Example

Imagine you have a series of users that you would like to display and provide a
form on that same page to create a new user. The index action of your
controller looks like this:

280
```ruby
S
Steve Klabnik 已提交
281 282 283 284 285 286 287 288 289 290
class UsersController < ApplicationController
  def index
    @users = User.all
    @user = User.new
  end
  # ...
```

The index view (`app/views/users/index.html.erb`) contains:

291
```erb
S
Steve Klabnik 已提交
292 293 294
<b>Users</b>

<ul id="users">
295
<%= render @users %>
S
Steve Klabnik 已提交
296 297 298 299 300 301 302 303 304 305 306 307 308
</ul>

<br>

<%= form_for(@user, remote: true) do |f| %>
  <%= f.label :name %><br>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>
```

The `app/views/users/_user.html.erb` partial contains the following:

309
```erb
S
Steve Klabnik 已提交
310 311 312 313 314 315
<li><%= user.name %></li>
```

The top portion of the index page displays the users. The bottom portion
provides a form to create a new user.

316
The bottom form will call the `create` action on the `UsersController`. Because
S
Steve Klabnik 已提交
317
the form's remote option is set to true, the request will be posted to the
318 319
`UsersController` as an Ajax request, looking for JavaScript. In order to
serve that request, the `create` action of your controller would look like
S
Steve Klabnik 已提交
320 321
this:

322
```ruby
S
Steve Klabnik 已提交
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
  # app/controllers/users_controller.rb
  # ......
  def create
    @user = User.new(params[:user])

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: 'User was successfully created.' }
        format.js   {}
        format.json { render json: @user, status: :created, location: @user }
      else
        format.html { render action: "new" }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end
```

341
Notice the format.js in the `respond_to` block; that allows the controller to
X
Xavier Noria 已提交
342
respond to your Ajax request. You then have a corresponding
S
Steve Klabnik 已提交
343 344 345
`app/views/users/create.js.erb` view file that generates the actual JavaScript
code that will be sent and executed on the client side.

346
```erb
S
Steve Klabnik 已提交
347 348 349 350 351 352
$("<%= escape_javascript(render @user) %>").appendTo("#users");
```

Turbolinks
----------

M
mabras 已提交
353
Rails 4 ships with the [Turbolinks gem](https://github.com/turbolinks/turbolinks).
X
Xavier Noria 已提交
354
This gem uses Ajax to speed up page rendering in most applications.
S
Steve Klabnik 已提交
355

356
### How Turbolinks Works
S
Steve Klabnik 已提交
357 358 359

Turbolinks attaches a click handler to all `<a>` on the page. If your browser
supports
360
[PushState](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Manipulating_the_browser_history#The_pushState%28%29_method),
X
Xavier Noria 已提交
361
Turbolinks will make an Ajax request for the page, parse the response, and
S
Steve Klabnik 已提交
362 363 364 365 366 367 368 369 370 371 372
replace the entire `<body>` of the page with the `<body>` of the response. It
will then use PushState to change the URL to the correct one, preserving
refresh semantics and giving you pretty URLs.

The only thing you have to do to enable Turbolinks is have it in your Gemfile,
and put `//= require turbolinks` in your CoffeeScript manifest, which is usually
`app/assets/javascripts/application.js`.

If you want to disable Turbolinks for certain links, add a `data-no-turbolink`
attribute to the tag:

373
```html
S
Steve Klabnik 已提交
374 375 376
<a href="..." data-no-turbolink>No turbolinks here</a>.
```

377
### Page Change Events
S
Steve Klabnik 已提交
378 379 380 381

When writing CoffeeScript, you'll often want to do some sort of processing upon
page load. With jQuery, you'd write something like this:

382
```coffeescript
S
Steve Klabnik 已提交
383 384 385 386 387 388 389 390
$(document).ready ->
  alert "page has loaded!"
```

However, because Turbolinks overrides the normal page loading process, the
event that this relies on will not be fired. If you have code that looks like
this, you must change your code to do this instead:

391
```coffeescript
S
Steve Klabnik 已提交
392 393 394 395 396 397
$(document).on "page:change", ->
  alert "page has loaded!"
```

For more details, including other events you can bind to, check out [the
Turbolinks
M
mabras 已提交
398
README](https://github.com/turbolinks/turbolinks/blob/master/README.md).
S
Steve Klabnik 已提交
399

400
Other Resources
S
Steve Klabnik 已提交
401 402 403 404 405 406 407 408
---------------

Here are some helpful links to help you learn even more:

* [jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki)
* [jquery-ujs list of external articles](https://github.com/rails/jquery-ujs/wiki/External-articles)
* [Rails 3 Remote Links and Forms: A Definitive Guide](http://www.alfajango.com/blog/rails-3-remote-links-and-forms/)
* [Railscasts: Unobtrusive JavaScript](http://railscasts.com/episodes/205-unobtrusive-javascript)
S
Sunny Ripert 已提交
409
* [Railscasts: Turbolinks](http://railscasts.com/episodes/390-turbolinks)